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:
parent
ff7498dd5c
commit
e122f16dea
202
README.md
202
README.md
@ -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
97
alembic.ini
Normal 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
87
alembic/env.py
Normal 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
24
alembic/script.py.mako
Normal 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"}
|
118
alembic/versions/001_initial_migration.py
Normal file
118
alembic/versions/001_initial_migration.py
Normal 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
1
app/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# Carbon Offset Trading Platform
|
1
app/api/__init__.py
Normal file
1
app/api/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# API modules
|
60
app/api/auth.py
Normal file
60
app/api/auth.py
Normal 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
175
app/api/projects.py
Normal 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
215
app/api/trading.py
Normal 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
56
app/api/wallet.py
Normal 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
1
app/core/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# Core modules
|
57
app/core/deps.py
Normal file
57
app/core/deps.py
Normal 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
39
app/core/security.py
Normal 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
3
app/db/base.py
Normal file
@ -0,0 +1,3 @@
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
Base = declarative_base()
|
24
app/db/session.py
Normal file
24
app/db/session.py
Normal 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
6
app/models/__init__.py
Normal 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"]
|
29
app/models/carbon_offset.py
Normal file
29
app/models/carbon_offset.py
Normal 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")
|
44
app/models/carbon_project.py
Normal file
44
app/models/carbon_project.py
Normal 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
33
app/models/transaction.py
Normal 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
25
app/models/user.py
Normal 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
1
app/schemas/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# Schema modules
|
50
app/schemas/carbon_project.py
Normal file
50
app/schemas/carbon_project.py
Normal 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
|
34
app/schemas/transaction.py
Normal file
34
app/schemas/transaction.py
Normal 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
38
app/schemas/user.py
Normal 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
1
app/services/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# Service modules
|
168
app/services/blockchain.py
Normal file
168
app/services/blockchain.py
Normal 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
130
app/services/wallet.py
Normal 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
88
main.py
Normal 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
59
openapi.json
Normal 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
13
requirements.txt
Normal 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
|
Loading…
x
Reference in New Issue
Block a user