按显卡世代和创世编号结算回本,机房批量接入需审核网段并用硬件指纹防克隆。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+132
-11
@@ -14,7 +14,17 @@ from app.config import settings
|
||||
from app.database import get_db
|
||||
from app.deps import AdminUser, CurrentUser
|
||||
from app.errors import AppError
|
||||
from app.models import ComputeNode, ComputeTask, LicenseOrder, User, UserRole, WithdrawalLog, WithdrawalStatus
|
||||
from app.models import (
|
||||
ComputeNode,
|
||||
ComputeTask,
|
||||
DatacenterApplication,
|
||||
IPWhitelist,
|
||||
LicenseOrder,
|
||||
User,
|
||||
UserRole,
|
||||
WithdrawalLog,
|
||||
WithdrawalStatus,
|
||||
)
|
||||
from app.money import money
|
||||
from app.schemas import (
|
||||
AdminUserOut,
|
||||
@@ -32,6 +42,9 @@ from app.schemas import (
|
||||
NodeCreate,
|
||||
NodeOut,
|
||||
NodeView,
|
||||
IPWhitelistCreate,
|
||||
IPWhitelistOut,
|
||||
WhitelistToggle,
|
||||
RegisterIn,
|
||||
SettleOut,
|
||||
SummaryOut,
|
||||
@@ -43,6 +56,9 @@ from app.schemas import (
|
||||
WithdrawalConfirm,
|
||||
WithdrawalCreate,
|
||||
WithdrawalOut,
|
||||
DatacenterApply,
|
||||
DatacenterOut,
|
||||
DatacenterView,
|
||||
WithdrawalReject,
|
||||
WithdrawalView,
|
||||
YieldOut,
|
||||
@@ -50,7 +66,11 @@ from app.schemas import (
|
||||
from app.security import create_access_token
|
||||
from app.services.auth import authenticate, register_account
|
||||
from app.services.common import get_node_by_sn, get_user_by_wallet
|
||||
from app.services.datacenter import approve_application, list_applications, reject_application, submit_application
|
||||
from app.services.hardware import HardwareReport
|
||||
from app.services.hardware import display_node_id, displayed_population
|
||||
from app.services.heartbeat import assert_node_token, record_heartbeat
|
||||
from app.services.ip_guard import add_whitelist_ip, client_ip, list_whitelist, remove_whitelist_ip
|
||||
from app.services.orders import approve_order, list_orders, place_order, reject_order, render_client
|
||||
from app.services.portal import (
|
||||
authorized_amount,
|
||||
@@ -63,7 +83,7 @@ from app.services.portal import (
|
||||
from app.services.registry import register_node, register_user
|
||||
from app.services.tasks import list_pending_tasks, report_task
|
||||
from app.services.withdrawals import available_balance, confirm_withdrawal, reject_withdrawal, request_withdrawal
|
||||
from app.services.yield_engine import compute_node_yield, list_node_yields, settle_yield_date
|
||||
from app.services.yield_engine import compute_node_yield, list_node_yields, network_yields, settle_yield_date
|
||||
|
||||
router = APIRouter(prefix="/api/v1")
|
||||
Db = Annotated[Session, Depends(get_db)]
|
||||
@@ -81,6 +101,12 @@ def login(body: LoginIn, db: Db) -> TokenOut:
|
||||
return _token(user)
|
||||
|
||||
|
||||
@router.get("/network")
|
||||
def network_population(db: Db) -> dict[str, int]:
|
||||
nodes, online = node_counts(db)
|
||||
return {"nodes": displayed_population(nodes), "online": displayed_population(online)}
|
||||
|
||||
|
||||
@router.get("/me", response_model=UserOut)
|
||||
def me(user: CurrentUser) -> User:
|
||||
return user
|
||||
@@ -93,6 +119,7 @@ def my_dashboard(user: CurrentUser, db: Db) -> DashboardOut:
|
||||
select(WithdrawalLog).where(WithdrawalLog.user_id == user.id).order_by(WithdrawalLog.id.desc()).limit(20)
|
||||
).all()
|
||||
today = money(sum((item["today_points"] for item in nodes), Decimal("0")))
|
||||
network_total, network_online = node_counts(db)
|
||||
return DashboardOut(
|
||||
wallet_address=user.wallet_address,
|
||||
username=user.username,
|
||||
@@ -104,7 +131,10 @@ def my_dashboard(user: CurrentUser, db: Db) -> DashboardOut:
|
||||
orders=[_order_out(item) for item in list_orders(db, user.id)],
|
||||
license_price=money(settings.license_price),
|
||||
license_asset=settings.license_asset,
|
||||
network_nodes=displayed_population(network_total),
|
||||
network_online=displayed_population(network_online),
|
||||
withdrawals=[WithdrawalOut.model_validate(item) for item in withdrawals],
|
||||
datacenter_applications=[_datacenter_out(item) for item in list_applications(db, user.id)],
|
||||
)
|
||||
|
||||
|
||||
@@ -128,8 +158,25 @@ def download_client(sn: str, request: Request, user: CurrentUser, db: Db) -> Res
|
||||
|
||||
|
||||
@router.post("/me/nodes", response_model=NodeOut)
|
||||
def create_my_node(body: MeNodeCreate, user: CurrentUser, db: Db) -> NodeOut:
|
||||
return register_node(db, user.wallet_address, body.sn, body.purchase_amount, body.callback_url)
|
||||
def create_my_node(body: MeNodeCreate, request: Request, user: CurrentUser, db: Db) -> NodeOut:
|
||||
return register_node(
|
||||
db, user.wallet_address, body.sn, body.purchase_amount, body.callback_url, client_ip=client_ip(request)
|
||||
)
|
||||
|
||||
|
||||
@router.post("/me/datacenter-applications", response_model=DatacenterOut)
|
||||
def create_datacenter_application(body: DatacenterApply, user: CurrentUser, db: Db) -> DatacenterOut:
|
||||
return _datacenter_out(
|
||||
submit_application(
|
||||
db,
|
||||
user.id,
|
||||
body.contact,
|
||||
body.node_count,
|
||||
body.subnet_ip,
|
||||
body.hardware_proof,
|
||||
body.gpu_generation,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@router.post("/me/withdrawals", response_model=WithdrawalOut)
|
||||
@@ -153,6 +200,46 @@ def admin_nodes(_: AdminUser, db: Db) -> list[NodeView]:
|
||||
return [NodeView.model_validate(item) for item in live_nodes(db)]
|
||||
|
||||
|
||||
@router.post("/admin/nodes/{sn}/whitelist", response_model=NodeView)
|
||||
def admin_toggle_node_whitelist(sn: str, body: WhitelistToggle, _: AdminUser, db: Db) -> NodeView:
|
||||
node = get_node_by_sn(db, sn)
|
||||
node.is_whitelist = body.is_whitelist
|
||||
node.updated_at = utcnow()
|
||||
db.flush()
|
||||
snapshot = next(item for item in live_nodes(db) if item["sn"] == node.sn)
|
||||
return NodeView.model_validate(snapshot)
|
||||
|
||||
|
||||
@router.get("/admin/ip-whitelist", response_model=list[IPWhitelistOut])
|
||||
def admin_ip_whitelist(_: AdminUser, db: Db) -> list[IPWhitelist]:
|
||||
return list_whitelist(db)
|
||||
|
||||
|
||||
@router.post("/admin/ip-whitelist", response_model=IPWhitelistOut)
|
||||
def admin_add_ip_whitelist(body: IPWhitelistCreate, _: AdminUser, db: Db) -> IPWhitelist:
|
||||
return add_whitelist_ip(db, body.ip_address, body.note)
|
||||
|
||||
|
||||
@router.delete("/admin/ip-whitelist/{entry_id}", response_model=IPWhitelistOut)
|
||||
def admin_remove_ip_whitelist(entry_id: int, _: AdminUser, db: Db) -> IPWhitelist:
|
||||
return remove_whitelist_ip(db, entry_id)
|
||||
|
||||
|
||||
@router.get("/admin/datacenter-applications", response_model=list[DatacenterView])
|
||||
def admin_datacenter_applications(_: AdminUser, db: Db) -> list[DatacenterView]:
|
||||
return [_datacenter_view(row) for row in list_applications(db)]
|
||||
|
||||
|
||||
@router.post("/admin/datacenter-applications/{application_id}/approve", response_model=DatacenterView)
|
||||
def admin_approve_datacenter(application_id: int, _: AdminUser, db: Db) -> DatacenterView:
|
||||
return _datacenter_view(approve_application(db, application_id))
|
||||
|
||||
|
||||
@router.post("/admin/datacenter-applications/{application_id}/reject", response_model=DatacenterView)
|
||||
def admin_reject_datacenter(application_id: int, body: WithdrawalReject, _: AdminUser, db: Db) -> DatacenterView:
|
||||
return _datacenter_view(reject_application(db, application_id, body.note))
|
||||
|
||||
|
||||
@router.get("/admin/orders", response_model=list[OrderView])
|
||||
def admin_orders(_: AdminUser, db: Db) -> list[OrderView]:
|
||||
return [_order_view(row) for row in list_orders(db)]
|
||||
@@ -204,9 +291,11 @@ def list_nodes(_: AdminUser, db: Db) -> list[NodeView]:
|
||||
|
||||
|
||||
@router.post("/nodes", response_model=NodeOut)
|
||||
def create_node(body: NodeCreate, user: CurrentUser, db: Db) -> NodeOut:
|
||||
def create_node(body: NodeCreate, request: Request, user: CurrentUser, db: Db) -> NodeOut:
|
||||
_must_own_wallet(user, body.wallet_address)
|
||||
return register_node(db, body.wallet_address, body.sn, body.purchase_amount, body.callback_url)
|
||||
return register_node(
|
||||
db, body.wallet_address, body.sn, body.purchase_amount, body.callback_url, client_ip=client_ip(request)
|
||||
)
|
||||
|
||||
|
||||
@router.get("/nodes/{sn}", response_model=NodeOut)
|
||||
@@ -215,13 +304,28 @@ def get_node(sn: str, user: CurrentUser, db: Db) -> NodeOut:
|
||||
|
||||
|
||||
@router.post("/node/heartbeat", response_model=HeartbeatOut)
|
||||
def heartbeat(body: HeartbeatIn, db: Db) -> HeartbeatOut:
|
||||
node = record_heartbeat(db, body.sn, body.token)
|
||||
def heartbeat(body: HeartbeatIn, request: Request, db: Db) -> HeartbeatOut:
|
||||
hardware = None
|
||||
if {"gpu_model", "vram_gb", "cpu_count", "memory_gb"} & body.model_fields_set:
|
||||
hardware = HardwareReport(body.gpu_model, body.vram_gb, body.cpu_count, body.memory_gb)
|
||||
node = record_heartbeat(
|
||||
db,
|
||||
body.sn,
|
||||
body.token,
|
||||
hardware=hardware,
|
||||
client_ip=client_ip(request),
|
||||
hardware_fingerprint=body.hardware_fingerprint,
|
||||
)
|
||||
return HeartbeatOut(
|
||||
sn=node.sn,
|
||||
display_node_id=node.display_node_id,
|
||||
status=node.status,
|
||||
last_heartbeat_at=node.last_heartbeat_at,
|
||||
server_time=node.last_heartbeat_at,
|
||||
hardware_weight=node.hardware_weight,
|
||||
gpu_generation=node.gpu_generation,
|
||||
recovery_days=node.recovery_days,
|
||||
recoverable_stake_amount=node.recoverable_stake_amount,
|
||||
)
|
||||
|
||||
|
||||
@@ -240,7 +344,12 @@ def report(body: TaskReportIn, db: Db) -> TaskOut:
|
||||
@router.get("/nodes/{sn}/contribution", response_model=ContributionOut)
|
||||
def contribution(sn: str, yield_date: date, user: CurrentUser, db: Db) -> ContributionOut:
|
||||
node = _owned_node(db, user, sn)
|
||||
computed = compute_node_yield(db, node, yield_date)
|
||||
computed = next(
|
||||
(item for current, item in network_yields(db, yield_date) if current.id == node.id),
|
||||
None,
|
||||
)
|
||||
if computed is None:
|
||||
computed = compute_node_yield(db, node, yield_date)
|
||||
return ContributionOut(sn=node.sn, **computed.__dict__)
|
||||
|
||||
|
||||
@@ -297,8 +406,8 @@ def _summary(db: Session) -> SummaryOut:
|
||||
nodes, online = node_counts(db)
|
||||
return SummaryOut(
|
||||
users=users,
|
||||
nodes=nodes,
|
||||
online=online,
|
||||
nodes=displayed_population(nodes),
|
||||
online=displayed_population(online),
|
||||
offline=nodes - online,
|
||||
authorized_amount=authorized_amount(db),
|
||||
settled_points=settled_points(db),
|
||||
@@ -338,6 +447,17 @@ def _withdrawal_rows(db: Session) -> list[WithdrawalLog]:
|
||||
)
|
||||
|
||||
|
||||
def _datacenter_out(row: DatacenterApplication) -> DatacenterOut:
|
||||
return DatacenterOut.model_validate(row)
|
||||
|
||||
|
||||
def _datacenter_view(row: DatacenterApplication) -> DatacenterView:
|
||||
return DatacenterView(
|
||||
**_datacenter_out(row).model_dump(),
|
||||
wallet_address=row.user.wallet_address,
|
||||
)
|
||||
|
||||
|
||||
def _order_out(order: LicenseOrder) -> OrderOut:
|
||||
return OrderOut(
|
||||
id=order.id,
|
||||
@@ -349,6 +469,7 @@ def _order_out(order: LicenseOrder) -> OrderOut:
|
||||
created_at=order.created_at,
|
||||
reviewed_at=order.reviewed_at,
|
||||
sn=order.node.sn if order.node is not None else None,
|
||||
display_node_id=display_node_id(order.node.node_index) if order.node is not None else None,
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user