diff --git a/apps/block_scout_web/assets/css/components/_tile.scss b/apps/block_scout_web/assets/css/components/_tile.scss index 9f07317c08..d7a83abdf0 100644 --- a/apps/block_scout_web/assets/css/components/_tile.scss +++ b/apps/block_scout_web/assets/css/components/_tile.scss @@ -63,6 +63,28 @@ } } + &-unique-token { + border-left: 4px solid $orange; + padding: 35px 0; + + .tile-label { + color: $orange; + } + } + + &-unique-token-image{ + border-left: 4px solid $orange; + padding: 0; + + .tile-label { + color: $orange; + } + + & .tile-content { + padding: 45px 0; + } + } + &-internal-transaction { border-left: 4px solid $teal; @@ -150,3 +172,8 @@ margin: 0; } } + +.tile-image { + max-width: 140px; + max-height: 140px; +} diff --git a/apps/block_scout_web/lib/block_scout_web/chain.ex b/apps/block_scout_web/lib/block_scout_web/chain.ex index c41229acec..d65d11de7d 100644 --- a/apps/block_scout_web/lib/block_scout_web/chain.ex +++ b/apps/block_scout_web/lib/block_scout_web/chain.ex @@ -29,6 +29,10 @@ defmodule BlockScoutWeb.Chain do @page_size 50 @default_paging_options %PagingOptions{page_size: @page_size + 1} + def default_paging_options do + @default_paging_options + end + def current_filter(%{paging_options: paging_options} = params) do params |> Map.get("filter") diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/inventory_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/inventory_controller.ex new file mode 100644 index 0000000000..a6a19a8523 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/inventory_controller.ex @@ -0,0 +1,52 @@ +defmodule BlockScoutWeb.Tokens.InventoryController do + use BlockScoutWeb, :controller + + alias Explorer.Chain + alias Explorer.Chain.TokenTransfer + + import BlockScoutWeb.Chain, only: [split_list_by_page: 1, default_paging_options: 0] + + def index(conn, %{"token_id" => address_hash_string} = params) do + with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), + {:ok, token} <- Chain.token_from_address_hash(address_hash) do + unique_tokens = + Chain.address_to_unique_tokens( + token.contract_address_hash, + unique_tokens_paging_options(params) + ) + + {unique_tokens_paginated, next_page} = split_list_by_page(unique_tokens) + + render( + conn, + "index.html", + token: token, + unique_tokens: unique_tokens_paginated, + total_token_transfers: Chain.count_token_transfers_from_token_hash(address_hash), + total_token_holders: Chain.count_token_holders_from_token_hash(address_hash), + next_page_params: unique_tokens_next_page(next_page, unique_tokens_paginated, params) + ) + else + :error -> + not_found(conn) + + {:error, :not_found} -> + not_found(conn) + end + end + + defp unique_tokens_paging_options(%{"unique_token" => token_id}), + do: [paging_options: %{default_paging_options() | key: {token_id}}] + + defp unique_tokens_paging_options(_params), do: [paging_options: default_paging_options()] + + defp unique_tokens_next_page([], _list, _params), do: nil + + defp unique_tokens_next_page(_, list, params) do + Map.merge(params, paging_params(List.last(list))) + end + + defp paging_params(%TokenTransfer{token_id: token_id}) do + %{"unique_token" => Decimal.to_integer(token_id)} + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/router.ex b/apps/block_scout_web/lib/block_scout_web/router.ex index 93e6532cd3..cc18314f4b 100644 --- a/apps/block_scout_web/lib/block_scout_web/router.ex +++ b/apps/block_scout_web/lib/block_scout_web/router.ex @@ -135,6 +135,13 @@ defmodule BlockScoutWeb.Router do only: [:index], as: :holder ) + + resources( + "/inventory", + Tokens.InventoryController, + only: [:index], + as: :inventory + ) end resources( diff --git a/apps/block_scout_web/lib/block_scout_web/templates/tokens/inventory/_token.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/tokens/inventory/_token.html.eex new file mode 100644 index 0000000000..88c36e5158 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/tokens/inventory/_token.html.eex @@ -0,0 +1,28 @@ + +
+
+
+ + <%= gettext "Unique Token" %> +
+ +
+ + <%= gettext "Token ID" %>: + + <%= @token_transfer.token_id %> + + + + + <%= gettext "Owner Address" %>: + + <%= render BlockScoutWeb.AddressView, + "_link.html", + address: @token_transfer.to_address, + contract: false %> + + +
+
+
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/tokens/inventory/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/tokens/inventory/index.html.eex new file mode 100644 index 0000000000..8b2fdcb5b7 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/tokens/inventory/index.html.eex @@ -0,0 +1,42 @@ +
+ <%= render( + OverviewView, + "_details.html", + token: @token, + total_token_transfers: @total_token_transfers, + total_token_holders: @total_token_holders, + conn: @conn + ) %> + +
+
+
+ <%= render OverviewView, "_tabs.html", assigns %> +
+ +
+

<%= gettext "Inventory" %>

+ + <%= if Enum.any?(@unique_tokens) do %> + <%= for token_transfer <- @unique_tokens do %> + <%= render "_token.html", token_transfer: token_transfer %> + <% end %> + <% else %> +
+ + <%= gettext "There are no tokens." %> + +
+ <% end %> + + <%= if @next_page_params do %> + <%= link( + gettext("Next Page"), + class: "button button-secondary button-small float-right mt-4", + to: token_inventory_path(@conn, :index, @token.contract_address_hash, @next_page_params) + ) %> + <% end %> +
+
+
+
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/tokens/overview/_tabs.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/tokens/overview/_tabs.html.eex index b5dc0dc9b8..b2ca5f3d18 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/tokens/overview/_tabs.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/tokens/overview/_tabs.html.eex @@ -25,6 +25,16 @@ to: token_holder_path(@conn, :index, @token.contract_address_hash) ) %> + + <%= if display_inventory?(@token) do %> + + <% end %> @@ -56,6 +66,14 @@ class: "dropdown-item #{tab_status("token_holders", @conn.request_path)}", to: token_holder_path(@conn, :index, @token.contract_address_hash) ) %> + + <%= if display_inventory?(@token) do %> + <%= link( + gettext("Inventory"), + class: "dropdown-item #{tab_status("inventory", @conn.request_path)}", + to: token_inventory_path(@conn, :index, @token.contract_address_hash) + ) %> + <% end %> diff --git a/apps/block_scout_web/lib/block_scout_web/views/tokens/inventory_view.ex b/apps/block_scout_web/lib/block_scout_web/views/tokens/inventory_view.ex new file mode 100644 index 0000000000..6c3f3a6845 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/tokens/inventory_view.ex @@ -0,0 +1,5 @@ +defmodule BlockScoutWeb.Tokens.InventoryView do + use BlockScoutWeb, :view + + alias BlockScoutWeb.Tokens.OverviewView +end diff --git a/apps/block_scout_web/lib/block_scout_web/views/tokens/overview_view.ex b/apps/block_scout_web/lib/block_scout_web/views/tokens/overview_view.ex index 80bb51a04e..f36bca8b80 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/tokens/overview_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/tokens/overview_view.ex @@ -4,7 +4,7 @@ defmodule BlockScoutWeb.Tokens.OverviewView do alias Explorer.Chain.Token alias BlockScoutWeb.Tokens.TransferView - @tabs ["token_transfers", "token_holders", "read_contract"] + @tabs ["token_transfers", "token_holders", "read_contract", "inventory"] def decimals?(%Token{decimals: nil}), do: false def decimals?(%Token{decimals: _}), do: true @@ -34,4 +34,8 @@ defmodule BlockScoutWeb.Tokens.OverviewView do defp tab_name(["token_transfers"]), do: gettext("Token Transfers") defp tab_name(["token_holders"]), do: gettext("Token Holders") defp tab_name(["read_contract"]), do: gettext("Read Contract") + defp tab_name(["inventory"]), do: gettext("Inventory") + + def display_inventory?(%Token{type: "ERC-721"}), do: true + def display_inventory?(_), do: false end diff --git a/apps/block_scout_web/test/block_scout_web/views/tokens/overview_view_test.exs b/apps/block_scout_web/test/block_scout_web/views/tokens/overview_view_test.exs index 5f18eff56a..22f1e81299 100644 --- a/apps/block_scout_web/test/block_scout_web/views/tokens/overview_view_test.exs +++ b/apps/block_scout_web/test/block_scout_web/views/tokens/overview_view_test.exs @@ -64,4 +64,18 @@ defmodule BlockScoutWeb.Tokens.OverviewViewTest do assert OverviewView.current_tab_name(read_contract_path) == "Read Contract" end end + + describe "display_inventory?/1" do + test "returns true when token is unique" do + token = insert(:token, type: "ERC-721") + + assert OverviewView.display_inventory?(token) == true + end + + test "returns false when token is not unique" do + token = insert(:token, type: "ERC-20") + + assert OverviewView.display_inventory?(token) == false + end + end end