diff --git a/src/actions/stake.js b/src/actions/stake.js index 570f508..1c77b01 100644 --- a/src/actions/stake.js +++ b/src/actions/stake.js @@ -5,6 +5,9 @@ import { CLAIM_REWARDS_DIALOG_HIDE, CLAIM_REWARDS_DIALOG_SHOW, CLAIM_REWARDS_VALIDATOR_SET, + CLAIM_DELEGATE_DIALOG_SHOW, + CLAIM_DELEGATE_DIALOG_HIDE, + CLAIM_DELEGATE_VALIDATOR_SET, DELEGATE_DIALOG_HIDE, DELEGATE_DIALOG_SHOW, DELEGATE_FAILED_DIALOG_HIDE, @@ -32,6 +35,7 @@ import { VALIDATORS_FETCH_ERROR, VALIDATORS_FETCH_IN_PROGRESS, VALIDATORS_FETCH_SUCCESS, + SELECTED_MULTI_VALIDATORS, } from '../constants/stake'; import Axios from 'axios'; import { @@ -274,6 +278,25 @@ export const setClaimRewardsValidator = (value) => { }; }; +export const showClaimDelegateDialog = () => { + return { + type: CLAIM_DELEGATE_DIALOG_SHOW, + }; +}; + +export const hideClaimDelegateDialog = () => { + return { + type: CLAIM_DELEGATE_DIALOG_HIDE, + }; +}; + +export const setClaimDelegateValidator = (value) => { + return { + type: CLAIM_DELEGATE_VALIDATOR_SET, + value, + }; +}; + const fetchValidatorImageInProgress = () => { return { type: VALIDATOR_IMAGE_FETCH_IN_PROGRESS, @@ -428,3 +451,10 @@ export const fetchAPR = () => (dispatch) => { } })(); }; + +export const selectMultiValidators = (value) => { + return { + type: SELECTED_MULTI_VALIDATORS, + value, + }; +}; diff --git a/src/app.css b/src/app.css index b8c7397..f96ef03 100644 --- a/src/app.css +++ b/src/app.css @@ -178,3 +178,33 @@ html::-webkit-scrollbar-thumb, margin: 10px; } } + +@keyframes fadeInAnimation { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +.stake .stake_content, +.proposals .proposals_content, +.home .card { + animation: fadeInAnimation ease-in-out 1s; +} + +@keyframes dialogOpen { + from { + transform: scale(0.5); + } + + to { + transform: scale(1); + } +} + +.dialog { + animation: dialogOpen 0.1s; +} diff --git a/src/constants/stake.js b/src/constants/stake.js index 24c34ef..00ce4fc 100644 --- a/src/constants/stake.js +++ b/src/constants/stake.js @@ -31,9 +31,12 @@ export const DELEGATED_VALIDATORS_FETCH_ERROR = 'DELEGATED_VALIDATORS_FETCH_ERRO export const CLAIM_REWARDS_DIALOG_SHOW = 'CLAIM_REWARDS_DIALOG_SHOW'; export const CLAIM_REWARDS_DIALOG_HIDE = 'CLAIM_REWARDS_DIALOG_HIDE'; - export const CLAIM_REWARDS_VALIDATOR_SET = 'CLAIM_REWARDS_VALIDATOR_SET'; +export const CLAIM_DELEGATE_DIALOG_SHOW = 'CLAIM_DELEGATE_DIALOG_SHOW'; +export const CLAIM_DELEGATE_DIALOG_HIDE = 'CLAIM_DELEGATE_DIALOG_HIDE'; +export const CLAIM_DELEGATE_VALIDATOR_SET = 'CLAIM_DELEGATE_VALIDATOR_SET'; + export const VALIDATOR_IMAGE_FETCH_IN_PROGRESS = 'VALIDATOR_IMAGE_FETCH_IN_PROGRESS'; export const VALIDATOR_IMAGE_FETCH_SUCCESS = 'VALIDATOR_IMAGE_FETCH_SUCCESS'; export const VALIDATOR_IMAGE_FETCH_ERROR = 'VALIDATOR_IMAGE_FETCH_ERROR'; @@ -45,3 +48,5 @@ export const INACTIVE_VALIDATORS_FETCH_ERROR = 'INACTIVE_VALIDATORS_FETCH_ERROR' export const APR_FETCH_IN_PROGRESS = 'APR_FETCH_IN_PROGRESS'; export const APR_FETCH_SUCCESS = 'APR_FETCH_SUCCESS'; export const APR_FETCH_ERROR = 'APR_FETCH_ERROR'; + +export const SELECTED_MULTI_VALIDATORS = 'SELECTED_MULTI_VALIDATORS'; diff --git a/src/containers/Home/ClaimDialog/ClaimDelegateDialog.js b/src/containers/Home/ClaimDialog/ClaimDelegateDialog.js new file mode 100644 index 0000000..72050bf --- /dev/null +++ b/src/containers/Home/ClaimDialog/ClaimDelegateDialog.js @@ -0,0 +1,248 @@ +import React, { useState } from 'react'; +import { Button, Dialog, DialogActions, DialogContent } from '@material-ui/core'; +import * as PropTypes from 'prop-types'; +import { + hideClaimDelegateDialog, + setTokens, + showDelegateFailedDialog, + showDelegateProcessingDialog, + showDelegateSuccessDialog, +} from '../../../actions/stake'; +import { connect } from 'react-redux'; +import '../../Stake/DelegateDialog/index.css'; +import { cosmoStationSign, signTxAndBroadcast } from '../../../helper'; +import { showMessage } from '../../../actions/snackbar'; +import { fetchRewards, fetchVestingBalance, getBalance } from '../../../actions/accounts'; +import { config } from '../../../config'; +import variables from '../../../utils/variables'; +import CircularProgress from '../../../components/CircularProgress'; +import { gas } from '../../../defaultGasValues'; +import ClaimDelegateValidatorsSelectField from './ClaimDelegateValidatorSelectField'; + +const ClaimDelegateDialog = (props) => { + const [inProgress, setInProgress] = useState(false); + + const handleClaimAll = () => { + setInProgress(true); + let gasValue = gas.claim_reward + gas.delegate; + let count = 0; + if (props.rewards && props.rewards.rewards && props.rewards.rewards.length > 1) { + props.rewards.rewards.map((item) => { + const tokens = item && item.reward && item.reward.length && + item.reward.filter((val) => val.amount > gasValue * config.GAS_PRICE_STEP_AVERAGE); + if (tokens) { + count += tokens.length; + } + return null; + }); + } + if (count) { + gasValue = count * gasValue / 1.1 + gasValue; + } + + const updatedTx = { + msgs: [], + fee: { + amount: [{ + amount: String(gasValue * config.GAS_PRICE_STEP_AVERAGE), + denom: config.COIN_MINIMAL_DENOM, + }], + gas: String(gasValue), + }, + memo: '', + }; + if (props.rewards && props.rewards.rewards && + props.rewards.rewards.length) { + props.rewards.rewards.map((item) => { + let tokens = item && item.reward && item.reward.length && + item.reward.find((val) => val.denom === config.COIN_MINIMAL_DENOM); + tokens = tokens && tokens.amount; + if (tokens && tokens > ((gas.claim_reward + gas.delegate) * config.GAS_PRICE_STEP_AVERAGE)) { + updatedTx.msgs.push({ + typeUrl: '/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward', + value: { + delegatorAddress: props.address, + validatorAddress: item.validator_address, + }, + }, { + typeUrl: '/cosmos.staking.v1beta1.MsgDelegate', + value: { + delegatorAddress: props.address, + validatorAddress: item.validator_address, + amount: { + amount: String(Math.floor(Number(tokens))), + denom: config.COIN_MINIMAL_DENOM, + }, + }, + }); + } + return null; + }); + } + + if (localStorage.getItem('of_co_wallet') === 'cosmostation') { + cosmoStationSign(updatedTx, props.address, handleFetch); + return; + } + + signTxAndBroadcast(updatedTx, props.address, handleFetch); + }; + + const handleFetch = (error, result) => { + setInProgress(false); + if (error) { + if (error.indexOf('not yet found on the chain') > -1) { + props.pendingDialog(); + return; + } + props.failedDialog(); + props.showMessage(error); + return; + } + if (result) { + props.setTokens(tokens); + props.successDialog(result.transactionHash); + props.fetchRewards(props.address); + props.getBalance(props.address); + props.fetchVestingBalance(props.address); + } + }; + + const handleClaim = () => { + setInProgress(true); + const updatedTx = { + msgs: [{ + typeUrl: '/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward', + value: { + delegatorAddress: props.address, + validatorAddress: props.value, + }, + }, { + typeUrl: '/cosmos.staking.v1beta1.MsgDelegate', + value: { + delegatorAddress: props.address, + validatorAddress: props.value, + amount: { + amount: String(Math.floor(Number(delegateableTokesn))), + denom: config.COIN_MINIMAL_DENOM, + }, + }, + }], + fee: { + amount: [{ + amount: String((gas.claim_reward + gas.delegate) * config.GAS_PRICE_STEP_AVERAGE), + denom: config.COIN_MINIMAL_DENOM, + }], + gas: String(gas.claim_reward + gas.delegate), + }, + memo: '', + }; + + if (localStorage.getItem('of_co_wallet') === 'cosmostation') { + cosmoStationSign(updatedTx, props.address, handleFetch); + return; + } + + signTxAndBroadcast(updatedTx, props.address, handleFetch); + }; + + const rewards = props.rewards && props.rewards.rewards && + props.rewards.rewards.length && + props.rewards.rewards.filter((value) => value.validator_address === props.value); + + let tokens = rewards && rewards.length && rewards[0] && rewards[0].reward && + rewards[0].reward.length && rewards[0].reward.find((val) => val.denom === config.COIN_MINIMAL_DENOM); + const delegateableTokesn = tokens && tokens.amount; + tokens = tokens && tokens.amount ? tokens.amount / 10 ** config.COIN_DECIMALS : 0; + + if (props.value === 'all' && props.rewards && props.rewards.rewards && + props.rewards.rewards.length) { + const gasValue = (gas.claim_reward + gas.delegate) * config.GAS_PRICE_STEP_AVERAGE; + let total = 0; + + props.rewards.rewards.map((value) => { + let rewards = value.reward && value.reward.length && + value.reward.find((val) => val.denom === config.COIN_MINIMAL_DENOM); + rewards = rewards && rewards.amount && rewards.amount > gasValue ? rewards.amount / 10 ** config.COIN_DECIMALS : 0; + total = rewards + total; + + return total; + }); + tokens = total; + } + + const disable = props.value === 'none' || inProgress; + + return ( + + {inProgress && } + +

Claim and Delegate Rewards

+

Select validator

+ + {tokens && tokens > 0 + ?

rewards: {tokens.toFixed(4)}

+ : null} +
+ + + +
+ ); +}; + +ClaimDelegateDialog.propTypes = { + failedDialog: PropTypes.func.isRequired, + fetchRewards: PropTypes.func.isRequired, + fetchVestingBalance: PropTypes.func.isRequired, + getBalance: PropTypes.func.isRequired, + handleClose: PropTypes.func.isRequired, + lang: PropTypes.string.isRequired, + open: PropTypes.bool.isRequired, + pendingDialog: PropTypes.func.isRequired, + rewards: PropTypes.shape({ + rewards: PropTypes.array, + total: PropTypes.array, + }).isRequired, + setTokens: PropTypes.func.isRequired, + showMessage: PropTypes.func.isRequired, + successDialog: PropTypes.func.isRequired, + value: PropTypes.string.isRequired, + address: PropTypes.string, +}; + +const stateToProps = (state) => { + return { + address: state.accounts.address.value, + lang: state.language, + open: state.stake.claimDelegateDialog.open, + value: state.stake.claimDelegateDialog.validator, + rewards: state.accounts.rewards.result, + }; +}; + +const actionToProps = { + handleClose: hideClaimDelegateDialog, + failedDialog: showDelegateFailedDialog, + successDialog: showDelegateSuccessDialog, + pendingDialog: showDelegateProcessingDialog, + getBalance, + fetchVestingBalance, + showMessage, + fetchRewards, + setTokens, +}; + +export default connect(stateToProps, actionToProps)(ClaimDelegateDialog); diff --git a/src/containers/Home/ClaimDialog/ClaimDelegateValidatorSelectField.js b/src/containers/Home/ClaimDialog/ClaimDelegateValidatorSelectField.js new file mode 100644 index 0000000..bbc0a31 --- /dev/null +++ b/src/containers/Home/ClaimDialog/ClaimDelegateValidatorSelectField.js @@ -0,0 +1,136 @@ +import React from 'react'; +import * as PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import SelectField from '../../../components/SelectField/WithChildren'; +import { setClaimDelegateValidator } from '../../../actions/stake'; +import { MenuItem } from '@material-ui/core'; +import { config } from '../../../config'; +import { gas } from '../../../defaultGasValues'; + +const colors = ['#0023DA', '#C9387E', '#EC2C00', '#80E3F2', + '#E86FC5', '#1F3278', '#FFE761', '#7041B9']; + +const ClaimDelegateValidatorSelectField = (props) => { + const gasValue = (gas.claim_reward + gas.delegate) * config.GAS_PRICE_STEP_AVERAGE; + const handleChange = (value) => { + if (props.value === value) { + return; + } + + props.onChange(value); + }; + + let total = 0; + const totalRewards = props.rewards && props.rewards.rewards && + props.rewards.rewards.length && props.rewards.rewards.map((value) => { + let rewards = value.reward && value.reward.length && + value.reward.find((val) => val.denom === config.COIN_MINIMAL_DENOM); + rewards = rewards && rewards.amount && rewards.amount > gasValue ? rewards.amount / 10 ** config.COIN_DECIMALS : 0; + if (rewards) { + total = rewards + total; + return total; + } + return null; + }); + + return ( + + + Select the validator + + {props.rewards && props.rewards.rewards && + props.rewards.rewards.length && + props.rewards.rewards.map((item, index) => { + const validator = item && item.validator_address && props.validatorList && props.validatorList.length && + props.validatorList.filter((value) => value.operator_address === item.validator_address); + + const image = validator && validator.length && validator[0] && + validator[0].description && validator[0].description.identity && + props.validatorImages && props.validatorImages.length && + props.validatorImages.filter((value) => value._id === validator[0].description.identity.toString()); + + let rewards = item.reward && item.reward.length && + item.reward.find((val) => val.denom === config.COIN_MINIMAL_DENOM); + rewards = rewards && rewards.amount && rewards.amount > gasValue ? rewards.amount / 10 ** config.COIN_DECIMALS : 0; + + return ( + rewards && rewards > 0.000001 + ? + {image && image.length && image[0] && image[0].them && image[0].them.length && + image[0].them[0] && image[0].them[0].pictures && image[0].them[0].pictures.primary && + image[0].them[0].pictures.primary.url + ? {validator + : } + {props.validatorList && props.validatorList.map((value) => { + if (value.operator_address === item.validator_address) { + return + {value.description && value.description.moniker} + {rewards && rewards > 0 + ?  ({rewards.toFixed(4)}) + : null} + ; + } + + return null; + })} + : null + ); + }, + )} + {totalRewards && totalRewards.length && + + + All  ({total.toFixed(4)}) + + } + + ); +}; + +ClaimDelegateValidatorSelectField.propTypes = { + rewards: PropTypes.shape({ + rewards: PropTypes.array, + total: PropTypes.array, + }).isRequired, + validatorImages: PropTypes.array.isRequired, + value: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, + inProgress: PropTypes.bool, + items: PropTypes.array, + validatorList: PropTypes.arrayOf( + PropTypes.shape({ + operator_address: PropTypes.string, + status: PropTypes.number, + description: PropTypes.shape({ + moniker: PropTypes.string, + }), + }), + ), +}; + +const stateToProps = (state) => { + return { + value: state.stake.claimDelegateDialog.validator, + rewards: state.accounts.rewards.result, + validatorList: state.stake.validators.list, + inProgress: state.accounts.rewards.inProgress, + validatorImages: state.stake.validators.images, + }; +}; + +const actionToProps = { + onChange: setClaimDelegateValidator, +}; + +export default connect(stateToProps, actionToProps)(ClaimDelegateValidatorSelectField); diff --git a/src/containers/Home/ClaimDialog/index.js b/src/containers/Home/ClaimDialog/index.js index 3dd30c0..fbb34bd 100644 --- a/src/containers/Home/ClaimDialog/index.js +++ b/src/containers/Home/ClaimDialog/index.js @@ -26,7 +26,7 @@ const ClaimDialog = (props) => { setInProgress(true); let gasValue = gas.claim_reward; if (props.rewards && props.rewards.rewards && props.rewards.rewards.length > 1) { - gasValue = props.rewards.rewards.length * gas.claim_reward/1.1 + gas.claim_reward; + gasValue = props.rewards.rewards.length * gas.claim_reward / 1.1 + gas.claim_reward; } const updatedTx = { diff --git a/src/containers/Home/TokenDetails/Compound.js b/src/containers/Home/TokenDetails/Compound.js new file mode 100644 index 0000000..80a0585 --- /dev/null +++ b/src/containers/Home/TokenDetails/Compound.js @@ -0,0 +1,50 @@ +import React from 'react'; +import { Button } from '@material-ui/core'; +import * as PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import variables from '../../../utils/variables'; +import { showClaimDelegateDialog } from '../../../actions/stake'; +import { showMessage } from '../../../actions/snackbar'; + +const Compound = (props) => { + const handleClick = () => { + if (!props.address) { + props.showMessage(variables[props.lang]['connect_account']); + return; + } + props.handleOpen('Compound'); + }; + + return ( + + ); +}; + +Compound.propTypes = { + disable: PropTypes.bool.isRequired, + handleOpen: PropTypes.func.isRequired, + lang: PropTypes.string.isRequired, + showMessage: PropTypes.func.isRequired, + address: PropTypes.string, +}; + +const stateToProps = (state) => { + return { + address: state.accounts.address.value, + lang: state.language, + }; +}; + +const actionToProps = { + handleOpen: showClaimDelegateDialog, + showMessage, + +}; + +export default connect(stateToProps, actionToProps)(Compound); diff --git a/src/containers/Home/TokenDetails/index.js b/src/containers/Home/TokenDetails/index.js index 0a4bd88..0b6943d 100644 --- a/src/containers/Home/TokenDetails/index.js +++ b/src/containers/Home/TokenDetails/index.js @@ -11,7 +11,9 @@ import StakeTokensButton from './StakeTokensButton'; import UnDelegateButton from './UnDelegateButton'; import ReDelegateButton from './ReDelegateButton'; import ClaimButton from './ClaimButton'; +// import Compound from './Compound'; import { config } from '../../../config'; +import { gas } from '../../../defaultGasValues'; const TokenDetails = (props) => { const staked = props.delegations && props.delegations.reduce((accumulator, currentValue) => { @@ -31,9 +33,13 @@ const TokenDetails = (props) => { return null; }); + const gasValue = (gas.claim_reward + gas.delegate) * config.GAS_PRICE_STEP_AVERAGE; let rewards = props.rewards && props.rewards.total && props.rewards.total.length && props.rewards.total.find((val) => val.denom === config.COIN_MINIMAL_DENOM); rewards = rewards && rewards.amount ? rewards.amount / 10 ** config.COIN_DECIMALS : 0; + let tokens = props.rewards && props.rewards.total && props.rewards.total.length && + props.rewards.total.find((val) => val.amount > gasValue); + tokens = tokens && tokens.amount ? tokens.amount / 10 ** config.COIN_DECIMALS : 0; return (
@@ -65,6 +71,8 @@ const TokenDetails = (props) => {
+ {/* */} + {/* */}
diff --git a/src/containers/Home/index.css b/src/containers/Home/index.css index 90f25d7..39f1865 100644 --- a/src/containers/Home/index.css +++ b/src/containers/Home/index.css @@ -159,13 +159,14 @@ .content_div .view_all { background: #FFFFFF; - border-radius: 50px; + border-radius: 30px; font-family: 'Blinker', sans-serif; font-weight: 600; - font-size: 20px; + font-size: 18px; line-height: 130%; color: #000000; - padding: 8px 20px; + padding: 9px 20px; + margin-left: 5px; } .content_div .view_all:hover { @@ -173,6 +174,12 @@ } @media (max-width: 2000px) { + .stake .stake_content .heading .buttons { + display: flex; + } +} + +@media (max-width: 1441px) { .home .card { flex-direction: column; text-align: center; @@ -180,7 +187,7 @@ } } -@media (max-width: 769px) { +@media (max-width: 770px) { .home .card { padding: 50px 20px; } diff --git a/src/containers/Home/index.js b/src/containers/Home/index.js index 95c4b0e..f9462ea 100644 --- a/src/containers/Home/index.js +++ b/src/containers/Home/index.js @@ -8,6 +8,7 @@ import DelegateDialog from '../Stake/DelegateDialog'; import SuccessDialog from '../Stake/DelegateDialog/SuccessDialog'; import UnSuccessDialog from '../Stake/DelegateDialog/UnSuccessDialog'; import ClaimDialog from './ClaimDialog'; +import ClaimDelegateDialog from './ClaimDialog/ClaimDelegateDialog'; import Table from '../Stake/Table'; import { Button } from '@material-ui/core'; import Cards from '../Proposals/Cards'; @@ -15,6 +16,7 @@ import ProposalDialog from '../Proposals/ProposalDialog'; import { withRouter } from 'react-router'; import { connect } from 'react-redux'; import PendingDialog from '../Stake/DelegateDialog/PendingDialog'; +import MultiDelegateButton from '../Stake/MultiDelegateButton'; class Home extends Component { constructor (props) { @@ -110,9 +112,12 @@ class Home extends Component { : null}

- +
+ + +
@@ -143,6 +148,7 @@ class Home extends Component { + ); } diff --git a/src/containers/Stake/DelegateDialog/MultiValidatorSelectField.js b/src/containers/Stake/DelegateDialog/MultiValidatorSelectField.js new file mode 100644 index 0000000..b65cb65 --- /dev/null +++ b/src/containers/Stake/DelegateDialog/MultiValidatorSelectField.js @@ -0,0 +1,135 @@ +import React from 'react'; +import * as PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { setValidator, selectMultiValidators } from '../../../actions/stake'; +import { MenuItem, Checkbox, FormControl, Select, OutlinedInput, Divider, ListItemIcon } from '@material-ui/core'; + +const colors = ['#0023DA', '#C9387E', '#EC2C00', '#80E3F2', + '#E86FC5', '#1F3278', '#FFE761', '#7041B9']; + +const MultiValidatorSelectField = (props) => { + const validatorList = [...props.validatorList]; + const handleChange = (event) => { + const value = event.target.value; + + if (value[value.length - 1] === 'all') { + props.selectMultiValidators(props.selectedMultiValidatorArray.length === validatorList.length ? [] : (validatorList.map((item) => item.operator_address))); + } else { + props.selectMultiValidators(typeof value === 'string' ? value.split(',') : value); + } + + if (props.value === value) { + return; + } + props.onChange(value); + }; + + const isAllSelected = validatorList.length > 0 && props.selectedMultiValidatorArray.length === validatorList.length; + + return ( + <> + + + + + ); +}; + +MultiValidatorSelectField.propTypes = { + selectMultiValidators: PropTypes.func.isRequired, + selectedMultiValidatorArray: PropTypes.array.isRequired, + validatorImages: PropTypes.array.isRequired, + value: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, + validatorList: PropTypes.arrayOf( + PropTypes.shape({ + operator_address: PropTypes.string, + status: PropTypes.number, + description: PropTypes.shape({ + moniker: PropTypes.string, + }), + }), + ), +}; + +const stateToProps = (state) => { + return { + value: state.stake.validator.value, + validatorList: state.stake.validators.list, + validatorImages: state.stake.validators.images, + selectedMultiValidatorArray: state.stake.selectMultiValidators.list, + }; +}; + +const actionToProps = { + onChange: setValidator, + selectMultiValidators, +}; + +export default connect(stateToProps, actionToProps)(MultiValidatorSelectField); diff --git a/src/containers/Stake/DelegateDialog/SuccessDialog.js b/src/containers/Stake/DelegateDialog/SuccessDialog.js index 5dee693..82b6a08 100644 --- a/src/containers/Stake/DelegateDialog/SuccessDialog.js +++ b/src/containers/Stake/DelegateDialog/SuccessDialog.js @@ -1,7 +1,7 @@ import React from 'react'; import * as PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import { Button, Dialog, DialogActions, DialogContent } from '@material-ui/core'; +import { Button, Dialog, DialogActions, DialogContent, ListItem, Tooltip, withStyles } from '@material-ui/core'; import './index.css'; import variables from '../../../utils/variables'; import { hideDelegateSuccessDialog } from '../../../actions/stake'; @@ -9,6 +9,30 @@ import success from '../../../assets/stake/success.svg'; import { config } from '../../../config'; import { withRouter } from 'react-router-dom'; +const colors = ['#0023DA', '#C9387E', '#EC2C00', '#80E3F2', + '#E86FC5', '#1F3278', '#FFE761', '#7041B9']; + +const CustomTooltip = withStyles(() => ({ + tooltip: { + maxWidth: '650px', + maxHeight: '180px', + background: '#1E1E1E', + color: '#ffffff', + overflow: 'auto', + scrollbarWidth: 'thin', + '&::-webkit-scrollbar': { + width: '4px', + }, + '&::-webkit-scrollbar-track': { + backgroundColor: '#1E1E1E', + }, + '&::-webkit-scrollbar-thumb': { + backgroundColor: '#ffffff', + borderRadius: '1px', + }, + }, +}))(Tooltip); + const SuccessDialog = (props) => { const handleRedirect = () => { if (config.EXPLORER_URL) { @@ -29,6 +53,7 @@ const SuccessDialog = (props) => { props.validatorList.find((val) => val.operator_address === props.validator); const toValidatorDetails = props.validatorList && props.validatorList.length && props.validatorList.find((val) => val.operator_address === props.toValidator); + const delegatedList = props.validatorList.filter((item) => props.selectedMultiValidatorArray.includes(item.operator_address)); return ( {
success - {props.name + {props.name && props.name !== 'Multi-Delegate' ?

{props.name + 'd Successfully'}

- : props.match && props.match.params && props.match.params.proposalID - ?

{variables[props.lang].vote_success}

- : props.claimValidator && props.claimValidator !== 'none' - ?

{variables[props.lang].claimed_success}

- :

{variables[props.lang].success}

} + : props.name + ?

{variables[props.lang].delegate + 'd Successfully'}

+ : props.match && props.match.params && props.match.params.proposalID + ?

{variables[props.lang].vote_success}

+ : props.claimValidator && props.claimValidator !== 'none' + ?

{variables[props.lang].claimed_success}

+ :

{variables[props.lang].success}

}
{props.match && props.match.params && props.match.params.proposalID && props.hash ?
@@ -127,19 +154,80 @@ const SuccessDialog = (props) => {
- :
-

{variables[props.lang]['validator_address']}

-
-
-

{props.validator}

- {props.validator && + : props.name === 'Multi-Delegate' + ? <> +
+

{variables[props.lang]['number_of_validators']}

+
+
+

{props.selectedMultiValidatorArray.length + ' '}

+ + {delegatedList && delegatedList.length > 0 && + delegatedList.map((item, index) => { + const image = item && item.description && item.description.identity && + props.validatorImages && props.validatorImages.length && + props.validatorImages.filter((value) => value._id === item.description.identity.toString()); + + return ( + + {image && image.length && image[0] && image[0].them && image[0].them.length && + image[0].them[0] && image[0].them[0].pictures && image[0].them[0].pictures.primary && + image[0].them[0].pictures.primary.url + ? {item.description + : item.description && item.description.moniker + ? + {item.description.moniker[0]} + + : } +
+ {item.name ? item.name : item.type + ? item.name : item.description && item.description.moniker} +
+
+ (

{item.operator_address}

+ {item.operator_address && + item.operator_address.slice(item.operator_address.length - 6, item.operator_address.length)}) +
+
+ ); + }, + )} +
+ }> +
?
+ +
+
+
+
+

{variables[props.lang]['tokens_to_each']}

+
+
+

{Number(props.tokens / props.selectedMultiValidatorArray.length).toFixed(4) + ' ' + config.COIN_DENOM}

+
+
+
+ + :
+

{variables[props.lang]['validator_address']}

+
+
+

{props.validator}

+ {props.validator && props.validator.slice(props.validator.length - 6, props.validator.length)} +
+

{validatorDetails && validatorDetails.description && validatorDetails.description.moniker + ? `(${validatorDetails.description.moniker})` + : null}

-

{validatorDetails && validatorDetails.description && validatorDetails.description.moniker - ? `(${validatorDetails.description.moniker})` - : null}

-
-
} +
}

{variables[props.lang].tokens}

{props.tokens @@ -167,8 +255,10 @@ SuccessDialog.propTypes = { lang: PropTypes.string.isRequired, name: PropTypes.string.isRequired, open: PropTypes.bool.isRequired, + selectedMultiValidatorArray: PropTypes.array.isRequired, toValidator: PropTypes.string.isRequired, validator: PropTypes.string.isRequired, + validatorImages: PropTypes.array.isRequired, address: PropTypes.string, match: PropTypes.shape({ params: PropTypes.shape({ @@ -198,7 +288,9 @@ const stateToProps = (state) => { validator: state.stake.validator.value, toValidator: state.stake.toValidator.value, validatorList: state.stake.validators.list, + validatorImages: state.stake.validators.images, claimValidator: state.stake.claimDialog.validator, + selectedMultiValidatorArray: state.stake.selectMultiValidators.list, }; }; diff --git a/src/containers/Stake/DelegateDialog/TokensTextField.js b/src/containers/Stake/DelegateDialog/TokensTextField.js index 71a8c7d..ee0edb4 100644 --- a/src/containers/Stake/DelegateDialog/TokensTextField.js +++ b/src/containers/Stake/DelegateDialog/TokensTextField.js @@ -44,7 +44,7 @@ const TokensTextField = (props) => { parseFloat(availableTokens + vestingTokens) - : props.name === 'Delegate' || props.name === 'Stake' + : props.name === 'Delegate' || props.name === 'Stake' || props.name === 'Multi-Delegate' ? props.value > parseFloat(availableTokens) : props.name === 'Undelegate' || props.name === 'Redelegate' ? props.value > parseFloat(stakedTokens) : false} @@ -66,7 +66,10 @@ const TokensTextField = (props) => { ?

props.onChange(stakedTokens)}> {stakedTokens}

- : null} + : props.name === 'Multi-Delegate' + ?

props.onChange(availableTokens)}> + {availableTokens} +

: null}
{vestingTokens && (props.name === 'Delegate' || props.name === 'Stake') ?
diff --git a/src/containers/Stake/DelegateDialog/index.css b/src/containers/Stake/DelegateDialog/index.css index 3668d2f..927f6be 100644 --- a/src/containers/Stake/DelegateDialog/index.css +++ b/src/containers/Stake/DelegateDialog/index.css @@ -168,3 +168,121 @@ .pending img { width: 100px; } + +.mv_formControl { + width: 100%; +} + +.mv_select { + border: solid #696969 0.3px; + height: 48px; +} + +.mv_select .MuiOutlinedInput-notchedOutline { + border: none !important; +} + +.mv_select em { + color : #696969; + padding-left: 5px; + font-size: 16px; + font-family: 'Blinker', sans-serif; + font-style: normal; +} + +.image { + background: #696969; + width: 30px; + height: 30px; + border-radius: 50px; + margin-right: 10px; + display: flex; + align-items: center; + justify-content: center; + color: #fff; +} + +div[role=tooltip] .image_small { + background: #696969; + width: 26px; + height: 26px; + border-radius: 50px; + margin-right: 8px; + display: flex; + align-items: center; + justify-content: center; + color: #fff; +} + +div[role=presentation] .mv_menuItem { + font-family: 'Blinker', sans-serif; + font-weight: 600; + font-size: 18px; + line-height: 130%; + color: #FFFFFF; + padding: 8px 40px; +} + +div[role=tooltip] .mv_menuItem_small { + font-family: 'Blinker', sans-serif; + font-weight: 600; + font-size: 16px; + line-height: 130%; + color: #FFFFFF; + padding: 8px 0 8px 4px; +} + +div[role=tooltip] .name_small, +div[role=tooltip] .hash_text_small { + font-size: 16px; + font-weight: 400; +} + +div[role=tooltip] .name_cut { + max-width: 130px; + overflow: hidden; + white-space: nowrap; +} + +div[role=tooltip] .hash_text_small { + margin-left: 10px; +} + +div[role=presentation] .mv_menuItem:hover { + background-color: #070707; +} + +div[role=presentation] .mv_menuItem .checkbox { + color: pink; +} + +div[role=presentation] .divider { + background-color: #545454; +} + +.content .row .validator .popover_button { + cursor: pointer; + display: flex; + justify-content: center; + align-items: center; + padding-bottom: 2px; + padding-left: 1px; + border-radius: 50%; + width: 25px; + height: 25px; + min-width: unset; + border: solid #676768; + margin-left: 10px; + color: white; +} + +div[role=tooltip] .validator_popover, +div[role=presentation] .validator_popover { + color: white; + text-align: start; + font-family: 'Blinker', sans-serif; + font-size: 16px; + font-weight: 350; + line-height: 160%; + margin-right: 15px; +} \ No newline at end of file diff --git a/src/containers/Stake/DelegateDialog/index.js b/src/containers/Stake/DelegateDialog/index.js index e38f1f3..b9eee82 100644 --- a/src/containers/Stake/DelegateDialog/index.js +++ b/src/containers/Stake/DelegateDialog/index.js @@ -9,10 +9,12 @@ import { showDelegateFailedDialog, showDelegateProcessingDialog, showDelegateSuccessDialog, + selectMultiValidators, } from '../../../actions/stake'; import ValidatorSelectField from './ValidatorSelectField'; import TokensTextField from './TokensTextField'; import ToValidatorSelectField from './ToValidatorSelectField'; +import MultiValidatorSelectField from './MultiValidatorSelectField'; import { cosmoStationSign, signTxAndBroadcast } from '../../../helper'; import { fetchRewards, @@ -64,6 +66,50 @@ const DelegateDialog = (props) => { signTxAndBroadcast(updatedTx, props.address, handleFetch); }; + const handleMultiDelegate = () => { + setInProgress(true); + let gasValue = gas.delegate; + if (props.selectedMultiValidatorArray && props.selectedMultiValidatorArray.length > 1) { + gasValue = ((gas.delegate * props.selectedMultiValidatorArray.length) / 1.1) + gas.delegate; + } + + const updatedTx = { + msgs: [], + fee: { + amount: [{ + amount: String(gasValue * config.GAS_PRICE_STEP_AVERAGE), + denom: config.COIN_MINIMAL_DENOM, + }], + gas: String(gasValue), + }, + memo: '', + }; + + if (props.selectedMultiValidatorArray.length) { + props.selectedMultiValidatorArray.map((item) => { + updatedTx.msgs.push({ + typeUrl: '/cosmos.staking.v1beta1.MsgDelegate', + value: { + delegatorAddress: props.address, + validatorAddress: item, + amount: { + amount: String(Math.floor((props.amount * (10 ** config.COIN_DECIMALS)) / (props.selectedMultiValidatorArray.length))), + denom: config.COIN_MINIMAL_DENOM, + }, + }, + }); + return null; + }); + } + + if (localStorage.getItem('of_co_wallet') === 'cosmostation') { + cosmoStationSign(updatedTx, props.address, handleFetch); + return; + } + + signTxAndBroadcast(updatedTx, props.address, handleFetch); + }; + const handleFetch = (error, result) => { setInProgress(false); if (error) { @@ -146,9 +192,9 @@ const DelegateDialog = (props) => { } const disable = !props.validator || !props.amount || inProgress || - ((props.name === 'Delegate' || props.name === 'Stake') && vestingTokens + ((props.name === 'Delegate' || props.name === 'Stake' || props.name === 'Multi-Delegate') && vestingTokens ? props.amount > parseFloat((available + vestingTokens) / (10 ** config.COIN_DECIMALS)) - : props.name === 'Delegate' || props.name === 'Stake' + : props.name === 'Delegate' || props.name === 'Stake' || props.name === 'Multi-Delegate' ? props.amount > parseFloat(available / (10 ** config.COIN_DECIMALS)) : props.name === 'Undelegate' || props.name === 'Redelegate' ? props.amount > parseFloat(staked / (10 ** config.COIN_DECIMALS)) : false); @@ -170,10 +216,16 @@ const DelegateDialog = (props) => {

To validator

- : <> -

Choose the validator

- - } + : props.name === 'Multi-Delegate' + ? <> +

Select Multi Validators

+ + + : <> +

Choose the validator

+ + + }

Enter tokens to {props.name || 'Delegate'}

@@ -181,7 +233,7 @@ const DelegateDialog = (props) => { + ); +}; + +MultiDelegateButton.propTypes = { + handleOpen: PropTypes.func.isRequired, + lang: PropTypes.string.isRequired, + showMessage: PropTypes.func.isRequired, + address: PropTypes.string, + valAddress: PropTypes.string, +}; + +const stateToProps = (state) => { + return { + address: state.accounts.address.value, + lang: state.language, + }; +}; + +const actionToProps = { + handleOpen: showDelegateDialog, + showMessage, +}; + +export default connect(stateToProps, actionToProps)(MultiDelegateButton); diff --git a/src/containers/Stake/index.css b/src/containers/Stake/index.css index 9ccbcd1..4660e7b 100644 --- a/src/containers/Stake/index.css +++ b/src/containers/Stake/index.css @@ -146,6 +146,19 @@ border-bottom: unset; } +.stake .heading .multi_delegate_button { + background-color: white; + color:black; + border-radius: 30px; + padding: 9px 20px; + font-family: 'Blinker', sans-serif; + font-weight: 600; + font-size: 18px; + text-transform: uppercase; + line-height: 130%; + margin-left: 5px; +} + @media (max-width: 1025px) { .stake .table { padding: 20px 30px; @@ -179,7 +192,7 @@ } } -@media (max-width: 769px) { +@media (max-width: 770px) { .stake .table { background: unset; padding: 0; @@ -190,9 +203,13 @@ .stake .tabs > p { font-size: 30px; } + + .stake .heading .multi_delegate_button { + font-size: 16px; + } } -@media (max-width: 426px) { +@media (max-width: 680px) { .stake .heading { overflow: auto; } @@ -200,7 +217,9 @@ .stake .tabs > p { width: max-content; } +} +@media (max-width: 426px) { .table .actions > span { display: none; } diff --git a/src/containers/Stake/index.js b/src/containers/Stake/index.js index 6c1ba29..1403d9c 100644 --- a/src/containers/Stake/index.js +++ b/src/containers/Stake/index.js @@ -9,6 +9,7 @@ import DelegateDialog from './DelegateDialog'; import SuccessDialog from './DelegateDialog/SuccessDialog'; import UnSuccessDialog from './DelegateDialog/UnSuccessDialog'; import PendingDialog from './DelegateDialog/PendingDialog'; +import MultiDelegateButton from './MultiDelegateButton'; const Stake = (props) => { const [active, setActive] = useState(1); @@ -50,6 +51,8 @@ const Stake = (props) => { ? ' (' + props.delegatedValidatorList.length + ')' : null}

+ +

Unbonding Period: 21 Days

diff --git a/src/reducers/stake.js b/src/reducers/stake.js index e15b13e..d5e811e 100644 --- a/src/reducers/stake.js +++ b/src/reducers/stake.js @@ -6,6 +6,9 @@ import { CLAIM_REWARDS_DIALOG_HIDE, CLAIM_REWARDS_DIALOG_SHOW, CLAIM_REWARDS_VALIDATOR_SET, + CLAIM_DELEGATE_DIALOG_SHOW, + CLAIM_DELEGATE_DIALOG_HIDE, + CLAIM_DELEGATE_VALIDATOR_SET, DELEGATE_DIALOG_HIDE, DELEGATE_DIALOG_SHOW, DELEGATE_FAILED_DIALOG_HIDE, @@ -31,6 +34,7 @@ import { VALIDATORS_FETCH_ERROR, VALIDATORS_FETCH_IN_PROGRESS, VALIDATORS_FETCH_SUCCESS, + SELECTED_MULTI_VALIDATORS, } from '../constants/stake'; import { DISCONNECT_SET } from '../constants/accounts'; @@ -300,6 +304,39 @@ const claimDialog = (state = { } }; +const claimDelegateDialog = (state = { + open: false, + validator: 'all', +}, action) => { + switch (action.type) { + case CLAIM_DELEGATE_DIALOG_SHOW: + return { + ...state, + open: true, + }; + case CLAIM_DELEGATE_DIALOG_HIDE: + return { + ...state, + open: false, + validator: 'all', + }; + case DELEGATE_SUCCESS_DIALOG_HIDE: + case DELEGATE_FAILED_DIALOG_HIDE: + return { + ...state, + open: false, + validator: 'all', + }; + case CLAIM_DELEGATE_VALIDATOR_SET: + return { + ...state, + validator: action.value, + }; + default: + return state; + } +}; + const inActiveValidators = (state = { inProgress: false, list: [], @@ -355,6 +392,27 @@ const apr = (state = { } }; +const selectMultiValidators = (state = { + list: [], +}, action) => { + switch (action.type) { + case SELECTED_MULTI_VALIDATORS: + return { + ...state, + list: action.value, + }; + case DELEGATE_DIALOG_HIDE: + case DELEGATE_SUCCESS_DIALOG_HIDE: + case DELEGATE_PROCESSING_DIALOG_HIDE: + case DELEGATE_FAILED_DIALOG_HIDE: + return { + list: [], + }; + default: + return state; + } +}; + export default combineReducers({ search, delegateDialog, @@ -368,6 +426,8 @@ export default combineReducers({ validatorDetails, delegatedValidators, claimDialog, + claimDelegateDialog, inActiveValidators, apr, + selectMultiValidators, }); diff --git a/src/utils/variables.js b/src/utils/variables.js index 93576e7..cfffb84 100644 --- a/src/utils/variables.js +++ b/src/utils/variables.js @@ -50,6 +50,7 @@ const variables = { closed: 'Rejected', rewards: 'Rewards', claim: 'Claim', + compound: 'Compound', vesting_tokens: 'Vesting tokens', select_validator: 'Select the validator', view_all: 'View All', @@ -67,6 +68,9 @@ const variables = { connect_account: 'Account not connected. Please connect to wallet', connecting: 'connecting', cosmostation: 'Cosmostation', + multi_delegate: 'Multi Delegate', + number_of_validators: 'Number of Validators', + tokens_to_each: 'Tokens to each', }, };