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)
|
||||
|
||||
|
||||
361
cli/test_exchange_e2e.py
Normal file
361
cli/test_exchange_e2e.py
Normal file
@@ -0,0 +1,361 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Exchange End-to-End Test
|
||||
Tests complete Bitcoin exchange workflow: rates → payment creation → monitoring → confirmation.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
import httpx
|
||||
|
||||
DEFAULT_COORDINATOR = "http://localhost:8000"
|
||||
DEFAULT_API_KEY = "${CLIENT_API_KEY}"
|
||||
DEFAULT_USER_ID = "e2e_test_user"
|
||||
DEFAULT_AITBC_AMOUNT = 1000
|
||||
DEFAULT_TIMEOUT = 300
|
||||
POLL_INTERVAL = 10
|
||||
|
||||
|
||||
def get_exchange_rates(client: httpx.Client, base_url: str) -> Optional[dict]:
|
||||
"""Get current exchange rates"""
|
||||
response = client.get(
|
||||
f"{base_url}/v1/exchange/rates",
|
||||
timeout=10,
|
||||
)
|
||||
if response.status_code != 200:
|
||||
print(f"❌ Failed to get exchange rates: {response.status_code} {response.text}")
|
||||
return None
|
||||
return response.json()
|
||||
|
||||
|
||||
def create_payment(client: httpx.Client, base_url: str, user_id: str,
|
||||
aitbc_amount: float, btc_amount: Optional[float] = None,
|
||||
notes: Optional[str] = None) -> Optional[dict]:
|
||||
"""Create a Bitcoin payment request"""
|
||||
if not btc_amount:
|
||||
# Get rates to calculate BTC amount
|
||||
rates = get_exchange_rates(client, base_url)
|
||||
if not rates:
|
||||
return None
|
||||
btc_amount = aitbc_amount / rates['btc_to_aitbc']
|
||||
|
||||
payload = {
|
||||
"user_id": user_id,
|
||||
"aitbc_amount": aitbc_amount,
|
||||
"btc_amount": btc_amount
|
||||
}
|
||||
if notes:
|
||||
payload["notes"] = notes
|
||||
|
||||
response = client.post(
|
||||
f"{base_url}/v1/exchange/create-payment",
|
||||
json=payload,
|
||||
timeout=10,
|
||||
)
|
||||
if response.status_code != 200:
|
||||
print(f"❌ Failed to create payment: {response.status_code} {response.text}")
|
||||
return None
|
||||
return response.json()
|
||||
|
||||
|
||||
def get_payment_status(client: httpx.Client, base_url: str, payment_id: str) -> Optional[dict]:
|
||||
"""Get payment status"""
|
||||
response = client.get(
|
||||
f"{base_url}/v1/exchange/payment-status/{payment_id}",
|
||||
timeout=10,
|
||||
)
|
||||
if response.status_code != 200:
|
||||
print(f"❌ Failed to get payment status: {response.status_code} {response.text}")
|
||||
return None
|
||||
return response.json()
|
||||
|
||||
|
||||
def confirm_payment(client: httpx.Client, base_url: str, payment_id: str,
|
||||
tx_hash: str) -> Optional[dict]:
|
||||
"""Confirm payment (simulating blockchain confirmation)"""
|
||||
response = client.post(
|
||||
f"{base_url}/v1/exchange/confirm-payment/{payment_id}",
|
||||
json={"tx_hash": tx_hash},
|
||||
timeout=10,
|
||||
)
|
||||
if response.status_code != 200:
|
||||
print(f"❌ Failed to confirm payment: {response.status_code} {response.text}")
|
||||
return None
|
||||
return response.json()
|
||||
|
||||
|
||||
def get_market_stats(client: httpx.Client, base_url: str) -> Optional[dict]:
|
||||
"""Get market statistics"""
|
||||
response = client.get(
|
||||
f"{base_url}/v1/exchange/market-stats",
|
||||
timeout=10,
|
||||
)
|
||||
if response.status_code != 200:
|
||||
print(f"❌ Failed to get market stats: {response.status_code} {response.text}")
|
||||
return None
|
||||
return response.json()
|
||||
|
||||
|
||||
def get_wallet_balance(client: httpx.Client, base_url: str) -> Optional[dict]:
|
||||
"""Get Bitcoin wallet balance"""
|
||||
response = client.get(
|
||||
f"{base_url}/v1/exchange/wallet/balance",
|
||||
timeout=10,
|
||||
)
|
||||
if response.status_code != 200:
|
||||
print(f"❌ Failed to get wallet balance: {response.status_code} {response.text}")
|
||||
return None
|
||||
return response.json()
|
||||
|
||||
|
||||
def monitor_payment_confirmation(client: httpx.Client, base_url: str,
|
||||
payment_id: str, timeout: int) -> Optional[str]:
|
||||
"""Monitor payment until confirmed or timeout"""
|
||||
deadline = time.time() + timeout
|
||||
|
||||
while time.time() < deadline:
|
||||
status_data = get_payment_status(client, base_url, payment_id)
|
||||
if not status_data:
|
||||
return None
|
||||
|
||||
status = status_data.get("status")
|
||||
print(f"⏳ Payment status: {status}")
|
||||
|
||||
if status == "confirmed":
|
||||
return status
|
||||
elif status == "expired":
|
||||
print("❌ Payment expired")
|
||||
return status
|
||||
|
||||
time.sleep(POLL_INTERVAL)
|
||||
|
||||
print("❌ Payment monitoring timed out")
|
||||
return None
|
||||
|
||||
|
||||
def test_basic_exchange_workflow(client: httpx.Client, base_url: str, user_id: str,
|
||||
aitbc_amount: float) -> bool:
|
||||
"""Test basic exchange workflow"""
|
||||
print("🧪 Testing basic exchange workflow...")
|
||||
|
||||
# Step 1: Get exchange rates
|
||||
print("💱 Getting exchange rates...")
|
||||
rates = get_exchange_rates(client, base_url)
|
||||
if not rates:
|
||||
print("❌ Failed to get exchange rates")
|
||||
return False
|
||||
|
||||
print(f"✅ Exchange rates: 1 BTC = {rates['btc_to_aitbc']:,} AITBC")
|
||||
print(f" Fee: {rates['fee_percent']}%")
|
||||
|
||||
# Step 2: Create payment
|
||||
print(f"💰 Creating payment for {aitbc_amount} AITBC...")
|
||||
payment = create_payment(client, base_url, user_id, aitbc_amount,
|
||||
notes="E2E test payment")
|
||||
if not payment:
|
||||
print("❌ Failed to create payment")
|
||||
return False
|
||||
|
||||
print(f"✅ Payment created: {payment['payment_id']}")
|
||||
print(f" Send {payment['btc_amount']:.8f} BTC to: {payment['payment_address']}")
|
||||
print(f" Expires at: {payment['expires_at']}")
|
||||
|
||||
# Step 3: Check initial payment status
|
||||
print("📋 Checking initial payment status...")
|
||||
status = get_payment_status(client, base_url, payment['payment_id'])
|
||||
if not status:
|
||||
print("❌ Failed to get payment status")
|
||||
return False
|
||||
|
||||
print(f"✅ Initial status: {status['status']}")
|
||||
|
||||
# Step 4: Simulate payment confirmation
|
||||
print("🔗 Simulating blockchain confirmation...")
|
||||
tx_hash = f"test_tx_{int(time.time())}"
|
||||
confirmation = confirm_payment(client, base_url, payment['payment_id'], tx_hash)
|
||||
if not confirmation:
|
||||
print("❌ Failed to confirm payment")
|
||||
return False
|
||||
|
||||
print(f"✅ Payment confirmed with transaction: {tx_hash}")
|
||||
|
||||
# Step 5: Verify final status
|
||||
print("📄 Verifying final payment status...")
|
||||
final_status = get_payment_status(client, base_url, payment['payment_id'])
|
||||
if not final_status:
|
||||
print("❌ Failed to get final payment status")
|
||||
return False
|
||||
|
||||
if final_status['status'] != 'confirmed':
|
||||
print(f"❌ Expected confirmed status, got: {final_status['status']}")
|
||||
return False
|
||||
|
||||
print(f"✅ Payment confirmed! AITBC amount: {final_status['aitbc_amount']}")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def test_market_statistics(client: httpx.Client, base_url: str) -> bool:
|
||||
"""Test market statistics functionality"""
|
||||
print("🧪 Testing market statistics...")
|
||||
|
||||
stats = get_market_stats(client, base_url)
|
||||
if not stats:
|
||||
print("❌ Failed to get market stats")
|
||||
return False
|
||||
|
||||
print(f"📊 Market Statistics:")
|
||||
print(f" Current price: ${stats['price']:.8f} per AITBC")
|
||||
print(f" 24h change: {stats['price_change_24h']:+.2f}%")
|
||||
print(f" Daily volume: {stats['daily_volume']:,} AITBC")
|
||||
print(f" Daily volume (BTC): {stats['daily_volume_btc']:.8f} BTC")
|
||||
print(f" Total payments: {stats['total_payments']}")
|
||||
print(f" Pending payments: {stats['pending_payments']}")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def test_wallet_operations(client: httpx.Client, base_url: str) -> bool:
|
||||
"""Test wallet operations"""
|
||||
print("🧪 Testing wallet operations...")
|
||||
|
||||
balance = get_wallet_balance(client, base_url)
|
||||
if not balance:
|
||||
print("❌ Failed to get wallet balance (service may be unavailable)")
|
||||
return True # Don't fail test if wallet service is unavailable
|
||||
|
||||
print(f"💰 Wallet Balance:")
|
||||
print(f" Address: {balance['address']}")
|
||||
print(f" Balance: {balance['balance']:.8f} BTC")
|
||||
print(f" Unconfirmed: {balance['unconfirmed_balance']:.8f} BTC")
|
||||
print(f" Total received: {balance['total_received']:.8f} BTC")
|
||||
print(f" Total sent: {balance['total_sent']:.8f} BTC")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def test_multiple_payments_scenario(client: httpx.Client, base_url: str,
|
||||
user_id: str) -> bool:
|
||||
"""Test multiple payments scenario"""
|
||||
print("🧪 Testing multiple payments scenario...")
|
||||
|
||||
# Create multiple payments
|
||||
payment_amounts = [500, 1000, 1500]
|
||||
payment_ids = []
|
||||
|
||||
for i, amount in enumerate(payment_amounts):
|
||||
print(f"💰 Creating payment {i+1}: {amount} AITBC...")
|
||||
payment = create_payment(client, base_url, f"{user_id}_{i}", amount,
|
||||
notes=f"Multi-payment test {i+1}")
|
||||
if not payment:
|
||||
print(f"❌ Failed to create payment {i+1}")
|
||||
return False
|
||||
|
||||
payment_ids.append(payment['payment_id'])
|
||||
print(f"✅ Payment {i+1} created: {payment['payment_id']}")
|
||||
time.sleep(1) # Small delay between payments
|
||||
|
||||
# Confirm all payments
|
||||
for i, payment_id in enumerate(payment_ids):
|
||||
print(f"🔗 Confirming payment {i+1}...")
|
||||
tx_hash = f"multi_tx_{i}_{int(time.time())}"
|
||||
confirmation = confirm_payment(client, base_url, payment_id, tx_hash)
|
||||
if not confirmation:
|
||||
print(f"❌ Failed to confirm payment {i+1}")
|
||||
return False
|
||||
print(f"✅ Payment {i+1} confirmed")
|
||||
time.sleep(0.5)
|
||||
|
||||
# Check updated market stats
|
||||
print("📊 Checking updated market statistics...")
|
||||
final_stats = get_market_stats(client, base_url)
|
||||
if final_stats:
|
||||
print(f"✅ Final stats: {final_stats['total_payments']} total payments")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def test_error_scenarios(client: httpx.Client, base_url: str) -> bool:
|
||||
"""Test error handling scenarios"""
|
||||
print("🧪 Testing error scenarios...")
|
||||
|
||||
# Test invalid payment creation
|
||||
print("❌ Testing invalid payment creation...")
|
||||
invalid_payment = create_payment(client, base_url, "test_user", -100)
|
||||
if invalid_payment:
|
||||
print("❌ Expected error for negative amount, but got success")
|
||||
return False
|
||||
print("✅ Correctly rejected negative amount")
|
||||
|
||||
# Test non-existent payment status
|
||||
print("❌ Testing non-existent payment status...")
|
||||
fake_status = get_payment_status(client, base_url, "fake_payment_id")
|
||||
if fake_status:
|
||||
print("❌ Expected error for fake payment ID, but got success")
|
||||
return False
|
||||
print("✅ Correctly rejected fake payment ID")
|
||||
|
||||
# Test invalid payment confirmation
|
||||
print("❌ Testing invalid payment confirmation...")
|
||||
fake_confirmation = confirm_payment(client, base_url, "fake_payment_id", "fake_tx")
|
||||
if fake_confirmation:
|
||||
print("❌ Expected error for fake payment confirmation, but got success")
|
||||
return False
|
||||
print("✅ Correctly rejected fake payment confirmation")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Exchange end-to-end test")
|
||||
parser.add_argument("--url", default=DEFAULT_COORDINATOR, help="Coordinator base URL")
|
||||
parser.add_argument("--api-key", default=DEFAULT_API_KEY, help="Client API key")
|
||||
parser.add_argument("--user-id", default=DEFAULT_USER_ID, help="User ID for payments")
|
||||
parser.add_argument("--aitbc-amount", type=float, default=DEFAULT_AITBC_AMOUNT, help="AITBC amount for test payment")
|
||||
parser.add_argument("--timeout", type=int, default=DEFAULT_TIMEOUT, help="Timeout in seconds")
|
||||
parser.add_argument("--test", choices=["basic", "stats", "wallet", "multi", "errors", "all"],
|
||||
default="all", help="Test scenario to run")
|
||||
args = parser.parse_args()
|
||||
|
||||
with httpx.Client() as client:
|
||||
print("🚀 Starting Exchange end-to-end test...")
|
||||
print(f"📍 Coordinator: {args.url}")
|
||||
print(f"🆔 User ID: {args.user_id}")
|
||||
print(f"💰 Test amount: {args.aitbc_amount} AITBC")
|
||||
print()
|
||||
|
||||
success = True
|
||||
|
||||
if args.test in ["basic", "all"]:
|
||||
success &= test_basic_exchange_workflow(client, args.url, args.user_id, args.aitbc_amount)
|
||||
print()
|
||||
|
||||
if args.test in ["stats", "all"]:
|
||||
success &= test_market_statistics(client, args.url)
|
||||
print()
|
||||
|
||||
if args.test in ["wallet", "all"]:
|
||||
success &= test_wallet_operations(client, args.url)
|
||||
print()
|
||||
|
||||
if args.test in ["multi", "all"]:
|
||||
success &= test_multiple_payments_scenario(client, args.url, args.user_id)
|
||||
print()
|
||||
|
||||
if args.test in ["errors", "all"]:
|
||||
success &= test_error_scenarios(client, args.url)
|
||||
print()
|
||||
|
||||
if success:
|
||||
print("✅ All exchange tests completed successfully!")
|
||||
return 0
|
||||
else:
|
||||
print("❌ Some exchange tests failed!")
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
294
cli/test_gpu_marketplace_bids.py
Normal file
294
cli/test_gpu_marketplace_bids.py
Normal file
@@ -0,0 +1,294 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
GPU Marketplace Bids Test
|
||||
Tests complete marketplace bid workflow: offers listing → bid submission → bid tracking.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
import httpx
|
||||
|
||||
DEFAULT_COORDINATOR = "http://localhost:8000"
|
||||
DEFAULT_API_KEY = "${CLIENT_API_KEY}"
|
||||
DEFAULT_PROVIDER = "test_miner_123"
|
||||
DEFAULT_CAPACITY = 100
|
||||
DEFAULT_PRICE = 0.05
|
||||
DEFAULT_TIMEOUT = 300
|
||||
POLL_INTERVAL = 5
|
||||
|
||||
|
||||
def list_offers(client: httpx.Client, base_url: str, api_key: str,
|
||||
status: Optional[str] = None, gpu_model: Optional[str] = None) -> Optional[dict]:
|
||||
"""List marketplace offers with optional filters"""
|
||||
params = {"limit": 20}
|
||||
if status:
|
||||
params["status"] = status
|
||||
if gpu_model:
|
||||
params["gpu_model"] = gpu_model
|
||||
|
||||
response = client.get(
|
||||
f"{base_url}/v1/marketplace/offers",
|
||||
headers={"X-Api-Key": api_key},
|
||||
params=params,
|
||||
timeout=10,
|
||||
)
|
||||
if response.status_code != 200:
|
||||
print(f"❌ Failed to list offers: {response.status_code} {response.text}")
|
||||
return None
|
||||
return response.json()
|
||||
|
||||
|
||||
def submit_bid(client: httpx.Client, base_url: str, api_key: str,
|
||||
provider: str, capacity: int, price: float,
|
||||
notes: Optional[str] = None) -> Optional[str]:
|
||||
"""Submit a marketplace bid"""
|
||||
payload = {
|
||||
"provider": provider,
|
||||
"capacity": capacity,
|
||||
"price": price
|
||||
}
|
||||
if notes:
|
||||
payload["notes"] = notes
|
||||
|
||||
response = client.post(
|
||||
f"{base_url}/v1/marketplace/bids",
|
||||
headers={"X-Api-Key": api_key, "Content-Type": "application/json"},
|
||||
json=payload,
|
||||
timeout=10,
|
||||
)
|
||||
if response.status_code != 202:
|
||||
print(f"❌ Bid submission failed: {response.status_code} {response.text}")
|
||||
return None
|
||||
return response.json().get("id")
|
||||
|
||||
|
||||
def list_bids(client: httpx.Client, base_url: str, api_key: str,
|
||||
status: Optional[str] = None, provider: Optional[str] = None) -> Optional[dict]:
|
||||
"""List marketplace bids with optional filters"""
|
||||
params = {"limit": 20}
|
||||
if status:
|
||||
params["status"] = status
|
||||
if provider:
|
||||
params["provider"] = provider
|
||||
|
||||
response = client.get(
|
||||
f"{base_url}/v1/marketplace/bids",
|
||||
headers={"X-Api-Key": api_key},
|
||||
params=params,
|
||||
timeout=10,
|
||||
)
|
||||
if response.status_code != 200:
|
||||
print(f"❌ Failed to list bids: {response.status_code} {response.text}")
|
||||
return None
|
||||
return response.json()
|
||||
|
||||
|
||||
def get_bid_details(client: httpx.Client, base_url: str, api_key: str, bid_id: str) -> Optional[dict]:
|
||||
"""Get detailed information about a specific bid"""
|
||||
response = client.get(
|
||||
f"{base_url}/v1/marketplace/bids/{bid_id}",
|
||||
headers={"X-Api-Key": api_key},
|
||||
timeout=10,
|
||||
)
|
||||
if response.status_code != 200:
|
||||
print(f"❌ Failed to get bid details: {response.status_code} {response.text}")
|
||||
return None
|
||||
return response.json()
|
||||
|
||||
|
||||
def get_marketplace_stats(client: httpx.Client, base_url: str, api_key: str) -> Optional[dict]:
|
||||
"""Get marketplace statistics"""
|
||||
response = client.get(
|
||||
f"{base_url}/v1/marketplace/stats",
|
||||
headers={"X-Api-Key": api_key},
|
||||
timeout=10,
|
||||
)
|
||||
if response.status_code != 200:
|
||||
print(f"❌ Failed to get marketplace stats: {response.status_code} {response.text}")
|
||||
return None
|
||||
return response.json()
|
||||
|
||||
|
||||
def monitor_bid_status(client: httpx.Client, base_url: str, api_key: str,
|
||||
bid_id: str, timeout: int) -> Optional[str]:
|
||||
"""Monitor bid status until it's accepted/rejected or timeout"""
|
||||
deadline = time.time() + timeout
|
||||
|
||||
while time.time() < deadline:
|
||||
bid_details = get_bid_details(client, base_url, api_key, bid_id)
|
||||
if not bid_details:
|
||||
return None
|
||||
|
||||
status = bid_details.get("status")
|
||||
print(f"⏳ Bid status: {status}")
|
||||
|
||||
if status in {"accepted", "rejected"}:
|
||||
return status
|
||||
|
||||
time.sleep(POLL_INTERVAL)
|
||||
|
||||
print("❌ Bid status monitoring timed out")
|
||||
return None
|
||||
|
||||
|
||||
def test_basic_workflow(client: httpx.Client, base_url: str, api_key: str,
|
||||
provider: str, capacity: int, price: float) -> bool:
|
||||
"""Test basic marketplace bid workflow"""
|
||||
print("🧪 Testing basic marketplace bid workflow...")
|
||||
|
||||
# Step 1: List available offers
|
||||
print("📋 Listing marketplace offers...")
|
||||
offers = list_offers(client, base_url, api_key, status="open")
|
||||
if not offers:
|
||||
print("❌ Failed to list offers")
|
||||
return False
|
||||
|
||||
offers_list = offers.get("offers", [])
|
||||
print(f"✅ Found {len(offers_list)} open offers")
|
||||
|
||||
if offers_list:
|
||||
print("📊 Sample offers:")
|
||||
for i, offer in enumerate(offers_list[:3]): # Show first 3 offers
|
||||
print(f" {i+1}. {offer.get('gpu_model', 'Unknown')} - ${offer.get('price', 0):.4f}/hr - {offer.get('provider', 'Unknown')}")
|
||||
|
||||
# Step 2: Submit bid
|
||||
print(f"💰 Submitting bid: {capacity} units at ${price:.4f}/unit from {provider}")
|
||||
bid_id = submit_bid(client, base_url, api_key, provider, capacity, price,
|
||||
notes="Test bid for GPU marketplace")
|
||||
if not bid_id:
|
||||
print("❌ Failed to submit bid")
|
||||
return False
|
||||
|
||||
print(f"✅ Bid submitted: {bid_id}")
|
||||
|
||||
# Step 3: Get bid details
|
||||
print("📄 Getting bid details...")
|
||||
bid_details = get_bid_details(client, base_url, api_key, bid_id)
|
||||
if not bid_details:
|
||||
print("❌ Failed to get bid details")
|
||||
return False
|
||||
|
||||
print(f"✅ Bid details: {bid_details['provider']} - {bid_details['capacity']} units - ${bid_details['price']:.4f}/unit - {bid_details['status']}")
|
||||
|
||||
# Step 4: List bids to verify it appears
|
||||
print("📋 Listing bids to verify...")
|
||||
bids = list_bids(client, base_url, api_key, provider=provider)
|
||||
if not bids:
|
||||
print("❌ Failed to list bids")
|
||||
return False
|
||||
|
||||
bids_list = bids.get("bids", [])
|
||||
our_bid = next((b for b in bids_list if b.get("id") == bid_id), None)
|
||||
if not our_bid:
|
||||
print("❌ Submitted bid not found in bid list")
|
||||
return False
|
||||
|
||||
print(f"✅ Bid found in list: {our_bid['status']}")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def test_competitive_bidding(client: httpx.Client, base_url: str, api_key: str) -> bool:
|
||||
"""Test competitive bidding scenario with multiple providers"""
|
||||
print("🧪 Testing competitive bidding scenario...")
|
||||
|
||||
# Submit multiple bids from different providers
|
||||
providers = ["provider_alpha", "provider_beta", "provider_gamma"]
|
||||
bid_ids = []
|
||||
|
||||
for i, provider in enumerate(providers):
|
||||
price = 0.05 - (i * 0.01) # Decreasing prices
|
||||
print(f"💰 {provider} submitting bid at ${price:.4f}/unit")
|
||||
|
||||
bid_id = submit_bid(client, base_url, api_key, provider, 50, price,
|
||||
notes=f"Competitive bid from {provider}")
|
||||
if not bid_id:
|
||||
print(f"❌ {provider} failed to submit bid")
|
||||
return False
|
||||
|
||||
bid_ids.append((provider, bid_id))
|
||||
time.sleep(1) # Small delay between submissions
|
||||
|
||||
print(f"✅ All {len(bid_ids)} competitive bids submitted")
|
||||
|
||||
# List all bids to see the competition
|
||||
all_bids = list_bids(client, base_url, api_key)
|
||||
if not all_bids:
|
||||
print("❌ Failed to list all bids")
|
||||
return False
|
||||
|
||||
bids_list = all_bids.get("bids", [])
|
||||
competitive_bids = [b for b in bids_list if b.get("provider") in providers]
|
||||
|
||||
print(f"📊 Found {len(competitive_bids)} competitive bids:")
|
||||
for bid in sorted(competitive_bids, key=lambda x: x.get("price", 0)):
|
||||
print(f" {bid['provider']}: ${bid['price']:.4f}/unit - {bid['status']}")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def test_marketplace_stats(client: httpx.Client, base_url: str, api_key: str) -> bool:
|
||||
"""Test marketplace statistics functionality"""
|
||||
print("🧪 Testing marketplace statistics...")
|
||||
|
||||
stats = get_marketplace_stats(client, base_url, api_key)
|
||||
if not stats:
|
||||
print("❌ Failed to get marketplace stats")
|
||||
return False
|
||||
|
||||
print(f"📊 Marketplace Statistics:")
|
||||
print(f" Total offers: {stats.get('totalOffers', 0)}")
|
||||
print(f" Open capacity: {stats.get('openCapacity', 0)}")
|
||||
print(f" Average price: ${stats.get('averagePrice', 0):.4f}")
|
||||
print(f" Active bids: {stats.get('activeBids', 0)}")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="GPU marketplace bids end-to-end test")
|
||||
parser.add_argument("--url", default=DEFAULT_COORDINATOR, help="Coordinator base URL")
|
||||
parser.add_argument("--api-key", default=DEFAULT_API_KEY, help="Client API key")
|
||||
parser.add_argument("--provider", default=DEFAULT_PROVIDER, help="Provider ID for bids")
|
||||
parser.add_argument("--capacity", type=int, default=DEFAULT_CAPACITY, help="Bid capacity")
|
||||
parser.add_argument("--price", type=float, default=DEFAULT_PRICE, help="Price per unit")
|
||||
parser.add_argument("--timeout", type=int, default=DEFAULT_TIMEOUT, help="Timeout in seconds")
|
||||
parser.add_argument("--test", choices=["basic", "competitive", "stats", "all"],
|
||||
default="all", help="Test scenario to run")
|
||||
args = parser.parse_args()
|
||||
|
||||
with httpx.Client() as client:
|
||||
print("🚀 Starting GPU marketplace bids test...")
|
||||
print(f"📍 Coordinator: {args.url}")
|
||||
print(f"🆔 Provider: {args.provider}")
|
||||
print(f"💰 Bid: {args.capacity} units at ${args.price:.4f}/unit")
|
||||
print()
|
||||
|
||||
success = True
|
||||
|
||||
if args.test in ["basic", "all"]:
|
||||
success &= test_basic_workflow(client, args.url, args.api_key,
|
||||
args.provider, args.capacity, args.price)
|
||||
print()
|
||||
|
||||
if args.test in ["competitive", "all"]:
|
||||
success &= test_competitive_bidding(client, args.url, args.api_key)
|
||||
print()
|
||||
|
||||
if args.test in ["stats", "all"]:
|
||||
success &= test_marketplace_stats(client, args.url, args.api_key)
|
||||
print()
|
||||
|
||||
if success:
|
||||
print("✅ All marketplace bid tests completed successfully!")
|
||||
return 0
|
||||
else:
|
||||
print("❌ Some marketplace bid tests failed!")
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user