Render lists of validators and staking pools (#2215)

staking
goodsoft 5 years ago committed by Victor Baranov
parent 76e61891e9
commit 26715dcf00
  1. 13
      apps/block_scout_web/assets/css/components/_card.scss
  2. 35
      apps/block_scout_web/assets/css/components/_stakes_table.scss
  3. 7
      apps/block_scout_web/assets/css/components/stakes/_stakes.scss
  4. 3
      apps/block_scout_web/config/config.exs
  5. 5
      apps/block_scout_web/lib/block_scout_web/chain.ex
  6. 88
      apps/block_scout_web/lib/block_scout_web/controllers/stakes_controller.ex
  7. 27
      apps/block_scout_web/lib/block_scout_web/templates/layout/_topnav.html.eex
  8. 40
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_rows.html.eex
  9. 8
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_address.html.eex
  10. 16
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_empty_content.html.eex
  11. 23
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_tabs.html.eex
  12. 6
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_th.html.eex
  13. 17
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_title.html.eex
  14. 38
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_table.html.eex
  15. 42
      apps/block_scout_web/lib/block_scout_web/templates/stakes/index.html.eex
  16. 40
      apps/block_scout_web/lib/block_scout_web/views/stakes_helpers.ex
  17. 4
      apps/block_scout_web/lib/block_scout_web/views/stakes_view.ex
  18. 4
      apps/block_scout_web/lib/block_scout_web/web_router.ex
  19. 49
      apps/block_scout_web/test/block_scout_web/controllers/stakes_controller_test.exs
  20. 24
      apps/block_scout_web/test/block_scout_web/views/stakes_helpers_test.exs
  21. 28
      apps/explorer/lib/explorer/chain.ex

@ -114,6 +114,19 @@ $card-tab-icon-color-active: #fff !default;
} }
} }
.card-title-paging {
padding: 0px $card-horizontal-padding;
display: flex;
justify-content: flex-end;
}
.card-footer-paging {
padding: 0px $card-horizontal-padding;
padding-bottom: 25px;
display: flex;
justify-content: flex-end;
}
.card-title-controls { .card-title-controls {
align-items: center; align-items: center;
display: flex; display: flex;

@ -123,9 +123,42 @@ $stakes-table-cell-separation: 25px !default;
} }
.stakes-td-link-style { .stakes-td-link-style {
color: $primary; color: $secondary;
.stakes-tr-banned & { .stakes-tr-banned & {
color: $stakes-banned-color; color: $stakes-banned-color;
} }
} }
.stakes-tr-banned td:last-child {
text-align: right;
padding-right: 30px;
}
.stakes-table {
.check-tooltip {
&:hover {
.check-tooltip-circle {
fill: $secondary;
}
}
}
.stakes-tr-banned {
.check-tooltip {
.check-tooltip-circle {
fill: rgba($stakes-banned-color, .15);
}
.check-tooltip-check {
fill: $stakes-banned-color;
}
&:hover {
.check-tooltip-circle {
fill: $stakes-banned-color;
}
.check-tooltip-check {
fill: #fff;
}
}
}
}
}

@ -138,3 +138,10 @@ $stakes-stats-item-border-color: #fff !default;
justify-self: center; justify-self: center;
} }
} }
.staking-pg-container {
padding: 0 30px 30px;
&.at-bottom {
padding-top: 30px;
}
}

@ -22,7 +22,8 @@ config :block_scout_web, BlockScoutWeb.Chain,
logo: System.get_env("LOGO"), logo: System.get_env("LOGO"),
logo_footer: System.get_env("LOGO_FOOTER"), logo_footer: System.get_env("LOGO_FOOTER"),
logo_text: System.get_env("LOGO_TEXT"), logo_text: System.get_env("LOGO_TEXT"),
has_emission_funds: false has_emission_funds: false,
staking_enabled: not is_nil(System.get_env("POS_STAKING_CONTRACT"))
config :block_scout_web, config :block_scout_web,
link_to_other_explorers: System.get_env("LINK_TO_OTHER_EXPLORERS") == "true", link_to_other_explorers: System.get_env("LINK_TO_OTHER_EXPLORERS") == "true",

@ -27,6 +27,7 @@ defmodule BlockScoutWeb.Chain do
Token, Token,
TokenTransfer, TokenTransfer,
Transaction, Transaction,
StakingPool,
Wei Wei
} }
@ -282,6 +283,10 @@ defmodule BlockScoutWeb.Chain do
%{"block_number" => block_number} %{"block_number" => block_number}
end end
defp paging_params(%StakingPool{staking_address_hash: address_hash, staked_ratio: value}) do
%{"address_hash" => address_hash, "value" => Decimal.to_string(value)}
end
defp block_or_transaction_from_param(param) do defp block_or_transaction_from_param(param) do
with {:error, :not_found} <- transaction_from_param(param) do with {:error, :not_found} <- transaction_from_param(param) do
hash_string_to_block(param) hash_string_to_block(param)

@ -0,0 +1,88 @@
defmodule BlockScoutWeb.StakesController do
use BlockScoutWeb, :controller
alias BlockScoutWeb.StakesView
alias Explorer.Chain
alias Explorer.Chain.Token
alias Explorer.Counters.AverageBlockTime
alias Explorer.Staking.ContractState
alias Phoenix.View
import BlockScoutWeb.Chain, only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1]
def index(%{assigns: assigns} = conn, params) do
render_template(assigns.filter, conn, params)
end
defp render_template(filter, conn, %{"type" => "JSON"} = params) do
[paging_options: options] = paging_options(params)
last_index =
params
|> Map.get("position", "0")
|> String.to_integer()
pools_plus_one = Chain.staking_pools(filter, options)
{pools, next_page} = split_list_by_page(pools_plus_one)
next_page_path =
case next_page_params(next_page, pools, params) do
nil ->
nil
next_page_params ->
updated_page_params =
next_page_params
|> Map.delete("type")
|> Map.put("position", last_index + 1)
next_page_path(filter, conn, updated_page_params)
end
average_block_time = AverageBlockTime.average_block_time()
token = ContractState.get(:token, %Token{})
items =
pools
|> Enum.with_index(last_index + 1)
|> Enum.map(fn {pool, index} ->
View.render_to_string(
StakesView,
"_rows.html",
token: token,
pool: pool,
index: index,
average_block_time: average_block_time,
pools_type: filter
)
end)
json(
conn,
%{
items: items,
next_page_path: next_page_path
}
)
end
defp render_template(filter, conn, _) do
render(conn, "index.html",
pools_type: filter,
current_path: current_path(conn)
)
end
defp next_page_path(:validator, conn, params) do
validators_path(conn, :index, params)
end
defp next_page_path(:active, conn, params) do
active_pools_path(conn, :index, params)
end
defp next_page_path(:inactive, conn, params) do
inactive_pools_path(conn, :index, params)
end
end

@ -156,6 +156,33 @@
</div> </div>
</li> </li>
<% end %> <% end %>
<%= if Application.get_env(:block_scout_web, BlockScoutWeb.Chain)[:staking_enabled] do %>
<li class="nav-item dropdown">
<a href="#" role="button" id="stakesDropdown" class="nav-link topnav-nav-link dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="nav-link-icon">
<%= render BlockScoutWeb.IconsView, "_apps_icon.html" %>
</span>
<%= gettext("Stakes") %>
</a>
<div class="dropdown-menu" aria-labeledby="navbarStakesDropdown">
<%= link(
gettext("Validators"),
class: "dropdown-item #{tab_status("validators", @conn.request_path)}",
to: validators_path(@conn, :index)
) %>
<%= link(
gettext("Active Pools"),
class: "dropdown-item #{tab_status("active-pools", @conn.request_path)}",
to: active_pools_path(@conn, :index)
) %>
<%= link(
gettext("Inactive Pools"),
class: "dropdown-item #{tab_status("inactive-pools", @conn.request_path)}",
to: inactive_pools_path(@conn, :index)
) %>
</div>
</li>
<% end %>
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-link topnav-nav-link active-icon <%= if dropdown_nets() != [], do: "dropdown-toggle js-show-network-selector" %>" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <a class="nav-link topnav-nav-link active-icon <%= if dropdown_nets() != [], do: "dropdown-toggle js-show-network-selector" %>" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="nav-link-icon"> <span class="nav-link-icon">

@ -0,0 +1,40 @@
<tr class=<%= if @pool.is_banned, do: "stakes-tr-banned" %>>
<td class="stakes-td"><div class="stakes-td-order"><%= @index %></div></td>
<td class="stakes-td">
<%=
tooltip = if @pool.is_validator, do: "This is a validator", else: false
render BlockScoutWeb.StakesView,
"_stakes_address.html",
address: @pool.staking_address_hash,
tooltip: tooltip,
index: @index
%>
</td>
<td class="stakes-td">
<%=
render BlockScoutWeb.CommonComponentsView,
"_progress_from_to.html",
from: format_according_to_decimals(@pool.self_staked_amount, @token.decimals),
to: format_according_to_decimals(@pool.staked_amount, @token.decimals),
progress: amount_ratio(@pool)
%>
</td>
<td class="stakes-td">
<%= if @pools_type == :inactive do %>
<%= if @pool.is_banned, do: "Yes", else: "No" %>
<% else %>
<%= if @pool.staked_ratio, do: "#{@pool.staked_ratio}%" %>
<% end %>
</td>
<td class="stakes-td"><span class="stakes-td-link-style"><%= @pool.delegators_count %></span></td>
<td class="stakes-td">
<%= if @pool.is_banned do %>
<span class="stakes-td-banned-info">
Banned until block #<%= @pool.banned_until %> (<%= estimated_unban_day(@pool.banned_until, @average_block_time) %>)
</span>
<% else %>
<div class="stakes-controls">
</div>
<% end %>
</td>
</tr>

@ -0,0 +1,8 @@
<div class="stakes-address-container">
<span class="stakes-address js-validator-info-modal">
<%= binary_part(to_string(@address), 0, 13) %>
</span>
<%= if @tooltip do %>
<%= render BlockScoutWeb.CommonComponentsView, "_check_tooltip.html", text: @tooltip %>
<% end %>
</div>

@ -0,0 +1,16 @@
<div class="stakes-empty-content">
<div class="stakes-empty-content-pic">
<svg class="stakes-empty-content-pic-svg" xmlns="http://www.w3.org/2000/svg" width="94" height="121">
<path class="stakes-empty-content-pic-svg-path" fill-rule="evenodd" d="M40 1.47l48 27.759c3.314 1.916 6 6.156 6 9.47v57.999c0 3.314-2.686 4.447-6 2.531L40 71.47c-3.314-1.916-6-6.156-6-9.47V4c0-3.314 2.686-4.446 6-2.53z" opacity=".2"/>
<path class="stakes-empty-content-pic-svg-path" fill-rule="evenodd" d="M23 11.47l48 27.759c3.314 1.916 6 6.156 6 9.47v58c0 3.313-2.686 4.446-6 2.53L23 81.47c-3.314-1.917-6-6.156-6-9.47V14c0-3.314 2.686-4.446 6-2.53z" opacity=".6"/>
<path class="stakes-empty-content-pic-svg-path" fill-rule="evenodd" d="M6 21.47l48 27.759c3.314 1.916 6 6.156 6 9.469v58.001c0 3.313-2.686 4.446-6 2.53L6 91.47C2.686 89.553 0 85.314 0 82V24c0-3.314 2.686-4.447 6-2.53z"/>
<path fill="#FFF" fill-rule="evenodd" d="M39 78.814l-9-5.18v11c0 1.104-.895 1.484-2 .849-1.105-.636-2-2.047-2-3.152v-11l-9-5.18c-1.105-.636-2-2.046-2-3.151s.895-1.485 2-.849l9 5.18v-11c0-1.104.895-1.484 2-.848 1.105.635 2 2.046 2 3.151v11l9 5.18c1.105.636 2 2.047 2 3.151 0 1.105-.895 1.485-2 .849z"/>
</svg>
</div>
<div class="stakes-empty-content-info">
<h1 class="stakes-empty-content-title">Lorem Ipsum</h1>
<p class="stakes-empty-content-text">Lorem ipsum dolor sit amet, consect adipiscing elit, sed do
eiusmod temp incididunt ut labore et dolore magna.</p>
<%= render BlockScoutWeb.CommonComponentsView, "_btn_add_line.html", text: gettext("Become Candidate"), extra_class: "js-become-candidate" %>
</div>
</div>

@ -0,0 +1,23 @@
<div class="card-tabs">
<%=
link(
gettext("Validators"),
class: "card-tab #{tab_status("validators", @conn.request_path)}",
to: validators_path(@conn, :index)
)
%>
<%=
link(
gettext("Active Pools"),
class: "card-tab #{tab_status("active-pools", @conn.request_path)}",
to: active_pools_path(@conn, :index)
)
%>
<%=
link(
gettext("Inactive Pools"),
class: "card-tab #{tab_status("inactive-pools", @conn.request_path)}",
to: inactive_pools_path(@conn, :index)
)
%>
</div>

@ -0,0 +1,6 @@
<th class="stakes-table-th">
<div class="stakes-table-th-content">
<span class="stakes-th-text"><%= @title %></span>
<%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip.html", text: @tooltip %>
</div>
</th>

@ -0,0 +1,17 @@
<div class="card-title-container">
<div class="card-title"><%= @title %></div>
<div class="card-title-controls">
<%= if @show_banned_checkbox do %>
<div class="check card-title-control">
<input type="checkbox" />
<div class="check-icon"></div>
<div class="check-text">Show banned only</div>
</div>
<% end %>
<div class="check card-title-control">
<input type="checkbox" />
<div class="check-icon"></div>
<div class="check-text">Show only those I staked into</div>
</div>
</div>
</div>

@ -0,0 +1,38 @@
<div class="stakes-table-container">
<table class="stakes-table">
<thead>
<tr>
<th class="stakes-table-th">&nbsp;</th>
<%= render BlockScoutWeb.StakesView, "_stakes_th.html", title: "Pool", tooltip: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut." %>
<%= render BlockScoutWeb.StakesView, "_stakes_th.html", title: "Staked Amount", tooltip: "Lorem ipsum dolor sit iusmod tempor incididunt ut." %>
<%= if @pools_type == :inactive do %>
<%= render BlockScoutWeb.StakesView, "_stakes_th.html", title: "Banned", tooltip: "Sed do eiusmod tempor incididunt ut." %>
<% else %>
<%= render BlockScoutWeb.StakesView, "_stakes_th.html", title: "Stakes Ratio", tooltip: "Sed do eiusmod tempor incididunt ut." %>
<% end %>
<%= render BlockScoutWeb.StakesView, "_stakes_th.html", title: "Delegators", tooltip: "Lorem ipsum dolor sit amet." %>
<th class="stakes-table-th">&nbsp;</th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="6">
<button data-error-message class="alert alert-danger col-12 text-left" style="display: none;">
<span href="#" class="alert-link"><%= gettext("Something went wrong, click to reload.") %></span>
</button>
</td>
</tr>
</tbody>
<tbody data-empty-response-message style="display: none;">
<tr>
<td colspan="6">
<%= render BlockScoutWeb.StakesView, "_stakes_empty_content.html" %>
</td>
</tr>
</tbody>
<tbody data-items></tbody>
</table>
</div>

@ -0,0 +1,42 @@
<section data-page="stakes" class="container">
<div class="card" data-async-load data-async-listing="<%= @current_path %>">
<%= render BlockScoutWeb.StakesView, "_stakes_tabs.html", conn: @conn %>
<%=
render BlockScoutWeb.StakesView,
"_stakes_title.html",
title: list_title(@pools_type),
show_banned_checkbox: @pools_type == :inactive
%>
<div class="card-title-paging">
<%=
render BlockScoutWeb.CommonComponentsView,
"_pagination_container.html",
position: "top",
show_pagination_limit: true,
data_next_page_button: true,
data_prev_page_button: true
%>
</div>
<%=
render BlockScoutWeb.StakesView,
"_table.html",
pools_type: @pools_type
%>
<div class="card-footer-paging">
<%=
render BlockScoutWeb.CommonComponentsView,
"_pagination_container.html",
position: "bottom",
cur_page_number: "1",
show_pagination_limit: true,
data_next_page_button: true,
data_prev_page_button: true
%>
</div>
</div>
</section>

@ -0,0 +1,40 @@
defmodule BlockScoutWeb.StakesHelpers do
@moduledoc """
Helpers for staking templates
"""
alias Explorer.Chain.Cache.BlockNumber
alias Timex.Duration
def amount_ratio(pool) do
zero = Decimal.new(0)
case pool do
%{staked_amount: ^zero} ->
0
%{staked_amount: staked_amount, self_staked_amount: self_staked} ->
amount = Decimal.to_float(staked_amount)
self = Decimal.to_float(self_staked)
self / amount * 100
end
end
def estimated_unban_day(banned_until, average_block_time) do
block_time = Duration.to_seconds(average_block_time)
try do
during_sec = (banned_until - BlockNumber.get_max()) * block_time
now = DateTime.utc_now() |> DateTime.to_unix()
date = DateTime.from_unix!(trunc(now + during_sec))
Timex.format!(date, "%d %b %Y", :strftime)
rescue
_e ->
DateTime.utc_now()
|> Timex.format!("%d %b %Y", :strftime)
end
end
def list_title(:validator), do: "Validators"
def list_title(:active), do: "Active Pools"
def list_title(:inactive), do: "Inactive Pools"
end

@ -0,0 +1,4 @@
defmodule BlockScoutWeb.StakesView do
use BlockScoutWeb, :view
import BlockScoutWeb.StakesHelpers
end

@ -42,6 +42,10 @@ defmodule BlockScoutWeb.WebRouter do
get("/uncles", BlockController, :uncle, as: :uncle) get("/uncles", BlockController, :uncle, as: :uncle)
get("/validators", StakesController, :index, as: :validators, assigns: %{filter: :validator})
get("/active-pools", StakesController, :index, as: :active_pools, assigns: %{filter: :active})
get("/inactive-pools", StakesController, :index, as: :inactive_pools, assigns: %{filter: :inactive})
resources("/pending-transactions", PendingTransactionController, only: [:index]) resources("/pending-transactions", PendingTransactionController, only: [:index])
resources("/recent-transactions", RecentTransactionsController, only: [:index]) resources("/recent-transactions", RecentTransactionsController, only: [:index])

@ -0,0 +1,49 @@
defmodule BlockScoutWeb.StakesControllerTest do
use BlockScoutWeb.ConnCase
alias Explorer.Counters.AverageBlockTime
setup do
start_supervised!(AverageBlockTime)
Application.put_env(:explorer, AverageBlockTime, enabled: true)
on_exit(fn ->
Application.put_env(:explorer, AverageBlockTime, enabled: false)
end)
end
describe "GET validators/2" do
test "returns page", %{conn: conn} do
conn = get(conn, validators_path(conn, :index))
assert conn.status == 200
end
test "returns rendered table", %{conn: conn} do
address_hashes = Enum.map(1..4, fn _ -> insert(:staking_pool) end)
conn = get(conn, validators_path(conn, :index, %{type: "JSON"}))
assert {:ok, %{"items" => items, "next_page_path" => _}} = Poison.decode(conn.resp_body)
assert Enum.count(items) == Enum.count(address_hashes)
end
end
describe "GET active_pools/2" do
test "returns rendered table", %{conn: conn} do
address_hashes = Enum.map(1..4, fn _ -> insert(:staking_pool) end)
conn = get(conn, active_pools_path(conn, :index, %{type: "JSON"}))
assert {:ok, %{"items" => items, "next_page_path" => _}} = Poison.decode(conn.resp_body)
assert Enum.count(items) == Enum.count(address_hashes)
end
end
describe "GET inactive_pools/2" do
test "returns rendered table", %{conn: conn} do
address_hashes = Enum.map(1..4, fn _ -> insert(:staking_pool, is_active: false) end)
conn = get(conn, inactive_pools_path(conn, :index, %{type: "JSON"}))
assert {:ok, %{"items" => items, "next_page_path" => _}} = Poison.decode(conn.resp_body)
assert Enum.count(items) == Enum.count(address_hashes)
end
end
end

@ -0,0 +1,24 @@
defmodule BlockScoutWeb.StakesHelpersTest do
use ExUnit.Case
alias BlockScoutWeb.StakesHelpers
alias Timex.Duration
setup do
Application.put_env(:explorer, Explorer.Chain.Cache.BlockNumber, enabled: true)
on_exit(fn ->
Application.put_env(:explorer, Explorer.Chain.Cache.BlockNumber, enabled: false)
end)
end
test "estimated_unban_day/2" do
block_average = Duration.from_seconds(5)
unban_day = StakesHelpers.estimated_unban_day(10, block_average)
now = DateTime.utc_now() |> DateTime.to_unix()
date = DateTime.from_unix!(trunc(now + 5 * 10))
assert Timex.format!(date, "%d %b %Y", :strftime) == unban_day
end
end

@ -11,7 +11,6 @@ defmodule Explorer.Chain do
lock: 2, lock: 2,
order_by: 2, order_by: 2,
order_by: 3, order_by: 3,
offset: 2,
preload: 2, preload: 2,
select: 2, select: 2,
subquery: 1, subquery: 1,
@ -4715,14 +4714,33 @@ defmodule Explorer.Chain do
@doc "Get staking pools from the DB" @doc "Get staking pools from the DB"
@spec staking_pools(filter :: :validator | :active | :inactive, options :: PagingOptions.t()) :: [map()] @spec staking_pools(filter :: :validator | :active | :inactive, options :: PagingOptions.t()) :: [map()]
def staking_pools(filter, %PagingOptions{page_size: page_size, page_number: page_number} \\ @default_paging_options) do def staking_pools(filter, paging_options \\ @default_paging_options) do
off = page_size * (page_number - 1) filter
|> staking_pools_query(paging_options)
|> Repo.all()
end
defp staking_pools_query(filter, paging_options) do
page_size = paging_options.page_size
base_query =
StakingPool StakingPool
|> staking_pool_filter(filter) |> staking_pool_filter(filter)
|> limit(^page_size) |> limit(^page_size)
|> offset(^off) |> order_by(desc: :staked_ratio, asc: :staking_address_hash)
|> Repo.all()
case paging_options.key do
{value, address_hash} ->
where(
base_query,
[p],
p.staked_ratio < ^value or
(p.staked_ratio == ^value and p.staking_address_hash > ^address_hash)
)
_ ->
base_query
end
end end
@doc "Get count of staking pools from the DB" @doc "Get count of staking pools from the DB"

Loading…
Cancel
Save