diff --git a/apps/block_scout_web/config/config.exs b/apps/block_scout_web/config/config.exs index 575bec2610..9b3321dc14 100644 --- a/apps/block_scout_web/config/config.exs +++ b/apps/block_scout_web/config/config.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", diff --git a/apps/block_scout_web/lib/block_scout_web/channels/stakes_channel.ex b/apps/block_scout_web/lib/block_scout_web/channels/stakes_channel.ex index 60c943ef70..62ef1a1daa 100644 --- a/apps/block_scout_web/lib/block_scout_web/channels/stakes_channel.ex +++ b/apps/block_scout_web/lib/block_scout_web/channels/stakes_channel.ex @@ -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 diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/stakes_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/stakes_controller.ex index 38e887542e..506f271b04 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/stakes_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/stakes_controller.ex @@ -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, diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index 45e0263baf..3de1589b91 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -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 diff --git a/apps/explorer/lib/explorer/chain/import.ex b/apps/explorer/lib/explorer/chain/import.ex index 86f27ddc19..3fad48ba6b 100644 --- a/apps/explorer/lib/explorer/chain/import.ex +++ b/apps/explorer/lib/explorer/chain/import.ex @@ -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 diff --git a/apps/explorer/lib/explorer/chain/import/runner/staking_pools.ex b/apps/explorer/lib/explorer/chain/import/runner/staking_pools.ex index 55b3bc9d0f..8d2871137c 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/staking_pools.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/staking_pools.ex @@ -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) diff --git a/apps/explorer/lib/explorer/chain/import/runner/staking_pools_delegators.ex b/apps/explorer/lib/explorer/chain/import/runner/staking_pools_delegators.ex index 992cd8cf83..b0e28f2ce6 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/staking_pools_delegators.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/staking_pools_delegators.ex @@ -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) diff --git a/apps/explorer/lib/explorer/staking/contract_reader.ex b/apps/explorer/lib/explorer/staking/contract_reader.ex index 0041d33850..1249127f51 100644 --- a/apps/explorer/lib/explorer/staking/contract_reader.ex +++ b/apps/explorer/lib/explorer/staking/contract_reader.ex @@ -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 diff --git a/apps/explorer/lib/explorer/staking/contract_state.ex b/apps/explorer/lib/explorer/staking/contract_state.ex index 12675fc9fb..fc4dd2a120 100644 --- a/apps/explorer/lib/explorer/staking/contract_state.ex +++ b/apps/explorer/lib/explorer/staking/contract_state.ex @@ -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 diff --git a/apps/explorer/lib/explorer/staking/stake_snapshotting.ex b/apps/explorer/lib/explorer/staking/stake_snapshotting.ex index 43b8f026f4..28a6f7400f 100644 --- a/apps/explorer/lib/explorer/staking/stake_snapshotting.ex +++ b/apps/explorer/lib/explorer/staking/stake_snapshotting.ex @@ -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 diff --git a/apps/explorer/priv/repo/migrations/20190807111216_remove_duplicate_indexes.exs b/apps/explorer/priv/repo/migrations/20190807111216_remove_duplicate_indexes.exs index 5c2ce80691..45d873e1de 100644 --- a/apps/explorer/priv/repo/migrations/20190807111216_remove_duplicate_indexes.exs +++ b/apps/explorer/priv/repo/migrations/20190807111216_remove_duplicate_indexes.exs @@ -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")) diff --git a/apps/explorer/priv/repo/migrations/20191128144250_add_index_for_snapshotted_stake_amount.exs b/apps/explorer/priv/repo/migrations/20191128144250_add_index_for_snapshotted_stake_amount.exs index e7a0836235..e3b5432f9f 100644 --- a/apps/explorer/priv/repo/migrations/20191128144250_add_index_for_snapshotted_stake_amount.exs +++ b/apps/explorer/priv/repo/migrations/20191128144250_add_index_for_snapshotted_stake_amount.exs @@ -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