diff --git a/server_py/requirements.txt b/server_py/requirements.txt index 43a9c35..c1c2674 100644 --- a/server_py/requirements.txt +++ b/server_py/requirements.txt @@ -2,3 +2,4 @@ Flask>=3.0.0 flask-cors>=4.0.0 requests>=2.31.0 pysha3>=1.0.5 +web3>=7.11.0 diff --git a/server_py/server/__pycache__/chain.cpython-312.pyc b/server_py/server/__pycache__/chain.cpython-312.pyc index 453fa35..ddc2300 100644 Binary files a/server_py/server/__pycache__/chain.cpython-312.pyc and b/server_py/server/__pycache__/chain.cpython-312.pyc differ diff --git a/server_py/server/chain.py b/server_py/server/chain.py index 7eaa28a..f624e8a 100644 --- a/server_py/server/chain.py +++ b/server_py/server/chain.py @@ -1,102 +1,85 @@ -# chain.py +# chain.py —— 直接覆盖此文件 import os -import json import re -import requests +import json +from decimal import Decimal, getcontext + +getcontext().prec = 78 # 大整数/小数精度 -# 优先使用 web3 的 ABI 调用;不可用时再走原始 eth_call -_USE_WEB3 = True +# 尝试引入 web3;没有则置为不可用 try: from web3 import Web3 + _USE_WEB3 = True except Exception: _USE_WEB3 = False +# ---------- 工具 ---------- def _clean_addr(addr: str) -> str: - if not isinstance(addr, str): - return "" - addr = addr.strip() - if not re.fullmatch(r"0x[0-9a-fA-F]{40}", addr or ""): - return "" - return addr - - -def _is_active_via_web3(rpc: str, contract_addr: str, address: str) -> bool: + if isinstance(addr, str) and re.fullmatch(r"0x[0-9a-fA-F]{40}", addr or ""): + return addr + return "" + +_ABI_CACHE = None +def _load_abi(): + """读取并缓存同目录下 StakePass.abi.json""" + global _ABI_CACHE + if _ABI_CACHE is None: + abi_path = os.path.join(os.path.dirname(__file__), "StakePass.abi.json") + with open(abi_path, "r", encoding="utf-8") as f: + _ABI_CACHE = json.load(f) + return _ABI_CACHE + +def _get_contract(): + """返回 (w3, contract) 或 (None, None)""" + if not _USE_WEB3: + return None, None + rpc = os.getenv("RPC") or "" + contract_addr = _clean_addr(os.getenv("CONTRACT", "")) + if not rpc or not contract_addr: + return None, None w3 = Web3(Web3.HTTPProvider(rpc, request_kwargs={"timeout": 30})) - # checksum 地址(等价于 ethers 的处理) - caddr = Web3.to_checksum_address(contract_addr) - aaddr = Web3.to_checksum_address(address) - - # 读取同目录 ABI(与 Node 版一致) - abi_path = os.path.join(os.path.dirname(__file__), "StakePass.abi.json") - with open(abi_path, "r", encoding="utf-8") as f: - abi = json.load(f) - - contract = w3.eth.contract(address=caddr, abi=abi) - try: - return bool(contract.functions.isActive(aaddr).call()) - except Exception as e: - print("[isActive web3 error]", e) - return False + contract = w3.eth.contract(address=Web3.to_checksum_address(contract_addr), abi=_load_abi()) + return w3, contract +# ---------- 业务函数 ---------- -# 原始 eth_call 作为兜底(尽量不走它) -def _keccak_selector(signature: str) -> str: - try: - import sha3 # pysha3 - return sha3.keccak_256(signature.encode()).digest()[:4].hex() - except Exception: - # isActive(address) 的选择器常量 - return "3c610c22" +def stake_amount(address: str) -> dict: + """ + 返回当前地址的质押总额(基于 balanceOf(address)): + { "raw": "uint256", "unit": "uint256", "formatted": "字符串小数" } + """ + address = _clean_addr(address) + if not address: + return {"raw": "0", "unit": "1", "formatted": "0"} + w3, c = _get_contract() + if not c: + # web3 不可用或环境变量缺失 + return {"raw": "0", "unit": "1", "formatted": "0"} -def _is_active_via_eth_call(rpc: str, contract_addr: str, address: str) -> bool: - selector = _keccak_selector("isActive(address)") - # address 是静态类型,20 字节,左填充到 32 字节 - data = "0x" + selector + address[2:].rjust(64, "0") + a = Web3.to_checksum_address(address) + amt = int(c.functions.balanceOf(a).call()) + # 读取单位:优先 unit(),其次 UNIT(),最后兜底 1e18 try: - r = requests.post( - rpc, - json={ - "jsonrpc": "2.0", - "method": "eth_call", - "params": [{"to": contract_addr, "data": data}, "latest"], - "id": 1, - }, - timeout=30, - ) - r.raise_for_status() - result = r.json().get("result") - # 部分节点在异常/找不到函数时可能返回 "0x" 或空,这里统一按 False - if not isinstance(result, str) or not result.startswith("0x") or len(result) < 3: - return False - # bool 编码为 32 字节,非零即 True - return int(result, 16) != 0 - except Exception as e: - print("[isActive eth_call error]", getattr(e, "response", None) or e) - return False + unit = int(c.functions.unit().call()) + except Exception: + try: + unit = int(c.functions.UNIT().call()) + except Exception: + unit = 10 ** 18 + formatted = str(Decimal(amt) / Decimal(unit)) if unit else str(amt) + return {"raw": str(amt), "unit": str(unit), "formatted": formatted} def isActive(address: str) -> bool: - address = _clean_addr(address) - rpc = os.getenv("RPC") or "" - contract_addr = _clean_addr(os.getenv("CONTRACT", "")) - - if not address or not rpc or not contract_addr: + """ + 是否解锁:balanceOf(address) >= unit + 与合约 StakePassFixed::isActive 等价 + """ + info = stake_amount(address) + try: + return int(info["raw"]) >= int(info["unit"]) + except Exception: return False - - # DEBUG:需要时打印原始调用信息 - if os.getenv("DEBUG_CHAIN") == "1": - print("[DEBUG_CHAIN] rpc:", rpc) - print("[DEBUG_CHAIN] contract:", contract_addr) - print("[DEBUG_CHAIN] address:", address) - - if _USE_WEB3: - ok = _is_active_via_web3(rpc, contract_addr, address) - if ok: - return True - # web3 路径失败时尝试一次 eth_call 兜底 - return _is_active_via_eth_call(rpc, contract_addr, address) - else: - return _is_active_via_eth_call(rpc, contract_addr, address) diff --git a/server_py/server/index.py b/server_py/server/index.py index 0216f71..b1b2a9e 100644 --- a/server_py/server/index.py +++ b/server_py/server/index.py @@ -13,6 +13,10 @@ from chain import isActive from aiProxy import callAI +from chain import isActive, stake_amount + + + # === DB === from db import db_session, engine from models import init_db, Topic, Message, now_utc @@ -69,8 +73,17 @@ def access_status(): return jsonify({"ok": False, "msg": "address required"}), 400 active = isActive(address) used = _daily_usage.get(f"{dayKey()}:{address}", 0) - daily_limit = int(os.getenv("DAILY_TOKENS", "20000")) - return jsonify({"ok": True, "data": {"active": active, "used": used, "daily_limit": daily_limit}}) + stake_amounts = stake_amount(address) + use_number = float(stake_amounts["formatted"]) * 10000 # 使用的数量 + return jsonify({"ok": True, "data": {"active": active, "used": used, "daily_limit": use_number}}) + +@app.get("/stake/amount") +def read_stake_amount(): + addr = (request.args.get("address") or request.headers.get("x-address") or "").strip() + if not addr: + return jsonify({"ok": False, "msg": "address required"}), 400 + return jsonify({"ok": True, "data": stake_amount(addr)}) + # ===== 主题:创建 / 列表 / 重命名 / 归档 =====