156 lines
4.8 KiB
Elixir
156 lines
4.8 KiB
Elixir
defmodule BlockScoutWeb.CouponView do
|
||
@moduledoc """
|
||
Blockscout 定制模块:券NFT详情页扩展
|
||
解析 CouponFactory / Coupon 合约事件,展示券业务字段
|
||
|
||
功能清单 (P0):
|
||
- 面值 (face_value)
|
||
- 券类型 (Utility / Security)
|
||
- 到期日 (expiry_date)
|
||
- 转售计数 (resale_count / max_resale)
|
||
- 发行方 (issuer)
|
||
- 可转让性 (transferable)
|
||
"""
|
||
|
||
alias Explorer.Chain.{Token, TokenTransfer}
|
||
alias Explorer.SmartContract.Reader
|
||
|
||
# CouponBatchMinted 事件签名
|
||
@coupon_batch_minted_topic "0x" <>
|
||
Base.encode16(:crypto.hash(:keccak256, "CouponBatchMinted(uint256,address,uint8,uint256,uint256,uint256)"), case: :lower)
|
||
|
||
# Coupon 合约 ABI 片段(只读函数)
|
||
@coupon_abi [
|
||
%{
|
||
"name" => "getConfig",
|
||
"type" => "function",
|
||
"stateMutability" => "view",
|
||
"inputs" => [%{"name" => "tokenId", "type" => "uint256"}],
|
||
"outputs" => [
|
||
%{"name" => "issuer", "type" => "address"},
|
||
%{"name" => "faceValue", "type" => "uint256"},
|
||
%{"name" => "couponType", "type" => "uint8"},
|
||
%{"name" => "expiryDate", "type" => "uint256"},
|
||
%{"name" => "maxResaleCount", "type" => "uint256"},
|
||
%{"name" => "transferable", "type" => "bool"}
|
||
]
|
||
},
|
||
%{
|
||
"name" => "getResaleCount",
|
||
"type" => "function",
|
||
"stateMutability" => "view",
|
||
"inputs" => [%{"name" => "tokenId", "type" => "uint256"}],
|
||
"outputs" => [%{"name" => "", "type" => "uint256"}]
|
||
}
|
||
]
|
||
|
||
@doc "渲染券NFT详情页数据"
|
||
def render_coupon_detail(token_id, contract_address) do
|
||
with {:ok, config} <- call_get_config(contract_address, token_id),
|
||
{:ok, resale_count} <- call_get_resale_count(contract_address, token_id) do
|
||
%{
|
||
token_id: token_id,
|
||
face_value: config.face_value,
|
||
coupon_type: decode_coupon_type(config.coupon_type),
|
||
expiry_date: DateTime.from_unix!(config.expiry_date),
|
||
max_resale_count: config.max_resale_count,
|
||
resale_count: resale_count,
|
||
transferable: config.transferable,
|
||
issuer: config.issuer,
|
||
expired: DateTime.utc_now() > DateTime.from_unix!(config.expiry_date)
|
||
}
|
||
else
|
||
{:error, reason} -> {:error, reason}
|
||
end
|
||
end
|
||
|
||
@doc "解析 CouponBatchMinted 事件日志"
|
||
def parse_batch_minted_event(log) do
|
||
case log.first_topic do
|
||
@coupon_batch_minted_topic ->
|
||
%{
|
||
batch_id: decode_uint256(log.second_topic),
|
||
issuer: decode_address(log.third_topic),
|
||
coupon_type: decode_coupon_type(decode_uint8_from_data(log.data, 0)),
|
||
face_value: decode_uint256_from_data(log.data, 1),
|
||
quantity: decode_uint256_from_data(log.data, 2),
|
||
start_token_id: decode_uint256_from_data(log.data, 3)
|
||
}
|
||
|
||
_ ->
|
||
nil
|
||
end
|
||
end
|
||
|
||
@doc "获取券批次摘要信息"
|
||
def get_batch_summary(batch_id, contract_address) do
|
||
%{
|
||
batch_id: batch_id,
|
||
contract: contract_address,
|
||
minted_event: find_batch_minted_event(batch_id, contract_address),
|
||
holder_count: count_current_holders(batch_id, contract_address)
|
||
}
|
||
end
|
||
|
||
# ── 私有函数 ──
|
||
|
||
defp call_get_config(contract_address, token_id) do
|
||
case Reader.query_contract(contract_address, @coupon_abi, %{
|
||
"getConfig" => [token_id]
|
||
}) do
|
||
%{"getConfig" => {:ok, [issuer, face_value, coupon_type, expiry_date, max_resale, transferable]}} ->
|
||
{:ok,
|
||
%{
|
||
issuer: issuer,
|
||
face_value: face_value,
|
||
coupon_type: coupon_type,
|
||
expiry_date: expiry_date,
|
||
max_resale_count: max_resale,
|
||
transferable: transferable
|
||
}}
|
||
|
||
_ ->
|
||
{:error, :contract_call_failed}
|
||
end
|
||
end
|
||
|
||
defp call_get_resale_count(contract_address, token_id) do
|
||
case Reader.query_contract(contract_address, @coupon_abi, %{
|
||
"getResaleCount" => [token_id]
|
||
}) do
|
||
%{"getResaleCount" => {:ok, [count]}} -> {:ok, count}
|
||
_ -> {:error, :contract_call_failed}
|
||
end
|
||
end
|
||
|
||
defp decode_coupon_type(0), do: :utility
|
||
defp decode_coupon_type(1), do: :security
|
||
defp decode_coupon_type(_), do: :unknown
|
||
|
||
defp decode_uint256(nil), do: 0
|
||
defp decode_uint256("0x" <> hex), do: String.to_integer(hex, 16)
|
||
|
||
defp decode_address(nil), do: nil
|
||
defp decode_address("0x000000000000000000000000" <> addr), do: "0x" <> addr
|
||
|
||
defp decode_uint256_from_data(data, offset) do
|
||
data
|
||
|> String.slice(2 + offset * 64, 64)
|
||
|> String.to_integer(16)
|
||
end
|
||
|
||
defp decode_uint8_from_data(data, offset) do
|
||
decode_uint256_from_data(data, offset)
|
||
end
|
||
|
||
defp find_batch_minted_event(_batch_id, _contract_address) do
|
||
# 查询链上日志获取铸造事件
|
||
nil
|
||
end
|
||
|
||
defp count_current_holders(_batch_id, _contract_address) do
|
||
# 聚合当前持有人数量
|
||
0
|
||
end
|
||
end
|