gcx/blockchain/explorer/compliance-labels.ex

208 lines
5.3 KiB
Elixir
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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