Add token details page backend

pull/486/head
Amanda Sposito 6 years ago committed by Amanda Sposito
parent 1414a94973
commit 95de1ebcb0
  1. 16
      apps/explorer/lib/explorer/chain.ex
  2. 2
      apps/explorer/lib/explorer/chain/token.ex
  3. 80
      apps/explorer/lib/explorer/chain/token_transfer.ex
  4. 189
      apps/explorer/test/explorer/chain/token_transfer_test.exs
  5. 14
      apps/explorer_web/lib/explorer_web/chain.ex
  6. 25
      apps/explorer_web/lib/explorer_web/controllers/token_controller.ex
  7. 48
      apps/explorer_web/lib/explorer_web/templates/token/_token_transfer.html.eex
  8. 79
      apps/explorer_web/lib/explorer_web/templates/token/show.html.eex
  9. 8
      apps/explorer_web/lib/explorer_web/views/currency_helpers.ex
  10. 6
      apps/explorer_web/lib/explorer_web/views/token_view.ex
  11. 36
      apps/explorer_web/priv/gettext/default.pot
  12. 36
      apps/explorer_web/priv/gettext/en/LC_MESSAGES/default.po
  13. 6
      apps/explorer_web/test/explorer_web/views/currency_helpers_test.exs

@ -28,6 +28,7 @@ defmodule Explorer.Chain do
Log,
SmartContract,
Token,
TokenTransfer,
Transaction,
Wei
}
@ -1701,4 +1702,19 @@ defmodule Explorer.Chain do
{:ok, token}
end
end
@spec fetch_token_transfers_from_token_hash(Hash.t(), [paging_options]) :: []
def fetch_token_transfers_from_token_hash(token_address_hash, options \\ []) do
TokenTransfer.fetch_token_transfers_from_token_hash(token_address_hash, options)
end
@spec count_token_transfers_from_token_hash(Hash.t()) :: non_neg_integer()
def count_token_transfers_from_token_hash(token_address_hash) do
TokenTransfer.count_token_transfers_from_token_hash(token_address_hash)
end
@spec count_addresses_in_token_transfers_from_token_hash(Hash.t()) :: non_neg_integer()
def count_addresses_in_token_transfers_from_token_hash(token_address_hash) do
TokenTransfer.count_addresses_in_token_transfers_from_token_hash(token_address_hash)
end
end

@ -19,7 +19,7 @@ defmodule Explorer.Chain.Token do
use Ecto.Schema
import Ecto.Changeset
import Ecto.{Changeset}
alias Explorer.Chain.{Address, Hash, Token}
@typedoc """

@ -24,9 +24,13 @@ defmodule Explorer.Chain.TokenTransfer do
use Ecto.Schema
import Ecto.Changeset
import Ecto.{Changeset, Query}
alias Explorer.Chain.{Address, Hash, Transaction, TokenTransfer}
alias Explorer.Chain.{Address, Block, Hash, Transaction, TokenTransfer}
alias Explorer.{PagingOptions, Repo}
alias Ecto.Adapters.SQL
@default_paging_options %PagingOptions{page_size: 50}
@typedoc """
* `:amount` - The token transferred amount
@ -56,6 +60,8 @@ defmodule Explorer.Chain.TokenTransfer do
log_index: non_neg_integer()
}
@typep paging_options :: {:paging_options, PagingOptions.t()}
@constant "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
schema "token_transfers" do
@ -100,4 +106,74 @@ defmodule Explorer.Chain.TokenTransfer do
`first_topic` field.
"""
def constant, do: @constant
@spec fetch_token_transfers_from_token_hash(Hash.t(), [paging_options]) :: []
def fetch_token_transfers_from_token_hash(token_address_hash, options) do
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
query =
from(
tt in TokenTransfer,
join: t in Transaction,
on: tt.transaction_hash == t.hash,
join: b in Block,
on: t.block_hash == b.hash,
where: tt.token_contract_address_hash == ^token_address_hash,
preload: [{:transaction, :block}, :token, :from_address, :to_address],
order_by: [desc: b.timestamp]
)
query
|> page_token_transfer(paging_options)
|> limit(^paging_options.page_size)
|> Repo.all()
end
@spec count_token_transfers_from_token_hash(Hash.t()) :: non_neg_integer()
def count_token_transfers_from_token_hash(token_address_hash) do
query =
from(
tt in TokenTransfer,
where: tt.token_contract_address_hash == ^token_address_hash,
select: count(tt.id)
)
Repo.one(query)
end
@spec count_addresses_in_token_transfers_from_token_hash(Hash.t()) :: non_neg_integer()
def count_addresses_in_token_transfers_from_token_hash(token_address_hash) do
{:ok, %{rows: [[result]]}} =
SQL.query(
Repo,
"""
select count(*) as "addresses"
from
(
select to_address_hash as "address_hash"
from token_transfers tt1
where tt1.token_contract_address_hash = $1
union
select from_address_hash as "address_hash"
from token_transfers tt2
where tt2.token_contract_address_hash = $1
) as addresses_count
""",
[token_address_hash.bytes]
)
result
end
defp page_token_transfer(query, %PagingOptions{key: nil}), do: query
defp page_token_transfer(query, %PagingOptions{key: inserted_at}) do
where(
query,
[token_transfer],
token_transfer.inserted_at < ^inserted_at
)
end
end

@ -0,0 +1,189 @@
defmodule Explorer.Chain.TokenTransferTest do
use Explorer.DataCase
import Explorer.Factory
alias Explorer.PagingOptions
alias Explorer.Chain.TokenTransfer
doctest Explorer.Chain.TokenTransfer
describe "fetch_token_transfers/2" do
test "returns token transfers for the given address" do
token_contract_address = insert(:contract_address)
transaction =
:transaction
|> insert()
|> with_block()
token = insert(:token, contract_address: token_contract_address)
token_transfer =
insert(
:token_transfer,
to_address: build(:address),
transaction: transaction,
token_contract_address: token_contract_address,
token: token
)
another_transfer =
insert(
:token_transfer,
to_address: build(:address),
transaction: transaction,
token_contract_address: token_contract_address,
token: token
)
insert(
:token_transfer,
to_address: build(:address),
transaction: transaction,
token_contract_address: build(:address),
token: token
)
transfers_ids =
token_contract_address.hash
|> TokenTransfer.fetch_token_transfers_from_token_hash([])
|> Enum.map(& &1.id)
assert transfers_ids == [another_transfer.id, token_transfer.id]
end
test "when there isn't token transfers won't show anything" do
token_contract_address = insert(:contract_address)
insert(:token, contract_address: token_contract_address)
transfers_ids =
token_contract_address.hash
|> TokenTransfer.fetch_token_transfers_from_token_hash([])
|> Enum.map(& &1.id)
assert transfers_ids == []
end
test "token transfers can be paginated" do
token_contract_address = insert(:contract_address)
transaction =
:transaction
|> insert()
|> with_block()
token = insert(:token)
second_page =
insert(
:token_transfer,
to_address: build(:address),
transaction: transaction,
token_contract_address: token_contract_address,
token: token
)
first_page =
insert(
:token_transfer,
to_address: build(:address),
transaction: transaction,
token_contract_address: token_contract_address,
token: token
)
paging_options = %PagingOptions{key: first_page.inserted_at, page_size: 1}
token_transfers_ids_paginated =
TokenTransfer.fetch_token_transfers_from_token_hash(
token_contract_address.hash,
paging_options: paging_options
)
|> Enum.map(& &1.id)
assert token_transfers_ids_paginated == [second_page.id]
end
end
describe "count_token_transfers/1" do
test "counts how many token transfers a token has" do
token_contract_address = insert(:contract_address)
transaction =
:transaction
|> insert()
|> with_block()
token = insert(:token)
insert(
:token_transfer,
to_address: build(:address),
transaction: transaction,
token_contract_address: token_contract_address,
token: token
)
insert(
:token_transfer,
to_address: build(:address),
transaction: transaction,
token_contract_address: token_contract_address,
token: token
)
assert TokenTransfer.count_token_transfers_from_token_hash(token_contract_address.hash) == 2
end
end
describe "count_addresses_in_transfers/1" do
test "counts how many unique addresses that appeared at `to` or `from`" do
token_contract_address = insert(:contract_address)
transaction =
:transaction
|> insert()
|> with_block()
john_address = insert(:address)
jane_address = insert(:address)
bob_address = insert(:address)
insert(
:token_transfer,
from_address: jane_address,
to_address: john_address,
transaction: transaction,
token_contract_address: token_contract_address
)
insert(
:token_transfer,
from_address: john_address,
to_address: jane_address,
transaction: transaction,
token_contract_address: token_contract_address
)
insert(
:token_transfer,
from_address: bob_address,
to_address: jane_address,
transaction: transaction,
token_contract_address: token_contract_address
)
insert(
:token_transfer,
from_address: jane_address,
to_address: bob_address,
transaction: transaction,
token_contract_address: token_contract_address
)
assert TokenTransfer.count_addresses_in_token_transfers_from_token_hash(token_contract_address.hash) == 3
end
end
end

@ -12,7 +12,7 @@ defmodule ExplorerWeb.Chain do
string_to_transaction_hash: 1
]
alias Explorer.Chain.{Address, Block, InternalTransaction, Log, Transaction}
alias Explorer.Chain.{Address, Block, InternalTransaction, Log, Transaction, TokenTransfer}
alias Explorer.PagingOptions
@page_size 50
@ -95,6 +95,9 @@ defmodule ExplorerWeb.Chain do
end
end
def paging_options(%{"inserted_at" => inserted_at}),
do: [paging_options: %{@default_paging_options | key: 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
@ -135,6 +138,15 @@ defmodule ExplorerWeb.Chain do
%{"block_number" => block_number, "index" => index}
end
defp paging_params(%TokenTransfer{inserted_at: inserted_at}) do
inserted_at_datetime =
inserted_at
|> DateTime.from_naive!("Etc/UTC")
|> DateTime.to_iso8601()
%{"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)

@ -1,10 +1,31 @@
defmodule ExplorerWeb.TokenController do
use ExplorerWeb, :controller
def show(conn, %{"id" => id, "locale" => locale}) do
alias Explorer.Chain
import ExplorerWeb.Chain, only: [split_list_by_page: 1, paging_options: 1, next_page_params: 3]
def show(conn, %{"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),
token_transfers <- Chain.fetch_token_transfers_from_token_hash(address_hash, paging_options(params)) do
{token_transfers_paginated, next_page} = split_list_by_page(token_transfers)
render(
conn,
"show.html"
"show.html",
transfers: token_transfers_paginated,
token: token,
total_token_transfers: Chain.count_token_transfers_from_token_hash(address_hash),
total_address_in_token_transfers: Chain.count_addresses_in_token_transfers_from_token_hash(address_hash),
next_page_params: next_page_params(next_page, token_transfers_paginated, params)
)
else
:error ->
not_found(conn)
{:error, :not_found} ->
not_found(conn)
end
end
end

@ -0,0 +1,48 @@
<div class="tile tile-type-token fade-in mb-10">
<div class="row">
<div class="pl-5 col-md-2 d-flex flex-column align-items-left justify-content-start justify-content-lg-center">
<span class="tile-label"><%= gettext "Token Transfer" %></span>
</div>
<div class="col-md-7 col-lg-8 d-flex flex-column">
<p class="tile-title text-truncate">
<%= render ExplorerWeb.TransactionView, "_link.html", locale: @locale, transaction_hash: @transfer.transaction_hash %>
</p>
<span>
<%= render ExplorerWeb.AddressView,
"_link.html",
address_hash: to_string(@transfer.from_address_hash),
contract: ExplorerWeb.AddressView.contract?(@transfer.from_address),
locale: @locale %>
&rarr;
<%= render ExplorerWeb.AddressView,
"_link.html",
address_hash: to_string(@transfer.to_address_hash),
contract: ExplorerWeb.AddressView.contract?(@transfer.to_address),
locale: @locale %>
</span>
<span>
<span>
<%= token_transfer_amount(@transfer) %> <%= @transfer.token.symbol %></span>
</span>
</div>
<div class="p-4 col-md-2 col-lg-2 d-flex flex-row flex-md-column justify-content-start align-items-end text-md-right">
<span class="ml-1 mr-sm-0 text-muted" data-from-now="<%= @transfer.transaction.block.timestamp %>"></span>
<span class="ml-2">
<%= link(
gettext(
"Block #%{number}",
number: @transfer.transaction.block_number
),
class: "mr-2 mr-sm-0 text-muted",
to: block_path(ExplorerWeb.Endpoint, :show, @locale, @transfer.transaction.block_number)
) %>
</span>
</div>
</div>
</div>

@ -4,14 +4,22 @@
<div class="col-md-12 col-lg-8">
<div class="card">
<div class="card-body">
<h1 class="card-title"><%= gettext("Token Details") %></h1>
<h1 class="card-title">
<%= if token_name?(@token.name) do %>
<%= @token.name %>
<% else %>
<%= gettext("Token Details") %>
<% end %>
</h1>
<h3>0x95426f2bc716022fcf1def006dbc4bb81f5b5164</h3>
<h3><%= to_string(@token.contract_address_hash) %></h3>
<div class="d-flex flex-row justify-content-start text-muted">
<span class="mr-4">10 <%= gettext "addresses" %></span>
<span class="mr-4">10 <%= gettext "Transfers" %></span>
<span class="mr-4">10 <%= gettext "decimals" %></span>
<span class="mr-4"><%= @total_address_in_token_transfers %> <%= gettext "addresses" %></span>
<span class="mr-4"><%= @total_token_transfers %> <%= gettext "Transfers" %></span>
<%= if decimals?(@token.decimals) do %>
<span class="mr-4"><%= @token.decimals %> <%= gettext "decimals" %></span>
<% end %>
</div>
</div>
</div>
@ -20,12 +28,12 @@
<div class="col-md-6 col-lg-4">
<div class="card">
<div class="card-body">
<h2 class="card-title">Total Suply</h2>
<h2 class="card-title"><%= gettext "Total Supply" %></h2>
<span></span>
<div class="text-right">
<h3 class="text-uppercase">12345661223123456612231234 symbol</h3>
<h3 class="text-uppercase"><%= "#{@token.total_supply} #{@token.symbol}" %></h3>
<br />
</div>
</div>
@ -37,7 +45,6 @@
<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">
@ -49,7 +56,6 @@
</li>
</ul>
<!-- MOBILE DROPDOWN NAV -->
<ul class="nav nav-tabs card-header-tabs d-md-none">
<li class="nav-item dropdown flex-fill text-center">
@ -66,60 +72,27 @@
</div>
<div class="card-body">
<%= if true do %>
<h2 class="card-title"><%= gettext "Token Transfers" %></h2>
<span>
<div class="tile tile tile-type-token fade-in">
<div class="row">
<div class="pl-5 col-md-2 d-flex flex-column align-items-left justify-content-start justify-content-lg-center">
<span class="tile-label">Token Transfer</span>
</div>
<div class="col-md-7 col-lg-8 d-flex flex-column">
<p class="tile-title text-truncate">0x95426f2bc716022fcf1def006dbc4bb81f5b5164</p>
<span>
<%= render ExplorerWeb.AddressView, "_link.html", address_hash: "0x95426f2bc716022fcf1def006dbc4bb81f5b5164", contract: false, locale: @locale %>
&rarr;
<%= render ExplorerWeb.AddressView, "_link.html", address_hash: "0x95426f2bc716022fcf1def006dbc4bb81f5b5164", contract: false, locale: @locale %>
</span>
<span>
<span>N symbol</span>
</span>
</div>
<div class="p-4 col-md-2 col-lg-2 d-flex flex-row flex-md-column justify-content-start align-items-end text-md-right">
<span class="ml-1 mr-sm-0 text-muted" data-from-now="123"></span>
<span class="ml-2">
<%= link(
gettext(
"Block #%{number}",
number: "12"
),
class: "mr-2 mr-sm-0 text-muted",
to: block_path(ExplorerWeb.Endpoint, :show, @locale, "12")
) %>
</span>
</div>
</div>
</div>
</span>
<%= if Enum.any?(@transfers) do %>
<%= for transfer <- @transfers do %>
<%= render("_token_transfer.html", locale: @locale, token: @token, transfer: transfer) %>
<% end %>
<% else %>
<div class="tile tile-muted text-center">
<span data-selector="empty-transactions-list"><%= gettext "There are no transfers for this Token." %></span>
<span data-selector="empty-transactions-list">
<%= gettext "There are no transfers for this Token." %>
</span>
</div>
<% end %>
<%= if @next_page_params do %>
<%= link(
gettext("Next"),
gettext("Older"),
class: "button button--secondary button--small float-right mt-4",
to: token_path(@conn, :show, @conn.assigns.locale, "1")
to: token_path(@conn, :show, @conn.assigns.locale, @token.contract_address_hash, @next_page_params)
) %>
<% end %>
</div>
</div>
</section>

@ -34,6 +34,14 @@ defmodule ExplorerWeb.CurrencyHelpers do
end
end
@doc """
Formats the given integer value to a currency format.
## Examples
iex> ExplorerWeb.CurrencyHelpers.format_integer_to_currency(1000000)
"1,000,000"
"""
@spec format_integer_to_currency(non_neg_integer()) :: String.t()
def format_integer_to_currency(value) do
{:ok, formatted} = Cldr.Number.to_string(value, format: "#,##0")

@ -1,3 +1,9 @@
defmodule ExplorerWeb.TokenView do
use ExplorerWeb, :view
def decimals?(nil), do: false
def decimals?(_), do: true
def token_name?(nil), do: false
def token_name?(_), do: true
end

@ -471,6 +471,7 @@ msgstr ""
#: lib/explorer_web/templates/block/index.html.eex:15
#: lib/explorer_web/templates/block_transaction/index.html.eex:51
#: lib/explorer_web/templates/pending_transaction/index.html.eex:78
#: lib/explorer_web/templates/token/show.html.eex:91
#: lib/explorer_web/templates/transaction/index.html.eex:66
msgid "Older"
msgstr ""
@ -623,6 +624,7 @@ msgstr ""
#, elixir-format
#: lib/explorer_web/templates/address_transaction/_transaction.html.eex:41
#: lib/explorer_web/templates/token/_token_transfer.html.eex:38
#: lib/explorer_web/templates/transaction/_tile.html.eex:28
msgid "Block #%{number}"
msgstr ""
@ -707,12 +709,14 @@ msgid "Used"
msgstr ""
#, elixir-format
#: lib/explorer_web/templates/token/_token_transfer.html.eex:4
#: lib/explorer_web/views/transaction_view.ex:116
msgid "Token Transfer"
msgstr ""
#, elixir-format
#: lib/explorer_web/templates/address_transaction/_transaction.html.eex:64
#: lib/explorer_web/templates/token/show.html.eex:19
msgid "Transfers"
msgstr ""
@ -739,3 +743,35 @@ msgstr ""
#: lib/explorer_web/templates/transaction/index.html.eex:57
msgid "Validated Transactions"
msgstr ""
#: lib/explorer_web/templates/token/show.html.eex:84
msgid "There are no transfers for this Token."
msgstr ""
#, elixir-format
#: lib/explorer_web/templates/token/show.html.eex:11
msgid "Token Details"
msgstr ""
#, elixir-format
#: lib/explorer_web/templates/token/show.html.eex:52
#: lib/explorer_web/templates/token/show.html.eex:62
#: lib/explorer_web/templates/token/show.html.eex:65
#: lib/explorer_web/templates/token/show.html.eex:75
msgid "Token Transfers"
msgstr ""
#, elixir-format
#: lib/explorer_web/templates/token/show.html.eex:18
msgid "addresses"
msgstr ""
#, elixir-format
#: lib/explorer_web/templates/token/show.html.eex:21
msgid "decimals"
msgstr ""
#, elixir-format
#: lib/explorer_web/templates/token/show.html.eex:31
msgid "Total Supply"
msgstr ""

@ -483,6 +483,7 @@ msgstr ""
#: lib/explorer_web/templates/block/index.html.eex:15
#: lib/explorer_web/templates/block_transaction/index.html.eex:51
#: lib/explorer_web/templates/pending_transaction/index.html.eex:78
#: lib/explorer_web/templates/token/show.html.eex:91
#: lib/explorer_web/templates/transaction/index.html.eex:66
msgid "Older"
msgstr ""
@ -635,6 +636,7 @@ msgstr ""
#, elixir-format
#: lib/explorer_web/templates/address_transaction/_transaction.html.eex:41
#: lib/explorer_web/templates/token/_token_transfer.html.eex:38
#: lib/explorer_web/templates/transaction/_tile.html.eex:28
msgid "Block #%{number}"
msgstr ""
@ -719,12 +721,14 @@ msgid "Used"
msgstr ""
#, elixir-format
#: lib/explorer_web/templates/token/_token_transfer.html.eex:4
#: lib/explorer_web/views/transaction_view.ex:116
msgid "Token Transfer"
msgstr ""
#, elixir-format
#: lib/explorer_web/templates/address_transaction/_transaction.html.eex:64
#: lib/explorer_web/templates/token/show.html.eex:19
msgid "Transfers"
msgstr ""
@ -751,3 +755,35 @@ msgstr ""
#: lib/explorer_web/templates/transaction/index.html.eex:57
msgid "Validated Transactions"
msgstr ""
#: lib/explorer_web/templates/token/show.html.eex:84
msgid "There are no transfers for this Token."
msgstr ""
#, elixir-format
#: lib/explorer_web/templates/token/show.html.eex:11
msgid "Token Details"
msgstr ""
#, elixir-format
#: lib/explorer_web/templates/token/show.html.eex:52
#: lib/explorer_web/templates/token/show.html.eex:62
#: lib/explorer_web/templates/token/show.html.eex:65
#: lib/explorer_web/templates/token/show.html.eex:75
msgid "Token Transfers"
msgstr ""
#, elixir-format
#: lib/explorer_web/templates/token/show.html.eex:18
msgid "addresses"
msgstr ""
#, elixir-format
#: lib/explorer_web/templates/token/show.html.eex:21
msgid "decimals"
msgstr ""
#, elixir-format, fuzzy
#: lib/explorer_web/templates/token/show.html.eex:31
msgid "Total Supply"
msgstr ""

@ -50,4 +50,10 @@ defmodule ExplorerWeb.CurrencyHelpersTest do
assert CurrencyHelpers.format_according_to_decimals(amount, decimals) == "10,004.5"
end
end
describe "format_integer_to_currency/1" do
test "formats the integer value to a currency format" do
assert CurrencyHelpers.format_integer_to_currency(9000) == "9,000"
end
end
end

Loading…
Cancel
Save