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",
"coingecko": "desmos",
"api": ["https://api.mainnet.desmos.network"],
"rpc": ["https://rpc.mainnet.desmos.network:443"],
"api": ["https://api.mainnet.desmos.network", "https://desmos-api.lavenderfive.com"],
"rpc": ["https://rpc.mainnet.desmos.network:443", "https://desmos-rpc.lavenderfive.com:443"],
"sdk_version": "0.45.8",
"coin_type": "118",
"min_tx_fee": "3000",

View File

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

View File

@ -2,8 +2,8 @@
{
"chain_name": "shentu",
"coingecko": "certik",
"api": ["https://certik-api.polkachu.com", "https://chainfull.noopsbycertik.com"],
"rpc": ["https://certik-rpc.polkachu.com:443"],
"api": ["https://shentu-api.polkachu.com", "https://chainfull.noopsbycertik.com"],
"rpc": ["https://shentu-rpc.polkachu.com:443"],
"snapshot_provider": "",
"sdk_version": "0.45.9",
"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",
"numeral": "^2.0.6",
"osmojs": "^14.0.0-rc.0",
"ping-widget": "^0.0.26",
"ping-widget": "^0.0.30",
"pinia": "^2.0.28",
"postcss": "^8.4.23",
"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">
<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="{
'!bg-primary text-white': color === 'btn-primary',
'!btn-primary': color === 'btn-primary',
}" @click="gotoPage(page)">
{{ page }}
</button>

View File

@ -3,7 +3,7 @@ import {
useBlockchain,
useFormatter,
useStakingStore,
useTxDialog
useTxDialog,
} from '@/stores';
import { select } from '@/components/dynamic/index';
import type { PaginatedProposals } from '@/types';
@ -45,55 +45,94 @@ const proposalInfo = ref();
<tbody>
<tr v-for="(item, index) in proposals?.proposals" :key="index">
<td class="px-4 w-20">
<label for="proposal-detail-modal" class="text-main text-base hover:text-indigo-400 cursor-pointer"
@click="proposalInfo = item">
#{{ item?.proposal_id }}</label>
<label
for="proposal-detail-modal"
class="text-main text-base hover:text-indigo-400 cursor-pointer"
@click="proposalInfo = item"
>
#{{ item?.proposal_id }}</label
>
</td>
<td class="w-full">
<div>
<RouterLink :to="`/${chain.chainName}/gov/${item?.proposal_id}`"
class="text-main text-base mb-1 block hover:text-indigo-400 truncate">
<RouterLink
:to="`/${chain.chainName}/gov/${item?.proposal_id}`"
class="text-main text-base mb-1 block hover:text-indigo-400 truncate"
>
{{ item?.content?.title }}
</RouterLink>
<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']) }}
</div>
</div>
</td>
<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 class="w-36">
<div class="pl-4">
<div class="flex items-center" :class="statusMap?.[item?.status] === 'PASSED'
? 'text-yes'
: statusMap?.[item?.status] === 'REJECTED'
? 'text-no'
: 'text-info'
">
<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="flex items-center"
:class="
statusMap?.[item?.status] === 'PASSED'
? 'text-yes'
: statusMap?.[item?.status] ===
'REJECTED'
? 'text-no'
: 'text-info'
"
>
<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">
{{ statusMap?.[item?.status] || item?.status }}
{{
statusMap?.[item?.status] ||
item?.status
}}
</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') }}
</div>
</div>
</td>
<td v-if="statusMap?.[item?.status] === 'VOTING'" class="w-40">
<td
v-if="statusMap?.[item?.status] === 'VOTING'"
class="w-40"
>
<div class="">
<label for="vote" class="btn btn-xs btn-primary rounded"
@click="dialog.open('vote', { proposal_id: item?.proposal_id })">
<span v-if="item?.voterStatus">{{ item?.voterStatus.replace("VOTE_OPTION_", "") }}</span>
<label
for="vote"
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>
</label>
</div>
@ -103,56 +142,99 @@ const proposalInfo = ref();
</table>
<div class="lg:!hidden">
<div v-for="(item, index) in proposals?.proposals" :key="index" class="px-4 py-4">
<div 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
v-for="(item, index) in proposals?.proposals"
:key="index"
class="px-4 py-4"
>
<div
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 class="grid grid-cols-4 mt-2 mb-2">
<div class="col-span-2">
<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']) }}
</div>
</div>
<div class="flex items-center" :class="statusMap?.[item?.status] === 'PASSED'
? 'text-yes'
: statusMap?.[item?.status] === 'REJECTED'
? 'text-no'
: 'text-info'
">
<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="flex items-center"
:class="
statusMap?.[item?.status] === 'PASSED'
? 'text-yes'
: statusMap?.[item?.status] === 'REJECTED'
? 'text-no'
: 'text-info'
"
>
<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">
{{ statusMap?.[item?.status] || item?.status }}
</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') }}
</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 class="mt-4" v-if="statusMap?.[item?.status] === 'VOTING'">
<div class="" v-show="item?.voterStatus === 'No With Veto'">
<label for="vote" class="btn btn-xs btn-primary rounded"
@click="dialog.open('vote', { proposal_id: item?.proposal_id })">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>
<label
for="vote"
class="btn btn-xs btn-primary rounded-sm"
@click="
dialog.open('vote', {
proposal_id: item?.proposal_id,
})
"
>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 }}
</div>
</div>
@ -160,15 +242,30 @@ const proposalInfo = ref();
</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 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>
<p class="py-4">
<Component v-if="proposalInfo?.content?.description"
:is="select(proposalInfo?.content?.description, 'horizontal')"
:value="proposalInfo?.content?.description">
<Component
v-if="proposalInfo?.content?.description"
:is="
select(
proposalInfo?.content?.description,
'horizontal'
)
"
:value="proposalInfo?.content?.description"
>
</Component>
</p>
</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>
import ApexCharts from 'vue3-apexcharts';
import { computed } from 'vue';
import { useBaseStore } from '@/stores';
import { getDonutChartConfig } from './apexChartConfig';
const props = defineProps(['series', 'labels']);
const expenseRationChartConfig = computed(() =>
getDonutChartConfig(window.localStorage.getItem('theme') || 'dark', props.labels)
);
const baseStore = useBaseStore();
const expenseRationChartConfig = computed(() => {
const theme = baseStore.theme;
getDonutChartConfig(theme, props.labels);
});
</script>
<template>
<ApexCharts type="donut" height="410" :options="expenseRationChartConfig" :series="series" />
<ApexCharts
type="donut"
height="410"
:options="expenseRationChartConfig"
:series="series"
/>
</template>
<script lang="ts">
export default {
name: 'DonetChart',
name: 'DonetChart',
};
</script>

View File

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

View File

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

View File

@ -4,11 +4,16 @@ import { select } from './index';
const props = defineProps(['value']);
</script>
<template>
<div class="overflow-x-auto">
<div class="overflow-auto">
<table class="table table-compact w-full text-sm">
<tbody>
<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>
<div
class="overflow-hidden w-auto whitespace-normal"

View File

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

View File

@ -19,171 +19,281 @@ const blockchain = useBlockchain();
const current = ref('');
blockchain.$subscribe((m, s) => {
if (current.value != s.chainName) {
current.value = s.chainName;
blockchain.initial();
}
if (current.value != s.chainName) {
current.value = s.chainName;
blockchain.initial();
}
});
const sidebarShow = ref(false);
const sidebarOpen = ref(true);
const changeOpen = (index: Number) => {
if (index === 0) {
sidebarOpen.value = !sidebarOpen.value;
}
if (index === 0) {
sidebarOpen.value = !sidebarOpen.value;
}
};
const showDiscord = window.location.host.search('ping.pub') > -1;
</script>
<template>
<div class="bg-gray-100 dark:bg-[#171d30]">
<!-- 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"
:class="{ block: sidebarShow, 'hidden xl:!block': !sidebarShow }">
<div class="flex items-center pl-4 py-4 mb-1">
<img class="w-10 h-10" src="../../assets/logo.svg" />
<h1 class="flex-1 ml-3 text-2xl font-semibold dark:text-white">
Ping.pub
</h1>
<div class="pr-4 cursor-pointer xl:!hidden" @click="sidebarShow = false">
<Icon icon="mdi-close" class="text-3xl" />
</div>
</div>
<div v-for="(item, index) of blockchain.computedChainMenu" :key="index" class="px-2">
<div v-if="item?.title && item?.children?.length" :tabindex="index" class="collapse" :class="{
'collapse-arrow': item?.children?.length > 0,
'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 class="bg-gray-100 dark:bg-[#171d30]">
<!-- 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"
:class="{ block: sidebarShow, 'hidden xl:!block': !sidebarShow }"
>
<div class="flex items-center pl-4 py-4 mb-1">
<img class="w-10 h-10" src="../../assets/logo.svg" />
<h1 class="flex-1 ml-3 text-2xl font-semibold dark:text-white">
Ping.pub
</h1>
<div
class="pr-4 cursor-pointer xl:!hidden"
@click="sidebarShow = false"
>
<Icon icon="mdi-close" class="text-3xl" />
</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>
<div
v-for="(item, index) of blockchain.computedChainMenu"
:key="index"
class="px-2"
>
<div
v-if="item?.title && item?.children?.length"
:tabindex="index"
class="collapse"
:class="{
'collapse-arrow': item?.children?.length > 0,
'collapse-open': index === 0 && sidebarOpen,
'collapse-close': index === 0 && !sidebarOpen,
}"
>
<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>
<!-- 👉 Pages -->
<RouterView v-slot="{ Component }">
<Transition mode="out-in">
<Component :is="Component" />
</Transition>
</RouterView>
<newFooter />
<div
v-if="item?.badgeContent"
class="mr-6 badge badge-sm text-white"
: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=" 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 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>

View File

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

View File

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

View File

@ -1,45 +1,48 @@
<script setup lang="ts">
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?: string) => {
let value = 'dark';
const currentValue = val || theme.value;
if (currentValue === 'dark') {
value = 'light';
}
if (
currentValue === '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);
theme.value = value;
const changeMode = (val?: 'dark' | 'light') => {
let value: 'dark' | 'light' = 'dark';
const currentValue: 'dark' | 'light' = val || theme.value;
if (currentValue === 'dark') {
value = 'light';
}
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);
baseStore.theme = value;
};
onMounted(() => {
changeMode(theme.value === 'light' ? 'dark' : 'light');
changeMode(theme.value === 'light' ? 'dark' : 'light');
});
</script>
<template>
<div class="tooltip tooltip-bottom delay-1000">
<button class=" btn btn-ghost btn-circle btn-sm mx-1" @click="changeMode()">
<Icon :icon="themeMap?.[theme]" class="text-2xl" />
</button>
</div>
<div class="tooltip tooltip-bottom delay-1000">
<button
class="btn btn-ghost btn-circle btn-sm mx-1"
@click="changeMode()"
>
<Icon :icon="themeMap?.[theme]" class="text-2xl text-gray-500 dark:text-gray-400" />
</button>
</div>
</template>

View File

@ -2,9 +2,9 @@
import { useWasmStore } from '../WasmStore';
import { ref } from 'vue';
import type {
ContractInfo,
PaginabledContractStates,
PaginabledContracts,
ContractInfo,
PaginabledContractStates,
PaginabledContracts,
} from '../types';
import DynamicComponent from '@/components/dynamic/DynamicComponent.vue';
import { useFormatter, useTxDialog } from '@/stores';
@ -13,20 +13,20 @@ import { PageRequest } from '@/types';
const props = defineProps(['code_id', 'chain']);
const pageRequest = ref(new PageRequest())
const pageRequest = ref(new PageRequest());
const response = ref({} as PaginabledContracts);
const wasmStore = useWasmStore();
function loadContract(pageNum: number) {
const pr = new PageRequest()
pr.setPage(pageNum)
wasmStore.wasmClient.getWasmCodeContracts(props.code_id, pr).then((x) => {
response.value = x;
});
const pr = new PageRequest();
pr.setPage(pageNum);
wasmStore.wasmClient.getWasmCodeContracts(props.code_id, pr).then((x) => {
response.value = x;
});
}
loadContract(1)
loadContract(1);
const dialog = useTxDialog()
const dialog = useTxDialog();
const format = useFormatter();
const infoDialog = ref(false);
const info = ref({} as ContractInfo);
@ -34,66 +34,68 @@ const state = ref({} as PaginabledContractStates);
const selected = ref('');
function showInfo(address: string) {
wasmStore.wasmClient.getWasmContracts(address).then((x) => {
info.value = x.contract_info;
});
wasmStore.wasmClient.getWasmContracts(address).then((x) => {
info.value = x.contract_info;
});
}
function showState(address: string) {
selected.value = address
pageload(1)
selected.value = address;
pageload(1);
}
function pageload(p: number) {
pageRequest.value.setPage(p)
wasmStore.wasmClient.getWasmContractStates(selected.value, pageRequest.value).then((x) => {
state.value = x;
pageRequest.value.setPage(p);
wasmStore.wasmClient
.getWasmContractStates(selected.value, pageRequest.value)
.then((x) => {
state.value = x;
});
}
function showQuery(address: string) {
selected.value = address;
query.value = '';
result.value = '';
selected.value = address;
query.value = '';
result.value = '';
}
function queryContract() {
try {
if (selectedRadio.value === 'raw') {
wasmStore.wasmClient
.getWasmContractRawQuery(selected.value, query.value)
.then((x) => {
result.value = JSON.stringify(x);
})
.catch((err) => {
result.value = JSON.stringify(err);
});
} else {
wasmStore.wasmClient
.getWasmContractSmartQuery(selected.value, query.value)
.then((x) => {
result.value = JSON.stringify(x);
})
.catch((err) => {
result.value = JSON.stringify(err);
});
}
} catch (err) {
result.value = JSON.stringify(err); // not works for now
try {
if (selectedRadio.value === 'raw') {
wasmStore.wasmClient
.getWasmContractRawQuery(selected.value, query.value)
.then((x) => {
result.value = JSON.stringify(x);
})
.catch((err) => {
result.value = JSON.stringify(err);
});
} else {
wasmStore.wasmClient
.getWasmContractSmartQuery(selected.value, query.value)
.then((x) => {
result.value = JSON.stringify(x);
})
.catch((err) => {
result.value = JSON.stringify(err);
});
}
// 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 = [
{
title: 'Raw Query',
desc: 'Return raw result',
value: 'raw',
},
{
title: 'Smart Query',
desc: 'Return structure result if possible',
value: 'smart',
},
{
title: 'Raw Query',
desc: 'Return raw result',
value: 'raw',
},
{
title: 'Smart Query',
desc: 'Return structure result if possible',
value: 'smart',
},
];
const selectedRadio = ref('raw');
@ -101,130 +103,193 @@ const query = ref('');
const result = ref('');
</script>
<template>
<div>
<div class="bg-base-100 px-4 pt-3 pb-4 rounded mb-4 shadow">
<h2 class="card-title truncate w-full">
Contract List of Code: {{ props.code_id }}
</h2>
<div class="overflow-x-auto">
<table class="table table-compact w-full mt-4">
<thead>
<tr>
<th style="position: relative; z-index: 2">Contract List</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="(v, index) in response.contracts" :key="index" class="hover">
<td>{{ v }}</td>
<td>
<label @click="showInfo(v)" for="modal-contract-detail"
class="btn btn-primary btn-xs text-xs mr-2">contract</label>
<div>
<div class="bg-base-100 px-4 pt-3 pb-4 rounded mb-4 shadow">
<h2 class="card-title truncate w-full">
Contract List of Code: {{ props.code_id }}
</h2>
<div class="overflow-x-auto">
<table class="table table-compact w-full mt-4">
<thead>
<tr>
<th style="position: relative; z-index: 2">Contract List</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr
v-for="(v, index) in response.contracts"
:key="index"
class="hover"
>
<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"
@click="showState(v)">
States
</label>
<label for="modal-contract-query" class="btn btn-primary btn-xs text-xs mr-2"
@click="showQuery(v)">
Query
</label>
<label for="wasm_execute_contract" class="btn btn-primary btn-xs text-xs"
@click="dialog.open('wasm_execute_contract', { contract: v })">
Execute
</label>
</td>
</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>
<label
class="btn btn-primary btn-xs text-xs mr-2"
for="modal-contract-states"
@click="showState(v)"
>
States
</label>
<label
for="modal-contract-query"
class="btn btn-primary btn-xs text-xs mr-2"
@click="showQuery(v)"
>
Query
</label>
<label
for="wasm_execute_contract"
class="btn btn-primary btn-xs text-xs"
@click="dialog.open('wasm_execute_contract', { contract: v })"
>
Execute
</label>
</td>
</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>
<input type="checkbox" id="modal-contract-detail" class="modal-toggle" />
<label for="modal-contract-detail" class="modal cursor-pointer">
<label class="modal-box relative p-2" for="">
<div>
<div class="flex items-center justify-between px-3 pt-2">
<div class="text-lg">Contract Detail</div>
<label @click="infoDialog = false" for="modal-contract-detail"
class="btn btn-sm btn-circle"></label>
</div>
<div>
<DynamicComponent :value="info" />
</div>
</div>
</label>
</label>
<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>
<input type="checkbox" id="modal-contract-detail" class="modal-toggle" />
<label for="modal-contract-detail" class="modal cursor-pointer">
<label class="modal-box relative p-2" for="">
<div>
<div class="flex items-center justify-between px-3 pt-2">
<div class="text-lg">Contract Detail</div>
<label
@click="infoDialog = false"
for="modal-contract-detail"
class="btn btn-sm btn-circle"
></label
>
</div>
<div>
<DynamicComponent :value="info" />
</div>
</div>
</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>
</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">
Application Versions
</div>
<!-- Application Version -->
<ArrayObjectElement :value="paramStore.appVersion?.items" :thead="false" />
<div class="h-4"></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">
Node Information
</div>
<ArrayObjectElement :value="paramStore.nodeVersion?.items" :thead="false" />
<ArrayObjectElement :value="paramStore.nodeVersion?.items" :thead="false" />
<div class="h-4"></div>
</div>
</div>
</template>

View File

@ -45,7 +45,7 @@ onMounted(() => {
</div>
<!-- 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>
<ArrayObjectElement :value="store.nodeVersion?.items" :thead="false" />
</div>

View File

@ -1,9 +1,9 @@
<script lang="ts" setup>
import {
useBaseStore,
useFormatter,
useStakingStore,
useTxDialog,
useBaseStore,
useFormatter,
useStakingStore,
useTxDialog,
} from '@/stores';
import { computed } from '@vue/reactivity';
import { onMounted, ref, type DebuggerEvent } from 'vue';
@ -22,319 +22,343 @@ const tab = ref('active');
const unbondList = ref([] as Validator[]);
const base = useBaseStore();
onMounted(() => {
staking.fetchInacitveValdiators().then((x) => {
unbondList.value = x;
});
staking.fetchInacitveValdiators().then((x) => {
unbondList.value = x;
});
});
async function fetchChange() {
let page = 0;
let page = 0;
let height = Number(base.latest?.block?.header?.height || 0);
if (height > 14400) {
height -= 14400;
} else {
height = 1;
}
// voting power in 24h ago
while (page < staking.validators.length && height > 0) {
await base.fetchValidatorByHeight(height, page).then((x) => {
x.validators.forEach((v) => {
yesterday.value[v.pub_key.key] = Number(v.voting_power);
});
});
page += 100;
}
let height = Number(base.latest?.block?.header?.height || 0);
if (height > 14400) {
height -= 14400;
} else {
height = 1;
}
// voting power in 24h ago
while (page < staking.validators.length && height > 0) {
await base.fetchValidatorByHeight(height, page).then((x) => {
x.validators.forEach((v) => {
yesterday.value[v.pub_key.key] = Number(v.voting_power);
});
});
page += 100;
}
page = 0;
// voting power for now
while (page < staking.validators.length) {
await base.fetchLatestValidators(page).then((x) => {
x.validators.forEach((v) => {
latest.value[v.pub_key.key] = Number(v.voting_power);
});
});
page += 100;
}
page = 0;
// voting power for now
while (page < staking.validators.length) {
await base.fetchLatestValidators(page).then((x) => {
x.validators.forEach((v) => {
latest.value[v.pub_key.key] = Number(v.voting_power);
});
});
page += 100;
}
}
const changes = computed(() => {
const changes = {} as Record<string, number>;
Object.keys(latest.value).forEach((k) => {
const l = latest.value[k] || 0;
const y = yesterday.value[k] || 0;
changes[k] = l - y;
});
return changes;
const changes = {} as Record<string, number>;
Object.keys(latest.value).forEach((k) => {
const l = latest.value[k] || 0;
const y = yesterday.value[k] || 0;
changes[k] = l - y;
});
return changes;
});
const change24 = (key: Key) => {
const txt = key.key;
// const n: number = latest.value[txt];
// const o: number = yesterday.value[txt];
// // console.log( txt, n, o)
// return n > 0 && o > 0 ? n - o : 0;
return changes.value[txt];
const txt = key.key;
// const n: number = latest.value[txt];
// const o: number = yesterday.value[txt];
// // console.log( txt, n, o)
// return n > 0 && o > 0 ? n - o : 0;
return changes.value[txt];
};
const change24Text = (key?: Key) => {
if (!key) return '';
const v = change24(key);
return v && v !== 0 ? format.showChanges(v) : '';
if (!key) return '';
const v = change24(key);
return v && v !== 0 ? format.showChanges(v) : '';
};
const change24Color = (key?: Key) => {
if (!key) return '';
const v = change24(key);
if (v > 0) return 'text-success';
if (v < 0) return 'text-error';
if (!key) return '';
const v = change24(key);
if (v > 0) return 'text-success';
if (v < 0) return 'text-error';
};
const list = computed(() => {
return tab.value === 'active' ? staking.validators : unbondList.value;
// return staking.validators
return tab.value === 'active' ? staking.validators : unbondList.value;
// return staking.validators
});
const loadAvatars = () => {
// fetch avatar from keybase
let promise = Promise.resolve();
staking.validators.forEach((item) => {
promise = promise.then(
() =>
new Promise((resolve) => {
const identity = item.description?.identity;
if (identity && !avatars.value[identity]) {
staking.keybase(identity).then((d) => {
if (Array.isArray(d.them) && d.them.length > 0) {
const uri = String(d.them[0]?.pictures?.primary?.url).replace(
'https://s3.amazonaws.com/keybase_processed_uploads/',
''
);
if (uri) {
avatars.value[identity] = uri;
localStorage.setItem(
'avatars',
JSON.stringify(avatars.value)
);
}
}
resolve();
});
} else {
resolve();
}
})
);
});
// fetch avatar from keybase
let promise = Promise.resolve();
staking.validators.forEach((item) => {
promise = promise.then(
() =>
new Promise((resolve) => {
const identity = item.description?.identity;
if (identity && !avatars.value[identity]) {
staking.keybase(identity).then((d) => {
if (Array.isArray(d.them) && d.them.length > 0) {
const uri = String(
d.them[0]?.pictures?.primary?.url
).replace(
'https://s3.amazonaws.com/keybase_processed_uploads/',
''
);
if (uri) {
avatars.value[identity] = uri;
localStorage.setItem(
'avatars',
JSON.stringify(avatars.value)
);
}
}
resolve();
});
} else {
resolve();
}
})
);
});
};
const logo = (identity?: string) => {
if (!identity) return '';
const url = avatars.value[identity] || '';
return url.startsWith('http')
? url
: `https://s3.amazonaws.com/keybase_processed_uploads/${url}`;
if (!identity) return '';
const url = avatars.value[identity] || '';
return url.startsWith('http')
? url
: `https://s3.amazonaws.com/keybase_processed_uploads/${url}`;
};
const rank = function (position: number) {
let sum = 0;
for (let i = 0; i < position; i++) {
sum += Number(staking.validators[i]?.delegator_shares);
}
const percent = sum / staking.totalPower;
let sum = 0;
for (let i = 0; i < position; i++) {
sum += Number(staking.validators[i]?.delegator_shares);
}
const percent = sum / staking.totalPower;
switch (true) {
case tab.value === 'active' && percent < 0.33:
return 'error';
case tab.value === 'active' && percent < 0.67:
return 'warning';
default:
return 'primary';
}
switch (true) {
case tab.value === 'active' && percent < 0.33:
return 'error';
case tab.value === 'active' && percent < 0.67:
return 'warning';
default:
return 'primary';
}
};
fetchChange();
loadAvatars();
</script>
<template>
<div>
<div class="flex items-center justify-between">
<div class="tabs tabs-boxed bg-transparent mb-4">
<a
class="tab text-gray-400"
:class="{ 'tab-active': tab === 'active' }"
@click="tab = 'active'"
>Active</a
>
<a
class="tab text-gray-400"
:class="{ 'tab-active': tab === 'inactive' }"
@click="tab = 'inactive'"
>Inactive</a
>
</div>
<div class="text-lg font-semibold">
{{ list.length }}/{{ staking.params.max_validators }}
</div>
</div>
<div 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)}`"
<div>
<div class="flex items-center justify-between">
<div class="tabs tabs-boxed bg-transparent mb-4">
<a
class="tab text-gray-400"
:class="{ 'tab-active': tab === 'active' }"
@click="tab = 'active'"
>Active</a
>
<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"
<a
class="tab text-gray-400"
:class="{ 'tab-active': tab === 'inactive' }"
@click="tab = 'inactive'"
>Inactive</a
>
<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>
<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 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 class="text-lg font-semibold">
{{ list.length }}/{{ staking.params.max_validators }}
</div>
</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 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
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>
</template>
<route>
@ -348,7 +372,7 @@ loadAvatars();
<style>
.staking-table.table :where(th, td) {
padding: 8px 5px;
background: transparent;
padding: 8px 5px;
background: transparent;
}
</style>

View File

@ -128,7 +128,7 @@ function changeTab(v: string) {
<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" />
</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 class="flex items-center justify-between py-0">
<label class="truncate text-sm">

View File

@ -2,15 +2,17 @@
import { CosmosRestClient } from '@/libs/client';
import { useDashboard, useFormatter } from '@/stores';
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 { computed } 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 format = useFormatter()
const editable = ref(false) // to edit addresses
const sourceAddress = ref('') //
const selectedSource = ref({} as LocalKey) //
function toggleEdit() {
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 delegations = ref({} as Record<string, Delegation[]>)
scanLocalKeys().forEach(wallet => {
const { data } = fromBech32(wallet.cosmosAddress)
const walletKey = toBase64(data)
let imported = conf.value[walletKey]
// save the default address to local storage
if (!imported) {
imported = []
dashboard.favorite.forEach(x => {
const chain = dashboard.chains[x]
if (chain && wallet.hdPath.indexOf(chain.coinType) === 6) {
imported.push({
chainName: chain.chainName,
logo: chain.logo,
address: toBech32(chain.bech32Prefix, data),
coinType: chain.coinType,
endpoint: chain.endpoints.rest?.at(0)?.address
})
// load balances
Object.values(conf.value).forEach(imported => {
let promise = Promise.resolve()
for (let i = 0; i < imported.length; i++) {
promise = promise.then(() => new Promise((resolve) => {
// continue only if the page is living
if (imported[i].endpoint) {
loadBalances(imported[i].endpoint || "", imported[i].address).finally(() => resolve())
} else {
resolve()
}
})
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(() => {
let a = [] as AccountEntry[]
@ -69,6 +50,8 @@ const accounts = computed(() => {
denom = b.balance.denom
})
entry.delegation = { amount: String(amount), denom }
} else {
entry.delegation = undefined
}
entry.balances = balances.value[entry.address]
})
@ -81,43 +64,97 @@ const addresses = computed(() => {
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(() => {
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) {
const newConf = {} as Record<string, AccountEntry[]>
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
localStorage.setItem("imported-addresses", JSON.stringify(conf.value))
}
async function addAddress(acc: AccountEntry) {
console.log('add', acc)
const {data} = fromBech32(acc.address)
const { data } = fromBech32(acc.address)
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)
} else {
conf.value[key] = [acc]
}
if(acc.endpoint) {
const client = CosmosRestClient.newDefault(acc.endpoint)
client.getBankBalances(acc.address).then(res => {
balances.value[acc.address] = res.balances.filter(x => x.denom.length < 10)
})
client.getStakingDelegations(acc.address).then(res => {
delegations.value[acc.address] = res.delegation_responses
})
// also add chain to favorite
if (!dashboard?.favoriteMap?.[acc.chainName]) {
dashboard.favoriteMap[acc.chainName] = true
window.localStorage.setItem(
'favoriteMap',
JSON.stringify(dashboard.favoriteMap)
);
}
if (acc.endpoint) {
loadBalances(acc.endpoint, acc.address)
}
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>
<template>
<div>
@ -140,38 +177,13 @@ async function addAddress(acc: AccountEntry) {
</div>
</div>
<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>
<table class="table w-full">
<table class="table table-compact w-full">
<!-- head -->
<thead>
<thead class="rounded-none">
<tr>
<th v-if="editable"></th>
<th>Account</th>
@ -186,11 +198,11 @@ async function addAddress(acc: AccountEntry) {
<td v-if="editable">
<Icon icon="mdi:close-box" class="text-error" @click="removeAddress(acc.address)"></Icon>
</td>
<td>
<td class="px-4">
<RouterLink :to="`/${acc.chainName}/account/${acc.address}`">
<div class="flex items-center space-x-2">
<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" />
</div>
</div>
@ -224,7 +236,33 @@ async function addAddress(acc: AccountEntry) {
</tbody>
<tfoot>
<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>
</tfoot>
</table>
@ -232,45 +270,57 @@ async function addAddress(acc: AccountEntry) {
<!-- Put this part before </body> tag -->
<div class="modal" id="address-modal">
<div class="modal-box">
<h3 class="font-bold text-lg">Import Accounts
<div class="dropdown dropdown-hover">
<label tabindex="0" class="text-info">
<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>
<h3 class="font-bold text-lg mb-2">Derive Account From Address
</h3>
<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>
<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>
</h3>
<p class="py-4 max-h-60 overflow-y-auto">
<table>
<tr v-for="acc in availableAccount" >
<td>
<div class="flex items-center space-x-2">
<div class="py-4 max-h-72 overflow-y-auto">
<table class="table table-compact">
<tr v-for="acc in availableAccount">
<td>
<div class="flex items-center space-x-2">
<div class="avatar">
<div class="mask mask-squircle w-8 h-8">
<img :src="acc.logo" :alt="acc.address" />
</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>
</div>
</td>
<td class="text-right">
<span class="btn !bg-yes !border-yes btn-xs text-white" @click="addAddress(acc)">
<Icon icon="mdi:plus"/>
</span>
</td>
</tr>
</table>
</p>
<div class="modal-action">
<a href="#" class="btn">Close</a>
</td>
<td class="text-right">
<span class="btn !bg-yes !border-yes btn-xs text-white" @click="addAddress(acc)">
<Icon icon="mdi:plus" />
</span>
</td>
</tr>
</table>
</div>
<div class="modal-action mt-2 mb-0">
<a href="#" class="btn btn-primary btn-sm">Close</a>
</div>
</div>
</div>
</div>
</div></template>
</template>

View File

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

View File

@ -1,4 +1,5 @@
<script lang="ts" setup>
import { Icon } from '@iconify/vue';
import {
useDashboard,
LoadingStatus,
@ -6,14 +7,11 @@ import {
} from '@/stores/useDashboard';
import ChainSummary from '@/components/ChainSummary.vue';
import { computed, ref } from 'vue';
import { useBlockchain } from '@/stores';
const dashboard = useDashboard();
dashboard.$subscribe((mutation, state) => {
localStorage.setItem('favorite', JSON.stringify(state.favorite));
// TODO: cause endless loop
// dashboard.loadingPrices()
});
const keywords = ref('');
const chains = computed(() => {
@ -28,7 +26,7 @@ const chains = computed(() => {
</script>
<template>
<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">
<img src="/logo.svg" />
</div>
@ -52,17 +50,11 @@ const chains = computed(() => {
<progress class="progress progress-info w-80 h-1"></progress>
</div>
<VTextField
v-model="keywords"
variant="underlined"
:placeholder="$t('index.search_placeholder')"
style="max-width: 300px"
app
>
<template #append-inner>
{{ chains.length }}/{{ dashboard.length }}
</template>
</VTextField>
<div class="flex items-center rounded-full bg-base-100 border border-gray-200 dark:border-gray-700 mt-10">
<Icon icon="mdi:magnify" class="text-2xl text-gray-400 ml-3"/>
<input :placeholder="$t('index.search_placeholder')" class="px-4 h-10 bg-transparent flex-1 outline-none text-base" v-model="keywords" />
<div class="px-4 text-base">{{ chains.length }}/{{ dashboard.length }}</div>
</div>
<div
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';
export const useBaseStore = defineStore('baseStore', {
state: () => {
return {
earlest: {} as Block,
latest: {} as Block,
recents: [] as Block[],
};
},
getters: {
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;
state: () => {
return {
earlest: {} as Block,
latest: {} as Block,
recents: [] as Block[],
theme: (window.localStorage.getItem('theme') || 'dark') as
| 'light'
| 'dark',
};
},
blockchain() {
return useBlockchain();
},
currentChainId(): string {
return this.latest.block?.header.chain_id || ""
},
txsInRecents() {
const txs = [] as { height: string; hash: string; tx: DecodedTxRaw }[];
this.recents.forEach((b) =>
b.block?.data?.txs.forEach((tx: string) => {
if (tx) {
const raw = fromBase64(tx);
try {
txs.push({
height: b.block.header.height,
hash: hashTx(raw),
tx: decodeTxRaw(raw),
});
} catch (e) {
console.error(e);
getters: {
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 txs;
},
},
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;
return 6000;
},
blockchain() {
return useBlockchain();
},
currentChainId(): string {
return this.latest.block?.header.chain_id || '';
},
txsInRecents() {
const txs = [] as {
height: string;
hash: string;
tx: DecodedTxRaw;
}[];
this.recents.forEach((b) =>
b.block?.data?.txs.forEach((tx: string) => {
if (tx) {
const raw = fromBase64(tx);
try {
txs.push({
height: b.block.header.height,
hash: hashTx(raw),
tx: decodeTxRaw(raw),
});
} catch (e) {
console.error(e);
}
}
})
);
return txs;
},
},
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) {
return this.blockchain.rpc.getBaseValidatorsetAt(String(height), offset);
async fetchValidatorByHeight(height?: number, offset = 0) {
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,
} from '@/types';
import { useStakingStore } from './useStakingStore';
import router from '@/router'
export const useWalletStore = defineStore('walletStore', {
state: () => {
@ -17,7 +18,7 @@ export const useWalletStore = defineStore('walletStore', {
delegations: [] as Delegation[],
unbonding: [] as UnbondingResponses[],
rewards: {} as DelegatorRewards,
walletIsConnected: {} as WalletConnected | null
walletIsConnected: {} as WalletConnected
};
},
getters: {
@ -133,6 +134,10 @@ export const useWalletStore = defineStore('walletStore', {
this.walletIsConnected = value || {}
// JSON.parse(localStorage.getItem(key) || '{}');
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 {
position: relative;
z-index: 2;
}
.btn {
@apply rounded;
}
}

View File

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

View File

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