diff --git a/apps/block_scout_web/assets/js/lib/queue.js b/apps/block_scout_web/assets/js/lib/queue.js new file mode 100644 index 0000000000..a092452954 --- /dev/null +++ b/apps/block_scout_web/assets/js/lib/queue.js @@ -0,0 +1,72 @@ +/* + +Queue.js + +A function to represent a queue + +Created by Kate Morley - http://code.iamkate.com/ - and released under the terms +of the CC0 1.0 Universal legal code: + +http://creativecommons.org/publicdomain/zero/1.0/legalcode + +*/ + +/* eslint-disable */ + +/* Creates a new queue. A queue is a first-in-first-out (FIFO) data structure - + * items are added to the end of the queue and removed from the front. + */ +module.exports = function Queue(){ + + // initialise the queue and offset + var queue = []; + var offset = 0; + + // Returns the length of the queue. + this.getLength = function(){ + return (queue.length - offset); + } + + // Returns true if the queue is empty, and false otherwise. + this.isEmpty = function(){ + return (queue.length == 0); + } + + /* Enqueues the specified item. The parameter is: + * + * item - the item to enqueue + */ + this.enqueue = function(item){ + queue.push(item); + } + + /* Dequeues an item and returns it. If the queue is empty, the value + * 'undefined' is returned. + */ + this.dequeue = function(){ + + // if the queue is empty, return immediately + if (queue.length == 0) return undefined; + + // store the item at the front of the queue + var item = queue[offset]; + + // increment the offset and remove the free space if necessary + if (++ offset * 2 >= queue.length){ + queue = queue.slice(offset); + offset = 0; + } + + // return the dequeued item + return item; + + } + + /* Returns the item at the front of the queue (without dequeuing it). If the + * queue is empty then undefined is returned. + */ + this.peek = function(){ + return (queue.length > 0 ? queue[offset] : undefined); + } + +} diff --git a/apps/block_scout_web/assets/js/pages/stakes.js b/apps/block_scout_web/assets/js/pages/stakes.js index aa12aa3268..aa74bfac4a 100644 --- a/apps/block_scout_web/assets/js/pages/stakes.js +++ b/apps/block_scout_web/assets/js/pages/stakes.js @@ -5,6 +5,7 @@ import _ from 'lodash' import { subscribeChannel } from '../socket' import { connectElements } from '../lib/redux_helpers.js' import { createAsyncLoadStore, refreshPage } from '../lib/async_listing_load' +import Queue from '../lib/queue' import Web3 from 'web3' import { openPoolInfoModal } from './stakes/validator_info' import { openDelegatorsListModal } from './stakes/delegators_list' @@ -25,6 +26,7 @@ export const initialState = { blockRewardContract: null, channel: null, currentBlockNumber: 0, // current block number + finishRequestResolve: null, lastEpochNumber: 0, network: null, refreshBlockNumber: 0, // last page refresh block number @@ -86,7 +88,8 @@ export function reducer (state = initialState, action) { } case 'PAGE_REFRESHED': { return Object.assign({}, state, { - refreshBlockNumber: action.refreshBlockNumber + refreshBlockNumber: action.refreshBlockNumber, + finishRequestResolve: action.finishRequestResolve }) } case 'RECEIVED_UPDATE': { @@ -108,6 +111,12 @@ export function reducer (state = initialState, action) { } case 'FINISH_REQUEST': { $(stakesPageSelector).fadeTo(0, 1) + if (state.finishRequestResolve) { + state.finishRequestResolve() + return Object.assign({}, state, { + finishRequestResolve: null + }) + } return state } default: @@ -139,10 +148,17 @@ if ($stakesPage.length) { const channel = subscribeChannel('stakes:staking_update') store.dispatch({ type: 'CHANNEL_CONNECTED', channel }) - channel.on('staking_update', msg => { + let updating = false + + async function onStakingUpdate (msg) { // eslint-disable-line no-inner-declarations const state = store.getState() + + if (state.finishRequestResolve || updating) { + return + } + updating = true + const firstMsg = (state.currentBlockNumber === 0) - const accountChanged = (msg.account !== state.account) store.dispatch({ type: 'BLOCK_CREATED', currentBlockNumber: msg.block_number }) @@ -153,7 +169,7 @@ if ($stakesPage.length) { $stakesTop.html(msg.top_html) - if (accountChanged) { + if (msg.account !== state.account) { store.dispatch({ type: 'ACCOUNT_UPDATED', account: msg.account }) resetFilterMy(store) } @@ -162,16 +178,15 @@ if ($stakesPage.length) { msg.staking_allowed !== state.stakingAllowed || msg.epoch_number > state.lastEpochNumber || msg.validator_set_apply_block !== state.validatorSetApplyBlock || - (state.refreshInterval && msg.block_number >= state.refreshBlockNumber + state.refreshInterval) + (state.refreshInterval && msg.block_number >= state.refreshBlockNumber + state.refreshInterval) || + msg.account !== state.account || msg.by_set_account ) { - if (firstMsg || accountChanged) { + if (firstMsg) { // Don't refresh the page for the first load - // as it is already refreshed by `initialize` function. - // Also, don't refresh that after reconnect - // as it is already refreshed by `setAccount` function. + // as it is already refreshed by the `initialize` function. msg.dont_refresh_page = true } - reloadPoolList(msg, store) + await reloadPoolList(msg, store) } const refreshBlockNumber = store.getState().refreshBlockNumber @@ -185,13 +200,36 @@ if ($stakesPage.length) { const $refreshInformerLink = $refreshInformer.find('a') $refreshInformerLink.off('click') - $refreshInformerLink.on('click', (event) => { + $refreshInformerLink.on('click', async (event) => { event.preventDefault() - $refreshInformer.hide() - $stakesPage.fadeTo(0, 0.5) - delete msg.dont_refresh_page // refresh anyway - reloadPoolList(msg, store) + if (!store.getState().finishRequestResolve) { + $refreshInformer.hide() + $stakesPage.fadeTo(0, 0.5) + delete msg.dont_refresh_page // refresh anyway + await reloadPoolList(msg, store) + } }) + + updating = false + } + + const messagesQueue = new Queue() + + setTimeout(async () => { + while (true) { + const msg = messagesQueue.dequeue() + if (msg) { + // Synchronously handle the message + await onStakingUpdate(msg) + } else { + // Wait for the next message + await new Promise(resolve => setTimeout(resolve, 10)) + } + } + }, 0) + + channel.on('staking_update', msg => { + messagesQueue.enqueue(msg) }) channel.on('contracts', msg => { @@ -298,10 +336,12 @@ async function checkNetworkAndAccount (store, web3) { const accounts = await web3.eth.getAccounts() const account = accounts[0] ? accounts[0].toLowerCase() : null - if (account !== state.account) { - setAccount(account, store) - } else if (refresh) { - refreshPageWrapper(store) + if (account !== state.account && await setAccount(account, store)) { + refresh = false // because refreshing will be done by `onStakingUpdate` + } + + if (refresh) { + await refreshPageWrapper(store) } setTimeout(() => { @@ -320,21 +360,29 @@ async function loginByMetamask () { } } -function refreshPageWrapper (store) { +async function refreshPageWrapper (store) { + while (store.getState().finishRequestResolve) { + // Don't let anything simultaneously refresh the page + await new Promise(resolve => setTimeout(resolve, 10)) + } + let currentBlockNumber = store.getState().currentBlockNumber if (!currentBlockNumber) { currentBlockNumber = $('[data-block-number]', $stakesTop).data('blockNumber') } - refreshPage(store) - store.dispatch({ - type: 'PAGE_REFRESHED', - refreshBlockNumber: currentBlockNumber + await new Promise(resolve => { + store.dispatch({ + type: 'PAGE_REFRESHED', + refreshBlockNumber: currentBlockNumber, + finishRequestResolve: resolve + }) + $refreshInformer.hide() + refreshPage(store) }) - $refreshInformer.hide() } -function reloadPoolList (msg, store) { +async function reloadPoolList (msg, store) { store.dispatch({ type: 'RECEIVED_UPDATE', lastEpochNumber: msg.epoch_number, @@ -343,7 +391,7 @@ function reloadPoolList (msg, store) { validatorSetApplyBlock: msg.validator_set_apply_block }) if (!msg.dont_refresh_page) { - refreshPageWrapper(store) + await refreshPageWrapper(store) } } @@ -353,24 +401,32 @@ function resetFilterMy (store) { } function setAccount (account, store) { - store.dispatch({ type: 'ACCOUNT_UPDATED', account }) - if (!account) { - resetFilterMy(store) - } + return new Promise(resolve => { + store.dispatch({ type: 'ACCOUNT_UPDATED', account }) + if (!account) { + resetFilterMy(store) + } - const errorMsg = 'Cannot properly set account due to connection loss. Please, reload the page.' - const $addressField = $('.stakes-top-stats-item-address .stakes-top-stats-value') - $addressField.html('Loading...') - store.getState().channel.push( - 'set_account', account - ).receive('ok', () => { - $addressField.html(account) - refreshPageWrapper(store) - hideCurrentModal() - }).receive('error', () => { - openErrorModal('Change account', errorMsg, true) - }).receive('timeout', () => { - openErrorModal('Change account', errorMsg, true) + const errorMsg = 'Cannot properly set account due to connection loss. Please, reload the page.' + const $addressField = $('.stakes-top-stats-item-address .stakes-top-stats-value') + $addressField.html('Loading...') + store.getState().channel.push( + 'set_account', account + ).receive('ok', () => { + $addressField.html(` +
+ ${account} +
+ `) + hideCurrentModal() + resolve(true) + }).receive('error', () => { + openErrorModal('Change account', errorMsg, true) + resolve(false) + }).receive('timeout', () => { + openErrorModal('Change account', errorMsg, true) + resolve(false) + }) }) } @@ -395,6 +451,17 @@ function updateFilters (store, filterType) { const filterBanned = $stakesPage.find('[pool-filter-banned]') const filterMy = $stakesPage.find('[pool-filter-my]') const state = store.getState() + + if (state.finishRequestResolve) { + if (filterType === 'my') { + filterMy.prop('checked', !filterMy.prop('checked')) + } else { + filterBanned.prop('checked', !filterBanned.prop('checked')) + } + openWarningModal('Still loading', 'The previous request to load pool list is not yet finished. Please, wait...') + return + } + if (filterType === 'my' && !state.account) { filterMy.prop('checked', false) openWarningModal('Unauthorized', 'Please login with MetaMask') diff --git a/apps/block_scout_web/assets/js/pages/stakes/utils.js b/apps/block_scout_web/assets/js/pages/stakes/utils.js index 315f6ebae0..d06044f57f 100644 --- a/apps/block_scout_web/assets/js/pages/stakes/utils.js +++ b/apps/block_scout_web/assets/js/pages/stakes/utils.js @@ -10,8 +10,8 @@ export async function makeContractCall (call, store, gasLimit, callbackFunc) { if (!callbackFunc) { callbackFunc = function (errorMessage) { if (!errorMessage) { - state.refreshPageFunc(store) openSuccessModal('Success', 'Transaction is confirmed.') + state.refreshPageFunc(store) } else { openErrorModal('Error', errorMessage) } diff --git a/apps/block_scout_web/lib/block_scout_web/application.ex b/apps/block_scout_web/lib/block_scout_web/application.ex index 9ec8962fa9..86f923482f 100644 --- a/apps/block_scout_web/lib/block_scout_web/application.ex +++ b/apps/block_scout_web/lib/block_scout_web/application.ex @@ -8,6 +8,7 @@ defmodule BlockScoutWeb.Application do alias BlockScoutWeb.Counters.BlocksIndexedCounter alias BlockScoutWeb.{Endpoint, Prometheus} alias BlockScoutWeb.RealtimeEventHandler + alias BlockScoutWeb.StakingEventHandler def start(_type, _args) do import Supervisor.Spec @@ -22,6 +23,7 @@ defmodule BlockScoutWeb.Application do supervisor(Endpoint, []), {Absinthe.Subscription, Endpoint}, {RealtimeEventHandler, name: RealtimeEventHandler}, + {StakingEventHandler, name: StakingEventHandler}, {BlocksIndexedCounter, name: BlocksIndexedCounter} ] 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 c1254c1e73..8b8b4c42fa 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 @@ -63,18 +63,22 @@ defmodule BlockScoutWeb.StakesChannel do |> assign(:mining_address, mining_address) |> push_contracts() - handle_out( - "staking_update", - %{ - block_number: BlockNumber.get_max(), - dont_refresh_page: true, - epoch_number: ContractState.get(:epoch_number, 0), - staking_allowed: ContractState.get(:staking_allowed, false), - staking_token_defined: ContractState.get(:token, nil) != nil, - validator_set_apply_block: ContractState.get(:validator_set_apply_block, 0) - }, - socket - ) + data = + case Map.fetch(socket.assigns, :staking_update_data) do + {:ok, staking_update_data} -> + staking_update_data + + _ -> + %{ + block_number: BlockNumber.get_max(), + epoch_number: ContractState.get(:epoch_number, 0), + staking_allowed: ContractState.get(:staking_allowed, false), + staking_token_defined: ContractState.get(:token, nil) != nil, + validator_set_apply_block: ContractState.get(:validator_set_apply_block, 0) + } + end + + handle_out("staking_update", Map.merge(data, %{by_set_account: true}), socket) {:reply, :ok, socket} end @@ -399,16 +403,30 @@ defmodule BlockScoutWeb.StakesChannel do end def handle_out("staking_update", data, socket) do - dont_refresh_page = - case Map.fetch(data, :dont_refresh_page) do + by_set_account = + case Map.fetch(data, :by_set_account) do {:ok, value} -> value _ -> false end + socket = + if by_set_account do + # if :by_set_account is in the `data`, + # it means that this function was called by + # handle_in("set_account", ...), so we + # shouldn't assign the incoming data to the socket + socket + else + # otherwise, we should do the assignment + # to use the incoming data later by + # handle_in("set_account", ...) and StakesController.render_top + assign(socket, :staking_update_data, data) + end + push(socket, "staking_update", %{ account: socket.assigns[:account], block_number: data.block_number, - dont_refresh_page: dont_refresh_page, + by_set_account: by_set_account, epoch_number: data.epoch_number, staking_allowed: data.staking_allowed, staking_token_defined: data.staking_token_defined, 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 83a6866d3e..0916553a5a 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 @@ -19,11 +19,21 @@ defmodule BlockScoutWeb.StakesController do # when a new block appears (see `staking_update` event handled in `StakesChannel`), # or when the page is loaded for the first time or reloaded by a user (i.e. it is called by the `render_template(filter, conn, _)`) def render_top(conn) do - active_pools_length = ContractState.get(:active_pools_length, 0) - block_number = BlockNumber.get_max() - epoch_end_block = ContractState.get(:epoch_end_block, 0) - epoch_number = ContractState.get(:epoch_number, 0) - max_candidates = ContractState.get(:max_candidates, 0) + staking_data = + case Map.fetch(conn.assigns, :staking_update_data) do + {:ok, data} -> + data + + _ -> + %{ + active_pools_length: ContractState.get(:active_pools_length, 0), + block_number: BlockNumber.get_max(), + epoch_end_block: ContractState.get(:epoch_end_block, 0), + epoch_number: ContractState.get(:epoch_number, 0), + max_candidates: ContractState.get(:max_candidates, 0) + } + end + token = ContractState.get(:token, %Token{}) account = @@ -38,14 +48,17 @@ defmodule BlockScoutWeb.StakesController do }) end - epoch_end_in = if epoch_end_block - block_number >= 0, do: epoch_end_block - block_number, else: 0 + epoch_end_in = + if staking_data.epoch_end_block - staking_data.block_number >= 0, + do: staking_data.epoch_end_block - staking_data.block_number, + else: 0 View.render_to_string(StakesView, "_stakes_top.html", account: account, - block_number: block_number, - candidates_limit_reached: active_pools_length >= max_candidates, + block_number: staking_data.block_number, + candidates_limit_reached: staking_data.active_pools_length >= staking_data.max_candidates, epoch_end_in: epoch_end_in, - epoch_number: epoch_number, + epoch_number: staking_data.epoch_number, token: token ) end diff --git a/apps/block_scout_web/lib/block_scout_web/notifier.ex b/apps/block_scout_web/lib/block_scout_web/notifier.ex index 21629d1167..d492612e53 100644 --- a/apps/block_scout_web/lib/block_scout_web/notifier.ex +++ b/apps/block_scout_web/lib/block_scout_web/notifier.ex @@ -7,13 +7,11 @@ defmodule BlockScoutWeb.Notifier do alias BlockScoutWeb.{AddressContractVerificationView, Endpoint} alias Explorer.{Chain, Market, Repo} alias Explorer.Chain.{Address, InternalTransaction, TokenTransfer, Transaction} - alias Explorer.Chain.Cache.BlockNumber alias Explorer.Chain.Supply.RSK alias Explorer.Chain.Transaction.History.TransactionStats alias Explorer.Counters.AverageBlockTime alias Explorer.ExchangeRates.Token alias Explorer.SmartContract.{Solidity.CodeCompiler, Solidity.CompilerVersion} - alias Explorer.Staking.ContractState alias Phoenix.View def handle_event({:chain_event, :addresses, type, addresses}) when type in [:realtime, :on_demand] do @@ -107,16 +105,6 @@ defmodule BlockScoutWeb.Notifier do }) end - def handle_event({:chain_event, :staking_update}) do - Endpoint.broadcast("stakes:staking_update", "staking_update", %{ - block_number: BlockNumber.get_max(), - epoch_number: ContractState.get(:epoch_number, 0), - staking_allowed: ContractState.get(:staking_allowed, false), - staking_token_defined: ContractState.get(:token, nil) != nil, - validator_set_apply_block: ContractState.get(:validator_set_apply_block, 0) - }) - end - def handle_event({:chain_event, :internal_transactions, :realtime, internal_transactions}) do internal_transactions |> Stream.map( diff --git a/apps/block_scout_web/lib/block_scout_web/realtime_event_handler.ex b/apps/block_scout_web/lib/block_scout_web/realtime_event_handler.ex index c9e42f3b8e..45126bace1 100644 --- a/apps/block_scout_web/lib/block_scout_web/realtime_event_handler.ex +++ b/apps/block_scout_web/lib/block_scout_web/realtime_event_handler.ex @@ -28,7 +28,6 @@ defmodule BlockScoutWeb.RealtimeEventHandler do # Does not come from the indexer Subscriber.to(:exchange_rate) Subscriber.to(:transaction_stats) - Subscriber.to(:staking_update) {:ok, []} end diff --git a/apps/block_scout_web/lib/block_scout_web/staking_event_handler.ex b/apps/block_scout_web/lib/block_scout_web/staking_event_handler.ex new file mode 100644 index 0000000000..99f2e30bf5 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/staking_event_handler.ex @@ -0,0 +1,26 @@ +defmodule BlockScoutWeb.StakingEventHandler do + @moduledoc """ + Subscribing process for broadcast events from staking app. + """ + + use GenServer + + alias BlockScoutWeb.Endpoint + alias Explorer.Chain.Events.Subscriber + + def start_link(_) do + GenServer.start_link(__MODULE__, [], name: __MODULE__) + end + + @impl true + def init([]) do + Subscriber.to(:staking_update, :realtime) + {:ok, []} + end + + @impl true + def handle_info({:chain_event, :staking_update, :realtime, data}, state) do + Endpoint.broadcast("stakes:staking_update", "staking_update", data) + {:noreply, state} + end +end diff --git a/apps/block_scout_web/test/block_scout_web/channels/stakes_channel_test.exs b/apps/block_scout_web/test/block_scout_web/channels/stakes_channel_test.exs index 35a1046325..72880728cf 100644 --- a/apps/block_scout_web/test/block_scout_web/channels/stakes_channel_test.exs +++ b/apps/block_scout_web/test/block_scout_web/channels/stakes_channel_test.exs @@ -1,13 +1,21 @@ defmodule BlockScoutWeb.StakesChannelTest do use BlockScoutWeb.ChannelCase - alias BlockScoutWeb.Notifier + alias BlockScoutWeb.StakingEventHandler test "subscribed user is notified of staking_update event" do topic = "stakes:staking_update" @endpoint.subscribe(topic) - Notifier.handle_event({:chain_event, :staking_update}) + data = %{ + block_number: 76, + epoch_number: 0, + staking_allowed: false, + staking_token_defined: false, + validator_set_apply_block: 0 + } + + StakingEventHandler.handle_info({:chain_event, :staking_update, :realtime, data}, nil) receive do %Phoenix.Socket.Broadcast{topic: ^topic, event: "staking_update", payload: %{epoch_number: _}} -> diff --git a/apps/explorer/lib/explorer/chain/events/publisher.ex b/apps/explorer/lib/explorer/chain/events/publisher.ex index 29d36be7ed..72a129dc10 100644 --- a/apps/explorer/lib/explorer/chain/events/publisher.ex +++ b/apps/explorer/lib/explorer/chain/events/publisher.ex @@ -3,7 +3,7 @@ defmodule Explorer.Chain.Events.Publisher do Publishes events related to the Chain context. """ - @allowed_events ~w(addresses address_coin_balances address_token_balances blocks block_rewards internal_transactions last_block_number token_transfers transactions contract_verification_result)a + @allowed_events ~w(addresses address_coin_balances address_token_balances blocks block_rewards internal_transactions last_block_number staking_update token_transfers transactions contract_verification_result)a def broadcast(_data, false), do: :ok diff --git a/apps/explorer/lib/explorer/chain/events/subscriber.ex b/apps/explorer/lib/explorer/chain/events/subscriber.ex index 91e1381afd..c363e459d7 100644 --- a/apps/explorer/lib/explorer/chain/events/subscriber.ex +++ b/apps/explorer/lib/explorer/chain/events/subscriber.ex @@ -3,11 +3,11 @@ defmodule Explorer.Chain.Events.Subscriber do Subscribes to events related to the Chain context. """ - @allowed_broadcast_events ~w(addresses address_coin_balances address_token_balances blocks block_rewards internal_transactions last_block_number token_transfers transactions contract_verification_result)a + @allowed_broadcast_events ~w(addresses address_coin_balances address_token_balances blocks block_rewards internal_transactions last_block_number staking_update token_transfers transactions contract_verification_result)a @allowed_broadcast_types ~w(catchup realtime on_demand contract_verification_result)a - @allowed_events ~w(exchange_rate transaction_stats staking_update)a + @allowed_events ~w(exchange_rate transaction_stats)a @type broadcast_type :: :realtime | :catchup | :on_demand diff --git a/apps/explorer/lib/explorer/smart_contract/reader.ex b/apps/explorer/lib/explorer/smart_contract/reader.ex index e1b15746ed..f8e0fe4f86 100644 --- a/apps/explorer/lib/explorer/smart_contract/reader.ex +++ b/apps/explorer/lib/explorer/smart_contract/reader.ex @@ -138,6 +138,32 @@ defmodule Explorer.SmartContract.Reader do end) end + @spec query_contract_by_block_number( + String.t(), + term(), + functions(), + non_neg_integer() + ) :: functions_results() + def query_contract_by_block_number(contract_address, abi, functions, block_number) do + requests = + functions + |> Enum.map(fn {method_id, args} -> + %{ + contract_address: contract_address, + method_id: method_id, + args: args, + block_number: block_number + } + end) + + requests + |> query_contracts(abi) + |> Enum.zip(requests) + |> Enum.into(%{}, fn {response, request} -> + {request.method_id, response} + end) + end + @doc """ Runs batch of contract functions on given addresses for smart contract with an expected ABI and functions. diff --git a/apps/explorer/lib/explorer/staking/contract_reader.ex b/apps/explorer/lib/explorer/staking/contract_reader.ex index afa2352a2c..5307646c95 100644 --- a/apps/explorer/lib/explorer/staking/contract_reader.ex +++ b/apps/explorer/lib/explorer/staking/contract_reader.ex @@ -5,38 +5,38 @@ defmodule Explorer.Staking.ContractReader do alias Explorer.SmartContract.Reader - def global_requests do + def global_requests(block_number) do [ # 673a2a1f = keccak256(getPools()) - active_pools: {:staking, "673a2a1f", []}, + active_pools: {:staking, "673a2a1f", [], block_number}, # 8c2243ae = keccak256(stakingEpochEndBlock()) - epoch_end_block: {:staking, "8c2243ae", []}, + epoch_end_block: {:staking, "8c2243ae", [], block_number}, # 794c0c68 = keccak256(stakingEpoch()) - epoch_number: {:staking, "794c0c68", []}, + epoch_number: {:staking, "794c0c68", [], block_number}, # 7069e746 = keccak256(stakingEpochStartBlock()) - epoch_start_block: {:staking, "7069e746", []}, + epoch_start_block: {:staking, "7069e746", [], block_number}, # df6f55f5 = keccak256(getPoolsInactive()) - inactive_pools: {:staking, "df6f55f5", []}, + inactive_pools: {:staking, "df6f55f5", [], block_number}, # f0786096 = keccak256(MAX_CANDIDATES()) - max_candidates: {:staking, "f0786096", []}, + max_candidates: {:staking, "f0786096", [], block_number}, # 5fef7643 = keccak256(candidateMinStake()) - min_candidate_stake: {:staking, "5fef7643", []}, + min_candidate_stake: {:staking, "5fef7643", [], block_number}, # da7a9b6a = keccak256(delegatorMinStake()) - min_delegator_stake: {:staking, "da7a9b6a", []}, + min_delegator_stake: {:staking, "da7a9b6a", [], block_number}, # 957950a7 = keccak256(getPoolsLikelihood()) - pools_likelihood: {:staking, "957950a7", []}, + pools_likelihood: {:staking, "957950a7", [], block_number}, # a5d54f65 = keccak256(getPoolsToBeElected()) - pools_to_be_elected: {:staking, "a5d54f65", []}, + pools_to_be_elected: {:staking, "a5d54f65", [], block_number}, # f4942501 = keccak256(areStakeAndWithdrawAllowed()) - staking_allowed: {:staking, "f4942501", []}, + staking_allowed: {:staking, "f4942501", [], block_number}, # 2d21d217 = keccak256(erc677TokenContract()) - token_contract_address: {:staking, "2d21d217", []}, + token_contract_address: {:staking, "2d21d217", [], block_number}, # 704189ca = keccak256(unremovableValidator()) - unremovable_validator: {:validator_set, "704189ca", []}, + unremovable_validator: {:validator_set, "704189ca", [], block_number}, # b7ab4db5 = keccak256(getValidators()) - validators: {:validator_set, "b7ab4db5", []}, + validators: {:validator_set, "b7ab4db5", [], block_number}, # b927ef43 = keccak256(validatorSetApplyBlock()) - validator_set_apply_block: {:validator_set, "b927ef43", []} + validator_set_apply_block: {:validator_set, "b927ef43", [], block_number} ] end @@ -199,10 +199,10 @@ defmodule Explorer.Staking.ContractReader do end # args = [staking_epoch, delegator_staked, validator_staked, total_staked, pool_reward \\ 10_00000] - def delegator_reward_request(args) do + def delegator_reward_request(args, block_number) do [ # 5fba554e = keccak256(delegatorShare(uint256,uint256,uint256,uint256,uint256)) - delegator_share: {:block_reward, "5fba554e", args} + delegator_share: {:block_reward, "5fba554e", args, block_number} ] end @@ -234,76 +234,83 @@ defmodule Explorer.Staking.ContractReader do ] end + def mining_by_staking_request(staking_address, block_number) do + [ + # 00535175 = keccak256(miningByStakingAddress(address)) + mining_address: {:validator_set, "00535175", [staking_address], block_number} + ] + end + def pool_staking_requests(staking_address, block_number) do [ active_delegators: active_delegators_request(staking_address, block_number)[:active_delegators], # 73c21803 = keccak256(poolDelegatorsInactive(address)) - inactive_delegators: {:staking, "73c21803", [staking_address]}, + inactive_delegators: {:staking, "73c21803", [staking_address], block_number}, # a711e6a1 = keccak256(isPoolActive(address)) - is_active: {:staking, "a711e6a1", [staking_address]}, - mining_address_hash: mining_by_staking_request(staking_address)[:mining_address], + is_active: {:staking, "a711e6a1", [staking_address], block_number}, + mining_address_hash: mining_by_staking_request(staking_address, block_number)[:mining_address], # a697ecff = keccak256(stakeAmount(address,address)) - self_staked_amount: {:staking, "a697ecff", [staking_address, staking_address]}, + self_staked_amount: {:staking, "a697ecff", [staking_address, staking_address], block_number}, # 5267e1d6 = keccak256(stakeAmountTotal(address)) - total_staked_amount: {:staking, "5267e1d6", [staking_address]}, + total_staked_amount: {:staking, "5267e1d6", [staking_address], block_number}, # 527d8bc4 = keccak256(validatorRewardPercent(address)) - validator_reward_percent: {:block_reward, "527d8bc4", [staking_address]} + validator_reward_percent: {:block_reward, "527d8bc4", [staking_address], block_number} ] end - def pool_mining_requests(mining_address) do + def pool_mining_requests(mining_address, block_number) do [ # a881c5fd = keccak256(areDelegatorsBanned(address)) - are_delegators_banned: {:validator_set, "a881c5fd", [mining_address]}, + are_delegators_banned: {:validator_set, "a881c5fd", [mining_address], block_number}, # c9e9694d = keccak256(banReason(address)) - ban_reason: {:validator_set, "c9e9694d", [mining_address]}, + ban_reason: {:validator_set, "c9e9694d", [mining_address], block_number}, # 5836d08a = keccak256(bannedUntil(address)) - banned_until: {:validator_set, "5836d08a", [mining_address]}, + banned_until: {:validator_set, "5836d08a", [mining_address], block_number}, # 1a7fa237 = keccak256(bannedDelegatorsUntil(address)) - banned_delegators_until: {:validator_set, "1a7fa237", [mining_address]}, + banned_delegators_until: {:validator_set, "1a7fa237", [mining_address], block_number}, # a92252ae = keccak256(isValidatorBanned(address)) - is_banned: {:validator_set, "a92252ae", [mining_address]}, + is_banned: {:validator_set, "a92252ae", [mining_address], block_number}, # b41832e4 = keccak256(validatorCounter(address)) - was_validator_count: {:validator_set, "b41832e4", [mining_address]}, + was_validator_count: {:validator_set, "b41832e4", [mining_address], block_number}, # 1d0cd4c6 = keccak256(banCounter(address)) - was_banned_count: {:validator_set, "1d0cd4c6", [mining_address]} + was_banned_count: {:validator_set, "1d0cd4c6", [mining_address], block_number} ] end - def staker_requests(pool_staking_address, staker_address) do + def staker_requests(pool_staking_address, staker_address, block_number) do [ # 950a6513 = keccak256(maxWithdrawOrderAllowed(address,address)) - max_ordered_withdraw_allowed: {:staking, "950a6513", [pool_staking_address, staker_address]}, + max_ordered_withdraw_allowed: {:staking, "950a6513", [pool_staking_address, staker_address], block_number}, # 6bda1577 = keccak256(maxWithdrawAllowed(address,address)) - max_withdraw_allowed: {:staking, "6bda1577", [pool_staking_address, staker_address]}, + max_withdraw_allowed: {:staking, "6bda1577", [pool_staking_address, staker_address], block_number}, # e9ab0300 = keccak256(orderedWithdrawAmount(address,address)) - ordered_withdraw: {:staking, "e9ab0300", [pool_staking_address, staker_address]}, + ordered_withdraw: {:staking, "e9ab0300", [pool_staking_address, staker_address], block_number}, # a4205967 = keccak256(orderWithdrawEpoch(address,address)) - ordered_withdraw_epoch: {:staking, "a4205967", [pool_staking_address, staker_address]}, + ordered_withdraw_epoch: {:staking, "a4205967", [pool_staking_address, staker_address], block_number}, # a697ecff = keccak256(stakeAmount(address,address)) - stake_amount: {:staking, "a697ecff", [pool_staking_address, staker_address]} + stake_amount: {:staking, "a697ecff", [pool_staking_address, staker_address], block_number} ] end - def staking_by_mining_request(mining_address) do + def staking_by_mining_request(mining_address, block_number) do [ # 1ee4d0bc = keccak256(stakingByMiningAddress(address)) - staking_address: {:validator_set, "1ee4d0bc", [mining_address]} + staking_address: {:validator_set, "1ee4d0bc", [mining_address], block_number} ] end - def validator_min_reward_percent_request(epoch_number) do + def validator_min_reward_percent_request(epoch_number, block_number) do [ # cdf7a090 = keccak256(validatorMinRewardPercent(uint256)) - value: {:block_reward, "cdf7a090", [epoch_number]} + value: {:block_reward, "cdf7a090", [epoch_number], block_number} ] end # args = [staking_epoch, validator_staked, total_staked, pool_reward \\ 10_00000] - def validator_reward_request(args) do + def validator_reward_request(args, block_number) do [ # 8737929a = keccak256(validatorShare(uint256,uint256,uint256,uint256)) - validator_share: {:block_reward, "8737929a", args} + validator_share: {:block_reward, "8737929a", args, block_number} ] end diff --git a/apps/explorer/lib/explorer/staking/contract_state.ex b/apps/explorer/lib/explorer/staking/contract_state.ex index 24ccdd0848..5b6d10eae2 100644 --- a/apps/explorer/lib/explorer/staking/contract_state.ex +++ b/apps/explorer/lib/explorer/staking/contract_state.ex @@ -128,31 +128,50 @@ defmodule Explorer.Staking.ContractState do @doc "Handles new blocks and decides to fetch fresh chain info" def handle_info({:chain_event, :last_block_number, :realtime, block_number}, state) do if block_number > state.seen_block do - fetch_state(state.contracts, state.abi, block_number) + # read general info from the contracts (including pool list and validator list) + global_responses = + ContractReader.perform_requests(ContractReader.global_requests(block_number), state.contracts, state.abi) + + epoch_very_beginning = global_responses.epoch_start_block == block_number + 1 + + if global_responses.epoch_number > get(:epoch_number) and not epoch_very_beginning and state.seen_block > 0 do + # if the previous staking epoch finished and we have blocks gap, + # call fetch_state in a loop until the blocks gap is closed + loop_block_start = state.seen_block + 1 + loop_block_end = block_number - 1 + + if loop_block_end >= loop_block_start do + for bn <- loop_block_start..loop_block_end do + gr = ContractReader.perform_requests(ContractReader.global_requests(bn), state.contracts, state.abi) + fetch_state(state.contracts, state.abi, gr, bn, gr.epoch_start_block == bn + 1) + end + end + end + + fetch_state(state.contracts, state.abi, global_responses, block_number, epoch_very_beginning) {:noreply, %{state | seen_block: block_number}} else {:noreply, state} end end - defp fetch_state(contracts, abi, block_number) do - # read general info from the contracts (including pool list and validator list) - global_responses = ContractReader.perform_requests(ContractReader.global_requests(), contracts, abi) + defp fetch_state(contracts, abi, global_responses, block_number, epoch_very_beginning) do + validator_min_reward_percent = + get_validator_min_reward_percent(global_responses.epoch_number, block_number, contracts, abi) - validator_min_reward_percent = get_validator_min_reward_percent(global_responses, contracts, abi) - - 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) + active_pools_length = Enum.count(global_responses.active_pools) + # save the general info to ETS (excluding pool list and validator list) settings = global_responses |> get_settings(validator_min_reward_percent, block_number) - |> Enum.concat(active_pools_length: Enum.count(global_responses.active_pools)) + |> Enum.concat(active_pools_length: active_pools_length) :ets.insert(@table_name, settings) @@ -162,11 +181,12 @@ defmodule Explorer.Staking.ContractState do start_snapshotting, global_responses, contracts, - abi + abi, + block_number ) # miningToStakingAddress mapping - mining_to_staking_address = get_mining_to_staking_address(validators, contracts, abi) + mining_to_staking_address = get_mining_to_staking_address(validators, contracts, abi, block_number) # the list of all pools (validators + active pools + inactive pools) pools = @@ -188,7 +208,14 @@ defmodule Explorer.Staking.ContractState do # 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) + get_candidate_reward_responses( + pool_staking_responses, + global_responses, + pool_staking_keys, + contracts, + abi, + block_number + ) # call `BlockReward.delegatorShare` function for each delegator # to get their reward share of the pool (needed for the `Delegators` list in UI) @@ -207,7 +234,8 @@ defmodule Explorer.Staking.ContractState do pool_staking_responses, global_responses, contracts, - abi + abi, + block_number ) # calculate total amount staked into all active pools @@ -265,8 +293,19 @@ defmodule Explorer.Staking.ContractState do ) end - # notify the UI about new block - Publisher.broadcast(:staking_update) + # notify the UI about a new block + data = %{ + active_pools_length: active_pools_length, + block_number: block_number, + epoch_end_block: global_responses.epoch_end_block, + epoch_number: global_responses.epoch_number, + max_candidates: global_responses.max_candidates, + staking_allowed: global_responses.staking_allowed, + staking_token_defined: get(:token, nil) != nil, + validator_set_apply_block: global_responses.validator_set_apply_block + } + + Publisher.broadcast([{:staking_update, data}], :realtime) end defp get_settings(global_responses, validator_min_reward_percent, block_number) do @@ -284,9 +323,11 @@ defmodule Explorer.Staking.ContractState do end end - defp get_mining_to_staking_address(validators, contracts, abi) do + defp get_mining_to_staking_address(validators, contracts, abi, block_number) do validators.all - |> Enum.map(&ContractReader.staking_by_mining_request/1) + |> Enum.map(fn mining_address -> + ContractReader.staking_by_mining_request(mining_address, block_number) + end) |> 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 @@ -303,7 +344,12 @@ defmodule Explorer.Staking.ContractState do # read pool info from the contracts by its mining address pool_mining_responses = pools - |> Enum.map(&ContractReader.pool_mining_requests(pool_staking_responses[&1].mining_address_hash)) + |> Enum.map(fn staking_address_hash -> + ContractReader.pool_mining_requests( + pool_staking_responses[staking_address_hash].mining_address_hash, + block_number + ) + end) |> ContractReader.perform_grouped_requests(pools, contracts, abi) # get a flat list of all stakers in the form of {pool_staking_address, staker_address, is_active} @@ -318,7 +364,7 @@ defmodule Explorer.Staking.ContractState do staker_responses = stakers |> Enum.map(fn {pool_staking_address, staker_address, _is_active} -> - ContractReader.staker_requests(pool_staking_address, staker_address) + ContractReader.staker_requests(pool_staking_address, staker_address, block_number) end) |> ContractReader.perform_grouped_requests(stakers, contracts, abi) @@ -329,15 +375,25 @@ defmodule Explorer.Staking.ContractState do } end - defp get_candidate_reward_responses(pool_staking_responses, global_responses, pool_staking_keys, contracts, abi) do + defp get_candidate_reward_responses( + pool_staking_responses, + global_responses, + pool_staking_keys, + contracts, + abi, + block_number + ) 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 - ]) + ContractReader.validator_reward_request( + [ + global_responses.epoch_number, + resp.self_staked_amount, + resp.total_staked_amount, + 1000_000 + ], + block_number + ) end) |> ContractReader.perform_grouped_requests(pool_staking_keys, contracts, abi) end @@ -347,7 +403,8 @@ defmodule Explorer.Staking.ContractState do pool_staking_responses, global_responses, contracts, - abi + abi, + block_number ) do # to keep sort order when using `perform_grouped_requests` (see below) delegator_keys = Enum.map(delegator_responses, fn {key, _} -> key end) @@ -356,13 +413,16 @@ defmodule Explorer.Staking.ContractState do |> 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 - ]) + ContractReader.delegator_reward_request( + [ + global_responses.epoch_number, + resp.stake_amount, + staking_resp.self_staked_amount, + staking_resp.total_staked_amount, + 1000_000 + ], + block_number + ) end) |> ContractReader.perform_grouped_requests(delegator_keys, contracts, abi) end @@ -385,9 +445,9 @@ defmodule Explorer.Staking.ContractState do end) end - defp get_validator_min_reward_percent(global_responses, contracts, abi) do + defp get_validator_min_reward_percent(epoch_number, block_number, contracts, abi) do ContractReader.perform_requests( - ContractReader.validator_min_reward_percent_request(global_responses.epoch_number), + ContractReader.validator_min_reward_percent_request(epoch_number, block_number), contracts, abi ).value @@ -414,7 +474,8 @@ defmodule Explorer.Staking.ContractState do start_snapshotting, global_responses, contracts, - abi + abi, + block_number ) do if start_snapshotting do # eebc7a39 = keccak256(getPendingValidators()) @@ -424,10 +485,15 @@ defmodule Explorer.Staking.ContractState do "eebc7a39" => {:ok, [validators_pending]}, "004a8803" => {:ok, [validators_to_be_finalized]} } = - Reader.query_contract(contracts.validator_set, abi, %{ - "eebc7a39" => [], - "004a8803" => [] - }) + Reader.query_contract_by_block_number( + contracts.validator_set, + abi, + %{ + "eebc7a39" => [], + "004a8803" => [] + }, + block_number + ) validators_pending = Enum.uniq(validators_pending ++ validators_to_be_finalized) @@ -613,6 +679,8 @@ defmodule Explorer.Staking.ContractState do mining_to_staking_address ) do # start snapshotting at the beginning of the staking epoch + :ets.insert(@table_name, is_snapshotting: true) + cached_pool_staking_responses = if epoch_very_beginning do pool_staking_responses @@ -659,7 +727,10 @@ defmodule Explorer.Staking.ContractState do 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 + # to read it from a contract and then write to DB. + # Since the Staking DApp doesn't use the token fields + # which dinamically change (such as totalSupply), + # we don't pass the current block_number to the RPC request token_functions = MetadataRetriever.get_functions_of(address) if map_size(token_functions) > 0 do diff --git a/apps/explorer/lib/explorer/staking/stake_snapshotting.ex b/apps/explorer/lib/explorer/staking/stake_snapshotting.ex index 5b8beb1abe..4ce07417b7 100644 --- a/apps/explorer/lib/explorer/staking/stake_snapshotting.ex +++ b/apps/explorer/lib/explorer/staking/stake_snapshotting.ex @@ -19,8 +19,6 @@ defmodule Explorer.Staking.StakeSnapshotting do mining_to_staking_address, block_number ) do - :ets.insert(ets_table_name, is_snapshotting: true) - # get staking addresses for the pending validators pool_staking_addresses = pools_mining_addresses @@ -89,12 +87,15 @@ defmodule Explorer.Staking.StakeSnapshotting do validator_reward_responses = pool_staking_responses |> Enum.map(fn {_pool_staking_address, resp} -> - ContractReader.validator_reward_request([ - epoch_number, - resp.snapshotted_self_staked_amount, - resp.snapshotted_total_staked_amount, - 1000_000 - ]) + ContractReader.validator_reward_request( + [ + epoch_number, + resp.snapshotted_self_staked_amount, + resp.snapshotted_total_staked_amount, + 1000_000 + ], + block_number + ) end) |> ContractReader.perform_grouped_requests(pool_staking_keys, contracts, abi) @@ -116,13 +117,16 @@ defmodule Explorer.Staking.StakeSnapshotting do |> Enum.map(fn {{pool_staking_address, _staker_address}, resp} -> staking_resp = pool_staking_responses[pool_staking_address] - ContractReader.delegator_reward_request([ - epoch_number, - resp.snapshotted_stake_amount, - staking_resp.snapshotted_self_staked_amount, - staking_resp.snapshotted_total_staked_amount, - 1000_000 - ]) + ContractReader.delegator_reward_request( + [ + epoch_number, + resp.snapshotted_stake_amount, + staking_resp.snapshotted_self_staked_amount, + staking_resp.snapshotted_total_staked_amount, + 1000_000 + ], + block_number + ) end) |> ContractReader.perform_grouped_requests(delegator_keys, contracts, abi)