Merge pull request #534 from poanetwork/frg-list-tokens-on-address-page

List of tokens associated with the Address on the Address' page
pull/556/head
Andrew Cravenho 6 years ago committed by GitHub
commit 67b415ddae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      apps/block_scout_web/assets/css/app.scss
  2. 35
      apps/block_scout_web/assets/css/components/_token-balance-dropdown.scss
  3. 10
      apps/block_scout_web/assets/css/components/address-overview.scss
  4. 1
      apps/block_scout_web/assets/js/app.js
  5. 19
      apps/block_scout_web/assets/js/lib/token_balance_dropdown.js
  6. 10
      apps/block_scout_web/lib/block_scout_web/controller.ex
  7. 24
      apps/block_scout_web/lib/block_scout_web/controllers/address_token_balance_controller.ex
  8. 7
      apps/block_scout_web/lib/block_scout_web/controllers/smart_contract_controller.ex
  9. 7
      apps/block_scout_web/lib/block_scout_web/router.ex
  10. 20
      apps/block_scout_web/lib/block_scout_web/templates/address/_token_holdings.html.eex
  11. 9
      apps/block_scout_web/lib/block_scout_web/templates/address/overview.html.eex
  12. 36
      apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_token_balances.html.eex
  13. 17
      apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_tokens.html.eex
  14. 20
      apps/block_scout_web/lib/block_scout_web/views/address_token_balance_view.ex
  15. 14
      apps/block_scout_web/lib/block_scout_web/views/currency_helpers.ex
  16. 26
      apps/block_scout_web/lib/block_scout_web/views/tokens/helpers.ex
  17. 26
      apps/block_scout_web/priv/gettext/default.pot
  18. 26
      apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
  19. 30
      apps/block_scout_web/test/block_scout_web/views/address_token_balance_view_test.exs
  20. 14
      apps/block_scout_web/test/block_scout_web/views/currency_helpers_test.exs
  21. 22
      apps/block_scout_web/test/block_scout_web/views/tokens/helpers_test.exs
  22. 7
      apps/explorer/lib/explorer/chain.ex
  23. 21
      apps/explorer/lib/explorer/chain/token.ex
  24. 74
      apps/explorer/lib/explorer/token/balance_reader.ex
  25. 97
      apps/explorer/test/explorer/chain_test.exs
  26. 130
      apps/explorer/test/explorer/token/balance_reader_test.exs

@ -76,7 +76,8 @@ $fa-font-path: "~@fortawesome/fontawesome-free/webfonts";
@import "components/badge"; @import "components/badge";
@import "components/description-list"; @import "components/description-list";
@import "components/nounderline-link"; @import "components/nounderline-link";
@import "components/token-balance-dropdown";
@import "components/address-overview";
:export { :export {
primary: $primary; primary: $primary;

@ -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;
}
}

@ -0,0 +1,10 @@
.address-overview {
.card-section {
margin-bottom: 3rem;
}
.card {
margin-bottom: 0;
height: 100%;
}
}

@ -27,6 +27,7 @@ import './lib/tooltip'
import './lib/smart_contract/read_only_functions' import './lib/smart_contract/read_only_functions'
import './lib/pretty_json' import './lib/pretty_json'
import './lib/try_api' import './lib/try_api'
import './lib/token_balance_dropdown'
import './pages/address' import './pages/address'
import './pages/block' import './pages/block'

@ -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))

@ -22,4 +22,14 @@ defmodule BlockScoutWeb.Controller do
|> put_view(BlockScoutWeb.ErrorView) |> put_view(BlockScoutWeb.ErrorView)
|> render("422.html") |> render("422.html")
end 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 end

@ -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

@ -57,11 +57,4 @@ defmodule BlockScoutWeb.SmartContractController do
not_found(conn) not_found(conn)
end end
end end
defp ajax?(conn) do
case get_req_header(conn, "x-requested-with") do
[value] -> value in ["XMLHttpRequest", "xmlhttprequest"]
[] -> false
end
end
end end

@ -95,6 +95,13 @@ defmodule BlockScoutWeb.Router do
only: [:index, :show], only: [:index, :show],
as: :read_contract as: :read_contract
) )
resources(
"/token_balances",
AddressTokenBalanceController,
only: [:index],
as: :token_balance
)
end end
resources "/tokens", Tokens.TokenController, only: [:show], as: :token do resources "/tokens", Tokens.TokenController, only: [:show], as: :token do

@ -0,0 +1,20 @@
<div class="card">
<div class="card-body">
<h2 class="card-title"><%= gettext "Token Holdings" %></h2>
<!-- Dropdown -->
<div data-token-balance-dropdown
data-api_path=<%= address_token_balance_path(@conn, :index, :en, @address.hash) %>
class="icon-links ml-3 mb-3"
>
<p data-loading class="mb-0" stytle="display: none">
<i class="fa fa-spinner fa-spin"></i>
<%= gettext("Fetching tokens...") %>
</p>
<p data-error-message class="mb-0" style="display: none">
<%= gettext("Error tryng to fetch balances.") %>
</p>
</div>
</div>
</div>

@ -1,6 +1,6 @@
<section> <section class="address-overview">
<div class="row"> <div class="row">
<div class="col-md-12 col-lg-8"> <div class="card-section col-md-12 col-lg-5">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<div class="icon-links float-right"> <div class="icon-links float-right">
@ -51,9 +51,12 @@
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-6 col-lg-4" data-selector="balance-card"> <div class="card-section col-md-6 col-lg-4" data-selector="balance-card">
<%= render BlockScoutWeb.AddressView, "_balance_card.html", address: @address, exchange_rate: @exchange_rate %> <%= render BlockScoutWeb.AddressView, "_balance_card.html", address: @address, exchange_rate: @exchange_rate %>
</div> </div>
<div class="card-section col-md-6 col-lg-3">
<%= render BlockScoutWeb.AddressView, "_token_holdings.html", conn: @conn, address: @address %>
</div>
</div> </div>
</section> </section>

@ -0,0 +1,36 @@
<%= if Enum.any?(@tokens) do %>
<a href="#"
data-dropdown-toggle
data-toggle="dropdown"
role="button"
class="icon-link"
id="dropdown-tokens"
aria-haspopup="true"
aria-expanded="false"
style="text-decoration: none;">
<i class="fas fa-chevron-circle-down"></i>
</a>
<% end %>
<h4 data-tokens-count class="ml-2 text-dark"><%= tokens_count_title(@tokens) %></h4>
<div class="dropdown-menu p-0 token-balance-dropdown" aria-labelledby="dropdown-tokens">
<div data-dropdown-items class="dropdown-items">
<%= if Enum.any?(@tokens, & &1.type == "ERC-721") do %>
<%= render(
"_tokens.html",
conn: @conn,
tokens: filter_by_type(@tokens, "ERC-721"),
type: "ERC-721"
) %>
<% end %>
<%= if Enum.any?(@tokens, & &1.type == "ERC-20") do %>
<%= render(
"_tokens.html",
conn: @conn,
tokens: filter_by_type(@tokens, "ERC-20"),
type: "ERC-20"
) %>
<% end %>
</div>
</div>

@ -0,0 +1,17 @@
<h6 class="dropdown-header border-bottom">
<%= @type %> (<%= Enum.count(@tokens)%>)
</h6>
<%= for token <- sort_by_name(@tokens) do %>
<div class="border-bottom">
<%= link(
to: token_path(@conn, :show, :en, token.contract_address_hash),
class: "dropdown-item"
) do %>
<p class="mb-0"><%= token_name(token) %></p>
<p class="mb-0">
<%= format_according_to_decimals(token.balance, token.decimals) %> <%= token.symbol %>
</p>
<% end %>
</div>
<% end %>

@ -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

@ -65,7 +65,21 @@ defmodule BlockScoutWeb.CurrencyHelpers do
iex> format_according_to_decimals(Decimal.new(205000), 2) iex> format_according_to_decimals(Decimal.new(205000), 2)
"2,050" "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, nil) do
format_according_to_decimals(value, 0)
end
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() @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 def format_according_to_decimals(%Decimal{sign: sign, coef: coef, exp: exp}, decimals) do
sign sign

@ -46,15 +46,29 @@ defmodule BlockScoutWeb.Tokens.Helpers do
When the token's symbol is nil, the function will return the contract address hash. 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 def token_symbol(%Token{symbol: nil, contract_address_hash: address_hash}) do
address_hash = "#{contract_address_hash_truncated(address_hash)}..."
address_hash
|> to_string()
|> String.slice(0..6)
"#{address_hash}..."
end end
def token_symbol(%Token{symbol: symbol}) do def token_symbol(%Token{symbol: symbol}) do
symbol symbol
end 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 end

@ -599,7 +599,7 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/address/overview.html.eex:13 #: 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" msgid "QR Code"
msgstr "" msgstr ""
@ -686,7 +686,7 @@ msgid "Block Height #%{height}"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/address/overview.html.eex:74 #: lib/block_scout_web/templates/address/overview.html.eex:77
msgid "Close" msgid "Close"
msgstr "" msgstr ""
@ -816,3 +816,25 @@ msgstr ""
#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:52 #: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:52
msgid "loading..." msgid "loading..."
msgstr "" 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 ""

@ -611,7 +611,7 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/address/overview.html.eex:13 #: 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" msgid "QR Code"
msgstr "" msgstr ""
@ -698,7 +698,7 @@ msgid "Block Height #%{height}"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/address/overview.html.eex:74 #: lib/block_scout_web/templates/address/overview.html.eex:77
msgid "Close" msgid "Close"
msgstr "" msgstr ""
@ -828,3 +828,25 @@ msgstr ""
#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:52 #: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:52
msgid "loading..." msgid "loading..."
msgstr "" 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 ""

@ -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

@ -49,6 +49,20 @@ defmodule BlockScoutWeb.CurrencyHelpersTest do
assert CurrencyHelpers.format_according_to_decimals(amount, decimals) == "10,004.5" assert CurrencyHelpers.format_according_to_decimals(amount, decimals) == "10,004.5"
end 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
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 end
describe "format_integer_to_currency/1" do describe "format_integer_to_currency/1" do

@ -48,15 +48,25 @@ defmodule BlockScoutWeb.Tokens.HelpersTest do
end end
test "returns the token contract address hash when the symbol is nil" do 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) token = build(:token, symbol: nil, contract_address_hash: address.hash)
address_hash = assert Helpers.token_symbol(token) == "de3fa0f..."
address.hash end
|> Explorer.Chain.Hash.to_string() end
|> String.slice(0..6)
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, hash: "de3fa0f9f8d47790ce88c2b2b82ab81f79f2e65f")
token = build(:token, name: nil, contract_address_hash: address.hash)
assert Helpers.token_symbol(token) == "#{address_hash}..." assert Helpers.token_name(token) == "de3fa0f..."
end end
end end
end end

@ -1564,4 +1564,11 @@ defmodule Explorer.Chain do
Repo.one(query) != nil Repo.one(query) != nil
end 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 end

@ -19,8 +19,8 @@ defmodule Explorer.Chain.Token do
use Ecto.Schema use Ecto.Schema
import Ecto.{Changeset} import Ecto.{Changeset, Query}
alias Explorer.Chain.{Address, Hash, Token} alias Explorer.Chain.{Address, Hash, Token, TokenTransfer}
@typedoc """ @typedoc """
* `:name` - Name of the token * `:name` - Name of the token
@ -74,4 +74,21 @@ defmodule Explorer.Chain.Token do
|> foreign_key_constraint(:contract_address) |> foreign_key_constraint(:contract_address)
|> unique_constraint(:contract_address_hash) |> unique_constraint(:contract_address_hash)
end 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 end

@ -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

@ -1604,4 +1604,101 @@ defmodule Explorer.ChainTest do
assert Chain.transaction_has_token_transfers?(transaction.hash) == false assert Chain.transaction_has_token_transfers?(transaction.hash) == false
end end
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 end

@ -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
Loading…
Cancel
Save