Merge pull request #14 from ping-pub/v3-single

V3 single
This commit is contained in:
Alisa | Side.one 2023-05-16 20:52:22 +08:00 committed by GitHub
commit add8a3466b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 330 additions and 84 deletions

View File

@ -8,6 +8,7 @@ import { ref } from 'vue';
defineProps({ defineProps({
proposals: { type: Object as PropType<PaginatedProposals> }, proposals: { type: Object as PropType<PaginatedProposals> },
votable: { type: Boolean, default: false }
}); });
const format = useFormatter(); const format = useFormatter();
@ -30,12 +31,12 @@ const proposalInfo = ref();
</script> </script>
<template> <template>
<div class="bg-white dark:bg-[#28334e] rounded text-sm"> <div class="bg-white dark:bg-[#28334e] rounded text-sm">
<table class="table-compact w-full table-fixed hidden lg:table"> <table class="table-compact w-full table-fixed lg:table">
<tbody> <tbody>
<tr v-for="(item, index) in proposals?.proposals" :key="index"> <tr v-for="(item, index) in proposals?.proposals" :key="index">
<td class="px-4 w-20"> <td class="px-4 w-20">
<label <label
for="proposal-detail-modal" for=""
class="text-main text-base hover:text-indigo-400 cursor-pointer" class="text-main text-base hover:text-indigo-400 cursor-pointer"
@click="proposalInfo = item" @click="proposalInfo = item"
> >
@ -97,7 +98,7 @@ const proposalInfo = ref();
</div> </div>
</td> </td>
<td> <td v-if="votable">
<div> <div>
<button class="btn btn-xs btn-primary rounded-sm">Vote</button> <button class="btn btn-xs btn-primary rounded-sm">Vote</button>
</div> </div>

View File

@ -5,7 +5,7 @@ const baseStore = useBaseStore();
chainStore.initial(); chainStore.initial();
chainStore.$subscribe((m, s) => { chainStore.$subscribe((m, s) => {
if (!Array.isArray(m.events) && m.events.key === 'endpoint') { if (!Array.isArray(m.events) && m.events.key === 'endpoint') {
chainStore.initial(); // chainStore.initial();
} }
}); });
</script> </script>

View File

@ -98,7 +98,7 @@ export class CosmosRestClient extends BaseRestClient<RequestRegistry> {
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 = 50) { async getGovProposals(status: string, limit = 20) {
const query = const query =
'?proposal_status={status}&pagination.limit={limit}&pagination.reverse=true&pagination.key='; '?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);

View File

@ -41,7 +41,7 @@ const changeTab = (val: '2' | '3' | '4') => {
>Rejected</a >Rejected</a
> >
</div> </div>
<ProposalListItem :proposals="store?.proposals[tab]" /> <ProposalListItem :proposals="store?.proposals[tab]" :votable="tab === '2'" />
</div> </div>
</template> </template>
<route> <route>

View File

@ -16,23 +16,14 @@ import type { SigningInfo } from '@/types/slashing';
const props = defineProps(['chain']); const props = defineProps(['chain']);
const stakingStore = useStakingStore(); const stakingStore = useStakingStore();
const format = useFormatter();
const baseStore = useBaseStore(); const baseStore = useBaseStore();
const chainStore = useBlockchain(); const chainStore = useBlockchain();
const latest = ref({} as Block); const latest = ref(0);
const commits = ref([] as Commit[]); const commits = ref([] as Commit[]);
const keyword = ref(''); const keyword = ref('');
const live = ref(true); const live = ref(true);
// storage local favorite validator ids
const local = ref(
JSON.parse(localStorage.getItem('uptime-validators') || '{}') as Record<
string,
string[]
>
);
const currentPined = local.value[chainStore.chainName]
const selected = ref(currentPined || []); // favorite validators on selected blockchain
const signingInfo = ref({} as Record<string, SigningInfo>); const signingInfo = ref({} as Record<string, SigningInfo>);
// filter validators by keywords // filter validators by keywords
@ -44,10 +35,18 @@ const validators = computed(() => {
return stakingStore.validators; return stakingStore.validators;
}); });
const list = computed(() => {
return validators.value.map(v => ({
v,
signing: signingInfo.value[consensusPubkeyToHexAddress(v.consensus_pubkey)],
hex: toBase64(fromHex(consensusPubkeyToHexAddress(v.consensus_pubkey)))
}))
})
onMounted(() => { onMounted(() => {
live.value = true; live.value = true;
baseStore.fetchLatest().then(b => { baseStore.fetchLatest().then(b => {
latest.value = b; latest.value = Number(b.block.header.height);
commits.value.unshift(b.block.last_commit); commits.value.unshift(b.block.last_commit);
const height = Number(b.block.header?.height || 0); const height = Number(b.block.header?.height || 0);
if (height === 0) { if (height === 0) {
@ -82,77 +81,102 @@ onMounted(() => {
const commits2 = computed(() => { const commits2 = computed(() => {
const la = baseStore.recents.map(b => b.block.last_commit) const la = baseStore.recents.map(b => b.block.last_commit)
const all = [...commits.value, ...la] const all = [...commits.value, ...la]
return all.length > 50 ? all.slice(all.length - 50): all return all.length > 50 ? all.slice(all.length - 50) : all
}) })
onUnmounted(() => { onUnmounted(() => {
live.value = false; live.value = false;
}); });
const tab = ref("3")
function changeTab(v: string) {
tab.value = v
}
watchEffect(() => {
local.value[chainStore.chainName] = selected.value;
localStorage.setItem('uptime-validators', JSON.stringify(local.value));
});
</script> </script>
<template> <template>
<div class="bg-base-100 px-5 pt-5"> <div>
<div class="flex items-center gap-x-4"> <div class="tabs tabs-boxed bg-transparent mb-4">
<input <a
type="text" class="tab text-gray-400 capitalize"
v-model="keyword" :class="{ 'tab-active': tab === '3' }"
placeholder="Keywords to filter validators" @click="changeTab('3')"
class="input input-sm w-full flex-1" >Overall</a
/> >
<button class="btn btn-primary btn-sm"> <a
<Icon icon="mdi-star" class="mr-2 text-lg" /> class="tab text-gray-400 capitalize"
<span class="">Favorite</span> :class="{ 'tab-active': tab === '2' }"
</button> @click="changeTab('2')"
>Blocks</a
>
<RouterLink :to="`/${chain}/uptime/overview`">
<a
class="tab text-gray-400 capitalize"
>Customize</a
></RouterLink>
</div> </div>
<div <div class="bg-base-100 px-5 pt-5">
class="grid grid-cols-1 md:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-6 gap-x-4 mt-4" <div class="flex items-center gap-x-4">
> <input type="text" v-model="keyword" placeholder="Keywords to filter validators"
<div v-for="(v, i) in validators" :key="i" > class="input input-sm w-full flex-1" />
<div class="flex items-center justify-between py-0"> <RouterLink class="btn btn-primary btn-sm" :to="`/${chain}/uptime/overview`">
<label class="text-truncate text-sm"> <Icon icon="mdi-star" class="mr-2 text-lg" />
<input type="checkbox" <span class="">Favorite</span>
v-model="selected" </RouterLink>
:value="v.operator_address" </div>
/> <div class="grid grid-cols-1 md:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-6 gap-x-4 mt-4" :class="tab === '2'?'':'hidden'">
<span class="ml-1 text-black dark:text-white">{{ i + 1 }}.{{ v.description.moniker }}</span> <div v-for="({v, signing, hex}, i) in list" :key="i">
</label> <div class="flex items-center justify-between py-0">
<div <label class="text-truncate text-sm">
v-if=" <span class="ml-1 text-black dark:text-white">{{ i + 1 }}.{{ v.description.moniker }}</span>
Number( </label>
signingInfo[consensusPubkeyToHexAddress(v.consensus_pubkey)] <div v-if="Number(signing?.missed_blocks_counter || 0) > 10" class="badge badge-error badge-sm text-white">
?.missed_blocks_counter || 0 {{ signing?.missed_blocks_counter }}
) > 0 </div>
" <div v-else class="mt-1 badge badge-sm text-white bg-yes border-0">
class="badge badge-error badge-sm text-white" {{ signing?.missed_blocks_counter }}
> </div>
{{ </div>
signingInfo[consensusPubkeyToHexAddress(v.consensus_pubkey)] <UptimeBar :blocks="commits2" :validator="hex" />
?.missed_blocks_counter </div>
}}
</div>
<div v-else class="mt-1 badge badge-sm text-white bg-yes border-0">
{{
signingInfo[consensusPubkeyToHexAddress(v.consensus_pubkey)]
?.missed_blocks_counter
}}
</div>
</div>
<UptimeBar
:blocks="commits2"
:validator="
toBase64(fromHex(consensusPubkeyToHexAddress(v.consensus_pubkey)))
"
/>
</div> </div>
</div>
<div class="h-6"></div> <div :class="tab === '3'?'':'hidden'">
<table class="table table-compact w-full mt-5">
<thead>
<tr>
<td rowspan="2">Validator</td>
<td rowspan="2">Start Height</td>
<td rowspan="2">Signed Blocks</td>
<td colspan="2">Missing blocks</td>
<td rowspan="2">Last Jailed Time</td>
<td rowspan="2">Tombstoned</td>
</tr>
<tr>
<td>In Window</td>
<td>Over All</td>
</tr>
</thead>
<tr v-for="({v, signing}, i) in list">
<td>{{ i+1 }}. {{ v.description.moniker }}</td>
<td>{{ signing?.start_height }}</td>
<td>{{ signing?.index_offset }}</td>
<td>
<span class="badge badge-sm text-white" :class="Number(signing?.missed_blocks_counter) < 10?'badge-success':'badge-error'">{{ signing?.missed_blocks_counter }}</span>
</td>
<td><span v-if="signing && signing.jailed_until.startsWith('1970')">{{ format.percent(Number(signing.index_offset)/(latest-Number(signing.start_height))) }}</span></td>
<td><span v-if="signing && !signing.jailed_until.startsWith('1970')">
<div class="tooltip" :data-tip="format.toDay(signing?.jailed_until, 'long')">
<span>{{ format.toDay(signing?.jailed_until, "from") }}</span>
</div>
</span></td>
<td class=" capitalize">{{ signing?.tombstoned }}</td>
</tr>
</table>
</div>
<div class="h-6"></div>
</div>
</div> </div>
</template> </template>
<route> <route>

View File

@ -0,0 +1,211 @@
<script lang="ts" setup>
import { ref, onMounted, computed, watchEffect } from 'vue';
import { fromHex, toBase64 } from '@cosmjs/encoding';
import { Icon } from '@iconify/vue';
import {
useFormatter,
useStakingStore,
useBaseStore,
useBlockchain,
useDashboard,
} from '@/stores';
import UptimeBar from '@/components/UptimeBar.vue';
import type { Block, Commit } from '@/types';
import { consensusPubkeyToHexAddress, valconsToBase64 } from '@/libs';
import type { SigningInfo } from '@/types/slashing';
import { CosmosRestClient } from '@/libs/client';
const props = defineProps(['chain']);
const stakingStore = useStakingStore();
const format = useFormatter();
const chainStore = useBlockchain();
const dashboard = useDashboard()
// storage local favorite validator ids
const local = ref(JSON.parse(localStorage.getItem('uptime-validators') || '{}') as Record<string, {name: string, address: string}[]>)
const signingInfo = ref({} as Record<string, SigningInfo[]>);
const selected = ref([] as string[])
const selectChain = ref(chainStore.chainName)
const validators = ref(stakingStore.validators)
const keyword = ref("")
if(local.value) Object.keys(local.value).map(chainName => {
const chain = dashboard.chains[chainName]
if(chain && chain.endpoints.rest) {
const client = CosmosRestClient.newDefault(chain.endpoints.rest[0].address)
client.getSlashingSigningInfos().then( resp => {
signingInfo.value[chainName] = resp.info
})
}
if(chainName === selectChain.value) {
const vals = local.value[chainName]
if(vals) {
selected.value = vals.map(x => x.address)
}
}
return chain
})
const filterValidators = computed(() => {
if(keyword.value) {
return validators.value.filter(x => x.description.moniker.indexOf(keyword.value) > -1)
}
return validators.value
})
const list = computed(() => {
const list = [] as any[]
if(local.value) Object.keys(local.value).map( chainName => {
const vals = local.value[chainName]
const info = signingInfo.value[chainName]
if(vals && info) {
vals.forEach(v => {
const sigingInfo = info.find(x => valconsToBase64(x.address) === v.address)
list.push( {
chainName,
v,
sigingInfo,
})
})
}
})
return list
})
function add() {
const newList = [] as { name: string; address: string; }[]
selected.value.forEach(x => {
const validator = validators.value.find(v => (consensusPubkeyToHexAddress(v.consensus_pubkey) === x))
if(validator) newList.push({
name: validator.description.moniker || x,
address: x
})
})
if(!local.value) local.value = {}
local.value[selectChain.value] = newList
localStorage.setItem("uptime-validators", JSON.stringify(local.value))
}
function changeChain() {
validators.value = []
const endpoint = dashboard.chains[selectChain.value].endpoints.rest?.at(0)?.address
if(!endpoint) return
const client = CosmosRestClient.newDefault(endpoint)
client.getStakingValidators("BOND_STATUS_BONDED").then(x => {
validators.value = x.validators
})
const vals = local.value[selectChain.value]
if(vals) {
selected.value = vals.map(x => x.address)
} else {
selected.value = []
}
}
function color(v: string) {
if(v) {
const n = Number(v)
if(n < 10) return " badge-success"
if(n > 1000) return " badge-error"
if(n > 0) return " badge-warning"
}
}
</script>
<template>
<div>
<div class="overflow-x-auto w-full card ">
<div class="lg:flex lg:items-center lg:justify-between bg-base-100 p-5">
<div class="min-w-0 flex-1">
<h2 class="text-2xl font-bold leading-7 sm:truncate sm:text-3xl sm:tracking-tight">My Validators</h2>
<div class="mt-1 flex flex-col sm:mt-0 sm:flex-row sm:flex-wrap sm:space-x-6">
<div class="mt-2 flex items-center text-sm text-gray-500">
<svg class="mr-1.5 h-5 w-5 flex-shrink-0 text-gray-400" viewBox="0 0 20 20" fill="currentColor"
aria-hidden="true">
<path fill-rule="evenodd"
d="M6 3.75A2.75 2.75 0 018.75 1h2.5A2.75 2.75 0 0114 3.75v.443c.572.055 1.14.122 1.706.2C17.053 4.582 18 5.75 18 7.07v3.469c0 1.126-.694 2.191-1.83 2.54-1.952.599-4.024.921-6.17.921s-4.219-.322-6.17-.921C2.694 12.73 2 11.665 2 10.539V7.07c0-1.321.947-2.489 2.294-2.676A41.047 41.047 0 016 4.193V3.75zm6.5 0v.325a41.622 41.622 0 00-5 0V3.75c0-.69.56-1.25 1.25-1.25h2.5c.69 0 1.25.56 1.25 1.25zM10 10a1 1 0 00-1 1v.01a1 1 0 001 1h.01a1 1 0 001-1V11a1 1 0 00-1-1H10z"
clip-rule="evenodd" />
<path
d="M3 15.055v-.684c.126.053.255.1.39.142 2.092.642 4.313.987 6.61.987 2.297 0 4.518-.345 6.61-.987.135-.041.264-.089.39-.142v.684c0 1.347-.985 2.53-2.363 2.686a41.454 41.454 0 01-9.274 0C3.985 17.585 3 16.402 3 15.055z" />
</svg>
Add validators you want to monitor
</div>
</div>
</div>
<div class="mt-5 flex lg:ml-4 lg:mt-0">
</div>
</div>
<table class="table w-full">
<thead>
<tr>
<th>Blockchain</th>
<th>Validator</th>
<th>Missing Blocks</th>
<th>Signed Blocks</th>
<th>Last Jailed Time</th>
<th>Tombstoned</th>
</tr>
</thead>
<tbody>
<tr v-for="v in list">
<td class=" capitalize">{{ v.chainName }}</td>
<td>{{ v.v.name }}</td>
<td><span class="badge " :class="color( v.sigingInfo?.missed_blocks_counter)">{{ v.sigingInfo?.missed_blocks_counter }}</span></td>
<td><span v-if="v.sigingInfo">{{ Number(v.sigingInfo.index_offset) - Number(v.sigingInfo.start_height) }}</span></td>
<td>
<div v-if="v.sigingInfo && !v.sigingInfo?.jailed_until.startsWith('1970')" class="text-xs flex flex-col">
<div class="badge">{{ format.toDay(v.sigingInfo.jailed_until, 'from') }}</div>
<div>{{v.sigingInfo?.jailed_until }}</div>
</div>
</td>
<td class=" capitalize">{{ v.sigingInfo?.tombstoned }}</td>
</tr>
</tbody>
</table>
</div>
<label for="add-validator" class="btn btn-primary mt-5">Add Validators</label>
<!-- Put this part before </body> tag -->
<input type="checkbox" id="add-validator" class="modal-toggle" />
<div class="modal">
<div class="modal-box relative">
<label for="add-validator" class="btn btn-sm btn-circle absolute right-2 top-2"></label>
<h3 class="text-lg font-bold">Add Validators</h3>
<div class="py-4 max-h-60 overflow-y-auto">
<div class="form-control my-5 border-2">
<div class="input-group input-group-md">
<select v-model="selectChain" class="select select-bordered capitalize" @change="changeChain">
<option v-for="v in dashboard.chains" :value="v.chainName">
{{ v.chainName }}
</option>
</select>
<input v-model="keyword" type="text" class="input w-full" placeholder="keywords to filter validator">
</div>
</div>
<table class="table table-compact w-full hover">
<thead>
<tr><th>Validator</th><th></th></tr>
</thead>
<tbody>
<tr v-for="v in filterValidators">
<td><label :for="v.operator_address"><div class=" w-full">{{ v.description.moniker }}</div></label></td>
<td><input :id="v.operator_address" v-model="selected" class="checkbox" type="checkbox" :value="consensusPubkeyToHexAddress(v.consensus_pubkey)"/></td>
</tr>
</tbody>
</table>
</div>
<div class="modal-action">
<label for="add-validator" class="btn" @click="add">add</label>
</div>
</div>
</div>
<div class="h-6"></div>
</div>
</template>

View File

@ -13,7 +13,9 @@ router.beforeEach((to) => {
const { chain } = to.params const { chain } = to.params
if(chain){ if(chain){
const blockchain = useBlockchain() const blockchain = useBlockchain()
blockchain.setCurrent(chain.toString()) if(chain !== blockchain.chainName) {
blockchain.setCurrent(chain.toString())
}
} }
}) })

View File

@ -122,11 +122,16 @@ export const useBlockchain = defineStore('blockchain', {
}, },
async randomSetupEndpoint() { async randomSetupEndpoint() {
const all = this.current?.endpoints?.rest; const end = localStorage.getItem(`endpoint-${this.chainName}`)
if (all) { if(end) {
const rn = Math.random(); this.setRestEndpoint(JSON.parse(end))
const endpoint = all[Math.floor(rn * all.length)]; } else {
await this.setRestEndpoint(endpoint); const all = this.current?.endpoints?.rest;
if (all) {
const rn = Math.random();
const endpoint = all[Math.floor(rn * all.length)];
await this.setRestEndpoint(endpoint);
}
} }
}, },
@ -134,9 +139,12 @@ export const useBlockchain = defineStore('blockchain', {
this.connErr = ''; this.connErr = '';
this.endpoint = endpoint; this.endpoint = endpoint;
this.rpc = new CosmosRestClient(endpoint.address, DEFAULT); this.rpc = new CosmosRestClient(endpoint.address, DEFAULT);
localStorage.setItem(`endpoint-${this.chainName}`, JSON.stringify(endpoint))
}, },
setCurrent(name: string) { setCurrent(name: string) {
this.chainName = name; if(name !== this.chainName) {
this.chainName = name;
}
}, },
}, },
}); });