```
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:
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
151
apps/coordinator-api/src/app/routers/exchange.py
Normal file
151
apps/coordinator-api/src/app/routers/exchange.py
Normal 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
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
|
||||
from ..models import (
|
||||
from ..schemas import (
|
||||
BlockListResponse,
|
||||
TransactionListResponse,
|
||||
AddressListResponse,
|
||||
|
||||
381
apps/coordinator-api/src/app/routers/governance.py
Normal file
381
apps/coordinator-api/src/app/routers/governance.py
Normal 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"]
|
||||
@@ -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
|
||||
|
||||
132
apps/coordinator-api/src/app/routers/marketplace_offers.py
Normal file
132
apps/coordinator-api/src/app/routers/marketplace_offers.py
Normal 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
|
||||
@@ -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
|
||||
|
||||
296
apps/coordinator-api/src/app/routers/partners.py
Normal file
296
apps/coordinator-api/src/app/routers/partners.py
Normal 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"]
|
||||
@@ -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
|
||||
|
||||
|
||||
236
apps/coordinator-api/src/app/routers/users.py
Normal file
236
apps/coordinator-api/src/app/routers/users.py
Normal 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
|
||||
}
|
||||
333
apps/coordinator-api/src/app/routers/zk_applications.py
Normal file
333
apps/coordinator-api/src/app/routers/zk_applications.py
Normal 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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user