feat: migrate coordinator-api routers and exchange_island CLI to use centralized aitbc package HTTP client
Some checks failed
API Endpoint Tests / test-api-endpoints (push) Successful in 9s
CLI Tests / test-cli (push) Failing after 3s
Integration Tests / test-service-integration (push) Successful in 42s
Python Tests / test-python (push) Failing after 39s
Security Scanning / security-scan (push) Successful in 2m36s
Multi-Node Blockchain Health Monitoring / health-check (push) Successful in 3s
Blockchain Synchronization Verification / sync-verification (push) Failing after 2s
Some checks failed
API Endpoint Tests / test-api-endpoints (push) Successful in 9s
CLI Tests / test-cli (push) Failing after 3s
Integration Tests / test-service-integration (push) Successful in 42s
Python Tests / test-python (push) Failing after 39s
Security Scanning / security-scan (push) Successful in 2m36s
Multi-Node Blockchain Health Monitoring / health-check (push) Successful in 3s
Blockchain Synchronization Verification / sync-verification (push) Failing after 2s
- Replace httpx.Client with aitbc.AITBCHTTPClient in client.py get_blocks endpoint - Migrate monitoring_dashboard.py from httpx.AsyncClient to AITBCHTTPClient - Replace httpx with AITBCHTTPClient in blockchain.py get_balance function - Add NetworkError exception handling across all migrated endpoints - Remove async context managers in favor of direct AITBCHTTPClient usage - Remove httpx imports
This commit is contained in:
@@ -226,22 +226,27 @@ async def get_blocks(
|
|||||||
) -> dict: # type: ignore[arg-type]
|
) -> dict: # type: ignore[arg-type]
|
||||||
"""Get recent blockchain blocks"""
|
"""Get recent blockchain blocks"""
|
||||||
try:
|
try:
|
||||||
import httpx
|
|
||||||
|
|
||||||
# Query the local blockchain node for blocks
|
# Query the local blockchain node for blocks
|
||||||
with httpx.Client() as client:
|
client = AITBCHTTPClient(timeout=5.0)
|
||||||
response = client.get(
|
try:
|
||||||
"http://10.1.223.93:8082/rpc/blocks-range", params={"start": offset, "end": offset + limit}, timeout=5
|
blocks_data = client.get(
|
||||||
|
"http://10.1.223.93:8082/rpc/blocks-range", params={"start": offset, "end": offset + limit}
|
||||||
)
|
)
|
||||||
|
return {
|
||||||
if response.status_code == 200:
|
"blocks": blocks_data.get("blocks", []),
|
||||||
blocks_data = response.json()
|
"total": blocks_data.get("total", 0),
|
||||||
return {
|
"limit": limit,
|
||||||
"blocks": blocks_data.get("blocks", []),
|
"offset": offset,
|
||||||
"total": blocks_data.get("total", 0),
|
}
|
||||||
"limit": limit,
|
except NetworkError as e:
|
||||||
"offset": offset,
|
logger.error(f"Failed to fetch blocks: {e}")
|
||||||
}
|
return {
|
||||||
|
"blocks": [],
|
||||||
|
"total": 0,
|
||||||
|
"limit": limit,
|
||||||
|
"offset": offset,
|
||||||
|
"error": "Failed to fetch blocks",
|
||||||
|
}
|
||||||
else:
|
else:
|
||||||
# Fallback to empty response if blockchain node is unavailable
|
# Fallback to empty response if blockchain node is unavailable
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -191,24 +191,25 @@ async def collect_all_health_data() -> dict[str, Any]:
|
|||||||
"""Collect health data from all enhanced services"""
|
"""Collect health data from all enhanced services"""
|
||||||
health_data = {}
|
health_data = {}
|
||||||
|
|
||||||
async with httpx.AsyncClient(timeout=5.0) as client:
|
client = AITBCHTTPClient(timeout=5.0)
|
||||||
tasks = []
|
tasks = []
|
||||||
|
|
||||||
for service_id, service_info in SERVICES.items():
|
for service_id, service_info in SERVICES.items():
|
||||||
task = check_service_health(client, service_id, service_info)
|
task = check_service_health(client, service_id, service_info)
|
||||||
tasks.append(task)
|
tasks.append(task)
|
||||||
|
|
||||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||||
|
|
||||||
for i, (service_id, service_info) in enumerate(SERVICES.items()):
|
for i, (service_id, service_info) in enumerate(SERVICES.items()):
|
||||||
result = results[i]
|
result = results[i]
|
||||||
if isinstance(result, Exception):
|
if isinstance(result, Exception):
|
||||||
health_data[service_id] = {
|
health_data[service_id] = {
|
||||||
"status": "unhealthy",
|
"status": "unhealthy",
|
||||||
"error": str(result),
|
"error": str(result),
|
||||||
"timestamp": datetime.utcnow().isoformat(),
|
"timestamp": datetime.utcnow().isoformat(),
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
|
health_data[service_id] = result
|
||||||
|
|
||||||
return health_data
|
return health_data
|
||||||
|
|
||||||
|
|||||||
@@ -53,17 +53,16 @@ def get_balance(address: str) -> float | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with httpx.Client() as client:
|
client = AITBCHTTPClient(timeout=10.0)
|
||||||
|
try:
|
||||||
response = client.get(
|
response = client.get(
|
||||||
f"{BLOCKCHAIN_RPC}/getBalance/{address}",
|
f"{BLOCKCHAIN_RPC}/getBalance/{address}",
|
||||||
headers={"X-Api-Key": settings.admin_api_keys[0] if settings.admin_api_keys else ""},
|
headers={"X-Api-Key": settings.admin_api_keys[0] if settings.admin_api_keys else ""},
|
||||||
)
|
)
|
||||||
|
return float(response.get("balance", 0))
|
||||||
if response.status_code == 200:
|
except NetworkError as e:
|
||||||
data = response.json()
|
logger.error("Error getting balance: %s", e)
|
||||||
return float(data.get("balance", 0))
|
return None
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Error getting balance: %s", e)
|
logger.error("Error getting balance: %s", e)
|
||||||
|
return None
|
||||||
return None
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import os
|
|||||||
import pickle
|
import pickle
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from aitbc import REPO_DIR
|
||||||
|
|
||||||
# Safe classes whitelist: builtins and common types
|
# Safe classes whitelist: builtins and common types
|
||||||
SAFE_MODULES = {
|
SAFE_MODULES = {
|
||||||
"builtins": {
|
"builtins": {
|
||||||
@@ -40,8 +42,8 @@ def _initialize_allowed_origins():
|
|||||||
# 1. All site-packages directories that are under the application venv
|
# 1. All site-packages directories that are under the application venv
|
||||||
for entry in os.sys.path:
|
for entry in os.sys.path:
|
||||||
if "site-packages" in entry and os.path.isdir(entry):
|
if "site-packages" in entry and os.path.isdir(entry):
|
||||||
# Only include if it's inside /opt/aitbc/apps/coordinator-api/.venv or similar
|
# Only include if it's inside the AITBC repository
|
||||||
if "/opt/aitbc" in entry: # restrict to our app directory
|
if str(REPO_DIR) in entry: # restrict to our app directory
|
||||||
_ALLOWED_ORIGINS.add(os.path.realpath(entry))
|
_ALLOWED_ORIGINS.add(os.path.realpath(entry))
|
||||||
# 2. Standard library paths (typically without site-packages)
|
# 2. Standard library paths (typically without site-packages)
|
||||||
# We'll allow any origin that resolves to a .py file outside site-packages and not in user dirs
|
# We'll allow any origin that resolves to a .py file outside site-packages and not in user dirs
|
||||||
@@ -95,14 +97,14 @@ def _lock_sys_path():
|
|||||||
if isinstance(sys.path, list):
|
if isinstance(sys.path, list):
|
||||||
trusted = []
|
trusted = []
|
||||||
for p in sys.path:
|
for p in sys.path:
|
||||||
# Keep site-packages under /opt/aitbc (our venv)
|
# Keep site-packages under REPO_DIR (our venv)
|
||||||
if "site-packages" in p and "/opt/aitbc" in p:
|
if "site-packages" in p and str(REPO_DIR) in p:
|
||||||
trusted.append(p)
|
trusted.append(p)
|
||||||
# Keep stdlib paths (no site-packages, under /usr/lib/python)
|
# Keep stdlib paths (no site-packages, under /usr/lib/python)
|
||||||
elif "site-packages" not in p and ("/usr/lib/python" in p or "/usr/local/lib/python" in p):
|
elif "site-packages" not in p and ("/usr/lib/python" in p or "/usr/local/lib/python" in p):
|
||||||
trusted.append(p)
|
trusted.append(p)
|
||||||
# Keep our application directory
|
# Keep our application directory
|
||||||
elif p.startswith("/opt/aitbc/apps/coordinator-api"):
|
elif p.startswith(str(REPO_DIR / "apps" / "coordinator-api")):
|
||||||
trusted.append(p)
|
trusted.append(p)
|
||||||
sys.path = trusted
|
sys.path = trusted
|
||||||
|
|
||||||
|
|||||||
@@ -198,44 +198,29 @@ def sell(ctx, ait_amount: float, quote_currency: str, min_price: Optional[float]
|
|||||||
|
|
||||||
# Submit transaction to blockchain
|
# Submit transaction to blockchain
|
||||||
try:
|
try:
|
||||||
import httpx
|
http_client = AITBCHTTPClient(base_url=rpc_endpoint, timeout=10)
|
||||||
with httpx.Client() as client:
|
result = http_client.post("/transaction", json=sell_order_data)
|
||||||
response = client.post(
|
success(f"Sell order created successfully!")
|
||||||
f"{rpc_endpoint}/transaction",
|
success(f"Order ID: {order_id}")
|
||||||
json=sell_order_data,
|
success(f"Selling {ait_amount} AIT for {quote_currency}")
|
||||||
timeout=10
|
|
||||||
)
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
if min_price:
|
||||||
result = response.json()
|
success(f"Min price: {min_price:.8f} {quote_currency}/AIT")
|
||||||
success(f"Sell order created successfully!")
|
|
||||||
success(f"Order ID: {order_id}")
|
|
||||||
success(f"Selling {ait_amount} AIT for {quote_currency}")
|
|
||||||
|
|
||||||
if min_price:
|
order_info = {
|
||||||
success(f"Min price: {min_price:.8f} {quote_currency}/AIT")
|
"Order ID": order_id,
|
||||||
|
"Pair": pair,
|
||||||
order_info = {
|
"Side": "SELL",
|
||||||
"Order ID": order_id,
|
"Amount": f"{ait_amount} AIT",
|
||||||
"Pair": pair,
|
"Min Price": f"{min_price:.8f} {quote_currency}/AIT" if min_price else "Market",
|
||||||
"Side": "SELL",
|
"Status": "open",
|
||||||
"Amount": f"{ait_amount} AIT",
|
"User": user_id[:16] + "...",
|
||||||
"Min Price": f"{min_price:.8f} {quote_currency}/AIT" if min_price else "Market",
|
"Island": island_id[:16] + "..."
|
||||||
"Status": "open",
|
}
|
||||||
"User": user_id[:16] + "...",
|
output(order_info, ctx.obj.get('output_format', 'table'))
|
||||||
"Island": island_id[:16] + "..."
|
except NetworkError as e:
|
||||||
}
|
|
||||||
|
|
||||||
output(order_info, ctx.obj.get('output_format', 'table'))
|
|
||||||
else:
|
|
||||||
error(f"Failed to submit transaction: {response.status_code}")
|
|
||||||
if response.text:
|
|
||||||
error(f"Error details: {response.text}")
|
|
||||||
raise click.Abort()
|
|
||||||
except Exception as e:
|
|
||||||
error(f"Network error submitting transaction: {e}")
|
error(f"Network error submitting transaction: {e}")
|
||||||
raise click.Abort()
|
raise click.Abort()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error(f"Error creating sell order: {str(e)}")
|
error(f"Error creating sell order: {str(e)}")
|
||||||
raise click.Abort()
|
raise click.Abort()
|
||||||
@@ -255,7 +240,6 @@ def orderbook(ctx, pair: str, limit: int):
|
|||||||
|
|
||||||
# Query blockchain for exchange orders
|
# Query blockchain for exchange orders
|
||||||
try:
|
try:
|
||||||
import httpx
|
|
||||||
params = {
|
params = {
|
||||||
'transaction_type': 'exchange',
|
'transaction_type': 'exchange',
|
||||||
'island_id': island_id,
|
'island_id': island_id,
|
||||||
@@ -264,41 +248,72 @@ def orderbook(ctx, pair: str, limit: int):
|
|||||||
'limit': limit * 2 # Get both buys and sells
|
'limit': limit * 2 # Get both buys and sells
|
||||||
}
|
}
|
||||||
|
|
||||||
with httpx.Client() as client:
|
http_client = AITBCHTTPClient(base_url=rpc_endpoint, timeout=10)
|
||||||
response = client.get(
|
transactions = http_client.get("/transactions", params=params)
|
||||||
f"{rpc_endpoint}/transactions",
|
|
||||||
params=params,
|
|
||||||
timeout=10
|
|
||||||
)
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
# Separate buy and sell orders
|
||||||
orders = response.json()
|
buy_orders = []
|
||||||
|
sell_orders = []
|
||||||
|
|
||||||
# Separate buy and sell orders
|
for order in transactions:
|
||||||
buy_orders = []
|
if order.get('side') == 'buy':
|
||||||
sell_orders = []
|
buy_orders.append(order)
|
||||||
|
elif order.get('side') == 'sell':
|
||||||
|
sell_orders.append(order)
|
||||||
|
|
||||||
for order in orders:
|
# Sort buy orders by price descending (highest first)
|
||||||
if order.get('side') == 'buy':
|
buy_orders.sort(key=lambda x: x.get('max_price', 0), reverse=True)
|
||||||
buy_orders.append(order)
|
# Sort sell orders by price ascending (lowest first)
|
||||||
elif order.get('side') == 'sell':
|
sell_orders.sort(key=lambda x: x.get('min_price', float('inf')))
|
||||||
sell_orders.append(order)
|
|
||||||
|
|
||||||
# Sort buy orders by price descending (highest first)
|
if not buy_orders and not sell_orders:
|
||||||
buy_orders.sort(key=lambda x: x.get('max_price', 0), reverse=True)
|
info(f"No open orders for {pair}")
|
||||||
# Sort sell orders by price ascending (lowest first)
|
return
|
||||||
sell_orders.sort(key=lambda x: x.get('min_price', float('inf')))
|
|
||||||
|
|
||||||
if not buy_orders and not sell_orders:
|
# Display sell orders (asks)
|
||||||
info(f"No open orders for {pair}")
|
if sell_orders:
|
||||||
return
|
asks_data = []
|
||||||
|
for order in sell_orders[:limit]:
|
||||||
|
asks_data.append({
|
||||||
|
"Price": f"{order.get('min_price', 0):.8f}",
|
||||||
|
"Amount": f"{order.get('amount', 0):.4f} AIT",
|
||||||
|
"Total": f"{order.get('min_price', 0) * order.get('amount', 0):.8f} {pair.split('/')[1]}",
|
||||||
|
"User": order.get('user_id', '')[:16] + "...",
|
||||||
|
"Order": order.get('order_id', '')[:16] + "..."
|
||||||
|
})
|
||||||
|
|
||||||
# Display sell orders (asks)
|
output(asks_data, ctx.obj.get('output_format', 'table'), title=f"Sell Orders (Asks) - {pair}")
|
||||||
if sell_orders:
|
|
||||||
asks_data = []
|
# Display buy orders (bids)
|
||||||
for order in sell_orders[:limit]:
|
if buy_orders:
|
||||||
asks_data.append({
|
bids_data = []
|
||||||
"Price": f"{order.get('min_price', 0):.8f}",
|
for order in buy_orders[:limit]:
|
||||||
|
bids_data.append({
|
||||||
|
"Price": f"{order.get('max_price', 0):.8f}",
|
||||||
|
"Amount": f"{order.get('amount', 0):.4f} AIT",
|
||||||
|
"Total": f"{order.get('max_price', 0) * order.get('amount', 0):.8f} {pair.split('/')[1]}",
|
||||||
|
"User": order.get('user_id', '')[:16] + "...",
|
||||||
|
"Order": order.get('order_id', '')[:16] + "..."
|
||||||
|
})
|
||||||
|
|
||||||
|
output(bids_data, ctx.obj.get('output_format', 'table'), title=f"Buy Orders (Bids) - {pair}")
|
||||||
|
|
||||||
|
# Calculate spread if both exist
|
||||||
|
if sell_orders and buy_orders:
|
||||||
|
best_ask = sell_orders[0].get('min_price', 0)
|
||||||
|
best_bid = buy_orders[0].get('max_price', 0)
|
||||||
|
spread = best_ask - best_bid
|
||||||
|
if best_bid > 0:
|
||||||
|
spread_pct = (spread / best_bid) * 100
|
||||||
|
info(f"Spread: {spread:.8f} ({spread_pct:.4f}%)")
|
||||||
|
info(f"Best Bid: {best_bid:.8f} {pair.split('/')[1]}/AIT")
|
||||||
|
info(f"Best Ask: {best_ask:.8f} {pair.split('/')[1]}/AIT")
|
||||||
|
except NetworkError as e:
|
||||||
|
error(f"Network error fetching order book: {e}")
|
||||||
|
raise click.Abort()
|
||||||
|
except Exception as e:
|
||||||
|
error(f"Error fetching order book: {str(e)}")
|
||||||
|
raise click.Abort()
|
||||||
"Amount": f"{order.get('amount', 0):.4f} AIT",
|
"Amount": f"{order.get('amount', 0):.4f} AIT",
|
||||||
"Total": f"{order.get('min_price', 0) * order.get('amount', 0):.8f} {pair.split('/')[1]}",
|
"Total": f"{order.get('min_price', 0) * order.get('amount', 0):.8f} {pair.split('/')[1]}",
|
||||||
"User": order.get('user_id', '')[:16] + "...",
|
"User": order.get('user_id', '')[:16] + "...",
|
||||||
|
|||||||
Reference in New Issue
Block a user