Fix tests: mix format and mix credo

staking
Victor Baranov 5 years ago
parent 930ffbf6e4
commit 415ffa4208
  1. 3
      apps/block_scout_web/config/config.exs
  2. 455
      apps/block_scout_web/lib/block_scout_web/channels/stakes_channel.ex
  3. 132
      apps/block_scout_web/lib/block_scout_web/controllers/stakes_controller.ex
  4. 39
      apps/explorer/lib/explorer/chain.ex
  5. 6
      apps/explorer/lib/explorer/chain/import.ex
  6. 68
      apps/explorer/lib/explorer/chain/import/runner/staking_pools.ex
  7. 34
      apps/explorer/lib/explorer/chain/import/runner/staking_pools_delegators.ex
  8. 116
      apps/explorer/lib/explorer/staking/contract_reader.ex
  9. 741
      apps/explorer/lib/explorer/staking/contract_state.ex
  10. 43
      apps/explorer/lib/explorer/staking/stake_snapshotting.ex
  11. 4
      apps/explorer/priv/repo/migrations/20190807111216_remove_duplicate_indexes.exs
  12. 2
      apps/explorer/priv/repo/migrations/20191128144250_add_index_for_snapshotted_stake_amount.exs

@ -24,7 +24,8 @@ config :block_scout_web, BlockScoutWeb.Chain,
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")), staking_enabled: not is_nil(System.get_env("POS_STAKING_CONTRACT")),
staking_pool_list_refresh_interval: 5 # how often (in blocks) the list of pools should autorefresh in UI (zero turns off autorefreshing) # how often (in blocks) the list of pools should autorefresh in UI (zero turns off autorefreshing)
staking_pool_list_refresh_interval: 5
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",

@ -28,6 +28,7 @@ defmodule BlockScoutWeb.StakesChannel do
# apps/block_scout_web/lib/block_scout_web/endpoint.ex # apps/block_scout_web/lib/block_scout_web/endpoint.ex
def terminate(_reason, socket) do def terminate(_reason, socket) do
s = socket.assigns[@claim_reward_long_op] s = socket.assigns[@claim_reward_long_op]
if s != nil do if s != nil do
:ets.delete(ContractState, claim_reward_long_op_key(s.staker)) :ets.delete(ContractState, claim_reward_long_op_key(s.staker))
end end
@ -251,55 +252,60 @@ defmodule BlockScoutWeb.StakesChannel do
def handle_in("render_claim_reward", data, socket) do def handle_in("render_claim_reward", data, socket) do
staker = socket.assigns[:account] staker = socket.assigns[:account]
staking_contract_address = try do ContractState.get(:staking_contract).address after end
cond do
claim_reward_long_op_active(socket) == true ->
{:reply, {:error, %{reason: gettext("Pools searching is already in progress for this address")}}, socket}
staker == nil || staker == "" || staker == "0x0000000000000000000000000000000000000000" ->
{:reply, {:error, %{reason: gettext("Unknown staker address. Please, choose your account in MetaMask")}}, socket}
staking_contract_address == nil || staking_contract_address == "" || staking_contract_address == "0x0000000000000000000000000000000000000000" ->
{:reply, {:error, %{reason: gettext("Unknown address of Staking contract. Please, contact support")}}, socket}
true ->
result = if data["preload"] do
%{
html: View.render_to_string(StakesView, "_stakes_modal_claim_reward.html", %{}),
socket: socket
}
else
task = Task.async(__MODULE__, :find_claim_reward_pools, [socket, staker, staking_contract_address])
%{
html: "OK",
socket: assign(socket, @claim_reward_long_op, %{task: task, staker: staker})
}
end
{:reply, {:ok, %{html: result.html}}, result.socket} staking_contract_address =
end try do
ContractState.get(:staking_contract).address
after
end
empty_staker = staker == nil || staker == "" || staker == "0x0000000000000000000000000000000000000000"
empty_staking_contract_address =
staking_contract_address == nil || staking_contract_address == "" ||
staking_contract_address == "0x0000000000000000000000000000000000000000"
handle_in_render_claim_reward_result(
socket,
data,
staker,
staking_contract_address,
empty_staker,
empty_staking_contract_address
)
end end
def handle_in("recalc_claim_reward", data, socket) do def handle_in("recalc_claim_reward", data, socket) do
epochs = data["epochs"] epochs = data["epochs"]
pool_staking_address = data["pool_staking_address"] pool_staking_address = data["pool_staking_address"]
staker = socket.assigns[:account] staker = socket.assigns[:account]
staking_contract_address = try do ContractState.get(:staking_contract).address after end
staking_contract_address =
cond do try do
claim_reward_long_op_active(socket) == true -> ContractState.get(:staking_contract).address
{:reply, {:error, %{reason: gettext("Reward calculating is already in progress for this address")}}, socket} after
Enum.count(epochs) == 0 -> end
{:reply, {:error, %{reason: gettext("Staking epochs are not specified or not in the allowed range")}}, socket}
pool_staking_address == nil || pool_staking_address == "" || pool_staking_address == "0x0000000000000000000000000000000000000000" -> empty_pool_staking_address =
{:reply, {:error, %{reason: gettext("Unknown pool staking address. Please, contact support")}}, socket} pool_staking_address == nil || pool_staking_address == "" ||
staker == nil || staker == "" || staker == "0x0000000000000000000000000000000000000000" -> pool_staking_address == "0x0000000000000000000000000000000000000000"
{:reply, {:error, %{reason: gettext("Unknown staker address. Please, choose your account in MetaMask")}}, socket}
staking_contract_address == nil || staking_contract_address == "" || staking_contract_address == "0x0000000000000000000000000000000000000000" -> empty_staker = staker == nil || staker == "" || staker == "0x0000000000000000000000000000000000000000"
{:reply, {:error, %{reason: gettext("Unknown address of Staking contract. Please, contact support")}}, socket}
true -> empty_staking_contract_address =
task = Task.async(__MODULE__, :recalc_claim_reward, [socket, staking_contract_address, epochs, pool_staking_address, staker]) staking_contract_address == nil || staking_contract_address == "" ||
socket = assign(socket, @claim_reward_long_op, %{task: task, staker: staker}) staking_contract_address == "0x0000000000000000000000000000000000000000"
{:reply, {:ok, %{html: "OK"}}, socket}
end handle_in_recalc_claim_reward_result(
socket,
epochs,
staking_contract_address,
pool_staking_address,
staker,
empty_pool_staking_address,
empty_staking_contract_address,
empty_staker
)
end end
def handle_in("render_claim_withdrawal", %{"address" => staking_address}, socket) do def handle_in("render_claim_withdrawal", %{"address" => staking_address}, socket) do
@ -325,12 +331,15 @@ defmodule BlockScoutWeb.StakesChannel do
def handle_info({:DOWN, ref, :process, pid, _reason}, socket) do def handle_info({:DOWN, ref, :process, pid, _reason}, socket) do
s = socket.assigns[@claim_reward_long_op] s = socket.assigns[@claim_reward_long_op]
socket = if s && s.task.ref == ref && s.task.pid == pid do
:ets.delete(ContractState, claim_reward_long_op_key(s.staker)) socket =
assign(socket, @claim_reward_long_op, nil) if s && s.task.ref == ref && s.task.pid == pid do
else :ets.delete(ContractState, claim_reward_long_op_key(s.staker))
socket assign(socket, @claim_reward_long_op, nil)
end else
socket
end
{:noreply, socket} {:noreply, socket}
end end
@ -339,10 +348,11 @@ defmodule BlockScoutWeb.StakesChannel do
end end
def handle_out("staking_update", data, socket) do def handle_out("staking_update", data, socket) do
dont_refresh_page = case Map.fetch(data, :dont_refresh_page) do dont_refresh_page =
{:ok, value} -> value case Map.fetch(data, :dont_refresh_page) do
_ -> false {:ok, value} -> value
end _ -> false
end
push(socket, "staking_update", %{ push(socket, "staking_update", %{
account: socket.assigns[:account], account: socket.assigns[:account],
@ -360,93 +370,138 @@ defmodule BlockScoutWeb.StakesChannel do
def find_claim_reward_pools(socket, staker, staking_contract_address) do def find_claim_reward_pools(socket, staker, staking_contract_address) do
:ets.insert(ContractState, {claim_reward_long_op_key(staker), true}) :ets.insert(ContractState, {claim_reward_long_op_key(staker), true})
try do try do
json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments) json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments)
staking_contract = ContractState.get(:staking_contract) staking_contract = ContractState.get(:staking_contract)
responses = responses =
ContractReader.get_staker_pools_length_request(staker) staker
|> ContractReader.get_staker_pools_length_request()
|> ContractReader.perform_requests(%{staking: staking_contract.address}, staking_contract.abi) |> ContractReader.perform_requests(%{staking: staking_contract.address}, staking_contract.abi)
staker_pools_length = responses[:length] staker_pools_length = responses[:length]
chunk_size = 100 chunk_size = 100
pools = if staker_pools_length > 0 do
chunks = 0..trunc(ceil(staker_pools_length / chunk_size) - 1)
Enum.reduce(chunks, [], fn i, acc ->
responses =
ContractReader.get_staker_pools_request(staker, i * chunk_size, chunk_size)
|> ContractReader.perform_requests(%{staking: staking_contract.address}, staking_contract.abi)
acc ++ Enum.map(responses[:pools], fn pool_staking_address ->
address_bytes_to_string(pool_staking_address)
end)
end)
else
[]
end
pools_amounts = Enum.map(pools, fn pool_staking_address -> pools =
ContractReader.call_get_reward_amount( if staker_pools_length > 0 do
staking_contract_address, chunks = 0..trunc(ceil(staker_pools_length / chunk_size) - 1)
[],
pool_staking_address,
staker,
json_rpc_named_arguments
)
end)
error = Enum.find_value(pools_amounts, fn result -> Enum.reduce(chunks, [], fn i, acc ->
case result do
{:error, reason} -> error_reason_to_string(reason)
_ -> nil
end
end)
{error, pools} = if error != nil do
{error, %{}}
else
block_reward_contract = ContractState.get(:block_reward_contract)
pools =
pools_amounts
|> Enum.map(fn {_, amounts} -> amounts end)
|> Enum.zip(pools)
|> Enum.filter(fn {amounts, _} -> amounts.token_reward_sum > 0 || amounts.native_reward_sum > 0 end)
|> Enum.map(fn {amounts, pool_staking_address} ->
responses = responses =
ContractReader.epochs_to_claim_reward_from_request(pool_staking_address, staker) staker
|> ContractReader.perform_requests(%{block_reward: block_reward_contract.address}, block_reward_contract.abi) |> ContractReader.get_staker_pools_request(i * chunk_size, chunk_size)
|> ContractReader.perform_requests(%{staking: staking_contract.address}, staking_contract.abi)
epochs = acc ++
array_to_ranges(responses[:epochs]) Enum.map(responses[:pools], fn pool_staking_address ->
|> Enum.map(fn {first, last} -> address_bytes_to_string(pool_staking_address)
Integer.to_string(first) <> (if first != last, do: "-" <> Integer.to_string(last), else: "")
end) end)
data = Map.put(amounts, :epochs, Enum.join(epochs, ","))
{data, pool_staking_address}
end) end)
|> Enum.filter(fn {data, _} -> data.epochs != "" end) else
[]
end
pools_gas_estimates = Enum.map(pools, fn {_data, pool_staking_address} -> pools_amounts =
result = ContractReader.claim_reward_estimate_gas( Enum.map(pools, fn pool_staking_address ->
ContractReader.call_get_reward_amount(
staking_contract_address, staking_contract_address,
[], [],
pool_staking_address, pool_staking_address,
staker, staker,
json_rpc_named_arguments json_rpc_named_arguments
) )
end)
error =
Enum.find_value(pools_amounts, fn result ->
case result do
{:error, reason} -> error_reason_to_string(reason)
_ -> nil
end
end)
{error, pools} =
get_pools(pools_amounts, pools, staking_contract_address, staker, json_rpc_named_arguments, error)
html =
View.render_to_string(
StakesView,
"_stakes_modal_claim_reward_content.html",
coin: %Token{symbol: Explorer.coin(), decimals: Decimal.new(18)},
error: error,
pools: pools,
token: ContractState.get(:token)
)
push(socket, "claim_reward_pools", %{
html: html
})
after
:ets.delete(ContractState, claim_reward_long_op_key(staker))
end
end
def get_pools(pools_amounts, pools, staking_contract_address, staker, json_rpc_named_arguments, error) do
if error != nil do
{error, %{}}
else
block_reward_contract = ContractState.get(:block_reward_contract)
pools =
pools_amounts
|> Enum.map(fn {_, amounts} -> amounts end)
|> Enum.zip(pools)
|> Enum.filter(fn {amounts, _} -> amounts.token_reward_sum > 0 || amounts.native_reward_sum > 0 end)
|> Enum.map(fn {amounts, pool_staking_address} ->
responses =
pool_staking_address
|> ContractReader.epochs_to_claim_reward_from_request(staker)
|> ContractReader.perform_requests(
%{block_reward: block_reward_contract.address},
block_reward_contract.abi
)
epochs =
responses[:epochs]
|> array_to_ranges()
|> Enum.map(fn {first, last} ->
Integer.to_string(first) <> if first != last, do: "-" <> Integer.to_string(last), else: ""
end)
data = Map.put(amounts, :epochs, Enum.join(epochs, ","))
{data, pool_staking_address}
end)
|> Enum.filter(fn {data, _} -> data.epochs != "" end)
pools_gas_estimates =
Enum.map(pools, fn {_data, pool_staking_address} ->
result =
ContractReader.claim_reward_estimate_gas(
staking_contract_address,
[],
pool_staking_address,
staker,
json_rpc_named_arguments
)
{pool_staking_address, result} {pool_staking_address, result}
end) end)
error = Enum.find_value(pools_gas_estimates, fn {_, result} -> error =
Enum.find_value(pools_gas_estimates, fn {_, result} ->
case result do case result do
{:error, reason} -> error_reason_to_string(reason) {:error, reason} -> error_reason_to_string(reason)
_ -> nil _ -> nil
end end
end) end)
pools = if error == nil do pools =
if error == nil do
pools_gas_estimates = Map.new(pools_gas_estimates) pools_gas_estimates = Map.new(pools_gas_estimates)
Map.new(pools, fn {data, pool_staking_address} -> Map.new(pools, fn {data, pool_staking_address} ->
{:ok, estimate} = pools_gas_estimates[pool_staking_address] {:ok, estimate} = pools_gas_estimates[pool_staking_address]
data = Map.put(data, :gas_estimate, estimate) data = Map.put(data, :gas_estimate, estimate)
@ -456,48 +511,18 @@ defmodule BlockScoutWeb.StakesChannel do
%{} %{}
end end
{error, pools} {error, pools}
end
html = View.render_to_string(
StakesView,
"_stakes_modal_claim_reward_content.html",
coin: %Token{symbol: Explorer.coin(), decimals: Decimal.new(18)},
error: error,
pools: pools,
token: ContractState.get(:token)
)
push(socket, "claim_reward_pools", %{
html: html
})
after
:ets.delete(ContractState, claim_reward_long_op_key(staker))
end end
end end
def recalc_claim_reward(socket, staking_contract_address, epochs, pool_staking_address, staker) do def recalc_claim_reward(socket, staking_contract_address, epochs, pool_staking_address, staker) do
:ets.insert(ContractState, {claim_reward_long_op_key(staker), true}) :ets.insert(ContractState, {claim_reward_long_op_key(staker), true})
try do try do
json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments) json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments)
amounts_result = ContractReader.call_get_reward_amount( amounts_result =
staking_contract_address, ContractReader.call_get_reward_amount(
epochs,
pool_staking_address,
staker,
json_rpc_named_arguments
)
{error, amounts} = case amounts_result do
{:ok, amounts} ->
{nil, amounts}
{:error, reason} ->
{error_reason_to_string(reason), %{token_reward_sum: 0, native_reward_sum: 0}}
end
{error, gas_limit} = if error == nil do
estimate_gas_result = ContractReader.claim_reward_estimate_gas(
staking_contract_address, staking_contract_address,
epochs, epochs,
pool_staking_address, pool_staking_address,
@ -505,22 +530,53 @@ defmodule BlockScoutWeb.StakesChannel do
json_rpc_named_arguments json_rpc_named_arguments
) )
case estimate_gas_result do {error, amounts} =
{:ok, gas_limit} -> case amounts_result do
{nil, gas_limit} {:ok, amounts} ->
{nil, amounts}
{:error, reason} -> {:error, reason} ->
{error_reason_to_string(reason), 0} {error_reason_to_string(reason), %{token_reward_sum: 0, native_reward_sum: 0}}
end
{error, gas_limit} =
if error == nil do
estimate_gas_result =
ContractReader.claim_reward_estimate_gas(
staking_contract_address,
epochs,
pool_staking_address,
staker,
json_rpc_named_arguments
)
case estimate_gas_result do
{:ok, gas_limit} ->
{nil, gas_limit}
{:error, reason} ->
{error_reason_to_string(reason), 0}
end
else
{error, 0}
end end
else
{error, 0}
end
token = ContractState.get(:token) token = ContractState.get(:token)
coin = %Token{symbol: Explorer.coin(), decimals: Decimal.new(18)} coin = %Token{symbol: Explorer.coin(), decimals: Decimal.new(18)}
push(socket, "claim_reward_recalculations", %{ push(socket, "claim_reward_recalculations", %{
token_reward_sum: StakesHelpers.format_token_amount(amounts.token_reward_sum, token, digits: token.decimals, ellipsize: false, symbol: false), token_reward_sum:
native_reward_sum: StakesHelpers.format_token_amount(amounts.native_reward_sum, coin, digits: coin.decimals, ellipsize: false, symbol: false), StakesHelpers.format_token_amount(amounts.token_reward_sum, token,
digits: token.decimals,
ellipsize: false,
symbol: false
),
native_reward_sum:
StakesHelpers.format_token_amount(amounts.native_reward_sum, coin,
digits: coin.decimals,
ellipsize: false,
symbol: false
),
gas_limit: gas_limit, gas_limit: gas_limit,
error: error error: error
}) })
@ -534,6 +590,7 @@ defmodule BlockScoutWeb.StakesChannel do
true true
else else
staker = socket.assigns[:account] staker = socket.assigns[:account]
with [{_, true}] <- :ets.lookup(ContractState, claim_reward_long_op_key(staker)) do with [{_, true}] <- :ets.lookup(ContractState, claim_reward_long_op_key(staker)) do
true true
end end
@ -544,13 +601,20 @@ defmodule BlockScoutWeb.StakesChannel do
defp array_to_ranges(numbers, prev_ranges \\ []) do defp array_to_ranges(numbers, prev_ranges \\ []) do
length = Enum.count(numbers) length = Enum.count(numbers)
if length > 0 do if length > 0 do
{first, last, next_index} = get_range(numbers) {first, last, next_index} = get_range(numbers)
ranges = prev_ranges ++ [{first, last}] prev_ranges_reversed = Enum.reverse(prev_ranges)
ranges =
[{first, last} | prev_ranges_reversed]
|> Enum.reverse()
if next_index == 0 || next_index >= length do if next_index == 0 || next_index >= length do
ranges ranges
else else
Enum.slice(numbers, next_index, length - next_index) numbers
|> Enum.slice(next_index, length - next_index)
|> array_to_ranges(ranges) |> array_to_ranges(ranges)
end end
else else
@ -567,11 +631,13 @@ defmodule BlockScoutWeb.StakesChannel do
end end
defp get_range(numbers) do defp get_range(numbers) do
last_index = last_index =
Enum.with_index(numbers) numbers
|> Enum.with_index()
|> Enum.find_index(fn {n, i} -> |> Enum.find_index(fn {n, i} ->
if i > 0, do: n != Enum.at(numbers, i - 1) + 1, else: false if i > 0, do: n != Enum.at(numbers, i - 1) + 1, else: false
end) end)
next_index = if last_index == nil, do: Enum.count(numbers), else: last_index next_index = if last_index == nil, do: Enum.count(numbers), else: last_index
first = Enum.at(numbers, 0) first = Enum.at(numbers, 0)
last = Enum.at(numbers, next_index - 1) last = Enum.at(numbers, next_index - 1)
@ -599,4 +665,85 @@ defmodule BlockScoutWeb.StakesChannel do
staker = if staker == nil, do: "", else: staker staker = if staker == nil, do: "", else: staker
Atom.to_string(@claim_reward_long_op) <> "_" <> staker Atom.to_string(@claim_reward_long_op) <> "_" <> staker
end end
defp handle_in_render_claim_reward_result(
socket,
data,
staker,
staking_contract_address,
empty_staker,
empty_staking_contract_address
) do
cond do
claim_reward_long_op_active(socket) == true ->
{:reply, {:error, %{reason: gettext("Pools searching is already in progress for this address")}}, socket}
empty_staker ->
{:reply, {:error, %{reason: gettext("Unknown staker address. Please, choose your account in MetaMask")}},
socket}
empty_staking_contract_address ->
{:reply, {:error, %{reason: gettext("Unknown address of Staking contract. Please, contact support")}}, socket}
true ->
result =
if data["preload"] do
%{
html: View.render_to_string(StakesView, "_stakes_modal_claim_reward.html", %{}),
socket: socket
}
else
task = Task.async(__MODULE__, :find_claim_reward_pools, [socket, staker, staking_contract_address])
%{
html: "OK",
socket: assign(socket, @claim_reward_long_op, %{task: task, staker: staker})
}
end
{:reply, {:ok, %{html: result.html}}, result.socket}
end
end
defp handle_in_recalc_claim_reward_result(
socket,
epochs,
staking_contract_address,
pool_staking_address,
staker,
empty_pool_staking_address,
empty_staking_contract_address,
empty_staker
) do
cond do
claim_reward_long_op_active(socket) == true ->
{:reply, {:error, %{reason: gettext("Reward calculating is already in progress for this address")}}, socket}
Enum.empty?(epochs) ->
{:reply, {:error, %{reason: gettext("Staking epochs are not specified or not in the allowed range")}}, socket}
empty_pool_staking_address ->
{:reply, {:error, %{reason: gettext("Unknown pool staking address. Please, contact support")}}, socket}
empty_staker ->
{:reply, {:error, %{reason: gettext("Unknown staker address. Please, choose your account in MetaMask")}},
socket}
empty_staking_contract_address ->
{:reply, {:error, %{reason: gettext("Unknown address of Staking contract. Please, contact support")}}, socket}
true ->
task =
Task.async(__MODULE__, :recalc_claim_reward, [
socket,
staking_contract_address,
epochs,
pool_staking_address,
staker
])
socket = assign(socket, @claim_reward_long_op, %{task: task, staker: staker})
{:reply, {:ok, %{html: "OK"}}, socket}
end
end
end end

@ -49,72 +49,74 @@ defmodule BlockScoutWeb.StakesController do
# this is called when account in MetaMask is changed on client side # this is called when account in MetaMask is changed on client side
# or when UI periodically reloads the pool list (e.g. once per 10 blocks) # or when UI periodically reloads the pool list (e.g. once per 10 blocks)
defp render_template(filter, conn, %{"type" => "JSON"} = params) do defp render_template(filter, conn, %{"type" => "JSON"} = params) do
{items, next_page_path} = if Map.has_key?(params, "filterMy") do {items, next_page_path} =
[paging_options: options] = paging_options(params) if Map.has_key?(params, "filterMy") do
[paging_options: options] = paging_options(params)
last_index =
params last_index =
|> Map.get("position", "0") params
|> String.to_integer() |> Map.get("position", "0")
|> String.to_integer()
pools_plus_one =
Chain.staking_pools( pools_plus_one =
filter, Chain.staking_pools(
options, filter,
unless params["account"] == "" do options,
params["account"] unless params["account"] == "" do
end, params["account"]
params["filterBanned"] == "true", end,
params["filterMy"] == "true" params["filterBanned"] == "true",
) params["filterMy"] == "true"
{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{})
epoch_number = ContractState.get(:epoch_number, 0)
staking_allowed = ContractState.get(:staking_allowed, false)
items =
pools
|> Enum.with_index(last_index + 1)
|> Enum.map(fn {%{pool: pool, delegator: delegator}, index} ->
View.render_to_string(
StakesView,
"_rows.html",
token: token,
pool: pool,
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)
}
) )
end)
{items, next_page_path} {pools, next_page} = split_list_by_page(pools_plus_one)
else
loading_item = View.render_to_string(StakesView, "_rows_loading.html", %{}) next_page_path =
{[loading_item], nil} case next_page_params(next_page, pools, params) do
end 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{})
epoch_number = ContractState.get(:epoch_number, 0)
staking_allowed = ContractState.get(:staking_allowed, false)
items =
pools
|> Enum.with_index(last_index + 1)
|> Enum.map(fn {%{pool: pool, delegator: delegator}, index} ->
View.render_to_string(
StakesView,
"_rows.html",
token: token,
pool: pool,
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)
}
)
end)
{items, next_page_path}
else
loading_item = View.render_to_string(StakesView, "_rows_loading.html", %{})
{[loading_item], nil}
end
json( json(
conn, conn,

@ -4823,25 +4823,32 @@ defmodule Explorer.Chain do
end end
def staking_pool_delegators(staking_address_hash, show_snapshotted_data) do def staking_pool_delegators(staking_address_hash, show_snapshotted_data) do
from( query =
d in StakingPoolsDelegator, from(
where: d in StakingPoolsDelegator,
d.staking_address_hash == ^staking_address_hash and where:
(d.is_active == true or ^show_snapshotted_data and d.snapshotted_stake_amount > 0 and d.is_active != true), d.staking_address_hash == ^staking_address_hash and
order_by: (d.is_active == true or (^show_snapshotted_data and d.snapshotted_stake_amount > 0 and d.is_active != true)),
[desc: d.stake_amount] order_by: [desc: d.stake_amount]
) |> Repo.all() )
query
|> Repo.all()
end end
def staking_pool_snapshotted_inactive_delegators_count(staking_address_hash) do def staking_pool_snapshotted_inactive_delegators_count(staking_address_hash) do
from( query =
d in StakingPoolsDelegator, from(
where: d in StakingPoolsDelegator,
d.staking_address_hash == ^staking_address_hash and where:
d.snapshotted_stake_amount > 0 and d.staking_address_hash == ^staking_address_hash and
d.is_active != true, d.snapshotted_stake_amount > 0 and
select: fragment("count(*)") d.is_active != true,
) |> Repo.one() select: fragment("count(*)")
)
query
|> Repo.one()
end end
def staking_pool_delegator(staking_address_hash, address_hash) do def staking_pool_delegator(staking_address_hash, address_hash) do

@ -229,7 +229,7 @@ defmodule Explorer.Chain.Import do
defp validate_runner_options(runner, options) when is_map(options) do defp validate_runner_options(runner, options) when is_map(options) do
option_key = runner.option_key() option_key = runner.option_key()
runner_specific_options = runner_specific_options =
if Map.has_key?(Enum.into(runner.__info__(:functions), %{}), :runner_specific_options) do if Map.has_key?(Enum.into(runner.__info__(:functions), %{}), :runner_specific_options) do
apply(runner, :runner_specific_options, []) apply(runner, :runner_specific_options, [])
else else
@ -260,8 +260,8 @@ defmodule Explorer.Chain.Import do
@local_options ~w(on_conflict params with timeout)a @local_options ~w(on_conflict params with timeout)a
defp validate_runner_options_known(runner_option_key, options, runner_specific_options) do defp validate_runner_options_known(runner_option_key, options, runner_specific_options) do
unknown_option_keys = Map.keys(options) -- @local_options base_unknown_option_keys = Map.keys(options) -- @local_options
unknown_option_keys = unknown_option_keys -- runner_specific_options unknown_option_keys = base_unknown_option_keys -- runner_specific_options
if Enum.empty?(unknown_option_keys) do if Enum.empty?(unknown_option_keys) do
:ok :ok

@ -43,19 +43,21 @@ defmodule Explorer.Chain.Import.Runner.StakingPools do
|> Map.put_new(:timeout, @timeout) |> Map.put_new(:timeout, @timeout)
|> Map.put(:timestamps, timestamps) |> Map.put(:timestamps, timestamps)
clear_snapshotted_values = case Map.fetch(insert_options, :clear_snapshotted_values) do clear_snapshotted_values =
{:ok, v} -> v case Map.fetch(insert_options, :clear_snapshotted_values) do
:error -> false {:ok, v} -> v
end :error -> false
end
multi = if not clear_snapshotted_values do
# Enforce ShareLocks tables order (see docs: sharelocks.md) multi =
Multi.run(multi, :acquire_all_staking_pools, fn repo, _ -> if clear_snapshotted_values do
acquire_all_staking_pools(repo) multi
end) else
else # Enforce ShareLocks tables order (see docs: sharelocks.md)
multi Multi.run(multi, :acquire_all_staking_pools, fn repo, _ ->
end acquire_all_staking_pools(repo)
end)
end
multi multi
|> Multi.run(:mark_as_deleted, fn repo, _ -> |> Multi.run(:mark_as_deleted, fn repo, _ ->
@ -84,26 +86,28 @@ defmodule Explorer.Chain.Import.Runner.StakingPools do
end end
defp mark_as_deleted(repo, changes_list, %{timeout: timeout}, clear_snapshotted_values) when is_list(changes_list) do defp mark_as_deleted(repo, changes_list, %{timeout: timeout}, clear_snapshotted_values) when is_list(changes_list) do
query = if clear_snapshotted_values do query =
from( if clear_snapshotted_values do
pool in StakingPool, from(
update: [ pool in StakingPool,
set: [ update: [
snapshotted_self_staked_amount: nil, set: [
snapshotted_total_staked_amount: nil, snapshotted_self_staked_amount: nil,
snapshotted_validator_reward_ratio: nil snapshotted_total_staked_amount: nil,
snapshotted_validator_reward_ratio: nil
]
] ]
] )
) else
else addresses = Enum.map(changes_list, & &1.staking_address_hash)
addresses = Enum.map(changes_list, & &1.staking_address_hash)
from( from(
pool in StakingPool, pool in StakingPool,
where: pool.staking_address_hash not in ^addresses, where: pool.staking_address_hash not in ^addresses,
# ShareLocks order already enforced by `acquire_all_staking_pools` (see docs: sharelocks.md) # ShareLocks order already enforced by `acquire_all_staking_pools` (see docs: sharelocks.md)
update: [set: [is_deleted: true, is_active: false]] update: [set: [is_deleted: true, is_active: false]]
) )
end end
try do try do
{_, result} = repo.update_all(query, [], timeout: timeout) {_, result} = repo.update_all(query, [], timeout: timeout)

@ -57,22 +57,24 @@ defmodule Explorer.Chain.Import.Runner.StakingPoolsDelegators do
def timeout, do: @timeout def timeout, do: @timeout
defp mark_as_deleted(repo, %{timeout: timeout} = options) do defp mark_as_deleted(repo, %{timeout: timeout} = options) do
clear_snapshotted_values = case Map.fetch(options, :clear_snapshotted_values) do clear_snapshotted_values =
{:ok, v} -> v case Map.fetch(options, :clear_snapshotted_values) do
:error -> false {:ok, v} -> v
end :error -> false
end
query = if clear_snapshotted_values do
from( query =
d in StakingPoolsDelegator, if clear_snapshotted_values do
update: [set: [snapshotted_reward_ratio: nil, snapshotted_stake_amount: nil]] from(
) d in StakingPoolsDelegator,
else update: [set: [snapshotted_reward_ratio: nil, snapshotted_stake_amount: nil]]
from( )
d in StakingPoolsDelegator, else
update: [set: [is_active: false, is_deleted: true]] from(
) d in StakingPoolsDelegator,
end update: [set: [is_active: false, is_deleted: true]]
)
end
try do try do
{_, result} = repo.update_all(query, [], timeout: timeout) {_, result} = repo.update_all(query, [], timeout: timeout)

@ -37,12 +37,12 @@ defmodule Explorer.Staking.ContractReader do
# address _staker # address _staker
# ) public view returns(uint256 tokenRewardSum, uint256 nativeRewardSum); # ) public view returns(uint256 tokenRewardSum, uint256 nativeRewardSum);
def call_get_reward_amount( def call_get_reward_amount(
staking_contract_address, staking_contract_address,
staking_epochs, staking_epochs,
pool_staking_address, pool_staking_address,
staker, staker,
json_rpc_named_arguments json_rpc_named_arguments
) do ) do
staking_epochs_joint = staking_epochs_joint =
staking_epochs staking_epochs
|> Enum.map(fn epoch -> |> Enum.map(fn epoch ->
@ -56,29 +56,44 @@ defmodule Explorer.Staking.ContractReader do
staker = address_pad_to_64(staker) staker = address_pad_to_64(staker)
staking_epochs_length = staking_epochs_length =
Enum.count(staking_epochs) staking_epochs
|> Enum.count()
|> Integer.to_string(16) |> Integer.to_string(16)
|> String.pad_leading(64, ["0"]) |> String.pad_leading(64, ["0"])
data = "0xfb367a9b" # `getRewardAmount` function signature # `getRewardAmount` function signature
data = data <> String.pad_leading("60", 64, ["0"]) # offset to the `_stakingEpochs` array function_signature = "0xfb367a9b"
data = data <> pool_staking_address # `_poolStakingAddress` parameter # offset to the `_stakingEpochs` array
data = data <> staker # `_staker` parameter function_signature_with_offset = function_signature <> String.pad_leading("60", 64, ["0"])
data = data <> staking_epochs_length # the length of `_stakingEpochs` array # `_poolStakingAddress` parameter
data = data <> staking_epochs_joint # encoded `_stakingEpochs` array function_with_param_1 = function_signature_with_offset <> pool_staking_address
# `_staker` parameter
result = EthereumJSONRPC.request(%{ function_with_param1_param2 = function_with_param_1 <> staker
# the length of `_stakingEpochs` array
function_with_param_1_length_param2 = function_with_param1_param2 <> staking_epochs_length
# encoded `_stakingEpochs` array
data = function_with_param_1_length_param2 <> staking_epochs_joint
request = %{
id: 0, id: 0,
method: "eth_call", method: "eth_call",
params: [%{ params: [
to: staking_contract_address, %{
data: data to: staking_contract_address,
}] data: data
}) |> EthereumJSONRPC.json_rpc(json_rpc_named_arguments) }
]
}
result =
request
|> EthereumJSONRPC.request()
|> EthereumJSONRPC.json_rpc(json_rpc_named_arguments)
case result do case result do
{:ok, response} -> {:ok, response} ->
response = String.replace_leading(response, "0x", "") response = String.replace_leading(response, "0x", "")
if String.length(response) != 64 * 2 do if String.length(response) != 64 * 2 do
{:error, "Invalid getRewardAmount response."} {:error, "Invalid getRewardAmount response."}
else else
@ -87,6 +102,7 @@ defmodule Explorer.Staking.ContractReader do
native_reward_sum = String.to_integer(native_reward_sum, 16) native_reward_sum = String.to_integer(native_reward_sum, 16)
{:ok, %{token_reward_sum: token_reward_sum, native_reward_sum: native_reward_sum}} {:ok, %{token_reward_sum: token_reward_sum, native_reward_sum: native_reward_sum}}
end end
{:error, reason} -> {:error, reason} ->
{:error, reason} {:error, reason}
end end
@ -98,12 +114,12 @@ defmodule Explorer.Staking.ContractReader do
# address _poolStakingAddress # address _poolStakingAddress
# ) public; # ) public;
def claim_reward_estimate_gas( def claim_reward_estimate_gas(
staking_contract_address, staking_contract_address,
staking_epochs, staking_epochs,
pool_staking_address, pool_staking_address,
staker, staker,
json_rpc_named_arguments json_rpc_named_arguments
) do ) do
staking_epochs_joint = staking_epochs_joint =
staking_epochs staking_epochs
|> Enum.map(fn epoch -> |> Enum.map(fn epoch ->
@ -116,34 +132,50 @@ defmodule Explorer.Staking.ContractReader do
pool_staking_address = address_pad_to_64(pool_staking_address) pool_staking_address = address_pad_to_64(pool_staking_address)
staking_epochs_length = staking_epochs_length =
Enum.count(staking_epochs) staking_epochs
|> Enum.count()
|> Integer.to_string(16) |> Integer.to_string(16)
|> String.pad_leading(64, ["0"]) |> String.pad_leading(64, ["0"])
data = "0x3ea15d62" # `claimReward` function signature # `claimReward` function signature
data = data <> String.pad_leading("40", 64, ["0"]) # offset to the `_stakingEpochs` array function_signature = "0x3ea15d62"
data = data <> pool_staking_address # `_poolStakingAddress` parameter # offset to the `_stakingEpochs` array
data = data <> staking_epochs_length # the length of `_stakingEpochs` array function_signature_with_offset = function_signature <> String.pad_leading("40", 64, ["0"])
data = data <> staking_epochs_joint # encoded `_stakingEpochs` array # `_poolStakingAddress` parameter
function_with_param_1 = function_signature_with_offset <> pool_staking_address
result = EthereumJSONRPC.request(%{ # the length of `_stakingEpochs` array
function_with_param_1_length_param2 = function_with_param_1 <> staking_epochs_length
# encoded `_stakingEpochs` array
data = function_with_param_1_length_param2 <> staking_epochs_joint
request = %{
id: 0, id: 0,
method: "eth_estimateGas", method: "eth_estimateGas",
params: [%{ params: [
from: staker, %{
to: staking_contract_address, from: staker,
gasPrice: "0x3B9ACA00", # 1 gwei to: staking_contract_address,
data: data # 1 gwei
}] gasPrice: "0x3B9ACA00",
}) |> EthereumJSONRPC.json_rpc(json_rpc_named_arguments) data: data
}
]
}
result =
request
|> EthereumJSONRPC.request()
|> EthereumJSONRPC.json_rpc(json_rpc_named_arguments)
case result do case result do
{:ok, response} -> {:ok, response} ->
estimate = estimate =
response response
|> String.replace_leading("0x", "") |> String.replace_leading("0x", "")
|> String.to_integer(16) |> String.to_integer(16)
{:ok, estimate} {:ok, estimate}
{:error, reason} -> {:error, reason} ->
{:error, reason} {:error, reason}
end end

@ -34,7 +34,8 @@ defmodule Explorer.Staking.ContractState do
:validator_set_contract :validator_set_contract
] ]
@token_renew_frequency 10 # frequency in blocks # frequency in blocks
@token_renew_frequency 10
defstruct [ defstruct [
:seen_block, :seen_block,
@ -128,81 +129,151 @@ defmodule Explorer.Staking.ContractState do
# read general info from the contracts (including pool list and validator list) # read general info from the contracts (including pool list and validator list)
global_responses = ContractReader.perform_requests(ContractReader.global_requests(), contracts, abi) global_responses = ContractReader.perform_requests(ContractReader.global_requests(), contracts, abi)
validator_min_reward_percent = ContractReader.perform_requests( validator_min_reward_percent = get_validator_min_reward_percent(global_responses, contracts, abi)
ContractReader.validator_min_reward_percent_request(global_responses.epoch_number), contracts, abi
).value
epoch_very_beginning = (global_responses.epoch_start_block == block_number + 1) epoch_very_beginning = global_responses.epoch_start_block == block_number + 1
is_validator = Enum.into(global_responses.validators, %{}, &{address_bytes_to_string(&1), true}) is_validator = Enum.into(global_responses.validators, %{}, &{address_bytes_to_string(&1), true})
start_snapshotting = (global_responses.epoch_number > get(:snapshotted_epoch_number) && global_responses.epoch_number > 0 && not get(:is_snapshotting))
# save the general info to ETS (excluding pool list and validator list) start_snapshotting =
settings = global_responses.epoch_number > get(:snapshotted_epoch_number) && global_responses.epoch_number > 0 &&
global_responses not get(:is_snapshotting)
|> Map.take([
:token_contract_address,
:min_candidate_stake,
:min_delegator_stake,
:epoch_number,
:epoch_start_block,
:epoch_end_block,
:staking_allowed,
:validator_set_apply_block
])
|> Map.to_list()
|> Enum.concat(validator_min_reward_percent: validator_min_reward_percent)
update_token = # save the general info to ETS (excluding pool list and validator list)
get(:token) == nil or settings = get_settings(global_responses, validator_min_reward_percent, block_number)
get(:token_contract_address) != global_responses.token_contract_address or
rem(block_number, @token_renew_frequency) == 0
settings = if update_token do
Enum.concat(settings, token: get_token(global_responses.token_contract_address))
else
settings
end
:ets.insert(@table_name, settings) :ets.insert(@table_name, settings)
# form the list of validator pools # form the list of validator pools
validators = if start_snapshotting do validators =
if global_responses.validator_set_apply_block == 0 do get_validators(
%{ start_snapshotting,
"getPendingValidators" => {:ok, [validators_pending]}, global_responses,
"validatorsToBeFinalized" => {:ok, [validators_to_be_finalized]} contracts,
} = Reader.query_contract(contracts.validator_set, abi, %{ abi
"getPendingValidators" => [], )
"validatorsToBeFinalized" => []
})
validators_pending = Enum.uniq(validators_pending ++ validators_to_be_finalized)
%{
all: Enum.uniq(global_responses.validators ++ validators_pending),
for_snapshot: validators_pending
}
else
%{
all: global_responses.validators,
for_snapshot: global_responses.validators
}
end
else
%{all: global_responses.validators}
end
# miningToStakingAddress mapping # miningToStakingAddress mapping
mining_to_staking_address = mining_to_staking_address = get_mining_to_staking_address(validators, contracts, abi)
validators.all
|> Enum.map(&ContractReader.staking_by_mining_request/1)
|> ContractReader.perform_grouped_requests(validators.all, contracts, abi)
|> Map.new(fn {mining_address, resp} -> {mining_address, address_string_to_bytes(resp.staking_address).bytes} end)
# the list of all pools (validators + active pools + inactive pools) # the list of all pools (validators + active pools + inactive pools)
pools = Enum.uniq( pools =
Map.values(mining_to_staking_address) ++ Enum.uniq(
global_responses.active_pools ++ Map.values(mining_to_staking_address) ++
global_responses.inactive_pools global_responses.active_pools ++
) global_responses.inactive_pools
)
%{
pool_staking_responses: pool_staking_responses,
pool_mining_responses: pool_mining_responses,
staker_responses: staker_responses
} = get_responses(pools, block_number, contracts, abi)
# to keep sort order when using `perform_grouped_requests` (see below)
pool_staking_keys = Enum.map(pool_staking_responses, fn {pool_staking_address, _} -> pool_staking_address end)
# call `BlockReward.validatorShare` function for each pool
# to get validator's reward share of the pool (needed for the `Delegators` list in UI)
candidate_reward_responses =
get_candidate_reward_responses(pool_staking_responses, global_responses, pool_staking_keys, contracts, abi)
# to keep sort order when using `perform_grouped_requests` (see below)
delegator_keys = Enum.map(staker_responses, fn {key, _} -> key end)
# call `BlockReward.delegatorShare` function for each delegator
# to get their reward share of the pool (needed for the `Delegators` list in UI)
delegator_reward_responses =
get_delegator_reward_responses(
staker_responses,
pool_staking_responses,
global_responses,
delegator_keys,
contracts,
abi
)
# calculate total amount staked into all active pools
staked_total = Enum.sum(for {_, pool} <- pool_staking_responses, pool.is_active, do: pool.total_staked_amount)
# calculate likelihood of becoming a validator on the next epoch
[likelihood_values, total_likelihood] = global_responses.pools_likelihood
# array of pool addresses (staking addresses)
likelihood =
global_responses.pools_to_be_elected
|> Enum.zip(likelihood_values)
|> Enum.into(%{})
snapshotted_epoch_number = get(:snapshotted_epoch_number)
# form entries for writing to the `staking_pools` table in DB
pool_entries =
get_pool_entries(%{
pools: pools,
pool_mining_responses: pool_mining_responses,
pool_staking_responses: pool_staking_responses,
is_validator: is_validator,
candidate_reward_responses: candidate_reward_responses,
global_responses: global_responses,
snapshotted_epoch_number: snapshotted_epoch_number,
likelihood: likelihood,
total_likelihood: total_likelihood,
staked_total: staked_total
})
# form entries for writing to the `staking_pools_delegators` table in DB
delegator_entries = get_delegator_entries(staker_responses, delegator_reward_responses)
# perform SQL queries
{:ok, _} =
Chain.import(%{
staking_pools: %{params: pool_entries},
staking_pools_delegators: %{params: delegator_entries},
timeout: :infinity
})
if epoch_very_beginning or start_snapshotting do
at_start_snapshotting(block_number)
end
if start_snapshotting do
do_start_snapshotting(
epoch_very_beginning,
pool_staking_responses,
global_responses,
contracts,
abi,
validators,
mining_to_staking_address
)
end
# notify the UI about new block
Publisher.broadcast(:staking_update)
end
defp get_settings(global_responses, validator_min_reward_percent, block_number) do
base_settings = get_base_settings(global_responses, validator_min_reward_percent)
update_token =
get(:token) == nil or
get(:token_contract_address) != global_responses.token_contract_address or
rem(block_number, @token_renew_frequency) == 0
if update_token do
Enum.concat(base_settings, token: get_token(global_responses.token_contract_address))
else
base_settings
end
end
defp get_mining_to_staking_address(validators, contracts, abi) do
validators.all
|> Enum.map(&ContractReader.staking_by_mining_request/1)
|> ContractReader.perform_grouped_requests(validators.all, contracts, abi)
|> Map.new(fn {mining_address, resp} -> {mining_address, address_string_to_bytes(resp.staking_address).bytes} end)
end
defp get_responses(pools, block_number, contracts, abi) do
# read pool info from the contracts by its staking address # read pool info from the contracts by its staking address
pool_staking_responses = pool_staking_responses =
pools pools
@ -233,258 +304,366 @@ defmodule Explorer.Staking.ContractState do
end) end)
|> ContractReader.perform_grouped_requests(stakers, contracts, abi) |> ContractReader.perform_grouped_requests(stakers, contracts, abi)
# to keep sort order when using `perform_grouped_requests` (see below) %{
pool_staking_keys = Enum.map(pool_staking_responses, fn {pool_staking_address, _} -> pool_staking_address end) pool_staking_responses: pool_staking_responses,
pool_mining_responses: pool_mining_responses,
staker_responses: staker_responses
}
end
# call `BlockReward.validatorShare` function for each pool defp get_candidate_reward_responses(pool_staking_responses, global_responses, pool_staking_keys, contracts, abi) do
# to get validator's reward share of the pool (needed for the `Delegators` list in UI) pool_staking_responses
candidate_reward_responses = |> Enum.map(fn {_pool_staking_address, resp} ->
pool_staking_responses ContractReader.validator_reward_request([
|> Enum.map(fn {_pool_staking_address, resp} -> global_responses.epoch_number,
ContractReader.validator_reward_request([ resp.self_staked_amount,
global_responses.epoch_number, resp.total_staked_amount,
resp.self_staked_amount, 1000_000
resp.total_staked_amount, ])
1000_000 end)
]) |> ContractReader.perform_grouped_requests(pool_staking_keys, contracts, abi)
end) end
|> ContractReader.perform_grouped_requests(pool_staking_keys, contracts, abi)
# to keep sort order when using `perform_grouped_requests` (see below) defp get_delegator_reward_responses(
delegator_keys = Enum.map(staker_responses, fn {key, _} -> key end) staker_responses,
pool_staking_responses,
global_responses,
delegator_keys,
contracts,
abi
) do
staker_responses
|> Enum.map(fn {{pool_staking_address, _staker_address, _is_active}, resp} ->
staking_resp = pool_staking_responses[pool_staking_address]
ContractReader.delegator_reward_request([
global_responses.epoch_number,
resp.stake_amount,
staking_resp.self_staked_amount,
staking_resp.total_staked_amount,
1000_000
])
end)
|> ContractReader.perform_grouped_requests(delegator_keys, contracts, abi)
end
# call `BlockReward.delegatorShare` function for each delegator defp get_delegator_entries(staker_responses, delegator_reward_responses) do
# to get their reward share of the pool (needed for the `Delegators` list in UI) Enum.map(staker_responses, fn {{pool_address, delegator_address, is_active}, response} ->
delegator_reward_responses = delegator_reward_response = delegator_reward_responses[{pool_address, delegator_address, is_active}]
staker_responses
|> Enum.map(fn {{pool_staking_address, _staker_address, _is_active}, resp} ->
staking_resp = pool_staking_responses[pool_staking_address]
ContractReader.delegator_reward_request([
global_responses.epoch_number,
resp.stake_amount,
staking_resp.self_staked_amount,
staking_resp.total_staked_amount,
1000_000
])
end)
|> ContractReader.perform_grouped_requests(delegator_keys, contracts, abi)
# calculate total amount staked into all active pools Map.merge(response, %{
staked_total = Enum.sum(for {_, pool} <- pool_staking_responses, pool.is_active, do: pool.total_staked_amount) address_hash: delegator_address,
staking_address_hash: pool_address,
is_active: is_active,
reward_ratio: Float.floor(delegator_reward_response.delegator_share / 10_000, 2)
})
end)
end
# calculate likelihood of becoming a validator on the next epoch defp get_validator_min_reward_percent(global_responses, contracts, abi) do
[likelihood_values, total_likelihood] = global_responses.pools_likelihood ContractReader.perform_requests(
likelihood = ContractReader.validator_min_reward_percent_request(global_responses.epoch_number),
global_responses.pools_to_be_elected # array of pool addresses (staking addresses) contracts,
|> Enum.zip(likelihood_values) abi
|> Enum.into(%{}) ).value
end
snapshotted_epoch_number = get(:snapshotted_epoch_number) defp get_base_settings(global_responses, validator_min_reward_percent) do
global_responses
|> Map.take([
:token_contract_address,
:min_candidate_stake,
:min_delegator_stake,
:epoch_number,
:epoch_start_block,
:epoch_end_block,
:staking_allowed,
:validator_set_apply_block
])
|> Map.to_list()
|> Enum.concat(validator_min_reward_percent: validator_min_reward_percent)
end
# form entries for writing to the `staking_pools` table in DB defp get_validators(
pool_entries = start_snapshotting,
Enum.map(pools, fn pool_staking_address -> global_responses,
staking_resp = pool_staking_responses[pool_staking_address] contracts,
mining_resp = pool_mining_responses[pool_staking_address] abi
candidate_reward_resp = candidate_reward_responses[pool_staking_address] ) do
is_validator = is_validator[staking_resp.mining_address_hash] || false if start_snapshotting do
if global_responses.validator_set_apply_block == 0 do
delegators_count = %{
length(staking_resp.active_delegators) + "getPendingValidators" => {:ok, [validators_pending]},
if show_snapshotted_data(is_validator, global_responses.validator_set_apply_block, snapshotted_epoch_number, global_responses.epoch_number) do "validatorsToBeFinalized" => {:ok, [validators_to_be_finalized]}
} =
Reader.query_contract(contracts.validator_set, abi, %{
"getPendingValidators" => [],
"validatorsToBeFinalized" => []
})
validators_pending = Enum.uniq(validators_pending ++ validators_to_be_finalized)
%{
all: Enum.uniq(global_responses.validators ++ validators_pending),
for_snapshot: validators_pending
}
else
%{
all: global_responses.validators,
for_snapshot: global_responses.validators
}
end
else
%{all: global_responses.validators}
end
end
def show_snapshotted_data(
is_validator,
validator_set_apply_block \\ nil,
snapshotted_epoch_number \\ nil,
epoch_number \\ nil
) do
validator_set_apply_block =
if validator_set_apply_block !== nil do
validator_set_apply_block
else
get(:validator_set_apply_block)
end
snapshotted_epoch_number =
if snapshotted_epoch_number !== nil do
snapshotted_epoch_number
else
get(:snapshotted_epoch_number)
end
epoch_number =
if epoch_number !== nil do
epoch_number
else
get(:epoch_number)
end
is_validator && validator_set_apply_block > 0 && snapshotted_epoch_number === epoch_number
end
defp get_pool_entries(%{
pools: pools,
pool_mining_responses: pool_mining_responses,
pool_staking_responses: pool_staking_responses,
is_validator: is_validator,
candidate_reward_responses: candidate_reward_responses,
global_responses: global_responses,
snapshotted_epoch_number: snapshotted_epoch_number,
likelihood: likelihood,
total_likelihood: total_likelihood,
staked_total: staked_total
}) do
Enum.map(pools, fn pool_staking_address ->
staking_resp = pool_staking_responses[pool_staking_address]
mining_resp = pool_mining_responses[pool_staking_address]
candidate_reward_resp = candidate_reward_responses[pool_staking_address]
is_validator = is_validator[staking_resp.mining_address_hash] || false
delegators_count =
length(staking_resp.active_delegators) +
if show_snapshotted_data(
is_validator,
global_responses.validator_set_apply_block,
snapshotted_epoch_number,
global_responses.epoch_number
) do
Chain.staking_pool_snapshotted_inactive_delegators_count(pool_staking_address) Chain.staking_pool_snapshotted_inactive_delegators_count(pool_staking_address)
else else
0 0
end end
%{ %{
staking_address_hash: pool_staking_address, staking_address_hash: pool_staking_address,
delegators_count: delegators_count, delegators_count: delegators_count,
stakes_ratio: stakes_ratio:
if staking_resp.is_active do if staking_resp.is_active do
ratio(staking_resp.total_staked_amount, staked_total) ratio(staking_resp.total_staked_amount, staked_total)
else else
0 0
end, end,
validator_reward_ratio: Float.floor(candidate_reward_resp.validator_share / 10_000, 2), validator_reward_ratio: Float.floor(candidate_reward_resp.validator_share / 10_000, 2),
likelihood: ratio(likelihood[pool_staking_address] || 0, total_likelihood), likelihood: ratio(likelihood[pool_staking_address] || 0, total_likelihood),
validator_reward_percent: staking_resp.validator_reward_percent / 10_000, validator_reward_percent: staking_resp.validator_reward_percent / 10_000,
is_deleted: false, is_deleted: false,
is_validator: is_validator, is_validator: is_validator,
is_unremovable: address_bytes_to_string(pool_staking_address) == global_responses.unremovable_validator, is_unremovable: address_bytes_to_string(pool_staking_address) == global_responses.unremovable_validator,
ban_reason: binary_to_string(mining_resp.ban_reason) ban_reason: binary_to_string(mining_resp.ban_reason)
} }
|> Map.merge( |> Map.merge(
Map.take(staking_resp, [ Map.take(staking_resp, [
:is_active, :is_active,
:mining_address_hash, :mining_address_hash,
:self_staked_amount, :self_staked_amount,
:total_staked_amount :total_staked_amount
]) ])
) )
|> Map.merge( |> Map.merge(
Map.take(mining_resp, [ Map.take(mining_resp, [
:are_delegators_banned, :are_delegators_banned,
:banned_delegators_until, :banned_delegators_until,
:banned_until, :banned_until,
:is_banned, :is_banned,
:was_banned_count, :was_banned_count,
:was_validator_count :was_validator_count
]) ])
) )
end) end)
end
# form entries for writing to the `staking_pools_delegators` table in DB defp at_start_snapshotting(block_number) do
delegator_entries = # update ERC balance of the BlockReward contract
Enum.map(staker_responses, fn {{pool_address, delegator_address, is_active}, response} -> token = get(:token)
delegator_reward_response = delegator_reward_responses[{pool_address, delegator_address, is_active}]
Map.merge(response, %{
address_hash: delegator_address,
staking_address_hash: pool_address,
is_active: is_active,
reward_ratio: Float.floor(delegator_reward_response.delegator_share / 10_000, 2)
})
end)
# perform SQL queries if token != nil do
{:ok, _} = block_reward_address = address_string_to_bytes(get(:block_reward_contract).address)
Chain.import(%{ token_contract_address_hash = token.contract_address_hash
staking_pools: %{params: pool_entries},
staking_pools_delegators: %{params: delegator_entries},
timeout: :infinity
})
if epoch_very_beginning or start_snapshotting do block_reward_balance =
# update ERC balance of the BlockReward contract BalanceReader.get_balances_of([
token = get(:token) %{
if token != nil do token_contract_address_hash: token_contract_address_hash,
block_reward_address = address_string_to_bytes(get(:block_reward_contract).address) address_hash: block_reward_address.bytes,
token_contract_address_hash = token.contract_address_hash block_number: block_number
}
block_reward_balance = BalanceReader.get_balances_of([%{ ])[:ok]
token_contract_address_hash: token_contract_address_hash,
address_hash: block_reward_address.bytes, token_params =
block_number: block_number token_contract_address_hash
}])[:ok] |> MetadataRetriever.get_functions_of()
|> Map.merge(%{
token_params = contract_address_hash: token_contract_address_hash,
token_contract_address_hash type: "ERC-20"
|> MetadataRetriever.get_functions_of() })
|> Map.merge(%{
contract_address_hash: token_contract_address_hash,
type: "ERC-20"
})
import_result = Chain.import(%{ import_result =
Chain.import(%{
addresses: %{params: [%{hash: block_reward_address.bytes}], on_conflict: :nothing}, addresses: %{params: [%{hash: block_reward_address.bytes}], on_conflict: :nothing},
address_current_token_balances: %{params: [%{ address_current_token_balances: %{
address_hash: block_reward_address.bytes, params: [
token_contract_address_hash: token_contract_address_hash, %{
block_number: block_number, address_hash: block_reward_address.bytes,
value: block_reward_balance, token_contract_address_hash: token_contract_address_hash,
value_fetched_at: DateTime.utc_now() block_number: block_number,
}]}, value: block_reward_balance,
value_fetched_at: DateTime.utc_now()
}
]
},
tokens: %{params: [token_params]} tokens: %{params: [token_params]}
}) })
with {:ok, _} <- import_result, do: with {:ok, _} <- import_result,
Publisher.broadcast([{ do:
:address_token_balances, [ Publisher.broadcast(
%{address_hash: block_reward_address.struct, block_number: block_number} [
] {
}], :address_token_balances,
:on_demand [
) %{address_hash: block_reward_address.struct, block_number: block_number}
end ]
}
],
:on_demand
)
end end
end
if start_snapshotting do defp do_start_snapshotting(
# start snapshotting at the beginning of the staking epoch epoch_very_beginning,
cached_pool_staking_responses = if epoch_very_beginning do pool_staking_responses,
global_responses,
contracts,
abi,
validators,
mining_to_staking_address
) do
# start snapshotting at the beginning of the staking epoch
cached_pool_staking_responses =
if epoch_very_beginning do
pool_staking_responses pool_staking_responses
else else
%{} %{}
end end
spawn(StakeSnapshotting, :do_snapshotting, [ spawn(StakeSnapshotting, :do_snapshotting, [
%{contracts: contracts, abi: abi, ets_table_name: @table_name}, %{contracts: contracts, abi: abi, ets_table_name: @table_name},
global_responses.epoch_number, global_responses.epoch_number,
cached_pool_staking_responses, cached_pool_staking_responses,
validators.for_snapshot, # mining addresses of pending/current validators # mining addresses of pending/current validators
mining_to_staking_address, validators.for_snapshot,
global_responses.epoch_start_block - 1 # the last block of the previous staking epoch mining_to_staking_address,
]) # the last block of the previous staking epoch
end global_responses.epoch_start_block - 1
])
# notify the UI about new block
Publisher.broadcast(:staking_update)
end end
def show_snapshotted_data(is_validator, validator_set_apply_block \\ nil, snapshotted_epoch_number \\ nil, epoch_number \\ nil) do defp get_token(address) do
validator_set_apply_block = if validator_set_apply_block !== nil do if address == "0x0000000000000000000000000000000000000000" do
validator_set_apply_block # the token address is empty, so return nil
else nil
get(:validator_set_apply_block)
end
snapshotted_epoch_number = if snapshotted_epoch_number !== nil do
snapshotted_epoch_number
else
get(:snapshotted_epoch_number)
end
epoch_number = if epoch_number !== nil do
epoch_number
else else
get(:epoch_number) case Chain.string_to_address_hash(address) do
{:ok, address_hash} ->
# the token address has correct format, so try to read the token
# from DB or from its contract
case Chain.token_from_address_hash(address_hash) do
{:ok, token} ->
# the token is read from DB
token
_ ->
fetch_token(address, address_hash)
end
_ ->
# the token address has incorrect format
nil
end
end end
is_validator && validator_set_apply_block > 0 && snapshotted_epoch_number === epoch_number
end end
defp get_token(address) do defp fetch_token(address, address_hash) do
if address == "0x0000000000000000000000000000000000000000" do # the token doesn't exist in DB, so try
nil # the token address is empty, so return nil # to read it from a contract and then write to DB
else token_functions = MetadataRetriever.get_functions_of(address)
with {:ok, address_hash} <- Chain.string_to_address_hash(address) do
# the token address has correct format, so try to read the token if map_size(token_functions) > 0 do
# from DB or from its contract # the token is successfully read from its contract
case Chain.token_from_address_hash(address_hash) do token_params =
{:ok, token} -> Map.merge(token_functions, %{
token # the token is read from DB contract_address_hash: address,
_ -> type: "ERC-20"
# the token doesn't exist in DB, so try })
# to read it from a contract and then write to DB
token_functions = MetadataRetriever.get_functions_of(address) # try to write the token info to DB
import_result =
if map_size(token_functions) > 0 do Chain.import(%{
# the token is successfully read from its contract addresses: %{params: [%{hash: address}], on_conflict: :nothing},
token_params = Map.merge(token_functions, %{ tokens: %{params: [token_params]}
contract_address_hash: address, })
type: "ERC-20"
}) case import_result do
{:ok, _} ->
# try to write the token info to DB # the token is successfully added to DB, so return it as a result
import_result = Chain.import(%{ case Chain.token_from_address_hash(address_hash) do
addresses: %{params: [%{hash: address}], on_conflict: :nothing}, {:ok, token} -> token
tokens: %{params: [token_params]} _ -> nil
}) end
with {:ok, _} <- import_result do _ ->
# the token is successfully added to DB, so return it as a result # cannot write the token info to DB
case Chain.token_from_address_hash(address_hash) do nil
{:ok, token} -> token
_ -> nil
end
else
_ -> nil # cannot write the token info to DB
end
else
nil # cannot read the token info from its contract
end
end
else
_ -> nil # the token address has incorrect format
end end
else
# cannot read the token info from its contract
nil
end end
end end

@ -12,13 +12,13 @@ defmodule Explorer.Staking.StakeSnapshotting do
alias Explorer.Staking.ContractReader alias Explorer.Staking.ContractReader
def do_snapshotting( def do_snapshotting(
%{contracts: contracts, abi: abi, ets_table_name: ets_table_name}, %{contracts: contracts, abi: abi, ets_table_name: ets_table_name},
epoch_number, epoch_number,
cached_pool_staking_responses, cached_pool_staking_responses,
pools_mining_addresses, pools_mining_addresses,
mining_to_staking_address, mining_to_staking_address,
block_number block_number
) do ) do
:ets.insert(ets_table_name, is_snapshotting: true) :ets.insert(ets_table_name, is_snapshotting: true)
# get staking addresses for the pending validators # get staking addresses for the pending validators
@ -36,10 +36,18 @@ defmodule Explorer.Staking.StakeSnapshotting do
# use `cached_pool_staking_responses` when possible # use `cached_pool_staking_responses` when possible
pool_staking_responses = pool_staking_responses =
pool_staking_addresses pool_staking_addresses
|> Enum.map(fn staking_address_hash -> |> Enum.map(fn staking_address_hash ->
case Map.fetch(cached_pool_staking_responses, staking_address_hash) do case Map.fetch(cached_pool_staking_responses, staking_address_hash) do
{:ok, resp} -> {:ok, resp} ->
Map.merge(resp, ContractReader.perform_requests(snapshotted_pool_amounts_requests(staking_address_hash, block_number), contracts, abi)) Map.merge(
resp,
ContractReader.perform_requests(
snapshotted_pool_amounts_requests(staking_address_hash, block_number),
contracts,
abi
)
)
:error -> :error ->
ContractReader.perform_requests( ContractReader.perform_requests(
ContractReader.active_delegators_request(staking_address_hash, block_number) ++ ContractReader.active_delegators_request(staking_address_hash, block_number) ++
@ -156,10 +164,14 @@ defmodule Explorer.Staking.StakeSnapshotting do
# perform SQL queries # perform SQL queries
case Chain.import(%{ case Chain.import(%{
staking_pools: %{params: pool_entries, on_conflict: staking_pools_update(), clear_snapshotted_values: true}, staking_pools: %{params: pool_entries, on_conflict: staking_pools_update(), clear_snapshotted_values: true},
staking_pools_delegators: %{params: delegator_entries, on_conflict: staking_pools_delegators_update(), clear_snapshotted_values: true}, staking_pools_delegators: %{
timeout: :infinity params: delegator_entries,
}) do on_conflict: staking_pools_delegators_update(),
clear_snapshotted_values: true
},
timeout: :infinity
}) do
{:ok, _} -> :ets.insert(ets_table_name, snapshotted_epoch_number: epoch_number) {:ok, _} -> :ets.insert(ets_table_name, snapshotted_epoch_number: epoch_number)
_ -> Logger.error("Cannot successfully finish snapshotting for the epoch #{epoch_number - 1}") _ -> Logger.error("Cannot successfully finish snapshotting for the epoch #{epoch_number - 1}")
end end
@ -172,7 +184,10 @@ defmodule Explorer.Staking.StakeSnapshotting do
defp snapshotted_pool_amounts_requests(pool_staking_address, block_number) do defp snapshotted_pool_amounts_requests(pool_staking_address, block_number) do
[ [
snapshotted_total_staked_amount: {:staking, "stakeAmountTotal", [pool_staking_address], block_number}, snapshotted_total_staked_amount: {:staking, "stakeAmountTotal", [pool_staking_address], block_number},
snapshotted_self_staked_amount: snapshotted_staker_amount_request(pool_staking_address, pool_staking_address, block_number)[:snapshotted_stake_amount] snapshotted_self_staked_amount:
snapshotted_staker_amount_request(pool_staking_address, pool_staking_address, block_number)[
:snapshotted_stake_amount
]
] ]
end end

@ -7,9 +7,7 @@ defmodule Explorer.Repo.Migrations.RemoveDuplicateIndexes do
) )
drop_if_exists( drop_if_exists(
index(:staking_pools_delegators, [:address_hash], index(:staking_pools_delegators, [:address_hash], name: "staking_pools_delegators_address_hash_index")
name: "staking_pools_delegators_address_hash_index"
)
) )
drop_if_exists(index(:transactions, [:to_address_hash], name: "transactions_to_address_hash_index")) drop_if_exists(index(:transactions, [:to_address_hash], name: "transactions_to_address_hash_index"))

@ -2,7 +2,7 @@ defmodule Explorer.Repo.Migrations.AddIndexForSnapshottedStakeAmount do
use Ecto.Migration use Ecto.Migration
def change do def change do
create( create(
index(:staking_pools_delegators, [:staking_address_hash, :snapshotted_stake_amount, :is_active], index(:staking_pools_delegators, [:staking_address_hash, :snapshotted_stake_amount, :is_active],
unique: false, unique: false,
name: :snapshotted_stake_amount_index name: :snapshotted_stake_amount_index

Loading…
Cancel
Save