
Features implemented: - User authentication with JWT tokens and role-based access (developer/buyer) - Blockchain wallet linking and management with Ethereum integration - Carbon project creation and management for developers - Marketplace for browsing and purchasing carbon offsets - Transaction tracking with blockchain integration - Database models for users, projects, offsets, and transactions - Comprehensive API with authentication, wallet, project, and trading endpoints - Health check endpoint and platform information - SQLite database with Alembic migrations - Full API documentation with OpenAPI/Swagger Technical stack: - FastAPI with Python - SQLAlchemy ORM with SQLite - Web3.py for blockchain integration - JWT authentication with bcrypt - CORS enabled for frontend integration - Comprehensive error handling and validation Environment variables required: - SECRET_KEY (JWT secret) - BLOCKCHAIN_RPC_URL (optional, defaults to localhost)
215 lines
6.6 KiB
Python
215 lines
6.6 KiB
Python
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from sqlalchemy.orm import Session
|
|
from sqlalchemy import desc, func
|
|
from typing import Optional
|
|
from datetime import datetime
|
|
from app.db.session import get_db
|
|
from app.core.deps import get_current_user, get_current_buyer
|
|
from app.models.user import User
|
|
from app.models.carbon_project import CarbonProject
|
|
from app.models.carbon_offset import CarbonOffset
|
|
from app.models.transaction import Transaction
|
|
from app.schemas.transaction import (
|
|
PurchaseRequest,
|
|
TransactionResponse,
|
|
TransactionListResponse
|
|
)
|
|
from app.services.blockchain import blockchain_service
|
|
import uuid
|
|
|
|
router = APIRouter()
|
|
|
|
@router.post("/purchase", response_model=TransactionResponse)
|
|
def purchase_carbon_offsets(
|
|
purchase: PurchaseRequest,
|
|
current_user: User = Depends(get_current_buyer),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Purchase carbon offsets from a project (Buyer only)"""
|
|
|
|
# Check if user has wallet linked
|
|
if not current_user.wallet_address:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail="Wallet must be linked to purchase carbon offsets"
|
|
)
|
|
|
|
# Get project
|
|
project = db.query(CarbonProject).filter(
|
|
CarbonProject.id == purchase.project_id,
|
|
CarbonProject.is_active == True,
|
|
CarbonProject.verification_status == "verified"
|
|
).first()
|
|
|
|
if not project:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail="Project not found or not verified"
|
|
)
|
|
|
|
# Check if enough credits are available
|
|
available_credits = project.total_credits_available - project.credits_sold
|
|
if purchase.quantity > available_credits:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"Not enough credits available. Available: {available_credits}"
|
|
)
|
|
|
|
# Get available offset
|
|
offset = db.query(CarbonOffset).filter(
|
|
CarbonOffset.project_id == purchase.project_id,
|
|
CarbonOffset.status == "available"
|
|
).first()
|
|
|
|
if not offset:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail="No available carbon offsets for this project"
|
|
)
|
|
|
|
# Calculate total amount
|
|
total_amount = purchase.quantity * project.price_per_credit
|
|
|
|
# Create transaction record
|
|
transaction_hash = f"tx_{uuid.uuid4().hex[:16]}"
|
|
|
|
db_transaction = Transaction(
|
|
transaction_hash=transaction_hash,
|
|
quantity=purchase.quantity,
|
|
price_per_credit=project.price_per_credit,
|
|
total_amount=total_amount,
|
|
buyer_id=current_user.id,
|
|
offset_id=offset.id,
|
|
status="pending"
|
|
)
|
|
|
|
db.add(db_transaction)
|
|
|
|
# Update project credits sold
|
|
project.credits_sold += purchase.quantity
|
|
|
|
# Update offset quantity or status
|
|
if offset.quantity <= purchase.quantity:
|
|
offset.status = "sold"
|
|
else:
|
|
offset.quantity -= purchase.quantity
|
|
# Create new offset for remaining quantity
|
|
new_offset = CarbonOffset(
|
|
serial_number=f"CO{project.id}-{offset.quantity}",
|
|
vintage_year=offset.vintage_year,
|
|
quantity=purchase.quantity,
|
|
status="sold",
|
|
project_id=project.id
|
|
)
|
|
db.add(new_offset)
|
|
|
|
try:
|
|
db.commit()
|
|
db.refresh(db_transaction)
|
|
|
|
# In a real implementation, you would integrate with actual blockchain here
|
|
# For now, we'll simulate transaction confirmation
|
|
db_transaction.status = "confirmed"
|
|
db_transaction.confirmed_at = datetime.utcnow()
|
|
db_transaction.block_number = 12345678 # Simulated block number
|
|
db_transaction.gas_used = 21000 # Simulated gas usage
|
|
|
|
db.commit()
|
|
db.refresh(db_transaction)
|
|
|
|
return db_transaction
|
|
|
|
except Exception as e:
|
|
db.rollback()
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail=f"Transaction failed: {str(e)}"
|
|
)
|
|
|
|
@router.get("/my-transactions", response_model=TransactionListResponse)
|
|
def get_my_transactions(
|
|
page: int = Query(1, ge=1),
|
|
page_size: int = Query(10, ge=1, le=100),
|
|
status: Optional[str] = None,
|
|
current_user: User = Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Get current user's transactions"""
|
|
|
|
query = db.query(Transaction).filter(Transaction.buyer_id == current_user.id)
|
|
|
|
if status:
|
|
query = query.filter(Transaction.status == status)
|
|
|
|
total = query.count()
|
|
|
|
transactions = query.order_by(desc(Transaction.created_at)).offset(
|
|
(page - 1) * page_size
|
|
).limit(page_size).all()
|
|
|
|
return TransactionListResponse(
|
|
transactions=transactions,
|
|
total=total,
|
|
page=page,
|
|
page_size=page_size
|
|
)
|
|
|
|
@router.get("/transactions/{transaction_id}", response_model=TransactionResponse)
|
|
def get_transaction(
|
|
transaction_id: int,
|
|
current_user: User = Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Get a specific transaction"""
|
|
|
|
transaction = db.query(Transaction).filter(
|
|
Transaction.id == transaction_id,
|
|
Transaction.buyer_id == current_user.id
|
|
).first()
|
|
|
|
if not transaction:
|
|
raise HTTPException(status_code=404, detail="Transaction not found")
|
|
|
|
return transaction
|
|
|
|
@router.get("/marketplace", response_model=dict)
|
|
def get_marketplace_stats(db: Session = Depends(get_db)):
|
|
"""Get marketplace statistics"""
|
|
|
|
# Total active projects
|
|
total_projects = db.query(CarbonProject).filter(
|
|
CarbonProject.is_active == True
|
|
).count()
|
|
|
|
# Total verified projects
|
|
verified_projects = db.query(CarbonProject).filter(
|
|
CarbonProject.is_active == True,
|
|
CarbonProject.verification_status == "verified"
|
|
).count()
|
|
|
|
# Total credits available
|
|
total_credits = db.query(CarbonProject).filter(
|
|
CarbonProject.is_active == True
|
|
).with_entities(
|
|
func.sum(CarbonProject.total_credits_available - CarbonProject.credits_sold)
|
|
).scalar() or 0
|
|
|
|
# Total transactions
|
|
total_transactions = db.query(Transaction).filter(
|
|
Transaction.status == "confirmed"
|
|
).count()
|
|
|
|
# Total volume traded
|
|
total_volume = db.query(Transaction).filter(
|
|
Transaction.status == "confirmed"
|
|
).with_entities(
|
|
func.sum(Transaction.total_amount)
|
|
).scalar() or 0
|
|
|
|
return {
|
|
"total_projects": total_projects,
|
|
"verified_projects": verified_projects,
|
|
"total_credits_available": total_credits,
|
|
"total_transactions": total_transactions,
|
|
"total_volume_traded": total_volume
|
|
} |