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