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. 307
      apps/block_scout_web/lib/block_scout_web/channels/stakes_channel.ex
  3. 4
      apps/block_scout_web/lib/block_scout_web/controllers/stakes_controller.ex
  4. 17
      apps/explorer/lib/explorer/chain.ex
  5. 4
      apps/explorer/lib/explorer/chain/import.ex
  6. 14
      apps/explorer/lib/explorer/chain/import/runner/staking_pools.ex
  7. 6
      apps/explorer/lib/explorer/chain/import/runner/staking_pools_delegators.ex
  8. 80
      apps/explorer/lib/explorer/staking/contract_reader.ex
  9. 483
      apps/explorer/lib/explorer/staking/contract_state.ex
  10. 21
      apps/explorer/lib/explorer/staking/stake_snapshotting.ex
  11. 4
      apps/explorer/priv/repo/migrations/20190807111216_remove_duplicate_indexes.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})
}
staking_contract_address =
try do
ContractState.get(:staking_contract).address
after
end
{:reply, {:ok, %{html: result.html}}, result.socket}
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}
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
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,7 +348,8 @@ defmodule BlockScoutWeb.StakesChannel do
end
def handle_out("staking_update", data, socket) do
dont_refresh_page = case Map.fetch(data, :dont_refresh_page) do
dont_refresh_page =
case Map.fetch(data, :dont_refresh_page) do
{:ok, value} -> value
_ -> false
end
@ -360,23 +370,32 @@ 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
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)
staker
|> ContractReader.get_staker_pools_request(i * chunk_size, chunk_size)
|> ContractReader.perform_requests(%{staking: staking_contract.address}, staking_contract.abi)
acc ++ Enum.map(responses[:pools], fn pool_staking_address ->
acc ++
Enum.map(responses[:pools], fn pool_staking_address ->
address_bytes_to_string(pool_staking_address)
end)
end)
@ -384,7 +403,8 @@ defmodule BlockScoutWeb.StakesChannel do
[]
end
pools_amounts = Enum.map(pools, fn pool_staking_address ->
pools_amounts =
Enum.map(pools, fn pool_staking_address ->
ContractReader.call_get_reward_amount(
staking_contract_address,
[],
@ -394,14 +414,37 @@ defmodule BlockScoutWeb.StakesChannel do
)
end)
error = Enum.find_value(pools_amounts, fn result ->
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, 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)
@ -413,40 +456,52 @@ defmodule BlockScoutWeb.StakesChannel do
|> Enum.filter(fn {amounts, _} -> amounts.token_reward_sum > 0 || amounts.native_reward_sum > 0 end)
|> Enum.map(fn {amounts, pool_staking_address} ->
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)
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 =
array_to_ranges(responses[:epochs])
responses[:epochs]
|> array_to_ranges()
|> Enum.map(fn {first, last} ->
Integer.to_string(first) <> (if first != last, do: "-" <> Integer.to_string(last), else: "")
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(
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)
@ -458,30 +513,16 @@ defmodule BlockScoutWeb.StakesChannel do
{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
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(
amounts_result =
ContractReader.call_get_reward_amount(
staking_contract_address,
epochs,
pool_staking_address,
@ -489,15 +530,19 @@ defmodule BlockScoutWeb.StakesChannel do
json_rpc_named_arguments
)
{error, amounts} = case amounts_result do
{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(
{error, gas_limit} =
if error == nil do
estimate_gas_result =
ContractReader.claim_reward_estimate_gas(
staking_contract_address,
epochs,
pool_staking_address,
@ -508,6 +553,7 @@ defmodule BlockScoutWeb.StakesChannel do
case estimate_gas_result do
{:ok, gas_limit} ->
{nil, gas_limit}
{:error, reason} ->
{error_reason_to_string(reason), 0}
end
@ -519,8 +565,18 @@ defmodule BlockScoutWeb.StakesChannel do
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
@ -568,10 +632,12 @@ defmodule BlockScoutWeb.StakesChannel do
defp get_range(numbers) do
last_index =
Enum.with_index(numbers)
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,7 +49,8 @@ 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
{items, next_page_path} =
if Map.has_key?(params, "filterMy") do
[paging_options: options] = paging_options(params)
last_index =
@ -110,6 +111,7 @@ defmodule BlockScoutWeb.StakesController do
}
)
end)
{items, next_page_path}
else
loading_item = View.render_to_string(StakesView, "_rows_loading.html", %{})

@ -4823,17 +4823,21 @@ defmodule Explorer.Chain do
end
def staking_pool_delegators(staking_address_hash, show_snapshotted_data) do
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]
) |> Repo.all()
(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
query =
from(
d in StakingPoolsDelegator,
where:
@ -4841,7 +4845,10 @@ defmodule Explorer.Chain do
d.snapshotted_stake_amount > 0 and
d.is_active != true,
select: fragment("count(*)")
) |> Repo.one()
)
query
|> Repo.one()
end
def staking_pool_delegator(staking_address_hash, address_hash) do

@ -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,18 +43,20 @@ 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
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
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)
else
multi
end
multi
@ -84,7 +86,8 @@ 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
query =
if clear_snapshotted_values do
from(
pool in StakingPool,
update: [
@ -97,6 +100,7 @@ defmodule Explorer.Chain.Import.Runner.StakingPools do
)
else
addresses = Enum.map(changes_list, & &1.staking_address_hash)
from(
pool in StakingPool,
where: pool.staking_address_hash not in ^addresses,

@ -57,12 +57,14 @@ 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
clear_snapshotted_values =
case Map.fetch(options, :clear_snapshotted_values) do
{:ok, v} -> v
:error -> false
end
query = if clear_snapshotted_values do
query =
if clear_snapshotted_values do
from(
d in StakingPoolsDelegator,
update: [set: [snapshotted_reward_ratio: nil, snapshotted_stake_amount: nil]]

@ -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: [%{
params: [
%{
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
{: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
@ -116,26 +132,40 @@ 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: [%{
params: [
%{
from: staker,
to: staking_contract_address,
gasPrice: "0x3B9ACA00", # 1 gwei
# 1 gwei
gasPrice: "0x3B9ACA00",
data: data
}]
}) |> EthereumJSONRPC.json_rpc(json_rpc_named_arguments)
}
]
}
result =
request
|> EthereumJSONRPC.request()
|> EthereumJSONRPC.json_rpc(json_rpc_named_arguments)
case result do
{:ok, response} ->
@ -143,7 +173,9 @@ defmodule Explorer.Staking.ContractReader do
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
validators =
get_validators(
start_snapshotting,
global_responses,
contracts,
abi
)
# miningToStakingAddress mapping
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
)
%{
"getPendingValidators" => {:ok, [validators_pending]},
"validatorsToBeFinalized" => {:ok, [validators_to_be_finalized]}
} = Reader.query_contract(contracts.validator_set, abi, %{
"getPendingValidators" => [],
"validatorsToBeFinalized" => []
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
})
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
}
# 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
%{all: global_responses.validators}
base_settings
end
end
# miningToStakingAddress mapping
mining_to_staking_address =
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
# 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
)
defp get_responses(pools, block_number, contracts, abi) do
# read pool info from the contracts by its staking address
pool_staking_responses =
pools
@ -233,12 +304,14 @@ 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 =
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([
@ -249,13 +322,16 @@ defmodule Explorer.Staking.ContractState do
])
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)
# 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 =
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]
@ -269,21 +345,121 @@ defmodule Explorer.Staking.ContractState do
])
end)
|> ContractReader.perform_grouped_requests(delegator_keys, contracts, abi)
end
# 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)
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 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(%{})
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
snapshotted_epoch_number = get(:snapshotted_epoch_number)
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
# form entries for writing to the `staking_pools` table in DB
pool_entries =
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
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]
@ -292,7 +468,12 @@ defmodule Explorer.Staking.ContractState do
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
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
@ -334,40 +515,24 @@ defmodule Explorer.Staking.ContractState do
])
)
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)
# 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
defp at_start_snapshotting(block_number) 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([%{
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]
}
])[:ok]
token_params =
token_contract_address_hash
@ -377,32 +542,51 @@ defmodule Explorer.Staking.ContractState do
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_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, [
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
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
cached_pool_staking_responses =
if epoch_very_beginning do
pool_staking_responses
else
%{}
@ -412,79 +596,74 @@ defmodule Explorer.Staking.ContractState do
%{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 addresses of pending/current validators
validators.for_snapshot,
mining_to_staking_address,
global_responses.epoch_start_block - 1 # the last block of the previous staking epoch
# the last block of the previous staking epoch
global_responses.epoch_start_block - 1
])
end
# notify the UI about new block
Publisher.broadcast(:staking_update)
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_token(address) do
if address == "0x0000000000000000000000000000000000000000" do
nil # the token address is empty, so return nil
# the token address is empty, so return nil
nil
else
with {:ok, address_hash} <- Chain.string_to_address_hash(address) do
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} ->
token # the token is read from DB
# the token is read from DB
token
_ ->
fetch_token(address, address_hash)
end
_ ->
# the token address has incorrect format
nil
end
end
end
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, %{
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(%{
import_result =
Chain.import(%{
addresses: %{params: [%{hash: address}], on_conflict: :nothing},
tokens: %{params: [token_params]}
})
with {:ok, _} <- import_result do
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
else
_ -> nil # cannot write the token info to DB
end
else
nil # cannot read the token info from its contract
end
_ ->
# cannot write the token info to DB
nil
end
else
_ -> nil # the token address has incorrect format
end
# cannot read the token info from its contract
nil
end
end

@ -39,7 +39,15 @@ defmodule Explorer.Staking.StakeSnapshotting do
|> 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) ++
@ -157,7 +165,11 @@ 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},
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)
@ -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"))

Loading…
Cancel
Save