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:
oib
2026-02-16 22:54:08 +01:00
parent fdc3012780
commit 31d3d70836
20 changed files with 3330 additions and 80 deletions

View File

@@ -12,7 +12,15 @@ import logging
logger = logging.getLogger(__name__)
from ..schemas import ExchangePaymentRequest, ExchangePaymentResponse
from ..schemas import (
ExchangePaymentRequest,
ExchangePaymentResponse,
ExchangeRatesResponse,
PaymentStatusResponse,
MarketStatsResponse,
WalletBalanceResponse,
WalletInfoResponse
)
from ..services.bitcoin_wallet import get_wallet_balance, get_wallet_info
router = APIRouter(tags=["exchange"])
@@ -70,7 +78,8 @@ async def create_payment(
return payment
@router.get("/exchange/payment-status/{payment_id}")
@router.get("/exchange/payment-status/{payment_id}", response_model=PaymentStatusResponse)
async def get_payment_status(payment_id: str) -> Dict[str, Any]:
"""Get payment status"""
@@ -85,6 +94,7 @@ async def get_payment_status(payment_id: str) -> Dict[str, Any]:
return payment
@router.post("/exchange/confirm-payment/{payment_id}")
async def confirm_payment(
payment_id: str,
@@ -121,18 +131,20 @@ async def confirm_payment(
'aitbc_amount': payment['aitbc_amount']
}
@router.get("/exchange/rates")
async def get_exchange_rates() -> Dict[str, float]:
@router.get("/exchange/rates", response_model=ExchangeRatesResponse)
async def get_exchange_rates() -> ExchangeRatesResponse:
"""Get current exchange rates"""
return {
'btc_to_aitbc': BITCOIN_CONFIG['exchange_rate'],
'aitbc_to_btc': 1.0 / BITCOIN_CONFIG['exchange_rate'],
'fee_percent': 0.5
}
return ExchangeRatesResponse(
btc_to_aitbc=BITCOIN_CONFIG['exchange_rate'],
aitbc_to_btc=1.0 / BITCOIN_CONFIG['exchange_rate'],
fee_percent=0.5
)
@router.get("/exchange/market-stats")
async def get_market_stats() -> Dict[str, Any]:
@router.get("/exchange/market-stats", response_model=MarketStatsResponse)
async def get_market_stats() -> MarketStatsResponse:
"""Get market statistics"""
# Calculate 24h volume from payments
@@ -148,28 +160,32 @@ async def get_market_stats() -> Dict[str, Any]:
base_price = 1.0 / BITCOIN_CONFIG['exchange_rate']
price_change_percent = 5.2 # Simulated +5.2%
return {
'price': base_price,
'price_change_24h': price_change_percent,
'daily_volume': daily_volume,
'daily_volume_btc': daily_volume / BITCOIN_CONFIG['exchange_rate'],
'total_payments': len([p for p in payments.values() if p['status'] == 'confirmed']),
'pending_payments': len([p for p in payments.values() if p['status'] == 'pending'])
}
return MarketStatsResponse(
price=base_price,
price_change_24h=price_change_percent,
daily_volume=daily_volume,
daily_volume_btc=daily_volume / BITCOIN_CONFIG['exchange_rate'],
total_payments=len([p for p in payments.values() if p['status'] == 'confirmed']),
pending_payments=len([p for p in payments.values() if p['status'] == 'pending'])
)
@router.get("/exchange/wallet/balance")
async def get_wallet_balance_api() -> Dict[str, Any]:
@router.get("/exchange/wallet/balance", response_model=WalletBalanceResponse)
async def get_wallet_balance_api() -> WalletBalanceResponse:
"""Get Bitcoin wallet balance"""
try:
return get_wallet_balance()
balance_data = get_wallet_balance()
return WalletBalanceResponse(**balance_data)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/exchange/wallet/info")
async def get_wallet_info_api() -> Dict[str, Any]:
@router.get("/exchange/wallet/info", response_model=WalletInfoResponse)
async def get_wallet_info_api() -> WalletInfoResponse:
"""Get comprehensive wallet information"""
try:
return get_wallet_info()
wallet_data = get_wallet_info()
return WalletInfoResponse(**wallet_data)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi import status as http_status
from ..schemas import MarketplaceBidRequest, MarketplaceOfferView, MarketplaceStatsView
from ..schemas import MarketplaceBidRequest, MarketplaceOfferView, MarketplaceStatsView, MarketplaceBidView
from ..services import MarketplaceService
from ..storage import SessionDep
from ..metrics import marketplace_requests_total, marketplace_errors_total
@@ -74,3 +74,51 @@ async def submit_marketplace_bid(
except Exception:
marketplace_errors_total.labels(endpoint="/marketplace/bids", method="POST", error_type="internal").inc()
raise
@router.get(
"/marketplace/bids",
response_model=list[MarketplaceBidView],
summary="List marketplace bids",
)
async def list_marketplace_bids(
*,
session: SessionDep,
status_filter: str | None = Query(default=None, alias="status", description="Filter by bid status"),
provider_filter: str | None = Query(default=None, alias="provider", description="Filter by provider ID"),
limit: int = Query(default=100, ge=1, le=500),
offset: int = Query(default=0, ge=0),
) -> list[MarketplaceBidView]:
marketplace_requests_total.labels(endpoint="/marketplace/bids", method="GET").inc()
service = _get_service(session)
try:
return service.list_bids(status=status_filter, provider=provider_filter, limit=limit, offset=offset)
except ValueError:
marketplace_errors_total.labels(endpoint="/marketplace/bids", method="GET", error_type="invalid_request").inc()
raise HTTPException(status_code=http_status.HTTP_400_BAD_REQUEST, detail="invalid filter") from None
except Exception:
marketplace_errors_total.labels(endpoint="/marketplace/bids", method="GET", error_type="internal").inc()
raise
@router.get(
"/marketplace/bids/{bid_id}",
response_model=MarketplaceBidView,
summary="Get bid details",
)
async def get_marketplace_bid(
bid_id: str,
session: SessionDep,
) -> MarketplaceBidView:
marketplace_requests_total.labels(endpoint="/marketplace/bids/{bid_id}", method="GET").inc()
service = _get_service(session)
try:
bid = service.get_bid(bid_id)
if not bid:
raise HTTPException(status_code=http_status.HTTP_404_NOT_FOUND, detail="bid not found")
return bid
except HTTPException:
raise
except Exception:
marketplace_errors_total.labels(endpoint="/marketplace/bids/{bid_id}", method="GET", error_type="internal").inc()
raise

View File

@@ -14,6 +14,8 @@ class WebVitalsEntry(BaseModel):
name: str
startTime: Optional[float] = None
duration: Optional[float] = None
value: Optional[float] = None
hadRecentInput: Optional[bool] = None
class WebVitalsMetric(BaseModel):
name: str
@@ -31,6 +33,20 @@ async def collect_web_vitals(metric: WebVitalsMetric):
This endpoint receives Core Web Vitals (LCP, FID, CLS, TTFB, FCP) for monitoring.
"""
try:
# Filter entries to only include supported fields
filtered_entries = []
for entry in metric.entries:
filtered_entry = {
"name": entry.name,
"startTime": entry.startTime,
"duration": entry.duration,
"value": entry.value,
"hadRecentInput": entry.hadRecentInput
}
# Remove None values
filtered_entry = {k: v for k, v in filtered_entry.items() if v is not None}
filtered_entries.append(filtered_entry)
# Log the metric for monitoring/analysis
logging.info(f"Web Vitals - {metric.name}: {metric.value}ms (ID: {metric.id}) from {metric.url or 'unknown'}")