208 lines
5.3 KiB
Elixir
208 lines
5.3 KiB
Elixir
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
|