diff --git a/src/assets/skip-white.svg b/src/assets/skip-white.svg deleted file mode 100644 index a2121d51..00000000 --- a/src/assets/skip-white.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/assets/skip.svg b/src/assets/skip.svg deleted file mode 100644 index 897e2305..00000000 --- a/src/assets/skip.svg +++ /dev/null @@ -1,46 +0,0 @@ - - - - -Created by potrace 1.16, written by Peter Selinger 2001-2019 - - - - - diff --git a/src/components/App.js b/src/components/App.js index 396fbbf1..f202ae74 100644 --- a/src/components/App.js +++ b/src/components/App.js @@ -5,7 +5,7 @@ import _ from 'lodash' import AlertMessage from './AlertMessage' import NetworkSelect from './NetworkSelect' import Delegations from './Delegations'; -import Coins from './Coins' +import Coin from './Coin' import About from './About' import { @@ -21,7 +21,7 @@ import { DropletFill, DropletHalf, CashCoin, - Coin, + Coin as CoinIcon, EnvelopePaper, Stars, WrenchAdjustableCircle, @@ -298,7 +298,8 @@ class App extends React.Component { return this.restClient().getBalance(this.state.address) .then( (balances) => { - const balance = balances?.find( + balances = _.compact(balances || []) + const balance = balances.find( (element) => element.denom === this.props.network.denom ) || { denom: this.props.network.denom, amount: 0 }; this.setState({ @@ -585,7 +586,7 @@ class App extends React.Component { <>
- Stake + Stake
@@ -682,10 +683,11 @@ class App extends React.Component {
  • this.showWalletModal({activeTab: this.state.wallet ? 'wallet' : 'saved'})}> {this.state.balance ? ( - ) : ( @@ -709,10 +711,11 @@ class App extends React.Component {
    {this.addressName()} this.showWalletModal({activeTab: this.state.wallet ? 'wallet' : 'saved'})}> - @@ -876,7 +879,7 @@ class App extends React.Component { network={this.props.network} address={this.state.address} wallet={this.state.wallet} - balance={this.state.balance} + balances={this.state.balances} favouriteAddresses={this.favouriteAddresses()} onHide={() => this.setState({ showSendModal: false })} onSend={this.onSend} diff --git a/src/components/ClaimRewards.js b/src/components/ClaimRewards.js index 0b2ffcce..bbc22628 100644 --- a/src/components/ClaimRewards.js +++ b/src/components/ClaimRewards.js @@ -5,10 +5,10 @@ import { Button } from 'react-bootstrap' -import { add, subtract, multiply, divide, bignumber } from 'mathjs' import { MsgDelegate } from "../messages/MsgDelegate.mjs"; import { MsgWithdrawDelegatorReward } from "../messages/MsgWithdrawDelegatorReward.mjs"; import { MsgWithdrawValidatorCommission } from "../messages/MsgWithdrawValidatorCommission.mjs"; +import { smallerEq } from "mathjs"; function ClaimRewards(props) { const { network, address, wallet, rewards } = props @@ -17,47 +17,16 @@ function ClaimRewards(props) { props.setError() props.setLoading(true) - const validatorRewards = mapRewards() - const gasSimMessages = buildMessages(validatorRewards) - - let gas - try { - gas = await wallet.simulate(gasSimMessages) - } catch (error) { - props.setLoading(false) - props.setError('Failed to broadcast: ' + error.message) - return - } - - const fee = wallet.getFee(gas) - const feeAmount = fee.amount[0].amount - - const totalReward = validatorRewards.reduce((sum, validatorReward) => add(sum, bignumber(validatorReward.reward)), 0); - const adjustedValidatorRewards = validatorRewards.map(validatorReward => { - const shareOfFee = multiply(divide(bignumber(validatorReward.reward), totalReward), feeAmount); // To take a proportional amount from each validator relative to total reward - return { - validatorAddress: validatorReward.validatorAddress, - reward: subtract(validatorReward.reward, shareOfFee), - } - }) - - if(!props.commission && (adjustedValidatorRewards.length < 1 || adjustedValidatorRewards.some(validatorReward => validatorReward.reward <= 0))) { - props.setLoading(false) - props.setError('Reward is too low') - return - } - - let messages = buildMessages(adjustedValidatorRewards) + let messages try { - gas = gas || await wallet.simulate(messages) + messages = await buildMessages() } catch (error) { props.setLoading(false) - props.setError('Failed to broadcast: ' + error.message) + props.setError(error.message) return } - console.log(messages, gas) - wallet.signAndBroadcastWithoutBalanceCheck(messages, gas).then((result) => { + wallet.signAndBroadcastWithoutBalanceCheck(messages).then((result) => { console.log("Successfully broadcasted:", result); props.setLoading(false) props.onClaimRewards(result) @@ -68,47 +37,36 @@ function ClaimRewards(props) { }) } - function mapRewards() { - if (!rewards) return []; - - const validatorRewards = rewards - .map(reward => { - return { - validatorAddress: reward.validator_address, - reward: rewardAmount(reward, network.denom), - } - }) - .filter(validatorReward => validatorReward.reward ); - - return validatorRewards; - } - - // Expects a map of string -> string (validator -> reward) - function buildMessages(validatorRewards){ - return validatorRewards.map(validatorReward => { + function buildMessages(){ + const messages = rewards.map(validatorRewards => { let valMessages = [] if(props.restake){ + const denomReward = rewardAmount(validatorRewards, network.denom) + if(smallerEq(denomReward, 0)){ + throw new Error(`You have no ${network.symbol} rewards to compound`) + } valMessages.push(new MsgDelegate({ delegatorAddress: address, - validatorAddress: validatorReward.validatorAddress, - amount: coin(validatorReward.reward, network.denom) + validatorAddress: validatorRewards.validator_address, + amount: coin(denomReward, network.denom) })) }else{ valMessages.push(new MsgWithdrawDelegatorReward({ delegatorAddress: address, - validatorAddress: validatorReward.validatorAddress + validatorAddress: validatorRewards.validator_address })) - } - if (props.commission) { - valMessages.push(new MsgWithdrawValidatorCommission({ - validatorAddress: validatorReward.validatorAddress - })) + if (props.commission) { + valMessages.push(new MsgWithdrawValidatorCommission({ + validatorAddress: validatorRewards.validator_address + })) + } } - - return execableMessage(valMessages, wallet.address, address) + return valMessages }).flat() + + return execableMessage(messages, wallet.address, address) } function hasPermission(){ diff --git a/src/components/Coin.js b/src/components/Coin.js new file mode 100644 index 00000000..3e861863 --- /dev/null +++ b/src/components/Coin.js @@ -0,0 +1,68 @@ +import _ from 'lodash' +import { divide, bignumber, round, format } from 'mathjs' +import { truncateDenom } from '../utils/Helpers.mjs' + +function Coin(props) { + const { amount, denom, asset, fullPrecision, showValue = true, showImage = true, className } = props + let { decimals, symbol, prices } = asset || {} + const { coingecko } = prices || {} + symbol = symbol || (denom && truncateDenom(denom?.toUpperCase())) + + function decimalAmount(){ + if(decimals){ + return round(divide(bignumber(amount), Math.pow(10, decimals)), precision()) + }else{ + return round(bignumber(amount), 0) + } + } + + function formattedAmount(){ + return separator(format(decimalAmount(), {notation: 'fixed'})) + } + + function value(){ + return (amount / Math.pow(10, decimals) * coingecko.usd).toLocaleString(undefined, { maximumFractionDigits: 2, minimumFractionDigits: 2 }) + } + + function separator(stringNum) { + var str = stringNum.split("."); + str[0] = str[0].replace(/\B(?=(\d{3})+(?!\d))/g, ","); + return str.join("."); + } + + function precision(){ + if(fullPrecision) return decimals; + if(props.precision) return props.precision; + if(amount >= (1000 * Math.pow(10, decimals))) return 2 + if(amount >= (100 * Math.pow(10, decimals))) return 3 + return 6 + } + + if(!denom){ + return null + } + + const classNames = ['coins', className] + if(showImage){ + classNames.push('d-inline-block align-top') + } + + return ( + + + {showImage && asset?.image && ( + + )} + {formattedAmount()}  + {symbol} + + {showValue && !!coingecko?.usd && !!amount && ( +
    + ${value()} +
    + )} +
    + ) +} + +export default Coin; diff --git a/src/components/Coins.js b/src/components/Coins.js index 36166bef..34ef8149 100644 --- a/src/components/Coins.js +++ b/src/components/Coins.js @@ -1,57 +1,74 @@ +import React, { useState } from 'react'; import _ from 'lodash' -import { divide, bignumber, round, format } from 'mathjs' +import Coin from './Coin' +import { divide, multiply, pow, bignumber, numeric } from 'mathjs' +import { sortCoins } from '../utils/Helpers.mjs'; -function Coins(props) { - const { asset, coins, fullPrecision, inBaseDenom, hideValue, className } = props - let { decimals, symbol, prices } = asset || {} - const { coingecko } = prices || {} - decimals = decimals ?? 6 - symbol = symbol || coins?.denom?.toUpperCase() +function Coins({ coins, network, showTotalValue = true, hideLowValue = true, allowShowLowValue = true, ...props }) { + const [showHidden, setShowHidden] = useState(!hideLowValue) - function amount(coins){ - if(inBaseDenom) return coins.amount + let items = sortCoins(coins || [], network).map((coin) => { + const asset = network.assetForDenom(coin.denom) + let value + if(asset && asset.prices?.coingecko?.usd){ + value = numeric(multiply(divide(bignumber(coin.amount || 0), pow(10, asset.decimals)), asset.prices.coingecko.usd), 'number') + } + return { + coin, + asset, + value + } + }) + const totalValue = items.reduce((a, v) => a + (v.value || 0), 0) - const prec = precision(coins) - return separator(format(round(divide(bignumber(coins.amount), Math.pow(10, decimals)), prec), {notation: 'fixed'})) - } - - function value(coins){ - return (coins.amount / Math.pow(10, decimals) * coingecko.usd).toLocaleString(undefined, { maximumFractionDigits: 2, minimumFractionDigits: 2 }) - } - - function separator(stringNum) { - var str = stringNum.split("."); - str[0] = str[0].replace(/\B(?=(\d{3})+(?!\d))/g, ","); - return str.join("."); -} - - if(!coins || !coins.denom){ - return null - } - - function precision(coins){ - if(fullPrecision) return decimals; - if(props.precision) return props.precision; - if(coins.amount >= (1000 * Math.pow(10, decimals))) return 2 - if(coins.amount >= (100 * Math.pow(10, decimals))) return 3 - return 6 - } + items.forEach((item, index) => { + if(index == 0) return + if(totalValue > 0 && (!item.value || item.value < 1)){ + item.hide = true + } + }) return ( - - - {amount(coins)}  - {symbol} - - {!!coingecko?.usd && !hideValue && !!coins.amount && ( +
    + {items.map((item) => ( +
    + +
    + ))} + {items.filter(item => item.hide).length > 0 && ( +
    + {showHidden ? ( + allowShowLowValue && setShowHidden(false)} + > + ...hide low value + + ) : ( + allowShowLowValue && setShowHidden(true)} + > + ...and {items.filter(item => item.hide).length} more + + )} +
    + )} + {showTotalValue && totalValue ? ( <> -
    - - ${value(coins)} - + + ${totalValue.toLocaleString(undefined, { maximumFractionDigits: 2, minimumFractionDigits: 2 })} + - )} - + ) : null} +
    ) } diff --git a/src/components/CountdownRestake.js b/src/components/CountdownRestake.js index d4e7c1e6..a69b2fb5 100644 --- a/src/components/CountdownRestake.js +++ b/src/components/CountdownRestake.js @@ -4,7 +4,7 @@ import TooltipIcon from './TooltipIcon'; import Countdown from 'react-countdown'; -import Coins from './Coins'; +import Coin from './Coin'; function CountdownRestake(props) { const { operator, network, maxAmount, icon } = props @@ -35,7 +35,7 @@ function CountdownRestake(props) { renderer={countdownRenderer} /> {maxAmount && ( -

    Grant remaining:

    +

    Grant remaining:

    )}
    @@ -46,7 +46,7 @@ function CountdownRestake(props) { renderer={countdownRenderer} /> {maxAmount && ( -
    Grant remaining:
    +
    Grant remaining:
    )}
    ) @@ -54,4 +54,3 @@ function CountdownRestake(props) { } export default CountdownRestake; - diff --git a/src/components/DelegateForm.js b/src/components/DelegateForm.js index c309165e..e793eb33 100644 --- a/src/components/DelegateForm.js +++ b/src/components/DelegateForm.js @@ -5,10 +5,10 @@ import { Form, } from 'react-bootstrap' -import { pow, multiply, divide, subtract, bignumber } from 'mathjs' +import { pow, multiply, divide, numeric, bignumber, format } from 'mathjs' import AlertMessage from './AlertMessage' -import Coins from './Coins' +import Coin from './Coin' import { coin, execableMessage } from '../utils/Helpers.mjs' import { MsgBeginRedelegate } from '../messages/MsgBeginRedelegate.mjs'; import { MsgUndelegate } from '../messages/MsgUndelegate.mjs'; @@ -21,6 +21,12 @@ function DelegateForm(props) { { amount: '', memo: '' } ) + const asset = network.baseAsset + let value + if(state.amount && asset && asset.prices?.coingecko?.usd){ + value = numeric(multiply(state.amount, asset.prices.coingecko.usd), 'number') + } + function handleInputChange(event) { const target = event.target; const value = target.value; @@ -90,22 +96,9 @@ function DelegateForm(props) { async function setAvailableAmount() { if (!wallet) return - setState({ error: undefined }) - const messages = buildMessages(multiply(availableBalance().amount, 0.95)) const decimals = pow(10, network.decimals) - const balance = bignumber(availableBalance().amount) - if (['redelegate', 'undelegate'].includes(action)) { - return setState({ amount: divide(balance, decimals) }) - } - wallet.simulate(messages).then(gas => { - const gasPrice = wallet.getFee(gas).amount[0].amount - const saveTxFeeNum = 10 - const amount = divide(subtract(balance, multiply(gasPrice, saveTxFeeNum)), decimals) - - setState({ amount: amount > 0 ? amount : 0 }) - }, error => { - setState({ error: error.message }) - }) + const amount = divide(bignumber(availableBalance().amount), decimals) + setState({ amount: format(amount, {notation: 'fixed'})}) } function availableBalance() { @@ -122,10 +115,6 @@ function DelegateForm(props) { return 'Delegate' } - function denom() { - return network.symbol - } - function step() { return 1 / pow(10, network.decimals) } @@ -149,13 +138,20 @@ function DelegateForm(props) {
    - {denom()} + {network.symbol} +
    +
    + + {value ? ( + ${value.toLocaleString(undefined, { maximumFractionDigits: 2, minimumFractionDigits: 2 })} + ) : null} + + {availableBalance() ? ( + setAvailableAmount()}> + Available: + + ) : }
    - {availableBalance() && -
    setAvailableAmount()}> - Available: -
    - }
    diff --git a/src/components/Delegations.js b/src/components/Delegations.js index bc9a61c4..dda9ec7c 100644 --- a/src/components/Delegations.js +++ b/src/components/Delegations.js @@ -1,6 +1,6 @@ import React from "react"; import _ from "lodash"; -import { larger, bignumber } from 'mathjs' +import { larger, largerEq, bignumber, add } from 'mathjs' import AlertMessage from "./AlertMessage"; import ClaimRewards from "./ClaimRewards"; import ValidatorModal from "./ValidatorModal"; @@ -150,7 +150,16 @@ class Delegations extends React.Component { .getRewards(this.props.address, this.props.network.denom) .then( (rewards) => { - this.setState({ rewards: rewards }); + this.setState({ rewards: rewards.reduce((sum, reward) => { + const validatorRewards = reward.reward.filter(reward => largerEq(reward.amount, 1)) + if(validatorRewards.length > 0){ + sum[reward.validator_address] = { + validator_address: reward.validator_address, + reward: validatorRewards + } + } + return sum + }, {}) }); }, (error) => { if ([404, 500].includes(error.response && error.response.status) && !this.state.rewards) { @@ -169,7 +178,7 @@ class Delegations extends React.Component { commission: _.set( state.commission, validator.address, - commission + { commission: commission.filter(comm => largerEq(comm.amount, 1)) } ), })); }) @@ -308,7 +317,7 @@ class Delegations extends React.Component { grantsValid: !!( grant.stakeGrant && (!grant.validators || grant.validators.includes(operator.address)) && - (grant.maxTokens === null || larger(grant.maxTokens, rewardAmount(this.state.rewards, this.props.network.denom))) + (grant.maxTokens === null || larger(grant.maxTokens, rewardAmount(this.state.rewards?.[operator.address], this.props.network.denom))) ), grantsExist: !!(grant.claimGrant || grant.stakeGrant), } @@ -326,12 +335,8 @@ class Delegations extends React.Component { const denom = this.props.network.denom; const total = Object.values(this.state.rewards).reduce((sum, item) => { const reward = item.reward.find((el) => el.denom === denom); - if ( - reward && - (validators === undefined || - validators.includes(item.validator_address)) - ) { - return sum + parseInt(reward.amount); + if (reward && (validators === undefined || validators.includes(item.validator_address))) { + return add(sum, reward.amount) } return sum; }, 0); @@ -495,7 +500,7 @@ class Delegations extends React.Component { setError={this.setError} /> All Rewards @@ -568,6 +572,7 @@ class Delegations extends React.Component { setError={this.setError} />

    - Enabling REStake will authorize the validator to send Delegate transactions on your behalf using Authz.
    + Enabling REStake will authorize the validator to send Delegate transactions on your behalf using Authz.
    They will only be authorized to delegate to their own validator. You can revoke the authorization at any time and everything is open source.

    diff --git a/src/components/Grants.js b/src/components/Grants.js index 83825432..00a75dbc 100644 --- a/src/components/Grants.js +++ b/src/components/Grants.js @@ -13,7 +13,7 @@ import { XCircle } from "react-bootstrap-icons"; import AlertMessage from './AlertMessage'; import RevokeGrant from './RevokeGrant'; -import Coins from './Coins'; +import Coin from './Coin'; import GrantModal from './GrantModal'; import Favourite from './Favourite'; import Address from './Address' @@ -101,7 +101,7 @@ function Grants(props) { const restrictionList = grant.authorization.allow_list || grant.authorization.deny_list return ( - Maximum: {maxTokens ? : 'unlimited'}
    + Maximum: {maxTokens ? : 'unlimited'}
    {restrictionType}: {restrictionList.address.map(address => { const validator = validators[address] return address ?

    {validator?.moniker ||
    }
    : null diff --git a/src/components/ProposalDetails.js b/src/components/ProposalDetails.js index 5fa607d1..0aa58e6f 100644 --- a/src/components/ProposalDetails.js +++ b/src/components/ProposalDetails.js @@ -11,7 +11,7 @@ import { Nav } from 'react-bootstrap' -import Coins from './Coins'; +import Coin from './Coin'; import ProposalProgress from './ProposalProgress'; import ProposalMessages from './ProposalMessages'; import VoteForm from './VoteForm'; @@ -137,8 +137,8 @@ function ProposalDetails(props) { Total deposit - {proposal.total_deposit.map(coins => { - return + {proposal.total_deposit.map(coin => { + return })} diff --git a/src/components/ProposalMessages.js b/src/components/ProposalMessages.js index 888b2d6f..00f70481 100644 --- a/src/components/ProposalMessages.js +++ b/src/components/ProposalMessages.js @@ -5,9 +5,9 @@ import { import moment from 'moment' import _ from 'lodash' import Moment from 'react-moment'; -import Coins from './Coins'; -import { omit } from '../utils/Helpers.mjs'; import truncateMiddle from 'truncate-middle'; +import { omit } from '../utils/Helpers.mjs'; +import Coins from './Coins'; function ProposalMessages(props) { const { proposal, network } = props @@ -46,13 +46,14 @@ function ProposalMessages(props) { ) } } - case '/cosmos.distribution.v1beta1.CommunityPoolSpendProposal': + case [ + '/cosmos.distribution.v1beta1.CommunityPoolSpendProposal', + '/cosmos.distribution.v1beta1.MsgCommunityPoolSpend' + ].find(type => type === message['@type']): return { ...data, amount: () => { - return data.amount?.map((coin, index) => { - return - }) + return } } default: diff --git a/src/components/REStakeGrantForm.js b/src/components/REStakeGrantForm.js index 3829c492..d7a83b10 100644 --- a/src/components/REStakeGrantForm.js +++ b/src/components/REStakeGrantForm.js @@ -7,7 +7,7 @@ import { Form, } from 'react-bootstrap' -import Coins from './Coins'; +import Coin from './Coin'; import { coin, execableMessage, rewardAmount } from '../utils/Helpers.mjs'; import RevokeGrant from './RevokeGrant'; import AlertMessage from './AlertMessage'; @@ -141,7 +141,7 @@ function REStakeGrantForm(props) { return ( <>

    {operator.moniker} will be able to carry out the following transactions on your behalf:

    -

    Delegate - allowed to delegate {maxTokensDenom() ? <>a maximum of : 'any amount'} to {!state.validators ? 'any validator' : !state.validators.length || (state.validators.length === 1 && state.validators.includes(operator.address)) ? 'only their own validator' : 'validators ' + state.validators.join(', ')}.

    +

    Delegate - allowed to delegate {maxTokensDenom() ? <>a maximum of : 'any amount'} to {!state.validators ? 'any validator' : !state.validators.length || (state.validators.length === 1 && state.validators.includes(operator.address)) ? 'only their own validator' : 'validators ' + state.validators.join(', ')}.

    This grant will expire automatically on {state.expiryDateValue} and you can revoke it at any time.

    {operator.moniker} will only auto-compound their accrued rewards and tries not to touch your balance.
    They will pay the transaction fees for you.

    {genericGrantOnly && ( diff --git a/src/components/REStakeStatus.js b/src/components/REStakeStatus.js index 79b5b98d..6cbd6754 100644 --- a/src/components/REStakeStatus.js +++ b/src/components/REStakeStatus.js @@ -7,7 +7,7 @@ import { } from 'react-bootstrap' import { XCircle, ToggleOn, ToggleOff } from "react-bootstrap-icons"; import { joinString } from "../utils/Helpers.mjs"; -import Coins from "./Coins"; +import Coin from "./Coin"; function REStakeStatus(props) { const { network, validator, operator, delegation, grants, authzSupport, className } = props @@ -26,7 +26,7 @@ function REStakeStatus(props) { if(grants?.grantsValid){ let limit if(grants.maxTokens){ - limit =
    ( remaining)
    + limit =
    ( remaining)
    }else{ limit = '(no limit)' } @@ -66,11 +66,12 @@ function REStakeStatus(props) {

    REStakes {operator.runTimesString()}

    Minimum reward is{" "} -

    {tooltipContent}

    diff --git a/src/components/SendModal.js b/src/components/SendModal.js index f7d36725..09de7f5a 100644 --- a/src/components/SendModal.js +++ b/src/components/SendModal.js @@ -1,6 +1,6 @@ import React, { useState, useEffect } from 'react'; import _ from 'lodash' -import { pow, multiply, divide, subtract, bignumber } from 'mathjs' +import { pow, multiply, divide, numeric, bignumber, format } from 'mathjs' import { MsgSend } from "../messages/MsgSend.mjs"; @@ -8,20 +8,28 @@ import { Modal, Button, Form, + Dropdown } from 'react-bootstrap' import AlertMessage from './AlertMessage'; -import { coin, execableMessage, truncateAddress } from '../utils/Helpers.mjs'; -import Coins from './Coins'; +import { coin, execableMessage, sortCoins, truncateAddress } from '../utils/Helpers.mjs'; +import Coin from './Coin'; function SendModal(props) { - const { show, network, address, wallet } = props + const { show, network, address, wallet, balances } = props const [loading, setLoading] = useState(false); const [error, setError] = useState() - const [state, setState] = useState({recipientValue: '', customRecipientValue: '', memoValue: ''}); - - const denom = network && network.symbol - const step = 1 / pow(10, network?.decimals || 6) + const [state, setState] = useState({recipientValue: '', customRecipientValue: '', amountValue: '', denomValue: '', memoValue: ''}); + + const asset = state.denomValue && network.assetForDenom(state.denomValue) + const balance = asset && balances?.find(el => el.denom === asset.denom) + const sortedBalances = sortCoins(balances, network) + const assets = _.compact(sortedBalances?.map(el => network.assetForDenom(el.denom)) || []) + const step = 1 / pow(10, asset?.decimals || 6) + let value + if(state.amountValue && asset && asset.prices?.coingecko?.usd){ + value = numeric(multiply(state.amountValue, asset.prices.coingecko.usd), 'number') + } useEffect(() => { setState({ @@ -29,6 +37,7 @@ function SendModal(props) { recipientValue: '', customRecipientValue: '', amountValue: '', + denomValue: network.denom, memoValue: '', }) setError(null) @@ -38,6 +47,10 @@ function SendModal(props) { setState({ ...state, [e.target.name]: e.target.value }); } + function handleDenomValueChange(denom) { + setState({ ...state, amountValue: '', denomValue: denom }); + } + function showLoading(isLoading) { setLoading(isLoading) props.setLoading && props.setLoading(isLoading) @@ -63,6 +76,7 @@ function SendModal(props) { recipientValue: '', customRecipientValue: '', amountValue: '', + denomValue: network.denom, memoValue: '', }) props.onSend(recipient(), coinValue); @@ -88,19 +102,9 @@ function SendModal(props) { } async function setAvailableAmount(){ - setError(null) - const decimals = pow(10, network.decimals) - const coinValue = coin(multiply(props.balance.amount, 0.95), network.denom) - const message = buildSendMsg(address, recipient(), [coinValue]) - wallet.simulate([message]).then(gas => { - const gasPrice = wallet.getFee(gas).amount[0].amount - const amount = divide(subtract(bignumber(props.balance.amount), gasPrice), decimals) - - setState({...state, amountValue: amount > 0 ? amount : 0}) - }, error => { - console.log(error) - setError(error.message) - }) + const decimals = pow(10, asset.decimals) + const amount = divide(bignumber(balance.amount), decimals) + setState({...state, amountValue: format(amount, {notation: 'fixed'})}) } function recipient(){ @@ -110,16 +114,16 @@ function SendModal(props) { function coinAmount(){ if(!state.amountValue) return null - const decimals = pow(10, network.decimals) + const decimals = pow(10, asset.decimals) const denomAmount = multiply(state.amountValue, decimals) if(denomAmount > 0){ - return coin(denomAmount, network.denom) + return coin(denomAmount, asset.denom) } } function valid(){ if(!state.recipientValue) return true - return validRecipient() && coinAmount() && wallet?.hasPermission(address, 'Send') + return validRecipient() && coinAmount() && validBalance() && wallet?.hasPermission(address, 'Send') } function validAmount(){ @@ -135,6 +139,15 @@ function SendModal(props) { return !network.prefix || value.startsWith(network.prefix) } + function validBalance(){ + if(!state.amountValue) return true + + const coinValue = coinAmount() + if(!coinValue) return false + + return bignumber(coinValue.amount).lte(balance.amount) + } + function favourites(){ return props.favouriteAddresses.filter(el => el.address !== props.address) } @@ -151,61 +164,85 @@ function SendModal(props) { {error} } -
    - - Recipient - - {state.recipientValue === 'custom' && ( - + + Recipient + + {state.recipientValue === 'custom' && ( + + )} + + {recipient() && ( + <> + + Amount +
    +
    + +
    + + + {asset?.symbol} + + + {assets.map((asset, index) => ( + handleDenomValueChange(asset.denom)} + > + {asset.symbol} + + ))} + +
    - {props.balance && -
    setAvailableAmount()}> - Available: -
    - }
    - - - Memo - - -

    - {!loading - ? ( - - ) - :

    +
    + + Memo + + +

    + {!loading + ? ( + - } -

    - - )} - + ) + : + } +

    + + )} diff --git a/src/components/ValidatorCalculator.js b/src/components/ValidatorCalculator.js index 0e4d1d14..32b91cb5 100644 --- a/src/components/ValidatorCalculator.js +++ b/src/components/ValidatorCalculator.js @@ -11,7 +11,7 @@ import { import { QuestionCircle } from "react-bootstrap-icons"; import TooltipIcon from './TooltipIcon' -import Coins from './Coins'; +import Coin from './Coin'; function ValidatorCalculator(props) { const { validator, operator, network, delegation } = props @@ -229,11 +229,14 @@ function ValidatorCalculator(props) { {amountDenom === network.symbol ? ( ${usdAmount().toLocaleString(undefined, { maximumFractionDigits: 2, minimumFractionDigits: 2 })} ) : ( - + showValue={false} + showImage={false} + /> )}
  • )} @@ -337,11 +340,14 @@ function ValidatorCalculator(props) { {frequencyLimited && operator && (
    - minimum reward required + showValue={false} + showImage={false} + /> minimum reward required
    )} @@ -410,19 +416,51 @@ function ValidatorCalculator(props) { Daily - + + + Weekly - + + + Monthly - + + + Yearly - + + + diff --git a/src/components/ValidatorProfile.js b/src/components/ValidatorProfile.js index 482b345f..347f8d43 100644 --- a/src/components/ValidatorProfile.js +++ b/src/components/ValidatorProfile.js @@ -3,7 +3,7 @@ import React from 'react'; import { round, add, multiply } from 'mathjs' import ValidatorLink from './ValidatorLink' -import Coins from './Coins' +import Coin from './Coin' import TooltipIcon from './TooltipIcon' import { @@ -19,8 +19,6 @@ import ValidatorNetworks from './ValidatorNetworks'; import OperatorLastRestake from './OperatorLastRestake'; import Address from './Address'; import ValidatorStatus from './ValidatorStatus' -import SkipIcon from '../assets/skip.svg' -import SkipWhiteIcon from '../assets/skip-white.svg' function ValidatorProfile(props) { const { validator, operator, network, networks, registryData, lastExec } = props @@ -78,7 +76,15 @@ function ValidatorProfile(props) { Minimum rewards - + + + {network.authzSupport && ( @@ -126,7 +132,14 @@ function ValidatorProfile(props) { Voting power - + + + @@ -149,7 +162,7 @@ function ValidatorProfile(props) { Profiles - + {validator?.path && ( @@ -165,42 +178,6 @@ function ValidatorProfile(props) {

    {validator.description?.details}

    - {!!network.chain.services?.skip && !!validator.services?.skip && ( - <> -

    - Skip MEV -

    - - - - - - - - - - - - - - - -
    Status - {validator.services.skip.active ? ( - <> - Active - {validator.services.skip.front_running_protection && <>
    Front-running protected} - - ) : Inactive} -
    Network profit - {100 - validator.services.skip.val_payment_percentage}%
    - -
    Validator profit - {validator.services.skip.val_payment_percentage}%
    - -
    - - )} {Object.entries(validator.public_nodes || {}).length > 0 && ( <>

    Public Nodes

    diff --git a/src/components/ValidatorStake.js b/src/components/ValidatorStake.js index 2bca2201..9201124e 100644 --- a/src/components/ValidatorStake.js +++ b/src/components/ValidatorStake.js @@ -10,7 +10,7 @@ import { } from 'react-bootstrap' import { ChevronLeft, CheckCircle, XCircle, QuestionCircle } from "react-bootstrap-icons"; -import Coins from './Coins'; +import Coin from './Coin'; import TooltipIcon from './TooltipIcon' import DelegateForm from './DelegateForm' @@ -21,9 +21,9 @@ import AlertMessage from './AlertMessage' import REStakeGrantForm from './REStakeGrantForm'; import Address from './Address'; import OperatorLastRestake from './OperatorLastRestake'; -import CountdownRestake from './CountdownRestake'; import RevokeGrant from './RevokeGrant'; import ValidatorStatus from './ValidatorStatus' +import Coins from './Coins'; function ValidatorStake(props) { const { network, validator, operator, balance, wallet, address, lastExec } = props @@ -33,10 +33,10 @@ function ValidatorStake(props) { const [error, setError] = useState() const delegation = props.delegations && props.delegations[validator.address] + const validatorOperator = validator.isValidatorOperator(address) const validatorRewards = props.rewards && props.rewards[validator.address] const reward = rewardAmount(validatorRewards, network.denom) const validatorCommission = props.commission && props.commission[validator.address] - const commission = rewardAmount(validatorCommission, network.denom, 'commission') const validatorGrants = props.grants && operator && props.grants[operator.botAddress] const { grantsValid, grantsExist, maxTokens, stakeGrant } = validatorGrants || {} @@ -220,7 +220,12 @@ function ValidatorStake(props) { Delegation {!props.isLoading('delegations') ? ( - + ) : ( Loading... @@ -234,7 +239,7 @@ function ValidatorStake(props) { Rewards {!props.isLoading('rewards') ? ( - + ) : ( Loading... @@ -243,12 +248,12 @@ function ValidatorStake(props) { )} - {!!commission && ( + {validatorOperator && ( Commission {!props.isLoading('commission') ? ( - + ) : ( Loading... @@ -292,7 +297,7 @@ function ValidatorStake(props) { Minimum Reward - + {network.authzSupport && ( @@ -303,15 +308,6 @@ function ValidatorStake(props) { - {/* - Next REStake - - - - */} )} @@ -345,7 +341,7 @@ function ValidatorStake(props) { Grant Remaining {maxTokens ? ( - + ) : ( 'Unlimited' )} @@ -382,6 +378,7 @@ function ValidatorStake(props) { setError={setError} /> - {!!commission && ( + {validatorOperator && ( validator.address === network.ownerAddress) } + function totalDelegation(){ + return results.reduce((sum, result) => { + const delegation = delegations && delegations[result.operator_address] + if (!delegation) return sum + + return add(sum, delegation.balance.amount) + }, 0) + } + + function totalRewards(){ + return sumCoins(results.flatMap(result => { + const rewards = props.rewards[result.operator_address]?.reward || [] + return rewards + })) + } + + function totalCommission(){ + return sumCoins(results.flatMap(result => { + const commission = props.commission[result.operator_address]?.commission || [] + return commission + })) + } + function renderValidator(validator) { const validatorAddress = validator.operator_address const delegation = delegations && delegations[validatorAddress]; const validatorOperator = validator.isValidatorOperator(address) - const rewards = - props.rewards && props.rewards[validatorAddress]; - const denomRewards = rewards && rewards.reward.find( - (reward) => reward.denom === network.denom - ); + const rewards = props.rewards && props.rewards[validatorAddress]; const commission = props.commission && props.commission[validatorAddress] - const denomCommission = commission && commission.commission.find( - (commission) => commission.denom === network.denom - ); const operator = operatorForValidator(validatorAddress); const grants = operator && operatorGrants[operator.botAddress] @@ -169,23 +186,31 @@ function Validators(props) { return ( - + props.showValidator(validator, { activeTab: 'profile' })} + > - -
    props.showValidator(validator, { activeTab: 'profile' })}> -
    - - {badge ? {badge.text} : null} -
    #{validator.rank}
    -
    + props.showValidator(validator, { activeTab: 'profile' })} + > +
    + + {badge ? {badge.text} : null} +
    #{validator.rank}
    - + {network.apyEnabled && ( - - props.showValidator(validator, { activeTab: 'stake' })}> - {props.validatorApy[validatorAddress] !== undefined - ? {round(props.validatorApy[validatorAddress] * 100, 1).toLocaleString() + "%"} - : "-" - } - + props.showValidator(validator, { activeTab: 'profile' })} + > + {props.validatorApy[validatorAddress] !== undefined + ? {round(props.validatorApy[validatorAddress] * 100, 1).toLocaleString() + "%"} + : "-" + } )} - + props.showValidator(validator, { activeTab: 'profile' })} + > {format(validator.commission.commission_rates.rate * 100, 2)}% {props.isLoading('delegations') || Object.keys(delegations || {}).length ? ( - + props.showValidator(validator, { activeTab: 'stake' })} + > {!props.isLoading('delegations') ? ( delegationBalance?.amount ? ( -
    props.showValidator(validator, { activeTab: 'stake' })}> - - - -
    + + + ) : null ) : ( @@ -232,42 +265,33 @@ function Validators(props) { )} ) : null} - {filter.group === 'delegated' && ( - <> - {!props.modal && ( - - {!props.isLoading('rewards') ? denomRewards && ( -
    props.showValidator(validator, { activeTab: 'stake' })}> - - - -
    - ) : ( - - Loading... - - )} - + {!props.modal && filter.group === 'delegated' && ( + props.showValidator(validator, { activeTab: 'stake' })} + > + {!props.isLoading('rewards') ? rewards && ( + + + + ) : ( + + Loading... + )} - - )} - {!props.modal && showCommission && ( - - {!props.isLoading('commission') ? ( -
    props.showValidator(validator, { activeTab: 'stake' })}> - - - -
    + + )} + {!props.modal && filter.group === 'delegated' && showCommission && ( + props.showValidator(validator, { activeTab: 'stake' })} + > + {!props.isLoading('commission') ? commission && ( + + + ) : ( Loading... @@ -372,14 +396,10 @@ function Validators(props) { {props.isLoading('delegations') || Object.keys(delegations || {}).length ? ( Delegation ) : null} - {filter.group === 'delegated' && ( - <> - {!props.modal && ( - Rewards - )} - + {!props.modal && filter.group === 'delegated' && ( + Rewards )} - {!props.modal && showCommission && ( + {!props.modal && filter.group === 'delegated' && showCommission && ( Commission )} {!props.modal && ( @@ -391,85 +411,60 @@ function Validators(props) { {results.map(item => renderValidator(item))} - - - - - {network.apyEnabled && ( - - )} - - {props.isLoading('delegations') || Object.keys(delegations || {}).length ? ( - - - { - const delegation = delegations && delegations[result.operator_address] - if (!delegation) return sum - - return add(sum, delegation.balance.amount) - }, 0), - denom: network.denom - }} - asset={network.baseAsset} - precision={3} - /> - - - ) : null} - {filter.group === 'delegated' && ( - <> - {!props.modal && ( - - {props.rewards && ( - - { - const reward = props.rewards[result.operator_address]?.reward?.find(el => el.denom === network.denom) - if (!reward) return sum - - return add(sum, reward.amount) - }, 0), - denom: network.denom - }} - asset={network.baseAsset} - precision={3} - /> - - )} - - )} - - )} - {!props.modal && showCommission && ( - - {props.commission && ( + {results.length > 1 && ( + + + + + {network.apyEnabled && ( + + )} + + {props.isLoading('delegations') || Object.keys(delegations || {}).length ? ( + - { - const commission = props.commission[result.operator_address]?.commission?.find(el => el.denom === network.denom) - if (!commission) return sum - - return add(sum, commission.amount) - }, 0), - denom: network.denom - }} + - )} - - )} - {!props.modal && ( - - )} - - - + + ) : null} + {!props.modal && filter.group === 'delegated' && ( + + {props.rewards && ( + + + + )} + + )} + {!props.modal && filter.group === 'delegated' && showCommission && ( + + {props.commission && ( + + + + )} + + )} + {!props.modal && ( + + )} + + + + )} } {results.length < 1 && diff --git a/src/components/WalletModal.js b/src/components/WalletModal.js index 9502cbba..f6674d96 100644 --- a/src/components/WalletModal.js +++ b/src/components/WalletModal.js @@ -10,9 +10,10 @@ import { } from 'react-bootstrap' import Address from './Address.js'; -import Coins from './Coins.js'; +import Coin from './Coin.js'; import Favourite from './Favourite.js'; import SavedAddresses from './SavedAddresses.js'; +import Coins from './Coins.js'; function WalletModal(props) { const { show, network, wallet, favouriteAddresses } = props @@ -32,18 +33,7 @@ function WalletModal(props) { if(!network) return null - // const balances = _.sortBy((props.balances || []).map(balance => { - const balances = _.sortBy((props.balances || []).filter(el => el.denom === network.denom).map(balance => { - const asset = network.assets.find(a => a.base?.denom === balance.denom) - return { - ...balance, - asset - } - }), ({ asset }) => { - if(!asset) return 0 - if(network.denom === asset.base?.denom) return -2 - return -1 - }) + const balances = props.balances?.length ? props.balances : [{amount: 0, denom: network.denom}] return ( <> @@ -102,13 +92,7 @@ function WalletModal(props) { Balance - {balances.map(balance => { - return ( -
    - -
    - ) - })} + diff --git a/src/utils/Chain.mjs b/src/utils/Chain.mjs index edb1de9e..617cf151 100644 --- a/src/utils/Chain.mjs +++ b/src/utils/Chain.mjs @@ -1,9 +1,11 @@ +import _ from 'lodash'; import { compareVersions, validate } from 'compare-versions'; import ChainAsset from "./ChainAsset.mjs"; -const Chain = (data) => { - const assets = data.assets?.map(el => ChainAsset(el)) || [] - const baseAsset = assets[0] +const Chain = (data, assets) => { + assets = _.uniqBy([...(assets || []), ...(data.assets?.map(el => ChainAsset(el)) || [])], 'denom') + const stakingTokens = data.staking?.staking_tokens + const baseAsset = stakingTokens && assets.find(el => el.denom === stakingTokens[0].denom) || assets[0] const { cosmos_sdk_version } = data.versions || {} const slip44 = data.slip44 || 118 const ethermint = data.ethermint ?? slip44 === 60 @@ -42,8 +44,7 @@ const Chain = (data) => { restakeSupport, sdk46OrLater, sdk50OrLater, - denom: data.denom || baseAsset?.base?.denom, - display: data.display || baseAsset?.display?.denom, + denom: data.denom || baseAsset.denom, symbol: data.symbol || baseAsset?.symbol, decimals: data.decimals || baseAsset?.decimals, image: baseAsset?.image || data.image, diff --git a/src/utils/ChainAsset.mjs b/src/utils/ChainAsset.mjs index f62b417f..cb67587c 100644 --- a/src/utils/ChainAsset.mjs +++ b/src/utils/ChainAsset.mjs @@ -1,15 +1,13 @@ function ChainAsset(data) { - const { symbol, base, display, image } = data - const decimals = display?.exponent ?? 6 + const { denom, symbol, decimals, image } = data return { ...data, + denom, symbol, - base, - display, decimals, image } } -export default ChainAsset \ No newline at end of file +export default ChainAsset diff --git a/src/utils/CoingeckoApi.mjs b/src/utils/CoingeckoApi.mjs new file mode 100644 index 00000000..b7cba1bd --- /dev/null +++ b/src/utils/CoingeckoApi.mjs @@ -0,0 +1,21 @@ +import _ from 'lodash' +import axios from 'axios' + +function CoingeckoApi(){ + async function getPrices(ids){ + if(!ids?.length) return {} + + try { + const res = await axios.get(`https://api.coingecko.com/api/v3/simple/price?ids=${ids.join(',')}&vs_currencies=usd`) + return res.data + } catch (err) { + console.error(err) + } + } + + return { + getPrices + } +} + +export default CoingeckoApi diff --git a/src/utils/Helpers.mjs b/src/utils/Helpers.mjs index d5de9d5d..e990364c 100644 --- a/src/utils/Helpers.mjs +++ b/src/utils/Helpers.mjs @@ -1,5 +1,5 @@ import _ from 'lodash' -import { format, floor, bignumber } from 'mathjs' +import { format, floor, bignumber, add, multiply, divide, numeric, pow } from 'mathjs' import truncateMiddle from 'truncate-middle' import { MsgExec } from '../messages/MsgExec.mjs'; @@ -23,6 +23,11 @@ export function truncateAddress(address) { return truncateMiddle(address, firstDigit + 6, 6, '…') } +export function truncateDenom(denom) { + const firstDigit = denom.search(/\//) + return truncateMiddle(denom, firstDigit + 5, 5, '…') +} + export function rewardAmount(rewards, denom, type){ if (!rewards) return 0; @@ -31,6 +36,50 @@ export function rewardAmount(rewards, denom, type){ return reward ? bignumber(reward.amount) : 0; } +export function sumCoins(coins){ + return Object.values(coins.reduce((sum, coin) => { + if(sum[coin.denom]){ + sum[coin.denom] = { + amount: add(sum[coin.denom].amount, coin.amount), + denom: coin.denom + } + }else{ + sum[coin.denom] = coin + } + return sum + }, {})) +} + +export function sortCoins(coins, network){ + return sortCoinsByPriority(sortCoinsByValue(coins, network), network) +} + +export function sortCoinsByValue(coins, network){ + return _.sortBy(coins, (coin) => { + const asset = network.assetForDenom(coin.denom) + let value + if(asset && asset.prices?.coingecko?.usd){ + value = numeric(multiply(divide(bignumber(coin.amount), pow(10, asset.decimals)), asset.prices.coingecko.usd), 'number') + } + return value + }).reverse() +} + +export function sortCoinsByPriority(coins, network){ + return _.sortBy(coins, (coin) => { + const asset = network.assetForDenom(coin.denom) + if (coin.denom === network.denom){ + return -1 + }else if(asset && !asset.prices?.coingecko?.usd){ + return 1 + }else if(!asset){ + return 2 + }else{ + return 0 + } + }) +} + export function overrideNetworks(networks, overrides){ networks = networks.reduce((a, v) => ({ ...a, [v.name]: v }), {}) const names = [...Object.keys(networks), ...Object.keys(overrides)] diff --git a/src/utils/Network.mjs b/src/utils/Network.mjs index aaa710b8..888d30d4 100644 --- a/src/utils/Network.mjs +++ b/src/utils/Network.mjs @@ -8,6 +8,7 @@ import Validator from './Validator.mjs' import Operator from './Operator.mjs' import Chain from './Chain.mjs' import CosmosDirectory from './CosmosDirectory.mjs' +import SkipApi from './SkipApi.mjs'; class Network { constructor(data, operatorAddresses) { @@ -60,11 +61,12 @@ class Network { } async load() { - const [chainData, validatorsData] = await Promise.all([ + const [chainData, validatorsData, skipAssets] = await Promise.all([ this.directory.getChainData(this.path), - this.directory.getValidators(this.path) + this.directory.getValidators(this.path), + SkipApi().getAssets(this.chainId) ]); - this.setChain({...this.data, ...chainData}); + this.setChain({...this.data, ...chainData}, skipAssets); this.validators = validatorsData.map(data => { return Validator(this, data); }); @@ -79,8 +81,8 @@ class Network { } } - async setChain(data){ - this.chain = Chain(data) + async setChain(data, assets){ + this.chain = Chain(data, assets) this.prettyName = this.chain.prettyName this.chainId = this.chain.chainId this.prefix = this.chain.prefix @@ -88,7 +90,6 @@ class Network { this.assets = this.chain.assets this.baseAsset = this.chain.baseAsset this.denom = this.chain.denom - this.display = this.chain.display this.symbol = this.chain.symbol this.decimals = this.chain.decimals this.image = this.chain.image @@ -205,7 +206,7 @@ class Network { } assetForDenom(denom){ - return this.assets.find(el => el.base.denom === denom) + return this.assets.find(el => el.denom === denom) } } diff --git a/src/utils/RestClient.mjs b/src/utils/RestClient.mjs index 508e83e8..682250db 100644 --- a/src/utils/RestClient.mjs +++ b/src/utils/RestClient.mjs @@ -106,11 +106,7 @@ const RestClient = async (chainId, restUrls, opts) => { .get(apiPath('distribution', `delegators/${address}/rewards`), opts) .then((res) => res.data) .then((result) => { - const rewards = result.rewards.reduce( - (a, v) => ({ ...a, [v.validator_address]: v }), - {} - ); - return rewards; + return result.rewards || []; }); } @@ -119,7 +115,7 @@ const RestClient = async (chainId, restUrls, opts) => { .get(apiPath('distribution', `validators/${validatorAddress}/commission`), opts) .then((res) => res.data) .then((result) => { - return result.commission; + return result.commission?.commission || []; }); } diff --git a/src/utils/SkipApi.mjs b/src/utils/SkipApi.mjs new file mode 100644 index 00000000..69898989 --- /dev/null +++ b/src/utils/SkipApi.mjs @@ -0,0 +1,37 @@ +import _ from 'lodash' +import axios from 'axios' +import CoingeckoApi from './CoingeckoApi.mjs' +import ChainAsset from './ChainAsset.mjs' + +function SkipApi(){ + async function getAssets(chainId){ + try { + const res = await axios.get(`https://api.skip.build/v2/fungible/assets?chain_ids=${chainId}`) + const data = res.data.chain_to_assets_map[chainId]?.assets || [] + const coingeckoIds = _.compact(data.map((el) => el.coingecko_id)) + const prices = await CoingeckoApi().getPrices(coingeckoIds) + return data.map((asset) => { + const { name, description, denom, symbol, decimals, coingecko_id } = asset + const price = prices[asset.coingecko_id] + return ChainAsset({ + name, + description, + denom, + symbol, + decimals, + coingecko_id, + image: asset.logo_uri, + prices: price && { coingecko: price } + }) + }) + } catch (err) { + console.error(err) + } + } + + return { + getAssets + } +} + +export default SkipApi