Build complete blockchain-enabled carbon offset trading platform

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)
This commit is contained in:
Automated Action 2025-06-20 13:45:14 +00:00
parent ff7498dd5c
commit e122f16dea
31 changed files with 1877 additions and 2 deletions

202
README.md
View File

@ -1,3 +1,201 @@
# FastAPI Application
# Carbon Offset Trading Platform
This is a FastAPI application bootstrapped by BackendIM, the AI-powered backend generation platform.
A blockchain-enabled carbon offset trading platform that connects project developers with willing buyers. This platform allows project developers to list their carbon offset projects and enables buyers to purchase verified carbon credits through blockchain transactions.
## Features
### For Project Developers
- Register as a project developer
- Create and manage carbon offset projects
- Upload project verification documents
- Track project performance and sales
- Integrate with blockchain for tokenization
### For Buyers
- Register as a buyer
- Browse verified carbon offset projects
- Purchase carbon credits
- Link blockchain wallets for transactions
- Track purchase history and carbon offset ownership
### Blockchain Integration
- Wallet linking support (Ethereum-compatible)
- Smart contract integration for carbon credit tokens
- Transaction verification and tracking
- Secure blockchain-based ownership records
## Tech Stack
- **Backend**: FastAPI (Python)
- **Database**: SQLite with SQLAlchemy ORM
- **Authentication**: JWT tokens with bcrypt password hashing
- **Blockchain**: Web3.py for Ethereum integration
- **Database Migrations**: Alembic
- **API Documentation**: OpenAPI/Swagger
## Project Structure
```
├── main.py # FastAPI application entry point
├── requirements.txt # Python dependencies
├── alembic.ini # Database migration configuration
├── openapi.json # API specification
├── app/
│ ├── api/ # API endpoints
│ │ ├── auth.py # Authentication endpoints
│ │ ├── wallet.py # Wallet management endpoints
│ │ ├── projects.py # Project management endpoints
│ │ └── trading.py # Trading and marketplace endpoints
│ ├── core/ # Core functionality
│ │ ├── security.py # Authentication and security
│ │ └── deps.py # Dependency injection
│ ├── db/ # Database configuration
│ │ ├── base.py # SQLAlchemy base
│ │ └── session.py # Database session management
│ ├── models/ # Database models
│ │ ├── user.py # User model
│ │ ├── carbon_project.py # Carbon project model
│ │ ├── carbon_offset.py # Carbon offset model
│ │ └── transaction.py # Transaction model
│ ├── schemas/ # Pydantic schemas
│ │ ├── user.py # User schemas
│ │ ├── carbon_project.py # Project schemas
│ │ └── transaction.py # Transaction schemas
│ └── services/ # Business logic services
│ ├── blockchain.py # Blockchain integration
│ └── wallet.py # Wallet management
└── alembic/ # Database migrations
└── versions/ # Migration files
```
## Installation
1. Install dependencies:
```bash
pip install -r requirements.txt
```
2. Run database migrations:
```bash
alembic upgrade head
```
3. Set required environment variables:
```bash
export SECRET_KEY="your-secret-key-here"
export BLOCKCHAIN_RPC_URL="https://your-ethereum-rpc-url" # Optional, defaults to localhost
```
## Running the Application
Start the development server:
```bash
uvicorn main:app --host 0.0.0.0 --port 8000 --reload
```
The application will be available at:
- **API**: http://localhost:8000
- **Documentation**: http://localhost:8000/docs
- **Alternative Docs**: http://localhost:8000/redoc
- **Health Check**: http://localhost:8000/health
## Environment Variables
| Variable | Description | Required | Default |
|----------|-------------|----------|---------|
| `SECRET_KEY` | JWT token secret key | Yes | - |
| `BLOCKCHAIN_RPC_URL` | Ethereum RPC endpoint | No | http://localhost:8545 |
## API Endpoints
### Authentication
- `POST /auth/register` - Register new user (developer or buyer)
- `POST /auth/login` - User login
### Wallet Management
- `POST /wallet/link` - Link blockchain wallet to user account
- `DELETE /wallet/unlink` - Unlink wallet from user account
- `GET /wallet/info` - Get wallet information and balance
- `POST /wallet/generate-test-wallet` - Generate test wallet for development
### Project Management (Developers)
- `POST /projects/` - Create new carbon offset project
- `GET /projects/my-projects` - Get developer's projects
- `PUT /projects/{project_id}` - Update project
- `DELETE /projects/{project_id}` - Delete project
### Marketplace & Trading
- `GET /projects/` - Browse all available projects
- `GET /projects/{project_id}` - Get project details
- `POST /trading/purchase` - Purchase carbon offsets
- `GET /trading/my-transactions` - Get user's transactions
- `GET /trading/marketplace` - Get marketplace statistics
### System
- `GET /` - Platform information
- `GET /health` - Health check endpoint
## Database Schema
### Users
- User authentication and profile information
- Wallet linking for blockchain integration
- User types: "developer" or "buyer"
### Carbon Projects
- Project details and metadata
- Verification status and documents
- Credit availability and pricing
- Blockchain contract information
### Carbon Offsets
- Individual carbon credit tokens
- Serial numbers and vintage years
- Blockchain token IDs and hashes
- Ownership tracking
### Transactions
- Purchase records and blockchain transactions
- Transaction status and confirmation details
- Gas usage and block information
## Development
### Database Migrations
Create a new migration:
```bash
alembic revision --autogenerate -m "Description of changes"
```
Apply migrations:
```bash
alembic upgrade head
```
### Testing Wallet Integration
Use the test wallet generation endpoint to create wallets for development:
```bash
curl -X POST http://localhost:8000/wallet/generate-test-wallet
```
## Security Features
- JWT-based authentication
- Password hashing with bcrypt
- Role-based access control (developer/buyer)
- Blockchain wallet verification
- Transaction signing and verification
## Contributing
1. Follow the existing code structure and patterns
2. Use type hints for all functions and methods
3. Add appropriate error handling and validation
4. Update documentation for any API changes
5. Test wallet integration thoroughly
## License
This project is part of a carbon offset trading platform implementation.

97
alembic.ini Normal file
View File

@ -0,0 +1,97 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts
script_location = alembic
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory.
prepend_sys_path = .
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python-dateutil library that can be
# installed by adding `alembic[tz]` to the pip requirements
# string value is passed to dateutil.tz.gettz()
# leave blank for localtime
# timezone =
# max length of characters to apply to the
# "slug" field
# max_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version path separator; As mentioned above, this is the character used to split
# version_locations. The default within new alembic.ini files is "os", which uses
# os.pathsep. If this key is omitted entirely, it falls back to the legacy
# behavior of splitting on spaces and/or commas.
# Valid values for version_path_separator are:
#
# version_path_separator = :
# version_path_separator = ;
# version_path_separator = space
version_path_separator = os
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
sqlalchemy.url = sqlite:////app/storage/db/db.sqlite
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

87
alembic/env.py Normal file
View File

@ -0,0 +1,87 @@
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
import sys
import os
from pathlib import Path
# Add the project root to Python path
sys.path.append(str(Path(__file__).parent.parent))
# Import models
from app.db.base import Base
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
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
if config.config_file_name is not None:
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
target_metadata = Base.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection, target_metadata=target_metadata
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

24
alembic/script.py.mako Normal file
View File

@ -0,0 +1,24 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade() -> None:
${upgrades if upgrades else "pass"}
def downgrade() -> None:
${downgrades if downgrades else "pass"}

View File

@ -0,0 +1,118 @@
"""Initial migration
Revision ID: 001
Revises:
Create Date: 2024-01-01 12:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '001'
down_revision = None
branch_labels = None
depends_on = None
def upgrade() -> None:
# Create users table
op.create_table('users',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('email', sa.String(), nullable=False),
sa.Column('hashed_password', sa.String(), nullable=False),
sa.Column('full_name', sa.String(), nullable=False),
sa.Column('user_type', sa.String(), nullable=False),
sa.Column('is_active', sa.Boolean(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('wallet_address', sa.String(), nullable=True),
sa.Column('wallet_public_key', sa.Text(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_users_id'), 'users', ['id'], unique=False)
op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True)
op.create_index(op.f('ix_users_wallet_address'), 'users', ['wallet_address'], unique=True)
# Create carbon_projects table
op.create_table('carbon_projects',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('title', sa.String(), nullable=False),
sa.Column('description', sa.Text(), nullable=False),
sa.Column('location', sa.String(), nullable=False),
sa.Column('project_type', sa.String(), nullable=False),
sa.Column('methodology', sa.String(), nullable=False),
sa.Column('total_credits_available', sa.Integer(), nullable=False),
sa.Column('credits_sold', sa.Integer(), nullable=True),
sa.Column('price_per_credit', sa.Float(), nullable=False),
sa.Column('start_date', sa.DateTime(), nullable=False),
sa.Column('end_date', sa.DateTime(), nullable=False),
sa.Column('verification_status', sa.String(), nullable=True),
sa.Column('verification_document_url', sa.String(), nullable=True),
sa.Column('is_active', sa.Boolean(), nullable=True),
sa.Column('contract_address', sa.String(), nullable=True),
sa.Column('token_id', sa.String(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('developer_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['developer_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_carbon_projects_id'), 'carbon_projects', ['id'], unique=False)
# Create carbon_offsets table
op.create_table('carbon_offsets',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('serial_number', sa.String(), nullable=False),
sa.Column('vintage_year', sa.Integer(), nullable=False),
sa.Column('quantity', sa.Integer(), nullable=False),
sa.Column('status', sa.String(), nullable=True),
sa.Column('token_id', sa.String(), nullable=True),
sa.Column('blockchain_hash', sa.String(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('project_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['project_id'], ['carbon_projects.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_carbon_offsets_id'), 'carbon_offsets', ['id'], unique=False)
op.create_index(op.f('ix_carbon_offsets_serial_number'), 'carbon_offsets', ['serial_number'], unique=True)
op.create_index(op.f('ix_carbon_offsets_token_id'), 'carbon_offsets', ['token_id'], unique=True)
# Create transactions table
op.create_table('transactions',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('transaction_hash', sa.String(), nullable=False),
sa.Column('quantity', sa.Integer(), nullable=False),
sa.Column('price_per_credit', sa.Float(), nullable=False),
sa.Column('total_amount', sa.Float(), nullable=False),
sa.Column('status', sa.String(), nullable=True),
sa.Column('block_number', sa.Integer(), nullable=True),
sa.Column('gas_used', sa.Integer(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('confirmed_at', sa.DateTime(), nullable=True),
sa.Column('buyer_id', sa.Integer(), nullable=False),
sa.Column('offset_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['buyer_id'], ['users.id'], ),
sa.ForeignKeyConstraint(['offset_id'], ['carbon_offsets.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_transactions_id'), 'transactions', ['id'], unique=False)
op.create_index(op.f('ix_transactions_transaction_hash'), 'transactions', ['transaction_hash'], unique=True)
def downgrade() -> None:
op.drop_index(op.f('ix_transactions_transaction_hash'), table_name='transactions')
op.drop_index(op.f('ix_transactions_id'), table_name='transactions')
op.drop_table('transactions')
op.drop_index(op.f('ix_carbon_offsets_token_id'), table_name='carbon_offsets')
op.drop_index(op.f('ix_carbon_offsets_serial_number'), table_name='carbon_offsets')
op.drop_index(op.f('ix_carbon_offsets_id'), table_name='carbon_offsets')
op.drop_table('carbon_offsets')
op.drop_index(op.f('ix_carbon_projects_id'), table_name='carbon_projects')
op.drop_table('carbon_projects')
op.drop_index(op.f('ix_users_wallet_address'), table_name='users')
op.drop_index(op.f('ix_users_email'), table_name='users')
op.drop_index(op.f('ix_users_id'), table_name='users')
op.drop_table('users')

1
app/__init__.py Normal file
View File

@ -0,0 +1 @@
# Carbon Offset Trading Platform

1
app/api/__init__.py Normal file
View File

@ -0,0 +1 @@
# API modules

60
app/api/auth.py Normal file
View File

@ -0,0 +1,60 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app.db.session import get_db
from app.models.user import User
from app.schemas.user import UserCreate, UserLogin, UserResponse, Token
from app.core.security import verify_password, get_password_hash, create_access_token
router = APIRouter()
@router.post("/register", response_model=UserResponse)
def register(user: UserCreate, db: Session = Depends(get_db)):
# Check if user already exists
existing_user = db.query(User).filter(User.email == user.email).first()
if existing_user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Email already registered"
)
# Validate user type
if user.user_type not in ["developer", "buyer"]:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid user type. Must be 'developer' or 'buyer'"
)
# Create new user
hashed_password = get_password_hash(user.password)
db_user = User(
email=user.email,
hashed_password=hashed_password,
full_name=user.full_name,
user_type=user.user_type
)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
@router.post("/login", response_model=Token)
def login(user_credentials: UserLogin, db: Session = Depends(get_db)):
user = db.query(User).filter(User.email == user_credentials.email).first()
if not user or not verify_password(user_credentials.password, user.hashed_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect email or password",
headers={"WWW-Authenticate": "Bearer"},
)
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Inactive user"
)
access_token = create_access_token(subject=user.id)
return {"access_token": access_token, "token_type": "bearer"}

175
app/api/projects.py Normal file
View File

@ -0,0 +1,175 @@
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from sqlalchemy import desc
from typing import Optional
from app.db.session import get_db
from app.core.deps import get_current_user, get_current_developer
from app.models.user import User
from app.models.carbon_project import CarbonProject
from app.models.carbon_offset import CarbonOffset
from app.schemas.carbon_project import (
CarbonProjectCreate,
CarbonProjectResponse,
CarbonProjectUpdate,
CarbonProjectListResponse
)
router = APIRouter()
@router.post("/", response_model=CarbonProjectResponse)
def create_project(
project: CarbonProjectCreate,
current_user: User = Depends(get_current_developer),
db: Session = Depends(get_db)
):
"""Create a new carbon offset project (Developer only)"""
# Validate project dates
if project.start_date >= project.end_date:
raise HTTPException(
status_code=400,
detail="Start date must be before end date"
)
# Create project
db_project = CarbonProject(
**project.dict(),
developer_id=current_user.id
)
db.add(db_project)
db.commit()
db.refresh(db_project)
# Create initial carbon offsets
db_offset = CarbonOffset(
serial_number=f"CO{db_project.id}-{project.total_credits_available}",
vintage_year=project.start_date.year,
quantity=project.total_credits_available,
project_id=db_project.id
)
db.add(db_offset)
db.commit()
return db_project
@router.get("/", response_model=CarbonProjectListResponse)
def list_projects(
page: int = Query(1, ge=1),
page_size: int = Query(10, ge=1, le=100),
project_type: Optional[str] = None,
verification_status: Optional[str] = None,
db: Session = Depends(get_db)
):
"""List all active carbon offset projects"""
query = db.query(CarbonProject).filter(CarbonProject.is_active == True)
if project_type:
query = query.filter(CarbonProject.project_type == project_type)
if verification_status:
query = query.filter(CarbonProject.verification_status == verification_status)
# Get total count
total = query.count()
# Apply pagination
projects = query.order_by(desc(CarbonProject.created_at)).offset(
(page - 1) * page_size
).limit(page_size).all()
return CarbonProjectListResponse(
projects=projects,
total=total,
page=page,
page_size=page_size
)
@router.get("/my-projects", response_model=CarbonProjectListResponse)
def get_my_projects(
page: int = Query(1, ge=1),
page_size: int = Query(10, ge=1, le=100),
current_user: User = Depends(get_current_developer),
db: Session = Depends(get_db)
):
"""Get projects created by the current developer"""
query = db.query(CarbonProject).filter(CarbonProject.developer_id == current_user.id)
total = query.count()
projects = query.order_by(desc(CarbonProject.created_at)).offset(
(page - 1) * page_size
).limit(page_size).all()
return CarbonProjectListResponse(
projects=projects,
total=total,
page=page,
page_size=page_size
)
@router.get("/{project_id}", response_model=CarbonProjectResponse)
def get_project(project_id: int, db: Session = Depends(get_db)):
"""Get a specific project by ID"""
project = db.query(CarbonProject).filter(
CarbonProject.id == project_id,
CarbonProject.is_active == True
).first()
if not project:
raise HTTPException(status_code=404, detail="Project not found")
return project
@router.put("/{project_id}", response_model=CarbonProjectResponse)
def update_project(
project_id: int,
project_update: CarbonProjectUpdate,
current_user: User = Depends(get_current_developer),
db: Session = Depends(get_db)
):
"""Update a project (Developer only - own projects)"""
project = db.query(CarbonProject).filter(
CarbonProject.id == project_id,
CarbonProject.developer_id == current_user.id
).first()
if not project:
raise HTTPException(status_code=404, detail="Project not found")
# Update project fields
update_data = project_update.dict(exclude_unset=True)
for field, value in update_data.items():
setattr(project, field, value)
db.commit()
db.refresh(project)
return project
@router.delete("/{project_id}")
def delete_project(
project_id: int,
current_user: User = Depends(get_current_developer),
db: Session = Depends(get_db)
):
"""Delete a project (Developer only - own projects)"""
project = db.query(CarbonProject).filter(
CarbonProject.id == project_id,
CarbonProject.developer_id == current_user.id
).first()
if not project:
raise HTTPException(status_code=404, detail="Project not found")
# Soft delete - mark as inactive
project.is_active = False
db.commit()
return {"message": "Project deleted successfully"}

215
app/api/trading.py Normal file
View File

@ -0,0 +1,215 @@
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
}

56
app/api/wallet.py Normal file
View File

@ -0,0 +1,56 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from app.db.session import get_db
from app.core.deps import get_current_user
from app.models.user import User
from app.schemas.user import WalletLinkRequest, WalletResponse
from app.services.wallet import wallet_service
router = APIRouter()
@router.post("/link", response_model=WalletResponse)
def link_wallet(
wallet_request: WalletLinkRequest,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
result = wallet_service.link_wallet(
db=db,
user_id=current_user.id,
wallet_address=wallet_request.wallet_address
)
if not result["success"]:
raise HTTPException(status_code=400, detail=result["message"])
return WalletResponse(**result)
@router.delete("/unlink", response_model=WalletResponse)
def unlink_wallet(
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
result = wallet_service.unlink_wallet(db=db, user_id=current_user.id)
if not result["success"]:
raise HTTPException(status_code=400, detail=result["message"])
return WalletResponse(**result)
@router.get("/info", response_model=WalletResponse)
def get_wallet_info(
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
result = wallet_service.get_wallet_info(db=db, user_id=current_user.id)
if not result["success"]:
raise HTTPException(status_code=400, detail=result["message"])
return WalletResponse(**result)
@router.post("/generate-test-wallet")
def generate_test_wallet():
"""Generate a test wallet for development purposes"""
result = wallet_service.generate_test_wallet()
return result

1
app/core/__init__.py Normal file
View File

@ -0,0 +1 @@
# Core modules

57
app/core/deps.py Normal file
View File

@ -0,0 +1,57 @@
from typing import Generator, Optional
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.orm import Session
from app.db.session import get_db
from app.core.security import verify_token
from app.models.user import User
security = HTTPBearer()
def get_current_user(
db: Session = Depends(get_db),
credentials: HTTPAuthorizationCredentials = Depends(security)
) -> User:
token = credentials.credentials
user_id = verify_token(token)
if user_id is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
headers={"WWW-Authenticate": "Bearer"},
)
user = db.query(User).filter(User.id == int(user_id)).first()
if user is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Inactive user"
)
return user
def get_current_developer(
current_user: User = Depends(get_current_user)
) -> User:
if current_user.user_type != "developer":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied: Developer role required"
)
return current_user
def get_current_buyer(
current_user: User = Depends(get_current_user)
) -> User:
if current_user.user_type != "buyer":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied: Buyer role required"
)
return current_user

39
app/core/security.py Normal file
View File

@ -0,0 +1,39 @@
from datetime import datetime, timedelta
from typing import Any, Union, Optional
from jose import jwt
from passlib.context import CryptContext
import os
# Password hashing
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# JWT settings
SECRET_KEY = os.getenv("SECRET_KEY", "your-secret-key-change-in-production")
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
def create_access_token(
subject: Union[str, Any], expires_delta: timedelta = None
) -> str:
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(
minutes=ACCESS_TOKEN_EXPIRE_MINUTES
)
to_encode = {"exp": expire, "sub": str(subject)}
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
return pwd_context.hash(password)
def verify_token(token: str) -> Optional[str]:
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
return payload.get("sub")
except jwt.JWTError:
return None

3
app/db/base.py Normal file
View File

@ -0,0 +1,3 @@
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()

24
app/db/session.py Normal file
View File

@ -0,0 +1,24 @@
from pathlib import Path
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.db.base import Base
DB_DIR = Path("/app") / "storage" / "db"
DB_DIR.mkdir(parents=True, exist_ok=True)
SQLALCHEMY_DATABASE_URL = f"sqlite:///{DB_DIR}/db.sqlite"
engine = create_engine(
SQLALCHEMY_DATABASE_URL,
connect_args={"check_same_thread": False}
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()

6
app/models/__init__.py Normal file
View File

@ -0,0 +1,6 @@
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
__all__ = ["User", "CarbonProject", "CarbonOffset", "Transaction"]

View File

@ -0,0 +1,29 @@
from sqlalchemy import Column, Integer, String, DateTime, Boolean, Text, Float, ForeignKey
from sqlalchemy.orm import relationship
from datetime import datetime
from app.db.base import Base
class CarbonOffset(Base):
__tablename__ = "carbon_offsets"
id = Column(Integer, primary_key=True, index=True)
serial_number = Column(String, unique=True, nullable=False)
vintage_year = Column(Integer, nullable=False)
quantity = Column(Integer, nullable=False) # Number of credits
status = Column(String, default="available") # "available", "sold", "retired"
# Blockchain information
token_id = Column(String, unique=True, nullable=True)
blockchain_hash = Column(String, nullable=True)
# Timestamps
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Foreign keys
project_id = Column(Integer, ForeignKey("carbon_projects.id"), nullable=False)
# Relationships
project = relationship("CarbonProject", back_populates="offsets")
transactions = relationship("Transaction", back_populates="offset")

View File

@ -0,0 +1,44 @@
from sqlalchemy import Column, Integer, String, DateTime, Boolean, Text, Float, ForeignKey
from sqlalchemy.orm import relationship
from datetime import datetime
from app.db.base import Base
class CarbonProject(Base):
__tablename__ = "carbon_projects"
id = Column(Integer, primary_key=True, index=True)
title = Column(String, nullable=False)
description = Column(Text, nullable=False)
location = Column(String, nullable=False)
project_type = Column(String, nullable=False) # "forestry", "renewable_energy", "waste_management", etc.
methodology = Column(String, nullable=False) # Certification methodology used
# Carbon offset details
total_credits_available = Column(Integer, nullable=False)
credits_sold = Column(Integer, default=0)
price_per_credit = Column(Float, nullable=False) # Price in USD
# Project timeline
start_date = Column(DateTime, nullable=False)
end_date = Column(DateTime, nullable=False)
# Verification and status
verification_status = Column(String, default="pending") # "pending", "verified", "rejected"
verification_document_url = Column(String, nullable=True)
is_active = Column(Boolean, default=True)
# Blockchain information
contract_address = Column(String, nullable=True)
token_id = Column(String, nullable=True)
# Timestamps
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Foreign keys
developer_id = Column(Integer, ForeignKey("users.id"), nullable=False)
# Relationships
developer = relationship("User", back_populates="projects")
offsets = relationship("CarbonOffset", back_populates="project")

33
app/models/transaction.py Normal file
View File

@ -0,0 +1,33 @@
from sqlalchemy import Column, Integer, String, DateTime, Float, ForeignKey
from sqlalchemy.orm import relationship
from datetime import datetime
from app.db.base import Base
class Transaction(Base):
__tablename__ = "transactions"
id = Column(Integer, primary_key=True, index=True)
transaction_hash = Column(String, unique=True, nullable=False)
quantity = Column(Integer, nullable=False)
price_per_credit = Column(Float, nullable=False)
total_amount = Column(Float, nullable=False)
# Transaction status
status = Column(String, default="pending") # "pending", "confirmed", "failed"
# Blockchain information
block_number = Column(Integer, nullable=True)
gas_used = Column(Integer, nullable=True)
# Timestamps
created_at = Column(DateTime, default=datetime.utcnow)
confirmed_at = Column(DateTime, nullable=True)
# Foreign keys
buyer_id = Column(Integer, ForeignKey("users.id"), nullable=False)
offset_id = Column(Integer, ForeignKey("carbon_offsets.id"), nullable=False)
# Relationships
buyer = relationship("User", back_populates="transactions")
offset = relationship("CarbonOffset", back_populates="transactions")

25
app/models/user.py Normal file
View File

@ -0,0 +1,25 @@
from sqlalchemy import Column, Integer, String, DateTime, Boolean, Text
from sqlalchemy.orm import relationship
from datetime import datetime
from app.db.base import Base
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True, index=True, nullable=False)
hashed_password = Column(String, nullable=False)
full_name = Column(String, nullable=False)
user_type = Column(String, nullable=False) # "developer" or "buyer"
is_active = Column(Boolean, default=True)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Blockchain wallet information
wallet_address = Column(String, unique=True, nullable=True)
wallet_public_key = Column(Text, nullable=True)
# Relationships
projects = relationship("CarbonProject", back_populates="developer")
transactions = relationship("Transaction", back_populates="buyer")

1
app/schemas/__init__.py Normal file
View File

@ -0,0 +1 @@
# Schema modules

View File

@ -0,0 +1,50 @@
from pydantic import BaseModel
from typing import Optional, List
from datetime import datetime
class CarbonProjectBase(BaseModel):
title: str
description: str
location: str
project_type: str
methodology: str
total_credits_available: int
price_per_credit: float
start_date: datetime
end_date: datetime
class CarbonProjectCreate(CarbonProjectBase):
pass
class CarbonProjectUpdate(BaseModel):
title: Optional[str] = None
description: Optional[str] = None
location: Optional[str] = None
project_type: Optional[str] = None
methodology: Optional[str] = None
total_credits_available: Optional[int] = None
price_per_credit: Optional[float] = None
start_date: Optional[datetime] = None
end_date: Optional[datetime] = None
verification_document_url: Optional[str] = None
class CarbonProjectResponse(CarbonProjectBase):
id: int
credits_sold: int
verification_status: str
verification_document_url: Optional[str] = None
is_active: bool
contract_address: Optional[str] = None
token_id: Optional[str] = None
created_at: datetime
updated_at: datetime
developer_id: int
class Config:
from_attributes = True
class CarbonProjectListResponse(BaseModel):
projects: List[CarbonProjectResponse]
total: int
page: int
page_size: int

View File

@ -0,0 +1,34 @@
from pydantic import BaseModel
from typing import Optional, List
from datetime import datetime
class TransactionCreate(BaseModel):
offset_id: int
quantity: int
class TransactionResponse(BaseModel):
id: int
transaction_hash: str
quantity: int
price_per_credit: float
total_amount: float
status: str
block_number: Optional[int] = None
gas_used: Optional[int] = None
created_at: datetime
confirmed_at: Optional[datetime] = None
buyer_id: int
offset_id: int
class Config:
from_attributes = True
class TransactionListResponse(BaseModel):
transactions: List[TransactionResponse]
total: int
page: int
page_size: int
class PurchaseRequest(BaseModel):
project_id: int
quantity: int

38
app/schemas/user.py Normal file
View File

@ -0,0 +1,38 @@
from pydantic import BaseModel, EmailStr
from typing import Optional
from datetime import datetime
class UserBase(BaseModel):
email: EmailStr
full_name: str
user_type: str
class UserCreate(UserBase):
password: str
class UserLogin(BaseModel):
email: EmailStr
password: str
class UserResponse(UserBase):
id: int
is_active: bool
created_at: datetime
wallet_address: Optional[str] = None
class Config:
from_attributes = True
class Token(BaseModel):
access_token: str
token_type: str
class WalletLinkRequest(BaseModel):
wallet_address: str
class WalletResponse(BaseModel):
success: bool
wallet_linked: bool
wallet_address: Optional[str] = None
balance: Optional[float] = None
message: Optional[str] = None

1
app/services/__init__.py Normal file
View File

@ -0,0 +1 @@
# Service modules

168
app/services/blockchain.py Normal file
View File

@ -0,0 +1,168 @@
from web3 import Web3
from eth_account import Account
from typing import Optional, Dict, Any
import os
from datetime import datetime
class BlockchainService:
def __init__(self):
# Use environment variable for RPC URL (defaults to local for development)
self.rpc_url = os.getenv("BLOCKCHAIN_RPC_URL", "http://localhost:8545")
self.w3 = Web3(Web3.HTTPProvider(self.rpc_url))
self.contract_abi = self._get_carbon_token_abi()
def _get_carbon_token_abi(self) -> list:
# Simplified ABI for a carbon credit token contract
return [
{
"inputs": [
{"name": "to", "type": "address"},
{"name": "tokenId", "type": "uint256"},
{"name": "credits", "type": "uint256"}
],
"name": "mintCarbonCredit",
"outputs": [{"name": "", "type": "bool"}],
"type": "function"
},
{
"inputs": [
{"name": "from", "type": "address"},
{"name": "to", "type": "address"},
{"name": "tokenId", "type": "uint256"}
],
"name": "transferFrom",
"outputs": [{"name": "", "type": "bool"}],
"type": "function"
},
{
"inputs": [{"name": "tokenId", "type": "uint256"}],
"name": "ownerOf",
"outputs": [{"name": "", "type": "address"}],
"type": "function"
}
]
def validate_wallet_address(self, address: str) -> bool:
"""Validate if the provided address is a valid Ethereum address"""
try:
return Web3.is_address(address) and Web3.is_checksum_address(Web3.to_checksum_address(address))
except:
return False
def generate_wallet(self) -> Dict[str, str]:
"""Generate a new wallet for testing purposes"""
account = Account.create()
return {
"address": account.address,
"private_key": account.key.hex(),
"public_key": account.address # In Ethereum, address is derived from public key
}
def get_wallet_balance(self, address: str) -> Optional[float]:
"""Get ETH balance for a wallet address"""
try:
if not self.validate_wallet_address(address):
return None
balance_wei = self.w3.eth.get_balance(Web3.to_checksum_address(address))
balance_eth = self.w3.from_wei(balance_wei, 'ether')
return float(balance_eth)
except Exception as e:
print(f"Error getting balance for {address}: {e}")
return None
def create_carbon_token_transaction(
self,
contract_address: str,
from_address: str,
to_address: str,
token_id: int,
private_key: str = None
) -> Optional[Dict[str, Any]]:
"""Create a transaction to transfer carbon credits"""
try:
if not all([
self.validate_wallet_address(contract_address),
self.validate_wallet_address(from_address),
self.validate_wallet_address(to_address)
]):
return None
contract = self.w3.eth.contract(
address=Web3.to_checksum_address(contract_address),
abi=self.contract_abi
)
# Build transaction
transaction = contract.functions.transferFrom(
Web3.to_checksum_address(from_address),
Web3.to_checksum_address(to_address),
token_id
).build_transaction({
'from': Web3.to_checksum_address(from_address),
'gas': 200000,
'gasPrice': self.w3.to_wei('20', 'gwei'),
'nonce': self.w3.eth.get_transaction_count(Web3.to_checksum_address(from_address))
})
return {
"transaction": transaction,
"contract_address": contract_address,
"from_address": from_address,
"to_address": to_address,
"token_id": token_id,
"created_at": datetime.utcnow().isoformat()
}
except Exception as e:
print(f"Error creating transaction: {e}")
return None
def sign_and_send_transaction(self, transaction_data: Dict[str, Any], private_key: str) -> Optional[str]:
"""Sign and send a transaction to the blockchain"""
try:
transaction = transaction_data["transaction"]
signed_txn = self.w3.eth.account.sign_transaction(transaction, private_key)
tx_hash = self.w3.eth.send_raw_transaction(signed_txn.rawTransaction)
return tx_hash.hex()
except Exception as e:
print(f"Error signing/sending transaction: {e}")
return None
def get_transaction_receipt(self, tx_hash: str) -> Optional[Dict[str, Any]]:
"""Get transaction receipt from blockchain"""
try:
receipt = self.w3.eth.get_transaction_receipt(tx_hash)
return {
"transaction_hash": receipt["transactionHash"].hex(),
"block_number": receipt["blockNumber"],
"gas_used": receipt["gasUsed"],
"status": receipt["status"] # 1 for success, 0 for failure
}
except Exception as e:
print(f"Error getting transaction receipt: {e}")
return None
def verify_token_ownership(self, contract_address: str, token_id: int, owner_address: str) -> bool:
"""Verify if an address owns a specific token"""
try:
if not all([
self.validate_wallet_address(contract_address),
self.validate_wallet_address(owner_address)
]):
return False
contract = self.w3.eth.contract(
address=Web3.to_checksum_address(contract_address),
abi=self.contract_abi
)
actual_owner = contract.functions.ownerOf(token_id).call()
return actual_owner.lower() == owner_address.lower()
except Exception as e:
print(f"Error verifying ownership: {e}")
return False
# Global instance
blockchain_service = BlockchainService()

130
app/services/wallet.py Normal file
View File

@ -0,0 +1,130 @@
from sqlalchemy.orm import Session
from app.models.user import User
from app.services.blockchain import blockchain_service
from typing import Optional, Dict, Any
class WalletService:
def link_wallet(self, db: Session, user_id: int, wallet_address: str) -> Dict[str, Any]:
"""Link a wallet address to a user account"""
# Validate wallet address format
if not blockchain_service.validate_wallet_address(wallet_address):
return {
"success": False,
"message": "Invalid wallet address format"
}
# Check if wallet is already linked to another user
existing_user = db.query(User).filter(
User.wallet_address == wallet_address,
User.id != user_id
).first()
if existing_user:
return {
"success": False,
"message": "Wallet address is already linked to another account"
}
# Get user
user = db.query(User).filter(User.id == user_id).first()
if not user:
return {
"success": False,
"message": "User not found"
}
# Update user wallet information
user.wallet_address = wallet_address
user.wallet_public_key = wallet_address # In Ethereum, address is derived from public key
try:
db.commit()
return {
"success": True,
"message": "Wallet linked successfully",
"wallet_address": wallet_address
}
except Exception as e:
db.rollback()
return {
"success": False,
"message": f"Database error: {str(e)}"
}
def unlink_wallet(self, db: Session, user_id: int) -> Dict[str, Any]:
"""Unlink wallet from user account"""
user = db.query(User).filter(User.id == user_id).first()
if not user:
return {
"success": False,
"message": "User not found"
}
if not user.wallet_address:
return {
"success": False,
"message": "No wallet linked to this account"
}
# Clear wallet information
user.wallet_address = None
user.wallet_public_key = None
try:
db.commit()
return {
"success": True,
"message": "Wallet unlinked successfully"
}
except Exception as e:
db.rollback()
return {
"success": False,
"message": f"Database error: {str(e)}"
}
def get_wallet_info(self, db: Session, user_id: int) -> Dict[str, Any]:
"""Get wallet information for a user"""
user = db.query(User).filter(User.id == user_id).first()
if not user:
return {
"success": False,
"message": "User not found"
}
if not user.wallet_address:
return {
"success": True,
"wallet_linked": False,
"wallet_address": None,
"balance": None
}
# Get wallet balance from blockchain
balance = blockchain_service.get_wallet_balance(user.wallet_address)
return {
"success": True,
"wallet_linked": True,
"wallet_address": user.wallet_address,
"balance": balance
}
def generate_test_wallet(self) -> Dict[str, Any]:
"""Generate a test wallet for development purposes"""
wallet_data = blockchain_service.generate_wallet()
return {
"success": True,
"message": "Test wallet generated successfully",
"wallet_data": wallet_data,
"warning": "This is for testing only. Keep private key secure!"
}
# Global instance
wallet_service = WalletService()

88
main.py Normal file
View File

@ -0,0 +1,88 @@
from fastapi import FastAPI, Depends
from fastapi.middleware.cors import CORSMiddleware
from fastapi.openapi.utils import get_openapi
from sqlalchemy.orm import Session
from app.db.session import get_db, engine
from app.db.base import Base
from app.api import auth, wallet, projects, trading
import os
# Create database tables
Base.metadata.create_all(bind=engine)
app = FastAPI(
title="Carbon Offset Trading Platform",
description="A blockchain-enabled platform for trading carbon offsets between project developers and buyers",
version="1.0.0",
docs_url="/docs",
redoc_url="/redoc"
)
# CORS configuration
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Include routers
app.include_router(auth.router, prefix="/auth", tags=["Authentication"])
app.include_router(wallet.router, prefix="/wallet", tags=["Wallet"])
app.include_router(projects.router, prefix="/projects", tags=["Projects"])
app.include_router(trading.router, prefix="/trading", tags=["Trading"])
@app.get("/")
async def root():
"""Base endpoint with platform information"""
return {
"title": "Carbon Offset Trading Platform",
"description": "A blockchain-enabled platform for trading carbon offsets",
"version": "1.0.0",
"documentation": "/docs",
"health_check": "/health"
}
@app.get("/health")
async def health_check(db: Session = Depends(get_db)):
"""Health check endpoint"""
try:
# Test database connection
db.execute("SELECT 1")
db_status = "healthy"
except Exception as e:
db_status = f"unhealthy: {str(e)}"
# Check environment variables
env_status = "healthy"
required_envs = ["SECRET_KEY"]
missing_envs = [env for env in required_envs if not os.getenv(env)]
if missing_envs:
env_status = f"missing environment variables: {', '.join(missing_envs)}"
return {
"status": "healthy" if db_status == "healthy" and env_status == "healthy" else "unhealthy",
"database": db_status,
"environment": env_status,
"version": "1.0.0"
}
# Custom OpenAPI schema
def custom_openapi():
if app.openapi_schema:
return app.openapi_schema
openapi_schema = get_openapi(
title="Carbon Offset Trading Platform API",
version="1.0.0",
description="A blockchain-enabled platform for trading carbon offsets between project developers and buyers",
routes=app.routes,
)
app.openapi_schema = openapi_schema
return app.openapi_schema
app.openapi = custom_openapi
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)

59
openapi.json Normal file
View File

@ -0,0 +1,59 @@
{
"openapi": "3.1.0",
"info": {
"title": "Carbon Offset Trading Platform API",
"description": "A blockchain-enabled platform for trading carbon offsets between project developers and buyers",
"version": "1.0.0"
},
"paths": {
"/": {
"get": {
"summary": "Root endpoint",
"description": "Returns platform information and available endpoints",
"responses": {
"200": {
"description": "Platform information",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"title": {"type": "string"},
"description": {"type": "string"},
"version": {"type": "string"},
"documentation": {"type": "string"},
"health_check": {"type": "string"}
}
}
}
}
}
}
}
},
"/health": {
"get": {
"summary": "Health check endpoint",
"description": "Returns the health status of the application and its dependencies",
"responses": {
"200": {
"description": "Health status information",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {"type": "string"},
"database": {"type": "string"},
"environment": {"type": "string"},
"version": {"type": "string"}
}
}
}
}
}
}
}
}
}
}

13
requirements.txt Normal file
View File

@ -0,0 +1,13 @@
fastapi==0.104.1
uvicorn==0.24.0
sqlalchemy==2.0.23
alembic==1.12.1
python-multipart==0.0.6
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
pydantic-settings==2.0.3
web3==6.11.3
eth-account==0.9.0
cryptography==41.0.7
ruff==0.1.5
python-dotenv==1.0.0