Live update chain statistics #33
pull/526/head
John Stamates 6 years ago committed by GitHub
commit da3419ddbb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      apps/explorer/config/config.exs
  2. 2
      apps/explorer/config/test.exs
  3. 1
      apps/explorer/lib/explorer/application.ex
  4. 43
      apps/explorer/lib/explorer/chain.ex
  5. 123
      apps/explorer/lib/explorer/chain/statistics.ex
  6. 77
      apps/explorer/lib/explorer/chain/statistics/server.ex
  7. 120
      apps/explorer/test/explorer/chain/statistics/server_test.exs
  8. 76
      apps/explorer/test/explorer/chain/statistics_test.exs
  9. 2
      apps/explorer_web/assets/__tests__/pages/chain.js
  10. 6
      apps/explorer_web/assets/css/components/_animations.scss
  11. 24
      apps/explorer_web/assets/js/pages/chain.js
  12. 2
      apps/explorer_web/config/test.exs
  13. 46
      apps/explorer_web/lib/explorer_web/channels/address_channel.ex
  14. 7
      apps/explorer_web/lib/explorer_web/channels/block_channel.ex
  15. 34
      apps/explorer_web/lib/explorer_web/controllers/chain_controller.ex
  16. 9
      apps/explorer_web/lib/explorer_web/notifier.ex
  17. 2
      apps/explorer_web/lib/explorer_web/templates/chain/_block.html.eex
  18. 12
      apps/explorer_web/lib/explorer_web/templates/chain/show.html.eex
  19. 8
      apps/explorer_web/test/explorer_web/controllers/chain_controller_test.exs
  20. 18
      apps/explorer_web/test/explorer_web/features/pages/chain_page.ex
  21. 11
      apps/explorer_web/test/explorer_web/features/viewing_addresses_test.exs
  22. 32
      apps/explorer_web/test/explorer_web/features/viewing_blocks_test.exs
  23. 184
      apps/explorer_web/test/explorer_web/features/viewing_chain_test.exs
  24. 73
      apps/explorer_web/test/explorer_web/features/viewing_transactions_test.exs
  25. 8
      apps/explorer_web/test/support/fake_adapter.ex

@ -14,8 +14,6 @@ config :explorer,
config :explorer, Explorer.Integrations.EctoLogger, query_time_ms_threshold: 2_000
config :explorer, Explorer.Chain.Statistics.Server, enabled: true
config :explorer, Explorer.ExchangeRates, enabled: true
config :explorer, Explorer.Market.History.Cataloger, enabled: true

@ -13,8 +13,6 @@ config :explorer, Explorer.Repo,
pool_timeout: 10_000,
ownership_timeout: 60_000
config :explorer, Explorer.Chain.Statistics.Server, enabled: false
config :explorer, Explorer.ExchangeRates, enabled: false
config :explorer, Explorer.Market.History.Cataloger, enabled: false

@ -24,7 +24,6 @@ defmodule Explorer.Application do
defp configurable_children do
[
configure(Explorer.Chain.Statistics.Server),
configure(Explorer.ExchangeRates),
configure(Explorer.Market.History.Cataloger)
]

@ -69,7 +69,7 @@ defmodule Explorer.Chain do
Estimated count of addresses
"""
@spec address_estimated_count() :: non_neg_integer()
@spec address_estimated_count :: non_neg_integer()
def address_estimated_count do
%Postgrex.Result{rows: [[rows]]} =
SQL.query!(Repo, "SELECT reltuples::BIGINT AS estimate FROM pg_class WHERE relname='addresses'")
@ -188,6 +188,30 @@ defmodule Explorer.Chain do
|> Repo.all()
end
@doc """
The average time it took to mine/validate the last <= 100 `t:Explorer.Chain.Block.t/0`
"""
@spec average_block_time :: %Timex.Duration{}
def average_block_time do
{:ok, %Postgrex.Result{rows: [[rows]]}} =
SQL.query(
Repo,
"""
SELECT coalesce(avg(difference), interval '0 seconds')
FROM (
SELECT b.timestamp - lag(b.timestamp) over (order by b.timestamp) as difference
FROM (SELECT * FROM blocks ORDER BY number DESC LIMIT 101) b
LIMIT 100 OFFSET 1
) t
""",
[]
)
{:ok, value} = Timex.Ecto.Time.load(rows)
value
end
@doc """
The `t:Explorer.Chain.Address.t/0` `balance` in `unit`.
"""
@ -627,23 +651,6 @@ defmodule Explorer.Chain do
Import.all(options)
end
@doc """
The number of `t:Explorer.Chain.Address.t/0`.
iex> insert_list(2, :address)
iex> Explorer.Chain.address_count()
2
When there are no `t:Explorer.Chain.Address.t/0`, the count is `0`.
iex> Explorer.Chain.address_count()
0
"""
def address_count do
Repo.aggregate(Address, :count, :hash)
end
@doc """
The number of `t:Explorer.Chain.InternalTransaction.t/0`.

@ -1,123 +0,0 @@
defmodule Explorer.Chain.Statistics do
@moduledoc """
Represents statistics about the chain.
"""
import Ecto.Query
alias Ecto.Adapters.SQL
alias Explorer.{Chain, PagingOptions, Repo}
alias Explorer.Chain.{Block, Transaction}
alias Timex.Duration
@average_time_query """
SELECT coalesce(avg(difference), interval '0 seconds')
FROM (
SELECT b.timestamp - lag(b.timestamp) over (order by b.timestamp) as difference
FROM (SELECT * FROM blocks ORDER BY number DESC LIMIT 101) b
LIMIT 100 OFFSET 1
) t
"""
@transaction_velocity_query """
SELECT count(transactions.inserted_at)
FROM transactions
WHERE transactions.inserted_at > NOW() - interval '1 minute'
"""
@typedoc """
The number of `t:Explorer.Chain.Block.t/0` mined/validated per minute.
"""
@type blocks_per_minute :: non_neg_integer()
@typedoc """
The number of `t:Explorer.Chain.Transaction.t/0` mined/validated per minute.
"""
@type transactions_per_minute :: non_neg_integer()
@typedoc """
* `average_time` - the average time it took to mine/validate the last <= 100 `t:Explorer.Chain.Block.t/0`
* `blocks` - the last <= 5 `t:Explorer.Chain.Block.t/0`
(`t:Explorer.Chain.Block.t/0` `timestamp`) and when it was inserted into the databasse
(`t:Explorer.Chain.Block.t/0` `inserted_at`)
* `number` - the latest `t:Explorer.Chain.Block.t/0` `number`
`t:Explorer.Chain.Block.t/0`
* `timestamp` - when the last `t:Explorer.Chain.Block.t/0` was mined/validated
* `transaction_velocity` - the number of `t:Explorer.Chain.Block.t/0` mined/validated in the last minute
* `transactions` - the last <= 5 `t:Explorer.Chain.Transaction.t/0`
"""
@type t :: %__MODULE__{
average_time: Duration.t(),
blocks: [Block.t()],
number: Block.block_number(),
timestamp: :calendar.datetime(),
transaction_velocity: transactions_per_minute(),
transactions: [Transaction.t()]
}
defstruct average_time: %Duration{seconds: 0, megaseconds: 0, microseconds: 0},
blocks: [],
number: -1,
timestamp: nil,
transaction_velocity: 0,
transactions: []
def fetch do
blocks =
from(
block in Block,
order_by: [desc: block.number],
preload: [:miner, :transactions],
limit: 4
)
transactions =
Chain.recent_collated_transactions(
necessity_by_association: %{
block: :required,
from_address: :required,
to_address: :optional
},
paging_options: %PagingOptions{page_size: 5}
)
%__MODULE__{
average_time: query_duration(@average_time_query),
blocks: Repo.all(blocks),
transaction_velocity: query_value(@transaction_velocity_query),
transactions: transactions
}
|> put_max_numbered_block()
end
defp put_max_numbered_block(state) do
case Chain.max_numbered_block() do
{:ok, %Block{number: number, timestamp: timestamp}} ->
%__MODULE__{
state
| number: number,
timestamp: timestamp
}
{:error, :not_found} ->
state
end
end
defp query_value(query, args \\ []) do
results = SQL.query!(Repo, query, args)
results.rows |> List.first() |> List.first()
end
defp query_duration(query) do
results = SQL.query!(Repo, query, [])
{:ok, value} =
results.rows
|> List.first()
|> List.first()
|> Timex.Ecto.Time.load()
value
end
end

@ -1,77 +0,0 @@
defmodule Explorer.Chain.Statistics.Server do
@moduledoc "Stores the latest chain statistics."
use GenServer
require Logger
alias Explorer.Chain.Statistics
@interval 1_000
defstruct statistics: %Statistics{},
task: nil
def child_spec(_) do
Supervisor.Spec.worker(__MODULE__, [[refresh: true]])
end
@spec fetch() :: Statistics.t()
def fetch do
GenServer.call(__MODULE__, :fetch)
end
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
@impl GenServer
def init(options) when is_list(options) do
if Keyword.get(options, :refresh, true) do
send(self(), :refresh)
end
{:ok, %__MODULE__{}}
end
@impl GenServer
def handle_info(:refresh, %__MODULE__{task: task} = state) do
new_state =
case task do
nil ->
%__MODULE__{state | task: Task.Supervisor.async_nolink(Explorer.TaskSupervisor, Statistics, :fetch, [])}
_ ->
state
end
{:noreply, new_state}
end
def handle_info({ref, %Statistics{} = statistics}, %__MODULE__{task: %Task{ref: ref}} = state) do
Process.demonitor(ref, [:flush])
Process.send_after(self(), :refresh, @interval)
{:noreply, %__MODULE__{state | statistics: statistics, task: nil}}
end
def handle_info({:DOWN, ref, :process, pid, reason}, %__MODULE__{task: %Task{pid: pid, ref: ref}} = state) do
Logger.error(fn -> "#{inspect(Statistics)}.fetch failed and could not be cached: #{inspect(reason)}" end)
Process.send_after(self(), :refresh, @interval)
{:noreply, %__MODULE__{state | task: nil}}
end
@impl GenServer
def handle_call(:fetch, _, %__MODULE__{statistics: %Statistics{} = statistics} = state),
do: {:reply, statistics, state}
@impl GenServer
def terminate(_, %__MODULE__{task: nil}), do: :ok
def terminate(_, %__MODULE__{task: task}) do
Task.shutdown(task)
end
end

@ -1,120 +0,0 @@
defmodule Explorer.Chain.Statistics.ServerTest do
use Explorer.DataCase, async: false
import ExUnit.CaptureLog
alias Explorer.Chain.Statistics
alias Explorer.Chain.Statistics.Server
# shutdown: "owner exited with: shutdown" error from polluting logs when tests are successful
@moduletag :capture_log
describe "child_spec/1" do
test "it defines a child_spec/1 that works with supervisors" do
insert(:block)
assert {:ok, pid} = start_supervised(Server)
%Server{task: %Task{pid: pid}} = :sys.get_state(pid)
ref = Process.monitor(pid)
assert_receive {:DOWN, ^ref, :process, ^pid, _}
end
end
describe "init/1" do
test "returns a new chain when not told to refresh" do
empty_statistics = %Statistics{}
assert {:ok, %Server{statistics: ^empty_statistics}} = Server.init(refresh: false)
end
test "returns a new Statistics when told to refresh" do
empty_statistics = %Statistics{}
assert {:ok, %Server{statistics: ^empty_statistics}} = Server.init(refresh: true)
end
test "refreshes when told to refresh" do
{:ok, _} = Server.init([])
assert_receive :refresh, 2_000
end
end
describe "handle_info/2" do
setup :state
test "returns the original statistics when sent a :refresh message", %{
state: %Server{statistics: statistics} = state
} do
assert {:noreply, %Server{statistics: ^statistics, task: task}} = Server.handle_info(:refresh, state)
Task.await(task)
end
test "launches a Statistics.fetch Task update when sent a :refresh message", %{state: state} do
assert {:noreply, %Server{task: %Task{} = task}} = Server.handle_info(:refresh, state)
assert %Statistics{} = Task.await(task)
end
test "stores successful Task in state", %{state: state} do
# so that `statistics` from Task will be different
insert(:block)
assert {:noreply, %Server{task: %Task{ref: ref}} = refresh_state} = Server.handle_info(:refresh, state)
assert_receive {^ref, %Statistics{} = message_statistics} = message
assert {:noreply, %Server{statistics: ^message_statistics, task: nil}} =
Server.handle_info(message, refresh_state)
refute message_statistics == state.statistics
end
test "logs crashed Task", %{state: state} do
assert {:noreply, %Server{task: %Task{pid: pid, ref: ref}} = refresh_state} = Server.handle_info(:refresh, state)
Process.exit(pid, :boom)
assert_receive {:DOWN, ^ref, :process, ^pid, :boom} = message
captured_log =
capture_log(fn ->
assert {:noreply, %Server{task: nil}} = Server.handle_info(message, refresh_state)
end)
assert captured_log =~ "Explorer.Chain.Statistics.fetch failed and could not be cached: :boom"
end
end
describe "handle_call/3" do
test "replies with statistics when sent a :fetch message" do
original = Statistics.fetch()
state = %Server{statistics: original}
assert {:reply, ^original, ^state} = Server.handle_call(:fetch, self(), state)
end
end
describe "terminate/2" do
setup :state
test "cleans up in-progress tasks when terminated", %{state: state} do
assert {:noreply, %Server{task: %Task{pid: pid}} = refresh_state} = Server.handle_info(:refresh, state)
second_ref = Process.monitor(pid)
Server.terminate(:boom, refresh_state)
assert_receive {:DOWN, ^second_ref, :process, ^pid, :shutdown}
end
end
defp state(_) do
{:ok, state} = Server.init([])
%{state: state}
end
end

@ -1,76 +0,0 @@
defmodule Explorer.Chain.StatisticsTest do
use Explorer.DataCase
alias Explorer.Chain.Statistics
alias Timex.Duration
describe "fetch/0" do
test "returns -1 for the number when there are no blocks" do
assert %Statistics{number: -1} = Statistics.fetch()
end
test "returns the highest block number when there is a block" do
insert(:block, number: 1)
max_number = 100
insert(:block, number: max_number)
assert %Statistics{number: ^max_number} = Statistics.fetch()
end
test "returns the latest block timestamp" do
time = DateTime.utc_now()
insert(:block, timestamp: time)
statistics = Statistics.fetch()
assert Timex.diff(statistics.timestamp, time, :seconds) == 0
end
test "returns the average time between blocks for the last 100 blocks" do
time = DateTime.utc_now()
insert(:block, timestamp: Timex.shift(time, seconds: -1000))
for x <- 100..0 do
insert(:block, timestamp: Timex.shift(time, seconds: -5 * x))
end
assert %Statistics{
average_time: %Duration{
seconds: 5,
megaseconds: 0,
microseconds: 0
}
} = Statistics.fetch()
end
test "returns the number of transactions inserted in the last minute" do
old_inserted_at = Timex.shift(DateTime.utc_now(), days: -1)
insert(:transaction, inserted_at: old_inserted_at)
insert(:transaction)
assert %Statistics{transaction_velocity: 1} = Statistics.fetch()
end
test "returns the last five blocks" do
insert_list(5, :block)
statistics = Statistics.fetch()
assert statistics.blocks |> Enum.count() == 4
end
test "returns the last five transactions with blocks" do
Enum.map(0..5, fn _ ->
:transaction
|> insert()
|> with_block()
end)
statistics = Statistics.fetch()
assert statistics.transactions |> Enum.count() == 5
end
end
end

@ -7,7 +7,7 @@ test('RECEIVED_NEW_BLOCK', () => {
const action = {
type: 'RECEIVED_NEW_BLOCK',
msg: {
homepageBlockHtml: 'new block'
chainBlockHtml: 'new block'
}
}
const output = reducer(state, action)

@ -3,7 +3,7 @@
100% {opacity: 1;}
}
@keyframes fade-up-blocks-homepage {
@keyframes fade-up-blocks-chain {
0% {
flex-basis: 0%;
width: 0%;
@ -45,10 +45,10 @@
animation: fade-in 1s ease-out forwards;
}
.fade-up-blocks-homepage {
.fade-up-blocks-chain {
will-change: transform, opacity, width;
max-height: 98px;
animation: fade-up-blocks-homepage 0.6s cubic-bezier(0.455, 0.03, 0.515, 0.955);
animation: fade-up-blocks-chain 0.6s cubic-bezier(0.455, 0.03, 0.515, 0.955);
}
.fade-up {

@ -10,6 +10,8 @@ import { batchChannel, initRedux } from '../utils'
const BATCH_THRESHOLD = 10
export const initialState = {
addressCount: null,
averageBlockTime: null,
batchCountAccumulator: 0,
newBlock: null,
newTransactions: [],
@ -23,9 +25,15 @@ export function reducer (state = initialState, action) {
transactionCount: numeral(action.transactionCount).value()
})
}
case 'RECEIVED_NEW_ADDRESS_COUNT': {
return Object.assign({}, state, {
addressCount: action.msg.count
})
}
case 'RECEIVED_NEW_BLOCK': {
return Object.assign({}, state, {
newBlock: action.msg.homepageBlockHtml
averageBlockTime: action.msg.averageBlockTime,
newBlock: action.msg.chainBlockHtml
})
}
case 'RECEIVED_NEW_TRANSACTION_BATCH': {
@ -51,8 +59,12 @@ export function reducer (state = initialState, action) {
router.when('', { exactPathMatch: true }).then(({ locale }) => initRedux(reducer, {
main (store) {
const blocksChannel = socket.channel(`blocks:new_block`)
numeral.locale(locale)
const addressesChannel = socket.channel(`addresses:new_address`)
addressesChannel.join()
addressesChannel.on('count', msg => store.dispatch({ type: 'RECEIVED_NEW_ADDRESS_COUNT', msg: humps.camelizeKeys(msg) }))
const blocksChannel = socket.channel(`blocks:new_block`)
store.dispatch({
type: 'PAGE_LOAD',
transactionCount: $('[data-selector="transaction-count"]').text()
@ -67,12 +79,20 @@ router.when('', { exactPathMatch: true }).then(({ locale }) => initRedux(reducer
)
},
render (state, oldState) {
const $addressCount = $('[data-selector="address-count"]')
const $averageBlockTime = $('[data-selector="average-block-time"]')
const $blockList = $('[data-selector="chain-block-list"]')
const $channelBatching = $('[data-selector="channel-batching-message"]')
const $channelBatchingCount = $('[data-selector="channel-batching-count"]')
const $transactionsList = $('[data-selector="transactions-list"]')
const $transactionCount = $('[data-selector="transaction-count"]')
if (oldState.addressCount !== state.addressCount) {
$addressCount.empty().append(state.addressCount)
}
if (oldState.averageBlockTime !== state.averageBlockTime) {
$averageBlockTime.empty().append(state.averageBlockTime)
}
if (oldState.newBlock !== state.newBlock) {
$blockList.children().last().remove()
$blockList.prepend(state.newBlock)

@ -11,3 +11,5 @@ config :explorer_web, ExplorerWeb.Endpoint,
# Configure wallaby
config :wallaby, screenshot_on_failure: true
config :explorer_web, :fake_adapter, ExplorerWeb.FakeAdapter

@ -7,50 +7,58 @@ defmodule ExplorerWeb.AddressChannel do
alias ExplorerWeb.{AddressTransactionView, AddressView}
alias Phoenix.View
intercept(["balance_update", "transaction"])
intercept(["balance_update", "count", "transaction"])
def join("addresses:" <> _address_hash, _params, socket) do
{:ok, %{}, socket}
end
def handle_out("transaction", %{address: address, transaction: transaction}, socket) do
def handle_out(
"balance_update",
%{address: address, exchange_rate: exchange_rate},
socket
) do
Gettext.put_locale(ExplorerWeb.Gettext, socket.assigns.locale)
rendered =
View.render_to_string(
AddressTransactionView,
"_transaction.html",
AddressView,
"_balance_card.html",
locale: socket.assigns.locale,
address: address,
transaction: transaction
exchange_rate: exchange_rate
)
push(socket, "transaction", %{
to_address_hash: to_string(transaction.to_address_hash),
from_address_hash: to_string(transaction.from_address_hash),
transaction_html: rendered
})
push(socket, "balance", %{balance: rendered})
{:noreply, socket}
end
def handle_out("count", %{count: count}, socket) do
Gettext.put_locale(ExplorerWeb.Gettext, socket.assigns.locale)
push(socket, "count", %{count: Cldr.Number.to_string!(count, format: "#,###")})
{:noreply, socket}
end
def handle_out(
"balance_update",
%{address: address, exchange_rate: exchange_rate},
socket
) do
def handle_out("transaction", %{address: address, transaction: transaction}, socket) do
Gettext.put_locale(ExplorerWeb.Gettext, socket.assigns.locale)
rendered =
View.render_to_string(
AddressView,
"_balance_card.html",
AddressTransactionView,
"_transaction.html",
locale: socket.assigns.locale,
address: address,
exchange_rate: exchange_rate
transaction: transaction
)
push(socket, "balance", %{balance: rendered})
push(socket, "transaction", %{
to_address_hash: to_string(transaction.to_address_hash),
from_address_hash: to_string(transaction.from_address_hash),
transaction_html: rendered
})
{:noreply, socket}
end
end

@ -13,7 +13,7 @@ defmodule ExplorerWeb.BlockChannel do
{:ok, %{}, socket}
end
def handle_out("new_block", %{block: block}, socket) do
def handle_out("new_block", %{block: block, average_block_time: average_block_time}, socket) do
Gettext.put_locale(ExplorerWeb.Gettext, socket.assigns.locale)
rendered_block =
@ -24,7 +24,7 @@ defmodule ExplorerWeb.BlockChannel do
block: block
)
rendered_homepage_block =
rendered_chain_block =
View.render_to_string(
ChainView,
"_block.html",
@ -33,7 +33,8 @@ defmodule ExplorerWeb.BlockChannel do
)
push(socket, "new_block", %{
homepage_block_html: rendered_homepage_block,
average_block_time: Timex.format_duration(average_block_time, :humanized),
chain_block_html: rendered_chain_block,
block_html: rendered_block,
blockNumber: block.number
})

@ -1,30 +1,48 @@
defmodule ExplorerWeb.ChainController do
use ExplorerWeb, :controller
alias Explorer.Chain.{Address, Block, Statistics, Transaction}
alias Explorer.{Chain, PagingOptions, Repo}
alias Explorer.Chain.{Address, Block, Transaction}
alias Explorer.ExchangeRates.Token
alias Explorer.Market
alias ExplorerWeb.Chain
def show(conn, _params) do
transaction_estimated_count = Explorer.Chain.transaction_estimated_count()
address_estimated_count = Explorer.Chain.address_estimated_count()
address_count_module = Application.get_env(:explorer_web, :fake_adapter) || Chain
blocks =
[paging_options: %PagingOptions{page_size: 4}]
|> Chain.list_blocks()
|> Repo.preload([:miner, :transactions])
transaction_estimated_count = Chain.transaction_estimated_count()
transactions =
Chain.recent_collated_transactions(
necessity_by_association: %{
block: :required,
from_address: :required,
to_address: :optional
},
paging_options: %PagingOptions{page_size: 5}
)
render(
conn,
"show.html",
address_estimated_count: address_estimated_count,
chain: Statistics.fetch(),
address_estimated_count: address_count_module.address_estimated_count(),
average_block_time: Chain.average_block_time(),
blocks: blocks,
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
market_history_data: Market.fetch_recent_history(30),
transaction_estimated_count: transaction_estimated_count
transaction_estimated_count: transaction_estimated_count,
transactions: transactions
)
end
def search(conn, %{"q" => query}) do
query
|> String.trim()
|> Chain.from_param()
|> ExplorerWeb.Chain.from_param()
|> case do
{:ok, item} ->
redirect_search_results(conn, item)

@ -9,6 +9,9 @@ defmodule ExplorerWeb.Notifier do
alias ExplorerWeb.Endpoint
def handle_event({:chain_event, :addresses, addresses}) do
address_count_module = Application.get_env(:explorer_web, :fake_adapter) || Chain
Endpoint.broadcast("addresses:new_address", "count", %{count: address_count_module.address_estimated_count()})
addresses
|> Stream.reject(fn %Address{fetched_balance: fetched_balance} -> is_nil(fetched_balance) end)
|> Enum.each(&broadcast_balance/1)
@ -40,7 +43,11 @@ defmodule ExplorerWeb.Notifier do
defp broadcast_block(block) do
preloaded_block = Repo.preload(block, [:miner, :transactions])
Endpoint.broadcast("blocks:new_block", "new_block", %{block: preloaded_block})
Endpoint.broadcast("blocks:new_block", "new_block", %{
block: preloaded_block,
average_block_time: Chain.average_block_time()
})
end
defp broadcast_transaction(transaction) do

@ -1,4 +1,4 @@
<div class="col-sm-3 fade-up-blocks-homepage mb-3 mb-sm-0" data-selector="chain-block" data-block-number="<%= @block.number %>">
<div class="col-sm-3 fade-up-blocks-chain mb-3 mb-sm-0" data-selector="chain-block" data-block-number="<%= @block.number %>">
<div class="tile d-flex flex-column">
<%= link(@block, to: block_path(ExplorerWeb.Endpoint, :show, @locale, @block), class: "tile-title") %>
<div>

@ -27,8 +27,8 @@
<span class="dashboard-banner-network-stats-label">
<%= gettext "Average block time" %>
</span>
<span class="dashboard-banner-network-stats-value">
<%= @chain.average_time |> Timex.format_duration(:humanized) %>
<span class="dashboard-banner-network-stats-value" data-selector="average-block-time">
<%= Timex.format_duration(@average_block_time, :humanized) %>
</span>
</div>
<div class="dashboard-banner-network-stats-item">
@ -43,8 +43,8 @@
<span class="dashboard-banner-network-stats-label">
<%= gettext "Wallet addresses" %>
</span>
<span class="dashboard-banner-network-stats-value">
<%= @address_estimated_count |> Cldr.Number.to_string!(format: "#,###") %>
<span class="dashboard-banner-network-stats-value" data-selector="address-count">
<%= Cldr.Number.to_string!(@address_estimated_count, format: "#,###") %>
</span>
</div>
</div>
@ -57,7 +57,7 @@
<%= link(gettext("View All Blocks →"), to: block_path(ExplorerWeb.Endpoint, :index, Gettext.get_locale), class: "button button--secondary button--xsmall float-right") %>
<h2 class="card-title"><%= gettext "Blocks" %></h2>
<div class="row" data-selector="chain-block-list">
<%= for block <- @chain.blocks do %>
<%= for block <- @blocks do %>
<%= render ExplorerWeb.ChainView, "_block.html", locale: @locale, block: block %>
<% end %>
</div>
@ -74,7 +74,7 @@
<%= link(gettext("View All Transactions →"), to: transaction_path(ExplorerWeb.Endpoint, :index, Gettext.get_locale), class: "button button--secondary button--xsmall float-right") %>
<h2 class="card-title"><%= gettext "Transactions" %></h2>
<span data-selector="transactions-list">
<%= for transaction <- @chain.transactions do %>
<%= for transaction <- @transactions do %>
<%= render ExplorerWeb.TransactionView, "_tile.html", locale: @locale, transaction: transaction %>
<% end %>
</span>

@ -22,7 +22,7 @@ defmodule ExplorerWeb.ChainControllerTest do
insert(:block, %{number: 23})
conn = get(conn, "/en")
assert(List.first(conn.assigns.chain.blocks).number == 23)
assert(List.first(conn.assigns.blocks).number == 23)
end
test "excludes all but the most recent five blocks", %{conn: conn} do
@ -30,7 +30,7 @@ defmodule ExplorerWeb.ChainControllerTest do
insert_list(5, :block)
conn = get(conn, "/en")
refute(Enum.member?(conn.assigns.chain.blocks, old_block))
refute(Enum.member?(conn.assigns.blocks, old_block))
end
test "only returns transactions with an associated block", %{conn: conn} do
@ -43,7 +43,7 @@ defmodule ExplorerWeb.ChainControllerTest do
conn = get(conn, "/en")
transaction_hashes = Enum.map(conn.assigns.chain.transactions, fn transaction -> transaction.hash end)
transaction_hashes = Enum.map(conn.assigns.transactions, fn transaction -> transaction.hash end)
assert(Enum.member?(transaction_hashes, associated.hash))
refute(Enum.member?(transaction_hashes, unassociated.hash))
@ -57,7 +57,7 @@ defmodule ExplorerWeb.ChainControllerTest do
conn = get(conn, "/en")
assert(List.first(conn.assigns.chain.transactions).hash == transaction.hash)
assert(List.first(conn.assigns.transactions).hash == transaction.hash)
end
test "returns market history data", %{conn: conn} do

@ -1,11 +1,19 @@
defmodule ExplorerWeb.HomePage do
defmodule ExplorerWeb.ChainPage do
@moduledoc false
use Wallaby.DSL
import Wallaby.Query, only: [css: 1, css: 2]
alias Explorer.Chain.{Block, InternalTransaction, Transaction}
alias Explorer.Chain.{Block, Transaction}
def address_count(count) do
css("[data-selector='address-count']", text: Integer.to_string(count))
end
def average_block_time(average_block_time) do
css("[data-selector='average-block-time']", text: average_block_time)
end
def block(%Block{number: number}) do
css("[data-selector='chain-block'][data-block-number='#{number}']")
@ -15,10 +23,14 @@ defmodule ExplorerWeb.HomePage do
css("[data-selector='chain-block']", count: count)
end
def contract_creation(%InternalTransaction{created_contract_address_hash: hash}) do
def contract_creation(%Transaction{created_contract_address_hash: hash}) do
css("[data-test='contract-creation'] [data-address-hash='#{hash}']")
end
def non_loaded_transaction_count(count) do
css("[data-selector='channel-batching-count']", text: count)
end
def search(session, text) do
session
|> fill_in(css("[data-test='search_input']"), with: text)

@ -3,7 +3,7 @@ defmodule ExplorerWeb.ViewingAddressesTest do
alias Explorer.Chain
alias Explorer.Chain.{Address, Wei}
alias ExplorerWeb.{AddressPage, HomePage, Notifier}
alias ExplorerWeb.{AddressPage, Notifier}
setup do
block = insert(:block)
@ -30,15 +30,6 @@ defmodule ExplorerWeb.ViewingAddressesTest do
}}
end
test "search for address", %{session: session} do
address = insert(:address)
session
|> HomePage.visit_page()
|> HomePage.search(to_string(address.hash))
|> assert_has(AddressPage.detail_hash(address))
end
test "viewing address overview information", %{session: session} do
address = insert(:address, fetched_balance: 500)

@ -1,7 +1,7 @@
defmodule ExplorerWeb.ViewingBlocksTest do
use ExplorerWeb.FeatureCase, async: true
alias ExplorerWeb.{BlockListPage, BlockPage, HomePage, Notifier}
alias ExplorerWeb.{BlockListPage, BlockPage, Notifier}
setup do
timestamp = Timex.now() |> Timex.shift(hours: -1)
@ -20,36 +20,6 @@ defmodule ExplorerWeb.ViewingBlocksTest do
{:ok, first_shown_block: newest_block, last_shown_block: oldest_block}
end
test "viewing blocks on the home page", %{session: session} do
session
|> HomePage.visit_page()
|> assert_has(HomePage.blocks(count: 4))
end
test "viewing new blocks via live update on homepage", %{session: session, last_shown_block: last_shown_block} do
session
|> HomePage.visit_page()
|> assert_has(HomePage.blocks(count: 4))
block = insert(:block, number: 42)
Notifier.handle_event({:chain_event, :blocks, [block]})
session
|> assert_has(HomePage.blocks(count: 4))
|> assert_has(HomePage.block(block))
|> refute_has(HomePage.block(last_shown_block))
end
test "search for blocks from home page", %{session: session} do
block = insert(:block, number: 42)
session
|> HomePage.visit_page()
|> HomePage.search(to_string(block.number))
|> assert_has(BlockPage.detail_number(block))
end
test "show block detail page", %{session: session} do
block = insert(:block, number: 42)

@ -0,0 +1,184 @@
defmodule ExplorerWeb.ViewingChainTest do
@moduledoc false
use ExplorerWeb.FeatureCase, async: true
alias ExplorerWeb.{AddressPage, BlockPage, ChainPage, Notifier, TransactionPage}
setup do
[oldest_block | _] = Enum.map(401..404, &insert(:block, number: &1))
block = insert(:block, number: 405)
[oldest_transaction | _] =
4
|> insert_list(:transaction)
|> with_block(block)
:transaction
|> insert()
|> with_block(block)
{:ok,
%{
block: block,
last_shown_block: oldest_block,
last_shown_transaction: oldest_transaction
}}
end
describe "statistics" do
test "average block time live updates", %{session: session} do
time = DateTime.utc_now()
for x <- 100..0 do
insert(:block, timestamp: Timex.shift(time, seconds: -5 * x), number: x + 500)
end
session
|> ChainPage.visit_page()
|> assert_has(ChainPage.average_block_time("5 seconds"))
block =
100..0
|> Enum.map(fn index ->
insert(:block, timestamp: Timex.shift(time, seconds: -10 * index), number: index + 800)
end)
|> hd()
Notifier.handle_event({:chain_event, :blocks, [block]})
assert_has(session, ChainPage.average_block_time("10 seconds"))
end
test "address count live updates", %{session: session} do
count = ExplorerWeb.FakeAdapter.address_estimated_count()
session
|> ChainPage.visit_page()
|> assert_has(ChainPage.address_count(count))
address = insert(:address)
Notifier.handle_event({:chain_event, :addresses, [address]})
assert_has(session, ChainPage.address_count(count + 1))
end
end
describe "viewing addresses" do
test "search for address", %{session: session} do
address = insert(:address)
session
|> ChainPage.visit_page()
|> ChainPage.search(to_string(address.hash))
|> assert_has(AddressPage.detail_hash(address))
end
end
describe "viewing blocks" do
test "search for blocks from chain page", %{session: session} do
block = insert(:block, number: 6)
session
|> ChainPage.visit_page()
|> ChainPage.search(to_string(block.number))
|> assert_has(BlockPage.detail_number(block))
end
test "blocks list", %{session: session} do
session
|> ChainPage.visit_page()
|> assert_has(ChainPage.blocks(count: 4))
end
test "viewing new blocks via live update", %{session: session, last_shown_block: last_shown_block} do
session
|> ChainPage.visit_page()
|> assert_has(ChainPage.blocks(count: 4))
block = insert(:block, number: 6)
Notifier.handle_event({:chain_event, :blocks, [block]})
session
|> assert_has(ChainPage.blocks(count: 4))
|> assert_has(ChainPage.block(block))
|> refute_has(ChainPage.block(last_shown_block))
end
end
describe "viewing transactions" do
test "search for transactions", %{session: session} do
transaction = insert(:transaction)
session
|> ChainPage.visit_page()
|> ChainPage.search(to_string(transaction.hash))
|> assert_has(TransactionPage.detail_hash(transaction))
end
test "transactions list", %{session: session} do
session
|> ChainPage.visit_page()
|> assert_has(ChainPage.transactions(count: 5))
end
test "viewing new transactions via live update", %{
session: session,
block: block,
last_shown_transaction: last_shown_transaction
} do
session
|> ChainPage.visit_page()
|> assert_has(ChainPage.transactions(count: 5))
transaction =
:transaction
|> insert()
|> with_block(block)
Notifier.handle_event({:chain_event, :transactions, [transaction.hash]})
session
|> assert_has(ChainPage.transactions(count: 5))
|> assert_has(ChainPage.transaction(transaction))
|> refute_has(ChainPage.transaction(last_shown_transaction))
end
test "count of non-loaded transactions live update when batch overflow", %{session: session, block: block} do
transaction_hashes =
30
|> insert_list(:transaction)
|> with_block(block)
|> Enum.map(& &1.hash)
session
|> ChainPage.visit_page()
|> assert_has(ChainPage.transactions(count: 5))
Notifier.handle_event({:chain_event, :transactions, transaction_hashes})
assert_has(session, ChainPage.non_loaded_transaction_count("30"))
end
test "contract creation is shown for to_address", %{session: session, block: block} do
contract_address = insert(:contract_address)
transaction =
:transaction
|> insert(to_address: nil)
|> with_contract_creation(contract_address)
|> with_block(block)
# internal_transaction =
# :internal_transaction_create
# |> insert(transaction: transaction, index: 0)
# |> with_contract_creation(contract_address)
session
|> ChainPage.visit_page()
|> assert_has(ChainPage.contract_creation(transaction))
end
end
end

@ -4,7 +4,7 @@ defmodule ExplorerWeb.ViewingTransactionsTest do
use ExplorerWeb.FeatureCase, async: true
alias Explorer.Chain.Wei
alias ExplorerWeb.{AddressPage, HomePage, Notifier, TransactionListPage, TransactionLogsPage, TransactionPage}
alias ExplorerWeb.{AddressPage, Notifier, TransactionListPage, TransactionLogsPage, TransactionPage}
setup do
block =
@ -63,78 +63,7 @@ defmodule ExplorerWeb.ViewingTransactionsTest do
}}
end
test "search for transactions", %{session: session} do
transaction = insert(:transaction, input: "0x736f636b73")
session
|> HomePage.visit_page()
|> HomePage.search(to_string(transaction.hash))
|> assert_has(TransactionPage.detail_hash(transaction))
end
describe "viewing transaction lists" do
test "transactions on the homepage", %{session: session} do
session
|> HomePage.visit_page()
|> assert_has(HomePage.transactions(count: 5))
end
test "viewing new transactions via live update on the homepage", %{
session: session,
last_shown_transaction: last_shown_transaction
} do
session
|> HomePage.visit_page()
|> assert_has(HomePage.transactions(count: 5))
transaction =
:transaction
|> insert()
|> with_block()
Notifier.handle_event({:chain_event, :transactions, [transaction.hash]})
session
|> assert_has(HomePage.transactions(count: 5))
|> assert_has(HomePage.transaction(transaction))
|> refute_has(HomePage.transaction(last_shown_transaction))
end
test "count of non-loaded transactions on homepage live update when batch overflow", %{session: session} do
transaction_hashes =
30
|> insert_list(:transaction)
|> with_block()
|> Enum.map(& &1.hash)
session
|> HomePage.visit_page()
|> assert_has(HomePage.transactions(count: 5))
Notifier.handle_event({:chain_event, :transactions, transaction_hashes})
assert_has(session, AddressPage.non_loaded_transaction_count("30"))
end
test "contract creation is shown for to_address on home page", %{session: session} do
contract_address = insert(:contract_address)
transaction =
:transaction
|> insert(to_address: nil)
|> with_contract_creation(contract_address)
|> with_block()
internal_transaction =
:internal_transaction_create
|> insert(transaction: transaction, index: 0)
|> with_contract_creation(contract_address)
session
|> HomePage.visit_page()
|> assert_has(HomePage.contract_creation(internal_transaction))
end
test "viewing the default transactions tab", %{
session: session,
first_shown_transaction: transaction,

@ -0,0 +1,8 @@
defmodule ExplorerWeb.FakeAdapter do
alias Explorer.Chain.Address
alias Explorer.Repo
def address_estimated_count do
Repo.aggregate(Address, :count, :hash)
end
end
Loading…
Cancel
Save