Render lists of validators and staking pools (#2215)
parent
76e61891e9
commit
26715dcf00
@ -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 |
@ -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"> </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"> </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 |
@ -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 |
Loading…
Reference in new issue