11import { useCallback , useEffect , useState } from 'react' ;
22import { useChain } from '@cosmos-kit/react' ;
3- import { Box , Skeleton } from '@chakra-ui/react' ;
3+ import { Box , SkeletonText } from '@chakra-ui/react' ;
44import { cosmos } from 'interchain' ;
55import BigNumber from 'bignumber.js' ;
66import { decodeCosmosSdkDecFromProto } from '@cosmjs/stargate' ;
@@ -16,6 +16,7 @@ import AllValidators from './all-validators';
1616import { getCoin } from '../../config' ;
1717import router from 'next/router' ;
1818import { ChainName } from '@cosmos-kit/core' ;
19+ import { ImageSource } from '../types' ;
1920
2021export const exponentiate = ( num : number | string , exp : number ) => {
2122 return new BigNumber ( num )
@@ -38,6 +39,109 @@ const splitIntoChunks = (arr: any[], chunkSize: number) => {
3839 return res ;
3940} ;
4041
42+ const convertChainName = ( chainName : string ) => {
43+ if ( chainName . endsWith ( 'testnet' ) ) {
44+ return chainName . replace ( 'testnet' , '-testnet' ) ;
45+ }
46+
47+ switch ( chainName ) {
48+ case 'cosmoshub' :
49+ return 'cosmos' ;
50+ case 'assetmantle' :
51+ return 'asset-mantle' ;
52+ case 'cryptoorgchain' :
53+ return 'crypto-org' ;
54+ case 'dig' :
55+ return 'dig-chain' ;
56+ case 'gravitybridge' :
57+ return 'gravity-bridge' ;
58+ case 'kichain' :
59+ return 'ki-chain' ;
60+ case 'oraichain' :
61+ return 'orai-chain' ;
62+ case 'terra' :
63+ return 'terra-classic' ;
64+ default :
65+ return chainName ;
66+ }
67+ } ;
68+
69+ const isUrlValid = async ( url : string ) => {
70+ const res = await fetch ( url , { method : 'HEAD' } ) ;
71+ const contentType = res ?. headers ?. get ( 'Content-Type' ) || '' ;
72+ return contentType . startsWith ( 'image' ) ;
73+ } ;
74+
75+ const getCosmostationUrl = ( chainName : string , validatorAddr : string ) => {
76+ const cosmostationChainName = convertChainName ( chainName ) ;
77+ return `https://raw.githubusercontent.com/cosmostation/chainlist/main/chain/${ cosmostationChainName } /moniker/${ validatorAddr } .png` ;
78+ } ;
79+
80+ const addImageSource = async (
81+ validator : Validator ,
82+ chainName : string
83+ ) : Promise < Validator & ImageSource > => {
84+ const url = getCosmostationUrl ( chainName , validator . operatorAddress ) ;
85+ const isValid = await isUrlValid ( url ) ;
86+ return { ...validator , imageSource : isValid ? 'cosmostation' : 'keybase' } ;
87+ } ;
88+
89+ const getKeybaseUrl = ( identity : string ) => {
90+ return `https://keybase.io/_/api/1.0/user/lookup.json?key_suffix=${ identity } &fields=pictures` ;
91+ } ;
92+
93+ const getImgUrls = async ( validators : Validator [ ] , chainName : string ) => {
94+ const validatorsWithImgSource = await Promise . all (
95+ validators . map ( ( validator ) => addImageSource ( validator , chainName ) )
96+ ) ;
97+
98+ // cosmostation urls
99+ const cosmostationUrls = validatorsWithImgSource
100+ . filter ( ( validator ) => validator . imageSource === 'cosmostation' )
101+ . map ( ( { operatorAddress } ) => {
102+ return {
103+ address : operatorAddress ,
104+ url : getCosmostationUrl ( chainName , operatorAddress ) ,
105+ } ;
106+ } ) ;
107+
108+ // keybase urls
109+ const keybaseIdentities = validatorsWithImgSource
110+ . filter ( ( validator ) => validator . imageSource === 'keybase' )
111+ . map ( ( validator ) => ( {
112+ address : validator . operatorAddress ,
113+ identity : validator . description ?. identity || '' ,
114+ } ) ) ;
115+
116+ const chunkedIdentities = splitIntoChunks ( keybaseIdentities , 20 ) ;
117+
118+ let responses : any [ ] = [ ] ;
119+
120+ for ( const chunk of chunkedIdentities ) {
121+ const thumbnailRequests = chunk . map ( ( { address, identity } ) => {
122+ if ( ! identity ) return { address, url : '' } ;
123+
124+ return fetch ( getKeybaseUrl ( identity ) )
125+ . then ( ( response ) => response . json ( ) )
126+ . then ( ( res ) => ( {
127+ address,
128+ url : res . them ?. [ 0 ] ?. pictures ?. primary . url || '' ,
129+ } ) ) ;
130+ } ) ;
131+ responses = [ ...responses , await Promise . all ( thumbnailRequests ) ] ;
132+ await new Promise ( ( resolve ) => setTimeout ( resolve , 500 ) ) ;
133+ }
134+
135+ const keybaseUrls = responses . flat ( ) ;
136+
137+ const allUrls = [ ...cosmostationUrls , ...keybaseUrls ] . reduce (
138+ ( prev , cur ) => ( { ...prev , [ cur . address ] : cur . url } ) ,
139+ { }
140+ ) ;
141+
142+ return allUrls ;
143+ } ;
144+
41145interface StakingTokens {
42146 balance : number ;
43147 rewards : Reward [ ] ;
@@ -91,7 +195,7 @@ export const StakingSection = ({ chainName }: { chainName: ChainName }) => {
91195 let rpcEndpoint = await getRpcEndpoint ( ) ;
92196
93197 if ( ! rpcEndpoint ) {
94- console . log ( 'no rpc endpoint — using a fallback' ) ;
198+ console . log ( 'no rpc endpoint — using a fallback' ) ;
95199 rpcEndpoint = `https://rpc.cosmos.directory/${ chainName } ` ;
96200 }
97201
@@ -163,42 +267,16 @@ export const StakingSection = ({ chainName }: { chainName: ChainName }) => {
163267 : 0 ;
164268
165269 // THUMBNAILS
270+ let thumbnails = { } ;
271+
166272 const validatorThumbnails = localStorage . getItem (
167273 `${ chainName } -validator-thumbnails`
168274 ) ;
169275
170- let thumbnails = { } ;
171-
172276 if ( validatorThumbnails ) {
173277 thumbnails = JSON . parse ( validatorThumbnails ) ;
174278 } else {
175- const identities = allValidators . map (
176- ( validator ) => validator . description ! . identity
177- ) ;
178-
179- const chunkedIdentities = splitIntoChunks ( identities , 30 ) ;
180-
181- let responses : any [ ] = [ ] ;
182-
183- for ( const chunk of chunkedIdentities ) {
184- const thumbnailRequests = chunk . map ( ( identity ) => {
185- const url = `https://keybase.io/_/api/1.0/user/lookup.json?key_suffix=${ identity } &fields=pictures` ;
186- return fetch ( url ) . then ( ( response ) => response . json ( ) ) ;
187- } ) ;
188- responses = [ ...responses , await Promise . all ( thumbnailRequests ) ] ;
189- await new Promise ( ( resolve ) => setTimeout ( resolve , 1000 ) ) ;
190- }
191-
192- const thumbnailUrls = responses
193- . flat ( )
194- . map ( ( value ) => value . them ?. [ 0 ] ?. pictures ?. primary . url ) ;
195-
196- thumbnails = thumbnailUrls . reduce (
197- ( prev , cur , idx ) =>
198- identities [ idx ] && cur ? { ...prev , [ identities [ idx ] ] : cur } : prev ,
199- { }
200- ) ;
201-
279+ thumbnails = await getImgUrls ( validators , chainName ) ;
202280 localStorage . setItem (
203281 `${ chainName } -validator-thumbnails` ,
204282 JSON . stringify ( thumbnails )
@@ -233,7 +311,13 @@ export const StakingSection = ({ chainName }: { chainName: ChainName }) => {
233311
234312 return (
235313 < Box my = { 14 } >
236- < Skeleton isLoaded = { ! isLoading } >
314+ < SkeletonText
315+ isLoaded = { ! isLoading }
316+ mt = "0"
317+ noOfLines = { 4 }
318+ spacing = "4"
319+ skeletonHeight = "4"
320+ >
237321 < Stats
238322 balance = { data . balance }
239323 rewards = { data . rewards }
@@ -242,9 +326,7 @@ export const StakingSection = ({ chainName }: { chainName: ChainName }) => {
242326 updateData = { getData }
243327 chainName = { chainName }
244328 />
245- </ Skeleton >
246- { data . myValidators . length > 0 && (
247- < Skeleton isLoaded = { ! isLoading } >
329+ { data . myValidators . length > 0 && (
248330 < MyValidators
249331 validators = { data . myValidators }
250332 allValidator = { data . allValidators }
@@ -256,10 +338,8 @@ export const StakingSection = ({ chainName }: { chainName: ChainName }) => {
256338 chainName = { chainName }
257339 thumbnails = { data . thumbnails }
258340 />
259- </ Skeleton >
260- ) }
261- { data . allValidators . length > 0 && (
262- < Skeleton isLoaded = { ! isLoading } >
341+ ) }
342+ { data . allValidators . length > 0 && (
263343 < AllValidators
264344 balance = { data . balance }
265345 validators = { data . allValidators }
@@ -269,8 +349,8 @@ export const StakingSection = ({ chainName }: { chainName: ChainName }) => {
269349 chainName = { chainName }
270350 thumbnails = { data . thumbnails }
271351 />
272- </ Skeleton >
273- ) }
352+ ) }
353+ </ SkeletonText >
274354 </ Box >
275355 ) ;
276356} ;
0 commit comments