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"),
has_emission_funds: false,
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,
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
def terminate(_reason, socket) do
s = socket.assigns[@claim_reward_long_op]
if s != nil do
:ets.delete(ContractState, claim_reward_long_op_key(s.staker))
end
@ -251,55 +252,60 @@ defmodule BlockScoutWeb.StakesChannel do
def handle_in("render_claim_reward", data, socket) do
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}
end
staking_contract_address =
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
def handle_in("recalc_claim_reward", data, socket) do
epochs = data["epochs"]
pool_staking_address = data["pool_staking_address"]
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("Reward calculating is already in progress for this address")}}, socket}
Enum.count(epochs) == 0 ->
{: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" ->
{:reply, {:error, %{reason: gettext("Unknown pool staking address. Please, contact support")}}, 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 ->
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
staking_contract_address =
try do
ContractState.get(:staking_contract).address
after
end
empty_pool_staking_address =
pool_staking_address == nil || pool_staking_address == "" ||
pool_staking_address == "0x0000000000000000000000000000000000000000"
empty_staker = staker == nil || staker == "" || staker == "0x0000000000000000000000000000000000000000"
empty_staking_contract_address =
staking_contract_address == nil || staking_contract_address == "" ||
staking_contract_address == "0x0000000000000000000000000000000000000000"
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
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
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))
assign(socket, @claim_reward_long_op, nil)
else
socket
end
socket =
if s && s.task.ref == ref && s.task.pid == pid do
:ets.delete(ContractState, claim_reward_long_op_key(s.staker))
assign(socket, @claim_reward_long_op, nil)
else
socket
end
{:noreply, socket}
end
@ -339,10 +348,11 @@ defmodule BlockScoutWeb.StakesChannel do
end
def handle_out("staking_update", data, socket) do
dont_refresh_page = case Map.fetch(data, :dont_refresh_page) do
{:ok, value} -> value
_ -> false
end
dont_refresh_page =
case Map.fetch(data, :dont_refresh_page) do
{:ok, value} -> value
_ -> false
end
push(socket, "staking_update", %{
account: socket.assigns[:account],
@ -360,93 +370,138 @@ defmodule BlockScoutWeb.StakesChannel do
def find_claim_reward_pools(socket, staker, staking_contract_address) do
:ets.insert(ContractState, {claim_reward_long_op_key(staker), true})
try do
json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments)
staking_contract = ContractState.get(:staking_contract)
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)
staker_pools_length = responses[:length]
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 ->
ContractReader.call_get_reward_amount(
staking_contract_address,
[],
pool_staking_address,
staker,
json_rpc_named_arguments
)
end)
pools =
if staker_pools_length > 0 do
chunks = 0..trunc(ceil(staker_pools_length / chunk_size) - 1)
error = Enum.find_value(pools_amounts, fn result ->
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} ->
Enum.reduce(chunks, [], fn i, acc ->
responses =
ContractReader.epochs_to_claim_reward_from_request(pool_staking_address, staker)
|> ContractReader.perform_requests(%{block_reward: block_reward_contract.address}, block_reward_contract.abi)
staker
|> ContractReader.get_staker_pools_request(i * chunk_size, chunk_size)
|> ContractReader.perform_requests(%{staking: staking_contract.address}, staking_contract.abi)
epochs =
array_to_ranges(responses[:epochs])
|> Enum.map(fn {first, last} ->
Integer.to_string(first) <> (if first != last, do: "-" <> Integer.to_string(last), else: "")
acc ++
Enum.map(responses[:pools], fn pool_staking_address ->
address_bytes_to_string(pool_staking_address)
end)
data = Map.put(amounts, :epochs, Enum.join(epochs, ","))
{data, pool_staking_address}
end)
|> Enum.filter(fn {data, _} -> data.epochs != "" end)
else
[]
end
pools_gas_estimates = Enum.map(pools, fn {_data, pool_staking_address} ->
result = ContractReader.claim_reward_estimate_gas(
pools_amounts =
Enum.map(pools, fn pool_staking_address ->
ContractReader.call_get_reward_amount(
staking_contract_address,
[],
pool_staking_address,
staker,
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}
end)
error = Enum.find_value(pools_gas_estimates, fn {_, result} ->
error =
Enum.find_value(pools_gas_estimates, fn {_, result} ->
case result do
{:error, reason} -> error_reason_to_string(reason)
_ -> nil
end
end)
pools = if error == nil do
pools =
if error == nil do
pools_gas_estimates = Map.new(pools_gas_estimates)
Map.new(pools, fn {data, pool_staking_address} ->
{:ok, estimate} = pools_gas_estimates[pool_staking_address]
data = Map.put(data, :gas_estimate, estimate)
@ -456,48 +511,18 @@ defmodule BlockScoutWeb.StakesChannel do
%{}
end
{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))
{error, pools}
end
end
def recalc_claim_reward(socket, staking_contract_address, epochs, pool_staking_address, staker) do
:ets.insert(ContractState, {claim_reward_long_op_key(staker), true})
try do
json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments)
amounts_result = ContractReader.call_get_reward_amount(
staking_contract_address,
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(
amounts_result =
ContractReader.call_get_reward_amount(
staking_contract_address,
epochs,
pool_staking_address,
@ -505,22 +530,53 @@ defmodule BlockScoutWeb.StakesChannel do
json_rpc_named_arguments
)
case estimate_gas_result do
{:ok, gas_limit} ->
{nil, gas_limit}
{error, amounts} =
case amounts_result do
{:ok, amounts} ->
{nil, amounts}
{: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
else
{error, 0}
end
token = ContractState.get(:token)
coin = %Token{symbol: Explorer.coin(), decimals: Decimal.new(18)}
push(socket, "claim_reward_recalculations", %{
token_reward_sum: 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),
token_reward_sum:
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,
error: error
})
@ -534,6 +590,7 @@ defmodule BlockScoutWeb.StakesChannel do
true
else
staker = socket.assigns[:account]
with [{_, true}] <- :ets.lookup(ContractState, claim_reward_long_op_key(staker)) do
true
end
@ -544,13 +601,20 @@ defmodule BlockScoutWeb.StakesChannel do
defp array_to_ranges(numbers, prev_ranges \\ []) do
length = Enum.count(numbers)
if length > 0 do
{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
ranges
else
Enum.slice(numbers, next_index, length - next_index)
numbers
|> Enum.slice(next_index, length - next_index)
|> array_to_ranges(ranges)
end
else
@ -567,11 +631,13 @@ defmodule BlockScoutWeb.StakesChannel do
end
defp get_range(numbers) do
last_index =
Enum.with_index(numbers)
last_index =
numbers
|> Enum.with_index()
|> Enum.find_index(fn {n, i} ->
if i > 0, do: n != Enum.at(numbers, i - 1) + 1, else: false
end)
next_index = if last_index == nil, do: Enum.count(numbers), else: last_index
first = Enum.at(numbers, 0)
last = Enum.at(numbers, next_index - 1)
@ -599,4 +665,85 @@ defmodule BlockScoutWeb.StakesChannel do
staker = if staker == nil, do: "", else: staker
Atom.to_string(@claim_reward_long_op) <> "_" <> staker
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

@ -49,72 +49,74 @@ defmodule BlockScoutWeb.StakesController do
# 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)
defp render_template(filter, conn, %{"type" => "JSON"} = params) do
{items, next_page_path} = if Map.has_key?(params, "filterMy") 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,
unless params["account"] == "" do
params["account"]
end,
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)
}
{items, next_page_path} =
if Map.has_key?(params, "filterMy") 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,
unless params["account"] == "" do
params["account"]
end,
params["filterBanned"] == "true",
params["filterMy"] == "true"
)
end)
{items, next_page_path}
else
loading_item = View.render_to_string(StakesView, "_rows_loading.html", %{})
{[loading_item], nil}
end
{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}
else
loading_item = View.render_to_string(StakesView, "_rows_loading.html", %{})
{[loading_item], nil}
end
json(
conn,

@ -4823,25 +4823,32 @@ defmodule Explorer.Chain do
end
def staking_pool_delegators(staking_address_hash, show_snapshotted_data) do
from(
d in StakingPoolsDelegator,
where:
d.staking_address_hash == ^staking_address_hash and
(d.is_active == true or ^show_snapshotted_data and d.snapshotted_stake_amount > 0 and d.is_active != true),
order_by:
[desc: d.stake_amount]
) |> Repo.all()
query =
from(
d in StakingPoolsDelegator,
where:
d.staking_address_hash == ^staking_address_hash and
(d.is_active == true or (^show_snapshotted_data and d.snapshotted_stake_amount > 0 and d.is_active != true)),
order_by: [desc: d.stake_amount]
)
query
|> Repo.all()
end
def staking_pool_snapshotted_inactive_delegators_count(staking_address_hash) do
from(
d in StakingPoolsDelegator,
where:
d.staking_address_hash == ^staking_address_hash and
d.snapshotted_stake_amount > 0 and
d.is_active != true,
select: fragment("count(*)")
) |> Repo.one()
query =
from(
d in StakingPoolsDelegator,
where:
d.staking_address_hash == ^staking_address_hash and
d.snapshotted_stake_amount > 0 and
d.is_active != true,
select: fragment("count(*)")
)
query
|> Repo.one()
end
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
option_key = runner.option_key()
runner_specific_options =
runner_specific_options =
if Map.has_key?(Enum.into(runner.__info__(:functions), %{}), :runner_specific_options) do
apply(runner, :runner_specific_options, [])
else
@ -260,8 +260,8 @@ defmodule Explorer.Chain.Import do
@local_options ~w(on_conflict params with timeout)a
defp validate_runner_options_known(runner_option_key, options, runner_specific_options) do
unknown_option_keys = Map.keys(options) -- @local_options
unknown_option_keys = unknown_option_keys -- runner_specific_options
base_unknown_option_keys = Map.keys(options) -- @local_options
unknown_option_keys = base_unknown_option_keys -- runner_specific_options
if Enum.empty?(unknown_option_keys) do
:ok

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

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

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

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

@ -12,13 +12,13 @@ defmodule Explorer.Staking.StakeSnapshotting do
alias Explorer.Staking.ContractReader
def do_snapshotting(
%{contracts: contracts, abi: abi, ets_table_name: ets_table_name},
epoch_number,
cached_pool_staking_responses,
pools_mining_addresses,
mining_to_staking_address,
block_number
) do
%{contracts: contracts, abi: abi, ets_table_name: ets_table_name},
epoch_number,
cached_pool_staking_responses,
pools_mining_addresses,
mining_to_staking_address,
block_number
) do
:ets.insert(ets_table_name, is_snapshotting: true)
# get staking addresses for the pending validators
@ -36,10 +36,18 @@ defmodule Explorer.Staking.StakeSnapshotting do
# use `cached_pool_staking_responses` when possible
pool_staking_responses =
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
{: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 ->
ContractReader.perform_requests(
ContractReader.active_delegators_request(staking_address_hash, block_number) ++
@ -156,10 +164,14 @@ defmodule Explorer.Staking.StakeSnapshotting do
# perform SQL queries
case Chain.import(%{
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},
timeout: :infinity
}) do
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
},
timeout: :infinity
}) do
{:ok, _} -> :ets.insert(ets_table_name, snapshotted_epoch_number: epoch_number)
_ -> Logger.error("Cannot successfully finish snapshotting for the epoch #{epoch_number - 1}")
end
@ -172,7 +184,10 @@ defmodule Explorer.Staking.StakeSnapshotting do
defp snapshotted_pool_amounts_requests(pool_staking_address, block_number) do
[
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

@ -7,9 +7,7 @@ defmodule Explorer.Repo.Migrations.RemoveDuplicateIndexes do
)
drop_if_exists(
index(:staking_pools_delegators, [:address_hash],
name: "staking_pools_delegators_address_hash_index"
)
index(:staking_pools_delegators, [:address_hash], name: "staking_pools_delegators_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
def change do
create(
create(
index(:staking_pools_delegators, [:staking_address_hash, :snapshotted_stake_amount, :is_active],
unique: false,
name: :snapshotted_stake_amount_index

Loading…
Cancel
Save