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