defmodule BlockScoutWeb.ComplianceLabels do @moduledoc """ Blockscout 定制模块:合规标签系统 - 冻结地址标红 (frozen → red) - 可疑交易标橙 (suspicious → orange) - Travel Rule 交易标记 - OFAC 命中高亮 """ alias Explorer.SmartContract.Reader # Compliance 合约 ABI 片段 @compliance_abi [ %{ "name" => "isFrozen", "type" => "function", "stateMutability" => "view", "inputs" => [%{"name" => "account", "type" => "address"}], "outputs" => [%{"name" => "", "type" => "bool"}] }, %{ "name" => "getKYCLevel", "type" => "function", "stateMutability" => "view", "inputs" => [%{"name" => "account", "type" => "address"}], "outputs" => [%{"name" => "", "type" => "uint8"}] }, %{ "name" => "isOFACListed", "type" => "function", "stateMutability" => "view", "inputs" => [%{"name" => "account", "type" => "address"}], "outputs" => [%{"name" => "", "type" => "bool"}] } ] # 事件签名 @address_frozen_topic "0x" <> Base.encode16(:crypto.hash(:keccak256, "AddressFrozen(address,string)"), case: :lower) @suspicious_activity_topic "0x" <> Base.encode16(:crypto.hash(:keccak256, "SuspiciousActivity(address,uint256,string)"), case: :lower) @travel_rule_topic "0x" <> Base.encode16(:crypto.hash(:keccak256, "TravelRuleRecord(bytes32,address,address,uint256)"), case: :lower) @type label :: %{ type: :frozen | :suspicious | :travel_rule | :ofac_hit, severity: :critical | :warning | :info, color: String.t(), text: String.t(), tooltip: String.t() } @doc "获取地址的所有合规标签" @spec get_address_labels(String.t(), String.t()) :: [label()] def get_address_labels(address, compliance_contract) do labels = [] labels = if is_frozen?(address, compliance_contract) do [ %{ type: :frozen, severity: :critical, color: "#DC2626", text: "FROZEN", tooltip: "此地址已被冻结,所有交易将被拒绝" } | labels ] else labels end labels = if is_ofac_listed?(address, compliance_contract) do [ %{ type: :ofac_hit, severity: :critical, color: "#DC2626", text: "OFAC", tooltip: "此地址命中 OFAC 制裁名单" } | labels ] else labels end labels = case get_kyc_level(address, compliance_contract) do 0 -> [ %{ type: :suspicious, severity: :warning, color: "#F97316", text: "NO KYC", tooltip: "此地址未完成 KYC 验证" } | labels ] _ -> labels end labels end @doc "获取交易的合规标签" @spec get_transaction_labels(map()) :: [label()] def get_transaction_labels(transaction) do labels = [] labels = if has_travel_rule_record?(transaction) do [ %{ type: :travel_rule, severity: :info, color: "#3B82F6", text: "TRAVEL RULE", tooltip: "此交易包含 Travel Rule 合规记录" } | labels ] else labels end labels = if is_suspicious_transaction?(transaction) do [ %{ type: :suspicious, severity: :warning, color: "#F97316", text: "SUSPICIOUS", tooltip: "此交易被标记为可疑(AI 分析或规则触发)" } | labels ] else labels end labels end @doc "检查日志是否包含合规事件" def parse_compliance_event(log) do case log.first_topic do @address_frozen_topic -> {:frozen, %{address: decode_address(log.second_topic)}} @suspicious_activity_topic -> {:suspicious, %{address: decode_address(log.second_topic)}} @travel_rule_topic -> {:travel_rule, %{record_hash: log.second_topic}} _ -> nil end end # ── 私有函数 ── defp is_frozen?(address, contract) do case Reader.query_contract(contract, @compliance_abi, %{"isFrozen" => [address]}) do %{"isFrozen" => {:ok, [true]}} -> true _ -> false end end defp is_ofac_listed?(address, contract) do case Reader.query_contract(contract, @compliance_abi, %{"isOFACListed" => [address]}) do %{"isOFACListed" => {:ok, [true]}} -> true _ -> false end end defp get_kyc_level(address, contract) do case Reader.query_contract(contract, @compliance_abi, %{"getKYCLevel" => [address]}) do %{"getKYCLevel" => {:ok, [level]}} -> level _ -> 0 end end defp has_travel_rule_record?(transaction) do Enum.any?(transaction.logs || [], fn log -> log.first_topic == @travel_rule_topic end) end defp is_suspicious_transaction?(transaction) do Enum.any?(transaction.logs || [], fn log -> log.first_topic == @suspicious_activity_topic end) end defp decode_address("0x000000000000000000000000" <> addr), do: "0x" <> addr defp decode_address(other), do: other end