diff --git a/src/components/Logo/AssetLogo.tsx b/src/components/Logo/AssetLogo.tsx index f418f8bf..9cf18c1a 100644 --- a/src/components/Logo/AssetLogo.tsx +++ b/src/components/Logo/AssetLogo.tsx @@ -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' @@ -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 + } else if (currencyLogo && currencyLogo.logoURI) { + return ( + + ) + } + + return ( + + {symbol?.toUpperCase().replace('$', '').replace(/\s+/g, '').slice(0, 3)} + + ) +} + /** * 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 ( - {logoURI ? ( - - void setImgLoaded(true)} - onError={nextSrc} - imgLoaded={imgLoaded} - loading="lazy" - /> - - ) : ( - - {/* use only first 3 characters of Symbol for design reasons */} - {symbol?.toUpperCase().replace('$', '').replace(/\s+/g, '').slice(0, 3)} - - )} + ) } diff --git a/src/components/SearchModal/CurrencyList/index.tsx b/src/components/SearchModal/CurrencyList/index.tsx index 0773a85d..1a2c6e44 100644 --- a/src/components/SearchModal/CurrencyList/index.tsx +++ b/src/components/SearchModal/CurrencyList/index.tsx @@ -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) => { diff --git a/src/hooks/Tokens.ts b/src/hooks/Tokens.ts index 83bf60b5..80ee2335 100644 --- a/src/hooks/Tokens.ts +++ b/src/hooks/Tokens.ts @@ -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' @@ -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 | null | undefined @@ -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, chainId?: ChainId): Currency | undefined { diff --git a/src/hooks/useContractV2.ts b/src/hooks/useContractV2.ts index 17e0e2c5..6162bccf 100644 --- a/src/hooks/useContractV2.ts +++ b/src/hooks/useContractV2.ts @@ -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 { @@ -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) diff --git a/src/lib/hooks/useCurrency.ts b/src/lib/hooks/useCurrency.ts index b6f97718..a80db941 100644 --- a/src/lib/hooks/useCurrency.ts +++ b/src/lib/hooks/useCurrency.ts @@ -5,8 +5,7 @@ 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' @@ -14,6 +13,9 @@ import { DEFAULT_CHAIN_ID, DEFAULT_ERC20_DECIMALS, WETH } from '../../constants/ // 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}$/ @@ -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. @@ -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 }