Merge pull request #3443 from poanetwork/va-staking-dapp-enhance-blocks-handling

Improve blocks handling in Staking DApp
pull/3450/head
Victor Baranov 4 years ago committed by GitHub
commit 4e520f7ec3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .dialyzer-ignore
  2. 1
      CHANGELOG.md
  3. 72
      apps/block_scout_web/assets/js/lib/queue.js
  4. 159
      apps/block_scout_web/assets/js/pages/stakes.js
  5. 2
      apps/block_scout_web/assets/js/pages/stakes/utils.js
  6. 3
      apps/block_scout_web/lib/block_scout_web/application.ex
  7. 48
      apps/block_scout_web/lib/block_scout_web/channels/stakes_channel.ex
  8. 31
      apps/block_scout_web/lib/block_scout_web/controllers/stakes_controller.ex
  9. 12
      apps/block_scout_web/lib/block_scout_web/notifier.ex
  10. 1
      apps/block_scout_web/lib/block_scout_web/realtime_event_handler.ex
  11. 26
      apps/block_scout_web/lib/block_scout_web/staking_event_handler.ex
  12. 18
      apps/block_scout_web/priv/gettext/default.pot
  13. 18
      apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
  14. 12
      apps/block_scout_web/test/block_scout_web/channels/stakes_channel_test.exs
  15. 2
      apps/explorer/lib/explorer/chain/events/publisher.ex
  16. 4
      apps/explorer/lib/explorer/chain/events/subscriber.ex
  17. 34
      apps/explorer/lib/explorer/smart_contract/reader.ex
  18. 95
      apps/explorer/lib/explorer/staking/contract_reader.ex
  19. 150
      apps/explorer/lib/explorer/staking/contract_state.ex
  20. 34
      apps/explorer/lib/explorer/staking/stake_snapshotting.ex

@ -25,4 +25,4 @@ lib/explorer/exchange_rates/source.ex:107
lib/explorer/smart_contract/verifier.ex:89
lib/block_scout_web/templates/address_contract/index.html.eex:117
lib/explorer/staking/stake_snapshotting.ex:14: Function do_snapshotting/6 has no local return
lib/explorer/staking/stake_snapshotting.ex:179
lib/explorer/staking/stake_snapshotting.ex:183

@ -18,6 +18,7 @@
### Fixes
- [#3449](https://github.com/poanetwork/blockscout/pull/3449) - Correct avg time calculation
- [#3443](https://github.com/poanetwork/blockscout/pull/3443) - Improve blocks handling in Staking DApp
- [#3440](https://github.com/poanetwork/blockscout/pull/3440) - Rewrite missing blocks range query
- [#3439](https://github.com/poanetwork/blockscout/pull/3439) - Dark mode color fixes (search, charts)
- [#3437](https://github.com/poanetwork/blockscout/pull/3437) - Fix Postgres Docker container

@ -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);
}
}

@ -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 (accountChanged(msg.account, state)) {
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) ||
accountChanged(msg.account, state) || 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 => {
@ -266,6 +304,10 @@ if ($stakesPage.length) {
initialize(store)
}
function accountChanged (account, state) {
return account !== state.account
}
function hideCurrentModal () {
const $modal = currentModal()
if ($modal) $modal.modal('hide')
@ -298,10 +340,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 (accountChanged(account, state) && await setAccount(account, store)) {
refresh = false // because refreshing will be done by `onStakingUpdate`
}
if (refresh) {
await refreshPageWrapper(store)
}
setTimeout(() => {
@ -320,21 +364,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 +395,7 @@ function reloadPoolList (msg, store) {
validatorSetApplyBlock: msg.validator_set_apply_block
})
if (!msg.dont_refresh_page) {
refreshPageWrapper(store)
await refreshPageWrapper(store)
}
}
@ -353,24 +405,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(`
<div data-placement="bottom" data-toggle="tooltip" title="${account}">
${account}
</div>
`)
hideCurrentModal()
resolve(true)
}).receive('error', () => {
openErrorModal('Change account', errorMsg, true)
resolve(false)
}).receive('timeout', () => {
openErrorModal('Change account', errorMsg, true)
resolve(false)
})
})
}
@ -395,6 +455,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')

@ -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)
}

@ -7,7 +7,7 @@ defmodule BlockScoutWeb.Application do
alias BlockScoutWeb.Counters.BlocksIndexedCounter
alias BlockScoutWeb.{Endpoint, Prometheus}
alias BlockScoutWeb.RealtimeEventHandler
alias BlockScoutWeb.{RealtimeEventHandler, StakingEventHandler}
def start(_type, _args) do
import Supervisor.Spec
@ -22,6 +22,7 @@ defmodule BlockScoutWeb.Application do
supervisor(Endpoint, []),
{Absinthe.Subscription, Endpoint},
{RealtimeEventHandler, name: RealtimeEventHandler},
{StakingEventHandler, name: StakingEventHandler},
{BlocksIndexedCounter, name: BlocksIndexedCounter}
]

@ -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,

@ -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

@ -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(

@ -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

@ -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

@ -2224,7 +2224,7 @@ msgid "It's me!"
msgstr ""
#, elixir-format
#: lib/block_scout_web/channels/stakes_channel.ex:680
#: lib/block_scout_web/channels/stakes_channel.ex:698
msgid "JSON RPC error"
msgstr ""
@ -2299,7 +2299,7 @@ msgid "Pool"
msgstr ""
#, elixir-format
#: lib/block_scout_web/channels/stakes_channel.ex:735
#: lib/block_scout_web/channels/stakes_channel.ex:753
msgid "Pools searching is already in progress for this address"
msgstr ""
@ -2332,7 +2332,7 @@ msgid "Remove My Pool"
msgstr ""
#, elixir-format
#: lib/block_scout_web/channels/stakes_channel.ex:776
#: lib/block_scout_web/channels/stakes_channel.ex:794
msgid "Reward calculating is already in progress for this address"
msgstr ""
@ -2412,7 +2412,7 @@ msgid "Stakes Ratio"
msgstr ""
#, elixir-format
#: lib/block_scout_web/channels/stakes_channel.ex:779
#: lib/block_scout_web/channels/stakes_channel.ex:797
msgid "Staking epochs are not specified or not in the allowed range"
msgstr ""
@ -2507,19 +2507,19 @@ msgid "Unable to find any pools you could claim a reward from."
msgstr ""
#, elixir-format
#: lib/block_scout_web/channels/stakes_channel.ex:742
#: lib/block_scout_web/channels/stakes_channel.ex:789
#: lib/block_scout_web/channels/stakes_channel.ex:760
#: lib/block_scout_web/channels/stakes_channel.ex:807
msgid "Unknown address of Staking contract. Please, contact support"
msgstr ""
#, elixir-format
#: lib/block_scout_web/channels/stakes_channel.ex:782
#: lib/block_scout_web/channels/stakes_channel.ex:800
msgid "Unknown pool staking address. Please, contact support"
msgstr ""
#, elixir-format
#: lib/block_scout_web/channels/stakes_channel.ex:738
#: lib/block_scout_web/channels/stakes_channel.ex:785
#: lib/block_scout_web/channels/stakes_channel.ex:756
#: lib/block_scout_web/channels/stakes_channel.ex:803
msgid "Unknown staker address. Please, choose your account in MetaMask"
msgstr ""

@ -2224,7 +2224,7 @@ msgid "It's me!"
msgstr ""
#, elixir-format
#: lib/block_scout_web/channels/stakes_channel.ex:680
#: lib/block_scout_web/channels/stakes_channel.ex:698
msgid "JSON RPC error"
msgstr ""
@ -2299,7 +2299,7 @@ msgid "Pool"
msgstr ""
#, elixir-format
#: lib/block_scout_web/channels/stakes_channel.ex:735
#: lib/block_scout_web/channels/stakes_channel.ex:753
msgid "Pools searching is already in progress for this address"
msgstr ""
@ -2332,7 +2332,7 @@ msgid "Remove My Pool"
msgstr ""
#, elixir-format
#: lib/block_scout_web/channels/stakes_channel.ex:776
#: lib/block_scout_web/channels/stakes_channel.ex:794
msgid "Reward calculating is already in progress for this address"
msgstr ""
@ -2412,7 +2412,7 @@ msgid "Stakes Ratio"
msgstr ""
#, elixir-format
#: lib/block_scout_web/channels/stakes_channel.ex:779
#: lib/block_scout_web/channels/stakes_channel.ex:797
msgid "Staking epochs are not specified or not in the allowed range"
msgstr ""
@ -2507,19 +2507,19 @@ msgid "Unable to find any pools you could claim a reward from."
msgstr ""
#, elixir-format
#: lib/block_scout_web/channels/stakes_channel.ex:742
#: lib/block_scout_web/channels/stakes_channel.ex:789
#: lib/block_scout_web/channels/stakes_channel.ex:760
#: lib/block_scout_web/channels/stakes_channel.ex:807
msgid "Unknown address of Staking contract. Please, contact support"
msgstr ""
#, elixir-format
#: lib/block_scout_web/channels/stakes_channel.ex:782
#: lib/block_scout_web/channels/stakes_channel.ex:800
msgid "Unknown pool staking address. Please, contact support"
msgstr ""
#, elixir-format
#: lib/block_scout_web/channels/stakes_channel.ex:738
#: lib/block_scout_web/channels/stakes_channel.ex:785
#: lib/block_scout_web/channels/stakes_channel.ex:756
#: lib/block_scout_web/channels/stakes_channel.ex:803
msgid "Unknown staker address. Please, choose your account in MetaMask"
msgstr ""

@ -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: _}} ->

@ -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

@ -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

@ -94,22 +94,7 @@ defmodule Explorer.SmartContract.Reader do
functions()
) :: functions_results()
def query_contract(contract_address, abi, functions) do
requests =
functions
|> Enum.map(fn {method_id, args} ->
%{
contract_address: contract_address,
method_id: method_id,
args: args
}
end)
requests
|> query_contracts(abi)
|> Enum.zip(requests)
|> Enum.into(%{}, fn {response, request} ->
{request.method_id, response}
end)
query_contract_inner(contract_address, abi, functions, nil, nil)
end
@spec query_contract(
@ -119,6 +104,20 @@ defmodule Explorer.SmartContract.Reader do
functions()
) :: functions_results()
def query_contract(contract_address, from, abi, functions) do
query_contract_inner(contract_address, abi, functions, nil, from)
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
query_contract_inner(contract_address, abi, functions, block_number, nil)
end
defp query_contract_inner(contract_address, abi, functions, block_number, from) do
requests =
functions
|> Enum.map(fn {method_id, args} ->
@ -126,7 +125,8 @@ defmodule Explorer.SmartContract.Reader do
contract_address: contract_address,
from: from,
method_id: method_id,
args: args
args: args,
block_number: block_number
}
end)

@ -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

@ -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,9 @@ 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(&ContractReader.staking_by_mining_request(&1, block_number))
|> 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
@ -295,15 +334,13 @@ defmodule Explorer.Staking.ContractState do
# read pool info from the contracts by its staking address
pool_staking_responses =
pools
|> Enum.map(fn staking_address_hash ->
ContractReader.pool_staking_requests(staking_address_hash, block_number)
end)
|> Enum.map(&ContractReader.pool_staking_requests(&1, block_number))
|> ContractReader.perform_grouped_requests(pools, contracts, abi)
# 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(&ContractReader.pool_mining_requests(pool_staking_responses[&1].mining_address_hash, block_number))
|> 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 +355,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 +366,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 +394,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 +404,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 +436,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 +465,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 +476,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 +670,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 +718,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

@ -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)

Loading…
Cancel
Save