```
chore: remove obsolete payment architecture and integration test documentation - Remove AITBC_PAYMENT_ARCHITECTURE.md (dual-currency system documentation) - Remove IMPLEMENTATION_COMPLETE_SUMMARY.md (integration test completion summary) - Remove INTEGRATION_TEST_FIXES.md (test fixes documentation) - Remove INTEGRATION_TEST_UPDATES.md (real features implementation notes) - Remove PAYMENT_INTEGRATION_COMPLETE.md (wallet-coordinator integration docs) - Remove WALLET_COORDINATOR_INTEGRATION.md (payment
This commit is contained in:
99
apps/blockchain-node/src/aitbc_chain/gossip/relay.py
Normal file
99
apps/blockchain-node/src/aitbc_chain/gossip/relay.py
Normal file
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Simple gossip relay service for blockchain nodes
|
||||
Uses Starlette Broadcast to share messages between nodes
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
from starlette.applications import Starlette
|
||||
from starlette.broadcast import Broadcast
|
||||
from starlette.middleware import Middleware
|
||||
from starlette.middleware.cors import CORSMiddleware
|
||||
from starlette.routing import Route, WebSocketRoute
|
||||
from starlette.websockets import WebSocket
|
||||
import uvicorn
|
||||
|
||||
# Setup logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Global broadcast instance
|
||||
broadcast = Broadcast("memory://")
|
||||
|
||||
|
||||
async def gossip_endpoint(request):
|
||||
"""HTTP endpoint for publishing gossip messages"""
|
||||
try:
|
||||
data = await request.json()
|
||||
channel = data.get("channel", "blockchain")
|
||||
message = data.get("message")
|
||||
|
||||
if message:
|
||||
await broadcast.publish(channel, message)
|
||||
logger.info(f"Published to {channel}: {str(message)[:50]}...")
|
||||
|
||||
return {"status": "published", "channel": channel}
|
||||
else:
|
||||
return {"status": "error", "message": "No message provided"}
|
||||
except Exception as e:
|
||||
logger.error(f"Error publishing: {e}")
|
||||
return {"status": "error", "message": str(e)}
|
||||
|
||||
|
||||
async def websocket_endpoint(websocket: WebSocket):
|
||||
"""WebSocket endpoint for real-time gossip"""
|
||||
await websocket.accept()
|
||||
|
||||
# Get channel from query params
|
||||
channel = websocket.query_params.get("channel", "blockchain")
|
||||
logger.info(f"WebSocket connected to channel: {channel}")
|
||||
|
||||
try:
|
||||
async with broadcast.subscribe(channel) as subscriber:
|
||||
async for message in subscriber:
|
||||
await websocket.send_text(message)
|
||||
except Exception as e:
|
||||
logger.error(f"WebSocket error: {e}")
|
||||
finally:
|
||||
logger.info("WebSocket disconnected")
|
||||
|
||||
|
||||
def create_app() -> Starlette:
|
||||
"""Create the Starlette application"""
|
||||
routes = [
|
||||
Route("/gossip", gossip_endpoint, methods=["POST"]),
|
||||
WebSocketRoute("/ws", websocket_endpoint),
|
||||
]
|
||||
|
||||
middleware = [
|
||||
Middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"])
|
||||
]
|
||||
|
||||
return Starlette(routes=routes, middleware=middleware)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="AITBC Gossip Relay")
|
||||
parser.add_argument("--host", default="127.0.0.1", help="Bind host")
|
||||
parser.add_argument("--port", type=int, default=7070, help="Bind port")
|
||||
parser.add_argument("--log-level", default="info", help="Log level")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
logger.info(f"Starting gossip relay on {args.host}:{args.port}")
|
||||
|
||||
app = create_app()
|
||||
uvicorn.run(
|
||||
app,
|
||||
host=args.host,
|
||||
port=args.port,
|
||||
log_level=args.log_level
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -105,6 +105,61 @@ async def get_block(height: int) -> Dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
@router.get("/blocks", summary="Get latest blocks")
|
||||
async def get_blocks(limit: int = 10, offset: int = 0) -> Dict[str, Any]:
|
||||
metrics_registry.increment("rpc_get_blocks_total")
|
||||
start = time.perf_counter()
|
||||
|
||||
# Validate parameters
|
||||
if limit < 1 or limit > 100:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="limit must be between 1 and 100")
|
||||
if offset < 0:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="offset must be non-negative")
|
||||
|
||||
with session_scope() as session:
|
||||
# Get blocks in descending order (newest first)
|
||||
blocks = session.exec(
|
||||
select(Block)
|
||||
.order_by(Block.height.desc())
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
).all()
|
||||
|
||||
# Get total count for pagination info
|
||||
total_count = len(session.exec(select(Block)).all())
|
||||
|
||||
if not blocks:
|
||||
metrics_registry.increment("rpc_get_blocks_empty_total")
|
||||
return {
|
||||
"blocks": [],
|
||||
"total": total_count,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
}
|
||||
|
||||
# Serialize blocks
|
||||
block_list = []
|
||||
for block in blocks:
|
||||
block_list.append({
|
||||
"height": block.height,
|
||||
"hash": block.hash,
|
||||
"parent_hash": block.parent_hash,
|
||||
"timestamp": block.timestamp.isoformat(),
|
||||
"tx_count": block.tx_count,
|
||||
"state_root": block.state_root,
|
||||
})
|
||||
|
||||
metrics_registry.increment("rpc_get_blocks_success_total")
|
||||
metrics_registry.observe("rpc_get_blocks_duration_seconds", time.perf_counter() - start)
|
||||
|
||||
return {
|
||||
"blocks": block_list,
|
||||
"total": total_count,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/tx/{tx_hash}", summary="Get transaction by hash")
|
||||
async def get_transaction(tx_hash: str) -> Dict[str, Any]:
|
||||
metrics_registry.increment("rpc_get_transaction_total")
|
||||
@@ -126,6 +181,61 @@ async def get_transaction(tx_hash: str) -> Dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
@router.get("/transactions", summary="Get latest transactions")
|
||||
async def get_transactions(limit: int = 20, offset: int = 0) -> Dict[str, Any]:
|
||||
metrics_registry.increment("rpc_get_transactions_total")
|
||||
start = time.perf_counter()
|
||||
|
||||
# Validate parameters
|
||||
if limit < 1 or limit > 100:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="limit must be between 1 and 100")
|
||||
if offset < 0:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="offset must be non-negative")
|
||||
|
||||
with session_scope() as session:
|
||||
# Get transactions in descending order (newest first)
|
||||
transactions = session.exec(
|
||||
select(Transaction)
|
||||
.order_by(Transaction.created_at.desc())
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
).all()
|
||||
|
||||
# Get total count for pagination info
|
||||
total_count = len(session.exec(select(Transaction)).all())
|
||||
|
||||
if not transactions:
|
||||
metrics_registry.increment("rpc_get_transactions_empty_total")
|
||||
return {
|
||||
"transactions": [],
|
||||
"total": total_count,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
}
|
||||
|
||||
# Serialize transactions
|
||||
tx_list = []
|
||||
for tx in transactions:
|
||||
tx_list.append({
|
||||
"tx_hash": tx.tx_hash,
|
||||
"block_height": tx.block_height,
|
||||
"sender": tx.sender,
|
||||
"recipient": tx.recipient,
|
||||
"payload": tx.payload,
|
||||
"created_at": tx.created_at.isoformat(),
|
||||
})
|
||||
|
||||
metrics_registry.increment("rpc_get_transactions_success_total")
|
||||
metrics_registry.observe("rpc_get_transactions_duration_seconds", time.perf_counter() - start)
|
||||
|
||||
return {
|
||||
"transactions": tx_list,
|
||||
"total": total_count,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/receipts/{receipt_id}", summary="Get receipt by ID")
|
||||
async def get_receipt(receipt_id: str) -> Dict[str, Any]:
|
||||
metrics_registry.increment("rpc_get_receipt_total")
|
||||
@@ -140,6 +250,54 @@ async def get_receipt(receipt_id: str) -> Dict[str, Any]:
|
||||
return _serialize_receipt(receipt)
|
||||
|
||||
|
||||
@router.get("/receipts", summary="Get latest receipts")
|
||||
async def get_receipts(limit: int = 20, offset: int = 0) -> Dict[str, Any]:
|
||||
metrics_registry.increment("rpc_get_receipts_total")
|
||||
start = time.perf_counter()
|
||||
|
||||
# Validate parameters
|
||||
if limit < 1 or limit > 100:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="limit must be between 1 and 100")
|
||||
if offset < 0:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="offset must be non-negative")
|
||||
|
||||
with session_scope() as session:
|
||||
# Get receipts in descending order (newest first)
|
||||
receipts = session.exec(
|
||||
select(Receipt)
|
||||
.order_by(Receipt.recorded_at.desc())
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
).all()
|
||||
|
||||
# Get total count for pagination info
|
||||
total_count = len(session.exec(select(Receipt)).all())
|
||||
|
||||
if not receipts:
|
||||
metrics_registry.increment("rpc_get_receipts_empty_total")
|
||||
return {
|
||||
"receipts": [],
|
||||
"total": total_count,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
}
|
||||
|
||||
# Serialize receipts
|
||||
receipt_list = []
|
||||
for receipt in receipts:
|
||||
receipt_list.append(_serialize_receipt(receipt))
|
||||
|
||||
metrics_registry.increment("rpc_get_receipts_success_total")
|
||||
metrics_registry.observe("rpc_get_receipts_duration_seconds", time.perf_counter() - start)
|
||||
|
||||
return {
|
||||
"receipts": receipt_list,
|
||||
"total": total_count,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/getBalance/{address}", summary="Get account balance")
|
||||
async def get_balance(address: str) -> Dict[str, Any]:
|
||||
metrics_registry.increment("rpc_get_balance_total")
|
||||
@@ -160,6 +318,131 @@ async def get_balance(address: str) -> Dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
@router.get("/address/{address}", summary="Get address details including transactions")
|
||||
async def get_address_details(address: str, limit: int = 20, offset: int = 0) -> Dict[str, Any]:
|
||||
metrics_registry.increment("rpc_get_address_total")
|
||||
start = time.perf_counter()
|
||||
|
||||
# Validate parameters
|
||||
if limit < 1 or limit > 100:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="limit must be between 1 and 100")
|
||||
if offset < 0:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="offset must be non-negative")
|
||||
|
||||
with session_scope() as session:
|
||||
# Get account info
|
||||
account = session.get(Account, address)
|
||||
|
||||
# Get transactions where this address is sender or recipient
|
||||
sent_txs = session.exec(
|
||||
select(Transaction)
|
||||
.where(Transaction.sender == address)
|
||||
.order_by(Transaction.created_at.desc())
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
).all()
|
||||
|
||||
received_txs = session.exec(
|
||||
select(Transaction)
|
||||
.where(Transaction.recipient == address)
|
||||
.order_by(Transaction.created_at.desc())
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
).all()
|
||||
|
||||
# Get total counts
|
||||
total_sent = len(session.exec(select(Transaction).where(Transaction.sender == address)).all())
|
||||
total_received = len(session.exec(select(Transaction).where(Transaction.recipient == address)).all())
|
||||
|
||||
# Serialize transactions
|
||||
serialize_tx = lambda tx: {
|
||||
"tx_hash": tx.tx_hash,
|
||||
"block_height": tx.block_height,
|
||||
"direction": "sent" if tx.sender == address else "received",
|
||||
"counterparty": tx.recipient if tx.sender == address else tx.sender,
|
||||
"payload": tx.payload,
|
||||
"created_at": tx.created_at.isoformat(),
|
||||
}
|
||||
|
||||
response = {
|
||||
"address": address,
|
||||
"balance": account.balance if account else 0,
|
||||
"nonce": account.nonce if account else 0,
|
||||
"total_transactions_sent": total_sent,
|
||||
"total_transactions_received": total_received,
|
||||
"latest_sent": [serialize_tx(tx) for tx in sent_txs],
|
||||
"latest_received": [serialize_tx(tx) for tx in received_txs],
|
||||
}
|
||||
|
||||
if account:
|
||||
response["updated_at"] = account.updated_at.isoformat()
|
||||
|
||||
metrics_registry.increment("rpc_get_address_success_total")
|
||||
metrics_registry.observe("rpc_get_address_duration_seconds", time.perf_counter() - start)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@router.get("/addresses", summary="Get list of active addresses")
|
||||
async def get_addresses(limit: int = 20, offset: int = 0, min_balance: int = 0) -> Dict[str, Any]:
|
||||
metrics_registry.increment("rpc_get_addresses_total")
|
||||
start = time.perf_counter()
|
||||
|
||||
# Validate parameters
|
||||
if limit < 1 or limit > 100:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="limit must be between 1 and 100")
|
||||
if offset < 0:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="offset must be non-negative")
|
||||
|
||||
with session_scope() as session:
|
||||
# Get addresses with balance >= min_balance
|
||||
addresses = session.exec(
|
||||
select(Account)
|
||||
.where(Account.balance >= min_balance)
|
||||
.order_by(Account.balance.desc())
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
).all()
|
||||
|
||||
# Get total count
|
||||
total_count = len(session.exec(select(Account).where(Account.balance >= min_balance)).all())
|
||||
|
||||
if not addresses:
|
||||
metrics_registry.increment("rpc_get_addresses_empty_total")
|
||||
return {
|
||||
"addresses": [],
|
||||
"total": total_count,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
}
|
||||
|
||||
# Serialize addresses
|
||||
address_list = []
|
||||
for addr in addresses:
|
||||
# Get transaction counts
|
||||
sent_count = len(session.exec(select(Transaction).where(Transaction.sender == addr.address)).all())
|
||||
received_count = len(session.exec(select(Transaction).where(Transaction.recipient == addr.address)).all())
|
||||
|
||||
address_list.append({
|
||||
"address": addr.address,
|
||||
"balance": addr.balance,
|
||||
"nonce": addr.nonce,
|
||||
"total_transactions_sent": sent_count,
|
||||
"total_transactions_received": received_count,
|
||||
"updated_at": addr.updated_at.isoformat(),
|
||||
})
|
||||
|
||||
metrics_registry.increment("rpc_get_addresses_success_total")
|
||||
metrics_registry.observe("rpc_get_addresses_duration_seconds", time.perf_counter() - start)
|
||||
|
||||
return {
|
||||
"addresses": address_list,
|
||||
"total": total_count,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/sendTx", summary="Submit a new transaction")
|
||||
async def send_transaction(request: TransactionRequest) -> Dict[str, Any]:
|
||||
metrics_registry.increment("rpc_send_tx_total")
|
||||
|
||||
Reference in New Issue
Block a user