parent
4886204f27
commit
969c222d43
@ -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, '"') |
||||
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 |
||||
} |
Before Width: | Height: | Size: 532 B |
Before Width: | Height: | Size: 412 B |
@ -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 |
@ -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,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 +0,0 @@ |
||||
<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") %> |
||||
<label><input type="radio" name="epoch_choice" value="all" id="epoch-choice-all" checked="checked" /> <%= gettext("all epochs") %></label> |
||||
<label><input type="radio" name="epoch_choice" value="specified" /> <%= 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 |
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 |
@ -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 |
@ -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…
Reference in new issue