From 821c3e729127905c357a42881c1e85d08e458cd7 Mon Sep 17 00:00:00 2001 From: Paul Tsupikoff Date: Tue, 3 Sep 2019 22:10:09 +0300 Subject: [PATCH] Validate staking contract calls in JS and allow adding <1 POA to existing stake (#2573) * Add input validation for "Become a Candidate" dialog - Mining address is trimmed before validation - Amount is checked against current token balance * Add input validation for "Make Stake" dialog - Amount is checked against current token balance - Incrementing stake by less than MIN_DELEGATOR_STAKE is allowed * Add input validation for "Move Stake" dialog - Moving stake is allowed only when resulting stakes are more that minimum * Add input validation for "Withdraw Stake" dialog - Either claim, or withdraw, or claim/withdraw dialogs are displayed depending on available amounts - Available amounts for immediate and ordered withdrawals are shown and taken into account - Remaining stake must not go under the minimal delegator stake - It is possible to reduce ordered amount, but not below zero * Validate input data in staking modals in real time (#2638) - Inputs in modal dialogs are validated on every input. - Errors are displayed on blur. - Submit button is enabled/disabled immediately. --- .../assets/css/components/_form.scss | 26 +++++ .../assets/js/lib/validation.js | 64 +++++++++++ .../js/pages/stakes/become_candidate.js | 58 +++++++--- .../assets/js/pages/stakes/make_stake.js | 35 +++++- .../assets/js/pages/stakes/move_stake.js | 45 ++++++-- .../assets/js/pages/stakes/withdraw_stake.js | 108 ++++++++++++++++-- .../channels/stakes_channel.ex | 93 +++++++++++---- .../controllers/stakes_controller.ex | 7 +- .../common_components/_input_group.html.eex | 17 +++ .../_stakes_modal_become_candidate.html.eex | 19 +-- .../stakes/_stakes_modal_move.html.eex | 20 ++-- .../stakes/_stakes_modal_stake.html.eex | 37 ++++-- .../stakes/_stakes_modal_withdraw.html.eex | 52 +++++---- 13 files changed, 455 insertions(+), 126 deletions(-) create mode 100644 apps/block_scout_web/assets/js/lib/validation.js create mode 100644 apps/block_scout_web/lib/block_scout_web/templates/common_components/_input_group.html.eex diff --git a/apps/block_scout_web/assets/css/components/_form.scss b/apps/block_scout_web/assets/css/components/_form.scss index 4b13065a05..7c6988f659 100644 --- a/apps/block_scout_web/assets/css/components/_form.scss +++ b/apps/block_scout_web/assets/css/components/_form.scss @@ -44,6 +44,32 @@ $form-control-border-color: #e2e5ec !default; } } +.input-group.input-status-error { + input { + border-color: #FF7884 !important; + border-radius: 2px 2px 0 0; + } + + .input-group-prepend { + margin-right: 0; + } + + .input-group-prepend.last { + .input-group-text { + border-color: #FF7884; + border-radius: 0 2px 0 0; + } + } + + .input-group-message { + width: 100%; + padding: 10px; + color: white; + background: #FF7884; + border-radius: 0 0 2px 2px; + } +} + .form-buttons { [class*="btn-"] { margin-bottom: 20px; diff --git a/apps/block_scout_web/assets/js/lib/validation.js b/apps/block_scout_web/assets/js/lib/validation.js new file mode 100644 index 0000000000..5574925f74 --- /dev/null +++ b/apps/block_scout_web/assets/js/lib/validation.js @@ -0,0 +1,64 @@ +import $ from 'jquery' + +export function setupValidation ($form, validators, $submit) { + const errors = {} + + updateSubmit($submit, errors) + + for (let [key, callback] of Object.entries(validators)) { + const $input = $form.find('[' + key + ']') + errors[key] = null + + $input + .ready(() => { + validateInput($input, callback, errors) + updateSubmit($submit, errors) + if (errors[key]) { + displayInputError($input, errors[key]) + } + }) + .blur(() => { + if (errors[key]) { + displayInputError($input, errors[key]) + } + }) + .on('input', () => { + hideInputError($input) + validateInput($input, callback, errors) + updateSubmit($submit, errors) + }) + } +} + +function validateInput ($input, callback, errors) { + if (!$input.val()) { + errors[$input.prop('id')] = null + return + } + + const validation = callback($input.val()) + if (validation === true) { + delete errors[$input.prop('id')] + return + } + + errors[$input.prop('id')] = validation +} + +function updateSubmit ($submit, errors) { + $submit.prop('disabled', !$.isEmptyObject(errors)) +} + +function displayInputError ($input, message) { + const group = $input.parent('.input-group') + + group.addClass('input-status-error') + group.find('.input-group-message').html(message) +} + +function hideInputError ($input) { + const group = $input.parent('.input-group') + + group.removeClass('input-status-error') + group.find('.input-group-message').html('') +} diff --git a/apps/block_scout_web/assets/js/pages/stakes/become_candidate.js b/apps/block_scout_web/assets/js/pages/stakes/become_candidate.js index 0a95a3a174..3a57335b93 100644 --- a/apps/block_scout_web/assets/js/pages/stakes/become_candidate.js +++ b/apps/block_scout_web/assets/js/pages/stakes/become_candidate.js @@ -1,6 +1,7 @@ import $ from 'jquery' import { BigNumber } from 'bignumber.js' import { openModal, openErrorModal, openWarningModal, lockModal } from '../../lib/modals' +import { setupValidation } from '../../lib/validation' import { makeContractCall } from './utils' export function openBecomeCandidateModal (store) { @@ -13,10 +14,21 @@ export function openBecomeCandidateModal (store) { .push('render_become_candidate') .receive('ok', msg => { const $modal = $(msg.html) + + setupValidation( + $modal.find('form'), + { + 'candidate-stake': value => isCandidateStakeValid(value, store, msg), + 'mining-address': value => isMiningAddressValid(value, store) + }, + $modal.find('form button') + ) + $modal.find('form').submit(() => { becomeCandidate($modal, store, msg) return false }) + openModal($modal) }) } @@ -24,30 +36,18 @@ export function openBecomeCandidateModal (store) { async function becomeCandidate ($modal, store, msg) { lockModal($modal) - const web3 = store.getState().web3 const stakingContract = store.getState().stakingContract const blockRewardContract = store.getState().blockRewardContract const decimals = store.getState().tokenDecimals - - const minStake = new BigNumber(msg.min_candidate_stake) const stake = new BigNumber($modal.find('[candidate-stake]').val().replace(',', '.').trim()).shiftedBy(decimals).integerValue() - if (!stake.isPositive() || stake.isLessThan(minStake)) { - openErrorModal('Error', `You cannot stake less than ${minStake.shiftedBy(-decimals)} ${store.getState().tokenSymbol}`) - return false - } - - const miningAddress = $modal.find('[mining-address]').val().toLowerCase() - if (miningAddress === store.getState().account || !web3.utils.isAddress(miningAddress)) { - openErrorModal('Error', 'Invalid Mining Address') - return false - } + const miningAddress = $modal.find('[mining-address]').val().trim().toLowerCase() try { if (!await stakingContract.methods.areStakeAndWithdrawAllowed().call()) { if (await blockRewardContract.methods.isSnapshotting().call()) { - openErrorModal('Error', 'Stakes are not allowed at the moment. Please try again in a few blocks') + openErrorModal('Error', 'Staking actions are temporarily restricted. Please try again in a few blocks.') } else { - openErrorModal('Error', 'Current staking epoch is finishing now, you will be able to place a stake during the next staking epoch. Please try again in a few blocks') + openErrorModal('Error', 'The current staking epoch is ending, and staking actions are temporarily restricted. Please try again when the new epoch starts.') } return false } @@ -61,3 +61,31 @@ async function becomeCandidate ($modal, store, msg) { openErrorModal('Error', err.message) } } + +function isCandidateStakeValid (value, store, msg) { + const decimals = store.getState().tokenDecimals + const minStake = new BigNumber(msg.min_candidate_stake) + const balance = new BigNumber(msg.balance) + const stake = new BigNumber(value.replace(',', '.').trim()).shiftedBy(decimals).integerValue() + + if (!stake.isPositive()) { + return 'Invalid amount' + } else if (stake.isLessThan(minStake)) { + return `Minimum candidate stake is ${minStake.shiftedBy(-decimals)} ${store.getState().tokenSymbol}` + } else if (stake.isGreaterThan(balance)) { + return 'Insufficient funds' + } + + return true +} + +function isMiningAddressValid (value, store) { + const web3 = store.getState().web3 + const miningAddress = value.trim().toLowerCase() + + if (miningAddress === store.getState().account || !web3.utils.isAddress(miningAddress)) { + return 'Invalid mining address' + } + + return true +} diff --git a/apps/block_scout_web/assets/js/pages/stakes/make_stake.js b/apps/block_scout_web/assets/js/pages/stakes/make_stake.js index f9dd8bbb5d..1667809123 100644 --- a/apps/block_scout_web/assets/js/pages/stakes/make_stake.js +++ b/apps/block_scout_web/assets/js/pages/stakes/make_stake.js @@ -1,6 +1,7 @@ import $ from 'jquery' import { BigNumber } from 'bignumber.js' -import { openModal, openErrorModal, openWarningModal, lockModal } from '../../lib/modals' +import { openModal, openWarningModal, lockModal } from '../../lib/modals' +import { setupValidation } from '../../lib/validation' import { makeContractCall, setupChart } from './utils' export function openMakeStakeModal (event, store) { @@ -16,6 +17,14 @@ export function openMakeStakeModal (event, store) { .receive('ok', msg => { const $modal = $(msg.html) setupChart($modal.find('.js-stakes-progress'), msg.self_staked_amount, msg.staked_amount) + setupValidation( + $modal.find('form'), + { + 'delegator-stake': value => isDelegatorStakeValid(value, store, msg, address) + }, + $modal.find('form button') + ) + $modal.find('form').submit(() => { makeStake($modal, address, store, msg) return false @@ -30,13 +39,27 @@ function makeStake ($modal, address, store, msg) { const stakingContract = store.getState().stakingContract const decimals = store.getState().tokenDecimals - const minStake = new BigNumber(msg.min_delegator_stake) const stake = new BigNumber($modal.find('[delegator-stake]').val().replace(',', '.').trim()).shiftedBy(decimals).integerValue() - if (!stake.isPositive() || stake.isLessThan(minStake)) { - openErrorModal('Error', `You cannot stake less than ${minStake.shiftedBy(-decimals)} ${store.getState().tokenSymbol}`) - return false + makeContractCall(stakingContract.methods.stake(address, stake.toString()), store) +} + +function isDelegatorStakeValid (value, store, msg, address) { + const decimals = store.getState().tokenDecimals + const minStake = new BigNumber(msg.min_stake) + const currentStake = new BigNumber(msg.delegator_staked) + const balance = new BigNumber(msg.balance) + const stake = new BigNumber(value.replace(',', '.').trim()).shiftedBy(decimals).integerValue() + const account = store.getState().account + + if (!stake.isPositive()) { + return 'Invalid amount' + } else if (stake.plus(currentStake).isLessThan(minStake)) { + const staker = (account === address) ? 'candidate' : 'delegate' + return `Minimum ${staker} stake is ${minStake.shiftedBy(-decimals)} ${store.getState().tokenSymbol}` + } else if (stake.isGreaterThan(balance)) { + return 'Insufficient funds' } - makeContractCall(stakingContract.methods.stake(address, stake.toString()), store) + return true } diff --git a/apps/block_scout_web/assets/js/pages/stakes/move_stake.js b/apps/block_scout_web/assets/js/pages/stakes/move_stake.js index f53acd03c1..a159a36bcc 100644 --- a/apps/block_scout_web/assets/js/pages/stakes/move_stake.js +++ b/apps/block_scout_web/assets/js/pages/stakes/move_stake.js @@ -1,6 +1,7 @@ import $ from 'jquery' import { BigNumber } from 'bignumber.js' -import { openModal, openErrorModal, lockModal } from '../../lib/modals' +import { openModal, lockModal } from '../../lib/modals' +import { setupValidation } from '../../lib/validation' import { makeContractCall, setupChart } from './utils' export function openMoveStakeModal (event, store) { @@ -16,9 +17,17 @@ export function openMoveStakeModal (event, store) { } function setupModal ($modal, fromAddress, store, msg) { - setupChart($modal.find('.js-pool-from-progress'), msg.from_self_staked_amount, msg.from_staked_amount) - if ($modal.find('.js-pool-to-progress').length) { - setupChart($modal.find('.js-pool-to-progress'), msg.to_self_staked_amount, msg.to_staked_amount) + setupChart($modal.find('.js-pool-from-progress'), msg.from.self_staked_amount, msg.from.staked_amount) + if (msg.to) { + setupChart($modal.find('.js-pool-to-progress'), msg.to.self_staked_amount, msg.to.staked_amount) + + setupValidation( + $modal.find('form'), + { + 'move-amount': value => isMoveAmountValid(value, store, msg) + }, + $modal.find('form button') + ) } $modal.find('form').submit(() => { @@ -44,16 +53,30 @@ function moveStake ($modal, fromAddress, store, msg) { const stakingContract = store.getState().stakingContract const decimals = store.getState().tokenDecimals + const stake = new BigNumber($modal.find('[move-amount]').val().replace(',', '.').trim()).shiftedBy(decimals).integerValue() + + const toAddress = $modal.find('[pool-select]').val() + makeContractCall(stakingContract.methods.moveStake(fromAddress, toAddress, stake.toString()), store) +} - const minStake = new BigNumber(msg.min_delegator_stake) +function isMoveAmountValid (value, store, msg) { + const decimals = store.getState().tokenDecimals + const minFromStake = new BigNumber(msg.from.min_stake) + const minToStake = (msg.to) ? new BigNumber(msg.to.min_stake) : null const maxAllowed = new BigNumber(msg.max_withdraw_allowed) - const stake = new BigNumber($modal.find('[move-amount]').val().replace(',', '.').trim()).shiftedBy(decimals).integerValue() + const currentFromStake = new BigNumber(msg.from.stake_amount) + const currentToStake = (msg.to) ? new BigNumber(msg.to.stake_amount) : null + const stake = new BigNumber(value.replace(',', '.').trim()).shiftedBy(decimals).integerValue() - if (!stake.isPositive() || stake.isLessThan(minStake) || stake.isGreaterThan(maxAllowed)) { - openErrorModal('Error', `You cannot stake less than ${minStake.shiftedBy(-decimals)} ${store.getState().tokenSymbol} and more than ${maxAllowed.shiftedBy(-decimals)} ${store.getState().tokenSymbol}`) - return false + if (!stake.isPositive()) { + return 'Invalid amount' + } else if (stake.plus(currentToStake).isLessThan(minToStake)) { + return `You must move at least ${minToStake.shiftedBy(-decimals)} ${store.getState().tokenSymbol} to the selected pool` + } else if (stake.isGreaterThan(maxAllowed)) { + return `You have ${maxAllowed.shiftedBy(-decimals)} ${store.getState().tokenSymbol} available to move` + } else if (stake.isLessThan(currentFromStake) && currentFromStake.minus(stake).isLessThan(minFromStake)) { + return `A minimum of ${minFromStake.shiftedBy(-decimals)} ${store.getState().tokenSymbol} is required to remain in the current pool, or move the entire amount to leave this pool` } - const toAddress = $modal.find('[pool-select]').val() - makeContractCall(stakingContract.methods.moveStake(fromAddress, toAddress, stake.toString()), store) + return true } diff --git a/apps/block_scout_web/assets/js/pages/stakes/withdraw_stake.js b/apps/block_scout_web/assets/js/pages/stakes/withdraw_stake.js index 6497a2fafd..34f844a322 100644 --- a/apps/block_scout_web/assets/js/pages/stakes/withdraw_stake.js +++ b/apps/block_scout_web/assets/js/pages/stakes/withdraw_stake.js @@ -1,6 +1,7 @@ import $ from 'jquery' import { BigNumber } from 'bignumber.js' -import { openModal, openQuestionModal, lockModal } from '../../lib/modals' +import { openModal, openErrorModal, openQuestionModal, lockModal } from '../../lib/modals' +import { setupValidation } from '../../lib/validation' import { makeContractCall, setupChart } from './utils' export function openWithdrawStakeModal (event, store) { @@ -9,13 +10,15 @@ export function openWithdrawStakeModal (event, store) { store.getState().channel .push('render_withdraw_stake', { address }) .receive('ok', msg => { - if (msg.claim_html) { + if (msg.claim_html && msg.withdraw_html) { openQuestionModal( 'Claim or order', 'Do you want withdraw or claim ordered withdraw?', () => setupClaimWithdrawModal(address, store, msg), () => setupWithdrawStakeModal(address, store, msg), 'Claim', 'Withdraw' ) + } else if (msg.claim_html) { + setupClaimWithdrawModal(address, store, msg) } else { setupWithdrawStakeModal(address, store, msg) } @@ -33,14 +36,38 @@ function setupClaimWithdrawModal (address, store, msg) { } function setupWithdrawStakeModal (address, store, msg) { - const $modal = $(msg.html) + const $modal = $(msg.withdraw_html) setupChart($modal.find('.js-stakes-progress'), msg.self_staked_amount, msg.staked_amount) + setupValidation( + $modal.find('form'), + { + 'amount': value => isAmountValid(value, store, msg) + }, + $modal.find('form button') + ) + + setupValidation( + $modal.find('form'), + { + 'amount': value => isWithdrawAmountValid(value, store, msg) + }, + $modal.find('form button.withdraw') + ) + + setupValidation( + $modal.find('form'), + { + 'amount': value => isOrderWithdrawAmountValid(value, store, msg) + }, + $modal.find('form button.order-withdraw') + ) + $modal.find('.btn-full-primary.withdraw').click(() => { - withdrawStake($modal, address, store) + withdrawStake($modal, address, store, msg) return false }) $modal.find('.btn-full-primary.order-withdraw').click(() => { - orderWithdraw($modal, address, store) + orderWithdraw($modal, address, store, msg) return false }) openModal($modal) @@ -50,28 +77,85 @@ function claimWithdraw ($modal, address, store) { lockModal($modal) const stakingContract = store.getState().stakingContract - makeContractCall(stakingContract.methods.claimOrderedWithdraw(address), store) } -function withdrawStake ($modal, address, store) { - lockModal($modal) +function withdrawStake ($modal, address, store, msg) { + lockModal($modal, $modal.find('.btn-full-primary.withdraw')) const stakingContract = store.getState().stakingContract const decimals = store.getState().tokenDecimals - const amount = new BigNumber($modal.find('[amount]').val().replace(',', '.').trim()).shiftedBy(decimals).integerValue() makeContractCall(stakingContract.methods.withdraw(address, amount.toString()), store) } -function orderWithdraw ($modal, address, store) { - lockModal($modal) +function orderWithdraw ($modal, address, store, msg) { + lockModal($modal, $modal.find('.btn-full-primary.order-withdraw')) const stakingContract = store.getState().stakingContract const decimals = store.getState().tokenDecimals - + const orderedWithdraw = new BigNumber(msg.ordered_withdraw) const amount = new BigNumber($modal.find('[amount]').val().replace(',', '.').trim()).shiftedBy(decimals).integerValue() + if (amount.isLessThan(orderedWithdraw.negated())) { + openErrorModal('Error', `You cannot reduce withdrawal by more than ${orderedWithdraw.shiftedBy(-decimals)} ${store.getState().tokenSymbol}`) + return false + } + makeContractCall(stakingContract.methods.orderWithdraw(address, amount.toString()), store) } + +function isAmountValid (value, store, msg) { + const decimals = store.getState().tokenDecimals + const minStake = new BigNumber(msg.min_stake) + const currentStake = new BigNumber(msg.delegator_staked) + const amount = new BigNumber(value.replace(',', '.').trim()).shiftedBy(decimals).integerValue() + + if (!amount.isPositive() && !amount.isNegative()) { + return 'Invalid amount' + } else if (amount.isLessThan(currentStake) && currentStake.minus(amount).isLessThan(minStake)) { + return `A minimum of ${minStake.shiftedBy(-decimals)} ${store.getState().tokenSymbol} is required to remain in the pool, or withdraw the entire amount to leave this pool` + } + + return true +} + +function isWithdrawAmountValid (value, store, msg) { + const decimals = store.getState().tokenDecimals + const minStake = new BigNumber(msg.min_stake) + const currentStake = new BigNumber(msg.delegator_staked) + const maxAllowed = new BigNumber(msg.max_withdraw_allowed) + const amount = new BigNumber(value.replace(',', '.').trim()).shiftedBy(decimals).integerValue() + + if (!amount.isPositive()) { + return null + } else if (amount.isLessThan(currentStake) && currentStake.minus(amount).isLessThan(minStake)) { + return null + } else if (!amount.isPositive() || amount.isGreaterThan(maxAllowed)) { + return null + } + + return true +} + +function isOrderWithdrawAmountValid (value, store, msg) { + const decimals = store.getState().tokenDecimals + const minStake = new BigNumber(msg.min_stake) + const currentStake = new BigNumber(msg.delegator_staked) + const orderedWithdraw = new BigNumber(msg.ordered_withdraw) + const maxAllowed = new BigNumber(msg.max_ordered_withdraw_allowed) + const amount = new BigNumber(value.replace(',', '.').trim()).shiftedBy(decimals).integerValue() + + if (!amount.isPositive() && !amount.isNegative()) { + return null + } else if (amount.isLessThan(currentStake) && currentStake.minus(amount).isLessThan(minStake)) { + return null + } else if (amount.isGreaterThan(maxAllowed)) { + return null + } else if (amount.isLessThan(orderedWithdraw.negated())) { + return null + } + + return true +} diff --git a/apps/block_scout_web/lib/block_scout_web/channels/stakes_channel.ex b/apps/block_scout_web/lib/block_scout_web/channels/stakes_channel.ex index 3e199e4aff..cd7e8d2992 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 @@ -102,21 +102,31 @@ defmodule BlockScoutWeb.StakesChannel do def handle_in("render_make_stake", %{"address" => staking_address}, socket) do pool = Chain.staking_pool(staking_address) - min_delegator_stake = ContractState.get(:min_delegator_stake) + delegator = Chain.staking_pool_delegator(staking_address, socket.assigns.account) token = ContractState.get(:token) balance = Chain.fetch_last_token_balance(socket.assigns.account, token.contract_address_hash) + min_stake = + if staking_address == socket.assigns.account do + ContractState.get(:min_candidate_stake) + else + ContractState.get(:min_delegator_stake) + end + html = View.render_to_string(StakesView, "_stakes_modal_stake.html", - min_delegator_stake: min_delegator_stake, + min_stake: min_stake, balance: balance, token: token, - pool: pool + pool: pool, + delegator: delegator ) result = %{ html: html, - min_delegator_stake: min_delegator_stake, + balance: balance, + delegator_staked: (delegator && delegator.stake_amount) || 0, + min_stake: min_stake, self_staked_amount: pool.self_staked_amount, staked_amount: pool.staked_amount } @@ -128,28 +138,53 @@ defmodule BlockScoutWeb.StakesChannel do pool_from = Chain.staking_pool(from_address) pool_to = to_address && Chain.staking_pool(to_address) pools = Chain.staking_pools(:active, :all) - delegator = Chain.staking_pool_delegator(from_address, socket.assigns.account) - min_delegator_stake = ContractState.get(:min_delegator_stake) + delegator_from = Chain.staking_pool_delegator(from_address, socket.assigns.account) + delegator_to = to_address && Chain.staking_pool_delegator(to_address, socket.assigns.account) token = ContractState.get(:token) + min_from_stake = + if delegator_from.delegator_address_hash == delegator_from.pool_address_hash do + ContractState.get(:min_candidate_stake) + else + ContractState.get(:min_delegator_stake) + end + + min_to_stake = + if to_address == socket.assigns.account do + ContractState.get(:min_candidate_stake) + else + ContractState.get(:min_delegator_stake) + end + html = View.render_to_string(StakesView, "_stakes_modal_move.html", token: token, pools: pools, pool_from: pool_from, pool_to: pool_to, - delegator: delegator, + delegator_from: delegator_from, + delegator_to: delegator_to, amount: amount ) result = %{ html: html, - min_delegator_stake: min_delegator_stake, - max_withdraw_allowed: delegator.max_withdraw_allowed, - from_self_staked_amount: pool_from.self_staked_amount, - from_staked_amount: pool_from.staked_amount, - to_self_staked_amount: pool_to && pool_to.self_staked_amount, - to_staked_amount: pool_to && pool_to.staked_amount + max_withdraw_allowed: delegator_from.max_withdraw_allowed, + from: %{ + stake_amount: delegator_from.stake_amount, + min_stake: min_from_stake, + self_staked_amount: pool_from.self_staked_amount, + staked_amount: pool_from.staked_amount + }, + to: + if pool_to do + %{ + stake_amount: (delegator_to && delegator_to.stake_amount) || 0, + min_stake: min_to_stake, + self_staked_amount: pool_to.self_staked_amount, + staked_amount: pool_to.staked_amount + } + end } {:reply, {:ok, result}, socket} @@ -161,6 +196,13 @@ defmodule BlockScoutWeb.StakesChannel do delegator = Chain.staking_pool_delegator(staking_address, socket.assigns.account) epoch_number = ContractState.get(:epoch_number, 0) + min_stake = + if delegator.delegator_address_hash == delegator.pool_address_hash do + ContractState.get(:min_candidate_stake) + else + ContractState.get(:min_delegator_stake) + end + claim_html = if Decimal.positive?(delegator.ordered_withdraw) and delegator.ordered_withdraw_epoch < epoch_number do View.render_to_string(StakesView, "_stakes_modal_claim.html", @@ -170,18 +212,27 @@ defmodule BlockScoutWeb.StakesChannel do ) end - html = - View.render_to_string(StakesView, "_stakes_modal_withdraw.html", - token: token, - delegator: delegator, - pool: pool - ) + withdraw_html = + if Decimal.positive?(delegator.ordered_withdraw) or + Decimal.positive?(delegator.max_withdraw_allowed) or + Decimal.positive?(delegator.max_ordered_withdraw_allowed) do + View.render_to_string(StakesView, "_stakes_modal_withdraw.html", + token: token, + delegator: delegator, + pool: pool + ) + end result = %{ claim_html: claim_html, - html: html, + withdraw_html: withdraw_html, self_staked_amount: pool.self_staked_amount, - staked_amount: pool.staked_amount + staked_amount: pool.staked_amount, + delegator_staked: delegator.stake_amount, + ordered_withdraw: delegator.ordered_withdraw, + max_withdraw_allowed: delegator.max_withdraw_allowed, + max_ordered_withdraw_allowed: delegator.max_ordered_withdraw_allowed, + min_stake: min_stake } {:reply, {:ok, result}, socket} 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 cabe631cc6..d20b5efd98 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 @@ -142,14 +142,15 @@ defmodule BlockScoutWeb.StakesController do defp move_allowed?(nil), do: false defp move_allowed?(delegator) do - delegator.is_active and Decimal.positive?(delegator.max_withdraw_allowed) + Decimal.positive?(delegator.max_withdraw_allowed) end defp withdraw_allowed?(nil, _epoch_number), do: false defp withdraw_allowed?(delegator, epoch_number) do - (delegator.is_active and Decimal.positive?(delegator.max_withdraw_allowed)) or - (delegator.is_active and Decimal.positive?(delegator.max_ordered_withdraw_allowed)) or + Decimal.positive?(delegator.max_withdraw_allowed) or + Decimal.positive?(delegator.max_ordered_withdraw_allowed) or + Decimal.positive?(delegator.ordered_withdraw) or (Decimal.positive?(delegator.ordered_withdraw) and delegator.ordered_withdraw_epoch < epoch_number) end end diff --git a/apps/block_scout_web/lib/block_scout_web/templates/common_components/_input_group.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/common_components/_input_group.html.eex new file mode 100644 index 0000000000..5c9c929618 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/common_components/_input_group.html.eex @@ -0,0 +1,17 @@ +
+ + type="<%= if assigns[:type] do @type end %>" + class="<%= if assigns[:input_classes] do @input_classes end %>" + placeholder="<%= if assigns[:placeholder] do @placeholder end %>" + value="<%= if assigns[:value] do @value end %>" + <%= if assigns[:disabled] do "disabled" end %> + /> + <%= if assigns[:prepend] do %> +
+
<%= @prepend %>
+
+ <% end %> +
<%= if assigns[:message] do @message end %>
+
\ No newline at end of file diff --git a/apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_modal_become_candidate.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_modal_become_candidate.html.eex index 33ef69fa04..6e1104737a 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_modal_become_candidate.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_modal_become_candidate.html.eex @@ -7,22 +7,9 @@ <%= render BlockScoutWeb.CommonComponentsView, "_modal_close_button.html" %> <%= render BlockScoutWeb.CommonComponentsView, "_modal_bottom_disclaimer.html", text: "Lorem ipsum dolor sit amet, consect adipiscing elit, sed do eiusmod temp incididunt ut labore et dolore magna. Sed do eiusmod temp incididunt ut.", extra_class: "b-b-r-0" %>