Merge pull request #15 from ping-pub/master

merge
This commit is contained in:
Alisa | Side.one 2023-05-29 13:54:46 +08:00 committed by GitHub
commit fdf5e07db6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 1450 additions and 1092 deletions

View File

@ -1,8 +1,8 @@
{ {
"chain_name": "desmos", "chain_name": "desmos",
"coingecko": "desmos", "coingecko": "desmos",
"api": ["https://api.mainnet.desmos.network"], "api": ["https://api.mainnet.desmos.network", "https://desmos-api.lavenderfive.com"],
"rpc": ["https://rpc.mainnet.desmos.network:443"], "rpc": ["https://rpc.mainnet.desmos.network:443", "https://desmos-rpc.lavenderfive.com:443"],
"sdk_version": "0.45.8", "sdk_version": "0.45.8",
"coin_type": "118", "coin_type": "118",
"min_tx_fee": "3000", "min_tx_fee": "3000",

View File

@ -1,8 +1,8 @@
{ {
"chain_name": "lum-network", "chain_name": "lum-network",
"coingecko": "lum-network", "coingecko": "lum-network",
"api": ["https://api-lum.degeno.de", "https://node0.mainnet.lum.network/rest"], "api": ["https://node0.mainnet.lum.network/rest", "https://lumnetwork-api.lavenderfive.com"],
"rpc": ["https://rpc-lum.degeno.de:443", "https://node0.mainnet.lum.network:443/rpc"], "rpc": ["https://node0.mainnet.lum.network:443/rpc", "https://lumnetwork-rpc.lavenderfive.com:443"],
"snapshot_provider": "", "snapshot_provider": "",
"sdk_version": "0.44.5", "sdk_version": "0.44.5",
"coin_type": "880", "coin_type": "880",

View File

@ -2,8 +2,8 @@
{ {
"chain_name": "shentu", "chain_name": "shentu",
"coingecko": "certik", "coingecko": "certik",
"api": ["https://certik-api.polkachu.com", "https://chainfull.noopsbycertik.com"], "api": ["https://shentu-api.polkachu.com", "https://chainfull.noopsbycertik.com"],
"rpc": ["https://certik-rpc.polkachu.com:443"], "rpc": ["https://shentu-rpc.polkachu.com:443"],
"snapshot_provider": "", "snapshot_provider": "",
"sdk_version": "0.45.9", "sdk_version": "0.45.9",
"coin_type": "118", "coin_type": "118",

19
chains/mainnet/terp.json Normal file
View File

@ -0,0 +1,19 @@
{
"chain_name": "terp",
"coingecko": "",
"api": ["https://lcd.terpnetwork.hexnodes.co"],
"rpc": ["https://rpc.terpnetwork.hexnodes.co"],
"snapshot_provider": "",
"sdk_version": "0.47.1",
"coin_type": "118",
"min_tx_fee": "500",
"addr_prefix": "terp",
"logo": "/logos/terp-network.jpg",
"assets": [{
"base": "uterp",
"symbol": "TERP",
"exponent": "6",
"coingecko_id": "",
"logo": "/logos/terp-network.jpg"
}]
}

View File

@ -35,7 +35,7 @@
"md-editor-v3": "^2.8.1", "md-editor-v3": "^2.8.1",
"numeral": "^2.0.6", "numeral": "^2.0.6",
"osmojs": "^14.0.0-rc.0", "osmojs": "^14.0.0-rc.0",
"ping-widget": "^0.0.26", "ping-widget": "^0.0.30",
"pinia": "^2.0.28", "pinia": "^2.0.28",
"postcss": "^8.4.23", "postcss": "^8.4.23",
"qrcode": "^1.5.3", "qrcode": "^1.5.3",

BIN
public/logos/ledger.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
public/logos/ledger.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1002 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -40,7 +40,7 @@ function gotoPage(pageNum: number) {
<div v-if="total && limit" class="btn-group"> <div v-if="total && limit" class="btn-group">
<button v-for="{ page, color } in pages" :key="page" <button v-for="{ page, color } in pages" :key="page"
class="btn bg-gray-100 text-gray-500 hover:text-white border-none dark:bg-gray-800 dark:text-white" :class="{ class="btn bg-gray-100 text-gray-500 hover:text-white border-none dark:bg-gray-800 dark:text-white" :class="{
'!bg-primary text-white': color === 'btn-primary', '!btn-primary': color === 'btn-primary',
}" @click="gotoPage(page)"> }" @click="gotoPage(page)">
{{ page }} {{ page }}
</button> </button>

View File

@ -3,7 +3,7 @@ import {
useBlockchain, useBlockchain,
useFormatter, useFormatter,
useStakingStore, useStakingStore,
useTxDialog useTxDialog,
} from '@/stores'; } from '@/stores';
import { select } from '@/components/dynamic/index'; import { select } from '@/components/dynamic/index';
import type { PaginatedProposals } from '@/types'; import type { PaginatedProposals } from '@/types';
@ -45,55 +45,94 @@ const proposalInfo = ref();
<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 for="proposal-detail-modal" class="text-main text-base hover:text-indigo-400 cursor-pointer" <label
@click="proposalInfo = item"> for="proposal-detail-modal"
#{{ item?.proposal_id }}</label> class="text-main text-base hover:text-indigo-400 cursor-pointer"
@click="proposalInfo = item"
>
#{{ item?.proposal_id }}</label
>
</td> </td>
<td class="w-full"> <td class="w-full">
<div> <div>
<RouterLink :to="`/${chain.chainName}/gov/${item?.proposal_id}`" <RouterLink
class="text-main text-base mb-1 block hover:text-indigo-400 truncate"> :to="`/${chain.chainName}/gov/${item?.proposal_id}`"
class="text-main text-base mb-1 block hover:text-indigo-400 truncate"
>
{{ item?.content?.title }} {{ item?.content?.title }}
</RouterLink> </RouterLink>
<div <div
class="bg-[#f6f2ff] text-[#9c6cff] dark:bg-gray-600 dark:text-gray-300 inline-block rounded-full px-2 py-[1px] text-xs mb-1"> class="bg-[#f6f2ff] text-[#9c6cff] dark:bg-gray-600 dark:text-gray-300 inline-block rounded-full px-2 py-[1px] text-xs mb-1"
>
{{ showType(item.content['@type']) }} {{ showType(item.content['@type']) }}
</div> </div>
</div> </div>
</td> </td>
<td class="w-60"> <td class="w-60">
<ProposalProcess :pool="staking.pool" :tally="item.final_tally_result"></ProposalProcess> <ProposalProcess
:pool="staking.pool"
:tally="item.final_tally_result"
></ProposalProcess>
</td> </td>
<td class="w-36"> <td class="w-36">
<div class="pl-4"> <div class="pl-4">
<div class="flex items-center" :class="statusMap?.[item?.status] === 'PASSED' <div
? 'text-yes' class="flex items-center"
: statusMap?.[item?.status] === 'REJECTED' :class="
? 'text-no' statusMap?.[item?.status] === 'PASSED'
: 'text-info' ? 'text-yes'
"> : statusMap?.[item?.status] ===
<div class="w-1 h-1 rounded-full mr-2" :class="statusMap?.[item?.status] === 'PASSED' 'REJECTED'
? 'bg-yes' ? 'text-no'
: statusMap?.[item?.status] === 'REJECTED' : 'text-info'
? 'bg-no' "
: 'bg-info' >
"></div> <div
class="w-1 h-1 rounded-full mr-2"
:class="
statusMap?.[item?.status] === 'PASSED'
? 'bg-yes'
: statusMap?.[item?.status] ===
'REJECTED'
? 'bg-no'
: 'bg-info'
"
></div>
<div class="text-xs"> <div class="text-xs">
{{ statusMap?.[item?.status] || item?.status }} {{
statusMap?.[item?.status] ||
item?.status
}}
</div> </div>
</div> </div>
<div <div
class="truncate col-span-2 md:!col-span-1 text-xs text-gray-500 dark:text-gray-400 text-right md:!flex md:!justify-start"> class="truncate col-span-2 md:!col-span-1 text-xs text-gray-500 dark:text-gray-400 text-right md:!flex md:!justify-start"
>
{{ format.toDay(item.voting_end_time, 'from') }} {{ format.toDay(item.voting_end_time, 'from') }}
</div> </div>
</div> </div>
</td> </td>
<td v-if="statusMap?.[item?.status] === 'VOTING'" class="w-40"> <td
v-if="statusMap?.[item?.status] === 'VOTING'"
class="w-40"
>
<div class=""> <div class="">
<label for="vote" class="btn btn-xs btn-primary rounded" <label
@click="dialog.open('vote', { proposal_id: item?.proposal_id })"> for="vote"
<span v-if="item?.voterStatus">{{ item?.voterStatus.replace("VOTE_OPTION_", "") }}</span> class="btn btn-xs btn-primary rounded-sm"
@click="
dialog.open('vote', {
proposal_id: item?.proposal_id,
})
"
>
<span v-if="item?.voterStatus">{{
item?.voterStatus.replace(
'VOTE_OPTION_',
''
)
}}</span>
<span v-else>Vote</span> <span v-else>Vote</span>
</label> </label>
</div> </div>
@ -103,56 +142,99 @@ const proposalInfo = ref();
</table> </table>
<div class="lg:!hidden"> <div class="lg:!hidden">
<div v-for="(item, index) in proposals?.proposals" :key="index" class="px-4 py-4"> <div
<div class="text-main text-base mb-1 flex justify-between hover:text-indigo-400"> v-for="(item, index) in proposals?.proposals"
<RouterLink :to="`/${chain.chainName}/gov/${item?.proposal_id}`" class="flex-1 w-0 truncate mr-4">{{ :key="index"
item?.content?.title }}</RouterLink> class="px-4 py-4"
<label for="proposal-detail-modal" class="text-main text-base hover:text-indigo-400 cursor-pointer" >
@click="proposalInfo = item"> <div
#{{ item?.proposal_id }}</label> class="text-main text-base mb-1 flex justify-between hover:text-indigo-400"
>
<RouterLink
:to="`/${chain.chainName}/gov/${item?.proposal_id}`"
class="flex-1 w-0 truncate mr-4"
>{{ item?.content?.title }}</RouterLink
>
<label
for="proposal-detail-modal"
class="text-main text-base hover:text-indigo-400 cursor-pointer"
@click="proposalInfo = item"
>
#{{ item?.proposal_id }}</label
>
</div> </div>
<div class="grid grid-cols-4 mt-2 mb-2"> <div class="grid grid-cols-4 mt-2 mb-2">
<div class="col-span-2"> <div class="col-span-2">
<div <div
class="bg-[#f6f2ff] text-[#9c6cff] dark:bg-gray-600 dark:text-gray-300 inline-block rounded-full px-2 py-[1px] text-xs mb-1"> class="bg-[#f6f2ff] text-[#9c6cff] dark:bg-gray-600 dark:text-gray-300 inline-block rounded-full px-2 py-[1px] text-xs mb-1"
>
{{ showType(item.content['@type']) }} {{ showType(item.content['@type']) }}
</div> </div>
</div> </div>
<div class="flex items-center" :class="statusMap?.[item?.status] === 'PASSED' <div
? 'text-yes' class="flex items-center"
: statusMap?.[item?.status] === 'REJECTED' :class="
? 'text-no' statusMap?.[item?.status] === 'PASSED'
: 'text-info' ? 'text-yes'
"> : statusMap?.[item?.status] === 'REJECTED'
<div class="w-1 h-1 rounded-full mr-2" :class="statusMap?.[item?.status] === 'PASSED' ? 'text-no'
? 'bg-yes' : 'text-info'
: statusMap?.[item?.status] === 'REJECTED' "
? 'bg-no' >
: 'bg-info' <div
"></div> class="w-1 h-1 rounded-full mr-2"
:class="
statusMap?.[item?.status] === 'PASSED'
? 'bg-yes'
: statusMap?.[item?.status] === 'REJECTED'
? 'bg-no'
: 'bg-info'
"
></div>
<div class="text-xs flex items-center"> <div class="text-xs flex items-center">
{{ statusMap?.[item?.status] || item?.status }} {{ statusMap?.[item?.status] || item?.status }}
</div> </div>
</div> </div>
<div class="truncate text-xs text-gray-500 dark:text-gray-400 flex items-center justify-end"> <div
class="truncate text-xs text-gray-500 dark:text-gray-400 flex items-center justify-end"
>
{{ format.toDay(item.voting_end_time, 'from') }} {{ format.toDay(item.voting_end_time, 'from') }}
</div> </div>
</div> </div>
<div> <div>
<ProposalProcess :pool="staking.pool" :tally="item.final_tally_result"></ProposalProcess> <ProposalProcess
:pool="staking.pool"
:tally="item.final_tally_result"
></ProposalProcess>
</div> </div>
<div class="mt-4" v-if="statusMap?.[item?.status] === 'VOTING'"> <div class="mt-4" v-if="statusMap?.[item?.status] === 'VOTING'">
<div class="" v-show="item?.voterStatus === 'No With Veto'"> <div class="" v-show="item?.voterStatus === 'No With Veto'">
<label for="vote" class="btn btn-xs btn-primary rounded" <label
@click="dialog.open('vote', { proposal_id: item?.proposal_id })">Vote</label> for="vote"
<div class="text-xs truncate relative py-1 px-3 rounded-full w-fit" class="btn btn-xs btn-primary rounded-sm"
:class="`text-${voterStatusMap?.[item?.voterStatus]}`" @click="
v-show="item?.voterStatus !== 'No With Veto'"> dialog.open('vote', {
<span class="inset-x-0 inset-y-0 opacity-10 absolute" proposal_id: item?.proposal_id,
:class="`bg-${voterStatusMap?.[item?.voterStatus]}`"></span> })
"
>Vote</label
>
<div
class="text-xs truncate relative py-1 px-3 rounded-full w-fit"
:class="`text-${
voterStatusMap?.[item?.voterStatus]
}`"
v-show="item?.voterStatus !== 'No With Veto'"
>
<span
class="inset-x-0 inset-y-0 opacity-10 absolute"
:class="`bg-${
voterStatusMap?.[item?.voterStatus]
}`"
></span>
{{ item?.voterStatus }} {{ item?.voterStatus }}
</div> </div>
</div> </div>
@ -160,15 +242,30 @@ const proposalInfo = ref();
</div> </div>
</div> </div>
<input type="checkbox" id="proposal-detail-modal" class="modal-toggle" /> <input
type="checkbox"
id="proposal-detail-modal"
class="modal-toggle"
/>
<label for="proposal-detail-modal" class="modal"> <label for="proposal-detail-modal" class="modal">
<label class="modal-box w-11/12 max-w-5xl" for=""> <label class="modal-box w-11/12 max-w-5xl" for="">
<label for="proposal-detail-modal" class="btn btn-sm btn-circle absolute right-2 top-2"></label> <label
for="proposal-detail-modal"
class="btn btn-sm btn-circle absolute right-2 top-2"
></label
>
<h3 class="font-bold text-lg">Description</h3> <h3 class="font-bold text-lg">Description</h3>
<p class="py-4"> <p class="py-4">
<Component v-if="proposalInfo?.content?.description" <Component
:is="select(proposalInfo?.content?.description, 'horizontal')" v-if="proposalInfo?.content?.description"
:value="proposalInfo?.content?.description"> :is="
select(
proposalInfo?.content?.description,
'horizontal'
)
"
:value="proposalInfo?.content?.description"
>
</Component> </Component>
</p> </p>
</label> </label>

View File

@ -1,50 +0,0 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue';
import { onMounted, ref } from 'vue';
const props = defineProps<{
themes: {
name: string;
icon: string;
}[];
}>();
const themeMap = { system: 'mdi-laptop', light: 'mdi-weather-sunny', dark: 'mdi-weather-night' }
const theme = ref(window.localStorage.getItem('theme') || 'dark');
const changeMode = () => {
let value = 'dark';
if (theme.value === 'dark') {
value = 'light';
}
if (
theme.value === 'system' &&
window.matchMedia('(prefers-color-scheme: dark)').matches
) {
value = 'dark';
}
if (value === 'light') {
document.documentElement.classList.add('light');
document.documentElement.classList.remove('dark');
} else {
document.documentElement.classList.add('dark');
document.documentElement.classList.remove('light');
}
document.documentElement.setAttribute('data-theme', value);
window.localStorage.setItem('theme', value);
};
onMounted(() => {
});
</script>
<template>
<div class="tooltip tooltip-bottom delay-1000">
<button class=" btn btn-ghost btn-circle btn-sm mx-1" @click="changeMode">
<Icon :icon="props.themes[theme].icon" class="text-2xl" />
</button>
</div>
</template>

View File

@ -1,21 +1,30 @@
<script lang="ts" setup> <script lang="ts" setup>
import ApexCharts from 'vue3-apexcharts'; import ApexCharts from 'vue3-apexcharts';
import { computed } from 'vue'; import { computed } from 'vue';
import { useBaseStore } from '@/stores';
import { getDonutChartConfig } from './apexChartConfig'; import { getDonutChartConfig } from './apexChartConfig';
const props = defineProps(['series', 'labels']); const props = defineProps(['series', 'labels']);
const expenseRationChartConfig = computed(() => const baseStore = useBaseStore();
getDonutChartConfig(window.localStorage.getItem('theme') || 'dark', props.labels)
); const expenseRationChartConfig = computed(() => {
const theme = baseStore.theme;
getDonutChartConfig(theme, props.labels);
});
</script> </script>
<template> <template>
<ApexCharts type="donut" height="410" :options="expenseRationChartConfig" :series="series" /> <ApexCharts
type="donut"
height="410"
:options="expenseRationChartConfig"
:series="series"
/>
</template> </template>
<script lang="ts"> <script lang="ts">
export default { export default {
name: 'DonetChart', name: 'DonetChart',
}; };
</script> </script>

View File

@ -3,40 +3,56 @@ import ApexCharts from 'vue3-apexcharts';
import { getMarketPriceChartConfig } from './apexChartConfig'; import { getMarketPriceChartConfig } from './apexChartConfig';
import { useIndexModule } from '@/modules/[chain]/indexStore'; import { useIndexModule } from '@/modules/[chain]/indexStore';
import { computed, ref } from '@vue/reactivity'; import { computed, ref } from '@vue/reactivity';
import { useBaseStore } from '@/stores';
const store = useIndexModule(); const store = useIndexModule();
const baseStore = useBaseStore();
const chartConfig = computed(() => { const chartConfig = computed(() => {
const labels = store.marketData.prices.map((item: any) => item[0]); const theme = baseStore.theme;
return getMarketPriceChartConfig(window.localStorage.getItem('theme') || 'dark', labels); const labels = store.marketData.prices.map((item: any) => item[0]);
return getMarketPriceChartConfig(theme, labels);
}); });
const kind = ref('price'); const kind = ref('price');
const series = computed(() => { const series = computed(() => {
return [ return [
{ {
name: 'Price', name: 'Price',
data: data:
kind.value === 'price' kind.value === 'price'
? store.marketData.prices.map((item: any) => item[1]) ? store.marketData.prices.map((item: any) => item[1])
: store.marketData.total_volumes.map((item: any) => item[1]), : store.marketData.total_volumes.map(
}, (item: any) => item[1]
]; ),
},
];
}); });
function changeChart(type: string) { function changeChart(type: string) {
kind.value = type; kind.value = type;
} }
</script> </script>
<template> <template>
<div class="tabs tabs-boxed bg-transparent justify-end"> <div class="tabs tabs-boxed bg-transparent justify-end">
<a class="tab text-xs mr-2 text-gray-400 uppercase" :class="{ 'tab-active': kind === 'price' }" <a
@click="changeChart('price')"> class="tab text-xs mr-2 text-gray-400 uppercase"
Price :class="{ 'tab-active': kind === 'price' }"
</a> @click="changeChart('price')"
<a class="tab text-xs text-gray-400 uppercase" :class="{ 'tab-active': kind === 'volume' }" >
@click="changeChart('volume')"> Price
Volume </a>
</a> <a
</div> class="tab text-xs text-gray-400 uppercase"
<ApexCharts type="area" height="230" :options="chartConfig" :series="series" /> :class="{ 'tab-active': kind === 'volume' }"
@click="changeChart('volume')"
>
Volume
</a>
</div>
<ApexCharts
type="area"
height="230"
:options="chartConfig"
:series="series"
/>
</template> </template>

View File

@ -18,7 +18,7 @@ const header = computed(() => {
}); });
</script> </script>
<template> <template>
<div class="overflow-x-auto p-4"> <div class="overflow-auto h-[500px] p-4">
<div <div
v-if="header.length > 0" v-if="header.length > 0"
class="table table-compact w-full" class="table table-compact w-full"
@ -35,7 +35,7 @@ const header = computed(() => {
:key="index" :key="index"
class="text-left text-capitalize" class="text-left text-capitalize"
> >
{{ item }} {{ item }}
</th> </th>
</tr> </tr>
</thead> </thead>

View File

@ -4,11 +4,16 @@ import { select } from './index';
const props = defineProps(['value']); const props = defineProps(['value']);
</script> </script>
<template> <template>
<div class="overflow-x-auto"> <div class="overflow-auto">
<table class="table table-compact w-full text-sm"> <table class="table table-compact w-full text-sm">
<tbody> <tbody>
<tr v-for="(v, k) of value"> <tr v-for="(v, k) of value">
<td class="text-capitalize whitespace-break-spaces w-1/5" style="min-width: 180px;">{{ String(k).replaceAll("_", " ") }}</td> <td
class="text-capitalize whitespace-break-spaces w-1/5"
style="min-width: 180px"
>
{{ String(k).replaceAll('_', ' ') }}
</td>
<td> <td>
<div <div
class="overflow-hidden w-auto whitespace-normal" class="overflow-hidden w-auto whitespace-normal"

View File

@ -85,6 +85,8 @@ function changeEndpoint(item: Endpoint) {
Height: {{ baseStore.latest.block?.header.height }} Height: {{ baseStore.latest.block?.header.height }}
</div> </div>
</div> </div>
<!-- bottom-->
<div class="px-4 py-2">&nbsp;</div>
</div> </div>
</div> </div>
</template> </template>

View File

@ -19,171 +19,281 @@ const blockchain = useBlockchain();
const current = ref(''); const current = ref('');
blockchain.$subscribe((m, s) => { blockchain.$subscribe((m, s) => {
if (current.value != s.chainName) { if (current.value != s.chainName) {
current.value = s.chainName; current.value = s.chainName;
blockchain.initial(); blockchain.initial();
} }
}); });
const sidebarShow = ref(false); const sidebarShow = ref(false);
const sidebarOpen = ref(true); const sidebarOpen = ref(true);
const changeOpen = (index: Number) => { const changeOpen = (index: Number) => {
if (index === 0) { if (index === 0) {
sidebarOpen.value = !sidebarOpen.value; sidebarOpen.value = !sidebarOpen.value;
} }
}; };
const showDiscord = window.location.host.search('ping.pub') > -1; const showDiscord = window.location.host.search('ping.pub') > -1;
</script> </script>
<template> <template>
<div class="bg-gray-100 dark:bg-[#171d30]"> <div class="bg-gray-100 dark:bg-[#171d30]">
<!-- sidebar --> <!-- sidebar -->
<div class="w-64 fixed z-50 left-0 top-0 bottom-0 overflow-auto bg-base-100 border-r border-gray-100 dark:border-gray-700" <div
:class="{ block: sidebarShow, 'hidden xl:!block': !sidebarShow }"> class="w-64 fixed z-50 left-0 top-0 bottom-0 overflow-auto bg-base-100 border-r border-gray-100 dark:border-gray-700"
<div class="flex items-center pl-4 py-4 mb-1"> :class="{ block: sidebarShow, 'hidden xl:!block': !sidebarShow }"
<img class="w-10 h-10" src="../../assets/logo.svg" /> >
<h1 class="flex-1 ml-3 text-2xl font-semibold dark:text-white"> <div class="flex items-center pl-4 py-4 mb-1">
Ping.pub <img class="w-10 h-10" src="../../assets/logo.svg" />
</h1> <h1 class="flex-1 ml-3 text-2xl font-semibold dark:text-white">
<div class="pr-4 cursor-pointer xl:!hidden" @click="sidebarShow = false"> Ping.pub
<Icon icon="mdi-close" class="text-3xl" /> </h1>
</div> <div
</div> class="pr-4 cursor-pointer xl:!hidden"
<div v-for="(item, index) of blockchain.computedChainMenu" :key="index" class="px-2"> @click="sidebarShow = false"
<div v-if="item?.title && item?.children?.length" :tabindex="index" class="collapse" :class="{ >
'collapse-arrow': item?.children?.length > 0, <Icon icon="mdi-close" class="text-3xl" />
'collapse-open': index === 0 && sidebarOpen,
'collapse-close': index === 0 && !sidebarOpen,
}">
<input type="checkbox" class="cursor-pointer" @click="changeOpen(index)" />
<div
class="collapse-title px-4 flex items-center h-12 cursor-pointer hover:bg-gray-100 dark:hover:bg-[#373f59]">
<Icon v-if="item?.icon?.icon" :icon="item?.icon?.icon" class="text-xl mr-2" :class="{
'text-yellow-500': item?.title === 'Favorite',
'text-blue-500': item?.title !== 'Favorite',
}" />
<img v-if="item?.icon?.image" :src="item?.icon?.image" class="w-6 h-6 rounded-full mr-3" />
<div class="text-base capitalize flex-1 text-gray-700 dark:text-gray-200">
{{ item?.title }}
</div>
<div v-if="item?.badgeContent" class="mr-6 badge badge-sm" :class="item?.badgeClass">
{{ item?.badgeContent }}
</div>
</div>
<div class="collapse-content">
<div class="menu bg-base-100 w-full">
<RouterLink v-for="(el, key) of item?.children" @click="sidebarShow = false" :key="key"
class="hover:bg-gray-100 dark:hover:bg-[#373f59] rounded cursor-pointer px-3 py-2 flex items-center"
:to="el?.to" :class="{
'!bg-primary':
$route.path === el?.to?.path && item?.title !== 'Favorite',
}">
<Icon v-if="!el?.icon?.image" icon="mdi:chevron-right" class="mr-2 ml-3" :class="{
'text-white': $route.path === el?.to?.path &&
item?.title !== 'Favorite',
}" />
<img v-if="el?.icon?.image" :src="el?.icon?.image" class="w-6 h-6 rounded-full mr-3 ml-4" />
<div class="text-base capitalize text-gray-500 dark:text-gray-300" :class="{
'text-white':
$route.path === el?.to?.path &&
item?.title !== 'Favorite',
}">
{{ item?.title === 'Favorite' ? el?.title : $t(el?.title) }}
</div>
</RouterLink>
</div>
</div>
</div>
<RouterLink :to="item?.to" v-if="item?.title && !item?.children?.length && item?.to"
@click="sidebarShow = false"
class="collapse-title px-4 flex items-center py-2 hover:bg-gray-100 dark:hover:bg-[#373f59]">
<Icon v-if="item?.icon?.icon" :icon="item?.icon?.icon" class="text-xl mr-2" :class="{
'text-yellow-500': item?.title === 'Favorite',
'text-blue-500': item?.title !== 'Favorite',
}" />
<img v-if="item?.icon?.image" :src="item?.icon?.image" class="w-6 h-6 rounded-full mr-3" />
<div class="text-base capitalize flex-1 text-gray-700 dark:text-gray-200">
{{ item?.title }}
</div>
<div v-if="item?.badgeContent" class="mr-6 badge badge-sm" :class="item?.badgeClass">
{{ item?.badgeContent }}
</div>
</RouterLink>
<div v-if="item?.heading" class="px-4 text-sm pt-3 text-gray-400 pb-2 uppercase">
{{ item?.heading }}
</div>
</div>
<div class="px-2">
<div class="px-4 text-sm pt-4 text-gray-400 pb-2 uppercase">
Sponsors
</div>
<a href="https://osmosis.zone"
class="collapse-title px-4 flex items-center py-2 rounded-lg hover:bg-gray-100 dark:hover:bg-[#373f59]">
<img src="https://ping.pub/logos/osmosis.jpg" class="w-6 h-6 rounded-full mr-3" />
<div class="text-base capitalize flex-1 text-gray-600 dark:text-gray-200">
Osmosis
</div>
</a>
<a href="https://becole.com"
class="collapse-title px-4 flex items-center py-2 rounded-lg hover:bg-gray-100 dark:hover:bg-[#373f59]">
<img src="https://becole.com/static/logo/logo_becole.png" class="w-6 h-6 rounded-full mr-3" />
<div class="text-base capitalize flex-1 text-gray-600 dark:text-gray-200">
Becole
</div>
</a>
<div class="px-4 text-sm pt-4 text-gray-400 pb-2 uppercase">Links</div>
<a href="https://twitter.com/ping_pub"
class="collapse-title px-4 flex items-center py-2 rounded-lg hover:bg-gray-100 dark:hover:bg-[#373f59]">
<Icon icon="mdi:twitter" class="text-xl mr-2" />
<div class="text-base capitalize flex-1 text-gray-600 dark:text-gray-200">
Twitter
</div>
</a>
<a v-if="showDiscord" href="https://discord.com/invite/CmjYVSr6GW"
class="collapse-title px-4 flex items-center py-2 rounded-lg hover:bg-gray-100 dark:hover:bg-[#373f59]">
<Icon icon="mdi:discord" class="text-xl mr-2" />
<div class="text-base capitalize flex-1 text-gray-600 dark:text-gray-200">
Discord
</div>
</a>
<a href="https://github.com/ping-pub/explorer/discussions"
class="collapse-title px-4 flex items-center py-2 rounded-lg hover:bg-gray-100 dark:hover:bg-[#373f59]">
<Icon icon="mdi:frequently-asked-questions" class="text-xl mr-2" />
<div class="text-base capitalize flex-1 text-gray-600 dark:text-gray-200">
FAQ
</div>
</a>
</div>
</div> </div>
<div class="xl:!ml-64 px-5 pt-4"> </div>
<!-- header --> <div
<div class="flex items-center py-3 bg-base-100 mb-4 rounded px-4 sticky top-0 z-10 shadow"> v-for="(item, index) of blockchain.computedChainMenu"
<div class="text-2xl pr-3 cursor-pointer xl:!hidden" @click="sidebarShow = true"> :key="index"
<Icon icon="mdi-menu" /> class="px-2"
</div> >
<div
<ChainProfile /> v-if="item?.title && item?.children?.length"
:tabindex="index"
<div class="flex-1 w-0"></div> class="collapse"
:class="{
<!-- <NavSearchBar />--> 'collapse-arrow': item?.children?.length > 0,
<NavBarI18n class="hidden md:!inline-block" /> 'collapse-open': index === 0 && sidebarOpen,
<NavbarThemeSwitcher class="!inline-block" /> 'collapse-close': index === 0 && !sidebarOpen,
}"
<NavBarWallet /> >
<input
type="checkbox"
class="cursor-pointer !h-10 block"
@click="changeOpen(index)"
/>
<div
class="collapse-title !py-0 px-4 flex items-center cursor-pointer hover:bg-gray-100 dark:hover:bg-[#373f59]"
>
<Icon
v-if="item?.icon?.icon"
:icon="item?.icon?.icon"
class="text-xl mr-2"
:class="{
'text-yellow-500': item?.title === 'Favorite',
'text-blue-500': item?.title !== 'Favorite',
}"
/>
<img
v-if="item?.icon?.image"
:src="item?.icon?.image"
class="w-6 h-6 rounded-full mr-3"
/>
<div
class="text-base capitalize flex-1 text-gray-700 dark:text-gray-200 whitespace-nowrap"
>
{{ item?.title }}
</div> </div>
<div
<!-- 👉 Pages --> v-if="item?.badgeContent"
<RouterView v-slot="{ Component }"> class="mr-6 badge badge-sm text-white"
<Transition mode="out-in"> :class="item?.badgeClass"
<Component :is="Component" /> >
</Transition> {{ item?.badgeContent }}
</RouterView> </div>
</div>
<newFooter /> <div class="collapse-content">
<div class="menu bg-base-100 w-full">
<RouterLink
v-for="(el, key) of item?.children"
@click="sidebarShow = false"
:key="key"
class="hover:bg-gray-100 dark:hover:bg-[#373f59] rounded cursor-pointer px-3 py-2 flex items-center"
:to="el?.to"
:class="{
'!bg-primary':
$route.path === el?.to?.path && item?.title !== 'Favorite',
}"
>
<Icon
v-if="!el?.icon?.image"
icon="mdi:chevron-right"
class="mr-2 ml-3"
:class="{
'text-white':
$route.path === el?.to?.path &&
item?.title !== 'Favorite',
}"
/>
<img
v-if="el?.icon?.image"
:src="el?.icon?.image"
class="w-6 h-6 rounded-full mr-3 ml-4"
/>
<div
class="text-base capitalize text-gray-500 dark:text-gray-300"
:class="{
'text-white':
$route.path === el?.to?.path &&
item?.title !== 'Favorite',
}"
>
{{ item?.title === 'Favorite' ? el?.title : $t(el?.title) }}
</div>
</RouterLink>
</div>
</div>
</div> </div>
<RouterLink
:to="item?.to"
v-if="item?.title && !item?.children?.length && item?.to"
@click="sidebarShow = false"
class=" cursor-pointer rounded-lg px-4 flex items-center py-2 hover:bg-gray-100 dark:hover:bg-[#373f59]"
>
<Icon
v-if="item?.icon?.icon"
:icon="item?.icon?.icon"
class="text-xl mr-2"
:class="{
'text-yellow-500': item?.title === 'Favorite',
'text-blue-500': item?.title !== 'Favorite',
}"
/>
<img
v-if="item?.icon?.image"
:src="item?.icon?.image"
class="w-6 h-6 rounded-full mr-3"
/>
<div
class="text-base capitalize flex-1 text-gray-700 dark:text-gray-200 whitespace-nowrap"
>
{{ item?.title }}
</div>
<div
v-if="item?.badgeContent"
class="badge badge-sm text-white"
:class="item?.badgeClass"
>
{{ item?.badgeContent }}
</div>
</RouterLink>
<div
v-if="item?.heading"
class="px-4 text-sm text-gray-400 pb-2 uppercase"
>
{{ item?.heading }}
</div>
</div>
<div class="px-2">
<div class="px-4 text-sm pt-2 text-gray-400 pb-2 uppercase">
Sponsors
</div>
<a
href="https://osmosis.zone"
target="_blank"
class="py-2 px-4 flex items-center cursor-pointer rounded-lg hover:bg-gray-100 dark:hover:bg-[#373f59]"
>
<img
src="https://ping.pub/logos/osmosis.jpg"
class="w-6 h-6 rounded-full mr-3"
/>
<div
class="text-sm capitalize flex-1 text-gray-600 dark:text-gray-200"
>
Osmosis
</div>
</a>
<a
href="https://becole.com"
target="_blank"
class="py-2 px-4 flex items-center cursor-pointer rounded-lg hover:bg-gray-100 dark:hover:bg-[#373f59]"
>
<img
src="https://becole.com/static/logo/logo_becole.png"
class="w-6 h-6 rounded-full mr-3"
/>
<div
class="text-sm capitalize flex-1 text-gray-600 dark:text-gray-200"
>
Becole
</div>
</a>
<div class="px-4 text-sm pt-2 text-gray-400 pb-2 uppercase">Links</div>
<a
href="https://twitter.com/ping_pub"
target="_blank"
class="py-2 px-4 flex items-center cursor-pointer rounded-lg hover:bg-gray-100 dark:hover:bg-[#373f59]"
>
<Icon icon="mdi:twitter" class="text-xl mr-2" />
<div
class="text-base capitalize flex-1 text-gray-600 dark:text-gray-200"
>
Twitter
</div>
</a>
<a
v-if="showDiscord"
href="https://discord.com/invite/CmjYVSr6GW"
target="_blank"
class="py-2 px-4 flex items-center rounded-lg cursor-pointer hover:bg-gray-100 dark:hover:bg-[#373f59]"
>
<Icon icon="mdi:discord" class="text-xl mr-2" />
<div
class="text-base capitalize flex-1 text-gray-600 dark:text-gray-200"
>
Discord
</div>
</a>
<a
href="https://github.com/ping-pub/explorer/discussions"
target="_blank"
class="py-2 px-4 flex items-center rounded-lg cursor-pointer hover:bg-gray-100 dark:hover:bg-[#373f59]"
>
<Icon icon="mdi:frequently-asked-questions" class="text-xl mr-2" />
<div
class="text-base capitalize flex-1 text-gray-600 dark:text-gray-200"
>
FAQ
</div>
</a>
</div>
</div> </div>
<div class="xl:!ml-64 px-5 pt-4">
<!-- header -->
<div
class="flex items-center py-3 bg-base-100 mb-4 rounded px-4 sticky top-0 z-10 shadow"
>
<div
class="text-2xl pr-3 cursor-pointer xl:!hidden"
@click="sidebarShow = true"
>
<Icon icon="mdi-menu" />
</div>
<ChainProfile />
<div class="flex-1 w-0"></div>
<!-- <NavSearchBar />-->
<NavBarI18n class="hidden md:!inline-block" />
<NavbarThemeSwitcher class="!inline-block" />
<NavBarWallet />
</div>
<!-- 👉 Pages -->
<RouterView v-slot="{ Component }">
<Transition mode="out-in">
<Component :is="Component" />
</Transition>
</RouterView>
<newFooter />
</div>
</div>
</template> </template>

View File

@ -2,56 +2,61 @@
import { ref, watch } from 'vue'; import { ref, watch } from 'vue';
import { Icon } from '@iconify/vue'; import { Icon } from '@iconify/vue';
const i18nLangs: Array<{ label: string; i18nLang: string }> = [ const i18nLangs: Array<{ label: string; i18nLang: string }> = [
{ {
label: 'English', label: 'English',
i18nLang: 'en', i18nLang: 'en',
}, },
{ {
label: '中文', label: '中文',
i18nLang: 'cn', i18nLang: 'cn',
}, },
]; ];
let locale = ref(useI18n({ useScope: 'global' }).locale); let locale = ref(useI18n({ useScope: 'global' }).locale);
watch(locale, (val: string) => { watch(locale, (val: string) => {
document.documentElement.setAttribute('lang', val as string); document.documentElement.setAttribute('lang', val as string);
}); });
let currentLang = ref(localStorage.getItem('lang') || 'en'); let currentLang = ref(localStorage.getItem('lang') || 'en');
watch(currentLang, (val: string) => { watch(currentLang, (val: string) => {
document.documentElement.setAttribute('lang', val as string); document.documentElement.setAttribute('lang', val as string);
}); });
const handleLangChange = (lang: string) => { const handleLangChange = (lang: string) => {
locale.value = lang; locale.value = lang;
currentLang.value = lang; currentLang.value = lang;
localStorage.setItem('lang', lang); localStorage.setItem('lang', lang);
}; };
</script> </script>
<template> <template>
<div <div
class="dropdown" class="dropdown"
:class=" :class="
currentLang === 'ar' ? 'dropdown-right' : 'dropdown-bottom dropdown-end' currentLang === 'ar'
" ? 'dropdown-right'
> : 'dropdown-bottom dropdown-end'
<label tabindex="0" class="btn btn-ghost btn-circle btn-sm mx-1"> "
<Icon icon="mdi-translate" class="text-2xl" />
</label>
<ul
tabindex="0"
class="dropdown-content menu p-2 shadow bg-base-100 rounded-box w-40"
> >
<li v-for="lang in i18nLangs" :key="lang.i18nLang"> <label tabindex="0" class="btn btn-ghost btn-circle btn-sm mx-1">
<a <Icon
class="hover:bg-gray-100 dark:hover:bg-[#373f59]" icon="mdi-translate"
:class="{ 'text-primary': currentLang === lang.i18nLang }" class="text-2xl text-gray-500 dark:text-gray-400"
@click="handleLangChange(lang.i18nLang)" />
>{{ lang.label }}</a </label>
<ul
tabindex="0"
class="dropdown-content menu p-2 shadow bg-base-100 rounded-box w-40"
> >
</li> <li v-for="lang in i18nLangs" :key="lang.i18nLang">
</ul> <a
</div> class="hover:bg-gray-100 dark:hover:bg-[#373f59]"
:class="{ 'text-primary': currentLang === lang.i18nLang }"
@click="handleLangChange(lang.i18nLang)"
>{{ lang.label }}</a
>
</li>
</ul>
</div>
</template> </template>

View File

@ -1,7 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { useBaseStore, useBlockchain, useWalletStore } from '@/stores'; import { useBaseStore, useBlockchain, useWalletStore } from '@/stores';
import { Icon } from '@iconify/vue'; import { Icon } from '@iconify/vue';
import { ref, computed } from 'vue'; import { ref, computed, } from 'vue';
const walletStore = useWalletStore(); const walletStore = useWalletStore();
const chainStore = useBlockchain(); const chainStore = useBlockchain();
const baseStore = useBaseStore(); const baseStore = useBaseStore();
@ -37,7 +38,7 @@ const tipMsg = computed(() => {
<div class="dropdown dropdown-hover dropdown-end"> <div class="dropdown dropdown-hover dropdown-end">
<label <label
tabindex="0" tabindex="0"
class="btn btn-sm m-1 lowercase hidden truncate md:!inline-flex text-xs md:!text-sm" class="btn btn-sm btn-primary m-1 lowercase hidden truncate md:!inline-flex text-xs md:!text-sm"
> >
<Icon icon="mdi:wallet" /> <Icon icon="mdi:wallet" />
<span class="ml-1 hidden md:block"> <span class="ml-1 hidden md:block">
@ -51,7 +52,7 @@ const tipMsg = computed(() => {
<label <label
v-if="!walletStore?.currentAddress" v-if="!walletStore?.currentAddress"
for="PingConnectWallet" for="PingConnectWallet"
class="btn btn-sm" class="btn btn-sm btn-primary"
><Icon icon="mdi:wallet" /><span class="ml-1 hidden md:block" ><Icon icon="mdi:wallet" /><span class="ml-1 hidden md:block"
>Connect Wallet</span >Connect Wallet</span
></label ></label
@ -108,6 +109,7 @@ const tipMsg = computed(() => {
:chain-id="baseStore.currentChainId" :chain-id="baseStore.currentChainId"
:hd-path="chainStore.defaultHDPath" :hd-path="chainStore.defaultHDPath"
@connect="walletStateChange" @connect="walletStateChange"
@keplr-config="walletStore.suggestChain()"
/> />
</Teleport> </Teleport>
</template> </template>

View File

@ -1,45 +1,48 @@
<script setup lang="ts"> <script setup lang="ts">
import { Icon } from '@iconify/vue'; import { Icon } from '@iconify/vue';
import { onMounted, ref } from 'vue'; import { onMounted, computed } from 'vue';
import { useBaseStore } from '@/stores';
const themeMap: Record<string, string> = { system: 'mdi-laptop', light: 'mdi-weather-sunny', dark: 'mdi-weather-night' } const themeMap: Record<string, string> = {
system: 'mdi-laptop',
light: 'mdi-weather-sunny',
dark: 'mdi-weather-night',
};
const baseStore = useBaseStore();
const theme = computed(() => {
return baseStore.theme;
});
const theme = ref(window.localStorage.getItem('theme') || 'dark'); const changeMode = (val?: 'dark' | 'light') => {
let value: 'dark' | 'light' = 'dark';
const changeMode = (val?: string) => { const currentValue: 'dark' | 'light' = val || theme.value;
let value = 'dark'; if (currentValue === 'dark') {
const currentValue = val || theme.value; value = 'light';
if (currentValue === 'dark') { }
value = 'light'; if (value === 'light') {
} document.documentElement.classList.add('light');
if ( document.documentElement.classList.remove('dark');
currentValue === 'system' && } else {
window.matchMedia('(prefers-color-scheme: dark)').matches document.documentElement.classList.add('dark');
) { document.documentElement.classList.remove('light');
value = 'dark'; }
} document.documentElement.setAttribute('data-theme', value);
if (value === 'light') { window.localStorage.setItem('theme', value);
document.documentElement.classList.add('light'); baseStore.theme = value;
document.documentElement.classList.remove('dark');
} else {
document.documentElement.classList.add('dark');
document.documentElement.classList.remove('light');
}
document.documentElement.setAttribute('data-theme', value);
window.localStorage.setItem('theme', value);
theme.value = value;
}; };
onMounted(() => { onMounted(() => {
changeMode(theme.value === 'light' ? 'dark' : 'light'); changeMode(theme.value === 'light' ? 'dark' : 'light');
}); });
</script> </script>
<template> <template>
<div class="tooltip tooltip-bottom delay-1000"> <div class="tooltip tooltip-bottom delay-1000">
<button class=" btn btn-ghost btn-circle btn-sm mx-1" @click="changeMode()"> <button
<Icon :icon="themeMap?.[theme]" class="text-2xl" /> class="btn btn-ghost btn-circle btn-sm mx-1"
</button> @click="changeMode()"
</div> >
<Icon :icon="themeMap?.[theme]" class="text-2xl text-gray-500 dark:text-gray-400" />
</button>
</div>
</template> </template>

View File

@ -2,9 +2,9 @@
import { useWasmStore } from '../WasmStore'; import { useWasmStore } from '../WasmStore';
import { ref } from 'vue'; import { ref } from 'vue';
import type { import type {
ContractInfo, ContractInfo,
PaginabledContractStates, PaginabledContractStates,
PaginabledContracts, PaginabledContracts,
} from '../types'; } from '../types';
import DynamicComponent from '@/components/dynamic/DynamicComponent.vue'; import DynamicComponent from '@/components/dynamic/DynamicComponent.vue';
import { useFormatter, useTxDialog } from '@/stores'; import { useFormatter, useTxDialog } from '@/stores';
@ -13,20 +13,20 @@ import { PageRequest } from '@/types';
const props = defineProps(['code_id', 'chain']); const props = defineProps(['code_id', 'chain']);
const pageRequest = ref(new PageRequest()) const pageRequest = ref(new PageRequest());
const response = ref({} as PaginabledContracts); const response = ref({} as PaginabledContracts);
const wasmStore = useWasmStore(); const wasmStore = useWasmStore();
function loadContract(pageNum: number) { function loadContract(pageNum: number) {
const pr = new PageRequest() const pr = new PageRequest();
pr.setPage(pageNum) pr.setPage(pageNum);
wasmStore.wasmClient.getWasmCodeContracts(props.code_id, pr).then((x) => { wasmStore.wasmClient.getWasmCodeContracts(props.code_id, pr).then((x) => {
response.value = x; response.value = x;
}); });
} }
loadContract(1) loadContract(1);
const dialog = useTxDialog() const dialog = useTxDialog();
const format = useFormatter(); const format = useFormatter();
const infoDialog = ref(false); const infoDialog = ref(false);
const info = ref({} as ContractInfo); const info = ref({} as ContractInfo);
@ -34,66 +34,68 @@ const state = ref({} as PaginabledContractStates);
const selected = ref(''); const selected = ref('');
function showInfo(address: string) { function showInfo(address: string) {
wasmStore.wasmClient.getWasmContracts(address).then((x) => { wasmStore.wasmClient.getWasmContracts(address).then((x) => {
info.value = x.contract_info; info.value = x.contract_info;
}); });
} }
function showState(address: string) { function showState(address: string) {
selected.value = address selected.value = address;
pageload(1) pageload(1);
} }
function pageload(p: number) { function pageload(p: number) {
pageRequest.value.setPage(p) pageRequest.value.setPage(p);
wasmStore.wasmClient.getWasmContractStates(selected.value, pageRequest.value).then((x) => { wasmStore.wasmClient
state.value = x; .getWasmContractStates(selected.value, pageRequest.value)
.then((x) => {
state.value = x;
}); });
} }
function showQuery(address: string) { function showQuery(address: string) {
selected.value = address; selected.value = address;
query.value = ''; query.value = '';
result.value = ''; result.value = '';
} }
function queryContract() { function queryContract() {
try { try {
if (selectedRadio.value === 'raw') { if (selectedRadio.value === 'raw') {
wasmStore.wasmClient wasmStore.wasmClient
.getWasmContractRawQuery(selected.value, query.value) .getWasmContractRawQuery(selected.value, query.value)
.then((x) => { .then((x) => {
result.value = JSON.stringify(x); result.value = JSON.stringify(x);
}) })
.catch((err) => { .catch((err) => {
result.value = JSON.stringify(err); result.value = JSON.stringify(err);
}); });
} else { } else {
wasmStore.wasmClient wasmStore.wasmClient
.getWasmContractSmartQuery(selected.value, query.value) .getWasmContractSmartQuery(selected.value, query.value)
.then((x) => { .then((x) => {
result.value = JSON.stringify(x); result.value = JSON.stringify(x);
}) })
.catch((err) => { .catch((err) => {
result.value = JSON.stringify(err); result.value = JSON.stringify(err);
}); });
}
} catch (err) {
result.value = JSON.stringify(err); // not works for now
} }
// TODO, show error in the result. } catch (err) {
result.value = JSON.stringify(err); // not works for now
}
// TODO, show error in the result.
} }
const radioContent = [ const radioContent = [
{ {
title: 'Raw Query', title: 'Raw Query',
desc: 'Return raw result', desc: 'Return raw result',
value: 'raw', value: 'raw',
}, },
{ {
title: 'Smart Query', title: 'Smart Query',
desc: 'Return structure result if possible', desc: 'Return structure result if possible',
value: 'smart', value: 'smart',
}, },
]; ];
const selectedRadio = ref('raw'); const selectedRadio = ref('raw');
@ -101,130 +103,193 @@ const query = ref('');
const result = ref(''); const result = ref('');
</script> </script>
<template> <template>
<div> <div>
<div class="bg-base-100 px-4 pt-3 pb-4 rounded mb-4 shadow"> <div class="bg-base-100 px-4 pt-3 pb-4 rounded mb-4 shadow">
<h2 class="card-title truncate w-full"> <h2 class="card-title truncate w-full">
Contract List of Code: {{ props.code_id }} Contract List of Code: {{ props.code_id }}
</h2> </h2>
<div class="overflow-x-auto"> <div class="overflow-x-auto">
<table class="table table-compact w-full mt-4"> <table class="table table-compact w-full mt-4">
<thead> <thead>
<tr> <tr>
<th style="position: relative; z-index: 2">Contract List</th> <th style="position: relative; z-index: 2">Contract List</th>
<th>Actions</th> <th>Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="(v, index) in response.contracts" :key="index" class="hover"> <tr
<td>{{ v }}</td> v-for="(v, index) in response.contracts"
<td> :key="index"
<label @click="showInfo(v)" for="modal-contract-detail" class="hover"
class="btn btn-primary btn-xs text-xs mr-2">contract</label> >
<td>{{ v }}</td>
<td>
<label
@click="showInfo(v)"
for="modal-contract-detail"
class="btn btn-primary btn-xs text-xs mr-2"
>contract</label
>
<label class="btn btn-primary btn-xs text-xs mr-2" for="modal-contract-states" <label
@click="showState(v)"> class="btn btn-primary btn-xs text-xs mr-2"
States for="modal-contract-states"
</label> @click="showState(v)"
<label for="modal-contract-query" class="btn btn-primary btn-xs text-xs mr-2" >
@click="showQuery(v)"> States
Query </label>
</label> <label
<label for="wasm_execute_contract" class="btn btn-primary btn-xs text-xs" for="modal-contract-query"
@click="dialog.open('wasm_execute_contract', { contract: v })"> class="btn btn-primary btn-xs text-xs mr-2"
Execute @click="showQuery(v)"
</label> >
</td> Query
</tr> </label>
</tbody> <label
</table> for="wasm_execute_contract"
<div class="flex justify-between"> class="btn btn-primary btn-xs text-xs"
<PaginationBar :limit="50" :total="response.pagination?.total" :callback="loadContract" /> @click="dialog.open('wasm_execute_contract', { contract: v })"
<label for="wasm_instantiate_contract" class="btn btn-primary my-5" >
@click="dialog.open('wasm_instantiate_contract', { codeId: props.code_id })">Instantiate Execute
Contract</label> </label>
</div> </td>
</div> </tr>
</tbody>
</table>
<div class="flex justify-between">
<PaginationBar
:limit="50"
:total="response.pagination?.total"
:callback="loadContract"
/>
<label
for="wasm_instantiate_contract"
class="btn btn-primary my-5"
@click="
dialog.open('wasm_instantiate_contract', {
codeId: props.code_id,
})
"
>Instantiate Contract</label
>
</div> </div>
</div>
</div>
<input type="checkbox" id="modal-contract-detail" class="modal-toggle" /> <input type="checkbox" id="modal-contract-detail" class="modal-toggle" />
<label for="modal-contract-detail" class="modal cursor-pointer"> <label for="modal-contract-detail" class="modal cursor-pointer">
<label class="modal-box relative p-2" for=""> <label class="modal-box relative p-2" for="">
<div> <div>
<div class="flex items-center justify-between px-3 pt-2"> <div class="flex items-center justify-between px-3 pt-2">
<div class="text-lg">Contract Detail</div> <div class="text-lg">Contract Detail</div>
<label @click="infoDialog = false" for="modal-contract-detail" <label
class="btn btn-sm btn-circle"></label> @click="infoDialog = false"
</div> for="modal-contract-detail"
<div> class="btn btn-sm btn-circle"
<DynamicComponent :value="info" /> ></label
</div> >
</div> </div>
</label> <div>
</label> <DynamicComponent :value="info" />
</div>
<input type="checkbox" id="modal-contract-states" class="modal-toggle" /> </div>
<label for="modal-contract-states" class="modal cursor-pointer"> </label>
<label class="modal-box w-11/12 max-w-5xl" for="">
<div>
<div class="flex items-center justify-between px-3 pt-2 mb-4">
<div class="text-lg">Contract States</div>
<label @click="infoDialog = false" for="modal-contract-states"
class="btn btn-sm btn-circle"></label>
</div>
<div class="overflow-auto">
<table class="table table-compact w-full text-sm">
<tr v-for="(v, index) in state.models" :key="index" class="hover">
<td class="" :data-tip="format.hexToString(v.key)">
<span class="font-bold">{{ format.hexToString(v.key) }}</span>
</td>
<td class="text-left w-3/4" :title="format.base64ToString(v.value)">
{{ format.base64ToString(v.value) }}
</td>
</tr>
</table>
<PaginationBar :limit="pageRequest.limit" :total="state.pagination?.total" :callback="pageload" />
</div>
</div>
</label>
</label>
<input type="checkbox" id="modal-contract-query" class="modal-toggle" />
<label for="modal-contract-query" class="modal cursor-pointer">
<label class="modal-box w-11/12 max-w-5xl" for="">
<div>
<div class="flex items-center justify-between px-3 pt-2 mb-4">
<div class="text-lg font-semibold">Query Contract</div>
<label @click="infoDialog = false" for="modal-contract-query"
class="btn btn-sm btn-circle"></label>
</div>
<div class="px-3">
<div>
<div class="grid grid-cols-2 gap-4 mb-4">
<div class="form-control border rounded px-4" v-for="(item, index) of radioContent"
:key="index" :class="{ 'pt-2': index === 0 }">
<label class="label cursor-pointer justify-start" @click="selectedRadio = item?.value">
<input type="radio" name="radio-10" class="radio radio-sm radio-primary mr-4"
:checked="item?.value === selectedRadio" style="border: 1px solid #d2d6dc" />
<div>
<div class="text-base font-semibold">
{{ item?.title }}
</div>
<div class="text-xs">{{ item?.desc }}</div>
</div>
</label>
</div>
</div>
<VTextarea v-model="query" label="Query String" class="my-2" />
<VTextarea v-model="result" label="Result" />
</div>
<div class="mt-4 mb-4">
<button class="btn !btn-yes !border-yes px-4 text-white" @click="queryContract()">
Query Contract
</button>
</div>
</div>
</div>
</label>
</label> </label>
</div></template>
<input type="checkbox" id="modal-contract-states" class="modal-toggle" />
<label for="modal-contract-states" class="modal cursor-pointer">
<label class="modal-box w-11/12 max-w-5xl" for="">
<div>
<div class="flex items-center justify-between px-3 pt-2 mb-4">
<div class="text-lg">Contract States</div>
<label
@click="infoDialog = false"
for="modal-contract-states"
class="btn btn-sm btn-circle"
></label
>
</div>
<div class="overflow-auto">
<table class="table table-compact w-full text-sm">
<tr v-for="(v, index) in state.models" :key="index" class="hover">
<td class="" :data-tip="format.hexToString(v.key)">
<span class="font-bold">{{ format.hexToString(v.key) }}</span>
</td>
<td
class="text-left w-3/4"
:title="format.base64ToString(v.value)"
>
{{ format.base64ToString(v.value) }}
</td>
</tr>
</table>
<PaginationBar
:limit="pageRequest.limit"
:total="state.pagination?.total"
:callback="pageload"
/>
</div>
</div>
</label>
</label>
<input type="checkbox" id="modal-contract-query" class="modal-toggle" />
<label for="modal-contract-query" class="modal cursor-pointer">
<label class="modal-box w-11/12 max-w-5xl" for="">
<div>
<div class="flex items-center justify-between px-3 pt-2 mb-4">
<div class="text-lg font-semibold">Query Contract</div>
<label
@click="infoDialog = false"
for="modal-contract-query"
class="btn btn-sm btn-circle"
></label
>
</div>
<div class="px-3">
<div>
<div class="grid grid-cols-2 gap-4 mb-4">
<div
class="form-control border rounded px-4"
v-for="(item, index) of radioContent"
:key="index"
:class="{ 'pt-2': index === 0 }"
>
<label
class="label cursor-pointer justify-start"
@click="selectedRadio = item?.value"
>
<input
type="radio"
name="radio-10"
class="radio radio-sm radio-primary mr-4"
:checked="item?.value === selectedRadio"
style="border: 1px solid #d2d6dc"
/>
<div>
<div class="text-base font-semibold">
{{ item?.title }}
</div>
<div class="text-xs">{{ item?.desc }}</div>
</div>
</label>
</div>
</div>
<VTextarea v-model="query" label="Query String" class="my-2" />
<VTextarea v-model="result" label="Result" />
</div>
<div class="mt-4 mb-4">
<button
class="btn !btn-yes !border-yes px-4 text-white"
@click="queryContract()"
>
Query Contract
</button>
</div>
</div>
</div>
</label>
</label>
</div>
</template>

View File

@ -321,19 +321,21 @@ const color = computed(() => {
</Teleport> </Teleport>
</div> </div>
<div class="bg-base-100 rounded mt-4 shadow"> <div class="bg-base-100 rounded mt-4">
<div class="px-4 pt-4 pb-2 text-lg font-semibold text-main"> <div class="px-4 pt-4 pb-2 text-lg font-semibold text-main">
Application Versions Application Versions
</div> </div>
<!-- Application Version --> <!-- Application Version -->
<ArrayObjectElement :value="paramStore.appVersion?.items" :thead="false" /> <ArrayObjectElement :value="paramStore.appVersion?.items" :thead="false" />
<div class="h-4"></div>
</div> </div>
<div v-if="!store.coingeckoId" class="bg-base-100 rounded mt-4 shadow"> <div v-if="!store.coingeckoId" class="bg-base-100 rounded mt-4">
<div class="px-4 pt-4 pb-2 text-lg font-semibold text-main"> <div class="px-4 pt-4 pb-2 text-lg font-semibold text-main">
Node Information Node Information
</div> </div>
<ArrayObjectElement :value="paramStore.nodeVersion?.items" :thead="false" /> <ArrayObjectElement :value="paramStore.nodeVersion?.items" :thead="false" />
<div class="h-4"></div>
</div> </div>
</div> </div>
</template> </template>

View File

@ -45,7 +45,7 @@ onMounted(() => {
</div> </div>
<!-- Node Information --> <!-- Node Information -->
<div class="bg-base-100 px-4 pt-3 pb-4ß rounded-sm mt-6"> <div class="bg-base-100 px-4 pt-3 pb-4 rounded-sm mt-6">
<div class="text-base mb-3 text-main">{{ store.nodeVersion?.title }}</div> <div class="text-base mb-3 text-main">{{ store.nodeVersion?.title }}</div>
<ArrayObjectElement :value="store.nodeVersion?.items" :thead="false" /> <ArrayObjectElement :value="store.nodeVersion?.items" :thead="false" />
</div> </div>

View File

@ -1,9 +1,9 @@
<script lang="ts" setup> <script lang="ts" setup>
import { import {
useBaseStore, useBaseStore,
useFormatter, useFormatter,
useStakingStore, useStakingStore,
useTxDialog, useTxDialog,
} from '@/stores'; } from '@/stores';
import { computed } from '@vue/reactivity'; import { computed } from '@vue/reactivity';
import { onMounted, ref, type DebuggerEvent } from 'vue'; import { onMounted, ref, type DebuggerEvent } from 'vue';
@ -22,319 +22,343 @@ const tab = ref('active');
const unbondList = ref([] as Validator[]); const unbondList = ref([] as Validator[]);
const base = useBaseStore(); const base = useBaseStore();
onMounted(() => { onMounted(() => {
staking.fetchInacitveValdiators().then((x) => { staking.fetchInacitveValdiators().then((x) => {
unbondList.value = x; unbondList.value = x;
}); });
}); });
async function fetchChange() { async function fetchChange() {
let page = 0; let page = 0;
let height = Number(base.latest?.block?.header?.height || 0); let height = Number(base.latest?.block?.header?.height || 0);
if (height > 14400) { if (height > 14400) {
height -= 14400; height -= 14400;
} else { } else {
height = 1; height = 1;
} }
// voting power in 24h ago // voting power in 24h ago
while (page < staking.validators.length && height > 0) { while (page < staking.validators.length && height > 0) {
await base.fetchValidatorByHeight(height, page).then((x) => { await base.fetchValidatorByHeight(height, page).then((x) => {
x.validators.forEach((v) => { x.validators.forEach((v) => {
yesterday.value[v.pub_key.key] = Number(v.voting_power); yesterday.value[v.pub_key.key] = Number(v.voting_power);
}); });
}); });
page += 100; page += 100;
} }
page = 0; page = 0;
// voting power for now // voting power for now
while (page < staking.validators.length) { while (page < staking.validators.length) {
await base.fetchLatestValidators(page).then((x) => { await base.fetchLatestValidators(page).then((x) => {
x.validators.forEach((v) => { x.validators.forEach((v) => {
latest.value[v.pub_key.key] = Number(v.voting_power); latest.value[v.pub_key.key] = Number(v.voting_power);
}); });
}); });
page += 100; page += 100;
} }
} }
const changes = computed(() => { const changes = computed(() => {
const changes = {} as Record<string, number>; const changes = {} as Record<string, number>;
Object.keys(latest.value).forEach((k) => { Object.keys(latest.value).forEach((k) => {
const l = latest.value[k] || 0; const l = latest.value[k] || 0;
const y = yesterday.value[k] || 0; const y = yesterday.value[k] || 0;
changes[k] = l - y; changes[k] = l - y;
}); });
return changes; return changes;
}); });
const change24 = (key: Key) => { const change24 = (key: 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;
return changes.value[txt]; return changes.value[txt];
}; };
const change24Text = (key?: Key) => { const change24Text = (key?: Key) => {
if (!key) return ''; if (!key) return '';
const v = change24(key); const v = change24(key);
return v && v !== 0 ? format.showChanges(v) : ''; return v && v !== 0 ? format.showChanges(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 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( promise = promise.then(
() => () =>
new Promise((resolve) => { new Promise((resolve) => {
const identity = item.description?.identity; const identity = item.description?.identity;
if (identity && !avatars.value[identity]) { if (identity && !avatars.value[identity]) {
staking.keybase(identity).then((d) => { 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( const uri = String(
'https://s3.amazonaws.com/keybase_processed_uploads/', d.them[0]?.pictures?.primary?.url
'' ).replace(
); 'https://s3.amazonaws.com/keybase_processed_uploads/',
if (uri) { ''
avatars.value[identity] = uri; );
localStorage.setItem( if (uri) {
'avatars', avatars.value[identity] = uri;
JSON.stringify(avatars.value) localStorage.setItem(
); 'avatars',
} JSON.stringify(avatars.value)
} );
resolve(); }
}); }
} else { resolve();
resolve(); });
} } else {
}) resolve();
); }
}); })
);
});
}; };
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') return url.startsWith('http')
? url ? url
: `https://s3.amazonaws.com/keybase_processed_uploads/${url}`; : `https://s3.amazonaws.com/keybase_processed_uploads/${url}`;
}; };
const rank = function (position: number) { const rank = function (position: number) {
let sum = 0; let sum = 0;
for (let i = 0; i < position; i++) { for (let i = 0; i < position; i++) {
sum += Number(staking.validators[i]?.delegator_shares); 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: case tab.value === 'active' && percent < 0.33:
return 'error'; return 'error';
case tab.value === 'active' && percent < 0.67: case tab.value === 'active' && percent < 0.67:
return 'warning'; return 'warning';
default: default:
return 'primary'; return 'primary';
} }
}; };
fetchChange(); fetchChange();
loadAvatars(); loadAvatars();
</script> </script>
<template> <template>
<div> <div>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="tabs tabs-boxed bg-transparent mb-4"> <div class="tabs tabs-boxed bg-transparent mb-4">
<a <a
class="tab text-gray-400" class="tab text-gray-400"
:class="{ 'tab-active': tab === 'active' }" :class="{ 'tab-active': tab === 'active' }"
@click="tab = 'active'" @click="tab = 'active'"
>Active</a >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 class="bg-base-100 px-4 pt-3 pb-4 rounded shadow">
<div class="overflow-x-auto">
<table class="table staking-table w-full">
<thead>
<tr>
<th scope="col" style="width: 3rem; position: relative">#</th>
<th scope="col">VALIDATOR</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" class="text-center">ACTIONS</th>
</tr>
</thead>
<tbody>
<tr
v-for="(v, i) in list"
:key="v.operator_address"
class="hover:bg-gray-100 dark:hover:bg-[#384059]"
>
<!-- 👉 rank -->
<td>
<div
class="text-xs truncate relative px-2 py-1 rounded-full w-fit"
:class="`text-${rank(i)}`"
> >
<span <a
class="inset-x-0 inset-y-0 opacity-10 absolute" class="tab text-gray-400"
:class="`bg-${rank(i)}`" :class="{ 'tab-active': tab === 'inactive' }"
></span> @click="tab = 'inactive'"
{{ i + 1 }} >Inactive</a
</div>
</td>
<!-- 👉 Validator -->
<td>
<div
class="flex items-center overflow-hidden"
style="max-width: 400px"
> >
<div </div>
class="avatar mr-4 relative w-8 h-8 rounded-full overflow-hidden"
>
<div
class="w-8 h-8 rounded-full bg-gray-400 absolute opacity-10"
></div>
<div class="w-8 h-8 rounded-full">
<img
v-if="v.description?.identity"
v-lazy="logo(v.description?.identity)"
class="object-contain"
/>
<Icon
v-else
class="text-4xl"
:icon="`mdi-help-circle-outline`"
/>
</div>
</div>
<div class="flex flex-col"> <div class="text-lg font-semibold">
<h6 class="text-sm text-primary"> {{ list.length }}/{{ staking.params.max_validators }}
<RouterLink </div>
:to="{
name: 'chain-staking-validator',
params: { validator: v.operator_address },
}"
class="font-weight-medium user-list-name"
>
{{ v.description?.moniker }}
</RouterLink>
</h6>
<span class="text-xs">{{
v.description?.website || v.description?.identity || '-'
}}</span>
</div>
</div>
</td>
<!-- 👉 Voting Power -->
<td class="text-right">
<div class="flex flex-col">
<h6 class="text-sm font-weight-medium">
{{
format.formatToken(
{
amount: parseInt(v.tokens).toString(),
denom: staking.params.bond_denom,
},
true,
'0,0'
)
}}
</h6>
<span class="text-xs">{{
format.calculatePercent(
v.delegator_shares,
staking.totalPower
)
}}</span>
</div>
</td>
<!-- 👉 24h Changes -->
<td
class="text-right text-xs"
:class="change24Color(v.consensus_pubkey)"
>
{{ change24Text(v.consensus_pubkey) }}
</td>
<!-- 👉 commission -->
<td class="text-right text-xs">
{{
format.formatCommissionRate(
v.commission?.commission_rates?.rate
)
}}
</td>
<!-- 👉 Action -->
<td class="text-center">
<div v-if="v.jailed" class="badge badge-error gap-2 text-white">
Jailed
</div>
<label
v-else
for="delegate"
class="btn btn-xs rounded bg-primary capitalize border-none"
@click="
dialog.open('delegate', {
validator_address: v.operator_address,
})
"
>Delegate</label>
</td>
</tr>
</tbody>
</table>
</div>
<div class="divider"></div>
<div class="flex flex-row">
<div
class="text-xs truncate relative py-2 px-4 rounded-md w-fit text-error mr-2"
>
<span class="inset-x-0 inset-y-0 opacity-10 absolute bg-error"></span>
Top 33%
</div> </div>
<div
class="text-xs truncate relative py-2 px-4 rounded-md w-fit text-warning" <div class="bg-base-100 px-4 pt-3 pb-4 rounded shadow">
> <div class="overflow-x-auto">
<span <table class="table staking-table w-full">
class="inset-x-0 inset-y-0 opacity-10 absolute bg-warning" <thead>
></span> <tr>
Top 67% <th
scope="col"
style="width: 3rem; position: relative"
>
#
</th>
<th scope="col">VALIDATOR</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" class="text-center">ACTIONS</th>
</tr>
</thead>
<tbody>
<tr
v-for="(v, i) in list"
:key="v.operator_address"
class="hover:bg-gray-100 dark:hover:bg-[#384059]"
>
<!-- 👉 rank -->
<td>
<div
class="text-xs truncate relative px-2 py-1 rounded-full w-fit"
:class="`text-${rank(i)}`"
>
<span
class="inset-x-0 inset-y-0 opacity-10 absolute"
:class="`bg-${rank(i)}`"
></span>
{{ i + 1 }}
</div>
</td>
<!-- 👉 Validator -->
<td>
<div
class="flex items-center overflow-hidden"
style="max-width: 400px"
>
<div
class="avatar mr-4 relative w-8 h-8 rounded-full overflow-hidden"
>
<div
class="w-8 h-8 rounded-full bg-gray-400 absolute opacity-10"
></div>
<div class="w-8 h-8 rounded-full">
<img
v-if="v.description?.identity"
v-lazy="
logo(
v.description?.identity
)
"
class="object-contain"
/>
<Icon
v-else
class="text-4xl"
:icon="`mdi-help-circle-outline`"
/>
</div>
</div>
<div class="flex flex-col">
<h6 class="text-sm text-primary">
<RouterLink
:to="{
name: 'chain-staking-validator',
params: {
validator:
v.operator_address,
},
}"
class="font-weight-medium user-list-name"
>
{{ v.description?.moniker }}
</RouterLink>
</h6>
<span class="text-xs">{{
v.description?.website ||
v.description?.identity ||
'-'
}}</span>
</div>
</div>
</td>
<!-- 👉 Voting Power -->
<td class="text-right">
<div class="flex flex-col">
<h6 class="text-sm font-weight-medium">
{{
format.formatToken(
{
amount: parseInt(
v.tokens
).toString(),
denom: staking.params
.bond_denom,
},
true,
'0,0'
)
}}
</h6>
<span class="text-xs">{{
format.calculatePercent(
v.delegator_shares,
staking.totalPower
)
}}</span>
</div>
</td>
<!-- 👉 24h Changes -->
<td
class="text-right text-xs"
:class="change24Color(v.consensus_pubkey)"
>
{{ change24Text(v.consensus_pubkey) }}
</td>
<!-- 👉 commission -->
<td class="text-right text-xs">
{{
format.formatCommissionRate(
v.commission?.commission_rates?.rate
)
}}
</td>
<!-- 👉 Action -->
<td class="text-center">
<div
v-if="v.jailed"
class="badge badge-error gap-2 text-white"
>
Jailed
</div>
<label
v-else
for="delegate"
class="btn btn-xs btn-primary rounded-sm capitalize"
@click="
dialog.open('delegate', {
validator_address:
v.operator_address,
})
"
>Delegate</label
>
</td>
</tr>
</tbody>
</table>
</div>
<div class="divider"></div>
<div class="flex flex-row">
<div
class="text-xs truncate relative py-2 px-4 rounded-md w-fit text-error mr-2"
>
<span
class="inset-x-0 inset-y-0 opacity-10 absolute bg-error"
></span>
Top 33%
</div>
<div
class="text-xs truncate relative py-2 px-4 rounded-md w-fit text-warning"
>
<span
class="inset-x-0 inset-y-0 opacity-10 absolute bg-warning"
></span>
Top 67%
</div>
</div>
</div> </div>
</div>
</div> </div>
</div>
</template> </template>
<route> <route>
@ -348,7 +372,7 @@ loadAvatars();
<style> <style>
.staking-table.table :where(th, td) { .staking-table.table :where(th, td) {
padding: 8px 5px; padding: 8px 5px;
background: transparent; background: transparent;
} }
</style> </style>

View File

@ -128,7 +128,7 @@ function changeTab(v: string) {
<input type="text" v-model="keyword" placeholder="Keywords to filter validators" <input type="text" v-model="keyword" placeholder="Keywords to filter validators"
class="input input-sm w-full flex-1 border border-gray-200 dark:border-gray-600" /> class="input input-sm w-full flex-1 border border-gray-200 dark:border-gray-600" />
</div> </div>
<div class="grid grid-cols-4 gap-x-4 mt-4" :class="tab === '2' ? '' : 'hidden'"> <div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-x-4 mt-4" :class="tab === '2' ? '' : 'hidden'">
<div v-for="({ v, signing, hex }, i) in list" :key="i"> <div v-for="({ v, signing, hex }, i) in list" :key="i">
<div class="flex items-center justify-between py-0"> <div class="flex items-center justify-between py-0">
<label class="truncate text-sm"> <label class="truncate text-sm">

View File

@ -2,15 +2,17 @@
import { CosmosRestClient } from '@/libs/client'; import { CosmosRestClient } from '@/libs/client';
import { useDashboard, useFormatter } from '@/stores'; import { useDashboard, useFormatter } from '@/stores';
import type { Coin, Delegation } from '@/types'; import type { Coin, Delegation } from '@/types';
import { fromBech32, toBase64, toBech32 } from '@cosmjs/encoding'; import { fromBech32, toBase64, toBech32, toHex } from '@cosmjs/encoding';
import { Icon } from '@iconify/vue'; import { Icon } from '@iconify/vue';
import { computed } from 'vue'; import { computed } from 'vue';
import { ref } from 'vue'; import { ref } from 'vue';
import { scanLocalKeys, type AccountEntry, scanCompatibleAccounts } from './utils'; import { scanLocalKeys, type AccountEntry, scanCompatibleAccounts, type LocalKey } from './utils';
const dashboard = useDashboard() const dashboard = useDashboard()
const format = useFormatter() const format = useFormatter()
const editable = ref(false) // to edit addresses const editable = ref(false) // to edit addresses
const sourceAddress = ref('') //
const selectedSource = ref({} as LocalKey) //
function toggleEdit() { function toggleEdit() {
editable.value = !editable.value editable.value = !editable.value
} }
@ -19,42 +21,21 @@ const conf = ref(JSON.parse(localStorage.getItem("imported-addresses") || "{}")
const balances = ref({} as Record<string, Coin[]>) const balances = ref({} as Record<string, Coin[]>)
const delegations = ref({} as Record<string, Delegation[]>) const delegations = ref({} as Record<string, Delegation[]>)
scanLocalKeys().forEach(wallet => { // load balances
const { data } = fromBech32(wallet.cosmosAddress) Object.values(conf.value).forEach(imported => {
const walletKey = toBase64(data) let promise = Promise.resolve()
let imported = conf.value[walletKey] for (let i = 0; i < imported.length; i++) {
// save the default address to local storage promise = promise.then(() => new Promise((resolve) => {
if (!imported) { // continue only if the page is living
imported = [] if (imported[i].endpoint) {
dashboard.favorite.forEach(x => { loadBalances(imported[i].endpoint || "", imported[i].address).finally(() => resolve())
const chain = dashboard.chains[x] } else {
if (chain && wallet.hdPath.indexOf(chain.coinType) === 6) { resolve()
imported.push({
chainName: chain.chainName,
logo: chain.logo,
address: toBech32(chain.bech32Prefix, data),
coinType: chain.coinType,
endpoint: chain.endpoints.rest?.at(0)?.address
})
} }
}) }))
conf.value[walletKey] = imported;
localStorage.setItem("imported-addresses", JSON.stringify(conf.value))
} }
// load balance & delegations
imported.forEach(x => {
if (x.endpoint) {
const client = CosmosRestClient.newDefault(x.endpoint)
client.getBankBalances(x.address).then(res => {
balances.value[x.address] = res.balances.filter(x => x.denom.length < 10)
})
client.getStakingDelegations(x.address).then(res => {
delegations.value[x.address] = res.delegation_responses
})
}
})
})
})
const accounts = computed(() => { const accounts = computed(() => {
let a = [] as AccountEntry[] let a = [] as AccountEntry[]
@ -69,6 +50,8 @@ const accounts = computed(() => {
denom = b.balance.denom denom = b.balance.denom
}) })
entry.delegation = { amount: String(amount), denom } entry.delegation = { amount: String(amount), denom }
} else {
entry.delegation = undefined
} }
entry.balances = balances.value[entry.address] entry.balances = balances.value[entry.address]
}) })
@ -81,43 +64,97 @@ const addresses = computed(() => {
return accounts.value.map(x => (x.address)) return accounts.value.map(x => (x.address))
}) })
const sourceOptions = computed(() => {
// scan all connected wallet
const keys = scanLocalKeys()
// all existed keys
Object.values(conf.value).forEach(x => {
const [first] = x
if (first) {
const { data } = fromBech32(first.address)
const hex = toHex(data)
if (keys.findIndex(k => toHex(fromBech32(k.cosmosAddress).data) === hex) === -1) {
keys.push({
cosmosAddress: first.address,
hdPath: `m/44/${first.coinType}/0'/0/0`
})
}
}
})
// address
if (sourceAddress.value) {
const { prefix, data } = fromBech32(sourceAddress.value)
const chain = Object.values(dashboard.chains).find(x => x.bech32Prefix === prefix)
if (chain) {
keys.push({
cosmosAddress: sourceAddress.value,
hdPath: `m/44/${chain.coinType}/0'/0/0`
})
}
}
if (!selectedSource.value.cosmosAddress && keys.length > 0) {
selectedSource.value = keys[0]
}
return keys
})
const availableAccount = computed(() => { const availableAccount = computed(() => {
return scanCompatibleAccounts().filter(x => !addresses.value.includes(x.address)) if (selectedSource.value.cosmosAddress) {
return scanCompatibleAccounts([selectedSource.value]).filter(x => !addresses.value.includes(x.address))
}
return []
}) })
function removeAddress(addr: string) { function removeAddress(addr: string) {
const newConf = {} as Record<string, AccountEntry[]> const newConf = {} as Record<string, AccountEntry[]>
Object.keys(conf.value).forEach(key => { Object.keys(conf.value).forEach(key => {
newConf[key] = conf.value[key].filter(x => x.address !== addr) const acc = conf.value[key].filter(x => x.address !== addr)
if (acc.length > 0) newConf[key] = acc
}) })
conf.value = newConf conf.value = newConf
localStorage.setItem("imported-addresses", JSON.stringify(conf.value)) localStorage.setItem("imported-addresses", JSON.stringify(conf.value))
} }
async function addAddress(acc: AccountEntry) { async function addAddress(acc: AccountEntry) {
console.log('add', acc) const { data } = fromBech32(acc.address)
const {data} = fromBech32(acc.address)
const key = toBase64(data) const key = toBase64(data)
if(conf.value[key]) { if (conf.value[key]) {
// existed
if (conf.value[key].findIndex(x => x.address === acc.address) > -1) {
return
}
conf.value[key].push(acc) conf.value[key].push(acc)
} else { } else {
conf.value[key] = [acc] conf.value[key] = [acc]
} }
if(acc.endpoint) { // also add chain to favorite
const client = CosmosRestClient.newDefault(acc.endpoint) if (!dashboard?.favoriteMap?.[acc.chainName]) {
client.getBankBalances(acc.address).then(res => { dashboard.favoriteMap[acc.chainName] = true
balances.value[acc.address] = res.balances.filter(x => x.denom.length < 10) window.localStorage.setItem(
}) 'favoriteMap',
client.getStakingDelegations(acc.address).then(res => { JSON.stringify(dashboard.favoriteMap)
delegations.value[acc.address] = res.delegation_responses );
}) }
if (acc.endpoint) {
loadBalances(acc.endpoint, acc.address)
} }
localStorage.setItem("imported-addresses", JSON.stringify(conf.value)) localStorage.setItem("imported-addresses", JSON.stringify(conf.value))
} }
async function loadBalances(endpoint: string, address: string) {
const client = CosmosRestClient.newDefault(endpoint)
await client.getBankBalances(address).then(res => {
balances.value[address] = res.balances.filter(x => x.denom.length < 10)
})
await client.getStakingDelegations(address).then(res => {
delegations.value[address] = res.delegation_responses
})
}
</script> </script>
<template> <template>
<div> <div>
@ -140,38 +177,13 @@ async function addAddress(acc: AccountEntry) {
</div> </div>
</div> </div>
<div class="mt-5 flex lg:!ml-4 lg:!mt-0"> <div class="mt-5 flex lg:!ml-4 lg:!mt-0">
<span class="hidden sm:!block">
<a href="#address-modal"
class="inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50">
<svg class="-ml-0.5 mr-1.5 h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="currentColor"
aria-hidden="true">
<path
d="M12.232 4.232a2.5 2.5 0 013.536 3.536l-1.225 1.224a.75.75 0 001.061 1.06l1.224-1.224a4 4 0 00-5.656-5.656l-3 3a4 4 0 00.225 5.865.75.75 0 00.977-1.138 2.5 2.5 0 01-.142-3.667l3-3z" />
<path
d="M11.603 7.963a.75.75 0 00-.977 1.138 2.5 2.5 0 01.142 3.667l-3 3a2.5 2.5 0 01-3.536-3.536l1.225-1.224a.75.75 0 00-1.061-1.06l-1.224 1.224a4 4 0 105.656 5.656l3-3a4 4 0 00-.225-5.865z" />
</svg>
Import
</a>
</span>
<span class="ml-3 hidden sm:!block">
<button type="button"
class="inline-flex items-center rounded-md bg-primary px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm"
@click="toggleEdit">
<svg class="-ml-0.5 mr-1.5 h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="currentColor"
aria-hidden="true">
<path
d="M2.695 14.763l-1.262 3.154a.5.5 0 00.65.65l3.155-1.262a4 4 0 001.343-.885L17.5 5.5a2.121 2.121 0 00-3-3L3.58 13.42a4 4 0 00-.885 1.343z" />
</svg>
Edit
</button>
</span>
</div> </div>
</div> </div>
<table class="table w-full">
<table class="table table-compact w-full">
<!-- head --> <!-- head -->
<thead> <thead class="rounded-none">
<tr> <tr>
<th v-if="editable"></th> <th v-if="editable"></th>
<th>Account</th> <th>Account</th>
@ -186,11 +198,11 @@ async function addAddress(acc: AccountEntry) {
<td v-if="editable"> <td v-if="editable">
<Icon icon="mdi:close-box" class="text-error" @click="removeAddress(acc.address)"></Icon> <Icon icon="mdi:close-box" class="text-error" @click="removeAddress(acc.address)"></Icon>
</td> </td>
<td> <td class="px-4">
<RouterLink :to="`/${acc.chainName}/account/${acc.address}`"> <RouterLink :to="`/${acc.chainName}/account/${acc.address}`">
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<div class="avatar"> <div class="avatar">
<div class="mask mask-squircle w-12 h-12"> <div class="mask mask-squircle w-8 h-8">
<img :src="acc.logo" :alt="acc.address" /> <img :src="acc.logo" :alt="acc.address" />
</div> </div>
</div> </div>
@ -224,7 +236,33 @@ async function addAddress(acc: AccountEntry) {
</tbody> </tbody>
<tfoot> <tfoot>
<th colspan="10"> <th colspan="10">
<RouterLink to="/wallet/keplr"> Add chain to Keplr </RouterLink> <div class="flex justify-between">
<span class="hidden sm:!block">
<button type="button"
class="inline-flex items-center rounded-md bg-primary px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm"
@click="toggleEdit">
<svg class="-ml-0.5 mr-1.5 h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="currentColor"
aria-hidden="true">
<path
d="M2.695 14.763l-1.262 3.154a.5.5 0 00.65.65l3.155-1.262a4 4 0 001.343-.885L17.5 5.5a2.121 2.121 0 00-3-3L3.58 13.42a4 4 0 00-.885 1.343z" />
</svg>
Edit
</button>
<a href="#address-modal"
class="inline-flex items-center ml-3 rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50">
<svg class="-ml-0.5 mr-1.5 h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="currentColor"
aria-hidden="true">
<path
d="M12.232 4.232a2.5 2.5 0 013.536 3.536l-1.225 1.224a.75.75 0 001.061 1.06l1.224-1.224a4 4 0 00-5.656-5.656l-3 3a4 4 0 00.225 5.865.75.75 0 00.977-1.138 2.5 2.5 0 01-.142-3.667l3-3z" />
<path
d="M11.603 7.963a.75.75 0 00-.977 1.138 2.5 2.5 0 01.142 3.667l-3 3a2.5 2.5 0 01-3.536-3.536l1.225-1.224a.75.75 0 00-1.061-1.06l-1.224 1.224a4 4 0 105.656 5.656l3-3a4 4 0 00-.225-5.865z" />
</svg>
Import
</a>
</span>
<RouterLink to="/wallet/keplr" class="btn btn-sm"> Add chain to Keplr </RouterLink>
</div>
</th> </th>
</tfoot> </tfoot>
</table> </table>
@ -232,45 +270,57 @@ async function addAddress(acc: AccountEntry) {
<!-- Put this part before </body> tag --> <!-- Put this part before </body> tag -->
<div class="modal" id="address-modal"> <div class="modal" id="address-modal">
<div class="modal-box"> <div class="modal-box">
<h3 class="font-bold text-lg">Import Accounts <h3 class="font-bold text-lg mb-2">Derive Account From Address
<div class="dropdown dropdown-hover">
<label tabindex="0" class="text-info"> </h3>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="w-4 h-4 stroke-current"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
<div>
<label class="input-group input-group-sm w-full">
<span>Connected</span>
<select v-model="selectedSource" class="select select-bordered select-sm w-3/4">
<option v-for="source in sourceOptions" :value="source">
<span class=" overflow-hidden">{{ source.cosmosAddress }}</span>
</option>
</select>
</label>
<label class="input-group input-group-sm my-2">
<span>Custom</span>
<input v-model="sourceAddress" class="input input-bordered w-full input-sm" placeholder="Input an address" />
</label> </label>
<div tabindex="0" class="card compact dropdown-content dark:bg-info-content bg-slate-300 shadow rounded-box w-64">
<div class="card-body">
<p>Only shows blockchains on your favorite list</p>
</div>
</div>
</div> </div>
</h3> <div class="py-4 max-h-72 overflow-y-auto">
<p class="py-4 max-h-60 overflow-y-auto"> <table class="table table-compact">
<table> <tr v-for="acc in availableAccount">
<tr v-for="acc in availableAccount" > <td>
<td> <div class="flex items-center space-x-2">
<div class="flex items-center space-x-2">
<div class="avatar"> <div class="avatar">
<div class="mask mask-squircle w-8 h-8"> <div class="mask mask-squircle w-8 h-8">
<img :src="acc.logo" :alt="acc.address" /> <img :src="acc.logo" :alt="acc.address" />
</div> </div>
</div> </div>
<div> <div>
<div class="font-bold capitalize">{{ acc.chainName }}</div> <div class="tooltip" :class="acc.compatiable ? 'tooltip-success' : 'tooltip-error'"
:data-tip="`Coin Type: ${acc.coinType}`">
<div class="font-bold capitalize" :class="acc.compatiable ? 'text-green-500' : 'text-red-500'">
{{ acc.chainName }}
</div>
</div>
<div class="text-xs opacity-50 hidden md:!block">{{ acc.address }}</div> <div class="text-xs opacity-50 hidden md:!block">{{ acc.address }}</div>
</div> </div>
</div> </div>
</td> </td>
<td class="text-right"> <td class="text-right">
<span class="btn !bg-yes !border-yes btn-xs text-white" @click="addAddress(acc)"> <span class="btn !bg-yes !border-yes btn-xs text-white" @click="addAddress(acc)">
<Icon icon="mdi:plus"/> <Icon icon="mdi:plus" />
</span> </span>
</td> </td>
</tr> </tr>
</table> </table>
</p> </div>
<div class="modal-action"> <div class="modal-action mt-2 mb-0">
<a href="#" class="btn">Close</a> <a href="#" class="btn btn-primary btn-sm">Close</a>
</div>
</div> </div>
</div> </div>
</div> </div>
</div></template> </template>

View File

@ -10,39 +10,42 @@ export interface AccountEntry {
endpoint?: string, endpoint?: string,
delegation?: Coin, delegation?: Coin,
balances?: Coin[], balances?: Coin[],
compatiable?: boolean,
}
export interface LocalKey {
cosmosAddress: string, hdPath: string
} }
export function scanLocalKeys() { export function scanLocalKeys() {
const connected = [] as {cosmosAddress: string, hdPath: string}[] const connected = [] as LocalKey[]
for (let i = 0; i < localStorage.length; i++) { for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i) const key = localStorage.key(i)
if (key?.startsWith("m/44")) { if (key?.startsWith("m/44")) {
const wallet = JSON.parse(localStorage.getItem(key) || "") const wallet = JSON.parse(localStorage.getItem(key) || "")
if (wallet) { if (wallet) {
connected.push(wallet) connected.push(wallet)
}
} }
} }
return connected
} }
return connected
}
export function scanCompatibleAccounts(keys: LocalKey[]) {
export function scanCompatibleAccounts() { const dashboard = useDashboard()
const dashboard = useDashboard() const available = [] as AccountEntry[]
const available = [] as AccountEntry[] keys.forEach(wallet => {
scanLocalKeys().forEach(wallet => { Object.values(dashboard.chains).forEach(chain => {
Object.values(dashboard.chains).forEach(chain => { const { data } = fromBech32(wallet.cosmosAddress)
if (wallet.hdPath.indexOf(chain.coinType) === 6) { available.push({
const { data } = fromBech32(wallet.cosmosAddress) chainName: chain.chainName,
available.push({ logo: chain.logo,
chainName: chain.chainName, address: toBech32(chain.bech32Prefix, data),
logo: chain.logo, coinType: chain.coinType,
address: toBech32(chain.bech32Prefix, data), compatiable: wallet.hdPath.indexOf(chain.coinType) > 0,
coinType: chain.coinType, endpoint: chain.endpoints.rest?.at(0)?.address
endpoint: chain.endpoints.rest?.at(0)?.address
})
}
}) })
}) })
return available })
} return available
}

View File

@ -1,4 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { Icon } from '@iconify/vue';
import { import {
useDashboard, useDashboard,
LoadingStatus, LoadingStatus,
@ -6,14 +7,11 @@ import {
} from '@/stores/useDashboard'; } from '@/stores/useDashboard';
import ChainSummary from '@/components/ChainSummary.vue'; import ChainSummary from '@/components/ChainSummary.vue';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { useBlockchain } from '@/stores';
const dashboard = useDashboard(); const dashboard = useDashboard();
dashboard.$subscribe((mutation, state) => { dashboard.$subscribe((mutation, state) => {
localStorage.setItem('favorite', JSON.stringify(state.favorite)); localStorage.setItem('favorite', JSON.stringify(state.favorite));
// TODO: cause endless loop
// dashboard.loadingPrices()
}); });
const keywords = ref(''); const keywords = ref('');
const chains = computed(() => { const chains = computed(() => {
@ -28,7 +26,7 @@ const chains = computed(() => {
</script> </script>
<template> <template>
<div class=""> <div class="">
<div class="flex items-center justify-center mb-6 mt-10"> <div class="flex items-center justify-center mb-6 mt-14">
<div class="w-8 md:!w-16 rounded-full mr-3"> <div class="w-8 md:!w-16 rounded-full mr-3">
<img src="/logo.svg" /> <img src="/logo.svg" />
</div> </div>
@ -52,17 +50,11 @@ const chains = computed(() => {
<progress class="progress progress-info w-80 h-1"></progress> <progress class="progress progress-info w-80 h-1"></progress>
</div> </div>
<VTextField <div class="flex items-center rounded-full bg-base-100 border border-gray-200 dark:border-gray-700 mt-10">
v-model="keywords" <Icon icon="mdi:magnify" class="text-2xl text-gray-400 ml-3"/>
variant="underlined" <input :placeholder="$t('index.search_placeholder')" class="px-4 h-10 bg-transparent flex-1 outline-none text-base" v-model="keywords" />
:placeholder="$t('index.search_placeholder')" <div class="px-4 text-base">{{ chains.length }}/{{ dashboard.length }}</div>
style="max-width: 300px" </div>
app
>
<template #append-inner>
{{ chains.length }}/{{ dashboard.length }}
</template>
</VTextField>
<div <div
class="grid grid-cols-2 gap-4 mt-6 md:!grid-cols-3 lg:!grid-cols-4 2xl:!grid-cols-5" class="grid grid-cols-2 gap-4 mt-6 md:!grid-cols-3 lg:!grid-cols-4 2xl:!grid-cols-5"

View File

@ -7,101 +7,111 @@ import { hashTx } from '@/libs';
import { fromBase64 } from '@cosmjs/encoding'; import { fromBase64 } from '@cosmjs/encoding';
export const useBaseStore = defineStore('baseStore', { export const useBaseStore = defineStore('baseStore', {
state: () => { state: () => {
return { return {
earlest: {} as Block, earlest: {} as Block,
latest: {} as Block, latest: {} as Block,
recents: [] as Block[], recents: [] as Block[],
}; theme: (window.localStorage.getItem('theme') || 'dark') as
}, | 'light'
getters: { | 'dark',
blocktime(): number { };
if (this.earlest && this.latest) {
if (
this.latest.block?.header?.height !==
this.earlest.block?.header?.height
) {
const diff = dayjs(this.latest.block?.header?.time).diff(
this.earlest.block?.header?.time
);
return diff;
}
}
return 6000;
}, },
blockchain() { getters: {
return useBlockchain(); blocktime(): number {
}, if (this.earlest && this.latest) {
currentChainId(): string { if (
return this.latest.block?.header.chain_id || "" this.latest.block?.header?.height !==
}, this.earlest.block?.header?.height
txsInRecents() { ) {
const txs = [] as { height: string; hash: string; tx: DecodedTxRaw }[]; const diff = dayjs(this.latest.block?.header?.time).diff(
this.recents.forEach((b) => this.earlest.block?.header?.time
b.block?.data?.txs.forEach((tx: string) => { );
if (tx) { return diff;
const raw = fromBase64(tx); }
try {
txs.push({
height: b.block.header.height,
hash: hashTx(raw),
tx: decodeTxRaw(raw),
});
} catch (e) {
console.error(e);
} }
} return 6000;
}) },
); blockchain() {
return txs; return useBlockchain();
}, },
}, currentChainId(): string {
actions: { return this.latest.block?.header.chain_id || '';
async initial() { },
this.fetchLatest(); txsInRecents() {
}, const txs = [] as {
async clearRecentBlocks() { height: string;
this.recents = []; hash: string;
}, tx: DecodedTxRaw;
async fetchLatest() { }[];
this.latest = await this.blockchain.rpc?.getBaseBlockLatest(); this.recents.forEach((b) =>
if ( b.block?.data?.txs.forEach((tx: string) => {
!this.earlest || if (tx) {
this.earlest?.block?.header?.chain_id != const raw = fromBase64(tx);
this.latest?.block?.header?.chain_id try {
) { txs.push({
//reset earlest and recents height: b.block.header.height,
this.earlest = this.latest; hash: hashTx(raw),
this.recents = []; tx: decodeTxRaw(raw),
} });
//check if the block exists in recents } catch (e) {
if ( console.error(e);
this.recents.findIndex( }
(x) => x?.block_id?.hash === this.latest?.block_id?.hash }
) === -1 })
) { );
if (this.recents.length >= 50) { return txs;
this.recents.shift(); },
}
this.recents.push(this.latest);
}
return this.latest;
}, },
actions: {
async initial() {
this.fetchLatest();
},
async clearRecentBlocks() {
this.recents = [];
},
async fetchLatest() {
this.latest = await this.blockchain.rpc?.getBaseBlockLatest();
if (
!this.earlest ||
this.earlest?.block?.header?.chain_id !=
this.latest?.block?.header?.chain_id
) {
//reset earlest and recents
this.earlest = this.latest;
this.recents = [];
}
//check if the block exists in recents
if (
this.recents.findIndex(
(x) => x?.block_id?.hash === this.latest?.block_id?.hash
) === -1
) {
if (this.recents.length >= 50) {
this.recents.shift();
}
this.recents.push(this.latest);
}
return this.latest;
},
async fetchValidatorByHeight(height?: number, offset = 0) { async fetchValidatorByHeight(height?: number, offset = 0) {
return this.blockchain.rpc.getBaseValidatorsetAt(String(height), offset); return this.blockchain.rpc.getBaseValidatorsetAt(
String(height),
offset
);
},
async fetchLatestValidators(offset = 0) {
return this.blockchain.rpc.getBaseValidatorsetLatest(offset);
},
async fetchBlock(height?: number | string) {
return this.blockchain.rpc.getBaseBlockAt(String(height));
},
async fetchAbciInfo() {
return this.blockchain.rpc.getBaseNodeInfo();
},
// async fetchNodeInfo() {
// return this.blockchain.rpc.no()
// }
}, },
async fetchLatestValidators(offset = 0) {
return this.blockchain.rpc.getBaseValidatorsetLatest(offset);
},
async fetchBlock(height?: number | string) {
return this.blockchain.rpc.getBaseBlockAt(String(height));
},
async fetchAbciInfo() {
return this.blockchain.rpc.getBaseNodeInfo();
},
// async fetchNodeInfo() {
// return this.blockchain.rpc.no()
// }
},
}); });

View File

@ -9,6 +9,7 @@ import type {
WalletConnected, WalletConnected,
} from '@/types'; } from '@/types';
import { useStakingStore } from './useStakingStore'; import { useStakingStore } from './useStakingStore';
import router from '@/router'
export const useWalletStore = defineStore('walletStore', { export const useWalletStore = defineStore('walletStore', {
state: () => { state: () => {
@ -17,7 +18,7 @@ export const useWalletStore = defineStore('walletStore', {
delegations: [] as Delegation[], delegations: [] as Delegation[],
unbonding: [] as UnbondingResponses[], unbonding: [] as UnbondingResponses[],
rewards: {} as DelegatorRewards, rewards: {} as DelegatorRewards,
walletIsConnected: {} as WalletConnected | null walletIsConnected: {} as WalletConnected
}; };
}, },
getters: { getters: {
@ -133,6 +134,10 @@ export const useWalletStore = defineStore('walletStore', {
this.walletIsConnected = value || {} this.walletIsConnected = value || {}
// JSON.parse(localStorage.getItem(key) || '{}'); // JSON.parse(localStorage.getItem(key) || '{}');
return this.walletIsConnected return this.walletIsConnected
},
suggestChain() {
// const router = useRouter()
router.push({path: '/wallet/keplr'})
} }
}, },
}); });

View File

@ -33,8 +33,4 @@ html[data-theme='dark'] {
.table th:first-child { .table th:first-child {
position: relative; position: relative;
z-index: 2; z-index: 2;
} }
.btn {
@apply rounded;
}

View File

@ -11,14 +11,7 @@ module.exports = {
main: 'var(--text-main)', main: 'var(--text-main)',
secondary: 'var(--text-secondary)', secondary: 'var(--text-secondary)',
active: 'var(--bg-active)', active: 'var(--bg-active)',
}, }
borderRadius: {
none: '0',
xs: '.125rem',
sm: '.25rem',
DEFAULT: '.5rem',
lg: '.75rem',
},
}, },
}, },
plugins: [require('daisyui')], plugins: [require('daisyui')],

View File

@ -5476,10 +5476,10 @@ pify@^3.0.0:
resolved "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz" resolved "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz"
integrity sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg== integrity sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==
ping-widget@^0.0.26: ping-widget@^0.0.30:
version "0.0.26" version "0.0.30"
resolved "https://registry.yarnpkg.com/ping-widget/-/ping-widget-0.0.26.tgz#25089b57c0ff8e22e9f67435d23d09d33399b100" resolved "https://registry.yarnpkg.com/ping-widget/-/ping-widget-0.0.30.tgz#f417cff47fb8a95e443e953bc5eb3c912801f605"
integrity sha512-XGwNCx8svozTTMPqCkj6YWYk87eMftLBwsrapRjDLNaJA5NL2ouUIkaEUwV5Llt287WGW+FR+8PiwvNkaYgTPQ== integrity sha512-bKa47dHNqUw/TaRBbjxQkb5+yXMwEq0+EiIs3b9Q5/0HlFcs19rdzH/RDQj4S3tpClOX7N5hTpAFikOInHq20g==
dependencies: dependencies:
"@cosmjs/amino" "^0.30.1" "@cosmjs/amino" "^0.30.1"
"@cosmjs/cosmwasm-stargate" "^0.30.1" "@cosmjs/cosmwasm-stargate" "^0.30.1"