Show token price in USD

pull/1264/head
fvictorio 6 years ago
parent 04d734fb1f
commit 616658adbf
  1. 28
      apps/block_scout_web/assets/js/lib/currency.js
  2. 6
      apps/block_scout_web/assets/js/lib/token_balance_dropdown.js
  3. 2
      apps/block_scout_web/config/test.exs
  4. 7
      apps/block_scout_web/lib/block_scout_web/controllers/address_token_balance_controller.ex
  5. 2
      apps/block_scout_web/lib/block_scout_web/controllers/address_token_controller.ex
  6. 4
      apps/block_scout_web/lib/block_scout_web/controllers/tokens/holder_controller.ex
  7. 4
      apps/block_scout_web/lib/block_scout_web/controllers/tokens/inventory_controller.ex
  8. 4
      apps/block_scout_web/lib/block_scout_web/controllers/tokens/read_contract_controller.ex
  9. 4
      apps/block_scout_web/lib/block_scout_web/controllers/tokens/transfer_controller.ex
  10. 3
      apps/block_scout_web/lib/block_scout_web/templates/address_token/_tokens.html.eex
  11. 22
      apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_tokens.html.eex
  12. 11
      apps/block_scout_web/lib/block_scout_web/templates/tokens/overview/_details.html.eex
  13. 15
      apps/block_scout_web/lib/block_scout_web/views/address_token_balance_view.ex
  14. 14
      apps/block_scout_web/lib/block_scout_web/views/currency_helpers.ex
  15. 11
      apps/block_scout_web/lib/block_scout_web/views/tokens/overview_view.ex
  16. 39
      apps/block_scout_web/test/block_scout_web/views/address_token_balance_view_test.exs
  17. 20
      apps/block_scout_web/test/block_scout_web/views/currency_helpers_test.exs
  18. 24
      apps/block_scout_web/test/block_scout_web/views/tokens/overview_view_test.exs
  19. 2
      apps/explorer/config/config.exs
  20. 2
      apps/explorer/config/test.exs
  21. 1
      apps/explorer/lib/explorer/application.ex
  22. 131
      apps/explorer/lib/explorer/known_tokens/known_tokens.ex
  23. 48
      apps/explorer/lib/explorer/known_tokens/source.ex
  24. 14
      apps/explorer/lib/explorer/known_tokens/source/my_ether_wallet.ex
  25. 44
      apps/explorer/lib/explorer/market/market.ex
  26. 131
      apps/explorer/test/explorer/known_tokens/known_tokens_test.exs
  27. 1
      apps/explorer/test/test_helper.exs
  28. 2
      config/test.exs

@ -8,12 +8,17 @@ export function formatUsdValue (value) {
return `${formatCurrencyValue(value)} USD`
}
function formatCurrencyValue (value) {
if (value === 0) return '$0.000000'
if (value < 0.000001) return `${window.localized['Less than']} $0.000001`
if (value < 1) return `$${numeral(value).format('0.000000')}`
if (value < 100000) return `$${numeral(value).format('0,0.00')}`
return `$${numeral(value).format('0,0')}`
function formatTokenUsdValue (value) {
return formatCurrencyValue(value, '@')
}
function formatCurrencyValue (value, symbol) {
symbol = symbol || '$'
if (value === 0) return `${symbol}0.000000`
if (value < 0.000001) return `${window.localized['Less than']} ${symbol}0.000001`
if (value < 1) return `${symbol}${numeral(value).format('0.000000')}`
if (value < 100000) return `${symbol}${numeral(value).format('0,0.00')}`
return `${symbol}${numeral(value).format('0,0')}`
}
function weiToEther (wei) {
@ -24,10 +29,17 @@ function etherToUSD (ether, usdExchangeRate) {
return new BigNumber(ether).multipliedBy(usdExchangeRate).toNumber()
}
function formatAllUsdValues () {
$('[data-usd-value]').each((i, el) => {
export function formatAllUsdValues (root) {
root = root || $(':root')
root.find('[data-usd-value]').each((i, el) => {
el.innerHTML = formatUsdValue(el.dataset.usdValue)
})
root.find('[data-token-usd-value]').each((i, el) => {
el.innerHTML = formatTokenUsdValue(el.dataset.tokenUsdValue)
})
return root
}
formatAllUsdValues()

@ -1,4 +1,5 @@
import $ from 'jquery'
import { formatAllUsdValues } from './currency'
const tokenBalanceDropdown = (element) => {
const $element = $(element)
@ -7,7 +8,10 @@ const tokenBalanceDropdown = (element) => {
const apiPath = element.dataset.api_path
$.get(apiPath)
.done(response => $element.html(response))
.done(response => {
const responseHtml = formatAllUsdValues($(response))
$element.html(responseHtml)
})
.fail(() => {
$loading.hide()
$errorMessage.show()

@ -20,4 +20,6 @@ config :wallaby, screenshot_on_failure: true
config :explorer, Explorer.ExchangeRates, enabled: false, store: :none
config :explorer, Explorer.KnownTokens, enabled: false, store: :none
config :block_scout_web, BlockScoutWeb.Counters.BlocksIndexedCounter, enabled: false

@ -1,12 +1,15 @@
defmodule BlockScoutWeb.AddressTokenBalanceController do
use BlockScoutWeb, :controller
alias Explorer.Chain
alias Explorer.{Chain, Market}
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 = Chain.fetch_last_token_balances(address_hash)
token_balances =
address_hash
|> Chain.fetch_last_token_balances()
|> Market.add_price()
conn
|> put_status(200)

@ -21,7 +21,7 @@ defmodule BlockScoutWeb.AddressTokenController do
transaction_count: transaction_count(address),
validation_count: validation_count(address),
next_page_params: next_page_params(next_page, tokens, params),
tokens: tokens
tokens: Market.add_price(tokens)
)
else
:error ->

@ -2,7 +2,7 @@ defmodule BlockScoutWeb.Tokens.HolderController do
use BlockScoutWeb, :controller
alias BlockScoutWeb.Tokens.HolderView
alias Explorer.Chain
alias Explorer.{Chain, Market}
alias Phoenix.View
import BlockScoutWeb.Chain,
@ -50,7 +50,7 @@ defmodule BlockScoutWeb.Tokens.HolderController do
"index.html",
current_path: current_path(conn),
holders_count_consolidation_enabled: Chain.token_holders_counter_consolidation_enabled?(),
token: token,
token: Market.add_price(token),
total_token_holders: Chain.count_token_holders_from_token_hash(address_hash),
total_token_transfers: Chain.count_token_transfers_from_token_hash(address_hash)
)

@ -1,7 +1,7 @@
defmodule BlockScoutWeb.Tokens.InventoryController do
use BlockScoutWeb, :controller
alias Explorer.Chain
alias Explorer.{Chain, Market}
alias Explorer.Chain.TokenTransfer
import BlockScoutWeb.Chain, only: [split_list_by_page: 1, default_paging_options: 0]
@ -20,7 +20,7 @@ defmodule BlockScoutWeb.Tokens.InventoryController do
render(
conn,
"index.html",
token: token,
token: Market.add_price(token),
unique_tokens: unique_tokens_paginated,
holders_count_consolidation_enabled: Chain.token_holders_counter_consolidation_enabled?(),
total_token_transfers: Chain.count_token_transfers_from_token_hash(address_hash),

@ -1,7 +1,7 @@
defmodule BlockScoutWeb.Tokens.ReadContractController do
use BlockScoutWeb, :controller
alias Explorer.Chain
alias Explorer.{Chain, Market}
def index(conn, %{"token_id" => address_hash_string}) do
with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string),
@ -9,7 +9,7 @@ defmodule BlockScoutWeb.Tokens.ReadContractController do
render(
conn,
"index.html",
token: token,
token: Market.add_price(token),
holders_count_consolidation_enabled: Chain.token_holders_counter_consolidation_enabled?(),
total_token_transfers: Chain.count_token_transfers_from_token_hash(address_hash),
total_token_holders: Chain.count_token_holders_from_token_hash(address_hash)

@ -2,7 +2,7 @@ defmodule BlockScoutWeb.Tokens.TransferController do
use BlockScoutWeb, :controller
alias BlockScoutWeb.Tokens.TransferView
alias Explorer.Chain
alias Explorer.{Chain, Market}
alias Phoenix.View
import BlockScoutWeb.Chain, only: [split_list_by_page: 1, paging_options: 1, next_page_params: 3]
@ -50,7 +50,7 @@ defmodule BlockScoutWeb.Tokens.TransferController do
conn,
"index.html",
current_path: current_path(conn),
token: token,
token: Market.add_price(token),
holders_count_consolidation_enabled: Chain.token_holders_counter_consolidation_enabled?(),
total_token_transfers: Chain.count_token_transfers_from_token_hash(address_hash),
total_token_holders: Chain.count_token_holders_from_token_hash(address_hash)

@ -9,6 +9,9 @@
<%= token_name(@token) %>
<% end %>
<span><%= @token.type %> - <%= @token.contract_address_hash %></span>
<%= if @token.usd_value do %>
<span data-selector="token-price" data-usd-value="<%= @token.usd_value %>"></span>
<% end %>
</div>
<div class="col-md-5 d-flex flex-column text-md-right mt-3 mt-md-0">
<span class="tile-title-lg text-md-right align-bottom">

@ -14,10 +14,24 @@
to: token_path(@conn, :show, token_balance.token.contract_address_hash),
class: "dropdown-item"
) do %>
<p class="mb-0"><%= token_name(token_balance.token) %></p>
<p class="mb-0">
<%= format_according_to_decimals(token_balance.value, token_balance.token.decimals) %> <%= token_balance.token.symbol %>
</p>
<div class="row">
<p class="mb-0 col-md-6"><%= token_name(token_balance.token) %></p>
<%= if token_balance.token.usd_value do %>
<p class="mb-0 col-md-6 text-right">
<span data-selector="token-balance-usd" data-usd-value="<%= balance_in_usd(token_balance) %>"></span>
</p>
<% end %>
</div>
<div class="row">
<p class="mb-0 col-md-6">
<%= format_according_to_decimals(token_balance.value, token_balance.token.decimals) %> <%= token_balance.token.symbol %>
</p>
<%= if token_balance.token.usd_value do %>
<p class="mb-0 col-md-6 text-right text-muted">
<span data-selector="token-price" data-token-usd-value="<%= token_balance.token.usd_value %>"></span>
</p>
<% end %>
</div>
<% end %>
</div>
<% end %>

@ -67,7 +67,16 @@
<%= @token.symbol %>
</h3>
<br />
<%= if @token.usd_value do %>
<div class="text-uppercase text-muted">
<span data-selector="token-supply-usd" data-usd-value="<%= total_supply_usd(@token) %>"></span>
|
<span data-selector="token-price" data-token-usd-value="<%= @token.usd_value %>"></span>
</div>
<% else %>
<br />
<% end %>
</div>
</div>
</div>

@ -1,6 +1,8 @@
defmodule BlockScoutWeb.AddressTokenBalanceView do
use BlockScoutWeb, :view
alias BlockScoutWeb.CurrencyHelpers
def tokens_count_title(token_balances) do
ngettext("%{count} token", "%{count} tokens", Enum.count(token_balances))
end
@ -17,4 +19,17 @@ defmodule BlockScoutWeb.AddressTokenBalanceView do
{unnamed, named} = Enum.split_with(token_balances, &is_nil(&1.token.name))
Enum.sort_by(named, &String.downcase(&1.token.name)) ++ unnamed
end
@doc """
Return the balance in usd corresponding to this token. Return nil if the usd_value of the token is not present.
"""
def balance_in_usd(%{token: %{usd_value: nil}}) do
nil
end
def balance_in_usd(token_balance) do
tokens = CurrencyHelpers.divide_decimals(token_balance.value, token_balance.token.decimals)
price = token_balance.token.usd_value
Decimal.mult(tokens, price)
end
end

@ -52,10 +52,9 @@ defmodule BlockScoutWeb.CurrencyHelpers do
end
@spec format_according_to_decimals(Decimal.t(), Decimal.t()) :: String.t()
def format_according_to_decimals(%Decimal{sign: sign, coef: coef, exp: exp}, decimals) do
sign
|> Decimal.new(coef, exp - Decimal.to_integer(decimals))
|> Decimal.reduce()
def format_according_to_decimals(value, decimals) do
value
|> divide_decimals(decimals)
|> thousands_separator()
end
@ -66,4 +65,11 @@ defmodule BlockScoutWeb.CurrencyHelpers do
Decimal.to_string(value, :normal)
end
end
@spec divide_decimals(Decimal.t(), Decimal.t()) :: Decimal.t()
def divide_decimals(%{sign: sign, coef: coef, exp: exp}, decimals) do
sign
|> Decimal.new(coef, exp - Decimal.to_integer(decimals))
|> Decimal.reduce()
end
end

@ -3,7 +3,7 @@ defmodule BlockScoutWeb.Tokens.OverviewView do
alias Explorer.Chain.{Address, SmartContract, Token}
alias BlockScoutWeb.LayoutView
alias BlockScoutWeb.{CurrencyHelpers, LayoutView}
@tabs ["token_transfers", "token_holders", "read_contract", "inventory"]
@ -47,4 +47,13 @@ defmodule BlockScoutWeb.Tokens.OverviewView do
end
def smart_contract_with_read_only_functions?(%Token{contract_address: %Address{smart_contract: nil}}), do: false
@doc """
Get the total value of the token supply in USD.
"""
def total_supply_usd(token) do
tokens = CurrencyHelpers.divide_decimals(token.total_supply, token.decimals)
price = token.usd_value
Decimal.mult(tokens, price)
end
end

@ -68,4 +68,43 @@ defmodule BlockScoutWeb.AddressTokenBalanceViewTest do
assert AddressTokenBalanceView.sort_by_name(token_balances) == expected
end
end
describe "balance_in_usd/1" do
test "return balance in usd" do
token =
:token
|> build(decimals: Decimal.new(0))
|> Map.put(:usd_value, Decimal.new(3))
token_balance = build(:token_balance, value: Decimal.new(10), token: token)
result = AddressTokenBalanceView.balance_in_usd(token_balance)
assert Decimal.cmp(result, 30) == :eq
end
test "return nil if usd_value is not present" do
token =
:token
|> build(decimals: Decimal.new(0))
|> Map.put(:usd_value, nil)
token_balance = build(:token_balance, value: 10, token: token)
assert AddressTokenBalanceView.balance_in_usd(token_balance) == nil
end
test "consider decimals when computing value" do
token =
:token
|> build(decimals: Decimal.new(2))
|> Map.put(:usd_value, Decimal.new(3))
token_balance = build(:token_balance, value: Decimal.new(10), token: token)
result = AddressTokenBalanceView.balance_in_usd(token_balance)
assert Decimal.cmp(result, Decimal.from_float(0.3)) == :eq
end
end
end

@ -61,4 +61,24 @@ defmodule BlockScoutWeb.CurrencyHelpersTest do
assert CurrencyHelpers.format_integer_to_currency(9000) == "9,000"
end
end
describe "divide_decimals/2" do
test "divide by the given decimal amount" do
result = CurrencyHelpers.divide_decimals(Decimal.new(1000), Decimal.new(3))
expected_result = Decimal.new(1)
assert Decimal.cmp(result, expected_result) == :eq
end
test "work when number of decimals is bigger than the number's digits" do
result = CurrencyHelpers.divide_decimals(Decimal.new(1000), Decimal.new(5))
expected_result = Decimal.from_float(0.01)
assert Decimal.cmp(result, expected_result) == :eq
end
test "return the same number when number of decimals is 0" do
result = CurrencyHelpers.divide_decimals(Decimal.new(1000), Decimal.new(0))
expected_result = Decimal.new(1000)
assert Decimal.cmp(result, expected_result) == :eq
end
end
end

@ -136,4 +136,28 @@ defmodule BlockScoutWeb.Tokens.OverviewViewTest do
refute OverviewView.smart_contract_with_read_only_functions?(token)
end
end
describe "total_supply_usd/1" do
test "returns the correct total supply value" do
token =
:token
|> build(decimals: Decimal.new(0), total_supply: Decimal.new(20))
|> Map.put(:usd_value, Decimal.new(10))
result = OverviewView.total_supply_usd(token)
assert Decimal.cmp(result, Decimal.new(200)) == :eq
end
test "takes decimals into account" do
token =
:token
|> build(decimals: Decimal.new(1), total_supply: Decimal.new(20))
|> Map.put(:usd_value, Decimal.new(10))
result = OverviewView.total_supply_usd(token)
assert Decimal.cmp(result, Decimal.new(20)) == :eq
end
end
end

@ -17,6 +17,8 @@ config :explorer, Explorer.Counters.TokenHoldersCounter, enabled: true, enable_c
config :explorer, Explorer.ExchangeRates, enabled: true, store: :ets
config :explorer, Explorer.KnownTokens, enabled: true, store: :ets
config :explorer, Explorer.Integrations.EctoLogger, query_time_ms_threshold: :timer.seconds(2)
config :explorer, Explorer.Market.History.Cataloger, enabled: true

@ -13,6 +13,8 @@ config :explorer, Explorer.Repo,
config :explorer, Explorer.ExchangeRates, enabled: false, store: :ets
config :explorer, Explorer.KnownTokens, enabled: false, store: :ets
config :explorer, Explorer.Counters.AddressesWithBalanceCounter, enabled: false, enable_consolidation: false
config :explorer, Explorer.Counters.TokenHoldersCounter, enabled: false, enable_consolidation: false

@ -34,6 +34,7 @@ defmodule Explorer.Application do
defp configurable_children do
[
configure(Explorer.ExchangeRates),
configure(Explorer.KnownTokens),
configure(Explorer.Market.History.Cataloger),
configure(Explorer.Counters.TokenHoldersCounter),
configure(Explorer.Counters.AddressesWithBalanceCounter),

@ -0,0 +1,131 @@
defmodule Explorer.KnownTokens do
@moduledoc """
Local cache for known tokens addresses. This fetches and exposes a mapping from a token symbol to the known contract
address for the token with that symbol. This data can be consumed through the Market module.
Data is updated every 1 hour.
"""
use GenServer
require Logger
alias Explorer.Chain.Hash
alias Explorer.KnownTokens.Source
@interval :timer.hours(1)
@table_name :known_tokens
@impl GenServer
def handle_info(:update, state) do
Logger.debug(fn -> "Updating cached known tokens" end)
fetch_known_tokens()
{:noreply, state}
end
# Callback for successful fetch
@impl GenServer
def handle_info({_ref, {:ok, addresses}}, state) do
if store() == :ets do
records = Enum.map(addresses, fn x -> {x["symbol"], x["address"]} end)
:ets.insert(table_name(), records)
end
{:noreply, state}
end
# Callback for errored fetch
@impl GenServer
def handle_info({_ref, {:error, reason}}, state) do
Logger.warn(fn -> "Failed to get known tokens with reason '#{reason}'." end)
fetch_known_tokens()
{:noreply, state}
end
# Callback that a monitored process has shutdown
@impl GenServer
def handle_info({:DOWN, _, :process, _, _}, state) do
{:noreply, state}
end
@impl GenServer
def init(_) do
send(self(), :update)
:timer.send_interval(@interval, :update)
table_opts = [
:set,
:named_table,
:public,
read_concurrency: true
]
if store() == :ets do
:ets.new(table_name(), table_opts)
end
{:ok, %{}}
end
def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
@doc """
Lists known tokens.
"""
@spec list :: [{String.t(), Hash.Address.t()}]
def list do
list_from_store(store())
end
@doc """
Returns a specific address from the known tokens by symbol
"""
@spec lookup(String.t()) :: {:ok, Hash.Address.t()} | :error | nil
def lookup(symbol) do
if store() == :ets do
case :ets.lookup(table_name(), symbol) do
[{_symbol, address} | _] -> Hash.Address.cast(address)
_ -> nil
end
end
end
@doc false
@spec table_name() :: atom()
def table_name do
config(:table_name) || @table_name
end
@spec config(atom()) :: term
defp config(key) do
Application.get_env(:explorer, __MODULE__, [])[key]
end
@spec fetch_known_tokens :: Task.t()
defp fetch_known_tokens do
Task.Supervisor.async_nolink(Explorer.MarketTaskSupervisor, fn ->
Source.fetch_known_tokens()
end)
end
defp list_from_store(:ets) do
table_name()
|> :ets.tab2list()
|> Enum.map(&elem(&1, 1))
|> Enum.map(&Hash.Address.cast/1)
|> Enum.sort()
end
defp list_from_store(_), do: []
defp store do
config(:store) || :ets
end
end

@ -0,0 +1,48 @@
defmodule Explorer.KnownTokens.Source do
@moduledoc """
Behaviour for fetching list of known tokens.
"""
alias Explorer.Chain.Hash
alias HTTPoison.{Error, Response}
@doc """
Fetches known tokens
"""
@spec fetch_known_tokens() :: {:ok, [Hash.Address.t()]} | {:error, any}
def fetch_known_tokens(source \\ known_tokens_source()) do
case HTTPoison.get(source.source_url(), headers()) do
{:ok, %Response{body: body, status_code: 200}} ->
{:ok, decode_json(body)}
{:ok, %Response{body: body, status_code: status_code}} when status_code in 400..499 ->
{:error, decode_json(body)["error"]}
{:error, %Error{reason: reason}} ->
{:error, reason}
end
end
@doc """
Url for querying the list of known tokens.
"""
@callback source_url() :: String.t()
def headers do
[{"Content-Type", "application/json"}]
end
def decode_json(data) do
Jason.decode!(data)
end
@spec known_tokens_source() :: module()
defp known_tokens_source do
config(:source) || Explorer.KnownTokens.Source.MyEtherWallet
end
@spec config(atom()) :: term
defp config(key) do
Application.get_env(:explorer, __MODULE__, [])[key]
end
end

@ -0,0 +1,14 @@
defmodule Explorer.KnownTokens.Source.MyEtherWallet do
@moduledoc """
Adapter for fetching known tokens from MyEtherWallet's GitHub
"""
alias Explorer.KnownTokens.Source
@behaviour Source
@impl Source
def source_url do
"https://raw.githubusercontent.com/kvhnuke/etherwallet/mercury/app/scripts/tokens/ethTokens.json"
end
end

@ -5,9 +5,11 @@ defmodule Explorer.Market do
import Ecto.Query
alias Explorer.{ExchangeRates, Repo}
alias Explorer.Chain.Address.CurrentTokenBalance
alias Explorer.Chain.Hash
alias Explorer.ExchangeRates.Token
alias Explorer.Market.MarketHistory
alias Explorer.{ExchangeRates, KnownTokens, Repo}
@doc """
Get most recent exchange rate for the given symbol.
@ -17,6 +19,17 @@ defmodule Explorer.Market do
ExchangeRates.lookup(symbol)
end
@doc """
Get the address of the token with the given symbol.
"""
@spec get_known_address(String.t()) :: Hash.Address.t() | nil
def get_known_address(symbol) do
case KnownTokens.lookup(symbol) do
{:ok, address} -> address
nil -> nil
end
end
@doc """
Retrieves the history for the recent specified amount of days.
@ -40,4 +53,33 @@ defmodule Explorer.Market do
def bulk_insert_history(records) do
Repo.insert_all(MarketHistory, records, on_conflict: :replace_all, conflict_target: [:date])
end
def add_price(%{symbol: symbol} = token) do
known_address = get_known_address(symbol)
matches_known_address = known_address && known_address == token.contract_address_hash
usd_value = fetch_token_usd_value(matches_known_address, symbol)
Map.put(token, :usd_value, usd_value)
end
def add_price(%CurrentTokenBalance{token: token} = token_balance) do
token_with_price = add_price(token)
Map.put(token_balance, :token, token_with_price)
end
def add_price(tokens) when is_list(tokens) do
Enum.map(tokens, &add_price/1)
end
defp fetch_token_usd_value(true, symbol) do
case get_exchange_rate(symbol) do
%{usd_value: usd_value} -> usd_value
nil -> nil
end
end
defp fetch_token_usd_value(_matches_known_address, _symbol), do: nil
end

@ -0,0 +1,131 @@
defmodule Explorer.KnownTokensTest do
use ExUnit.Case, async: false
import Mox
alias Plug.Conn
alias Explorer.Chain.Hash
alias Explorer.KnownTokens
alias Explorer.KnownTokens.Source.TestSource
@moduletag :capture_log
setup :verify_on_exit!
setup do
set_mox_global()
# Use TestSource mock and ets table for this test set
source_configuration = Application.get_env(:explorer, Explorer.KnownTokens.Source)
known_tokens_configuration = Application.get_env(:explorer, Explorer.KnownTokens)
Application.put_env(:explorer, Explorer.KnownTokens.Source, source: TestSource)
Application.put_env(:explorer, Explorer.KnownTokens, table_name: :known_tokens)
on_exit(fn ->
Application.put_env(:explorer, Explorer.KnownTokens.Source, source_configuration)
Application.put_env(:explorer, Explorer.KnownTokens, known_tokens_configuration)
end)
end
test "init" do
assert :ets.info(KnownTokens.table_name()) == :undefined
assert {:ok, %{}} == KnownTokens.init([])
assert_received :update
table = :ets.info(KnownTokens.table_name())
refute table == :undefined
assert table[:name] == KnownTokens.table_name()
assert table[:named_table]
assert table[:read_concurrency]
assert table[:type] == :set
refute table[:write_concurrency]
end
test "handle_info with :update" do
bypass = Bypass.open()
Bypass.expect(bypass, "GET", "/", fn conn ->
Conn.resp(conn, 200, ~s([{"symbol": "TEST1","address": "0x0000000000000000000000000000000000000001"}]))
end)
stub(TestSource, :source_url, fn -> "http://localhost:#{bypass.port}" end)
KnownTokens.init([])
state = %{}
assert {:noreply, ^state} = KnownTokens.handle_info(:update, state)
assert_receive {_, {:ok, [%{"symbol" => "TEST1", "address" => "0x0000000000000000000000000000000000000001"}]}}
end
describe "ticker fetch task" do
setup do
KnownTokens.init([])
:ok
end
test "with successful fetch" do
symbol = "TEST2"
address = "0x0000000000000000000000000000000000000002"
token = %{
"symbol" => symbol,
"address" => address
}
state = %{}
assert {:noreply, ^state} = KnownTokens.handle_info({nil, {:ok, [token]}}, state)
assert [{"TEST2", "0x0000000000000000000000000000000000000002"}] == :ets.lookup(KnownTokens.table_name(), symbol)
end
test "with failed fetch" do
bypass = Bypass.open()
Bypass.expect(bypass, "GET", "/", fn conn ->
Conn.resp(conn, 200, "{}")
end)
stub(TestSource, :source_url, fn -> "http://localhost:#{bypass.port}" end)
state = %{}
assert {:noreply, ^state} = KnownTokens.handle_info({nil, {:error, "some error"}}, state)
assert_receive {_, {:ok, _}}
end
end
test "list/0" do
KnownTokens.init([])
known_tokens = [
{"TEST1", "0x0000000000000000000000000000000000000001"},
{"TEST2", "0x0000000000000000000000000000000000000002"}
]
:ets.insert(KnownTokens.table_name(), known_tokens)
expected_tokens =
known_tokens
|> Enum.map(&elem(&1, 1))
|> Enum.map(&Hash.Address.cast/1)
assert expected_tokens == KnownTokens.list()
end
test "lookup/1" do
KnownTokens.init([])
known_tokens = [
{"TEST1", "0x0000000000000000000000000000000000000001"},
{"TEST2", "0x0000000000000000000000000000000000000002"}
]
:ets.insert(KnownTokens.table_name(), known_tokens)
assert Hash.Address.cast("0x0000000000000000000000000000000000000001") == KnownTokens.lookup("TEST1")
assert nil == KnownTokens.lookup("nope")
end
end

@ -14,6 +14,7 @@ ExUnit.start()
Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo, :manual)
Mox.defmock(Explorer.ExchangeRates.Source.TestSource, for: Explorer.ExchangeRates.Source)
Mox.defmock(Explorer.KnownTokens.Source.TestSource, for: Explorer.KnownTokens.Source)
Mox.defmock(Explorer.Market.History.Source.TestSource, for: Explorer.Market.History.Source)
Mox.defmock(EthereumJSONRPC.Mox, for: EthereumJSONRPC.Transport)

@ -13,3 +13,5 @@ config :logger, :error, path: Path.absname("logs/test/error.log")
config :explorer, Explorer.ExchangeRates,
source: Explorer.ExchangeRates.Source.NoOpSource,
store: :none
config :explorer, Explorer.KnownTokens, store: :none

Loading…
Cancel
Save