chore: enhance security configuration across applications
- Add root-level *.json to .gitignore to prevent wallet backup leaks - Replace wildcard CORS origins with explicit localhost URLs across all apps - Add OPTIONS method to CORS allowed methods for preflight requests - Update coordinator database to use absolute path in data/ directory to prevent duplicates - Add JWT secret validation in coordinator config (must be set via environment) - Replace deprecated get_session dependency with Session
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -150,6 +150,9 @@ home/client/client_wallet.json
|
|||||||
home/genesis_wallet.json
|
home/genesis_wallet.json
|
||||||
home/miner/miner_wallet.json
|
home/miner/miner_wallet.json
|
||||||
|
|
||||||
|
# Root-level wallet backups (contain private keys)
|
||||||
|
*.json
|
||||||
|
|
||||||
# ===================
|
# ===================
|
||||||
# Stale source copies
|
# Stale source copies
|
||||||
# ===================
|
# ===================
|
||||||
|
|||||||
@@ -111,8 +111,13 @@ def create_app() -> FastAPI:
|
|||||||
app.add_middleware(RateLimitMiddleware, max_requests=200, window_seconds=60)
|
app.add_middleware(RateLimitMiddleware, max_requests=200, window_seconds=60)
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=["*"],
|
allow_origins=[
|
||||||
allow_methods=["GET", "POST"],
|
"http://localhost:3000",
|
||||||
|
"http://localhost:8080",
|
||||||
|
"http://localhost:8000",
|
||||||
|
"http://localhost:8011"
|
||||||
|
],
|
||||||
|
allow_methods=["GET", "POST", "OPTIONS"],
|
||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -70,7 +70,16 @@ def create_app() -> Starlette:
|
|||||||
]
|
]
|
||||||
|
|
||||||
middleware = [
|
middleware = [
|
||||||
Middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"])
|
Middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=[
|
||||||
|
"http://localhost:3000",
|
||||||
|
"http://localhost:8080",
|
||||||
|
"http://localhost:8000",
|
||||||
|
"http://localhost:8011"
|
||||||
|
],
|
||||||
|
allow_methods=["POST", "GET", "OPTIONS"]
|
||||||
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
return Starlette(routes=routes, middleware=middleware)
|
return Starlette(routes=routes, middleware=middleware)
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
from pathlib import Path
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
@@ -9,14 +11,35 @@ class Settings(BaseSettings):
|
|||||||
app_host: str = "127.0.0.1"
|
app_host: str = "127.0.0.1"
|
||||||
app_port: int = 8011
|
app_port: int = 8011
|
||||||
|
|
||||||
database_url: str = "sqlite:///./coordinator.db"
|
# Use absolute path to avoid database duplicates in different working directories
|
||||||
|
@property
|
||||||
|
def database_url(self) -> str:
|
||||||
|
# Find project root by looking for .git directory
|
||||||
|
current = Path(__file__).resolve()
|
||||||
|
while current.parent != current:
|
||||||
|
if (current / ".git").exists():
|
||||||
|
project_root = current
|
||||||
|
break
|
||||||
|
current = current.parent
|
||||||
|
else:
|
||||||
|
# Fallback to relative path if .git not found
|
||||||
|
project_root = Path(__file__).resolve().parents[3]
|
||||||
|
|
||||||
|
db_path = project_root / "data" / "coordinator.db"
|
||||||
|
db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
return f"sqlite:///{db_path}"
|
||||||
|
|
||||||
client_api_keys: List[str] = []
|
client_api_keys: List[str] = []
|
||||||
miner_api_keys: List[str] = []
|
miner_api_keys: List[str] = []
|
||||||
admin_api_keys: List[str] = []
|
admin_api_keys: List[str] = []
|
||||||
|
|
||||||
hmac_secret: Optional[str] = None
|
hmac_secret: Optional[str] = None
|
||||||
allow_origins: List[str] = ["*"]
|
allow_origins: List[str] = [
|
||||||
|
"http://localhost:3000",
|
||||||
|
"http://localhost:8080",
|
||||||
|
"http://localhost:8000",
|
||||||
|
"http://localhost:8011"
|
||||||
|
]
|
||||||
|
|
||||||
job_ttl_seconds: int = 900
|
job_ttl_seconds: int = 900
|
||||||
heartbeat_interval_seconds: int = 10
|
heartbeat_interval_seconds: int = 10
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class Settings(BaseSettings):
|
|||||||
database_url: str = "postgresql://localhost:5432/aitbc_coordinator"
|
database_url: str = "postgresql://localhost:5432/aitbc_coordinator"
|
||||||
|
|
||||||
# JWT Configuration
|
# JWT Configuration
|
||||||
jwt_secret: str = "change-me-in-production"
|
jwt_secret: str = "" # Must be provided via environment
|
||||||
jwt_algorithm: str = "HS256"
|
jwt_algorithm: str = "HS256"
|
||||||
jwt_expiration_hours: int = 24
|
jwt_expiration_hours: int = 24
|
||||||
|
|
||||||
@@ -51,7 +51,17 @@ class Settings(BaseSettings):
|
|||||||
class Config:
|
class Config:
|
||||||
env_file = ".env"
|
env_file = ".env"
|
||||||
env_file_encoding = "utf-8"
|
env_file_encoding = "utf-8"
|
||||||
|
|
||||||
|
def validate_secrets(self) -> None:
|
||||||
|
"""Validate that all required secrets are provided"""
|
||||||
|
if not self.jwt_secret:
|
||||||
|
raise ValueError("JWT_SECRET environment variable is required")
|
||||||
|
if self.jwt_secret == "change-me-in-production":
|
||||||
|
raise ValueError("JWT_SECRET must be changed from default value")
|
||||||
|
|
||||||
|
|
||||||
# Create global settings instance
|
# Create global settings instance
|
||||||
settings = Settings()
|
settings = Settings()
|
||||||
|
|
||||||
|
# Validate secrets on import
|
||||||
|
settings.validate_secrets()
|
||||||
|
|||||||
@@ -1,21 +1,9 @@
|
|||||||
from typing import Callable, Generator, Annotated
|
from typing import Callable, Annotated
|
||||||
from fastapi import Depends, Header, HTTPException
|
from fastapi import Depends, Header, HTTPException
|
||||||
from sqlmodel import Session
|
|
||||||
|
|
||||||
from .config import settings
|
from .config import settings
|
||||||
|
|
||||||
|
|
||||||
def get_session() -> Generator[Session, None, None]:
|
|
||||||
"""Get database session"""
|
|
||||||
from .database import engine
|
|
||||||
with Session(engine) as session:
|
|
||||||
yield session
|
|
||||||
|
|
||||||
|
|
||||||
# Type alias for session dependency
|
|
||||||
SessionDep = Annotated[Session, Depends(get_session)]
|
|
||||||
|
|
||||||
|
|
||||||
class APIKeyValidator:
|
class APIKeyValidator:
|
||||||
def __init__(self, allowed_keys: list[str]):
|
def __init__(self, allowed_keys: list[str]):
|
||||||
self.allowed_keys = {key.strip() for key in allowed_keys if key}
|
self.allowed_keys = {key.strip() for key in allowed_keys if key}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ from fastapi.middleware.cors import CORSMiddleware
|
|||||||
from prometheus_client import make_asgi_app
|
from prometheus_client import make_asgi_app
|
||||||
|
|
||||||
from .config import settings
|
from .config import settings
|
||||||
from .database import create_db_and_tables
|
|
||||||
from .storage import init_db
|
from .storage import init_db
|
||||||
from .routers import (
|
from .routers import (
|
||||||
client,
|
client,
|
||||||
@@ -38,8 +37,8 @@ def create_app() -> FastAPI:
|
|||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=settings.allow_origins,
|
allow_origins=settings.allow_origins,
|
||||||
allow_credentials=True,
|
allow_credentials=True,
|
||||||
allow_methods=["*"],
|
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||||
allow_headers=["*"]
|
allow_headers=["*"] # Allow all headers for API keys and content types
|
||||||
)
|
)
|
||||||
|
|
||||||
app.include_router(client, prefix="/v1")
|
app.include_router(client, prefix="/v1")
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import time
|
|||||||
import hashlib
|
import hashlib
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from ..deps import get_session
|
from ..storage import SessionDep
|
||||||
from ..domain import User, Wallet
|
from ..domain import User, Wallet
|
||||||
from ..schemas import UserCreate, UserLogin, UserProfile, UserBalance
|
from ..schemas import UserCreate, UserLogin, UserProfile, UserBalance
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ def verify_session_token(token: str) -> Optional[str]:
|
|||||||
@router.post("/register", response_model=UserProfile)
|
@router.post("/register", response_model=UserProfile)
|
||||||
async def register_user(
|
async def register_user(
|
||||||
user_data: UserCreate,
|
user_data: UserCreate,
|
||||||
session: Session = Depends(get_session)
|
session: SessionDep
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Register a new user"""
|
"""Register a new user"""
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ async def register_user(
|
|||||||
@router.post("/login", response_model=UserProfile)
|
@router.post("/login", response_model=UserProfile)
|
||||||
async def login_user(
|
async def login_user(
|
||||||
login_data: UserLogin,
|
login_data: UserLogin,
|
||||||
session: Session = Depends(get_session)
|
session: SessionDep
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Login user with wallet address"""
|
"""Login user with wallet address"""
|
||||||
|
|
||||||
@@ -161,7 +161,7 @@ async def login_user(
|
|||||||
@router.get("/users/me", response_model=UserProfile)
|
@router.get("/users/me", response_model=UserProfile)
|
||||||
async def get_current_user(
|
async def get_current_user(
|
||||||
token: str,
|
token: str,
|
||||||
session: Session = Depends(get_session)
|
session: SessionDep
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Get current user profile"""
|
"""Get current user profile"""
|
||||||
|
|
||||||
@@ -190,7 +190,7 @@ async def get_current_user(
|
|||||||
@router.get("/users/{user_id}/balance", response_model=UserBalance)
|
@router.get("/users/{user_id}/balance", response_model=UserBalance)
|
||||||
async def get_user_balance(
|
async def get_user_balance(
|
||||||
user_id: str,
|
user_id: str,
|
||||||
session: Session = Depends(get_session)
|
session: SessionDep
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Get user's AITBC balance"""
|
"""Get user's AITBC balance"""
|
||||||
|
|
||||||
@@ -223,7 +223,7 @@ async def logout_user(token: str) -> Dict[str, str]:
|
|||||||
@router.get("/users/{user_id}/transactions")
|
@router.get("/users/{user_id}/transactions")
|
||||||
async def get_user_transactions(
|
async def get_user_transactions(
|
||||||
user_id: str,
|
user_id: str,
|
||||||
session: Session = Depends(get_session)
|
session: SessionDep
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Get user's transaction history"""
|
"""Get user's transaction history"""
|
||||||
|
|
||||||
|
|||||||
@@ -30,12 +30,16 @@ Base = declarative_base()
|
|||||||
# Direct PostgreSQL connection for performance
|
# Direct PostgreSQL connection for performance
|
||||||
def get_pg_connection():
|
def get_pg_connection():
|
||||||
"""Get direct PostgreSQL connection"""
|
"""Get direct PostgreSQL connection"""
|
||||||
|
# Parse database URL from settings
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
parsed = urlparse(settings.database_url)
|
||||||
|
|
||||||
return psycopg2.connect(
|
return psycopg2.connect(
|
||||||
host="localhost",
|
host=parsed.hostname or "localhost",
|
||||||
database="aitbc_coordinator",
|
database=parsed.path[1:] if parsed.path else "aitbc_coordinator",
|
||||||
user="aitbc_user",
|
user=parsed.username or "aitbc_user",
|
||||||
password="aitbc_password",
|
password=parsed.password or "aitbc_password",
|
||||||
port=5432,
|
port=parsed.port or 5432,
|
||||||
cursor_factory=RealDictCursor
|
cursor_factory=RealDictCursor
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -194,8 +198,16 @@ class PostgreSQLAdapter:
|
|||||||
if self.connection:
|
if self.connection:
|
||||||
self.connection.close()
|
self.connection.close()
|
||||||
|
|
||||||
# Global adapter instance
|
# Global adapter instance (lazy initialization)
|
||||||
db_adapter = PostgreSQLAdapter()
|
db_adapter: Optional[PostgreSQLAdapter] = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_db_adapter() -> PostgreSQLAdapter:
|
||||||
|
"""Get or create database adapter instance"""
|
||||||
|
global db_adapter
|
||||||
|
if db_adapter is None:
|
||||||
|
db_adapter = PostgreSQLAdapter()
|
||||||
|
return db_adapter
|
||||||
|
|
||||||
# Database initialization
|
# Database initialization
|
||||||
def init_db():
|
def init_db():
|
||||||
@@ -212,7 +224,8 @@ def init_db():
|
|||||||
def check_db_health() -> Dict[str, Any]:
|
def check_db_health() -> Dict[str, Any]:
|
||||||
"""Check database health"""
|
"""Check database health"""
|
||||||
try:
|
try:
|
||||||
result = db_adapter.execute_query("SELECT 1 as health_check")
|
adapter = get_db_adapter()
|
||||||
|
result = adapter.execute_query("SELECT 1 as health_check")
|
||||||
return {
|
return {
|
||||||
"status": "healthy",
|
"status": "healthy",
|
||||||
"database": "postgresql",
|
"database": "postgresql",
|
||||||
|
|||||||
@@ -5,11 +5,14 @@ FastAPI backend for the AITBC Trade Exchange
|
|||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
from fastapi import FastAPI, Depends, HTTPException, status
|
from fastapi import FastAPI, Depends, HTTPException, status, Header
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from sqlalchemy import desc, func, and_
|
from sqlalchemy import desc, func, and_
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
import hashlib
|
||||||
|
import time
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
from database import init_db, get_db_session
|
from database import init_db, get_db_session
|
||||||
from models import User, Order, Trade, Balance
|
from models import User, Order, Trade, Balance
|
||||||
@@ -17,13 +20,59 @@ from models import User, Order, Trade, Balance
|
|||||||
# Initialize FastAPI app
|
# Initialize FastAPI app
|
||||||
app = FastAPI(title="AITBC Trade Exchange API", version="1.0.0")
|
app = FastAPI(title="AITBC Trade Exchange API", version="1.0.0")
|
||||||
|
|
||||||
|
# In-memory session storage (use Redis in production)
|
||||||
|
user_sessions = {}
|
||||||
|
|
||||||
|
def verify_session_token(token: str = Header(..., alias="Authorization")) -> int:
|
||||||
|
"""Verify session token and return user_id"""
|
||||||
|
# Remove "Bearer " prefix if present
|
||||||
|
if token.startswith("Bearer "):
|
||||||
|
token = token[7:]
|
||||||
|
|
||||||
|
if token not in user_sessions:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Invalid or expired token"
|
||||||
|
)
|
||||||
|
|
||||||
|
session = user_sessions[token]
|
||||||
|
|
||||||
|
# Check if expired
|
||||||
|
if int(time.time()) > session["expires_at"]:
|
||||||
|
del user_sessions[token]
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Token expired"
|
||||||
|
)
|
||||||
|
|
||||||
|
return session["user_id"]
|
||||||
|
|
||||||
|
def optional_auth(token: Optional[str] = Header(None, alias="Authorization")) -> Optional[int]:
|
||||||
|
"""Optional authentication - returns user_id if token is valid, None otherwise"""
|
||||||
|
if not token:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
return verify_session_token(token)
|
||||||
|
except HTTPException:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Type annotations for dependencies
|
||||||
|
UserDep = Annotated[int, Depends(verify_session_token)]
|
||||||
|
OptionalUserDep = Annotated[Optional[int], Depends(optional_auth)]
|
||||||
|
|
||||||
# Add CORS middleware
|
# Add CORS middleware
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=["*"],
|
allow_origins=[
|
||||||
|
"http://localhost:3000",
|
||||||
|
"http://localhost:8080",
|
||||||
|
"http://localhost:8000",
|
||||||
|
"http://localhost:3003"
|
||||||
|
],
|
||||||
allow_credentials=True,
|
allow_credentials=True,
|
||||||
allow_methods=["*"],
|
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||||
allow_headers=["*"],
|
allow_headers=["*"], # Allow all headers for auth tokens
|
||||||
)
|
)
|
||||||
|
|
||||||
# Pydantic models
|
# Pydantic models
|
||||||
@@ -110,6 +159,41 @@ def get_recent_trades(limit: int = 20, db: Session = Depends(get_db_session)):
|
|||||||
trades = db.query(Trade).order_by(desc(Trade.created_at)).limit(limit).all()
|
trades = db.query(Trade).order_by(desc(Trade.created_at)).limit(limit).all()
|
||||||
return trades
|
return trades
|
||||||
|
|
||||||
|
@app.get("/api/orders", response_model=List[OrderResponse])
|
||||||
|
def get_orders(
|
||||||
|
status_filter: Optional[str] = None,
|
||||||
|
user_only: bool = False,
|
||||||
|
db: Session = Depends(get_db_session),
|
||||||
|
user_id: OptionalUserDep = None
|
||||||
|
):
|
||||||
|
"""Get all orders with optional status filter"""
|
||||||
|
query = db.query(Order)
|
||||||
|
|
||||||
|
# Filter by user if requested and authenticated
|
||||||
|
if user_only and user_id:
|
||||||
|
query = query.filter(Order.user_id == user_id)
|
||||||
|
|
||||||
|
if status_filter:
|
||||||
|
query = query.filter(Order.status == status_filter.upper())
|
||||||
|
|
||||||
|
orders = query.order_by(Order.created_at.desc()).all()
|
||||||
|
return orders
|
||||||
|
|
||||||
|
@app.get("/api/my/orders", response_model=List[OrderResponse])
|
||||||
|
def get_my_orders(
|
||||||
|
user_id: UserDep,
|
||||||
|
status_filter: Optional[str] = None,
|
||||||
|
db: Session = Depends(get_db_session)
|
||||||
|
):
|
||||||
|
"""Get current user's orders"""
|
||||||
|
query = db.query(Order).filter(Order.user_id == user_id)
|
||||||
|
|
||||||
|
if status_filter:
|
||||||
|
query = query.filter(Order.status == status_filter.upper())
|
||||||
|
|
||||||
|
orders = query.order_by(Order.created_at.desc()).all()
|
||||||
|
return orders
|
||||||
|
|
||||||
@app.get("/api/orders/orderbook", response_model=OrderBookResponse)
|
@app.get("/api/orders/orderbook", response_model=OrderBookResponse)
|
||||||
def get_orderbook(db: Session = Depends(get_db_session)):
|
def get_orderbook(db: Session = Depends(get_db_session)):
|
||||||
"""Get current order book"""
|
"""Get current order book"""
|
||||||
@@ -127,7 +211,11 @@ def get_orderbook(db: Session = Depends(get_db_session)):
|
|||||||
return OrderBookResponse(buys=buys, sells=sells)
|
return OrderBookResponse(buys=buys, sells=sells)
|
||||||
|
|
||||||
@app.post("/api/orders", response_model=OrderResponse)
|
@app.post("/api/orders", response_model=OrderResponse)
|
||||||
def create_order(order: OrderCreate, db: Session = Depends(get_db_session)):
|
def create_order(
|
||||||
|
order: OrderCreate,
|
||||||
|
db: Session = Depends(get_db_session),
|
||||||
|
user_id: UserDep
|
||||||
|
):
|
||||||
"""Create a new order"""
|
"""Create a new order"""
|
||||||
|
|
||||||
# Validate order type
|
# Validate order type
|
||||||
@@ -140,7 +228,7 @@ def create_order(order: OrderCreate, db: Session = Depends(get_db_session)):
|
|||||||
# Create order
|
# Create order
|
||||||
total = order.amount * order.price
|
total = order.amount * order.price
|
||||||
db_order = Order(
|
db_order = Order(
|
||||||
user_id=1, # TODO: Get from authentication
|
user_id=user_id, # Use authenticated user_id
|
||||||
order_type=order.order_type,
|
order_type=order.order_type,
|
||||||
amount=order.amount,
|
amount=order.amount,
|
||||||
price=order.price,
|
price=order.price,
|
||||||
@@ -219,6 +307,45 @@ def try_match_order(order: Order, db: Session):
|
|||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
@app.post("/api/auth/login")
|
||||||
|
def login_user(wallet_address: str, db: Session = Depends(get_db_session)):
|
||||||
|
"""Login with wallet address"""
|
||||||
|
# Find or create user
|
||||||
|
user = db.query(User).filter(User.wallet_address == wallet_address).first()
|
||||||
|
if not user:
|
||||||
|
user = User(
|
||||||
|
wallet_address=wallet_address,
|
||||||
|
email=f"{wallet_address}@aitbc.local",
|
||||||
|
is_active=True
|
||||||
|
)
|
||||||
|
db.add(user)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(user)
|
||||||
|
|
||||||
|
# Create session token
|
||||||
|
token_data = f"{user.id}:{int(time.time())}"
|
||||||
|
token = hashlib.sha256(token_data.encode()).hexdigest()
|
||||||
|
|
||||||
|
# Store session
|
||||||
|
user_sessions[token] = {
|
||||||
|
"user_id": user.id,
|
||||||
|
"created_at": int(time.time()),
|
||||||
|
"expires_at": int(time.time()) + 86400 # 24 hours
|
||||||
|
}
|
||||||
|
|
||||||
|
return {"token": token, "user_id": user.id}
|
||||||
|
|
||||||
|
@app.post("/api/auth/logout")
|
||||||
|
def logout_user(token: str = Header(..., alias="Authorization")):
|
||||||
|
"""Logout user"""
|
||||||
|
if token.startswith("Bearer "):
|
||||||
|
token = token[7:]
|
||||||
|
|
||||||
|
if token in user_sessions:
|
||||||
|
del user_sessions[token]
|
||||||
|
|
||||||
|
return {"message": "Logged out successfully"}
|
||||||
|
|
||||||
@app.get("/api/health")
|
@app.get("/api/health")
|
||||||
def health_check():
|
def health_check():
|
||||||
"""Health check endpoint"""
|
"""Health check endpoint"""
|
||||||
|
|||||||
@@ -9,7 +9,70 @@ import yaml
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, Dict, Any, List
|
from typing import Optional, Dict, Any, List
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from ..utils import output, error, success
|
from ..utils import output, error, success, encrypt_value, decrypt_value
|
||||||
|
import getpass
|
||||||
|
|
||||||
|
|
||||||
|
def _get_wallet_password(wallet_name: str) -> str:
|
||||||
|
"""Get or prompt for wallet encryption password"""
|
||||||
|
# Try to get from keyring first
|
||||||
|
try:
|
||||||
|
import keyring
|
||||||
|
password = keyring.get_password("aitbc-wallet", wallet_name)
|
||||||
|
if password:
|
||||||
|
return password
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Prompt for password
|
||||||
|
while True:
|
||||||
|
password = getpass.getpass(f"Enter password for wallet '{wallet_name}': ")
|
||||||
|
if not password:
|
||||||
|
error("Password cannot be empty")
|
||||||
|
continue
|
||||||
|
|
||||||
|
confirm = getpass.getpass("Confirm password: ")
|
||||||
|
if password != confirm:
|
||||||
|
error("Passwords do not match")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Store in keyring for future use
|
||||||
|
try:
|
||||||
|
import keyring
|
||||||
|
keyring.set_password("aitbc-wallet", wallet_name, password)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return password
|
||||||
|
|
||||||
|
|
||||||
|
def _save_wallet(wallet_path: Path, wallet_data: Dict[str, Any], password: str = None):
|
||||||
|
"""Save wallet with encrypted private key"""
|
||||||
|
# Encrypt private key if provided
|
||||||
|
if password and 'private_key' in wallet_data:
|
||||||
|
wallet_data['private_key'] = encrypt_value(wallet_data['private_key'], password)
|
||||||
|
wallet_data['encrypted'] = True
|
||||||
|
|
||||||
|
# Save wallet
|
||||||
|
with open(wallet_path, 'w') as f:
|
||||||
|
json.dump(wallet_data, f, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
def _load_wallet(wallet_path: Path, wallet_name: str) -> Dict[str, Any]:
|
||||||
|
"""Load wallet and decrypt private key if needed"""
|
||||||
|
with open(wallet_path, 'r') as f:
|
||||||
|
wallet_data = json.load(f)
|
||||||
|
|
||||||
|
# Decrypt private key if encrypted
|
||||||
|
if wallet_data.get('encrypted') and 'private_key' in wallet_data:
|
||||||
|
password = _get_wallet_password(wallet_name)
|
||||||
|
try:
|
||||||
|
wallet_data['private_key'] = decrypt_value(wallet_data['private_key'], password)
|
||||||
|
except Exception:
|
||||||
|
error("Invalid password for wallet")
|
||||||
|
raise click.Abort()
|
||||||
|
|
||||||
|
return wallet_data
|
||||||
|
|
||||||
|
|
||||||
@click.group()
|
@click.group()
|
||||||
@@ -56,8 +119,9 @@ def wallet(ctx, wallet_name: Optional[str], wallet_path: Optional[str]):
|
|||||||
@wallet.command()
|
@wallet.command()
|
||||||
@click.argument('name')
|
@click.argument('name')
|
||||||
@click.option('--type', 'wallet_type', default='hd', help='Wallet type (hd, simple)')
|
@click.option('--type', 'wallet_type', default='hd', help='Wallet type (hd, simple)')
|
||||||
|
@click.option('--no-encrypt', is_flag=True, help='Skip wallet encryption (not recommended)')
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def create(ctx, name: str, wallet_type: str):
|
def create(ctx, name: str, wallet_type: str, no_encrypt: bool):
|
||||||
"""Create a new wallet"""
|
"""Create a new wallet"""
|
||||||
wallet_dir = ctx.obj['wallet_dir']
|
wallet_dir = ctx.obj['wallet_dir']
|
||||||
wallet_path = wallet_dir / f"{name}.json"
|
wallet_path = wallet_dir / f"{name}.json"
|
||||||
@@ -70,10 +134,29 @@ def create(ctx, name: str, wallet_type: str):
|
|||||||
if wallet_type == 'hd':
|
if wallet_type == 'hd':
|
||||||
# Hierarchical Deterministic wallet
|
# Hierarchical Deterministic wallet
|
||||||
import secrets
|
import secrets
|
||||||
seed = secrets.token_hex(32)
|
from cryptography.hazmat.primitives import hashes
|
||||||
address = f"aitbc1{seed[:40]}"
|
from cryptography.hazmat.primitives.asymmetric import ec
|
||||||
private_key = f"0x{seed}"
|
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat, NoEncryption, PrivateFormat
|
||||||
public_key = f"0x{secrets.token_hex(32)}"
|
import base64
|
||||||
|
|
||||||
|
# Generate private key
|
||||||
|
private_key_bytes = secrets.token_bytes(32)
|
||||||
|
private_key = f"0x{private_key_bytes.hex()}"
|
||||||
|
|
||||||
|
# Derive public key from private key using ECDSA
|
||||||
|
priv_key = ec.derive_private_key(int.from_bytes(private_key_bytes, 'big'), ec.SECP256K1())
|
||||||
|
pub_key = priv_key.public_key()
|
||||||
|
pub_key_bytes = pub_key.public_bytes(
|
||||||
|
encoding=Encoding.X962,
|
||||||
|
format=PublicFormat.UncompressedPoint
|
||||||
|
)
|
||||||
|
public_key = f"0x{pub_key_bytes.hex()}"
|
||||||
|
|
||||||
|
# Generate address from public key (simplified)
|
||||||
|
digest = hashes.Hash(hashes.SHA256())
|
||||||
|
digest.update(pub_key_bytes)
|
||||||
|
address_hash = digest.finalize()
|
||||||
|
address = f"aitbc1{address_hash[:20].hex()}"
|
||||||
else:
|
else:
|
||||||
# Simple wallet
|
# Simple wallet
|
||||||
import secrets
|
import secrets
|
||||||
@@ -92,9 +175,14 @@ def create(ctx, name: str, wallet_type: str):
|
|||||||
"transactions": []
|
"transactions": []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Get password for encryption unless skipped
|
||||||
|
password = None
|
||||||
|
if not no_encrypt:
|
||||||
|
success("Wallet encryption is enabled. Your private key will be encrypted at rest.")
|
||||||
|
password = _get_wallet_password(name)
|
||||||
|
|
||||||
# Save wallet
|
# Save wallet
|
||||||
with open(wallet_path, 'w') as f:
|
_save_wallet(wallet_path, wallet_data, password)
|
||||||
json.dump(wallet_data, f, indent=2)
|
|
||||||
|
|
||||||
success(f"Wallet '{name}' created successfully")
|
success(f"Wallet '{name}' created successfully")
|
||||||
output({
|
output({
|
||||||
@@ -123,13 +211,16 @@ def list(ctx):
|
|||||||
for wallet_file in wallet_dir.glob("*.json"):
|
for wallet_file in wallet_dir.glob("*.json"):
|
||||||
with open(wallet_file, 'r') as f:
|
with open(wallet_file, 'r') as f:
|
||||||
wallet_data = json.load(f)
|
wallet_data = json.load(f)
|
||||||
wallets.append({
|
wallet_info = {
|
||||||
"name": wallet_data['wallet_id'],
|
"name": wallet_data['wallet_id'],
|
||||||
"type": wallet_data.get('type', 'simple'),
|
"type": wallet_data.get('type', 'simple'),
|
||||||
"address": wallet_data['address'],
|
"address": wallet_data['address'],
|
||||||
"created_at": wallet_data['created_at'],
|
"created_at": wallet_data['created_at'],
|
||||||
"active": wallet_data['wallet_id'] == active_wallet
|
"active": wallet_data['wallet_id'] == active_wallet
|
||||||
})
|
}
|
||||||
|
if wallet_data.get('encrypted'):
|
||||||
|
wallet_info['encrypted'] = True
|
||||||
|
wallets.append(wallet_info)
|
||||||
|
|
||||||
output(wallets, ctx.obj.get('output_format', 'table'))
|
output(wallets, ctx.obj.get('output_format', 'table'))
|
||||||
|
|
||||||
@@ -163,9 +254,11 @@ def switch(ctx, name: str):
|
|||||||
yaml.dump(config, f, default_flow_style=False)
|
yaml.dump(config, f, default_flow_style=False)
|
||||||
|
|
||||||
success(f"Switched to wallet '{name}'")
|
success(f"Switched to wallet '{name}'")
|
||||||
|
# Load wallet to get address (will handle encryption)
|
||||||
|
wallet_data = _load_wallet(wallet_path, name)
|
||||||
output({
|
output({
|
||||||
"active_wallet": name,
|
"active_wallet": name,
|
||||||
"address": json.load(open(wallet_path))['address']
|
"address": wallet_data['address']
|
||||||
}, ctx.obj.get('output_format', 'table'))
|
}, ctx.obj.get('output_format', 'table'))
|
||||||
|
|
||||||
|
|
||||||
@@ -255,7 +348,8 @@ def restore(ctx, backup_path: str, name: str, force: bool):
|
|||||||
wallet_data['wallet_id'] = name
|
wallet_data['wallet_id'] = name
|
||||||
wallet_data['restored_at'] = datetime.utcnow().isoformat() + "Z"
|
wallet_data['restored_at'] = datetime.utcnow().isoformat() + "Z"
|
||||||
|
|
||||||
# Save restored wallet
|
# Save restored wallet (preserve encryption state)
|
||||||
|
# If wallet was encrypted, we save it as-is (still encrypted with original password)
|
||||||
with open(wallet_path, 'w') as f:
|
with open(wallet_path, 'w') as f:
|
||||||
json.dump(wallet_data, f, indent=2)
|
json.dump(wallet_data, f, indent=2)
|
||||||
|
|
||||||
@@ -279,8 +373,7 @@ def info(ctx):
|
|||||||
error(f"Wallet '{wallet_name}' not found. Use 'aitbc wallet create' to create one.")
|
error(f"Wallet '{wallet_name}' not found. Use 'aitbc wallet create' to create one.")
|
||||||
return
|
return
|
||||||
|
|
||||||
with open(wallet_path, 'r') as f:
|
wallet_data = _load_wallet(wallet_path, wallet_name)
|
||||||
wallet_data = json.load(f)
|
|
||||||
|
|
||||||
# Get active wallet from config
|
# Get active wallet from config
|
||||||
active_wallet = 'default'
|
active_wallet = 'default'
|
||||||
@@ -317,22 +410,46 @@ def balance(ctx):
|
|||||||
# Auto-create wallet if it doesn't exist
|
# Auto-create wallet if it doesn't exist
|
||||||
if not wallet_path.exists():
|
if not wallet_path.exists():
|
||||||
import secrets
|
import secrets
|
||||||
|
from cryptography.hazmat.primitives import hashes
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import ec
|
||||||
|
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
|
||||||
|
|
||||||
|
# Generate proper key pair
|
||||||
|
private_key_bytes = secrets.token_bytes(32)
|
||||||
|
private_key = f"0x{private_key_bytes.hex()}"
|
||||||
|
|
||||||
|
# Derive public key from private key
|
||||||
|
priv_key = ec.derive_private_key(int.from_bytes(private_key_bytes, 'big'), ec.SECP256K1())
|
||||||
|
pub_key = priv_key.public_key()
|
||||||
|
pub_key_bytes = pub_key.public_bytes(
|
||||||
|
encoding=Encoding.X962,
|
||||||
|
format=PublicFormat.UncompressedPoint
|
||||||
|
)
|
||||||
|
public_key = f"0x{pub_key_bytes.hex()}"
|
||||||
|
|
||||||
|
# Generate address from public key
|
||||||
|
digest = hashes.Hash(hashes.SHA256())
|
||||||
|
digest.update(pub_key_bytes)
|
||||||
|
address_hash = digest.finalize()
|
||||||
|
address = f"aitbc1{address_hash[:20].hex()}"
|
||||||
|
|
||||||
wallet_data = {
|
wallet_data = {
|
||||||
"wallet_id": wallet_name,
|
"wallet_id": wallet_name,
|
||||||
"type": "simple",
|
"type": "simple",
|
||||||
"address": f"aitbc1{secrets.token_hex(20)}",
|
"address": address,
|
||||||
"public_key": f"0x{secrets.token_hex(32)}",
|
"public_key": public_key,
|
||||||
"private_key": f"0x{secrets.token_hex(32)}",
|
"private_key": private_key,
|
||||||
"created_at": datetime.utcnow().isoformat() + "Z",
|
"created_at": datetime.utcnow().isoformat() + "Z",
|
||||||
"balance": 0.0,
|
"balance": 0.0,
|
||||||
"transactions": []
|
"transactions": []
|
||||||
}
|
}
|
||||||
wallet_path.parent.mkdir(parents=True, exist_ok=True)
|
wallet_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
with open(wallet_path, 'w') as f:
|
# Auto-create with encryption
|
||||||
json.dump(wallet_data, f, indent=2)
|
success("Creating new wallet with encryption enabled")
|
||||||
|
password = _get_wallet_password(wallet_name)
|
||||||
|
_save_wallet(wallet_path, wallet_data, password)
|
||||||
else:
|
else:
|
||||||
with open(wallet_path, 'r') as f:
|
wallet_data = _load_wallet(wallet_path, wallet_name)
|
||||||
wallet_data = json.load(f)
|
|
||||||
|
|
||||||
# Try to get balance from blockchain if available
|
# Try to get balance from blockchain if available
|
||||||
if config:
|
if config:
|
||||||
@@ -377,8 +494,7 @@ def history(ctx, limit: int):
|
|||||||
error(f"Wallet '{wallet_name}' not found")
|
error(f"Wallet '{wallet_name}' not found")
|
||||||
return
|
return
|
||||||
|
|
||||||
with open(wallet_path, 'r') as f:
|
wallet_data = _load_wallet(wallet_path, wallet_name)
|
||||||
wallet_data = json.load(f)
|
|
||||||
|
|
||||||
transactions = wallet_data.get('transactions', [])[-limit:]
|
transactions = wallet_data.get('transactions', [])[-limit:]
|
||||||
|
|
||||||
@@ -413,8 +529,7 @@ def earn(ctx, amount: float, job_id: str, desc: Optional[str]):
|
|||||||
error(f"Wallet '{wallet_name}' not found")
|
error(f"Wallet '{wallet_name}' not found")
|
||||||
return
|
return
|
||||||
|
|
||||||
with open(wallet_path, 'r') as f:
|
wallet_data = _load_wallet(wallet_path, wallet_name)
|
||||||
wallet_data = json.load(f)
|
|
||||||
|
|
||||||
# Add transaction
|
# Add transaction
|
||||||
transaction = {
|
transaction = {
|
||||||
@@ -428,9 +543,11 @@ def earn(ctx, amount: float, job_id: str, desc: Optional[str]):
|
|||||||
wallet_data['transactions'].append(transaction)
|
wallet_data['transactions'].append(transaction)
|
||||||
wallet_data['balance'] = wallet_data.get('balance', 0) + amount
|
wallet_data['balance'] = wallet_data.get('balance', 0) + amount
|
||||||
|
|
||||||
# Save wallet
|
# Save wallet with encryption
|
||||||
with open(wallet_path, 'w') as f:
|
password = None
|
||||||
json.dump(wallet_data, f, indent=2)
|
if wallet_data.get('encrypted'):
|
||||||
|
password = _get_wallet_password(wallet_name)
|
||||||
|
_save_wallet(wallet_path, wallet_data, password)
|
||||||
|
|
||||||
success(f"Earnings added: {amount} AITBC")
|
success(f"Earnings added: {amount} AITBC")
|
||||||
output({
|
output({
|
||||||
@@ -454,8 +571,7 @@ def spend(ctx, amount: float, description: str):
|
|||||||
error(f"Wallet '{wallet_name}' not found")
|
error(f"Wallet '{wallet_name}' not found")
|
||||||
return
|
return
|
||||||
|
|
||||||
with open(wallet_path, 'r') as f:
|
wallet_data = _load_wallet(wallet_path, wallet_name)
|
||||||
wallet_data = json.load(f)
|
|
||||||
|
|
||||||
balance = wallet_data.get('balance', 0)
|
balance = wallet_data.get('balance', 0)
|
||||||
if balance < amount:
|
if balance < amount:
|
||||||
@@ -474,9 +590,11 @@ def spend(ctx, amount: float, description: str):
|
|||||||
wallet_data['transactions'].append(transaction)
|
wallet_data['transactions'].append(transaction)
|
||||||
wallet_data['balance'] = balance - amount
|
wallet_data['balance'] = balance - amount
|
||||||
|
|
||||||
# Save wallet
|
# Save wallet with encryption
|
||||||
with open(wallet_path, 'w') as f:
|
password = None
|
||||||
json.dump(wallet_data, f, indent=2)
|
if wallet_data.get('encrypted'):
|
||||||
|
password = _get_wallet_password(wallet_name)
|
||||||
|
_save_wallet(wallet_path, wallet_data, password)
|
||||||
|
|
||||||
success(f"Spent: {amount} AITBC")
|
success(f"Spent: {amount} AITBC")
|
||||||
output({
|
output({
|
||||||
@@ -498,8 +616,7 @@ def address(ctx):
|
|||||||
error(f"Wallet '{wallet_name}' not found")
|
error(f"Wallet '{wallet_name}' not found")
|
||||||
return
|
return
|
||||||
|
|
||||||
with open(wallet_path, 'r') as f:
|
wallet_data = _load_wallet(wallet_path, wallet_name)
|
||||||
wallet_data = json.load(f)
|
|
||||||
|
|
||||||
output({
|
output({
|
||||||
"wallet": wallet_name,
|
"wallet": wallet_name,
|
||||||
@@ -522,8 +639,7 @@ def send(ctx, to_address: str, amount: float, description: Optional[str]):
|
|||||||
error(f"Wallet '{wallet_name}' not found")
|
error(f"Wallet '{wallet_name}' not found")
|
||||||
return
|
return
|
||||||
|
|
||||||
with open(wallet_path, 'r') as f:
|
wallet_data = _load_wallet(wallet_path, wallet_name)
|
||||||
wallet_data = json.load(f)
|
|
||||||
|
|
||||||
balance = wallet_data.get('balance', 0)
|
balance = wallet_data.get('balance', 0)
|
||||||
if balance < amount:
|
if balance < amount:
|
||||||
@@ -589,8 +705,11 @@ def send(ctx, to_address: str, amount: float, description: Optional[str]):
|
|||||||
wallet_data['transactions'].append(transaction)
|
wallet_data['transactions'].append(transaction)
|
||||||
wallet_data['balance'] = balance - amount
|
wallet_data['balance'] = balance - amount
|
||||||
|
|
||||||
with open(wallet_path, 'w') as f:
|
# Save wallet with encryption
|
||||||
json.dump(wallet_data, f, indent=2)
|
password = None
|
||||||
|
if wallet_data.get('encrypted'):
|
||||||
|
password = _get_wallet_password(wallet_name)
|
||||||
|
_save_wallet(wallet_path, wallet_data, password)
|
||||||
|
|
||||||
output({
|
output({
|
||||||
"wallet": wallet_name,
|
"wallet": wallet_name,
|
||||||
@@ -615,8 +734,7 @@ def request_payment(ctx, to_address: str, amount: float, description: Optional[s
|
|||||||
error(f"Wallet '{wallet_name}' not found")
|
error(f"Wallet '{wallet_name}' not found")
|
||||||
return
|
return
|
||||||
|
|
||||||
with open(wallet_path, 'r') as f:
|
wallet_data = _load_wallet(wallet_path, wallet_name)
|
||||||
wallet_data = json.load(f)
|
|
||||||
|
|
||||||
# Create payment request
|
# Create payment request
|
||||||
request = {
|
request = {
|
||||||
@@ -645,8 +763,7 @@ def stats(ctx):
|
|||||||
error(f"Wallet '{wallet_name}' not found")
|
error(f"Wallet '{wallet_name}' not found")
|
||||||
return
|
return
|
||||||
|
|
||||||
with open(wallet_path, 'r') as f:
|
wallet_data = _load_wallet(wallet_path, wallet_name)
|
||||||
wallet_data = json.load(f)
|
|
||||||
|
|
||||||
transactions = wallet_data.get('transactions', [])
|
transactions = wallet_data.get('transactions', [])
|
||||||
|
|
||||||
@@ -680,8 +797,7 @@ def stake(ctx, amount: float, duration: int):
|
|||||||
error(f"Wallet '{wallet_name}' not found")
|
error(f"Wallet '{wallet_name}' not found")
|
||||||
return
|
return
|
||||||
|
|
||||||
with open(wallet_path, 'r') as f:
|
wallet_data = _load_wallet(wallet_path, wallet_name)
|
||||||
wallet_data = json.load(f)
|
|
||||||
|
|
||||||
balance = wallet_data.get('balance', 0)
|
balance = wallet_data.get('balance', 0)
|
||||||
if balance < amount:
|
if balance < amount:
|
||||||
@@ -714,8 +830,11 @@ def stake(ctx, amount: float, duration: int):
|
|||||||
"timestamp": datetime.now().isoformat()
|
"timestamp": datetime.now().isoformat()
|
||||||
})
|
})
|
||||||
|
|
||||||
with open(wallet_path, 'w') as f:
|
# Save wallet with encryption
|
||||||
json.dump(wallet_data, f, indent=2)
|
password = None
|
||||||
|
if wallet_data.get('encrypted'):
|
||||||
|
password = _get_wallet_password(wallet_name)
|
||||||
|
_save_wallet(wallet_path, wallet_data, password)
|
||||||
|
|
||||||
success(f"Staked {amount} AITBC for {duration} days")
|
success(f"Staked {amount} AITBC for {duration} days")
|
||||||
output({
|
output({
|
||||||
@@ -774,8 +893,11 @@ def unstake(ctx, stake_id: str):
|
|||||||
"timestamp": datetime.now().isoformat()
|
"timestamp": datetime.now().isoformat()
|
||||||
})
|
})
|
||||||
|
|
||||||
with open(wallet_path, 'w') as f:
|
# Save wallet with encryption
|
||||||
json.dump(wallet_data, f, indent=2)
|
password = None
|
||||||
|
if wallet_data.get('encrypted'):
|
||||||
|
password = _get_wallet_password(wallet_name)
|
||||||
|
_save_wallet(wallet_path, wallet_data, password)
|
||||||
|
|
||||||
success(f"Unstaked {stake_record['amount']} AITBC + {rewards:.4f} rewards")
|
success(f"Unstaked {stake_record['amount']} AITBC + {rewards:.4f} rewards")
|
||||||
output({
|
output({
|
||||||
@@ -800,8 +922,7 @@ def staking_info(ctx):
|
|||||||
error(f"Wallet '{wallet_name}' not found")
|
error(f"Wallet '{wallet_name}' not found")
|
||||||
return
|
return
|
||||||
|
|
||||||
with open(wallet_path, 'r') as f:
|
wallet_data = _load_wallet(wallet_path, wallet_name)
|
||||||
wallet_data = json.load(f)
|
|
||||||
|
|
||||||
staking = wallet_data.get('staking', [])
|
staking = wallet_data.get('staking', [])
|
||||||
active_stakes = [s for s in staking if s['status'] == 'active']
|
active_stakes = [s for s in staking if s['status'] == 'active']
|
||||||
@@ -995,14 +1116,14 @@ def multisig_sign(ctx, wallet_name: str, tx_id: str, signer: str):
|
|||||||
@click.pass_context
|
@click.pass_context
|
||||||
def liquidity_stake(ctx, amount: float, pool: str, lock_days: int):
|
def liquidity_stake(ctx, amount: float, pool: str, lock_days: int):
|
||||||
"""Stake tokens into a liquidity pool"""
|
"""Stake tokens into a liquidity pool"""
|
||||||
|
wallet_name = ctx.obj['wallet_name']
|
||||||
wallet_path = ctx.obj.get('wallet_path')
|
wallet_path = ctx.obj.get('wallet_path')
|
||||||
if not wallet_path or not Path(wallet_path).exists():
|
if not wallet_path or not Path(wallet_path).exists():
|
||||||
error("Wallet not found")
|
error("Wallet not found")
|
||||||
ctx.exit(1)
|
ctx.exit(1)
|
||||||
return
|
return
|
||||||
|
|
||||||
with open(wallet_path) as f:
|
wallet_data = _load_wallet(Path(wallet_path), wallet_name)
|
||||||
wallet_data = json.load(f)
|
|
||||||
|
|
||||||
balance = wallet_data.get('balance', 0)
|
balance = wallet_data.get('balance', 0)
|
||||||
if balance < amount:
|
if balance < amount:
|
||||||
@@ -1051,8 +1172,11 @@ def liquidity_stake(ctx, amount: float, pool: str, lock_days: int):
|
|||||||
"timestamp": now.isoformat()
|
"timestamp": now.isoformat()
|
||||||
})
|
})
|
||||||
|
|
||||||
with open(wallet_path, "w") as f:
|
# Save wallet with encryption
|
||||||
json.dump(wallet_data, f, indent=2)
|
password = None
|
||||||
|
if wallet_data.get('encrypted'):
|
||||||
|
password = _get_wallet_password(wallet_name)
|
||||||
|
_save_wallet(Path(wallet_path), wallet_data, password)
|
||||||
|
|
||||||
success(f"Staked {amount} AITBC into '{pool}' pool ({tier} tier, {apy}% APY)")
|
success(f"Staked {amount} AITBC into '{pool}' pool ({tier} tier, {apy}% APY)")
|
||||||
output({
|
output({
|
||||||
@@ -1071,14 +1195,14 @@ def liquidity_stake(ctx, amount: float, pool: str, lock_days: int):
|
|||||||
@click.pass_context
|
@click.pass_context
|
||||||
def liquidity_unstake(ctx, stake_id: str):
|
def liquidity_unstake(ctx, stake_id: str):
|
||||||
"""Withdraw from a liquidity pool with rewards"""
|
"""Withdraw from a liquidity pool with rewards"""
|
||||||
|
wallet_name = ctx.obj['wallet_name']
|
||||||
wallet_path = ctx.obj.get('wallet_path')
|
wallet_path = ctx.obj.get('wallet_path')
|
||||||
if not wallet_path or not Path(wallet_path).exists():
|
if not wallet_path or not Path(wallet_path).exists():
|
||||||
error("Wallet not found")
|
error("Wallet not found")
|
||||||
ctx.exit(1)
|
ctx.exit(1)
|
||||||
return
|
return
|
||||||
|
|
||||||
with open(wallet_path) as f:
|
wallet_data = _load_wallet(Path(wallet_path), wallet_name)
|
||||||
wallet_data = json.load(f)
|
|
||||||
|
|
||||||
liquidity = wallet_data.get('liquidity', [])
|
liquidity = wallet_data.get('liquidity', [])
|
||||||
record = next((r for r in liquidity if r["stake_id"] == stake_id and r["status"] == "active"), None)
|
record = next((r for r in liquidity if r["stake_id"] == stake_id and r["status"] == "active"), None)
|
||||||
@@ -1118,8 +1242,11 @@ def liquidity_unstake(ctx, stake_id: str):
|
|||||||
"timestamp": datetime.now().isoformat()
|
"timestamp": datetime.now().isoformat()
|
||||||
})
|
})
|
||||||
|
|
||||||
with open(wallet_path, "w") as f:
|
# Save wallet with encryption
|
||||||
json.dump(wallet_data, f, indent=2)
|
password = None
|
||||||
|
if wallet_data.get('encrypted'):
|
||||||
|
password = _get_wallet_password(wallet_name)
|
||||||
|
_save_wallet(Path(wallet_path), wallet_data, password)
|
||||||
|
|
||||||
success(f"Withdrawn {total:.6f} AITBC (principal: {record['amount']}, rewards: {rewards:.6f})")
|
success(f"Withdrawn {total:.6f} AITBC (principal: {record['amount']}, rewards: {rewards:.6f})")
|
||||||
output({
|
output({
|
||||||
@@ -1138,14 +1265,14 @@ def liquidity_unstake(ctx, stake_id: str):
|
|||||||
@click.pass_context
|
@click.pass_context
|
||||||
def rewards(ctx):
|
def rewards(ctx):
|
||||||
"""View all earned rewards (staking + liquidity)"""
|
"""View all earned rewards (staking + liquidity)"""
|
||||||
|
wallet_name = ctx.obj['wallet_name']
|
||||||
wallet_path = ctx.obj.get('wallet_path')
|
wallet_path = ctx.obj.get('wallet_path')
|
||||||
if not wallet_path or not Path(wallet_path).exists():
|
if not wallet_path or not Path(wallet_path).exists():
|
||||||
error("Wallet not found")
|
error("Wallet not found")
|
||||||
ctx.exit(1)
|
ctx.exit(1)
|
||||||
return
|
return
|
||||||
|
|
||||||
with open(wallet_path) as f:
|
wallet_data = _load_wallet(Path(wallet_path), wallet_name)
|
||||||
wallet_data = json.load(f)
|
|
||||||
|
|
||||||
staking = wallet_data.get('staking', [])
|
staking = wallet_data.get('staking', [])
|
||||||
liquidity = wallet_data.get('liquidity', [])
|
liquidity = wallet_data.get('liquidity', [])
|
||||||
|
|||||||
@@ -76,20 +76,40 @@ class AuditLogger:
|
|||||||
return entries[-limit:]
|
return entries[-limit:]
|
||||||
|
|
||||||
|
|
||||||
def encrypt_value(value: str, key: str = None) -> str:
|
def _get_fernet_key(key: str = None) -> bytes:
|
||||||
"""Simple XOR-based obfuscation for config values (not cryptographic security)"""
|
"""Derive a Fernet key from a password or use default"""
|
||||||
|
from cryptography.fernet import Fernet
|
||||||
import base64
|
import base64
|
||||||
key = key or "aitbc_config_key_2026"
|
import hashlib
|
||||||
encrypted = bytes([ord(c) ^ ord(key[i % len(key)]) for i, c in enumerate(value)])
|
|
||||||
|
if key is None:
|
||||||
|
# Use a default key (should be overridden in production)
|
||||||
|
key = "aitbc_config_key_2026_default"
|
||||||
|
|
||||||
|
# Derive a 32-byte key suitable for Fernet
|
||||||
|
return base64.urlsafe_b64encode(hashlib.sha256(key.encode()).digest())
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt_value(value: str, key: str = None) -> str:
|
||||||
|
"""Encrypt a value using Fernet symmetric encryption"""
|
||||||
|
from cryptography.fernet import Fernet
|
||||||
|
import base64
|
||||||
|
|
||||||
|
fernet_key = _get_fernet_key(key)
|
||||||
|
f = Fernet(fernet_key)
|
||||||
|
encrypted = f.encrypt(value.encode())
|
||||||
return base64.b64encode(encrypted).decode()
|
return base64.b64encode(encrypted).decode()
|
||||||
|
|
||||||
|
|
||||||
def decrypt_value(encrypted: str, key: str = None) -> str:
|
def decrypt_value(encrypted: str, key: str = None) -> str:
|
||||||
"""Decrypt an XOR-obfuscated config value"""
|
"""Decrypt a Fernet-encrypted value"""
|
||||||
|
from cryptography.fernet import Fernet
|
||||||
import base64
|
import base64
|
||||||
key = key or "aitbc_config_key_2026"
|
|
||||||
|
fernet_key = _get_fernet_key(key)
|
||||||
|
f = Fernet(fernet_key)
|
||||||
data = base64.b64decode(encrypted)
|
data = base64.b64decode(encrypted)
|
||||||
return ''.join(chr(b ^ ord(key[i % len(key)])) for i, b in enumerate(data))
|
return f.decrypt(data).decode()
|
||||||
|
|
||||||
|
|
||||||
def setup_logging(verbosity: int, debug: bool = False) -> str:
|
def setup_logging(verbosity: int, debug: bool = False) -> str:
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"wallet_id": "my-wallet",
|
|
||||||
"type": "hd",
|
|
||||||
"address": "aitbc1e9056b875c03773b067fd0345248abd3db762af5",
|
|
||||||
"public_key": "0x2619e06fca452e9a524a688ceccbbb07ebdaaa824c3e78c4a406ed2a60cee47f",
|
|
||||||
"private_key": "0xe9056b875c03773b067fd0345248abd3db762af5274c8e30ab304cb04a7b4c79",
|
|
||||||
"created_at": "2026-02-12T16:41:34.206767Z",
|
|
||||||
"balance": 0,
|
|
||||||
"transactions": []
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user