improve signed reads, crypto hardening, and scraper transport

This commit is contained in:
k
2026-06-14 21:43:43 +08:00
parent 54884f3e24
commit 78fb63be8e
27 changed files with 1326 additions and 202 deletions
+5 -5
View File
@@ -8,7 +8,8 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.models import BotTrade
from app.schemas import BotPerformance
from app.services.signed_request import verify_signed_request_any
from app.services.signed_request import (
SignedReadCreds, signed_read_creds, verify_signed_request_any)
router = APIRouter()
logger = logging.getLogger(__name__)
@@ -21,8 +22,7 @@ ACTION_VIEW_USER = "view_user"
@router.get("/performance", response_model=BotPerformance)
async def get_performance(
wallet: str = Query(..., description="Wallet address (lower-cased internally)"),
ts: int = Query(..., description="Signed timestamp (ms)"),
sig: str = Query(..., description="EIP-191 signature"),
creds: SignedReadCreds = Depends(signed_read_creds),
include_paper: bool = Query(
False,
description="Include paper (simulated) trades. Default false — this "
@@ -35,8 +35,8 @@ async def get_performance(
verify_signed_request_any(
actions=[ACTION_VIEW_PERFORMANCE, ACTION_VIEW_USER],
wallet=wallet,
timestamp_ms=ts,
signature=sig,
timestamp_ms=creds.ts,
signature=creds.sig,
body=None,
allow_replay=True,
)
+12 -13
View File
@@ -35,7 +35,9 @@ from app.database import get_db
from app.models import BotTrade, Subscription, iso_utc
from app.services.crypto import decrypt_api_key
from app.services.price_store import price_store
from app.services.signed_request import verify_signed_request, verify_signed_request_any
from app.services.signed_request import (
SignedReadCreds, signed_read_creds,
verify_signed_request, verify_signed_request_any)
router = APIRouter()
logger = logging.getLogger(__name__)
@@ -144,8 +146,7 @@ def _enrich(trade: BotTrade) -> OpenPosition:
@router.get("/positions/open", response_model=OpenPositionsResponse)
async def get_open_positions(
wallet: str = Query(..., description="Wallet address (lower-cased internally)"),
ts: int = Query(..., description="Signed timestamp (ms)"),
sig: str = Query(..., description="EIP-191 signature"),
creds: SignedReadCreds = Depends(signed_read_creds),
db: AsyncSession = Depends(get_db),
):
"""Live open positions for the wallet, with mark-to-market PnL."""
@@ -153,8 +154,8 @@ async def get_open_positions(
verify_signed_request_any(
actions=[ACTION_VIEW_POSITIONS, ACTION_VIEW_USER],
wallet=wallet,
timestamp_ms=ts,
signature=sig,
timestamp_ms=creds.ts,
signature=creds.sig,
body=None,
allow_replay=True,
)
@@ -177,8 +178,7 @@ async def get_open_positions(
@router.get("/positions/today", response_model=TodayStatsResponse)
async def get_today_stats(
wallet: str = Query(...),
ts: int = Query(..., description="Signed timestamp (ms)"),
sig: str = Query(..., description="EIP-191 signature"),
creds: SignedReadCreds = Depends(signed_read_creds),
db: AsyncSession = Depends(get_db),
):
"""Today's realized P&L (since UTC midnight) + open count.
@@ -190,8 +190,8 @@ async def get_today_stats(
verify_signed_request_any(
actions=[ACTION_VIEW_POSITIONS, ACTION_VIEW_USER],
wallet=wallet,
timestamp_ms=ts,
signature=sig,
timestamp_ms=creds.ts,
signature=creds.sig,
body=None,
allow_replay=True,
)
@@ -453,8 +453,7 @@ class HLPositionsResponse(BaseModel):
@router.get("/positions/hl/{wallet}", response_model=HLPositionsResponse)
async def list_hl_positions_endpoint(
wallet: str,
ts: int = Query(..., description="Signed timestamp (ms)"),
sig: str = Query(..., description="EIP-191 signature"),
creds: SignedReadCreds = Depends(signed_read_creds),
):
"""Read the wallet's CURRENT Hyperliquid open positions, annotated with
'already adopted' flag. Used by the Adopt picker on the frontend.
@@ -469,8 +468,8 @@ async def list_hl_positions_endpoint(
verify_signed_request_any(
actions=[ACTION_VIEW_POSITIONS, ACTION_VIEW_USER],
wallet=wallet,
timestamp_ms=ts,
signature=sig,
timestamp_ms=creds.ts,
signature=creds.sig,
body=None,
allow_replay=True,
)
+3 -4
View File
@@ -1,7 +1,6 @@
import logging
from typing import List
import httpx
from fastapi import APIRouter, HTTPException, Query, Request
from fastapi.responses import Response
@@ -33,9 +32,9 @@ async def fetch_binance_candles(asset: str, tf: str, limit: int) -> List[Candle]
symbol = SYMBOL_MAP[asset]
interval = BINANCE_INTERVAL[tf]
url = f"{settings.binance_rest_url}/api/v3/klines?symbol={symbol}&interval={interval}&limit={limit}"
async with httpx.AsyncClient(timeout=15) as client:
resp = await client.get(url)
resp.raise_for_status()
from app.services.http_client import get_client
resp = await get_client().get(url, timeout=15)
resp.raise_for_status()
rows = resp.json()
return [
Candle(
+6 -6
View File
@@ -25,7 +25,8 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.database import get_db
from app.models import TelegramBinding, Subscription
from app.services.signed_request import verify_signed_request
from app.services.signed_request import (
SignedReadCreds, optional_signed_read_creds, verify_signed_request)
from app.services.telegram import send_test_message
from app.services.telegram_bot import issue_binding_code, unbind_wallet
@@ -144,8 +145,7 @@ async def _build_status(
@router.get("/{wallet}/status", response_model=StatusResponse)
async def status(
wallet: str,
timestamp: Optional[int] = None,
signature: Optional[str] = None,
creds: Optional[SignedReadCreds] = Depends(optional_signed_read_creds),
db: AsyncSession = Depends(get_db),
) -> StatusResponse:
"""Wallet Telegram status.
@@ -162,14 +162,14 @@ async def status(
# Accepting view_user avoids requiring a separate wallet signature just to
# see Telegram status — the frontend reuses its cached view_user envelope.
authenticated = False
if timestamp is not None and signature:
if creds is not None:
for _action in ("view_telegram_status", "view_user"):
try:
verify_signed_request(
action=_action,
wallet=wallet,
timestamp_ms=timestamp,
signature=signature,
timestamp_ms=creds.ts,
signature=creds.sig,
body=None,
allow_replay=True,
)
+5 -5
View File
@@ -9,7 +9,8 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.models import BotTrade, iso_utc
from app.schemas import BotTrade as BotTradeSchema
from app.services.signed_request import verify_signed_request_any
from app.services.signed_request import (
SignedReadCreds, signed_read_creds, verify_signed_request_any)
router = APIRouter()
logger = logging.getLogger(__name__)
@@ -55,8 +56,7 @@ def _trade_to_schema(trade: BotTrade) -> BotTradeSchema:
@router.get("/trades", response_model=List[BotTradeSchema])
async def get_trades(
wallet: str = Query(..., description="Wallet address (lower-cased internally)"),
ts: int = Query(..., description="Signed timestamp (ms)"),
sig: str = Query(..., description="EIP-191 signature"),
creds: SignedReadCreds = Depends(signed_read_creds),
limit: int = Query(default=20, ge=1, le=500), # raised from 100: Analytics needs 500 for full history
page: int = Query(default=1, ge=1),
db: AsyncSession = Depends(get_db),
@@ -65,8 +65,8 @@ async def get_trades(
verify_signed_request_any(
actions=[ACTION_VIEW_TRADES, ACTION_VIEW_USER],
wallet=wallet,
timestamp_ms=ts,
signature=sig,
timestamp_ms=creds.ts,
signature=creds.sig,
body=None,
allow_replay=True,
)
+5 -5
View File
@@ -16,7 +16,8 @@ from app.schemas import (
)
from app.services.crypto import encrypt_api_key
from app.services.hyperliquid import HyperliquidTrader
from app.services.signed_request import verify_signed_request
from app.services.signed_request import (
SignedReadCreds, signed_read_creds, verify_signed_request)
router = APIRouter()
logger = logging.getLogger(__name__)
@@ -132,8 +133,7 @@ async def get_user_public(wallet: str, db: AsyncSession = Depends(get_db)):
@router.get("/user/{wallet}", response_model=UserResponse)
async def get_user(
wallet: str,
ts: int = Query(..., description="Signed timestamp (ms)"),
sig: str = Query(..., description="EIP-191 signature"),
creds: SignedReadCreds = Depends(signed_read_creds),
db: AsyncSession = Depends(get_db),
):
wallet = wallet.lower().strip()
@@ -143,8 +143,8 @@ async def get_user(
verify_signed_request(
action=ACTION_VIEW_USER,
wallet=wallet,
timestamp_ms=ts,
signature=sig,
timestamp_ms=creds.ts,
signature=creds.sig,
body=None,
allow_replay=True, # idempotent GET — allow sessionStorage caching for UX
)