chore: refactor logging module, update genesis timestamp, remove model relationships, and reorganize routers

- Rename logging.py to logger.py and update import paths in poa.py and main.py
- Update devnet genesis timestamp to 1766828620
- Remove SQLModel Relationship declarations from Block, Transaction, and Receipt models
- Add SessionDep type alias and get_session dependency in coordinator-api deps
- Reorganize coordinator-api routers: replace explorer/registry with exchange, users, marketplace
This commit is contained in:
oib
2025-12-28 21:05:53 +01:00
parent 930ee31a8f
commit b3fd0ea05c
145 changed files with 33301 additions and 219 deletions

View File

@@ -6,6 +6,9 @@ from .admin import router as admin
from .marketplace import router as marketplace
from .explorer import router as explorer
from .services import router as services
from .registry import router as registry
from .users import router as users
from .exchange import router as exchange
from .marketplace_offers import router as marketplace_offers
# from .registry import router as registry
__all__ = ["client", "miner", "admin", "marketplace", "explorer", "services", "registry"]
__all__ = ["client", "miner", "admin", "marketplace", "explorer", "services", "users", "exchange", "marketplace_offers", "registry"]

View File

@@ -1,7 +1,7 @@
from fastapi import APIRouter, Depends, HTTPException, status
from ..deps import require_client_key
from ..models import JobCreate, JobView, JobResult
from ..schemas import JobCreate, JobView, JobResult
from ..services import JobService
from ..storage import SessionDep

View File

@@ -10,7 +10,7 @@ import json
from slowapi import Limiter
from slowapi.util import get_remote_address
from ..models import (
from ..schemas import (
ConfidentialTransaction,
ConfidentialTransactionCreate,
ConfidentialTransactionView,

View File

@@ -0,0 +1,151 @@
"""
Bitcoin Exchange Router for AITBC
"""
from typing import Dict, Any
from fastapi import APIRouter, HTTPException, BackgroundTasks
from sqlmodel import Session
import uuid
import time
import json
import os
from ..deps import SessionDep
from ..domain import Wallet
from ..schemas import ExchangePaymentRequest, ExchangePaymentResponse
router = APIRouter(tags=["exchange"])
# In-memory storage for demo (use database in production)
payments: Dict[str, Dict] = {}
# Bitcoin configuration
BITCOIN_CONFIG = {
'testnet': True,
'main_address': 'tb1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh', # Testnet address
'exchange_rate': 100000, # 1 BTC = 100,000 AITBC
'min_confirmations': 1,
'payment_timeout': 3600 # 1 hour
}
@router.post("/exchange/create-payment", response_model=ExchangePaymentResponse)
async def create_payment(
request: ExchangePaymentRequest,
session: SessionDep,
background_tasks: BackgroundTasks
) -> Dict[str, Any]:
"""Create a new Bitcoin payment request"""
# Validate request
if request.aitbc_amount <= 0 or request.btc_amount <= 0:
raise HTTPException(status_code=400, detail="Invalid amount")
# Calculate expected BTC amount
expected_btc = request.aitbc_amount / BITCOIN_CONFIG['exchange_rate']
# Allow small difference for rounding
if abs(request.btc_amount - expected_btc) > 0.00000001:
raise HTTPException(status_code=400, detail="Amount mismatch")
# Create payment record
payment_id = str(uuid.uuid4())
payment = {
'payment_id': payment_id,
'user_id': request.user_id,
'aitbc_amount': request.aitbc_amount,
'btc_amount': request.btc_amount,
'payment_address': BITCOIN_CONFIG['main_address'],
'status': 'pending',
'created_at': int(time.time()),
'expires_at': int(time.time()) + BITCOIN_CONFIG['payment_timeout'],
'confirmations': 0,
'tx_hash': None
}
# Store payment
payments[payment_id] = payment
# Start payment monitoring in background
background_tasks.add_task(monitor_payment, payment_id)
return payment
@router.get("/exchange/payment-status/{payment_id}")
async def get_payment_status(payment_id: str) -> Dict[str, Any]:
"""Get payment status"""
if payment_id not in payments:
raise HTTPException(status_code=404, detail="Payment not found")
payment = payments[payment_id]
# Check if expired
if payment['status'] == 'pending' and time.time() > payment['expires_at']:
payment['status'] = 'expired'
return payment
@router.post("/exchange/confirm-payment/{payment_id}")
async def confirm_payment(
payment_id: str,
tx_hash: str,
session: SessionDep
) -> Dict[str, Any]:
"""Confirm payment (webhook from payment processor)"""
if payment_id not in payments:
raise HTTPException(status_code=404, detail="Payment not found")
payment = payments[payment_id]
if payment['status'] != 'pending':
raise HTTPException(status_code=400, detail="Payment not in pending state")
# Verify transaction (in production, verify with blockchain API)
# For demo, we'll accept any tx_hash
payment['status'] = 'confirmed'
payment['tx_hash'] = tx_hash
payment['confirmed_at'] = int(time.time())
# Mint AITBC tokens to user's wallet
try:
from ..services.blockchain import mint_tokens
mint_tokens(payment['user_id'], payment['aitbc_amount'])
except Exception as e:
print(f"Error minting tokens: {e}")
# In production, handle this error properly
return {
'status': 'ok',
'payment_id': payment_id,
'aitbc_amount': payment['aitbc_amount']
}
@router.get("/exchange/rates")
async def get_exchange_rates() -> Dict[str, float]:
"""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
}
async def monitor_payment(payment_id: str):
"""Monitor payment for confirmation (background task)"""
import asyncio
while payment_id in payments:
payment = payments[payment_id]
# Check if expired
if payment['status'] == 'pending' and time.time() > payment['expires_at']:
payment['status'] = 'expired'
break
# In production, check blockchain for payment
# For demo, we'll wait for manual confirmation
await asyncio.sleep(30) # Check every 30 seconds

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
from fastapi import APIRouter, Depends, Query
from ..models import (
from ..schemas import (
BlockListResponse,
TransactionListResponse,
AddressListResponse,

View File

@@ -0,0 +1,381 @@
"""
Governance Router - Proposal voting and parameter changes
"""
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
from pydantic import BaseModel, Field
from typing import Optional, Dict, Any, List
from datetime import datetime, timedelta
import json
from ..schemas import UserProfile
from ..storage import SessionDep
from ..storage.models_governance import GovernanceProposal, ProposalVote
from sqlmodel import select, func
router = APIRouter(tags=["governance"])
class ProposalCreate(BaseModel):
"""Create a new governance proposal"""
title: str = Field(..., min_length=10, max_length=200)
description: str = Field(..., min_length=50, max_length=5000)
type: str = Field(..., pattern="^(parameter_change|protocol_upgrade|fund_allocation|policy_change)$")
target: Optional[Dict[str, Any]] = Field(default_factory=dict)
voting_period: int = Field(default=7, ge=1, le=30) # days
quorum_threshold: float = Field(default=0.1, ge=0.01, le=1.0) # 10% default
approval_threshold: float = Field(default=0.5, ge=0.01, le=1.0) # 50% default
class ProposalResponse(BaseModel):
"""Governance proposal response"""
id: str
title: str
description: str
type: str
target: Dict[str, Any]
proposer: str
status: str
created_at: datetime
voting_deadline: datetime
quorum_threshold: float
approval_threshold: float
current_quorum: float
current_approval: float
votes_for: int
votes_against: int
votes_abstain: int
total_voting_power: int
class VoteSubmit(BaseModel):
"""Submit a vote on a proposal"""
proposal_id: str
vote: str = Field(..., pattern="^(for|against|abstain)$")
reason: Optional[str] = Field(max_length=500)
@router.post("/governance/proposals", response_model=ProposalResponse)
async def create_proposal(
proposal: ProposalCreate,
user: UserProfile,
session: SessionDep
) -> ProposalResponse:
"""Create a new governance proposal"""
# Check if user has voting power
voting_power = await get_user_voting_power(user.user_id, session)
if voting_power == 0:
raise HTTPException(403, "You must have voting power to create proposals")
# Create proposal
db_proposal = GovernanceProposal(
title=proposal.title,
description=proposal.description,
type=proposal.type,
target=proposal.target,
proposer=user.user_id,
status="active",
created_at=datetime.utcnow(),
voting_deadline=datetime.utcnow() + timedelta(days=proposal.voting_period),
quorum_threshold=proposal.quorum_threshold,
approval_threshold=proposal.approval_threshold
)
session.add(db_proposal)
session.commit()
session.refresh(db_proposal)
# Return response
return await format_proposal_response(db_proposal, session)
@router.get("/governance/proposals", response_model=List[ProposalResponse])
async def list_proposals(
status: Optional[str] = None,
limit: int = 20,
offset: int = 0,
session: SessionDep = None
) -> List[ProposalResponse]:
"""List governance proposals"""
query = select(GovernanceProposal)
if status:
query = query.where(GovernanceProposal.status == status)
query = query.order_by(GovernanceProposal.created_at.desc())
query = query.offset(offset).limit(limit)
proposals = session.exec(query).all()
responses = []
for proposal in proposals:
formatted = await format_proposal_response(proposal, session)
responses.append(formatted)
return responses
@router.get("/governance/proposals/{proposal_id}", response_model=ProposalResponse)
async def get_proposal(
proposal_id: str,
session: SessionDep
) -> ProposalResponse:
"""Get a specific proposal"""
proposal = session.get(GovernanceProposal, proposal_id)
if not proposal:
raise HTTPException(404, "Proposal not found")
return await format_proposal_response(proposal, session)
@router.post("/governance/vote")
async def submit_vote(
vote: VoteSubmit,
user: UserProfile,
session: SessionDep
) -> Dict[str, str]:
"""Submit a vote on a proposal"""
# Check proposal exists and is active
proposal = session.get(GovernanceProposal, vote.proposal_id)
if not proposal:
raise HTTPException(404, "Proposal not found")
if proposal.status != "active":
raise HTTPException(400, "Proposal is not active for voting")
if datetime.utcnow() > proposal.voting_deadline:
raise HTTPException(400, "Voting period has ended")
# Check user voting power
voting_power = await get_user_voting_power(user.user_id, session)
if voting_power == 0:
raise HTTPException(403, "You have no voting power")
# Check if already voted
existing = session.exec(
select(ProposalVote).where(
ProposalVote.proposal_id == vote.proposal_id,
ProposalVote.voter_id == user.user_id
)
).first()
if existing:
# Update existing vote
existing.vote = vote.vote
existing.reason = vote.reason
existing.voted_at = datetime.utcnow()
else:
# Create new vote
db_vote = ProposalVote(
proposal_id=vote.proposal_id,
voter_id=user.user_id,
vote=vote.vote,
voting_power=voting_power,
reason=vote.reason,
voted_at=datetime.utcnow()
)
session.add(db_vote)
session.commit()
# Check if proposal should be finalized
if datetime.utcnow() >= proposal.voting_deadline:
await finalize_proposal(proposal, session)
return {"message": "Vote submitted successfully"}
@router.get("/governance/voting-power/{user_id}")
async def get_voting_power(
user_id: str,
session: SessionDep
) -> Dict[str, int]:
"""Get a user's voting power"""
power = await get_user_voting_power(user_id, session)
return {"user_id": user_id, "voting_power": power}
@router.get("/governance/parameters")
async def get_governance_parameters(
session: SessionDep
) -> Dict[str, Any]:
"""Get current governance parameters"""
# These would typically be stored in a config table
return {
"min_proposal_voting_power": 1000,
"max_proposal_title_length": 200,
"max_proposal_description_length": 5000,
"default_voting_period_days": 7,
"max_voting_period_days": 30,
"min_quorum_threshold": 0.01,
"max_quorum_threshold": 1.0,
"min_approval_threshold": 0.01,
"max_approval_threshold": 1.0,
"execution_delay_hours": 24
}
@router.post("/governance/execute/{proposal_id}")
async def execute_proposal(
proposal_id: str,
background_tasks: BackgroundTasks,
session: SessionDep
) -> Dict[str, str]:
"""Execute an approved proposal"""
proposal = session.get(GovernanceProposal, proposal_id)
if not proposal:
raise HTTPException(404, "Proposal not found")
if proposal.status != "passed":
raise HTTPException(400, "Proposal must be passed to execute")
if datetime.utcnow() < proposal.voting_deadline + timedelta(hours=24):
raise HTTPException(400, "Must wait 24 hours after voting ends to execute")
# Execute proposal based on type
if proposal.type == "parameter_change":
await execute_parameter_change(proposal.target, background_tasks)
elif proposal.type == "protocol_upgrade":
await execute_protocol_upgrade(proposal.target, background_tasks)
elif proposal.type == "fund_allocation":
await execute_fund_allocation(proposal.target, background_tasks)
elif proposal.type == "policy_change":
await execute_policy_change(proposal.target, background_tasks)
# Update proposal status
proposal.status = "executed"
proposal.executed_at = datetime.utcnow()
session.commit()
return {"message": "Proposal executed successfully"}
# Helper functions
async def get_user_voting_power(user_id: str, session) -> int:
"""Calculate a user's voting power based on AITBC holdings"""
# In a real implementation, this would query the blockchain
# For now, return a mock value
return 10000 # Mock voting power
async def format_proposal_response(proposal: GovernanceProposal, session) -> ProposalResponse:
"""Format a proposal for API response"""
# Get vote counts
votes = session.exec(
select(ProposalVote).where(ProposalVote.proposal_id == proposal.id)
).all()
votes_for = sum(1 for v in votes if v.vote == "for")
votes_against = sum(1 for v in votes if v.vote == "against")
votes_abstain = sum(1 for v in votes if v.vote == "abstain")
# Get total voting power
total_power = sum(v.voting_power for v in votes)
power_for = sum(v.voting_power for v in votes if v.vote == "for")
# Calculate quorum and approval
total_voting_power = await get_total_voting_power(session)
current_quorum = total_power / total_voting_power if total_voting_power > 0 else 0
current_approval = power_for / total_power if total_power > 0 else 0
return ProposalResponse(
id=proposal.id,
title=proposal.title,
description=proposal.description,
type=proposal.type,
target=proposal.target,
proposer=proposal.proposer,
status=proposal.status,
created_at=proposal.created_at,
voting_deadline=proposal.voting_deadline,
quorum_threshold=proposal.quorum_threshold,
approval_threshold=proposal.approval_threshold,
current_quorum=current_quorum,
current_approval=current_approval,
votes_for=votes_for,
votes_against=votes_against,
votes_abstain=votes_abstain,
total_voting_power=total_voting_power
)
async def get_total_voting_power(session) -> int:
"""Get total voting power in the system"""
# In a real implementation, this would sum all AITBC tokens
return 1000000 # Mock total voting power
async def finalize_proposal(proposal: GovernanceProposal, session):
"""Finalize a proposal after voting ends"""
# Get final vote counts
votes = session.exec(
select(ProposalVote).where(ProposalVote.proposal_id == proposal.id)
).all()
total_power = sum(v.voting_power for v in votes)
power_for = sum(v.voting_power for v in votes if v.vote == "for")
total_voting_power = await get_total_voting_power(session)
quorum = total_power / total_voting_power if total_voting_power > 0 else 0
approval = power_for / total_power if total_power > 0 else 0
# Check if quorum met
if quorum < proposal.quorum_threshold:
proposal.status = "rejected"
proposal.rejection_reason = "Quorum not met"
# Check if approval threshold met
elif approval < proposal.approval_threshold:
proposal.status = "rejected"
proposal.rejection_reason = "Approval threshold not met"
else:
proposal.status = "passed"
session.commit()
async def execute_parameter_change(target: Dict[str, Any], background_tasks):
"""Execute a parameter change proposal"""
# This would update system parameters
print(f"Executing parameter change: {target}")
# Implementation would depend on the specific parameters
async def execute_protocol_upgrade(target: Dict[str, Any], background_tasks):
"""Execute a protocol upgrade proposal"""
# This would trigger a protocol upgrade
print(f"Executing protocol upgrade: {target}")
# Implementation would involve coordinating with nodes
async def execute_fund_allocation(target: Dict[str, Any], background_tasks):
"""Execute a fund allocation proposal"""
# This would transfer funds from treasury
print(f"Executing fund allocation: {target}")
# Implementation would involve treasury management
async def execute_policy_change(target: Dict[str, Any], background_tasks):
"""Execute a policy change proposal"""
# This would update system policies
print(f"Executing policy change: {target}")
# Implementation would depend on the specific policy
# Export the router
__all__ = ["router"]

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 ..models import MarketplaceBidRequest, MarketplaceOfferView, MarketplaceStatsView
from ..schemas import MarketplaceBidRequest, MarketplaceOfferView, MarketplaceStatsView
from ..services import MarketplaceService
from ..storage import SessionDep
from ..metrics import marketplace_requests_total, marketplace_errors_total

View File

@@ -0,0 +1,132 @@
"""
Router to create marketplace offers from registered miners
"""
from typing import Any
from fastapi import APIRouter, Depends, HTTPException
from sqlmodel import Session, select
from ..deps import require_admin_key
from ..domain import MarketplaceOffer, Miner, OfferStatus
from ..schemas import MarketplaceOfferView
from ..storage import SessionDep
router = APIRouter(tags=["marketplace-offers"])
@router.post("/marketplace/sync-offers", summary="Create offers from registered miners")
async def sync_offers(
session: SessionDep,
admin_key: str = Depends(require_admin_key()),
) -> dict[str, Any]:
"""Create marketplace offers from all registered miners"""
# Get all registered miners
miners = session.exec(select(Miner).where(Miner.status == "ONLINE")).all()
created_offers = []
for miner in miners:
# Check if offer already exists
existing = session.exec(
select(MarketplaceOffer).where(MarketplaceOffer.provider == miner.id)
).first()
if not existing:
# Create offer from miner capabilities
capabilities = miner.capabilities or {}
offer = MarketplaceOffer(
provider=miner.id,
capacity=miner.concurrency or 1,
price=capabilities.get("pricing_per_hour", 0.50),
attributes={
"gpu_model": capabilities.get("gpu", "Unknown GPU"),
"gpu_memory_gb": capabilities.get("gpu_memory_gb", 0),
"cuda_version": capabilities.get("cuda_version", "Unknown"),
"supported_models": capabilities.get("supported_models", []),
"region": miner.region or "unknown"
}
)
session.add(offer)
created_offers.append(offer.id)
session.commit()
return {
"status": "ok",
"created_offers": len(created_offers),
"offer_ids": created_offers
}
@router.get("/marketplace/offers", summary="List all marketplace offers")
async def list_offers() -> list[dict]:
"""List all marketplace offers"""
# Return simple mock data
return [
{
"id": "mock-offer-1",
"provider_id": "miner_001",
"provider_name": "GPU Miner Alpha",
"capacity": 4,
"price": 0.50,
"gpu_model": "RTX 4090",
"gpu_memory_gb": 24,
"cuda_version": "12.0",
"supported_models": ["llama2-7b", "stable-diffusion-xl"],
"region": "us-west",
"status": "OPEN",
"created_at": "2025-12-28T10:00:00Z",
},
{
"id": "mock-offer-2",
"provider_id": "miner_002",
"provider_name": "GPU Miner Beta",
"capacity": 2,
"price": 0.35,
"gpu_model": "RTX 3080",
"gpu_memory_gb": 16,
"cuda_version": "11.8",
"supported_models": ["llama2-13b", "gpt-j"],
"region": "us-east",
"status": "OPEN",
"created_at": "2025-12-28T09:30:00Z",
},
]
@router.get("/marketplace/miner-offers", summary="List all miner offers", response_model=list[MarketplaceOfferView])
async def list_miner_offers(session: SessionDep) -> list[MarketplaceOfferView]:
"""List all offers created from miners"""
# Get all offers with miner details
offers = session.exec(select(MarketplaceOffer).where(MarketplaceOffer.provider.like("miner_%"))).all()
result = []
for offer in offers:
# Get miner details
miner = session.get(Miner, offer.provider)
# Extract attributes
attrs = offer.attributes or {}
offer_view = MarketplaceOfferView(
id=offer.id,
provider_id=offer.provider,
provider_name=f"Miner {offer.provider}" if miner else "Unknown Miner",
capacity=offer.capacity,
price=offer.price,
gpu_model=attrs.get("gpu_model", "Unknown"),
gpu_memory_gb=attrs.get("gpu_memory_gb", 0),
cuda_version=attrs.get("cuda_version", "Unknown"),
supported_models=attrs.get("supported_models", []),
region=attrs.get("region", "unknown"),
status=offer.status.value,
created_at=offer.created_at,
)
result.append(offer_view)
return result

View File

@@ -4,7 +4,7 @@ from typing import Any
from fastapi import APIRouter, Depends, HTTPException, Response, status
from ..deps import require_miner_key
from ..models import AssignedJob, JobFailSubmit, JobResultSubmit, JobState, MinerHeartbeat, MinerRegister, PollRequest
from ..schemas import AssignedJob, JobFailSubmit, JobResultSubmit, JobState, MinerHeartbeat, MinerRegister, PollRequest
from ..services import JobService, MinerService
from ..services.receipts import ReceiptService
from ..storage import SessionDep

View File

@@ -0,0 +1,296 @@
"""
Partner Router - Third-party integration management
"""
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
from pydantic import BaseModel, Field
from typing import Optional, Dict, Any, List
from datetime import datetime, timedelta
import secrets
import hashlib
from ..schemas import UserProfile
from ..storage import SessionDep
from sqlmodel import select
router = APIRouter(tags=["partners"])
class PartnerRegister(BaseModel):
"""Register a new partner application"""
name: str = Field(..., min_length=3, max_length=100)
description: str = Field(..., min_length=10, max_length=500)
website: str = Field(..., regex=r'^https?://')
contact: str = Field(..., regex=r'^[^@]+@[^@]+\.[^@]+$')
integration_type: str = Field(..., regex="^(explorer|analytics|wallet|exchange|other)$")
class PartnerResponse(BaseModel):
"""Partner registration response"""
partner_id: str
api_key: str
api_secret: str
rate_limit: Dict[str, int]
created_at: datetime
class WebhookCreate(BaseModel):
"""Create a webhook subscription"""
url: str = Field(..., regex=r'^https?://')
events: List[str] = Field(..., min_items=1)
secret: Optional[str] = Field(max_length=100)
class WebhookResponse(BaseModel):
"""Webhook subscription response"""
webhook_id: str
url: str
events: List[str]
status: str
created_at: datetime
# Mock partner storage (in production, use database)
PARTNERS_DB = {}
WEBHOOKS_DB = {}
@router.post("/partners/register", response_model=PartnerResponse)
async def register_partner(
partner: PartnerRegister,
session: SessionDep
) -> PartnerResponse:
"""Register a new partner application"""
# Generate credentials
partner_id = secrets.token_urlsafe(16)
api_key = f"aitbc_{secrets.token_urlsafe(24)}"
api_secret = secrets.token_urlsafe(32)
# Set rate limits based on integration type
rate_limits = {
"explorer": {"requests_per_minute": 1000, "requests_per_hour": 50000},
"analytics": {"requests_per_minute": 500, "requests_per_hour": 25000},
"wallet": {"requests_per_minute": 100, "requests_per_hour": 5000},
"exchange": {"requests_per_minute": 2000, "requests_per_hour": 100000},
"other": {"requests_per_minute": 100, "requests_per_hour": 5000}
}
# Store partner (in production, save to database)
PARTNERS_DB[partner_id] = {
"id": partner_id,
"name": partner.name,
"description": partner.description,
"website": partner.website,
"contact": partner.contact,
"integration_type": partner.integration_type,
"api_key": api_key,
"api_secret_hash": hashlib.sha256(api_secret.encode()).hexdigest(),
"rate_limit": rate_limits.get(partner.integration_type, rate_limits["other"]),
"created_at": datetime.utcnow(),
"status": "active"
}
return PartnerResponse(
partner_id=partner_id,
api_key=api_key,
api_secret=api_secret,
rate_limit=PARTNERS_DB[partner_id]["rate_limit"],
created_at=PARTNERS_DB[partner_id]["created_at"]
)
@router.get("/partners/{partner_id}")
async def get_partner(
partner_id: str,
session: SessionDep,
api_key: str
) -> Dict[str, Any]:
"""Get partner information"""
# Verify API key
partner = verify_partner_api_key(partner_id, api_key)
if not partner:
raise HTTPException(401, "Invalid credentials")
# Return safe partner info
return {
"partner_id": partner["id"],
"name": partner["name"],
"integration_type": partner["integration_type"],
"rate_limit": partner["rate_limit"],
"created_at": partner["created_at"],
"status": partner["status"]
}
@router.post("/partners/webhooks", response_model=WebhookResponse)
async def create_webhook(
webhook: WebhookCreate,
session: SessionDep,
api_key: str
) -> WebhookResponse:
"""Create a webhook subscription"""
# Verify partner from API key
partner = find_partner_by_api_key(api_key)
if not partner:
raise HTTPException(401, "Invalid API key")
# Validate events
valid_events = [
"block.created",
"transaction.confirmed",
"marketplace.offer_created",
"marketplace.bid_placed",
"governance.proposal_created",
"governance.vote_cast"
]
for event in webhook.events:
if event not in valid_events:
raise HTTPException(400, f"Invalid event: {event}")
# Generate webhook secret if not provided
if not webhook.secret:
webhook.secret = secrets.token_urlsafe(32)
# Create webhook
webhook_id = secrets.token_urlsafe(16)
WEBHOOKS_DB[webhook_id] = {
"id": webhook_id,
"partner_id": partner["id"],
"url": webhook.url,
"events": webhook.events,
"secret": webhook.secret,
"status": "active",
"created_at": datetime.utcnow()
}
return WebhookResponse(
webhook_id=webhook_id,
url=webhook.url,
events=webhook.events,
status="active",
created_at=WEBHOOKS_DB[webhook_id]["created_at"]
)
@router.get("/partners/webhooks")
async def list_webhooks(
session: SessionDep,
api_key: str
) -> List[WebhookResponse]:
"""List partner webhooks"""
# Verify partner
partner = find_partner_by_api_key(api_key)
if not partner:
raise HTTPException(401, "Invalid API key")
# Get webhooks for partner
webhooks = []
for webhook in WEBHOOKS_DB.values():
if webhook["partner_id"] == partner["id"]:
webhooks.append(WebhookResponse(
webhook_id=webhook["id"],
url=webhook["url"],
events=webhook["events"],
status=webhook["status"],
created_at=webhook["created_at"]
))
return webhooks
@router.delete("/partners/webhooks/{webhook_id}")
async def delete_webhook(
webhook_id: str,
session: SessionDep,
api_key: str
) -> Dict[str, str]:
"""Delete a webhook"""
# Verify partner
partner = find_partner_by_api_key(api_key)
if not partner:
raise HTTPException(401, "Invalid API key")
# Find webhook
webhook = WEBHOOKS_DB.get(webhook_id)
if not webhook or webhook["partner_id"] != partner["id"]:
raise HTTPException(404, "Webhook not found")
# Delete webhook
del WEBHOOKS_DB[webhook_id]
return {"message": "Webhook deleted successfully"}
@router.get("/partners/analytics/usage")
async def get_usage_analytics(
session: SessionDep,
api_key: str,
period: str = "24h"
) -> Dict[str, Any]:
"""Get API usage analytics"""
# Verify partner
partner = find_partner_by_api_key(api_key)
if not partner:
raise HTTPException(401, "Invalid API key")
# Mock usage data (in production, query from analytics)
usage = {
"period": period,
"requests": {
"total": 15420,
"blocks": 5000,
"transactions": 8000,
"marketplace": 2000,
"analytics": 420
},
"rate_limit": {
"used": 15420,
"limit": partner["rate_limit"]["requests_per_hour"],
"percentage": 30.84
},
"errors": {
"4xx": 12,
"5xx": 3
},
"top_endpoints": [
{ "endpoint": "/blocks", "requests": 5000 },
{ "endpoint": "/transactions", "requests": 8000 },
{ "endpoint": "/marketplace/offers", "requests": 2000 }
]
}
return usage
# Helper functions
def verify_partner_api_key(partner_id: str, api_key: str) -> Optional[Dict[str, Any]]:
"""Verify partner credentials"""
partner = PARTNERS_DB.get(partner_id)
if not partner:
return None
# Check API key
if partner["api_key"] != api_key:
return None
return partner
def find_partner_by_api_key(api_key: str) -> Optional[Dict[str, Any]]:
"""Find partner by API key"""
for partner in PARTNERS_DB.values():
if partner["api_key"] == api_key:
return partner
return None
# Export the router
__all__ = ["router"]

View File

@@ -7,7 +7,7 @@ from fastapi import APIRouter, Depends, HTTPException, status, Header
from fastapi.responses import StreamingResponse
from ..deps import require_client_key
from ..models import JobCreate, JobView, JobResult
from ..schemas import JobCreate, JobView, JobResult
from ..models.services import (
ServiceType,
ServiceRequest,
@@ -18,7 +18,7 @@ from ..models.services import (
FFmpegRequest,
BlenderRequest,
)
from ..models.registry import ServiceRegistry, service_registry
# from ..models.registry import ServiceRegistry, service_registry
from ..services import JobService
from ..storage import SessionDep

View File

@@ -0,0 +1,236 @@
"""
User Management Router for AITBC
"""
from typing import Dict, Any, Optional
from fastapi import APIRouter, HTTPException, status, Depends
from sqlmodel import Session, select
import uuid
import time
import hashlib
from datetime import datetime, timedelta
from ..deps import get_session
from ..domain import User, Wallet
from ..schemas import UserCreate, UserLogin, UserProfile, UserBalance
router = APIRouter(tags=["users"])
# In-memory session storage for demo (use Redis in production)
user_sessions: Dict[str, Dict] = {}
def create_session_token(user_id: str) -> str:
"""Create a session token for a user"""
token_data = f"{user_id}:{int(time.time())}"
token = hashlib.sha256(token_data.encode()).hexdigest()
# Store session
user_sessions[token] = {
"user_id": user_id,
"created_at": int(time.time()),
"expires_at": int(time.time()) + 86400 # 24 hours
}
return token
def verify_session_token(token: str) -> Optional[str]:
"""Verify a session token and return user_id"""
if token not in user_sessions:
return None
session = user_sessions[token]
# Check if expired
if int(time.time()) > session["expires_at"]:
del user_sessions[token]
return None
return session["user_id"]
@router.post("/register", response_model=UserProfile)
async def register_user(
user_data: UserCreate,
session: Session = Depends(get_session)
) -> Dict[str, Any]:
"""Register a new user"""
# Check if user already exists
existing_user = session.exec(
select(User).where(User.email == user_data.email)
).first()
if existing_user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Email already registered"
)
# Create new user
user = User(
id=str(uuid.uuid4()),
email=user_data.email,
username=user_data.username,
created_at=datetime.utcnow(),
last_login=datetime.utcnow()
)
session.add(user)
session.commit()
session.refresh(user)
# Create wallet for user
wallet = Wallet(
user_id=user.id,
address=f"aitbc_{user.id[:8]}",
balance=0.0,
created_at=datetime.utcnow()
)
session.add(wallet)
session.commit()
# Create session token
token = create_session_token(user.id)
return {
"user_id": user.id,
"email": user.email,
"username": user.username,
"created_at": user.created_at.isoformat(),
"session_token": token
}
@router.post("/login", response_model=UserProfile)
async def login_user(
login_data: UserLogin,
session: Session = Depends(get_session)
) -> Dict[str, Any]:
"""Login user with wallet address"""
# For demo, we'll create or get user by wallet address
# In production, implement proper authentication
# Find user by wallet address
wallet = session.exec(
select(Wallet).where(Wallet.address == login_data.wallet_address)
).first()
if not wallet:
# Create new user for wallet
user = User(
id=str(uuid.uuid4()),
email=f"{login_data.wallet_address}@aitbc.local",
username=f"user_{login_data.wallet_address[-8:]}_{str(uuid.uuid4())[:8]}",
created_at=datetime.utcnow(),
last_login=datetime.utcnow()
)
session.add(user)
session.commit()
session.refresh(user)
# Create wallet
wallet = Wallet(
user_id=user.id,
address=login_data.wallet_address,
balance=0.0,
created_at=datetime.utcnow()
)
session.add(wallet)
session.commit()
else:
# Update last login
user = session.exec(
select(User).where(User.id == wallet.user_id)
).first()
user.last_login = datetime.utcnow()
session.commit()
# Create session token
token = create_session_token(user.id)
return {
"user_id": user.id,
"email": user.email,
"username": user.username,
"created_at": user.created_at.isoformat(),
"session_token": token
}
@router.get("/users/me", response_model=UserProfile)
async def get_current_user(
token: str,
session: Session = Depends(get_session)
) -> Dict[str, Any]:
"""Get current user profile"""
user_id = verify_session_token(token)
if not user_id:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or expired token"
)
user = session.get(User, user_id)
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
return {
"user_id": user.id,
"email": user.email,
"username": user.username,
"created_at": user.created_at.isoformat(),
"session_token": token
}
@router.get("/users/{user_id}/balance", response_model=UserBalance)
async def get_user_balance(
user_id: str,
session: Session = Depends(get_session)
) -> Dict[str, Any]:
"""Get user's AITBC balance"""
wallet = session.exec(
select(Wallet).where(Wallet.user_id == user_id)
).first()
if not wallet:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Wallet not found"
)
return {
"user_id": user_id,
"address": wallet.address,
"balance": wallet.balance,
"updated_at": wallet.updated_at.isoformat() if wallet.updated_at else None
}
@router.post("/logout")
async def logout_user(token: str) -> Dict[str, str]:
"""Logout user and invalidate session"""
if token in user_sessions:
del user_sessions[token]
return {"message": "Logged out successfully"}
@router.get("/users/{user_id}/transactions")
async def get_user_transactions(
user_id: str,
session: Session = Depends(get_session)
) -> Dict[str, Any]:
"""Get user's transaction history"""
# For demo, return empty list
# In production, query from transaction table
return {
"user_id": user_id,
"transactions": [],
"total": 0
}

View File

@@ -0,0 +1,333 @@
"""
ZK Applications Router - Privacy-preserving features for AITBC
"""
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from typing import Optional, Dict, Any, List
import hashlib
import secrets
from datetime import datetime
import json
from ..schemas import UserProfile
from ..storage import SessionDep
router = APIRouter(tags=["zk-applications"])
class ZKProofRequest(BaseModel):
"""Request for ZK proof generation"""
commitment: str = Field(..., description="Commitment to private data")
public_inputs: Dict[str, Any] = Field(default_factory=dict)
proof_type: str = Field(default="membership", description="Type of proof")
class ZKMembershipRequest(BaseModel):
"""Request to prove group membership privately"""
group_id: str = Field(..., description="Group to prove membership in")
nullifier: str = Field(..., description="Unique nullifier to prevent double-spending")
proof: str = Field(..., description="ZK-SNARK proof")
class PrivateBidRequest(BaseModel):
"""Submit a bid without revealing amount"""
auction_id: str = Field(..., description="Auction identifier")
bid_commitment: str = Field(..., description="Hash of bid amount + salt")
proof: str = Field(..., description="Proof that bid is within valid range")
class ZKComputationRequest(BaseModel):
"""Request to verify AI computation with privacy"""
job_id: str = Field(..., description="Job identifier")
result_hash: str = Field(..., description="Hash of computation result")
proof_of_execution: str = Field(..., description="ZK proof of correct execution")
public_inputs: Dict[str, Any] = Field(default_factory=dict)
@router.post("/zk/identity/commit")
async def create_identity_commitment(
user: UserProfile,
session: SessionDep,
salt: Optional[str] = None
) -> Dict[str, str]:
"""Create a privacy-preserving identity commitment"""
# Generate salt if not provided
if not salt:
salt = secrets.token_hex(16)
# Create commitment: H(email || salt)
commitment_input = f"{user.email}:{salt}"
commitment = hashlib.sha256(commitment_input.encode()).hexdigest()
return {
"commitment": commitment,
"salt": salt,
"user_id": user.user_id,
"created_at": datetime.utcnow().isoformat()
}
@router.post("/zk/membership/verify")
async def verify_group_membership(
request: ZKMembershipRequest,
session: SessionDep
) -> Dict[str, Any]:
"""
Verify that a user is a member of a group without revealing which user
Demo implementation - in production would use actual ZK-SNARKs
"""
# In a real implementation, this would:
# 1. Verify the ZK-SNARK proof
# 2. Check the nullifier hasn't been used before
# 3. Confirm membership in the group's Merkle tree
# For demo, we'll simulate verification
group_members = {
"miners": ["user1", "user2", "user3"],
"clients": ["user4", "user5", "user6"],
"developers": ["user7", "user8", "user9"]
}
if request.group_id not in group_members:
raise HTTPException(status_code=404, detail="Group not found")
# Simulate proof verification
is_valid = len(request.proof) > 10 and len(request.nullifier) == 64
if not is_valid:
raise HTTPException(status_code=400, detail="Invalid proof")
return {
"group_id": request.group_id,
"verified": True,
"nullifier": request.nullifier,
"timestamp": datetime.utcnow().isoformat()
}
@router.post("/zk/marketplace/private-bid")
async def submit_private_bid(
request: PrivateBidRequest,
session: SessionDep
) -> Dict[str, str]:
"""
Submit a bid to the marketplace without revealing the amount
Uses commitment scheme to hide bid amount while allowing verification
"""
# In production, would verify:
# 1. The ZK proof shows the bid is within valid range
# 2. The commitment matches the hidden bid amount
# 3. User has sufficient funds
bid_id = f"bid_{secrets.token_hex(8)}"
return {
"bid_id": bid_id,
"auction_id": request.auction_id,
"commitment": request.bid_commitment,
"status": "submitted",
"timestamp": datetime.utcnow().isoformat()
}
@router.get("/zk/marketplace/auctions/{auction_id}/bids")
async def get_auction_bids(
auction_id: str,
session: SessionDep,
reveal: bool = False
) -> Dict[str, Any]:
"""
Get bids for an auction
If reveal=False, returns only commitments (privacy-preserving)
If reveal=True, reveals actual bid amounts (after auction ends)
"""
# Mock data - in production would query database
mock_bids = [
{
"bid_id": "bid_12345678",
"commitment": "0x1a2b3c4d5e6f...",
"timestamp": "2025-12-28T10:00:00Z"
},
{
"bid_id": "bid_87654321",
"commitment": "0x9f8e7d6c5b4a...",
"timestamp": "2025-12-28T10:05:00Z"
}
]
if reveal:
# In production, would use pre-images to reveal amounts
for bid in mock_bids:
bid["amount"] = 100.0 if bid["bid_id"] == "bid_12345678" else 150.0
return {
"auction_id": auction_id,
"bids": mock_bids,
"revealed": reveal,
"total_bids": len(mock_bids)
}
@router.post("/zk/computation/verify")
async def verify_computation_proof(
request: ZKComputationRequest,
session: SessionDep
) -> Dict[str, Any]:
"""
Verify that an AI computation was performed correctly without revealing inputs
"""
# In production, would verify actual ZK-SNARK proof
# For demo, simulate verification
verification_result = {
"job_id": request.job_id,
"verified": len(request.proof_of_execution) > 20,
"result_hash": request.result_hash,
"public_inputs": request.public_inputs,
"verification_key": "demo_vk_12345",
"timestamp": datetime.utcnow().isoformat()
}
return verification_result
@router.post("/zk/receipt/attest")
async def create_private_receipt(
job_id: str,
user_address: str,
computation_result: str,
privacy_level: str = "basic"
) -> Dict[str, Any]:
"""
Create a privacy-preserving receipt attestation
"""
# Generate commitment for private data
salt = secrets.token_hex(16)
private_data = f"{job_id}:{computation_result}:{salt}"
commitment = hashlib.sha256(private_data.encode()).hexdigest()
# Create public receipt
receipt = {
"job_id": job_id,
"user_address": user_address,
"commitment": commitment,
"privacy_level": privacy_level,
"timestamp": datetime.utcnow().isoformat(),
"verified": True
}
return receipt
@router.get("/zk/anonymity/sets")
async def get_anonymity_sets() -> Dict[str, Any]:
"""Get available anonymity sets for privacy operations"""
return {
"sets": {
"miners": {
"size": 100,
"description": "Registered GPU miners",
"type": "merkle_tree"
},
"clients": {
"size": 500,
"description": "Active clients",
"type": "merkle_tree"
},
"transactions": {
"size": 1000,
"description": "Recent transactions",
"type": "ring_signature"
}
},
"min_anonymity": 3,
"recommended_sets": ["miners", "clients"]
}
@router.post("/zk/stealth/address")
async def generate_stealth_address(
recipient_public_key: str,
sender_random: Optional[str] = None
) -> Dict[str, str]:
"""
Generate a stealth address for private payments
Demo implementation
"""
if not sender_random:
sender_random = secrets.token_hex(16)
# In production, use elliptic curve diffie-hellman
shared_secret = hashlib.sha256(
f"{recipient_public_key}:{sender_random}".encode()
).hexdigest()
stealth_address = hashlib.sha256(
f"{shared_secret}:{recipient_public_key}".encode()
).hexdigest()[:40]
return {
"stealth_address": f"0x{stealth_address}",
"shared_secret_hash": shared_secret,
"ephemeral_key": sender_random,
"view_key": f"0x{hashlib.sha256(shared_secret.encode()).hexdigest()[:40]}"
}
@router.get("/zk/status")
async def get_zk_status() -> Dict[str, Any]:
"""Get the status of ZK features in AITBC"""
# Check if ZK service is enabled
from ..services.zk_proofs import ZKProofService
zk_service = ZKProofService()
return {
"zk_features": {
"identity_commitments": "active",
"group_membership": "demo",
"private_bidding": "demo",
"computation_proofs": "demo",
"stealth_addresses": "demo",
"receipt_attestation": "active",
"circuits_compiled": zk_service.enabled,
"trusted_setup": "completed"
},
"supported_proof_types": [
"membership",
"bid_range",
"computation",
"identity",
"receipt"
],
"privacy_levels": [
"basic", # Hash-based commitments
"medium", # Simple ZK proofs
"maximum" # Full ZK-SNARKs (when circuits are compiled)
],
"circuit_status": {
"receipt": "compiled",
"membership": "not_compiled",
"bid": "not_compiled"
},
"next_steps": [
"Compile additional circuits (membership, bid)",
"Deploy verification contracts",
"Integrate with marketplace",
"Enable recursive proofs"
],
"zkey_files": {
"receipt_simple_0001.zkey": "available",
"receipt_simple.wasm": "available",
"verification_key.json": "available"
}
}