Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

universal token search added #166

Merged
merged 1 commit into from
Feb 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading