diff --git a/examples/stake-tokens/components/react/all-validators.tsx b/examples/stake-tokens/components/react/all-validators.tsx index 0f0a7d2c1..2207e865b 100644 --- a/examples/stake-tokens/components/react/all-validators.tsx +++ b/examples/stake-tokens/components/react/all-validators.tsx @@ -67,8 +67,8 @@ export const Thumbnail = ({ mr={2} /> ) : ( -
- {name && name.slice(0, 1).toUpperCase()} +
+ {name && name.trim().slice(0, 1).toUpperCase()}
)} @@ -195,8 +195,8 @@ const AllValidators = ({ {validator?.description?.moniker} - {Math.floor(exponentiate(validator.tokens, -exp))} + {Math.floor( + exponentiate(validator.tokens, -exp) + ).toLocaleString()}   diff --git a/examples/stake-tokens/components/react/my-validators.tsx b/examples/stake-tokens/components/react/my-validators.tsx index 184f3e98c..d957fab88 100644 --- a/examples/stake-tokens/components/react/my-validators.tsx +++ b/examples/stake-tokens/components/react/my-validators.tsx @@ -387,9 +387,7 @@ const MyValidators = ({ {validator?.description?.moniker} - {Math.floor(exponentiate(validator.tokens, -exp))} + {Math.floor( + exponentiate(validator.tokens, -exp) + ).toLocaleString()}   @@ -687,9 +679,7 @@ const MyValidators = ({ {validator.name} diff --git a/examples/stake-tokens/components/react/staking.tsx b/examples/stake-tokens/components/react/staking.tsx index f17ca7fcf..3e35b568b 100644 --- a/examples/stake-tokens/components/react/staking.tsx +++ b/examples/stake-tokens/components/react/staking.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useState } from 'react'; import { useChain } from '@cosmos-kit/react'; -import { Box, Skeleton } from '@chakra-ui/react'; +import { Box, SkeletonText } from '@chakra-ui/react'; import { cosmos } from 'interchain'; import BigNumber from 'bignumber.js'; import { decodeCosmosSdkDecFromProto } from '@cosmjs/stargate'; @@ -16,6 +16,7 @@ import AllValidators from './all-validators'; import { getCoin } from '../../config'; import router from 'next/router'; import { ChainName } from '@cosmos-kit/core'; +import { ImageSource } from '../types'; export const exponentiate = (num: number | string, exp: number) => { return new BigNumber(num) @@ -38,6 +39,109 @@ const splitIntoChunks = (arr: any[], chunkSize: number) => { return res; }; +const convertChainName = (chainName: string) => { + if (chainName.endsWith('testnet')) { + return chainName.replace('testnet', '-testnet'); + } + + switch (chainName) { + case 'cosmoshub': + return 'cosmos'; + case 'assetmantle': + return 'asset-mantle'; + case 'cryptoorgchain': + return 'crypto-org'; + case 'dig': + return 'dig-chain'; + case 'gravitybridge': + return 'gravity-bridge'; + case 'kichain': + return 'ki-chain'; + case 'oraichain': + return 'orai-chain'; + case 'terra': + return 'terra-classic'; + default: + return chainName; + } +}; + +const isUrlValid = async (url: string) => { + const res = await fetch(url, { method: 'HEAD' }); + const contentType = res?.headers?.get('Content-Type') || ''; + return contentType.startsWith('image'); +}; + +const getCosmostationUrl = (chainName: string, validatorAddr: string) => { + const cosmostationChainName = convertChainName(chainName); + return `https://raw.githubusercontent.com/cosmostation/chainlist/main/chain/${cosmostationChainName}/moniker/${validatorAddr}.png`; +}; + +const addImageSource = async ( + validator: Validator, + chainName: string +): Promise => { + const url = getCosmostationUrl(chainName, validator.operatorAddress); + const isValid = await isUrlValid(url); + return { ...validator, imageSource: isValid ? 'cosmostation' : 'keybase' }; +}; + +const getKeybaseUrl = (identity: string) => { + return `https://keybase.io/_/api/1.0/user/lookup.json?key_suffix=${identity}&fields=pictures`; +}; + +const getImgUrls = async (validators: Validator[], chainName: string) => { + const validatorsWithImgSource = await Promise.all( + validators.map((validator) => addImageSource(validator, chainName)) + ); + + // cosmostation urls + const cosmostationUrls = validatorsWithImgSource + .filter((validator) => validator.imageSource === 'cosmostation') + .map(({ operatorAddress }) => { + return { + address: operatorAddress, + url: getCosmostationUrl(chainName, operatorAddress), + }; + }); + + // keybase urls + const keybaseIdentities = validatorsWithImgSource + .filter((validator) => validator.imageSource === 'keybase') + .map((validator) => ({ + address: validator.operatorAddress, + identity: validator.description?.identity || '', + })); + + const chunkedIdentities = splitIntoChunks(keybaseIdentities, 20); + + let responses: any[] = []; + + for (const chunk of chunkedIdentities) { + const thumbnailRequests = chunk.map(({ address, identity }) => { + if (!identity) return { address, url: '' }; + + return fetch(getKeybaseUrl(identity)) + .then((response) => response.json()) + .then((res) => ({ + address, + url: res.them?.[0]?.pictures?.primary.url || '', + })); + }); + responses = [...responses, await Promise.all(thumbnailRequests)]; + await new Promise((resolve) => setTimeout(resolve, 500)); + } + + const keybaseUrls = responses.flat(); + + const allUrls = [...cosmostationUrls, ...keybaseUrls].reduce( + (prev, cur) => ({ ...prev, [cur.address]: cur.url }), + {} + ); + + return allUrls; +}; + interface StakingTokens { balance: number; rewards: Reward[]; @@ -91,7 +195,7 @@ export const StakingSection = ({ chainName }: { chainName: ChainName }) => { let rpcEndpoint = await getRpcEndpoint(); if (!rpcEndpoint) { - console.log('no rpc endpoint — using a fallback'); + console.log('no rpc endpoint — using a fallback'); rpcEndpoint = `https://rpc.cosmos.directory/${chainName}`; } @@ -163,42 +267,16 @@ export const StakingSection = ({ chainName }: { chainName: ChainName }) => { : 0; // THUMBNAILS + let thumbnails = {}; + const validatorThumbnails = localStorage.getItem( `${chainName}-validator-thumbnails` ); - let thumbnails = {}; - if (validatorThumbnails) { thumbnails = JSON.parse(validatorThumbnails); } else { - const identities = allValidators.map( - (validator) => validator.description!.identity - ); - - const chunkedIdentities = splitIntoChunks(identities, 30); - - let responses: any[] = []; - - for (const chunk of chunkedIdentities) { - const thumbnailRequests = chunk.map((identity) => { - const url = `https://keybase.io/_/api/1.0/user/lookup.json?key_suffix=${identity}&fields=pictures`; - return fetch(url).then((response) => response.json()); - }); - responses = [...responses, await Promise.all(thumbnailRequests)]; - await new Promise((resolve) => setTimeout(resolve, 1000)); - } - - const thumbnailUrls = responses - .flat() - .map((value) => value.them?.[0]?.pictures?.primary.url); - - thumbnails = thumbnailUrls.reduce( - (prev, cur, idx) => - identities[idx] && cur ? { ...prev, [identities[idx]]: cur } : prev, - {} - ); - + thumbnails = await getImgUrls(validators, chainName); localStorage.setItem( `${chainName}-validator-thumbnails`, JSON.stringify(thumbnails) @@ -233,7 +311,13 @@ export const StakingSection = ({ chainName }: { chainName: ChainName }) => { return ( - + { updateData={getData} chainName={chainName} /> - - {data.myValidators.length > 0 && ( - + {data.myValidators.length > 0 && ( { chainName={chainName} thumbnails={data.thumbnails} /> - - )} - {data.allValidators.length > 0 && ( - + )} + {data.allValidators.length > 0 && ( { chainName={chainName} thumbnails={data.thumbnails} /> - - )} + )} + ); }; diff --git a/examples/stake-tokens/components/types.tsx b/examples/stake-tokens/components/types.tsx index 913fd9910..cfc751d4c 100644 --- a/examples/stake-tokens/components/types.tsx +++ b/examples/stake-tokens/components/types.tsx @@ -99,3 +99,7 @@ export interface MyValidator { identity: string | undefined; commission: string | undefined; } + +export type ImageSource = { + imageSource: 'cosmostation' | 'keybase'; +};