Files
k 752652f463 fix(safety): force Auto-Trade OFF on paper→live promotion
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>
2026-05-30 01:03:02 +08:00

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)