import logging from datetime import datetime, timezone from eth_account import Account from eth_account.messages import encode_defunct from fastapi import APIRouter, Depends, HTTPException from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_db from app.models import Subscription from app.schemas import SubscribeRequest, SubscribeResponse router = APIRouter() logger = logging.getLogger(__name__) SIGN_MESSAGE = "Sign in to TrumpSignal" from typing import Optional def _recover_signer(signature: str) -> Optional[str]: """Recover signer address from an EIP-191 personal_sign signature.""" try: message = encode_defunct(text=SIGN_MESSAGE) recovered = Account.recover_message(message, signature=signature) return recovered.lower() except Exception as exc: logger.warning("Signature recovery failed: %s", exc) return None @router.post("/subscribe", response_model=SubscribeResponse) async def subscribe(body: SubscribeRequest, db: AsyncSession = Depends(get_db)): wallet = body.wallet.lower().strip() # Verify EIP-191 signature recovered = _recover_signer(body.signature) if recovered is None or recovered != wallet: raise HTTPException( status_code=401, detail="Signature verification failed: recovered address does not match wallet", ) # Upsert subscription result = await db.execute( select(Subscription).where(Subscription.wallet_address == wallet) ) sub = result.scalar_one_or_none() now = datetime.now(timezone.utc).replace(tzinfo=None) if sub is None: sub = Subscription( wallet_address=wallet, active=True, subscribed_at=now, ) db.add(sub) else: sub.active = True if sub.subscribed_at is None: sub.subscribed_at = now await db.commit() logger.info("Subscription activated for wallet %s", wallet) return SubscribeResponse(status="ok", wallet=wallet)