From d576e9bf90d2394857409a2ab7896beeff04620a Mon Sep 17 00:00:00 2001 From: nacadmin Date: Sun, 22 Mar 2026 23:06:18 +0800 Subject: [PATCH] =?UTF-8?q?fix(#1):=20=E4=BF=AE=E5=A4=8D=20onchain=5Finfo?= =?UTF-8?q?=20=E5=AD=97=E6=AE=B5=E8=81=9A=E5=90=88=E4=B8=8D=E5=AE=8C?= =?UTF-8?q?=E6=95=B4=20-=20=E6=96=B0=E5=A2=9E=20=5Fbuild=5Fonchain=5Finfo(?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/routers/assets.py | 258 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 258 insertions(+) create mode 100644 backend/routers/assets.py diff --git a/backend/routers/assets.py b/backend/routers/assets.py new file mode 100644 index 0000000..aaa3f7a --- /dev/null +++ b/backend/routers/assets.py @@ -0,0 +1,258 @@ +""" +NAC 一键上链系统 - 资产管理路由 +版本: 2.1 (集成GNACS微服务 + 20大类 + 跨境交易双轨合规) +""" +import logging +import random +import string +import httpx +from datetime import datetime, timezone +from fastapi import APIRouter, Depends, HTTPException, Header + +from database import assets_col, now_utc, GNACS_SERVICE_URL +from models import AssetCreateRequest, OnboardingStep +from routers.auth import get_current_user as auth_get_current_user +from bson import ObjectId + +logger = logging.getLogger("nac-onboarding.assets") +router = APIRouter() + + +def serialize_doc(doc: dict) -> dict: + if doc is None: + return {} + result = {} + for k, v in doc.items(): + if k == "_id": + continue + elif isinstance(v, ObjectId): + result[k] = str(v) + elif isinstance(v, datetime): + result[k] = v.isoformat() + elif isinstance(v, dict): + result[k] = serialize_doc(v) + elif isinstance(v, list): + result[k] = [serialize_doc(i) if isinstance(i, dict) else i for i in v] + else: + result[k] = v + return result + + +def generate_asset_id(asset_type: str) -> str: + type_map = { + "RealEstate": "RE", "FinancialSecurities": "FS", "Commodities": "CM", + "ArtCollectibles": "AT", "IntellectualProperty": "IP", "DigitalAssets": "DA", + "Infrastructure": "IF", "NaturalResources": "NR", "EnvironmentalRights": "ER", + "CorporateEquity": "CE", "DebtAssets": "DB", "InsuranceAssets": "IN", + "AgriculturalAssets": "AG", "TransportationAssets": "TR", "EquipmentMachinery": "EM", + "DataAssets": "DT", "IntangibleBusiness": "IB", "SportsAssets": "SP", + "CulturalEntertainment": "CU", "Custom": "CX", + } + prefix = type_map.get(asset_type, "XX") + rand = "".join(random.choices("0123456789ABCDEF", k=12)) + return f"NAC-{prefix}-{rand}" + + +async def call_gnacs(endpoint: str, params: dict = None) -> dict: + try: + async with httpx.AsyncClient(timeout=3.0) as client: + resp = await client.get(f"{GNACS_SERVICE_URL}{endpoint}", params=params) + if resp.status_code == 200: + return resp.json() + except Exception as e: + logger.warning(f"GNACS调用失败: {e}") + return {} + + +def determine_tx_type(jurisdiction: str, investor_jurisdiction: str = None) -> str: + if not investor_jurisdiction: + return "domestic" + j, ij = jurisdiction.upper(), investor_jurisdiction.upper() + if j == ij: + return "domestic" + asean = {"SG", "MY", "TH", "ID", "PH", "VN", "MM", "KH", "LA", "BN"} + gcc = {"AE", "SA", "QA", "KW", "BH", "OM"} + if j in asean and ij in asean: + return "domestic_asean" + if j in gcc and ij in gcc: + return "domestic_gcc" + return "cross_border" + + +def get_kyc_req(asset_type: str, tx_type: str) -> int: + base = { + "RealEstate": 2, "FinancialSecurities": 3, "Commodities": 2, + "ArtCollectibles": 2, "IntellectualProperty": 2, "DigitalAssets": 1, + "Infrastructure": 3, "NaturalResources": 3, "EnvironmentalRights": 2, + "CorporateEquity": 3, "DebtAssets": 3, "InsuranceAssets": 3, + "AgriculturalAssets": 2, "TransportationAssets": 2, "EquipmentMachinery": 2, + "DataAssets": 2, "IntangibleBusiness": 2, "SportsAssets": 2, + "CulturalEntertainment": 2, "Custom": 2, + } + kyc = base.get(asset_type, 2) + if tx_type == "cross_border": + kyc = max(kyc, 3) + return kyc + + +def get_acc_standard(asset_type: str) -> str: + acc_map = { + "RealEstate": "ACC-20", "FinancialSecurities": "ACC-1400", "Commodities": "ACC-20", + "ArtCollectibles": "ACC-721", "IntellectualProperty": "ACC-721", "DigitalAssets": "ACC-20", + "Infrastructure": "ACC-20", "NaturalResources": "ACC-1155", "EnvironmentalRights": "ACC-1155", + "CorporateEquity": "ACC-1400", "DebtAssets": "ACC-1400", "InsuranceAssets": "ACC-1400", + "AgriculturalAssets": "ACC-1155", "TransportationAssets": "ACC-20", "EquipmentMachinery": "ACC-20", + "DataAssets": "ACC-20", "IntangibleBusiness": "ACC-721", "SportsAssets": "ACC-721", + "CulturalEntertainment": "ACC-721", "Custom": "ACC-20", + } + return acc_map.get(asset_type, "ACC-20") + + +# Use auth.py's get_current_user for proper DEV_MODE support +get_current_user = auth_get_current_user + + +@router.post("") +async def create_asset(req: AssetCreateRequest, current_user: dict = Depends(get_current_user)): + tx_type = req.transaction_type or determine_tx_type(req.jurisdiction, req.investor_jurisdiction) + required_kyc = get_kyc_req(req.asset_type, tx_type) + user_kyc = current_user.get("kyc_level", 0) + if user_kyc < required_kyc: + raise HTTPException( + status_code=403, + detail=f"KYC等级不足:{req.asset_type}({tx_type})需要KYC-{required_kyc},当前KYC-{user_kyc}" + ) + gnacs_info = await call_gnacs("/api/gnacs/classify/info", {"asset_type": req.asset_type}) + acc_standard = get_acc_standard(req.asset_type) + asset_id = generate_asset_id(req.asset_type) + doc = { + "asset_id": asset_id, + "name": req.name, + "asset_type": req.asset_type, + "asset_subtype": req.asset_subtype, + "acc_standard": acc_standard, + "gnacs_info": gnacs_info.get("data", {}), + "jurisdiction": req.jurisdiction, + "investor_jurisdiction": req.investor_jurisdiction or req.jurisdiction, + "transaction_type": tx_type, + "owner_id": current_user["did"], + "owner_kyc_level": user_kyc, + "total_supply": req.total_supply, + "asset_value": req.asset_value, + "currency": req.currency, + "xtzh_staked": req.xtzh_staked, + "xtzh_ratio": 0.8, + "description": req.description, + "details": req.details, + "created_at": now_utc(), + "updated_at": now_utc(), + "onboarding_status": { + "current_step": OnboardingStep.APPLICATION_SUBMITTED, + "progress": 5, + "is_active": True, + "history": [{ + "step": OnboardingStep.APPLICATION_SUBMITTED, + "status": "completed", + "timestamp": now_utc().isoformat(), + "operator": current_user["did"], + "details": f"申请提交,类型:{req.asset_type},辖区:{req.jurisdiction},交易类型:{tx_type},ACC:{acc_standard}" + }] + }, + "compliance": None, "valuation": None, "dna": None, "warrant": None, + "rights_offering": None, "custody": None, "xtzh": None, "token": None, "documents": [] + } + await assets_col.insert_one(doc) + logger.info(f"资产申请: {asset_id} by {current_user['did']} ({tx_type})") + return { + "success": True, + "data": { + "asset_id": asset_id, + "current_step": OnboardingStep.APPLICATION_SUBMITTED, + "progress": 5, + "transaction_type": tx_type, + "acc_standard": acc_standard, + "required_kyc": required_kyc + }, + "message": f"资产申请已提交: {asset_id},交易类型: {tx_type}" + } + + +@router.get("") +async def list_assets(current_user: dict = Depends(get_current_user), page: int = 1, limit: int = 20): + skip = (page - 1) * limit + cursor = assets_col.find({"owner_id": current_user["did"]}, sort=[("created_at", -1)]).skip(skip).limit(limit) + assets = [] + async for doc in cursor: + assets.append(serialize_doc(doc)) + total = await assets_col.count_documents({"owner_id": current_user["did"]}) + return {"success": True, "data": {"assets": assets, "total": total, "page": page, "limit": limit}} + + +@router.get("/admin/all") +async def admin_list_all(current_user: dict = Depends(get_current_user)): + if current_user.get("role") not in ["admin", "operator"]: + raise HTTPException(status_code=403, detail="需要管理员权限") + cursor = assets_col.find({}, sort=[("created_at", -1)]).limit(100) + assets = [] + async for doc in cursor: + assets.append(serialize_doc(doc)) + total = await assets_col.count_documents({}) + return {"success": True, "data": {"assets": assets, "total": total}} + + +@router.get("/{asset_id}") +async def get_asset(asset_id: str, current_user: dict = Depends(get_current_user)): + doc = await assets_col.find_one({"asset_id": asset_id, "owner_id": current_user["did"]}) + if not doc: + raise HTTPException(status_code=404, detail="资产不存在") + serialized = serialize_doc(doc) + # 聚合链上关键信息到 onchain_info 字段 + serialized["onchain_info"] = _build_onchain_info(doc) + return {"success": True, "data": serialized} + + +def _build_onchain_info(doc: dict) -> dict: + """从各步骤的专用字段中聚合链上关键信息""" + # dna_hash: Step 5 generate-dna 写入 doc["dna"]["hash"] + dna = doc.get("dna") or {} + dna_hash = dna.get("hash") or "--" + + # chain_tx: Step 6 chain-confirm 写入 doc["warrant"]["tx_hash"] + warrant = doc.get("warrant") or {} + chain_tx = warrant.get("tx_hash") or "--" + block_height = warrant.get("block_height") or "--" + + # token_symbol / token_address: Step 14 issue-token 写入 doc["token"] + token = doc.get("token") or {} + token_symbol = token.get("symbol") or "--" + token_address = token.get("address") or "--" + token_supply = token.get("total_supply") or "--" + issue_tx_hash = token.get("issue_tx_hash") or "--" + + # xtzh_minted: Step 13 mint-xtzh 写入 doc["xtzh"]["amount"] + xtzh = doc.get("xtzh") or {} + xtzh_minted = xtzh.get("amount") or "--" + xtzh_mint_tx = xtzh.get("mint_tx") or "--" + + # warrant_id: Step 9 issue-warrant 写入 doc["warrant_cert"] + warrant_cert = doc.get("warrant_cert") or {} + warrant_id = warrant_cert.get("warrant_id") or "--" + + # custody_tx: Step 11 custody 写入 doc["custody"]["warrant_custody_tx"] + custody = doc.get("custody") or {} + custody_tx = custody.get("warrant_custody_tx") or "--" + + return { + "dna_hash": dna_hash, + "chain_tx": chain_tx, + "block_height": block_height, + "token_symbol": token_symbol, + "token_address": token_address, + "token_supply": token_supply, + "issue_tx_hash": issue_tx_hash, + "xtzh_minted": xtzh_minted, + "xtzh_mint_tx": xtzh_mint_tx, + "warrant_id": warrant_id, + "custody_tx": custody_tx, + "is_complete": doc.get("onboarding_status", {}).get("progress", 0) == 100 + }