feat: add blockchain RPC blocks-range endpoint and marketplace bid listing
Blockchain Node: - Replace /blocks (pagination) with /blocks-range (height range query) - Add start/end height parameters with 1000-block max range validation - Return blocks in ascending height order instead of descending - Update metrics names (rpc_get_blocks_range_*) - Remove total count from response (return start/end/count instead) Coordinator API: - Add effective_url property to DatabaseConfig (SQLite/PostgreSQL defaults
This commit is contained in:
224
cli/aitbc_cli/commands/exchange.py
Normal file
224
cli/aitbc_cli/commands/exchange.py
Normal file
@@ -0,0 +1,224 @@
|
||||
"""Exchange commands for AITBC CLI"""
|
||||
|
||||
import click
|
||||
import httpx
|
||||
from typing import Optional
|
||||
|
||||
from ..config import get_config
|
||||
from ..utils import success, error, output
|
||||
|
||||
|
||||
@click.group()
|
||||
def exchange():
|
||||
"""Bitcoin exchange operations"""
|
||||
pass
|
||||
|
||||
|
||||
@exchange.command()
|
||||
@click.pass_context
|
||||
def rates(ctx):
|
||||
"""Get current exchange rates"""
|
||||
config = ctx.obj['config']
|
||||
|
||||
try:
|
||||
with httpx.Client() as client:
|
||||
response = client.get(
|
||||
f"{config.coordinator_url}/v1/exchange/rates",
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
rates_data = response.json()
|
||||
success("Current exchange rates:")
|
||||
output(rates_data, ctx.obj['output_format'])
|
||||
else:
|
||||
error(f"Failed to get exchange rates: {response.status_code}")
|
||||
except Exception as e:
|
||||
error(f"Network error: {e}")
|
||||
|
||||
|
||||
@exchange.command()
|
||||
@click.option("--aitbc-amount", type=float, help="Amount of AITBC to buy")
|
||||
@click.option("--btc-amount", type=float, help="Amount of BTC to spend")
|
||||
@click.option("--user-id", help="User ID for the payment")
|
||||
@click.option("--notes", help="Additional notes for the payment")
|
||||
@click.pass_context
|
||||
def create_payment(ctx, aitbc_amount: Optional[float], btc_amount: Optional[float],
|
||||
user_id: Optional[str], notes: Optional[str]):
|
||||
"""Create a Bitcoin payment request for AITBC purchase"""
|
||||
config = ctx.obj['config']
|
||||
|
||||
# Validate input
|
||||
if aitbc_amount is not None and aitbc_amount <= 0:
|
||||
error("AITBC amount must be greater than 0")
|
||||
return
|
||||
|
||||
if btc_amount is not None and btc_amount <= 0:
|
||||
error("BTC amount must be greater than 0")
|
||||
return
|
||||
|
||||
if not aitbc_amount and not btc_amount:
|
||||
error("Either --aitbc-amount or --btc-amount must be specified")
|
||||
return
|
||||
|
||||
# Get exchange rates to calculate missing amount
|
||||
try:
|
||||
with httpx.Client() as client:
|
||||
rates_response = client.get(
|
||||
f"{config.coordinator_url}/v1/exchange/rates",
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if rates_response.status_code != 200:
|
||||
error("Failed to get exchange rates")
|
||||
return
|
||||
|
||||
rates = rates_response.json()
|
||||
btc_to_aitbc = rates.get('btc_to_aitbc', 100000)
|
||||
|
||||
# Calculate missing amount
|
||||
if aitbc_amount and not btc_amount:
|
||||
btc_amount = aitbc_amount / btc_to_aitbc
|
||||
elif btc_amount and not aitbc_amount:
|
||||
aitbc_amount = btc_amount * btc_to_aitbc
|
||||
|
||||
# Prepare payment request
|
||||
payment_data = {
|
||||
"user_id": user_id or "cli_user",
|
||||
"aitbc_amount": aitbc_amount,
|
||||
"btc_amount": btc_amount
|
||||
}
|
||||
|
||||
if notes:
|
||||
payment_data["notes"] = notes
|
||||
|
||||
# Create payment
|
||||
response = client.post(
|
||||
f"{config.coordinator_url}/v1/exchange/create-payment",
|
||||
json=payment_data,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
payment = response.json()
|
||||
success(f"Payment created: {payment.get('payment_id')}")
|
||||
success(f"Send {btc_amount:.8f} BTC to: {payment.get('payment_address')}")
|
||||
success(f"Expires at: {payment.get('expires_at')}")
|
||||
output(payment, ctx.obj['output_format'])
|
||||
else:
|
||||
error(f"Failed to create payment: {response.status_code}")
|
||||
if response.text:
|
||||
error(f"Error details: {response.text}")
|
||||
|
||||
except Exception as e:
|
||||
error(f"Network error: {e}")
|
||||
|
||||
|
||||
@exchange.command()
|
||||
@click.option("--payment-id", required=True, help="Payment ID to check")
|
||||
@click.pass_context
|
||||
def payment_status(ctx, payment_id: str):
|
||||
"""Check payment confirmation status"""
|
||||
config = ctx.obj['config']
|
||||
|
||||
try:
|
||||
with httpx.Client() as client:
|
||||
response = client.get(
|
||||
f"{config.coordinator_url}/v1/exchange/payment-status/{payment_id}",
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
status_data = response.json()
|
||||
status = status_data.get('status', 'unknown')
|
||||
|
||||
if status == 'confirmed':
|
||||
success(f"Payment {payment_id} is confirmed!")
|
||||
success(f"AITBC amount: {status_data.get('aitbc_amount', 0)}")
|
||||
elif status == 'pending':
|
||||
success(f"Payment {payment_id} is pending confirmation")
|
||||
elif status == 'expired':
|
||||
error(f"Payment {payment_id} has expired")
|
||||
else:
|
||||
success(f"Payment {payment_id} status: {status}")
|
||||
|
||||
output(status_data, ctx.obj['output_format'])
|
||||
else:
|
||||
error(f"Failed to get payment status: {response.status_code}")
|
||||
except Exception as e:
|
||||
error(f"Network error: {e}")
|
||||
|
||||
|
||||
@exchange.command()
|
||||
@click.pass_context
|
||||
def market_stats(ctx):
|
||||
"""Get exchange market statistics"""
|
||||
config = ctx.obj['config']
|
||||
|
||||
try:
|
||||
with httpx.Client() as client:
|
||||
response = client.get(
|
||||
f"{config.coordinator_url}/v1/exchange/market-stats",
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
stats = response.json()
|
||||
success("Exchange market statistics:")
|
||||
output(stats, ctx.obj['output_format'])
|
||||
else:
|
||||
error(f"Failed to get market stats: {response.status_code}")
|
||||
except Exception as e:
|
||||
error(f"Network error: {e}")
|
||||
|
||||
|
||||
@exchange.group()
|
||||
def wallet():
|
||||
"""Bitcoin wallet operations"""
|
||||
pass
|
||||
|
||||
|
||||
@wallet.command()
|
||||
@click.pass_context
|
||||
def balance(ctx):
|
||||
"""Get Bitcoin wallet balance"""
|
||||
config = ctx.obj['config']
|
||||
|
||||
try:
|
||||
with httpx.Client() as client:
|
||||
response = client.get(
|
||||
f"{config.coordinator_url}/v1/exchange/wallet/balance",
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
balance_data = response.json()
|
||||
success("Bitcoin wallet balance:")
|
||||
output(balance_data, ctx.obj['output_format'])
|
||||
else:
|
||||
error(f"Failed to get wallet balance: {response.status_code}")
|
||||
except Exception as e:
|
||||
error(f"Network error: {e}")
|
||||
|
||||
|
||||
@wallet.command()
|
||||
@click.pass_context
|
||||
def info(ctx):
|
||||
"""Get comprehensive Bitcoin wallet information"""
|
||||
config = ctx.obj['config']
|
||||
|
||||
try:
|
||||
with httpx.Client() as client:
|
||||
response = client.get(
|
||||
f"{config.coordinator_url}/v1/exchange/wallet/info",
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
wallet_info = response.json()
|
||||
success("Bitcoin wallet information:")
|
||||
output(wallet_info, ctx.obj['output_format'])
|
||||
else:
|
||||
error(f"Failed to get wallet info: {response.status_code}")
|
||||
except Exception as e:
|
||||
error(f"Network error: {e}")
|
||||
@@ -305,3 +305,164 @@ def review(ctx, gpu_id: str, rating: int, comment: Optional[str]):
|
||||
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("--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}")
|
||||
|
||||
@@ -21,6 +21,7 @@ from .commands.admin import admin
|
||||
from .commands.config import config
|
||||
from .commands.monitor import monitor
|
||||
from .commands.governance import governance
|
||||
from .commands.exchange import exchange
|
||||
from .plugins import plugin, load_plugins
|
||||
|
||||
|
||||
@@ -98,6 +99,7 @@ cli.add_command(admin)
|
||||
cli.add_command(config)
|
||||
cli.add_command(monitor)
|
||||
cli.add_command(governance)
|
||||
cli.add_command(exchange)
|
||||
cli.add_command(plugin)
|
||||
load_plugins(cli)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user