network: add hub registration, Redis persistence, and federated mesh join protocol
Some checks failed
CLI Tests / test-cli (push) Has been cancelled
Integration Tests / test-service-integration (push) Has been cancelled
Python Tests / test-python (push) Has been cancelled
Security Scanning / security-scan (push) Has been cancelled
Documentation Validation / validate-docs (push) Has been cancelled
API Endpoint Tests / test-api-endpoints (push) Has been cancelled
Systemd Sync / sync-systemd (push) Has been cancelled

- Change default P2P port from 7070 to 8001 in config and .env.example
- Add redis_url configuration option for hub persistence (default: redis://localhost:6379)
- Implement DNS-based hub registration/unregistration via HTTPS API endpoints
- Add Redis persistence for hub registrations with 1-hour TTL
- Add island join request/response protocol with member list and blockchain credentials
- Add GPU marketplace tracking (offers, bids, providers) in hub manager
- Add
This commit is contained in:
aitbc
2026-04-13 11:47:34 +02:00
parent fefa6c4435
commit d72945f20c
42 changed files with 3802 additions and 1022 deletions

View File

@@ -0,0 +1,556 @@
"""
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
)
# 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 = load_island_credentials()
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:
import httpx
with httpx.Client() as client:
response = client.post(
f"{rpc_endpoint}/transaction",
json=buy_order_data,
timeout=10
)
if response.status_code == 200:
result = response.json()
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'))
else:
error(f"Failed to submit transaction: {response.status_code}")
if response.text:
error(f"Error details: {response.text}")
raise click.Abort()
except Exception as e:
error(f"Network 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 = load_island_credentials()
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:
import httpx
with httpx.Client() as client:
response = client.post(
f"{rpc_endpoint}/transaction",
json=sell_order_data,
timeout=10
)
if response.status_code == 200:
result = response.json()
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'))
else:
error(f"Failed to submit transaction: {response.status_code}")
if response.text:
error(f"Error details: {response.text}")
raise click.Abort()
except Exception 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 = load_island_credentials()
rpc_endpoint = get_rpc_endpoint()
island_id = get_island_id()
# Query blockchain for exchange orders
try:
import httpx
params = {
'transaction_type': 'exchange',
'island_id': island_id,
'pair': pair,
'status': 'open',
'limit': limit * 2 # Get both buys and sells
}
with httpx.Client() as client:
response = client.get(
f"{rpc_endpoint}/transactions",
params=params,
timeout=10
)
if response.status_code == 200:
orders = response.json()
# Separate buy and sell orders
buy_orders = []
sell_orders = []
for order in orders:
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")
else:
error(f"Failed to query blockchain: {response.status_code}")
raise click.Abort()
except Exception as e:
error(f"Network error querying blockchain: {e}")
raise click.Abort()
except Exception as e:
error(f"Error viewing 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 = load_island_credentials()
rpc_endpoint = get_rpc_endpoint()
island_id = get_island_id()
# Query blockchain for exchange orders to calculate rates
try:
import httpx
rates_data = []
for pair in SUPPORTED_PAIRS:
params = {
'transaction_type': 'exchange',
'island_id': island_id,
'pair': pair,
'status': 'open',
'limit': 100
}
with httpx.Client() as client:
response = client.get(
f"{rpc_endpoint}/transactions",
params=params,
timeout=10
)
if response.status_code == 200:
orders = response.json()
# 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)
})
else:
rates_data.append({
"Pair": pair,
"Best Bid": "Error",
"Best Ask": "Error",
"Mid Price": "Error",
"Buy Orders": 0,
"Sell Orders": 0
})
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 = load_island_credentials()
rpc_endpoint = get_rpc_endpoint()
island_id = get_island_id()
# Query blockchain for exchange orders
try:
import httpx
params = {
'transaction_type': 'exchange',
'island_id': island_id
}
if user:
params['user_id'] = user
if status:
params['status'] = status
if pair:
params['pair'] = pair
with httpx.Client() as client:
response = client.get(
f"{rpc_endpoint}/transactions",
params=params,
timeout=10
)
if response.status_code == 200:
orders = response.json()
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]}...)")
else:
error(f"Failed to query blockchain: {response.status_code}")
raise click.Abort()
except Exception 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 = load_island_credentials()
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:
import httpx
with httpx.Client() as client:
response = client.post(
f"{rpc_endpoint}/transaction",
json=cancel_data,
timeout=10
)
if response.status_code == 200:
success(f"Order {order_id} cancelled successfully!")
else:
error(f"Failed to cancel order: {response.status_code}")
if response.text:
error(f"Error details: {response.text}")
raise click.Abort()
except Exception 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()

View File

@@ -0,0 +1,716 @@
"""
GPU Marketplace CLI Commands
Commands for bidding on and offering GPU power in the AITBC island marketplace
"""
import click
import json
import hashlib
import socket
import os
import asyncio
from datetime import datetime
from decimal import Decimal
from typing import Optional, List
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
)
@click.group()
def gpu():
"""GPU marketplace commands for bidding and offering GPU power"""
pass
@gpu.command()
@click.argument('gpu_count', type=int)
@click.argument('price_per_gpu', type=float)
@click.argument('duration_hours', type=int)
@click.option('--specs', help='GPU specifications (JSON string)')
@click.option('--description', help='Description of the GPU offer')
@click.pass_context
def offer(ctx, gpu_count: int, price_per_gpu: float, duration_hours: int, specs: Optional[str], description: Optional[str]):
"""Offer GPU power for sale in the marketplace"""
try:
# Load island credentials
credentials = load_island_credentials()
rpc_endpoint = get_rpc_endpoint()
chain_id = get_chain_id()
island_id = get_island_id()
# Get provider 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}"
provider_node_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()
# Calculate total price
total_price = price_per_gpu * gpu_count * duration_hours
# Generate offer ID
offer_id = f"gpu_offer_{datetime.now().strftime('%Y%m%d%H%M%S')}_{hashlib.sha256(f'{provider_node_id}{gpu_count}{price_per_gpu}'.encode()).hexdigest()[:8]}"
# Parse specifications
gpu_specs = {}
if specs:
try:
gpu_specs = json.loads(specs)
except json.JSONDecodeError:
error("Invalid JSON specifications")
raise click.Abort()
# Create offer transaction
offer_data = {
'type': 'gpu_marketplace',
'action': 'offer',
'offer_id': offer_id,
'provider_node_id': provider_node_id,
'gpu_count': gpu_count,
'price_per_gpu': float(price_per_gpu),
'duration_hours': duration_hours,
'total_price': float(total_price),
'status': 'active',
'specs': gpu_specs,
'description': description or f"{gpu_count} GPUs for {duration_hours} hours",
'island_id': island_id,
'chain_id': chain_id,
'created_at': datetime.now().isoformat()
}
# Submit transaction to blockchain
try:
import httpx
with httpx.Client() as client:
response = client.post(
f"{rpc_endpoint}/transaction",
json=offer_data,
timeout=10
)
if response.status_code == 200:
result = response.json()
success(f"GPU offer created successfully!")
success(f"Offer ID: {offer_id}")
success(f"Total Price: {total_price:.2f} AIT")
offer_info = {
"Offer ID": offer_id,
"GPU Count": gpu_count,
"Price per GPU": f"{price_per_gpu:.4f} AIT/hour",
"Duration": f"{duration_hours} hours",
"Total Price": f"{total_price:.2f} AIT",
"Status": "active",
"Provider Node": provider_node_id[:16] + "...",
"Island": island_id[:16] + "..."
}
output(offer_info, ctx.obj.get('output_format', 'table'))
else:
error(f"Failed to submit transaction: {response.status_code}")
if response.text:
error(f"Error details: {response.text}")
raise click.Abort()
except Exception as e:
error(f"Network error submitting transaction: {e}")
raise click.Abort()
except Exception as e:
error(f"Error creating GPU offer: {str(e)}")
raise click.Abort()
@gpu.command()
@click.argument('gpu_count', type=int)
@click.argument('max_price', type=float)
@click.argument('duration_hours', type=int)
@click.option('--specs', help='Required GPU specifications (JSON string)')
@click.pass_context
def bid(ctx, gpu_count: int, max_price: float, duration_hours: int, specs: Optional[str]):
"""Bid on GPU power in the marketplace"""
try:
# Load island credentials
credentials = load_island_credentials()
rpc_endpoint = get_rpc_endpoint()
chain_id = get_chain_id()
island_id = get_island_id()
# Get bidder 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}"
bidder_node_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()
# Calculate max total price
max_total_price = max_price * gpu_count * duration_hours
# Generate bid ID
bid_id = f"gpu_bid_{datetime.now().strftime('%Y%m%d%H%M%S')}_{hashlib.sha256(f'{bidder_node_id}{gpu_count}{max_price}'.encode()).hexdigest()[:8]}"
# Parse specifications
gpu_specs = {}
if specs:
try:
gpu_specs = json.loads(specs)
except json.JSONDecodeError:
error("Invalid JSON specifications")
raise click.Abort()
# Create bid transaction
bid_data = {
'type': 'gpu_marketplace',
'action': 'bid',
'bid_id': bid_id,
'bidder_node_id': bidder_node_id,
'gpu_count': gpu_count,
'max_price_per_gpu': float(max_price),
'duration_hours': duration_hours,
'max_total_price': float(max_total_price),
'status': 'pending',
'specs': gpu_specs,
'island_id': island_id,
'chain_id': chain_id,
'created_at': datetime.now().isoformat()
}
# Submit transaction to blockchain
try:
import httpx
with httpx.Client() as client:
response = client.post(
f"{rpc_endpoint}/v1/transactions",
json=bid_data,
timeout=10
)
if response.status_code == 200:
result = response.json()
success(f"GPU bid created successfully!")
success(f"Bid ID: {bid_id}")
success(f"Max Total Price: {max_total_price:.2f} AIT")
bid_info = {
"Bid ID": bid_id,
"GPU Count": gpu_count,
"Max Price per GPU": f"{max_price:.4f} AIT/hour",
"Duration": f"{duration_hours} hours",
"Max Total Price": f"{max_total_price:.2f} AIT",
"Status": "pending",
"Bidder Node": bidder_node_id[:16] + "...",
"Island": island_id[:16] + "..."
}
output(bid_info, ctx.obj.get('output_format', 'table'))
else:
error(f"Failed to submit transaction: {response.status_code}")
if response.text:
error(f"Error details: {response.text}")
raise click.Abort()
except Exception as e:
error(f"Network error submitting transaction: {e}")
raise click.Abort()
except Exception as e:
error(f"Error creating GPU bid: {str(e)}")
raise click.Abort()
@gpu.command()
@click.option('--provider', help='Filter by provider node ID')
@click.option('--status', help='Filter by status (active, pending, accepted, completed, cancelled)')
@click.option('--type', type=click.Choice(['offer', 'bid', 'all']), default='all', help='Filter by type')
@click.pass_context
def list(ctx, provider: Optional[str], status: Optional[str], type: str):
"""List GPU marketplace offers and bids"""
try:
# Load island credentials
credentials = load_island_credentials()
rpc_endpoint = get_rpc_endpoint()
island_id = get_island_id()
# Query blockchain for GPU marketplace transactions
try:
import httpx
params = {
'transaction_type': 'gpu_marketplace',
'island_id': island_id
}
if provider:
params['provider_node_id'] = provider
if status:
params['status'] = status
if type != 'all':
params['action'] = type
with httpx.Client() as client:
response = client.get(
f"{rpc_endpoint}/transactions",
params=params,
timeout=10
)
if response.status_code == 200:
transactions = response.json()
if not transactions:
info("No GPU marketplace transactions found")
return
# Format output
market_data = []
for tx in transactions:
action = tx.get('action')
if action == 'offer':
market_data.append({
"ID": tx.get('offer_id', tx.get('transaction_id', 'N/A'))[:20] + "...",
"Type": "OFFER",
"GPU Count": tx.get('gpu_count'),
"Price": f"{tx.get('price_per_gpu', 0):.4f} AIT/h",
"Duration": f"{tx.get('duration_hours')}h",
"Total": f"{tx.get('total_price', 0):.2f} AIT",
"Status": tx.get('status'),
"Provider": tx.get('provider_node_id', '')[:16] + "...",
"Created": tx.get('created_at', '')[:19]
})
elif action == 'bid':
market_data.append({
"ID": tx.get('bid_id', tx.get('transaction_id', 'N/A'))[:20] + "...",
"Type": "BID",
"GPU Count": tx.get('gpu_count'),
"Max Price": f"{tx.get('max_price_per_gpu', 0):.4f} AIT/h",
"Duration": f"{tx.get('duration_hours')}h",
"Max Total": f"{tx.get('max_total_price', 0):.2f} AIT",
"Status": tx.get('status'),
"Bidder": tx.get('bidder_node_id', '')[:16] + "...",
"Created": tx.get('created_at', '')[:19]
})
output(market_data, ctx.obj.get('output_format', 'table'), title=f"GPU Marketplace ({island_id[:16]}...)")
else:
error(f"Failed to query blockchain: {response.status_code}")
raise click.Abort()
except Exception as e:
error(f"Network error querying blockchain: {e}")
raise click.Abort()
except Exception as e:
error(f"Error listing GPU marketplace: {str(e)}")
raise click.Abort()
@gpu.command()
@click.argument('order_id')
@click.pass_context
def cancel(ctx, order_id: str):
"""Cancel a GPU offer or bid"""
try:
# Load island credentials
credentials = load_island_credentials()
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()
# Determine if it's an offer or bid
if order_id.startswith('gpu_offer'):
action = 'cancel_offer'
node_id_field = 'provider_node_id'
elif order_id.startswith('gpu_bid'):
action = 'cancel_bid'
node_id_field = 'bidder_node_id'
else:
error("Invalid order ID format. Must start with 'gpu_offer' or 'gpu_bid'")
raise click.Abort()
# Create cancel transaction
cancel_data = {
'type': 'gpu_marketplace',
'action': action,
'order_id': order_id,
'node_id': local_node_id,
'status': 'cancelled',
'cancelled_at': datetime.now().isoformat(),
'island_id': island_id,
'chain_id': chain_id
}
# Submit transaction to blockchain
try:
import httpx
with httpx.Client() as client:
response = client.post(
f"{rpc_endpoint}/transaction",
json=cancel_data,
timeout=10
)
if response.status_code == 200:
success(f"Order {order_id} cancelled successfully!")
else:
error(f"Failed to cancel order: {response.status_code}")
if response.text:
error(f"Error details: {response.text}")
raise click.Abort()
except Exception 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()
@gpu.command()
@click.argument('bid_id')
@click.pass_context
def accept(ctx, bid_id: str):
"""Accept a GPU bid (provider only)"""
try:
# Load island credentials
credentials = load_island_credentials()
rpc_endpoint = get_rpc_endpoint()
chain_id = get_chain_id()
island_id = get_island_id()
# Get provider 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}"
provider_node_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()
# Create accept transaction
accept_data = {
'type': 'gpu_marketplace',
'action': 'accept',
'bid_id': bid_id,
'provider_node_id': provider_node_id,
'status': 'accepted',
'accepted_at': datetime.now().isoformat(),
'island_id': island_id,
'chain_id': chain_id
}
# Submit transaction to blockchain
try:
import httpx
with httpx.Client() as client:
response = client.post(
f"{rpc_endpoint}/transaction",
json=accept_data,
timeout=10
)
if response.status_code == 200:
success(f"Bid {bid_id} accepted successfully!")
else:
error(f"Failed to accept bid: {response.status_code}")
if response.text:
error(f"Error details: {response.text}")
raise click.Abort()
except Exception as e:
error(f"Network error submitting transaction: {e}")
raise click.Abort()
except Exception as e:
error(f"Error accepting bid: {str(e)}")
raise click.Abort()
@gpu.command()
@click.argument('order_id')
@click.pass_context
def status(ctx, order_id: str):
"""Check the status of a GPU order"""
try:
# Load island credentials
credentials = load_island_credentials()
rpc_endpoint = get_rpc_endpoint()
island_id = get_island_id()
# Query blockchain for the order
try:
import httpx
params = {
'transaction_type': 'gpu_marketplace',
'island_id': island_id,
'order_id': order_id
}
with httpx.Client() as client:
response = client.get(
f"{rpc_endpoint}/transactions",
params=params,
timeout=10
)
if response.status_code == 200:
transactions = response.json()
if not transactions:
error(f"Order {order_id} not found")
raise click.Abort()
tx = transactions[0]
action = tx.get('action')
order_info = {
"Order ID": order_id,
"Type": action.upper(),
"Status": tx.get('status'),
"Created": tx.get('created_at'),
}
if action == 'offer':
order_info.update({
"GPU Count": tx.get('gpu_count'),
"Price per GPU": f"{tx.get('price_per_gpu', 0):.4f} AIT/h",
"Duration": f"{tx.get('duration_hours')}h",
"Total Price": f"{tx.get('total_price', 0):.2f} AIT",
"Provider": tx.get('provider_node_id', '')[:16] + "..."
})
elif action == 'bid':
order_info.update({
"GPU Count": tx.get('gpu_count'),
"Max Price": f"{tx.get('max_price_per_gpu', 0):.4f} AIT/h",
"Duration": f"{tx.get('duration_hours')}h",
"Max Total": f"{tx.get('max_total_price', 0):.2f} AIT",
"Bidder": tx.get('bidder_node_id', '')[:16] + "..."
})
if 'accepted_at' in tx:
order_info["Accepted"] = tx['accepted_at']
if 'cancelled_at' in tx:
order_info["Cancelled"] = tx['cancelled_at']
output(order_info, ctx.obj.get('output_format', 'table'), title=f"Order Status: {order_id}")
else:
error(f"Failed to query blockchain: {response.status_code}")
raise click.Abort()
except Exception as e:
error(f"Network error querying blockchain: {e}")
raise click.Abort()
except Exception as e:
error(f"Error checking order status: {str(e)}")
raise click.Abort()
@gpu.command()
@click.pass_context
def match(ctx):
"""Match GPU bids with offers (price discovery)"""
try:
# Load island credentials
credentials = load_island_credentials()
rpc_endpoint = get_rpc_endpoint()
island_id = get_island_id()
# Query blockchain for open offers and bids
try:
import httpx
params = {
'transaction_type': 'gpu_marketplace',
'island_id': island_id,
'status': 'active'
}
with httpx.Client() as client:
response = client.get(
f"{rpc_endpoint}/transactions",
params=params,
timeout=10
)
if response.status_code == 200:
transactions = response.json()
# Separate offers and bids
offers = []
bids = []
for tx in transactions:
if tx.get('action') == 'offer':
offers.append(tx)
elif tx.get('action') == 'bid':
bids.append(tx)
if not offers or not bids:
info("No active offers or bids to match")
return
# Sort offers by price (lowest first)
offers.sort(key=lambda x: x.get('price_per_gpu', float('inf')))
# Sort bids by price (highest first)
bids.sort(key=lambda x: x.get('max_price_per_gpu', 0), reverse=True)
# Match bids with offers
matches = []
for bid in bids:
for offer in offers:
# Check if bid price >= offer price
if bid.get('max_price_per_gpu', 0) >= offer.get('price_per_gpu', float('inf')):
# Check if GPU count matches
if bid.get('gpu_count') == offer.get('gpu_count'):
# Check if duration matches
if bid.get('duration_hours') == offer.get('duration_hours'):
# Create match transaction
match_data = {
'type': 'gpu_marketplace',
'action': 'match',
'bid_id': bid.get('bid_id'),
'offer_id': offer.get('offer_id'),
'bidder_node_id': bid.get('bidder_node_id'),
'provider_node_id': offer.get('provider_node_id'),
'gpu_count': bid.get('gpu_count'),
'matched_price': offer.get('price_per_gpu'),
'duration_hours': bid.get('duration_hours'),
'total_price': offer.get('total_price'),
'status': 'matched',
'matched_at': datetime.now().isoformat(),
'island_id': island_id,
'chain_id': get_chain_id()
}
# Submit match transaction
match_response = client.post(
f"{rpc_endpoint}/transaction",
json=match_data,
timeout=10
)
if match_response.status_code == 200:
matches.append({
"Bid ID": bid.get('bid_id')[:16] + "...",
"Offer ID": offer.get('offer_id')[:16] + "...",
"GPU Count": bid.get('gpu_count'),
"Matched Price": f"{offer.get('price_per_gpu', 0):.4f} AIT/h",
"Total Price": f"{offer.get('total_price', 0):.2f} AIT",
"Duration": f"{bid.get('duration_hours')}h"
})
if matches:
success(f"Matched {len(matches)} GPU orders!")
output(matches, ctx.obj.get('output_format', 'table'), title="GPU Order Matches")
else:
info("No matching orders found")
else:
error(f"Failed to query blockchain: {response.status_code}")
raise click.Abort()
except Exception as e:
error(f"Network error querying blockchain: {e}")
raise click.Abort()
except Exception as e:
error(f"Error matching orders: {str(e)}")
raise click.Abort()
@gpu.command()
@click.pass_context
def providers(ctx):
"""Query island members for GPU providers"""
try:
# Load island credentials
credentials = load_island_credentials()
island_id = get_island_id()
# Load island members from credentials
members = credentials.get('members', [])
if not members:
warning("No island members found in credentials")
return
# Query each member for GPU availability via P2P
info(f"Querying {len(members)} island members for GPU availability...")
# For now, display the members
# In a full implementation, this would use P2P network to query each member
provider_data = []
for member in members:
provider_data.append({
"Node ID": member.get('node_id', '')[:16] + "...",
"Address": member.get('address', 'N/A'),
"Port": member.get('port', 'N/A'),
"Is Hub": member.get('is_hub', False),
"Public Address": member.get('public_address', 'N/A'),
"Public Port": member.get('public_port', 'N/A')
})
output(provider_data, ctx.obj.get('output_format', 'table'), title=f"Island Members ({island_id[:16]}...)")
info("Note: GPU availability query via P2P network to be implemented")
except Exception as e:
error(f"Error querying GPU providers: {str(e)}")
raise click.Abort()

View File

@@ -1,7 +1,19 @@
"""Node management commands for AITBC CLI"""
"""
Node management commands for AITBC
"""
import os
import sys
import socket
import json
import hashlib
import click
import asyncio
from pathlib import Path
from typing import Optional
from datetime import datetime
from ..utils.output import output, success, error, warning, info
from ..core.config import MultiChainConfig, load_multichain_config, get_default_node_config, add_node_config, remove_node_config
from ..core.node_client import NodeClient
from ..utils import output, error, success
@@ -480,23 +492,107 @@ def create(ctx, island_id, island_name, chain_id):
@click.argument('island_id')
@click.argument('island_name')
@click.argument('chain_id')
@click.option('--hub', default='hub.aitbc.bubuit.net', help='Hub domain name to connect to')
@click.option('--is-hub', is_flag=True, help='Register this node as a hub for the island')
@click.pass_context
def join(ctx, island_id, island_name, chain_id, is_hub):
def join(ctx, island_id, island_name, chain_id, hub, is_hub):
"""Join an existing island"""
try:
join_info = {
"Island ID": island_id,
"Island Name": island_name,
"Chain ID": chain_id,
"As Hub": is_hub
}
output(join_info, ctx.obj.get('output_format', 'table'), title=f"Joining Island: {island_name}")
success(f"Successfully joined island {island_name}")
# Note: In a real implementation, this would update the island manager
# Get system hostname
hostname = socket.gethostname()
# Get public key from keystore
keystore_path = '/var/lib/aitbc/keystore/validator_keys.json'
public_key_pem = None
if os.path.exists(keystore_path):
with open(keystore_path, 'r') as f:
keys = json.load(f)
# Get first key's public key
for key_id, key_data in keys.items():
public_key_pem = key_data.get('public_key_pem')
break
else:
error(f"Keystore not found at {keystore_path}")
raise click.Abort()
if not public_key_pem:
error("No public key found in keystore")
raise click.Abort()
# Generate node_id using hostname-based method
local_address = socket.gethostbyname(hostname)
local_port = 8001 # Default hub port
content = f"{hostname}:{local_address}:{local_port}:{public_key_pem}"
node_id = hashlib.sha256(content.encode()).hexdigest()
# Resolve hub domain to IP
hub_ip = socket.gethostbyname(hub)
hub_port = 8001 # Default hub port
info(f"Connecting to hub {hub} ({hub_ip}:{hub_port})...")
# Create P2P network service instance for sending join request
sys.path.insert(0, '/opt/aitbc/apps/blockchain-node/src')
from aitbc_chain.p2p_network import P2PNetworkService
# Create a minimal P2P service just for sending the join request
p2p_service = P2PNetworkService(local_address, local_port, node_id, [])
# Send join request
async def send_join():
return await p2p_service.send_join_request(
hub_ip, hub_port, island_id, island_name, node_id, public_key_pem
)
response = asyncio.run(send_join())
if response:
# Store credentials locally
credentials_path = '/var/lib/aitbc/island_credentials.json'
credentials_data = {
"island_id": response.get('island_id'),
"island_name": response.get('island_name'),
"island_chain_id": response.get('island_chain_id'),
"credentials": response.get('credentials'),
"joined_at": datetime.now().isoformat()
}
with open(credentials_path, 'w') as f:
json.dump(credentials_data, f, indent=2)
# Display join info
join_info = {
"Island ID": response.get('island_id'),
"Island Name": response.get('island_name'),
"Chain ID": response.get('island_chain_id'),
"Member Count": len(response.get('members', [])),
"Credentials Stored": credentials_path
}
output(join_info, ctx.obj.get('output_format', 'table'), title=f"Joined Island: {island_name}")
# Display member list
members = response.get('members', [])
if members:
output(members, ctx.obj.get('output_format', 'table'), title="Island Members")
# Display credentials
credentials = response.get('credentials', {})
if credentials:
output(credentials, ctx.obj.get('output_format', 'table'), title="Blockchain Credentials")
success(f"Successfully joined island {island_name}")
# If registering as hub
if is_hub:
info("Registering as hub...")
# Hub registration would happen here via the hub register command
info("Run 'aitbc node hub register' to complete hub registration")
else:
error("Failed to join island - no response from hub")
raise click.Abort()
except Exception as e:
error(f"Error joining island: {str(e)}")
raise click.Abort()
@@ -568,57 +664,225 @@ def hub():
@hub.command()
@click.option('--public-address', help='Public IP address')
@click.option('--public-port', type=int, help='Public port')
@click.option('--redis-url', default='redis://localhost:6379', help='Redis URL for persistence')
@click.option('--hub-discovery-url', default='hub.aitbc.bubuit.net', help='DNS hub discovery URL')
@click.pass_context
def register(ctx, public_address, public_port):
def register(ctx, public_address, public_port, redis_url, hub_discovery_url):
"""Register this node as a hub"""
try:
hub_info = {
"Node ID": "local-node",
"Status": "Registered",
"Public Address": public_address or "auto-discovered",
"Public Port": public_port or "auto-discovered"
}
output(hub_info, ctx.obj.get('output_format', 'table'), title="Hub Registration")
success("Successfully registered as hub")
# Note: In a real implementation, this would update the hub manager
# Get environment variables
island_id = os.getenv('ISLAND_ID', 'default-island-id')
island_name = os.getenv('ISLAND_NAME', 'default')
# Get system hostname
hostname = socket.gethostname()
# Get public key from keystore
keystore_path = '/var/lib/aitbc/keystore/validator_keys.json'
public_key_pem = None
if os.path.exists(keystore_path):
with open(keystore_path, 'r') as f:
keys = json.load(f)
# Get first key's public key
for key_id, key_data in keys.items():
public_key_pem = key_data.get('public_key_pem')
break
else:
error(f"Keystore not found at {keystore_path}")
raise click.Abort()
if not public_key_pem:
error("No public key found in keystore")
raise click.Abort()
# Generate node_id using hostname-based method
local_address = socket.gethostbyname(hostname)
local_port = 7070 # Default hub port
content = f"{hostname}:{local_address}:{local_port}:{public_key_pem}"
node_id = hashlib.sha256(content.encode()).hexdigest()
# Create HubManager instance
sys.path.insert(0, '/opt/aitbc/apps/blockchain-node/src')
from aitbc_chain.network.hub_manager import HubManager
from aitbc_chain.network.hub_discovery import HubDiscovery
hub_manager = HubManager(
node_id,
local_address,
local_port,
island_id,
island_name,
redis_url
)
# Register as hub (async)
async def register_hub():
success = await hub_manager.register_as_hub(public_address, public_port)
if success:
# Register with DNS discovery service
hub_discovery = HubDiscovery(hub_discovery_url, local_port)
hub_info_dict = {
"node_id": node_id,
"address": local_address,
"port": local_port,
"island_id": island_id,
"island_name": island_name,
"public_address": public_address,
"public_port": public_port,
"public_key_pem": public_key_pem
}
dns_success = await hub_discovery.register_hub(hub_info_dict)
return success and dns_success
return False
result = asyncio.run(register_hub())
if result:
hub_info = {
"Node ID": node_id,
"Hostname": hostname,
"Address": local_address,
"Port": local_port,
"Island ID": island_id,
"Island Name": island_name,
"Public Address": public_address or "auto-discovered",
"Public Port": public_port or "auto-discovered",
"Status": "Registered"
}
output(hub_info, ctx.obj.get('output_format', 'table'), title="Hub Registration")
success("Successfully registered as hub")
else:
error("Failed to register as hub")
raise click.Abort()
except Exception as e:
error(f"Error registering as hub: {str(e)}")
raise click.Abort()
@hub.command()
@click.option('--redis-url', default='redis://localhost:6379', help='Redis URL for persistence')
@click.option('--hub-discovery-url', default='hub.aitbc.bubuit.net', help='DNS hub discovery URL')
@click.pass_context
def unregister(ctx):
def unregister(ctx, redis_url, hub_discovery_url):
"""Unregister this node as a hub"""
try:
success("Successfully unregistered as hub")
# Note: In a real implementation, this would update the hub manager
# Get environment variables
island_id = os.getenv('ISLAND_ID', 'default-island-id')
island_name = os.getenv('ISLAND_NAME', 'default')
# Get system hostname
hostname = socket.gethostname()
# Get public key from keystore
keystore_path = '/var/lib/aitbc/keystore/validator_keys.json'
public_key_pem = None
if os.path.exists(keystore_path):
with open(keystore_path, 'r') as f:
keys = json.load(f)
# Get first key's public key
for key_id, key_data in keys.items():
public_key_pem = key_data.get('public_key_pem')
break
else:
error(f"Keystore not found at {keystore_path}")
raise click.Abort()
if not public_key_pem:
error("No public key found in keystore")
raise click.Abort()
# Generate node_id using hostname-based method
local_address = socket.gethostbyname(hostname)
local_port = 7070 # Default hub port
content = f"{hostname}:{local_address}:{local_port}:{public_key_pem}"
node_id = hashlib.sha256(content.encode()).hexdigest()
# Create HubManager instance
sys.path.insert(0, '/opt/aitbc/apps/blockchain-node/src')
from aitbc_chain.network.hub_manager import HubManager
from aitbc_chain.network.hub_discovery import HubDiscovery
hub_manager = HubManager(
node_id,
local_address,
local_port,
island_id,
island_name,
redis_url
)
# Unregister as hub (async)
async def unregister_hub():
success = await hub_manager.unregister_as_hub()
if success:
# Unregister from DNS discovery service
hub_discovery = HubDiscovery(hub_discovery_url, local_port)
dns_success = await hub_discovery.unregister_hub(node_id)
return success and dns_success
return False
result = asyncio.run(unregister_hub())
if result:
hub_info = {
"Node ID": node_id,
"Status": "Unregistered"
}
output(hub_info, ctx.obj.get('output_format', 'table'), title="Hub Unregistration")
success("Successfully unregistered as hub")
else:
error("Failed to unregister as hub")
raise click.Abort()
except Exception as e:
error(f"Error unregistering as hub: {str(e)}")
raise click.Abort()
@hub.command()
@click.option('--redis-url', default='redis://localhost:6379', help='Redis URL for persistence')
@click.pass_context
def list(ctx):
"""List known hubs"""
def list(ctx, redis_url):
"""List registered hubs from Redis"""
try:
# Note: In a real implementation, this would query the hub manager
hubs = [
{
"Node ID": "hub-node-1",
"Address": "10.1.1.1",
"Port": 7070,
"Island ID": "550e8400-e29b-41d4-a716-446655440000",
"Peer Count": "5"
}
]
output(hubs, ctx.obj.get('output_format', 'table'), title="Known Hubs")
import redis.asyncio as redis
async def list_hubs():
hubs = []
try:
r = redis.from_url(redis_url)
# Get all hub keys
keys = await r.keys("hub:*")
for key in keys:
value = await r.get(key)
if value:
hub_data = json.loads(value)
hubs.append({
"Node ID": hub_data.get("node_id"),
"Address": hub_data.get("address"),
"Port": hub_data.get("port"),
"Island ID": hub_data.get("island_id"),
"Island Name": hub_data.get("island_name"),
"Public Address": hub_data.get("public_address", "N/A"),
"Public Port": hub_data.get("public_port", "N/A"),
"Peer Count": hub_data.get("peer_count", 0)
})
await r.close()
except Exception as e:
error(f"Failed to query Redis: {e}")
return []
return hubs
hubs = asyncio.run(list_hubs())
if hubs:
output(hubs, ctx.obj.get('output_format', 'table'), title="Registered Hubs")
else:
info("No registered hubs found")
except Exception as e:
error(f"Error listing hubs: {str(e)}")
raise click.Abort()

View File

@@ -0,0 +1,181 @@
"""
Island Credential Loading Utility
Provides functions to load and validate island credentials from the local filesystem
"""
import json
import os
from typing import Dict, Optional
from pathlib import Path
CREDENTIALS_PATH = '/var/lib/aitbc/island_credentials.json'
def load_island_credentials() -> Dict:
"""
Load island credentials from the local filesystem
Returns:
dict: Island credentials containing island_id, island_name, chain_id, credentials, etc.
Raises:
FileNotFoundError: If credentials file does not exist
json.JSONDecodeError: If credentials file is invalid JSON
ValueError: If credentials are invalid or missing required fields
"""
credentials_path = Path(CREDENTIALS_PATH)
if not credentials_path.exists():
raise FileNotFoundError(
f"Island credentials not found at {CREDENTIALS_PATH}. "
f"Run 'aitbc node island join' to join an island first."
)
with open(credentials_path, 'r') as f:
credentials = json.load(f)
# Validate required fields
required_fields = ['island_id', 'island_name', 'island_chain_id', 'credentials']
for field in required_fields:
if field not in credentials:
raise ValueError(f"Invalid credentials: missing required field '{field}'")
return credentials
def get_rpc_endpoint() -> str:
"""
Get the RPC endpoint from island credentials
Returns:
str: RPC endpoint URL
Raises:
FileNotFoundError: If credentials file does not exist
ValueError: If RPC endpoint is missing from credentials
"""
credentials = load_island_credentials()
rpc_endpoint = credentials.get('credentials', {}).get('rpc_endpoint')
if not rpc_endpoint:
raise ValueError("RPC endpoint not found in island credentials")
return rpc_endpoint
def get_chain_id() -> str:
"""
Get the chain ID from island credentials
Returns:
str: Chain ID
Raises:
FileNotFoundError: If credentials file does not exist
ValueError: If chain ID is missing from credentials
"""
credentials = load_island_credentials()
chain_id = credentials.get('island_chain_id')
if not chain_id:
raise ValueError("Chain ID not found in island credentials")
return chain_id
def get_island_id() -> str:
"""
Get the island ID from island credentials
Returns:
str: Island ID
Raises:
FileNotFoundError: If credentials file does not exist
ValueError: If island ID is missing from credentials
"""
credentials = load_island_credentials()
island_id = credentials.get('island_id')
if not island_id:
raise ValueError("Island ID not found in island credentials")
return island_id
def get_island_name() -> str:
"""
Get the island name from island credentials
Returns:
str: Island name
Raises:
FileNotFoundError: If credentials file does not exist
ValueError: If island name is missing from credentials
"""
credentials = load_island_credentials()
island_name = credentials.get('island_name')
if not island_name:
raise ValueError("Island name not found in island credentials")
return island_name
def get_genesis_block_hash() -> Optional[str]:
"""
Get the genesis block hash from island credentials
Returns:
str: Genesis block hash, or None if not available
"""
try:
credentials = load_island_credentials()
return credentials.get('credentials', {}).get('genesis_block_hash')
except (FileNotFoundError, ValueError):
return None
def get_genesis_address() -> Optional[str]:
"""
Get the genesis address from island credentials
Returns:
str: Genesis address, or None if not available
"""
try:
credentials = load_island_credentials()
return credentials.get('credentials', {}).get('genesis_address')
except (FileNotFoundError, ValueError):
return None
def validate_credentials() -> bool:
"""
Validate that island credentials exist and are valid
Returns:
bool: True if credentials are valid, False otherwise
"""
try:
credentials = load_island_credentials()
# Check for essential fields
return all(key in credentials for key in ['island_id', 'island_name', 'island_chain_id', 'credentials'])
except (FileNotFoundError, json.JSONDecodeError, ValueError):
return False
def get_p2p_port() -> Optional[int]:
"""
Get the P2P port from island credentials
Returns:
int: P2P port, or None if not available
"""
try:
credentials = load_island_credentials()
return credentials.get('credentials', {}).get('p2p_port')
except (FileNotFoundError, ValueError):
return None