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)