Files
aitbc/cli/aitbc_cli/commands/marketplace_cmd.py
aitbc 51209844c9 Implement 36 missing CLI subcommands for scenarios 04-47
Phase 1: Simple GET Commands (system, resource, edge, config)
- system: restart, status, config
- resource: status, deallocate
- edge: status, balance, transfer
- config: import (already existed)

Phase 2: Exchange & Market Commands
- exchange: order, orders, book, history
- marketplace: bid, bids, ask, asks

Phase 3: AI & GPU Commands
- agent (AI): job, jobs, submit, cancel
- gpu: register, update

Phase 4: Wallet, Hermes, Operations
- wallet: export, import
- hermes: send, receive, peers
- operations: vote, proposal, delegate

Phase 5: Simulate Commands
- simulate: run, status, result

All commands follow common pattern using AITBCHTTPClient for backend API interaction.
2026-05-26 22:03:58 +02:00

620 lines
24 KiB
Python
Executable File

"""Global chain marketplace commands for AITBC CLI"""
import click
import asyncio
import json
from decimal import Decimal
from datetime import datetime
from typing import Optional
from ..core.config import load_multichain_config
from ..core.marketplace import (
GlobalChainMarketplace, ChainType, MarketplaceStatus,
TransactionStatus
)
from ..utils import output, error, success
from ..config import get_config
from aitbc import get_logger, AITBCHTTPClient, NetworkError
logger = get_logger(__name__)
@click.group()
@click.option("--chain-id", help="Chain ID for multichain operations (e.g., ait-mainnet, ait-devnet)")
@click.pass_context
def marketplace(ctx, chain_id: Optional[str]):
"""Global chain marketplace commands"""
ctx.ensure_object(dict)
# Handle chain_id with auto-detection
from ..utils.chain_id import get_chain_id
config = load_multichain_config()
default_rpc_url = config.blockchain_rpc_url if hasattr(config, 'blockchain_rpc_url') else 'http://localhost:8006'
ctx.obj['chain_id'] = get_chain_id(default_rpc_url, override=chain_id)
@marketplace.command()
@click.argument('chain_id')
@click.argument('chain_name')
@click.argument('chain_type')
@click.argument('description')
@click.argument('seller_id')
@click.argument('price')
@click.option('--currency', default='ETH', help='Currency for pricing')
@click.option('--specs', help='Chain specifications (JSON string)')
@click.option('--metadata', help='Additional metadata (JSON string)')
@click.pass_context
def list(ctx, chain_id, chain_name, chain_type, description, seller_id, price, currency, specs, metadata):
"""List a chain for sale in the marketplace"""
try:
config = get_config()
from aitbc import AITBCHTTPClient
# Parse chain type
try:
chain_type_enum = ChainType(chain_type)
except ValueError:
error(f"Invalid chain type: {chain_type}")
error(f"Valid types: {[t.value for t in ChainType]}")
raise click.Abort()
# Parse price
try:
price_decimal = Decimal(price)
except (ValueError, TypeError):
error("Invalid price format")
raise click.Abort()
# Parse specifications
chain_specs = {}
if specs:
try:
chain_specs = json.loads(specs)
except json.JSONDecodeError:
error("Invalid JSON specifications")
raise click.Abort()
# Parse metadata
metadata_dict = {}
if metadata:
try:
metadata_dict = json.loads(metadata)
except json.JSONDecodeError:
error("Invalid JSON metadata")
raise click.Abort()
# Create listing transaction
listing_id = f"chain_listing_{datetime.now().strftime('%Y%m%d%H%M%S')}"
listing_data = {
'type': 'marketplace',
'action': 'list',
'listing_id': listing_id,
'chain_id': chain_id,
'chain_name': chain_name,
'chain_type': chain_type,
'description': description,
'seller_id': seller_id,
'price': float(price),
'currency': currency,
'specs': chain_specs,
'metadata': metadata_dict,
'status': 'active',
'created_at': datetime.now().isoformat()
}
# Submit transaction to marketplace service
try:
http_client = AITBCHTTPClient(base_url=config.marketplace_service_url, timeout=10)
result = http_client.post("/v1/transactions", json=listing_data)
success(f"Chain listed successfully! Listing ID: {listing_id}")
listing_info = {
"Listing ID": listing_id,
"Chain ID": chain_id,
"Chain Name": chain_name,
"Type": chain_type,
"Price": f"{price} {currency}",
"Seller": seller_id,
"Status": "active",
"Created": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
output(listing_info, ctx.obj.get('output_format', 'table'))
except Exception as e:
error(f"Error submitting transaction: {e}")
raise click.Abort()
except Exception as e:
error(f"Error creating listing: {str(e)}")
raise click.Abort()
@marketplace.command()
@click.argument('listing_id')
@click.argument('buyer_id')
@click.option('--payment', default='crypto', help='Payment method')
@click.pass_context
def buy(ctx, listing_id, buyer_id, payment):
"""Purchase a chain from the marketplace"""
try:
config = load_multichain_config()
marketplace = GlobalChainMarketplace(config)
# Purchase chain
transaction_id = asyncio.run(marketplace.purchase_chain(listing_id, buyer_id, payment))
if transaction_id:
success(f"Purchase initiated! Transaction ID: {transaction_id}")
transaction_data = {
"Transaction ID": transaction_id,
"Listing ID": listing_id,
"Buyer": buyer_id,
"Payment Method": payment,
"Status": "pending",
"Created": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
output(transaction_data, ctx.obj.get('output_format', 'table'))
else:
error("Failed to purchase chain")
raise click.Abort()
except Exception as e:
error(f"Error purchasing chain: {str(e)}")
raise click.Abort()
@marketplace.command()
@click.argument('transaction_id')
@click.argument('transaction_hash')
@click.pass_context
def complete(ctx, transaction_id, transaction_hash):
"""Complete a marketplace transaction"""
try:
config = load_multichain_config()
marketplace = GlobalChainMarketplace(config)
# Complete transaction
success = asyncio.run(marketplace.complete_transaction(transaction_id, transaction_hash))
if success:
success(f"Transaction {transaction_id} completed successfully!")
transaction_data = {
"Transaction ID": transaction_id,
"Transaction Hash": transaction_hash,
"Status": "completed",
"Completed": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
output(transaction_data, ctx.obj.get('output_format', 'table'))
else:
error(f"Failed to complete transaction {transaction_id}")
raise click.Abort()
except Exception as e:
error(f"Error completing transaction: {str(e)}")
raise click.Abort()
@marketplace.command()
@click.option('--type', help='Filter by chain type')
@click.option('--min-price', help='Minimum price')
@click.option('--max-price', help='Maximum price')
@click.option('--seller', help='Filter by seller ID')
@click.option('--status', help='Filter by listing status')
@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format')
@click.pass_context
def search(ctx, type, min_price, max_price, seller, status, format):
"""Search chain listings in the marketplace"""
try:
config = load_multichain_config()
marketplace = GlobalChainMarketplace(config)
# Parse filters
chain_type = None
if type:
try:
chain_type = ChainType(type)
except ValueError:
error(f"Invalid chain type: {type}")
raise click.Abort()
min_price_dec = None
if min_price:
try:
min_price_dec = Decimal(min_price)
except (ValueError, TypeError):
error("Invalid minimum price format")
raise click.Abort()
max_price_dec = None
if max_price:
try:
max_price_dec = Decimal(max_price)
except (ValueError, TypeError):
error("Invalid maximum price format")
raise click.Abort()
listing_status = None
if status:
try:
listing_status = MarketplaceStatus(status)
except ValueError:
error(f"Invalid status: {status}")
raise click.Abort()
# Search listings
listings = asyncio.run(marketplace.search_listings(
chain_type, min_price_dec, max_price_dec, seller, listing_status
))
if not listings:
output("No listings found matching your criteria", ctx.obj.get('output_format', 'table'))
return
# Format output
listing_data = [
{
"Listing ID": listing.listing_id,
"Chain ID": listing.chain_id,
"Chain Name": listing.chain_name,
"Type": listing.chain_type.value,
"Price": f"{listing.price} {listing.currency}",
"Seller": listing.seller_id,
"Status": listing.status.value,
"Created": listing.created_at.strftime("%Y-%m-%d %H:%M:%S"),
"Expires": listing.expires_at.strftime("%Y-%m-%d %H:%M:%S")
}
for listing in listings
]
output(listing_data, ctx.obj.get('output_format', format), title="Marketplace Listings")
except Exception as e:
error(f"Error searching listings: {str(e)}")
raise click.Abort()
@marketplace.command()
@click.argument('chain_id')
@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format')
@click.pass_context
def economy(ctx, chain_id, format):
"""Get economic metrics for a specific chain"""
try:
config = load_multichain_config()
marketplace = GlobalChainMarketplace(config)
# Get chain economy
economy = asyncio.run(marketplace.get_chain_economy(chain_id))
if not economy:
error(f"No economic data available for chain {chain_id}")
raise click.Abort()
# Format output
economy_data = [
{"Metric": "Chain ID", "Value": economy.chain_id},
{"Metric": "Total Value Locked", "Value": f"{economy.total_value_locked} ETH"},
{"Metric": "Daily Volume", "Value": f"{economy.daily_volume} ETH"},
{"Metric": "Market Cap", "Value": f"{economy.market_cap} ETH"},
{"Metric": "Transaction Count", "Value": economy.transaction_count},
{"Metric": "Active Users", "Value": economy.active_users},
{"Metric": "Agent Count", "Value": economy.agent_count},
{"Metric": "Governance Tokens", "Value": f"{economy.governance_tokens}"},
{"Metric": "Staking Rewards", "Value": f"{economy.staking_rewards}"},
{"Metric": "Last Updated", "Value": economy.last_updated.strftime("%Y-%m-%d %H:%M:%S")}
]
output(economy_data, ctx.obj.get('output_format', format), title=f"Chain Economy: {chain_id}")
except Exception as e:
error(f"Error getting chain economy: {str(e)}")
raise click.Abort()
@marketplace.command()
@click.argument('user_id')
@click.option('--role', type=click.Choice(['buyer', 'seller', 'both']), default='both', help='User role')
@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format')
@click.pass_context
def transactions(ctx, user_id, role, format):
"""Get transactions for a specific user"""
try:
config = load_multichain_config()
marketplace = GlobalChainMarketplace(config)
# Get user transactions
transactions = asyncio.run(marketplace.get_user_transactions(user_id, role))
if not transactions:
output(f"No transactions found for user {user_id}", ctx.obj.get('output_format', 'table'))
return
# Format output
transaction_data = [
{
"Transaction ID": transaction.transaction_id,
"Listing ID": transaction.listing_id,
"Chain ID": transaction.chain_id,
"Price": f"{transaction.price} {transaction.currency}",
"Role": "buyer" if transaction.buyer_id == user_id else "seller",
"Counterparty": transaction.seller_id if transaction.buyer_id == user_id else transaction.buyer_id,
"Status": transaction.status.value,
"Created": transaction.created_at.strftime("%Y-%m-%d %H:%M:%S"),
"Completed": transaction.completed_at.strftime("%Y-%m-%d %H:%M:%S") if transaction.completed_at else "N/A"
}
for transaction in transactions
]
output(transaction_data, ctx.obj.get('output_format', format), title=f"Transactions for {user_id}")
except Exception as e:
error(f"Error getting user transactions: {str(e)}")
raise click.Abort()
@marketplace.command()
@click.option('--format', type=click.Choice(['table', 'json']), default='table', help='Output format')
@click.pass_context
def overview(ctx, format):
"""Get comprehensive marketplace overview"""
try:
config = load_multichain_config()
marketplace = GlobalChainMarketplace(config)
# Get marketplace overview
overview = asyncio.run(marketplace.get_marketplace_overview())
if not overview:
error("No marketplace data available")
raise click.Abort()
# Marketplace metrics
if "marketplace_metrics" in overview:
metrics = overview["marketplace_metrics"]
metrics_data = [
{"Metric": "Total Listings", "Value": metrics["total_listings"]},
{"Metric": "Active Listings", "Value": metrics["active_listings"]},
{"Metric": "Total Transactions", "Value": metrics["total_transactions"]},
{"Metric": "Total Volume", "Value": f"{metrics['total_volume']} ETH"},
{"Metric": "Average Price", "Value": f"{metrics['average_price']} ETH"},
{"Metric": "Market Sentiment", "Value": f"{metrics['market_sentiment']:.2f}"}
]
output(metrics_data, ctx.obj.get('output_format', format), title="Marketplace Metrics")
# Volume 24h
if "volume_24h" in overview:
volume_data = [
{"Metric": "24h Volume", "Value": f"{overview['volume_24h']} ETH"}
]
output(volume_data, ctx.obj.get('output_format', format), title="24-Hour Volume")
# Top performing chains
if "top_performing_chains" in overview:
chains = overview["top_performing_chains"]
if chains:
chain_data = [
{
"Chain ID": chain["chain_id"],
"Volume": f"{chain['volume']} ETH",
"Transactions": chain["transactions"]
}
for chain in chains[:5] # Top 5
]
output(chain_data, ctx.obj.get('output_format', format), title="Top Performing Chains")
# Chain types distribution
if "chain_types_distribution" in overview:
distribution = overview["chain_types_distribution"]
if distribution:
dist_data = [
{"Chain Type": chain_type, "Count": count}
for chain_type, count in distribution.items()
]
output(dist_data, ctx.obj.get('output_format', format), title="Chain Types Distribution")
# User activity
if "user_activity" in overview:
activity = overview["user_activity"]
activity_data = [
{"Metric": "Active Buyers (7d)", "Value": activity["active_buyers_7d"]},
{"Metric": "Active Sellers (7d)", "Value": activity["active_sellers_7d"]},
{"Metric": "Total Unique Users", "Value": activity["total_unique_users"]},
{"Metric": "Average Reputation", "Value": f"{activity['average_reputation']:.3f}"}
]
output(activity_data, ctx.obj.get('output_format', format), title="User Activity")
# Escrow summary
if "escrow_summary" in overview:
escrow = overview["escrow_summary"]
escrow_data = [
{"Metric": "Active Escrows", "Value": escrow["active_escrows"]},
{"Metric": "Released Escrows", "Value": escrow["released_escrows"]},
{"Metric": "Total Escrow Value", "Value": f"{escrow['total_escrow_value']} ETH"},
{"Metric": "Escrow Fees Collected", "Value": f"{escrow['escrow_fee_collected']} ETH"}
]
output(escrow_data, ctx.obj.get('output_format', format), title="Escrow Summary")
except Exception as e:
error(f"Error getting marketplace overview: {str(e)}")
raise click.Abort()
@marketplace.command()
@click.option('--realtime', is_flag=True, help='Real-time monitoring')
@click.option('--interval', default=30, help='Update interval in seconds')
@click.pass_context
def monitor(ctx, realtime, interval):
"""Monitor marketplace activity"""
try:
config = load_multichain_config()
marketplace = GlobalChainMarketplace(config)
if realtime:
# Real-time monitoring
from rich.console import Console
from rich.live import Live
from rich.table import Table
import time
console = Console()
def generate_monitor_table():
try:
overview = asyncio.run(marketplace.get_marketplace_overview())
table = Table(title=f"Marketplace Monitor - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
table.add_column("Metric", style="cyan")
table.add_column("Value", style="green")
if "marketplace_metrics" in overview:
metrics = overview["marketplace_metrics"]
table.add_row("Total Listings", str(metrics["total_listings"]))
table.add_row("Active Listings", str(metrics["active_listings"]))
table.add_row("Total Transactions", str(metrics["total_transactions"]))
table.add_row("Total Volume", f"{metrics['total_volume']} ETH")
table.add_row("Market Sentiment", f"{metrics['market_sentiment']:.2f}")
if "volume_24h" in overview:
table.add_row("24h Volume", f"{overview['volume_24h']} ETH")
if "user_activity" in overview:
activity = overview["user_activity"]
table.add_row("Active Users (7d)", str(activity["active_buyers_7d"] + activity["active_sellers_7d"]))
return table
except Exception as e:
return f"Error getting marketplace data: {e}"
with Live(generate_monitor_table(), refresh_per_second=1) as live:
try:
while True:
live.update(generate_monitor_table())
time.sleep(interval)
except KeyboardInterrupt:
console.click.echo("\n[yellow]Monitoring stopped by user[/yellow]")
else:
# Single snapshot
overview = asyncio.run(marketplace.get_marketplace_overview())
monitor_data = []
if "marketplace_metrics" in overview:
metrics = overview["marketplace_metrics"]
monitor_data.extend([
{"Metric": "Total Listings", "Value": metrics["total_listings"]},
{"Metric": "Active Listings", "Value": metrics["active_listings"]},
{"Metric": "Total Transactions", "Value": metrics["total_transactions"]},
{"Metric": "Total Volume", "Value": f"{metrics['total_volume']} ETH"},
{"Metric": "Market Sentiment", "Value": f"{metrics['market_sentiment']:.2f}"}
])
if "volume_24h" in overview:
monitor_data.append({"Metric": "24h Volume", "Value": f"{overview['volume_24h']} ETH"})
if "user_activity" in overview:
activity = overview["user_activity"]
monitor_data.append({"Metric": "Active Users (7d)", "Value": activity["active_buyers_7d"] + activity["active_sellers_7d"]})
output(monitor_data, ctx.obj.get('output_format', 'table'), title="Marketplace Monitor")
except Exception as e:
error(f"Error during monitoring: {str(e)}")
raise click.Abort()
@marketplace.command()
@click.argument('price', type=float)
@click.argument('quantity', type=float)
@click.option('--market', help='Market identifier')
@click.pass_context
def bid(ctx, price: float, quantity: float, market: Optional[str]):
"""Place a bid in the marketplace"""
config = get_config()
try:
http_client = AITBCHTTPClient(base_url=config.marketplace_service_url, timeout=10)
bid_data = {
"price": price,
"quantity": quantity,
"market": market or "default"
}
result = http_client.post("/marketplace/bid", json=bid_data)
success(f"Bid placed: {quantity} @ {price}")
output(result, ctx.obj.get("output_format", "table"))
except NetworkError as e:
error(f"Network error: {e}")
except Exception as e:
error(f"Error placing bid: {e}")
@marketplace.command()
@click.option('--market', help='Filter by market')
@click.option('--limit', type=int, default=20, help='Number of bids to return')
@click.pass_context
def bids(ctx, market: Optional[str], limit: int):
"""List bids from the marketplace"""
config = get_config()
try:
http_client = AITBCHTTPClient(base_url=config.marketplace_service_url, timeout=10)
params = {"limit": limit}
if market:
params["market"] = market
bids_data = http_client.get("/marketplace/bids", params=params)
success("Bids:")
output(bids_data, ctx.obj.get("output_format", "table"))
except NetworkError as e:
error(f"Network error: {e}")
except Exception as e:
error(f"Error fetching bids: {e}")
@marketplace.command()
@click.argument('price', type=float)
@click.argument('quantity', type=float)
@click.option('--market', help='Market identifier')
@click.pass_context
def ask(ctx, price: float, quantity: float, market: Optional[str]):
"""Place an ask in the marketplace"""
config = get_config()
try:
http_client = AITBCHTTPClient(base_url=config.marketplace_service_url, timeout=10)
ask_data = {
"price": price,
"quantity": quantity,
"market": market or "default"
}
result = http_client.post("/marketplace/ask", json=ask_data)
success(f"Ask placed: {quantity} @ {price}")
output(result, ctx.obj.get("output_format", "table"))
except NetworkError as e:
error(f"Network error: {e}")
except Exception as e:
error(f"Error placing ask: {e}")
@marketplace.command()
@click.option('--market', help='Filter by market')
@click.option('--limit', type=int, default=20, help='Number of asks to return')
@click.pass_context
def asks(ctx, market: Optional[str], limit: int):
"""List asks from the marketplace"""
config = get_config()
try:
http_client = AITBCHTTPClient(base_url=config.marketplace_service_url, timeout=10)
params = {"limit": limit}
if market:
params["market"] = market
asks_data = http_client.get("/marketplace/asks", params=params)
success("Asks:")
output(asks_data, ctx.obj.get("output_format", "table"))
except NetworkError as e:
error(f"Network error: {e}")
except Exception as e:
error(f"Error fetching asks: {e}")