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.
staking
Paul Tsupikoff 5 years ago committed by Victor Baranov
parent 7ef37f0071
commit 821c3e7291
  1. 26
      apps/block_scout_web/assets/css/components/_form.scss
  2. 64
      apps/block_scout_web/assets/js/lib/validation.js
  3. 58
      apps/block_scout_web/assets/js/pages/stakes/become_candidate.js
  4. 35
      apps/block_scout_web/assets/js/pages/stakes/make_stake.js
  5. 45
      apps/block_scout_web/assets/js/pages/stakes/move_stake.js
  6. 108
      apps/block_scout_web/assets/js/pages/stakes/withdraw_stake.js
  7. 93
      apps/block_scout_web/lib/block_scout_web/channels/stakes_channel.ex
  8. 7
      apps/block_scout_web/lib/block_scout_web/controllers/stakes_controller.ex
  9. 17
      apps/block_scout_web/lib/block_scout_web/templates/common_components/_input_group.html.eex
  10. 19
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_modal_become_candidate.html.eex
  11. 20
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_modal_move.html.eex
  12. 37
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_modal_stake.html.eex
  13. 52
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_modal_withdraw.html.eex

@ -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 { .form-buttons {
[class*="btn-"] { [class*="btn-"] {
margin-bottom: 20px; margin-bottom: 20px;

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

@ -1,6 +1,7 @@
import $ from 'jquery' 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 { makeContractCall } from './utils' import { makeContractCall } from './utils'
export function openBecomeCandidateModal (store) { export function openBecomeCandidateModal (store) {
@ -13,10 +14,21 @@ export function openBecomeCandidateModal (store) {
.push('render_become_candidate') .push('render_become_candidate')
.receive('ok', msg => { .receive('ok', msg => {
const $modal = $(msg.html) 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(() => { $modal.find('form').submit(() => {
becomeCandidate($modal, store, msg) becomeCandidate($modal, store, msg)
return false return false
}) })
openModal($modal) openModal($modal)
}) })
} }
@ -24,30 +36,18 @@ export function openBecomeCandidateModal (store) {
async function becomeCandidate ($modal, store, msg) { async function becomeCandidate ($modal, store, msg) {
lockModal($modal) lockModal($modal)
const web3 = store.getState().web3
const stakingContract = store.getState().stakingContract const stakingContract = store.getState().stakingContract
const blockRewardContract = store.getState().blockRewardContract const blockRewardContract = store.getState().blockRewardContract
const decimals = store.getState().tokenDecimals 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() const stake = new BigNumber($modal.find('[candidate-stake]').val().replace(',', '.').trim()).shiftedBy(decimals).integerValue()
if (!stake.isPositive() || stake.isLessThan(minStake)) { const miningAddress = $modal.find('[mining-address]').val().trim().toLowerCase()
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
}
try { try {
if (!await stakingContract.methods.areStakeAndWithdrawAllowed().call()) { if (!await stakingContract.methods.areStakeAndWithdrawAllowed().call()) {
if (await blockRewardContract.methods.isSnapshotting().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 { } 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 return false
} }
@ -61,3 +61,31 @@ async function becomeCandidate ($modal, store, msg) {
openErrorModal('Error', err.message) 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
}

@ -1,6 +1,7 @@
import $ from 'jquery' import $ from 'jquery'
import { BigNumber } from 'bignumber.js' 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' import { makeContractCall, setupChart } from './utils'
export function openMakeStakeModal (event, store) { export function openMakeStakeModal (event, store) {
@ -16,6 +17,14 @@ export function openMakeStakeModal (event, store) {
.receive('ok', msg => { .receive('ok', msg => {
const $modal = $(msg.html) const $modal = $(msg.html)
setupChart($modal.find('.js-stakes-progress'), msg.self_staked_amount, msg.staked_amount) 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(() => { $modal.find('form').submit(() => {
makeStake($modal, address, store, msg) makeStake($modal, address, store, msg)
return false return false
@ -30,13 +39,27 @@ function makeStake ($modal, address, store, msg) {
const stakingContract = store.getState().stakingContract const stakingContract = store.getState().stakingContract
const decimals = store.getState().tokenDecimals 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() const stake = new BigNumber($modal.find('[delegator-stake]').val().replace(',', '.').trim()).shiftedBy(decimals).integerValue()
if (!stake.isPositive() || stake.isLessThan(minStake)) { makeContractCall(stakingContract.methods.stake(address, stake.toString()), store)
openErrorModal('Error', `You cannot stake less than ${minStake.shiftedBy(-decimals)} ${store.getState().tokenSymbol}`) }
return false
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
} }

@ -1,6 +1,7 @@
import $ from 'jquery' import $ from 'jquery'
import { BigNumber } from 'bignumber.js' 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' import { makeContractCall, setupChart } from './utils'
export function openMoveStakeModal (event, store) { export function openMoveStakeModal (event, store) {
@ -16,9 +17,17 @@ export function openMoveStakeModal (event, store) {
} }
function setupModal ($modal, fromAddress, store, msg) { function setupModal ($modal, fromAddress, store, msg) {
setupChart($modal.find('.js-pool-from-progress'), msg.from_self_staked_amount, msg.from_staked_amount) setupChart($modal.find('.js-pool-from-progress'), msg.from.self_staked_amount, msg.from.staked_amount)
if ($modal.find('.js-pool-to-progress').length) { if (msg.to) {
setupChart($modal.find('.js-pool-to-progress'), msg.to_self_staked_amount, msg.to_staked_amount) 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(() => { $modal.find('form').submit(() => {
@ -44,16 +53,30 @@ function moveStake ($modal, fromAddress, store, msg) {
const stakingContract = store.getState().stakingContract const stakingContract = store.getState().stakingContract
const decimals = store.getState().tokenDecimals 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 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)) { if (!stake.isPositive()) {
openErrorModal('Error', `You cannot stake less than ${minStake.shiftedBy(-decimals)} ${store.getState().tokenSymbol} and more than ${maxAllowed.shiftedBy(-decimals)} ${store.getState().tokenSymbol}`) return 'Invalid amount'
return false } 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() return true
makeContractCall(stakingContract.methods.moveStake(fromAddress, toAddress, stake.toString()), store)
} }

@ -1,6 +1,7 @@
import $ from 'jquery' import $ from 'jquery'
import { BigNumber } from 'bignumber.js' 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' import { makeContractCall, setupChart } from './utils'
export function openWithdrawStakeModal (event, store) { export function openWithdrawStakeModal (event, store) {
@ -9,13 +10,15 @@ export function openWithdrawStakeModal (event, store) {
store.getState().channel store.getState().channel
.push('render_withdraw_stake', { address }) .push('render_withdraw_stake', { address })
.receive('ok', msg => { .receive('ok', msg => {
if (msg.claim_html) { if (msg.claim_html && msg.withdraw_html) {
openQuestionModal( openQuestionModal(
'Claim or order', 'Do you want withdraw or claim ordered withdraw?', 'Claim or order', 'Do you want withdraw or claim ordered withdraw?',
() => setupClaimWithdrawModal(address, store, msg), () => setupClaimWithdrawModal(address, store, msg),
() => setupWithdrawStakeModal(address, store, msg), () => setupWithdrawStakeModal(address, store, msg),
'Claim', 'Withdraw' 'Claim', 'Withdraw'
) )
} else if (msg.claim_html) {
setupClaimWithdrawModal(address, store, msg)
} else { } else {
setupWithdrawStakeModal(address, store, msg) setupWithdrawStakeModal(address, store, msg)
} }
@ -33,14 +36,38 @@ function setupClaimWithdrawModal (address, store, msg) {
} }
function setupWithdrawStakeModal (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) 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(() => { $modal.find('.btn-full-primary.withdraw').click(() => {
withdrawStake($modal, address, store) withdrawStake($modal, address, store, msg)
return false return false
}) })
$modal.find('.btn-full-primary.order-withdraw').click(() => { $modal.find('.btn-full-primary.order-withdraw').click(() => {
orderWithdraw($modal, address, store) orderWithdraw($modal, address, store, msg)
return false return false
}) })
openModal($modal) openModal($modal)
@ -50,28 +77,85 @@ function claimWithdraw ($modal, address, store) {
lockModal($modal) lockModal($modal)
const stakingContract = store.getState().stakingContract const stakingContract = store.getState().stakingContract
makeContractCall(stakingContract.methods.claimOrderedWithdraw(address), store) makeContractCall(stakingContract.methods.claimOrderedWithdraw(address), store)
} }
function withdrawStake ($modal, address, store) { function withdrawStake ($modal, address, store, msg) {
lockModal($modal) lockModal($modal, $modal.find('.btn-full-primary.withdraw'))
const stakingContract = store.getState().stakingContract const stakingContract = store.getState().stakingContract
const decimals = store.getState().tokenDecimals const decimals = store.getState().tokenDecimals
const amount = new BigNumber($modal.find('[amount]').val().replace(',', '.').trim()).shiftedBy(decimals).integerValue() const amount = new BigNumber($modal.find('[amount]').val().replace(',', '.').trim()).shiftedBy(decimals).integerValue()
makeContractCall(stakingContract.methods.withdraw(address, amount.toString()), store) makeContractCall(stakingContract.methods.withdraw(address, amount.toString()), store)
} }
function orderWithdraw ($modal, address, store) { function orderWithdraw ($modal, address, store, msg) {
lockModal($modal) lockModal($modal, $modal.find('.btn-full-primary.order-withdraw'))
const stakingContract = store.getState().stakingContract const stakingContract = store.getState().stakingContract
const decimals = store.getState().tokenDecimals 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() 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) 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
}

@ -102,21 +102,31 @@ defmodule BlockScoutWeb.StakesChannel do
def handle_in("render_make_stake", %{"address" => staking_address}, socket) do def handle_in("render_make_stake", %{"address" => staking_address}, socket) do
pool = Chain.staking_pool(staking_address) 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) token = ContractState.get(:token)
balance = Chain.fetch_last_token_balance(socket.assigns.account, token.contract_address_hash) 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 = html =
View.render_to_string(StakesView, "_stakes_modal_stake.html", View.render_to_string(StakesView, "_stakes_modal_stake.html",
min_delegator_stake: min_delegator_stake, min_stake: min_stake,
balance: balance, balance: balance,
token: token, token: token,
pool: pool pool: pool,
delegator: delegator
) )
result = %{ result = %{
html: html, 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, self_staked_amount: pool.self_staked_amount,
staked_amount: pool.staked_amount staked_amount: pool.staked_amount
} }
@ -128,28 +138,53 @@ defmodule BlockScoutWeb.StakesChannel do
pool_from = Chain.staking_pool(from_address) pool_from = Chain.staking_pool(from_address)
pool_to = to_address && Chain.staking_pool(to_address) pool_to = to_address && Chain.staking_pool(to_address)
pools = Chain.staking_pools(:active, :all) pools = Chain.staking_pools(:active, :all)
delegator = Chain.staking_pool_delegator(from_address, socket.assigns.account) delegator_from = Chain.staking_pool_delegator(from_address, socket.assigns.account)
min_delegator_stake = ContractState.get(:min_delegator_stake) delegator_to = to_address && Chain.staking_pool_delegator(to_address, socket.assigns.account)
token = ContractState.get(:token) 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 = html =
View.render_to_string(StakesView, "_stakes_modal_move.html", View.render_to_string(StakesView, "_stakes_modal_move.html",
token: token, token: token,
pools: pools, pools: pools,
pool_from: pool_from, pool_from: pool_from,
pool_to: pool_to, pool_to: pool_to,
delegator: delegator, delegator_from: delegator_from,
delegator_to: delegator_to,
amount: amount amount: amount
) )
result = %{ result = %{
html: html, html: html,
min_delegator_stake: min_delegator_stake, max_withdraw_allowed: delegator_from.max_withdraw_allowed,
max_withdraw_allowed: delegator.max_withdraw_allowed, from: %{
from_self_staked_amount: pool_from.self_staked_amount, stake_amount: delegator_from.stake_amount,
from_staked_amount: pool_from.staked_amount, min_stake: min_from_stake,
to_self_staked_amount: pool_to && pool_to.self_staked_amount, self_staked_amount: pool_from.self_staked_amount,
to_staked_amount: pool_to && pool_to.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} {:reply, {:ok, result}, socket}
@ -161,6 +196,13 @@ defmodule BlockScoutWeb.StakesChannel do
delegator = Chain.staking_pool_delegator(staking_address, socket.assigns.account) delegator = Chain.staking_pool_delegator(staking_address, socket.assigns.account)
epoch_number = ContractState.get(:epoch_number, 0) 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 = claim_html =
if Decimal.positive?(delegator.ordered_withdraw) and delegator.ordered_withdraw_epoch < epoch_number do if Decimal.positive?(delegator.ordered_withdraw) and delegator.ordered_withdraw_epoch < epoch_number do
View.render_to_string(StakesView, "_stakes_modal_claim.html", View.render_to_string(StakesView, "_stakes_modal_claim.html",
@ -170,18 +212,27 @@ defmodule BlockScoutWeb.StakesChannel do
) )
end end
html = withdraw_html =
View.render_to_string(StakesView, "_stakes_modal_withdraw.html", if Decimal.positive?(delegator.ordered_withdraw) or
token: token, Decimal.positive?(delegator.max_withdraw_allowed) or
delegator: delegator, Decimal.positive?(delegator.max_ordered_withdraw_allowed) do
pool: pool View.render_to_string(StakesView, "_stakes_modal_withdraw.html",
) token: token,
delegator: delegator,
pool: pool
)
end
result = %{ result = %{
claim_html: claim_html, claim_html: claim_html,
html: html, withdraw_html: withdraw_html,
self_staked_amount: pool.self_staked_amount, 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} {:reply, {:ok, result}, socket}

@ -142,14 +142,15 @@ defmodule BlockScoutWeb.StakesController do
defp move_allowed?(nil), do: false defp move_allowed?(nil), do: false
defp move_allowed?(delegator) do defp move_allowed?(delegator) do
delegator.is_active and Decimal.positive?(delegator.max_withdraw_allowed) Decimal.positive?(delegator.max_withdraw_allowed)
end end
defp withdraw_allowed?(nil, _epoch_number), do: false defp withdraw_allowed?(nil, _epoch_number), do: false
defp withdraw_allowed?(delegator, epoch_number) do defp withdraw_allowed?(delegator, epoch_number) do
(delegator.is_active and Decimal.positive?(delegator.max_withdraw_allowed)) or Decimal.positive?(delegator.max_withdraw_allowed) or
(delegator.is_active and Decimal.positive?(delegator.max_ordered_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) (Decimal.positive?(delegator.ordered_withdraw) and delegator.ordered_withdraw_epoch < epoch_number)
end end
end end

@ -0,0 +1,17 @@
<div class="input-group <%= if assigns[:classes] do @classes end %>">
<input
id="<%= if assigns[:id] do @id end %>"
<%= if assigns[:attributes] do @attributes end %>
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 %>
<div class="input-group-prepend last">
<div class="input-group-text"><%= @prepend %></div>
</div>
<% end %>
<div class="input-group-message"><%= if assigns[:message] do @message end %></div>
</div>

@ -7,22 +7,9 @@
<%= render BlockScoutWeb.CommonComponentsView, "_modal_close_button.html" %> <%= render BlockScoutWeb.CommonComponentsView, "_modal_close_button.html" %>
<div class="modal-body"> <div class="modal-body">
<form> <form>
<div class="input-group form-group"> <%= render BlockScoutWeb.CommonComponentsView, "_input_group.html", id: "candidate-stake", classes: "form-group", input_classes: "form-control n-b-r", attributes: "candidate-stake", type: "text", placeholder: gettext("Amount"), prepend: @token.symbol %>
<input candidate-stake type="text" class="form-control n-b-r" placeholder="<%= gettext("Amount") %>">
<div class="input-group-prepend last"> <%= render BlockScoutWeb.CommonComponentsView, "_input_group.html", id: "mining-address", classes: "form-group", input_classes: "form-control", attributes: "mining-address", type: "text", placeholder: gettext("Your Mining Address"), value: @pool && @pool.mining_address_hash, disabled: @pool %>
<div class="input-group-text"><%= @token.symbol %></div>
</div>
</div>
<div class="form-group">
<input
mining-address
type="text"
class="form-control"
placeholder="<%= gettext("Your Mining Address") %>"
value="<%= @pool && @pool.mining_address_hash %>"
<%= if @pool do "disabled" end %>
/>
</div>
<p class="form-p m-b-0">Minimum Stake: <p class="form-p m-b-0">Minimum Stake:
<span class="text-dark"> <span class="text-dark">
<%= format_according_to_decimals(@min_candidate_stake, @token.decimals) %> <%= @token.symbol %> <%= format_according_to_decimals(@min_candidate_stake, @token.decimals) %> <%= @token.symbol %>

@ -12,15 +12,10 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form> <form>
<div class="input-group form-group"> <%= render BlockScoutWeb.CommonComponentsView, "_input_group.html", id: "move-amount", classes: "form-group", input_classes: "form-control n-b-r", attributes: "move-amount", type: "text", placeholder: gettext("Amount"), prepend: @token.symbol, value: @amount %>
<input move-amount type="text" class="form-control n-b-r" placeholder="<%= gettext("Amount") %>" value="<%= @amount %>">
<div class="input-group-prepend last">
<div class="input-group-text"><%= @token.symbol %></div>
</div>
</div>
<p class="form-p">You Staked: <p class="form-p">You Staked:
<span class="text-dark"> <span class="text-dark">
<%= format_according_to_decimals(@delegator.stake_amount, @token.decimals) %> <%= @token.symbol %> <%= format_according_to_decimals(@delegator_from.stake_amount, @token.decimals) %> <%= @token.symbol %>
</span> </span>
</p> </p>
<div class="input-group form-group"> <div class="input-group form-group">
@ -28,7 +23,7 @@
<option disabled <%= unless @pool_to do "selected" end %>><%= gettext("Choose Pool") %></option> <option disabled <%= unless @pool_to do "selected" end %>><%= gettext("Choose Pool") %></option>
<%= for %{pool: pool} <- @pools, <%= for %{pool: pool} <- @pools,
pool.staking_address_hash != @pool_from.staking_address_hash, pool.staking_address_hash != @pool_from.staking_address_hash,
Decimal.positive?(pool.self_staked_amount) or pool.staking_address_hash == @delegator.delegator_address_hash do %> Decimal.positive?(pool.self_staked_amount) or pool.staking_address_hash == @delegator_from.delegator_address_hash do %>
<option <option
value="<%= pool.staking_address_hash %>" value="<%= pool.staking_address_hash %>"
<%= if @pool_to && pool.staking_address_hash == @pool_to.staking_address_hash do "selected" end %> <%= if @pool_to && pool.staking_address_hash == @pool_to.staking_address_hash do "selected" end %>
@ -38,9 +33,16 @@
<% end %> <% end %>
</select> </select>
</div> </div>
<%= if @delegator_to && Decimal.positive?(@delegator_to.stake_amount) do %>
<p class="form-p m-b-0">Current stake:
<span class="text-dark">
<%= format_according_to_decimals(@delegator_to.stake_amount, @token.decimals) %> <%= @token.symbol %>
</span>
</p>
<% end %>
<p class="form-p">Max Amount to Move: <p class="form-p">Max Amount to Move:
<span class="text-dark"> <span class="text-dark">
<%= format_according_to_decimals(@delegator.max_withdraw_allowed, @token.decimals) %> <%= @token.symbol %> <%= format_according_to_decimals(@delegator_from.max_withdraw_allowed, @token.decimals) %> <%= @token.symbol %>
</span> </span>
</p> </p>
<div class="form-buttons"> <div class="form-buttons">

@ -9,24 +9,37 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form> <form>
<div class="input-group form-group"> <%= render BlockScoutWeb.CommonComponentsView, "_input_group.html", id: "delegator-stake", classes: "form-group", input_classes: "form-control n-b-r", attributes: "delegator-stake", type: "text", placeholder: gettext("Amount"), prepend: @token.symbol %>
<input delegator-stake type="text" class="form-control n-b-r" placeholder="<%= gettext("Amount") %>">
<div class="input-group-prepend last"> <%= if @delegator && Decimal.positive?(@delegator.stake_amount) do %>
<div class="input-group-text"><%= @token.symbol %></div> <p class="form-p m-b-0">Current stake:
</div> <span class="text-dark">
</div> <%= format_according_to_decimals(@delegator.stake_amount, @token.decimals) %> <%= @token.symbol %>
<p class="form-p m-b-0">Minimum Stake: </span>
<span class="text-dark"> </p>
<%= format_according_to_decimals(@min_delegator_stake, @token.decimals) %> <%= @token.symbol %> <% else %>
</span> <p class="form-p m-b-0">Minimum Stake:
</p> <span class="text-dark">
<%= format_according_to_decimals(@min_stake, @token.decimals) %> <%= @token.symbol %>
</span>
</p>
<% end %>
<p class="form-p">Your Balance: <p class="form-p">Your Balance:
<span class="text-dark"> <span class="text-dark">
<%= format_according_to_decimals(@balance, @token.decimals) %> <%= @token.symbol %> <%= format_according_to_decimals(@balance, @token.decimals) %> <%= @token.symbol %>
</span> </span>
</p> </p>
<div class="form-buttons"> <div class="form-buttons">
<%= render BlockScoutWeb.StakesView, "_stakes_btn_stake.html", text: gettext("Stake More"), extra_class: "full-width btn-add-full" %> <%=
label =
if @delegator && Decimal.positive?(@delegator.stake_amount) do
gettext("Stake More")
else
gettext("Place stake")
end
render BlockScoutWeb.StakesView, "_stakes_btn_stake.html", text: label, extra_class: "full-width btn-add-full"
%>
</div> </div>
</form> </form>
</div> </div>

@ -9,35 +9,45 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form> <form>
<div class="input-group form-group"> <%= render BlockScoutWeb.CommonComponentsView, "_input_group.html", id: "amount", classes: "form-group", input_classes: "form-control n-b-r", attributes: "amount", type: "text", placeholder: gettext("Amount"), prepend: @token.symbol %>
<input amount type="text" class="form-control n-b-r" placeholder="<%= gettext("Amount") %>"> <p class="form-p m-b-0">You Staked:
<div class="input-group-prepend last">
<div class="input-group-text"><%= @token.symbol %></div>
</div>
</div>
<p class="form-p">You Staked:
<span class="text-dark"> <span class="text-dark">
<%= format_according_to_decimals(@delegator.stake_amount, @token.decimals) %> <%= @token.symbol %> <%= format_according_to_decimals(@delegator.stake_amount, @token.decimals) %> <%= @token.symbol %>
</span> </span>
</p> </p>
<div class="form-buttons"> <%= if Decimal.positive?(@delegator.ordered_withdraw) do %>
<p class="form-p m-b-0">Already ordered:
<span class="text-dark">
<%= format_according_to_decimals(@delegator.ordered_withdraw, @token.decimals) %> <%= @token.symbol %>
</span>
</p>
<% end %>
<%= if Decimal.positive?(@delegator.max_withdraw_allowed) do %>
<p class="form-p m-b-0">Available now:
<span class="text-dark">
<%= format_according_to_decimals(@delegator.max_withdraw_allowed, @token.decimals) %> <%= @token.symbol %>
</span>
</p>
<%= <%=
if Decimal.positive?(@delegator.max_withdraw_allowed) do render BlockScoutWeb.StakesView,
render BlockScoutWeb.StakesView, "_stakes_btn_withdraw.html",
"_stakes_btn_withdraw.html", text: gettext("Withdraw Now"),
text: gettext("Withdraw Now"), extra_class: "full-width btn-add-full withdraw"
extra_class: "full-width btn-add-full withdraw"
end
%> %>
<% end %>
<%= if Decimal.positive?(@delegator.max_ordered_withdraw_allowed) or Decimal.positive?(@delegator.ordered_withdraw) do %>
<p class="form-p m-b-0">Available after the current epoch:
<span class="text-dark">
<%= format_according_to_decimals(@delegator.max_ordered_withdraw_allowed, @token.decimals) %> <%= @token.symbol %>
</span>
</p>
<%= <%=
if Decimal.positive?(@delegator.max_ordered_withdraw_allowed) do render BlockScoutWeb.StakesView,
render BlockScoutWeb.StakesView, "_stakes_btn_withdraw.html",
"_stakes_btn_withdraw.html", text: gettext("Order Withdrawal"),
text: gettext("Order Withdrawal"), extra_class: "full-width btn-add-full order-withdraw"
extra_class: "full-width btn-add-full order-withdraw"
end
%> %>
</div> <% end %>
</form> </form>
</div> </div>
<%= 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" %> <%= 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" %>

Loading…
Cancel
Save