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

@@ -1,5 +1,6 @@
from __future__ import annotations
import httpx
from collections import defaultdict, deque
from datetime import datetime
from typing import Optional
@@ -38,25 +39,51 @@ class ExplorerService:
self.session = session
def list_blocks(self, *, limit: int = 20, offset: int = 0) -> BlockListResponse:
statement = select(Job).order_by(Job.requested_at.desc())
jobs = self.session.exec(statement.offset(offset).limit(limit)).all()
# Fetch real blockchain data from RPC API
try:
# Use the blockchain RPC API running on localhost:8082
with httpx.Client(timeout=10.0) as client:
response = client.get("http://localhost:8082/rpc/blocks", params={"limit": limit, "offset": offset})
response.raise_for_status()
rpc_data = response.json()
items: list[BlockSummary] = []
for block in rpc_data.get("blocks", []):
items.append(
BlockSummary(
height=block["height"],
hash=block["hash"],
timestamp=datetime.fromisoformat(block["timestamp"]),
txCount=block["tx_count"],
proposer=block["proposer"],
)
)
next_offset: Optional[int] = offset + len(items) if len(items) == limit else None
return BlockListResponse(items=items, next_offset=next_offset)
except Exception as e:
# Fallback to fake data if RPC is unavailable
print(f"Warning: Failed to fetch blocks from RPC: {e}, falling back to fake data")
statement = select(Job).order_by(Job.requested_at.desc())
jobs = self.session.exec(statement.offset(offset).limit(limit)).all()
items: list[BlockSummary] = []
for index, job in enumerate(jobs):
height = _DEFAULT_HEIGHT_BASE + offset + index
proposer = job.assigned_miner_id or "unassigned"
items.append(
BlockSummary(
height=height,
hash=job.id,
timestamp=job.requested_at,
txCount=1,
proposer=proposer,
items: list[BlockSummary] = []
for index, job in enumerate(jobs):
height = _DEFAULT_HEIGHT_BASE + offset + index
proposer = job.assigned_miner_id or "unassigned"
items.append(
BlockSummary(
height=height,
hash=job.id,
timestamp=job.requested_at,
txCount=1,
proposer=proposer,
)
)
)
next_offset: Optional[int] = offset + len(items) if len(items) == limit else None
return BlockListResponse(items=items, next_offset=next_offset)
next_offset: Optional[int] = offset + len(items) if len(items) == limit else None
return BlockListResponse(items=items, next_offset=next_offset)
def list_transactions(self, *, limit: int = 50, offset: int = 0) -> TransactionListResponse:
statement = (

View File

@@ -10,6 +10,7 @@ from ..schemas import (
MarketplaceBidRequest,
MarketplaceOfferView,
MarketplaceStatsView,
MarketplaceBidView,
)
@@ -70,6 +71,47 @@ class MarketplaceService:
self.session.refresh(bid)
return bid
def list_bids(
self,
*,
status: Optional[str] = None,
provider: Optional[str] = None,
limit: int = 100,
offset: int = 0,
) -> list[MarketplaceBidView]:
stmt = select(MarketplaceBid).order_by(MarketplaceBid.submitted_at.desc())
if status is not None:
normalised = status.strip().lower()
if normalised not in ("pending", "accepted", "rejected"):
raise ValueError(f"invalid status: {status}")
stmt = stmt.where(MarketplaceBid.status == normalised)
if provider is not None:
stmt = stmt.where(MarketplaceBid.provider == provider)
stmt = stmt.offset(offset).limit(limit)
bids = self.session.exec(stmt).all()
return [self._to_bid_view(bid) for bid in bids]
def get_bid(self, bid_id: str) -> Optional[MarketplaceBidView]:
bid = self.session.get(MarketplaceBid, bid_id)
if bid:
return self._to_bid_view(bid)
return None
@staticmethod
def _to_bid_view(bid: MarketplaceBid) -> MarketplaceBidView:
return MarketplaceBidView(
id=bid.id,
provider=bid.provider,
capacity=bid.capacity,
price=bid.price,
notes=bid.notes,
status=bid.status,
submitted_at=bid.submitted_at,
)
@staticmethod
def _to_offer_view(offer: MarketplaceOffer) -> MarketplaceOfferView:
status_val = offer.status.value if hasattr(offer.status, "value") else offer.status