From 3970233b3a638d4d6d3867c29013011738503b8e Mon Sep 17 00:00:00 2001 From: Felipe Renan Date: Thu, 9 Aug 2018 17:51:53 -0300 Subject: [PATCH 01/10] Build query to fetch tokens from an address --- apps/explorer/lib/explorer/chain.ex | 7 ++ apps/explorer/lib/explorer/chain/token.ex | 21 ++++- apps/explorer/test/explorer/chain_test.exs | 97 ++++++++++++++++++++++ 3 files changed, 123 insertions(+), 2 deletions(-) diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index 445d393fb9..ecceedd1f2 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -1564,4 +1564,11 @@ 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 + address_hash + |> Token.with_transfers_by_address() + |> Repo.all() + end end diff --git a/apps/explorer/lib/explorer/chain/token.ex b/apps/explorer/lib/explorer/chain/token.ex index e3b0ee58b8..94292ff003 100644 --- a/apps/explorer/lib/explorer/chain/token.ex +++ b/apps/explorer/lib/explorer/chain/token.ex @@ -19,8 +19,8 @@ defmodule Explorer.Chain.Token do use Ecto.Schema - import Ecto.{Changeset} - alias Explorer.Chain.{Address, Hash, Token} + import Ecto.{Changeset, Query} + alias Explorer.Chain.{Address, Hash, Token, TokenTransfer} @typedoc """ * `:name` - Name of the token @@ -74,4 +74,21 @@ defmodule Explorer.Chain.Token do |> foreign_key_constraint(:contract_address) |> unique_constraint(:contract_address_hash) end + + @doc """ + 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 an another address. + """ + def with_transfers_by_address(address_hash) do + 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 + ) + end end diff --git a/apps/explorer/test/explorer/chain_test.exs b/apps/explorer/test/explorer/chain_test.exs index 4c26e9c221..675a66b11b 100644 --- a/apps/explorer/test/explorer/chain_test.exs +++ b/apps/explorer/test/explorer/chain_test.exs @@ -1604,4 +1604,101 @@ defmodule Explorer.ChainTest do assert Chain.transaction_has_token_transfers?(transaction.hash) == false 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) + + token_a = + :token + |> insert(name: "token-1") + |> Repo.preload(:contract_address) + + token_b = + :token + |> insert(name: "token-2") + |> Repo.preload(:contract_address) + + token_c = + :token + |> insert(name: "token-3") + |> Repo.preload(:contract_address) + + insert( + :token_transfer, + token_contract_address: token_a.contract_address, + from_address: alice, + to_address: build(:address) + ) + + insert( + :token_transfer, + token_contract_address: token_b.contract_address, + from_address: build(:address), + to_address: alice + ) + + insert( + :token_transfer, + token_contract_address: token_c.contract_address, + from_address: build(:address), + to_address: build(:address) + ) + + expected_tokens = + alice.hash + |> Chain.fetch_tokens_from_address_hash() + |> Enum.map(& &1.name) + + assert expected_tokens == [token_a.name, token_b.name] + end + + test "returns a empty list when the given address hasn't interacted with one" do + alice = insert(:address) + + token = + :token + |> insert(name: "token-1") + |> Repo.preload(:contract_address) + + insert( + :token_transfer, + token_contract_address: token.contract_address, + from_address: build(:address), + to_address: build(:address) + ) + + assert Chain.fetch_tokens_from_address_hash(alice.hash) == [] + end + + test "distinct tokens by contract_address_hash" do + alice = insert(:address) + + token = + :token + |> insert(name: "token-1") + |> Repo.preload(:contract_address) + + insert( + :token_transfer, + token_contract_address: token.contract_address, + from_address: alice, + to_address: build(:address) + ) + + insert( + :token_transfer, + token_contract_address: token.contract_address, + from_address: build(:address), + to_address: alice + ) + + expected_tokens = + alice.hash + |> Chain.fetch_tokens_from_address_hash() + |> Enum.map(& &1.name) + + assert expected_tokens == [token.name] + end + end end From 3eb7c9f7ecb7cdf5d930a30d94b9f17e6bf340fb Mon Sep 17 00:00:00 2001 From: Felipe Renan Date: Thu, 9 Aug 2018 18:36:49 -0300 Subject: [PATCH 02/10] Add to format_according_to_decimals/2 supports integers --- .../lib/block_scout_web/views/currency_helpers.ex | 10 ++++++++++ .../block_scout_web/views/currency_helpers_test.exs | 7 +++++++ 2 files changed, 17 insertions(+) diff --git a/apps/block_scout_web/lib/block_scout_web/views/currency_helpers.ex b/apps/block_scout_web/lib/block_scout_web/views/currency_helpers.ex index 0e3f357225..e6952eb662 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/currency_helpers.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/currency_helpers.ex @@ -65,7 +65,17 @@ defmodule BlockScoutWeb.CurrencyHelpers do iex> format_according_to_decimals(Decimal.new(205000), 2) "2,050" + + iex> format_according_to_decimals(205000, 2) + "2,050" """ + @spec format_according_to_decimals(non_neg_integer(), non_neg_integer()) :: String.t() + def format_according_to_decimals(value, decimals) when is_integer(value) do + value + |> Decimal.new() + |> format_according_to_decimals(decimals) + end + @spec format_according_to_decimals(Decimal.t(), non_neg_integer()) :: String.t() def format_according_to_decimals(%Decimal{sign: sign, coef: coef, exp: exp}, decimals) do sign diff --git a/apps/block_scout_web/test/block_scout_web/views/currency_helpers_test.exs b/apps/block_scout_web/test/block_scout_web/views/currency_helpers_test.exs index 4b00a6ad0d..b0931b9298 100644 --- a/apps/block_scout_web/test/block_scout_web/views/currency_helpers_test.exs +++ b/apps/block_scout_web/test/block_scout_web/views/currency_helpers_test.exs @@ -49,6 +49,13 @@ defmodule BlockScoutWeb.CurrencyHelpersTest do assert CurrencyHelpers.format_according_to_decimals(amount, decimals) == "10,004.5" end + + test "supports value as integer" do + amount = 1_000_450 + decimals = 2 + + assert CurrencyHelpers.format_according_to_decimals(amount, decimals) == "10,004.5" + end end describe "format_integer_to_currency/1" do From dc6bc8f51d1ec8af076450082eed7cba1be019c1 Mon Sep 17 00:00:00 2001 From: Felipe Renan Date: Thu, 9 Aug 2018 18:43:20 -0300 Subject: [PATCH 03/10] Add module TokenBalanceReader --- .../lib/explorer/token/balance_reader.ex | 74 ++++++++++ .../explorer/token/balance_reader_test.exs | 130 ++++++++++++++++++ 2 files changed, 204 insertions(+) create mode 100644 apps/explorer/lib/explorer/token/balance_reader.ex create mode 100644 apps/explorer/test/explorer/token/balance_reader_test.exs diff --git a/apps/explorer/lib/explorer/token/balance_reader.ex b/apps/explorer/lib/explorer/token/balance_reader.ex new file mode 100644 index 0000000000..c9ad273bc6 --- /dev/null +++ b/apps/explorer/lib/explorer/token/balance_reader.ex @@ -0,0 +1,74 @@ +defmodule Explorer.Token.BalanceReader do + @moduledoc """ + Reads Token's balances using Smart Contract functions from the blockchain. + """ + + alias Explorer.SmartContract.Reader + + @balance_function_abi [ + %{ + "type" => "function", + "stateMutability" => "view", + "payable" => false, + "outputs" => [ + %{ + "type" => "uint256", + "name" => "balance" + } + ], + "name" => "balanceOf", + "inputs" => [ + %{ + "type" => "address", + "name" => "tokenOwner" + } + ], + "constant" => true + } + ] + + @doc """ + Fetches the token balances that were fetched without error and have balances more than 0. + """ + def fetch_token_balances_without_error(tokens, address_hash_string) do + tokens + |> fetch_token_balances_from_blockchain(address_hash_string) + |> Stream.filter(&token_without_error?/1) + |> Stream.map(&format_result/1) + |> Enum.filter(&tokens_with_no_zero_balance?/1) + end + + defp token_without_error?({:ok, _token}), do: true + defp token_without_error?({:error, _token}), do: false + defp format_result({:ok, token}), do: token + defp tokens_with_no_zero_balance?(%{balance: balance}), do: balance != 0 + + @doc """ + Fetches the token balances given the tokens and the address hash as string. + + This function is going to perform one request async for each token inside a list of tokens in + order to fetch the balance. + """ + @spec fetch_token_balances_from_blockchain([], String.t()) :: [] + def fetch_token_balances_from_blockchain(tokens, address_hash_string) do + tokens + |> Task.async_stream(&fetch_from_blockchain(&1, address_hash_string)) + |> Enum.map(&blockchain_result_from_tasks/1) + end + + defp fetch_from_blockchain(%{contract_address_hash: address_hash} = token, address_hash_string) do + address_hash + |> Reader.query_unverified_contract(@balance_function_abi, %{"balanceOf" => [address_hash_string]}) + |> format_blockchain_result(token) + end + + defp format_blockchain_result(%{"balanceOf" => {:ok, balance}}, token) do + {:ok, Map.put(token, :balance, balance)} + end + + defp format_blockchain_result(%{"balanceOf" => {:error, error}}, token) do + {:error, Map.put(token, :balance, error)} + end + + defp blockchain_result_from_tasks({:ok, blockchain_result}), do: blockchain_result +end diff --git a/apps/explorer/test/explorer/token/balance_reader_test.exs b/apps/explorer/test/explorer/token/balance_reader_test.exs new file mode 100644 index 0000000000..8935cb5a96 --- /dev/null +++ b/apps/explorer/test/explorer/token/balance_reader_test.exs @@ -0,0 +1,130 @@ +defmodule Explorer.Token.BalanceReaderTest do + use EthereumJSONRPC.Case + use Explorer.DataCase + + doctest Explorer.Token.BalanceReader + + alias Explorer.Token.{BalanceReader} + alias Explorer.Chain.Hash + + import Mox + + setup :verify_on_exit! + setup :set_mox_global + + describe "fetch_token_balances_from_blockchain/2" do + test "fetches balances of tokens given the address hash" do + address = insert(:address) + token = insert(:token, contract_address: build(:contract_address)) + address_hash_string = Hash.to_string(address.hash) + + get_balance_from_blockchain() + + result = + [token] + |> BalanceReader.fetch_token_balances_from_blockchain(address_hash_string) + |> List.first() + + assert result == {:ok, Map.put(token, :balance, 1_000_000_000_000_000_000_000_000)} + end + + test "does not ignore calls that were returned with error" do + address = insert(:address) + token = insert(:token, contract_address: build(:contract_address)) + address_hash_string = Hash.to_string(address.hash) + + get_balance_from_blockchain_with_error() + + result = + [token] + |> BalanceReader.fetch_token_balances_from_blockchain(address_hash_string) + |> List.first() + + assert result == {:error, Map.put(token, :balance, "(-32015) VM execution error.")} + end + end + + describe "fetch_token_balances_without_error/2" do + test "filters token balances that were fetched without error" do + address = insert(:address) + token_a = insert(:token, contract_address: build(:contract_address)) + token_b = insert(:token, contract_address: build(:contract_address)) + address_hash_string = Hash.to_string(address.hash) + + get_balance_from_blockchain() + get_balance_from_blockchain_with_error() + + results = + [token_a, token_b] + |> BalanceReader.fetch_token_balances_without_error(address_hash_string) + + assert Enum.count(results) == 1 + assert List.first(results) == Map.put(token_a, :balance, 1_000_000_000_000_000_000_000_000) + end + + test "does not considers balances equal 0" do + address = insert(:address) + token = insert(:token, contract_address: build(:contract_address)) + address_hash_string = Hash.to_string(address.hash) + + get_balance_from_blockchain_with_balance_zero() + + results = + [token] + |> BalanceReader.fetch_token_balances_without_error(address_hash_string) + + assert Enum.count(results) == 0 + end + end + + defp get_balance_from_blockchain() do + expect( + EthereumJSONRPC.Mox, + :json_rpc, + fn [%{id: _, method: _, params: [%{data: _, to: _}]}], _options -> + {:ok, + [ + %{ + id: "balanceOf", + jsonrpc: "2.0", + result: "0x00000000000000000000000000000000000000000000d3c21bcecceda1000000" + } + ]} + end + ) + end + + defp get_balance_from_blockchain_with_balance_zero() do + expect( + EthereumJSONRPC.Mox, + :json_rpc, + fn [%{id: _, method: _, params: [%{data: _, to: _}]}], _options -> + {:ok, + [ + %{ + id: "balanceOf", + jsonrpc: "2.0", + result: "0x0000000000000000000000000000000000000000000000000000000000000000" + } + ]} + end + ) + end + + defp get_balance_from_blockchain_with_error() do + expect( + EthereumJSONRPC.Mox, + :json_rpc, + fn [%{id: _, method: _, params: [%{data: _, to: _}]}], _options -> + {:ok, + [ + %{ + error: %{code: -32015, data: "Reverted 0x", message: "VM execution error."}, + id: "balanceOf", + jsonrpc: "2.0" + } + ]} + end + ) + end +end From efe429e389b4ddfbc88b8ade5fbbea883537bd33 Mon Sep 17 00:00:00 2001 From: Felipe Renan Date: Thu, 9 Aug 2018 18:44:08 -0300 Subject: [PATCH 04/10] Add Token Holdings box --- apps/block_scout_web/assets/css/app.scss | 2 +- .../components/_token-balance-dropdown.scss | 35 ++++++++++++++++++ apps/block_scout_web/assets/js/app.js | 1 + .../assets/js/lib/token_balance_dropdown.js | 19 ++++++++++ .../address_token_balance_controller.ex | 24 +++++++++++++ .../lib/block_scout_web/router.ex | 7 ++++ .../address/_token_holdings.html.eex | 20 +++++++++++ .../templates/address/overview.html.eex | 5 ++- .../_token_balances.html.eex | 36 +++++++++++++++++++ .../address_token_balance/_tokens.html.eex | 17 +++++++++ .../views/address_token_balance_view.ex | 20 +++++++++++ .../views/address_token_balance_view_test.exs | 30 ++++++++++++++++ 12 files changed, 214 insertions(+), 2 deletions(-) create mode 100644 apps/block_scout_web/assets/css/components/_token-balance-dropdown.scss create mode 100644 apps/block_scout_web/assets/js/lib/token_balance_dropdown.js create mode 100644 apps/block_scout_web/lib/block_scout_web/controllers/address_token_balance_controller.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/templates/address/_token_holdings.html.eex create mode 100644 apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_token_balances.html.eex create mode 100644 apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_tokens.html.eex create mode 100644 apps/block_scout_web/lib/block_scout_web/views/address_token_balance_view.ex create mode 100644 apps/block_scout_web/test/block_scout_web/views/address_token_balance_view_test.exs diff --git a/apps/block_scout_web/assets/css/app.scss b/apps/block_scout_web/assets/css/app.scss index f855fffa48..32aa84d2df 100644 --- a/apps/block_scout_web/assets/css/app.scss +++ b/apps/block_scout_web/assets/css/app.scss @@ -76,7 +76,7 @@ $fa-font-path: "~@fortawesome/fontawesome-free/webfonts"; @import "components/badge"; @import "components/description-list"; @import "components/nounderline-link"; - +@import "components/token-balance-dropdown"; :export { primary: $primary; diff --git a/apps/block_scout_web/assets/css/components/_token-balance-dropdown.scss b/apps/block_scout_web/assets/css/components/_token-balance-dropdown.scss new file mode 100644 index 0000000000..fd3cde0bcd --- /dev/null +++ b/apps/block_scout_web/assets/css/components/_token-balance-dropdown.scss @@ -0,0 +1,35 @@ +.token-balance-dropdown { + min-width: 14.375rem; + margin-top: 1rem; + background-color: $gray-100; + box-shadow: 0px 2px 3px 2px $gray-200; + border: none; + + // Overriding style added by Bootstrap dropdown via JS. + left: -17px !important; + + .dropdown-items { + overflow-y: auto; + max-height: 18.5rem; + + .dropdown-item:hover { + color: $white; + } + } + + &:after, &:before { + bottom: 100%; + left: 14%; + border: solid transparent; + content: " "; + height: 0; + width: 0; + position: absolute; + } + + &:before { + border-bottom-color: $gray-100; + border-width: 1rem; + margin-left: -1rem; + } +} diff --git a/apps/block_scout_web/assets/js/app.js b/apps/block_scout_web/assets/js/app.js index d70012eff8..6ac7d9cebf 100644 --- a/apps/block_scout_web/assets/js/app.js +++ b/apps/block_scout_web/assets/js/app.js @@ -27,6 +27,7 @@ import './lib/tooltip' import './lib/smart_contract/read_only_functions' import './lib/pretty_json' import './lib/try_api' +import './lib/token_balance_dropdown' import './pages/address' import './pages/block' diff --git a/apps/block_scout_web/assets/js/lib/token_balance_dropdown.js b/apps/block_scout_web/assets/js/lib/token_balance_dropdown.js new file mode 100644 index 0000000000..c1e8759859 --- /dev/null +++ b/apps/block_scout_web/assets/js/lib/token_balance_dropdown.js @@ -0,0 +1,19 @@ +import $ from 'jquery' + +const tokenBalanceDropdown = (element) => { + const $element = $(element) + const $loading = $element.find('[data-loading]') + const $errorMessage = $element.find('[data-error-message]') + const apiPath = element.dataset.api_path + + $loading.show() + + $.get(apiPath) + .done(response => $element.html(response)) + .fail(() => { + $loading.hide() + $errorMessage.show() + }) +} + +$('[data-token-balance-dropdown]').each((_index, element) => tokenBalanceDropdown(element)) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_token_balance_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_token_balance_controller.ex new file mode 100644 index 0000000000..ddadeac55c --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_token_balance_controller.ex @@ -0,0 +1,24 @@ +defmodule BlockScoutWeb.AddressTokenBalanceController do + use BlockScoutWeb, :controller + + alias Explorer.Chain + alias Explorer.Token.BalanceReader + + def index(conn, %{"address_id" => address_hash_string}) do + with true <- ajax?(conn), + {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string) do + token_balances = + address_hash + |> Chain.fetch_tokens_from_address_hash() + |> BalanceReader.fetch_token_balances_without_error(address_hash_string) + + conn + |> put_status(200) + |> put_layout(false) + |> render("_token_balances.html", tokens: token_balances) + else + _ -> + not_found(conn) + end + 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 a2ff629bae..5db35e6b50 100644 --- a/apps/block_scout_web/lib/block_scout_web/router.ex +++ b/apps/block_scout_web/lib/block_scout_web/router.ex @@ -95,6 +95,13 @@ defmodule BlockScoutWeb.Router do only: [:index, :show], as: :read_contract ) + + resources( + "/token_balances", + AddressTokenBalanceController, + only: [:index], + as: :token_balance + ) end resources "/tokens", Tokens.TokenController, only: [:show], as: :token do diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address/_token_holdings.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address/_token_holdings.html.eex new file mode 100644 index 0000000000..4e80ebcb81 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/address/_token_holdings.html.eex @@ -0,0 +1,20 @@ +
+
+

<%= gettext "Token Holdings" %>

+ + +
+ class="icon-links ml-3 mb-3" + > +

+ + <%= gettext("Fetching tokens...") %> +

+ + +
+
+
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address/overview.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address/overview.html.eex index 4c0997ffe4..218625241f 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address/overview.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address/overview.html.eex @@ -1,6 +1,6 @@
-
+
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_token_balances.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_token_balances.html.eex new file mode 100644 index 0000000000..c83a069163 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_token_balances.html.eex @@ -0,0 +1,36 @@ +<%= if Enum.any?(@tokens) do %> + +<% end %> +

<%= tokens_count_title(@tokens) %>

+ + diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_tokens.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_tokens.html.eex new file mode 100644 index 0000000000..7b423c7560 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_tokens.html.eex @@ -0,0 +1,17 @@ + + +<%= for token <- sort_by_name(@tokens) do %> +
+ <%= link( + to: token_path(@conn, :show, :en, token.contract_address_hash), + class: "dropdown-item" + ) do %> +

<%= token_name(token) %>

+

+ <%= format_according_to_decimals(token.balance, token.decimals) %> <%= token.symbol %> +

+ <% end %> +
+<% end %> diff --git a/apps/block_scout_web/lib/block_scout_web/views/address_token_balance_view.ex b/apps/block_scout_web/lib/block_scout_web/views/address_token_balance_view.ex new file mode 100644 index 0000000000..52636774e6 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/address_token_balance_view.ex @@ -0,0 +1,20 @@ +defmodule BlockScoutWeb.AddressTokenBalanceView do + use BlockScoutWeb, :view + + def tokens_count_title(tokens) do + ngettext("%{count} token", "%{count} tokens", Enum.count(tokens)) + end + + def filter_by_type(tokens, type) do + Enum.filter(tokens, &(&1.type == type)) + end + + @doc """ + Sorts the given list of tokens in alphabetically order considering nil values in the bottom of + the list. + """ + def sort_by_name(tokens) do + {unnamed, named} = Enum.split_with(tokens, &is_nil(&1.name)) + Enum.sort_by(named, &String.downcase(&1.name)) ++ unnamed + end +end diff --git a/apps/block_scout_web/test/block_scout_web/views/address_token_balance_view_test.exs b/apps/block_scout_web/test/block_scout_web/views/address_token_balance_view_test.exs new file mode 100644 index 0000000000..51c4b25b9a --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/views/address_token_balance_view_test.exs @@ -0,0 +1,30 @@ +defmodule BlockScoutWeb.AddressTokenBalanceViewTest do + use BlockScoutWeb.ConnCase, async: true + + alias BlockScoutWeb.AddressTokenBalanceView + + describe "sort_by_name/1" do + test "sorts the given tokens by its name" do + token_a = build(:token, name: "token name") + token_b = build(:token, name: "token") + token_c = build(:token, name: "atoken") + + assert AddressTokenBalanceView.sort_by_name([token_a, token_b, token_c]) == [token_c, token_b, token_a] + end + + test "considers nil values in the bottom of the list" do + token_a = build(:token, name: nil) + token_b = build(:token, name: "token name") + token_c = build(:token, name: "token") + + assert AddressTokenBalanceView.sort_by_name([token_a, token_b, token_c]) == [token_c, token_b, token_a] + end + + test "considers capitalization" do + token_a = build(:token, name: "Token") + token_b = build(:token, name: "atoken") + + assert AddressTokenBalanceView.sort_by_name([token_a, token_b]) == [token_b, token_a] + end + end +end From 1fc1c061e56b6387cbe137cf9d9d8e30cdc20770 Mon Sep 17 00:00:00 2001 From: Felipe Renan Date: Tue, 14 Aug 2018 10:34:00 -0300 Subject: [PATCH 05/10] Fix cards height on address overview --- apps/block_scout_web/assets/css/app.scss | 1 + .../assets/css/components/address-overview.scss | 10 ++++++++++ .../templates/address/overview.html.eex | 8 ++++---- 3 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 apps/block_scout_web/assets/css/components/address-overview.scss diff --git a/apps/block_scout_web/assets/css/app.scss b/apps/block_scout_web/assets/css/app.scss index 32aa84d2df..3982d7038a 100644 --- a/apps/block_scout_web/assets/css/app.scss +++ b/apps/block_scout_web/assets/css/app.scss @@ -77,6 +77,7 @@ $fa-font-path: "~@fortawesome/fontawesome-free/webfonts"; @import "components/description-list"; @import "components/nounderline-link"; @import "components/token-balance-dropdown"; +@import "components/address-overview"; :export { primary: $primary; diff --git a/apps/block_scout_web/assets/css/components/address-overview.scss b/apps/block_scout_web/assets/css/components/address-overview.scss new file mode 100644 index 0000000000..77e89e69b7 --- /dev/null +++ b/apps/block_scout_web/assets/css/components/address-overview.scss @@ -0,0 +1,10 @@ +.address-overview { + .card-section { + margin-bottom: 3rem; + } + + .card { + margin-bottom: 0; + height: 100%; + } +} diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address/overview.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address/overview.html.eex index 218625241f..611caae3bf 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address/overview.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address/overview.html.eex @@ -1,6 +1,6 @@ -
+
-
+
-
+
<%= render BlockScoutWeb.AddressView, "_balance_card.html", address: @address, exchange_rate: @exchange_rate %>
-
+
<%= render BlockScoutWeb.AddressView, "_token_holdings.html", conn: @conn, address: @address %>
From 96db42473d5f9a847f0b0465008d48f864e66d61 Mon Sep 17 00:00:00 2001 From: Felipe Renan Date: Wed, 15 Aug 2018 10:25:32 -0300 Subject: [PATCH 06/10] Handle decimals nil at format_according_to_decimals/2 --- .../lib/block_scout_web/views/currency_helpers.ex | 4 ++++ .../test/block_scout_web/views/currency_helpers_test.exs | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/apps/block_scout_web/lib/block_scout_web/views/currency_helpers.ex b/apps/block_scout_web/lib/block_scout_web/views/currency_helpers.ex index e6952eb662..7b43825dd9 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/currency_helpers.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/currency_helpers.ex @@ -70,6 +70,10 @@ defmodule BlockScoutWeb.CurrencyHelpers do "2,050" """ @spec format_according_to_decimals(non_neg_integer(), non_neg_integer()) :: String.t() + def format_according_to_decimals(value, nil) do + format_according_to_decimals(value, 0) + end + def format_according_to_decimals(value, decimals) when is_integer(value) do value |> Decimal.new() diff --git a/apps/block_scout_web/test/block_scout_web/views/currency_helpers_test.exs b/apps/block_scout_web/test/block_scout_web/views/currency_helpers_test.exs index b0931b9298..b47e5deeea 100644 --- a/apps/block_scout_web/test/block_scout_web/views/currency_helpers_test.exs +++ b/apps/block_scout_web/test/block_scout_web/views/currency_helpers_test.exs @@ -56,6 +56,13 @@ defmodule BlockScoutWeb.CurrencyHelpersTest do assert CurrencyHelpers.format_according_to_decimals(amount, decimals) == "10,004.5" end + + test "considers 0 when decimals is nil" do + amount = 1_000_450 + decimals = nil + + assert CurrencyHelpers.format_according_to_decimals(amount, decimals) == "1,000,450" + end end describe "format_integer_to_currency/1" do From 96abbe4390e16a9cb8ce156240ec3627fcb2a0c9 Mon Sep 17 00:00:00 2001 From: Felipe Renan Date: Wed, 15 Aug 2018 10:43:41 -0300 Subject: [PATCH 07/10] Move ajax?/1 to controller helper --- apps/block_scout_web/lib/block_scout_web/controller.ex | 10 ++++++++++ .../controllers/smart_contract_controller.ex | 7 ------- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/controller.ex b/apps/block_scout_web/lib/block_scout_web/controller.ex index f914e10b53..fe2851c197 100644 --- a/apps/block_scout_web/lib/block_scout_web/controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controller.ex @@ -22,4 +22,14 @@ defmodule BlockScoutWeb.Controller do |> put_view(BlockScoutWeb.ErrorView) |> render("422.html") end + + @doc """ + Checks if the request is AJAX or not. + """ + def ajax?(conn) do + case get_req_header(conn, "x-requested-with") do + [value] -> value in ["XMLHttpRequest", "xmlhttprequest"] + [] -> false + end + end end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/smart_contract_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/smart_contract_controller.ex index 271068f0c8..b9398ca056 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/smart_contract_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/smart_contract_controller.ex @@ -57,11 +57,4 @@ defmodule BlockScoutWeb.SmartContractController do not_found(conn) end end - - defp ajax?(conn) do - case get_req_header(conn, "x-requested-with") do - [value] -> value in ["XMLHttpRequest", "xmlhttprequest"] - [] -> false - end - end end From 383265a79a996e0caae36042eb8f64f2c9a78324 Mon Sep 17 00:00:00 2001 From: Felipe Renan Date: Fri, 17 Aug 2018 11:03:50 -0300 Subject: [PATCH 08/10] Add Token.Helpers.token_name/1 --- .../block_scout_web/views/tokens/helpers.ex | 26 ++++++++++++++----- .../views/tokens/helpers_test.exs | 20 ++++++++++++++ 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/views/tokens/helpers.ex b/apps/block_scout_web/lib/block_scout_web/views/tokens/helpers.ex index 8d4196dd7f..da25457a65 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/tokens/helpers.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/tokens/helpers.ex @@ -46,15 +46,29 @@ defmodule BlockScoutWeb.Tokens.Helpers do When the token's symbol is nil, the function will return the contract address hash. """ def token_symbol(%Token{symbol: nil, contract_address_hash: address_hash}) do - address_hash = - address_hash - |> to_string() - |> String.slice(0..6) - - "#{address_hash}..." + "#{contract_address_hash_truncated(address_hash)}..." end def token_symbol(%Token{symbol: symbol}) do symbol end + + @doc """ + Returns the token's name. + + When the token's name is nil, the function will return the contract address hash. + """ + def token_name(%Token{name: nil, contract_address_hash: address_hash}) do + "#{contract_address_hash_truncated(address_hash)}..." + end + + def token_name(%Token{name: name}) do + name + end + + defp contract_address_hash_truncated(address_hash) do + address_hash + |> to_string() + |> String.slice(0..6) + end end diff --git a/apps/block_scout_web/test/block_scout_web/views/tokens/helpers_test.exs b/apps/block_scout_web/test/block_scout_web/views/tokens/helpers_test.exs index 6efc94460f..4258bb37dc 100644 --- a/apps/block_scout_web/test/block_scout_web/views/tokens/helpers_test.exs +++ b/apps/block_scout_web/test/block_scout_web/views/tokens/helpers_test.exs @@ -59,4 +59,24 @@ defmodule BlockScoutWeb.Tokens.HelpersTest do assert Helpers.token_symbol(token) == "#{address_hash}..." end end + + describe "token_name/1" do + test "returns the token name" do + token = build(:token, name: "Batman") + + assert Helpers.token_name(token) == "Batman" + end + + test "returns the token contract address hash when the name is nil" do + address = build(:address) + token = build(:token, name: nil, contract_address_hash: address.hash) + + address_hash = + address.hash + |> Explorer.Chain.Hash.to_string() + |> String.slice(0..6) + + assert Helpers.token_name(token) == "#{address_hash}..." + end + end end From 456d71b14ceeb254feb7033c0349e0bbb33ee244 Mon Sep 17 00:00:00 2001 From: Felipe Renan Date: Fri, 17 Aug 2018 15:53:22 -0300 Subject: [PATCH 09/10] Generate gettext --- apps/block_scout_web/priv/gettext/default.pot | 26 +++++++++++++++++-- .../priv/gettext/en/LC_MESSAGES/default.po | 26 +++++++++++++++++-- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/apps/block_scout_web/priv/gettext/default.pot b/apps/block_scout_web/priv/gettext/default.pot index 719cc92aff..9e139c59b0 100644 --- a/apps/block_scout_web/priv/gettext/default.pot +++ b/apps/block_scout_web/priv/gettext/default.pot @@ -599,7 +599,7 @@ msgstr "" #, elixir-format #: lib/block_scout_web/templates/address/overview.html.eex:13 -#: lib/block_scout_web/templates/address/overview.html.eex:65 +#: lib/block_scout_web/templates/address/overview.html.eex:68 msgid "QR Code" msgstr "" @@ -686,7 +686,7 @@ msgid "Block Height #%{height}" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address/overview.html.eex:74 +#: lib/block_scout_web/templates/address/overview.html.eex:77 msgid "Close" msgstr "" @@ -816,3 +816,25 @@ msgstr "" #: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:52 msgid "loading..." msgstr "" + +#, elixir-format +#: lib/block_scout_web/views/address_token_balance_view.ex:5 +msgid "%{count} token" +msgid_plural "%{count} tokens" +msgstr[0] "" +msgstr[1] "" + +#, elixir-format +#: lib/block_scout_web/templates/address/_token_holdings.html.eex:16 +msgid "Error tryng to fetch balances." +msgstr "" + +#, elixir-format +#: lib/block_scout_web/templates/address/_token_holdings.html.eex:12 +msgid "Fetching tokens..." +msgstr "" + +#, elixir-format +#: lib/block_scout_web/templates/address/_token_holdings.html.eex:3 +msgid "Token Holdings" +msgstr "" diff --git a/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po b/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po index cb18524956..05bc7753a9 100644 --- a/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po +++ b/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po @@ -611,7 +611,7 @@ msgstr "" #, elixir-format #: lib/block_scout_web/templates/address/overview.html.eex:13 -#: lib/block_scout_web/templates/address/overview.html.eex:65 +#: lib/block_scout_web/templates/address/overview.html.eex:68 msgid "QR Code" msgstr "" @@ -698,7 +698,7 @@ msgid "Block Height #%{height}" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address/overview.html.eex:74 +#: lib/block_scout_web/templates/address/overview.html.eex:77 msgid "Close" msgstr "" @@ -828,3 +828,25 @@ msgstr "" #: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:52 msgid "loading..." msgstr "" + +#, elixir-format +#: lib/block_scout_web/views/address_token_balance_view.ex:5 +msgid "%{count} token" +msgid_plural "%{count} tokens" +msgstr[0] "" +msgstr[1] "" + +#, elixir-format +#: lib/block_scout_web/templates/address/_token_holdings.html.eex:16 +msgid "Error tryng to fetch balances." +msgstr "" + +#, elixir-format +#: lib/block_scout_web/templates/address/_token_holdings.html.eex:12 +msgid "Fetching tokens..." +msgstr "" + +#, elixir-format +#: lib/block_scout_web/templates/address/_token_holdings.html.eex:3 +msgid "Token Holdings" +msgstr "" From d7ae814da90ae8b8041a7073ed9751d3bb9fc2de Mon Sep 17 00:00:00 2001 From: Felipe Renan Date: Fri, 17 Aug 2018 17:59:12 -0300 Subject: [PATCH 10/10] Improve HelperTest tests --- .../views/tokens/helpers_test.exs | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/apps/block_scout_web/test/block_scout_web/views/tokens/helpers_test.exs b/apps/block_scout_web/test/block_scout_web/views/tokens/helpers_test.exs index 4258bb37dc..65f832838d 100644 --- a/apps/block_scout_web/test/block_scout_web/views/tokens/helpers_test.exs +++ b/apps/block_scout_web/test/block_scout_web/views/tokens/helpers_test.exs @@ -48,15 +48,10 @@ defmodule BlockScoutWeb.Tokens.HelpersTest do end test "returns the token contract address hash when the symbol is nil" do - address = build(:address) + address = build(:address, hash: "de3fa0f9f8d47790ce88c2b2b82ab81f79f2e65f") token = build(:token, symbol: nil, contract_address_hash: address.hash) - address_hash = - address.hash - |> Explorer.Chain.Hash.to_string() - |> String.slice(0..6) - - assert Helpers.token_symbol(token) == "#{address_hash}..." + assert Helpers.token_symbol(token) == "de3fa0f..." end end @@ -68,15 +63,10 @@ defmodule BlockScoutWeb.Tokens.HelpersTest do end test "returns the token contract address hash when the name is nil" do - address = build(:address) + address = build(:address, hash: "de3fa0f9f8d47790ce88c2b2b82ab81f79f2e65f") token = build(:token, name: nil, contract_address_hash: address.hash) - address_hash = - address.hash - |> Explorer.Chain.Hash.to_string() - |> String.slice(0..6) - - assert Helpers.token_name(token) == "#{address_hash}..." + assert Helpers.token_name(token) == "de3fa0f..." end end end