Add a "Tokens" tab on the Address page

- Add the route and the controller;
- Add the overview and the token list html;
  - Sort tokens by type and then number of transfers;
  - Support pagination.
pull/606/head
Lucas Narciso 6 years ago
parent 46998b5afb
commit 1942248670
No known key found for this signature in database
GPG Key ID: 9E89F4CF3FBAB001
  1. 2
      apps/block_scout_web/assets/css/components/_tile.scss
  2. 14
      apps/block_scout_web/lib/block_scout_web/chain.ex
  3. 33
      apps/block_scout_web/lib/block_scout_web/controllers/address_token_controller.ex
  4. 7
      apps/block_scout_web/lib/block_scout_web/router.ex
  5. 17
      apps/block_scout_web/lib/block_scout_web/templates/address_contract/index.html.eex
  6. 32
      apps/block_scout_web/lib/block_scout_web/templates/address_internal_transaction/index.html.eex
  7. 15
      apps/block_scout_web/lib/block_scout_web/templates/address_read_contract/index.html.eex
  8. 10
      apps/block_scout_web/lib/block_scout_web/templates/address_token/_tokens.html.eex
  9. 132
      apps/block_scout_web/lib/block_scout_web/templates/address_token/index.html.eex
  10. 32
      apps/block_scout_web/lib/block_scout_web/templates/address_transaction/index.html.eex
  11. 2
      apps/block_scout_web/lib/block_scout_web/templates/tokens/overview/_details.html.eex
  12. 9
      apps/block_scout_web/lib/block_scout_web/views/address_token_view.ex
  13. 110
      apps/block_scout_web/test/block_scout_web/controllers/address_token_controller_test.exs
  14. 25
      apps/block_scout_web/test/block_scout_web/views/address_token_view_test.exs
  15. 13
      apps/explorer/lib/explorer/chain.ex
  16. 38
      apps/explorer/lib/explorer/chain/token.ex
  17. 144
      apps/explorer/test/explorer/chain_test.exs

@ -49,7 +49,6 @@
&-token {
border: 1px solid $border-color;
}
&-token-transfer {
@ -114,6 +113,7 @@
&-hash {
font-weight: 300;
}
&-lg {
font-size: 16px;
color: $body-color;

@ -12,7 +12,7 @@ defmodule BlockScoutWeb.Chain do
string_to_transaction_hash: 1
]
alias Explorer.Chain.{Address, Block, InternalTransaction, Log, TokenTransfer, Transaction}
alias Explorer.Chain.{Address, Block, InternalTransaction, Log, Token, TokenTransfer, Transaction}
alias Explorer.PagingOptions
@page_size 50
@ -118,6 +118,9 @@ defmodule BlockScoutWeb.Chain do
def paging_options(%{"inserted_at" => inserted_at}),
do: [paging_options: %{@default_paging_options | key: inserted_at}]
def paging_options(%{"token_name" => name, "token_type" => type, "token_inserted_at" => inserted_at}),
do: [paging_options: %{@default_paging_options | key: {name, type, inserted_at}}]
def paging_options(_params), do: [paging_options: @default_paging_options]
def param_to_block_number(formatted_number) when is_binary(formatted_number) do
@ -167,6 +170,15 @@ defmodule BlockScoutWeb.Chain do
%{"inserted_at" => inserted_at_datetime}
end
defp paging_params(%Token{name: name, type: type, inserted_at: inserted_at}) do
inserted_at_datetime =
inserted_at
|> DateTime.from_naive!("Etc/UTC")
|> DateTime.to_iso8601()
%{"token_name" => name, "token_type" => type, "token_inserted_at" => inserted_at_datetime}
end
defp transaction_from_param(param) do
with {:ok, hash} <- string_to_transaction_hash(param) do
hash_to_transaction(hash)

@ -0,0 +1,33 @@
defmodule BlockScoutWeb.AddressTokenController do
use BlockScoutWeb, :controller
alias Explorer.{Chain, Market}
alias Explorer.ExchangeRates.Token
import BlockScoutWeb.AddressController, only: [transaction_count: 1]
import BlockScoutWeb.Chain, only: [next_page_params: 3, paging_options: 1, split_list_by_page: 1]
def index(conn, %{"address_id" => address_hash_string} = params) do
with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string),
{:ok, address} <- Chain.hash_to_address(address_hash) do
tokens_plus_one = Chain.tokens_with_number_of_transfers_from_address(address_hash, paging_options(params))
{tokens, next_page} = split_list_by_page(tokens_plus_one)
render(
conn,
"index.html",
address: address,
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
transaction_count: transaction_count(address),
next_page_params: next_page_params(next_page, tokens, params),
tokens: tokens
)
else
:error ->
unprocessable_entity(conn)
{:error, :not_found} ->
not_found(conn)
end
end
end

@ -90,6 +90,13 @@ defmodule BlockScoutWeb.Router do
as: :read_contract
)
resources(
"/tokens",
AddressTokenController,
only: [:index],
as: :token
)
resources(
"/token_balances",
AddressTokenBalanceController,

@ -9,7 +9,14 @@
<%= link(
gettext("Transactions"),
class: "nav-link",
to: address_transaction_path(@conn, :index, @conn.params["address_id"])
to: address_transaction_path(@conn, :index, @address.hash)
) %>
</li>
<li class="nav-item">
<%= link(
gettext("Tokens"),
class: "nav-link",
to: address_token_path(@conn, :index, @address.hash)
) %>
</li>
<li class="nav-item">
@ -17,12 +24,12 @@
gettext("Internal Transactions"),
class: "nav-link",
"data-test": "internal_transactions_tab_link",
to: address_internal_transaction_path(@conn, :index, @conn.params["address_id"])
to: address_internal_transaction_path(@conn, :index, @address.hash)
) %>
</li>
<li class="nav-item">
<%= link(
to: address_contract_path(@conn, :index, @conn.params["address_id"]),
to: address_contract_path(@conn, :index, @address.hash),
class: "nav-link active") do %>
<%= gettext("Code") %>
@ -35,7 +42,7 @@
<li class="nav-item">
<%= link(
gettext("Read Contract"),
to: address_read_contract_path(@conn, :index, @conn.params["address_id"]),
to: address_read_contract_path(@conn, :index, @address.hash),
class: "nav-link")%>
</li>
<% end %>
@ -46,7 +53,7 @@
<%= if !smart_contract_verified?(@address) do %>
<%= link(
gettext("Verify and Publish"),
to: address_verify_contract_path(@conn, :new, @conn.params["address_id"]),
to: address_verify_contract_path(@conn, :new, @address.hash),
class: "button button-primary button-sm float-right ml-3",
"data-test": "verify_and_publish"
) %>

@ -12,7 +12,14 @@
<%= link(
gettext("Transactions"),
class: "nav-link",
to: address_transaction_path(@conn, :index, @conn.params["address_id"])
to: address_transaction_path(@conn, :index, @address.hash)
) %>
</li>
<li class="nav-item">
<%= link(
gettext("Tokens"),
class: "nav-link",
to: address_token_path(@conn, :index, @address.hash)
) %>
</li>
<li class="nav-item">
@ -20,13 +27,13 @@
gettext("Internal Transactions"),
class: "nav-link active",
"data-test": "internal_transactions_tab_link",
to: address_internal_transaction_path(@conn, :index, @conn.params["address_id"])
to: address_internal_transaction_path(@conn, :index, @address.hash)
) %>
</li>
<%= if contract?(@address) do %>
<li class="nav-item">
<%= link(
to: address_contract_path(@conn, :index, @conn.params["address_id"]),
to: address_contract_path(@conn, :index, @address.hash),
class: "nav-link") do %>
<%= gettext("Code") %>
@ -40,7 +47,7 @@
<li class="nav-item">
<%= link(
gettext("Read Contract"),
to: address_read_contract_path(@conn, :index, @conn.params["address_id"]),
to: address_read_contract_path(@conn, :index, @address.hash),
class: "nav-link")%>
</li>
<% end %>
@ -54,17 +61,22 @@
<%= link(
gettext("Transactions"),
class: "dropdown-item",
to: address_transaction_path(@conn, :index, @conn.params["address_id"])
to: address_transaction_path(@conn, :index, @address.hash)
) %>
<%= link(
gettext("Tokens"),
class: "dropdown-item",
to: address_token_path(@conn, :index, @address.hash)
) %>
<%= link(
gettext("Internal Transactions"),
class: "dropdown-item",
"data-test": "internal_transactions_tab_link",
to: address_internal_transaction_path(@conn, :index, @conn.params["address_id"])
to: address_internal_transaction_path(@conn, :index, @address.hash)
) %>
<%= if contract?(@address) do %>
<%= link(
to: address_contract_path(@conn, :index, @conn.params["address_id"]),
to: address_contract_path(@conn, :index, @address.hash),
class: "dropdown-item") do %>
<%= gettext("Code") %>
@ -96,7 +108,7 @@
<div class="dropdown-menu dropdown-menu-right filter" aria-labelledby="dropdownMenu2">
<%= link(
gettext("All"),
to: address_internal_transaction_path(@conn, :index, @conn.params["address_id"]),
to: address_internal_transaction_path(@conn, :index, @address.hash),
class: "address__link address__link--active dropdown-item",
"data-test": "filter_option"
) %>
@ -105,7 +117,7 @@
to: address_internal_transaction_path(
@conn,
:index,
@conn.params["address_id"],
@address.hash,
filter: "to"
),
class: "address__link address__link--active dropdown-item",
@ -116,7 +128,7 @@
to: address_internal_transaction_path(
@conn,
:index,
@conn.params["address_id"],
@address.hash,
filter: "from"
),
class: "address__link address__link--active dropdown-item",

@ -9,7 +9,14 @@
<%= link(
gettext("Transactions"),
class: "nav-link",
to: address_transaction_path(@conn, :index, @conn.params["address_id"])
to: address_transaction_path(@conn, :index, @address.hash)
) %>
</li>
<li class="nav-item">
<%= link(
gettext("Tokens"),
class: "nav-link",
to: address_token_path(@conn, :index, @address.hash)
) %>
</li>
<li class="nav-item">
@ -17,12 +24,12 @@
gettext("Internal Transactions"),
class: "nav-link",
"data-test": "internal_transactions_tab_link",
to: address_internal_transaction_path(@conn, :index, @conn.params["address_id"])
to: address_internal_transaction_path(@conn, :index, @address.hash)
) %>
</li>
<li class="nav-item">
<%= link(
to: address_contract_path(@conn, :index, @conn.params["address_id"]),
to: address_contract_path(@conn, :index, @address.hash),
class: "nav-link") do %>
<%= gettext("Code") %>
@ -34,7 +41,7 @@
<li class="nav-item">
<%= link(
gettext("Read Contract"),
to: address_read_contract_path(@conn, :index, @conn.params["address_id"]),
to: address_read_contract_path(@conn, :index, @address.hash),
class: "nav-link active")%>
</li>
</ul>

@ -0,0 +1,10 @@
<div class="tile tile-type-token">
<div class="row justify-content">
<div class="col-md-12 d-flex flex-column tile-label">
<%= link(to: token_path(@conn, :show, @token.contract_address_hash), class: "tile-title-lg") do %>
<%= token_name(@token) %>
<% end %>
<span><%= @token.type %> - <%= number_of_transfers(@token) %></span>
</div>
</div>
</div>

@ -0,0 +1,132 @@
<section class="container">
<%= render BlockScoutWeb.AddressView, "overview.html", assigns %>
<section>
<div class="card">
<div class="card-header">
<!-- DESKTOP TAB NAV -->
<ul class="nav nav-tabs card-header-tabs d-none d-md-inline-flex">
<li class="nav-item">
<%= link(
gettext("Transactions"),
class: "nav-link",
to: address_transaction_path(@conn, :index, @address.hash)
) %>
</li>
<li class="nav-item">
<%= link(
gettext("Tokens"),
class: "nav-link active",
to: address_token_path(@conn, :index, @address.hash)
) %>
</li>
<li class="nav-item"> <%= link(
gettext("Internal Transactions"),
class: "nav-link",
"data-test": "internal_transactions_tab_link",
to: address_internal_transaction_path(@conn, :index, @address.hash)
) %>
</li>
<%= if AddressView.contract?(@address) do %>
<li class="nav-item">
<%= link(
to: address_contract_path(@conn, :index, @address.hash),
class: "nav-link") do %>
<%= gettext("Code") %>
<%= if AddressView.smart_contract_verified?(@address) do %>
<i class="far fa-check-circle"></i>
<% end %>
<% end %>
</li>
<% end %>
<%= if AddressView.smart_contract_with_read_only_functions?(@address) do %>
<li class="nav-item">
<%= link(
gettext("Read Contract"),
to: address_read_contract_path(@conn, :index, @address.hash),
class: "nav-link")%>
</li>
<% end %>
<%= if AddressView.smart_contract_with_read_only_functions?(@address) do %>
<li class="nav-item">
<%= link(
gettext("Read Contract"),
to: address_read_contract_path(@conn, :index, @address.hash),
class: "nav-link")%>
</li>
<% end %>
</ul>
<!-- MOBILE DROPDOWN NAV -->
<ul class="nav nav-tabs card-header-tabs d-md-none">
<li class="nav-item dropdown flex-fill text-center">
<a class="nav-link active dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">Tokens</a>
<div class="dropdown-menu">
<%= link(
gettext("Transactions"),
class: "dropdown-item",
to: address_transaction_path(@conn, :index, @address.hash)
) %>
<%= link(
gettext("Tokens"),
class: "dropdown-item",
to: address_token_path(@conn, :index, @address.hash)
) %>
<%= link(
gettext("Internal Transactions"),
class: "dropdown-item",
"data-test": "internal_transactions_tab_link",
to: address_internal_transaction_path(@conn, :index, @address.hash)
) %>
<%= if AddressView.contract?(@address) do %>
<%= link(
to: address_contract_path(@conn, :index, @address.hash),
class: "dropdown-item") do %>
<%= gettext("Code") %>
<%= if AddressView.smart_contract_verified?(@address) do %>
<i class="far fa-check-circle"></i>
<% end %>
<% end %>
<% end %>
</div>
</li>
</ul>
</div>
<div class="card-body">
<h2 class="card-title"><%= gettext "Tokens" %></h2>
<%= if Enum.any?(@tokens) do %>
<%= for token <- @tokens do %>
<%= render "_tokens.html", conn: @conn, token: token %>
<% end %>
<% else %>
<div class="tile tile-muted text-center">
<span><%= gettext "There are no tokens for this address." %></span>
</div>
<% end %>
<div>
<%= if @next_page_params do %>
<%= link(
gettext("Next"),
class: "button button-secondary button-sm float-right",
to: address_token_path(
@conn,
:index,
@address,
@next_page_params
)
) %>
<% end %>
</div>
</div>
</div>
</section>
</section>

@ -12,7 +12,14 @@
<%= link(
gettext("Transactions"),
class: "nav-link active",
to: address_transaction_path(@conn, :index, @conn.params["address_id"])
to: address_transaction_path(@conn, :index, @address.hash)
) %>
</li>
<li class="nav-item">
<%= link(
gettext("Tokens"),
class: "nav-link",
to: address_token_path(@conn, :index, @address.hash)
) %>
</li>
<li class="nav-item">
@ -20,13 +27,13 @@
gettext("Internal Transactions"),
class: "nav-link",
"data-test": "internal_transactions_tab_link",
to: address_internal_transaction_path(@conn, :index, @conn.params["address_id"])
to: address_internal_transaction_path(@conn, :index, @address.hash)
) %>
</li>
<%= if contract?(@address) do %>
<li class="nav-item">
<%= link(
to: address_contract_path(@conn, :index, @conn.params["address_id"]),
to: address_contract_path(@conn, :index, @address.hash),
class: "nav-link") do %>
<%= gettext("Code") %>
@ -40,7 +47,7 @@
<li class="nav-item">
<%= link(
gettext("Read Contract"),
to: address_read_contract_path(@conn, :index, @conn.params["address_id"]),
to: address_read_contract_path(@conn, :index, @address.hash),
class: "nav-link")%>
</li>
<% end %>
@ -54,17 +61,22 @@
<%= link(
gettext("Transactions"),
class: "dropdown-item",
to: address_transaction_path(@conn, :index, @conn.params["address_id"])
to: address_transaction_path(@conn, :index, @address.hash)
) %>
<%= link(
gettext("Tokens"),
class: "dropdown-item",
to: address_token_path(@conn, :index, @address.hash)
) %>
<%= link(
gettext("Internal Transactions"),
class: "dropdown-item",
"data-test": "internal_transactions_tab_link",
to: address_internal_transaction_path(@conn, :index, @conn.params["address_id"])
to: address_internal_transaction_path(@conn, :index, @address.hash)
) %>
<%= if contract?(@address) do %>
<%= link(
to: address_contract_path(@conn, :index, @conn.params["address_id"]),
to: address_contract_path(@conn, :index, @address.hash),
class: "dropdown-item") do %>
<%= gettext("Code") %>
@ -97,7 +109,7 @@
<div class="dropdown-menu dropdown-menu-right filter" aria-labelledby="dropdownMenu2">
<%= link(
gettext("All"),
to: address_transaction_path(@conn, :index, @conn.params["address_id"]),
to: address_transaction_path(@conn, :index, @address.hash),
class: "address__link address__link--active dropdown-item",
"data-test": "filter_option"
) %>
@ -106,7 +118,7 @@
to: address_transaction_path(
@conn,
:index,
@conn.params["address_id"],
@address.hash,
filter: "to"
),
class: "address__link address__link--active dropdown-item",
@ -117,7 +129,7 @@
to: address_transaction_path(
@conn,
:index,
@conn.params["address_id"],
@address.hash,
filter: "from"
),
class: "address__link address__link--active dropdown-item",

@ -23,8 +23,6 @@
<% end %>
</h1>
<h3><%= to_string(@token.contract_address_hash) %></h3>
<div class="d-flex flex-row justify-content-start text-muted">

@ -0,0 +1,9 @@
defmodule BlockScoutWeb.AddressTokenView do
use BlockScoutWeb, :view
alias BlockScoutWeb.AddressView
def number_of_transfers(token) do
ngettext("%{count} transfer", "%{count} transfers", token.number_of_transfers)
end
end

@ -0,0 +1,110 @@
defmodule BlockScoutWeb.AddressTokenControllerTest do
use BlockScoutWeb.ConnCase
import BlockScoutWeb.Router.Helpers, only: [address_token_path: 3]
alias Explorer.Chain.{Token}
describe "GET index/2" do
test "with invalid address hash", %{conn: conn} do
conn = get(conn, address_token_path(conn, :index, "invalid_address"))
assert html_response(conn, 422)
end
test "with valid address hash without address", %{conn: conn} do
conn = get(conn, address_token_path(conn, :index, "0x8bf38d4764929064f2d4d3a56520a76ab3df415b"))
assert html_response(conn, 404)
end
test "returns tokens for the address", %{conn: conn} do
address = insert(:address)
token1 =
:token
|> insert(name: "token1")
token2 =
:token
|> insert(name: "token2")
insert(
:token_transfer,
token_contract_address: token1.contract_address,
from_address: address,
to_address: build(:address)
)
insert(
:token_transfer,
token_contract_address: token2.contract_address,
from_address: build(:address),
to_address: address
)
conn = get(conn, address_token_path(conn, :index, address))
actual_token_hashes =
conn.assigns.tokens
|> Enum.map(& &1.contract_address_hash)
assert html_response(conn, 200)
assert Enum.member?(actual_token_hashes, token1.contract_address_hash)
assert Enum.member?(actual_token_hashes, token2.contract_address_hash)
end
test "returns next page of results based on last seen token", %{conn: conn} do
address = insert(:address)
second_page_tokens =
1..50
|> Enum.reduce([], fn i, acc ->
token = insert(:token, name: "A Token#{i}", type: "ERC-20")
insert(:token_transfer, token_contract_address: token.contract_address, from_address: address)
acc ++ [token.name]
end)
|> Enum.sort()
token = insert(:token, name: "Another Token", type: "ERC-721")
insert(:token_transfer, token: token, from_address: address)
%Token{name: name, type: type} = token
conn =
get(conn, address_token_path(BlockScoutWeb.Endpoint, :index, address.hash), %{
"name" => name,
"type" => type
})
actual_tokens =
conn.assigns.tokens
|> Enum.map(& &1.name)
|> Enum.sort()
assert second_page_tokens == actual_tokens
end
test "next_page_params exists if not on last page", %{conn: conn} do
address = insert(:address)
Enum.each(1..51, fn i ->
token = insert(:token, name: "A Token#{i}", type: "ERC-20")
insert(:token_transfer, token_contract_address: token.contract_address, from_address: address)
end)
conn = get(conn, address_token_path(BlockScoutWeb.Endpoint, :index, address.hash))
assert conn.assigns.next_page_params
end
test "next_page_params are empty if on last page", %{conn: conn} do
address = insert(:address)
token = insert(:token)
insert(:token_transfer, token_contract_address: token.contract_address, from_address: address)
conn = get(conn, address_token_path(BlockScoutWeb.Endpoint, :index, address.hash))
refute conn.assigns.next_page_params
end
end
end

@ -0,0 +1,25 @@
defmodule BlockScoutWeb.AddressTokenViewTest do
use BlockScoutWeb.ConnCase, async: true
alias BlockScoutWeb.AddressTokenView
describe "number_of_transfers/1" do
test "returns the singular form when there is only one transfer" do
token = %{number_of_transfers: 1}
assert AddressTokenView.number_of_transfers(token) == "1 transfer"
end
test "returns the plural form when there is more than one transfer" do
token = %{number_of_transfers: 2}
assert AddressTokenView.number_of_transfers(token) == "2 transfers"
end
test "returns the plural form when there are 0 transfers" do
token = %{number_of_transfers: 0}
assert AddressTokenView.number_of_transfers(token) == "0 transfers"
end
end
end

@ -1647,10 +1647,17 @@ defmodule Explorer.Chain do
Repo.one(query) != nil
end
@spec fetch_tokens_from_address_hash(Hash.Address.t()) :: []
def fetch_tokens_from_address_hash(address_hash) do
@spec tokens_with_number_of_transfers_from_address(Hash.Address.t(), [any()]) :: []
def tokens_with_number_of_transfers_from_address(address_hash, paging_options \\ []) do
address_hash
|> Token.with_transfers_by_address()
|> fetch_tokens_from_address_hash(paging_options)
|> add_number_of_transfers_to_tokens_from_address(address_hash)
end
@spec fetch_tokens_from_address_hash(Hash.Address.t(), [any()]) :: []
def fetch_tokens_from_address_hash(address_hash, paging_options \\ []) do
address_hash
|> Token.with_transfers_by_address(paging_options)
|> Repo.all()
end

@ -20,8 +20,11 @@ defmodule Explorer.Chain.Token do
use Ecto.Schema
import Ecto.{Changeset, Query}
alias Explorer.PagingOptions
alias Explorer.Chain.{Address, Hash, Token, TokenTransfer}
@default_paging_options %PagingOptions{page_size: 50}
@typedoc """
* `:name` - Name of the token
* `:symbol` - Trading symbol of the token
@ -43,6 +46,8 @@ defmodule Explorer.Chain.Token do
contract_address_hash: Hash.Address.t()
}
@typep paging_options :: {:paging_options, PagingOptions.t()}
@primary_key false
schema "tokens" do
field(:name, :string)
@ -79,17 +84,26 @@ defmodule Explorer.Chain.Token do
Builds an `Ecto.Query` to fetch tokens that the given address has interacted with.
In order to fetch a token, the given address must have transfered tokens to or received tokens
from another address.
from another address. This quey orders by the token type and name.
"""
def with_transfers_by_address(address_hash) do
@spec with_transfers_by_address(Hash.t(), [paging_options()]) :: %Ecto.Query{}
def with_transfers_by_address(address_hash, options \\ []) do
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
subquery =
from(
token in Token,
join: tt in TokenTransfer,
on: tt.token_contract_address_hash == token.contract_address_hash,
where: tt.to_address_hash == ^address_hash or tt.from_address_hash == ^address_hash,
distinct: tt.token_contract_address_hash,
select: token
distinct: [:contract_address_hash]
)
query = from(t in subquery(subquery), order_by: [desc: :type, asc: :name])
query
|> page_token(paging_options)
|> limit(^paging_options.page_size)
end
@doc """
@ -99,9 +113,21 @@ defmodule Explorer.Chain.Token do
from(
t in Token,
join: tt in TokenTransfer,
on: tt.token_contract_address_hash == ^token_hash,
on: tt.token_contract_address_hash == t.contract_address_hash,
where: t.contract_address_hash == ^token_hash,
where: tt.to_address_hash == ^address_hash or tt.from_address_hash == ^address_hash
where: tt.to_address_hash == ^address_hash or tt.from_address_hash == ^address_hash,
select: tt
)
end
def page_token(query, %PagingOptions{key: nil}), do: query
def page_token(query, %PagingOptions{key: {name, type, inserted_at}}) do
where(
query,
[token],
token.type < ^type or (token.type == ^type and token.name > ^name) or
(token.type == ^type and token.name == ^name and token.inserted_at < ^inserted_at)
)
end
end

@ -2295,6 +2295,39 @@ defmodule Explorer.ChainTest do
end
end
describe "tokens_with_number_of_transfers_from_address/2" do
test "returns tokens with number of transfers attached" do
address = insert(:address)
token =
:token
|> insert(name: "token-c", type: "ERC-721")
|> Repo.preload(:contract_address)
insert(
:token_transfer,
token_contract_address: token.contract_address,
from_address: address,
to_address: build(:address)
)
insert(
:token_transfer,
token_contract_address: token.contract_address,
from_address: build(:address),
to_address: address
)
fetched_token =
address.hash
|> Chain.tokens_with_number_of_transfers_from_address()
|> List.first()
assert fetched_token.name == "token-c"
assert fetched_token.number_of_transfers == 2
end
end
describe "fetch_tokens_from_address_hash/1" do
test "only returns tokens that a given address has interacted with" do
alice = insert(:address)
@ -2390,6 +2423,117 @@ defmodule Explorer.ChainTest do
assert expected_tokens == [token.name]
end
test "orders by type, name and inserted_at time" do
address = insert(:address)
first_token =
:token
|> insert(name: "token-c", type: "ERC-721")
|> Repo.preload(:contract_address)
second_token =
:token
|> insert(name: "token-a", type: "ERC-20")
|> Repo.preload(:contract_address)
third_token =
:token
|> insert(name: "token-b", type: "ERC-20")
|> Repo.preload(:contract_address)
fourth_token =
:token
|> insert(name: "token-b", type: "ERC-20", inserted_at: third_token.inserted_at)
|> Repo.preload(:contract_address)
insert(
:token_transfer,
token_contract_address: first_token.contract_address,
from_address: address,
to_address: build(:address)
)
insert(
:token_transfer,
token_contract_address: second_token.contract_address,
from_address: address,
to_address: build(:address)
)
insert(
:token_transfer,
token_contract_address: third_token.contract_address,
from_address: build(:address),
to_address: address
)
insert(
:token_transfer,
token_contract_address: fourth_token.contract_address,
from_address: build(:address),
to_address: address
)
fetched_tokens =
address.hash
|> Chain.fetch_tokens_from_address_hash()
|> Enum.map(&Repo.preload(&1, :contract_address))
assert fetched_tokens == [first_token, second_token, third_token, fourth_token]
end
test "supports pagination" do
address = insert(:address)
first_token =
:token
|> insert(name: "token-c", type: "ERC-721")
|> Repo.preload(:contract_address)
second_token =
:token
|> insert(name: "token-a", type: "ERC-20")
|> Repo.preload(:contract_address)
third_token =
:token
|> insert(name: "token-b", type: "ERC-20")
|> Repo.preload(:contract_address)
paging_options = %PagingOptions{
page_size: 1,
key: {first_token.name, first_token.type, first_token.inserted_at}
}
insert(
:token_transfer,
token_contract_address: first_token.contract_address,
from_address: address,
to_address: build(:address)
)
insert(
:token_transfer,
token_contract_address: second_token.contract_address,
from_address: address,
to_address: build(:address)
)
insert(
:token_transfer,
token_contract_address: third_token.contract_address,
from_address: build(:address),
to_address: address
)
fetched_tokens =
address.hash
|> Chain.fetch_tokens_from_address_hash(paging_options: paging_options)
|> Enum.map(& &1.name)
assert fetched_tokens == [second_token.name]
end
end
describe "count_token_transfers_from_address_hash/2" do

Loading…
Cancel
Save