forked from cerc-io/cosmos-explorer
feat: staking tabs
This commit is contained in:
parent
3f42e542f5
commit
728d8bf02d
@ -6,7 +6,6 @@ import { computed } from '@vue/reactivity';
|
|||||||
import { onBeforeRouteUpdate } from 'vue-router';
|
import { onBeforeRouteUpdate } from 'vue-router';
|
||||||
const props = defineProps(['height', 'chain']);
|
const props = defineProps(['height', 'chain']);
|
||||||
|
|
||||||
const store = useBlockModule()
|
|
||||||
const store = useBlockModule();
|
const store = useBlockModule();
|
||||||
store.fetchBlock(props.height);
|
store.fetchBlock(props.height);
|
||||||
const tab = ref('summary');
|
const tab = ref('summary');
|
||||||
|
@ -1,30 +1,30 @@
|
|||||||
<script lang=ts setup>
|
<script lang="ts" setup>
|
||||||
import { useBaseStore, useFormatter, useStakingStore } from '@/stores';
|
import { useBaseStore, useFormatter, useStakingStore } from '@/stores';
|
||||||
import { toBase64, toHex } from '@cosmjs/encoding';
|
import { toBase64, toHex } from '@cosmjs/encoding';
|
||||||
import { computed } from '@vue/reactivity';
|
import { computed } from '@vue/reactivity';
|
||||||
import { onMounted, ref, type DebuggerEvent } from 'vue';
|
import { onMounted, ref, type DebuggerEvent } from 'vue';
|
||||||
import { consensusPubkeyToHexAddress } from '@/libs'
|
import { consensusPubkeyToHexAddress } from '@/libs';
|
||||||
import type { Key, Validator } from '@/types';
|
import type { Key, Validator } from '@/types';
|
||||||
const staking = useStakingStore()
|
const staking = useStakingStore();
|
||||||
const format = useFormatter()
|
const format = useFormatter();
|
||||||
|
|
||||||
const cache = JSON.parse(localStorage.getItem('avatars')||'{}')
|
const cache = JSON.parse(localStorage.getItem('avatars') || '{}');
|
||||||
const avatars = ref( cache || {} )
|
const avatars = ref(cache || {});
|
||||||
const latest = ref({} as Record<string, number>)
|
const latest = ref({} as Record<string, number>);
|
||||||
const yesterday = ref({} as Record<string, number>)
|
const yesterday = ref({} as Record<string, number>);
|
||||||
const tab = ref('active')
|
const tab = ref('active');
|
||||||
const unbondList = ref([] as Validator[])
|
const unbondList = ref([] as Validator[]);
|
||||||
const base = useBaseStore()
|
const base = useBaseStore();
|
||||||
onMounted(()=> {
|
onMounted(() => {
|
||||||
fetchChange(0)
|
fetchChange(0);
|
||||||
staking.fetchInacitveValdiators().then(x => {
|
staking.fetchInacitveValdiators().then((x) => {
|
||||||
unbondList.value = x
|
unbondList.value = x;
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
function fetchChange(offset: number) {
|
function fetchChange(offset: number) {
|
||||||
const base = useBaseStore()
|
const base = useBaseStore();
|
||||||
const diff = 86400000 / base.blocktime
|
const diff = 86400000 / base.blocktime;
|
||||||
// base.fetchAbciInfo().then(h => {
|
// base.fetchAbciInfo().then(h => {
|
||||||
// // console.log('block:', h)
|
// // console.log('block:', h)
|
||||||
// base.fetchValidatorByHeight(h.lastBlockHeight, offset).then(x => {
|
// base.fetchValidatorByHeight(h.lastBlockHeight, offset).then(x => {
|
||||||
@ -44,128 +44,139 @@ function fetchChange(offset: number) {
|
|||||||
|
|
||||||
const change24 = (key: Key) => {
|
const change24 = (key: Key) => {
|
||||||
// console.log('hex key:', consensusPubkeyToHexAddress(key))
|
// console.log('hex key:', consensusPubkeyToHexAddress(key))
|
||||||
const txt = key.key
|
const txt = key.key;
|
||||||
const n : number = latest.value[txt];
|
const n: number = latest.value[txt];
|
||||||
const o : number = yesterday.value[txt]
|
const o: number = yesterday.value[txt];
|
||||||
// console.log( txt, n, o)
|
// console.log( txt, n, o)
|
||||||
return n >0 && o > 0 ? n - o : 0
|
return n > 0 && o > 0 ? n - o : 0;
|
||||||
}
|
};
|
||||||
|
|
||||||
const change24Text = (key?: Key) => {
|
const change24Text = (key?: Key) => {
|
||||||
if(!key) return ''
|
if (!key) return '';
|
||||||
const v = change24(key)
|
const v = change24(key);
|
||||||
return v!==0 ? format.numberAndSign(v) : ''
|
return v !== 0 ? format.numberAndSign(v) : '';
|
||||||
}
|
};
|
||||||
|
|
||||||
const change24Color = (key?: Key) => {
|
const change24Color = (key?: Key) => {
|
||||||
if(!key) return ''
|
if (!key) return '';
|
||||||
const v = change24(key)
|
const v = change24(key);
|
||||||
if(v > 0) return 'text-success'
|
if (v > 0) return 'text-success';
|
||||||
if(v < 0) return 'text-error'
|
if (v < 0) return 'text-error';
|
||||||
}
|
};
|
||||||
|
|
||||||
const update = (m: DebuggerEvent) => {
|
const update = (m: DebuggerEvent) => {
|
||||||
if(m.key === 'validators') {
|
if (m.key === 'validators') {
|
||||||
loadAvatars()
|
loadAvatars();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const list = computed(() => {
|
const list = computed(() => {
|
||||||
return tab.value === 'active' ? staking.validators: unbondList.value
|
return tab.value === 'active' ? staking.validators : unbondList.value;
|
||||||
// return staking.validators
|
// return staking.validators
|
||||||
})
|
});
|
||||||
|
|
||||||
const loadAvatars = () => {
|
const loadAvatars = () => {
|
||||||
// fetch avatar from keybase
|
// fetch avatar from keybase
|
||||||
let promise = Promise.resolve()
|
let promise = Promise.resolve();
|
||||||
staking.validators.forEach(item => {
|
staking.validators.forEach((item) => {
|
||||||
promise = promise.then(() => new Promise(resolve => {
|
promise = promise.then(
|
||||||
const identity = item.description?.identity
|
() =>
|
||||||
if(identity && !avatars.value[identity]){
|
new Promise((resolve) => {
|
||||||
staking.keybase(identity).then(d => {
|
const identity = item.description?.identity;
|
||||||
|
if (identity && !avatars.value[identity]) {
|
||||||
|
staking.keybase(identity).then((d) => {
|
||||||
if (Array.isArray(d.them) && d.them.length > 0) {
|
if (Array.isArray(d.them) && d.them.length > 0) {
|
||||||
const uri = String(d.them[0]?.pictures?.primary?.url).replace("https://s3.amazonaws.com/keybase_processed_uploads/", "")
|
const uri = String(d.them[0]?.pictures?.primary?.url).replace(
|
||||||
if(uri) {
|
'https://s3.amazonaws.com/keybase_processed_uploads/',
|
||||||
avatars.value[identity] = uri
|
''
|
||||||
localStorage.setItem('avatars', JSON.stringify(avatars.value))
|
);
|
||||||
|
if (uri) {
|
||||||
|
avatars.value[identity] = uri;
|
||||||
|
localStorage.setItem(
|
||||||
|
'avatars',
|
||||||
|
JSON.stringify(avatars.value)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
resolve()
|
resolve();
|
||||||
})
|
});
|
||||||
}else{
|
|
||||||
resolve()
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
staking.$subscribe((m, s)=> {
|
|
||||||
if (Array.isArray(m.events)) {
|
|
||||||
m.events.forEach(x => {
|
|
||||||
update(x)
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
update(m.events)
|
resolve();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
staking.$subscribe((m, s) => {
|
||||||
|
if (Array.isArray(m.events)) {
|
||||||
|
m.events.forEach((x) => {
|
||||||
|
update(x);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
update(m.events);
|
||||||
|
}
|
||||||
|
});
|
||||||
const logo = (identity?: string) => {
|
const logo = (identity?: string) => {
|
||||||
if(!identity) return ''
|
if (!identity) return '';
|
||||||
const url = avatars.value[identity] || ''
|
const url = avatars.value[identity] || '';
|
||||||
return url.startsWith('http')? url: `https://s3.amazonaws.com/keybase_processed_uploads/${url}`
|
return url.startsWith('http')
|
||||||
}
|
? url
|
||||||
const rank = function(position: number) {
|
: `https://s3.amazonaws.com/keybase_processed_uploads/${url}`;
|
||||||
let sum = 0
|
};
|
||||||
for(let i = 0;i < position; i++) {
|
const rank = function (position: number) {
|
||||||
sum += Number(staking.validators[i]?.delegator_shares)
|
let sum = 0;
|
||||||
|
for (let i = 0; i < position; i++) {
|
||||||
|
sum += Number(staking.validators[i]?.delegator_shares);
|
||||||
}
|
}
|
||||||
const percent = (sum / staking.totalPower)
|
const percent = sum / staking.totalPower;
|
||||||
|
|
||||||
switch (true) {
|
switch (true) {
|
||||||
case tab.value ==='active' && percent < 0.33: return 'error'
|
case tab.value === 'active' && percent < 0.33:
|
||||||
case tab.value ==='active' && percent < 0.67: return 'warning'
|
return 'error';
|
||||||
default: return 'primary'
|
case tab.value === 'active' && percent < 0.67:
|
||||||
|
return 'warning';
|
||||||
|
default:
|
||||||
|
return 'primary';
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="tabs tabs-boxed bg-transparent mb-4">
|
||||||
|
<a
|
||||||
|
class="tab text-gray-400"
|
||||||
|
:class="{ 'tab-active': tab === 'active' }"
|
||||||
|
@click="tab = 'active'"
|
||||||
|
>Active</a
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="tab text-gray-400"
|
||||||
|
:class="{ 'tab-active': tab === 'inactive' }"
|
||||||
|
@click="tab = 'inactive'"
|
||||||
|
>Inactive</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-lg font-semibold">
|
||||||
|
{{ list.length }}/{{ staking.params.max_validators }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<VCard>
|
<VCard>
|
||||||
<VCardTitle class="d-flex justify-space-between">
|
|
||||||
<VBtnToggle v-model="tab" size="small" color="primary">
|
|
||||||
<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>
|
|
||||||
</VCardTitle>
|
|
||||||
<VTable class="text-no-wrap table-header-bg rounded-0">
|
<VTable class="text-no-wrap table-header-bg rounded-0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th
|
<th scope="col" style="width: 3rem">#</th>
|
||||||
scope="col"
|
<th scope="col">VALIDATOR</th>
|
||||||
style="width: 3rem;"
|
<th scope="col" class="text-right">VOTING POWER</th>
|
||||||
>#</th>
|
<th scope="col" class="text-right">24h CHANGES</th>
|
||||||
<th scope="col">
|
<th scope="col" class="text-right">COMMISSION</th>
|
||||||
VALIDATOR
|
<th scope="col">ACTIONS</th>
|
||||||
</th>
|
|
||||||
<th scope="col" class="text-right">
|
|
||||||
VOTING POWER
|
|
||||||
</th>
|
|
||||||
<th scope="col" class="text-right">
|
|
||||||
24h CHANGES
|
|
||||||
</th>
|
|
||||||
<th scope="col" class="text-right">
|
|
||||||
COMMISSION
|
|
||||||
</th>
|
|
||||||
<th scope="col">
|
|
||||||
ACTIONS
|
|
||||||
</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr
|
<tr v-for="(v, i) in list" :key="v.operator_address">
|
||||||
v-for="(v, i) in list"
|
|
||||||
:key="v.operator_address"
|
|
||||||
>
|
|
||||||
<!-- 👉 rank -->
|
<!-- 👉 rank -->
|
||||||
<td>
|
<td>
|
||||||
<VChip label :color="rank(i)">
|
<VChip label :color="rank(i)">
|
||||||
@ -175,7 +186,10 @@ const rank = function(position: number) {
|
|||||||
|
|
||||||
<!-- 👉 Validator -->
|
<!-- 👉 Validator -->
|
||||||
<td>
|
<td>
|
||||||
<div class="d-flex align-center overflow-hidden" style="max-width: 400px;">
|
<div
|
||||||
|
class="d-flex align-center overflow-hidden"
|
||||||
|
style="max-width: 400px"
|
||||||
|
>
|
||||||
<VAvatar
|
<VAvatar
|
||||||
variant="tonal"
|
variant="tonal"
|
||||||
class="me-3"
|
class="me-3"
|
||||||
@ -186,14 +200,18 @@ const rank = function(position: number) {
|
|||||||
<div class="d-flex flex-column">
|
<div class="d-flex flex-column">
|
||||||
<h6 class="text-sm text-primary">
|
<h6 class="text-sm text-primary">
|
||||||
<RouterLink
|
<RouterLink
|
||||||
:to="{name: 'chain-staking-validator', params: {validator: v.operator_address}}"
|
:to="{
|
||||||
|
name: 'chain-staking-validator',
|
||||||
|
params: { validator: v.operator_address },
|
||||||
|
}"
|
||||||
class="font-weight-medium user-list-name"
|
class="font-weight-medium user-list-name"
|
||||||
>
|
>
|
||||||
{{ v.description?.moniker }}
|
{{ v.description?.moniker }}
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
|
||||||
</h6>
|
</h6>
|
||||||
<span class="text-xs">{{ v.description?.website || v.description?.identity || '-' }}</span>
|
<span class="text-xs">{{
|
||||||
|
v.description?.website || v.description?.identity || '-'
|
||||||
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@ -202,18 +220,40 @@ const rank = function(position: number) {
|
|||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
<div class="d-flex flex-column">
|
<div class="d-flex flex-column">
|
||||||
<h6 class="text-sm font-weight-medium">
|
<h6 class="text-sm font-weight-medium">
|
||||||
{{ format.formatToken( {amount: parseInt(v.tokens).toString(), denom: staking.params.bond_denom }, true, "0,0") }}
|
{{
|
||||||
|
format.formatToken(
|
||||||
|
{
|
||||||
|
amount: parseInt(v.tokens).toString(),
|
||||||
|
denom: staking.params.bond_denom,
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
'0,0'
|
||||||
|
)
|
||||||
|
}}
|
||||||
</h6>
|
</h6>
|
||||||
<span class="text-xs">{{ format.calculatePercent(v.delegator_shares, staking.totalPower) }}</span>
|
<span class="text-xs">{{
|
||||||
|
format.calculatePercent(
|
||||||
|
v.delegator_shares,
|
||||||
|
staking.totalPower
|
||||||
|
)
|
||||||
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<!-- 👉 24h Changes -->
|
<!-- 👉 24h Changes -->
|
||||||
<td class="text-right text-xs" :class="change24Color(v.consensus_pubkey)">
|
<td
|
||||||
{{ change24Text(v.consensus_pubkey) }} <VChip label v-if="v.jailed" color="error">Jailed</VChip>
|
class="text-right text-xs"
|
||||||
|
:class="change24Color(v.consensus_pubkey)"
|
||||||
|
>
|
||||||
|
{{ change24Text(v.consensus_pubkey) }}
|
||||||
|
<VChip label v-if="v.jailed" color="error">Jailed</VChip>
|
||||||
</td>
|
</td>
|
||||||
<!-- 👉 commission -->
|
<!-- 👉 commission -->
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
{{ format.formatCommissionRate(v.commission?.commission_rates?.rate) }}
|
{{
|
||||||
|
format.formatCommissionRate(
|
||||||
|
v.commission?.commission_rates?.rate
|
||||||
|
)
|
||||||
|
}}
|
||||||
</td>
|
</td>
|
||||||
<!-- 👉 Action -->
|
<!-- 👉 Action -->
|
||||||
<td>
|
<td>
|
||||||
@ -222,9 +262,10 @@ const rank = function(position: number) {
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</VTable>
|
</VTable>
|
||||||
<VDivider/>
|
<VDivider />
|
||||||
<VCardActions class="py-2">
|
<VCardActions class="py-2">
|
||||||
<VChip label color="error">Top 33%</VChip> <VChip label color="warning" class="mx-2">Top 67%</VChip>
|
<VChip label color="error">Top 33%</VChip>
|
||||||
|
<VChip label color="warning" class="mx-2">Top 67%</VChip>
|
||||||
</VCardActions>
|
</VCardActions>
|
||||||
</VCard>
|
</VCard>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user