Skip to content

Commit

Permalink
universal token search added
Browse files Browse the repository at this point in the history
  • Loading branch information
vnaysngh-mudrex committed Feb 15, 2024
1 parent a83cc22 commit dbfdaa7
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 79 deletions.
66 changes: 28 additions & 38 deletions src/components/Logo/AssetLogo.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ChainId, Currency } from '@vnaysn/jediswap-sdk-core'
import useTokenLogoSource from 'hooks/useAssetLogoSource'
import React, { useState } from 'react'
import React, { useMemo, useState } from 'react'
import styled from 'styled-components'
import EthereumLogo from 'assets/images/ethereum-logo.png'

Expand Down Expand Up @@ -55,48 +55,38 @@ const LogoContainer = styled.div`
display: flex;
`

const StyledEthereumLogo = styled.img<{ size: number }>`
width: ${({ size }) => size};
height: ${({ size }) => size};
transition: background-color ${({ theme }) => `${theme.transition.duration.medium} ${theme.transition.timing.in}`};
box-shadow: 0 0 1px white;
border-radius: 50%;
`

const CurrencyLogo = ({ currency, symbol, size }: { currency: any; symbol: any; size: any }) => {
const currencyLogo: any = currency
if (currencyLogo && (currencyLogo.name === 'ETHER' || currencyLogo.name === 'ETH')) {
return <StyledEthereumLogo src={EthereumLogo} alt={`${symbol ?? 'token'} logo`} size={size} loading="lazy" />
} else if (currencyLogo && currencyLogo.logoURI) {
return (
<StyledEthereumLogo src={currencyLogo.logoURI} alt={`${symbol ?? 'token'} logo`} size={size} loading="lazy" />
)
}

return (
<MissingImageLogo size={size}>
{symbol?.toUpperCase().replace('$', '').replace(/\s+/g, '').slice(0, 3)}
</MissingImageLogo>
)
}

/**
* Renders an image by prioritizing a list of sources, and then eventually a fallback triangle alert
*/
export default function AssetLogo({
currency,
isNative,
address,
chainId = ChainId.MAINNET,
symbol,
backupImg,
size = '24px',
style,
}: AssetLogoProps) {
const [src, nextSrc] = useTokenLogoSource(address, chainId, isNative, backupImg)
const [imgLoaded, setImgLoaded] = useState(() => {
const img = document.createElement('img')
img.src = src ?? ''
return src ? img.complete : false
})

const logoURI = currency && currency.name === 'ETHER' ? EthereumLogo : (currency as any)?.logoURI

export default function AssetLogo({ currency, symbol, size = '24px', style }: AssetLogoProps) {
return (
<LogoContainer style={{ height: size, width: size, ...style }}>
{logoURI ? (
<LogoImageWrapper size={size} imgLoaded={imgLoaded}>
<LogoImage
src={logoURI}
alt={`${symbol ?? 'token'} logo`}
size={size}
onLoad={() => void setImgLoaded(true)}
onError={nextSrc}
imgLoaded={imgLoaded}
loading="lazy"
/>
</LogoImageWrapper>
) : (
<MissingImageLogo size={size}>
{/* use only first 3 characters of Symbol for design reasons */}
{symbol?.toUpperCase().replace('$', '').replace(/\s+/g, '').slice(0, 3)}
</MissingImageLogo>
)}
<CurrencyLogo currency={currency} symbol={symbol} size={size} />
</LogoContainer>
)
}
11 changes: 1 addition & 10 deletions src/components/SearchModal/CurrencyList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -291,16 +291,7 @@ export default function CurrencyList({
}
return null
},
[
selectedCurrency,
otherCurrency,
isLoading,
onCurrencySelect,
showCurrencyAmount,
searchQuery,
isAddressSearch,
balances,
]
[searchQuery, isAddressSearch]
)

const itemKey = useCallback((index: number, data: typeof itemData) => {
Expand Down
33 changes: 31 additions & 2 deletions src/hooks/Tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ChainId, Currency, Token } from '@vnaysn/jediswap-sdk-core'
import { useAccountDetails } from 'hooks/starknet-react'
import { getChainInfo } from 'constants/chainInfo'
import { DEFAULT_INACTIVE_LIST_URLS, DEFAULT_LIST_OF_LISTS } from 'constants/lists'
import { useCurrencyFromMap, useTokenFromMapOrNetwork } from 'lib/hooks/useCurrency'
import { parseStringFromArgs, useCurrencyFromMap, useTokenFromMapOrNetwork } from 'lib/hooks/useCurrency'
import { getTokenFilter } from 'lib/hooks/useTokenList/filtering'
import { TokenAddressMap } from 'lib/hooks/useTokenList/utils'
import { useMemo } from 'react'
Expand All @@ -12,6 +12,9 @@ import { isL2ChainId } from 'utils/chains'
import { useAllLists, useCombinedActiveList, useCombinedTokenMapFromUrls } from '../state/lists/hooks'
import { WrappedTokenInfo } from '../state/lists/wrappedTokenInfo'
import { deserializeToken, useUserAddedTokens } from '../state/user/hooks'
import { isAddressValidForStarknet } from 'utils/addresses'
import { useTokenContract } from './useContractV2'
import { NEVER_RELOAD, useSingleCallResult } from 'state/multicall/hooks'

type Maybe<T> = T | null | undefined

Expand Down Expand Up @@ -186,7 +189,33 @@ export function useIsUserAddedToken(currency: Currency | undefined | null): bool
export function useToken(tokenAddress?: string | null): Token | null | undefined {
const { chainId } = useAccountDetails()
const tokens = useDefaultActiveTokens(chainId)
return useTokenFromMapOrNetwork(tokens, tokenAddress)
const address = isAddressValidForStarknet(tokenAddress)
const token: Token | undefined = address ? tokens[address] : undefined

const tokenContract = useTokenContract(address ? address : undefined)

const tokenName = useSingleCallResult(token ? undefined : tokenContract, 'name', undefined, NEVER_RELOAD)

const symbol = useSingleCallResult(token ? undefined : tokenContract, 'symbol', undefined, NEVER_RELOAD)

const decimals = useSingleCallResult(token ? undefined : tokenContract, 'decimals', undefined, NEVER_RELOAD)

return useMemo(() => {
if (token) return token
if (!chainId || !address) return undefined
if (decimals.loading || symbol.loading || tokenName.loading) return null
if (decimals.result) {
const token = new Token(
chainId,
address,
parseInt(decimals.result[0]),
parseStringFromArgs(symbol.result?.[0]),
parseStringFromArgs(symbol.result?.[0])
)
return token
}
return undefined
}, [address, chainId, decimals, symbol, token, tokenName])
}

export function useCurrency(currencyId: Maybe<string>, chainId?: ChainId): Currency | undefined {
Expand Down
8 changes: 4 additions & 4 deletions src/hooks/useContractV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { useAccountDetails } from './starknet-react'
import { DEFAULT_CHAIN_ID, NONFUNGIBLE_POOL_MANAGER_ADDRESS } from 'constants/tokens'
import { getContractV2 } from 'utils/getContract'
import { MULTICALL_ABI, MULTICALL_NETWORKS } from 'contracts/multicall'
import { NonfungiblePositionManager } from '@vnaysn/jediswap-sdk-v3'
import NFTPositionManagerABI from 'contracts/nonfungiblepositionmanager/abi.json'
import ERC20_ABI from 'abis/erc20.json'

// returns null on errors
function useContract(address: string | undefined, ABI: any, withSignerIfPossible = true): Contract | null {
Expand All @@ -24,9 +24,9 @@ function useContract(address: string | undefined, ABI: any, withSignerIfPossible
}, [address, ABI, account, connector, chainId])
}

// export function useTokenContract(tokenAddress?: string, withSignerIfPossible?: boolean): Contract | null {
// return useContract(tokenAddress, ERC20_ABI, withSignerIfPossible)
// }
export function useTokenContract(tokenAddress?: string, withSignerIfPossible?: boolean): Contract | null {
return useContract(tokenAddress, ERC20_ABI, withSignerIfPossible)
}

// export function usePairContract(pairAddress?: string, withSignerIfPossible?: boolean): Contract | null {
// return useContract(pairAddress, PAIR_ABI, withSignerIfPossible)
Expand Down
82 changes: 57 additions & 25 deletions src/lib/hooks/useCurrency.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@ import { ChainId, Currency, Token } from '@vnaysn/jediswap-sdk-core'
import { useAccountDetails } from 'hooks/starknet-react'
import { sendAnalyticsEvent } from 'analytics'
import { isSupportedChain } from 'constants/chains'
import { useBytes32TokenContract, useTokenContract } from 'hooks/useContract'
import { NEVER_RELOAD, useSingleCallResult } from 'lib/hooks/multicall'
import { useTokenContract } from 'hooks/useContractV2'
import useNativeCurrency from 'lib/hooks/useNativeCurrency'
import { useEffect, useMemo } from 'react'

import { DEFAULT_CHAIN_ID, DEFAULT_ERC20_DECIMALS, WETH } from '../../constants/tokens'
// import { TOKEN_SHORTHANDS } from '../../constants/tokens'
import { isAddress } from '../../utils'
import { isAddressValidForStarknet } from 'utils/addresses'
import { useContractRead } from '@starknet-react/core'
import ERC20_ABI from 'abis/erc20.json'
import { cairo, num, shortString } from 'starknet'

// parse a name or symbol from a token response
const BYTES32_REGEX = /^0x[a-fA-F0-9]{64}$/
Expand All @@ -30,6 +32,30 @@ function parseStringOrBytes32(str: string | undefined, bytes32: string | undefin
export const UNKNOWN_TOKEN_SYMBOL = 'UNKNOWN'
const UNKNOWN_TOKEN_NAME = 'Unknown Token'

const useSingleCallResult = (address: string | undefined, type: string) => {
const { data: result, isLoading } = useContractRead({
functionName: type,
args: [],
abi: ERC20_ABI,
address,
watch: true,
})

return { result, isLoading }
}

export function parseStringFromArgs(data: any, isHexNumber?: boolean): string | undefined {
if (typeof data === 'string') {
if (isHexNumber) {
return num.hexToDecimalString(data)
} else if (shortString.isShortString(data)) {
return shortString.decodeShortString(data)
}
return data
}
return undefined
}

/**
* Returns a Token from the tokenAddress.
* Returns null if token is loading or null was passed.
Expand All @@ -38,40 +64,46 @@ const UNKNOWN_TOKEN_NAME = 'Unknown Token'
export function useTokenFromActiveNetwork(tokenAddress: string | undefined): Token | null | undefined {
const { chainId } = useAccountDetails()

const formattedAddress = isAddress(tokenAddress)
const formattedAddress = isAddressValidForStarknet(tokenAddress)
const tokenContract = useTokenContract(formattedAddress ? formattedAddress : undefined, false)
const tokenContractBytes32 = useBytes32TokenContract(formattedAddress ? formattedAddress : undefined, false)

// TODO (WEB-1709): reduce this to one RPC call instead of 5
// TODO: Fix redux-multicall so that these values do not reload.
const tokenName = useSingleCallResult(tokenContract, 'name', undefined, NEVER_RELOAD)
const tokenNameBytes32 = useSingleCallResult(tokenContractBytes32, 'name', undefined, NEVER_RELOAD)
const symbol = useSingleCallResult(tokenContract, 'symbol', undefined, NEVER_RELOAD)
const symbolBytes32 = useSingleCallResult(tokenContractBytes32, 'symbol', undefined, NEVER_RELOAD)
const decimals = useSingleCallResult(tokenContract, 'decimals', undefined, NEVER_RELOAD)
const tokenName: any = useSingleCallResult(tokenContract?.address, 'name')
const symbol: any = useSingleCallResult(tokenContract?.address, 'symbol')
const decimals: any = useSingleCallResult(tokenContract?.address, 'decimals')

const isLoading = useMemo(
() => decimals.loading || symbol.loading || tokenName.loading,
[decimals.loading, symbol.loading, tokenName.loading]
() => decimals.isLoading || symbol.isLoading || tokenName.isLoading,
[decimals.isLoading, symbol.isLoading, tokenName.isLoading]
)
const parsedDecimals = useMemo(() => decimals?.result?.[0] ?? DEFAULT_ERC20_DECIMALS, [decimals.result])

const parsedSymbol = useMemo(
() => parseStringOrBytes32(symbol.result?.[0], symbolBytes32.result?.[0], UNKNOWN_TOKEN_SYMBOL),
[symbol.result, symbolBytes32.result]
)
const parsedName = useMemo(
() => parseStringOrBytes32(tokenName.result?.[0], tokenNameBytes32.result?.[0], UNKNOWN_TOKEN_NAME),
[tokenName.result, tokenNameBytes32.result]
const parsedDecimals = useMemo(
() => parseInt(decimals?.result?.decimals) ?? DEFAULT_ERC20_DECIMALS,
[decimals.result]
)

return useMemo(() => {
// If the token is on another chain, we cannot fetch it on-chain, and it is invalid.
if (typeof tokenAddress !== 'string' || !isSupportedChain(chainId) || !formattedAddress) return undefined
if (isLoading || !chainId) return null

return new Token(chainId, formattedAddress, parsedDecimals, parsedSymbol, parsedName)
}, [chainId, tokenAddress, formattedAddress, isLoading, parsedDecimals, parsedSymbol, parsedName])
if (!chainId || !formattedAddress || isLoading) return undefined
if (decimals.isLoading || symbol.isLoading || tokenName.isLoading) return null
const parsedTokenNameHexString =
tokenName && tokenName.result ? num.getHexString(tokenName?.result?.name.toString()) : UNKNOWN_TOKEN_NAME

const parsedSymbolHexString =
tokenName && tokenName.result ? num.getHexString(symbol?.result?.symbol.toString()) : UNKNOWN_TOKEN_SYMBOL

if (decimals.result) {
const token = new Token(
chainId,
formattedAddress,
parsedDecimals,
parseStringFromArgs(parsedSymbolHexString),
parseStringFromArgs(parsedTokenNameHexString)
)
return token
}
return undefined
}, [formattedAddress, chainId, decimals, symbol, tokenName])
}

type TokenMap = { [address: string]: Token }
Expand Down

0 comments on commit dbfdaa7

Please sign in to comment.