feat: staking tabs

This commit is contained in:
Alisa | Side.one 2023-05-05 09:06:18 +08:00
parent 3f42e542f5
commit 728d8bf02d
2 changed files with 185 additions and 145 deletions

View File

@ -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');

View File

@ -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>