Merge branch 'master' into 906-ui-improvements

pull/915/head
Andrew Cravenho 6 years ago committed by GitHub
commit 5b5db39e62
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      apps/block_scout_web/assets/css/components/_loading-spinner.scss
  2. 2
      apps/block_scout_web/assets/css/theme/_base_variables.scss
  3. 1
      apps/block_scout_web/assets/js/app.js
  4. 4
      apps/block_scout_web/assets/js/lib/currency.js
  5. 25
      apps/block_scout_web/assets/js/lib/indexing.js
  6. 2
      apps/block_scout_web/assets/js/pages/address.js
  7. 16
      apps/block_scout_web/lib/block_scout_web/notifier.ex
  8. 13
      apps/block_scout_web/lib/block_scout_web/templates/layout/app.html.eex
  9. 23
      apps/block_scout_web/priv/gettext/default.pot
  10. 23
      apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
  11. 15
      apps/block_scout_web/test/block_scout_web/channels/block_channel_test.exs
  12. 19
      apps/block_scout_web/test/block_scout_web/features/pages/app_page.ex
  13. 78
      apps/block_scout_web/test/block_scout_web/features/viewing_app_test.exs
  14. 2
      apps/explorer/config/config.exs
  15. 6
      apps/explorer/lib/explorer/application.ex
  16. 123
      apps/explorer/lib/explorer/chain.ex
  17. 94
      apps/explorer/lib/explorer/counters/block_validation_counter.ex
  18. 63
      apps/explorer/test/explorer/chain_test.exs
  19. 39
      apps/explorer/test/explorer/counters/block_validation_counter_test.exs

@ -13,7 +13,7 @@
}
.loading-spinner-block-1, .loading-spinner-block-2 {
background-color: $light;
background-color: currentColor;
width: 12px;
height: 12px;
position: absolute;

@ -62,7 +62,7 @@ $secondary: #7dd79f !default;
$tertiary: $purple !default;
$success: $green !default;
$info: $cyan !default;
$warning: $orange !default;
$warning: $yellow !default;
$danger: $red !default;
$light: $gray-400 !default;
$dark: $gray-800 !default;

@ -23,6 +23,7 @@ import './locale'
import './lib/clipboard_buttons'
import './lib/currency'
import './lib/from_now'
import './lib/indexing'
import './lib/loading_element'
import './lib/market_history_chart'
import './lib/reload_button'

@ -34,10 +34,8 @@ function tryUpdateCalculatedUsdValues (el, usdExchangeRate = el.dataset.usdExcha
const formattedUsd = formatUsdValue(usd)
if (formattedUsd !== el.innerHTML) el.innerHTML = formattedUsd
}
let currentUsdExchangeRate
export function updateAllCalculatedUsdValues (usdExchangeRate) {
currentUsdExchangeRate = usdExchangeRate
$('[data-usd-exchange-rate]').each((i, el) => tryUpdateCalculatedUsdValues(el, currentUsdExchangeRate))
$('[data-usd-exchange-rate]').each((i, el) => tryUpdateCalculatedUsdValues(el, usdExchangeRate))
}
updateAllCalculatedUsdValues()

@ -0,0 +1,25 @@
import $ from 'jquery'
import humps from 'humps'
import numeral from 'numeral'
import socket from '../socket'
function tryUpdateIndexedStatus (el, indexedRatio = el.dataset.indexedRatio, indexingFinished = false) {
if (indexingFinished) return $("[data-selector='indexed-status']").remove()
const blocksPercentComplete = numeral(indexedRatio).format('0%')
let indexedText
if (blocksPercentComplete === '100%') {
indexedText = window.localized['Indexing Tokens']
} else {
indexedText = `${blocksPercentComplete} ${window.localized['Blocks Indexed']}`
}
if (indexedText !== el.innerHTML) el.innerHTML = indexedText
}
export function updateIndexStatus (msg = {}) {
$('[data-indexed-ratio]').each((i, el) => tryUpdateIndexedStatus(el, msg.ratio, msg.finished))
}
updateIndexStatus()
const indexingChannel = socket.channel(`blocks:indexing`)
indexingChannel.join()
indexingChannel.on('index_status', (msg) => updateIndexStatus(humps.camelizeKeys(msg)))

@ -36,7 +36,7 @@ export function reducer (state = initialState, action) {
beyondPageOne: action.beyondPageOne,
filter: action.filter,
transactionCount: numeral(action.transactionCount).value(),
validationCount: action.transactionCount ? numeral(action.transactionCount).value() : null
validationCount: action.validationCount ? numeral(action.validationCount).value() : null
})
}
case 'CHANNEL_DISCONNECTED': {

@ -16,6 +16,22 @@ defmodule BlockScoutWeb.Notifier do
|> Enum.each(&broadcast_balance/1)
end
def handle_event({:chain_event, :blocks, :catchup, _blocks}) do
ratio = Chain.indexed_ratio()
finished? =
if ratio < 1 do
false
else
Chain.finished_indexing?()
end
Endpoint.broadcast("blocks:indexing", "index_status", %{
ratio: ratio,
finished: finished?
})
end
def handle_event({:chain_event, :blocks, :realtime, blocks}) do
Enum.each(blocks, &broadcast_block/1)
end

@ -21,7 +21,18 @@
<body>
<div class="layout-container">
<%= if not Explorer.Chain.finished_indexing?() do %>
<div class="alert alert-warning text-center mb-0 p-3" data-selector="indexed-status">
<span class="loading-spinner-small mr-2">
<span class="loading-spinner-block-1"></span>
<span class="loading-spinner-block-2"></span>
</span>
<span data-indexed-ratio="<%=Explorer.Chain.indexed_ratio() %>"></span>
<%= gettext("- We're indexing this chain right now. Some of the counts may be inaccurate.") %>
</div>
<% end %>
<%= render BlockScoutWeb.LayoutView, "_topnav.html", assigns %>
<main class="pt-5">
<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
@ -31,7 +42,9 @@
</div>
<script>
window.localized = {
'Blocks Indexed': '<%= gettext("Blocks Indexed") %>',
'Block Processing': '<%= gettext("Block Mined, awaiting import...") %>',
'Indexing Tokens': '<%= gettext("Indexing Tokens") %>',
'Less than': '<%= gettext("Less than") %>',
'Market Cap': '<%= gettext("Market Cap") %>',
'Price': '<%= gettext("Price") %>',

@ -437,7 +437,7 @@ msgid "Inventory"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/app.html.eex:35
#: lib/block_scout_web/templates/layout/app.html.eex:48
msgid "Less than"
msgstr ""
@ -456,7 +456,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/chain/show.html.eex:18
#: lib/block_scout_web/templates/layout/app.html.eex:36
#: lib/block_scout_web/templates/layout/app.html.eex:49
#: lib/block_scout_web/views/address_view.ex:102
msgid "Market Cap"
msgstr ""
@ -615,7 +615,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/chain/show.html.eex:11
#: lib/block_scout_web/templates/layout/app.html.eex:37
#: lib/block_scout_web/templates/layout/app.html.eex:50
msgid "Price"
msgstr ""
@ -1158,6 +1158,21 @@ msgid "Uncles"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/app.html.eex:34
#: lib/block_scout_web/templates/layout/app.html.eex:46
msgid "Block Mined, awaiting import..."
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/app.html.eex:31
msgid "- We're indexing this chain right now. Some of the counts may be inaccurate."
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/app.html.eex:45
msgid "Blocks Indexed"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/app.html.eex:47
msgid "Indexing Tokens"
msgstr ""

@ -437,7 +437,7 @@ msgid "Inventory"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/app.html.eex:35
#: lib/block_scout_web/templates/layout/app.html.eex:48
msgid "Less than"
msgstr ""
@ -456,7 +456,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/chain/show.html.eex:18
#: lib/block_scout_web/templates/layout/app.html.eex:36
#: lib/block_scout_web/templates/layout/app.html.eex:49
#: lib/block_scout_web/views/address_view.ex:102
msgid "Market Cap"
msgstr ""
@ -615,7 +615,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/chain/show.html.eex:11
#: lib/block_scout_web/templates/layout/app.html.eex:37
#: lib/block_scout_web/templates/layout/app.html.eex:50
msgid "Price"
msgstr ""
@ -1158,6 +1158,21 @@ msgid "Uncles"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/app.html.eex:34
#: lib/block_scout_web/templates/layout/app.html.eex:46
msgid "Block Mined, awaiting import..."
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/app.html.eex:31
msgid "- We're indexing this chain right now. Some of the counts may be inaccurate."
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/app.html.eex:45
msgid "Blocks Indexed"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/app.html.eex:47
msgid "Indexing Tokens"
msgstr ""

@ -19,4 +19,19 @@ defmodule BlockScoutWeb.BlockChannelTest do
assert false, "Expected message received nothing."
end
end
test "subscribed user is notified of new_block event for catchup" do
topic = "blocks:indexing"
@endpoint.subscribe(topic)
Notifier.handle_event({:chain_event, :blocks, :catchup, []})
receive do
%Phoenix.Socket.Broadcast{topic: ^topic, event: "index_status", payload: %{}} ->
assert true
after
5_000 ->
assert false, "Expected message received nothing."
end
end
end

@ -0,0 +1,19 @@
defmodule BlockScoutWeb.AppPage do
@moduledoc false
use Wallaby.DSL
import Wallaby.Query, only: [css: 1, css: 2]
def visit_page(session) do
visit(session, "/")
end
def indexed_status(text) do
css("[data-selector='indexed-status'] [data-indexed-ratio]", text: text)
end
def still_indexing?() do
css("[data-selector='indexed-status']")
end
end

@ -0,0 +1,78 @@
defmodule BlockScoutWeb.ViewingAppTest do
@moduledoc false
use BlockScoutWeb.FeatureCase, async: true
alias BlockScoutWeb.{AppPage, Notifier}
describe "loading bar when indexing" do
test "shows blocks indexed percentage", %{session: session} do
for index <- 6..10 do
insert(:block, number: index)
end
session
|> AppPage.visit_page()
|> assert_has(AppPage.indexed_status("50% Blocks Indexed"))
end
test "shows tokens loading", %{session: session} do
for index <- 1..10 do
insert(:block, number: index)
end
session
|> AppPage.visit_page()
|> assert_has(AppPage.indexed_status("Indexing Tokens"))
end
test "live updates blocks indexed percentage", %{session: session} do
for index <- 6..10 do
insert(:block, number: index)
end
session
|> AppPage.visit_page()
|> assert_has(AppPage.indexed_status("50% Blocks Indexed"))
insert(:block, number: 5)
Notifier.handle_event({:chain_event, :blocks, :catchup, []})
assert_has(session, AppPage.indexed_status("60% Blocks Indexed"))
end
test "live updates when blocks are fully indexed", %{session: session} do
for index <- 2..10 do
insert(:block, number: index)
end
session
|> AppPage.visit_page()
|> assert_has(AppPage.indexed_status("90% Blocks Indexed"))
insert(:block, number: 1)
Notifier.handle_event({:chain_event, :blocks, :catchup, []})
assert_has(session, AppPage.indexed_status("Indexing Tokens"))
end
test "live removes message when chain is indexed", %{session: session} do
[block | _] =
for index <- 1..10 do
insert(:block, number: index)
end
session
|> AppPage.visit_page()
|> assert_has(AppPage.indexed_status("Indexing Tokens"))
:transaction
|> insert()
|> with_block(block, internal_transactions_indexed_at: DateTime.utc_now())
Notifier.handle_event({:chain_event, :blocks, :catchup, []})
refute_has(session, AppPage.still_indexing?())
end
end
end

@ -16,6 +16,8 @@ config :explorer, Explorer.Integrations.EctoLogger, query_time_ms_threshold: 2_0
config :explorer, Explorer.ExchangeRates, enabled: true
config :explorer, Explorer.Counters.BlockValidationCounter, enabled: true
config :explorer, Explorer.Market.History.Cataloger, enabled: true
config :explorer, Explorer.Repo,

@ -16,9 +16,6 @@ defmodule Explorer.Application do
Explorer.Repo,
Supervisor.child_spec({Task.Supervisor, name: Explorer.MarketTaskSupervisor}, id: Explorer.MarketTaskSupervisor),
Supervisor.child_spec({Task.Supervisor, name: Explorer.TaskSupervisor}, id: Explorer.TaskSupervisor),
Supervisor.child_spec({Task.Supervisor, name: Explorer.CounterTokenSupervisor},
id: Explorer.CounterTokenSupervisor
),
{Registry, keys: :duplicate, name: Registry.ChainEvents, id: Registry.ChainEvents}
]
@ -33,7 +30,8 @@ defmodule Explorer.Application do
[
configure(Explorer.ExchangeRates),
configure(Explorer.Market.History.Cataloger),
configure(Explorer.Counters.TokenTransferCounter)
configure(Explorer.Counters.TokenTransferCounter),
configure(Explorer.Counters.BlockValidationCounter)
]
|> List.flatten()
end

@ -12,8 +12,7 @@ defmodule Explorer.Chain do
order_by: 3,
preload: 2,
where: 2,
where: 3,
select: 3
where: 3
]
alias Ecto.Adapters.SQL
@ -38,7 +37,7 @@ defmodule Explorer.Chain do
alias Explorer.Chain.Block.Reward
alias Explorer.{PagingOptions, Repo}
alias Explorer.Counters.TokenTransferCounter
alias Explorer.Counters.{TokenTransferCounter, BlockValidationCounter}
@default_paging_options %PagingOptions{page_size: 50}
@ -555,6 +554,28 @@ defmodule Explorer.Chain do
{:actual, fee}
end
@doc """
Checks to see if the chain is down indexing based on the transaction from the oldest block having
an `internal_transactions_indexed_at` date.
"""
@spec finished_indexing?() :: boolean()
def finished_indexing? do
min_block_number_transaction = Repo.aggregate(Transaction, :min, :block_number)
if min_block_number_transaction do
Transaction
|> where([t], t.block_number == ^min_block_number_transaction and is_nil(t.internal_transactions_indexed_at))
|> limit(1)
|> Repo.one()
|> case do
nil -> true
_ -> false
end
else
false
end
end
@doc """
The `t:Explorer.Chain.Transaction.t/0` `gas_price` of the `transaction` in `unit`.
"""
@ -801,6 +822,32 @@ defmodule Explorer.Chain do
Import.all(options)
end
@doc """
The percentage of indexed blocks on the chain.
iex> for index <- 6..10 do
...> insert(:block, number: index)
...> end
iex> Explorer.Chain.indexed_ratio()
0.5
If there are no blocks, the percentage is 0.
iex> Explorer.Chain.indexed_ratio()
0
"""
@spec indexed_ratio() :: float()
def indexed_ratio do
with {:ok, min_block_number} <- min_block_number(),
{:ok, max_block_number} <- max_block_number() do
indexed_blocks = max_block_number - min_block_number + 1
indexed_blocks / max_block_number
else
{:error, _} -> 0
end
end
@doc """
The number of `t:Explorer.Chain.InternalTransaction.t/0`.
@ -822,31 +869,6 @@ defmodule Explorer.Chain do
Repo.aggregate(InternalTransaction, :count, :id)
end
@doc """
Finds block with greatest number.
iex> insert(:block, number: 2)
iex> insert(:block, number: 1)
iex> {:ok, %Explorer.Chain.Block{number: number}} = Explorer.Chain.max_numbered_block()
iex> number
2
If there are no blocks `{:error, :not_found}` is returned.
iex> Explorer.Chain.max_numbered_block()
{:error, :not_found}
"""
@spec max_numbered_block() :: {:ok, Block.t()} | {:error, :not_found}
def max_numbered_block do
query = from(block in Block, order_by: [desc: block.number], limit: 1)
case Repo.one(query) do
nil -> {:error, :not_found}
block -> {:ok, block}
end
end
@doc """
Finds all `t:Explorer.Chain.Transaction.t/0` in the `t:Explorer.Chain.Block.t/0`.
@ -919,15 +941,28 @@ defmodule Explorer.Chain do
|> Repo.all()
end
@doc """
Counts all of the block validations and groups by the `miner_hash`.
"""
def group_block_validations_by_address do
query =
from(
b in Block,
join: addr in Address,
where: b.miner_hash == addr.hash,
select: {b.miner_hash, count(b.miner_hash)},
group_by: b.miner_hash
)
Repo.all(query)
end
@doc """
Counts the number of `t:Explorer.Chain.Block.t/0` validated by the `address`.
"""
@spec address_to_validation_count(Address.t()) :: non_neg_integer()
def address_to_validation_count(%Address{hash: hash}) do
Block
|> where(miner_hash: ^hash)
|> select([b], count(b.hash))
|> Repo.one()
BlockValidationCounter.fetch(hash)
end
@doc """
@ -1142,6 +1177,30 @@ defmodule Explorer.Chain do
end
end
@doc """
The minimum `t:Explorer.Chain.Block.t/0` `number` (used to show loading status while indexing)
If blocks are skipped and inserted out of number order, the min number is still returned
iex> insert(:block, number: 2)
iex> insert(:block, number: 1)
iex> Explorer.Chain.min_block_number()
{:ok, 1}
If there are no blocks, `{:error, :not_found}` is returned
iex> Explorer.Chain.min_block_number()
{:error, :not_found}
"""
@spec min_block_number() :: {:ok, Block.block_number()} | {:error, :not_found}
def min_block_number do
case Repo.aggregate(Block, :min, :number) do
nil -> {:error, :not_found}
number -> {:ok, number}
end
end
@doc """
Calculates the ranges of missing consensus blocks in `range`.

@ -0,0 +1,94 @@
defmodule Explorer.Counters.BlockValidationCounter do
use GenServer
@moduledoc """
Module responsible for fetching and consolidating the number of
validations from an address.
"""
alias Explorer.Chain
alias Explorer.Chain.Hash
@table :block_validation_counter
def table_name do
@table
end
@doc """
Creates a process to continually monitor the validation counts.
"""
@spec start_link(term()) :: GenServer.on_start()
def start_link(_) do
GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
end
## Server
@impl true
def init(args) do
create_table()
Task.start_link(&consolidate_blocks/0)
Chain.subscribe_to_events(:blocks)
{:ok, args}
end
def create_table do
opts = [
:set,
:named_table,
:public,
read_concurrency: true,
write_concurrency: true
]
:ets.new(table_name(), opts)
end
@doc """
Consolidates the number of block validations grouped by `address_hash`.
"""
def consolidate_blocks do
total_block_validations = Chain.group_block_validations_by_address()
for {address_hash, total} <- total_block_validations do
insert_or_update_counter(address_hash, total)
end
end
@doc """
Fetches the number of validations related to an `address_hash`.
"""
@spec fetch(Hash.Address.t()) :: non_neg_integer
def fetch(addr_hash) do
do_fetch(:ets.lookup(table_name(), to_string(addr_hash)))
end
defp do_fetch([{_, result} | _]), do: result
defp do_fetch([]), do: 0
@impl true
def handle_info({:chain_event, :blocks, _type, blocks}, state) do
blocks
|> Enum.map(& &1.miner_hash)
|> Enum.each(&insert_or_update_counter(&1, 1))
{:noreply, state}
end
@doc """
Inserts a new item into the `:ets` table.
When the record exist, the counter will be incremented by one. When the
record does not exist, the counter will be inserted with a default value.
"""
@spec insert_or_update_counter(Hash.Address.t(), non_neg_integer) :: term()
def insert_or_update_counter(addr_hash, number) do
string_addr = to_string(addr_hash)
default = {string_addr, 0}
:ets.update_counter(table_name(), string_addr, number, default)
end
end

@ -558,6 +558,28 @@ defmodule Explorer.ChainTest do
end
end
describe "finished_indexing?/0" do
test "finished indexing" do
block = insert(:block, number: 1)
:transaction
|> insert()
|> with_block(block, internal_transactions_indexed_at: DateTime.utc_now())
assert Chain.finished_indexing?()
end
test "not finished indexing" do
block = insert(:block, number: 1)
:transaction
|> insert()
|> with_block(block)
refute Chain.finished_indexing?()
end
end
describe "gas_price/2" do
test ":wei unit" do
assert Chain.gas_price(%Transaction{gas_price: %Wei{value: Decimal.new(1)}}, :wei) == Decimal.new(1)
@ -719,6 +741,28 @@ defmodule Explorer.ChainTest do
end
end
describe "indexed_ratio/0" do
test "returns indexed ratio" do
for index <- 6..10 do
insert(:block, number: index)
end
assert 0.5 == Chain.indexed_ratio()
end
test "returns 0 if no blocks" do
assert 0 == Chain.indexed_ratio()
end
test "returns 1.0 if fully indexed blocks" do
for index <- 1..10 do
insert(:block, number: index)
end
assert 1.0 == Chain.indexed_ratio()
end
end
# Full tests in `test/explorer/import_test.exs`
describe "import/1" do
@import_data %{
@ -1127,23 +1171,16 @@ defmodule Explorer.ChainTest do
end
end
describe "address_to_validation_count/1" do
test "returns 0 when there aren't any blocks" do
address = insert(:address)
assert 0 = Chain.address_to_validation_count(address)
end
test "returns the number of blocks mined by addres" do
describe "group_block_validations_by_address/0" do
test "returns block validations grouped by the address that validated them (`address_hash`)" do
address = insert(:address)
another_address = insert(:address)
insert(:block, miner: address, miner_hash: address.hash)
insert(:block, miner: another_address, miner_hash: another_address.hash)
insert(:block, miner: another_address, miner_hash: another_address.hash)
assert 1 = Chain.address_to_validation_count(address)
assert 2 = Chain.address_to_validation_count(another_address)
results = Chain.group_block_validations_by_address()
assert length(results) == 1
assert results == [{address.hash, 1}]
end
end

@ -0,0 +1,39 @@
defmodule Explorer.Counters.BlockValidationCounterTest do
use Explorer.DataCase
alias Explorer.Counters.BlockValidationCounter
describe "consolidate/0" do
test "loads the address' validations consolidated info" do
address = insert(:address)
insert(:block, miner: address, miner_hash: address.hash)
insert(:block, miner: address, miner_hash: address.hash)
another_address = insert(:address)
insert(:block, miner: another_address, miner_hash: another_address.hash)
BlockValidationCounter.consolidate_blocks()
assert BlockValidationCounter.fetch(address.hash) == 2
assert BlockValidationCounter.fetch(another_address.hash) == 1
end
end
describe "fetch/1" do
test "fetches the total block validations by a given address" do
address = insert(:address)
another_address = insert(:address)
assert BlockValidationCounter.fetch(address.hash) == 0
assert BlockValidationCounter.fetch(another_address.hash) == 0
BlockValidationCounter.insert_or_update_counter(address.hash, 1)
BlockValidationCounter.insert_or_update_counter(another_address.hash, 10)
assert BlockValidationCounter.fetch(address.hash) == 1
assert BlockValidationCounter.fetch(another_address.hash) == 10
end
end
end
Loading…
Cancel
Save