cosmos-explorer/src/views/Dashboard.vue
2023-01-31 16:50:30 -07:00

873 lines
26 KiB
Vue

<template>
<div>
<b-alert
variant="danger"
:show="syncing"
>
<div class="alert-body">
<span>{{ $t('dashboard.no_new_blocks') }}<strong>{{ latestTime }}</strong> </span>
</div>
</b-alert>
<b-row>
<b-col><dashboard-price-chart-2 /></b-col>
</b-row>
<!-- Stats Card Vertical -->
<b-row class="match-height">
<b-col
xl="2"
md="4"
sm="6"
>
<dashboard-card-vertical
icon="BoxIcon"
:statistic="height"
statistic-title="Height"
color="info"
/>
</b-col>
<b-col
xl="2"
md="4"
sm="6"
>
<dashboard-card-vertical
color="warning"
icon="DollarSignIcon"
:statistic="supply"
statistic-title="Total Supply"
/>
</b-col>
<b-col
xl="2"
md="4"
sm="6"
>
<dashboard-card-vertical
color="danger"
icon="PercentIcon"
:statistic="ratio"
:statistic-title="`Bonded: ${bonded}`"
/>
</b-col>
<b-col
xl="2"
md="4"
sm="6"
>
<dashboard-card-vertical
color="primary"
icon="TrendingUpIcon"
:statistic="inflation"
statistic-title="Inflation"
/>
</b-col>
<b-col
xl="2"
md="4"
sm="6"
>
<dashboard-card-vertical
color="success"
icon="AwardIcon"
:statistic="communityPool"
statistic-title="Community Pool"
/>
</b-col>
<b-col
xl="2"
md="4"
sm="6"
>
<dashboard-card-vertical
hide-chart
color="danger"
icon="UserCheckIcon"
:statistic="validators"
statistic-title="Active Validators"
/>
</b-col>
</b-row>
<b-card no-body>
<b-card-header>
<b-card-title>{{ $t('dashboard.active_props') }}</b-card-title>
</b-card-header>
<b-card-body>
<b-row
v-for="prop in proprosals2"
:key="prop.id"
>
<b-col
md="6"
sm="12"
>
<b-media
no-body
class="mb-1"
>
<b-media-aside
@click="showDetail(prop.id)"
>
<b-avatar
rounded
size="42"
variant="light-primary"
>
{{ prop.id }}
</b-avatar>
</b-media-aside>
<b-link :to="`./${chain}/gov/${prop.id}`">
<b-media-body class="d-flex flex-column justify-content-center">
<h6 class="transaction-title">
<b-badge
pill
variant="light-primary"
>
{{ formatType(prop.contents['@type']) }}
</b-badge>{{ prop.title }}
</h6>
<small>will {{ caculateTallyResult(prop.tally) }} {{ formatEnding(prop.voting_end_time) }}</small>
</b-media-body>
</b-link>
</b-media>
</b-col>
<b-col
md="6"
sm="12"
>
<b-row>
<b-col cols="8">
<div class="scale">
<div class="box">
<b-progress
:max="totalPower? 100 * (totalPower/prop.tally.total) :100"
height="2rem"
show-progress
class="font-small-1"
>
<b-progress-bar
:id="'vote-yes'+prop.id"
variant="success"
:value="percent(prop.tally.yes)"
show-progress
:label="`${percent(prop.tally.yes).toFixed()}%`"
/>
<b-progress-bar
:id="'vote-no'+prop.id"
variant="danger"
:value="percent(prop.tally.no)"
:label="`${percent(prop.tally.no).toFixed()}%`"
show-progress
/>
<b-progress-bar
:id="'vote-veto'+prop.id"
class="bg-danger bg-darken-4"
:value="percent(prop.tally.veto)"
:label="`${percent(prop.tally.veto).toFixed()}%`"
show-progress
/>
<b-progress-bar
:id="'vote-abstain'+prop.id"
variant="secondary"
:value="percent(prop.tally.abstain)"
:label="`${percent(prop.tally.abstain).toFixed()}%`"
show-progress
/>
</b-progress>
</div>
<div
v-b-tooltip.hover
title="Threshold"
class="box overlay"
:style="`left:${scaleWidth(prop)}%;`"
/>
<div
v-if="tallyParam"
v-b-tooltip.hover
title="Quorum"
class="box overlay"
:style="`left:${Number(tallyParam.quorum) * 100}%; border-color:black`"
/>
</div>
<b-tooltip
:target="'vote-yes'+prop.id"
>
{{ percent(prop.tally.yes) }}% {{ $t('dashboard.proposal_votes_yes') }}
</b-tooltip>
<b-tooltip
:target="'vote-no'+prop.id"
>
{{ percent(prop.tally.no) }}% {{ $t('dashboard.proposal_votes_no') }}
</b-tooltip>
<b-tooltip
:target="'vote-veto'+prop.id"
>
{{ percent(prop.tally.veto) }}% {{ $t('dashboard.proposal_votes_nwv') }}
</b-tooltip>
<b-tooltip
:target="'vote-abstain'+prop.id"
>
{{ percent(prop.tally.abstain) }}% {{ $t('dashboard.proposal_votes_abstain') }}
</b-tooltip>
</b-col>
<b-col
cols="4"
style="padding-top: 0.5em"
>
<b-button
v-b-modal.operation-modal
variant="primary"
size="sm"
class="mb-2"
@click="selectProposal('Vote',prop.id, prop.title)"
>
{{ myVotes[prop.id] ? `${myVotes[prop.id]}`: 'Vote' }}
</b-button>
</b-col>
</b-row>
</b-col>
<b-col
cols="12"
:class="detailId === prop.id? 'd-block': 'd-none'"
>
<b-card
border-variant="primary"
bg-variant="transparent"
class="shadow-none"
style="max-height:350px;overflow: auto;"
>
<VueMarkdown class="pb-1">
{{ addNewLine(prop.description) }}
</VueMarkdown>
</b-card>
</b-col>
</b-row>
<div v-if="proprosals2.length === 0">
{{ $t('dashboard.no_active_prop') }}
<b-link :to="`./${chain}/gov`">
{{ $t('dashboard.browse') }}
</b-link>
</div>
</b-card-body>
</b-card>
<b-card
border-variant="primary"
bg-variant="transparent"
class="shadow-none"
>
<b-card-title class="d-flex justify-content-between text-capitalize">
<span>{{ walletName }} {{ $t('dashboard.assets') }} </span>
<small>
<b-link
v-if="address"
:to="`./${chain}/account/${address}`"
>
{{ $t('dashboard.more') }}
</b-link>
<b-link
v-else
:to="`/wallet/accounts`"
>
{{ $t('dashboard.not_conn') }}
</b-link>
</small>
</b-card-title>
<b-row>
<b-col
lg="3"
sm="6"
>
<dashboard-card-horizontal
icon="DollarSignIcon"
color="success"
:statistic="walletBalances"
statistic-title="Balances"
/>
</b-col>
<b-col
lg="3"
sm="6"
>
<dashboard-card-horizontal
icon="LockIcon"
:statistic="walletStaking"
statistic-title="Staking"
/>
</b-col>
<b-col
lg="3"
sm="6"
>
<dashboard-card-horizontal
icon="ArrowUpCircleIcon"
color="info"
:statistic="walletRewards"
statistic-title="Rewards"
/>
</b-col>
<b-col
lg="3"
sm="6"
>
<dashboard-card-horizontal
icon="UnlockIcon"
color="danger"
:statistic="walletUnbonding"
statistic-title="Unbonding"
/>
</b-col>
</b-row>
<b-row v-if="stakingList && stakingList.length > 0">
<b-col>
<b-card no-body>
<b-table
:items="stakingList"
:fields="fields"
striped
hover
responsive="sm"
stacked="sm"
>
<template #cell(action)="data">
<!-- size -->
<b-button-group
size="sm"
class="d-none"
>
<b-button
v-b-modal.operation-modal
v-ripple.400="'rgba(113, 102, 240, 0.15)'"
v-b-tooltip.hover.top="'Delegate'"
variant="outline-primary"
@click="selectDelegation(data,'Delegate')"
>
<feather-icon icon="LogInIcon" />
</b-button>
<b-button
v-b-modal.operation-modal
v-ripple.400="'rgba(113, 102, 240, 0.15)'"
v-b-tooltip.hover.top="'Redelegate'"
variant="outline-primary"
@click="selectDelegation(data,'Redelegate')"
>
<feather-icon icon="ShuffleIcon" />
</b-button>
<b-button
v-b-modal.operation-modal
v-ripple.400="'rgba(113, 102, 240, 0.15)'"
v-b-tooltip.hover.top="'Unbond'"
variant="outline-primary"
@click="selectDelegation(data,'Unbond')"
>
<feather-icon icon="LogOutIcon" />
</b-button>
</b-button-group>
<b-dropdown
v-b-modal.operation-modal
split
variant="outline-primary"
text="Delegate"
class="mr-1"
size="sm"
@click="selectDelegation(data,'Delegate')"
>
<template #button-content>
{{ $t('dashboard.delegate') }}
</template>
<b-dropdown-item
v-b-modal.operation-modal
@click="selectDelegation(data,'Redelegate')"
>
{{ $t('dashboard.redelegate') }}
</b-dropdown-item>
<b-dropdown-item
v-b-modal.operation-modal
@click="selectDelegation(data,'Unbond')"
>
{{ $t('dashboard.unbond') }}
</b-dropdown-item>
</b-dropdown>
<b-button
v-b-modal.operation-modal
variant="outline-primary"
size="sm"
@click="selectWithdraw()"
>
{{ $t('dashboard.withdraw_reward') }}
</b-button>
</template>
</b-table>
</b-card>
</b-col>
</b-row>
<b-row v-if="unbonding && unbonding.length > 0">
<b-col>
<b-card>
<b-card-header class="pt-0 pl-0 pr-0">
<b-card-title>{{ $t('dashboard.unbonding_token') }}</b-card-title>
</b-card-header>
<b-card-body class="pl-0 pr-0">
<b-row
v-for="item in unbonding"
:key="item.validator_address"
>
<b-col cols="12">
<span class="font-weight-bolder">From: <router-link :to="`../staking/${item.validator_address}`">{{ item.validator_address }}</router-link></span>
</b-col>
<b-col cols="12">
<b-table
:items="item.entries"
class="mt-1"
striped
hover
responsive="sm"
stacked="sm"
>
<template #cell(completion_time)="data">
{{ formatDate(data.item.completion_time) }}
</template>
<template #cell(initial_balance)="data">
{{ data.item.initial_balance }}
</template>
<template #cell(balance)="data">
{{ data.item.balance }}
</template>
</b-table>
</b-col>
</b-row>
</b-card-body>
</b-card>
</b-col>
</b-row>
<b-row
v-if="address"
class="mt-1"
>
<b-col cols="6">
<b-button
v-b-modal.operation-modal
block
variant="success"
@click="selectSend()"
>
<feather-icon icon="SendIcon" />
{{ $t('dashboard.send') }}
</b-button>
</b-col>
<b-col cols="6">
<b-button
block
variant="info"
:to="`${chain}/account/${address}/receive`"
>
<feather-icon
icon="PlusCircleIcon"
/>
{{ $t('dashboard.receive') }}
</b-button>
</b-col>
</b-row>
</b-card>
<router-link to="/wallet/import">
<b-card class="addzone text-center">
<feather-icon icon="PlusIcon" />
{{ $t('dashboard.connect_wal') }}
</b-card>
</router-link>
<operation-modal
:address="address"
:validator-address="selectedValidator"
:type="operationModalType"
:proposal-id="selectedProposalId"
:proposal-title="selectedTitle"
/>
<div id="txevent" />
</div>
</template>
<script>
import {
BRow, BCol, BAlert, BCard, BTable, BFormCheckbox, BCardHeader, BCardTitle, BMedia, BMediaAside, BMediaBody, BAvatar,
BCardBody, BLink, BButtonGroup, BButton, BTooltip, VBModal, VBTooltip, BCardFooter, BProgress, BProgressBar, BBadge,
BDropdown, BDropdownItem,
} from 'bootstrap-vue'
import {
formatNumber, formatTokenAmount, isToken, percent, timeIn, toDay, toDuration, tokenFormatter, getLocalAccounts,
getStakingValidatorOperator, formatToken,
} from '@/libs/utils'
import OperationModal from '@/views/components/OperationModal/index.vue'
import Ripple from 'vue-ripple-directive'
import dayjs from 'dayjs'
import VueMarkdown from 'vue-markdown'
import ParametersModuleComponent from './components/parameters/ParametersModuleComponent.vue'
import DashboardCardHorizontal from './components/dashboard/DashboardCardHorizontal.vue'
import DashboardCardVertical from './components/dashboard/DashboardCardVertical.vue'
import DashboardPriceChart2 from './components/dashboard/DashboardPriceChart2.vue'
import FeatherIcon from '../@core/components/feather-icon/FeatherIcon.vue'
export default {
components: {
BAvatar,
BButtonGroup,
BTooltip,
BButton,
BDropdown,
BDropdownItem,
BRow,
BCol,
BAlert,
BCard,
BTable,
BFormCheckbox,
BCardHeader,
BCardTitle,
BMediaBody,
BMediaAside,
BMedia,
BCardBody,
BLink,
BCardFooter,
BProgress,
BProgressBar,
VueMarkdown,
BBadge,
OperationModal,
ParametersModuleComponent,
DashboardCardHorizontal,
DashboardPriceChart2,
DashboardCardVertical,
FeatherIcon,
},
directives: {
'b-modal': VBModal,
'b-tooltip': VBTooltip,
Ripple,
},
data() {
return {
detailId: 0,
fields: ['validator', 'delegation', 'rewards', 'action'],
delegations: [],
rewards: [],
unbonding: [],
chain: this.$store.state.chains.selected.chain_name,
syncing: false,
latestTime: '',
marketData: null,
height: '-',
supply: '-',
bonded: '-',
validators: '-',
communityPool: '-',
ratio: '-',
inflation: '-',
proposals: [],
myVotes: {},
selectedValidator: '',
selectedProposalId: 0,
selectedTitle: '',
operationModalType: '',
tallyParam: null,
totalPower: 0,
voteColors: {
YES: 'success',
NO: 'warning',
ABSTAIN: 'info',
NO_WITH_VETO: 'danger',
},
walletBalances: '-',
walletStaking: '-',
walletRewards: '-',
walletUnbonding: '-',
address: null,
}
},
computed: {
walletName() {
const key = this.$store?.state?.chains?.defaultWallet
if (key) {
const accounts = getLocalAccounts() || {}
const account = Object.entries(accounts)
.map(v => ({ wallet: v[0], address: v[1].address.find(x => x.chain === this.$store.state.chains.selected.chain_name) }))
.filter(v => v.address)
.find(x => x.wallet === key)
if (account) {
this.fetchAccount(account.address.addr)
}
}
return key || 'Wallet'
},
proprosals2() {
return this.proposals
},
stakingList() {
return this.delegations.map(x => {
const rewards = this.rewards.find(r => r.validator_address === x.delegation.validator_address)
const conf = this.$http.getSelectedConfig()
const decimal = conf.assets[0].exponent || '6'
return {
valAddress: x.delegation.validator_address,
validator: getStakingValidatorOperator(this.$store.state.chains.selected.chain_name, x.delegation.validator_address),
delegation: formatToken(x.balance, {}, decimal),
rewards: rewards ? this.formatToken(rewards.reward) : '',
action: '',
}
})
},
},
created() {
this.$http.getStakingParameters().then(res => {
Promise.all([this.$http.getStakingPool(), this.$http.getBankTotal(res.bond_denom)])
.then(pool => {
this.supply = `${formatNumber(formatTokenAmount(pool[1].amount, 2, res.bond_denom, false), true, 2)}`
this.bonded = `${formatNumber(formatTokenAmount(pool[0].bondedToken, 2, res.bond_denom, false), true, 2)}`
this.ratio = `${percent(pool[0].bondedToken / pool[1].amount)}%`
this.totalPower = pool[0].bondedToken
})
})
this.$http.getGovernanceListByStatus(2).then(gov => {
this.proposals = gov.proposals
this.proposals.forEach(p => {
this.$http.getGovernanceTally(p.id, 0).then(update => {
// const p2 = p
// p2.tally = update
// this.proposals.push(p2)
// this.proposals.sort((a, b) => a.id - b.id)
this.$set(p, 'tally', update)
})
})
})
this.$http.getLatestBlock().then(res => {
this.height = res.block.header.height
if (timeIn(res.block.header.time, 3, 'm')) {
this.syncing = true
} else {
this.syncing = false
}
this.latestTime = toDay(res.block.header.time, 'long')
this.validators = res.block.last_commit.signatures.length
})
this.$http.getCommunityPool().then(res => {
this.communityPool = this.formatToken(res.pool)
})
this.$http.getGovernanceParameterTallying().then(res => {
this.tallyParam = res
})
const conf = this.$http.getSelectedConfig()
if (conf.excludes && conf.excludes.indexOf('mint') > -1) {
this.inflation = '-'
} else {
this.$http.getMintingInflation().then(res => {
this.inflation = `${percent(res)}%`
}).catch(() => {
this.inflation = '-'
})
}
},
mounted() {
const elem = document.getElementById('txevent')
elem.addEventListener('txcompleted', () => {
const key = this.$store?.state?.chains?.defaultWallet
if (key) {
const accounts = getLocalAccounts() || {}
const account = Object.entries(accounts)
.map(v => ({ wallet: v[0], address: v[1].address.find(x => x.chain === this.$store.state.chains.selected.chain_name) }))
.filter(v => v.address)
.find(x => x.wallet === key)
if (account) {
this.fetchAccount(account.address.addr)
}
}
})
},
methods: {
caculateTallyResult(tally) {
if (this.tallyParam && tally && this.totalPower > 0) {
if (tally.veto < Number(this.tallyParam.veto_threshold)
&& tally.yes > Number(this.tallyParam.threshold)
&& tally.total / this.totalPower > Number(this.tallyParam.quorum)) {
return 'pass'
}
}
return 'be rejected'
},
scaleWidth(p) {
if (this.tallyParam) {
return Number(this.tallyParam.quorum) * Number(this.tallyParam.threshold) * (1 - p.tally.abstain) * 100
}
return 50
},
selectProposal(modal, pid, title) {
this.operationModalType = modal
this.selectedProposalId = Number(pid)
this.selectedTitle = title
},
selectDelegation(v, type) {
this.selectedValidator = v.item.valAddress
this.operationModalType = type
},
selectSend() {
this.operationModalType = 'Transfer'
},
selectWithdraw() {
this.operationModalType = 'Withdraw'
},
formatToken(tokens) {
if (Array.isArray(tokens)) {
let nativeToken = tokens.filter(x => x.denom.length < 11)
if (tokens.length > 1) {
const sum = {}
const reduce = nativeToken.reduce((b, a) => {
const b2 = b
if (b2[a.denom]) {
b2[a.denom] += Number(a.amount)
} else {
b2[a.denom] = Number(a.amount)
}
return b2
}, sum)
nativeToken = Object.keys(reduce).map(k => ({ denom: k, amount: reduce[k] }))
}
return tokenFormatter(nativeToken, {}, 0)
}
return '-'
},
fetchAccount(address) {
this.address = address
this.$http.getBankAccountBalance(address).then(bal => {
this.walletBalances = this.formatToken(bal)
})
this.$http.getStakingReward(address).then(res => {
this.rewards = res.rewards
this.walletRewards = this.formatToken(res.rewards.map(x => x.reward).flat())
})
this.$http.getStakingDelegations(address).then(res => {
const delegations = res.delegation_responses || res
this.delegations = delegations
this.walletStaking = this.formatToken(delegations.map(x => x.balance).flat())
})
this.$http.getStakingUnbonding(address).then(res => {
const token = this.$store.state.chains.selected.assets[0]
if (token) {
const newTokens = []
const denom = token.base
const unbonding = res.unbonding_responses || res
this.unbonding = unbonding
unbonding.forEach(x => {
x.entries.forEach(y => {
newTokens.push({
amount: y.balance,
denom,
})
})
})
if (newTokens.length > 0) {
this.walletUnbonding = this.formatToken(newTokens)
}
}
})
this.proposals.forEach(x => {
this.$http.getGovernanceProposalVote(x.id, address, null)
.then(v => {
this.myVotes[x.id] = this.formatVoteOption(v.vote.option)
})
.catch(() => {
this.myVotes[x.id] = null
})
})
},
formatVoteOption(v) {
return v.replaceAll('VOTE_OPTION_', '')
},
formatEnding(v) {
return toDay(v, 'from')
},
formatType(v) {
const txt = String(v).replace('Proposal', '')
const index = txt.lastIndexOf('.')
return index > 0 ? txt.substring(index + 1) : txt
},
normalize(data, title) {
if (!data) return null
const items = this.makeItems(data)
return {
title,
items,
}
},
makeItems(data) {
return Object.keys(data).map(k => {
if (isToken(data[k])) {
return { title: tokenFormatter(data[k]), subtitle: k }
}
if (typeof data[k] === 'boolean') {
return { title: data[k], subtitle: k }
}
return { title: this.convert(data[k]), subtitle: k }
})
},
addNewLine(value) {
return value ? value.replace(/(?:\\[rn])+/g, '\n') : '-'
},
percent: v => percent(v),
processBarLength(v) {
return percent(v)
},
formatDate: v => dayjs(v).format('YYYY-MM-DD HH:mm:ss'),
convert(v) {
if (typeof v === 'object') {
const v2 = {}
Object.entries(v).forEach(e => {
const k = e[0]
const x = e[1]
v2[k] = this.convert(x)
})
return v2
}
const d = parseFloat(v)
if (d === 0) return '0'
if (d < 1.01) {
return `${percent(d)}%`
}
if (d > 1000000000) {
return `${toDuration(d / 1000000)}`
}
if (d > 0) {
return d.toFixed()
}
return v
},
showDetail(id) {
if (this.detailId !== id) {
this.detailId = id
} else {
this.detailId = 0
}
},
},
}
</script>
<style>
.addzone {
border: 2px dashed #ced4da;
background: #fff;
border-radius: 6px;
cursor: pointer;
box-shadow: none;
}
.addzone :hover {
border: 2px dashed #7367F0;
}
</style>