forked from cerc-io/cosmos-explorer
rpc endpoint version
This commit is contained in:
parent
d02c0c646e
commit
52de377644
@ -7,38 +7,11 @@
|
||||
"address": "http://rpc-cosmoshub.freshstaking.com:26657/",
|
||||
"provider": "FreshSTAKING"
|
||||
},
|
||||
{
|
||||
"address": "https://rpc.cosmos.bh.rocks/",
|
||||
"provider": "BlockHunters 🎯"
|
||||
},
|
||||
{
|
||||
"address": "https://cosmoshub-rpc.lavenderfive.com/",
|
||||
"provider": "Lavender.Five Nodes 🐝"
|
||||
},
|
||||
{
|
||||
"address": "https://cosmos-rpc.polkachu.com/",
|
||||
"provider": "Polkachu"
|
||||
},
|
||||
{
|
||||
"address": "https://rpc.cosmos.dragonstake.io/",
|
||||
"provider": "DragonStake"
|
||||
},
|
||||
{
|
||||
"address": "https://cosmos-rpc.rockrpc.net/",
|
||||
"provider": "RockawayX Infra"
|
||||
},
|
||||
{
|
||||
"address": "https://rpc-cosmoshub.pupmos.network/",
|
||||
"provider": "PUPMØS"
|
||||
},
|
||||
{
|
||||
"address": "https://cosmos-rpc.icycro.org/",
|
||||
"provider": "IcyCRO 🧊"
|
||||
},
|
||||
{
|
||||
"address": "https://rpc.cosmos.interbloc.org/",
|
||||
"provider": "Interbloc"
|
||||
},
|
||||
|
||||
{
|
||||
"address": "https://rpc.cosmoshub.strange.love/",
|
||||
"provider": "strangelove-ventures"
|
||||
|
@ -8,21 +8,24 @@ const props = defineProps({
|
||||
tally: { type: Object as PropType<{
|
||||
yes: string,
|
||||
no: string,
|
||||
no_with_veto: string,
|
||||
noWithVeto: string,
|
||||
abstain: string
|
||||
}>},
|
||||
pool: {
|
||||
type: Object as PropType<{
|
||||
not_bonded_tokens: string;
|
||||
bonded_tokens: string;
|
||||
notBondedTokens: string;
|
||||
bondedTokens: string;
|
||||
}>,
|
||||
},
|
||||
})
|
||||
const format = useFormatter()
|
||||
const yes = computed(() => (format.calculatePercent(props.tally?.yes, props.pool?.bonded_tokens)))
|
||||
const no = computed(() => ref(format.calculatePercent(props.tally?.no, props.pool?.bonded_tokens)))
|
||||
const abstain = computed(() => (format.calculatePercent(props.tally?.abstain, props.pool?.bonded_tokens)))
|
||||
const veto = computed(() => (format.calculatePercent(props.tally?.no_with_veto, props.pool?.bonded_tokens)))
|
||||
const yes = computed(() => (format.calculatePercent(props.tally?.yes, props.pool?.bondedTokens)))
|
||||
const no = computed(() => ref(format.calculatePercent(props.tally?.no, props.pool?.bondedTokens)))
|
||||
const abstain = computed(() => (format.calculatePercent(props.tally?.abstain, props.pool?.bondedTokens)))
|
||||
const veto = computed(() => (format.calculatePercent(props.tally?.noWithVeto, props.pool?.bondedTokens)))
|
||||
|
||||
|
||||
console.log(yes.value, no.value, abstain.value, veto.value)
|
||||
</script>
|
||||
<template>
|
||||
<div class="progress">
|
||||
|
155
packages/dashboard/src/components/ValidatorCommissionRate.vue
Normal file
155
packages/dashboard/src/components/ValidatorCommissionRate.vue
Normal file
@ -0,0 +1,155 @@
|
||||
<script setup lang="ts">
|
||||
import VueApexCharts from 'vue3-apexcharts'
|
||||
import { useTheme } from 'vuetify'
|
||||
import { hexToRgb } from '@/plugins/vuetify/@layouts/utils'
|
||||
import type { PropType } from 'vue';
|
||||
import { useFormatter } from '@/stores';
|
||||
|
||||
const props = defineProps({
|
||||
commission: { type: Object as PropType<{
|
||||
commissionRates: {
|
||||
rate: string,
|
||||
maxRate: string,
|
||||
maxChangeRate: string,
|
||||
},
|
||||
updateTime: string,
|
||||
}>},
|
||||
})
|
||||
console.log('commission:', props)
|
||||
|
||||
const zeros = Math.pow(10, 16)
|
||||
let rate = Number(props.commission?.commissionRates.rate || 0)
|
||||
let change = Number(props.commission?.commissionRates.maxChangeRate || 10)
|
||||
let max = Number(props.commission?.commissionRates.maxRate || 100)
|
||||
|
||||
if(rate > 100) {
|
||||
rate = rate / zeros
|
||||
}
|
||||
if(change > 100) {
|
||||
change = change / zeros
|
||||
}
|
||||
if(max > 100) {
|
||||
max = max / zeros
|
||||
}
|
||||
|
||||
// const rate = 15 // props.commision?.commissionRates.rate
|
||||
// const change = 15
|
||||
// const max = 20
|
||||
|
||||
const left = rate
|
||||
const right = max - rate
|
||||
|
||||
const s1 = left > change ? left - change : 0
|
||||
const s2 = left > change ? change: left
|
||||
const s3 = 2
|
||||
const s4 = right > change? change: right
|
||||
const s5 = right > change? right - change: 0
|
||||
|
||||
const series = [s1, s2, s3, s4, s5]
|
||||
|
||||
const vuetifyTheme = useTheme()
|
||||
const format = useFormatter()
|
||||
|
||||
const chartConfig = computed(() => {
|
||||
const themeColors = vuetifyTheme.current.value.colors
|
||||
const variableTheme = vuetifyTheme.current.value.variables
|
||||
|
||||
const secondaryText = `rgba(${hexToRgb(String(themeColors['on-background']))},${variableTheme['medium-emphasis-opacity']})`
|
||||
const primaryText = `rgba(${hexToRgb(String(themeColors['on-background']))},${variableTheme['high-emphasis-opacity']})`
|
||||
|
||||
return {
|
||||
chart: {
|
||||
sparkline: { enabled: false },
|
||||
},
|
||||
colors: [
|
||||
`rgba(${hexToRgb(String(themeColors.secondary))},0.2)`,
|
||||
`rgba(${hexToRgb(String(themeColors.success))},0.2)`,
|
||||
`rgba(${hexToRgb(String(themeColors.success))},1)`,
|
||||
`rgba(${hexToRgb(String(themeColors.success))},0.2)`,
|
||||
`rgba(${hexToRgb(String(themeColors.secondary))},0.2)`,
|
||||
],
|
||||
legend: { show: false },
|
||||
tooltip: { enabled: false },
|
||||
dataLabels: { enabled: false },
|
||||
stroke: { width: 3, lineCap: 'round', colors: ['rgba(var(--v-theme-surface), 1)'] },
|
||||
labels: ['Available', 'Daily Change', 'Commission Rate', 'Daily Change', 'Available'],
|
||||
states: {
|
||||
hover: {
|
||||
filter: { type: 'none' },
|
||||
},
|
||||
active: {
|
||||
filter: { type: 'none' },
|
||||
},
|
||||
},
|
||||
plotOptions: {
|
||||
pie: {
|
||||
endAngle: 130,
|
||||
startAngle: -130,
|
||||
customScale: 0.9,
|
||||
donut: {
|
||||
size: '83%',
|
||||
labels: {
|
||||
show: true,
|
||||
name: {
|
||||
offsetY: 25,
|
||||
fontSize: '1rem',
|
||||
color: secondaryText,
|
||||
},
|
||||
value: {
|
||||
offsetY: -15,
|
||||
fontWeight: 500,
|
||||
fontSize: '2.125rem',
|
||||
formatter: (value: unknown) => `${rate}%`,
|
||||
color: primaryText,
|
||||
},
|
||||
total: {
|
||||
show: true,
|
||||
label: 'Commission Rate',
|
||||
fontSize: '1rem',
|
||||
color: secondaryText,
|
||||
formatter: ( ) => `${rate}%`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responsive: [
|
||||
{
|
||||
breakpoint: 1709,
|
||||
options: {
|
||||
chart: { height: 237 },
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard title="Commission Rate" :subtitle="`Updated at ${format.toDay(props.commision?.updateTime, 'short')}`">
|
||||
<VCardText>
|
||||
<VueApexCharts
|
||||
type="donut"
|
||||
height="257"
|
||||
:options="chartConfig"
|
||||
:series="series"
|
||||
/>
|
||||
|
||||
<div class="d-flex align-center justify-center flex-wrap mx-2 gap-x-6">
|
||||
<div class="d-flex align-center gap-2">
|
||||
<VBadge dot color="success"/>
|
||||
<span class="mt-1 text-caption">Rate:{{ rate }}%</span>
|
||||
</div>
|
||||
<div class="d-flex align-center gap-2">
|
||||
<VBadge dot color="success" style="opacity:0.2"/>
|
||||
<span class="mt-1 text-caption">24h: ±{{ change }}%</span>
|
||||
</div>
|
||||
<div class="d-flex align-center gap-2">
|
||||
<VBadge dot color="secondary"/>
|
||||
<span class="mt-1 text-caption">Max:{{ max }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</template>
|
@ -0,0 +1,15 @@
|
||||
<script lang="ts" setup>
|
||||
import { fromBase64, toBase64 } from '@cosmjs/encoding';
|
||||
|
||||
const props = defineProps({
|
||||
value: { type: Array<Uint8Array>},
|
||||
})
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<div v-for="v in props.value">
|
||||
{{ toBase64(v) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
31
packages/dashboard/src/components/dynamic/ArrayElement.vue
Normal file
31
packages/dashboard/src/components/dynamic/ArrayElement.vue
Normal file
@ -0,0 +1,31 @@
|
||||
<script lang="ts" setup>
|
||||
import { toBase64 } from '@cosmjs/encoding';
|
||||
import type { PropType } from 'vue';
|
||||
import { computed } from '@vue/reactivity';
|
||||
import DynamicComponentVue from './DynamicComponent.vue';
|
||||
import {select} from './index'
|
||||
import ArrayBytesElement from './ArrayBytesElement.vue';
|
||||
import ArrayObjectElement from './ArrayObjectElement.vue';
|
||||
import TextElement from './TextElement.vue';
|
||||
|
||||
const props = defineProps({
|
||||
value: { type: Array<Object>},
|
||||
})
|
||||
|
||||
function selectByElement() {
|
||||
if(props.value && props.value.length > 0) {
|
||||
const [first] = props.value
|
||||
switch(true) {
|
||||
case first instanceof Uint8Array:
|
||||
return ArrayBytesElement
|
||||
default:
|
||||
return ArrayObjectElement
|
||||
}
|
||||
}
|
||||
return TextElement
|
||||
}
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<Component :is="selectByElement()" :value="props.value"></Component>
|
||||
</template>
|
@ -0,0 +1,30 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from '@vue/reactivity';
|
||||
import DynamicComponent from './DynamicComponent.vue';
|
||||
|
||||
const props = defineProps({
|
||||
value: { type: Array<Object>},
|
||||
})
|
||||
|
||||
const header = computed(() => {
|
||||
if(props.value && props.value.length > 0) {
|
||||
return Object.keys(props.value[0])
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<VTable v-if="header.length > 0" density="compact" height="300px" fixed-header>
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-for="k in header" class="text-left text-capitalize">{{ k }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="v in props.value">
|
||||
<td v-for="k in header"> <DynamicComponent :value="v[k]" /></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</VTable>
|
||||
</template>
|
@ -0,0 +1,11 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import TextElement from './TextElement.vue'
|
||||
import ObjectElement from './ObjectElement.vue'
|
||||
import { select } from './index'
|
||||
|
||||
const props = defineProps(["value"]);
|
||||
</script>
|
||||
<template>
|
||||
<Component :is="select(value)" :value="value"></Component>
|
||||
</template>
|
@ -0,0 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
const props = defineProps(["value"]);
|
||||
</script>
|
||||
<template>
|
||||
<span>{{ Number(props.value) }}</span>
|
||||
</template>
|
19
packages/dashboard/src/components/dynamic/ObjectElement.vue
Normal file
19
packages/dashboard/src/components/dynamic/ObjectElement.vue
Normal file
@ -0,0 +1,19 @@
|
||||
<script lang="ts" setup>
|
||||
import DynamicComponent from './DynamicComponent.vue';
|
||||
import {select} from './index'
|
||||
|
||||
const props = defineProps(["value"]);
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<VTable>
|
||||
<tbody>
|
||||
<tr v-for="(v, k) of value">
|
||||
<td class="text-capitalize">{{ k }}</td>
|
||||
<td><div class="overflow-hidden w-auto" style="max-width: 1000px;">
|
||||
<Component v-if="v" :is="select(v, k)" :value="v"></Component></div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</VTable>
|
||||
</template>
|
@ -0,0 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
const props = defineProps(["value"]);
|
||||
</script>
|
||||
<template>
|
||||
<span>{{ props.value }}</span>
|
||||
</template>
|
35
packages/dashboard/src/components/dynamic/TxsElement.vue
Normal file
35
packages/dashboard/src/components/dynamic/TxsElement.vue
Normal file
@ -0,0 +1,35 @@
|
||||
<script lang="ts" setup>
|
||||
import { fromBase64, toBase64 } from '@cosmjs/encoding';
|
||||
import { decodeTxRaw } from '@cosmjs/proto-signing'
|
||||
import { computed } from '@vue/reactivity';
|
||||
import { hashTx } from '@/libs'
|
||||
import { useBlockchain, useFormatter } from '@/stores';
|
||||
const props = defineProps({
|
||||
value: { type: Array<Uint8Array>},
|
||||
});
|
||||
|
||||
const txs = computed(() => {
|
||||
return props.value?.map(x => ({ hash: hashTx(x) , tx: decodeTxRaw(x) })) || []
|
||||
})
|
||||
|
||||
const format = useFormatter()
|
||||
const chain = useBlockchain()
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<VTable density="compact" v-if="txs.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Hash</th><th>Msgs</th><th>Memo</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in txs">
|
||||
<td><RouterLink :to="`/${chain.chainName}/tx/${item.hash}`">{{ item.hash }}</RouterLink></td>
|
||||
<td>{{ format.messages(item.tx.body.messages) }}</td>
|
||||
<td>{{ item.tx.body.memo }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</VTable>
|
||||
<div v-else>[]</div>
|
||||
</template>
|
16
packages/dashboard/src/components/dynamic/UInt8Array.vue
Normal file
16
packages/dashboard/src/components/dynamic/UInt8Array.vue
Normal file
@ -0,0 +1,16 @@
|
||||
<script lang="ts" setup>
|
||||
import { toBase64, toHex } from '@cosmjs/encoding';
|
||||
import { computed } from '@vue/reactivity';
|
||||
|
||||
const props = defineProps(["value"]);
|
||||
const format = ref('base64')
|
||||
const text = computed(()=> {
|
||||
return format.value === 'hex'? toHex(props.value) : toBase64(props.value)
|
||||
})
|
||||
function change() {
|
||||
format.value = format.value === 'hex'? 'base64': 'hex'
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<span>{{ text }} <VIcon size="12" icon="mdi-cached" @click="change()"/></span>
|
||||
</template>
|
37
packages/dashboard/src/components/dynamic/index.ts
Normal file
37
packages/dashboard/src/components/dynamic/index.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import ObjectElement from './ObjectElement.vue'
|
||||
import TextElement from './TextElement.vue'
|
||||
import ArrayElement from './ArrayElement.vue'
|
||||
import UInt8Array from './UInt8Array.vue'
|
||||
import NumberElement from './NumberElement.vue'
|
||||
import TxsElement from './TxsElement.vue'
|
||||
import Long from 'long'
|
||||
|
||||
export function select(v: any, k?: any) {
|
||||
if(k === 'txs' && v) {
|
||||
console.log("=======txs=======", k, v)
|
||||
return TxsElement
|
||||
} else {
|
||||
const type = typeof v
|
||||
switch(type) {
|
||||
case 'object':
|
||||
return selectObject(v)
|
||||
case 'number':
|
||||
return NumberElement
|
||||
default:
|
||||
return TextElement
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function selectObject(v: Object) {
|
||||
switch(true) {
|
||||
case v instanceof Long:
|
||||
return NumberElement
|
||||
case v instanceof Uint8Array:
|
||||
return UInt8Array
|
||||
case Array.isArray(v):
|
||||
return ArrayElement
|
||||
default:
|
||||
return ObjectElement
|
||||
}
|
||||
}
|
@ -1,17 +1,21 @@
|
||||
import {fromBase64, fromBech32, toBech32, toHex} from '@cosmjs/encoding'
|
||||
import { sha256 } from '@cosmjs/crypto'
|
||||
import {fromBase64, fromBech32, fromHex, toBase64, toBech32, toHex} from '@cosmjs/encoding'
|
||||
import { Ripemd160, sha256 } from '@cosmjs/crypto'
|
||||
import { cosmos } from '@ping-pub/codegen'
|
||||
import type { PubKey } from '@ping-pub/codegen/src/cosmos/crypto/ed25519/keys'
|
||||
|
||||
export function decodeAddress(address: string) {
|
||||
return fromBech32(address)
|
||||
}
|
||||
|
||||
export function valoperToPrefix(valoper: string) {
|
||||
export function valoperToPrefix(valoper?: string) {
|
||||
if(!valoper) return ''
|
||||
const prefixIndex = valoper.indexOf('valoper')
|
||||
if (prefixIndex === -1) return null
|
||||
return valoper.slice(0, prefixIndex)
|
||||
}
|
||||
|
||||
export function operatorAddressToAccount(operAddress: string) {
|
||||
export function operatorAddressToAccount(operAddress?: string) {
|
||||
if(!operAddress) return ''
|
||||
const { prefix, data } = fromBech32(operAddress)
|
||||
if (prefix === 'iva') { // handle special cases
|
||||
return toBech32('iaa', data)
|
||||
@ -22,9 +26,42 @@ export function valoperToPrefix(valoper: string) {
|
||||
return toBech32(prefix.replace('valoper', ''), data)
|
||||
}
|
||||
|
||||
export function pubKeyToValcons(pubkey: string, prefix: string) {
|
||||
const addressData = sha256(fromBase64(pubkey.key)).slice(0, 20)
|
||||
return toBech32(`${prefix}valcons`, addressData)
|
||||
export function decodeKey(consensusPubkey: {typeUrl: string, value: Uint8Array}) {
|
||||
let raw = null
|
||||
if (consensusPubkey.typeUrl === '/cosmos.crypto.ed25519.PubKey') {
|
||||
const pubkey = cosmos.crypto.ed25519.PubKey.decode(consensusPubkey.value)
|
||||
return pubkey
|
||||
}
|
||||
|
||||
if (consensusPubkey.typeUrl === '/cosmos.crypto.secp256k1.PubKey') {
|
||||
const pubkey = cosmos.crypto.secp256k1.PubKey.decode(consensusPubkey.value)
|
||||
return pubkey
|
||||
}
|
||||
return raw
|
||||
|
||||
}
|
||||
export function consensusPubkeyToHexAddress(consensusPubkey?: {typeUrl: string, value: Uint8Array}) {
|
||||
if(!consensusPubkey) return ""
|
||||
let raw = ""
|
||||
if (consensusPubkey.typeUrl === '/cosmos.crypto.ed25519.PubKey') {
|
||||
const pubkey = decodeKey(consensusPubkey)
|
||||
if(pubkey) return toHex(sha256(pubkey.key)).slice(0, 40).toUpperCase()
|
||||
}
|
||||
|
||||
if (consensusPubkey.typeUrl === '/cosmos.crypto.secp256k1.PubKey') {
|
||||
const pubkey = decodeKey(consensusPubkey)
|
||||
if(pubkey) return toHex(new Ripemd160().update(sha256(pubkey.key)).digest())
|
||||
}
|
||||
return raw
|
||||
|
||||
}
|
||||
|
||||
export function pubKeyToValcons(consensusPubkey: {typeUrl: string, value: Uint8Array}, prefix: string) {
|
||||
const pubkey = decodeKey(consensusPubkey)
|
||||
if(pubkey) {
|
||||
const addressData = sha256(pubkey.key).slice(0, 20)
|
||||
return toBech32(`${prefix}valcons`, addressData)
|
||||
}
|
||||
}
|
||||
|
||||
export function toETHAddress(cosmosAddress: string) {
|
||||
|
@ -4,6 +4,8 @@ import { cosmos } from '@ping-pub/codegen/src/index'
|
||||
import type { BondStatus } from "@ping-pub/codegen/src/cosmos/staking/v1beta1/staking";
|
||||
import long from "long";
|
||||
import type { ProposalStatus } from "@ping-pub/codegen/src/cosmos/gov/v1/gov";
|
||||
import { OrderBy } from "@ping-pub/codegen/src/cosmos/tx/v1beta1/service";
|
||||
import { fromHex } from "@cosmjs/encoding";
|
||||
|
||||
export declare type BondStatusString = keyof Pick<typeof BondStatus, "BOND_STATUS_BONDED" | "BOND_STATUS_UNBONDED" | "BOND_STATUS_UNBONDING"> | "";
|
||||
|
||||
@ -37,8 +39,13 @@ export class RPCClient {
|
||||
async allBalance(address: string) {
|
||||
return cosmos.bank.v1beta1.createRpcQueryExtension(await this.getQueryClient()).allBalances({address})
|
||||
}
|
||||
async latestBlock() {
|
||||
const base = cosmos.base.tendermint.v1beta1.createRpcQueryExtension(await this.getQueryClient())
|
||||
return base.getLatestBlock()
|
||||
}
|
||||
async block(height?: number) {
|
||||
return (await this.getTMClient()).block(height)
|
||||
const base = cosmos.base.tendermint.v1beta1.createRpcQueryExtension(await this.getQueryClient())
|
||||
return height ? base.getBlockByHeight({height: long.fromNumber(height)}) : base.getLatestBlock()
|
||||
}
|
||||
async abciInfo() {
|
||||
return (await this.getTMClient()).abciInfo()
|
||||
@ -46,9 +53,16 @@ export class RPCClient {
|
||||
async status() {
|
||||
return (await this.getTMClient()).status()
|
||||
}
|
||||
async validatorsAtHeight(height?: number) {
|
||||
async tmTx(hashStr: string) {
|
||||
const hash = fromHex(hashStr)
|
||||
return (await this.getTMClient()).tx({hash})
|
||||
}
|
||||
async validatorsAllAtHeight(height?: number) {
|
||||
return (await this.getTMClient()).validatorsAll(height)
|
||||
}
|
||||
async validatorsAtHeight(height?: number) {
|
||||
return (await this.getTMClient()).validators({height})
|
||||
}
|
||||
|
||||
async proposal(id: number) {
|
||||
return cosmos.gov.v1beta1.createRpcQueryExtension(await this.getQueryClient()).proposal({proposalId: long.fromNumber(id)})
|
||||
@ -100,6 +114,15 @@ export class RPCClient {
|
||||
async stakingParams() {
|
||||
return cosmos.staking.v1beta1.createRpcQueryExtension(await this.getQueryClient()).params()
|
||||
}
|
||||
async delegatorDelegations(delegatorAddr: string) {
|
||||
return cosmos.staking.v1beta1.createRpcQueryExtension(await this.getQueryClient()).delegatorDelegations({delegatorAddr, })
|
||||
}
|
||||
async validatorDelegations(validatorAddr: string) {
|
||||
return cosmos.staking.v1beta1.createRpcQueryExtension(await this.getQueryClient()).validatorDelegations({validatorAddr })
|
||||
}
|
||||
async validatorDelegation(validatorAddr: string, delegatorAddr: string) {
|
||||
return cosmos.staking.v1beta1.createRpcQueryExtension(await this.getQueryClient()).delegation({validatorAddr, delegatorAddr })
|
||||
}
|
||||
async historicalInfo(height: number) {
|
||||
return cosmos.staking.v1beta1.createRpcQueryExtension(await this.getQueryClient()).historicalInfo({height: long.fromNumber(height)})
|
||||
}
|
||||
@ -109,6 +132,12 @@ export class RPCClient {
|
||||
async inflation() {
|
||||
return cosmos.mint.v1beta1.createRpcQueryExtension(await this.getQueryClient()).inflation()
|
||||
}
|
||||
async txs(events: string[]) {
|
||||
return cosmos.tx.v1beta1.createRpcQueryExtension(await this.getQueryClient()).getTxsEvent({events, orderBy: OrderBy.ORDER_BY_DESC})
|
||||
}
|
||||
async tx(hash: string) {
|
||||
return cosmos.tx.v1beta1.createRpcQueryExtension(await this.getQueryClient()).getTx({hash})
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
import { sha256 } from "@cosmjs/crypto";
|
||||
import { toHex } from "@cosmjs/encoding";
|
||||
import { PageRequest } from "@ping-pub/codegen/src/cosmos/base/query/v1beta1/pagination";
|
||||
|
||||
export function newPageRequest(param: {
|
||||
@ -11,3 +13,7 @@ export function newPageRequest(param: {
|
||||
param
|
||||
)
|
||||
}
|
||||
|
||||
export function hashTx(raw: Uint8Array) {
|
||||
return toHex(sha256(raw)).toUpperCase()
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { createBaseClientForChain } from "@/libs/client";
|
||||
import { useBlockchain } from "@/stores";
|
||||
import type { GetLatestBlockResponseSDKType } from "@ping-pub/codegen/src/cosmos/base/tendermint/v1beta1/query";
|
||||
|
||||
export const useBlockModule = defineStore('blockModule', {
|
||||
state: () => {
|
||||
return {
|
||||
latest: {} as GetLatestBlockResponseSDKType,
|
||||
recents: [] as GetLatestBlockResponseSDKType[]
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
client() {
|
||||
const chain = useBlockchain()
|
||||
return createBaseClientForChain(chain.name, chain.restClient)
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
async clearRecentBlocks() {
|
||||
this.recents = []
|
||||
},
|
||||
async fetchLatest() {
|
||||
this.latest = await this.client.getLatestBlock()
|
||||
if(this.recents.length>= 50) {
|
||||
this.recents.pop()
|
||||
}
|
||||
this.recents.push(this.latest)
|
||||
return this.latest
|
||||
},
|
||||
async fetchSync() {
|
||||
return this.client.getSyncing()
|
||||
},
|
||||
async fetchNodeInfo() {
|
||||
return this.client.getNodeInfo()
|
||||
}
|
||||
}
|
||||
})
|
58
packages/dashboard/src/modules/[chain]/block/[height].vue
Normal file
58
packages/dashboard/src/modules/[chain]/block/[height].vue
Normal file
@ -0,0 +1,58 @@
|
||||
<script lang="ts" setup>
|
||||
import TxsElement from '@/components/dynamic/TxsElement.vue';
|
||||
import { useBlockModule } from './block'
|
||||
import DynamicComponent from '@/components/dynamic/DynamicComponent.vue';
|
||||
import { computed } from '@vue/reactivity';
|
||||
import { onBeforeRouteUpdate } from 'vue-router';
|
||||
const props = defineProps(["height", "chain"]);
|
||||
|
||||
const store = useBlockModule()
|
||||
store.fetchBlock(props.height)
|
||||
const tab = ref('summary')
|
||||
|
||||
const height = computed(() => {
|
||||
return Number(store.current.block?.header?.height || props.height || 0)
|
||||
})
|
||||
|
||||
onBeforeRouteUpdate(async (to, from, next) => {
|
||||
if (from.path !== to.path) {
|
||||
store.fetchBlock(Number(to.params.height))
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<VCard>
|
||||
<VCardTitle class="d-flex justify-space-between">
|
||||
<span class="mt-2">#{{ store.current.block?.header?.height }}</span>
|
||||
<span v-if="props.height" class="mt-2">
|
||||
<VBtn size="32" :to="`/${store.blockchain.chainName}/block/${height - 1}`" class="mr-2"><VIcon icon="mdi-arrow-left"/></VBtn>
|
||||
<VBtn size="32" :to="`/${store.blockchain.chainName}/block/${height + 1}`"><VIcon icon="mdi-arrow-right"/></VBtn>
|
||||
</span>
|
||||
</VCardTitle>
|
||||
<VCardItem class="pt-0">
|
||||
<DynamicComponent :value="store.current.blockId"/>
|
||||
</VCardItem>
|
||||
</VCard>
|
||||
|
||||
<VCard title="Block Header" class="my-5">
|
||||
<VCardItem class="pt-0">
|
||||
<DynamicComponent :value="store.current.block?.header"/>
|
||||
</VCardItem>
|
||||
</VCard>
|
||||
|
||||
<VCard title="Transactions">
|
||||
<VCardItem class="pt-0">
|
||||
<TxsElement :value="store.current.block?.data?.txs"/>
|
||||
</VCardItem>
|
||||
</VCard>
|
||||
|
||||
<VCard title="Last Commit" class="mt-5">
|
||||
<VCardItem class="pt-0">
|
||||
<DynamicComponent :value="store.current.block?.lastCommit"/>
|
||||
</VCardItem>
|
||||
</VCard>
|
||||
</div>
|
||||
</template>
|
62
packages/dashboard/src/modules/[chain]/block/block.ts
Normal file
62
packages/dashboard/src/modules/[chain]/block/block.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { decodeTxRaw, type DecodedTxRaw } from '@cosmjs/proto-signing'
|
||||
import { createBaseClientForChain } from "@/libs/client";
|
||||
import { useBlockchain } from "@/stores";
|
||||
import type { GetLatestBlockResponse, GetLatestBlockResponseSDKType } from "@ping-pub/codegen/src/cosmos/base/tendermint/v1beta1/query";
|
||||
import { hashTx } from "@/libs";
|
||||
|
||||
export const useBlockModule = defineStore('blockModule', {
|
||||
state: () => {
|
||||
return {
|
||||
latest: {} as GetLatestBlockResponse,
|
||||
current: {} as GetLatestBlockResponse,
|
||||
recents: [] as GetLatestBlockResponse[]
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
blockchain() {
|
||||
return useBlockchain()
|
||||
},
|
||||
blocktime() {
|
||||
if(this.recents.length<2) return 6000
|
||||
return 6000 // todo later
|
||||
},
|
||||
txsInRecents() {
|
||||
const txs = [] as {hash:string, tx: DecodedTxRaw}[]
|
||||
this.recents.forEach((x:GetLatestBlockResponse) => x.block?.data?.txs.forEach((tx:Uint8Array) => txs.push({
|
||||
hash: hashTx(tx),
|
||||
tx :decodeTxRaw(tx)
|
||||
})))
|
||||
return txs
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
initial() {
|
||||
this.clearRecentBlocks()
|
||||
this.autoFetch()
|
||||
},
|
||||
async clearRecentBlocks() {
|
||||
this.recents = []
|
||||
},
|
||||
autoFetch() {
|
||||
this.fetchLatest().then(x => {
|
||||
const timer = this.autoFetch
|
||||
this.latest = x;
|
||||
// if(this.recents.length >= 50) this.recents.pop()
|
||||
// this.recents.push(x)
|
||||
// setTimeout(timer, 6000)
|
||||
})
|
||||
},
|
||||
async fetchLatest() {
|
||||
this.latest = await this.blockchain.rpc.block()
|
||||
if(this.recents.length >= 50) this.recents.shift()
|
||||
this.recents.push(this.latest)
|
||||
return this.latest
|
||||
},
|
||||
async fetchBlock(height?: number) {
|
||||
this.current = await this.blockchain.rpc.block(height)
|
||||
return this.current
|
||||
},
|
||||
}
|
||||
})
|
||||
|
74
packages/dashboard/src/modules/[chain]/block/index.vue
Normal file
74
packages/dashboard/src/modules/[chain]/block/index.vue
Normal file
@ -0,0 +1,74 @@
|
||||
<script lang="ts" setup>
|
||||
import TxsElement from '@/components/dynamic/TxsElement.vue';
|
||||
import { useBlockModule } from './block'
|
||||
import DynamicComponent from '@/components/dynamic/DynamicComponent.vue';
|
||||
import { computed } from '@vue/reactivity';
|
||||
import { toBase64, toHex } from '@cosmjs/encoding';
|
||||
import { useFormatter } from '@/stores';
|
||||
const props = defineProps(["height", "chain"]);
|
||||
|
||||
const store = useBlockModule()
|
||||
store.fetchBlock(props.height)
|
||||
const tab = ref('blocks')
|
||||
|
||||
const format = useFormatter()
|
||||
</script>
|
||||
<template>
|
||||
|
||||
<VCard>
|
||||
<VCardTitle class="d-flex justify-space-between">
|
||||
<VTabs v-model="tab">
|
||||
<VTab value="blocks">Blocks</VTab>
|
||||
<VTab value="transactions">Transactions</VTab>
|
||||
</VTabs>
|
||||
</VCardTitle>
|
||||
<VWindow v-model="tab">
|
||||
<VWindowItem value="blocks">
|
||||
<VTable>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Height</th><th>Hash</th><th>Proposor</th><th>Txs</th><th>Time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in store.recents">
|
||||
<td><RouterLink :to="`/${props.chain}/block/${item.block?.header?.height}`">{{ item.block?.header?.height }}</RouterLink></td>
|
||||
<td>{{ toBase64(item.blockId?.hash) }}</td>
|
||||
<td>{{ format.validator(item.block?.header?.proposerAddress) }}</td>
|
||||
<td>{{ item.block?.data?.txs.length }}</td>
|
||||
<td>{{ format.toDay(item.block?.header?.time, 'from') }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</VTable>
|
||||
</VWindowItem>
|
||||
<VWindowItem value="transactions">
|
||||
<VTable>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Hash</th><th>Messages</th><th>Fees</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in store.txsInRecents">
|
||||
<td><RouterLink :to="`/${props.chain}/tx/${item.hash}`">{{ item.hash }}</RouterLink></td>
|
||||
<td>{{ format.messages(item.tx.body.messages) }}</td>
|
||||
<td>{{ format.formatTokens(item.tx.authInfo.fee?.amount) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</VTable>
|
||||
|
||||
<VCardItem>
|
||||
<v-alert
|
||||
type="info"
|
||||
text="Only show txs in recent blocks"
|
||||
variant="tonal"
|
||||
></v-alert>
|
||||
</VCardItem>
|
||||
</VWindowItem>
|
||||
</VWindow>
|
||||
<VCardActions>
|
||||
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
|
||||
</template>
|
@ -146,11 +146,11 @@ function shortName(name: string, id: string) {
|
||||
<VExpansionPanels variant="accordion">
|
||||
<VExpansionPanel v-for="(x, i) in store.proposals">
|
||||
<VExpansionPanelTitle disable-icon-rotate>
|
||||
<VChip label color="primary" class="mr-2">{{x.proposal_id}}</VChip>
|
||||
<VChip label color="primary" class="mr-2">{{x.proposalId}}</VChip>
|
||||
<div class="w-100">{{ x.content?.title }}
|
||||
<div class="d-flex mt-1">
|
||||
<small class="text-secondary me-auto"> {{ format.toDay(x.voting_end_time, 'from') }}</small>
|
||||
<ProposalProcess style="width:300px;" :pool="store.pool" :tally="store.tally[Number(x.proposal_id)]"></ProposalProcess>
|
||||
<small class="text-secondary me-auto"> {{ format.toDay(x.votingEndTime, 'from') }}</small>
|
||||
<ProposalProcess style="width:300px;" :pool="store.pool" :tally="store.tally[Number(x.proposalId)]"></ProposalProcess>
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -191,15 +191,16 @@ export const useIndexModule = defineStore('module-index', {
|
||||
denom: t.denom
|
||||
}))
|
||||
})
|
||||
// const gov = useGovStore()
|
||||
// gov.fetchProposals(ProposalStatus.PROPOSAL_STATUS_VOTING_PERIOD).then(x => {
|
||||
// this.proposals = x.proposals
|
||||
// x.proposals.forEach(x1 => {
|
||||
// gov.fetchTally(Number(x1.proposalId)).then(t => {
|
||||
// if(t.tally) this.tally[Number(x1.proposalId)] = t.tally
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
const gov = useGovStore()
|
||||
gov.fetchProposals(ProposalStatus.PROPOSAL_STATUS_VOTING_PERIOD).then(x => {
|
||||
this.proposals = x.proposals
|
||||
x.proposals.forEach(x1 => {
|
||||
gov.fetchTally(Number(x1.proposalId)).then(t => {
|
||||
console.log("log: ", t)
|
||||
if(t.tally) this.tally[Number(x1.proposalId)] = t.tally
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
tickerColor(color: string) {
|
||||
return colorMap(color)
|
||||
|
@ -1,16 +1,66 @@
|
||||
<script setup lang="ts">
|
||||
import { useStakingStore } from '@/stores';
|
||||
import { useBlockchain, useFormatter, useMintStore, useStakingStore } from '@/stores';
|
||||
import type { QueryValidatorResponseSDKType } from '@ping-pub/codegen/src/cosmos/staking/v1beta1/query';
|
||||
import { onMounted } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import ValidatorCommissionRate from '@/components/ValidatorCommissionRate.vue'
|
||||
import type { Coin, DecCoin } from '@ping-pub/codegen/src/cosmos/base/v1beta1/coin';
|
||||
import { consensusPubkeyToHexAddress, operatorAddressToAccount, pubKeyToValcons, valoperToPrefix } from '@/libs';
|
||||
import { computed } from '@vue/reactivity';
|
||||
import type { GetTxsEventResponse } from '@ping-pub/codegen/src/cosmos/tx/v1beta1/service';
|
||||
|
||||
const props = defineProps(['validator', 'chain'])
|
||||
|
||||
const route = useRoute()
|
||||
const staking = useStakingStore()
|
||||
const { validator } = route.params
|
||||
const blockchain = useBlockchain()
|
||||
const format = useFormatter()
|
||||
|
||||
const validator: string = props.validator
|
||||
|
||||
const v = ref({} as QueryValidatorResponseSDKType)
|
||||
const cache = JSON.parse(localStorage.getItem('avatars')||'{}')
|
||||
const avatars = ref( cache || {} )
|
||||
const identity = ref("")
|
||||
const rewards = ref([] as DecCoin[])
|
||||
const addresses = ref({} as {
|
||||
account: string
|
||||
operAddress: string
|
||||
hex: string
|
||||
valCons: string,
|
||||
})
|
||||
const selfBonded = ref({} as Coin)
|
||||
|
||||
addresses.value.account = operatorAddressToAccount(validator)
|
||||
// load self bond
|
||||
staking.fetchValidatorDelegation(validator, addresses.value.account).then(x => {
|
||||
if(x) {
|
||||
selfBonded.value = x
|
||||
}
|
||||
})
|
||||
|
||||
const txs = ref({} as GetTxsEventResponse)
|
||||
|
||||
blockchain.rpc.txs([`message.sender='${addresses.value.account}'`]).then(x => {
|
||||
console.log("txs", x)
|
||||
txs.value = x
|
||||
})
|
||||
|
||||
const apr = computed(()=> {
|
||||
const rate = v.value.commission?.commissionRates.rate || 0
|
||||
const inflation = useMintStore().inflation
|
||||
if(Number(inflation)) {
|
||||
return format.percent((1 - rate) * Number(inflation))
|
||||
}
|
||||
return "-"
|
||||
})
|
||||
|
||||
const selfRate = computed(()=> {
|
||||
console.log("self rate", selfBonded.value.balance?.amount, v.value.tokens)
|
||||
if(selfBonded.value.balance?.amount) {
|
||||
return format.calculatePercent(selfBonded.value.balance.amount, v.value.tokens)
|
||||
}
|
||||
return "-"
|
||||
})
|
||||
|
||||
onMounted(()=> {
|
||||
if(validator) {
|
||||
@ -29,6 +79,13 @@ onMounted(()=> {
|
||||
}
|
||||
})
|
||||
}
|
||||
const prefix = valoperToPrefix(v.value.operatorAddress) || '<Invalid>'
|
||||
addresses.value.hex = consensusPubkeyToHexAddress(v.value.consensusPubkey)
|
||||
addresses.value.valCons = pubKeyToValcons(v.value.consensusPubkey, prefix)
|
||||
})
|
||||
blockchain.rpc.validatorOutstandingRewards(validator).then(res => {
|
||||
console.log(res)
|
||||
rewards.value = res.rewards?.rewards
|
||||
})
|
||||
}
|
||||
|
||||
@ -36,20 +93,162 @@ onMounted(()=> {
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<VCard class="card-box">
|
||||
<VCardItem>
|
||||
<VRow>
|
||||
<VCol cols="12" md="6">
|
||||
<VAvatar icon="mdi-help-circle-outline" :image="avatars[identity]" size="80" rounded variant="outlined" color="secondary"/>
|
||||
<VList>
|
||||
<VListItem>
|
||||
<VListItemTitle>{{ v.description?.moniker }}</VListItemTitle>
|
||||
<VListItemSubtitle> {{ v.description?.website || v.description?.identity || '-'}}</VListItemsubtitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
<div class="d-flex pl-2">
|
||||
<VAvatar icon="mdi-help-circle-outline" :image="avatars[identity]" size="90" rounded variant="outlined" color="secondary"/>
|
||||
<div class="mx-2">
|
||||
<h4>{{ v.description?.moniker }}</h4>
|
||||
<div class="text-sm mb-2">{{ v.description?.identity || '-'}}</div>
|
||||
<VBtn>Delegate</VBtn>
|
||||
</div>
|
||||
</div>
|
||||
<VSpacer/>
|
||||
<VCardText>
|
||||
<p class="text-xs">
|
||||
About Us
|
||||
</p>
|
||||
<VList class="card-list">
|
||||
<VListItem prepend-icon="mdi-web">
|
||||
<span>Website: </span><span> {{ v.description?.website || '-' }}</span>
|
||||
</VListItem>
|
||||
<VListItem prepend-icon="mdi-phone">
|
||||
<span>Contact: </span><span> {{ v.description?.securityContact }}</span>
|
||||
</VListItem>
|
||||
</VList>
|
||||
|
||||
<p class="text-xs mt-3">
|
||||
Validator Status
|
||||
</p>
|
||||
<VList class="card-list">
|
||||
<VListItem prepend-icon="mdi-check">
|
||||
<span>Status: </span><span> {{ v.status }}</span>
|
||||
</VListItem>
|
||||
<VListItem prepend-icon="mdi-lock">
|
||||
<span>Jailed: </span><span> {{ v.jailed }}</span>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VCardText>
|
||||
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">
|
||||
<div class="d-flex flex-column py-3 justify-space-between">
|
||||
<div class="d-flex">
|
||||
<VAvatar color="secondary" rounded variant="outlined" icon="mdi-coin"></VAvatar>
|
||||
<div class="ml-3 d-flex flex-column justify-center">
|
||||
<h4>{{ format.formatToken2({amount: v.tokens, denom: staking.params.bondDenom}) }}</h4>
|
||||
<span class="text-sm">Bonded Tokens</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<VAvatar color="secondary" rounded variant="outlined" icon="mdi-percent"></VAvatar>
|
||||
<div class="ml-3 d-flex flex-column justify-center">
|
||||
<h4>{{ format.formatToken(selfBonded.balance) }} ({{ selfRate }})</h4>
|
||||
<span class="text-sm">Self Bonded</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex">
|
||||
<VAvatar color="secondary" rounded variant="outlined" icon="mdi-flag"></VAvatar>
|
||||
<div class="ml-3 d-flex flex-column justify-center">
|
||||
<h4>{{ v.minSelfDelegation }} {{ staking.params.bondDenom }}</h4>
|
||||
<span class="text-sm">Min Self Delegation:</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<VAvatar color="secondary" rounded variant="outlined" icon="mdi-coin"></VAvatar>
|
||||
<div class="ml-3 d-flex flex-column justify-center">
|
||||
<h4>{{ apr }}</h4>
|
||||
<span class="text-sm">Annual Profit</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex">
|
||||
<VAvatar color="secondary" rounded variant="outlined" icon="mdi-pound"></VAvatar>
|
||||
<div class="ml-3 d-flex flex-column justify-center">
|
||||
<h4>{{ v.unbondingHeight }}</h4>
|
||||
<span class="text-sm">Unbonding Height</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex">
|
||||
<VAvatar color="secondary" rounded variant="outlined" icon="mdi-clock"></VAvatar>
|
||||
<div class="ml-3 d-flex flex-column justify-center">
|
||||
<h4>{{ format.toDay(v.unbondingTime, 'from') }}</h4>
|
||||
<span class="text-sm">Unbonding Time</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</VCol>
|
||||
<VCol cols="12" md="6">2</VCol>
|
||||
</VRow>
|
||||
<VDivider />
|
||||
<VCardText>{{ v.description?.details }}</VCardText>
|
||||
</VCardItem>
|
||||
</VCard>
|
||||
|
||||
<VRow class="mt-3">
|
||||
<VCol md="4" sm="12">
|
||||
<ValidatorCommissionRate :commission="v.commission"></ValidatorCommissionRate>
|
||||
</VCol>
|
||||
<VCol md="4" sm="12">
|
||||
<VCard title="Outstanding Rewards" class="h-100">
|
||||
<VList>
|
||||
<VListItem v-for="(i, k) in rewards" :key="`reward-${k}`">
|
||||
<VAlertTitle>{{ format.formatToken2(i) }}</VAlertTitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VCard>
|
||||
</VCol>
|
||||
<VCol md="4" sm="12">
|
||||
<VCard title="Addresses" class="h-100">
|
||||
<VList class="pt-0">
|
||||
<VListItem>
|
||||
<VListItemTitle>Account</VListItemTitle>
|
||||
<VListItemSubtitle class="text-caption">{{ addresses.account }}</VListItemSubtitle>
|
||||
</VListItem>
|
||||
<VListItem>
|
||||
<VListItemTitle>Operator Address</VListItemTitle>
|
||||
<VListItemSubtitle class="text-caption">{{ v.operatorAddress }}</VListItemSubtitle>
|
||||
</VListItem>
|
||||
<VListItem>
|
||||
<VListItemTitle>Hex Address</VListItemTitle>
|
||||
<VListItemSubtitle class="text-caption">{{ addresses.hex }}</VListItemSubtitle>
|
||||
</VListItem>
|
||||
<VListItem>
|
||||
<VListItemTitle>Signer Address</VListItemTitle>
|
||||
<VListItemSubtitle class="text-caption">{{ addresses.valCons }}</VListItemSubtitle>
|
||||
</VListItem>
|
||||
</VList>
|
||||
</VCard>
|
||||
</VCol>
|
||||
</VRow>
|
||||
<VCard title="Transactions" class="mt-5">
|
||||
<VCardItem class="pt-0">
|
||||
<VTable>
|
||||
<thead>
|
||||
<th class="text-left pl-4">Height</th>
|
||||
<th class="text-left pl-4">Hash</th>
|
||||
<th class="text-left pl-4" width="40%">Messages</th>
|
||||
<th class="text-left pl-4">Time</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(item, i) in txs.txResponses">
|
||||
<td>{{ item.height }}</td>
|
||||
<td class="text-truncate" style="max-width: 200px;">{{ item.txhash }}</td>
|
||||
<td>{{ format.messages(txs.txs[i]?.body.messages) }}</td>
|
||||
<td width="150">{{ format.toDay(item.timestamp,'from') }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</VTable>
|
||||
</VCardItem>
|
||||
</VCard>
|
||||
</div>
|
||||
</template>
|
||||
<style>
|
||||
.card-list {
|
||||
--v-card-list-gap: 10px;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,9 +1,11 @@
|
||||
<script lang=ts setup>
|
||||
import { useBaseStore, useFormatter, useStakingStore } from '@/stores';
|
||||
import { toBase64, toHex } from '@cosmjs/encoding';
|
||||
import { pubkeyToAddress } from '@cosmjs/tendermint-rpc';
|
||||
import type { ValidatorSDKType } from '@ping-pub/codegen/src/cosmos/staking/v1beta1/staking';
|
||||
import { computed } from '@vue/reactivity';
|
||||
import { onMounted, ref, type DebuggerEvent } from 'vue';
|
||||
|
||||
import { consensusPubkeyToHexAddress } from '@/libs'
|
||||
const staking = useStakingStore()
|
||||
const format = useFormatter()
|
||||
|
||||
@ -13,7 +15,7 @@ const latest = ref({} as Record<string, number>)
|
||||
const yesterday = ref({} as Record<string, number>)
|
||||
const tab = ref('active')
|
||||
const unbondList = ref([] as ValidatorSDKType[])
|
||||
|
||||
const base = useBaseStore()
|
||||
onMounted(()=> {
|
||||
fetchChange(0)
|
||||
staking.fetchInacitveValdiators().then(x => {
|
||||
@ -24,36 +26,39 @@ onMounted(()=> {
|
||||
function fetchChange(offset: number) {
|
||||
const base = useBaseStore()
|
||||
const diff = 86400000 / base.blocktime
|
||||
base.fetchLatestValidators(offset).then(x => {
|
||||
const height = Number(x.block_height) - diff
|
||||
x.validators.forEach(v => {
|
||||
latest.value[v.pub_key?.key] = Number(v.voting_power)
|
||||
base.fetchAbciInfo().then(h => {
|
||||
// console.log('block:', h)
|
||||
base.fetchValidatorByHeight(h.lastBlockHeight, offset).then(x => {
|
||||
x.validators.forEach(v => {
|
||||
if(v.pubkey) latest.value[pubkeyToAddress(v.pubkey.algorithm, v.pubkey.data)] = Number(v.votingPower)
|
||||
})
|
||||
})
|
||||
const len = Object.keys(latest.value).length
|
||||
if(x.pagination && len < Number(x.pagination.total) ) {
|
||||
fetchChange(len)
|
||||
}
|
||||
const height = Number(h.lastBlockHeight) - diff
|
||||
base.fetchValidatorByHeight(height > 0 ? height : 1, offset).then(old => {
|
||||
old.validators.forEach(v => {
|
||||
yesterday.value[v.pub_key?.key] = Number(v.voting_power)
|
||||
})
|
||||
if(v.pubkey) yesterday.value[pubkeyToAddress(v.pubkey.algorithm, v.pubkey.data)] = Number(v.votingPower)
|
||||
})
|
||||
// console.log(Object.keys(yesterday.value).map(x => x.toUpperCase()))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const change24 = (key: string) => {
|
||||
const n : number = latest.value[key];
|
||||
const o : number = yesterday.value[key]
|
||||
const change24 = (key: {typeUrl: string, value: Uint8Array}) => {
|
||||
// console.log('hex key:', consensusPubkeyToHexAddress(key))
|
||||
const txt = toBase64(key.value)
|
||||
const n : number = latest.value[txt];
|
||||
const o : number = yesterday.value[txt]
|
||||
// console.log( txt, n, o)
|
||||
return n >0 && o > 0 ? n - o : 0
|
||||
}
|
||||
|
||||
const change24Text = (key?: string) => {
|
||||
const change24Text = (key?: {typeUrl: string, value: Uint8Array}) => {
|
||||
if(!key) return ''
|
||||
const v = change24(key)
|
||||
return v!==0 ? format.numberAndSign(v) : ''
|
||||
}
|
||||
|
||||
const change24Color = (key?: string) => {
|
||||
const change24Color = (key?: {typeUrl: string, value: Uint8Array}) => {
|
||||
if(!key) return ''
|
||||
const v = change24(key)
|
||||
if(v > 0) return 'text-success'
|
||||
@ -67,7 +72,8 @@ const update = (m: DebuggerEvent) => {
|
||||
}
|
||||
|
||||
const list = computed(() => {
|
||||
return tab.value === 'active' ? staking.validators: unbondList.value
|
||||
// return tab.value === 'active' ? staking.validators: unbondList.value
|
||||
return staking.validators
|
||||
})
|
||||
|
||||
const loadAvatars = () => {
|
||||
@ -111,7 +117,7 @@ const logo = (identity?: string) => {
|
||||
const rank = function(position: number) {
|
||||
let sum = 0
|
||||
for(let i = 0;i < position; i++) {
|
||||
sum += Number(staking.validators[i]?.delegator_shares)
|
||||
sum += Number(staking.validators[i]?.delegatorShares)
|
||||
}
|
||||
const percent = (sum / staking.totalPower)
|
||||
|
||||
@ -130,7 +136,7 @@ const rank = function(position: number) {
|
||||
<VBtn value="active" variant="outlined" >Active</VBtn>
|
||||
<VBtn value="inactive" variant="outlined">Inactive</VBtn>
|
||||
</VBtnToggle>
|
||||
<span class="mt-2">{{ list.length }}/{{ staking.params.max_validators }}</span>
|
||||
<span class="mt-2">{{ list.length }}/{{ staking.params.maxValidators }}</span>
|
||||
</VCardTitle>
|
||||
<VTable class="text-no-wrap table-header-bg rounded-0">
|
||||
<thead>
|
||||
@ -159,7 +165,7 @@ const rank = function(position: number) {
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(v, i) in list"
|
||||
:key="v.operator_address"
|
||||
:key="v.operatorAddress"
|
||||
>
|
||||
<!-- 👉 rank -->
|
||||
<td>
|
||||
@ -181,7 +187,7 @@ const rank = function(position: number) {
|
||||
<div class="d-flex flex-column">
|
||||
<h6 class="text-sm">
|
||||
<RouterLink
|
||||
:to="{name: 'chain-staking-validator', params: {validator: v.operator_address}}"
|
||||
:to="{name: 'chain-staking-validator', params: {validator: v.operatorAddress}}"
|
||||
class="font-weight-medium user-list-name"
|
||||
>
|
||||
{{ v.description?.moniker }}
|
||||
@ -197,18 +203,18 @@ const rank = function(position: number) {
|
||||
<td class="text-right">
|
||||
<div class="d-flex flex-column">
|
||||
<h6 class="text-sm font-weight-medium">
|
||||
{{ format.formatToken( {amount: parseInt(v.delegator_shares).toString(), denom: staking.params.bond_denom }, true, "0,0") }}
|
||||
{{ format.formatToken( {amount: parseInt(v.tokens).toString(), denom: staking.params.bondDenom }, true, "0,0") }}
|
||||
</h6>
|
||||
<span class="text-xs">{{ format.calculatePercent(v.delegator_shares, staking.totalPower) }}</span>
|
||||
<span class="text-xs">{{ format.calculatePercent(v.delegatorShares, staking.totalPower) }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<!-- 👉 24h Changes -->
|
||||
<td class="text-right text-xs" :class="change24Color(v.consensus_pubkey?.key)">
|
||||
{{ change24Text(v.consensus_pubkey?.key) }} <VChip label v-if="v.jailed" color="error">Jailed</VChip>
|
||||
<td class="text-right text-xs" :class="change24Color(v.consensusPubkey)">
|
||||
{{ change24Text(v.consensusPubkey) }} <VChip label v-if="v.jailed" color="error">Jailed</VChip>
|
||||
</td>
|
||||
<!-- 👉 commission -->
|
||||
<td class="text-right">
|
||||
{{ format.percent(v.commission?.commission_rates?.rate) }}
|
||||
{{ format.formatCommissionRate(v.commission?.commissionRates?.rate) }}
|
||||
</td>
|
||||
<!-- 👉 Action -->
|
||||
<td>
|
||||
|
66
packages/dashboard/src/modules/[chain]/tx/[hash].vue
Normal file
66
packages/dashboard/src/modules/[chain]/tx/[hash].vue
Normal file
@ -0,0 +1,66 @@
|
||||
<script lang="ts" setup>
|
||||
import { useBlockchain, useFormatter } from '@/stores';
|
||||
import type { GetTxResponse } from 'cosmjs-types/cosmos/tx/v1beta1/service';
|
||||
import DynamicComponent from '@/components/dynamic/DynamicComponent.vue';
|
||||
import { computed } from '@vue/reactivity';
|
||||
import { fromBase64, toBase64 } from '@cosmjs/encoding';
|
||||
|
||||
const props = defineProps(['hash', 'chain'])
|
||||
|
||||
const blockchain = useBlockchain()
|
||||
const format = useFormatter()
|
||||
const tx = ref({} as GetTxResponse)
|
||||
if(props.hash) {
|
||||
blockchain.rpc.tx(props.hash).then(x => tx.value = x)
|
||||
}
|
||||
const messages = computed(() => {
|
||||
return tx.value.tx?.body?.messages||[]
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<VCard v-if="tx.txResponse" title="Summary">
|
||||
<VCardItem class="pt-0">
|
||||
<VTable>
|
||||
<tbody>
|
||||
<tr><td>Tx Hash</td><td>{{ tx.txResponse.txhash }}</td></tr>
|
||||
<tr><td>Height</td><td><RouterLink :to="`/${props.chain}/block/${tx.txResponse.height}`">{{ tx.txResponse.height }}</RouterLink></td></tr>
|
||||
<tr><td>Status</td><td>
|
||||
<VChip v-if="tx.txResponse.code === 0" color="success">Success</VChip>
|
||||
<span v-else><VChip color="error">Failded</VChip></span>
|
||||
</td></tr>
|
||||
<tr><td>Time</td><td>{{ tx.txResponse.timestamp }} ({{ format.toDay(tx.txResponse.timestamp, "from") }})</td></tr>
|
||||
<tr><td>Gas</td><td>{{ tx.txResponse.gasUsed }} / {{ tx.txResponse.gasWanted }}</td></tr>
|
||||
<tr><td>Fee</td><td>{{ format.formatTokens(tx.tx?.authInfo?.fee?.amount, true, '0,0.[00]') }}</td></tr>
|
||||
<tr><td>Memo</td><td>{{ tx.tx.body.memo }}</td></tr>
|
||||
</tbody>
|
||||
</VTable>
|
||||
</VCardItem>
|
||||
</VCard>
|
||||
|
||||
<VCard title="Messages" class="my-5">
|
||||
<VCardItem>
|
||||
<div v-for="(msg, i) in messages">
|
||||
<div><VChip label color="primary">#{{ i+1 }}</VChip>{{ msg.typeUrl }}</div>
|
||||
<div>{{ toBase64(msg.value) }}</div>
|
||||
</div>
|
||||
</VCardItem>
|
||||
</VCard>
|
||||
|
||||
<VCard title="Detail">
|
||||
<VExpansionPanels>
|
||||
<VExpansionPanel title="Transaction">
|
||||
<v-expansion-panel-text>
|
||||
<DynamicComponent :value="tx.tx" />
|
||||
</v-expansion-panel-text>
|
||||
</VExpansionPanel>
|
||||
<VExpansionPanel title="Transaction Response">
|
||||
<v-expansion-panel-text>
|
||||
<DynamicComponent :value="tx.txResponse" />
|
||||
</v-expansion-panel-text>
|
||||
</VExpansionPanel>
|
||||
</VExpansionPanels>
|
||||
</VCard>
|
||||
|
||||
</div>
|
||||
</template>
|
@ -59,9 +59,12 @@ export const useBaseStore = defineStore('baseStore', {
|
||||
async fetchLatestValidators(offset = 0) {
|
||||
return this.blockchain.rpc.validatorsAtHeight()
|
||||
},
|
||||
async fetchBlock(height: number) {
|
||||
async fetchBlock(height?: number) {
|
||||
return this.blockchain.rpc.block(height)
|
||||
},
|
||||
async fetchAbciInfo() {
|
||||
return this.blockchain.rpc.abciInfo()
|
||||
}
|
||||
// async fetchNodeInfo() {
|
||||
// return this.blockchain.rpc.no()
|
||||
// }
|
||||
|
@ -9,6 +9,8 @@ import { useBaseStore } from "./useBaseStore";
|
||||
import { useGovStore } from "./useGovStore";
|
||||
import { RPCClient } from '../libs/client.rpc'
|
||||
import { ref } from "vue";
|
||||
import { useMintStore } from "./useMintStore";
|
||||
import { useBlockModule } from "@/modules/[chain]/block/block";
|
||||
|
||||
export const useBlockchain = defineStore("blockchain", {
|
||||
state: () => {
|
||||
@ -94,14 +96,13 @@ export const useBlockchain = defineStore("blockchain", {
|
||||
},
|
||||
actions: {
|
||||
async initial() {
|
||||
console.log('begin Setup')
|
||||
await this.randomSetupEndpoint()
|
||||
console.log('rpc setup')
|
||||
await useStakingStore().init()
|
||||
useBankStore().initial()
|
||||
useBaseStore().initial()
|
||||
useGovStore().initial()
|
||||
|
||||
useMintStore().initial()
|
||||
useBlockModule().initial()
|
||||
},
|
||||
|
||||
async randomSetupEndpoint() {
|
||||
|
@ -9,6 +9,9 @@ import updateLocale from 'dayjs/plugin/updateLocale'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
import localeData from 'dayjs/plugin/localeData'
|
||||
import type { PoolSDKType } from "@ping-pub/codegen/src/cosmos/staking/v1beta1/staking";
|
||||
import { useStakingStore } from "./useStakingStore";
|
||||
import { fromBech32, toBase64, toHex } from "@cosmjs/encoding";
|
||||
import { consensusPubkeyToHexAddress, operatorAddressToAccount } from "@/libs";
|
||||
|
||||
dayjs.extend(localeData)
|
||||
dayjs.extend(duration)
|
||||
@ -41,6 +44,9 @@ export const useFormatter = defineStore('formatter', {
|
||||
getters: {
|
||||
blockchain() {
|
||||
return useBlockchain()
|
||||
},
|
||||
staking() {
|
||||
return useStakingStore()
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
@ -56,7 +62,7 @@ export const useFormatter = defineStore('formatter', {
|
||||
let denom = token.denom
|
||||
const conf = this.blockchain.current?.assets?.find(x => x.base === token.denom || x.base.denom === token.denom)
|
||||
if(conf) {
|
||||
let unit = {exponent: 0, denom: ''}
|
||||
let unit = {exponent: 6, denom: ''}
|
||||
// find the max exponent for display
|
||||
conf.denom_units.forEach(x => {
|
||||
if(x.exponent >= unit.exponent) {
|
||||
@ -64,7 +70,7 @@ export const useFormatter = defineStore('formatter', {
|
||||
}
|
||||
})
|
||||
if(unit && unit.exponent > 0) {
|
||||
amount = amount / Math.pow(10, unit?.exponent)
|
||||
amount = amount / Math.pow(10, unit.exponent || 6)
|
||||
denom = unit.denom.toUpperCase()
|
||||
}
|
||||
}
|
||||
@ -72,8 +78,9 @@ export const useFormatter = defineStore('formatter', {
|
||||
}
|
||||
return '-'
|
||||
},
|
||||
formatTokens(tokens: { denom: string, amount: string;}[], withDenom = true) : string {
|
||||
return tokens.map(x => this.formatToken(x, withDenom)).join(', ')
|
||||
formatTokens(tokens?: { denom: string, amount: string;}[], withDenom = true, fmt='0.0a') : string {
|
||||
if(!tokens) return ''
|
||||
return tokens.map(x => this.formatToken(x, withDenom, fmt)).join(', ')
|
||||
},
|
||||
calculateBondedRatio(pool: {bonded_tokens: string, not_bonded_tokens: string}|undefined) {
|
||||
if(pool && pool.bonded_tokens) {
|
||||
@ -85,6 +92,11 @@ export const useFormatter = defineStore('formatter', {
|
||||
}
|
||||
return '-'
|
||||
},
|
||||
validator(address: Uint8Array) {
|
||||
const txt = toHex(address).toUpperCase()
|
||||
const validator = this.staking.validators.find(x => consensusPubkeyToHexAddress(x.consensusPubkey) === txt)
|
||||
return validator?.description?.moniker
|
||||
},
|
||||
calculatePercent(input?: string, total?: string|number ) {
|
||||
if(!input || !total) return '0'
|
||||
const percent = Number(input)/Number(total)
|
||||
@ -93,7 +105,13 @@ export const useFormatter = defineStore('formatter', {
|
||||
formatDecimalToPercent(decimal: string) {
|
||||
return numeral(decimal).format('0.[00]%')
|
||||
},
|
||||
percent(decimal?: string) {
|
||||
formatCommissionRate(v?: string) {
|
||||
console.log(v)
|
||||
if(!v) return '-'
|
||||
const rate = Number(v) / Number("1000000000000000000")
|
||||
return this.percent(rate)
|
||||
},
|
||||
percent(decimal?: string|number) {
|
||||
return decimal ? numeral(decimal).format('0.[00]%') : '-'
|
||||
},
|
||||
numberAndSign(input: number, fmt="+0,0") {
|
||||
@ -117,7 +135,27 @@ export const useFormatter = defineStore('formatter', {
|
||||
return dayjs(time).toNow()
|
||||
}
|
||||
return dayjs(time).format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
},
|
||||
messages(msgs: {typeUrl: string}[]) {
|
||||
if(msgs) {
|
||||
const sum: Record<string, number> = msgs.map(msg => {
|
||||
return msg.typeUrl.substring(msg.typeUrl.lastIndexOf('.') + 1).replace('Msg', '')
|
||||
}).reduce((s, c) => {
|
||||
const sh: Record<string, number> = s
|
||||
if (sh[c]) {
|
||||
sh[c] += 1
|
||||
} else {
|
||||
sh[c] = 1
|
||||
}
|
||||
return sh
|
||||
}, {})
|
||||
const output: string[] = []
|
||||
Object.keys(sum).forEach(k => {
|
||||
output.push(sum[k] > 1 ? `${k}×${sum[k]}` : k)
|
||||
})
|
||||
return output.join(', ')
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -33,7 +33,9 @@ export const useGovStore = defineStore('govStore', {
|
||||
depositor: '',
|
||||
pagination,
|
||||
}
|
||||
return this.blockchain.rpc.proposals(proposalStatus, '', '')
|
||||
const proposals = await this.blockchain.rpc.proposals(proposalStatus, '', '')
|
||||
console.log(proposals)
|
||||
return proposals
|
||||
},
|
||||
async fetchParams() {
|
||||
// this.blockchain.rpc.govParam().then(x => {
|
||||
|
@ -5,11 +5,11 @@ import { createMintClientForChain } from "@/libs/client";
|
||||
import { createRpcQueryExtension } from '@ping-pub/codegen/src/cosmos/mint/v1beta1/query.rpc.Query'
|
||||
import { Tendermint34Client } from "@cosmjs/tendermint-rpc";
|
||||
import { QueryClient, setupMintExtension } from "@cosmjs/stargate";
|
||||
|
||||
import { cosmos } from '@ping-pub/codegen'
|
||||
export const useMintStore = defineStore('mintStore', {
|
||||
state: () => {
|
||||
return {
|
||||
inflation: "",
|
||||
inflation: "0",
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
@ -18,11 +18,14 @@ export const useMintStore = defineStore('mintStore', {
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
initial() {
|
||||
this.fetchInflation()
|
||||
},
|
||||
async fetchInflation() {
|
||||
this.blockchain.rpc.inflation().then(x => {
|
||||
this.inflation = String(x)
|
||||
this.inflation = x.inflation
|
||||
}).catch(err => {
|
||||
this.inflation = ""
|
||||
this.inflation = "0"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -5,12 +5,13 @@ import type { Params, Pool, Validator} from "@ping-pub/codegen/src/cosmos/stakin
|
||||
import { get } from "@/libs/http";
|
||||
|
||||
import type { BondStatusString } from "@/libs/client.rpc";
|
||||
import type { QueryParamsResponse } from "@ping-pub/codegen/src/cosmos/staking/v1beta1/query";
|
||||
|
||||
export const useStakingStore = defineStore('stakingStore', {
|
||||
state: () => {
|
||||
return {
|
||||
validators: [] as Validator[],
|
||||
params: {} ,
|
||||
params: {} as QueryParamsResponse,
|
||||
pool: {} as Pool | undefined,
|
||||
}
|
||||
},
|
||||
@ -51,6 +52,9 @@ export const useStakingStore = defineStore('stakingStore', {
|
||||
async fetchValidator(validatorAddr: string) {
|
||||
return this.blockchain.rpc.validator(validatorAddr)
|
||||
},
|
||||
async fetchValidatorDelegation(validatorAddr: string, delegatorAddr: string) {
|
||||
return (await this.blockchain.rpc.validatorDelegation(validatorAddr, delegatorAddr)).delegationResponse
|
||||
},
|
||||
async fetchValidators(status: BondStatusString) {
|
||||
return this.blockchain.rpc.validators(status, undefined).then(res => {
|
||||
const vals = res.validators.sort((a, b) => (Number(b.delegatorShares) - Number(a.delegatorShares)))
|
||||
|
Loading…
Reference in New Issue
Block a user