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
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:
556
cli/aitbc_cli/commands/exchange_island.py
Normal file
556
cli/aitbc_cli/commands/exchange_island.py
Normal 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()
|
||||
716
cli/aitbc_cli/commands/gpu_marketplace.py
Normal file
716
cli/aitbc_cli/commands/gpu_marketplace.py
Normal 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()
|
||||
@@ -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()
|
||||
|
||||
181
cli/aitbc_cli/utils/island_credentials.py
Normal file
181
cli/aitbc_cli/utils/island_credentials.py
Normal 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
|
||||
Reference in New Issue
Block a user