- Auto-detect GPU hardware using nvidia-smi - Validate GPU name and memory against detected hardware - Prevent fake GPU registrations (RTX 4080 on RTX 4060 Ti system) - Add --force flag for emergency override situations - Ensure only real hardware can be registered Fixes issue where fake GPUs could be registered on systems with different hardware.
1040 lines
36 KiB
Python
Executable File
1040 lines
36 KiB
Python
Executable File
"""Marketplace commands for AITBC CLI"""
|
|
|
|
import click
|
|
import httpx
|
|
import json
|
|
import asyncio
|
|
from typing import Optional, List, Dict, Any
|
|
from ..utils import output, error, success
|
|
|
|
|
|
@click.group()
|
|
def marketplace():
|
|
"""GPU marketplace operations"""
|
|
pass
|
|
|
|
|
|
@marketplace.group()
|
|
def gpu():
|
|
"""GPU marketplace operations"""
|
|
pass
|
|
|
|
|
|
@gpu.command()
|
|
@click.option("--name", help="GPU name/model (auto-detected if not provided)")
|
|
@click.option("--memory", type=int, help="GPU memory in GB (auto-detected if not provided)")
|
|
@click.option("--cuda-cores", type=int, help="Number of CUDA cores")
|
|
@click.option("--compute-capability", help="Compute capability (e.g., 8.9)")
|
|
@click.option("--price-per-hour", type=float, required=True, help="Price per hour in AITBC")
|
|
@click.option("--description", help="GPU description")
|
|
@click.option("--miner-id", help="Miner ID (uses auth key if not provided)")
|
|
@click.option("--force", is_flag=True, help="Force registration even if hardware validation fails")
|
|
@click.pass_context
|
|
def register(ctx, name: Optional[str], memory: Optional[int], cuda_cores: Optional[int],
|
|
compute_capability: Optional[str], price_per_hour: Optional[float],
|
|
description: Optional[str], miner_id: Optional[str], force: bool):
|
|
"""Register GPU on marketplace (auto-detects hardware)"""
|
|
config = ctx.obj['config']
|
|
|
|
# Auto-detect GPU hardware
|
|
try:
|
|
import subprocess
|
|
result = subprocess.run(['nvidia-smi', '--query-gpu=name,memory.total', '--format=csv,noheader,nounits'],
|
|
capture_output=True, text=True, check=True)
|
|
|
|
if result.returncode == 0:
|
|
gpu_info = result.stdout.strip().split(', ')
|
|
detected_name = gpu_info[0].strip()
|
|
detected_memory = int(gpu_info[1].strip())
|
|
|
|
# Use detected values if not provided
|
|
if not name:
|
|
name = detected_name
|
|
if memory is None:
|
|
memory = detected_memory
|
|
|
|
# Validate provided specs against detected hardware
|
|
if not force:
|
|
if name and name != detected_name:
|
|
error(f"GPU name mismatch! Detected: '{detected_name}', Provided: '{name}'. Use --force to override.")
|
|
return
|
|
if memory and memory != detected_memory:
|
|
error(f"GPU memory mismatch! Detected: {detected_memory}GB, Provided: {memory}GB. Use --force to override.")
|
|
return
|
|
|
|
success(f"Auto-detected GPU: {detected_name} with {detected_memory}GB memory")
|
|
else:
|
|
if not force:
|
|
error("Failed to detect GPU hardware. Use --force to register without hardware validation.")
|
|
return
|
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
if not force:
|
|
error("nvidia-smi not available. Use --force to register without hardware validation.")
|
|
return
|
|
|
|
# Build GPU specs
|
|
gpu_specs = {
|
|
"name": name,
|
|
"memory_gb": memory,
|
|
"cuda_cores": cuda_cores,
|
|
"compute_capability": compute_capability,
|
|
"price_per_hour": price_per_hour,
|
|
"description": description
|
|
}
|
|
|
|
# Remove None values
|
|
gpu_specs = {k: v for k, v in gpu_specs.items() if v is not None}
|
|
|
|
try:
|
|
with httpx.Client() as client:
|
|
response = client.post(
|
|
f"{config.coordinator_url}/v1/marketplace/gpu/register",
|
|
headers={
|
|
"Content-Type": "application/json",
|
|
"X-Api-Key": config.api_key or "",
|
|
"X-Miner-ID": miner_id or "default"
|
|
},
|
|
json={"gpu": gpu_specs}
|
|
)
|
|
|
|
if response.status_code in (200, 201):
|
|
result = response.json()
|
|
success(f"GPU registered successfully: {result.get('gpu_id')}")
|
|
output(result, ctx.obj['output_format'])
|
|
else:
|
|
error(f"Failed to register GPU: {response.status_code}")
|
|
except Exception as e:
|
|
error(f"Network error: {e}")
|
|
|
|
|
|
@gpu.command()
|
|
@click.option("--available", is_flag=True, help="Show only available GPUs")
|
|
@click.option("--model", help="Filter by GPU model (supports wildcards)")
|
|
@click.option("--memory-min", type=int, help="Minimum memory in GB")
|
|
@click.option("--price-max", type=float, help="Maximum price per hour")
|
|
@click.option("--limit", type=int, default=20, help="Maximum number of results")
|
|
@click.pass_context
|
|
def list(ctx, available: bool, model: Optional[str], memory_min: Optional[int],
|
|
price_max: Optional[float], limit: int):
|
|
"""List available GPUs"""
|
|
config = ctx.obj['config']
|
|
|
|
# Build query params
|
|
params = {"limit": limit}
|
|
if available:
|
|
params["available"] = "true"
|
|
if model:
|
|
params["model"] = model
|
|
if memory_min:
|
|
params["memory_min"] = memory_min
|
|
if price_max:
|
|
params["price_max"] = price_max
|
|
|
|
try:
|
|
with httpx.Client() as client:
|
|
response = client.get(
|
|
f"{config.coordinator_url}/v1/marketplace/gpu/list",
|
|
params=params,
|
|
headers={"X-Api-Key": config.api_key or ""}
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
gpus = response.json()
|
|
output(gpus, ctx.obj['output_format'])
|
|
else:
|
|
error(f"Failed to list GPUs: {response.status_code}")
|
|
except Exception as e:
|
|
error(f"Network error: {e}")
|
|
|
|
|
|
@gpu.command()
|
|
@click.argument("gpu_id")
|
|
@click.pass_context
|
|
def details(ctx, gpu_id: str):
|
|
"""Get GPU details"""
|
|
config = ctx.obj['config']
|
|
|
|
try:
|
|
with httpx.Client() as client:
|
|
response = client.get(
|
|
f"{config.coordinator_url}/v1/marketplace/gpu/{gpu_id}",
|
|
headers={"X-Api-Key": config.api_key or ""}
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
gpu_data = response.json()
|
|
output(gpu_data, ctx.obj['output_format'])
|
|
else:
|
|
error(f"GPU not found: {response.status_code}")
|
|
except Exception as e:
|
|
error(f"Network error: {e}")
|
|
|
|
|
|
@gpu.command()
|
|
@click.argument("gpu_id")
|
|
@click.option("--hours", type=float, required=True, help="Rental duration in hours")
|
|
@click.option("--job-id", help="Job ID to associate with rental")
|
|
@click.pass_context
|
|
def book(ctx, gpu_id: str, hours: float, job_id: Optional[str]):
|
|
"""Book a GPU"""
|
|
config = ctx.obj['config']
|
|
|
|
try:
|
|
booking_data = {
|
|
"gpu_id": gpu_id,
|
|
"duration_hours": hours
|
|
}
|
|
if job_id:
|
|
booking_data["job_id"] = job_id
|
|
|
|
with httpx.Client() as client:
|
|
response = client.post(
|
|
f"{config.coordinator_url}/v1/marketplace/gpu/{gpu_id}/book",
|
|
headers={
|
|
"Content-Type": "application/json",
|
|
"X-Api-Key": config.api_key or ""
|
|
},
|
|
json=booking_data
|
|
)
|
|
|
|
if response.status_code in (200, 201):
|
|
booking = response.json()
|
|
success(f"GPU booked successfully: {booking.get('booking_id')}")
|
|
output(booking, ctx.obj['output_format'])
|
|
else:
|
|
error(f"Failed to book GPU: {response.status_code}")
|
|
except Exception as e:
|
|
error(f"Network error: {e}")
|
|
|
|
|
|
@gpu.command()
|
|
@click.argument("gpu_id")
|
|
@click.pass_context
|
|
def release(ctx, gpu_id: str):
|
|
"""Release a booked GPU"""
|
|
config = ctx.obj['config']
|
|
|
|
try:
|
|
with httpx.Client() as client:
|
|
response = client.post(
|
|
f"{config.coordinator_url}/v1/marketplace/gpu/{gpu_id}/release",
|
|
headers={"X-Api-Key": config.api_key or ""}
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
success(f"GPU {gpu_id} released")
|
|
output({"status": "released", "gpu_id": gpu_id}, ctx.obj['output_format'])
|
|
else:
|
|
error(f"Failed to release GPU: {response.status_code}")
|
|
except Exception as e:
|
|
error(f"Network error: {e}")
|
|
|
|
|
|
@marketplace.command()
|
|
@click.option("--status", help="Filter by status (active, completed, cancelled)")
|
|
@click.option("--limit", type=int, default=10, help="Number of orders to show")
|
|
@click.pass_context
|
|
def orders(ctx, status: Optional[str], limit: int):
|
|
"""List marketplace orders"""
|
|
config = ctx.obj['config']
|
|
|
|
params = {"limit": limit}
|
|
if status:
|
|
params["status"] = status
|
|
|
|
try:
|
|
with httpx.Client() as client:
|
|
response = client.get(
|
|
f"{config.coordinator_url}/v1/marketplace/orders",
|
|
params=params,
|
|
headers={"X-Api-Key": config.api_key or ""}
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
orders = response.json()
|
|
output(orders, ctx.obj['output_format'])
|
|
else:
|
|
error(f"Failed to get orders: {response.status_code}")
|
|
except Exception as e:
|
|
error(f"Network error: {e}")
|
|
|
|
|
|
@marketplace.command()
|
|
@click.argument("model")
|
|
@click.pass_context
|
|
def pricing(ctx, model: str):
|
|
"""Get pricing information for a GPU model"""
|
|
config = ctx.obj['config']
|
|
|
|
try:
|
|
with httpx.Client() as client:
|
|
response = client.get(
|
|
f"{config.coordinator_url}/v1/marketplace/pricing/{model}",
|
|
headers={"X-Api-Key": config.api_key or ""}
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
pricing_data = response.json()
|
|
output(pricing_data, ctx.obj['output_format'])
|
|
else:
|
|
error(f"Pricing not found: {response.status_code}")
|
|
except Exception as e:
|
|
error(f"Network error: {e}")
|
|
|
|
|
|
@marketplace.command()
|
|
@click.argument("gpu_id")
|
|
@click.option("--limit", type=int, default=10, help="Number of reviews to show")
|
|
@click.pass_context
|
|
def reviews(ctx, gpu_id: str, limit: int):
|
|
"""Get GPU reviews"""
|
|
config = ctx.obj['config']
|
|
|
|
try:
|
|
with httpx.Client() as client:
|
|
response = client.get(
|
|
f"{config.coordinator_url}/v1/marketplace/gpu/{gpu_id}/reviews",
|
|
params={"limit": limit},
|
|
headers={"X-Api-Key": config.api_key or ""}
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
reviews = response.json()
|
|
output(reviews, ctx.obj['output_format'])
|
|
else:
|
|
error(f"Failed to get reviews: {response.status_code}")
|
|
except Exception as e:
|
|
error(f"Network error: {e}")
|
|
|
|
|
|
@marketplace.command()
|
|
@click.argument("gpu_id")
|
|
@click.option("--rating", type=int, required=True, help="Rating (1-5)")
|
|
@click.option("--comment", help="Review comment")
|
|
@click.pass_context
|
|
def review(ctx, gpu_id: str, rating: int, comment: Optional[str]):
|
|
"""Add a review for a GPU"""
|
|
config = ctx.obj['config']
|
|
|
|
if not 1 <= rating <= 5:
|
|
error("Rating must be between 1 and 5")
|
|
return
|
|
|
|
try:
|
|
review_data = {
|
|
"rating": rating,
|
|
"comment": comment
|
|
}
|
|
|
|
with httpx.Client() as client:
|
|
response = client.post(
|
|
f"{config.coordinator_url}/v1/marketplace/gpu/{gpu_id}/reviews",
|
|
headers={
|
|
"Content-Type": "application/json",
|
|
"X-Api-Key": config.api_key or ""
|
|
},
|
|
json=review_data
|
|
)
|
|
|
|
if response.status_code in (200, 201):
|
|
success("Review added successfully")
|
|
output({"status": "review_added", "gpu_id": gpu_id}, ctx.obj['output_format'])
|
|
else:
|
|
error(f"Failed to add review: {response.status_code}")
|
|
except Exception as e:
|
|
error(f"Network error: {e}")
|
|
|
|
|
|
@marketplace.group()
|
|
def bid():
|
|
"""Marketplace bid operations"""
|
|
pass
|
|
|
|
|
|
@bid.command()
|
|
@click.option("--provider", required=True, help="Provider ID (e.g., miner123)")
|
|
@click.option("--capacity", type=int, required=True, help="Bid capacity (number of units)")
|
|
@click.option("--price", type=float, required=True, help="Price per unit in AITBC")
|
|
@click.option("--notes", help="Additional notes for the bid")
|
|
@click.pass_context
|
|
def submit(ctx, provider: str, capacity: int, price: float, notes: Optional[str]):
|
|
"""Submit a bid to the marketplace"""
|
|
config = ctx.obj['config']
|
|
|
|
# Validate inputs
|
|
if capacity <= 0:
|
|
error("Capacity must be greater than 0")
|
|
return
|
|
if price <= 0:
|
|
error("Price must be greater than 0")
|
|
return
|
|
|
|
# Build bid data
|
|
bid_data = {
|
|
"provider": provider,
|
|
"capacity": capacity,
|
|
"price": price
|
|
}
|
|
if notes:
|
|
bid_data["notes"] = notes
|
|
|
|
try:
|
|
with httpx.Client() as client:
|
|
response = client.post(
|
|
f"{config.coordinator_url}/v1/marketplace/bids",
|
|
headers={
|
|
"Content-Type": "application/json",
|
|
"X-Api-Key": config.api_key or ""
|
|
},
|
|
json=bid_data
|
|
)
|
|
|
|
if response.status_code == 202:
|
|
result = response.json()
|
|
success(f"Bid submitted successfully: {result.get('id')}")
|
|
output(result, ctx.obj['output_format'])
|
|
else:
|
|
error(f"Failed to submit bid: {response.status_code}")
|
|
if response.text:
|
|
error(f"Error details: {response.text}")
|
|
except Exception as e:
|
|
error(f"Network error: {e}")
|
|
|
|
|
|
@bid.command()
|
|
@click.option("--status", help="Filter by bid status (pending, accepted, rejected)")
|
|
@click.option("--provider", help="Filter by provider ID")
|
|
@click.option("--limit", type=int, default=20, help="Maximum number of results")
|
|
@click.pass_context
|
|
def list(ctx, status: Optional[str], provider: Optional[str], limit: int):
|
|
"""List marketplace bids"""
|
|
config = ctx.obj['config']
|
|
|
|
# Build query params
|
|
params = {"limit": limit}
|
|
if status:
|
|
params["status"] = status
|
|
if provider:
|
|
params["provider"] = provider
|
|
|
|
try:
|
|
with httpx.Client() as client:
|
|
response = client.get(
|
|
f"{config.coordinator_url}/v1/marketplace/bids",
|
|
params=params,
|
|
headers={"X-Api-Key": config.api_key or ""}
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
bids = response.json()
|
|
output(bids, ctx.obj['output_format'])
|
|
else:
|
|
error(f"Failed to list bids: {response.status_code}")
|
|
except Exception as e:
|
|
error(f"Network error: {e}")
|
|
|
|
|
|
@bid.command()
|
|
@click.argument("bid_id")
|
|
@click.pass_context
|
|
def details(ctx, bid_id: str):
|
|
"""Get bid details"""
|
|
config = ctx.obj['config']
|
|
|
|
try:
|
|
with httpx.Client() as client:
|
|
response = client.get(
|
|
f"{config.coordinator_url}/v1/marketplace/bids/{bid_id}",
|
|
headers={"X-Api-Key": config.api_key or ""}
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
bid_data = response.json()
|
|
output(bid_data, ctx.obj['output_format'])
|
|
else:
|
|
error(f"Bid not found: {response.status_code}")
|
|
except Exception as e:
|
|
error(f"Network error: {e}")
|
|
|
|
|
|
@marketplace.group()
|
|
def offers():
|
|
"""Marketplace offers operations"""
|
|
pass
|
|
|
|
|
|
@offers.command()
|
|
@click.option("--gpu-id", required=True, help="GPU ID to create offer for")
|
|
@click.option("--price-per-hour", type=float, required=True, help="Price per hour in AITBC")
|
|
@click.option("--min-hours", type=float, default=1, help="Minimum rental hours")
|
|
@click.option("--max-hours", type=float, default=24, help="Maximum rental hours")
|
|
@click.option("--models", help="Supported models (comma-separated, e.g. gemma3:1b,qwen2.5)")
|
|
@click.pass_context
|
|
def create(ctx, gpu_id: str, price_per_hour: float, min_hours: float,
|
|
max_hours: float, models: Optional[str]):
|
|
"""Create a marketplace offer for a registered GPU"""
|
|
config = ctx.obj['config']
|
|
|
|
offer_data = {
|
|
"gpu_id": gpu_id,
|
|
"price_per_hour": price_per_hour,
|
|
"min_hours": min_hours,
|
|
"max_hours": max_hours,
|
|
"supported_models": models.split(",") if models else [],
|
|
"status": "open"
|
|
}
|
|
|
|
try:
|
|
with httpx.Client() as client:
|
|
response = client.post(
|
|
f"{config.coordinator_url}/v1/marketplace/offers",
|
|
headers={
|
|
"Content-Type": "application/json",
|
|
"X-Api-Key": config.api_key or ""
|
|
},
|
|
json=offer_data
|
|
)
|
|
|
|
if response.status_code in (200, 201, 202):
|
|
result = response.json()
|
|
success(f"Offer created: {result.get('id', 'ok')}")
|
|
output(result, ctx.obj['output_format'])
|
|
else:
|
|
error(f"Failed to create offer: {response.status_code}")
|
|
if response.text:
|
|
error(response.text)
|
|
except Exception as e:
|
|
error(f"Network error: {e}")
|
|
|
|
|
|
@offers.command()
|
|
@click.option("--status", help="Filter by offer status (open, reserved, closed)")
|
|
@click.option("--gpu-model", help="Filter by GPU model")
|
|
@click.option("--price-max", type=float, help="Maximum price per hour")
|
|
@click.option("--memory-min", type=int, help="Minimum memory in GB")
|
|
@click.option("--region", help="Filter by region")
|
|
@click.option("--limit", type=int, default=20, help="Maximum number of results")
|
|
@click.pass_context
|
|
def list(ctx, status: Optional[str], gpu_model: Optional[str], price_max: Optional[float],
|
|
memory_min: Optional[int], region: Optional[str], limit: int):
|
|
"""List marketplace offers"""
|
|
config = ctx.obj['config']
|
|
|
|
# Build query params
|
|
params = {"limit": limit}
|
|
if status:
|
|
params["status"] = status
|
|
if gpu_model:
|
|
params["gpu_model"] = gpu_model
|
|
if price_max:
|
|
params["price_max"] = price_max
|
|
if memory_min:
|
|
params["memory_min"] = memory_min
|
|
if region:
|
|
params["region"] = region
|
|
|
|
try:
|
|
with httpx.Client() as client:
|
|
response = client.get(
|
|
f"{config.coordinator_url}/v1/marketplace/offers",
|
|
params=params,
|
|
headers={"X-Api-Key": config.api_key or ""}
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
offers = response.json()
|
|
output(offers, ctx.obj['output_format'])
|
|
else:
|
|
error(f"Failed to list offers: {response.status_code}")
|
|
except Exception as e:
|
|
error(f"Network error: {e}")
|
|
|
|
|
|
# OpenClaw Agent Marketplace Commands
|
|
@marketplace.group()
|
|
def agents():
|
|
"""OpenClaw agent marketplace operations"""
|
|
pass
|
|
|
|
|
|
@agents.command()
|
|
@click.option("--agent-id", required=True, help="Agent ID")
|
|
@click.option("--agent-type", required=True, help="Agent type (compute_provider, compute_consumer, power_trader)")
|
|
@click.option("--capabilities", help="Agent capabilities (comma-separated)")
|
|
@click.option("--region", help="Agent region")
|
|
@click.option("--reputation", type=float, default=0.8, help="Initial reputation score")
|
|
@click.pass_context
|
|
def register(ctx, agent_id: str, agent_type: str, capabilities: Optional[str],
|
|
region: Optional[str], reputation: float):
|
|
"""Register agent on OpenClaw marketplace"""
|
|
config = ctx.obj['config']
|
|
|
|
agent_data = {
|
|
"agent_id": agent_id,
|
|
"agent_type": agent_type,
|
|
"capabilities": capabilities.split(",") if capabilities else [],
|
|
"region": region,
|
|
"initial_reputation": reputation
|
|
}
|
|
|
|
try:
|
|
with httpx.Client() as client:
|
|
response = client.post(
|
|
f"{config.coordinator_url}/agents/register",
|
|
json=agent_data,
|
|
headers={"X-Api-Key": config.api_key or ""}
|
|
)
|
|
|
|
if response.status_code in (200, 201):
|
|
success(f"Agent {agent_id} registered successfully")
|
|
output(response.json(), ctx.obj['output_format'])
|
|
else:
|
|
error(f"Failed to register agent: {response.status_code}")
|
|
except Exception as e:
|
|
error(f"Network error: {e}")
|
|
|
|
|
|
@agents.command()
|
|
@click.option("--agent-id", help="Filter by agent ID")
|
|
@click.option("--agent-type", help="Filter by agent type")
|
|
@click.option("--region", help="Filter by region")
|
|
@click.option("--reputation-min", type=float, help="Minimum reputation score")
|
|
@click.option("--limit", type=int, default=20, help="Maximum number of results")
|
|
@click.pass_context
|
|
def list_agents(ctx, agent_id: Optional[str], agent_type: Optional[str],
|
|
region: Optional[str], reputation_min: Optional[float], limit: int):
|
|
"""List registered agents"""
|
|
config = ctx.obj['config']
|
|
|
|
params = {"limit": limit}
|
|
if agent_id:
|
|
params["agent_id"] = agent_id
|
|
if agent_type:
|
|
params["agent_type"] = agent_type
|
|
if region:
|
|
params["region"] = region
|
|
if reputation_min:
|
|
params["reputation_min"] = reputation_min
|
|
|
|
try:
|
|
with httpx.Client() as client:
|
|
response = client.get(
|
|
f"{config.coordinator_url}/agents",
|
|
params=params,
|
|
headers={"X-Api-Key": config.api_key or ""}
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
agents = response.json()
|
|
output(agents, ctx.obj['output_format'])
|
|
else:
|
|
error(f"Failed to list agents: {response.status_code}")
|
|
except Exception as e:
|
|
error(f"Network error: {e}")
|
|
|
|
|
|
@agents.command()
|
|
@click.option("--resource-id", required=True, help="AI resource ID")
|
|
@click.option("--resource-type", required=True, help="Resource type (nvidia_a100, nvidia_h100, edge_gpu)")
|
|
@click.option("--compute-power", type=float, required=True, help="Compute power (TFLOPS)")
|
|
@click.option("--gpu-memory", type=int, required=True, help="GPU memory in GB")
|
|
@click.option("--price-per-hour", type=float, required=True, help="Price per hour in AITBC")
|
|
@click.option("--provider-id", required=True, help="Provider agent ID")
|
|
@click.pass_context
|
|
def list_resource(ctx, resource_id: str, resource_type: str, compute_power: float,
|
|
gpu_memory: int, price_per_hour: float, provider_id: str):
|
|
"""List AI resource on marketplace"""
|
|
config = ctx.obj['config']
|
|
|
|
resource_data = {
|
|
"resource_id": resource_id,
|
|
"resource_type": resource_type,
|
|
"compute_power": compute_power,
|
|
"gpu_memory": gpu_memory,
|
|
"price_per_hour": price_per_hour,
|
|
"provider_id": provider_id,
|
|
"availability": True
|
|
}
|
|
|
|
try:
|
|
with httpx.Client() as client:
|
|
response = client.post(
|
|
f"{config.coordinator_url}/v1/marketplace/list",
|
|
json=resource_data,
|
|
headers={"X-Api-Key": config.api_key or ""}
|
|
)
|
|
|
|
if response.status_code in (200, 201):
|
|
success(f"Resource {resource_id} listed successfully")
|
|
output(response.json(), ctx.obj['output_format'])
|
|
else:
|
|
error(f"Failed to list resource: {response.status_code}")
|
|
except Exception as e:
|
|
error(f"Network error: {e}")
|
|
|
|
|
|
@agents.command()
|
|
@click.option("--resource-id", required=True, help="AI resource ID to rent")
|
|
@click.option("--consumer-id", required=True, help="Consumer agent ID")
|
|
@click.option("--duration", type=int, required=True, help="Rental duration in hours")
|
|
@click.option("--max-price", type=float, help="Maximum price per hour")
|
|
@click.pass_context
|
|
def rent(ctx, resource_id: str, consumer_id: str, duration: int, max_price: Optional[float]):
|
|
"""Rent AI resource from marketplace"""
|
|
config = ctx.obj['config']
|
|
|
|
rental_data = {
|
|
"resource_id": resource_id,
|
|
"consumer_id": consumer_id,
|
|
"duration_hours": duration,
|
|
"max_price_per_hour": max_price or 10.0,
|
|
"requirements": {
|
|
"min_compute_power": 50.0,
|
|
"min_gpu_memory": 8,
|
|
"gpu_required": True
|
|
}
|
|
}
|
|
|
|
try:
|
|
with httpx.Client() as client:
|
|
response = client.post(
|
|
f"{config.coordinator_url}/v1/marketplace/rent",
|
|
json=rental_data,
|
|
headers={"X-Api-Key": config.api_key or ""}
|
|
)
|
|
|
|
if response.status_code in (200, 201):
|
|
success("AI resource rented successfully")
|
|
output(response.json(), ctx.obj['output_format'])
|
|
else:
|
|
error(f"Failed to rent resource: {response.status_code}")
|
|
except Exception as e:
|
|
error(f"Network error: {e}")
|
|
|
|
|
|
@agents.command()
|
|
@click.option("--contract-type", required=True, help="Smart contract type")
|
|
@click.option("--params", required=True, help="Contract parameters (JSON string)")
|
|
@click.option("--gas-limit", type=int, default=1000000, help="Gas limit")
|
|
@click.pass_context
|
|
def execute_contract(ctx, contract_type: str, params: str, gas_limit: int):
|
|
"""Execute blockchain smart contract"""
|
|
config = ctx.obj['config']
|
|
|
|
try:
|
|
contract_params = json.loads(params)
|
|
except json.JSONDecodeError:
|
|
error("Invalid JSON parameters")
|
|
return
|
|
|
|
contract_data = {
|
|
"contract_type": contract_type,
|
|
"parameters": contract_params,
|
|
"gas_limit": gas_limit,
|
|
"value": contract_params.get("value", 0)
|
|
}
|
|
|
|
try:
|
|
with httpx.Client() as client:
|
|
response = client.post(
|
|
f"{config.coordinator_url}/blockchain/contracts/execute",
|
|
json=contract_data,
|
|
headers={"X-Api-Key": config.api_key or ""}
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
success("Smart contract executed successfully")
|
|
output(response.json(), ctx.obj['output_format'])
|
|
else:
|
|
error(f"Failed to execute contract: {response.status_code}")
|
|
except Exception as e:
|
|
error(f"Network error: {e}")
|
|
|
|
|
|
@agents.command()
|
|
@click.option("--from-agent", required=True, help="From agent ID")
|
|
@click.option("--to-agent", required=True, help="To agent ID")
|
|
@click.option("--amount", type=float, required=True, help="Amount in AITBC")
|
|
@click.option("--payment-type", default="ai_power_rental", help="Payment type")
|
|
@click.pass_context
|
|
def pay(ctx, from_agent: str, to_agent: str, amount: float, payment_type: str):
|
|
"""Process AITBC payment between agents"""
|
|
config = ctx.obj['config']
|
|
|
|
payment_data = {
|
|
"from_agent": from_agent,
|
|
"to_agent": to_agent,
|
|
"amount": amount,
|
|
"currency": "AITBC",
|
|
"payment_type": payment_type
|
|
}
|
|
|
|
try:
|
|
with httpx.Client() as client:
|
|
response = client.post(
|
|
f"{config.coordinator_url}/payments/process",
|
|
json=payment_data,
|
|
headers={"X-Api-Key": config.api_key or ""}
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
success(f"Payment of {amount} AITBC processed successfully")
|
|
output(response.json(), ctx.obj['output_format'])
|
|
else:
|
|
error(f"Failed to process payment: {response.status_code}")
|
|
except Exception as e:
|
|
error(f"Network error: {e}")
|
|
|
|
|
|
@agents.command()
|
|
@click.option("--agent-id", required=True, help="Agent ID")
|
|
@click.pass_context
|
|
def reputation(ctx, agent_id: str):
|
|
"""Get agent reputation information"""
|
|
config = ctx.obj['config']
|
|
|
|
try:
|
|
with httpx.Client() as client:
|
|
response = client.get(
|
|
f"{config.coordinator_url}/agents/{agent_id}/reputation",
|
|
headers={"X-Api-Key": config.api_key or ""}
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
output(response.json(), ctx.obj['output_format'])
|
|
else:
|
|
error(f"Failed to get reputation: {response.status_code}")
|
|
except Exception as e:
|
|
error(f"Network error: {e}")
|
|
|
|
|
|
@agents.command()
|
|
@click.option("--agent-id", required=True, help="Agent ID")
|
|
@click.pass_context
|
|
def balance(ctx, agent_id: str):
|
|
"""Get agent AITBC balance"""
|
|
config = ctx.obj['config']
|
|
|
|
try:
|
|
with httpx.Client() as client:
|
|
response = client.get(
|
|
f"{config.coordinator_url}/agents/{agent_id}/balance",
|
|
headers={"X-Api-Key": config.api_key or ""}
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
output(response.json(), ctx.obj['output_format'])
|
|
else:
|
|
error(f"Failed to get balance: {response.status_code}")
|
|
except Exception as e:
|
|
error(f"Network error: {e}")
|
|
|
|
|
|
@agents.command()
|
|
@click.option("--time-range", default="daily", help="Time range (daily, weekly, monthly)")
|
|
@click.pass_context
|
|
def analytics(ctx, time_range: str):
|
|
"""Get marketplace analytics"""
|
|
config = ctx.obj['config']
|
|
|
|
try:
|
|
with httpx.Client() as client:
|
|
response = client.get(
|
|
f"{config.coordinator_url}/analytics/marketplace",
|
|
params={"time_range": time_range},
|
|
headers={"X-Api-Key": config.api_key or ""}
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
output(response.json(), ctx.obj['output_format'])
|
|
else:
|
|
error(f"Failed to get analytics: {response.status_code}")
|
|
except Exception as e:
|
|
error(f"Network error: {e}")
|
|
|
|
|
|
# Governance Commands
|
|
@marketplace.group()
|
|
def governance():
|
|
"""OpenClaw agent governance operations"""
|
|
pass
|
|
|
|
|
|
@governance.command()
|
|
@click.option("--title", required=True, help="Proposal title")
|
|
@click.option("--description", required=True, help="Proposal description")
|
|
@click.option("--proposal-type", required=True, help="Proposal type")
|
|
@click.option("--params", required=True, help="Proposal parameters (JSON string)")
|
|
@click.option("--voting-period", type=int, default=72, help="Voting period in hours")
|
|
@click.pass_context
|
|
def create_proposal(ctx, title: str, description: str, proposal_type: str,
|
|
params: str, voting_period: int):
|
|
"""Create governance proposal"""
|
|
config = ctx.obj['config']
|
|
|
|
try:
|
|
proposal_params = json.loads(params)
|
|
except json.JSONDecodeError:
|
|
error("Invalid JSON parameters")
|
|
return
|
|
|
|
proposal_data = {
|
|
"title": title,
|
|
"description": description,
|
|
"proposal_type": proposal_type,
|
|
"proposed_changes": proposal_params,
|
|
"voting_period_hours": voting_period
|
|
}
|
|
|
|
try:
|
|
with httpx.Client() as client:
|
|
response = client.post(
|
|
f"{config.coordinator_url}/proposals/create",
|
|
json=proposal_data,
|
|
headers={"X-Api-Key": config.api_key or ""}
|
|
)
|
|
|
|
if response.status_code in (200, 201):
|
|
success("Proposal created successfully")
|
|
output(response.json(), ctx.obj['output_format'])
|
|
else:
|
|
error(f"Failed to create proposal: {response.status_code}")
|
|
except Exception as e:
|
|
error(f"Network error: {e}")
|
|
|
|
|
|
@governance.command()
|
|
@click.option("--proposal-id", required=True, help="Proposal ID")
|
|
@click.option("--vote", required=True, type=click.Choice(["for", "against", "abstain"]), help="Vote type")
|
|
@click.option("--reasoning", help="Vote reasoning")
|
|
@click.pass_context
|
|
def vote(ctx, proposal_id: str, vote: str, reasoning: Optional[str]):
|
|
"""Vote on governance proposal"""
|
|
config = ctx.obj['config']
|
|
|
|
vote_data = {
|
|
"proposal_id": proposal_id,
|
|
"vote": vote,
|
|
"reasoning": reasoning or ""
|
|
}
|
|
|
|
try:
|
|
with httpx.Client() as client:
|
|
response = client.post(
|
|
f"{config.coordinator_url}/voting/cast-vote",
|
|
json=vote_data,
|
|
headers={"X-Api-Key": config.api_key or ""}
|
|
)
|
|
|
|
if response.status_code in (200, 201):
|
|
success(f"Vote '{vote}' cast successfully")
|
|
output(response.json(), ctx.obj['output_format'])
|
|
else:
|
|
error(f"Failed to cast vote: {response.status_code}")
|
|
except Exception as e:
|
|
error(f"Network error: {e}")
|
|
|
|
|
|
@governance.command()
|
|
@click.option("--status", help="Filter by status")
|
|
@click.option("--limit", type=int, default=20, help="Maximum number of results")
|
|
@click.pass_context
|
|
def list_proposals(ctx, status: Optional[str], limit: int):
|
|
"""List governance proposals"""
|
|
config = ctx.obj['config']
|
|
|
|
params = {"limit": limit}
|
|
if status:
|
|
params["status"] = status
|
|
|
|
try:
|
|
with httpx.Client() as client:
|
|
response = client.get(
|
|
f"{config.coordinator_url}/proposals",
|
|
params=params,
|
|
headers={"X-Api-Key": config.api_key or ""}
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
output(response.json(), ctx.obj['output_format'])
|
|
else:
|
|
error(f"Failed to list proposals: {response.status_code}")
|
|
except Exception as e:
|
|
error(f"Network error: {e}")
|
|
|
|
|
|
# Performance Testing Commands
|
|
@marketplace.group()
|
|
def test():
|
|
"""OpenClaw marketplace testing operations"""
|
|
pass
|
|
|
|
|
|
@test.command()
|
|
@click.option("--concurrent-users", type=int, default=10, help="Concurrent users")
|
|
@click.option("--rps", type=int, default=50, help="Requests per second")
|
|
@click.option("--duration", type=int, default=30, help="Test duration in seconds")
|
|
@click.pass_context
|
|
def load(ctx, concurrent_users: int, rps: int, duration: int):
|
|
"""Run marketplace load test"""
|
|
config = ctx.obj['config']
|
|
|
|
test_config = {
|
|
"concurrent_users": concurrent_users,
|
|
"requests_per_second": rps,
|
|
"test_duration_seconds": duration,
|
|
"ramp_up_period_seconds": 5
|
|
}
|
|
|
|
try:
|
|
with httpx.Client() as client:
|
|
response = client.post(
|
|
f"{config.coordinator_url}/testing/load-test",
|
|
json=test_config,
|
|
headers={"X-Api-Key": config.api_key or ""}
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
success("Load test completed successfully")
|
|
output(response.json(), ctx.obj['output_format'])
|
|
else:
|
|
error(f"Failed to run load test: {response.status_code}")
|
|
except Exception as e:
|
|
error(f"Network error: {e}")
|
|
|
|
|
|
@test.command()
|
|
@click.pass_context
|
|
def health(ctx):
|
|
"""Test marketplace health endpoints"""
|
|
config = ctx.obj['config']
|
|
|
|
endpoints = [
|
|
"/health",
|
|
"/v1/v1/marketplace/status",
|
|
"/v1/agents/health",
|
|
"/v1/blockchain/health"
|
|
]
|
|
|
|
results = {}
|
|
|
|
for endpoint in endpoints:
|
|
try:
|
|
with httpx.Client() as client:
|
|
response = client.get(
|
|
f"{config.coordinator_url}{endpoint}",
|
|
headers={"X-Api-Key": config.api_key or ""}
|
|
)
|
|
results[endpoint] = {
|
|
"status_code": response.status_code,
|
|
"healthy": response.status_code == 200
|
|
}
|
|
except Exception as e:
|
|
results[endpoint] = {
|
|
"status_code": 0,
|
|
"healthy": False,
|
|
"error": str(e)
|
|
}
|
|
|
|
output(results, ctx.obj['output_format'])
|