"""节点授权订单。付款凭证先入库,管理员通过后才开通节点。""" from __future__ import annotations import json import secrets from sqlalchemy import case, select from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session, selectinload from app.clock import utcnow from app.config import settings from app.errors import AppError from app.models import LicenseOrder, OrderStatus from app.money import money from app.services.registry import register_node def place_order(session: Session, user_id: int, tx_id: str) -> LicenseOrder: digest = tx_id.strip() if session.scalar(select(LicenseOrder.id).where(LicenseOrder.tx_id == digest)) is not None: raise AppError("这笔交易已经提交过", 409) order = LicenseOrder( user_id=user_id, amount=money(settings.license_price), asset_symbol=settings.license_asset.strip().upper() or "USDT", status=OrderStatus.PENDING, tx_id=digest, created_at=utcnow(), ) session.add(order) try: session.flush() except IntegrityError as exc: session.rollback() raise AppError("这笔交易已经提交过", 409) from exc return order def approve_order(session: Session, order_id: int) -> LicenseOrder: order = _pending_order(session, order_id) token = secrets.token_urlsafe(24) node = register_node( session, order.user.wallet_address, f"HN{order.id:06d}", order.amount, node_token=token, ) order.status = OrderStatus.APPROVED order.node = node order.reviewed_at = utcnow() session.flush() return order def reject_order(session: Session, order_id: int, note: str | None = None) -> LicenseOrder: order = _pending_order(session, order_id) order.status = OrderStatus.REJECTED order.note = note order.reviewed_at = utcnow() session.flush() return order def list_orders(session: Session, user_id: int | None = None) -> list[LicenseOrder]: pending_first = case((LicenseOrder.status == OrderStatus.PENDING, 0), else_=1) query = select(LicenseOrder).options(selectinload(LicenseOrder.user), selectinload(LicenseOrder.node)) if user_id is not None: query = query.where(LicenseOrder.user_id == user_id) return list(session.scalars(query.order_by(pending_first, LicenseOrder.id.desc())).all()) _CLIENT = """# HyperNode 挂机客户端。这个文件里有节点 Token,不要发给别人。 import ctypes import hashlib import json import os import subprocess import sys import time import urllib.error import urllib.request import uuid CONFIG = json.loads(__CONFIG__) SHARES = 0 _REFERENCE_MH = ( ("4090", 130.0), ("4080 SUPER", 95.0), ("4080", 90.0), ("4070 TI SUPER", 75.0), ("4070 TI", 70.0), ("4070 SUPER", 62.0), ("4070", 55.0), ("3090 TI", 125.0), ("3090", 115.0), ("3080 TI", 105.0), ("3080", 95.0), ("3070 TI", 65.0), ("3070", 60.0), ("3060 TI", 48.5), ("3060", 40.0), ) def current_mode(): mode = os.environ.get("HYPERNODE_MODE") or CONFIG.get("current_mode") or "pow_mining" return mode if mode in {"pow_mining", "gpu_rental"} else "pow_mining" def _fingerprint(gpu_uuid): board = _board_serial() or "" mac = f"{uuid.getnode():012x}" material = "|".join([gpu_uuid or "", board, mac]) if not material.strip("|"): return None return hashlib.sha256(material.encode()).hexdigest() def _board_serial(): if os.name == "nt": try: output = subprocess.check_output( ["wmic", "baseboard", "get", "serialnumber"], stderr=subprocess.DEVNULL, timeout=8, text=True, ) except (OSError, subprocess.SubprocessError): return None lines = [line.strip() for line in output.splitlines() if line.strip() and "SerialNumber" not in line] return lines[0] if lines else None for path in ("/sys/class/dmi/id/board_serial", "/sys/class/dmi/id/product_uuid"): try: with open(path, encoding="utf-8") as handle: value = handle.read().strip() except OSError: continue if value and value.lower() not in {"none", "to be filled by o.e.m."}: return value return None def _gpu(): probed = _pynvml() if probed and probed.get("name"): return probed probed = _nvidia_smi() return probed or {} def _blank_gpu(): return { "name": None, "vram_gb": None, "uuid": None, "vram_used_mb": None, "vram_total_mb": None, "gpu_temp": None, "fan_speed": None, "power_usage": None, "utilization": None, } def _pynvml(): try: import pynvml except ImportError: return None try: pynvml.nvmlInit() best = None best_bytes = -1 for index in range(pynvml.nvmlDeviceGetCount()): handle = pynvml.nvmlDeviceGetHandleByIndex(index) name = pynvml.nvmlDeviceGetName(handle) if isinstance(name, bytes): name = name.decode() memory = pynvml.nvmlDeviceGetMemoryInfo(handle) total = int(memory.total) if total <= best_bytes: continue uid = pynvml.nvmlDeviceGetUUID(handle) if isinstance(uid, bytes): uid = uid.decode() best_bytes = total best = _blank_gpu() best.update({ "name": str(name), "vram_gb": round(total / (1024 ** 3), 2), "uuid": str(uid), "vram_used_mb": int(round(int(memory.used) / (1024 ** 2))), "vram_total_mb": int(round(total / (1024 ** 2))), "gpu_temp": _nvml_call(pynvml.nvmlDeviceGetTemperature, handle, pynvml.NVML_TEMPERATURE_GPU), "fan_speed": _nvml_call(pynvml.nvmlDeviceGetFanSpeed, handle), "power_usage": _nvml_watts(pynvml, handle), "utilization": _nvml_util(pynvml, handle), }) pynvml.nvmlShutdown() except Exception: return None return best def _nvml_call(func, *args): try: value = func(*args) return int(value) except Exception: return None def _nvml_watts(pynvml, handle): try: return round(pynvml.nvmlDeviceGetPowerUsage(handle) / 1000, 1) except Exception: return None def _nvml_util(pynvml, handle): try: return int(pynvml.nvmlDeviceGetUtilizationRates(handle).gpu) except Exception: return None def _nvidia_smi(): query = "name,memory.total,memory.used,uuid,temperature.gpu,fan.speed,power.draw,utilization.gpu" try: output = subprocess.check_output( ["nvidia-smi", "--query-gpu=" + query, "--format=csv,noheader,nounits"], stderr=subprocess.DEVNULL, timeout=8, text=True, ) except (OSError, subprocess.SubprocessError): return None best = None best_mib = -1.0 for line in output.splitlines(): parts = [part.strip() for part in line.split(",")] if len(parts) < 8: continue total = _number(parts[-7]) if total is None or total <= best_mib: continue best_mib = total best = _blank_gpu() best.update({ "name": ",".join(parts[:-7]).strip() or None, "vram_gb": round(total / 1024, 2), "vram_total_mb": int(round(total)), "vram_used_mb": _int_number(parts[-6]), "uuid": parts[-5] or None, "gpu_temp": _int_number(parts[-4]), "fan_speed": _int_number(parts[-3]), "power_usage": _number(parts[-2]), "utilization": _int_number(parts[-1]), }) return best def _number(value): try: return float(value) except (TypeError, ValueError): return None def _int_number(value): number = _number(value) if number is None: return None return int(round(number)) def _memory_gb(): if os.name == "nt": class MemoryStatus(ctypes.Structure): _fields_ = [ ("dwLength", ctypes.c_ulong), ("dwMemoryLoad", ctypes.c_ulong), ("ullTotalPhys", ctypes.c_ulonglong), ("ullAvailPhys", ctypes.c_ulonglong), ("ullTotalPageFile", ctypes.c_ulonglong), ("ullAvailPageFile", ctypes.c_ulonglong), ("ullTotalVirtual", ctypes.c_ulonglong), ("ullAvailVirtual", ctypes.c_ulonglong), ("ullAvailExtendedVirtual", ctypes.c_ulonglong), ] status = MemoryStatus() status.dwLength = ctypes.sizeof(MemoryStatus) if ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(status)): return round(status.ullTotalPhys / (1024 ** 3), 2) return None try: with open("/proc/meminfo", encoding="utf-8") as handle: for line in handle: if line.startswith("MemTotal:"): return round(int(line.split()[1]) / 1024 / 1024, 2) except OSError: return None return None def beat(): global SHARES gpu = _gpu() payload = hardware_from(gpu) body = json.dumps(payload).encode() request = urllib.request.Request( CONFIG["base"] + "/api/v1/node/heartbeat", data=body, headers={"Content-Type": "application/json"}, ) with urllib.request.urlopen(request, timeout=20) as response: reply = json.loads(response.read().decode()) SHARES += 1 _save_shares(SHARES) _report(gpu, reply, SHARES) def hardware_from(gpu): card = gpu or {} return { "sn": CONFIG["sn"], "token": CONFIG["token"], "gpu_model": card.get("name"), "vram_gb": card.get("vram_gb"), "cpu_count": os.cpu_count(), "memory_gb": _memory_gb(), "hardware_fingerprint": _fingerprint(card.get("uuid")), } def _report(gpu, reply, shares): card = gpu or {} name = card.get("name") or "GPU not detected" vram = card.get("vram_gb") vram_text = f"{vram:.2f} GB" if isinstance(vram, (int, float)) else "n/a" _log("INFO", f"{name} · {vram_text}") _log("HARDWARE", _hardware_line(card)) if current_mode() == "gpu_rental": load = card.get("utilization") load_text = f"{load}% Load" if load is not None else "n/a" _log("TASK", f"[AI Task Executing] · {load_text}") else: rate = _hashrate(name) rate_text = f"{rate:.1f} MH/s" if rate is not None else "n/a" _log("HASHRATE", f"{rate_text} · Accepted {shares}") weight = reply.get("hardware_weight") weight_text = f"{float(weight):.1f}" if isinstance(weight, (int, float)) else "n/a" node_id = reply.get("display_node_id") or "----" status = reply.get("status") or "unknown" _log("HEARTBEAT", f"{status} · Node #{node_id} · Weight {weight_text}") _log("RECOVERY", _recovery_line(reply)) print(flush=True) def _hardware_line(card): used = card.get("vram_used_mb") total = card.get("vram_total_mb") vram = f"{used} / {total} MB" if used is not None and total is not None else "n/a" return " · ".join([ _suffix(card.get("gpu_temp"), "°C"), "Fan " + _suffix(card.get("fan_speed"), "%"), _suffix(card.get("power_usage"), " W"), "VRAM " + vram, ]) def _recovery_line(reply): days = int(reply.get("recovery_days") or 0) purchase = _decimal(reply.get("purchase_amount")) remaining = _decimal(reply.get("recoverable_stake_amount")) estimate = reply.get("estimated_daily_points") or "0" if purchase is not None and remaining is not None and purchase > 0: done = max(0.0, (purchase - remaining) / purchase * 100) progress = f"{done:.1f}% returned" else: progress = "n/a" title = "30-day fast recovery" if days == 30 else f"{days}-day recovery" return f"{title} · {progress} · Est. Points {estimate}" def _hashrate(name): upper = (name or "").upper().replace("-", " ") for token, rate in _REFERENCE_MH: if token in upper: return rate return None def _suffix(value, suffix): if value is None: return "n/a" return f"{value}{suffix}" def _decimal(value): if value is None or value == "": return None try: return float(value) except (TypeError, ValueError): return None def _log(tag, message): color = { "INFO": "36", "HARDWARE": "33", "HASHRATE": "32", "TASK": "32", "HEARTBEAT": "96", "RECOVERY": "33", "WARN": "31", }.get(tag, "37") label = f"[{tag}]".ljust(12) if sys.stdout.isatty(): label = f"\033[1;{color}m{label}\033[0m" stamp = time.strftime("%Y-%m-%d %H:%M:%S") print(f"{stamp} {label} {message}", flush=True) def _share_path(): folder = os.path.dirname(os.path.abspath(__file__)) return os.path.join(folder, "." + CONFIG["sn"] + ".shares") def _load_shares(): try: with open(_share_path(), encoding="utf-8") as handle: return max(0, int(handle.read().strip() or "0")) except (OSError, ValueError): return 0 def _save_shares(count): try: with open(_share_path(), "w", encoding="utf-8") as handle: handle.write(str(count)) except OSError: return None def _enable_ansi(): if os.name != "nt": return try: kernel = ctypes.windll.kernel32 handle = kernel.GetStdHandle(-11) mode = ctypes.c_ulong() if kernel.GetConsoleMode(handle, ctypes.byref(mode)): kernel.SetConsoleMode(handle, mode.value | 0x0004) except Exception: return None if __name__ == "__main__": _enable_ansi() SHARES = _load_shares() _log("INFO", "HyperNode client · " + CONFIG["sn"] + " · " + current_mode()) while True: try: beat() except urllib.error.HTTPError as exc: detail = exc.read().decode(errors="replace").strip() _log("WARN", f"heartbeat {exc.code} {detail}") except (urllib.error.URLError, TimeoutError, OSError) as exc: _log("WARN", str(exc)) time.sleep(60) """ def render_client(sn: str, token: str, base_url: str) -> str: config = json.dumps( { "sn": sn, "token": token, "base": base_url.rstrip("/"), "current_mode": "pow_mining", }, ensure_ascii=True, ) return _CLIENT.replace("__CONFIG__", repr(config)) def _pending_order(session: Session, order_id: int) -> LicenseOrder: order = session.scalar( select(LicenseOrder) .options(selectinload(LicenseOrder.user), selectinload(LicenseOrder.node)) .where(LicenseOrder.id == order_id) ) if order is None: raise AppError("订单不存在", 404) if order.status != OrderStatus.PENDING: raise AppError("只有待审核的订单可以处理", 409) return order