752652f463
Money-safety bug: subscribe() changed paper_mode on a paper→live promotion but left auto_trade untouched. A user could flip Auto-Trade ON while in paper mode (harmless — SystemControl only checks 'subscribed', not paper), then promote to live and add an API key — at which point the next high-confidence Trump signal opens a REAL position, carried over from a switch they flipped when it cost nothing. Now: when paper_mode transitions True→False AND auto_trade is on, force auto_trade=False. The user must re-enable it explicitly while live (a signed, intentional action). Risk-raising transition resets the dangerous switch — standard financial-product safety. Frontend handleUpgradeToLive copy updated to say Auto-Trade is turned off on the switch. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
83 lines
3.1 KiB
Python
83 lines
3.1 KiB
Python
import logging
|
|
from datetime import datetime, timezone
|
|
|
|
from fastapi import APIRouter, Depends, Request
|
|
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
|
|
from app.services.signed_request import verify_signed_request
|
|
|
|
router = APIRouter()
|
|
logger = logging.getLogger(__name__)
|
|
|
|
ACTION_SUBSCRIBE = "subscribe"
|
|
|
|
|
|
@router.post("/subscribe", response_model=SubscribeResponse)
|
|
async def subscribe(request: Request, db: AsyncSession = Depends(get_db)):
|
|
"""Activate a subscription. Optional `paper_mode` flag in the body lets
|
|
new users try the system safely (no Hyperliquid call, simulated fills).
|
|
|
|
Signed message body is the RAW dict so the canonical hash matches what
|
|
the frontend signed — same pattern as set_user_settings.
|
|
"""
|
|
raw = await request.json()
|
|
body = SubscribeRequest(**raw)
|
|
wallet = body.wallet.lower().strip()
|
|
|
|
# The signed body excludes the envelope fields. For backwards compatibility
|
|
# with old clients that signed `body=None` (no paper_mode key), accept
|
|
# either signature.
|
|
paper_mode = bool(raw.get("paper_mode", False))
|
|
signed_body = {"paper_mode": paper_mode} if paper_mode else None
|
|
|
|
verify_signed_request(
|
|
action=ACTION_SUBSCRIBE,
|
|
wallet=wallet,
|
|
timestamp_ms=body.timestamp,
|
|
signature=body.signature,
|
|
body=signed_body,
|
|
)
|
|
|
|
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,
|
|
paper_mode=paper_mode,
|
|
)
|
|
db.add(sub)
|
|
else:
|
|
sub.active = True
|
|
if sub.subscribed_at is None:
|
|
sub.subscribed_at = now
|
|
# Re-subscribing is allowed to change paper mode (e.g. user wants to
|
|
# promote from paper to live). Otherwise leave existing flag alone.
|
|
if paper_mode != sub.paper_mode:
|
|
# SAFETY: promoting paper → live raises the risk level from
|
|
# "simulated" to "real money". Force Auto-Trade OFF on that
|
|
# transition so a switch the user flipped while it was harmless
|
|
# (paper) can NEVER carry over and silently start opening real
|
|
# positions the moment they add an API key. They must re-enable
|
|
# Auto-Trade explicitly while live — an intentional, signed action.
|
|
if sub.paper_mode and not paper_mode and sub.auto_trade:
|
|
sub.auto_trade = False
|
|
logger.info(
|
|
"Subscription %s promoted paper→live — Auto-Trade forced "
|
|
"OFF (must be re-enabled explicitly while live)", wallet,
|
|
)
|
|
sub.paper_mode = paper_mode
|
|
|
|
await db.commit()
|
|
logger.info("Subscription activated for %s (paper_mode=%s)", wallet, paper_mode)
|
|
return SubscribeResponse(status="ok", wallet=wallet, paper_mode=paper_mode)
|