Add general elements to Claim Reward dialog popup

staking
Vadim 5 years ago committed by Victor Baranov
parent 8db83a720a
commit f581780558
  1. 39
      apps/block_scout_web/assets/css/components/stakes/_modal_claim_reward.scss
  2. 1
      apps/block_scout_web/assets/css/stakes.scss
  3. 44
      apps/block_scout_web/assets/js/pages/stakes/claim_reward.js
  4. 70
      apps/block_scout_web/lib/block_scout_web/channels/stakes_channel.ex
  5. 2
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_btn_stake.html.eex
  6. 2
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_btn_withdraw.html.eex
  7. 3
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_modal_claim_reward.html.eex
  8. 35
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_modal_claim_reward_content.html.eex
  9. 57
      apps/explorer/lib/explorer/staking/contract_reader.ex

@ -0,0 +1,39 @@
.modal-claim-reward {
max-width: 400px;
width: 100%;
@include media-breakpoint-down(sm) {
margin-left: auto;
margin-right: auto;
}
}
.modal-claim-reward {
label {
margin-bottom: 0;
}
p {
margin-bottom: 0.3rem;
width: 100%;
&.m-b-0 {
margin-bottom: 0;
}
&.left {
width: 49%;
float: left;
}
&.right {
width: 49%;
float: right;
}
}
textarea {
background: #fff!important;
height: 38px;
}
}

@ -16,6 +16,7 @@
@import "components/stakes/stakes_progress"; @import "components/stakes/stakes_progress";
@import "components/stakes/modal_stake"; @import "components/stakes/modal_stake";
@import "components/stakes/modal_become_candidate"; @import "components/stakes/modal_become_candidate";
@import "components/stakes/modal_claim_reward";
@import "components/stakes/modal_validator_info"; @import "components/stakes/modal_validator_info";
@import "components/stakes/modal_delegators_info"; @import "components/stakes/modal_delegators_info";
@import "components/stakes/modal_bottom_disclaimer"; @import "components/stakes/modal_bottom_disclaimer";

@ -10,29 +10,30 @@ export function openClaimRewardModal(store) {
channel.push('render_claim_reward', { preload: true }).receive('ok', msg => { channel.push('render_claim_reward', { preload: true }).receive('ok', msg => {
const $modal = $(msg.html) const $modal = $(msg.html)
const closeButton = $modal.find('.close-modal') const $closeButton = $modal.find('.close-modal')
const modalBody = $('.modal-body', $modal) const $modalBody = $('.modal-body', $modal)
const waitingMessageContainer = modalBody.find('p') const $waitingMessageContainer = $modalBody.find('p')
let dotCounter = 0 let dotCounter = 0
const dotCounterInterval = setInterval(() => { const dotCounterInterval = setInterval(() => {
let waitingMessage = $.trim(waitingMessageContainer.text()) let waitingMessage = $.trim($waitingMessageContainer.text())
if (!waitingMessage.endsWith('.')) { if (!waitingMessage.endsWith('.')) {
waitingMessage = waitingMessage + '.' waitingMessage = waitingMessage + '.'
} }
waitingMessage = waitingMessage.replace(/\.+$/g, " " + ".".repeat(dotCounter)) waitingMessage = waitingMessage.replace(/\.+$/g, " " + ".".repeat(dotCounter))
waitingMessageContainer.text(waitingMessage) $waitingMessageContainer.text(waitingMessage)
dotCounter = (dotCounter + 1) % 4 dotCounter = (dotCounter + 1) % 4
}, 500) }, 500)
closeButton.hide() $closeButton.hide()
lockModal($modal) lockModal($modal)
channel.on('claim_reward_pools', msg_pools => { channel.on('claim_reward_pools', msg_pools => {
channel.off('claim_reward_pools') channel.off('claim_reward_pools')
closeButton.show() $closeButton.show()
unlockModal($modal) unlockModal($modal)
clearInterval(dotCounterInterval) clearInterval(dotCounterInterval)
modalBody.html(msg_pools.html) $modalBody.html(msg_pools.html)
onPoolsFound($modal, $modalBody)
}) })
$modal.on('shown.bs.modal', () => { $modal.on('shown.bs.modal', () => {
channel.push('render_claim_reward', {}).receive('error', (error) => { channel.push('render_claim_reward', {}).receive('error', (error) => {
@ -40,7 +41,7 @@ export function openClaimRewardModal(store) {
}) })
}) })
$modal.on('hidden.bs.modal', () => { $modal.on('hidden.bs.modal', () => {
$(this).remove() $modal.remove()
}) })
openModal($modal); openModal($modal);
@ -48,3 +49,28 @@ export function openClaimRewardModal(store) {
openErrorModal('Claim Reward', error.reason) openErrorModal('Claim Reward', error.reason)
}) })
} }
function onPoolsFound($modal, $modalBody) {
const $poolsDropdown = $('[pool-select]', $modalBody)
const $epochChoiceRadio = $('input[name="epoch_choice"]', $modalBody)
const $specifiedEpochsText = $('.specified-epochs', $modalBody)
$poolsDropdown.on('change', () => {
const data = $('option:selected', this).data()
const $poolInfo = $('.selected-pool-info', $modalBody)
$poolsDropdown.blur()
$('textarea', $poolInfo).val(data.epochs ? data.epochs : '')
$('#token-reward-sum', $poolInfo).html(data.tokenRewardSum ? data.tokenRewardSum : '0')
$('#native-reward-sum', $poolInfo).html(data.nativeRewardSum ? data.nativeRewardSum : '0')
$('#tx-gas-limit', $poolInfo).html(data.gasLimit ? '~' + data.gasLimit : '0')
$('#epoch-choice-all', $poolInfo).click()
$specifiedEpochsText.val('')
$poolInfo.removeClass('hidden')
$('.modal-bottom-disclaimer', $modal).removeClass('hidden')
})
$epochChoiceRadio.on('change', () => {
$specifiedEpochsText.toggleClass('hidden')
})
}

@ -375,29 +375,61 @@ defmodule BlockScoutWeb.StakesChannel do
end end
end) end)
pools = if error != nil do {error, pools} = if error != nil do
%{} {error, %{}}
else else
block_reward_contract = ContractState.get(:block_reward_contract) block_reward_contract = ContractState.get(:block_reward_contract)
Enum.map(pools_amounts, fn {_, amounts} -> amounts end) pools =
|> Enum.zip(pools) pools_amounts
|> Enum.filter(fn {amounts, _} -> amounts.token_reward_sum > 0 || amounts.native_reward_sum > 0 end) |> Enum.map(fn {_, amounts} -> amounts end)
|> Enum.map(fn {amounts, pool_staking_address} -> |> Enum.zip(pools)
responses = |> Enum.filter(fn {amounts, _} -> amounts.token_reward_sum > 0 || amounts.native_reward_sum > 0 end)
ContractReader.epochs_to_claim_reward_from_request(pool_staking_address, staker) |> Enum.map(fn {amounts, pool_staking_address} ->
|> ContractReader.perform_requests(%{block_reward: block_reward_contract.address}, block_reward_contract.abi) responses =
ContractReader.epochs_to_claim_reward_from_request(pool_staking_address, staker)
epochs = |> ContractReader.perform_requests(%{block_reward: block_reward_contract.address}, block_reward_contract.abi)
array_to_ranges(responses[:epochs])
|> Enum.map(fn {first, last} -> epochs =
Integer.to_string(first) <> (if first != last, do: "-" <> Integer.to_string(last), else: "") array_to_ranges(responses[:epochs])
end) |> Enum.map(fn {first, last} ->
data = Map.put(amounts, :epochs, Enum.join(epochs, ",")) Integer.to_string(first) <> (if first != last, do: "-" <> Integer.to_string(last), else: "")
end)
{data, pool_staking_address} data = Map.put(amounts, :epochs, Enum.join(epochs, ","))
{data, pool_staking_address}
end)
pools_gas_estimates = Enum.map(pools, fn {_data, pool_staking_address} ->
result = ContractReader.claim_reward_estimate_gas(
staking_contract_address,
[],
pool_staking_address,
staker,
json_rpc_named_arguments
)
{pool_staking_address, result}
end) end)
|> Map.new(fn {val, key} -> {key, val} end)
error = Enum.find_value(pools_gas_estimates, fn {_, result} ->
case result do
{:error, reason} -> error_reason_to_string(reason)
_ -> nil
end
end)
pools = if error == nil do
pools_gas_estimates = Map.new(pools_gas_estimates)
Map.new(pools, fn {data, pool_staking_address} ->
{:ok, estimate} = pools_gas_estimates[pool_staking_address]
data = Map.put(data, :gas_estimate, estimate)
{pool_staking_address, data}
end)
else
%{}
end
{error, pools}
end end
{error, pools} {error, pools}

@ -1,5 +1,5 @@
<button <button
class="btn-stake-more-full btn-full-primary <%= if assigns[:extra_class] do @extra_class end %>" class="btn-full-primary <%= if assigns[:extra_class] do @extra_class end %>"
<%= if assigns[:disabled] do "disabled" end %> <%= if assigns[:disabled] do "disabled" end %>
> >
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">

@ -1,5 +1,5 @@
<button <button
class="btn-stake-more-full btn-full-primary <%= if assigns[:extra_class] do @extra_class end %>" class="btn-full-primary <%= if assigns[:extra_class] do @extra_class end %>"
<%= if assigns[:disabled] do "disabled" end %> <%= if assigns[:disabled] do "disabled" end %>
> >
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">

@ -1,5 +1,5 @@
<div class="modal modal-fullwidth-xs fade" tabindex="-1" role="dialog" aria-hidden="true"> <div class="modal modal-fullwidth-xs fade" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-stake" role="document"> <div class="modal-dialog modal-dialog-centered modal-claim-reward" role="document">
<div class="modal-content"> <div class="modal-content">
<%= render BlockScoutWeb.CommonComponentsView, "_modal_close_button.html" %> <%= render BlockScoutWeb.CommonComponentsView, "_modal_close_button.html" %>
<div class="modal-header"> <div class="modal-header">
@ -8,6 +8,7 @@
<div class="modal-body"> <div class="modal-body">
<p><%= gettext("Searching for pools you have ever staked into. Please, wait...") %></p> <p><%= gettext("Searching for pools you have ever staked into. Please, wait...") %></p>
</div> </div>
<%= render BlockScoutWeb.CommonComponentsView, "_modal_bottom_disclaimer.html", text: gettext("You can get your reward for all staking epochs during which the pool was a validator or specify separate epochs if <b>Tx Gas Limit</b> is too high. <b>Tx Gas Limit</b> depends on how long the pool was a validator and how many staking epochs you held your stake in the pool without movement.") |> raw(), extra_class: "hidden" %>
</div> </div>
</div> </div>
</div> </div>

@ -1,5 +1,5 @@
<%= if map_size(@pools) > 0 do %> <%= if map_size(@pools) > 0 do %>
<p><%= gettext("We found the following pools you can claim your reward from:") %></p> <p class="form-p"><%= gettext("We found the following pools you can claim reward from:") %></p>
<form> <form>
<div class="input-group form-group"> <div class="input-group form-group">
<select pool-select class="form-control"> <select pool-select class="form-control">
@ -9,12 +9,12 @@
token_reward_sum = format_token_amount(data.token_reward_sum, @token, digits: 5, ellipsize: false, symbol: false) token_reward_sum = format_token_amount(data.token_reward_sum, @token, digits: 5, ellipsize: false, symbol: false)
native_reward_sum = format_token_amount(data.native_reward_sum, @coin, digits: 5, ellipsize: false, symbol: false) native_reward_sum = format_token_amount(data.native_reward_sum, @coin, digits: 5, ellipsize: false, symbol: false)
%> %>
<option value="<%= pool_staking_address %>" data-token-reward-sum="<%= token_reward_sum %>" data-native-reward-sum="<%= native_reward_sum %>" data-epochs="<%= data.epochs %>"> <option value="<%= pool_staking_address %>" data-token-reward-sum="<%= token_reward_sum %>" data-native-reward-sum="<%= native_reward_sum %>" data-epochs="<%= data.epochs %>" data-gas-limit="<%= data.gas_estimate %>">
<%= <%=
BlockScoutWeb.AddressView.trimmed_hash(pool_staking_address) <> BlockScoutWeb.AddressView.trimmed_hash(pool_staking_address) <>
" (" <> " (" <>
token_reward_sum <> " " <> @token.symbol <> token_reward_sum <> " " <> @token.symbol <>
", " <> "; " <>
native_reward_sum <> " " <> @coin.symbol <> native_reward_sum <> " " <> @coin.symbol <>
")" ")"
%> %>
@ -22,6 +22,35 @@
<% end %> <% end %>
</select> </select>
</div> </div>
<div class="selected-pool-info hidden">
<p class="form-p"><%= gettext("The staking epochs for which the reward could be claimed:") %></p>
<div class="input-group form-group">
<textarea class="form-control" readonly="readonly"></textarea>
</div>
<div class="input-group form-group">
<p class="form-p">
<%= gettext("Claim for") %>&nbsp;
<label><input type="radio" name="epoch_choice" value="all" id="epoch-choice-all" checked="checked" />&nbsp;<%= gettext("all epochs") %></label>&nbsp;
<label><input type="radio" name="epoch_choice" value="specified" />&nbsp;<%= gettext("specified epochs only") %></label>
</p>
<input type="text" class="form-control specified-epochs hidden" placeholder='<%= gettext("Epochs range(s) or enum, e.g.: 5-9,23-27,47,50") %>' />
</div>
<div class="clearfix form-group">
<p class="form-p left">
<%= gettext("You will receive:") %><br />
<span class="text-dark" id="token-reward-sum"></span> <span class="text-dark"><%= @token.symbol %></span><br />
<span class="text-dark" id="native-reward-sum"></span> <span class="text-dark"><%= @coin.symbol %></span><br />
<a href="#" class="recalculate hidden"><%= gettext("Recalculate") %></a>
</p>
<p class="form-p right">
<%= gettext("Tx Gas Limit:") %><br />
<span class="text-dark" id="tx-gas-limit"></span><br />
<a href="#" class="recalculate hidden"><%= gettext("Recalculate") %></a>
</p>
</div>
<%= render BlockScoutWeb.StakesView, "_stakes_btn_withdraw.html", text: gettext("Claim Reward"), extra_class: "full-width" %>
</div>
</form> </form>
<% else %> <% else %>
<p> <p>

@ -92,6 +92,63 @@ defmodule Explorer.Staking.ContractReader do
end end
end end
# makes a raw `eth_estimateGas` for the `claimReward` function of the Staking contract:
# function claimReward(
# uint256[] memory _stakingEpochs,
# address _poolStakingAddress
# ) public;
def claim_reward_estimate_gas(
staking_contract_address,
staking_epochs,
pool_staking_address,
staker,
json_rpc_named_arguments
) do
staking_epochs_joint =
staking_epochs
|> Enum.map(fn epoch ->
epoch
|> Integer.to_string(16)
|> String.pad_leading(64, ["0"])
end)
|> Enum.join("")
pool_staking_address = address_pad_to_64(pool_staking_address)
staking_epochs_length =
Enum.count(staking_epochs)
|> Integer.to_string(16)
|> String.pad_leading(64, ["0"])
data = "0x3ea15d62" # `claimReward` function signature
data = data <> String.pad_leading("40", 64, ["0"]) # offset to the `_stakingEpochs` array
data = data <> pool_staking_address # `_poolStakingAddress` parameter
data = data <> staking_epochs_length # the length of `_stakingEpochs` array
data = data <> staking_epochs_joint # encoded `_stakingEpochs` array
result = EthereumJSONRPC.request(%{
id: 0,
method: "eth_estimateGas",
params: [%{
from: staker,
to: staking_contract_address,
gasPrice: "0x3B9ACA00", # 1 gwei
data: data
}]
}) |> EthereumJSONRPC.json_rpc(json_rpc_named_arguments)
case result do
{:ok, response} ->
estimate =
response
|> String.replace_leading("0x", "")
|> String.to_integer(16)
{:ok, estimate}
{:error, reason} ->
{:error, reason}
end
end
# args = [staking_epoch, delegator_staked, validator_staked, total_staked, pool_reward \\ 10_00000] # args = [staking_epoch, delegator_staked, validator_staked, total_staked, pool_reward \\ 10_00000]
def delegator_reward_request(args) do def delegator_reward_request(args) do
[ [

Loading…
Cancel
Save