Rebuild platform with Node.js, Express.js and TypeScript

Complete rewrite from Python/FastAPI to Node.js stack:

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
- Comprehensive input validation with Joi
- Advanced security with Helmet, CORS, and rate limiting
- Error handling and logging middleware
- Health check endpoint with service monitoring

Technical stack:
- Node.js with Express.js and TypeScript
- Prisma ORM with SQLite database
- Web3.js and Ethers.js for blockchain integration
- JWT authentication with bcrypt password hashing
- Comprehensive validation and security middleware
- Production-ready error handling and logging

Database schema:
- Users with wallet linking capabilities
- Carbon projects with verification status
- Carbon offsets with blockchain token tracking
- Transactions with confirmation details

Environment variables required:
- JWT_SECRET (required)
- DATABASE_URL (optional, defaults to SQLite)
- BLOCKCHAIN_RPC_URL (optional, defaults to localhost)
- NODE_ENV, PORT, CORS_ORIGIN (optional)

Run with: npm install && npm run db:generate && npm run db:migrate && npm run dev
This commit is contained in:
Automated Action 2025-06-20 17:14:37 +00:00
parent e122f16dea
commit 3ef47ed096
51 changed files with 2608 additions and 1936 deletions

18
.env.example Normal file
View File

@ -0,0 +1,18 @@
# Application Configuration
NODE_ENV=development
PORT=8000
JWT_SECRET=your-jwt-secret-key-change-in-production
# Database Configuration
DATABASE_URL="file:./storage/db/database.db"
# Blockchain Configuration
BLOCKCHAIN_RPC_URL=http://localhost:8545
ETHEREUM_NETWORK=localhost
# CORS Configuration
CORS_ORIGIN=*
# Rate Limiting
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX_REQUESTS=100

28
.eslintrc.js Normal file
View File

@ -0,0 +1,28 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2022,
sourceType: 'module',
project: './tsconfig.json',
},
plugins: ['@typescript-eslint'],
extends: [
'eslint:recommended',
'@typescript-eslint/recommended',
],
env: {
node: true,
es2022: true,
},
rules: {
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-non-null-assertion': 'warn',
'prefer-const': 'error',
'no-var': 'error',
'no-console': 'off',
},
ignorePatterns: ['dist/', 'node_modules/', '*.js'],
};

230
.gitignore vendored
View File

@ -1,195 +1,59 @@
repos*
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
media/
*.db
whitelist.txt
ai_docs/
specs/
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# C extensions
*.so
test_cases.py
# Distribution / packaging
.Python
build/
develop-eggs/
# Production builds
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
test_case1.py
api/core/dependencies/mailjet.py
tests/v1/waitlist/waitlist_test.py
result.json
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
build/
# Installer logs
pip-log.txt
test_case1.py
pip-delete-this-directory.txt
# Environment variables
.env
.env.local
.env.production
.env.test
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
case_test.py
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
*.sqlite3
# Database
storage/
*.db
*.sqlite
# Flask stuff:
instance/
.webassets-cache
# Logs
logs/
*.log
# Scrapy stuff:
.scrapy
# Runtime data
pids/
*.pid
*.seed
*.pid.lock
# Sphinx documentation
docs/_build/
# Coverage directory used by tools like istanbul
coverage/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env*
!.env.sample
.venv
.blog_env/
env/
venv*
*venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# IDE files
.vscode/
jeff.py
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
**/.DS_Store
.aider*
.idea/
.dump.rdb
.celery.log
docker-compose.yaml
# project analysis result
analysis_results.json
*.swp
*.swo
*~
**/.claude/settings.local.json
*.aider
.claude/
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Temporary files
tmp/
temp/
# TypeScript
*.tsbuildinfo
# Prisma
prisma/migrations/

198
README.md
View File

@ -26,110 +26,119 @@ A blockchain-enabled carbon offset trading platform that connects project develo
## Tech Stack
- **Backend**: FastAPI (Python)
- **Database**: SQLite with SQLAlchemy ORM
- **Backend**: Node.js with Express.js and TypeScript
- **Database**: SQLite with Prisma ORM
- **Authentication**: JWT tokens with bcrypt password hashing
- **Blockchain**: Web3.py for Ethereum integration
- **Database Migrations**: Alembic
- **API Documentation**: OpenAPI/Swagger
- **Blockchain**: Web3.js and Ethers.js for Ethereum integration
- **Validation**: Joi for request validation
- **Security**: Helmet, CORS, Rate limiting
## 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
├── src/
│ ├── server.ts # Express application entry point
│ ├── types/ # TypeScript type definitions
│ │ └── index.ts # Main types and interfaces
│ ├── routes/ # API endpoints
│ │ ├── auth.ts # Authentication endpoints
│ │ ├── wallet.ts # Wallet management endpoints
│ │ ├── projects.ts # Project management endpoints
│ │ └── trading.ts # Trading and marketplace endpoints
│ ├── middleware/ # Express middleware
│ │ ├── auth.ts # Authentication middleware
│ │ ├── validation.ts # Request validation middleware
│ │ ├── security.ts # Security middleware
│ │ └── error.ts # Error handling middleware
│ ├── services/ # Business logic services
│ │ ├── blockchain.ts # Blockchain integration
│ │ └── wallet.ts # Wallet management
│ └── utils/ # Utility functions
│ ├── database.ts # Database connection and utilities
│ └── auth.ts # Authentication utilities
├── prisma/
│ └── schema.prisma # Database schema
├── storage/ # Application storage directory
│ └── db/ # SQLite database files
├── package.json # Node.js dependencies and scripts
├── tsconfig.json # TypeScript configuration
└── .env.example # Environment variables template
```
## Installation
1. Install dependencies:
```bash
pip install -r requirements.txt
npm install
```
2. Run database migrations:
2. Set up environment variables:
```bash
alembic upgrade head
cp .env.example .env
# Edit .env with your configuration
```
3. Set required environment variables:
3. Generate Prisma client and run migrations:
```bash
export SECRET_KEY="your-secret-key-here"
export BLOCKCHAIN_RPC_URL="https://your-ethereum-rpc-url" # Optional, defaults to localhost
npm run db:generate
npm run db:migrate
```
## Running the Application
Start the development server:
### Development
Start the development server with hot reload:
```bash
uvicorn main:app --host 0.0.0.0 --port 8000 --reload
npm run dev
```
### Production
Build and start the production server:
```bash
npm run build
npm start
```
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
- **API Documentation**: http://localhost:8000/api/docs
## Environment Variables
| Variable | Description | Required | Default |
|----------|-------------|----------|---------|
| `SECRET_KEY` | JWT token secret key | Yes | - |
| `NODE_ENV` | Environment mode | No | development |
| `PORT` | Server port | No | 8000 |
| `JWT_SECRET` | JWT token secret | Yes | - |
| `DATABASE_URL` | SQLite database file path | No | file:./storage/db/database.db |
| `BLOCKCHAIN_RPC_URL` | Ethereum RPC endpoint | No | http://localhost:8545 |
| `CORS_ORIGIN` | CORS allowed origins | No | * |
## API Endpoints
### Authentication
- `POST /auth/register` - Register new user (developer or buyer)
- `POST /auth/login` - User login
- `POST /api/auth/register` - Register new user (developer or buyer)
- `POST /api/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
- `POST /api/wallet/link` - Link blockchain wallet to user account
- `DELETE /api/wallet/unlink` - Unlink wallet from user account
- `GET /api/wallet/info` - Get wallet information and balance
- `POST /api/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
- `POST /api/projects/` - Create new carbon offset project
- `GET /api/projects/my-projects` - Get developer's projects
- `PUT /api/projects/{project_id}` - Update project
- `DELETE /api/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
- `GET /api/projects/` - Browse all available projects
- `GET /api/projects/{project_id}` - Get project details
- `POST /api/trading/purchase` - Purchase carbon offsets
- `GET /api/trading/my-transactions` - Get user's transactions
- `GET /api/trading/marketplace/stats` - Get marketplace statistics
### System
- `GET /` - Platform information
@ -140,7 +149,7 @@ The application will be available at:
### Users
- User authentication and profile information
- Wallet linking for blockchain integration
- User types: "developer" or "buyer"
- User types: DEVELOPER or BUYER
### Carbon Projects
- Project details and metadata
@ -163,39 +172,76 @@ The application will be available at:
### Database Migrations
Create a new migration:
Generate Prisma client:
```bash
alembic revision --autogenerate -m "Description of changes"
npm run db:generate
```
Apply migrations:
Create and apply migrations:
```bash
alembic upgrade head
npm run db:migrate
```
View database in Prisma Studio:
```bash
npm run db:studio
```
### Code Quality
Run linting:
```bash
npm run lint
```
Fix linting issues:
```bash
npm run lint:fix
```
Build TypeScript:
```bash
npm run build
```
### 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
curl -X POST http://localhost:8000/api/wallet/generate-test-wallet
```
## Security Features
- JWT-based authentication
- Password hashing with bcrypt
- Role-based access control (developer/buyer)
- JWT-based authentication with secure token generation
- Password hashing with bcrypt (12 rounds)
- Role-based access control (DEVELOPER/BUYER)
- Rate limiting and CORS protection
- Helmet security headers
- Input validation with Joi
- Blockchain wallet verification
- Transaction signing and verification
## Production Deployment
1. Set production environment variables in `.env`
2. Build the application: `npm run build`
3. Start the production server: `npm start`
4. Ensure database is properly migrated
5. Configure reverse proxy (nginx/Apache)
6. Set up SSL certificates
7. Configure firewall and security groups
## 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
1. Follow TypeScript best practices
2. Use the existing code structure and patterns
3. Add comprehensive input validation
4. Include proper error handling
5. Update documentation for API changes
6. Test blockchain integration thoroughly
7. Follow the established commit message format
## License
This project is part of a carbon offset trading platform implementation.
This project is part of a carbon offset trading platform implementation.

View File

@ -1,97 +0,0 @@
# 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

View File

@ -1,87 +0,0 @@
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()

View File

@ -1,24 +0,0 @@
"""${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

@ -1,118 +0,0 @@
"""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')

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,24 +0,0 @@
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()

View File

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

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

@ -1,44 +0,0 @@
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")

View File

@ -1,33 +0,0 @@
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")

View File

@ -1,25 +0,0 @@
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")

View File

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

View File

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

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

View File

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

View File

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

View File

@ -1,168 +0,0 @@
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()

View File

@ -1,130 +0,0 @@
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
View File

@ -1,88 +0,0 @@
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)

View File

@ -1,59 +0,0 @@
{
"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"}
}
}
}
}
}
}
}
}
}
}

66
package.json Normal file
View File

@ -0,0 +1,66 @@
{
"name": "carbon-offset-trading-platform",
"version": "1.0.0",
"description": "A blockchain-enabled carbon offset trading platform for project developers and buyers",
"main": "dist/server.js",
"scripts": {
"build": "tsc",
"start": "node dist/server.js",
"dev": "tsx watch src/server.ts",
"db:migrate": "npx prisma migrate dev",
"db:generate": "npx prisma generate",
"db:studio": "npx prisma studio",
"lint": "eslint src/**/*.ts",
"lint:fix": "eslint src/**/*.ts --fix",
"test": "jest"
},
"keywords": [
"carbon-offset",
"blockchain",
"trading",
"sustainability",
"ethereum",
"express",
"typescript"
],
"author": "BackendIM",
"license": "MIT",
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5",
"helmet": "^7.1.0",
"bcryptjs": "^2.4.3",
"jsonwebtoken": "^9.0.2",
"prisma": "^5.6.0",
"@prisma/client": "^5.6.0",
"web3": "^4.2.2",
"ethers": "^6.8.1",
"uuid": "^9.0.1",
"joi": "^17.11.0",
"express-rate-limit": "^7.1.5",
"compression": "^1.7.4",
"morgan": "^1.10.0",
"dotenv": "^16.3.1"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/cors": "^2.8.17",
"@types/bcryptjs": "^2.4.6",
"@types/jsonwebtoken": "^9.0.5",
"@types/uuid": "^9.0.7",
"@types/compression": "^1.7.5",
"@types/morgan": "^1.9.9",
"@types/node": "^20.9.0",
"@typescript-eslint/eslint-plugin": "^6.12.0",
"@typescript-eslint/parser": "^6.12.0",
"eslint": "^8.54.0",
"typescript": "^5.3.2",
"tsx": "^4.6.0",
"jest": "^29.7.0",
"@types/jest": "^29.5.8",
"ts-jest": "^29.1.1"
},
"engines": {
"node": ">=18.0.0"
}
}

143
prisma/schema.prisma Normal file
View File

@ -0,0 +1,143 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
email String @unique
password String
fullName String @map("full_name")
userType UserType @map("user_type")
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// Blockchain wallet information
walletAddress String? @unique @map("wallet_address")
walletPublicKey String? @map("wallet_public_key")
// Relationships
projects CarbonProject[] @relation("ProjectDeveloper")
transactions Transaction[] @relation("TransactionBuyer")
@@map("users")
}
model CarbonProject {
id Int @id @default(autoincrement())
title String
description String
location String
projectType String @map("project_type")
methodology String
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// Carbon offset details
totalCreditsAvailable Int @map("total_credits_available")
creditsSold Int @default(0) @map("credits_sold")
pricePerCredit Float @map("price_per_credit")
// Project timeline
startDate DateTime @map("start_date")
endDate DateTime @map("end_date")
// Verification and status
verificationStatus VerificationStatus @default(PENDING) @map("verification_status")
verificationDocumentUrl String? @map("verification_document_url")
isActive Boolean @default(true) @map("is_active")
// Blockchain information
contractAddress String? @map("contract_address")
tokenId String? @map("token_id")
// Foreign keys
developerId Int @map("developer_id")
// Relationships
developer User @relation("ProjectDeveloper", fields: [developerId], references: [id])
offsets CarbonOffset[]
@@map("carbon_projects")
}
model CarbonOffset {
id Int @id @default(autoincrement())
serialNumber String @unique @map("serial_number")
vintageYear Int @map("vintage_year")
quantity Int
status OffsetStatus @default(AVAILABLE)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// Blockchain information
tokenId String? @unique @map("token_id")
blockchainHash String? @map("blockchain_hash")
// Foreign keys
projectId Int @map("project_id")
// Relationships
project CarbonProject @relation(fields: [projectId], references: [id])
transactions Transaction[]
@@map("carbon_offsets")
}
model Transaction {
id Int @id @default(autoincrement())
transactionHash String @unique @map("transaction_hash")
quantity Int
pricePerCredit Float @map("price_per_credit")
totalAmount Float @map("total_amount")
createdAt DateTime @default(now()) @map("created_at")
// Transaction status
status TransactionStatus @default(PENDING)
confirmedAt DateTime? @map("confirmed_at")
// Blockchain information
blockNumber Int? @map("block_number")
gasUsed Int? @map("gas_used")
// Foreign keys
buyerId Int @map("buyer_id")
offsetId Int @map("offset_id")
// Relationships
buyer User @relation("TransactionBuyer", fields: [buyerId], references: [id])
offset CarbonOffset @relation(fields: [offsetId], references: [id])
@@map("transactions")
}
enum UserType {
DEVELOPER
BUYER
}
enum VerificationStatus {
PENDING
VERIFIED
REJECTED
}
enum OffsetStatus {
AVAILABLE
SOLD
RETIRED
}
enum TransactionStatus {
PENDING
CONFIRMED
FAILED
}

View File

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

76
src/middleware/auth.ts Normal file
View File

@ -0,0 +1,76 @@
import { Request, Response, NextFunction } from 'express';
import { AuthUtils } from '@/utils/auth';
import { UserType } from '@/types';
import { prisma } from '@/utils/database';
export const authenticateToken = async (req: Request, res: Response, next: NextFunction) => {
try {
const authHeader = req.headers.authorization;
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) {
return res.status(401).json({
success: false,
error: 'Access token required'
});
}
const decoded = AuthUtils.verifyAccessToken(token);
if (!decoded) {
return res.status(401).json({
success: false,
error: 'Invalid or expired token'
});
}
// Verify user still exists and is active
const user = await prisma.user.findUnique({
where: { id: decoded.userId }
});
if (!user || !user.isActive) {
return res.status(401).json({
success: false,
error: 'User not found or inactive'
});
}
// Add user info to request
req.user = {
id: user.id,
email: user.email,
userType: user.userType
};
next();
} catch (error) {
console.error('Authentication middleware error:', error);
return res.status(500).json({
success: false,
error: 'Internal server error during authentication'
});
}
};
export const requireUserType = (requiredType: UserType) => {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).json({
success: false,
error: 'Authentication required'
});
}
if (req.user.userType !== requiredType) {
return res.status(403).json({
success: false,
error: `Access denied: ${requiredType} role required`
});
}
next();
};
};
export const requireDeveloper = requireUserType(UserType.DEVELOPER);
export const requireBuyer = requireUserType(UserType.BUYER);

46
src/middleware/error.ts Normal file
View File

@ -0,0 +1,46 @@
import { Request, Response, NextFunction } from 'express';
import { ApiResponse } from '@/types';
export interface AppError extends Error {
statusCode?: number;
isOperational?: boolean;
}
export const createError = (message: string, statusCode: number = 500): AppError => {
const error: AppError = new Error(message);
error.statusCode = statusCode;
error.isOperational = true;
return error;
};
export const errorHandler = (
error: AppError,
req: Request,
res: Response,
next: NextFunction
) => {
console.error('Error:', {
message: error.message,
stack: error.stack,
url: req.url,
method: req.method,
ip: req.ip,
userAgent: req.get('User-Agent')
});
const statusCode = error.statusCode || 500;
const message = error.isOperational ? error.message : 'Internal Server Error';
return res.status(statusCode).json({
success: false,
error: message,
...(process.env.NODE_ENV === 'development' && { stack: error.stack })
} as ApiResponse);
};
export const notFoundHandler = (req: Request, res: Response) => {
return res.status(404).json({
success: false,
error: `Route ${req.method} ${req.path} not found`
} as ApiResponse);
};

View File

@ -0,0 +1,60 @@
import { Request, Response, NextFunction } from 'express';
import rateLimit from 'express-rate-limit';
import helmet from 'helmet';
import cors from 'cors';
// Rate limiting configuration
export const createRateLimiter = () => {
return rateLimit({
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || '900000'), // 15 minutes
max: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS || '100'), // limit each IP
message: {
success: false,
error: 'Too many requests from this IP, please try again later'
},
standardHeaders: true,
legacyHeaders: false,
});
};
// CORS configuration
export const corsOptions = {
origin: process.env.CORS_ORIGIN === '*' ? true : process.env.CORS_ORIGIN?.split(',') || 'http://localhost:3000',
credentials: true,
optionsSuccessStatus: 200,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With']
};
// Helmet configuration for security headers
export const helmetOptions = {
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"],
},
},
crossOriginEmbedderPolicy: false,
};
// Security middleware setup
export const setupSecurity = (app: any) => {
// Basic security headers
app.use(helmet(helmetOptions));
// CORS
app.use(cors(corsOptions));
// Rate limiting
app.use('/api/', createRateLimiter());
// Trust proxy (if behind reverse proxy)
app.set('trust proxy', 1);
};

View File

@ -0,0 +1,186 @@
import Joi from 'joi';
import { Request, Response, NextFunction } from 'express';
import { UserType } from '@/types';
// User validation schemas
export const registerSchema = Joi.object({
email: Joi.string().email().required().messages({
'string.email': 'Please provide a valid email address',
'any.required': 'Email is required'
}),
password: Joi.string().min(8).pattern(new RegExp('^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)')).required().messages({
'string.min': 'Password must be at least 8 characters long',
'string.pattern.base': 'Password must contain at least one uppercase letter, one lowercase letter, and one number',
'any.required': 'Password is required'
}),
fullName: Joi.string().min(2).max(100).required().messages({
'string.min': 'Full name must be at least 2 characters long',
'string.max': 'Full name must not exceed 100 characters',
'any.required': 'Full name is required'
}),
userType: Joi.string().valid(...Object.values(UserType)).required().messages({
'any.only': 'User type must be either DEVELOPER or BUYER',
'any.required': 'User type is required'
})
});
export const loginSchema = Joi.object({
email: Joi.string().email().required().messages({
'string.email': 'Please provide a valid email address',
'any.required': 'Email is required'
}),
password: Joi.string().required().messages({
'any.required': 'Password is required'
})
});
// Wallet validation schemas
export const walletLinkSchema = Joi.object({
walletAddress: Joi.string().pattern(new RegExp('^0x[a-fA-F0-9]{40}$')).required().messages({
'string.pattern.base': 'Please provide a valid Ethereum wallet address',
'any.required': 'Wallet address is required'
})
});
// Project validation schemas
export const createProjectSchema = Joi.object({
title: Joi.string().min(5).max(200).required().messages({
'string.min': 'Project title must be at least 5 characters long',
'string.max': 'Project title must not exceed 200 characters',
'any.required': 'Project title is required'
}),
description: Joi.string().min(20).max(2000).required().messages({
'string.min': 'Project description must be at least 20 characters long',
'string.max': 'Project description must not exceed 2000 characters',
'any.required': 'Project description is required'
}),
location: Joi.string().min(2).max(100).required().messages({
'string.min': 'Location must be at least 2 characters long',
'string.max': 'Location must not exceed 100 characters',
'any.required': 'Location is required'
}),
projectType: Joi.string().min(2).max(50).required().messages({
'string.min': 'Project type must be at least 2 characters long',
'string.max': 'Project type must not exceed 50 characters',
'any.required': 'Project type is required'
}),
methodology: Joi.string().min(2).max(100).required().messages({
'string.min': 'Methodology must be at least 2 characters long',
'string.max': 'Methodology must not exceed 100 characters',
'any.required': 'Methodology is required'
}),
totalCreditsAvailable: Joi.number().integer().min(1).max(1000000).required().messages({
'number.base': 'Total credits available must be a number',
'number.integer': 'Total credits available must be an integer',
'number.min': 'Total credits available must be at least 1',
'number.max': 'Total credits available must not exceed 1,000,000',
'any.required': 'Total credits available is required'
}),
pricePerCredit: Joi.number().min(0.01).max(10000).required().messages({
'number.base': 'Price per credit must be a number',
'number.min': 'Price per credit must be at least $0.01',
'number.max': 'Price per credit must not exceed $10,000',
'any.required': 'Price per credit is required'
}),
startDate: Joi.date().iso().required().messages({
'date.base': 'Start date must be a valid date',
'date.format': 'Start date must be in ISO format',
'any.required': 'Start date is required'
}),
endDate: Joi.date().iso().greater(Joi.ref('startDate')).required().messages({
'date.base': 'End date must be a valid date',
'date.format': 'End date must be in ISO format',
'date.greater': 'End date must be after start date',
'any.required': 'End date is required'
})
});
export const updateProjectSchema = Joi.object({
title: Joi.string().min(5).max(200).optional(),
description: Joi.string().min(20).max(2000).optional(),
location: Joi.string().min(2).max(100).optional(),
projectType: Joi.string().min(2).max(50).optional(),
methodology: Joi.string().min(2).max(100).optional(),
totalCreditsAvailable: Joi.number().integer().min(1).max(1000000).optional(),
pricePerCredit: Joi.number().min(0.01).max(10000).optional(),
startDate: Joi.date().iso().optional(),
endDate: Joi.date().iso().optional(),
verificationDocumentUrl: Joi.string().uri().optional().messages({
'string.uri': 'Verification document URL must be a valid URL'
})
});
// Transaction validation schemas
export const purchaseSchema = Joi.object({
projectId: Joi.number().integer().min(1).required().messages({
'number.base': 'Project ID must be a number',
'number.integer': 'Project ID must be an integer',
'number.min': 'Project ID must be at least 1',
'any.required': 'Project ID is required'
}),
quantity: Joi.number().integer().min(1).max(100000).required().messages({
'number.base': 'Quantity must be a number',
'number.integer': 'Quantity must be an integer',
'number.min': 'Quantity must be at least 1',
'number.max': 'Quantity must not exceed 100,000',
'any.required': 'Quantity is required'
})
});
// Pagination validation schema
export const paginationSchema = Joi.object({
page: Joi.number().integer().min(1).default(1).optional(),
pageSize: Joi.number().integer().min(1).max(100).default(10).optional()
});
// Validation middleware factory
export const validate = (schema: Joi.ObjectSchema) => {
return (req: Request, res: Response, next: NextFunction) => {
const { error, value } = schema.validate(req.body, {
abortEarly: false,
stripUnknown: true
});
if (error) {
const errors = error.details.map(detail => ({
field: detail.path.join('.'),
message: detail.message
}));
return res.status(400).json({
success: false,
error: 'Validation failed',
details: errors
});
}
req.body = value;
next();
};
};
// Query validation middleware
export const validateQuery = (schema: Joi.ObjectSchema) => {
return (req: Request, res: Response, next: NextFunction) => {
const { error, value } = schema.validate(req.query, {
abortEarly: false,
stripUnknown: true
});
if (error) {
const errors = error.details.map(detail => ({
field: detail.path.join('.'),
message: detail.message
}));
return res.status(400).json({
success: false,
error: 'Query validation failed',
details: errors
});
}
req.query = value;
next();
};
};

131
src/routes/auth.ts Normal file
View File

@ -0,0 +1,131 @@
import { Router, Request, Response } from 'express';
import { prisma } from '@/utils/database';
import { AuthUtils } from '@/utils/auth';
import { validate, registerSchema, loginSchema } from '@/middleware/validation';
import { CreateUserRequest, LoginRequest, UserResponse, ApiResponse } from '@/types';
const router = Router();
// Register endpoint
router.post('/register', validate(registerSchema), async (req: Request, res: Response) => {
try {
const { email, password, fullName, userType }: CreateUserRequest = req.body;
// Check if user already exists
const existingUser = await prisma.user.findUnique({
where: { email }
});
if (existingUser) {
return res.status(400).json({
success: false,
error: 'Email already registered'
} as ApiResponse);
}
// Hash password
const hashedPassword = await AuthUtils.hashPassword(password);
// Create user
const user = await prisma.user.create({
data: {
email,
password: hashedPassword,
fullName,
userType
},
select: {
id: true,
email: true,
fullName: true,
userType: true,
isActive: true,
createdAt: true,
walletAddress: true
}
});
return res.status(201).json({
success: true,
data: user,
message: 'User registered successfully'
} as ApiResponse<UserResponse>);
} catch (error) {
console.error('Registration error:', error);
return res.status(500).json({
success: false,
error: 'Internal server error during registration'
} as ApiResponse);
}
});
// Login endpoint
router.post('/login', validate(loginSchema), async (req: Request, res: Response) => {
try {
const { email, password }: LoginRequest = req.body;
// Find user
const user = await prisma.user.findUnique({
where: { email }
});
if (!user) {
return res.status(401).json({
success: false,
error: 'Invalid credentials'
} as ApiResponse);
}
// Verify password
const isPasswordValid = await AuthUtils.verifyPassword(password, user.password);
if (!isPasswordValid) {
return res.status(401).json({
success: false,
error: 'Invalid credentials'
} as ApiResponse);
}
// Check if user is active
if (!user.isActive) {
return res.status(401).json({
success: false,
error: 'Account is deactivated'
} as ApiResponse);
}
// Generate JWT token
const token = AuthUtils.generateAccessToken({
userId: user.id,
email: user.email,
userType: user.userType
});
return res.status(200).json({
success: true,
data: {
accessToken: token,
tokenType: 'Bearer',
user: {
id: user.id,
email: user.email,
fullName: user.fullName,
userType: user.userType,
isActive: user.isActive,
createdAt: user.createdAt,
walletAddress: user.walletAddress
}
},
message: 'Login successful'
} as ApiResponse);
} catch (error) {
console.error('Login error:', error);
return res.status(500).json({
success: false,
error: 'Internal server error during login'
} as ApiResponse);
}
});
export default router;

292
src/routes/projects.ts Normal file
View File

@ -0,0 +1,292 @@
import { Router, Request, Response } from 'express';
import { authenticateToken, requireDeveloper } from '@/middleware/auth';
import { validate, validateQuery, createProjectSchema, updateProjectSchema, paginationSchema } from '@/middleware/validation';
import { prisma } from '@/utils/database';
import { CreateProjectRequest, UpdateProjectRequest, ApiResponse, PaginationOptions, FilterOptions } from '@/types';
import { VerificationStatus } from '@prisma/client';
const router = Router();
// Create project endpoint (Developer only)
router.post('/', authenticateToken, requireDeveloper, validate(createProjectSchema), async (req: Request, res: Response) => {
try {
const projectData: CreateProjectRequest = req.body;
const developerId = req.user!.id;
// Create project
const project = await prisma.carbonProject.create({
data: {
...projectData,
developerId
}
});
// Create initial carbon offset
await prisma.carbonOffset.create({
data: {
serialNumber: `CO${project.id}-${projectData.totalCreditsAvailable}`,
vintageYear: new Date(projectData.startDate).getFullYear(),
quantity: projectData.totalCreditsAvailable,
projectId: project.id
}
});
return res.status(201).json({
success: true,
data: project,
message: 'Project created successfully'
} as ApiResponse);
} catch (error) {
console.error('Project creation error:', error);
return res.status(500).json({
success: false,
error: 'Internal server error during project creation'
} as ApiResponse);
}
});
// List all projects endpoint
router.get('/', validateQuery(paginationSchema), async (req: Request, res: Response) => {
try {
const { page = 1, pageSize = 10 } = req.query as PaginationOptions;
const { projectType, verificationStatus } = req.query as FilterOptions;
// Build where clause
const where: any = { isActive: true };
if (projectType) where.projectType = projectType;
if (verificationStatus) where.verificationStatus = verificationStatus as VerificationStatus;
// Get total count
const total = await prisma.carbonProject.count({ where });
// Get projects with pagination
const projects = await prisma.carbonProject.findMany({
where,
orderBy: { createdAt: 'desc' },
skip: (page - 1) * pageSize,
take: pageSize,
include: {
developer: {
select: {
id: true,
fullName: true,
email: true
}
}
}
});
return res.status(200).json({
success: true,
data: {
projects,
total,
page,
pageSize
}
} as ApiResponse);
} catch (error) {
console.error('Projects list error:', error);
return res.status(500).json({
success: false,
error: 'Internal server error during projects retrieval'
} as ApiResponse);
}
});
// Get my projects endpoint (Developer only)
router.get('/my-projects', authenticateToken, requireDeveloper, validateQuery(paginationSchema), async (req: Request, res: Response) => {
try {
const { page = 1, pageSize = 10 } = req.query as PaginationOptions;
const developerId = req.user!.id;
// Get total count
const total = await prisma.carbonProject.count({
where: { developerId }
});
// Get projects with pagination
const projects = await prisma.carbonProject.findMany({
where: { developerId },
orderBy: { createdAt: 'desc' },
skip: (page - 1) * pageSize,
take: pageSize
});
return res.status(200).json({
success: true,
data: {
projects,
total,
page,
pageSize
}
} as ApiResponse);
} catch (error) {
console.error('My projects error:', error);
return res.status(500).json({
success: false,
error: 'Internal server error during projects retrieval'
} as ApiResponse);
}
});
// Get specific project endpoint
router.get('/:projectId', async (req: Request, res: Response) => {
try {
const projectId = parseInt(req.params.projectId);
if (isNaN(projectId)) {
return res.status(400).json({
success: false,
error: 'Invalid project ID'
} as ApiResponse);
}
const project = await prisma.carbonProject.findFirst({
where: {
id: projectId,
isActive: true
},
include: {
developer: {
select: {
id: true,
fullName: true,
email: true
}
},
offsets: {
select: {
id: true,
serialNumber: true,
quantity: true,
status: true
}
}
}
});
if (!project) {
return res.status(404).json({
success: false,
error: 'Project not found'
} as ApiResponse);
}
return res.status(200).json({
success: true,
data: project
} as ApiResponse);
} catch (error) {
console.error('Project retrieval error:', error);
return res.status(500).json({
success: false,
error: 'Internal server error during project retrieval'
} as ApiResponse);
}
});
// Update project endpoint (Developer only - own projects)
router.put('/:projectId', authenticateToken, requireDeveloper, validate(updateProjectSchema), async (req: Request, res: Response) => {
try {
const projectId = parseInt(req.params.projectId);
const updateData: UpdateProjectRequest = req.body;
const developerId = req.user!.id;
if (isNaN(projectId)) {
return res.status(400).json({
success: false,
error: 'Invalid project ID'
} as ApiResponse);
}
// Check if project exists and belongs to user
const existingProject = await prisma.carbonProject.findFirst({
where: {
id: projectId,
developerId
}
});
if (!existingProject) {
return res.status(404).json({
success: false,
error: 'Project not found or access denied'
} as ApiResponse);
}
// Update project
const project = await prisma.carbonProject.update({
where: { id: projectId },
data: updateData
});
return res.status(200).json({
success: true,
data: project,
message: 'Project updated successfully'
} as ApiResponse);
} catch (error) {
console.error('Project update error:', error);
return res.status(500).json({
success: false,
error: 'Internal server error during project update'
} as ApiResponse);
}
});
// Delete project endpoint (Developer only - own projects)
router.delete('/:projectId', authenticateToken, requireDeveloper, async (req: Request, res: Response) => {
try {
const projectId = parseInt(req.params.projectId);
const developerId = req.user!.id;
if (isNaN(projectId)) {
return res.status(400).json({
success: false,
error: 'Invalid project ID'
} as ApiResponse);
}
// Check if project exists and belongs to user
const existingProject = await prisma.carbonProject.findFirst({
where: {
id: projectId,
developerId
}
});
if (!existingProject) {
return res.status(404).json({
success: false,
error: 'Project not found or access denied'
} as ApiResponse);
}
// Soft delete - mark as inactive
await prisma.carbonProject.update({
where: { id: projectId },
data: { isActive: false }
});
return res.status(200).json({
success: true,
message: 'Project deleted successfully'
} as ApiResponse);
} catch (error) {
console.error('Project deletion error:', error);
return res.status(500).json({
success: false,
error: 'Internal server error during project deletion'
} as ApiResponse);
}
});
export default router;

329
src/routes/trading.ts Normal file
View File

@ -0,0 +1,329 @@
import { Router, Request, Response } from 'express';
import { authenticateToken, requireBuyer } from '@/middleware/auth';
import { validate, validateQuery, purchaseSchema, paginationSchema } from '@/middleware/validation';
import { prisma } from '@/utils/database';
import { PurchaseRequest, ApiResponse, PaginationOptions, FilterOptions } from '@/types';
import { TransactionStatus, OffsetStatus } from '@prisma/client';
import { v4 as uuidv4 } from 'uuid';
const router = Router();
// Purchase carbon offsets endpoint (Buyer only)
router.post('/purchase', authenticateToken, requireBuyer, validate(purchaseSchema), async (req: Request, res: Response) => {
try {
const { projectId, quantity }: PurchaseRequest = req.body;
const buyerId = req.user!.id;
// Check if user has wallet linked
const user = await prisma.user.findUnique({
where: { id: buyerId }
});
if (!user?.walletAddress) {
return res.status(400).json({
success: false,
error: 'Wallet must be linked to purchase carbon offsets'
} as ApiResponse);
}
// Get project
const project = await prisma.carbonProject.findFirst({
where: {
id: projectId,
isActive: true,
verificationStatus: 'VERIFIED'
}
});
if (!project) {
return res.status(404).json({
success: false,
error: 'Project not found or not verified'
} as ApiResponse);
}
// Check if enough credits are available
const availableCredits = project.totalCreditsAvailable - project.creditsSold;
if (quantity > availableCredits) {
return res.status(400).json({
success: false,
error: `Not enough credits available. Available: ${availableCredits}`
} as ApiResponse);
}
// Get available offset
const offset = await prisma.carbonOffset.findFirst({
where: {
projectId,
status: OffsetStatus.AVAILABLE
}
});
if (!offset) {
return res.status(400).json({
success: false,
error: 'No available carbon offsets for this project'
} as ApiResponse);
}
// Calculate total amount
const totalAmount = quantity * project.pricePerCredit;
// Create transaction in a database transaction
const result = await prisma.$transaction(async (tx) => {
// Create transaction record
const transactionHash = `tx_${uuidv4().replace(/-/g, '').substring(0, 16)}`;
const transaction = await tx.transaction.create({
data: {
transactionHash,
quantity,
pricePerCredit: project.pricePerCredit,
totalAmount,
buyerId,
offsetId: offset.id,
status: TransactionStatus.PENDING
}
});
// Update project credits sold
await tx.carbonProject.update({
where: { id: projectId },
data: {
creditsSold: { increment: quantity }
}
});
// Update offset quantity or status
if (offset.quantity <= quantity) {
await tx.carbonOffset.update({
where: { id: offset.id },
data: { status: OffsetStatus.SOLD }
});
} else {
// Update existing offset and create new offset for sold quantity
await tx.carbonOffset.update({
where: { id: offset.id },
data: { quantity: { decrement: quantity } }
});
await tx.carbonOffset.create({
data: {
serialNumber: `CO${projectId}-sold-${transaction.id}`,
vintageYear: offset.vintageYear,
quantity,
status: OffsetStatus.SOLD,
projectId
}
});
}
// Simulate blockchain transaction confirmation
// In a real implementation, you would integrate with actual blockchain
const confirmedTransaction = await tx.transaction.update({
where: { id: transaction.id },
data: {
status: TransactionStatus.CONFIRMED,
confirmedAt: new Date(),
blockNumber: Math.floor(Math.random() * 1000000) + 12000000, // Simulated
gasUsed: 21000 // Simulated
}
});
return confirmedTransaction;
});
return res.status(201).json({
success: true,
data: result,
message: 'Carbon offsets purchased successfully'
} as ApiResponse);
} catch (error) {
console.error('Purchase error:', error);
return res.status(500).json({
success: false,
error: 'Internal server error during purchase'
} as ApiResponse);
}
});
// Get user's transactions endpoint
router.get('/my-transactions', authenticateToken, validateQuery(paginationSchema), async (req: Request, res: Response) => {
try {
const { page = 1, pageSize = 10 } = req.query as PaginationOptions;
const { status } = req.query as FilterOptions;
const buyerId = req.user!.id;
// Build where clause
const where: any = { buyerId };
if (status) where.status = status as TransactionStatus;
// Get total count
const total = await prisma.transaction.count({ where });
// Get transactions with pagination
const transactions = await prisma.transaction.findMany({
where,
orderBy: { createdAt: 'desc' },
skip: (page - 1) * pageSize,
take: pageSize,
include: {
offset: {
include: {
project: {
select: {
id: true,
title: true,
projectType: true,
location: true
}
}
}
}
}
});
return res.status(200).json({
success: true,
data: {
transactions,
total,
page,
pageSize
}
} as ApiResponse);
} catch (error) {
console.error('Transactions retrieval error:', error);
return res.status(500).json({
success: false,
error: 'Internal server error during transactions retrieval'
} as ApiResponse);
}
});
// Get specific transaction endpoint
router.get('/transactions/:transactionId', authenticateToken, async (req: Request, res: Response) => {
try {
const transactionId = parseInt(req.params.transactionId);
const userId = req.user!.id;
if (isNaN(transactionId)) {
return res.status(400).json({
success: false,
error: 'Invalid transaction ID'
} as ApiResponse);
}
const transaction = await prisma.transaction.findFirst({
where: {
id: transactionId,
buyerId: userId
},
include: {
offset: {
include: {
project: {
select: {
id: true,
title: true,
projectType: true,
location: true,
developer: {
select: {
fullName: true,
email: true
}
}
}
}
}
}
}
});
if (!transaction) {
return res.status(404).json({
success: false,
error: 'Transaction not found'
} as ApiResponse);
}
return res.status(200).json({
success: true,
data: transaction
} as ApiResponse);
} catch (error) {
console.error('Transaction retrieval error:', error);
return res.status(500).json({
success: false,
error: 'Internal server error during transaction retrieval'
} as ApiResponse);
}
});
// Get marketplace statistics endpoint
router.get('/marketplace/stats', async (req: Request, res: Response) => {
try {
// Get marketplace statistics
const [
totalProjects,
verifiedProjects,
totalTransactions,
totalVolumeResult,
availableCreditsResult
] = await Promise.all([
prisma.carbonProject.count({
where: { isActive: true }
}),
prisma.carbonProject.count({
where: {
isActive: true,
verificationStatus: 'VERIFIED'
}
}),
prisma.transaction.count({
where: { status: TransactionStatus.CONFIRMED }
}),
prisma.transaction.aggregate({
where: { status: TransactionStatus.CONFIRMED },
_sum: { totalAmount: true }
}),
prisma.carbonProject.aggregate({
where: { isActive: true },
_sum: {
totalCreditsAvailable: true,
creditsSold: true
}
})
]);
const totalCreditsAvailable = (availableCreditsResult._sum.totalCreditsAvailable || 0) -
(availableCreditsResult._sum.creditsSold || 0);
const totalVolumeTraded = totalVolumeResult._sum.totalAmount || 0;
const stats = {
totalProjects,
verifiedProjects,
totalCreditsAvailable,
totalTransactions,
totalVolumeTraded
};
return res.status(200).json({
success: true,
data: stats
} as ApiResponse);
} catch (error) {
console.error('Marketplace stats error:', error);
return res.status(500).json({
success: false,
error: 'Internal server error during marketplace stats retrieval'
} as ApiResponse);
}
});
export default router;

119
src/routes/wallet.ts Normal file
View File

@ -0,0 +1,119 @@
import { Router, Request, Response } from 'express';
import { authenticateToken } from '@/middleware/auth';
import { validate, walletLinkSchema } from '@/middleware/validation';
import { walletService } from '@/services/wallet';
import { WalletLinkRequest, ApiResponse } from '@/types';
const router = Router();
// All wallet routes require authentication
router.use(authenticateToken);
// Link wallet endpoint
router.post('/link', validate(walletLinkSchema), async (req: Request, res: Response) => {
try {
const { walletAddress }: WalletLinkRequest = req.body;
const userId = req.user!.id;
const result = await walletService.linkWallet(userId, walletAddress);
if (!result.success) {
return res.status(400).json({
success: false,
error: result.message
} as ApiResponse);
}
return res.status(200).json({
success: true,
data: result,
message: result.message
} as ApiResponse);
} catch (error) {
console.error('Wallet link error:', error);
return res.status(500).json({
success: false,
error: 'Internal server error during wallet linking'
} as ApiResponse);
}
});
// Unlink wallet endpoint
router.delete('/unlink', async (req: Request, res: Response) => {
try {
const userId = req.user!.id;
const result = await walletService.unlinkWallet(userId);
if (!result.success) {
return res.status(400).json({
success: false,
error: result.message
} as ApiResponse);
}
return res.status(200).json({
success: true,
data: result,
message: result.message
} as ApiResponse);
} catch (error) {
console.error('Wallet unlink error:', error);
return res.status(500).json({
success: false,
error: 'Internal server error during wallet unlinking'
} as ApiResponse);
}
});
// Get wallet info endpoint
router.get('/info', async (req: Request, res: Response) => {
try {
const userId = req.user!.id;
const result = await walletService.getWalletInfo(userId);
if (!result.success) {
return res.status(400).json({
success: false,
error: result.message
} as ApiResponse);
}
return res.status(200).json({
success: true,
data: result
} as ApiResponse);
} catch (error) {
console.error('Wallet info error:', error);
return res.status(500).json({
success: false,
error: 'Internal server error during wallet info retrieval'
} as ApiResponse);
}
});
// Generate test wallet endpoint
router.post('/generate-test-wallet', async (req: Request, res: Response) => {
try {
const result = walletService.generateTestWallet();
return res.status(200).json({
success: true,
data: result,
message: result.message
} as ApiResponse);
} catch (error) {
console.error('Test wallet generation error:', error);
return res.status(500).json({
success: false,
error: 'Internal server error during test wallet generation'
} as ApiResponse);
}
});
export default router;

214
src/server.ts Normal file
View File

@ -0,0 +1,214 @@
import express, { Request, Response } from 'express';
import compression from 'compression';
import morgan from 'morgan';
import dotenv from 'dotenv';
import Database from '@/utils/database';
import { setupSecurity } from '@/middleware/security';
import { errorHandler, notFoundHandler } from '@/middleware/error';
import { blockchainService } from '@/services/blockchain';
import { ApiResponse } from '@/types';
// Import routes
import authRoutes from '@/routes/auth';
import walletRoutes from '@/routes/wallet';
import projectRoutes from '@/routes/projects';
import tradingRoutes from '@/routes/trading';
// Load environment variables
dotenv.config();
class Server {
private app: express.Application;
private port: number;
constructor() {
this.app = express();
this.port = parseInt(process.env.PORT || '8000');
this.setupMiddleware();
this.setupRoutes();
this.setupErrorHandling();
}
private setupMiddleware(): void {
// Security middleware
setupSecurity(this.app);
// Compression middleware
this.app.use(compression());
// Logging middleware
if (process.env.NODE_ENV !== 'test') {
this.app.use(morgan('combined'));
}
// Body parsing middleware
this.app.use(express.json({ limit: '10mb' }));
this.app.use(express.urlencoded({ extended: true, limit: '10mb' }));
}
private setupRoutes(): void {
// Health check endpoint
this.app.get('/health', async (req: Request, res: Response) => {
try {
// Check database connection
const dbHealthy = await Database.healthCheck();
// Check blockchain connection (optional)
const blockchainHealthy = await blockchainService.isNetworkConnected();
// Check environment variables
const requiredEnvVars = ['JWT_SECRET'];
const missingEnvVars = requiredEnvVars.filter(env => !process.env[env]);
const envHealthy = missingEnvVars.length === 0;
const isHealthy = dbHealthy && envHealthy;
const healthData = {
status: isHealthy ? 'healthy' : 'unhealthy',
timestamp: new Date().toISOString(),
version: '1.0.0',
environment: process.env.NODE_ENV || 'development',
services: {
database: dbHealthy ? 'healthy' : 'unhealthy',
blockchain: blockchainHealthy ? 'connected' : 'disconnected',
environment: envHealthy ? 'healthy' : `missing: ${missingEnvVars.join(', ')}`
}
};
return res.status(isHealthy ? 200 : 503).json({
success: isHealthy,
data: healthData
} as ApiResponse);
} catch (error) {
console.error('Health check error:', error);
return res.status(503).json({
success: false,
error: 'Health check failed',
data: {
status: 'unhealthy',
timestamp: new Date().toISOString(),
version: '1.0.0'
}
} as ApiResponse);
}
});
// Root endpoint with platform information
this.app.get('/', (req: Request, res: Response) => {
return res.status(200).json({
success: true,
data: {
title: 'Carbon Offset Trading Platform',
description: 'A blockchain-enabled platform for trading carbon offsets between project developers and buyers',
version: '1.0.0',
documentation: '/api/docs',
healthCheck: '/health',
endpoints: {
auth: '/api/auth',
wallet: '/api/wallet',
projects: '/api/projects',
trading: '/api/trading'
}
}
} as ApiResponse);
});
// API routes
this.app.use('/api/auth', authRoutes);
this.app.use('/api/wallet', walletRoutes);
this.app.use('/api/projects', projectRoutes);
this.app.use('/api/trading', tradingRoutes);
// API documentation placeholder
this.app.get('/api/docs', (req: Request, res: Response) => {
return res.status(200).json({
success: true,
data: {
message: 'API Documentation',
version: '1.0.0',
endpoints: {
'POST /api/auth/register': 'Register new user (developer or buyer)',
'POST /api/auth/login': 'User login',
'POST /api/wallet/link': 'Link blockchain wallet to user account',
'DELETE /api/wallet/unlink': 'Unlink wallet from user account',
'GET /api/wallet/info': 'Get wallet information and balance',
'POST /api/wallet/generate-test-wallet': 'Generate test wallet for development',
'POST /api/projects': 'Create new carbon offset project (developers only)',
'GET /api/projects': 'Browse all available projects',
'GET /api/projects/my-projects': 'Get developer\'s projects',
'GET /api/projects/:id': 'Get project details',
'PUT /api/projects/:id': 'Update project (own projects only)',
'DELETE /api/projects/:id': 'Delete project (own projects only)',
'POST /api/trading/purchase': 'Purchase carbon offsets (buyers only)',
'GET /api/trading/my-transactions': 'Get user\'s transactions',
'GET /api/trading/transactions/:id': 'Get transaction details',
'GET /api/trading/marketplace/stats': 'Get marketplace statistics'
}
}
} as ApiResponse);
});
}
private setupErrorHandling(): void {
// 404 handler
this.app.use(notFoundHandler);
// Global error handler
this.app.use(errorHandler);
}
public async start(): Promise<void> {
try {
// Connect to database
await Database.connect();
// Start server
this.app.listen(this.port, '0.0.0.0', () => {
console.log('🚀 Carbon Offset Trading Platform Started');
console.log(`📍 Server running on http://0.0.0.0:${this.port}`);
console.log(`🏥 Health check: http://0.0.0.0:${this.port}/health`);
console.log(`📚 API docs: http://0.0.0.0:${this.port}/api/docs`);
console.log(`🌍 Environment: ${process.env.NODE_ENV || 'development'}`);
});
// Handle graceful shutdown
this.setupGracefulShutdown();
} catch (error) {
console.error('❌ Failed to start server:', error);
process.exit(1);
}
}
private setupGracefulShutdown(): void {
const shutdown = async (signal: string) => {
console.log(`\n🛑 Received ${signal}. Starting graceful shutdown...`);
try {
await Database.disconnect();
console.log('✅ Database disconnected');
console.log('✅ Server shut down gracefully');
process.exit(0);
} catch (error) {
console.error('❌ Error during shutdown:', error);
process.exit(1);
}
};
process.on('SIGINT', () => shutdown('SIGINT'));
process.on('SIGTERM', () => shutdown('SIGTERM'));
}
}
// Start server if this file is run directly
if (require.main === module) {
const server = new Server();
server.start().catch(error => {
console.error('❌ Failed to start application:', error);
process.exit(1);
});
}
export default Server;

188
src/services/blockchain.ts Normal file
View File

@ -0,0 +1,188 @@
import Web3 from 'web3';
import { ethers } from 'ethers';
import { BlockchainTransaction, WalletInfo } from '@/types';
export class BlockchainService {
private web3: Web3;
private provider: ethers.JsonRpcProvider;
private contractAbi: any[];
constructor() {
const rpcUrl = process.env.BLOCKCHAIN_RPC_URL || 'http://localhost:8545';
this.web3 = new Web3(rpcUrl);
this.provider = new ethers.JsonRpcProvider(rpcUrl);
this.contractAbi = this.getCarbonTokenAbi();
}
private getCarbonTokenAbi(): any[] {
// 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'
}
];
}
public validateWalletAddress(address: string): boolean {
try {
return this.web3.utils.isAddress(address);
} catch (error) {
console.error('Error validating wallet address:', error);
return false;
}
}
public generateWallet(): WalletInfo {
try {
const wallet = ethers.Wallet.createRandom();
return {
address: wallet.address,
privateKey: wallet.privateKey,
publicKey: wallet.publicKey
};
} catch (error) {
console.error('Error generating wallet:', error);
throw new Error('Failed to generate wallet');
}
}
public async getWalletBalance(address: string): Promise<number | null> {
try {
if (!this.validateWalletAddress(address)) {
return null;
}
const balanceWei = await this.web3.eth.getBalance(address);
const balanceEth = this.web3.utils.fromWei(balanceWei, 'ether');
return parseFloat(balanceEth);
} catch (error) {
console.error(`Error getting balance for ${address}:`, error);
return null;
}
}
public async createCarbonTokenTransaction(
contractAddress: string,
fromAddress: string,
toAddress: string,
tokenId: number
): Promise<BlockchainTransaction | null> {
try {
if (!this.validateWalletAddress(contractAddress) ||
!this.validateWalletAddress(fromAddress) ||
!this.validateWalletAddress(toAddress)) {
return null;
}
const contract = new this.web3.eth.Contract(this.contractAbi, contractAddress);
const data = contract.methods.transferFrom(fromAddress, toAddress, tokenId).encodeABI();
const gasPrice = await this.web3.eth.getGasPrice();
const gasLimit = await contract.methods.transferFrom(fromAddress, toAddress, tokenId)
.estimateGas({ from: fromAddress });
return {
to: contractAddress,
value: '0',
data,
gasLimit: gasLimit.toString(),
gasPrice: gasPrice.toString()
};
} catch (error) {
console.error('Error creating transaction:', error);
return null;
}
}
public async signAndSendTransaction(
transaction: BlockchainTransaction,
privateKey: string
): Promise<string | null> {
try {
const wallet = new ethers.Wallet(privateKey, this.provider);
const tx = await wallet.sendTransaction({
to: transaction.to,
value: transaction.value,
data: transaction.data,
gasLimit: transaction.gasLimit,
gasPrice: transaction.gasPrice
});
return tx.hash;
} catch (error) {
console.error('Error signing/sending transaction:', error);
return null;
}
}
public async getTransactionReceipt(txHash: string): Promise<any | null> {
try {
const receipt = await this.web3.eth.getTransactionReceipt(txHash);
return {
transactionHash: receipt.transactionHash,
blockNumber: receipt.blockNumber,
gasUsed: receipt.gasUsed,
status: receipt.status
};
} catch (error) {
console.error('Error getting transaction receipt:', error);
return null;
}
}
public async verifyTokenOwnership(
contractAddress: string,
tokenId: number,
ownerAddress: string
): Promise<boolean> {
try {
if (!this.validateWalletAddress(contractAddress) ||
!this.validateWalletAddress(ownerAddress)) {
return false;
}
const contract = new this.web3.eth.Contract(this.contractAbi, contractAddress);
const actualOwner = await contract.methods.ownerOf(tokenId).call();
return actualOwner.toLowerCase() === ownerAddress.toLowerCase();
} catch (error) {
console.error('Error verifying ownership:', error);
return false;
}
}
public async isNetworkConnected(): Promise<boolean> {
try {
await this.web3.eth.getBlockNumber();
return true;
} catch (error) {
console.error('Blockchain network connection failed:', error);
return false;
}
}
}
export const blockchainService = new BlockchainService();

179
src/services/wallet.ts Normal file
View File

@ -0,0 +1,179 @@
import { prisma } from '@/utils/database';
import { blockchainService } from './blockchain';
import { WalletResponse, WalletInfo } from '@/types';
export class WalletService {
public async linkWallet(userId: number, walletAddress: string): Promise<WalletResponse> {
try {
// Validate wallet address format
if (!blockchainService.validateWalletAddress(walletAddress)) {
return {
success: false,
walletLinked: false,
message: 'Invalid wallet address format'
};
}
// Check if wallet is already linked to another user
const existingUser = await prisma.user.findFirst({
where: {
walletAddress,
id: { not: userId }
}
});
if (existingUser) {
return {
success: false,
walletLinked: false,
message: 'Wallet address is already linked to another account'
};
}
// Get user
const user = await prisma.user.findUnique({
where: { id: userId }
});
if (!user) {
return {
success: false,
walletLinked: false,
message: 'User not found'
};
}
// Update user wallet information
await prisma.user.update({
where: { id: userId },
data: {
walletAddress,
walletPublicKey: walletAddress // In Ethereum, address is derived from public key
}
});
return {
success: true,
walletLinked: true,
walletAddress,
message: 'Wallet linked successfully'
};
} catch (error) {
console.error('Error linking wallet:', error);
return {
success: false,
walletLinked: false,
message: `Database error: ${error}`
};
}
}
public async unlinkWallet(userId: number): Promise<WalletResponse> {
try {
const user = await prisma.user.findUnique({
where: { id: userId }
});
if (!user) {
return {
success: false,
walletLinked: false,
message: 'User not found'
};
}
if (!user.walletAddress) {
return {
success: false,
walletLinked: false,
message: 'No wallet linked to this account'
};
}
// Clear wallet information
await prisma.user.update({
where: { id: userId },
data: {
walletAddress: null,
walletPublicKey: null
}
});
return {
success: true,
walletLinked: false,
message: 'Wallet unlinked successfully'
};
} catch (error) {
console.error('Error unlinking wallet:', error);
return {
success: false,
walletLinked: false,
message: `Database error: ${error}`
};
}
}
public async getWalletInfo(userId: number): Promise<WalletResponse> {
try {
const user = await prisma.user.findUnique({
where: { id: userId }
});
if (!user) {
return {
success: false,
walletLinked: false,
message: 'User not found'
};
}
if (!user.walletAddress) {
return {
success: true,
walletLinked: false,
walletAddress: undefined,
balance: undefined
};
}
// Get wallet balance from blockchain
const balance = await blockchainService.getWalletBalance(user.walletAddress);
return {
success: true,
walletLinked: true,
walletAddress: user.walletAddress,
balance: balance || undefined
};
} catch (error) {
console.error('Error getting wallet info:', error);
return {
success: false,
walletLinked: false,
message: `Error retrieving wallet information: ${error}`
};
}
}
public generateTestWallet(): { success: boolean; message: string; walletData?: WalletInfo; warning?: string } {
try {
const walletData = blockchainService.generateWallet();
return {
success: true,
message: 'Test wallet generated successfully',
walletData,
warning: 'This is for testing only. Keep private key secure!'
};
} catch (error) {
console.error('Error generating test wallet:', error);
return {
success: false,
message: `Failed to generate test wallet: ${error}`
};
}
}
}
export const walletService = new WalletService();

184
src/types/index.ts Normal file
View File

@ -0,0 +1,184 @@
import { UserType, VerificationStatus, OffsetStatus, TransactionStatus } from '@prisma/client';
// User Types
export interface CreateUserRequest {
email: string;
password: string;
fullName: string;
userType: UserType;
}
export interface LoginRequest {
email: string;
password: string;
}
export interface UserResponse {
id: number;
email: string;
fullName: string;
userType: UserType;
isActive: boolean;
createdAt: Date;
walletAddress?: string;
}
export interface JwtPayload {
userId: number;
email: string;
userType: UserType;
}
// Wallet Types
export interface WalletLinkRequest {
walletAddress: string;
}
export interface WalletResponse {
success: boolean;
walletLinked: boolean;
walletAddress?: string;
balance?: number;
message?: string;
}
// Carbon Project Types
export interface CreateProjectRequest {
title: string;
description: string;
location: string;
projectType: string;
methodology: string;
totalCreditsAvailable: number;
pricePerCredit: number;
startDate: Date;
endDate: Date;
}
export interface UpdateProjectRequest {
title?: string;
description?: string;
location?: string;
projectType?: string;
methodology?: string;
totalCreditsAvailable?: number;
pricePerCredit?: number;
startDate?: Date;
endDate?: Date;
verificationDocumentUrl?: string;
}
export interface ProjectResponse {
id: number;
title: string;
description: string;
location: string;
projectType: string;
methodology: string;
totalCreditsAvailable: number;
creditsSold: number;
pricePerCredit: number;
startDate: Date;
endDate: Date;
verificationStatus: VerificationStatus;
verificationDocumentUrl?: string;
isActive: boolean;
contractAddress?: string;
tokenId?: string;
createdAt: Date;
updatedAt: Date;
developerId: number;
}
export interface ProjectListResponse {
projects: ProjectResponse[];
total: number;
page: number;
pageSize: number;
}
// Transaction Types
export interface PurchaseRequest {
projectId: number;
quantity: number;
}
export interface TransactionResponse {
id: number;
transactionHash: string;
quantity: number;
pricePerCredit: number;
totalAmount: number;
status: TransactionStatus;
blockNumber?: number;
gasUsed?: number;
createdAt: Date;
confirmedAt?: Date;
buyerId: number;
offsetId: number;
}
export interface TransactionListResponse {
transactions: TransactionResponse[];
total: number;
page: number;
pageSize: number;
}
// Marketplace Types
export interface MarketplaceStats {
totalProjects: number;
verifiedProjects: number;
totalCreditsAvailable: number;
totalTransactions: number;
totalVolumeTraded: number;
}
// Blockchain Types
export interface BlockchainTransaction {
to: string;
value: string;
data: string;
gasLimit: string;
gasPrice: string;
}
export interface WalletInfo {
address: string;
privateKey: string;
publicKey: string;
}
// API Response Types
export interface ApiResponse<T = any> {
success: boolean;
data?: T;
message?: string;
error?: string;
}
export interface PaginationOptions {
page?: number;
pageSize?: number;
}
export interface FilterOptions {
projectType?: string;
verificationStatus?: VerificationStatus;
status?: TransactionStatus;
}
// Express Request Extensions
declare global {
namespace Express {
interface Request {
user?: {
id: number;
email: string;
userType: UserType;
};
}
}
}
export { UserType, VerificationStatus, OffsetStatus, TransactionStatus };

89
src/utils/auth.ts Normal file
View File

@ -0,0 +1,89 @@
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import { JwtPayload, UserType } from '@/types';
export class AuthUtils {
private static readonly SALT_ROUNDS = 12;
private static readonly JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production';
private static readonly JWT_EXPIRES_IN = '24h';
public static async hashPassword(password: string): Promise<string> {
try {
return await bcrypt.hash(password, this.SALT_ROUNDS);
} catch (error) {
console.error('Error hashing password:', error);
throw new Error('Failed to hash password');
}
}
public static async verifyPassword(password: string, hashedPassword: string): Promise<boolean> {
try {
return await bcrypt.compare(password, hashedPassword);
} catch (error) {
console.error('Error verifying password:', error);
return false;
}
}
public static generateAccessToken(payload: JwtPayload): string {
try {
return jwt.sign(payload, this.JWT_SECRET, {
expiresIn: this.JWT_EXPIRES_IN,
issuer: 'carbon-offset-platform',
audience: 'carbon-offset-users'
});
} catch (error) {
console.error('Error generating access token:', error);
throw new Error('Failed to generate access token');
}
}
public static verifyAccessToken(token: string): JwtPayload | null {
try {
const decoded = jwt.verify(token, this.JWT_SECRET, {
issuer: 'carbon-offset-platform',
audience: 'carbon-offset-users'
}) as JwtPayload;
return decoded;
} catch (error) {
if (error instanceof jwt.TokenExpiredError) {
console.warn('JWT token expired');
} else if (error instanceof jwt.JsonWebTokenError) {
console.warn('Invalid JWT token');
} else {
console.error('Error verifying access token:', error);
}
return null;
}
}
public static validateEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
public static validatePassword(password: string): { isValid: boolean; message?: string } {
if (password.length < 8) {
return { isValid: false, message: 'Password must be at least 8 characters long' };
}
if (!/(?=.*[a-z])/.test(password)) {
return { isValid: false, message: 'Password must contain at least one lowercase letter' };
}
if (!/(?=.*[A-Z])/.test(password)) {
return { isValid: false, message: 'Password must contain at least one uppercase letter' };
}
if (!/(?=.*\d)/.test(password)) {
return { isValid: false, message: 'Password must contain at least one number' };
}
return { isValid: true };
}
public static validateUserType(userType: string): boolean {
return Object.values(UserType).includes(userType as UserType);
}
}

47
src/utils/database.ts Normal file
View File

@ -0,0 +1,47 @@
import { PrismaClient } from '@prisma/client';
class Database {
private static instance: PrismaClient;
public static getInstance(): PrismaClient {
if (!Database.instance) {
Database.instance = new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['query', 'info', 'warn', 'error'] : ['warn', 'error'],
});
}
return Database.instance;
}
public static async connect(): Promise<void> {
try {
await Database.getInstance().$connect();
console.log('📦 Database connected successfully');
} catch (error) {
console.error('❌ Database connection failed:', error);
throw error;
}
}
public static async disconnect(): Promise<void> {
try {
await Database.getInstance().$disconnect();
console.log('📦 Database disconnected successfully');
} catch (error) {
console.error('❌ Database disconnection failed:', error);
throw error;
}
}
public static async healthCheck(): Promise<boolean> {
try {
await Database.getInstance().$queryRaw`SELECT 1`;
return true;
} catch (error) {
console.error('❌ Database health check failed:', error);
return false;
}
}
}
export const prisma = Database.getInstance();
export default Database;

44
tsconfig.json Normal file
View File

@ -0,0 +1,44 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"removeComments": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitThis": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"baseUrl": "./src",
"paths": {
"@/*": ["./*"],
"@/types/*": ["./types/*"],
"@/models/*": ["./models/*"],
"@/services/*": ["./services/*"],
"@/routes/*": ["./routes/*"],
"@/middleware/*": ["./middleware/*"],
"@/utils/*": ["./utils/*"]
}
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist",
"**/*.test.ts"
]
}