improve validator and proposal

This commit is contained in:
liangping 2023-04-19 12:05:19 +08:00
parent d3a7c6f176
commit d6d56ba7c0
21 changed files with 1552 additions and 3276 deletions

View File

@ -15,6 +15,7 @@
"dependencies": { "dependencies": {
"@casl/ability": "^6.3.3", "@casl/ability": "^6.3.3",
"@casl/vue": "^2.2.1", "@casl/vue": "^2.2.1",
"@chenfengyuan/vue-countdown": "2",
"@cosmjs/crypto": "^0.29.5", "@cosmjs/crypto": "^0.29.5",
"@cosmjs/encoding": "^0.29.5", "@cosmjs/encoding": "^0.29.5",
"@floating-ui/dom": "^1.2.0", "@floating-ui/dom": "^1.2.0",
@ -22,6 +23,7 @@
"@intlify/unplugin-vue-i18n": "^0.8.2", "@intlify/unplugin-vue-i18n": "^0.8.2",
"@osmonauts/lcd": "^0.8.0", "@osmonauts/lcd": "^0.8.0",
"@ping-pub/chain-registry-client": "^0.0.25", "@ping-pub/chain-registry-client": "^0.0.25",
"@tomieric/vue-flip-countdown": "^0.0.5",
"@vitejs/plugin-vue-jsx": "^3.0.0", "@vitejs/plugin-vue-jsx": "^3.0.0",
"@vueuse/core": "^9.12.0", "@vueuse/core": "^9.12.0",
"@vueuse/math": "^9.12.0", "@vueuse/math": "^9.12.0",
@ -41,6 +43,7 @@
"vue-json-pretty": "^2.2.4", "vue-json-pretty": "^2.2.4",
"vue-router": "^4.1.6", "vue-router": "^4.1.6",
"vue3-apexcharts": "^1.4.1", "vue3-apexcharts": "^1.4.1",
"vue3-flip-countdown": "^0.1.6",
"vue3-perfect-scrollbar": "^1.6.1", "vue3-perfect-scrollbar": "^1.6.1",
"vuetify": "3.0.6", "vuetify": "3.0.6",
"webfontloader": "^1.6.28" "webfontloader": "^1.6.28"

View File

@ -0,0 +1,12 @@
<script lang="ts" setup >
import VueCountdown from '@chenfengyuan/vue-countdown';
const props = defineProps({
time: { type: Number},
})
</script>
<template>
<vue-countdown v-if="time" :time="time > 0? time: 0" v-slot="{ days, hours, minutes, seconds }">
Time Remaining{{ days }} days, {{ hours }} hours, {{ minutes }} minutes, {{ seconds }} seconds.
</vue-countdown>
</template>

View File

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import MdEditor from 'md-editor-v3'; import MdEditor from 'md-editor-v3';
import { useFormatter, useStakingStore } from '@/stores'; import { useBlockchain, useFormatter, useStakingStore } from '@/stores';
import type { GovProposal, PaginatedProposals } from '@/types'; import type { GovProposal, PaginatedProposals } from '@/types';
import ProposalProcess from './ProposalProcess.vue'; import ProposalProcess from './ProposalProcess.vue';
import type { PropType } from 'vue'; import type { PropType } from 'vue';
@ -11,13 +11,20 @@ const props = defineProps({
// const list = computed(()=> proposl) // const list = computed(()=> proposl)
const format = useFormatter() const format = useFormatter()
const staking = useStakingStore() const staking = useStakingStore()
const chain = useBlockchain()
function showType(v: string){
if(v) {
return v.substring(v.lastIndexOf('.')+1)
}
return v
}
</script> </script>
<template> <template>
<VExpansionPanels variant="accordion"> <VExpansionPanels variant="accordion">
<VExpansionPanel v-for="(x, i) in proposals?.proposals"> <VExpansionPanel v-for="(x, i) in proposals?.proposals">
<VExpansionPanelTitle disable-icon-rotate> <VExpansionPanelTitle disable-icon-rotate>
<VChip label color="primary" class="mr-2">{{x.proposal_id}}</VChip> <VChip label color="primary" class="mr-2">{{x.proposal_id}}</VChip>
<div class="w-100">{{ x.content?.title }} <div class="w-100"><VChip label>{{ showType(x.content['@type']) }}</VChip> {{ x.content?.title }}
<div class="d-flex mt-1"> <div class="d-flex mt-1">
<small class="text-secondary me-auto"> {{ format.toDay(x.voting_end_time, 'from') }}</small> <small class="text-secondary me-auto"> {{ format.toDay(x.voting_end_time, 'from') }}</small>
<ProposalProcess style="width:300px;" :pool="staking.pool" :tally="x.final_tally_result"></ProposalProcess> <ProposalProcess style="width:300px;" :pool="staking.pool" :tally="x.final_tally_result"></ProposalProcess>
@ -42,11 +49,11 @@ const staking = useStakingStore()
<VExpansionPanelText> <VExpansionPanelText>
<VCard class="card-box"> <VCard class="card-box">
<VCardText> <VCardText>
{{ x.final_tally_result }}
<MdEditor :model-value="format.multiLine(x.content?.description)" previewOnly></MdEditor> <MdEditor :model-value="format.multiLine(x.content?.description)" previewOnly></MdEditor>
</VCardText> </VCardText>
<div class="text-center w-100 my-2"> <div class="text-center w-100 my-2">
<VBtn color="primary" variant="flat">Vote</VBtn> <VBtn :to="`/${chain.chainName}/gov/${x.proposal_id}`" color="primary" variant="flat" size="small">Detail</VBtn>
<VBtn color="primary" variant="flat" class="ml-2" size="small">Vote</VBtn>
</div> </div>
</VCard> </VCard>
</VExpansionPanelText> </VExpansionPanelText>

View File

@ -0,0 +1,21 @@
<script lang="ts" setup>
import { useFormatter } from '@/stores';
import type { Coin } from '@/types';
const props = defineProps({
value: { type: Array<Coin>},
})
const format = useFormatter()
</script>
<template>
<div>
{{ format.formatTokens(value, true, "0,0.[000000]") }}
</div>
</template>
<script lang="ts">
export default {
name: 'ArrayCoinElement'
}
</script>

View File

@ -7,6 +7,7 @@ import {select} from './index'
import ArrayBytesElement from './ArrayBytesElement.vue'; import ArrayBytesElement from './ArrayBytesElement.vue';
import ArrayObjectElement from './ArrayObjectElement.vue'; import ArrayObjectElement from './ArrayObjectElement.vue';
import TextElement from './TextElement.vue'; import TextElement from './TextElement.vue';
import ArrayCoinElement from './ArrayCoinElement.vue';
const props = defineProps({ const props = defineProps({
value: { type: Array<Object>}, value: { type: Array<Object>},
@ -18,6 +19,8 @@ function selectByElement() {
switch(true) { switch(true) {
case first instanceof Uint8Array: case first instanceof Uint8Array:
return ArrayBytesElement return ArrayBytesElement
case Object.keys(first).includes('denom'):
return ArrayCoinElement
default: default:
return ArrayObjectElement return ArrayObjectElement
} }

View File

@ -1,7 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import TextElement from './TextElement.vue'
import ObjectElement from './ObjectElement.vue'
import { select } from './index' import { select } from './index'
const props = defineProps(["value", "direct"]); const props = defineProps(["value", "direct"]);

View File

@ -1,6 +1,17 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useFormatter } from '@/stores';
import MdEditor from 'md-editor-v3';
const props = defineProps(["value"]); const props = defineProps(["value"]);
const format = useFormatter()
function isMD() {
if(props.value && (props.value.indexOf("\n") > -1 || props.value.indexOf("\\n") > -1)){
return true
}
return false
}
</script> </script>
<template> <template>
<span>{{ props.value }}</span> <MdEditor v-if="isMD()" :model-value="format.multiLine(value)" previewOnly></MdEditor>
<span v-else>{{ value }}</span>
</template> </template>

View File

@ -10,7 +10,7 @@ export const DEFAULT: RequestRegistry = {
bank_supply: { url: "/cosmos/bank/v1beta1/supply", adapter }, bank_supply: { url: "/cosmos/bank/v1beta1/supply", adapter },
bank_supply_by_denom: { url: "/cosmos/bank/v1beta1/supply/{denom}", adapter }, bank_supply_by_denom: { url: "/cosmos/bank/v1beta1/supply/{denom}", adapter },
distribution_params: { url: "/cosmos/distribution/v1beta1/params", adapter }, distribution_params: { url: "/cosmos/distribution/v1beta1/params", adapter },
distributino_community_pool: {url: "/cosmos/distribution/v1beta1/community_pool", adapter}, distributino_community_pool: { url: "/cosmos/distribution/v1beta1/community_pool", adapter },
distribution_validator_commission: { url: "/cosmos/distribution/v1beta1/validators/{validator_address}/commission", adapter }, distribution_validator_commission: { url: "/cosmos/distribution/v1beta1/validators/{validator_address}/commission", adapter },
distribution_validator_outstanding_rewards: { url: "/cosmos/distribution/v1beta1/validators/{validator_address}/outstanding_rewards", adapter }, distribution_validator_outstanding_rewards: { url: "/cosmos/distribution/v1beta1/validators/{validator_address}/outstanding_rewards", adapter },
distribution_validator_slashes: { url: "/cosmos/distribution/v1beta1/validators/{validator_address}/slashes", adapter }, distribution_validator_slashes: { url: "/cosmos/distribution/v1beta1/validators/{validator_address}/slashes", adapter },
@ -20,10 +20,10 @@ export const DEFAULT: RequestRegistry = {
gov_params_tally: { url: "/cosmos/gov/v1beta1/params/tallying", adapter }, gov_params_tally: { url: "/cosmos/gov/v1beta1/params/tallying", adapter },
gov_params_deposit: { url: "/cosmos/gov/v1beta1/params/deposit", adapter }, gov_params_deposit: { url: "/cosmos/gov/v1beta1/params/deposit", adapter },
gov_proposals: { url: "/cosmos/gov/v1beta1/proposals", adapter }, gov_proposals: { url: "/cosmos/gov/v1beta1/proposals", adapter },
gov_proposals_proposal_id: {url: "/cosmos/gov/v1beta1/proposals/{proposal_id}", adapter}, gov_proposals_proposal_id: { url: "/cosmos/gov/v1beta1/proposals/{proposal_id}", adapter },
gov_proposals_deposits: { url: "/cosmos/gov/v1beta1/proposals/{proposal_id}/deposits", adapter }, gov_proposals_deposits: { url: "/cosmos/gov/v1beta1/proposals/{proposal_id}/deposits", adapter },
gov_proposals_tally: { url: "/cosmos/gov/v1beta1/proposals/{proposal_id}/tally", adapter }, gov_proposals_tally: { url: "/cosmos/gov/v1beta1/proposals/{proposal_id}/tally", adapter },
gov_proposals_votes: { url: "/cosmos/gov/v1beta1/proposals/{proposal_id}/votes", adapter }, gov_proposals_votes: { url: "/cosmos/gov/v1beta1/proposals/{proposal_id}/votes?pagination.key={next_key}", adapter },
gov_proposals_votes_voter: { url: "/cosmos/gov/v1beta1/proposals/{proposal_id}/votes/{voter}", adapter }, gov_proposals_votes_voter: { url: "/cosmos/gov/v1beta1/proposals/{proposal_id}/votes/{voter}", adapter },
staking_deletations: { url: "/cosmos/staking/v1beta1/delegations/{delegator_addr}", adapter }, staking_deletations: { url: "/cosmos/staking/v1beta1/delegations/{delegator_addr}", adapter },
staking_delegator_redelegations: { url: "/cosmos/staking/v1beta1/delegators/{delegator_addr}/redelegations", adapter }, staking_delegator_redelegations: { url: "/cosmos/staking/v1beta1/delegators/{delegator_addr}/redelegations", adapter },
@ -45,6 +45,20 @@ export const DEFAULT: RequestRegistry = {
tx_txs: { url: "/cosmos/tx/v1beta1/txs", adapter }, tx_txs: { url: "/cosmos/tx/v1beta1/txs", adapter },
tx_txs_block: { url: "/cosmos/tx/v1beta1/txs/block/{height}", adapter }, tx_txs_block: { url: "/cosmos/tx/v1beta1/txs/block/{height}", adapter },
tx_hash: { url: "/cosmos/tx/v1beta1/txs/{hash}", adapter }, tx_hash: { url: "/cosmos/tx/v1beta1/txs/{hash}", adapter },
mint_inflation: { url: "/cosmos/mint/v1beta1/inflation", adapter},
mint_params: { url: "/cosmos/mint/v1beta1/params", adapter},
mint_annual_provisions: { url: "/cosmos/mint/v1beta1/annual_provisions", adapter},
// ibc
ibc_app_ica_controller_params: { url: "/ibc/apps/interchain_accounts/controller/v1/params", adapter },
ibc_app_ica_host_params: { url: "/ibc/apps/interchain_accounts/host/v1/params", adapter},
ibc_app_transfer_escrow_address: { url: "/ibc/apps/transfer/v1/channels/{channel_id}/ports/{port_id}/escrow_address", adapter},
ibc_app_transfer_denom_traces: { url: "/ibc/apps/transfer/v1/denom_traces", adapter},
ibc_app_transfer_denom_traces_hash: { url: "/ibc/apps/transfer/v1/denom_traces/{hash}", adapter},
ibc_core_channel_channels: { url: "/ibc/core/channel/v1/channels", adapter},
ibc_core_channel_channels_next_sequence: { url: "/ibc/core/channel/v1/channels/{channel_id}/ports/{port_id}/next_sequence", adapter},
ibc_core_channel_channels_acknowledgements: { url: "/ibc/core/channel/v1/channels/{channel_id}/ports/{port_id}/packet_acknowledgements", adapter}
}; };
export const VERSION_REGISTRY: Registry = { export const VERSION_REGISTRY: Registry = {

View File

@ -12,7 +12,7 @@ export class CosmosRestClient {
async request<T>(request: Request<T>, args: Record<string, any>, query="") { async request<T>(request: Request<T>, args: Record<string, any>, query="") {
let url = `${this.endpoint}${request.url}${query}` let url = `${this.endpoint}${request.url}${query}`
Object.keys(args).forEach(k => { Object.keys(args).forEach(k => {
url = url.replace(`{${k}}`, args[k]) url = url.replace(`{${k}}`, args[k] || "")
}) })
return fetchData<T>(url, adapter) return fetchData<T>(url, adapter)
} }
@ -72,7 +72,7 @@ export class CosmosRestClient {
async getGovParamsTally() { async getGovParamsTally() {
return this.request(this.registry.gov_params_tally, {}) return this.request(this.registry.gov_params_tally, {})
} }
async getGovProposals(status: string, limit = 100) { async getGovProposals(status: string, limit = 50) {
const query = "?proposal_status={status}&pagination.limit={limit}&pagination.reverse=true&pagination.key=" const query = "?proposal_status={status}&pagination.limit={limit}&pagination.reverse=true&pagination.key="
return this.request(this.registry.gov_proposals, {status, limit}, query) return this.request(this.registry.gov_proposals, {status, limit}, query)
} }
@ -85,8 +85,8 @@ export class CosmosRestClient {
async getGovProposalTally(proposal_id: string) { async getGovProposalTally(proposal_id: string) {
return this.request(this.registry.gov_proposals_tally, {proposal_id}) return this.request(this.registry.gov_proposals_tally, {proposal_id})
} }
async getGovProposalVotes(proposal_id: string) { async getGovProposalVotes(proposal_id: string, next_key?: string) {
return this.request(this.registry.gov_proposals_votes, {proposal_id}) return this.request(this.registry.gov_proposals_votes, {proposal_id, next_key})
} }
async getGovProposalVotesVoter(proposal_id: string, voter: string ) { async getGovProposalVotesVoter(proposal_id: string, voter: string ) {
return this.request(this.registry.gov_proposals_votes_voter, {proposal_id, voter}) return this.request(this.registry.gov_proposals_votes_voter, {proposal_id, voter})
@ -147,7 +147,7 @@ export class CosmosRestClient {
} }
// tx // tx
async getTxsBySender(sender: string) { async getTxsBySender(sender: string) {
const query = `?events=message.sender='${sender}'&pagination.reverse=true` const query = `?pagination.reverse=true&events=message.sender='${sender}'`
return this.request(this.registry.tx_txs, {}, query) return this.request(this.registry.tx_txs, {}, query)
} }
async getTxsAt(height: string|number) { async getTxsAt(height: string|number) {
@ -168,4 +168,10 @@ export class CosmosRestClient {
return this.request(this.registry.mint_annual_provisions, {}) return this.request(this.registry.mint_annual_provisions, {})
} }
// ibc
async getIBCAppTransferDenom(hash: string) {
return this.request(this.registry.ibc_app_transfer_denom_traces_hash, {hash})
}
} }

View File

@ -76,6 +76,21 @@ export interface RequestRegistry {
tx_txs_block: Request<Tx>; tx_txs_block: Request<Tx>;
tx_hash: Request<{tx: Tx, tx_response: TxResponse}>; tx_hash: Request<{tx: Tx, tx_response: TxResponse}>;
ibc_app_ica_controller_params: Request<any>;
ibc_app_ica_host_params: Request<any>
ibc_app_transfer_escrow_address: Request<any>;
ibc_app_transfer_denom_traces: Request<any>;
ibc_app_transfer_denom_traces_hash: Request<{
"denom_trace": {
"path": "string",
"base_denom": "string"
}
}>;
ibc_core_channel_channels: Request<any>;
ibc_core_channel_channels_next_sequence: Request<any>;
ibc_core_channel_channels_acknowledgements: Request<any>;
} }
export function adapter<T>(source: any): T { export function adapter<T>(source: any): T {

View File

@ -0,0 +1,317 @@
<script lang="ts" setup>
import ObjectElement from '@/components/dynamic/ObjectElement.vue';
import { useBaseStore, useFormatter, useGovStore, useStakingStore } from '@/stores';
import type { GovProposal, GovVote, PaginabledAccounts, PaginatedProposalDeposit, PaginatedProposalVotes, Pagination } from '@/types';
import { ref } from 'vue';
import Countdown from '@/components/Countdown.vue';
import { computed } from '@vue/reactivity';
const props = defineProps(["proposal_id", "chain"]);
const proposal = ref({} as GovProposal)
const format = useFormatter()
const store = useGovStore()
store.fetchProposal(props.proposal_id).then((x) => proposal.value = x.proposal)
const color = computed(() => {
if (proposal.value.status==='PROPOSAL_STATUS_PASSED') {
return "success"
}else if (proposal.value.status==='PROPOSAL_STATUS_REJECTED') {
return "error"
}
return ""
})
const status = computed(() => {
if(proposal.value.status) {
return proposal.value.status.replace("PROPOSAL_STATUS_", "")
}
return ""
})
const deposit = ref({} as PaginatedProposalDeposit)
store.fetchProposalDeposits(props.proposal_id).then(x => deposit.value = x)
const votes = ref({} as GovVote[])
const votePage = ref({} as Pagination)
const loading = ref(false)
store.fetchProposalVotes(props.proposal_id).then(x => {
votes.value = x.votes
votePage.value = x.pagination
})
function loadMore() {
if(votePage.value.next_key) {
loading.value = true
store.fetchProposalVotes(props.proposal_id, votePage.value.next_key).then(x => {
votes.value = votes.value.concat(x.votes)
votePage.value = x.pagination
loading.value = false
})
}
}
function shortTime(v: string) {
if(v) {
return format.toDay(v, "from")
}
return ""
}
const votingCountdown = computed((): number => {
const now = new Date();
const end = new Date(proposal.value.voting_end_time)
return end.getTime() - now.getTime()
})
const upgradeCountdown = computed((): number => {
const height = Number(proposal.value.content?.plan?.height || 0)
if(height > 0) {
const base = useBaseStore()
const current = Number(base.latest?.block?.header?.height || 0)
return (height - current) * 6 * 1000
}
const now = new Date();
const end = new Date(proposal.value.content?.plan?.time || "")
return end.getTime() - now.getTime()
})
const total = computed(()=> {
const tally = proposal.value.final_tally_result
let sum = 0
if(tally) {
sum += Number(tally.abstain || 0)
sum += Number(tally.yes || 0)
sum += Number(tally.no || 0)
sum += Number(tally.no_with_veto || 0)
}
return sum
})
const turnout = computed(() => {
const bonded = useStakingStore().pool?.bonded_tokens || "1"
return format.percent(total.value / Number(bonded))
})
const yes = computed(()=> {
if(total.value > 0) {
const yes = proposal.value?.final_tally_result?.yes || 0
return format.percent(Number(yes) / total.value )
}
return 0
})
const no = computed(()=> {
if(total.value > 0) {
const value = proposal.value?.final_tally_result?.no || 0
return format.percent(Number(value) / total.value)
}
return 0
})
const veto = computed(()=> {
if(total.value > 0) {
const value = proposal.value?.final_tally_result?.no_with_veto || 0
return format.percent(Number(value) / total.value)
}
return 0
})
const abstain = computed(()=> {
if(total.value > 0) {
const value = proposal.value?.final_tally_result?.abstain || 0
return format.percent(Number(value) / total.value)
}
return 0
})
</script>
<template>
<div>
<VCard>
<VCardItem>
<VCardTitle>
{{ proposal_id }}. {{ proposal.content?.title }} <VChip label :color="color" class="float-right">{{ status }}</VChip>
</VCardTitle>
<ObjectElement :value="proposal.content"/>
</VCardItem>
</VCard>
<VRow class="my-5">
<VCol cols=12 md="4">
<VCard class="h-100">
<VCardItem>
<VCardTitle>Tally</VCardTitle>
<label>Turnout</label>
<v-progress-linear
:model-value="turnout"
height="25"
color="info"
>
<strong>{{ turnout }}</strong>
</v-progress-linear>
<label>Yes</label>
<v-progress-linear
:model-value="yes"
height="25"
color="success"
>
<strong>{{ yes }}</strong>
</v-progress-linear>
<label>No</label>
<v-progress-linear
:model-value="no"
height="25"
color="error"
>
<strong>{{ no }}</strong>
</v-progress-linear>
<label>No With Veto</label>
<v-progress-linear
:model-value="veto"
height="25"
color="primary"
>
<strong>{{ veto }}</strong>
</v-progress-linear>
<label>Abstain</label>
<v-progress-linear
:model-value="abstain"
height="25"
color="dark"
>
<strong>{{ abstain }}</strong>
</v-progress-linear>
</VCardItem>
</VCard>
</VCol>
<VCol cols=12 md="8">
<VCard>
<VCardItem>
<VCardTitle>
Timeline
</VCardTitle>
<VTimeline
class="mt-2"
side="end"
align="start"
line-inset="8"
truncate-line="both"
density="compact"
>
<VTimelineItem
dot-color="error"
size="x-small"
>
<!-- 👉 Header -->
<div class="d-flex justify-space-between flex-wrap mb-3">
<h6 class="text-base font-weight-medium me-3">
Submited at: {{ format.toDay(proposal.submit_time) }}
</h6>
<small class="text-xs text-disabled my-1">{{ shortTime(proposal.submit_time) }}</small>
</div>
</VTimelineItem>
<VTimelineItem
size="x-small"
dot-color="primary"
>
<!-- 👉 Header -->
<div class="d-flex justify-space-between flex-wrap mb-3">
<h6 class="text-base font-weight-medium me-3">
Deposited at: {{ format.toDay(proposal.status==="PROPOSAL_STATUS_DEPOSIT_PERIOD"?proposal.deposit_end_time: proposal.voting_start_time) }}
</h6>
<small class="text-xs text-disabled text-no-wrap my-1">{{ shortTime(proposal.status==="PROPOSAL_STATUS_DEPOSIT_PERIOD"?proposal.deposit_end_time: proposal.voting_start_time) }}</small>
</div>
<p class="mb-0">
<div v-for="x of deposit.deposits">
{{ x.depositor }} {{ format.formatTokens(x.amount) }}
</div>
</p>
</VTimelineItem>
<VTimelineItem
size="x-small"
dot-color="success"
>
<!-- 👉 Header -->
<div class="d-flex justify-space-between flex-wrap mb-3">
<h6 class="text-base font-weight-medium me-3">
Voting start from {{ format.toDay(proposal.voting_start_time) }}
</h6>
<small class="text-xs text-disabled text-no-wrap my-1">{{ shortTime(proposal.voting_start_time) }}</small>
</div>
<!-- 👉 Content -->
<p class="mb-0">
<Countdown :time="votingCountdown"/>
</p>
</VTimelineItem>
<VTimelineItem
size="x-small"
dot-color="success"
>
<!-- 👉 Header -->
<div class="d-flex justify-space-between flex-wrap mb-3">
<h6 class="text-base font-weight-medium me-3">
Voting end {{ format.toDay(proposal.voting_end_time) }}
</h6>
<small class="text-xs text-disabled text-no-wrap my-1">{{ shortTime(proposal.voting_end_time) }}</small>
</div>
<!-- 👉 Content -->
<p class="mb-0">
Current Status: {{ proposal.status }}
</p>
</VTimelineItem>
<VTimelineItem
v-if="proposal.content && proposal.content['@type'].endsWith('SoftwareUpgradeProposal')"
size="x-small"
dot-color="success"
>
<!-- 👉 Header -->
<div class="d-flex justify-space-between flex-wrap mb-3">
<h6 class="text-base font-weight-medium me-3">
Upgrade Plan:
<span v-if="Number(proposal.content?.plan?.height||'0') > 0"> (EST)</span>
<span v-else>{{ format.toDay(proposal.content?.plan?.time) }}</span>
</h6>
<small class="text-xs text-disabled text-no-wrap my-1">{{ shortTime(proposal.voting_end_time) }}</small>
</div>
<!-- 👉 Content -->
<p class="mb-0">
<Countdown :time="upgradeCountdown"/>
</p>
</VTimelineItem>
</VTimeline>
</VCardItem>
</VCard>
</VCol>
</VRow>
<VCard>
<VCardItem>
<VCardTitle>
Votes
</VCardTitle>
<VTable>
<tbody>
<tr v-for="x in votes">
<td>{{ x.voter }}</td>
<td>{{ x.option }}</td>
</tr>
</tbody>
</VTable>
<VBtn v-if="votePage.next_key" block variant="outlined" @click="loadMore()" :disabled="loading">Load more</VBtn>
</VCardItem>
</VCard>
</div>
</template>

View File

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { useBlockchain, useFormatter, useMintStore, useStakingStore } from '@/stores'; import { useBankStore, useBlockchain, useFormatter, useMintStore, useStakingStore } from '@/stores';
import { onMounted, computed, ref } from 'vue'; import { onMounted, computed, ref } from 'vue';
import ValidatorCommissionRate from '@/components/ValidatorCommissionRate.vue' import ValidatorCommissionRate from '@/components/ValidatorCommissionRate.vue'
import { consensusPubkeyToHexAddress, operatorAddressToAccount, pubKeyToValcons, valoperToPrefix } from '@/libs'; import { consensusPubkeyToHexAddress, operatorAddressToAccount, pubKeyToValcons, valoperToPrefix } from '@/libs';
@ -18,6 +18,7 @@ const cache = JSON.parse(localStorage.getItem('avatars')||'{}')
const avatars = ref( cache || {} ) const avatars = ref( cache || {} )
const identity = ref("") const identity = ref("")
const rewards = ref([] as Coin[]|undefined) const rewards = ref([] as Coin[]|undefined)
const commission = ref([] as Coin[]|undefined)
const addresses = ref({} as { const addresses = ref({} as {
account: string account: string
operAddress: string operAddress: string
@ -79,7 +80,20 @@ onMounted(()=> {
addresses.value.valCons = pubKeyToValcons(v.value.consensus_pubkey, prefix) addresses.value.valCons = pubKeyToValcons(v.value.consensus_pubkey, prefix)
}) })
blockchain.rpc.getDistributionValidatorOutstandingRewards(validator).then(res => { blockchain.rpc.getDistributionValidatorOutstandingRewards(validator).then(res => {
rewards.value = res.rewards?.rewards rewards.value = res.rewards?.rewards?.sort((a, b) => Number(b.amount) - Number(a.amount))
res.rewards?.rewards?.forEach(x => {
if(x.denom.startsWith("ibc/")) {
format.fetchDenomTrace(x.denom)
}
})
})
blockchain.rpc.getDistributionValidatorCommission(validator).then(res => {
commission.value = res.commission?.commission?.sort((a, b) => Number(b.amount) - Number(a.amount))
res.commission?.commission?.forEach(x => {
if(x.denom.startsWith("ibc/")) {
format.fetchDenomTrace(x.denom)
}
})
}) })
} }
@ -148,7 +162,7 @@ onMounted(()=> {
<div class="d-flex"> <div class="d-flex">
<VAvatar color="secondary" rounded variant="outlined" icon="mdi-flag"></VAvatar> <VAvatar color="secondary" rounded variant="outlined" icon="mdi-flag"></VAvatar>
<div class="ml-3 d-flex flex-column justify-center"> <div class="ml-3 d-flex flex-column justify-center">
<h4>{{ v.minSelfDelegation }} {{ staking.params.bond_denom }}</h4> <h4>{{ v.min_self_delegation }} {{ staking.params.bond_denom }}</h4>
<span class="text-sm">Min Self Delegation:</span> <span class="text-sm">Min Self Delegation:</span>
</div> </div>
</div> </div>
@ -184,16 +198,26 @@ onMounted(()=> {
</VCard> </VCard>
<VRow class="mt-3"> <VRow class="mt-3">
<VCol md="4" sm="12"> <VCol md="4" sm="12" class="h-100">
<ValidatorCommissionRate :commission="v.commission"></ValidatorCommissionRate> <ValidatorCommissionRate :commission="v.commission"></ValidatorCommissionRate>
</VCol> </VCol>
<VCol md="4" sm="12"> <VCol md="4" sm="12">
<VCard title="Outstanding Rewards" class="h-100"> <VCard class="h-100">
<VList> <VCardTitle>Commissions & Rewards</VCardTitle>
<VListItem v-for="(i, k) in rewards" :key="`reward-${k}`"> <VCardItem class="pt-0 pb-0">
<VAlertTitle>{{ format.formatToken2(i) }}</VAlertTitle> <div class="overflow-auto" style="max-height: 280px;">
</VListItem> <VCardSubtitle>Commissions <VBtn size="small" class="float-right" variant="text">Withdraw</VBtn></VCardSubtitle>
</VList> <VDivider class="mb-2"></VDivider>
<VChip v-for="(i, k) in commission" :key="`reward-${k}`" color="info" label variant="outlined" class="mr-1 mb-1">
{{ format.formatToken2(i) }}
</VChip>
<VCardSubtitle class="mt-2">Outstanding Rewards</VCardSubtitle>
<VDivider class="mb-2"></VDivider>
<VChip v-for="(i, k) in rewards" :key="`reward-${k}`" color="success" label variant="outlined" class="mr-1 mb-1">
{{ format.formatToken2(i) }}
</VChip>
</div>
</VCardItem>
</VCard> </VCard>
</VCol> </VCol>
<VCol md="4" sm="12"> <VCol md="4" sm="12">

View File

@ -2,7 +2,7 @@ import { defineStore } from "pinia";
import { useBlockchain } from "./useBlockchain"; import { useBlockchain } from "./useBlockchain";
import { useStakingStore } from "./useStakingStore"; import { useStakingStore } from "./useStakingStore";
import type { Coin } from "@/types"; import type { Coin, DenomTrace } from "@/types";
export const useBankStore = defineStore('bankstore', { export const useBankStore = defineStore('bankstore', {
state: () => { state: () => {
@ -31,14 +31,17 @@ export const useBankStore = defineStore('bankstore', {
}) })
} }
}, },
// async fetchTotalSupply(param: QueryTotalSupplyRequest): Promise<QueryTotalSupplyResponse> {
// const response = await this.blockchain.rpc.(param)
// this.totalSupply.supply = [...this.totalSupply.supply, ...response.supply]
// this.totalSupply.pagination = response.pagination
// return response
// },
async fetchSupply(denom: string) { async fetchSupply(denom: string) {
return this.blockchain.rpc.getBankSupplyByDenom( denom ) return this.blockchain.rpc.getBankSupplyByDenom( denom )
},
async fetchDenomTrace(denom: string) {
const hash = denom.replace("ibc/", "")
let trace = this.ibcDenoms[hash]
if(!trace) {
trace = (await this.blockchain.rpc.getIBCAppTransferDenom( hash )).denom_trace
this.ibcDenoms[hash] = trace
}
return trace
} }
} }
}) })

View File

@ -10,6 +10,8 @@ import localeData from 'dayjs/plugin/localeData'
import { useStakingStore } from "./useStakingStore"; import { useStakingStore } from "./useStakingStore";
import { fromBase64, toHex } from "@cosmjs/encoding"; import { fromBase64, toHex } from "@cosmjs/encoding";
import { consensusPubkeyToHexAddress } from "@/libs"; import { consensusPubkeyToHexAddress } from "@/libs";
import { useBankStore } from "./useBankStore";
import type { DenomTrace } from "@/types";
dayjs.extend(localeData) dayjs.extend(localeData)
dayjs.extend(duration) dayjs.extend(duration)
@ -37,6 +39,7 @@ dayjs.updateLocale('en', {
export const useFormatter = defineStore('formatter', { export const useFormatter = defineStore('formatter', {
state: () => { state: () => {
return { return {
ibcDenoms: {} as Record<string, DenomTrace>
} }
}, },
getters: { getters: {
@ -45,9 +48,21 @@ export const useFormatter = defineStore('formatter', {
}, },
staking() { staking() {
return useStakingStore() return useStakingStore()
},
useBank() {
return useBankStore()
} }
}, },
actions: { actions: {
async fetchDenomTrace(denom: string) {
const hash = denom.replace("ibc/", "")
let trace = this.ibcDenoms[hash]
if(!trace) {
trace = (await this.blockchain.rpc.getIBCAppTransferDenom( hash )).denom_trace
this.ibcDenoms[hash] = trace
}
return trace
},
formatTokenAmount(token: {denom: string, amount: string;}) { formatTokenAmount(token: {denom: string, amount: string;}) {
return this.formatToken(token, false) return this.formatToken(token, false)
}, },
@ -58,6 +73,15 @@ export const useFormatter = defineStore('formatter', {
if(token && token.amount) { if(token && token.amount) {
let amount = Number(token.amount) let amount = Number(token.amount)
let denom = token.denom let denom = token.denom
if( denom && denom.startsWith("ibc/")) {
console.log(denom)
let ibcDenom = this.ibcDenoms[denom.replace("ibc/", "")]
if(ibcDenom) {
denom = ibcDenom.base_denom
}
}
const conf = this.blockchain.current?.assets?.find(x => x.base === token.denom || x.base.denom === token.denom) const conf = this.blockchain.current?.assets?.find(x => x.base === token.denom || x.base.denom === token.denom)
if(conf) { if(conf) {
let unit = {exponent: 6, denom: ''} let unit = {exponent: 6, denom: ''}

View File

@ -49,6 +49,15 @@ export const useGovStore = defineStore('govStore', {
}, },
async fetchTally(proposalId: string) { async fetchTally(proposalId: string) {
return this.blockchain.rpc.getGovProposalTally(proposalId) return this.blockchain.rpc.getGovProposalTally(proposalId)
},
async fetchProposal(proposalId: string) {
return this.blockchain.rpc.getGovProposal(proposalId)
},
async fetchProposalDeposits(proposalId: string) {
return this.blockchain.rpc.getGovProposalDeposits(proposalId)
},
async fetchProposalVotes(proposalId: string, next_key?: string) {
return this.blockchain.rpc.getGovProposalVotes(proposalId, next_key)
} }
} }
}) })

View File

@ -10,7 +10,7 @@ export enum LoadingStatus {
} }
export interface Pagination { export interface Pagination {
key?: string; next_key?: string;
total?: string; total?: string;
} }

View File

@ -62,7 +62,11 @@ export interface PaginatedProposals extends PaginatedResponse {
} }
export interface PaginatedProposalDeposit extends PaginatedResponse { export interface PaginatedProposalDeposit extends PaginatedResponse {
deposits: Coin[] deposits: {
amount: Coin[],
proposal_id: string,
depositor: string
}
} }
export interface PaginatedProposalVotes extends PaginatedResponse { export interface PaginatedProposalVotes extends PaginatedResponse {

4
src/types/ibc.ts Normal file
View File

@ -0,0 +1,4 @@
export interface DenomTrace {
"path": "string",
"base_denom": "string"
}

View File

@ -7,3 +7,4 @@ export * from './distribution'
export * from './gov' export * from './gov'
export * from './staking' export * from './staking'
export * from './tx' export * from './tx'
export * from './ibc'

File diff suppressed because it is too large Load Diff

View File

@ -1435,6 +1435,11 @@
dependencies: dependencies:
"@babel/runtime" "^7.19.4" "@babel/runtime" "^7.19.4"
"@chenfengyuan/vue-countdown@2":
version "2.1.1"
resolved "https://registry.yarnpkg.com/@chenfengyuan/vue-countdown/-/vue-countdown-2.1.1.tgz#0e19b59b46eecab54b3a569d6a2dba78f7b6996f"
integrity sha512-HARJ62AFyxrBH/nMzwuaHUd20waKLN07mjyo2+8YfVouALkFERNRabqt5i3lfp3OISHb2054lWgyaX9L60hdPw==
"@confio/ics23@^0.6.8": "@confio/ics23@^0.6.8":
version "0.6.8" version "0.6.8"
resolved "https://registry.yarnpkg.com/@confio/ics23/-/ics23-0.6.8.tgz#2a6b4f1f2b7b20a35d9a0745bb5a446e72930b3d" resolved "https://registry.yarnpkg.com/@confio/ics23/-/ics23-0.6.8.tgz#2a6b4f1f2b7b20a35d9a0745bb5a446e72930b3d"
@ -2469,6 +2474,13 @@
dependencies: dependencies:
"@sinonjs/commons" "^2.0.0" "@sinonjs/commons" "^2.0.0"
"@tomieric/vue-flip-countdown@^0.0.5":
version "0.0.5"
resolved "https://registry.yarnpkg.com/@tomieric/vue-flip-countdown/-/vue-flip-countdown-0.0.5.tgz#ecf832ebad03aef7cfefad76ca3988af2d66e427"
integrity sha512-V2Y37zFiing2cSTD1xLOs9S+ZLLwKvcOEMoCRpzElyWLxEyY9Q2lydU7jwRpA+8QRzhJ9P7JKCHGdd8Aukt7LQ==
dependencies:
vue "^3.0.2"
"@trysound/sax@0.2.0": "@trysound/sax@0.2.0":
version "0.2.0" version "0.2.0"
resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad"
@ -7847,6 +7859,13 @@ vue3-apexcharts@^1.4.1:
resolved "https://registry.yarnpkg.com/vue3-apexcharts/-/vue3-apexcharts-1.4.1.tgz#ea561308430a1c5213b7f17c44ba3c845f6c490d" resolved "https://registry.yarnpkg.com/vue3-apexcharts/-/vue3-apexcharts-1.4.1.tgz#ea561308430a1c5213b7f17c44ba3c845f6c490d"
integrity sha512-96qP8JDqB9vwU7bkG5nVU+E0UGQn7yYQVqUUCLQMYWDuQyu2vE77H/UFZ1yI+hwzlSTBKT9BqnNG8JsFegB3eg== integrity sha512-96qP8JDqB9vwU7bkG5nVU+E0UGQn7yYQVqUUCLQMYWDuQyu2vE77H/UFZ1yI+hwzlSTBKT9BqnNG8JsFegB3eg==
vue3-flip-countdown@^0.1.6:
version "0.1.6"
resolved "https://registry.yarnpkg.com/vue3-flip-countdown/-/vue3-flip-countdown-0.1.6.tgz#995255932223a2cf6d58ae6ee1c1c78364157684"
integrity sha512-RRz+iZ7Zvr1U9mrZRya7I5815jboDyRJz9vzgILq8ZCc2fQ6SxZPYwOr3pD5oWCDBprAEsPF9x4fsTtEitSmXw==
dependencies:
vue "^3.0.0"
vue3-perfect-scrollbar@^1.6.1: vue3-perfect-scrollbar@^1.6.1:
version "1.6.1" version "1.6.1"
resolved "https://registry.yarnpkg.com/vue3-perfect-scrollbar/-/vue3-perfect-scrollbar-1.6.1.tgz#296e0e0c61a8f6278184f5b09bb45d137af92327" resolved "https://registry.yarnpkg.com/vue3-perfect-scrollbar/-/vue3-perfect-scrollbar-1.6.1.tgz#296e0e0c61a8f6278184f5b09bb45d137af92327"
@ -7856,7 +7875,7 @@ vue3-perfect-scrollbar@^1.6.1:
perfect-scrollbar "^1.5.5" perfect-scrollbar "^1.5.5"
postcss-import "^12.0.0" postcss-import "^12.0.0"
vue@^3.2.45: vue@^3.0.0, vue@^3.0.2, vue@^3.2.45:
version "3.2.47" version "3.2.47"
resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.47.tgz#3eb736cbc606fc87038dbba6a154707c8a34cff0" resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.47.tgz#3eb736cbc606fc87038dbba6a154707c8a34cff0"
integrity sha512-60188y/9Dc9WVrAZeUVSDxRQOZ+z+y5nO2ts9jWXSTkMvayiWxCWOWtBQoYjLeccfXkiiPZWAHcV+WTPhkqJHQ== integrity sha512-60188y/9Dc9WVrAZeUVSDxRQOZ+z+y5nO2ts9jWXSTkMvayiWxCWOWtBQoYjLeccfXkiiPZWAHcV+WTPhkqJHQ==