Remove Staking dapp logic from Blockscout core

pull/5700/head
Viktor Baranov 2 years ago
parent 4886204f27
commit 969c222d43
  1. 2
      .dialyzer-ignore
  2. 140
      apps/block_scout_web/assets/css/components/_modal_stake.scss
  3. 17
      apps/block_scout_web/assets/css/components/stakes/_copy_icon.scss
  4. 9
      apps/block_scout_web/assets/css/components/stakes/_modal_become_candidate.scss
  5. 33
      apps/block_scout_web/assets/css/components/stakes/_modal_bottom_disclaimer.scss
  6. 36
      apps/block_scout_web/assets/css/components/stakes/_modal_claim_reward.scss
  7. 9
      apps/block_scout_web/assets/css/components/stakes/_modal_delegators_info.scss
  8. 140
      apps/block_scout_web/assets/css/components/stakes/_modal_stake.scss
  9. 72
      apps/block_scout_web/assets/css/components/stakes/_modal_validator_info.scss
  10. 53
      apps/block_scout_web/assets/css/components/stakes/_progress_from_to.scss
  11. 14
      apps/block_scout_web/assets/css/components/stakes/_stakes-btn-close-alert.scss
  12. 209
      apps/block_scout_web/assets/css/components/stakes/_stakes.scss
  13. 21
      apps/block_scout_web/assets/css/components/stakes/_stakes_btn_remove_pool.scss
  14. 35
      apps/block_scout_web/assets/css/components/stakes/_stakes_empty_content.scss
  15. 85
      apps/block_scout_web/assets/css/components/stakes/_stakes_progress.scss
  16. 23
      apps/block_scout_web/assets/css/stakes.scss
  17. 585
      apps/block_scout_web/assets/js/pages/stakes.js
  18. 172
      apps/block_scout_web/assets/js/pages/stakes/become_candidate.js
  19. 340
      apps/block_scout_web/assets/js/pages/stakes/claim_reward.js
  20. 29
      apps/block_scout_web/assets/js/pages/stakes/claim_withdrawal.js
  21. 5
      apps/block_scout_web/assets/js/pages/stakes/constants.js
  22. 10
      apps/block_scout_web/assets/js/pages/stakes/delegators_list.js
  23. 98
      apps/block_scout_web/assets/js/pages/stakes/make_stake.js
  24. 92
      apps/block_scout_web/assets/js/pages/stakes/move_stake.js
  25. 25
      apps/block_scout_web/assets/js/pages/stakes/remove_pool.js
  26. 142
      apps/block_scout_web/assets/js/pages/stakes/utils.js
  27. 37
      apps/block_scout_web/assets/js/pages/stakes/validator_info.js
  28. 143
      apps/block_scout_web/assets/js/pages/stakes/withdraw_stake.js
  29. 3
      apps/block_scout_web/assets/package.json
  30. 3
      apps/block_scout_web/assets/static/images/icons/move-stake.svg
  31. 1
      apps/block_scout_web/assets/static/images/icons/stake.svg
  32. 2
      apps/block_scout_web/assets/webpack.config.js
  33. 7
      apps/block_scout_web/config/config.exs
  34. 3
      apps/block_scout_web/lib/block_scout_web/application.ex
  35. 5
      apps/block_scout_web/lib/block_scout_web/chain.ex
  36. 954
      apps/block_scout_web/lib/block_scout_web/channels/stakes_channel.ex
  37. 1
      apps/block_scout_web/lib/block_scout_web/channels/user_socket.ex
  38. 314
      apps/block_scout_web/lib/block_scout_web/controllers/stakes_controller.ex
  39. 26
      apps/block_scout_web/lib/block_scout_web/staking_event_handler.ex
  40. 9
      apps/block_scout_web/lib/block_scout_web/templates/layout/_topnav.html.eex
  41. 3
      apps/block_scout_web/lib/block_scout_web/templates/layout/app.html.eex
  42. 15
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_learn-more.html.eex
  43. 7
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_metatags.html.eex
  44. 77
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_rows.html.eex
  45. 1
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_rows_loading.html.eex
  46. 8
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_address.html.eex
  47. 6
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_btn_claim_reward.html.eex
  48. 28
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_btn_recalculate.html.eex
  49. 6
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_btn_remove_pool.html.eex
  50. 9
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_btn_stake.html.eex
  51. 9
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_btn_withdraw.html.eex
  52. 5
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_control_claim.html.eex
  53. 5
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_control_move.html.eex
  54. 5
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_control_stake.html.eex
  55. 5
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_control_withdraw.html.eex
  56. 15
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_empty_content.html.eex
  57. 40
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_modal_become_candidate.html.eex
  58. 14
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_modal_claim_reward.html.eex
  59. 72
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_modal_claim_reward_content.html.eex
  60. 29
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_modal_claim_withdrawal.html.eex
  61. 197
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_modal_delegators_list.html.eex
  62. 73
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_modal_move.html.eex
  63. 95
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_modal_pool_info.html.eex
  64. 60
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_modal_stake.html.eex
  65. 86
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_modal_withdraw.html.eex
  66. 4
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_pool_info_item.html.eex
  67. 37
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_progress.html.eex
  68. 4
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_stats_item.html.eex
  69. 57
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_stats_item_account.html.eex
  70. 23
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_tabs.html.eex
  71. 6
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_th.html.eex
  72. 31
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_title.html.eex
  73. 57
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_stakes_top.html.eex
  74. 60
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_table.html.eex
  75. 19
      apps/block_scout_web/lib/block_scout_web/templates/stakes/_warning.html.eex
  76. 53
      apps/block_scout_web/lib/block_scout_web/templates/stakes/index.html.eex
  77. 100
      apps/block_scout_web/lib/block_scout_web/views/stakes_helpers.ex
  78. 5
      apps/block_scout_web/lib/block_scout_web/views/stakes_view.ex
  79. 4
      apps/block_scout_web/lib/block_scout_web/web_router.ex
  80. 755
      apps/block_scout_web/priv/gettext/default.pot
  81. 755
      apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
  82. 28
      apps/block_scout_web/test/block_scout_web/channels/stakes_channel_test.exs
  83. 49
      apps/block_scout_web/test/block_scout_web/controllers/stakes_controller_test.exs
  84. 24
      apps/block_scout_web/test/block_scout_web/views/stakes_helpers_test.exs
  85. 12
      apps/explorer/config/config.exs
  86. 2
      apps/explorer/config/test.exs
  87. 1
      apps/explorer/lib/explorer/application.ex
  88. 207
      apps/explorer/lib/explorer/chain.ex
  89. 2
      apps/explorer/lib/explorer/chain/events/publisher.ex
  90. 4
      apps/explorer/lib/explorer/chain/events/subscriber.ex
  91. 180
      apps/explorer/lib/explorer/chain/import/runner/staking_pools.ex
  92. 134
      apps/explorer/lib/explorer/chain/import/runner/staking_pools_delegators.ex
  93. 2
      apps/explorer/lib/explorer/chain/import/stage/address_referencing.ex
  94. 148
      apps/explorer/lib/explorer/chain/staking_pool.ex
  95. 93
      apps/explorer/lib/explorer/chain/staking_pools_delegator.ex
  96. 585
      apps/explorer/lib/explorer/staking/contract_reader.ex
  97. 1186
      apps/explorer/lib/explorer/staking/contract_state.ex
  98. 311
      apps/explorer/lib/explorer/staking/stake_snapshotting.ex
  99. 32
      apps/explorer/test/explorer/chain/import/runner/staking_pools_delegators_test.exs
  100. 33
      apps/explorer/test/explorer/chain/import/runner/staking_pools_test.exs
  101. Some files were not shown because too many files have changed in this diff Show More

@ -25,8 +25,6 @@ lib/explorer/exchange_rates/source.ex:123
lib/explorer/smart_contract/solidity/verifier.ex:223
lib/block_scout_web/templates/address_contract/index.html.eex:158
lib/block_scout_web/templates/address_contract/index.html.eex:195
lib/explorer/staking/stake_snapshotting.ex:15: Function do_snapshotting/7 has no local return
lib/explorer/staking/stake_snapshotting.ex:147
lib/explorer/third_party_integrations/sourcify.ex:73
lib/explorer/third_party_integrations/sourcify.ex:76
lib/block_scout_web/views/transaction_view.ex:137

@ -0,0 +1,140 @@
.modal-stake {
max-width: 100%;
width: 560px;
@include media-breakpoint-down(sm) {
margin-left: auto;
margin-right: auto;
}
@include media-breakpoint-down(xs) {
width: 100%;
}
}
.modal-stake-move {
max-width: 740px;
min-width: 740px;
width: 100%;
@include media-breakpoint-down(sm) {
margin-left: auto;
margin-right: auto;
}
}
.modal-stake-three-cols {
display: grid;
grid-template-rows: [row1-start] auto [row2-start] auto [row3-start] auto [row3-end];
grid-template-columns: [col1-start] auto [col2-start] auto [col3-start] auto [col3-end];
> div {
grid-column: col2-start;
}
.modal-stake-left {
grid-column: col1-start;
grid-row: row1-start / row3-end;
}
.modal-stake-right {
grid-column: col3-start;
grid-row: row1-start / row3-end;
}
@include media-breakpoint-down(xs) {
grid-template-columns: [col1-start] auto [col1-end];
grid-template-rows: auto;
> div {
grid-column: col1-start;
}
.modal-stake-left {
width: 100%;
grid-column: col1-start;
grid-row: 3;
}
.modal-stake-right {
grid-column: col1-start;
grid-row: 4;
}
.stakes-progress {
width: auto;
padding-top: 15px;
padding-bottom: 15px;
.stakes-progress-graph {
float: left;
width: 130px;
margin-right: 15px;
}
.stakes-progress-info-title {
float: left;
margin-right: 15px;
}
.stakes-progress-info {
margin-bottom: 15px;
}
}
}
}
.modal-stake-two-cols {
display: grid;
grid-template-rows: [row1-start] auto [row2-start] auto [row3-start] auto [row3-end];
grid-template-columns: [col1-start] auto [col2-start] 190px [col2-end];
> div {
grid-column: col1-start;
}
.modal-stake-right {
grid-column: col2-start;
grid-row: row1-start / row3-end;
}
@include media-breakpoint-down(xs) {
grid-template-columns: [col1-start] auto [col1-end];
grid-template-rows: auto;
.modal-stake-right {
grid-column: col1-start;
grid-row: 3;
}
.stakes-progress {
width: auto;
padding-top: 15px;
padding-bottom: 15px;
.stakes-progress-graph {
float: left;
width: 130px;
margin-right: 15px;
}
.stakes-progress-info-title {
float: left;
margin-right: 15px;
}
.stakes-progress-info {
margin-bottom: 15px;
}
}
}
}
.modal-stake-left {
flex-shrink: 0;
height: 100%;
padding: $modal-vertical-padding $modal-horizontal-padding;
width: 190px;
height: 100%;
border-right: 1px solid $base-border-color;
}

@ -1,17 +0,0 @@
$copy-icon-color: $primary !default;
.copy-icon {
cursor: pointer;
height: 18px;
width: 18px;
svg {
display: block;
height: 100%;
width: 100%;
}
path {
fill: $copy-icon-color;
}
}

@ -1,9 +0,0 @@
.modal-become-candidate {
max-width: 400px;
width: 100%;
@include media-breakpoint-down(sm) {
margin-left: auto;
margin-right: auto;
}
}

@ -1,33 +0,0 @@
.modal-bottom-disclaimer {
background-color: $modal-gray-background;
border-bottom-left-radius: $modal-border-radius;
border-bottom-right-radius: $modal-border-radius;
color: #a3a9b5;
display: flex;
font-size: 12px;
font-weight: normal;
line-height: 1.67;
padding: #{$modal-vertical-padding} #{$modal-horizontal-padding};
text-align: left;
&.b-b-r-0 {
border-bottom-right-radius: 0;
}
&.b-b-l-0 {
border-bottom-left-radius: 0;
}
.modal-bottom-disclaimer-graphic {
flex-shrink: 0;
padding-right: 15px;
svg {
fill: #333;
}
}
.modal-bottom-disclaimer-text {
flex-grow: 1;
}
}

@ -1,36 +0,0 @@
.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;
}
}
textarea {
background: #fff!important;
height: 38px;
}
.amounts {
display: grid;
grid-template-rows: 1fr;
grid-template-columns: 1fr 80px;
grid-gap: 2vw;
}
}

@ -1,9 +0,0 @@
.modal-delegators-info {
max-width: map-get($container-max-widths, "lg");
min-width: map-get($container-max-widths, "lg");
width: 100%;
.modal-body {
padding: 0 0 25px;
}
}

@ -1,140 +0,0 @@
.modal-stake {
max-width: 100%;
width: 560px;
@include media-breakpoint-down(sm) {
margin-left: auto;
margin-right: auto;
}
@include media-breakpoint-down(xs) {
width: 100%;
}
}
.modal-stake-move {
max-width: 740px;
min-width: 740px;
width: 100%;
@include media-breakpoint-down(sm) {
margin-left: auto;
margin-right: auto;
}
}
.modal-stake-three-cols {
display: grid;
grid-template-rows: [row1-start] auto [row2-start] auto [row3-start] auto [row3-end];
grid-template-columns: [col1-start] auto [col2-start] auto [col3-start] auto [col3-end];
> div {
grid-column: col2-start;
}
.modal-stake-left {
grid-column: col1-start;
grid-row: row1-start / row3-end;
}
.modal-stake-right {
grid-column: col3-start;
grid-row: row1-start / row3-end;
}
@include media-breakpoint-down(xs) {
grid-template-columns: [col1-start] auto [col1-end];
grid-template-rows: auto;
> div {
grid-column: col1-start;
}
.modal-stake-left {
width: 100%;
grid-column: col1-start;
grid-row: 3;
}
.modal-stake-right {
grid-column: col1-start;
grid-row: 4;
}
.stakes-progress {
width: auto;
padding-top: 15px;
padding-bottom: 15px;
.stakes-progress-graph {
float: left;
width: 130px;
margin-right: 15px;
}
.stakes-progress-info-title {
float: left;
margin-right: 15px;
}
.stakes-progress-info {
margin-bottom: 15px;
}
}
}
}
.modal-stake-two-cols {
display: grid;
grid-template-rows: [row1-start] auto [row2-start] auto [row3-start] auto [row3-end];
grid-template-columns: [col1-start] auto [col2-start] 190px [col2-end];
> div {
grid-column: col1-start;
}
.modal-stake-right {
grid-column: col2-start;
grid-row: row1-start / row3-end;
}
@include media-breakpoint-down(xs) {
grid-template-columns: [col1-start] auto [col1-end];
grid-template-rows: auto;
.modal-stake-right {
grid-column: col1-start;
grid-row: 3;
}
.stakes-progress {
width: auto;
padding-top: 15px;
padding-bottom: 15px;
.stakes-progress-graph {
float: left;
width: 130px;
margin-right: 15px;
}
.stakes-progress-info-title {
float: left;
margin-right: 15px;
}
.stakes-progress-info {
margin-bottom: 15px;
}
}
}
}
.modal-stake-left {
flex-shrink: 0;
height: 100%;
padding: $modal-vertical-padding $modal-horizontal-padding;
width: 190px;
height: 100%;
border-right: 1px solid $base-border-color;
}

@ -1,72 +0,0 @@
.modal-validator-info {
max-width: 660px;
width: 100%;
@include media-breakpoint-down(sm) {
margin-left: auto;
margin-right: auto;
}
.modal-title {
margin-bottom: 10px;
}
#pool_name {
width: 100%;
margin-bottom: 10px;
}
#pool_description {
width: 100%;
}
}
.modal-validator-info-content {
background-color: $modal-gray-background;
border-bottom-left-radius: $modal-border-radius;
border-bottom-right-radius: $modal-border-radius;
color: #a3a9b5;
column-gap: 60px;
display: grid;
font-size: 12px;
font-weight: normal;
grid-template-columns: 1fr 1fr 1fr;
line-height: 1.67;
padding: #{$modal-vertical-padding} #{$modal-horizontal-padding};
row-gap: 30px;
@include media-breakpoint-down(sm) {
grid-template-columns: 1fr 1fr;
}
}
.modal-validator-info-item {
&-title {
color: #a3a9b5;
font-size: 12px;
font-weight: normal;
line-height: 1.2;
margin-bottom: 15px;
padding: 0;
text-align: left;
}
&-value {
color: #333;
font-size: 14px;
font-weight: normal;
line-height: 1.2;
margin: 0;
padding: 0;
}
}
.modal-validator-alert {
padding: 10px 30px;
background-color: #fff3f7;
color: #ff7986;
border-top: 1px solid #fee6ef;
border-bottom: 1px solid #fee6ef;
line-height: 1.5;
font-size: 14px;
}

@ -1,53 +0,0 @@
$progress-from-to-background: #f5f6fa !default;
$progress-from-to-progress-background: $primary !default;
.progress-from-to {
min-width: 120px;
max-width: 100%;
.stakes-table & {
width: 120px;
}
}
.progress-from-to-values {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
}
.progress-from-to-value {
color: #333;
font-size: 12px;
font-weight: normal;
line-height: 1.2;
.stakes-tr-banned & {
color: $stakes-banned-color;
}
}
.progress-from-to-background {
background-color: $progress-from-to-background;
border-radius: 2px;
height: 4px;
overflow: hidden;
position: relative;
.stakes-tr-banned & {
background-color: darken($stakes-banned-background, 5%);
}
}
.progress-from-to-progress {
background-color: $progress-from-to-progress-background;
height: 100%;
left: 0;
max-width: 100%;
position: absolute;
top: 0;
.stakes-tr-banned & {
background-color: $stakes-banned-color;
}
}

@ -1,14 +0,0 @@
$stakes-btn-close-alert-color: $primary !default;
.stakes-btn-close-alert {
position: absolute;
left: 0.5em;
top: 1em;
border-style: none;
background: transparent;
outline: none !important;
path {
fill: $stakes-btn-close-alert-color;
}
}

@ -1,209 +0,0 @@
$stakes-dashboard-copy-icon-color: $copy-icon-color !default;
$stakes-address-color: $primary !default;
$stakes-control-color: $primary !default;
$stakes-stats-item-color: #fff !default;
$stakes-stats-item-border-color: #fff !default;
.stakes-top {
@include gradient-container();
margin-bottom: 3rem;
padding: 50px 0;
}
.stakes-top-stats {
display: flex;
justify-content: space-between;
align-items: center;
@include stats-item($stakes-stats-item-border-color, $stakes-stats-item-color);
@include media-breakpoint-down(md) {
column-gap: 30px;
display: grid;
grid-template-columns: 1fr 1fr;
row-gap: 30px;
}
.stakes-top-stats-item {
@include media-breakpoint-down(md) {
&:nth-child(1),
&:nth-child(2),
&:nth-child(3) {
grid-column-start: 1;
}
&:nth-child(4) {
grid-column-start: 2;
grid-row-start: 1;
}
}
@include media-breakpoint-down(sm) {
grid-column-start: auto !important;
grid-row-start: auto !important;
}
}
@include media-breakpoint-down(sm) {
grid-template-columns: 1fr 1fr;
}
.copy-icon {
min-width: 18px;
margin-left: 20px;
path {
fill: $stakes-dashboard-copy-icon-color;
}
}
}
.stakes-top-stats-value {
align-items: center;
display: flex;
div:nth-child(1) {
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
}
.stakes-top-stats-label {
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
.stakes-top-stats-login {
&, &:hover {
color: $secondary;
text-decoration: none;
cursor: pointer;
margin-right: 8px;
}
}
.stakes-address-container {
display: flex;
justify-content: flex-start;
.stakes-address {
margin-right: 10px;
.stakes-tr-banned & {
color: $stakes-banned-color;
}
a {
text-decoration: none;
color: $stakes-address-color;
}
}
.stakes-address-active {
color: $stakes-address-color;
cursor: pointer;
}
.check-tooltip {
position: relative;
top: -1px;
}
}
.stakes-controls {
align-items: center;
display: flex;
justify-content: flex-end;
padding-right: 30px;
float: left;
}
.stakes-control {
cursor: pointer;
display: flex;
justify-content: flex-start;
color: $stakes-control-color;
font-size: 14px;
font-weight: normal;
line-height: 1.2;
margin-right: 25px;
text-align: left;
&:last-child {
margin-right: 0;
}
}
.stakes-control-icon {
path {
fill: $stakes-control-color;
}
}
.stakes-top-stats-item {
min-width: 180px;
&.stakes-top-stats-item-address {
min-width: 260px;
width: 100%;
}
@include media-breakpoint-down(sm) {
min-width: auto;
&.stakes-top-stats-item-address {
grid-column-end: span 2 !important;
}
}
}
.stakes-top-buttons {
min-width: 180px;
align-items: center;
display: flex;
justify-content: center;
flex-direction: column;
.btn-add-full {
margin-bottom: 10px;
}
.btn-external-link:hover {
color: #fff;
}
&.right {
@include media-breakpoint-up(sm) {
margin-left: 10px;
}
}
@include media-breakpoint-down(md) {
grid-column-start: 2;
grid-row-start: 2;
justify-self: left;
}
@include media-breakpoint-down(sm) {
min-width: auto;
grid-column-end: span 2;
grid-column-start: auto !important;
grid-row-start: auto !important;
justify-self: center;
}
}
.staking-pg-container {
padding: 0 30px 30px;
&.at-bottom {
padding-top: 30px;
}
}
.stake-stats-container {
@include media-breakpoint-up(lg) {
margin-bottom: -30px;
}
margin-top: 10px;
font-size: 12px;
}

@ -1,21 +0,0 @@
$stakes-btn-remove-pool-color: $primary !default;
.stakes-btn-remove-pool {
align-items: center;
color: $stakes-btn-remove-pool-color;
cursor: pointer;
display: flex;
font-size: 12px;
font-weight: 600;
height: 36px;
justify-content: center;
text-align: center;
svg {
margin-right: 10px;
}
path {
fill: $stakes-btn-remove-pool-color;
}
}

@ -1,35 +0,0 @@
.stakes-empty-content {
display: flex;
justify-content: center;
padding: 100px 15px 20px;
}
.stakes-empty-content-pic {
flex-shrink: 0;
margin: 0 50px 0 0;
}
.stakes-empty-content-pic-svg-path {
fill: $primary;
}
.stakes-empty-content-info {
max-width: 400px;
}
.stakes-empty-content-title {
font-size: 18px;
font-weight: normal;
line-height: 1.2;
margin: 0 0 15px;
text-align: left;
}
.stakes-empty-content-text {
color: #a3a9b5;
font-size: 14px;
font-weight: normal;
line-height: 1.71;
margin: 0 0 25px;
text-align: left;
}

@ -1,85 +0,0 @@
$stakes-progress-graph-color: $primary !default;
.stakes-progress {
.modal-stake-right & {
border-left: 1px solid $base-border-color;
flex-shrink: 0;
height: 100%;
padding: $modal-vertical-padding $modal-horizontal-padding;
width: 190px;
}
}
.stakes-progress-info {
margin-bottom: 25px;
&:last-child {
margin-bottom: 0;
}
}
.stakes-progress-info-title {
color: #a3a9b5;
font-size: 12px;
font-weight: normal;
line-height: 1.2;
margin: 0 0 12px;
text-align: left;
}
.stakes-progress-info-value {
color: #333;
font-size: 14px;
font-weight: normal;
line-height: 1.2;
margin: 0;
text-align: left;
&.link-color {
color: $primary;
}
}
.stakes-progress-info-link {
color: $primary;
cursor: pointer;
}
.stakes-progress-graph {
margin: 0 0 20px;
position: relative;
}
.stakes-progress-graph-canvas {
position: relative;
z-index: 1;
}
.stakes-progress-graph-thing-for-getting-color {
color: $stakes-progress-graph-color;
}
.stakes-progress-data {
left: 50%;
position: absolute;
top: 50%;
transform: translateX(-50%) translateY(-50%);
z-index: 12;
}
.stakes-progress-data-total {
color: #a3a9b5;
font-size: 12px;
font-weight: normal;
line-height: 1.2;
text-align: center;
}
.stakes-progress-data-progress {
color: #333;
font-size: 24px;
font-weight: bold;
line-height: 1.2;
margin: 0 0 8px;
text-align: center;
}

@ -1,23 +0,0 @@
@import "./mixins";
// Bootstrap Core CSS
@import "node_modules/bootstrap/scss/functions";
@import "node_modules/bootstrap/scss/mixins";
@import "theme/variables";
@import "components/stakes_variables";
@import "components/stakes/copy_icon";
@import "components/stakes/stakes";
@import "components/stakes/progress_from_to";
@import "components/stakes/stakes_empty_content";
@import "components/stakes/stakes_btn_remove_pool";
@import "components/modal_variables";
@import "components/stakes/stakes_progress";
@import "components/stakes/modal_stake";
@import "components/stakes/modal_become_candidate";
@import "components/stakes/modal_claim_reward";
@import "components/stakes/modal_validator_info";
@import "components/stakes/modal_delegators_info";
@import "components/stakes/modal_bottom_disclaimer";
@import "components/stakes/stakes-btn-close-alert";

@ -1,585 +0,0 @@
import $ from 'jquery'
import _ from 'lodash'
import { subscribeChannel } from '../socket'
import { connectElements } from '../lib/redux_helpers.js'
import { createAsyncLoadStore, refreshPage } from '../lib/async_listing_load'
import { showHideDisconnectButton } from '../lib/smart_contract/common_helpers'
import { connectToProvider, disconnect, fetchAccountData, web3ModalInit } from '../lib/smart_contract/connect'
import Queue from '../lib/queue'
import Web3 from 'web3'
import { openPoolInfoModal } from './stakes/validator_info'
import { openDelegatorsListModal } from './stakes/delegators_list'
import { openBecomeCandidateModal, becomeCandidateConnectionLost } from './stakes/become_candidate'
import { openRemovePoolModal } from './stakes/remove_pool'
import { openMakeStakeModal } from './stakes/make_stake'
import { openMoveStakeModal } from './stakes/move_stake'
import { openWithdrawStakeModal } from './stakes/withdraw_stake'
import { openClaimRewardModal, claimRewardConnectionLost } from './stakes/claim_reward'
import { openClaimWithdrawalModal } from './stakes/claim_withdrawal'
import { checkForTokenDefinition, isSupportedNetwork } from './stakes/utils'
import { currentModal, openWarningModal, openErrorModal } from '../lib/modals'
import constants from './stakes/constants'
const stakesPageSelector = '[data-page="stakes"]'
let provider = null
if (localStorage.getItem('stakes-alert-read') === 'true') {
$('.js-stakes-welcome-alert').hide()
} else {
$('.js-stakes-welcome-alert').show()
}
if (localStorage.getItem('stakes-warning-read') === 'true') {
$('.js-stakes-warning-alert').hide()
} else {
$('.js-stakes-warning-alert').show()
}
export const initialState = {
account: null,
blockRewardContract: null,
channel: null,
currentBlockNumber: 0, // current block number
finishRequestResolve: null,
lastEpochNumber: 0,
loading: true,
network: null,
refreshBlockNumber: 0, // last page refresh block number
refreshInterval: null,
refreshPageFunc: refreshPageWrapper,
stakingAllowed: false,
stakingTokenDefined: false,
stakingContract: null,
tokenContract: null,
tokenDecimals: 0,
tokenSymbol: '',
validatorSetApplyBlock: 0,
validatorSetContract: null,
web3: null,
stakingErrorShown: false
}
// 100 - id of xDai network, 101 - id of xDai test network
export const allowedNetworkIds = [100, 101]
export function reducer (state = initialState, action) {
switch (action.type) {
case 'PAGE_LOAD':
case 'ELEMENTS_LOAD': {
return Object.assign({}, state, _.omit(action, 'type'))
}
case 'CHANNEL_CONNECTED': {
return Object.assign({}, state, { channel: action.channel })
}
case 'WEB3_DETECTED': {
return Object.assign({}, state, { web3: action.web3 })
}
case 'ACCOUNT_UPDATED': {
return Object.assign({}, state, {
account: action.account,
additionalParams: Object.assign({}, state.additionalParams, {
account: action.account
})
})
}
case 'BLOCK_CREATED': {
return Object.assign({}, state, {
currentBlockNumber: action.currentBlockNumber
})
}
case 'NETWORK_UPDATED': {
return Object.assign({}, state, {
network: action.network,
additionalParams: Object.assign({}, state.additionalParams, {
network: action.network
})
})
}
case 'FILTERS_UPDATED': {
return Object.assign({}, state, {
additionalParams: Object.assign({}, state.additionalParams, {
filterBanned: 'filterBanned' in action ? action.filterBanned : state.additionalParams.filterBanned,
filterMy: 'filterMy' in action ? action.filterMy : state.additionalParams.filterMy
})
})
}
case 'PAGE_REFRESHED': {
return Object.assign({}, state, {
refreshBlockNumber: action.refreshBlockNumber,
finishRequestResolve: action.finishRequestResolve
})
}
case 'RECEIVED_UPDATE': {
return Object.assign({}, state, {
lastEpochNumber: action.lastEpochNumber,
stakingAllowed: action.stakingAllowed,
stakingTokenDefined: action.stakingTokenDefined,
validatorSetApplyBlock: action.validatorSetApplyBlock
})
}
case 'RECEIVED_CONTRACTS': {
return Object.assign({}, state, {
stakingContract: action.stakingContract,
blockRewardContract: action.blockRewardContract,
validatorSetContract: action.validatorSetContract,
tokenContract: action.tokenContract,
tokenDecimals: action.tokenDecimals,
tokenSymbol: action.tokenSymbol
})
}
case 'FINISH_REQUEST': {
$(stakesPageSelector).fadeTo(0, 1)
if (state.finishRequestResolve) {
state.finishRequestResolve()
return Object.assign({}, state, {
finishRequestResolve: null
})
}
return state
}
case 'UNHEALTHY_APP_ERROR_SHOWN': {
return Object.assign({}, state, {
stakingErrorShown: true
})
}
default:
return state
}
}
const elements = {
'[data-page="stakes"]': {
load ($el) {
return {
refreshInterval: $el.data('refresh-interval') || null,
additionalParams: {
filterBanned: $el.find('[pool-filter-banned]').prop('checked'),
filterMy: $el.find('[pool-filter-my]').prop('checked')
}
}
}
}
}
const $stakesPage = $(stakesPageSelector)
const $stakesTop = $('[data-selector="stakes-top"]')
const $refreshInformer = $('.refresh-informer', $stakesPage)
const observer = new MutationObserver(function (mutationsList) {
mutationsList.forEach(function (mutation) {
mutation.addedNodes.forEach(function (addedNode) {
if (addedNode.className === 'stakes-top') {
showHideDisconnectButton()
}
})
})
})
if ($stakesPage.length) {
const store = createAsyncLoadStore(reducer, initialState, 'dataset.identifierPool')
connectElements({ store, elements })
const channel = subscribeChannel('stakes:staking_update')
store.dispatch({ type: 'CHANNEL_CONNECTED', channel })
let updating = false
async function onStakingUpdate (msg) { // eslint-disable-line no-inner-declarations
const state = store.getState()
if (state.finishRequestResolve || updating) {
return
}
updating = true
store.dispatch({ type: 'BLOCK_CREATED', currentBlockNumber: msg.block_number })
// hide tooltip on tooltip triggering element reloading
// due to issues with bootstrap tooltips https://github.com/twbs/bootstrap/issues/13133
const stakesTopTooltipID = $('[aria-describedby]', $stakesTop).attr('aria-describedby')
$('#' + stakesTopTooltipID).hide()
$stakesTop.html(msg.top_html)
if (accountChanged(msg.account, state)) {
store.dispatch({ type: 'ACCOUNT_UPDATED', account: msg.account })
resetFilterMy(store)
}
if (
msg.staking_allowed !== state.stakingAllowed ||
msg.epoch_number > state.lastEpochNumber ||
msg.validator_set_apply_block !== state.validatorSetApplyBlock ||
(state.refreshInterval && msg.block_number >= state.refreshBlockNumber + state.refreshInterval) ||
accountChanged(msg.account, state) ||
msg.by_set_account
) {
await reloadPoolList(msg, store)
}
const refreshBlockNumber = store.getState().refreshBlockNumber
const refreshGap = msg.block_number - refreshBlockNumber
$refreshInformer.find('span').html(refreshGap)
if (refreshGap > 0 && refreshBlockNumber > 0) {
$refreshInformer.show()
} else {
$refreshInformer.hide()
}
const $refreshInformerLink = $refreshInformer.find('a')
$refreshInformerLink.off('click')
$refreshInformerLink.on('click', async (event) => {
event.preventDefault()
if (!store.getState().finishRequestResolve) {
$refreshInformer.hide()
$stakesPage.fadeTo(0, 0.5)
await reloadPoolList(msg, store)
}
})
if (msg.epoch_end_block === 0 && !state.stakingErrorShown) {
openErrorModal('Staking DApp is currently unavailable', 'Not all functions are active at the moment. Please try again later.')
store.dispatch({ type: 'UNHEALTHY_APP_ERROR_SHOWN' })
}
updating = false
}
const messagesQueue = new Queue()
setTimeout(async () => {
while (true) {
const msg = messagesQueue.dequeue()
if (msg) {
// Synchronously handle the message
await onStakingUpdate(msg)
} else {
// Wait for the next message
await new Promise(resolve => setTimeout(resolve, 10))
}
}
}, 0)
channel.on('staking_update', msg => {
messagesQueue.enqueue(msg)
})
channel.on('contracts', msg => {
const web3 = store.getState().web3
const stakingContract =
new web3.eth.Contract(msg.staking_contract.abi, msg.staking_contract.address)
const blockRewardContract =
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)
const tokenContract =
new web3.eth.Contract(msg.token_contract.abi, msg.token_contract.address)
store.dispatch({
type: 'RECEIVED_CONTRACTS',
stakingContract,
blockRewardContract,
validatorSetContract,
tokenContract,
tokenDecimals: parseInt(msg.token_decimals, 10),
tokenSymbol: msg.token_symbol
})
})
channel.onError(becomeCandidateConnectionLost)
channel.onError(claimRewardConnectionLost)
$(document.body)
.on('click', '.js-pool-info', event => {
if (checkForTokenDefinition(store)) {
openPoolInfoModal(event, store)
}
})
.on('click', '.js-delegators-list', event => {
openDelegatorsListModal(event, store)
})
.on('click', '.js-become-candidate', event => {
if (checkForTokenDefinition(store)) {
openBecomeCandidateModal(event, store)
}
})
.on('click', '.js-remove-pool', () => {
openRemovePoolModal(store)
})
.on('click', '.js-make-stake', event => {
if (checkForTokenDefinition(store)) {
openMakeStakeModal(event, store)
}
})
.on('click', '.js-move-stake', event => {
if (checkForTokenDefinition(store)) {
openMoveStakeModal(event, store)
}
})
.on('click', '.js-withdraw-stake', event => {
if (checkForTokenDefinition(store)) {
openWithdrawStakeModal(event, store)
}
})
.on('click', '.js-claim-reward', event => {
if (checkForTokenDefinition(store)) {
openClaimRewardModal(event, store)
}
})
.on('click', '.js-claim-withdrawal', event => {
if (checkForTokenDefinition(store)) {
openClaimWithdrawalModal(event, store)
}
})
.on('click', '.js-stakes-btn-close-welcome-alert', event => {
$(event.target).closest('section.container').hide()
localStorage.setItem('stakes-alert-read', 'true')
})
.on('click', '.js-stakes-btn-close-warning', event => {
$(event.target).closest('section.container').hide()
localStorage.setItem('stakes-warning-read', 'true')
})
$stakesPage
.on('change', '[pool-filter-banned]', () => updateFilters(store, 'banned'))
.on('change', '[pool-filter-my]', () => updateFilters(store, 'my'))
web3ModalInit(connectToWallet, store)
$stakesTop.on('click', '[data-selector="login-button"]', async (_event) => {
login(store)
})
$stakesTop.on('click', '[disconnect-wallet]', async (_event) => {
disconnectWalletFromStakingDapp(store)
})
observer.observe(document.querySelector('[data-selector="stakes-top"]'), { subtree: false, childList: true })
}
function accountChanged (account, state) {
return account !== state.account
}
async function getAccounts () {
let accounts = []
try {
accounts = await window.ethereum.request({ method: 'eth_accounts' })
} catch (e) {
console.error(`eth_accounts request failed. ${constants.METAMASK_VERSION_WARNING}`)
openErrorModal('Get account', `Cannot get your account address. ${constants.METAMASK_VERSION_WARNING}`)
}
return accounts
}
async function getNetId (web3) {
if (window.web3 && window.web3.currentProvider && window.web3.currentProvider.wc) {
return window.web3.currentProvider.chainId
} else {
let netId = window.ethereum.chainId
if (!netId) {
netId = await window.ethereum.request({ method: 'eth_chainId' })
}
if (!netId) {
const msg = `Cannot get chainId. ${constants.METAMASK_VERSION_WARNING}`
console.error(msg)
} else {
netId = web3.utils.isHex(netId) ? web3.utils.hexToNumber(netId) : netId
}
return netId
}
}
function hideCurrentModal () {
const $modal = currentModal()
if ($modal) $modal.modal('hide')
}
async function disconnectWalletFromStakingDapp (store) {
await disconnect()
provider = null
if (accountChanged(null, store.getState())) {
await setAccount(null, store)
}
}
async function connectToWallet (store) {
provider = await connectToProvider()
provider.on('chainChanged', async (chainId) => {
const newNetId = web3.utils.isHex(chainId) ? web3.utils.hexToNumber(chainId) : chainId
setNetwork(newNetId, store, true)
})
provider.on('accountsChanged', async (accs) => {
const newAccount = accs && accs.length > 0 ? accs[0].toLowerCase() : null
if (!newAccount) {
await disconnectWalletFromStakingDapp(store)
}
if (accountChanged(newAccount, store.getState())) {
await setAccount(newAccount, store)
}
})
provider.on('disconnect', async () => {
await disconnectWalletFromStakingDapp(store)
})
const web3 = new Web3(provider)
if (provider.autoRefreshOnNetworkChange) {
provider.autoRefreshOnNetworkChange = false
}
store.dispatch({ type: 'WEB3_DETECTED', web3 })
initNetworkAndAccount(store, web3)
await fetchAccountData(setAccount, [store])
}
async function initNetworkAndAccount (store, web3) {
const state = store.getState()
const networkId = await getNetId(web3)
if (!state.network || (networkId !== state.network.id)) {
setNetwork(networkId, store, false)
}
const accounts = await getAccounts()
const account = accounts[0] ? accounts[0].toLowerCase() : null
if (accountChanged(account, state)) {
await setAccount(account, store)
// We don't call `refreshPageWrapper` in this case because it will be called
// by the `onStakingUpdate` function
} else {
await refreshPageWrapper(store)
}
}
async function login (store) {
event.stopPropagation()
event.preventDefault()
connectToWallet(store)
}
async function refreshPageWrapper (store) {
while (store.getState().finishRequestResolve) {
// Don't let anything simultaneously refresh the page
await new Promise(resolve => setTimeout(resolve, 10))
}
let currentBlockNumber = store.getState().currentBlockNumber
if (!currentBlockNumber) {
currentBlockNumber = $('[data-block-number]', $stakesTop).data('blockNumber')
}
await new Promise(resolve => {
store.dispatch({
type: 'PAGE_REFRESHED',
refreshBlockNumber: currentBlockNumber,
finishRequestResolve: resolve
})
$refreshInformer.hide()
refreshPage(store)
})
}
async function reloadPoolList (msg, store) {
store.dispatch({
type: 'RECEIVED_UPDATE',
lastEpochNumber: msg.epoch_number,
stakingAllowed: msg.staking_allowed,
stakingTokenDefined: msg.staking_token_defined,
validatorSetApplyBlock: msg.validator_set_apply_block
})
await refreshPageWrapper(store)
}
function resetFilterMy (store) {
$stakesPage.find('[pool-filter-my]').prop('checked', false)
store.dispatch({ type: 'FILTERS_UPDATED', filterMy: false })
}
function setAccount (account, store) {
return new Promise(resolve => {
store.dispatch({ type: 'ACCOUNT_UPDATED', account })
if (!account) {
resetFilterMy(store)
resolve(true)
}
const errorMsg = 'Cannot properly set account due to connection loss. Please, reload the page.'
const $addressField = $('.stakes-top-stats-item-address .stakes-top-stats-value')
$addressField.html('Loading...')
store.getState().channel.push(
'set_account', account
).receive('ok', () => {
if (account) {
$addressField.html(`
<div data-placement="bottom" data-toggle="tooltip" title="${account}">
${account}
</div>
`)
}
hideCurrentModal()
resolve(true)
}).receive('error', () => {
openErrorModal('Change account', errorMsg, true)
resolve(false)
}).receive('timeout', () => {
openErrorModal('Change account', errorMsg, true)
resolve(false)
})
})
}
function setNetwork (networkId, store, checkSupportedNetwork) {
hideCurrentModal()
const network = {
id: networkId,
authorized: false
}
if (allowedNetworkIds.includes(networkId)) {
network.authorized = true
}
store.dispatch({ type: 'NETWORK_UPDATED', network })
if (checkSupportedNetwork) {
isSupportedNetwork(store)
}
}
function updateFilters (store, filterType) {
const filterBanned = $stakesPage.find('[pool-filter-banned]')
const filterMy = $stakesPage.find('[pool-filter-my]')
const state = store.getState()
if (state.finishRequestResolve) {
if (filterType === 'my') {
filterMy.prop('checked', !filterMy.prop('checked'))
} else {
filterBanned.prop('checked', !filterBanned.prop('checked'))
}
openWarningModal('Still loading', 'The previous request to load pool list is not yet finished. Please, wait...')
return
}
if (filterType === 'my' && !state.account) {
filterMy.prop('checked', false)
openWarningModal('Unauthorized', constants.METAMASK_PLEASE_LOGIN)
return
}
store.dispatch({
type: 'FILTERS_UPDATED',
filterBanned: filterBanned.prop('checked'),
filterMy: filterMy.prop('checked')
})
refreshPageWrapper(store)
}

@ -1,172 +0,0 @@
import $ from 'jquery'
import { BigNumber } from 'bignumber.js'
import { openModal, openErrorModal, openWarningModal, lockModal } from '../../lib/modals'
import { setupValidation, displayInputError } from '../../lib/validation'
import { makeContractCall, isSupportedNetwork, isStakingAllowed } from './utils'
import constants from './constants'
let status = 'modalClosed'
export async function openBecomeCandidateModal (event, store) {
const state = store.getState()
if (!state.account) {
openWarningModal('Unauthorized', constants.METAMASK_ACCOUNTS_EMPTY)
return
}
if (!isSupportedNetwork(store)) return
if (!isStakingAllowed(state)) return
$(event.currentTarget).prop('disabled', true)
state.channel
.push('render_become_candidate')
.receive('ok', msg => {
$(event.currentTarget).prop('disabled', false)
const $modal = $(msg.html)
const $form = $modal.find('form')
setupValidation(
$form,
{
'candidate-stake': value => isCandidateStakeValid(value, store, msg),
'mining-address': value => isMiningAddressValid(value, store),
'pool-name': value => isPoolNameValid(value, store),
'pool-description': value => isPoolDescriptionValid(value, store)
},
$modal.find('form button')
)
$modal.find('[data-available-amount]').click(e => {
const amount = $(e.currentTarget).data('available-amount')
$('[candidate-stake]', $form).val(amount).trigger('input')
$('.tooltip').tooltip('hide')
return false
})
$form.submit(() => {
becomeCandidate($modal, store, msg)
return false
})
$modal.on('shown.bs.modal', () => {
status = 'modalOpened'
})
$modal.on('hidden.bs.modal', () => {
status = 'modalClosed'
$modal.remove()
})
openModal($modal)
})
.receive('timeout', () => {
$(event.currentTarget).prop('disabled', false)
openErrorModal('Become a Candidate', 'Connection timeout')
})
}
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)
}
}
async function becomeCandidate ($modal, store, msg) {
const state = store.getState()
const web3 = state.web3
const stakingContract = state.stakingContract
const tokenContract = state.tokenContract
const decimals = state.tokenDecimals
const stake = new BigNumber($modal.find('[candidate-stake]').val().replace(',', '.').trim()).shiftedBy(decimals).integerValue()
const $miningAddressInput = $modal.find('[mining-address]')
const $poolNameInput = $modal.find('[pool-name]')
const $poolDescriptionInput = $modal.find('[pool-description]')
const miningAddress = $miningAddressInput.val().trim().toLowerCase()
const poolName = $poolNameInput.val().trim()
const poolDescription = $poolDescriptionInput.val().trim()
try {
if (!isSupportedNetwork(store)) return false
if (!isStakingAllowed(state)) return false
const validatorSetContract = state.validatorSetContract
const hasEverBeenMiningAddress = await validatorSetContract.methods.hasEverBeenMiningAddress(miningAddress).call()
if (hasEverBeenMiningAddress !== '0') {
displayInputError($miningAddressInput, 'This mining address has already been used for another pool. Please use another mining address.')
$modal.find('form button').blur()
return false
}
lockModal($modal)
const poolNameHex = web3.utils.stripHexPrefix(web3.utils.utf8ToHex(poolName))
const poolNameLength = web3.utils.stripHexPrefix(web3.utils.padLeft(web3.utils.numberToHex(poolNameHex.length / 2), 2, '0'))
const poolDescriptionHex = web3.utils.stripHexPrefix(web3.utils.utf8ToHex(poolDescription))
const poolDescriptionLength = web3.utils.stripHexPrefix(web3.utils.padLeft(web3.utils.numberToHex(poolDescriptionHex.length / 2), 4, '0'))
makeContractCall(tokenContract.methods.transferAndCall(stakingContract.options.address, stake.toFixed(), `${miningAddress}01${poolNameLength}${poolNameHex}${poolDescriptionLength}${poolDescriptionHex}`), store)
} catch (err) {
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() || stake.isZero()) {
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 (!web3.utils.isAddress(miningAddress)) {
return 'Invalid mining address'
} else if (miningAddress === store.getState().account.toLowerCase()) {
return 'The mining address cannot match the staking address'
}
return true
}
function isPoolNameValid (name, store) {
const web3 = store.getState().web3
const nameHex = web3.utils.stripHexPrefix(web3.utils.utf8ToHex(name.trim()))
const nameLength = nameHex.length / 2
const maxLength = 256
if (nameLength > maxLength) {
return `Pool name length cannot exceed ${maxLength} bytes`
} else if (nameLength === 0) {
return 'Pool name shouldn\'t be empty'
}
return true
}
function isPoolDescriptionValid (description, store) {
const web3 = store.getState().web3
const descriptionHex = web3.utils.stripHexPrefix(web3.utils.utf8ToHex(description.trim()))
const maxLength = 1024
if (descriptionHex.length / 2 > maxLength) {
return `Pool description length cannot exceed ${maxLength} bytes`
}
return true
}

@ -1,340 +0,0 @@
import $ from 'jquery'
import {
currentModal,
lockModal,
openErrorModal,
openModal,
openSuccessModal,
openWarningModal,
unlockModal
} from '../../lib/modals'
import { displayInputError, hideInputError } from '../../lib/validation'
import { isSupportedNetwork, makeContractCall } from './utils'
import constants from './constants'
let status = 'modalClosed'
export function openClaimRewardModal (event, store) {
const state = store.getState()
if (!state.account) {
openWarningModal('Unauthorized', constants.METAMASK_PLEASE_LOGIN)
return
}
if (!isSupportedNetwork(store)) {
return
}
const channel = state.channel
$(event.currentTarget).prop('disabled', true)
channel.push('render_claim_reward', { preload: true }).receive('ok', msg => {
$(event.currentTarget).prop('disabled', false)
const $modal = $(msg.html)
const $closeButton = $modal.find('.close-modal')
const $modalBody = $('.modal-body', $modal)
const dotCounterInterval = poolsSearchingStarted()
const ref = channel.on('claim_reward_pools', msgPools => {
$modalBody.html(msgPools.html)
poolsSearchingFinished()
})
$modal.on('shown.bs.modal', () => {
status = 'modalOpened'
channel.push('render_claim_reward', {
}).receive('error', (error) => {
poolsSearchingFinished(error.reason)
}).receive('timeout', () => {
poolsSearchingFinished('Connection timeout')
})
})
$modal.on('hidden.bs.modal', () => {
status = 'modalClosed'
$modal.remove()
})
function poolsSearchingStarted () {
const $waitingMessageContainer = $modalBody.find('p')
let dotCounter = 0
return setInterval(() => {
let waitingMessage = $.trim($waitingMessageContainer.text())
if (!waitingMessage.endsWith('.')) {
waitingMessage = waitingMessage + '.'
}
waitingMessage = waitingMessage.replace(/\.+$/g, ' ' + '.'.repeat(dotCounter))
$waitingMessageContainer.text(waitingMessage)
dotCounter = (dotCounter + 1) % 4
}, 500)
}
function poolsSearchingFinished (error) {
channel.off('claim_reward_pools', ref)
$closeButton.removeClass('hidden')
unlockModal($modal)
clearInterval(dotCounterInterval)
if (error) {
openErrorModal('Claim Reward', error)
} else {
onPoolsFound($modal, $modalBody, channel, store)
}
}
openModal($modal, true)
}).receive('error', (error) => {
$(event.currentTarget).prop('disabled', false)
openErrorModal('Claim Reward', error.reason)
}).receive('timeout', () => {
$(event.currentTarget).prop('disabled', false)
openErrorModal('Claim Reward', 'Connection timeout')
})
}
export function claimRewardConnectionLost () {
const errorMsg = 'Connection with server is lost. Please, reload the page.'
if (status === 'modalOpened') {
status = 'modalClosed'
openErrorModal('Claim Reward', errorMsg, true)
} else if (status === 'recalculation') {
const $recalculateButton = $('button.recalculate', currentModal())
displayInputError($recalculateButton, errorMsg)
}
}
function onPoolsFound ($modal, $modalBody, channel, store) {
const $poolsDropdown = $('select', $modalBody)
const $epochChoiceRadio = $('input[name="epoch_choice"]', $modalBody)
const $specifiedEpochsText = $('input.specified-epochs', $modalBody)
const $recalculateButton = $('button.recalculate', $modalBody)
const $submitButton = $('button.submit', $modalBody)
let allowedEpochs = []
$poolsDropdown.on('change', () => {
if (status === 'recalculation' || status === 'claiming') return false
const data = $('option:selected', $poolsDropdown).data()
const tokenRewardSum = data.tokenRewardSum ? data.tokenRewardSum : '0'
const nativeRewardSum = data.nativeRewardSum ? data.nativeRewardSum : '0'
const gasLimit = data.gasLimit ? data.gasLimit : '0'
const $poolInfo = $('.selected-pool-info', $modalBody)
const epochs = data.epochs ? data.epochs.toString() : ''
allowedEpochs = expandEpochsToArray(epochs)
$poolsDropdown.blur()
$('textarea', $poolInfo).val(epochs)
$('#token-reward-sum', $poolInfo).text(tokenRewardSum).data('default', tokenRewardSum)
$('#native-reward-sum', $poolInfo).text(nativeRewardSum).data('default', nativeRewardSum)
$('#tx-gas-limit', $poolInfo).text('~' + gasLimit).data('default', gasLimit)
$('#epoch-choice-all', $poolInfo).click()
$specifiedEpochsText.val('')
$poolInfo.removeClass('hidden')
$('.modal-bottom-disclaimer', $modal).removeClass('hidden')
hideInputError($recalculateButton)
})
$epochChoiceRadio.on('change', () => {
if (status === 'recalculation' || status === 'claiming') return false
if ($('#epoch-choice-all', $modalBody).is(':checked')) {
$specifiedEpochsText.addClass('hidden')
showButton('submit', $modalBody)
hideInputError($recalculateButton)
} else {
$specifiedEpochsText.removeClass('hidden')
$specifiedEpochsText.trigger('input')
}
})
$specifiedEpochsText.on('input', () => {
if (status === 'recalculation' || status === 'claiming') return false
const filtered = filterSpecifiedEpochs($specifiedEpochsText.val()).toString()
$specifiedEpochsText.val(filtered)
const pointedEpochs = expandEpochsToArray(filtered)
const pointedEpochsAllowed = pointedEpochs.filter(item => allowedEpochs.indexOf(item) !== -1)
const needsRecalc = pointedEpochs.length > 0 && pointedEpochsAllowed.length !== allowedEpochs.length
showButton(needsRecalc ? 'recalculate' : 'submit', $modalBody)
if (needsRecalc && pointedEpochsAllowed.length === 0) {
$recalculateButton.prop('disabled', true)
displayInputError($recalculateButton, 'The specified staking epochs are not in the allowed range')
} else {
$recalculateButton.prop('disabled', false)
hideInputError($recalculateButton)
}
})
$recalculateButton.on('click', (e) => {
if (status === 'recalculation' || status === 'claiming') return false
e.preventDefault()
recalcStarted()
const specifiedEpochs = $specifiedEpochsText.val().toString().replace(/[-|,]$/g, '').trim()
$specifiedEpochsText.val(specifiedEpochs)
const epochs = expandEpochsToArray(specifiedEpochs).filter(item => allowedEpochs.indexOf(item) !== -1)
const poolStakingAddress = $poolsDropdown.val()
const ref = channel.on('claim_reward_recalculations', result => {
recalcFinished(result)
})
channel.push('recalc_claim_reward', {
epochs,
pool_staking_address: poolStakingAddress
}).receive('error', (error) => {
recalcFinished({ error: error.reason })
}).receive('timeout', () => {
recalcFinished({ error: 'Connection timeout' })
})
function recalcStarted () {
status = 'recalculation'
hideInputError($recalculateButton)
lockUI(true, $modal, $recalculateButton, $poolsDropdown, $epochChoiceRadio, $specifiedEpochsText)
}
function recalcFinished (result) {
channel.off('claim_reward_recalculations', ref)
status = 'modalOpened'
if (result.error) {
displayInputError($recalculateButton, result.error)
} else {
showButton('submit', $modalBody, result)
}
lockUI(false, $modal, $recalculateButton, $poolsDropdown, $epochChoiceRadio, $specifiedEpochsText)
}
})
$submitButton.on('click', async (e) => {
if (status === 'recalculation' || status === 'claiming') return false
e.preventDefault()
const specifiedEpochs = $specifiedEpochsText.val().toString().replace(/[-|,]$/g, '').trim()
const epochs = expandEpochsToArray(specifiedEpochs).filter(item => allowedEpochs.indexOf(item) !== -1)
const poolStakingAddress = $poolsDropdown.val()
claimStarted()
function claimStarted () {
status = 'claiming'
hideInputError($submitButton)
lockUI(
true,
$modal,
$submitButton,
$poolsDropdown,
$epochChoiceRadio,
$specifiedEpochsText,
'Please, sign transaction in MetaMask'
)
const gasLimit = parseInt($('#tx-gas-limit', $modalBody).text().replace(/~/g, '').trim(), 10)
const state = store.getState()
const stakingContract = state.stakingContract
if (isNaN(gasLimit)) {
claimFinished('Invalid gas limit. Please, contact support.')
} else if (!stakingContract) {
claimFinished('Staking contract is undefined. Please, contact support.')
} else if (!poolStakingAddress) {
claimFinished('Pool staking address is undefined. Please, contact support.')
} else {
makeContractCall(
stakingContract.methods.claimReward(epochs, poolStakingAddress),
store,
gasLimit,
claimFinished
)
}
}
function claimFinished (error) {
lockUI(false, $modal, $submitButton, $poolsDropdown, $epochChoiceRadio, $specifiedEpochsText)
if (error) {
status = 'modalOpened'
displayInputError($submitButton, error)
} else {
status = 'modalClosed'
openSuccessModal('Success', 'Transaction is confirmed.')
}
}
})
}
function lockUI (lock, $modal, $button, $poolsDropdown, $epochChoiceRadio, $specifiedEpochsText, spinnerText) {
if (lock) {
lockModal($modal, $button, spinnerText)
} else {
unlockModal($modal, $button)
}
$poolsDropdown.prop('disabled', lock)
$epochChoiceRadio.prop('disabled', lock)
$specifiedEpochsText.prop('disabled', lock)
}
function showButton (type, $modalBody, calculations) {
const $recalculateButton = $('button.recalculate', $modalBody)
const $submitButton = $('button.submit', $modalBody)
const $tokenRewardSum = $('#token-reward-sum', $modalBody)
const $nativeRewardSum = $('#native-reward-sum', $modalBody)
const $gasLimit = $('#tx-gas-limit', $modalBody)
if (type === 'submit') {
$recalculateButton.addClass('hidden')
$submitButton.removeClass('hidden')
const tokenRewardSum = !calculations ? $tokenRewardSum.data('default') : calculations.token_reward_sum
const nativeRewardSum = !calculations ? $nativeRewardSum.data('default') : calculations.native_reward_sum
const gasLimit = !calculations ? $gasLimit.data('default') : calculations.gas_limit
$tokenRewardSum.text(tokenRewardSum).css('text-decoration', '')
$nativeRewardSum.text(nativeRewardSum).css('text-decoration', '')
$gasLimit.text('~' + gasLimit).css('text-decoration', '')
} else {
$recalculateButton.removeClass('hidden')
$submitButton.addClass('hidden');
[$tokenRewardSum, $nativeRewardSum, $gasLimit].forEach(
$item => $item.css('text-decoration', 'line-through')
)
}
}
function expandEpochsToArray (epochs) {
const filtered = epochs.toString().replace(/[-|,]$/g, '').trim()
if (filtered === '') return []
let ranges = filtered.split(',')
ranges = ranges.map((v) => {
if (v.indexOf('-') > -1) {
v = v.split('-')
v[0] = parseInt(v[0], 10)
v[1] = parseInt(v[1], 10)
v.sort((a, b) => a - b)
const min = v[0]
const max = v[1]
const expanded = []
for (let i = min; i <= max; i++) {
expanded.push(i)
}
return expanded
} else {
return parseInt(v, 10)
}
})
ranges = ranges.reduce((acc, val) => acc.concat(val), []) // similar to ranges.flat()
ranges.sort((a, b) => a - b)
ranges = [...new Set(ranges)] // make unique
ranges = ranges.filter(epoch => epoch !== 0)
return ranges
}
function filterSpecifiedEpochs (epochs) {
let filtered = epochs.toString()
filtered = filtered.replace(/[^0-9,-]+/g, '')
filtered = filtered.replace(/-{2,}/g, '-')
filtered = filtered.replace(/,{2,}/g, ',')
filtered = filtered.replace(/,-/g, ',')
filtered = filtered.replace(/-,/g, '-')
filtered = filtered.replace(/(-[0-9]+)-/g, '$1,')
filtered = filtered.replace(/^[,|-|0]/g, '')
return filtered
}

@ -1,29 +0,0 @@
import $ from 'jquery'
import { openModal, lockModal } from '../../lib/modals'
import { makeContractCall, setupChart, isSupportedNetwork } from './utils'
export function openClaimWithdrawalModal (event, store) {
if (!isSupportedNetwork(store)) return
const address = $(event.target).closest('[data-address]').data('address')
store.getState().channel
.push('render_claim_withdrawal', { address })
.receive('ok', msg => {
const $modal = $(msg.html)
setupChart($modal.find('.js-stakes-progress'), msg.self_staked_amount, msg.total_staked_amount)
$modal.find('form').submit(() => {
claimWithdraw($modal, address, store)
return false
})
openModal($modal)
})
}
function claimWithdraw ($modal, address, store) {
lockModal($modal)
const stakingContract = store.getState().stakingContract
makeContractCall(stakingContract.methods.claimOrderedWithdraw(address), store)
}

@ -1,5 +0,0 @@
module.exports = {
METAMASK_ACCOUNTS_EMPTY: 'You haven\'t approved the reading of account list from your MetaMask or the latest MetaMask version is not installed.',
METAMASK_PLEASE_LOGIN: 'You are not logged in. Please login with the latest version of MetaMask.',
METAMASK_VERSION_WARNING: 'Make sure you are using the latest version of MetaMask.'
}

@ -1,10 +0,0 @@
import $ from 'jquery'
import { openModal } from '../../lib/modals'
export function openDelegatorsListModal (event, store) {
const address = $(event.target).closest('[data-address]').data('address')
store.getState().channel
.push('render_delegators_list', { address })
.receive('ok', msg => openModal($(msg.html)))
}

@ -1,98 +0,0 @@
import $ from 'jquery'
import { BigNumber } from 'bignumber.js'
import { openErrorModal, openModal, openWarningModal, lockModal } from '../../lib/modals'
import { setupValidation } from '../../lib/validation'
import { makeContractCall, setupChart, isSupportedNetwork, isStakingAllowed } from './utils'
import constants from './constants'
export function openMakeStakeModal (event, store) {
const state = store.getState()
if (!state.account) {
openWarningModal('Unauthorized', constants.METAMASK_ACCOUNTS_EMPTY)
return
}
if (!isSupportedNetwork(store)) return
if (!isStakingAllowed(state)) return
const address = $(event.target).closest('[data-address]').data('address') || store.getState().account
state.channel
.push('render_make_stake', { address })
.receive('ok', msg => {
const $modal = $(msg.html)
const $form = $modal.find('form')
setupChart($modal.find('.js-stakes-progress'), msg.self_staked_amount, msg.total_staked_amount)
setupValidation(
$form,
{
'delegator-stake': value => isDelegatorStakeValid(value, store, msg, address)
},
$modal.find('form button')
)
$modal.find('[data-available-amount]').click(e => {
const amount = $(e.currentTarget).data('available-amount')
$('[delegator-stake]', $form).val(amount).trigger('input')
$('.tooltip').tooltip('hide')
return false
})
$form.submit(() => {
makeStake($modal, address, store, msg)
return false
})
openModal($modal)
})
}
async function makeStake ($modal, address, store, msg) {
const state = store.getState()
const stakingContract = state.stakingContract
const tokenContract = state.tokenContract
const validatorSetContract = state.validatorSetContract
const decimals = state.tokenDecimals
const stake = new BigNumber($modal.find('[delegator-stake]').val().replace(',', '.').trim()).shiftedBy(decimals).integerValue()
if (!isSupportedNetwork(store)) return
if (!isStakingAllowed(state)) return
lockModal($modal)
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(tokenContract.methods.transferAndCall(stakingContract.options.address, stake.toFixed(), address), 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() || stake.isZero()) {
return 'Invalid amount'
} else if (stake.plus(currentStake).isLessThan(minStake)) {
const staker = (account.toLowerCase() === address.toLowerCase()) ? 'candidate' : 'delegate'
return `Minimum ${staker} stake is ${minStake.shiftedBy(-decimals)} ${store.getState().tokenSymbol}`
} else if (stake.isGreaterThan(balance)) {
return 'Insufficient funds'
}
return true
}

@ -1,92 +0,0 @@
import $ from 'jquery'
import { BigNumber } from 'bignumber.js'
import { openModal, lockModal } from '../../lib/modals'
import { setupValidation } from '../../lib/validation'
import { makeContractCall, setupChart, isSupportedNetwork } from './utils'
export function openMoveStakeModal (event, store) {
if (!isSupportedNetwork(store)) return
const fromAddress = $(event.target).closest('[data-address]').data('address')
store.getState().channel
.push('render_move_stake', { from: fromAddress, to: null, amount: null })
.receive('ok', msg => {
const $modal = $(msg.html)
setupModal($modal, fromAddress, store, msg)
openModal($modal)
})
}
function setupModal ($modal, fromAddress, store, msg) {
const $form = $modal.find('form')
setupChart($modal.find('.js-pool-from-progress'), msg.from.self_staked_amount, msg.from.total_staked_amount)
if (msg.to) {
setupChart($modal.find('.js-pool-to-progress'), msg.to.self_staked_amount, msg.to.total_staked_amount)
setupValidation(
$form,
{
'move-amount': value => isMoveAmountValid(value, store, msg)
},
$modal.find('form button')
)
}
$form.submit(() => {
moveStake($modal, fromAddress, store, msg)
return false
})
$modal.find('[pool-select]').on('change', event => {
const toAddress = $modal.find('[pool-select]').val()
const amount = $modal.find('[move-amount]').val()
store.getState().channel
.push('render_move_stake', { from: fromAddress, to: toAddress, amount })
.receive('ok', msg => {
$modal.html($(msg.html).html())
$modal.modal('show')
setupModal($modal, fromAddress, store, msg)
})
})
$modal.find('[data-available-amount]').click(e => {
const amount = $(e.currentTarget).data('available-amount')
$('[move-amount]', $form).val(amount).trigger('input')
$('.tooltip').tooltip('hide')
return false
})
}
function moveStake ($modal, fromAddress, store, msg) {
lockModal($modal)
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.toFixed()), store)
}
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 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.isZero()) {
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`
}
return true
}

@ -1,25 +0,0 @@
import { openErrorModal, openQuestionModal } from '../../lib/modals'
import { makeContractCall, isSupportedNetwork } from './utils'
export function openRemovePoolModal (store) {
if (!isSupportedNetwork(store)) return
openQuestionModal('Remove my Pool', 'Do you really want to remove your pool?', () => removePool(store))
}
async function removePool (store) {
const state = store.getState()
const call = state.stakingContract.methods.removeMyPool()
let gasLimit
try {
gasLimit = await call.estimateGas({
from: state.account,
gasPrice: 1000000000
})
} catch (err) {
openErrorModal('Error', 'Currently you cannot remove your pool. Please try again during the next epoch.')
return
}
makeContractCall(call, store, gasLimit)
}

@ -1,142 +0,0 @@
import $ from 'jquery'
import { ArcElement, Chart, DoughnutController } from 'chart.js'
import { openErrorModal, openSuccessModal, openWarningModal } from '../../lib/modals'
Chart.defaults.font.family = 'Nunito, "Helvetica Neue", Arial, sans-serif,"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"'
Chart.register(ArcElement, DoughnutController)
export async function makeContractCall (call, store, gasLimit, callbackFunc) {
const state = store.getState()
const from = state.account
const web3 = state.web3
if (!callbackFunc) {
callbackFunc = function (errorMessage) {
if (!errorMessage) {
openSuccessModal('Success', 'Transaction is confirmed.')
state.refreshPageFunc(store)
} else {
openErrorModal('Error', errorMessage)
}
}
}
if (!from) {
return callbackFunc('Your MetaMask account is undefined. Please, ensure you are using the latest version of MetaMask and connected it to the page')
} else if (!web3) {
return callbackFunc('Web3 is undefined. Please, contact support.')
}
const gasPrice = web3.utils.toWei('20', 'gwei')
if (!gasLimit) {
try {
gasLimit = await call.estimateGas({ from, gasPrice })
} catch (e) {
console.log(`from = ${from}`)
console.error(e)
return callbackFunc('Your transaction cannot be mined at the moment. Please, try again in a few blocks.')
}
}
call.send({
from,
gasPrice,
gas: Math.ceil(gasLimit * 1.2) // +20% reserve to ensure enough gas
}, async function (error, txHash) {
if (error) {
let errorMessage = 'Your transaction wasn\'t processed, please try again in a few blocks.'
if (error.message) {
const detailsMessage = error.message.replace(/["]/g, '&quot;')
console.log(detailsMessage)
const detailsHTML = ` <a href="javascript:void(0);" data-boundary="window" data-container="body" data-html="false" data-placement="top" data-toggle="tooltip" title="${detailsMessage}" data-original-title="${detailsMessage}" class="link-helptip">Details</a>`
errorMessage = errorMessage + detailsHTML
}
callbackFunc(errorMessage)
} else {
try {
let tx
let currentBlockNumber
const maxWaitBlocks = 6
const startBlockNumber = (await web3.eth.getBlockNumber()) - 0
const finishBlockNumber = startBlockNumber + maxWaitBlocks
do {
await sleep(5) // seconds
tx = await web3.eth.getTransactionReceipt(txHash)
currentBlockNumber = await web3.eth.getBlockNumber()
} while (tx === null && currentBlockNumber <= finishBlockNumber)
if (tx) {
if (tx.status === true || tx.status === '0x1') {
callbackFunc() // success
} else {
callbackFunc('Transaction reverted')
}
} else {
const msg = `Your transaction wasn't processed in ${maxWaitBlocks} blocks. Please, try again with the increased gas price or fixed nonce (use Reset Account feature of MetaMask).`
callbackFunc(msg)
}
} catch (e) {
callbackFunc(e.message)
}
}
})
}
export function setupChart ($canvas, self, total) {
const primaryColor = $('.stakes-progress-graph-thing-for-getting-color').css('color')
const backgroundColors = [
primaryColor,
'rgba(202, 199, 226, 0.5)'
]
const data = total > 0 ? [self, total - self] : [0, 1]
// eslint-disable-next-line no-new
new Chart($canvas, {
type: 'doughnut',
data: {
datasets: [{
data,
backgroundColor: backgroundColors,
hoverBackgroundColor: backgroundColors,
borderWidth: 0
}]
},
options: {
cutout: '80%',
plugins: {
legend: {
display: false
}
}
}
})
}
export function checkForTokenDefinition (store) {
if (store.getState().stakingTokenDefined) {
return true
}
openWarningModal('Token unavailable', 'Token contract is not defined yet. Please try later.')
return false
}
export function isStakingAllowed (state) {
if (!state.stakingAllowed) {
openWarningModal('Actions temporarily disallowed', 'The current staking epoch is ending, and staking actions are temporarily restricted. Please try again after the new epoch starts. If the epoch has just started, try again in a few blocks.')
return false
}
return true
}
export function isSupportedNetwork (store) {
const state = store.getState()
if (state.network && state.network.authorized) {
return true
}
openWarningModal('Unauthorized', 'Please, connect to the xDai Chain.<br /><a href="https://xdaichain.com" target="_blank">Instructions</a>. If you have already connected to, please update MetaMask to the latest version.')
return false
}
function sleep (seconds) {
return new Promise(resolve => setTimeout(resolve, seconds * 1000))
}

@ -1,37 +0,0 @@
import $ from 'jquery'
import { openModal, openErrorModal } from '../../lib/modals'
import { makeContractCall, isSupportedNetwork } from './utils'
export function openPoolInfoModal (event, store) {
const address = $(event.target).closest('[data-address]').data('address')
store.getState().channel.push('render_validator_info', { address }).receive('ok', msg => {
const $modal = $(msg.html)
$modal.on('click', '#save_pool_metadata', event => {
event.preventDefault()
if (!isSupportedNetwork(store)) return
const validatorSetContract = store.getState().validatorSetContract
const nameField = $('#pool_name', $modal)
const name = nameField.val()
const descriptionField = $('#pool_description', $modal)
const description = descriptionField.val()
nameField.attr('disabled', true)
descriptionField.attr('disabled', true)
$('#save_pool_metadata_container', $modal).hide()
$('#waiting_message', $modal).show()
makeContractCall(validatorSetContract.methods.changeMetadata(name, description), store, null, (errorMessage) => {
nameField.attr('disabled', false)
descriptionField.attr('disabled', false)
$('#waiting_message', $modal).hide()
$('#save_pool_metadata_container', $modal).show()
if (errorMessage) {
openErrorModal('Error', errorMessage)
}
})
})
openModal($modal)
})
}

@ -1,143 +0,0 @@
import $ from 'jquery'
import { BigNumber } from 'bignumber.js'
import { openModal, openErrorModal, lockModal } from '../../lib/modals'
import { setupValidation } from '../../lib/validation'
import { makeContractCall, setupChart, isSupportedNetwork } from './utils'
export function openWithdrawStakeModal (event, store) {
if (!isSupportedNetwork(store)) return
const address = $(event.target).closest('[data-address]').data('address')
store.getState().channel
.push('render_withdraw_stake', { address })
.receive('ok', msg => setupWithdrawStakeModal(address, store, msg))
}
function setupWithdrawStakeModal (address, store, msg) {
const $modal = $(msg.html)
const $form = $modal.find('form')
setupChart($modal.find('.js-stakes-progress'), msg.self_staked_amount, msg.total_staked_amount)
setupValidation(
$form,
{
amount: value => isAmountValid(value, store, msg)
},
$modal.find('form button')
)
setupValidation(
$form,
{
amount: value => isWithdrawAmountValid(value, store, msg)
},
$modal.find('form button.withdraw')
)
setupValidation(
$form,
{
amount: value => isOrderWithdrawAmountValid(value, store, msg)
},
$modal.find('form button.order-withdraw')
)
$modal.find('[data-available-amount]').click(e => {
const amount = $(e.currentTarget).data('available-amount')
$('[amount]', $form).val(amount).trigger('input')
$('.tooltip').tooltip('hide')
return false
})
$modal.find('.btn-full-primary.withdraw').click(() => {
withdrawStake($modal, address, store, msg)
return false
})
$modal.find('.btn-full-primary.order-withdraw').click(() => {
orderWithdraw($modal, address, store, msg)
return false
})
openModal($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.toFixed()), store)
}
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.toFixed()), 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()) || amount.isZero()) {
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() || amount.isZero()) {
return null
} else if (amount.isLessThan(currentStake) && currentStake.minus(amount).isLessThan(minStake)) {
return null
} else if (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()) || amount.isZero()) {
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,8 +102,7 @@
},
"jest": {
"moduleNameMapper": {
"/css/app.scss": "<rootDir>/__mocks__/css/app.scss.js",
"/css/stakes.scss": "<rootDir>/__mocks__/css/app.scss.js"
"/css/app.scss": "<rootDir>/__mocks__/css/app.scss.js"
}
}
}

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
<path fill-rule="evenodd" d="M15 16H1a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1h4.347l1.778 2H2v1h12v-1H8.875l1.778-2H15a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1zm0-11H9v3.532l1.252-1.253a1.028 1.028 0 1 1 1.455 1.455l-2.909 2.911c-.038.038-.087.054-.129.085a.945.945 0 0 1-.653.27h-.032a.971.971 0 0 1-.713-.315c-.006-.005-.013-.006-.018-.011L4.326 8.745A1.036 1.036 0 0 1 5.79 7.281L7 8.492V5H1a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1zm-1-3H2v1h12V2z"/>
</svg>

Before

Width:  |  Height:  |  Size: 532 B

@ -1 +0,0 @@
<svg height="16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m15 16h-14a1 1 0 0 1 -1-1v-3a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v3a1 1 0 0 1 -1 1zm-1-3h-12v1h12zm-5.215-4.325a1.023 1.023 0 0 1 -.394.237c-.007.003-.012.01-.02.013a1.036 1.036 0 0 1 -1.136-.221l-2.939-2.939a1.038 1.038 0 1 1 1.469-1.469l1.235 1.235v-4.531a1 1 0 0 1 2 0v4.54l1.246-1.246a1.033 1.033 0 0 1 1.46 1.46z" fill-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 412 B

@ -36,7 +36,6 @@ const appJs =
{
entry: {
'app': './js/app.js',
'stakes': './js/pages/stakes.js',
'chart-loader': './js/chart-loader.js',
'balance-chart-loader': './js/balance-chart-loader.js',
'chain': './js/pages/chain.js',
@ -63,7 +62,6 @@ const appJs =
'async-listing-load': './js/lib/async_listing_load',
'non-critical': './css/non-critical.scss',
'main-page': './css/main-page.scss',
'staking': './css/stakes.scss',
'tokens': './js/pages/token/search.js',
'text-ad': './js/lib/text_ad.js',
'banner': './js/lib/banner.js',

@ -23,12 +23,7 @@ config :block_scout_web, BlockScoutWeb.Chain,
logo_footer: System.get_env("LOGO_FOOTER"),
logo_text: System.get_env("LOGO_TEXT"),
has_emission_funds: false,
staking_enabled: not is_nil(System.get_env("POS_STAKING_CONTRACT")),
staking_enabled_in_menu: System.get_env("ENABLE_POS_STAKING_IN_MENU", "false") == "true",
show_staking_warning: System.get_env("SHOW_STAKING_WARNING", "false") == "true",
show_maintenance_alert: System.get_env("SHOW_MAINTENANCE_ALERT", "false") == "true",
# how often (in blocks) the list of pools should autorefresh in UI (zero turns off autorefreshing)
staking_pool_list_refresh_interval: 5
show_maintenance_alert: System.get_env("SHOW_MAINTENANCE_ALERT", "false") == "true"
config :block_scout_web,
link_to_other_explorers: System.get_env("LINK_TO_OTHER_EXPLORERS") == "true",

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

@ -24,7 +24,6 @@ defmodule BlockScoutWeb.Chain do
Block,
InternalTransaction,
Log,
StakingPool,
Token,
TokenTransfer,
Transaction,
@ -350,10 +349,6 @@ defmodule BlockScoutWeb.Chain do
%{"block_number" => block_number}
end
defp paging_params(%StakingPool{staking_address_hash: address_hash, stakes_ratio: value}) do
%{"address_hash" => address_hash, "value" => Decimal.to_string(value)}
end
defp paging_params(%{
address_hash: address_hash,
tx_hash: tx_hash,

@ -1,954 +0,0 @@
defmodule BlockScoutWeb.StakesChannel do
@moduledoc """
Establishes pub/sub channel for staking page live updates.
"""
use BlockScoutWeb, :channel
alias BlockScoutWeb.{StakesController, StakesHelpers, StakesView}
alias Explorer.Chain
alias Explorer.Chain.Cache.BlockNumber
alias Explorer.Chain.Token
alias Explorer.Counters.AverageBlockTime
alias Explorer.Staking.{ContractReader, ContractState}
alias Phoenix.View
alias Timex.Duration
import BlockScoutWeb.Gettext
@claim_reward_long_op :claim_reward_long_op
intercept(["staking_update"])
def join("stakes:staking_update", _params, socket) do
{:ok, %{}, socket}
end
# called when socket is closed on a client side
# or socket timeout is reached - see `timeout` option in
# https://hexdocs.pm/phoenix/Phoenix.Endpoint.html#socket/3-websocket-configuration
# apps/block_scout_web/lib/block_scout_web/endpoint.ex
def terminate(_reason, socket) do
s = socket.assigns[@claim_reward_long_op]
if s != nil do
:ets.delete(ContractState, claim_reward_long_op_key(s.staker))
end
end
def handle_in("set_account", account, socket) do
# fetch pool id by staking address to show `Make stake` modal
# instead of `Become a candidate` for the staking address which
# has ever been a pool
pool_id_raw =
try do
validator_set_contract = ContractState.get(:validator_set_contract)
ContractReader.perform_requests(
ContractReader.id_by_staking_request(account),
%{validator_set: validator_set_contract.address},
validator_set_contract.abi
).pool_id
rescue
_ -> nil
end
# convert 0 to nil
pool_id =
if pool_id_raw != 0 do
pool_id_raw
end
socket =
socket
|> assign(:account, account)
|> assign(:pool_id, pool_id)
|> push_contracts()
data =
case Map.fetch(socket.assigns, :staking_update_data) do
{:ok, staking_update_data} ->
staking_update_data
_ ->
%{
block_number: BlockNumber.get_max(),
epoch_number: ContractState.get(:epoch_number, 0),
epoch_end_block: ContractState.get(:epoch_end_block, 0),
staking_allowed: ContractState.get(:staking_allowed, false),
staking_token_defined: ContractState.get(:token, nil) != nil,
validator_set_apply_block: ContractState.get(:validator_set_apply_block, 0)
}
end
handle_out("staking_update", Map.merge(data, %{by_set_account: true}), socket)
{:reply, :ok, socket}
end
def handle_in("render_validator_info", %{"address" => staking_address}, socket) do
pool = Chain.staking_pool(staking_address)
delegator = socket.assigns[:account] && Chain.staking_pool_delegator(staking_address, socket.assigns.account)
average_block_time = AverageBlockTime.average_block_time()
token = ContractState.get(:token)
html =
View.render_to_string(StakesView, "_stakes_modal_pool_info.html",
validator: pool,
delegator: delegator,
average_block_time: average_block_time,
token: token
)
{:reply, {:ok, %{html: html}}, socket}
end
def handle_in("render_delegators_list", %{"address" => pool_staking_address}, socket) do
pool_staking_address_downcased = String.downcase(pool_staking_address)
pool = Chain.staking_pool(pool_staking_address)
pool_rewards = ContractState.get(:pool_rewards, %{})
calc_apy_enabled = ContractState.calc_apy_enabled?()
token = ContractState.get(:token)
validator_min_reward_percent = ContractState.get(:validator_min_reward_percent)
show_snapshotted_data = ContractState.show_snapshotted_data(pool.is_validator)
staking_epoch_duration = ContractState.staking_epoch_duration()
average_block_time =
try do
Duration.to_seconds(AverageBlockTime.average_block_time())
rescue
_ -> nil
end
pool_reward =
case Map.fetch(pool_rewards, String.downcase(to_string(pool.mining_address_hash))) do
{:ok, pool_reward} -> pool_reward
:error -> nil
end
stakers =
pool_staking_address
|> Chain.staking_pool_delegators(show_snapshotted_data)
|> Enum.sort_by(fn staker ->
staker_address = to_string(staker.address_hash)
cond do
staker_address == pool_staking_address -> 0
staker_address == socket.assigns[:account] -> 1
true -> 2
end
end)
|> Enum.map(fn staker ->
apy =
if calc_apy_enabled do
calc_apy(
pool,
staker,
pool_staking_address_downcased,
pool_reward,
average_block_time,
staking_epoch_duration
)
end
Map.put(staker, :apy, apy)
end)
html =
View.render_to_string(StakesView, "_stakes_modal_delegators_list.html",
account: socket.assigns[:account],
pool: pool,
conn: socket,
stakers: stakers,
token: token,
show_snapshotted_data: show_snapshotted_data,
validator_min_reward_percent: validator_min_reward_percent
)
{:reply, {:ok, %{html: html}}, socket}
end
def handle_in("render_become_candidate", _, socket) do
min_candidate_stake = Decimal.new(ContractState.get(:min_candidate_stake))
token = ContractState.get(:token)
balance = Chain.fetch_last_token_balance(socket.assigns.account, token.contract_address_hash)
html =
View.render_to_string(StakesView, "_stakes_modal_become_candidate.html",
min_candidate_stake: min_candidate_stake,
balance: balance,
coin: get_coin(),
token: token
)
result = %{
html: html,
balance: balance,
min_candidate_stake: min_candidate_stake
}
{:reply, {:ok, result}, socket}
end
def handle_in("render_make_stake", %{"address" => staking_address}, socket) do
staking_pool = Chain.staking_pool(staking_address)
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 =
Decimal.new(
if staking_address == socket.assigns.account do
ContractState.get(:min_candidate_stake)
else
ContractState.get(:min_delegator_stake)
end
)
delegator_staked = Decimal.new((delegator && delegator.stake_amount) || 0)
# if pool doesn't exist, fill it with empty values
# to be able to display _stakes_progress.html.eex template
pool =
staking_pool ||
%{
delegators_count: 0,
is_active: false,
is_deleted: true,
self_staked_amount: 0,
mining_address_hash: nil,
name: nil,
staking_address_hash: staking_address,
total_staked_amount: 0
}
html =
View.render_to_string(StakesView, "_stakes_modal_stake.html",
balance: balance,
delegator_staked: delegator_staked,
min_stake: min_stake,
pool: pool,
token: token
)
result = %{
html: html,
balance: balance,
delegator_staked: delegator_staked,
mining_address: nil,
min_stake: min_stake,
self_staked_amount: pool.self_staked_amount,
total_staked_amount: pool.total_staked_amount
}
{:reply, {:ok, result}, socket}
end
def handle_in("render_move_stake", %{"from" => from_address, "to" => to_address, "amount" => amount}, socket) do
pool_from = Chain.staking_pool(from_address)
pool_to = to_address && Chain.staking_pool(to_address)
pools = Chain.staking_pools(:active, :all)
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)
html =
View.render_to_string(StakesView, "_stakes_modal_move.html",
token: token,
pools: pools,
pool_from: pool_from,
pool_to: pool_to,
delegator_from: delegator_from,
delegator_to: delegator_to,
amount: amount
)
min_from_stake =
Decimal.new(
if delegator_from.address_hash == delegator_from.staking_address_hash do
ContractState.get(:min_candidate_stake)
else
ContractState.get(:min_delegator_stake)
end
)
result = %{
html: html,
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,
total_staked_amount: pool_from.total_staked_amount
},
to:
if pool_to do
stake_amount = Decimal.new((delegator_to && delegator_to.stake_amount) || 0)
min_to_stake =
Decimal.new(
if to_address == socket.assigns.account do
ContractState.get(:min_candidate_stake)
else
ContractState.get(:min_delegator_stake)
end
)
%{
stake_amount: stake_amount,
min_stake: min_to_stake,
self_staked_amount: pool_to.self_staked_amount,
total_staked_amount: pool_to.total_staked_amount
}
end
}
{:reply, {:ok, result}, socket}
end
def handle_in("render_withdraw_stake", %{"address" => staking_address}, socket) do
pool = Chain.staking_pool(staking_address)
token = ContractState.get(:token)
delegator = Chain.staking_pool_delegator(staking_address, socket.assigns.account)
min_stake =
if delegator.address_hash == delegator.staking_address_hash do
ContractState.get(:min_candidate_stake)
else
ContractState.get(:min_delegator_stake)
end
html =
View.render_to_string(StakesView, "_stakes_modal_withdraw.html",
token: token,
delegator: delegator,
pool: pool
)
result = %{
html: html,
self_staked_amount: pool.self_staked_amount,
total_staked_amount: pool.total_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}
end
def handle_in("render_claim_reward", data, socket) do
staker = socket.assigns[:account]
staking_contract_address =
try do
ContractState.get(:staking_contract).address
rescue
_ -> nil
end
empty_staker = staker == nil || staker == "" || staker == "0x0000000000000000000000000000000000000000"
empty_staking_contract_address =
staking_contract_address == nil || staking_contract_address == "" ||
staking_contract_address == "0x0000000000000000000000000000000000000000"
handle_in_render_claim_reward_result(
socket,
data,
staker,
staking_contract_address,
empty_staker,
empty_staking_contract_address
)
end
def handle_in("recalc_claim_reward", data, socket) do
epochs = data["epochs"]
pool_staking_address = data["pool_staking_address"]
staker = socket.assigns[:account]
staking_contract_address =
try do
ContractState.get(:staking_contract).address
rescue
_ -> nil
end
empty_pool_staking_address =
pool_staking_address == nil || pool_staking_address == "" ||
pool_staking_address == "0x0000000000000000000000000000000000000000"
empty_staker = staker == nil || staker == "" || staker == "0x0000000000000000000000000000000000000000"
empty_staking_contract_address =
staking_contract_address == nil || staking_contract_address == "" ||
staking_contract_address == "0x0000000000000000000000000000000000000000"
handle_in_recalc_claim_reward_result(
socket,
epochs,
staking_contract_address,
pool_staking_address,
staker,
empty_pool_staking_address,
empty_staking_contract_address,
empty_staker
)
end
def handle_in("render_claim_withdrawal", %{"address" => staking_address}, socket) do
pool = Chain.staking_pool(staking_address)
token = ContractState.get(:token)
delegator = Chain.staking_pool_delegator(staking_address, socket.assigns.account)
html =
View.render_to_string(StakesView, "_stakes_modal_claim_withdrawal.html",
token: token,
delegator: delegator,
pool: pool
)
result = %{
html: html,
self_staked_amount: pool.self_staked_amount,
total_staked_amount: pool.total_staked_amount
}
{:reply, {:ok, result}, socket}
end
def handle_info({:DOWN, ref, :process, pid, _reason}, socket) do
s = socket.assigns[@claim_reward_long_op]
socket =
if s && s.task.ref == ref && s.task.pid == pid do
:ets.delete(ContractState, claim_reward_long_op_key(s.staker))
assign(socket, @claim_reward_long_op, nil)
else
socket
end
{:noreply, socket}
end
def handle_info(_, socket) do
{:noreply, socket}
end
def handle_out("staking_update", data, socket) do
by_set_account =
case Map.fetch(data, :by_set_account) do
{:ok, value} -> value
_ -> false
end
socket =
if by_set_account do
# if :by_set_account is in the `data`,
# it means that this function was called by
# handle_in("set_account", ...), so we
# shouldn't assign the incoming data to the socket
socket
else
# otherwise, we should do the assignment
# to use the incoming data later by
# handle_in("set_account", ...) and StakesController.render_top
assign(socket, :staking_update_data, data)
end
epoch_end_block = if Map.has_key?(data, :epoch_end_block), do: data.epoch_end_block, else: 0
push(socket, "staking_update", %{
account: socket.assigns[:account],
block_number: data.block_number,
by_set_account: by_set_account,
epoch_number: data.epoch_number,
epoch_end_block: epoch_end_block,
staking_allowed: data.staking_allowed,
staking_token_defined: data.staking_token_defined,
validator_set_apply_block: data.validator_set_apply_block,
top_html: StakesController.render_top(socket)
})
{:noreply, socket}
end
def find_claim_reward_pools(socket, staker, staking_contract_address) do
:ets.insert(ContractState, {claim_reward_long_op_key(staker), true})
try do
json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments)
staking_contract = ContractState.get(:staking_contract)
validator_set_contract = ContractState.get(:validator_set_contract)
responses =
staker
|> ContractReader.get_delegator_pools_length_request()
|> ContractReader.perform_requests(%{staking: staking_contract.address}, staking_contract.abi)
delegator_pools_length = responses[:length]
chunk_size = 100
pool_ids =
if delegator_pools_length > 0 do
chunks = 0..trunc(ceil(delegator_pools_length / chunk_size) - 1)
Enum.reduce(chunks, [], fn i, acc ->
responses =
staker
|> ContractReader.get_delegator_pools_request(i * chunk_size, chunk_size)
|> ContractReader.perform_requests(%{staking: staking_contract.address}, staking_contract.abi)
acc ++ responses[:pools]
end)
else
[]
end
staker_pool_id = socket.assigns[:pool_id]
# convert pool ids to staking addresses
pools =
pool_ids
|> Enum.map(&ContractReader.staking_by_id_request(&1))
|> ContractReader.perform_grouped_requests(
pool_ids,
%{validator_set: validator_set_contract.address},
validator_set_contract.abi
)
|> Enum.map(fn {_, resp} -> resp.staking_address end)
# if `staker` is a pool, prepend its address to the `pools` array
pools =
if is_nil(staker_pool_id) do
pools
else
[staker | pools]
end
# if `staker` is a pool, prepend its pool ID to the `pool_ids` array
pool_ids =
if is_nil(staker_pool_id) do
pool_ids
else
[staker_pool_id | pool_ids]
end
pools_amounts =
Enum.map(pools, fn pool_staking_address ->
ContractReader.call_get_reward_amount(
staking_contract_address,
[],
pool_staking_address,
staker,
json_rpc_named_arguments
)
end)
error =
Enum.find_value(pools_amounts, fn result ->
case result do
{:error, reason} -> error_reason_to_string(reason)
_ -> nil
end
end)
{error, pools} =
get_pools(pools_amounts, pools, pool_ids, staking_contract_address, staker, json_rpc_named_arguments, error)
html =
View.render_to_string(
StakesView,
"_stakes_modal_claim_reward_content.html",
coin: get_coin(),
error: error,
pools: pools,
token: ContractState.get(:token)
)
push(socket, "claim_reward_pools", %{
html: html
})
after
:ets.delete(ContractState, claim_reward_long_op_key(staker))
end
end
def get_pools(pools_amounts, pools, pool_ids, staking_contract_address, staker, json_rpc_named_arguments, error) do
if is_nil(error) do
block_reward_contract = ContractState.get(:block_reward_contract)
validator_set_contract = ContractState.get(:validator_set_contract)
staking_address_to_id =
pools
|> Enum.map(fn staking_address -> String.downcase(to_string(staking_address)) end)
|> Enum.zip(pool_ids)
|> Map.new()
pools =
pools_amounts
|> Enum.map(fn {_, amounts} -> amounts end)
|> Enum.zip(pools)
|> Enum.filter(fn {amounts, _} -> amounts.token_reward_sum > 0 || amounts.native_reward_sum > 0 end)
|> Enum.map(fn {amounts, pool_staking_address} ->
responses =
pool_staking_address
|> ContractReader.epochs_to_claim_reward_from_request(staker)
|> ContractReader.perform_requests(
%{block_reward: block_reward_contract.address},
block_reward_contract.abi
)
epochs =
responses[:epochs]
|> array_to_ranges()
|> Enum.map(fn {first, last} ->
Integer.to_string(first) <> if first != last, do: "-" <> Integer.to_string(last), else: ""
end)
data = Map.put(amounts, :epochs, Enum.join(epochs, ","))
{data, pool_staking_address}
end)
|> Enum.filter(fn {data, _} -> data.epochs != "" 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)
error =
Enum.find_value(pools_gas_estimates, fn {_, result} ->
case result do
{:error, reason} -> error_reason_to_string(reason)
_ -> nil
end
end)
pools =
if is_nil(error) do
pools_gas_estimates = Map.new(pools_gas_estimates)
staking_addresses =
Enum.map(pools, fn {_, staking_address} -> String.downcase(to_string(staking_address)) end)
# first, try to retrieve pool name from database
pool_name_by_staking_address_from_db =
staking_addresses
|> Chain.staking_pool_names()
|> Map.new(fn row -> {String.downcase(to_string(row.staking_address_hash)), row.name} end)
# if the staking address is not found in database,
# call the `poolName` getter from the contract
unknown_staking_addresses =
staking_addresses
|> Enum.reduce([], fn staking_address, acc ->
case Map.fetch(pool_name_by_staking_address_from_db, staking_address) do
{:ok, _name} ->
acc
:error ->
[staking_address | acc]
end
end)
pool_name_by_staking_address_from_chain =
unknown_staking_addresses
|> Enum.map(fn staking_address ->
pool_id = staking_address_to_id[staking_address]
ContractReader.pool_name_request(pool_id, nil)
end)
|> ContractReader.perform_grouped_requests(
unknown_staking_addresses,
%{validator_set: validator_set_contract.address},
validator_set_contract.abi
)
|> Map.new(fn {staking_address, resp} -> {staking_address, resp.name} end)
pool_name_by_staking_address =
Map.merge(pool_name_by_staking_address_from_db, pool_name_by_staking_address_from_chain)
Map.new(pools, fn {data, pool_staking_address} ->
{:ok, estimate} = pools_gas_estimates[pool_staking_address]
data = Map.put(data, :gas_estimate, estimate)
name = pool_name_by_staking_address[String.downcase(to_string(pool_staking_address))]
data = Map.put(data, :name, name)
{pool_staking_address, data}
end)
else
%{}
end
{error, pools}
else
{error, %{}}
end
end
def recalc_claim_reward(socket, staking_contract_address, epochs, pool_staking_address, staker) do
:ets.insert(ContractState, {claim_reward_long_op_key(staker), true})
try do
json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments)
amounts_result =
ContractReader.call_get_reward_amount(
staking_contract_address,
epochs,
pool_staking_address,
staker,
json_rpc_named_arguments
)
{error, amounts} =
case amounts_result do
{:ok, amounts} ->
{nil, amounts}
{:error, reason} ->
{error_reason_to_string(reason), %{token_reward_sum: 0, native_reward_sum: 0}}
end
{error, gas_limit} =
if error == nil do
estimate_gas_result =
ContractReader.claim_reward_estimate_gas(
staking_contract_address,
epochs,
pool_staking_address,
staker,
json_rpc_named_arguments
)
case estimate_gas_result do
{:ok, gas_limit} ->
{nil, gas_limit}
{:error, reason} ->
{error_reason_to_string(reason), 0}
end
else
{error, 0}
end
token = ContractState.get(:token)
coin = get_coin()
push(socket, "claim_reward_recalculations", %{
token_reward_sum:
StakesHelpers.format_token_amount(amounts.token_reward_sum, token,
digits: token.decimals,
ellipsize: false,
symbol: false
),
native_reward_sum:
StakesHelpers.format_token_amount(amounts.native_reward_sum, coin,
digits: coin.decimals,
ellipsize: false,
symbol: false
),
gas_limit: gas_limit,
error: error
})
after
:ets.delete(ContractState, claim_reward_long_op_key(staker))
end
end
defp calc_apy(pool, staker, pool_staking_address_downcased, pool_reward, average_block_time, staking_epoch_duration) do
staker_address = String.downcase(to_string(staker.address_hash))
{reward_ratio, stake_amount} =
if staker_address == pool_staking_address_downcased do
{pool.snapshotted_validator_reward_ratio, pool.snapshotted_self_staked_amount}
else
{staker.snapshotted_reward_ratio, staker.snapshotted_stake_amount}
end
ContractState.calc_apy(
reward_ratio,
pool_reward,
stake_amount,
average_block_time,
staking_epoch_duration
)
end
defp claim_reward_long_op_active(socket) do
if socket.assigns[@claim_reward_long_op] do
true
else
staker = socket.assigns[:account]
with [{_, true}] <- :ets.lookup(ContractState, claim_reward_long_op_key(staker)) do
true
end
end
end
defp array_to_ranges(numbers, prev_ranges \\ []) do
length = Enum.count(numbers)
if length > 0 do
{first, last, next_index} = get_range(numbers)
prev_ranges_reversed = Enum.reverse(prev_ranges)
ranges =
[{first, last} | prev_ranges_reversed]
|> Enum.reverse()
if next_index == 0 || next_index >= length do
ranges
else
numbers
|> Enum.slice(next_index, length - next_index)
|> array_to_ranges(ranges)
end
else
[]
end
end
defp error_reason_to_string(reason) do
if is_map(reason) && Map.has_key?(reason, :message) && String.length(String.trim(reason.message)) > 0 do
reason.message
else
gettext("JSON RPC error") <> ": " <> inspect(reason)
end
end
defp get_range(numbers) do
last_index =
numbers
|> Enum.with_index()
|> Enum.find_index(fn {n, i} ->
if i > 0, do: n != Enum.at(numbers, i - 1) + 1, else: false
end)
next_index = if last_index == nil, do: Enum.count(numbers), else: last_index
first = Enum.at(numbers, 0)
last = Enum.at(numbers, next_index - 1)
{first, last, next_index}
end
defp push_contracts(socket) do
if socket.assigns[:contracts_sent] do
socket
else
token = ContractState.get(:token)
push(socket, "contracts", %{
staking_contract: ContractState.get(:staking_contract),
block_reward_contract: ContractState.get(:block_reward_contract),
validator_set_contract: ContractState.get(:validator_set_contract),
token_contract: ContractState.get(:token_contract),
token_decimals: to_string(token.decimals),
token_symbol: token.symbol
})
assign(socket, :contracts_sent, true)
end
end
defp claim_reward_long_op_key(staker) do
staker = if staker == nil, do: "", else: staker
Atom.to_string(@claim_reward_long_op) <> "_" <> staker
end
defp get_coin do
%Token{symbol: Explorer.coin(), decimals: Decimal.new(18)}
end
defp handle_in_render_claim_reward_result(
socket,
data,
staker,
staking_contract_address,
empty_staker,
empty_staking_contract_address
) do
cond do
claim_reward_long_op_active(socket) == true ->
{:reply, {:error, %{reason: gettext("Pools searching is already in progress for this address")}}, socket}
empty_staker ->
{:reply, {:error, %{reason: gettext("Unknown staker address. Please, choose your account in MetaMask")}},
socket}
empty_staking_contract_address ->
{:reply, {:error, %{reason: gettext("Unknown address of Staking contract. Please, contact support")}}, socket}
true ->
result =
if data["preload"] do
%{
html: View.render_to_string(StakesView, "_stakes_modal_claim_reward.html", %{}),
socket: socket
}
else
task = Task.async(__MODULE__, :find_claim_reward_pools, [socket, staker, staking_contract_address])
%{
html: "OK",
socket: assign(socket, @claim_reward_long_op, %{task: task, staker: staker})
}
end
{:reply, {:ok, %{html: result.html}}, result.socket}
end
end
defp handle_in_recalc_claim_reward_result(
socket,
epochs,
staking_contract_address,
pool_staking_address,
staker,
empty_pool_staking_address,
empty_staking_contract_address,
empty_staker
) do
cond do
claim_reward_long_op_active(socket) == true ->
{:reply, {:error, %{reason: gettext("Reward calculating is already in progress for this address")}}, socket}
Enum.empty?(epochs) ->
{:reply, {:error, %{reason: gettext("Staking epochs are not specified or not in the allowed range")}}, socket}
empty_pool_staking_address ->
{:reply, {:error, %{reason: gettext("Unknown pool staking address. Please, contact support")}}, socket}
empty_staker ->
{:reply, {:error, %{reason: gettext("Unknown staker address. Please, choose your account in MetaMask")}},
socket}
empty_staking_contract_address ->
{:reply, {:error, %{reason: gettext("Unknown address of Staking contract. Please, contact support")}}, socket}
true ->
task =
Task.async(__MODULE__, :recalc_claim_reward, [
socket,
staking_contract_address,
epochs,
pool_staking_address,
staker
])
socket = assign(socket, @claim_reward_long_op, %{task: task, staker: staker})
{:reply, {:ok, %{html: "OK"}}, socket}
end
end
end

@ -8,7 +8,6 @@ defmodule BlockScoutWeb.UserSocket do
channel("rewards:*", BlockScoutWeb.RewardChannel)
channel("transactions:*", BlockScoutWeb.TransactionChannel)
channel("tokens:*", BlockScoutWeb.TokenChannel)
channel("stakes:*", BlockScoutWeb.StakesChannel)
def connect(%{"locale" => locale}, socket) do
{:ok, assign(socket, :locale, locale)}

@ -1,314 +0,0 @@
defmodule BlockScoutWeb.StakesController do
use BlockScoutWeb, :controller
alias BlockScoutWeb.{Controller, StakesView}
alias Explorer.Chain
alias Explorer.Chain.{Cache.BlockNumber, Hash, Token}
alias Explorer.Counters.AverageBlockTime
alias Explorer.Staking.ContractState
alias Phoenix.View
alias Timex.Duration
import BlockScoutWeb.Chain, only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1]
def index(%{assigns: assigns} = conn, params) do
render_template(assigns.filter, conn, params)
end
# this is called when account in MetaMask is changed on client side (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, _)`)
def render_top(conn) do
staking_data =
case Map.fetch(conn.assigns, :staking_update_data) do
{:ok, data} ->
data
_ ->
%{
active_pools_length: ContractState.get(:active_pools_length, 0),
block_number: BlockNumber.get_max(),
epoch_end_block: ContractState.get(:epoch_end_block, 0),
epoch_number: ContractState.get(:epoch_number, 0),
max_candidates: ContractState.get(:max_candidates, 0)
}
end
token = ContractState.get(:token, %Token{})
account =
if account_address = conn.assigns[:account] do
account_address
|> Chain.get_total_staked_and_ordered()
|> Map.merge(%{
address: account_address,
balance: Chain.fetch_last_token_balance(account_address, token.contract_address_hash),
pool: Chain.staking_pool(account_address),
pool_id: conn.assigns[:pool_id]
})
end
epoch_end_in =
if staking_data.epoch_end_block - staking_data.block_number >= 0,
do: staking_data.epoch_end_block - staking_data.block_number,
else: 0
View.render_to_string(StakesView, "_stakes_top.html",
account: account,
block_number: staking_data.block_number,
candidates_limit_reached: staking_data.active_pools_length >= staking_data.max_candidates,
epoch_end_in: epoch_end_in,
epoch_number: staking_data.epoch_number,
token: token
)
end
# this is called when account in MetaMask is changed on client side
# or when UI periodically reloads the pool list (e.g. once per 10 blocks)
defp render_template(filter, conn, %{"type" => "JSON"} = params) do
{items, next_page_path} =
if Map.has_key?(params, "filterMy") do
[paging_options: options] = paging_options(params)
# turn off paging for Validators page as we sort by APY below
# and max number of validators is no more than 19
# (for the current POSDAO implementation)
options =
if is_page_unlimited?(filter) do
Map.put(options, :page_size, 1_000_000)
else
options
end
pools_plus_one =
Chain.staking_pools(
filter,
options,
unless params["account"] == "" do
params["account"]
end,
params["filterBanned"] == "true",
params["filterMy"] == "true"
)
{pools, next_page_path, last_index} = get_one_page(filter, conn, params, pools_plus_one)
average_block_time = AverageBlockTime.average_block_time()
average_block_time_seconds =
try do
Duration.to_seconds(average_block_time)
rescue
_ -> nil
end
staking_epoch_duration = ContractState.staking_epoch_duration()
token = ContractState.get(:token, %Token{})
epoch_number = ContractState.get(:epoch_number, 0)
staking_allowed = ContractState.get(:staking_allowed, false)
pool_rewards = ContractState.get(:pool_rewards, %{})
calc_apy_enabled = ContractState.calc_apy_enabled?()
snapshotted_delegator_data = snapshotted_delegator_data(filter, calc_apy_enabled)
pools =
pools
|> Enum.map(fn %{pool: pool} = item ->
apy =
if calc_apy_enabled and snapshotted_delegator_data !== nil do
calc_apy(
item.pool,
pool_rewards,
snapshotted_delegator_data,
average_block_time_seconds,
staking_epoch_duration
)
end
pool = Map.put(pool, :apy, apy)
Map.put(item, :pool, pool)
end)
# sort pools on Validators page by descending APY if all APYs known
pools = sort_pools_by_apy(pools)
items =
pools
|> Enum.with_index(last_index + 1)
|> Enum.map(fn {%{pool: pool, delegator: delegator}, index} ->
View.render_to_string(
StakesView,
"_rows.html",
token: token,
pool: pool,
delegator: delegator,
index: index,
average_block_time: average_block_time,
pools_type: filter,
buttons: staking_buttons(pool, delegator, staking_allowed, epoch_number)
)
end)
{items, next_page_path}
else
loading_item = View.render_to_string(StakesView, "_rows_loading.html", %{})
{[loading_item], nil}
end
json(
conn,
%{
items: items,
next_page_path: next_page_path
}
)
end
# this is called when the page is loaded for the first time
# or when it is reloaded by a user
defp render_template(filter, conn, _) do
token = ContractState.get(:token, %Token{})
render(conn, "index.html",
top: render_top(conn),
token: token,
pools_type: filter,
current_path: Controller.current_full_path(conn),
average_block_time: AverageBlockTime.average_block_time(),
refresh_interval: Application.get_env(:block_scout_web, BlockScoutWeb.Chain)[:staking_pool_list_refresh_interval]
)
end
defp staking_buttons(pool, delegator, staking_allowed, epoch_number) do
%{
stake: staking_allowed and stake_allowed?(pool, delegator),
move: staking_allowed and move_allowed?(delegator),
withdraw: staking_allowed and withdraw_allowed?(delegator),
claim: staking_allowed and claim_allowed?(delegator, epoch_number)
}
end
defp calc_apy(pool, pool_rewards, snapshotted_delegator_data, average_block_time, staking_epoch_duration) do
staking_address_str = String.downcase(Hash.to_string(pool.staking_address_hash))
mining_address_str = String.downcase(Hash.to_string(pool.mining_address_hash))
pool_reward =
case Map.fetch(pool_rewards, mining_address_str) do
{:ok, pool_reward} -> pool_reward
:error -> nil
end
case Map.fetch(snapshotted_delegator_data, staking_address_str) do
{:ok, data} ->
ContractState.calc_apy(
data.snapshotted_reward_ratio,
pool_reward,
data.snapshotted_stake_amount,
average_block_time,
staking_epoch_duration
)
:error ->
ContractState.calc_apy(
pool.snapshotted_validator_reward_ratio,
pool_reward,
pool.snapshotted_self_staked_amount,
average_block_time,
staking_epoch_duration
)
end
end
defp snapshotted_delegator_data(filter, calc_apy_enabled) do
if filter == :validator and calc_apy_enabled do
Chain.staking_pool_snapshotted_delegator_data_for_apy()
|> Enum.reduce(%{}, fn item, acc ->
staking_address_str = address_bytes_to_string(item.staking_address_hash)
Map.put(acc, staking_address_str, item)
end)
end
end
defp sort_pools_by_apy(pools) do
if Enum.all?(pools, fn item -> item.pool.apy !== nil end) do
Enum.sort(pools, fn item1, item2 -> item1.pool.apy.apy_raw >= item2.pool.apy.apy_raw end)
else
pools
end
end
defp address_bytes_to_string(hash), do: "0x" <> Base.encode16(hash, case: :lower)
defp get_one_page(filter, conn, params, pools_plus_one) do
last_index =
params
|> Map.get("position", "0")
|> String.to_integer()
{pools, next_page} =
if is_page_unlimited?(filter) do
{pools_plus_one, []}
else
split_list_by_page(pools_plus_one)
end
next_page_path =
case next_page_params(next_page, pools, params) do
nil ->
nil
next_page_params ->
updated_page_params =
next_page_params
|> Map.delete("type")
|> Map.put("position", last_index + 1)
next_page_path(filter, conn, updated_page_params)
end
{pools, next_page_path, last_index}
end
defp is_page_unlimited?(filter) do
filter == :validator
end
defp next_page_path(:validator, conn, params) do
validators_path(conn, :index, params)
end
defp next_page_path(:active, conn, params) do
active_pools_path(conn, :index, params)
end
defp next_page_path(:inactive, conn, params) do
inactive_pools_path(conn, :index, params)
end
defp stake_allowed?(pool, nil) do
Decimal.positive?(pool.self_staked_amount)
end
defp stake_allowed?(pool, delegator) do
Decimal.positive?(pool.self_staked_amount) or delegator.address_hash == pool.staking_address_hash
end
defp move_allowed?(nil), do: false
defp move_allowed?(delegator) do
Decimal.positive?(delegator.max_withdraw_allowed)
end
defp withdraw_allowed?(nil), do: false
defp withdraw_allowed?(delegator) do
Decimal.positive?(delegator.max_withdraw_allowed) or
Decimal.positive?(delegator.max_ordered_withdraw_allowed) or
Decimal.positive?(delegator.ordered_withdraw)
end
defp claim_allowed?(nil, _epoch_number), do: false
defp claim_allowed?(delegator, epoch_number) do
Decimal.positive?(delegator.ordered_withdraw) and delegator.ordered_withdraw_epoch < epoch_number
end
end

@ -1,26 +0,0 @@
defmodule BlockScoutWeb.StakingEventHandler do
@moduledoc """
Subscribing process for broadcast events from staking app.
"""
use GenServer
alias BlockScoutWeb.Endpoint
alias Explorer.Chain.Events.Subscriber
def start_link(_) do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
@impl true
def init([]) do
Subscriber.to(:staking_update, :realtime)
{:ok, []}
end
@impl true
def handle_info({:chain_event, :staking_update, :realtime, data}, state) do
Endpoint.broadcast("stakes:staking_update", "staking_update", data)
{:noreply, state}
end
end

@ -1,4 +1,3 @@
<% staking_enabled_in_menu = Application.get_env(:block_scout_web, BlockScoutWeb.Chain)[:staking_enabled_in_menu] %>
<% apps_menu = Application.get_env(:block_scout_web, :apps_menu) %>
<nav class="navbar navbar-dark navbar-expand-lg navbar-primary" data-selector="navbar" id="top-navbar">
<div class="container-fluid navbar-container">
@ -142,7 +141,7 @@
</div>
</li>
<% end %>
<%= if apps_menu == true || staking_enabled_in_menu do %>
<%= if apps_menu == true do %>
<li class="nav-item dropdown">
<a href="#" role="button" id="navbarAppsDropdown" class="nav-link topnav-nav-link dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="nav-link-icon">
@ -155,12 +154,6 @@
<% end %>
</a>
<div class="dropdown-menu" aria-labeledby="navbarAppsDropdown">
<%= if staking_enabled_in_menu do %>
<a class="dropdown-item <%= #{tab_status("validators", @conn.request_path)} %>" href="<%= validators_path(@conn, :index) %>">
<%= gettext("Staking") %>
<span class="bs-label secondary right from-dropdown">New</span>
</a>
<% end %>
<%= if apps_menu == true do %>
<%= for %{url: url, title: title} <- external_apps_list() do %>
<a href="<%= url %>" class="dropdown-item" target="_blank"><%= title %>

@ -11,9 +11,6 @@
<link rel="preload" href="<%= static_path(@conn, "/js/chain.js") %>" as="script">
<link rel="preload" href="<%= static_path(@conn, "/js/chart-loader.js") %>" as="script">
<link rel="preload" href="<%= static_path(@conn, "/js/token-transfers-toggle.js") %>" as="script">
<% Elixir.BlockScoutWeb.StakesView -> %>
<link rel="stylesheet" href="<%= static_path(@conn, "/css/staking.css") %>">
<link rel="stylesheet" href="<%= static_path(@conn, "/css/app.css") %>">
<% _ -> %>
<link rel="stylesheet" href="<%= static_path(@conn, "/css/app.css") %>">
<% end %>

@ -1,15 +0,0 @@
<section class="container js-stakes-welcome-alert d-none">
<div class="card">
<div class="card-body card-body-flex-column-space-between" style="padding-left: 50px;">
<button type="button" class="stakes-btn-close-alert js-stakes-btn-close-welcome-alert">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18">
<path fill="#ddd" fill-rule="evenodd" d="M10.435 8.983l7.261 7.261a1.027 1.027 0 1 1-1.452 1.452l-7.261-7.261-7.262 7.262a1.025 1.025 0 1 1-1.449-1.45l7.262-7.261L.273 1.725A1.027 1.027 0 1 1 1.725.273l7.261 7.261 7.23-7.231a1.025 1.025 0 1 1 1.449 1.45l-7.23 7.23z"></path>
</svg>
</button>
<span style="font-size: 1.3em;">
<%= render BlockScoutWeb.CommonComponentsView, "_info.html", extra_class: "ml-1" %>
Participate in Proof-of-Stake consensus on the xDai chain as a delegator or validator. To start you will need STAKE on the xDai network. <a href="https://www.xdaichain.com/for-stakers/staking-protocol" target="_blank">Learn More</a>
</span>
</div>
</div>
</section>

@ -1,7 +0,0 @@
<title>
<%= gettext("%{subnetwork} Staking DApp - BlockScout", subnetwork: BlockScoutWeb.LayoutView.subnetwork_title()) %>
</title>
<%= if assigns[:token] do %>
<meta name="keywords" content="<%= "#{BlockScoutWeb.LayoutView.subnetwork_title()} #{@token.symbol} staking app" %>">
<meta name="description" content="<%= gettext "DApp for Staking %{symbol} tokens", symbol: @token.symbol %>">
<% end %>

@ -1,77 +0,0 @@
<div data-identifier-pool="<%= @pool.staking_address_hash %>" class="row <%= if @pool.is_banned, do: "stakes-tr-banned" %>">
<div class="col-1 stakes-td stakes-cell"><div class="stakes-td-order"><%= @index %></div></div>
<div class="col-2 stakes-td stakes-cell">
<%=
tooltip = if @pool.is_validator, do: gettext("This is a validator"), else: false
render BlockScoutWeb.StakesView,
"_stakes_address.html",
pool: @pool,
tooltip: tooltip,
index: @index
%>
</div>
<div class="col-2 stakes-td stakes-cell">
<%=
render BlockScoutWeb.CommonComponentsView,
"_progress_from_to.html",
from: format_token_amount(@pool.self_staked_amount, @token, digits: 0, ellipsize: false, symbol: false),
to: format_token_amount(@pool.total_staked_amount, @token, digits: 0, ellipsize: false, symbol: false),
progress: amount_ratio(@pool)
%>
</div>
<div class="col-2 stakes-td stakes-cell">
<%= if @pools_type == :inactive do %>
<%= if @pool.is_banned, do: gettext("Yes"), else: gettext("No") %>
<% else %>
<%= if @pool.is_active, do: "#{@pool.stakes_ratio}%", else: gettext("(inactive pool)") %>
<% end %>
</div>
<%= if @pools_type == :validator do %>
<div class="col-1 stakes-td stakes-cell">
<%= if @pool.apy do %>
<%= @pool.apy.apy %>
<% else %>
<%= gettext("N/A") %>
<% end %>
</div>
<% end %>
<div class="col-2 stakes-td stakes-cell">
<span class="stakes-td-link-style js-delegators-list" data-address="<%= @pool.staking_address_hash %>">
<%= @pool.delegators_count %>
</span>
</div>
<div class="<%= if @pools_type == :validator do %>col-2<% else %>col-3<% end %> stakes-td stakes-cell justify-content-end">
<%= if @pool.is_banned do %>
<span class="stakes-td-banned-info">
<%= gettext("Banned until block #%{banned_until} (%{estimated_unban_day})", banned_until: @pool.banned_until, estimated_unban_day: estimated_unban_day(@pool.banned_until, @average_block_time)) %>
<%= if @delegator &&
@delegator.address_hash != @pool.staking_address_hash &&
@pool.are_delegators_banned &&
@pool.banned_until != @pool.banned_delegators_until do %>
<%= raw(".<br />") %>
<%= gettext("Withdraw after block #%{banned_delegators_until} (%{estimated_unban_day})", banned_delegators_until: @pool.banned_delegators_until, estimated_unban_day: estimated_unban_day(@pool.banned_delegators_until, @average_block_time)) %>
<% end %>
</span>
<% end %>
<%= if !@pool.is_banned ||
(!@pool.are_delegators_banned &&
@delegator &&
@delegator.address_hash != @pool.staking_address_hash
) do %>
<div class="stakes-controls">
<%= if @buttons.move do %>
<%= render BlockScoutWeb.StakesView, "_stakes_control_move.html", address: @pool.staking_address_hash %>
<% end %>
<%= if @buttons.claim do %>
<%= render BlockScoutWeb.StakesView, "_stakes_control_claim.html", address: @pool.staking_address_hash %>
<% end %>
<%= if @buttons.withdraw do %>
<%= render BlockScoutWeb.StakesView, "_stakes_control_withdraw.html", address: @pool.staking_address_hash %>
<% end %>
<%= if @buttons.stake do %>
<%= render BlockScoutWeb.StakesView, "_stakes_control_stake.html", address: @pool.staking_address_hash %>
<% end %>
</div>
<% end %>
</div>
</div>

@ -1,8 +0,0 @@
<div class="stakes-address-container js-pool-info" data-address="<%= to_string(@pool.staking_address_hash) %>">
<span class="stakes-address stakes-address-active">
<%= if is_nil(@pool.name), do: BlockScoutWeb.AddressView.trimmed_hash(@pool.staking_address_hash), else: @pool.name %>
</span>
<%= if @tooltip do %>
<%= render BlockScoutWeb.CommonComponentsView, "_check_tooltip.html", text: @tooltip %>
<% end %>
</div>

@ -1,6 +0,0 @@
<button class="btn-full-primary js-claim-reward <%= if assigns[:extra_class] do @extra_class end %>">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
<path fill-rule="evenodd" d="M15 16H1a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1zm-1-3H2v1h12v-1zm-3.754-8.279L9 3.479V8a1 1 0 0 1-2 0V3.489l-1.235 1.23a1.042 1.042 0 0 1-1.469 0 1.032 1.032 0 0 1 0-1.464L7.235.326a1.041 1.041 0 0 1 1.137-.22c.007.003.012.01.019.013.144.049.28.122.394.236l2.921 2.911a1.027 1.027 0 0 1 0 1.455 1.034 1.034 0 0 1-1.46 0z"/>
</svg>
<span class="btn-full-primary-text"><%= @text %></span>
</button>

@ -1,28 +0,0 @@
<button
class="btn-full-primary <%= if assigns[:extra_class] do @extra_class end %>"
<%= if assigns[:disabled] do "disabled" end %>
>
<svg
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns="http://www.w3.org/2000/svg"
xmlns:cc="http://web.resource.org/cc/"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
xml:space="preserve"
enable-background="new 0 0 65 65"
y="0px"
x="0px"
viewBox="0 0 65 65"
width="16"
height="16"
>
<g fill="#555753">
<path d="m32.5 4.999c-5.405 0-10.444 1.577-14.699 4.282l-5.75-5.75v16.11h16.11l-6.395-6.395c3.18-1.787 6.834-2.82 10.734-2.82 12.171 0 22.073 9.902 22.073 22.074 0 2.899-0.577 5.664-1.599 8.202l4.738 2.762c1.47-3.363 2.288-7.068 2.288-10.964 0-15.164-12.337-27.501-27.5-27.501z" />
<path d="m43.227 51.746c-3.179 1.786-6.826 2.827-10.726 2.827-12.171 0-22.073-9.902-22.073-22.073 0-2.739 0.524-5.35 1.439-7.771l-4.731-2.851c-1.375 3.271-2.136 6.858-2.136 10.622 0 15.164 12.336 27.5 27.5 27.5 5.406 0 10.434-1.584 14.691-4.289l5.758 5.759v-16.112h-16.111l6.389 6.388z" />
</g>
</svg>
<span class="btn-full-primary-text"><%= @text %></span>
</button>

@ -1,6 +0,0 @@
<span class="stakes-btn-remove-pool js-remove-pool <%= if assigns[:extra_class] do @extra_class end %>">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14">
<path fill-rule="evenodd" d="M13 5h-1v8a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V5H1a1 1 0 0 1 0-2h3V1a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2h3a1 1 0 0 1 0 2zM8 2H6v1h2V2zm2 3H4v7h6V5z"/>
</svg>
<span class="stakes-btn-remove-pool-text"><%= @text %></span>
</span>

@ -1,9 +0,0 @@
<button
class="btn-full-primary <%= if assigns[:extra_class] do @extra_class end %>"
<%= if assigns[:disabled] do "disabled" end %>
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
<path fill-rule="evenodd" d="M15 16H1a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1zm-1-3H2v1h12v-1zM8.785 8.675c-.114.114-.25.187-.394.237-.007.003-.012.01-.02.013a1.036 1.036 0 0 1-1.136-.221L4.296 5.765a1.038 1.038 0 1 1 1.469-1.469L7 5.531V1a1 1 0 0 1 2 0v4.54l1.246-1.246a1.033 1.033 0 0 1 1.46 1.46L8.785 8.675z"/>
</svg>
<span class="btn-full-primary-text"><%= @text %></span>
</button>

@ -1,9 +0,0 @@
<button
class="btn-full-primary <%= if assigns[:extra_class] do @extra_class end %>"
<%= if assigns[:disabled] do "disabled" end %>
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
<path fill-rule="evenodd" d="M15 16H1a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1zm-1-3H2v1h12v-1zm-3.754-8.279L9 3.479V8a1 1 0 0 1-2 0V3.489l-1.235 1.23a1.042 1.042 0 0 1-1.469 0 1.032 1.032 0 0 1 0-1.464L7.235.326a1.041 1.041 0 0 1 1.137-.22c.007.003.012.01.019.013.144.049.28.122.394.236l2.921 2.911a1.027 1.027 0 0 1 0 1.455 1.034 1.034 0 0 1-1.46 0z"/>
</svg>
<span class="btn-full-primary-text"><%= @text %></span>
</button>

@ -1,5 +0,0 @@
<span class="stakes-control js-claim-withdrawal <%= if assigns[:extra_class] do @extra_class end %>" data-address="<%= to_string(@address) %>" data-placement="top" data-toggle="tooltip" title="Claim Withdrawal">
<svg class="stakes-control-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16">
<path fill-rule="evenodd" d="M15 16H1a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1zm-1-3H2v1h12v-1zm-3.235-8.312c-.008.008-.019.01-.027.018L9.722 5.718c-.008.009-.01.019-.018.027L6.765 8.674a1.036 1.036 0 0 1-1.156.207 1.008 1.008 0 0 1-.394-.236L2.294 5.734a1.027 1.027 0 0 1 0-1.455 1.036 1.036 0 0 1 1.46 0l2.241 2.234 2.24-2.232c.008-.008.019-.01.027-.018l1.016-1.012c.007-.008.01-.019.018-.027L12.235.295a1.042 1.042 0 0 1 1.469 0c.406.404.406 1.06 0 1.464l-2.939 2.929z"/>
</svg>
</span>

@ -1,5 +0,0 @@
<span class="stakes-control js-move-stake <%= if assigns[:extra_class] do @extra_class end %>" data-address="<%= to_string(@address) %>" data-placement="top" data-toggle="tooltip" title="Move">
<svg class="stakes-control-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16">
<path fill-rule="evenodd" d="M15 16H1a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1h4.347l1.778 2H2v1h12v-1H8.875l1.778-2H15a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1zm0-11H9v3.532l1.252-1.253a1.028 1.028 0 1 1 1.455 1.456l-2.909 2.91c-.038.038-.087.054-.129.085a.945.945 0 0 1-.653.27h-.032a.971.971 0 0 1-.713-.315c-.006-.005-.013-.006-.018-.011L4.326 8.745A1.036 1.036 0 0 1 5.79 7.281L7 8.492V5H1a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1zm-1-3H2v1h12V2z"/>
</svg>
</span>

@ -1,5 +0,0 @@
<span class="stakes-control js-make-stake <%= if assigns[:extra_class] do @extra_class end %>" data-address="<%= to_string(@address) %>" data-placement="top" data-toggle="tooltip" title="Stake">
<svg class="stakes-control-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16">
<path fill-rule="evenodd" d="M15 16H1a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1zm-1-3H2v1h12v-1zM8.785 8.675c-.114.114-.25.187-.394.237-.007.003-.012.01-.02.013a1.035 1.035 0 0 1-1.136-.221L4.296 5.765a1.038 1.038 0 1 1 1.469-1.469L7 5.531V1a1 1 0 0 1 2 0v4.54l1.246-1.246a1.033 1.033 0 0 1 1.46 1.46L8.785 8.675z"/>
</svg>
</span>

@ -1,5 +0,0 @@
<span class="stakes-control js-withdraw-stake <%= if assigns[:extra_class] do @extra_class end %>" data-address="<%= to_string(@address) %>" data-placement="top" data-toggle="tooltip" title="Withdraw">
<svg class="stakes-control-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16">
<path fill-rule="evenodd" d="M15 16H1a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1zm-1-3H2v1h12v-1zm-3.754-8.279L9 3.479V8a1 1 0 0 1-2 0V3.488L5.765 4.719a1.042 1.042 0 0 1-1.469 0 1.032 1.032 0 0 1 0-1.464L7.235.326a1.04 1.04 0 0 1 1.137-.22c.007.003.012.01.019.013.144.049.28.122.394.236l2.921 2.911a1.027 1.027 0 0 1 0 1.455 1.036 1.036 0 0 1-1.46 0z"/>
</svg>
</span>

@ -1,15 +0,0 @@
<div class="stakes-empty-content">
<div class="stakes-empty-content-pic">
<svg class="stakes-empty-content-pic-svg" xmlns="http://www.w3.org/2000/svg" width="94" height="121">
<path class="stakes-empty-content-pic-svg-path" fill-rule="evenodd" d="M40 1.47l48 27.759c3.314 1.916 6 6.156 6 9.47v57.999c0 3.314-2.686 4.447-6 2.531L40 71.47c-3.314-1.916-6-6.156-6-9.47V4c0-3.314 2.686-4.446 6-2.53z" opacity=".2"/>
<path class="stakes-empty-content-pic-svg-path" fill-rule="evenodd" d="M23 11.47l48 27.759c3.314 1.916 6 6.156 6 9.47v58c0 3.313-2.686 4.446-6 2.53L23 81.47c-3.314-1.917-6-6.156-6-9.47V14c0-3.314 2.686-4.446 6-2.53z" opacity=".6"/>
<path class="stakes-empty-content-pic-svg-path" fill-rule="evenodd" d="M6 21.47l48 27.759c3.314 1.916 6 6.156 6 9.469v58.001c0 3.313-2.686 4.446-6 2.53L6 91.47C2.686 89.553 0 85.314 0 82V24c0-3.314 2.686-4.447 6-2.53z"/>
<path fill="#FFF" fill-rule="evenodd" d="M39 78.814l-9-5.18v11c0 1.104-.895 1.484-2 .849-1.105-.636-2-2.047-2-3.152v-11l-9-5.18c-1.105-.636-2-2.046-2-3.151s.895-1.485 2-.849l9 5.18v-11c0-1.104.895-1.484 2-.848 1.105.635 2 2.046 2 3.151v11l9 5.18c1.105.636 2 2.047 2 3.151 0 1.105-.895 1.485-2 .849z"/>
</svg>
</div>
<div class="stakes-empty-content-info">
<h1 class="stakes-empty-content-title"><%= gettext("No Information") %></h1>
<p class="stakes-empty-content-text"><%= gettext("There is no information currently available for this view. Deselect filters or choose another pool view to see current info. To participate as a delegator, select a pool address and click the Stake icon. To become a candidate, click the button below.") %></p>
<%= render BlockScoutWeb.CommonComponentsView, "_btn_add_line.html", text: gettext("Become a Candidate"), extra_class: "js-become-candidate" %>
</div>
</div>

@ -1,40 +0,0 @@
<div class="modal modal-fullwidth-xs fade" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-become-candidate" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><%= gettext("Become a Candidate") %></h5>
</div>
<%= render BlockScoutWeb.CommonComponentsView, "_modal_close_button.html" %>
<div class="modal-body">
<form>
<%= 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 %>
<%= 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") %>
<%= render BlockScoutWeb.CommonComponentsView, "_input_group.html", id: "pool-name", classes: "form-group", input_classes: "form-control", attributes: "pool-name", type: "text", placeholder: gettext("Your Pool Name") %>
<%= render BlockScoutWeb.CommonComponentsView, "_input_group.html", id: "pool-description", classes: "form-group", input_classes: "form-control", attributes: "pool-description", type: "text", placeholder: gettext("Your Pool Short Description (optional)") %>
<p class="form-p m-b-0"><%= gettext("Minimum Stake:") %>
<span class="text-dark">
<%= format_token_amount(@min_candidate_stake, @token) %>
</span>
</p>
<p class="form-p"><%= gettext("Your Balance:") %>
<%= if @balance >= @min_candidate_stake do %>
<span class="text-dark link-dotted" data-available-amount="<%= from_wei(@balance, @token) %>">
<%= format_token_amount(@balance, @token) %>
</span>
<% else %>
<span class="text-dark">
<%= format_token_amount(@balance, @token) %>
</span>
<% end %>
</p>
<div class="form-buttons"><%= render BlockScoutWeb.CommonComponentsView, "_btn_add_full.html", text: gettext("Become a Candidate"), extra_class: "full-width" %></div>
</form>
</div>
<%= render BlockScoutWeb.CommonComponentsView, "_modal_bottom_disclaimer.html", text: gettext("<p>To become a candidate, your staking address must be funded with %{tokenSymbol} tokens <strong>and</strong> %{coinSymbol} coins, and your OpenEthereum node must be active and configured with the mining address you specify here.</p>
<p>To become a delegator, close this window and select an address from the list of pools you would like to place stake on. Click the <strong>Stake</strong> button next to the address to begin the process.</p>", tokenSymbol: @token.symbol, coinSymbol: @coin.symbol) |> raw() %>
</div>
</div>
</div>

@ -1,14 +0,0 @@
<div class="modal modal-fullwidth-xs fade" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-claim-reward" role="document">
<div class="modal-content">
<%= render BlockScoutWeb.CommonComponentsView, "_modal_close_button.html" %>
<div class="modal-header">
<h5 class="modal-title"><%= gettext("Claim Reward") %></h5>
</div>
<div class="modal-body">
<p><%= gettext("Searching for pools you have ever staked into. Please, wait...") %></p>
</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>

@ -1,72 +0,0 @@
<%= if map_size(@pools) > 0 do %>
<p class="form-p"><%= gettext("We found the following pools you can claim reward from:") %></p>
<form>
<div class="input-group form-group">
<select class="form-control">
<option disabled="disabled" selected="selected"><%= gettext("Choose Pool") %></option>
<%= for {pool_staking_address, data} <- @pools do %>
<%
token_reward_sum = format_token_amount(data.token_reward_sum, @token, digits: @token.decimals, ellipsize: false, symbol: false)
token_reward_sum_short = 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: @coin.decimals, ellipsize: false, symbol: false)
native_reward_sum_short = 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 %>" data-gas-limit="<%= data.gas_estimate %>">
<%=
if String.valid?(data.name) and String.length(String.trim(data.name)) > 0 do
data.name
else
BlockScoutWeb.AddressView.trimmed_hash(pool_staking_address)
end
<>
" (" <>
token_reward_sum_short <> " " <> @token.symbol <>
"; " <>
native_reward_sum_short <> " " <> @coin.symbol <>
")"
%>
</option>
<% end %>
</select>
</div>
<div class="selected-pool-info hidden">
<p class="form-p"><%= gettext("The staking epochs for which the reward could be claimed (read-only field):") %></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="form-group amounts">
<p class="form-p" align="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 />
</p>
<p class="form-p" align="right">
<%= gettext("Tx Gas Limit:") %><br />
<span class="text-dark" id="tx-gas-limit"></span><br />
</p>
</div>
<div class="input-group">
<%= render BlockScoutWeb.StakesView, "_stakes_btn_recalculate.html", text: gettext("Recalculate"), extra_class: "full-width recalculate hidden" %>
<%= render BlockScoutWeb.StakesView, "_stakes_btn_withdraw.html", text: gettext("Claim Reward"), extra_class: "full-width submit" %>
<div class="input-group-message"></div>
</div>
</div>
</form>
<% else %>
<p>
<%= if @error do %>
<%= @error %>
<% else %>
<%= gettext("Unable to find any pools you could claim a reward from.") %>
<% end %>
</p>
<% end %>

@ -1,29 +0,0 @@
<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-content">
<%= render BlockScoutWeb.CommonComponentsView, "_modal_close_button.html" %>
<div class="modal-stake-two-cols">
<div class="modal-header">
<h5 class="modal-title"><%= gettext("Claim Ordered Withdraw") %></h5>
</div>
<div class="modal-body">
<form>
<p class="form-p"><%= gettext("Your ordered amount") %></p>
<p class="form-p">
<span class="text-dark">
<%= format_token_amount(@delegator.ordered_withdraw, @token) %>
</span>
</p>
<div class="form-buttons">
<%= render BlockScoutWeb.StakesView, "_stakes_btn_withdraw.html", text: gettext("Claim the Amount"), extra_class: "full-width btn-add-full" %>
</div>
</form>
</div>
<%= render BlockScoutWeb.CommonComponentsView, "_modal_bottom_disclaimer.html", text: gettext("Withdrawal orders made during an active staking epoch are available to claim after the epoch is complete.") |> raw(), extra_class: "b-b-r-0" %>
<div class="modal-stake-right">
<%= render BlockScoutWeb.StakesView, "_stakes_progress.html", pool: @pool, token: @token %>
</div>
</div>
</div>
</div>
</div>

@ -1,197 +0,0 @@
<div class="modal modal-fullwidth-xs fade" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-delegators-info" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<%= gettext("Delegators of ") %>
<%= if not is_nil(@pool.name), do: @pool.name, else: BlockScoutWeb.AddressView.trimmed_hash(@pool.staking_address_hash) %>
</h5>
</div>
<%= render BlockScoutWeb.CommonComponentsView, "_modal_close_button.html" %>
<div class="modal-body">
<div class="stakes-table-container">
<div class="stakes-table-head">
<div class="col-1"></div>
<div class="<%= if @pool.is_validator, do: "col-2", else: "col-4" %>">
<%=
pool_type = cond do
@pool.is_validator -> gettext("validator")
@pool.is_active -> gettext("candidate")
true -> gettext("pool owner")
end
render(
BlockScoutWeb.StakesView,
"_stakes_th.html",
title: gettext("Staker's Address"),
tooltip: gettext("All pool participant addresses. The top address belongs to the %{pool_type}.", pool_type: pool_type)
)
%>
</div>
<div class="<%= if @pool.is_validator, do: "col-3", else: "col-4" %>">
<%=
title =
if @show_snapshotted_data do
gettext("Current Stake Amount") <> "<br />(" <> gettext("Working Stake Amount") <> ")"
else
gettext("Current Stake Amount")
end
tooltip =
gettext("Amount of %{symbol} placed by an address.", symbol: @token.symbol) <>
if @show_snapshotted_data do
gettext(" Working Stake Amount is an amount which is accounted and working at the current staking epoch.")
else
""
end
render BlockScoutWeb.StakesView, "_stakes_th.html", title: title, tooltip: tooltip
%>
</div>
<div class="col-3">
<%=
title =
if @show_snapshotted_data do
gettext("Potential Reward Share") <> "<br />(" <> gettext("Current Reward Share") <> ")"
else
gettext("Potential Reward Share")
end
tooltip =
gettext("Reward distribution is based on stake amount. Validator receives at least %{min}% of the pool reward.", min: @validator_min_reward_percent) <>
if @show_snapshotted_data do
" " <> gettext("Current Reward Share is calculated based on the Working Stake Amount.")
else
""
end
render BlockScoutWeb.StakesView,
"_stakes_th.html",
title: title,
tooltip: tooltip
%>
</div>
<%= if @pool.is_validator do %>
<div class="col-3">
<%=
render BlockScoutWeb.StakesView,
"_stakes_th.html",
title: gettext("APY & Predicted Reward"),
tooltip: gettext("Approximate Current Annual Percentage Yield. If you see N/A, please reopen the popup in a few blocks (APY cannot be calculated at the very beginning of a staking epoch). Predicted Reward is the amount of %{symbol} a participant will receive for staking and can claim once the current epoch ends.", symbol: @token.symbol)
%>
</div>
<% end %>
</div>
<div class="stakes-table-body">
<%= for {staker, index} <- Enum.with_index(@stakers, 1) do %>
<div class="row">
<div class="col-1 stakes-td stakes-cell"><div class="stakes-td-order"><%= index %></div></div>
<div class="<%= if @pool.is_validator, do: "col-2", else: "col-4" %> stakes-td stakes-cell">
<div class="stakes-address-container">
<span class="stakes-address">
<%=
link(
BlockScoutWeb.AddressView.trimmed_hash(staker.address_hash),
to: address_path(@conn, :show, staker.address_hash),
target: "_blank"
)
%>
</span>
<%= if staker.address_hash == @pool.staking_address_hash do %>
<%=
tooltip_text = gettext("This is a %{pool_type}.", pool_type: pool_type)
tooltip_text = if Enum.count(@stakers) > 1 do
tooltip_text <> " " <> gettext("The rest addresses are delegators of its pool.")
else
tooltip_text
end
render(
BlockScoutWeb.CommonComponentsView,
"_check_tooltip.html",
text: tooltip_text
)
%>
<% end %>
<%= if to_string(staker.address_hash) == @account do %>
<div
class="me-tooltip"
data-boundary="window"
data-container="body"
data-html="true"
data-placement="top"
data-toggle="tooltip"
title="<%= gettext("It's me!") %>"
>
<%= gettext("ME") %>
</div>
<% end %>
</div>
</div>
<div class="<%= if @pool.is_validator, do: "col-3", else: "col-4" %> stakes-td stakes-cell">
<%= format_token_amount(staker.stake_amount, @token, symbol: false) %>
<%= if @show_snapshotted_data do %>
(
<%=
if staker.snapshotted_stake_amount do
format_token_amount(staker.snapshotted_stake_amount, @token, symbol: false)
else
0
end
%>
)
<% end %>
</div>
<div class="col-3 stakes-td stakes-cell">
<%
reward =
if staker.address_hash == @pool.staking_address_hash do
%{
reward_ratio: @pool.validator_reward_ratio,
snapshotted_reward_ratio: @pool.snapshotted_validator_reward_ratio
}
else
%{
reward_ratio: staker.reward_ratio,
snapshotted_reward_ratio: staker.snapshotted_reward_ratio
}
end
%>
<%= if reward.reward_ratio do %>
<%= reward.reward_ratio %>%
<%= if @show_snapshotted_data do %>
(
<%=
if reward.snapshotted_reward_ratio do
"#{reward.snapshotted_reward_ratio}%"
else
"0%"
end
%>
)
<% end %>
<% else %>
-
<% end %>
</div>
<%= if @pool.is_validator do %>
<div class="col-3 stakes-td stakes-cell">
<%= cond do %>
<% staker.apy -> %>
<%= staker.apy.apy %> (<%= format_token_amount(staker.apy.predicted_reward, @token, symbol: false, digits: 2) %>)
<% @show_snapshotted_data and staker.snapshotted_stake_amount == nil -> %>
<%= gettext("Pending") %>
<% true -> %>
<%= gettext("N/A") %>
<% end %>
</div>
<% end %>
</div>
<% end %>
</div>
</div>
</div>
</div>
</div>
</div>

@ -1,73 +0,0 @@
<div class="modal modal-fullwidth-xs fade" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered <%= if @pool_to do "modal-stake-move" else "modal-stake" end %>" role="document">
<div class="modal-content">
<%= render BlockScoutWeb.CommonComponentsView, "_modal_close_button.html" %>
<div class="modal-stake-three-cols">
<div class="modal-stake-left">
<%= render BlockScoutWeb.StakesView, "_stakes_progress.html", pool: @pool_from, pool_label: gettext("Source Pool"), token: @token, extra_class: "js-pool-from-progress" %>
</div>
<div class="modal-header">
<h5 class="modal-title"><%= gettext("Move Stake") %></h5>
</div>
<div class="modal-body">
<form>
<%= 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 %>
<p class="form-p"><%= gettext("You Staked:") %>
<span class="text-dark">
<%= format_token_amount(@delegator_from.stake_amount, @token) %>
</span>
</p>
<div class="input-group form-group">
<select pool-select class="form-control">
<option disabled <%= unless @pool_to do "selected" end %>><%= gettext("Choose Target Pool") %></option>
<%= for %{pool: pool} <- @pools,
pool.staking_address_hash != @pool_from.staking_address_hash,
Decimal.positive?(pool.self_staked_amount) or pool.staking_address_hash == @delegator_from.address_hash do %>
<option
value="<%= pool.staking_address_hash %>"
<%= if @pool_to && pool.staking_address_hash == @pool_to.staking_address_hash do "selected" end %>
>
<%= if is_nil(pool.name), do: BlockScoutWeb.AddressView.trimmed_hash(pool.staking_address_hash), else: pool.name %>
</option>
<% end %>
</select>
</div>
<%= if @delegator_to && Decimal.positive?(@delegator_to.stake_amount) do %>
<p class="form-p m-b-0"><%= gettext("Current Stake:") %>
<span class="text-dark">
<%= format_token_amount(@delegator_to.stake_amount, @token) %>
</span>
</p>
<% end %>
<p class="form-p"><%= gettext("Max Amount to Move:") %>
<%= if @pool_to && Decimal.positive?(@delegator_from.max_withdraw_allowed) do %>
<span class="text-dark link-dotted" data-available-amount="<%= from_wei(@delegator_from.max_withdraw_allowed, @token) %>">
<%= format_token_amount(@delegator_from.max_withdraw_allowed, @token) %>
</span>
<% else %>
<span class="text-dark">
<%= format_token_amount(@delegator_from.max_withdraw_allowed, @token) %>
</span>
<% end %>
</p>
<div class="form-buttons">
<%=
render BlockScoutWeb.StakesView,
"_stakes_btn_stake.html",
text: gettext("Move Stake"),
extra_class: "full-width btn-add-full",
disabled: true
%>
</div>
</form>
</div>
<%= render BlockScoutWeb.CommonComponentsView, "_modal_bottom_disclaimer.html", text: gettext("Stake placed on a candidate pool or an active validator pool <strong>during the current staking epoch</strong> can be moved from one pool to another. Active stake cannot be moved. To re-delegate active stake: order a withdrawal, claim the amount on the next staking epoch, and stake the amount on a different pool.") |> raw(), extra_class: "b-b-l-0 #{if @pool_to do "b-b-r-0" end}" %>
<%= if @pool_to do %>
<div class="modal-stake-right">
<%= render BlockScoutWeb.StakesView, "_stakes_progress.html", pool: @pool_to, pool_label: gettext("Target Pool"), token: @token, extra_class: "js-pool-to-progress" %>
</div>
<% end %>
</div>
</div>
</div>
</div>

@ -1,95 +0,0 @@
<div class="modal modal-fullwidth-xs fade" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-validator-info" role="document">
<div class="modal-content">
<div class="modal-header">
<div>
<%= if @delegator && @delegator.address_hash == @validator.staking_address_hash do %>
<div>
<input type="text" id="pool_name" value="<%= @validator.name %>" placeholder="<%= gettext("Pool name") %>" maxlength="256" />
<textarea id="pool_description" placeholder="<%= gettext("Pool description") %>" maxlength="1024"><%= @validator.description %></textarea>
<p id="save_pool_metadata_container"><a href="#" id="save_pool_metadata"><%= gettext("Save changes") %></a></p>
<p style="display:none" id="waiting_message"><%= gettext("Please, sign transaction and wait for its mining...") %></p>
</div>
<% else %>
<h5 class="modal-title"><%= if not is_nil(@validator.name), do: @validator.name, else: BlockScoutWeb.AddressView.trimmed_hash(@validator.staking_address_hash) %></h5>
<%= if not is_nil(@validator.description) do %>
<p><%= @validator.description %></p>
<% end %>
<% end %>
<div>
<span class="modal-validator-info-item-title"><%= gettext("Staking Address:") %></span>
<span class="modal-validator-info-item-value"><%= to_string(@validator.staking_address_hash) %></span>
</div>
<div>
<span class="modal-validator-info-item-title"><%= gettext("Mining Address:") %></span>
<span class="modal-validator-info-item-value"><%= to_string(@validator.mining_address_hash) %></span>
</div>
</div>
</div>
<%= if @validator.is_banned do %>
<div class="modal-validator-alert">
<%= gettext("This pool is banned until block #%{banned_until} (%{estimated_unban_day})", banned_until: @validator.banned_until, estimated_unban_day: estimated_unban_day(@validator.banned_until, @average_block_time)) %><br />
<%= gettext("Reason for Ban: %{ban_reason}", ban_reason: String.capitalize(@validator.ban_reason)) %>
<%= if @delegator &&
@delegator.address_hash != @validator.staking_address_hash &&
@validator.are_delegators_banned &&
@validator.banned_until != @validator.banned_delegators_until do %>
<br />
<%= gettext("You will be able to withdraw after block #%{banned_delegators_until} (%{estimated_unban_day})", banned_delegators_until: @validator.banned_delegators_until, estimated_unban_day: estimated_unban_day(@validator.banned_delegators_until, @average_block_time)) %>
<% end %>
</div>
<% end %>
<%= render BlockScoutWeb.CommonComponentsView, "_modal_close_button.html" %>
<div class="modal-validator-info-content">
<%=
render BlockScoutWeb.StakesView,
"_stakes_pool_info_item.html",
title: gettext("Candidate’s Staked Amount"),
value: format_token_amount(@validator.self_staked_amount, @token)
%>
<%=
render BlockScoutWeb.StakesView,
"_stakes_pool_info_item.html",
title: gettext("Delegators’ Staked Amount"),
value: format_token_amount(Decimal.sub(@validator.total_staked_amount, @validator.self_staked_amount), @token)
%>
<%=
render BlockScoutWeb.StakesView,
"_stakes_pool_info_item.html",
title: gettext("Stakes Ratio"),
value: if(@validator.is_active, do: "#{@validator.stakes_ratio}%")
%>
<%=
render BlockScoutWeb.StakesView,
"_stakes_pool_info_item.html",
title: gettext("Share of Pool’s Reward"),
value: "#{@validator.validator_reward_percent}%"
%>
<%=
render BlockScoutWeb.StakesView,
"_stakes_pool_info_item.html",
title: gettext("How Many Times this Address has been a Validator"),
value: @validator.was_validator_count
%>
<%=
render BlockScoutWeb.StakesView,
"_stakes_pool_info_item.html",
title: gettext("How Many Times this Address has been Banned"),
value: @validator.was_banned_count
%>
<%=
render BlockScoutWeb.StakesView,
"_stakes_pool_info_item.html",
title: gettext("Likelihood of Becoming a Validator on the Next Epoch"),
value: "#{@validator.likelihood}%"
%>
<%=
render BlockScoutWeb.StakesView,
"_stakes_pool_info_item.html",
title: gettext("The Number of Delegators in the Pool"),
value: @validator.delegators_count
%>
</div>
</div>
</div>
</div>

@ -1,60 +0,0 @@
<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-content">
<%= render BlockScoutWeb.CommonComponentsView, "_modal_close_button.html" %>
<div class="modal-stake-two-cols">
<div class="modal-header">
<h5 class="modal-title"><%= gettext("Stake") %></h5>
</div>
<div class="modal-body">
<form>
<%= 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 %>
<% has_stake = Decimal.positive?(@delegator_staked) %>
<%= if has_stake do %>
<p class="form-p m-b-0"><%= gettext("Your Current Stake:") %>
<span class="text-dark">
<%= format_token_amount(@delegator_staked, @token) %>
</span>
</p>
<% else %>
<p class="form-p m-b-0"><%= gettext("Minimum Stake Allowed:") %>
<span class="text-dark">
<%= format_token_amount(@min_stake, @token) %>
</span>
</p>
<% end %>
<p class="form-p"><%= gettext("Your Balance:") %>
<%= if Decimal.add(@delegator_staked, @balance) >= @min_stake && Decimal.positive?(@balance) do %>
<span class="text-dark link-dotted" data-available-amount="<%= from_wei(@balance, @token) %>">
<%= format_token_amount(@balance, @token) %>
</span>
<% else %>
<span class="text-dark">
<%= format_token_amount(@balance, @token) %>
</span>
<% end %>
</p>
<div class="form-buttons">
<%=
label =
if has_stake 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>
</form>
</div>
<%= render BlockScoutWeb.CommonComponentsView, "_modal_bottom_disclaimer.html", text: gettext("Stake placed on a pool is <strong>pending</strong> for the current staking epoch. It will be applied to the next staking epoch. You may withdraw or move pending stake at any time until it becomes active. Once active (the pool you staked with becomes a validator), a withdrawal order can be placed. This amount will be available to claim after that staking epoch is complete.") |> raw(), extra_class: "b-b-r-0" %>
<div class="modal-stake-right">
<%= render BlockScoutWeb.StakesView, "_stakes_progress.html", pool: @pool, token: @token %>
</div>
</div>
</div>
</div>
</div>

@ -1,86 +0,0 @@
<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-content">
<%= render BlockScoutWeb.CommonComponentsView, "_modal_close_button.html" %>
<div class="modal-stake-two-cols">
<div class="modal-header">
<h5 class="modal-title"><%= gettext("Withdraw") %></h5>
</div>
<div class="modal-body">
<form>
<%= 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 %>
<p class="form-p m-b-0"><%= gettext("You Staked:") %>
<span class="text-dark">
<%= format_token_amount(@delegator.stake_amount, @token) %>
</span>
</p>
<%
withdraw_now_allowed = Decimal.positive?(@delegator.max_withdraw_allowed)
order_allowed = Decimal.positive?(@delegator.max_ordered_withdraw_allowed)
has_already_ordered = Decimal.positive?(@delegator.ordered_withdraw)
show_order_withdrawal = order_allowed or has_already_ordered
%>
<% # display available amounts %>
<%= if has_already_ordered do %>
<p class="form-p m-b-0"><%= gettext("Already Ordered:") %>
<span class="text-dark">
<%= format_token_amount(@delegator.ordered_withdraw, @token) %>
</span>
</p>
<% end %>
<%= if show_order_withdrawal do %>
<div class="form-p m-b-0<%= if not withdraw_now_allowed, do: "-7" %>"><%= gettext("You Can Order:") %>
<%= if order_allowed do %>
<span class="text-dark link-dotted" data-available-amount="<%= from_wei(@delegator.max_ordered_withdraw_allowed, @token) %>">
<%= format_token_amount(@delegator.max_ordered_withdraw_allowed, @token) %>
</span>
<%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip.html", text: gettext("The amount can be claimed after the current epoch finishes.") %>
<% else %>
<span class="text-dark">
<%= format_token_amount(@delegator.max_ordered_withdraw_allowed, @token) %>
</span>
<% end %>
</div>
<% end %>
<%= if withdraw_now_allowed do %>
<div class="form-p m-b-0-7"><%= gettext("Available Now:") %>
<span class="text-dark link-dotted" data-available-amount="<%= from_wei(@delegator.max_withdraw_allowed, @token) %>">
<%= format_token_amount(@delegator.max_withdraw_allowed, @token) %>
</span>
<%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip.html", text: gettext("You can withdraw this amount right now.") %>
</div>
<% end %>
<% # display buttons %>
<%=
if show_order_withdrawal do
bottom_margin = if withdraw_now_allowed, do: " m-b-0-7", else: ""
render BlockScoutWeb.StakesView,
"_stakes_btn_withdraw.html",
text: gettext("Order Withdrawal"),
extra_class: "full-width btn-add-full order-withdraw" <> bottom_margin
end
%>
<%=
if withdraw_now_allowed do
render BlockScoutWeb.StakesView,
"_stakes_btn_withdraw.html",
text: gettext("Withdraw Now"),
extra_class: "full-width btn-add-full withdraw"
end
%>
</form>
</div>
<%= render BlockScoutWeb.CommonComponentsView, "_modal_bottom_disclaimer.html", text: gettext("<p>Pending stake (stake placed on a candidate pool or placed during the current staking epoch) may be withdrawn now.</p>
<p>Active stake (stake available after the current epoch) can be ordered for withdrawal from the pool, and will be available to claim after the current staking epoch is complete.</p>
<p>If you have already ordered (and the staking window is still open), you may increase your current order by entering a positive value, or decrease your current order by entering a negative value in the box and clicking 'Order Withdrawal'. You must either keep the minimum stake amount in the pool, or order your entire stake for withdrawal.</p>
") |> raw(), extra_class: "b-b-r-0" %>
<div class="modal-stake-right">
<%= render BlockScoutWeb.StakesView, "_stakes_progress.html", pool: @pool, token: @token %>
</div>
</div>
</div>
</div>
</div>

@ -1,4 +0,0 @@
<div class="modal-validator-info-item">
<h2 class="modal-validator-info-item-title"><%= @title %></h2>
<p class="modal-validator-info-item-value"><%= if assigns[:value] do @value else "-" end %></p>
</div>

@ -1,37 +0,0 @@
<div class="stakes-progress">
<div class="stakes-progress-graph">
<canvas
class="stakes-progress-graph-canvas js-stakes-progress <%= if assigns[:extra_class], do: @extra_class %>"
height="125"
width="125"
></canvas>
<div class="stakes-progress-data">
<div class="stakes-progress-data-progress js-stakes-progress-data-progress">
<%= format_token_amount(@pool.self_staked_amount, @token, symbol: false, digits: 2) %>
</div>
<div class="stakes-progress-data-total js-stakes-progress-data-total">
<%= format_token_amount(@pool.total_staked_amount, @token, symbol: false, digits: 2) %>
</div>
</div>
</div>
<div class="stakes-progress-infos">
<div class="stakes-progress-info">
<h2 class="stakes-progress-info-title"><%= (if assigns[:pool_label], do: @pool_label) || gettext("Pool") %></h2>
<p class="stakes-progress-info-value<%= unless @pool.is_deleted do %> stakes-progress-info-link js-pool-info<% end %>" data-address="<%= to_string(@pool.staking_address_hash) %>">
<%= if is_nil(@pool.name), do: BlockScoutWeb.AddressView.trimmed_hash(@pool.staking_address_hash), else: @pool.name %>
</p>
</div>
<div class="stakes-progress-info">
<h2 class="stakes-progress-info-title"><%= gettext("Stakes Ratio") %></h2>
<p class="stakes-progress-info-value">
<%= if @pool.is_active, do: "#{@pool.stakes_ratio}%", else: gettext("(inactive pool)") %>
</p>
</div>
<div class="stakes-progress-info">
<h2 class="stakes-progress-info-title"><%= gettext("Delegators") %></h2>
<p class="stakes-progress-info-value<%= unless @pool.is_deleted do %> stakes-progress-info-link js-delegators-list<% end %>" data-address="<%= to_string(@pool.staking_address_hash) %>">
<%= @pool.delegators_count %>
</p>
</div>
</div>
</div>

@ -1,4 +0,0 @@
<div class="stakes-top-stats-item">
<span class="stakes-top-stats-label"><%= @title %></span>
<span class="stakes-top-stats-value"><%= @value %></span>
</div>

@ -1,57 +0,0 @@
<div class="stakes-top-stats-item stakes-top-stats-item-address"
data-user-address="<%= if @account, do: @account.address %>"
>
<span class="stakes-top-stats-value">
<%= if @account do %>
<div data-placement="bottom" data-toggle="tooltip" title=<%= @account.address %>>
<%= @account.address %>
</div>
<div class="copy-icon" data-clipboard-text="<%= @account.address %>">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14">
<path fill-rule="evenodd" d="M13 10a1 1 0 0 1-1-1V2H5a1 1 0 0 1 0-2h8a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1zm-3-5v8a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1zM8 6H2v6h6V6z"/>
</svg>
</div>
<% else %>
<a target="_blank" data-selector="login-button" class="stakes-top-stats-login">Login</a>
<% end %>
</span>
<span class="stakes-top-stats-label">
<%
balance = format_token_amount(@account[:balance], @token, digits: 5, no_tooltip: true, symbol: true)
balance_full = if @account[:balance] do
"#{from_wei(@account[:balance], @token)} #{@token.symbol}"
else
"-"
end
stake_amount = format_token_amount(@account[:stake_amount], @token, digits: 5, no_tooltip: true, symbol: true)
stake_amount_full = if @account[:stake_amount] do
"#{from_wei(@account[:stake_amount], @token)} #{@token.symbol}"
else
"-"
end
ordered_withdraw = format_token_amount(@account[:ordered_withdraw], @token, digits: 5, no_tooltip: true, symbol: true)
ordered_withdraw_full = if @account[:ordered_withdraw] do
"#{from_wei(@account[:ordered_withdraw], @token)} #{@token.symbol}"
else
"-"
end
%>
<span class="stakes-top-stats-label-item" data-placement="bottom" data-toggle="tooltip" title="<%= gettext "Balance" %>: <%= balance_full %>">
<%= gettext "Balance" %>: <%= balance %>
</span>
<span class="stakes-top-stats-label-item" data-placement="bottom" data-toggle="tooltip" title="<%= gettext "Staked" %>: <%= stake_amount_full %>">
<%= gettext "Staked" %>: <%= stake_amount %>
</span>
<%= if @account[:ordered_withdraw] && @account[:ordered_withdraw] > 0 do %>
<span class="stakes-top-stats-label-item" data-placement="bottom" data-toggle="tooltip" title="<%= gettext "Ordered" %>: <%= ordered_withdraw_full %>">
<%= gettext "Ordered" %>: <%= ordered_withdraw %>
</span>
<% end %>
</span>
<%= if @account do %>
<button disconnect-wallet class="button btn-full-primary mt-2 mr-4 hidden">Disconnect wallet</button>
<% end %>
</div>

@ -1,23 +0,0 @@
<div class="card-tabs js-card-tabs">
<%=
link(
list_title(:validator),
class: "card-tab #{tab_status("validators", @conn.request_path)}",
to: validators_path(@conn, :index)
)
%>
<%=
link(
list_title(:active),
class: "card-tab #{tab_status("active-pools", @conn.request_path)}",
to: active_pools_path(@conn, :index)
)
%>
<%=
link(
list_title(:inactive),
class: "card-tab #{tab_status("inactive-pools", @conn.request_path)}",
to: inactive_pools_path(@conn, :index)
)
%>
</div>

@ -1,6 +0,0 @@
<th class="stakes-table-th">
<div class="stakes-table-th-content">
<span class="stakes-th-text"><%= raw(@title) %></span>
<%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip.html", text: @tooltip %>
</div>
</th>

@ -1,31 +0,0 @@
<div class="card-title-container">
<div class="card-title">
<%= @title %>
<%= if @is_validator do%>
<div class="stake-stats-container">
<span><b><%= Chain.staking_pools_count(:validator) %></b> validator pools</span>
<span> and <b><%= Chain.delegators_count_sum(:validator) %></b> delegators</span>
<span> staking a total of <b><%= format_token_amount(Chain.total_staked_amount_sum(:validator), @token, digits: 0, ellipsize: false, symbol: false) %></b> STAKE</span>
</div>
<% end %>
<%= if @show_banned_checkbox do%>
<div class="stake-stats-container">
<span>A validator pool may be inactivated during a staking epoch. If the inactive pool is a current validator, STAKE and any accrued delegator rewards cannot be moved or withdrawn until the current staking epoch is complete. See the <a href="https://www.xdaichain.com/about-xdai/faqs/public-staking-validators-and-delegators#what-is-an-inactive-pool" target="_blank">Inactive Pool FAQs</a> for more information.</span>
</div>
<% end %>
</div>
<div class="card-title-controls">
<%= if @show_banned_checkbox do %>
<div class="check card-title-control">
<input type="checkbox" pool-filter-banned />
<div class="check-icon"></div>
<div class="check-text"><%= gettext("Show banned only") %></div>
</div>
<% end %>
<div class="check card-title-control">
<input type="checkbox" pool-filter-my />
<div class="check-icon"></div>
<div class="check-text"><%= gettext("Show only those I have stake in") %></div>
</div>
</div>
</div>

@ -1,57 +0,0 @@
<div class="stakes-top" style="padding-top: 70px; padding-bottom: 70px;">
<div class="container">
<div class="stakes-top-stats" data-block-number="<%= @block_number %>">
<%= render BlockScoutWeb.StakesView, "_stakes_stats_item.html", title: gettext("Epoch number"), value: @epoch_number %>
<%= render BlockScoutWeb.StakesView, "_stakes_stats_item.html", title: gettext("Block number"), value: @block_number %>
<%= render BlockScoutWeb.StakesView, "_stakes_stats_item.html", title: gettext("Next epoch in"), value: ngettext("%{blocks} block", "%{blocks} blocks", @epoch_end_in, blocks: @epoch_end_in) %>
<%= render BlockScoutWeb.StakesView, "_stakes_stats_item_account.html", account: @account, token: @token %>
<!-- Buttons -->
<div class="stakes-top-buttons">
<%= if @account[:pool] && @account.pool.is_active 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" %>
<% end %>
<% else %>
<%=
button_class = "full-width " <>
if !is_nil(@account[:pool]) or !is_nil(@account[:pool_id]) 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 %>
<%= render BlockScoutWeb.StakesView, "_stakes_btn_claim_reward.html", text: gettext("Claim Reward"), extra_class: "full-width" %>
</div>
<div class="stakes-top-buttons right" style="margin-bottom: -56px;">
<a
class="btn-full-primary full-width btn-add-full"
href="https://app.honeyswap.org/#/swap?outputCurrency=0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d&inputCurrency=0xb7D311E2Eb55F2f68a9440da38e7989210b9A05e&exactField=input"
target="_blank"
>
<span class="btn-full-primary-text"><%= gettext("Swap STAKE on Honeyswap") %></span>
</a>
<a
class="btn-full-primary full-width btn-add-full"
href="https://omni.xdaichain.com/"
target="_blank"
>
<span class="btn-full-primary-text"><%= gettext("Bridge STAKE to Ethereum") %></span>
</a>
<a
class="btn-full-primary full-width btn-add-full"
href="https://ascendex.com/register?inviteCode=5EYVQSTQ"
target="_blank"
>
<span class="btn-full-primary-text"><%= gettext("Trade STAKE on AscendEX") %></span>
</a>
</div>
</div>
</div>
</div>

@ -1,60 +0,0 @@
<div class="stakes-table-container">
<div class="stakes-table-head">
<div class="col-1"></div>
<div class="col-2">
<%
tooltip = cond do
@pools_type == :validator ->
gettext("Validator Pool Addresses.")
@pools_type == :active ->
gettext("Candidate and Validator Pool Addresses. Current validator pools are specified by a checkmark.")
@pools_type == :inactive ->
gettext("Inactive Pool Addresses. Current validator pools are specified by a checkmark.")
end
%>
<%= render BlockScoutWeb.StakesView, "_stakes_th.html", title: gettext("Pool"), tooltip: tooltip %>
</div>
<div class="col-2">
<%
tooltip = cond do
@pools_type == :validator ->
gettext("The first amount is the validator’s own stake, the second is the total amount staked into the pool by the validator and all delegators.")
@pools_type == :active ->
gettext("The first amount is the candidate’s own stake, the second is the total amount staked into the pool by the candidate and all delegators.")
@pools_type == :inactive ->
gettext("The first amount is the pool owner’s stake, the second is the total amount staked into the pool by the pool owner and all delegators.")
end
%>
<%= render BlockScoutWeb.StakesView, "_stakes_th.html", title: gettext("Staked Amount"), tooltip: tooltip %>
</div>
<div class="col-2">
<%= if @pools_type == :inactive do %>
<%= render BlockScoutWeb.StakesView, "_stakes_th.html", title: gettext("Banned"), tooltip: gettext("Validator pools can be banned for misbehavior (such as not revealing secret numbers). Validator and delegator stake contained in a banned pool cannot be withdrawn until the ban is over.") %>
<% else %>
<%= render BlockScoutWeb.StakesView, "_stakes_th.html", title: gettext("Stakes Ratio"), tooltip: gettext("The percentage of stake in a single pool relative to the total amount staked in all active pools. A higher ratio results in a greater likelihood of validator pool selection.") %>
<% end %>
</div>
<%= if @pools_type == :validator do %>
<div class="col-1">
<%= render BlockScoutWeb.StakesView, "_stakes_th.html", title: gettext("APY"), tooltip: gettext("Approximate Current Annual Percentage Yield. If you see N/A, please wait for a few blocks (APY cannot be calculated at the very beginning of a staking epoch).") %>
</div>
<% end %>
<div class="col-2">
<%= render BlockScoutWeb.StakesView, "_stakes_th.html", title: gettext("Delegators"), tooltip: gettext("The number of delegators providing stake to the pool. Click on the number to see more details.") %>
</div>
<div class="<%= if @pools_type == :validator do %>col-2<% else %>col-3<% end %>"></div>
</div>
<div class="stakes-table-body">
<button data-error-message class="alert alert-danger col-12 text-left" style="display: none;">
<span href="#" class="alert-link"><%= gettext("Something went wrong, click to reload.") %></span>
</button>
<div data-empty-response-message style="display: none;">
<%= render BlockScoutWeb.StakesView, "_stakes_empty_content.html" %>
</div>
<div data-items></div>
<div class="refresh-informer"><%= raw gettext("The table refreshed <span></span> block(s) ago.") %> <a href="#"><%= gettext("Refresh now") %></a></div>
</div>
</div>

@ -1,19 +0,0 @@
<section class="container js-stakes-warning-alert d-none">
<div class="card">
<div class="card-body card-body-flex-column-space-between" style="padding-left: 50px;">
<button type="button" class="stakes-btn-close-alert js-stakes-btn-close-warning">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18">
<path fill="#ddd" fill-rule="evenodd" d="M10.435 8.983l7.261 7.261a1.027 1.027 0 1 1-1.452 1.452l-7.261-7.261-7.262 7.262a1.025 1.025 0 1 1-1.449-1.45l7.262-7.261L.273 1.725A1.027 1.027 0 1 1 1.725.273l7.261 7.261 7.23-7.231a1.025 1.025 0 1 1 1.449 1.45l-7.23 7.23z"></path>
</svg>
</button>
<span style="font-size: 1.3em;">
<%= render BlockScoutWeb.CommonComponentsView, "_info.html", extra_class: "ml-1" %>
<%= if @message do %>
<%= "#{@message}" %>
<% else %>
Due to high load volumes, current staking data display may lag behind actual transactions. Transactions are being processed correctly on-chain. We are currently working to address this UI display issue.
<% end %>
</span>
</div>
</div>
</section>

@ -1,53 +0,0 @@
<div data-selector="stakes-top">
<%= raw(@top) %>
</div>
<%= render BlockScoutWeb.StakesView, "_learn-more.html", conn: @conn %>
<%= if Application.get_env(:block_scout_web, BlockScoutWeb.Chain)[:show_staking_warning] do %>
<%= render BlockScoutWeb.StakesView, "_warning.html", conn: @conn, message: System.get_env("STAKING_WARNING_MESSAGE", nil) %>
<% end %>
<section data-page="stakes" class="container" data-refresh-interval="<%= @refresh_interval %>">
<div class="card" data-async-load data-async-listing="<%= @current_path %>" data-no-first-loading>
<%= render BlockScoutWeb.StakesView, "_stakes_tabs.html", conn: @conn %>
<%=
render BlockScoutWeb.StakesView,
"_stakes_title.html",
title: list_title(@pools_type),
token: @token,
show_banned_checkbox: @pools_type == :inactive,
is_validator: @pools_type == :validator
%>
<div class="card-title-paging">
<%=
render BlockScoutWeb.CommonComponentsView,
"_pagination_container.html",
position: "top",
show_pagination_limit: true,
data_next_page_button: true,
data_prev_page_button: true
%>
</div>
<%=
render BlockScoutWeb.StakesView,
"_table.html",
pools_type: @pools_type
%>
<div class="card-footer-paging">
<%=
render BlockScoutWeb.CommonComponentsView,
"_pagination_container.html",
position: "bottom",
cur_page_number: "1",
show_pagination_limit: true,
data_next_page_button: true,
data_prev_page_button: true
%>
</div>
</div>
</section>
<div class="stakes-progress-graph-thing-for-getting-color"></div>
<script defer data-cfasync="false" src="<%= static_path(@conn, "/js/stakes.js") %>"></script>

@ -1,100 +0,0 @@
defmodule BlockScoutWeb.StakesHelpers do
@moduledoc """
Helpers for staking templates
"""
alias BlockScoutWeb.CldrHelper.Number
alias Explorer.Chain.Cache.BlockNumber
alias Explorer.Chain.Token
alias Phoenix.HTML
alias Timex.Duration
def amount_ratio(pool) do
zero = Decimal.new(0)
case pool do
%{total_staked_amount: ^zero} ->
0
%{total_staked_amount: total_staked_amount, self_staked_amount: self_staked} ->
amount = Decimal.to_float(total_staked_amount)
self = Decimal.to_float(self_staked)
self / amount * 100
end
end
def estimated_unban_day(banned_until, average_block_time) do
block_time = Duration.to_seconds(average_block_time)
try do
during_sec = (banned_until - BlockNumber.get_max()) * block_time
now = DateTime.utc_now() |> DateTime.to_unix()
date = DateTime.from_unix!(trunc(now + during_sec))
Timex.format!(date, "%d %b %Y", :strftime)
rescue
_e ->
DateTime.utc_now()
|> Timex.format!("%d %b %Y", :strftime)
end
end
def list_title(:validator), do: Gettext.dgettext(BlockScoutWeb.Gettext, "default", "Validators")
def list_title(:active), do: Gettext.dgettext(BlockScoutWeb.Gettext, "default", "Active Pools (Candidates)")
def list_title(:inactive), do: Gettext.dgettext(BlockScoutWeb.Gettext, "default", "Inactive Pools")
def from_wei(%Decimal{} = amount, %Token{} = token, to_string \\ true) do
decimals = if token.decimals, do: Decimal.to_integer(token.decimals), else: 0
result =
amount.sign
|> Decimal.new(amount.coef, amount.exp - decimals)
|> Decimal.normalize()
if to_string do
Decimal.to_string(result, :normal)
else
result
end
end
def format_token_amount(amount, token, options \\ [])
def format_token_amount(nil, _token, _options), do: "-"
def format_token_amount(amount, nil, options), do: format_token_amount(amount, %Token{}, options)
def format_token_amount(amount, token, options) when is_integer(amount) do
amount
|> Decimal.new()
|> format_token_amount(token, options)
end
def format_token_amount(%Decimal{} = amount, %Token{} = token, options) do
symbol = if Keyword.get(options, :symbol, true), do: " #{token.symbol}"
digits = Keyword.get(options, :digits, 5)
ellipsize = Keyword.get(options, :ellipsize, true)
no_tooltip = Keyword.get(options, :no_tooltip, false)
reduced = from_wei(amount, token, false)
if digits >= -reduced.exp or not ellipsize do
if min(digits, -reduced.exp) >= 0 do
"#{Number.to_string!(reduced, fractional_digits: min(digits, -reduced.exp))}#{symbol}"
else
"#{Number.to_string!(reduced, fractional_digits: 0)}#{symbol}"
end
else
clipped = "#{Number.to_string!(reduced, fractional_digits: digits)}...#{symbol}"
if no_tooltip do
clipped
else
HTML.raw(~s"""
<span
data-placement="top"
data-toggle="tooltip"
title="#{Number.to_string!(reduced, fractional_digits: -reduced.exp)}#{symbol}">
#{clipped}
</span>
""")
end
end
end
end

@ -1,5 +0,0 @@
defmodule BlockScoutWeb.StakesView do
use BlockScoutWeb, :view
import BlockScoutWeb.StakesHelpers
alias Explorer.Chain
end

@ -48,10 +48,6 @@ defmodule BlockScoutWeb.WebRouter do
get("/uncles", BlockController, :uncle, as: :uncle)
get("/validators", StakesController, :index, as: :validators, assigns: %{filter: :validator})
get("/active-pools", StakesController, :index, as: :active_pools, assigns: %{filter: :active})
get("/inactive-pools", StakesController, :index, as: :inactive_pools, assigns: %{filter: :inactive})
resources("/pending-transactions", PendingTransactionController, only: [:index])
resources("/recent-transactions", RecentTransactionsController, only: [:index])

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -1,28 +0,0 @@
defmodule BlockScoutWeb.StakesChannelTest do
use BlockScoutWeb.ChannelCase
alias BlockScoutWeb.StakingEventHandler
test "subscribed user is notified of staking_update event" do
topic = "stakes:staking_update"
@endpoint.subscribe(topic)
data = %{
block_number: 76,
epoch_number: 0,
staking_allowed: false,
staking_token_defined: false,
validator_set_apply_block: 0
}
StakingEventHandler.handle_info({:chain_event, :staking_update, :realtime, data}, nil)
receive do
%Phoenix.Socket.Broadcast{topic: ^topic, event: "staking_update", payload: %{epoch_number: _}} ->
assert true
after
:timer.seconds(5) ->
assert false, "Expected message received nothing."
end
end
end

@ -1,49 +0,0 @@
defmodule BlockScoutWeb.StakesControllerTest do
use BlockScoutWeb.ConnCase
alias Explorer.Counters.AverageBlockTime
setup do
start_supervised!(AverageBlockTime)
Application.put_env(:explorer, AverageBlockTime, enabled: true)
on_exit(fn ->
Application.put_env(:explorer, AverageBlockTime, enabled: false)
end)
end
describe "GET validators/2" do
test "returns page", %{conn: conn} do
conn = get(conn, validators_path(conn, :index))
assert conn.status == 200
end
test "returns rendered table", %{conn: conn} do
pools = Enum.map(1..4, fn _ -> insert(:staking_pool) end)
conn = get(conn, validators_path(conn, :index, %{type: "JSON", filterMy: false}))
assert {:ok, %{"items" => items, "next_page_path" => _}} = Poison.decode(conn.resp_body)
assert Enum.count(items) == Enum.count(pools)
end
end
describe "GET active_pools/2" do
test "returns rendered table", %{conn: conn} do
pools = Enum.map(1..4, fn _ -> insert(:staking_pool) end)
conn = get(conn, active_pools_path(conn, :index, %{type: "JSON", filterMy: false}))
assert {:ok, %{"items" => items, "next_page_path" => _}} = Poison.decode(conn.resp_body)
assert Enum.count(items) == Enum.count(pools)
end
end
describe "GET inactive_pools/2" do
test "returns rendered table", %{conn: conn} do
pools = Enum.map(1..4, fn _ -> insert(:staking_pool, is_active: false) end)
conn = get(conn, inactive_pools_path(conn, :index, %{type: "JSON", filterMy: false}))
assert {:ok, %{"items" => items, "next_page_path" => _}} = Poison.decode(conn.resp_body)
assert Enum.count(items) == Enum.count(pools)
end
end
end

@ -1,24 +0,0 @@
defmodule BlockScoutWeb.StakesHelpersTest do
use ExUnit.Case
alias BlockScoutWeb.StakesHelpers
alias Timex.Duration
setup do
Application.put_env(:explorer, Explorer.Chain.Cache.BlockNumber, enabled: true)
on_exit(fn ->
Application.put_env(:explorer, Explorer.Chain.Cache.BlockNumber, enabled: false)
end)
end
test "estimated_unban_day/2" do
block_average = Duration.from_seconds(5)
unban_day = StakesHelpers.estimated_unban_day(10, block_average)
now = DateTime.utc_now() |> DateTime.to_unix()
date = DateTime.from_unix!(trunc(now + 5 * 10))
assert Timex.format!(date, "%d %b %Y", :strftime) == unban_day
end
end

@ -213,18 +213,6 @@ config :explorer, Explorer.Chain.Block.Reward,
validators_contract_address: System.get_env("VALIDATORS_CONTRACT"),
keys_manager_contract_address: System.get_env("KEYS_MANAGER_CONTRACT")
pos_staking_contract = System.get_env("POS_STAKING_CONTRACT")
if pos_staking_contract do
config :explorer, Explorer.Staking.ContractState,
enabled: true,
staking_contract_address: pos_staking_contract,
eth_subscribe_max_delay: System.get_env("POS_ETH_SUBSCRIBE_MAX_DELAY", "60"),
eth_blocknumber_pull_interval: System.get_env("POS_ETH_BLOCKNUMBER_PULL_INTERVAL", "500")
else
config :explorer, Explorer.Staking.ContractState, enabled: false
end
case System.get_env("SUPPLY_MODULE") do
"TokenBridge" ->
config :explorer, supply: Explorer.Chain.Supply.TokenBridge

@ -44,8 +44,6 @@ config :explorer, Explorer.Market.History.Cataloger, enabled: false
config :explorer, Explorer.Tracer, disabled?: false
config :explorer, Explorer.Staking.ContractState, enabled: false
config :logger, :explorer,
level: :warn,
path: Path.absname("logs/test/explorer.log")

@ -97,7 +97,6 @@ defmodule Explorer.Application do
configure(Explorer.Counters.AverageBlockTime),
configure(Explorer.Counters.Bridge),
configure(Explorer.Validator.MetadataProcessor),
configure(Explorer.Staking.ContractState),
configure(MinMissingBlockNumber)
]
|> List.flatten()

@ -7,7 +7,6 @@ defmodule Explorer.Chain do
only: [
from: 2,
join: 4,
join: 5,
limit: 2,
lock: 2,
offset: 2,
@ -55,8 +54,6 @@ defmodule Explorer.Chain do
PendingBlockOperation,
SmartContract,
SmartContractAdditionalSource,
StakingPool,
StakingPoolsDelegator,
Token,
Token.Instance,
TokenTransfer,
@ -81,7 +78,6 @@ defmodule Explorer.Chain do
alias Explorer.Market.MarketHistoryCache
alias Explorer.{PagingOptions, Repo}
alias Explorer.SmartContract.{Helper, Reader}
alias Explorer.Staking.ContractState
alias Dataloader.Ecto, as: DataloaderEcto
@ -5408,13 +5404,20 @@ defmodule Explorer.Chain do
defp convert_binary_to_string(binary, type) do
case type do
{:bytes, _} ->
ContractState.binary_to_string(binary)
binary_to_string(binary)
_ ->
binary
end
end
defp binary_to_string(binary) do
binary
|> :binary.bin_to_list()
|> Enum.filter(fn x -> x != 0 end)
|> List.to_string()
end
defp compose_foreign_json_rpc_named_arguments(json_rpc_named_arguments, foreign_json_rpc)
when foreign_json_rpc != "" do
{_, eth_call_foreign_json_rpc_named_arguments} =
@ -6152,200 +6155,6 @@ defmodule Explorer.Chain do
value
end
@doc "Get staking pools from the DB"
@spec staking_pools(
filter :: :validator | :active | :inactive,
paging_options :: PagingOptions.t() | :all,
address_hash :: Hash.t() | nil,
filter_banned :: boolean() | nil,
filter_my :: boolean() | nil
) :: [map()]
def staking_pools(
filter,
paging_options \\ @default_paging_options,
address_hash \\ nil,
filter_banned \\ false,
filter_my \\ false
) do
base_query =
StakingPool
|> where(is_deleted: false)
|> staking_pool_filter(filter)
|> staking_pools_paging_query(paging_options)
delegator_query =
if address_hash do
base_query
|> join(:left, [p], pd in StakingPoolsDelegator,
on:
p.staking_address_hash == pd.staking_address_hash and pd.address_hash == ^address_hash and
not pd.is_deleted
)
|> select([p, pd], %{pool: p, delegator: pd})
else
base_query
|> select([p], %{pool: p, delegator: nil})
end
banned_query =
if filter_banned do
where(delegator_query, is_banned: true)
else
delegator_query
end
filtered_query =
if address_hash && filter_my do
where(banned_query, [..., pd], not is_nil(pd))
else
banned_query
end
Repo.all(filtered_query)
end
defp staking_pools_paging_query(base_query, :all) do
base_query
|> order_by(asc: :staking_address_hash)
end
defp staking_pools_paging_query(base_query, paging_options) do
paging_query =
base_query
|> limit(^paging_options.page_size)
|> order_by(desc: :stakes_ratio, desc: :is_active, asc: :staking_address_hash)
case paging_options.key do
{value, address_hash} ->
where(
paging_query,
[p],
p.stakes_ratio < ^value or
(p.stakes_ratio == ^value and p.staking_address_hash > ^address_hash)
)
_ ->
paging_query
end
end
@doc "Get count of staking pools from the DB"
@spec staking_pools_count(filter :: :validator | :active | :inactive) :: integer
def staking_pools_count(filter) do
StakingPool
|> where(is_deleted: false)
|> staking_pool_filter(filter)
|> Repo.aggregate(:count, :staking_address_hash)
end
@doc "Get sum of delegators count from the DB"
@spec delegators_count_sum(filter :: :validator | :active | :inactive) :: integer
def delegators_count_sum(filter) do
StakingPool
|> where(is_deleted: false)
|> staking_pool_filter(filter)
|> Repo.aggregate(:sum, :delegators_count)
end
@doc "Get sum of total staked amount from the DB"
@spec total_staked_amount_sum(filter :: :validator | :active | :inactive) :: integer
def total_staked_amount_sum(filter) do
StakingPool
|> where(is_deleted: false)
|> staking_pool_filter(filter)
|> Repo.aggregate(:sum, :total_staked_amount)
end
defp staking_pool_filter(query, :validator) do
where(query, is_validator: true)
end
defp staking_pool_filter(query, :active) do
where(query, is_active: true)
end
defp staking_pool_filter(query, :inactive) do
where(query, is_active: false)
end
def staking_pool(staking_address_hash) do
Repo.get_by(StakingPool, staking_address_hash: staking_address_hash)
end
def staking_pool_names(staking_addresses) do
StakingPool
|> where([p], p.staking_address_hash in ^staking_addresses and p.is_deleted == false)
|> select([:staking_address_hash, :name])
|> Repo.all()
end
def staking_pool_delegators(staking_address_hash, show_snapshotted_data) do
query =
from(
d in StakingPoolsDelegator,
where:
d.staking_address_hash == ^staking_address_hash and
(d.is_active == true or (^show_snapshotted_data and d.snapshotted_stake_amount > 0 and d.is_active != true)),
order_by: [desc: d.stake_amount]
)
query
|> Repo.all()
end
def staking_pool_snapshotted_delegator_data_for_apy do
query =
from(
d in StakingPoolsDelegator,
select: %{
:staking_address_hash => fragment("DISTINCT ON (?) ?", d.staking_address_hash, d.staking_address_hash),
:snapshotted_reward_ratio => d.snapshotted_reward_ratio,
:snapshotted_stake_amount => d.snapshotted_stake_amount
},
where: d.staking_address_hash != d.address_hash and d.snapshotted_stake_amount > 0
)
query
|> Repo.all()
end
def staking_pool_snapshotted_inactive_delegators_count(staking_address_hash) do
query =
from(
d in StakingPoolsDelegator,
where:
d.staking_address_hash == ^staking_address_hash and
d.snapshotted_stake_amount > 0 and
d.is_active != true,
select: fragment("count(*)")
)
query
|> Repo.one()
end
def staking_pool_delegator(staking_address_hash, address_hash) do
Repo.get_by(StakingPoolsDelegator,
staking_address_hash: staking_address_hash,
address_hash: address_hash,
is_deleted: false
)
end
def get_total_staked_and_ordered(""), do: nil
def get_total_staked_and_ordered(address_hash) when is_binary(address_hash) do
StakingPoolsDelegator
|> where([delegator], delegator.address_hash == ^address_hash and not delegator.is_deleted)
|> select([delegator], %{
stake_amount: coalesce(sum(delegator.stake_amount), 0),
ordered_withdraw: coalesce(sum(delegator.ordered_withdraw), 0)
})
|> Repo.one()
end
def get_total_staked_and_ordered(_), do: nil
defp with_decompiled_code_flag(query, _hash, false), do: query
defp with_decompiled_code_flag(query, hash, true) do

@ -3,7 +3,7 @@ defmodule Explorer.Chain.Events.Publisher do
Publishes events related to the Chain context.
"""
@allowed_events ~w(addresses address_coin_balances address_token_balances blocks block_rewards internal_transactions last_block_number staking_update token_transfers transactions contract_verification_result)a
@allowed_events ~w(addresses address_coin_balances address_token_balances blocks block_rewards internal_transactions last_block_number token_transfers transactions contract_verification_result)a
def broadcast(_data, false), do: :ok

@ -3,11 +3,11 @@ defmodule Explorer.Chain.Events.Subscriber do
Subscribes to events related to the Chain context.
"""
@allowed_broadcast_events ~w(addresses address_coin_balances address_token_balances blocks block_rewards internal_transactions last_block_number staking_update token_transfers transactions contract_verification_result)a
@allowed_broadcast_events ~w(addresses address_coin_balances address_token_balances blocks block_rewards internal_transactions last_block_number token_transfers transactions contract_verification_result)a
@allowed_broadcast_types ~w(catchup realtime on_demand contract_verification_result)a
@allowed_events ~w(exchange_rate stake_snapshotting_finished transaction_stats)a
@allowed_events ~w(exchange_rate transaction_stats)a
@type broadcast_type :: :realtime | :catchup | :on_demand

@ -1,180 +0,0 @@
defmodule Explorer.Chain.Import.Runner.StakingPools do
@moduledoc """
Bulk imports staking pools to StakingPool tabe.
"""
require Ecto.Query
alias Ecto.{Changeset, Multi, Repo}
alias Explorer.Chain.{Import, StakingPool}
import Ecto.Query, only: [from: 2]
@behaviour Import.Runner
# milliseconds
@timeout 60_000
@type imported :: [StakingPool.t()]
@impl Import.Runner
def ecto_schema_module, do: StakingPool
@impl Import.Runner
def option_key, do: :staking_pools
@impl Import.Runner
def runner_specific_options, do: [:clear_snapshotted_values]
@impl Import.Runner
def imported_table_row do
%{
value_type: "[#{ecto_schema_module()}.t()]",
value_description: "List of `t:#{ecto_schema_module()}.t/0`s"
}
end
@impl Import.Runner
def run(multi, changes_list, %{timestamps: timestamps} = options) do
insert_options =
options
|> Map.get(option_key(), %{})
|> Map.take(~w(on_conflict timeout clear_snapshotted_values)a)
|> Map.put_new(:timeout, @timeout)
|> Map.put(:timestamps, timestamps)
clear_snapshotted_values =
case Map.fetch(insert_options, :clear_snapshotted_values) do
{:ok, v} -> v
:error -> false
end
multi =
if clear_snapshotted_values do
multi
else
# Enforce ShareLocks tables order (see docs: sharelocks.md)
Multi.run(multi, :acquire_all_staking_pools, fn repo, _ ->
acquire_all_staking_pools(repo)
end)
end
multi
|> Multi.run(:mark_as_deleted, fn repo, _ ->
mark_as_deleted(repo, changes_list, insert_options, clear_snapshotted_values)
end)
|> Multi.run(:insert_staking_pools, fn repo, _ ->
insert(repo, changes_list, insert_options)
end)
end
@impl Import.Runner
def timeout, do: @timeout
defp acquire_all_staking_pools(repo) do
query =
from(
pool in StakingPool,
# Enforce StackingPool ShareLocks order (see docs: sharelocks.md)
order_by: pool.staking_address_hash,
lock: "FOR UPDATE"
)
pools = repo.all(query)
{:ok, pools}
end
defp mark_as_deleted(repo, changes_list, %{timeout: timeout}, clear_snapshotted_values) when is_list(changes_list) do
query =
if clear_snapshotted_values do
from(
pool in StakingPool,
update: [
set: [
snapshotted_self_staked_amount: nil,
snapshotted_total_staked_amount: nil,
snapshotted_validator_reward_ratio: nil
]
]
)
else
addresses = Enum.map(changes_list, & &1.staking_address_hash)
from(
pool in StakingPool,
where: pool.staking_address_hash not in ^addresses,
# ShareLocks order already enforced by `acquire_all_staking_pools` (see docs: sharelocks.md)
update: [set: [is_deleted: true, is_active: false]]
)
end
try do
{_, result} = repo.update_all(query, [], timeout: timeout)
{:ok, result}
rescue
postgrex_error in Postgrex.Error ->
{:error, %{exception: postgrex_error}}
end
end
@spec insert(Repo.t(), [map()], %{
optional(:on_conflict) => Import.Runner.on_conflict(),
required(:timeout) => timeout,
required(:timestamps) => Import.timestamps()
}) ::
{:ok, [StakingPool.t()]}
| {:error, [Changeset.t()]}
defp insert(repo, changes_list, %{timeout: timeout, timestamps: timestamps} = options) when is_list(changes_list) do
on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0)
# Enforce StackingPool ShareLocks order (see docs: sharelocks.md)
ordered_changes_list = Enum.sort_by(changes_list, & &1.staking_address_hash)
{:ok, _} =
Import.insert_changes_list(
repo,
ordered_changes_list,
conflict_target: :staking_address_hash,
on_conflict: on_conflict,
for: StakingPool,
returning: [:staking_address_hash],
timeout: timeout,
timestamps: timestamps
)
end
defp default_on_conflict do
from(
pool in StakingPool,
update: [
set: [
mining_address_hash: fragment("EXCLUDED.mining_address_hash"),
delegators_count: fragment("EXCLUDED.delegators_count"),
is_active: fragment("EXCLUDED.is_active"),
is_banned: fragment("EXCLUDED.is_banned"),
is_validator: fragment("EXCLUDED.is_validator"),
is_unremovable: fragment("EXCLUDED.is_unremovable"),
are_delegators_banned: fragment("EXCLUDED.are_delegators_banned"),
likelihood: fragment("EXCLUDED.likelihood"),
validator_reward_percent: fragment("EXCLUDED.validator_reward_percent"),
stakes_ratio: fragment("EXCLUDED.stakes_ratio"),
validator_reward_ratio: fragment("EXCLUDED.validator_reward_ratio"),
self_staked_amount: fragment("EXCLUDED.self_staked_amount"),
total_staked_amount: fragment("EXCLUDED.total_staked_amount"),
ban_reason: fragment("EXCLUDED.ban_reason"),
was_banned_count: fragment("EXCLUDED.was_banned_count"),
was_validator_count: fragment("EXCLUDED.was_validator_count"),
is_deleted: fragment("EXCLUDED.is_deleted"),
banned_until: fragment("EXCLUDED.banned_until"),
banned_delegators_until: fragment("EXCLUDED.banned_delegators_until"),
name: fragment("EXCLUDED.name"),
description: fragment("EXCLUDED.description"),
inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", pool.inserted_at),
updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", pool.updated_at)
]
]
)
end
end

@ -1,134 +0,0 @@
defmodule Explorer.Chain.Import.Runner.StakingPoolsDelegators do
@moduledoc """
Bulk imports delegators to StakingPoolsDelegator table.
"""
require Ecto.Query
require Logger
alias Ecto.{Changeset, Multi, Repo}
alias Explorer.Chain.{Import, StakingPoolsDelegator}
import Ecto.Query, only: [from: 2]
@behaviour Import.Runner
# milliseconds
@timeout 60_000
@type imported :: [StakingPoolsDelegator.t()]
@impl Import.Runner
def ecto_schema_module, do: StakingPoolsDelegator
@impl Import.Runner
def option_key, do: :staking_pools_delegators
@impl Import.Runner
def runner_specific_options, do: [:clear_snapshotted_values]
@impl Import.Runner
def imported_table_row do
%{
value_type: "[#{ecto_schema_module()}.t()]",
value_description: "List of `t:#{ecto_schema_module()}.t/0`s"
}
end
@impl Import.Runner
def run(multi, changes_list, %{timestamps: timestamps} = options) do
insert_options =
options
|> Map.get(option_key(), %{})
|> Map.take(~w(on_conflict timeout clear_snapshotted_values)a)
|> Map.put_new(:timeout, @timeout)
|> Map.put(:timestamps, timestamps)
multi
|> Multi.run(:clear_delegators, fn repo, _ ->
mark_as_deleted(repo, insert_options)
end)
|> Multi.run(:insert_staking_pools_delegators, fn repo, _ ->
insert(repo, changes_list, insert_options)
end)
end
@impl Import.Runner
def timeout, do: @timeout
defp mark_as_deleted(repo, %{timeout: timeout} = options) do
clear_snapshotted_values =
case Map.fetch(options, :clear_snapshotted_values) do
{:ok, v} -> v
:error -> false
end
query =
if clear_snapshotted_values do
from(
d in StakingPoolsDelegator,
update: [set: [snapshotted_reward_ratio: nil, snapshotted_stake_amount: nil]]
)
else
from(
d in StakingPoolsDelegator,
update: [set: [is_active: false, is_deleted: true]]
)
end
try do
{_, result} = repo.update_all(query, [], timeout: timeout)
{:ok, result}
rescue
postgrex_error in Postgrex.Error ->
{:error, %{exception: postgrex_error}}
end
end
@spec insert(Repo.t(), [map()], %{
optional(:on_conflict) => Import.Runner.on_conflict(),
required(:timeout) => timeout,
required(:timestamps) => Import.timestamps()
}) ::
{:ok, [StakingPoolsDelegator.t()]}
| {:error, [Changeset.t()]}
defp insert(repo, changes_list, %{timeout: timeout, timestamps: timestamps} = options) when is_list(changes_list) do
on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0)
# Enforce StackingPoolDelegator ShareLocks order (see docs: sharelocks.md)
ordered_changes_list = Enum.sort_by(changes_list, &{&1.address_hash, &1.staking_address_hash})
{:ok, _} =
Import.insert_changes_list(
repo,
ordered_changes_list,
conflict_target: [:staking_address_hash, :address_hash],
on_conflict: on_conflict,
for: StakingPoolsDelegator,
returning: [:staking_address_hash, :address_hash],
timeout: timeout,
timestamps: timestamps
)
end
defp default_on_conflict do
from(
delegator in StakingPoolsDelegator,
update: [
set: [
stake_amount: fragment("EXCLUDED.stake_amount"),
ordered_withdraw: fragment("EXCLUDED.ordered_withdraw"),
max_withdraw_allowed: fragment("EXCLUDED.max_withdraw_allowed"),
max_ordered_withdraw_allowed: fragment("EXCLUDED.max_ordered_withdraw_allowed"),
ordered_withdraw_epoch: fragment("EXCLUDED.ordered_withdraw_epoch"),
reward_ratio: fragment("EXCLUDED.reward_ratio"),
inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", delegator.inserted_at),
updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", delegator.updated_at),
is_active: fragment("EXCLUDED.is_active"),
is_deleted: fragment("EXCLUDED.is_deleted")
]
]
)
end
end

@ -13,8 +13,6 @@ defmodule Explorer.Chain.Import.Stage.AddressReferencing do
do: [
Runner.Address.CoinBalances,
Runner.Blocks,
Runner.StakingPools,
Runner.StakingPoolsDelegators,
Runner.Address.CoinBalancesDaily
]

@ -1,148 +0,0 @@
defmodule Explorer.Chain.StakingPool do
@moduledoc """
The representation of staking pool from POSDAO network.
Staking pools might be candidate or validator.
"""
use Ecto.Schema
import Ecto.Changeset
alias Explorer.Chain.{
Address,
Hash,
StakingPoolsDelegator
}
@type t :: %__MODULE__{
are_delegators_banned: boolean,
banned_delegators_until: integer,
banned_until: integer,
ban_reason: String.t(),
delegators_count: integer,
description: String.t(),
is_active: boolean,
is_banned: boolean,
is_deleted: boolean,
is_unremovable: boolean,
is_validator: boolean,
likelihood: Decimal.t(),
mining_address_hash: Hash.Address.t(),
name: String.t(),
self_staked_amount: Decimal.t(),
snapshotted_self_staked_amount: Decimal.t(),
snapshotted_total_staked_amount: Decimal.t(),
snapshotted_validator_reward_ratio: Decimal.t(),
stakes_ratio: Decimal.t(),
staking_address_hash: Hash.Address.t(),
total_staked_amount: Decimal.t(),
validator_reward_percent: Decimal.t(),
validator_reward_ratio: Decimal.t(),
was_banned_count: integer,
was_validator_count: integer
}
@attrs ~w(
are_delegators_banned
banned_delegators_until
banned_until
ban_reason
delegators_count
description
is_active
is_banned
is_unremovable
is_validator
likelihood
mining_address_hash
name
self_staked_amount
snapshotted_self_staked_amount
snapshotted_total_staked_amount
snapshotted_validator_reward_ratio
stakes_ratio
staking_address_hash
total_staked_amount
validator_reward_percent
validator_reward_ratio
was_banned_count
was_validator_count
)a
@req_attrs ~w(
banned_until
delegators_count
is_active
is_banned
is_unremovable
is_validator
mining_address_hash
self_staked_amount
staking_address_hash
total_staked_amount
was_banned_count
was_validator_count
)a
schema "staking_pools" do
field(:are_delegators_banned, :boolean, default: false)
field(:banned_delegators_until, :integer)
field(:banned_until, :integer)
field(:ban_reason, :string)
field(:delegators_count, :integer)
field(:description, :string, size: 1024)
field(:is_active, :boolean, default: false)
field(:is_banned, :boolean, default: false)
field(:is_deleted, :boolean, default: false)
field(:is_unremovable, :boolean, default: false)
field(:is_validator, :boolean, default: false)
field(:likelihood, :decimal)
field(:name, :string, size: 256)
field(:self_staked_amount, :decimal)
field(:stakes_ratio, :decimal)
field(:snapshotted_self_staked_amount, :decimal)
field(:snapshotted_total_staked_amount, :decimal)
field(:snapshotted_validator_reward_ratio, :decimal)
field(:total_staked_amount, :decimal)
field(:validator_reward_percent, :decimal)
field(:validator_reward_ratio, :decimal)
field(:was_banned_count, :integer)
field(:was_validator_count, :integer)
has_many(:delegators, StakingPoolsDelegator, foreign_key: :staking_address_hash)
belongs_to(
:staking_address,
Address,
foreign_key: :staking_address_hash,
references: :hash,
type: Hash.Address
)
belongs_to(
:mining_address,
Address,
foreign_key: :mining_address_hash,
references: :hash,
type: Hash.Address
)
timestamps(null: false, type: :utc_datetime_usec)
end
@doc false
def changeset(staking_pool, attrs) do
staking_pool
|> cast(attrs, @attrs)
|> cast_assoc(:delegators)
|> validate_required(@req_attrs)
|> validate_staked_amount()
|> unique_constraint(:staking_address_hash)
end
defp validate_staked_amount(%{valid?: false} = c), do: c
defp validate_staked_amount(changeset) do
if get_field(changeset, :total_staked_amount) < get_field(changeset, :self_staked_amount) do
add_error(changeset, :total_staked_amount, "must be greater or equal to self_staked_amount")
else
changeset
end
end
end

@ -1,93 +0,0 @@
defmodule Explorer.Chain.StakingPoolsDelegator do
@moduledoc """
The representation of delegators from POSDAO network.
Delegators make stakes on staking pools and withdraw from them.
"""
use Ecto.Schema
import Ecto.Changeset
alias Explorer.Chain.{
Address,
Hash,
StakingPool
}
@type t :: %__MODULE__{
address_hash: Hash.Address.t(),
is_active: boolean(),
is_deleted: boolean(),
max_ordered_withdraw_allowed: Decimal.t(),
max_withdraw_allowed: Decimal.t(),
ordered_withdraw: Decimal.t(),
ordered_withdraw_epoch: integer(),
reward_ratio: Decimal.t(),
snapshotted_reward_ratio: Decimal.t(),
snapshotted_stake_amount: Decimal.t(),
stake_amount: Decimal.t(),
staking_address_hash: Hash.Address.t()
}
@attrs ~w(
address_hash
is_active
is_deleted
max_ordered_withdraw_allowed
max_withdraw_allowed
ordered_withdraw
ordered_withdraw_epoch
reward_ratio
snapshotted_reward_ratio
snapshotted_stake_amount
stake_amount
staking_address_hash
)a
@req_attrs ~w(
address_hash
max_ordered_withdraw_allowed
max_withdraw_allowed
ordered_withdraw
ordered_withdraw_epoch
stake_amount
staking_address_hash
)a
schema "staking_pools_delegators" do
field(:is_active, :boolean, default: true)
field(:is_deleted, :boolean, default: false)
field(:max_ordered_withdraw_allowed, :decimal)
field(:max_withdraw_allowed, :decimal)
field(:ordered_withdraw, :decimal)
field(:ordered_withdraw_epoch, :integer)
field(:reward_ratio, :decimal)
field(:snapshotted_reward_ratio, :decimal)
field(:snapshotted_stake_amount, :decimal)
field(:stake_amount, :decimal)
belongs_to(
:staking_pool,
StakingPool,
foreign_key: :staking_address_hash,
references: :staking_address_hash,
type: Hash.Address
)
belongs_to(
:delegator_address,
Address,
foreign_key: :address_hash,
references: :hash,
type: Hash.Address
)
timestamps(null: false, type: :utc_datetime_usec)
end
@doc false
def changeset(staking_pools_delegator, attrs) do
staking_pools_delegator
|> cast(attrs, @attrs)
|> validate_required(@req_attrs)
|> unique_constraint(:staking_address_hash, name: :pools_delegator_index)
end
end

@ -1,585 +0,0 @@
defmodule Explorer.Staking.ContractReader do
@moduledoc """
Routines for batched fetching of information from POSDAO contracts.
"""
alias Explorer.SmartContract.Reader
def global_requests(block_number) do
[
# 673a2a1f = keccak256(getPools())
active_pools: {:staking, "673a2a1f", [], block_number},
# 8c2243ae = keccak256(stakingEpochEndBlock())
epoch_end_block: {:staking, "8c2243ae", [], block_number},
# 794c0c68 = keccak256(stakingEpoch())
epoch_number: {:staking, "794c0c68", [], block_number},
# 7069e746 = keccak256(stakingEpochStartBlock())
epoch_start_block: {:staking, "7069e746", [], block_number},
# df6f55f5 = keccak256(getPoolsInactive())
inactive_pools: {:staking, "df6f55f5", [], block_number},
# f0786096 = keccak256(MAX_CANDIDATES())
max_candidates: {:staking, "f0786096", [], block_number},
# 714897df = keccak256(MAX_VALIDATORS())
max_validators: {:validator_set, "714897df", [], block_number},
# 5fef7643 = keccak256(candidateMinStake())
min_candidate_stake: {:staking, "5fef7643", [], block_number},
# da7a9b6a = keccak256(delegatorMinStake())
min_delegator_stake: {:staking, "da7a9b6a", [], block_number},
# 957950a7 = keccak256(getPoolsLikelihood())
pools_likelihood: {:staking, "957950a7", [], block_number},
# a5d54f65 = keccak256(getPoolsToBeElected())
pools_to_be_elected: {:staking, "a5d54f65", [], block_number},
# f4942501 = keccak256(areStakeAndWithdrawAllowed())
staking_allowed: {:staking, "f4942501", [], block_number},
# 74bdb372 = keccak256(lastChangeBlock())
staking_last_change_block: {:staking, "74bdb372", [], block_number},
# 2d21d217 = keccak256(erc677TokenContract())
token_contract_address: {:staking, "2d21d217", [], block_number},
# 704189ca = keccak256(unremovableValidator())
unremovable_validator: {:validator_set, "704189ca", [], block_number},
# b7ab4db5 = keccak256(getValidators())
validators: {:validator_set, "b7ab4db5", [], block_number},
# b927ef43 = keccak256(validatorSetApplyBlock())
validator_set_apply_block: {:validator_set, "b927ef43", [], block_number},
# 74bdb372 = keccak256(lastChangeBlock())
validator_set_last_change_block: {:validator_set, "74bdb372", [], block_number}
]
end
def active_delegators_request(pool_id, block_number) do
[
# 561c4c81 = keccak256(poolDelegators(uint256))
active_delegators: {:staking, "561c4c81", [pool_id], block_number}
]
end
# makes a raw `eth_call` for the `currentPoolRewards` function of the BlockReward contract:
# function currentPoolRewards(
# uint256 _rewardToDistribute,
# uint256[] memory _blocksCreatedShareNum,
# uint256 _blocksCreatedShareDenom,
# uint256 _stakingEpoch
# ) public view returns(uint256[] memory poolRewards);
def call_current_pool_rewards(block_reward_address, reward_to_distribute, staking_epoch, block_number) do
json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments)
reward_to_distribute =
reward_to_distribute
|> Integer.to_string(16)
|> String.pad_leading(64, ["0"])
staking_epoch =
staking_epoch
|> Integer.to_string(16)
|> String.pad_leading(64, ["0"])
function_signature = "0x212329f3"
data =
function_signature <>
reward_to_distribute <>
"00000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000" <>
staking_epoch <> "0000000000000000000000000000000000000000000000000000000000000000"
request = %{
id: 0,
method: "eth_call",
params: [
%{
to: block_reward_address,
data: data
},
"0x" <> Integer.to_string(block_number, 16)
]
}
result =
request
|> EthereumJSONRPC.request()
|> EthereumJSONRPC.json_rpc(json_rpc_named_arguments)
case result do
{:ok, response} ->
response =
response
|> String.replace_leading("0x", "")
|> Base.decode16!(case: :lower)
decoded = ABI.decode("res(uint256[])", response)
Enum.at(decoded, 0)
{:error, _} ->
[]
end
end
# makes a raw `eth_call` for the `currentTokenRewardToDistribute` function of the BlockReward contract:
# function currentTokenRewardToDistribute(
# address _stakingContract,
# uint256 _stakingEpoch,
# uint256 _totalRewardShareNum,
# uint256 _totalRewardShareDenom,
# uint256[] memory _validators
# ) public view returns(uint256 rewardToDistribute, uint256 totalReward);
def call_current_token_reward_to_distribute(
block_reward_address,
staking_contract_address,
staking_epoch,
block_number
) do
json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments)
staking_contract_address = address_pad_to_64(staking_contract_address)
staking_epoch =
staking_epoch
|> Integer.to_string(16)
|> String.pad_leading(64, ["0"])
function_signature = "0x43544960"
mandatory_params = staking_contract_address <> staking_epoch
optional_params =
"0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000"
data = function_signature <> mandatory_params <> optional_params
request = %{
id: 0,
method: "eth_call",
params: [
%{
to: block_reward_address,
data: data
},
"0x" <> Integer.to_string(block_number, 16)
]
}
result =
request
|> EthereumJSONRPC.request()
|> EthereumJSONRPC.json_rpc(json_rpc_named_arguments)
case result do
{:ok, response} ->
response =
response
|> String.replace_leading("0x", "")
|> Base.decode16!(case: :lower)
decoded = ABI.decode("res(uint256,uint256)", response)
Enum.at(decoded, 0)
{:error, _} ->
0
end
end
# makes a raw `eth_call` for the `getRewardAmount` function of the Staking contract:
# function getRewardAmount(
# uint256[] memory _stakingEpochs,
# address _poolStakingAddress,
# address _staker
# ) public view returns(uint256 tokenRewardSum, uint256 nativeRewardSum);
def call_get_reward_amount(
staking_contract_address,
staking_epochs,
pool_staking_address,
staker,
json_rpc_named_arguments
) do
staking_epochs_joint =
staking_epochs
|> Enum.map_join(fn epoch ->
epoch
|> Integer.to_string(16)
|> String.pad_leading(64, ["0"])
end)
pool_staking_address = address_pad_to_64(pool_staking_address)
staker = address_pad_to_64(staker)
staking_epochs_length =
staking_epochs
|> Enum.count()
|> Integer.to_string(16)
|> String.pad_leading(64, ["0"])
# `getRewardAmount` function signature
function_signature = "0xfb367a9b"
# offset to the `_stakingEpochs` array
function_signature_with_offset = function_signature <> String.pad_leading("60", 64, ["0"])
# `_poolStakingAddress` parameter
function_with_param_1 = function_signature_with_offset <> pool_staking_address
# `_staker` parameter
function_with_param1_param2 = function_with_param_1 <> staker
# the length of `_stakingEpochs` array
function_with_param_1_length_param2 = function_with_param1_param2 <> staking_epochs_length
# encoded `_stakingEpochs` array
data = function_with_param_1_length_param2 <> staking_epochs_joint
request = %{
id: 0,
method: "eth_call",
params: [
%{
to: staking_contract_address,
data: data
}
]
}
result =
request
|> EthereumJSONRPC.request()
|> EthereumJSONRPC.json_rpc(json_rpc_named_arguments)
case result do
{:ok, response} ->
response = String.replace_leading(response, "0x", "")
if String.length(response) != 64 * 2 do
{:error, "Invalid getRewardAmount response."}
else
{token_reward_sum, native_reward_sum} = String.split_at(response, 64)
token_reward_sum = String.to_integer(token_reward_sum, 16)
native_reward_sum = String.to_integer(native_reward_sum, 16)
{:ok, %{token_reward_sum: token_reward_sum, native_reward_sum: native_reward_sum}}
end
{:error, reason} ->
{:error, reason}
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_join(fn epoch ->
epoch
|> Integer.to_string(16)
|> String.pad_leading(64, ["0"])
end)
pool_staking_address = address_pad_to_64(pool_staking_address)
staking_epochs_length =
staking_epochs
|> Enum.count()
|> Integer.to_string(16)
|> String.pad_leading(64, ["0"])
# `claimReward` function signature
function_signature = "0x3ea15d62"
# offset to the `_stakingEpochs` array
function_signature_with_offset = function_signature <> String.pad_leading("40", 64, ["0"])
# `_poolStakingAddress` parameter
function_with_param_1 = function_signature_with_offset <> pool_staking_address
# the length of `_stakingEpochs` array
function_with_param_1_length_param2 = function_with_param_1 <> staking_epochs_length
# encoded `_stakingEpochs` array
data = function_with_param_1_length_param2 <> staking_epochs_joint
request = %{
id: 0,
method: "eth_estimateGas",
params: [
%{
from: staker,
to: staking_contract_address,
# 1 gwei
gasPrice: "0x3B9ACA00",
data: data
}
]
}
result =
request
|> EthereumJSONRPC.request()
|> 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]
def delegator_reward_request(args, block_number) do
[
# 5fba554e = keccak256(delegatorShare(uint256,uint256,uint256,uint256,uint256))
delegator_share: {:block_reward, "5fba554e", args, block_number}
]
end
def epochs_to_claim_reward_from_request(staking_address, staker) do
[
# 4de6c036 = keccak256(epochsToClaimRewardFrom(address,address))
epochs: {:block_reward, "4de6c036", [staking_address, staker]}
]
end
def get_delegator_pools_request(delegator, offset, length) do
[
# 2ebfaf4e = keccak256(getDelegatorPools(address,uint256,uint256))
pools: {:staking, "2ebfaf4e", [delegator, offset, length]}
]
end
def get_delegator_pools_length_request(delegator) do
[
# 8ba31a1c = keccak256(getDelegatorPoolsLength(address))
length: {:staking, "8ba31a1c", [delegator]}
]
end
def mining_by_id_request(pool_id, block_number) do
[
# e2847895 = keccak256(miningAddressById(uint256))
mining_address: {:validator_set, "e2847895", [pool_id], block_number}
]
end
def mining_by_staking_request(staking_address) do
[
# 00535175 = keccak256(miningByStakingAddress(address))
mining_address: {:validator_set, "00535175", [staking_address]}
]
end
def mining_by_staking_request(staking_address, block_number) do
[
# 00535175 = keccak256(miningByStakingAddress(address))
mining_address: {:validator_set, "00535175", [staking_address], block_number}
]
end
def pool_staking_requests(pool_id, block_number) do
[
active_delegators: active_delegators_request(pool_id, block_number)[:active_delegators],
# 378bf28b = keccak256(poolDescription(uint256))
description: {:validator_set, "378bf28b", [pool_id], block_number},
# a1fc2753 = keccak256(poolDelegatorsInactive(uint256))
inactive_delegators: {:staking, "a1fc2753", [pool_id], block_number},
# bbbaf8c8 = keccak256(isPoolActive(uint256))
is_active: {:staking, "bbbaf8c8", [pool_id], block_number},
mining_address_hash: mining_by_id_request(pool_id, block_number)[:mining_address],
name: pool_name_request(pool_id, block_number)[:name],
staking_address_hash: staking_by_id_request(pool_id, block_number)[:staking_address],
# 3fb1a1e4 = keccak256(stakeAmount(uint256,address))
self_staked_amount: {:staking, "3fb1a1e4", [pool_id, "0x0000000000000000000000000000000000000000"], block_number},
# 2a8f6ecd = keccak256(stakeAmountTotal(uint256))
total_staked_amount: {:staking, "2a8f6ecd", [pool_id], block_number},
# 3bf47e96 = keccak256(validatorRewardPercent(uint256))
validator_reward_percent: {:block_reward, "3bf47e96", [pool_id], block_number}
]
end
def pool_mining_requests(mining_address, block_number) do
[
# a881c5fd = keccak256(areDelegatorsBanned(address))
are_delegators_banned: {:validator_set, "a881c5fd", [mining_address], block_number},
# c9e9694d = keccak256(banReason(address))
ban_reason: {:validator_set, "c9e9694d", [mining_address], block_number},
# 5836d08a = keccak256(bannedUntil(address))
banned_until: {:validator_set, "5836d08a", [mining_address], block_number},
# 1a7fa237 = keccak256(bannedDelegatorsUntil(address))
banned_delegators_until: {:validator_set, "1a7fa237", [mining_address], block_number},
# a92252ae = keccak256(isValidatorBanned(address))
is_banned: {:validator_set, "a92252ae", [mining_address], block_number},
# b41832e4 = keccak256(validatorCounter(address))
was_validator_count: {:validator_set, "b41832e4", [mining_address], block_number},
# 1d0cd4c6 = keccak256(banCounter(address))
was_banned_count: {:validator_set, "1d0cd4c6", [mining_address], block_number}
]
end
def pool_name_request(pool_id, block_number) do
[
# cccf3a02 = keccak256(poolName(uint256))
name: {:validator_set, "cccf3a02", [pool_id], block_number}
]
end
def staker_requests(pool_id, pool_staking_address, staker_address, block_number) do
delegator_or_zero =
if staker_address == pool_staking_address do
"0x0000000000000000000000000000000000000000"
else
staker_address
end
[
# 950a6513 = keccak256(maxWithdrawOrderAllowed(address,address))
max_ordered_withdraw_allowed: {:staking, "950a6513", [pool_staking_address, staker_address], block_number},
# 6bda1577 = keccak256(maxWithdrawAllowed(address,address))
max_withdraw_allowed: {:staking, "6bda1577", [pool_staking_address, staker_address], block_number},
# e3f0ff66 = keccak256(orderedWithdrawAmount(uint256,address))
ordered_withdraw: {:staking, "e3f0ff66", [pool_id, delegator_or_zero], block_number},
# d2f2a136 = keccak256(orderWithdrawEpoch(uint256,address))
ordered_withdraw_epoch: {:staking, "d2f2a136", [pool_id, delegator_or_zero], block_number},
# 3fb1a1e4 = keccak256(stakeAmount(uint256,address))
stake_amount: {:staking, "3fb1a1e4", [pool_id, delegator_or_zero], block_number}
]
end
def staking_by_id_request(pool_id) do
[
# 16cf66ab = keccak256(stakingAddressById(uint256))
staking_address: {:validator_set, "16cf66ab", [pool_id]}
]
end
def staking_by_id_request(pool_id, block_number) do
[
# 16cf66ab = keccak256(stakingAddressById(uint256))
staking_address: {:validator_set, "16cf66ab", [pool_id], block_number}
]
end
def id_by_mining_request(mining_address, block_number) do
[
# 2bbb7b72 = keccak256(idByMiningAddress(address))
pool_id: {:validator_set, "2bbb7b72", [mining_address], block_number}
]
end
def id_by_staking_request(staking_address) do
[
# a26301f9 = keccak256(idByStakingAddress(address))
pool_id: {:validator_set, "a26301f9", [staking_address]}
]
end
def staking_by_mining_request(mining_address, block_number) do
[
# 1ee4d0bc = keccak256(stakingByMiningAddress(address))
staking_address: {:validator_set, "1ee4d0bc", [mining_address], block_number}
]
end
def validator_min_reward_percent_request(epoch_number, block_number) do
[
# cdf7a090 = keccak256(validatorMinRewardPercent(uint256))
value: {:block_reward, "cdf7a090", [epoch_number], block_number}
]
end
# args = [staking_epoch, validator_staked, total_staked, pool_reward \\ 10_00000]
def validator_reward_request(args, block_number) do
[
# 8737929a = keccak256(validatorShare(uint256,uint256,uint256,uint256))
validator_share: {:block_reward, "8737929a", args, block_number}
]
end
def perform_requests(requests, contracts, abi) do
requests
|> generate_requests(contracts)
|> Reader.query_contracts(abi)
|> parse_responses(requests)
end
def perform_grouped_requests(requests, keys, contracts, abi) do
requests
|> List.flatten()
|> generate_requests(contracts)
|> Reader.query_contracts(abi)
|> parse_grouped_responses(keys, requests)
end
def get_contract_events(contract_address, from_block, to_block, event_hash) do
json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments)
result =
%{
id: 0,
method: "eth_getLogs",
params: [
%{
fromBlock: "0x" <> Integer.to_string(from_block, 16),
toBlock: "0x" <> Integer.to_string(to_block, 16),
address: contract_address,
topics: [event_hash]
}
]
}
|> EthereumJSONRPC.request()
|> EthereumJSONRPC.json_rpc(json_rpc_named_arguments)
case result do
{:ok, events} ->
events
{:error, _reason} ->
[]
end
end
defp address_pad_to_64(address) do
address
|> String.replace_leading("0x", "")
|> String.pad_leading(64, ["0"])
end
defp generate_requests(functions, contracts) do
Enum.map(functions, fn
{_, {contract, method_id, args}} ->
%{
contract_address: contracts[contract],
method_id: method_id,
args: args
}
{_, {contract, method_id, args, block_number}} ->
%{
contract_address: contracts[contract],
method_id: method_id,
args: args,
block_number: block_number
}
end)
end
defp parse_responses(responses, requests) do
requests
|> Enum.zip(responses)
|> Enum.into(%{}, fn {{key, _}, {:ok, response}} ->
case response do
[item] -> {key, item}
items -> {key, items}
end
end)
end
defp parse_grouped_responses(responses, keys, grouped_requests) do
{grouped_responses, _} = Enum.map_reduce(grouped_requests, responses, &Enum.split(&2, length(&1)))
[keys, grouped_requests, grouped_responses]
|> Enum.zip()
|> Enum.into(%{}, fn {key, requests, responses} ->
{key, parse_responses(responses, requests)}
end)
end
end

File diff suppressed because it is too large Load Diff

@ -1,311 +0,0 @@
defmodule Explorer.Staking.StakeSnapshotting do
@moduledoc """
Makes snapshots of staked amounts.
"""
import Ecto.Query, only: [from: 2]
require Logger
alias Explorer.Chain
alias Explorer.Chain.Events.Publisher
alias Explorer.Chain.{StakingPool, StakingPoolsDelegator}
alias Explorer.Staking.ContractReader
def do_snapshotting(
%{contracts: contracts, abi: abi, ets_table_name: ets_table_name},
epoch_number,
cached_pool_staking_responses,
pools_mining_addresses,
mining_to_staking_address,
mining_address_to_id,
block_number
) do
# get pool ids and staking addresses for the pending validators
pool_ids =
pools_mining_addresses
|> Enum.map(&mining_address_to_id[&1])
pool_staking_addresses =
pools_mining_addresses
|> Enum.map(&mining_to_staking_address[&1])
id_to_mining_address =
pool_ids
|> Enum.zip(pools_mining_addresses)
|> Map.new()
id_to_staking_address =
pool_ids
|> Enum.zip(pool_staking_addresses)
|> Map.new()
# get snapshotted amounts and active delegator list for the pool for each
# pending validator by their pool id.
# use `cached_pool_staking_responses` when possible
pool_staking_responses =
pool_ids
|> Enum.map(fn pool_id ->
case Map.fetch(cached_pool_staking_responses, pool_id) do
{:ok, resp} ->
snapshotted_pool_amounts_requests(pool_id, resp.staking_address_hash, block_number)
:error ->
pool_staking_address = id_to_staking_address[pool_id]
ContractReader.active_delegators_request(pool_id, block_number) ++
snapshotted_pool_amounts_requests(pool_id, pool_staking_address, block_number)
end
end)
|> ContractReader.perform_grouped_requests(pool_ids, contracts, abi)
|> Map.new(fn {pool_id, resp} ->
{pool_id,
case Map.fetch(cached_pool_staking_responses, pool_id) do
{:ok, cached_resp} ->
Map.merge(cached_resp, resp)
:error ->
pool_staking_address = id_to_staking_address[pool_id]
Map.merge(%{staking_address_hash: pool_staking_address}, resp)
end}
end)
# get a flat list of all stakers of each validator
# in the form of {pool_id, pool_staking_address, staker_address}
stakers =
Enum.flat_map(pool_staking_responses, fn {pool_id, resp} ->
[{pool_id, resp.staking_address_hash, resp.staking_address_hash}] ++
Enum.map(resp.active_delegators, &{pool_id, resp.staking_address_hash, &1})
end)
# read info about each staker from the contracts
staker_responses = get_staker_responses(stakers, block_number, contracts, abi)
# call `BlockReward.validatorShare` function for each pool
# to get validator's reward share of the pool (needed for the `Delegators` list in UI)
validator_reward_responses =
get_validator_reward_responses(pool_staking_responses, epoch_number, block_number, contracts, abi)
# call `BlockReward.delegatorShare` function for each delegator
# to get their reward share of the pool (needed for the `Delegators` list in UI)
delegator_reward_responses =
staker_responses
|> get_delegator_responses()
|> get_delegator_reward_responses(pool_staking_responses, epoch_number, block_number, contracts, abi)
# form entries for updating the `staking_pools` table in DB
pool_entries =
Enum.map(pool_ids, fn pool_id ->
staking_resp = pool_staking_responses[pool_id]
validator_reward_resp = validator_reward_responses[pool_id]
pool_staking_address = id_to_staking_address[pool_id]
%{
banned_until: 0,
is_active: false,
is_banned: false,
is_unremovable: false,
is_validator: false,
staking_address_hash: pool_staking_address,
delegators_count: 0,
mining_address_hash: address_bytes_to_string(id_to_mining_address[pool_id]),
self_staked_amount: 0,
snapshotted_self_staked_amount: staking_resp.snapshotted_self_staked_amount,
snapshotted_total_staked_amount: staking_resp.snapshotted_total_staked_amount,
snapshotted_validator_reward_ratio: Float.floor(validator_reward_resp.validator_share / 10_000, 2),
total_staked_amount: 0,
was_banned_count: 0,
was_validator_count: 0
}
end)
# form entries for updating the `staking_pools_delegators` table in DB
delegator_entries =
Enum.map(staker_responses, fn {{_pool_id, pool_staking_address, staker_address} = key, resp} ->
delegator_share =
if Map.has_key?(delegator_reward_responses, key) do
delegator_reward_responses[key].delegator_share
else
0
end
%{
address_hash: staker_address,
is_active: false,
max_ordered_withdraw_allowed: 0,
max_withdraw_allowed: 0,
ordered_withdraw: 0,
ordered_withdraw_epoch: 0,
snapshotted_reward_ratio: Float.floor(delegator_share / 10_000, 2),
snapshotted_stake_amount: resp.snapshotted_stake_amount,
stake_amount: 0,
staking_address_hash: pool_staking_address
}
end)
# perform SQL queries
case Chain.import(%{
staking_pools: %{params: pool_entries, on_conflict: staking_pools_update(), clear_snapshotted_values: true},
staking_pools_delegators: %{
params: delegator_entries,
on_conflict: staking_pools_delegators_update(),
clear_snapshotted_values: true
},
timeout: :infinity
}) do
{:ok, _} -> :ets.insert(ets_table_name, snapshotted_epoch_number: epoch_number)
_ -> Logger.error("Cannot successfully finish snapshotting for the epoch #{epoch_number - 1}")
end
:ets.insert(ets_table_name, is_snapshotting: false)
Publisher.broadcast(:stake_snapshotting_finished)
end
defp address_bytes_to_string(hash), do: "0x" <> Base.encode16(hash, case: :lower)
defp get_delegator_responses(staker_responses) do
Enum.reduce(staker_responses, %{}, fn {{_pool_id, pool_staking_address, staker_address} = key, value}, acc ->
if pool_staking_address != staker_address do
Map.put(acc, key, value)
else
acc
end
end)
end
defp get_delegator_reward_responses(
delegator_responses,
pool_staking_responses,
epoch_number,
block_number,
contracts,
abi
) do
delegator_keys = Enum.map(delegator_responses, fn {key, _} -> key end)
delegator_requests =
delegator_responses
|> Enum.map(fn {{pool_id, _pool_staking_address, _staker_address}, resp} ->
staking_resp = pool_staking_responses[pool_id]
ContractReader.delegator_reward_request(
[
epoch_number,
resp.snapshotted_stake_amount,
staking_resp.snapshotted_self_staked_amount,
staking_resp.snapshotted_total_staked_amount,
1000_000
],
block_number
)
end)
chunk_size = 100
chunks = 0..trunc(ceil(Enum.count(delegator_keys) / chunk_size) - 1)
Enum.reduce(chunks, %{}, fn i, acc ->
delegator_keys_slice = Enum.slice(delegator_keys, i * chunk_size, chunk_size)
responses =
delegator_requests
|> Enum.slice(i * chunk_size, chunk_size)
|> ContractReader.perform_grouped_requests(delegator_keys_slice, contracts, abi)
Map.merge(acc, responses)
end)
end
defp get_staker_responses(stakers, block_number, contracts, abi) do
# we split batch requests by chunks
chunk_size = 100
chunks = 0..trunc(ceil(Enum.count(stakers) / chunk_size) - 1)
Enum.reduce(chunks, %{}, fn i, acc ->
stakers_slice = Enum.slice(stakers, i * chunk_size, chunk_size)
responses =
stakers_slice
|> Enum.map(fn {pool_id, pool_staking_address, staker_address} ->
snapshotted_staker_amount_request(pool_id, pool_staking_address, staker_address, block_number)
end)
|> ContractReader.perform_grouped_requests(stakers_slice, contracts, abi)
Map.merge(acc, responses)
end)
end
defp get_validator_reward_responses(pool_staking_responses, epoch_number, block_number, contracts, abi) do
# to keep sort order when using `perform_grouped_requests` (see below)
pool_ids = Enum.map(pool_staking_responses, fn {pool_id, _} -> pool_id end)
pool_staking_responses
|> Enum.map(fn {_pool_id, resp} ->
ContractReader.validator_reward_request(
[
epoch_number,
resp.snapshotted_self_staked_amount,
resp.snapshotted_total_staked_amount,
1000_000
],
block_number
)
end)
|> ContractReader.perform_grouped_requests(pool_ids, contracts, abi)
end
defp snapshotted_pool_amounts_requests(pool_id, pool_staking_address, block_number) do
[
# 2a8f6ecd = keccak256(stakeAmountTotal(uint256))
snapshotted_total_staked_amount: {:staking, "2a8f6ecd", [pool_id], block_number},
snapshotted_self_staked_amount:
snapshotted_staker_amount_request(
pool_id,
pool_staking_address,
pool_staking_address,
block_number
)[:snapshotted_stake_amount]
]
end
defp snapshotted_staker_amount_request(pool_id, pool_staking_address, staker_address, block_number) do
delegator_or_zero =
if staker_address == pool_staking_address do
"0x0000000000000000000000000000000000000000"
else
staker_address
end
[
# 3fb1a1e4 = keccak256(stakeAmount(uint256,address))
snapshotted_stake_amount: {:staking, "3fb1a1e4", [pool_id, delegator_or_zero], block_number}
]
end
defp staking_pools_update do
from(
pool in StakingPool,
update: [
set: [
snapshotted_self_staked_amount: fragment("EXCLUDED.snapshotted_self_staked_amount"),
snapshotted_total_staked_amount: fragment("EXCLUDED.snapshotted_total_staked_amount"),
snapshotted_validator_reward_ratio: fragment("EXCLUDED.snapshotted_validator_reward_ratio"),
updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", pool.updated_at)
]
]
)
end
defp staking_pools_delegators_update do
from(
delegator in StakingPoolsDelegator,
update: [
set: [
snapshotted_reward_ratio: fragment("EXCLUDED.snapshotted_reward_ratio"),
snapshotted_stake_amount: fragment("EXCLUDED.snapshotted_stake_amount"),
updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", delegator.updated_at)
]
]
)
end
end

@ -1,32 +0,0 @@
defmodule Explorer.Chain.Import.Runner.StakingPoolsDelegatorsTest do
use Explorer.DataCase
import Explorer.Factory
alias Ecto.Multi
alias Explorer.Chain.Import.Runner.StakingPoolsDelegators
alias Explorer.Chain.StakingPoolsDelegator
describe "run/1" do
test "insert new pools list" do
delegators =
[params_for(:staking_pools_delegator), params_for(:staking_pools_delegator)]
|> Enum.map(fn param ->
changeset = StakingPoolsDelegator.changeset(%StakingPoolsDelegator{}, param)
changeset.changes
end)
assert {:ok, %{insert_staking_pools_delegators: list}} = run_changes(delegators)
assert Enum.count(list) == Enum.count(delegators)
end
end
defp run_changes(changes) do
Multi.new()
|> StakingPoolsDelegators.run(changes, %{
timeout: :infinity,
timestamps: %{inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()}
})
|> Repo.transaction()
end
end

@ -1,33 +0,0 @@
defmodule Explorer.Chain.Import.Runner.StakingPoolsTest do
use Explorer.DataCase
import Explorer.Factory
alias Ecto.Multi
alias Explorer.Chain.Import.Runner.StakingPools
alias Explorer.Chain.StakingPool
describe "run/1" do
test "insert new pools list" do
pools =
[_pool1, _pool2] =
[params_for(:staking_pool), params_for(:staking_pool)]
|> Enum.map(fn param ->
changeset = StakingPool.changeset(%StakingPool{}, param)
changeset.changes
end)
assert {:ok, %{insert_staking_pools: list}} = run_changes(pools)
assert Enum.count(list) == Enum.count(pools)
end
end
defp run_changes(changes) do
Multi.new()
|> StakingPools.run(changes, %{
timeout: :infinity,
timestamps: %{inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()}
})
|> Repo.transaction()
end
end

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save