Merge pull request #647 from burnt-labs/feat/blocktime

add optional basestore features, eliminate redundant block fetching
This commit is contained in:
ping 2025-07-31 18:57:25 +08:00 committed by GitHub
commit 6d85f2f441
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 274 additions and 112 deletions

4
.env.example Normal file
View File

@ -0,0 +1,4 @@
# rename to .env.local or .env.<your_environment>
VITE_REFRESH_INTERVAL=2000
VITE_FETCH_ALL_BLOCKS=true
VITE_RECENT_BLOCK_LIMIT=100

5
.gitignore vendored
View File

@ -2,4 +2,7 @@ node_modules/
**/.vscode
yarn-error.log
dist
.idea
.idea
.env*
!.env.example

149
chains/mainnet/xion.json Normal file
View File

@ -0,0 +1,149 @@
{
"chain_name": "xion",
"registry_name": "xion",
"coingecko": "xion",
"network_type": "mainnet",
"rpc": [
{
"address": "https://rpc.xion-mainnet-1.burnt.com",
"provider": "🔥BurntLabs🔥"
},
{
"address": "https://rpc-burnt.imperator.co/",
"provider": "Imperator.co"
},
{
"address": "https://xion-rpc.polkachu.com",
"provider": "Polkachu"
}
],
"api": [
{
"address": "https://api.xion-mainnet-1.burnt.com",
"provider": "🔥BurntLabs🔥"
},
{
"address": "https://lcd-burnt.imperator.co/",
"provider": "Imperator.co"
},
{
"address": "https://xion-api.polkachu.com",
"provider": "Polkachu"
}
],
"snapshot_provider": "",
"sdk_version": "0.50.13",
"coin_type": "118",
"min_tx_fee": "100",
"addr_prefix": "xion",
"theme_color": "#96b325",
"logo": "https://raw.githubusercontent.com/cosmos/chain-registry/master/xion/images/burnt-round.png",
"assets": [
{
"base": "uxion",
"symbol": "XION",
"exponent": "6",
"coingecko_id": "xion-2",
"logo": "https://raw.githubusercontent.com/cosmos/chain-registry/master/xion/images/burnt-round.png"
},
{
"base": "ibc/0471F1C4E7AFD3F07702BEF6DC365268D64570F7C1FDC98EA6098DD6DE59817B",
"symbol": "OSMO",
"exponent": "6",
"coingecko_id": "osmosis",
"logo": "https://raw.githubusercontent.com/cosmos/chain-registry/master/osmosis/images/osmo.png"
},
{
"base": "ibc/F082B65C88E4B6D5EF1DB243CDA1D331D002759E938A0F5CD3FFDC5D53B3E349",
"symbol": "USDC",
"exponent": "6",
"coingecko_id": "usd-coin",
"logo": "https://raw.githubusercontent.com/cosmos/chain-registry/master/noble/images/USDCoin.png"
},
{
"base": "ibc/CC7B293B3F08EA7DB96AFD4765BD0C7F95ABD7ECEAF21C74F3ACCBF7CEFB6591",
"symbol": "OSMO",
"exponent": "6",
"coingecko_id": "osmosis",
"logo": "https://raw.githubusercontent.com/cosmos/chain-registry/master/osmosis/images/osmo.png"
},
{
"base": "ibc/9463E39D230614B313B487836D13A392BD1731928713D4C8427A083627048DB3",
"symbol": "AXL",
"exponent": "6",
"coingecko_id": "axelar",
"logo": "https://raw.githubusercontent.com/cosmos/chain-registry/master/axelar/images/axl.png"
},
{
"base": "ibc/6490A7EAB61059BFC1CDDEB05917DD70BDF3A611654162A1A47DB930D40D8AF4",
"symbol": "axlUSDC",
"exponent": "6",
"coingecko_id": "usd-coin",
"logo": "https://raw.githubusercontent.com/cosmos/chain-registry/master/axelar/images/axlusdc.png"
},
{
"base": "ibc/0000000000000000000000000000000000000000000000000000000000000000",
"symbol": "axlUSDT",
"exponent": "6",
"coingecko_id": "tether",
"logo": "https://raw.githubusercontent.com/cosmos/chain-registry/master/axelar/images/axlusdc.png"
},
{
"base": "ibc/0000000000000000000000000000000000000000000000000000000000000000",
"symbol": "axlDAI",
"exponent": "18",
"coingecko_id": "dai",
"logo": "https://raw.githubusercontent.com/cosmos/chain-registry/master/axelar/images/axldai.png"
},
{
"base": "ibc/0000000000000000000000000000000000000000000000000000000000000000",
"symbol": "axlFRAX",
"exponent": "6",
"coingecko_id": "frax",
"logo": "https://raw.githubusercontent.com/cosmos/chain-registry/master/axelar/images/axlfrax.png"
},
{
"base": "ibc/AAD7136DD626569C3DDE7C5F764968BB2E939875EFC568AE5712B62081850814",
"symbol": "axlWETH",
"exponent": "18",
"coingecko_id": "axlweth",
"logo": "https://raw.githubusercontent.com/cosmos/chain-registry/master/axelar/images/weth.png"
},
{
"base": "ibc/056EA54C3D9B49B3C0418955A27980A91DD4F210914BFE240A1DB19E27895ECA",
"symbol": "KYVE",
"exponent": "6",
"coingecko_id": "kyve-network",
"logo": "https://raw.githubusercontent.com/cosmos/chain-registry/master/kyve/images/kyve-token.png"
},
{
"base": "ibc/DBE9697AC1044255A305A2034AD360B4152632BFBFB5785234731F60196B9645",
"symbol": "ELYS",
"exponent": "6",
"coingecko_id": "elys",
"logo": "https://raw.githubusercontent.com/cosmos/chain-registry/master/elys/images/elys.png"
},
{
"base": "ibc/E706A0C6CACB374ADC2BCF6A74FE1B260840FC822E45DCB776DEA962A57FED30",
"symbol": "axlARB",
"exponent": "18",
"coingecko_id": "arb",
"logo": "https://raw.githubusercontent.com/cosmos/chain-registry/master/_non-cosmos/arbitrum/images/arb.png"
}
],
"features": [
"dashboard",
"governance",
"staking",
"blocks",
"tx",
"uptime",
"ibc",
"supply",
"parameters",
"consensus",
"cosmwasm",
"account"
],
"keplr_features": ["ibc-go", "ibc-transfer", "no-legacy-stdTx"]
}

View File

@ -1,5 +1,5 @@
<script lang="ts" setup>
import { useDashboard } from '@/stores/useDashboard';
import { useDashboard } from '@/stores';
import { computed } from 'vue';
import { Icon } from '@iconify/vue';

View File

@ -28,8 +28,9 @@ const options = computed(() => {
},
show: false,
categories: baseStore.recents
.slice(0, 50)
.map((x) => x.block.header.height)
.concat(Array(50 - baseStore.recents.length).fill('')),
.concat(Array(Math.max(0, 50)).fill('')),
},
};
});

View File

@ -7,7 +7,7 @@ import { createPinia } from 'pinia';
import LazyLoad from 'lazy-load-vue3';
import router from './router';
import { useBaseStore } from './stores/useBaseStore';
import { useBaseStore } from '@/stores';
// Create vue app
const app = createApp(App);
@ -19,6 +19,8 @@ app.use(LazyLoad, { component: true });
// Mount vue app
app.mount('#app');
const REFRESH_INTERVAL = import.meta.env.VITE_REFRESH_INTERVAL || 6000; // 6 seconds
// fetch latest block every 6s
const blockStore = useBaseStore();
const requestCounter = ref(0);
@ -28,4 +30,4 @@ setInterval(() => {
// max allowed request
blockStore.fetchLatest().finally(() => (requestCounter.value -= 1));
}
}, 6000);
}, REFRESH_INTERVAL);

View File

@ -1,68 +1,42 @@
import { defineStore } from 'pinia';
import { decodeTxRaw, type DecodedTxRaw } from '@cosmjs/proto-signing';
import { useBlockchain } from '@/stores';
import { hashTx } from '@/libs';
import type { Block } from '@/types';
import { useBaseStore, useBlockchain } from '@/stores';
export const useBlockModule = defineStore('blockModule', {
state: () => {
return {
latest: {} as Block,
current: {} as Block,
recents: [] as Block[],
};
},
getters: {
baseStore() {
return useBaseStore();
},
blockchain() {
return useBlockchain();
},
blocktime() {
if (this.recents.length < 2) return 6000;
return 6000; // todo later
return useBaseStore().blocktime;
},
txsInRecents() {
const txs = [] as { hash: string; tx: DecodedTxRaw }[];
this.recents.forEach((x) =>
x.block?.data?.txs.forEach((tx: Uint8Array) => {
if (tx) {
try {
txs.push({
hash: hashTx(tx),
tx: decodeTxRaw(tx),
});
} catch (e) {}
}
})
);
return txs;
return useBaseStore().txsInRecents;
},
latest(){
return useBaseStore().latest;
},
earliest() {
return useBaseStore().earliest;
},
recents() {
return useBaseStore().recents;
}
},
actions: {
initial() {
this.clearRecentBlocks();
this.autoFetch();
},
async clearRecentBlocks() {
this.recents = [];
},
autoFetch() {
this.fetchLatest().then((x) => {
const timer = this.autoFetch;
this.latest = x;
// if(this.recents.length >= 50) this.recents.pop()
// this.recents.push(x)
// setTimeout(timer, 6000)
});
return this.baseStore.clearRecentBlocks()
},
async fetchLatest() {
this.latest = await this.blockchain.rpc?.getBaseBlockLatest();
if (this.recents.length >= 50) this.recents.shift();
this.recents.push(this.latest);
return this.latest;
return this.baseStore.fetchLatest()
},
async fetchBlock(height: string) {
this.current = await this.blockchain.rpc?.getBaseBlockAt(height);
return this.current;
return this.baseStore.fetchBlock(height)
},
},
});

View File

@ -1,12 +1,13 @@
<script lang="ts" setup>
import fetch from 'cross-fetch';
import { onMounted, ref, computed, onUnmounted } from 'vue';
import { useBlockchain, useFormatter, useStakingStore } from '@/stores';
import { useBlockchain, useFormatter, useStakingStore, useBaseStore } from '@/stores';
import { consensusPubkeyToHexAddress } from '@/libs';
const format = useFormatter();
const chainStore = useBlockchain();
const stakingStore = useStakingStore();
const baseStore = useBaseStore();
const rpcList = ref(chainStore.current?.endpoints?.rpc || [{ address: '', provider: '' }]);
let rpc = ref('');
const validators = ref(stakingStore.validators);
@ -31,7 +32,7 @@ onMounted(async () => {
clearTime();
timer = setInterval(() => {
update();
}, 6000);
}, Math.round(baseStore.blocktime / 2));
});
onUnmounted(() => {
clearTime();
@ -87,7 +88,7 @@ async function onChange() {
update();
timer = setInterval(() => {
update();
}, 6000);
}, Math.round(baseStore.blocktime / 2));
}
async function fetchPosition() {

View File

@ -1,7 +1,14 @@
import { useBlockchain, useCoingecko, useBaseStore, useBankStore, useFormatter, useGovStore } from '@/stores';
import { useDistributionStore } from '@/stores/useDistributionStore';
import { useMintStore } from '@/stores/useMintStore';
import { useStakingStore } from '@/stores/useStakingStore';
import {
useBlockchain,
useCoingecko,
useBaseStore,
useBankStore,
useFormatter,
useGovStore,
useDistributionStore,
useMintStore,
useStakingStore,
} from '@/stores';
import type { Coin, Tally } from '@/types';
import numeral from 'numeral';
import { defineStore } from 'pinia';
@ -157,9 +164,7 @@ export const useIndexModule = defineStore('module-index', {
title: 'Validators',
color: 'error',
icon: 'mdi-human-queue',
stats: String(
base?.latest?.block?.last_commit?.signatures.length || 0
),
stats: String(base?.latest?.block?.last_commit?.signatures.length || 0),
change: 0,
},
{
@ -256,11 +261,7 @@ export const useIndexModule = defineStore('module-index', {
* @param value - The value to set for the parameter.
* @returns The new URL with the parameter added or replaced.
*/
export function addOrReplaceUrlParam(
url: string,
param: string,
value: string
): string {
export function addOrReplaceUrlParam(url: string, param: string, value: string): string {
// Parse the URL
const urlObj = new URL(url, window.location.origin);

View File

@ -5,32 +5,37 @@ import dayjs from 'dayjs';
import type { Block } from '@/types';
import { hashTx } from '@/libs';
import { fromBase64 } from '@cosmjs/encoding';
import { useRouter } from 'vue-router';
const FETCH_ALL_BLOCKS = import.meta.env.VITE_FETCH_ALL_BLOCKS || false;
const RECENT_BLOCKS_LIMIT = import.meta.env.VITE_RECENT_BLOCK_LIMIT || 50;
export const useBaseStore = defineStore('baseStore', {
state: () => {
return {
earlest: {} as Block,
earliest: {} as Block,
latest: {} as Block,
recents: [] as Block[],
theme: (window.localStorage.getItem('theme') || 'dark') as 'light' | 'dark',
connected: true,
connected: false,
};
},
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);
const blocks = Number(this.latest.block.header.height) - Number(this.earlest.block.header.height);
return diff / blocks;
if (this.earliest && this.latest) {
if (this.latest.block?.header?.height !== this.earliest.block?.header?.height) {
const diff = dayjs(this.latest.block?.header?.time).diff(this.earliest.block?.header?.time);
const blocks = Number(this.latest.block.header.height) - Number(this.earliest.block.header.height);
return Math.round(diff / blocks);
}
}
return 6000;
return 1000; // better to start low and increase
},
blockchain() {
return useBlockchain();
},
hasRpc(): boolean {
return this.blockchain?.rpc as unknown as boolean;
},
currentChainId(): string {
return this.latest.block?.header.chain_id || '';
},
@ -63,33 +68,57 @@ export const useBaseStore = defineStore('baseStore', {
},
actions: {
async initial() {
while (!this.hasRpc) {
await new Promise((resolve) => setTimeout(resolve, 1000));
}
this.fetchLatest();
},
async clearRecentBlocks() {
this.recents = [];
},
async fetchLatest() {
if (!this.hasRpc) return this.latest;
try {
this.latest = await this.blockchain.rpc?.getBaseBlockLatest();
this.connected = true;
} catch (e) {
} catch (error) {
console.error('Error fetching latest block:', error);
this.connected = false;
}
if (!this.earlest || this.earlest?.block?.header?.chain_id != this.latest?.block?.header?.chain_id) {
//reset earlest and recents
this.earlest = this.latest;
if (!this.earliest || this.earliest?.block?.header?.chain_id != this.latest?.block?.header?.chain_id) {
//reset earliest and recents
this.earliest = 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);
const newBlocks = await this.fetchNewBlocks();
const combined = [...this.recents, ...newBlocks];
this.recents = combined.slice(-RECENT_BLOCKS_LIMIT);
}
return this.latest;
},
/**
* Fetches all blocks since the last block in recents.
* Only fetches blocks with height greater than this.recents[-1].block.header.height.
* Returns an array of new blocks to be added to recents.
*/
async fetchNewBlocks() {
if (!this.latest?.block?.header?.height) return [];
if (!FETCH_ALL_BLOCKS) return [this.latest];
const oldHeight = Number(this.recents[this.recents.length - 1]?.block?.header?.height);
const newHeight = Number(this.latest.block.header.height);
let newBlocks = [];
// Fetch all blocks between oldHeight+1 and less than newHeight
for (let h = oldHeight + 1; h < newHeight; h++) {
const block = await this.fetchBlock(h);
if (!block?.block?.header?.height) continue; // skip if block not found
newBlocks.push(block);
}
// Add the latest block
newBlocks.push(this.latest);
return newBlocks;
},
async fetchValidatorByHeight(height?: number, offset = 0) {
return this.blockchain.rpc.getBaseValidatorsetAt(String(height), offset);
},
@ -97,7 +126,15 @@ export const useBaseStore = defineStore('baseStore', {
return this.blockchain.rpc.getBaseValidatorsetLatest(offset);
},
async fetchBlock(height?: number | string) {
return this.blockchain.rpc.getBaseBlockAt(String(height));
try {
const block = await this.blockchain.rpc.getBaseBlockAt(String(height));
this.connected = true;
return block;
} catch (error) {
console.error('Error fetching latest block:', error);
this.connected = false;
}
return {} as Block;
},
async fetchAbciInfo() {
return this.blockchain.rpc.getBaseNodeInfo();

View File

@ -1,6 +1,6 @@
import { defineStore } from 'pinia';
import { get } from '../libs/http';
import type { LoadingStatus } from './useDashboard';
import { get } from '@/libs/http';
import type { LoadingStatus } from '@/stores';
export interface PriceMeta {
usd?: string;
@ -13,6 +13,8 @@ export interface PriceMeta {
const LocalStoreKey = 'currency';
export const coingeckoUrl = import.meta.env.VITE_COINGECKO_URL || 'https://api.coingecko.com';
export const useCoingecko = defineStore('coingecko', {
state: () => {
const currency = localStorage.getItem(LocalStoreKey);
@ -27,11 +29,11 @@ export const useCoingecko = defineStore('coingecko', {
actions: {
getMarketChart(days = 30, coinId = 'cosmos') {
return get(`https://api.coingecko.com/api/v3/coins/${coinId}/market_chart?vs_currency=usd&days=${days}`);
return get(`${coingeckoUrl}/api/v3/coins/${coinId}/market_chart?vs_currency=usd&days=${days}`);
},
fetchCoinPrice(ids: string[]) {
const url = `https://api.coingecko.com/api/v3/simple/price?include_24hr_change=true&vs_currencies=${[
const url = `${coingeckoUrl}/api/v3/simple/price?include_24hr_change=true&vs_currencies=${[
'usd',
this.currency,
].join(',')}&ids=${ids.join(',')}`;
@ -40,7 +42,7 @@ export const useCoingecko = defineStore('coingecko', {
});
},
getCoinInfo(coinId: string) {
return get(`https://api.coingecko.com/api/v3/coins/${coinId}`);
return get(`${coingeckoUrl}/api/v3/coins/${coinId}`);
},
setSecondaryCurrency(currency: string) {
if (currency !== 'usd') {

View File

@ -1,10 +1,9 @@
import { defineStore } from 'pinia';
import { get } from '../libs/http';
import { get } from '@/libs/http';
import type { ChainConfig, DirectoryChainConfig, Endpoint, LocalChainConfig } from '@/types/chaindata';
import { ConfigSource, NetworkType } from '@/types/chaindata';
import { useBlockchain } from './useBlockchain';
import { coingeckoUrl } from '@/stores';
function apiConverter(api: any[]) {
if (!api) return [];
@ -37,15 +36,14 @@ export function convertFromLocal(lc: LocalChainConfig): ChainConfig {
{ denom: x.base, exponent: 0 },
{ denom: x.symbol.toLowerCase(), exponent: Number(x.exponent) },
],
type_asset: 'sdk.coin'
type_asset: 'sdk.coin',
}));
}
conf.versions = {
cosmosSdk: lc.sdk_version,
};
conf.bech32Prefix = lc.addr_prefix;
conf.bech32ConsensusPrefix =
lc.consensus_prefix ?? lc.addr_prefix + 'valcons';
conf.bech32ConsensusPrefix = lc.consensus_prefix ?? lc.addr_prefix + 'valcons';
conf.chainName = lc.chain_name;
conf.coinType = lc.coin_type;
conf.prettyName = lc.registry_name || lc.chain_name;
@ -138,7 +136,6 @@ export enum LoadingStatus {
Loaded,
}
export const useDashboard = defineStore('dashboard', {
state: () => {
const favMap = JSON.parse(localStorage.getItem('favoriteMap') || '{"cosmos":true, "osmosis":true}');
@ -159,8 +156,8 @@ export const useDashboard = defineStore('dashboard', {
},
actions: {
async initial() {
//await this.loadingFromLocal();
await this.loadingFromRegistry()
await this.loadingFromLocal();
//await this.loadingFromRegistry()
},
loadingPrices() {
const coinIds = [] as string[];
@ -184,7 +181,7 @@ export const useDashboard = defineStore('dashboard', {
const currencies = ['usd, cny']; // usd,cny,eur,jpy,krw,sgd,hkd
get(
`https://api.coingecko.com/api/v3/simple/price?include_24hr_change=true&vs_currencies=${currencies.join(
`${coingeckoUrl}/api/v3/simple/price?include_24hr_change=true&vs_currencies=${currencies.join(
','
)}&ids=${coinIds.join(',')}`
).then((x) => {
@ -194,7 +191,7 @@ export const useDashboard = defineStore('dashboard', {
async loadingFromRegistry() {
if (this.status === LoadingStatus.Empty) {
this.status = LoadingStatus.Loading;
get(this.source).then((res) => {
get(this.source).then((res: { chains: DirectoryChainConfig[] }) => {
res.chains.forEach((x: DirectoryChainConfig) => {
this.chains[x.chain_name] = convertFromDirectory(x);
});
@ -232,11 +229,7 @@ export const useDashboard = defineStore('dashboard', {
const blockchain = useBlockchain();
const keys = Object.keys(this.favoriteMap);
for (let i = 0; i < keys.length; i++) {
if (
!blockchain.chainName &&
this.chains[keys[i]] &&
this.favoriteMap[keys[i]]
) {
if (!blockchain.chainName && this.chains[keys[i]] && this.favoriteMap[keys[i]]) {
blockchain.setCurrent(keys[i]);
break;
}

View File

@ -1,5 +1,5 @@
import { defineStore } from 'pinia';
import { useBlockchain } from './useBlockchain';
import { useBlockchain, useBankStore, useStakingStore, useDashboard } from '@/stores';
import numeral from 'numeral';
import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration';
@ -7,12 +7,9 @@ import relativeTime from 'dayjs/plugin/relativeTime';
import updateLocale from 'dayjs/plugin/updateLocale';
import utc from 'dayjs/plugin/utc';
import localeData from 'dayjs/plugin/localeData';
import { useStakingStore } from './useStakingStore';
import { fromBase64, fromBech32, fromHex, toHex } from '@cosmjs/encoding';
import { fromBase64, fromHex, toHex } from '@cosmjs/encoding';
import { consensusPubkeyToHexAddress, get } from '@/libs';
import { useBankStore } from './useBankStore';
import type { Coin, DenomTrace } from '@/types';
import { useDashboard } from './useDashboard';
import type { Asset } from '@/types/chaindata';
dayjs.extend(localeData);
@ -131,9 +128,7 @@ export const useFormatter = defineStore('formatter', {
// find the symbol
const symbol = this.dashboard.coingecko[token.denom]?.symbol || token.denom;
// convert denomination to symbol
const exponent =
this.dashboard.coingecko[symbol?.toLowerCase()]?.exponent ||
this.specialDenom(token.denom);
const exponent = this.dashboard.coingecko[symbol?.toLowerCase()]?.exponent || this.specialDenom(token.denom);
// caculate amount of symbol
const amount = Number(token.amount) / 10 ** exponent;
return amount;