Improve 'Become a Candidate' appearance, checkups, and connectivity control

staking
Vadim 5 years ago committed by Victor Baranov
parent 7763c9ead2
commit 7d44866e9c
  1. 18
      apps/block_scout_web/assets/js/pages/stakes.js
  2. 55
      apps/block_scout_web/assets/js/pages/stakes/become_candidate.js
  3. 2
      apps/block_scout_web/assets/js/pages/stakes/claim_reward.js
  4. 23
      apps/block_scout_web/assets/js/pages/stakes/make_stake.js
  5. 8
      apps/block_scout_web/assets/js/pages/stakes/utils.js
  6. 12
      apps/block_scout_web/lib/block_scout_web/channels/stakes_channel.ex
  7. 15
      apps/block_scout_web/lib/block_scout_web/controllers/stakes_controller.ex
  8. 36
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_top.html.eex
  9. 1
      apps/explorer/lib/explorer/staking/contract_reader.ex
  10. 8
      apps/explorer/lib/explorer/staking/contract_state.ex

@ -8,12 +8,12 @@ import { createAsyncLoadStore, refreshPage } from '../lib/async_listing_load'
import Web3 from 'web3' import Web3 from 'web3'
import { openPoolInfoModal } from './stakes/validator_info' import { openPoolInfoModal } from './stakes/validator_info'
import { openDelegatorsListModal } from './stakes/delegators_list' import { openDelegatorsListModal } from './stakes/delegators_list'
import { openBecomeCandidateModal } from './stakes/become_candidate' import { openBecomeCandidateModal, becomeCandidateConnectionLost } from './stakes/become_candidate'
import { openRemovePoolModal } from './stakes/remove_pool' import { openRemovePoolModal } from './stakes/remove_pool'
import { openMakeStakeModal } from './stakes/make_stake' import { openMakeStakeModal } from './stakes/make_stake'
import { openMoveStakeModal } from './stakes/move_stake' import { openMoveStakeModal } from './stakes/move_stake'
import { openWithdrawStakeModal } from './stakes/withdraw_stake' import { openWithdrawStakeModal } from './stakes/withdraw_stake'
import { openClaimRewardModal, connectionLost } from './stakes/claim_reward' import { openClaimRewardModal, claimRewardConnectionLost } from './stakes/claim_reward'
import { openClaimWithdrawalModal } from './stakes/claim_withdrawal' import { openClaimWithdrawalModal } from './stakes/claim_withdrawal'
import { checkForTokenDefinition } from './stakes/utils' import { checkForTokenDefinition } from './stakes/utils'
import { currentModal, openWarningModal, openErrorModal } from '../lib/modals' import { currentModal, openWarningModal, openErrorModal } from '../lib/modals'
@ -35,6 +35,7 @@ export const initialState = {
tokenDecimals: 0, tokenDecimals: 0,
tokenSymbol: '', tokenSymbol: '',
validatorSetApplyBlock: 0, validatorSetApplyBlock: 0,
validatorSetContract: null,
web3: null web3: null
} }
@ -99,6 +100,7 @@ export function reducer (state = initialState, action) {
return Object.assign({}, state, { return Object.assign({}, state, {
stakingContract: action.stakingContract, stakingContract: action.stakingContract,
blockRewardContract: action.blockRewardContract, blockRewardContract: action.blockRewardContract,
validatorSetContract: action.validatorSetContract,
tokenDecimals: action.tokenDecimals, tokenDecimals: action.tokenDecimals,
tokenSymbol: action.tokenSymbol tokenSymbol: action.tokenSymbol
}) })
@ -191,23 +193,27 @@ if ($stakesPage.length) {
}) })
}) })
channel.on('contracts', msg => { channel.on('contracts', async (msg) => {
const web3 = store.getState().web3 const web3 = store.getState().web3
const stakingContract = const stakingContract =
new web3.eth.Contract(msg.staking_contract.abi, msg.staking_contract.address) new web3.eth.Contract(msg.staking_contract.abi, msg.staking_contract.address)
const blockRewardContract = const blockRewardContract =
new web3.eth.Contract(msg.block_reward_contract.abi, msg.block_reward_contract.address) new web3.eth.Contract(msg.block_reward_contract.abi, msg.block_reward_contract.address)
const validatorSetContract =
new web3.eth.Contract(msg.validator_set_contract.abi, msg.validator_set_contract.address)
store.dispatch({ store.dispatch({
type: 'RECEIVED_CONTRACTS', type: 'RECEIVED_CONTRACTS',
stakingContract, stakingContract,
blockRewardContract, blockRewardContract,
validatorSetContract,
tokenDecimals: parseInt(msg.token_decimals, 10), tokenDecimals: parseInt(msg.token_decimals, 10),
tokenSymbol: msg.token_symbol tokenSymbol: msg.token_symbol
}) })
}) })
channel.onError(connectionLost) channel.onError(becomeCandidateConnectionLost)
channel.onError(claimRewardConnectionLost)
$(document.body) $(document.body)
.on('click', '.js-pool-info', event => { .on('click', '.js-pool-info', event => {
@ -218,9 +224,9 @@ if ($stakesPage.length) {
.on('click', '.js-delegators-list', event => { .on('click', '.js-delegators-list', event => {
openDelegatorsListModal(event, store) openDelegatorsListModal(event, store)
}) })
.on('click', '.js-become-candidate', () => { .on('click', '.js-become-candidate', event => {
if (checkForTokenDefinition(store)) { if (checkForTokenDefinition(store)) {
openBecomeCandidateModal(store) openBecomeCandidateModal(event, store)
} }
}) })
.on('click', '.js-remove-pool', () => { .on('click', '.js-remove-pool', () => {

@ -2,19 +2,27 @@ import $ from 'jquery'
import { BigNumber } from 'bignumber.js' import { BigNumber } from 'bignumber.js'
import { openModal, openErrorModal, openWarningModal, lockModal } from '../../lib/modals' import { openModal, openErrorModal, openWarningModal, lockModal } from '../../lib/modals'
import { setupValidation } from '../../lib/validation' import { setupValidation } from '../../lib/validation'
import { makeContractCall, isSupportedNetwork } from './utils' import { makeContractCall, isSupportedNetwork, isStakingAllowed } from './utils'
export function openBecomeCandidateModal (store) { let status = 'modalClosed'
if (!store.getState().account) {
openWarningModal('Unauthorized', 'Please login with MetaMask') export async function openBecomeCandidateModal (event, store) {
const state = store.getState()
if (!state.account) {
openWarningModal('Unauthorized', 'You haven\'t approved the reading of account list from your MetaMask or MetaMask is not installed.')
return return
} }
if (!isSupportedNetwork(store)) return if (!isSupportedNetwork(store)) return
if (!isStakingAllowed(state)) return
store.getState().channel $(event.currentTarget).prop('disabled', true)
state.channel
.push('render_become_candidate') .push('render_become_candidate')
.receive('ok', msg => { .receive('ok', msg => {
$(event.currentTarget).prop('disabled', false)
const $modal = $(msg.html) const $modal = $(msg.html)
const $form = $modal.find('form') const $form = $modal.find('form')
@ -39,23 +47,42 @@ export function openBecomeCandidateModal (store) {
return false return false
}) })
$modal.on('shown.bs.modal', () => {
status = 'modalOpened'
})
$modal.on('hidden.bs.modal', () => {
status = 'modalClosed'
$modal.remove()
})
openModal($modal) openModal($modal)
}) })
.receive('timeout', () => {
$(event.currentTarget).prop('disabled', false)
openErrorModal('Become a Candidate', 'Connection timeout')
})
} }
async function becomeCandidate ($modal, store, msg) { export function becomeCandidateConnectionLost () {
const errorMsg = 'Connection with server is lost. Please, reload the page.'
if (status === 'modalOpened') {
status = 'modalClosed'
openErrorModal('Become a Candidate', errorMsg, true)
}
}
function becomeCandidate ($modal, store, msg) {
lockModal($modal) lockModal($modal)
const stakingContract = store.getState().stakingContract const state = store.getState()
const decimals = store.getState().tokenDecimals const stakingContract = state.stakingContract
const decimals = state.tokenDecimals
const stake = new BigNumber($modal.find('[candidate-stake]').val().replace(',', '.').trim()).shiftedBy(decimals).integerValue() const stake = new BigNumber($modal.find('[candidate-stake]').val().replace(',', '.').trim()).shiftedBy(decimals).integerValue()
const miningAddress = $modal.find('[mining-address]').val().trim().toLowerCase() const miningAddress = $modal.find('[mining-address]').val().trim().toLowerCase()
try { try {
if (!await stakingContract.methods.areStakeAndWithdrawAllowed().call()) { if (!isSupportedNetwork(store)) return false
openErrorModal('Error', 'The current staking epoch is ending, and staking actions are temporarily restricted. Please try again when the new epoch starts.') if (!isStakingAllowed(state)) return false
return false
}
makeContractCall(stakingContract.methods.addPool(stake.toString(), miningAddress), store) makeContractCall(stakingContract.methods.addPool(stake.toString(), miningAddress), store)
} catch (err) { } catch (err) {
@ -84,8 +111,10 @@ function isMiningAddressValid (value, store) {
const web3 = store.getState().web3 const web3 = store.getState().web3
const miningAddress = value.trim().toLowerCase() const miningAddress = value.trim().toLowerCase()
if (miningAddress === store.getState().account.toLowerCase() || !web3.utils.isAddress(miningAddress)) { if (!web3.utils.isAddress(miningAddress)) {
return 'Invalid mining address' return 'Invalid mining address'
} else if (miningAddress === store.getState().account.toLowerCase()) {
return 'The mining address cannot match the staking address'
} }
return true return true

@ -90,7 +90,7 @@ export function openClaimRewardModal (event, store) {
}) })
} }
export function connectionLost () { export function claimRewardConnectionLost () {
const errorMsg = 'Connection with server is lost. Please, reload the page.' const errorMsg = 'Connection with server is lost. Please, reload the page.'
if (status === 'modalOpened') { if (status === 'modalOpened') {
status = 'modalClosed' status = 'modalClosed'

@ -1,12 +1,12 @@
import $ from 'jquery' import $ from 'jquery'
import { BigNumber } from 'bignumber.js' import { BigNumber } from 'bignumber.js'
import { openModal, openWarningModal, lockModal } from '../../lib/modals' import { openErrorModal, openModal, openWarningModal, lockModal } from '../../lib/modals'
import { setupValidation } from '../../lib/validation' import { setupValidation } from '../../lib/validation'
import { makeContractCall, setupChart, isSupportedNetwork } from './utils' import { makeContractCall, setupChart, isSupportedNetwork } from './utils'
export function openMakeStakeModal (event, store) { export function openMakeStakeModal (event, store) {
if (!store.getState().account) { if (!store.getState().account) {
openWarningModal('Unauthorized', 'Please login with MetaMask') openWarningModal('Unauthorized', 'You haven\'t approved the reading of account list from your MetaMask or MetaMask is not installed.')
return return
} }
@ -46,14 +46,27 @@ export function openMakeStakeModal (event, store) {
}) })
} }
function makeStake ($modal, address, store, msg) { async function makeStake ($modal, address, store, msg) {
lockModal($modal) lockModal($modal)
const stakingContract = store.getState().stakingContract const state = store.getState()
const decimals = store.getState().tokenDecimals const stakingContract = state.stakingContract
const validatorSetContract = state.validatorSetContract
const decimals = state.tokenDecimals
const stake = new BigNumber($modal.find('[delegator-stake]').val().replace(',', '.').trim()).shiftedBy(decimals).integerValue() const stake = new BigNumber($modal.find('[delegator-stake]').val().replace(',', '.').trim()).shiftedBy(decimals).integerValue()
let miningAddress = msg.mining_address
if (!miningAddress || miningAddress === '0x0000000000000000000000000000000000000000') {
miningAddress = await validatorSetContract.methods.miningByStakingAddress(address).call()
}
const isBanned = await validatorSetContract.methods.isValidatorBanned(miningAddress).call()
if (isBanned) {
openErrorModal('This pool is banned', 'You cannot stake into a banned pool.')
return
}
makeContractCall(stakingContract.methods.stake(address, stake.toString()), store) makeContractCall(stakingContract.methods.stake(address, stake.toString()), store)
} }

@ -91,6 +91,14 @@ export function checkForTokenDefinition (store) {
return false return false
} }
export function isStakingAllowed (state) {
if (!state.stakingAllowed) {
openErrorModal('Actions temporarily disallowed', 'The current staking epoch is ending, and staking actions are temporarily restricted. Please try again after the new epoch starts.')
return false
}
return true
}
export function isSupportedNetwork (store) { export function isSupportedNetwork (store) {
const state = store.getState() const state = store.getState()
if (state.network && state.network.authorized) { if (state.network && state.network.authorized) {

@ -47,7 +47,8 @@ defmodule BlockScoutWeb.StakesChannel do
%{validator_set: validator_set_contract.address}, %{validator_set: validator_set_contract.address},
validator_set_contract.abi validator_set_contract.abi
).mining_address ).mining_address
after rescue
_ -> nil
end end
# convert zero address to nil # convert zero address to nil
@ -175,6 +176,7 @@ defmodule BlockScoutWeb.StakesChannel do
is_active: false, is_active: false,
is_deleted: true, is_deleted: true,
self_staked_amount: 0, self_staked_amount: 0,
mining_address_hash: nil,
staking_address_hash: staking_address, staking_address_hash: staking_address,
total_staked_amount: 0 total_staked_amount: 0
} }
@ -192,6 +194,7 @@ defmodule BlockScoutWeb.StakesChannel do
html: html, html: html,
balance: balance, balance: balance,
delegator_staked: delegator_staked, delegator_staked: delegator_staked,
mining_address: nil,
min_stake: min_stake, min_stake: min_stake,
self_staked_amount: pool.self_staked_amount, self_staked_amount: pool.self_staked_amount,
total_staked_amount: pool.total_staked_amount total_staked_amount: pool.total_staked_amount
@ -301,7 +304,8 @@ defmodule BlockScoutWeb.StakesChannel do
staking_contract_address = staking_contract_address =
try do try do
ContractState.get(:staking_contract).address ContractState.get(:staking_contract).address
after rescue
_ -> nil
end end
empty_staker = staker == nil || staker == "" || staker == "0x0000000000000000000000000000000000000000" empty_staker = staker == nil || staker == "" || staker == "0x0000000000000000000000000000000000000000"
@ -328,7 +332,8 @@ defmodule BlockScoutWeb.StakesChannel do
staking_contract_address = staking_contract_address =
try do try do
ContractState.get(:staking_contract).address ContractState.get(:staking_contract).address
after rescue
_ -> nil
end end
empty_pool_staking_address = empty_pool_staking_address =
@ -698,6 +703,7 @@ defmodule BlockScoutWeb.StakesChannel do
push(socket, "contracts", %{ push(socket, "contracts", %{
staking_contract: ContractState.get(:staking_contract), staking_contract: ContractState.get(:staking_contract),
block_reward_contract: ContractState.get(:block_reward_contract), block_reward_contract: ContractState.get(:block_reward_contract),
validator_set_contract: ContractState.get(:validator_set_contract),
token_decimals: to_string(token.decimals), token_decimals: to_string(token.decimals),
token_symbol: token.symbol token_symbol: token.symbol
}) })

@ -19,11 +19,12 @@ defmodule BlockScoutWeb.StakesController do
# when a new block appears (see `staking_update` event handled in `StakesChannel`), # 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, _)`) # 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 def render_top(conn) do
epoch_number = ContractState.get(:epoch_number, 0) active_pools_length = ContractState.get(:active_pools_length, 0)
epoch_end_block = ContractState.get(:epoch_end_block, 0)
block_number = BlockNumber.get_max() 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)
token = ContractState.get(:token, %Token{}) token = ContractState.get(:token, %Token{})
staking_allowed = ContractState.get(:staking_allowed, false)
account = account =
if account_address = conn.assigns[:account] do if account_address = conn.assigns[:account] do
@ -38,11 +39,11 @@ defmodule BlockScoutWeb.StakesController do
end end
View.render_to_string(StakesView, "_stakes_top.html", View.render_to_string(StakesView, "_stakes_top.html",
epoch_number: epoch_number,
epoch_end_in: epoch_end_block - block_number,
staking_allowed: staking_allowed,
block_number: block_number,
account: account, account: account,
block_number: block_number,
candidates_limit_reached: active_pools_length >= max_candidates,
epoch_end_in: epoch_end_block - block_number,
epoch_number: epoch_number,
token: token token: token
) )
end end

@ -7,26 +7,24 @@
<%= render BlockScoutWeb.StakesView, "_stakes_stats_item_account.html", account: @account, token: @token %> <%= render BlockScoutWeb.StakesView, "_stakes_stats_item_account.html", account: @account, token: @token %>
<!-- Buttons --> <!-- Buttons -->
<div class="stakes-top-buttons"> <div class="stakes-top-buttons">
<%= if @staking_allowed do %> <%= if @account[:pool] && @account.pool.is_active do %>
<%= if @account[:pool] && @account.pool.is_active do %> <%= unless @account.pool.is_unremovable do %>
<%= unless @account.pool.is_unremovable do %> <%= render BlockScoutWeb.StakesView, "_stakes_btn_remove_pool.html", text: gettext("Remove My Pool"), extra_class: "js-remove-pool" %>
<%= render BlockScoutWeb.StakesView, "_stakes_btn_remove_pool.html", text: gettext("Remove My Pool"), extra_class: "js-remove-pool" %>
<% end %>
<% else %>
<%=
button_class = "full-width " <>
if !is_nil(@account[:pool]) or !is_nil(@account[:pool_mining_address]) do
"js-make-stake"
else
"js-become-candidate"
end
render BlockScoutWeb.CommonComponentsView, "_btn_add_full.html",
text: gettext("Become a Candidate"),
extra_class: button_class,
disabled: @account[:pool] && @account.pool.is_banned
%>
<% end %> <% end %>
<% else %>
<%=
button_class = "full-width " <>
if !is_nil(@account[:pool]) or !is_nil(@account[:pool_mining_address]) do
"js-make-stake"
else
"js-become-candidate"
end
render BlockScoutWeb.CommonComponentsView, "_btn_add_full.html",
text: gettext("Become a Candidate"),
extra_class: button_class,
disabled: @account[:pool] && @account.pool.is_banned || @candidates_limit_reached
%>
<% end %> <% end %>
<%= render BlockScoutWeb.StakesView, "_stakes_btn_claim_reward.html", text: gettext("Claim Reward"), extra_class: "full-width" %> <%= render BlockScoutWeb.StakesView, "_stakes_btn_claim_reward.html", text: gettext("Claim Reward"), extra_class: "full-width" %>

@ -12,6 +12,7 @@ defmodule Explorer.Staking.ContractReader do
epoch_number: {:staking, "stakingEpoch", []}, epoch_number: {:staking, "stakingEpoch", []},
epoch_start_block: {:staking, "stakingEpochStartBlock", []}, epoch_start_block: {:staking, "stakingEpochStartBlock", []},
inactive_pools: {:staking, "getPoolsInactive", []}, inactive_pools: {:staking, "getPoolsInactive", []},
max_candidates: {:staking, "MAX_CANDIDATES", []},
min_candidate_stake: {:staking, "candidateMinStake", []}, min_candidate_stake: {:staking, "candidateMinStake", []},
min_delegator_stake: {:staking, "delegatorMinStake", []}, min_delegator_stake: {:staking, "delegatorMinStake", []},
pools_likelihood: {:staking, "getPoolsLikelihood", []}, pools_likelihood: {:staking, "getPoolsLikelihood", []},

@ -17,11 +17,13 @@ defmodule Explorer.Staking.ContractState do
@table_name __MODULE__ @table_name __MODULE__
@table_keys [ @table_keys [
:active_pools_length,
:block_reward_contract, :block_reward_contract,
:epoch_end_block, :epoch_end_block,
:epoch_number, :epoch_number,
:epoch_start_block, :epoch_start_block,
:is_snapshotting, :is_snapshotting,
:max_candidates,
:min_candidate_stake, :min_candidate_stake,
:min_delegator_stake, :min_delegator_stake,
:snapshotted_epoch_number, :snapshotted_epoch_number,
@ -139,7 +141,10 @@ defmodule Explorer.Staking.ContractState do
not get(:is_snapshotting) not get(:is_snapshotting)
# save the general info to ETS (excluding pool list and validator list) # save the general info to ETS (excluding pool list and validator list)
settings = get_settings(global_responses, validator_min_reward_percent, block_number) settings =
global_responses
|> get_settings(validator_min_reward_percent, block_number)
|> Enum.concat(active_pools_length: Enum.count(global_responses.active_pools))
:ets.insert(@table_name, settings) :ets.insert(@table_name, settings)
@ -384,6 +389,7 @@ defmodule Explorer.Staking.ContractState do
global_responses global_responses
|> Map.take([ |> Map.take([
:token_contract_address, :token_contract_address,
:max_candidates,
:min_candidate_stake, :min_candidate_stake,
:min_delegator_stake, :min_delegator_stake,
:epoch_number, :epoch_number,

Loading…
Cancel
Save