Skip to content

Commit

Permalink
Merge pull request #3597 from poanetwork/va-staking-dapp-apy
Browse files Browse the repository at this point in the history
Show APY for delegators in Staking DApp
  • Loading branch information
vbaranov committed Feb 3, 2021
2 parents 3c92013 + edbf8b1 commit 082a928
Show file tree
Hide file tree
Showing 15 changed files with 535 additions and 102 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
## Current

### Features
- [#3603](https://github.com/poanetwork/blockscout/pull/3603) - Display method output parameter name at contract read page
- [#3603](https://github.com/poanetwork/blockscout/pull/3603) - Display method output parameter name at contract read page
- [#3597](https://github.com/poanetwork/blockscout/pull/3597) - Show APY for delegators in Staking DApp
- [#3584](https://github.com/poanetwork/blockscout/pull/3584) - Token holders API endpoint
- [#3564](https://github.com/poanetwork/blockscout/pull/3564) - Staking welcome message

Expand Down
10 changes: 6 additions & 4 deletions apps/block_scout_web/assets/js/pages/stakes.js
Original file line number Diff line number Diff line change
Expand Up @@ -352,12 +352,14 @@ async function getAccounts () {
}

async function getNetId (web3) {
let netId = null
if (!window.ethereum.chainId) {
let netId = window.ethereum.chainId
if (!netId) {
netId = await window.ethereum.request({ method: 'eth_chainId' })
}
if (!netId) {
console.error(`Cannot get chainId. ${constants.METAMASK_VERSION_WARNING}`)
} else {
const { chainId } = window.ethereum
netId = web3.utils.isHex(chainId) ? web3.utils.hexToNumber(chainId) : chainId
netId = web3.utils.isHex(netId) ? web3.utils.hexToNumber(netId) : netId
}
return netId
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ defmodule BlockScoutWeb.StakesChannel do
alias Explorer.Counters.AverageBlockTime
alias Explorer.Staking.{ContractReader, ContractState}
alias Phoenix.View
alias Timex.Duration

import BlockScoutWeb.Gettext

Expand Down Expand Up @@ -102,10 +103,27 @@ defmodule BlockScoutWeb.StakesChannel do
end

def handle_in("render_delegators_list", %{"address" => pool_staking_address}, socket) do
pool_staking_address_downcased = String.downcase(pool_staking_address)
pool = Chain.staking_pool(pool_staking_address)
pool_rewards = ContractState.get(:pool_rewards, %{})
calc_apy_enabled = ContractState.calc_apy_enabled?()
token = ContractState.get(:token)
validator_min_reward_percent = ContractState.get(:validator_min_reward_percent)
show_snapshotted_data = ContractState.show_snapshotted_data(pool.is_validator)
staking_epoch_duration = ContractState.staking_epoch_duration()

average_block_time =
try do
Duration.to_seconds(AverageBlockTime.average_block_time())
rescue
_ -> nil
end

pool_reward =
case Map.fetch(pool_rewards, String.downcase(to_string(pool.mining_address_hash))) do
{:ok, pool_reward} -> pool_reward
:error -> nil
end

stakers =
pool_staking_address
Expand All @@ -119,6 +137,21 @@ defmodule BlockScoutWeb.StakesChannel do
true -> 2
end
end)
|> Enum.map(fn staker ->
apy =
if calc_apy_enabled do
calc_apy(
pool,
staker,
pool_staking_address_downcased,
pool_reward,
average_block_time,
staking_epoch_duration
)
end

Map.put(staker, :apy, apy)
end)

html =
View.render_to_string(StakesView, "_stakes_modal_delegators_list.html",
Expand Down Expand Up @@ -658,6 +691,25 @@ defmodule BlockScoutWeb.StakesChannel do
end
end

defp calc_apy(pool, staker, pool_staking_address_downcased, pool_reward, average_block_time, staking_epoch_duration) do
staker_address = String.downcase(to_string(staker.address_hash))

{reward_ratio, stake_amount} =
if staker_address == pool_staking_address_downcased do
{pool.snapshotted_validator_reward_ratio, pool.snapshotted_self_staked_amount}
else
{staker.snapshotted_reward_ratio, staker.snapshotted_stake_amount}
end

ContractState.calc_apy(
reward_ratio,
pool_reward,
stake_amount,
average_block_time,
staking_epoch_duration
)
end

defp claim_reward_long_op_active(socket) do
if socket.assigns[@claim_reward_long_op] do
true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ defmodule BlockScoutWeb.StakesController do

alias BlockScoutWeb.StakesView
alias Explorer.Chain
alias Explorer.Chain.Cache.BlockNumber
alias Explorer.Chain.Token
alias Explorer.Chain.{Cache.BlockNumber, Hash, Token}
alias Explorer.Counters.AverageBlockTime
alias Explorer.Staking.ContractState
alias Phoenix.View
alias Timex.Duration

import BlockScoutWeb.Chain, only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1]

Expand Down Expand Up @@ -103,29 +103,47 @@ defmodule BlockScoutWeb.StakesController do
end

average_block_time = AverageBlockTime.average_block_time()

average_block_time_seconds =
try do
Duration.to_seconds(average_block_time)
rescue
_ -> nil
end

staking_epoch_duration = ContractState.staking_epoch_duration()
token = ContractState.get(:token, %Token{})
epoch_number = ContractState.get(:epoch_number, 0)
staking_allowed = ContractState.get(:staking_allowed, false)
pool_rewards = ContractState.get(:pool_rewards, %{})
calc_apy_enabled = ContractState.calc_apy_enabled?()
snapshotted_delegator_data = snapshotted_delegator_data(filter, calc_apy_enabled)

items =
pools
|> Enum.with_index(last_index + 1)
|> Enum.map(fn {%{pool: pool, delegator: delegator}, index} ->
apy =
if calc_apy_enabled and snapshotted_delegator_data != nil do
calc_apy(
pool,
pool_rewards,
snapshotted_delegator_data,
average_block_time_seconds,
staking_epoch_duration
)
end

View.render_to_string(
StakesView,
"_rows.html",
token: token,
pool: pool,
pool: Map.put(pool, :apy, apy),
delegator: delegator,
index: index,
average_block_time: average_block_time,
pools_type: filter,
buttons: %{
stake: staking_allowed and stake_allowed?(pool, delegator),
move: staking_allowed and move_allowed?(delegator),
withdraw: staking_allowed and withdraw_allowed?(delegator),
claim: staking_allowed and claim_allowed?(delegator, epoch_number)
}
buttons: staking_buttons(pool, delegator, staking_allowed, epoch_number)
)
end)

Expand Down Expand Up @@ -159,6 +177,58 @@ defmodule BlockScoutWeb.StakesController do
)
end

defp staking_buttons(pool, delegator, staking_allowed, epoch_number) do
%{
stake: staking_allowed and stake_allowed?(pool, delegator),
move: staking_allowed and move_allowed?(delegator),
withdraw: staking_allowed and withdraw_allowed?(delegator),
claim: staking_allowed and claim_allowed?(delegator, epoch_number)
}
end

defp calc_apy(pool, pool_rewards, snapshotted_delegator_data, average_block_time, staking_epoch_duration) do
staking_address_str = String.downcase(Hash.to_string(pool.staking_address_hash))
mining_address_str = String.downcase(Hash.to_string(pool.mining_address_hash))

pool_reward =
case Map.fetch(pool_rewards, mining_address_str) do
{:ok, pool_reward} -> pool_reward
:error -> nil
end

case Map.fetch(snapshotted_delegator_data, staking_address_str) do
{:ok, data} ->
ContractState.calc_apy(
data.snapshotted_reward_ratio,
pool_reward,
data.snapshotted_stake_amount,
average_block_time,
staking_epoch_duration
)

:error ->
ContractState.calc_apy(
pool.snapshotted_validator_reward_ratio,
pool_reward,
pool.snapshotted_self_staked_amount,
average_block_time,
staking_epoch_duration
)
end
end

defp snapshotted_delegator_data(filter, calc_apy_enabled) do
if filter == :validator and calc_apy_enabled do
Chain.staking_pool_snapshotted_delegator_data_for_apy()
|> Enum.reduce(%{}, fn item, acc ->
staking_address_str = address_bytes_to_string(item.staking_address_hash)
Map.put(acc, staking_address_str, item)
end)
end
end

defp address_bytes_to_string(hash), do: "0x" <> Base.encode16(hash, case: :lower)

defp next_page_path(:validator, conn, params) do
validators_path(conn, :index, params)
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,21 @@
<%= if @pool.is_active, do: "#{@pool.stakes_ratio}%", else: gettext("(inactive pool)") %>
<% end %>
</div>
<%= if @pools_type == :validator do %>
<div class="col-1 stakes-td stakes-cell">
<%= if @pool.apy do %>
<%= @pool.apy.apy %>
<% else %>
<%= gettext("N/A") %>
<% end %>
</div>
<% end %>
<div class="col-2 stakes-td stakes-cell">
<span class="stakes-td-link-style js-delegators-list" data-address="<%= @pool.staking_address_hash %>">
<%= @pool.delegators_count %>
</span>
</div>
<div class="col-3 stakes-td stakes-cell justify-content-end">
<div class="<%= if @pools_type == :validator do %>col-2<% else %>col-3<% end %> stakes-td stakes-cell justify-content-end">
<%= if @pool.is_banned do %>
<span class="stakes-td-banned-info">
<%= gettext("Banned until block #%{banned_until} (%{estimated_unban_day})", banned_until: @pool.banned_until, estimated_unban_day: estimated_unban_day(@pool.banned_until, @average_block_time)) %>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<div class="stakes-table-container">
<div class="stakes-table-head">
<div class="col-1"></div>
<div class="col-4">
<div class="<%= if @pool.is_validator, do: "col-2", else: "col-4" %>">
<%=
pool_type = cond do
@pool.is_validator -> gettext("validator")
Expand All @@ -25,7 +25,7 @@
)
%>
</div>
<div class="col-4">
<div class="<%= if @pool.is_validator, do: "col-3", else: "col-4" %>">
<%=
title =
if @show_snapshotted_data do
Expand Down Expand Up @@ -68,12 +68,22 @@
tooltip: tooltip
%>
</div>
<%= if @pool.is_validator do %>
<div class="col-3">
<%=
render BlockScoutWeb.StakesView,
"_stakes_th.html",
title: gettext("APY & Predicted Reward"),
tooltip: gettext("Approximate Current Annual Percentage Yield. If you see N/A, please reopen the popup in a few blocks (APY cannot be calculated at the very beginning of a staking epoch). Predicted Reward is the amount of %{symbol} a participant will receive for staking and can claim once the current epoch ends.", symbol: @token.symbol)
%>
</div>
<% end %>
</div>
<div class="stakes-table-body">
<%= for {staker, index} <- Enum.with_index(@stakers, 1) do %>
<div class="row">
<div class="col-1 stakes-td stakes-cell"><div class="stakes-td-order"><%= index %></div></div>
<div class="col-4 stakes-td stakes-cell">
<div class="<%= if @pool.is_validator, do: "col-2", else: "col-4" %> stakes-td stakes-cell">
<div class="stakes-address-container">
<span class="stakes-address">
<%=
Expand Down Expand Up @@ -116,7 +126,7 @@
<% end %>
</div>
</div>
<div class="col-4 stakes-td stakes-cell">
<div class="<%= if @pool.is_validator, do: "col-3", else: "col-4" %> stakes-td stakes-cell">
<%= format_token_amount(staker.stake_amount, @token, symbol: false) %>
<%= if @show_snapshotted_data do %>
(
Expand Down Expand Up @@ -162,6 +172,18 @@
-
<% end %>
</div>
<%= if @pool.is_validator do %>
<div class="col-3 stakes-td stakes-cell">
<%= cond do %>
<% staker.apy -> %>
<%= staker.apy.apy %> (<%= format_token_amount(staker.apy.predicted_reward, @token, symbol: false, digits: 2) %>)
<% @show_snapshotted_data and staker.snapshotted_stake_amount == nil -> %>
<%= gettext("Pending") %>
<% true -> %>
<%= gettext("N/A") %>
<% end %>
</div>
<% end %>
</div>
<% end %>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
<div class="card-tabs js-card-tabs">
<%=
link(
gettext("Validators"),
list_title(:validator),
class: "card-tab #{tab_status("validators", @conn.request_path)}",
to: validators_path(@conn, :index)
)
%>
<%=
link(
gettext("Active Pools"),
list_title(:active),
class: "card-tab #{tab_status("active-pools", @conn.request_path)}",
to: active_pools_path(@conn, :index)
)
%>
<%=
link(
gettext("Inactive Pools"),
list_title(:inactive),
class: "card-tab #{tab_status("inactive-pools", @conn.request_path)}",
to: inactive_pools_path(@conn, :index)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,15 @@
<%= render BlockScoutWeb.StakesView, "_stakes_th.html", title: gettext("Stakes Ratio"), tooltip: gettext("The percentage of stake in a single pool relative to the total amount staked in all active pools. A higher ratio results in a greater likelihood of validator pool selection.") %>
<% end %>
</div>
<%= if @pools_type == :validator do %>
<div class="col-1">
<%= render BlockScoutWeb.StakesView, "_stakes_th.html", title: gettext("APY"), tooltip: gettext("Approximate Current Annual Percentage Yield. If you see N/A, please wait for a few blocks (APY cannot be calculated at the very beginning of a staking epoch).") %>
</div>
<% end %>
<div class="col-2">
<%= render BlockScoutWeb.StakesView, "_stakes_th.html", title: gettext("Delegators"), tooltip: gettext("The number of delegators providing stake to the pool. Click on the number to see more details.") %>
</div>
<div class="col-3"></div>
<div class="<%= if @pools_type == :validator do %>col-2<% else %>col-3<% end %>"></div>
</div>
<div class="stakes-table-body">
<button data-error-message class="alert alert-danger col-12 text-left" style="display: none;">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ defmodule BlockScoutWeb.StakesHelpers do
end

def list_title(:validator), do: Gettext.dgettext(BlockScoutWeb.Gettext, "default", "Validators")
def list_title(:active), do: Gettext.dgettext(BlockScoutWeb.Gettext, "default", "Active Pools")
def list_title(:active), do: Gettext.dgettext(BlockScoutWeb.Gettext, "default", "Active Pools (Candidates)")
def list_title(:inactive), do: Gettext.dgettext(BlockScoutWeb.Gettext, "default", "Inactive Pools")

def from_wei(%Decimal{} = amount, %Token{} = token, to_string \\ true) do
Expand Down
Loading

0 comments on commit 082a928

Please sign in to comment.