diff --git a/webapp/src/components/AssetImage/AssetImage.tsx b/webapp/src/components/AssetImage/AssetImage.tsx index 07c5e8d94..598c215e2 100644 --- a/webapp/src/components/AssetImage/AssetImage.tsx +++ b/webapp/src/components/AssetImage/AssetImage.tsx @@ -677,7 +677,13 @@ const AssetImageWrapper = (props: Props) => { } = props useEffect(() => { - if (!item && isNFT(asset) && asset.itemId) { + if ( + !item && + isNFT(asset) && + asset.itemId && + (asset.category === NFTCategory.WEARABLE || + asset.category === NFTCategory.EMOTE) + ) { onFetchItem(asset.contractAddress, asset.itemId) } }, [asset, item, onFetchItem]) diff --git a/webapp/src/components/Modals/BuyWithCryptoModal/BuyNftWithCryptoModal/BuyNftWithCryptoModal.tsx b/webapp/src/components/Modals/BuyWithCryptoModal/BuyNftWithCryptoModal/BuyNftWithCryptoModal.tsx index 2e280f1d9..2cb8afb55 100644 --- a/webapp/src/components/Modals/BuyWithCryptoModal/BuyNftWithCryptoModal/BuyNftWithCryptoModal.tsx +++ b/webapp/src/components/Modals/BuyWithCryptoModal/BuyNftWithCryptoModal/BuyNftWithCryptoModal.tsx @@ -79,15 +79,8 @@ const BuyNftWithCryptoModalHOC = (props: Props) => { [order] ) const onGetGasCost: OnGetGasCost = useCallback( - (selectedToken, selectedChain, wallet, providerTokens) => - useBuyNftGasCost( - nft, - order, - selectedToken, - selectedChain, - wallet, - providerTokens - ), + (selectedToken, chainNativeToken, wallet) => + useBuyNftGasCost(nft, order, selectedToken, chainNativeToken, wallet), [nft, order] ) diff --git a/webapp/src/components/Modals/BuyWithCryptoModal/BuyWithCryptoModal.tsx b/webapp/src/components/Modals/BuyWithCryptoModal/BuyWithCryptoModal.tsx index bf44dd709..cea25bcf1 100644 --- a/webapp/src/components/Modals/BuyWithCryptoModal/BuyWithCryptoModal.tsx +++ b/webapp/src/components/Modals/BuyWithCryptoModal/BuyWithCryptoModal.tsx @@ -29,7 +29,6 @@ import { ManaToFiat } from '../../ManaToFiat' import { config } from '../../../config' import ChainAndTokenSelector from './ChainAndTokenSelector/ChainAndTokenSelector' import { - DEFAULT_CHAINS, getDefaultChains, getMANAToken, getShouldUseMetaTx, @@ -111,11 +110,24 @@ export const BuyWithCryptoModal = (props: Props) => { ) }, [providerTokens, manaAddressOnAssetChain]) + const selectedProviderChain = useMemo(() => { + return providerChains.find( + c => c.chainId.toString() === selectedChain.toString() + ) + }, [providerChains, selectedChain]) + + const chainNativeToken = useMemo(() => { + return providerTokens.find( + t => + +t.chainId === selectedChain && + t.symbol === selectedProviderChain?.nativeCurrency.symbol + ) + }, [selectedChain, selectedProviderChain, providerTokens]) + const { gasCost, isFetchingGasCost } = onGetGasCost( selectedToken, - selectedChain, - wallet, - providerTokens + chainNativeToken, + wallet ) const { @@ -147,7 +159,6 @@ export const BuyWithCryptoModal = (props: Props) => { // if the tx should be done through the provider const shouldUseCrossChainProvider = useShouldUseCrossChainProvider( selectedToken, - selectedChain, asset.network ) @@ -166,15 +177,9 @@ export const BuyWithCryptoModal = (props: Props) => { ) }, [asset, manaAddressOnAssetChain, selectedChain, selectedToken, wallet]) - const selectedProviderChain = useMemo(() => { - return providerChains.find( - c => c.chainId.toString() === selectedChain.toString() - ) - }, [providerChains, selectedChain]) - // Compute if the price is too low for meta tx const hasLowPriceForMetaTx = useMemo( - () => wallet?.chainId !== ChainId.MATIC_MAINNET && isPriceTooLow(price), // not connected to polygon AND has price < minimun for meta tx + () => wallet?.chainId !== ChainId.MATIC_MAINNET && isPriceTooLow(price), // not connected to polygon AND has price < minimum for meta tx [price, wallet?.chainId] ) @@ -186,18 +191,19 @@ export const BuyWithCryptoModal = (props: Props) => { if (!crossChainProvider.isLibInitialized()) { await crossChainProvider.init() } + const defaultChains = getDefaultChains() const supportedTokens = crossChainProvider.getSupportedTokens() const supportedChains = [ - ...DEFAULT_CHAINS, + ...defaultChains, ...crossChainProvider .getSupportedChains() - .filter(c => DEFAULT_CHAINS.every(dc => dc.chainId !== c.chainId)) + .filter(c => defaultChains.every(dc => dc.chainId !== c.chainId)) ] // keep the defaults since we support MANA on them natively setProviderChains( supportedChains.filter( c => CROSS_CHAIN_SUPPORTED_CHAINS.includes(+c.chainId) && - getDefaultChains().find(t => t.chainId === c.chainId) + defaultChains.find(t => t.chainId === c.chainId) ) ) setProviderTokens( @@ -344,14 +350,17 @@ export const BuyWithCryptoModal = (props: Props) => { ) : ( t('buy_with_crypto_modal.switch_network', { - chain: providerChains.find( - c => c.chainId === selectedChain.toString() - )?.networkName + chain: selectedProviderChain?.networkName }) )} ) - }, [isSwitchingNetwork, onSwitchNetwork, providerChains, selectedChain]) + }, [ + isSwitchingNetwork, + onSwitchNetwork, + selectedProviderChain, + selectedChain + ]) const handleBuyWithCard = useCallback(() => { if (onBuyWithCard) { diff --git a/webapp/src/components/Modals/BuyWithCryptoModal/BuyWithCryptoModal.types.ts b/webapp/src/components/Modals/BuyWithCryptoModal/BuyWithCryptoModal.types.ts index 04ab4b098..0c11ccd04 100644 --- a/webapp/src/components/Modals/BuyWithCryptoModal/BuyWithCryptoModal.types.ts +++ b/webapp/src/components/Modals/BuyWithCryptoModal/BuyWithCryptoModal.types.ts @@ -17,9 +17,8 @@ export type MapStateProps = Pick< export type MapDispatchProps = Pick export type OnGetGasCost = ( selectedToken: Token, - selectedChain: ChainId, - wallet: Wallet | null, - providerTokens: Token[] + nativeChainToken: Token | undefined, + wallet: Wallet | null ) => GasCost export type OnGetCrossChainRoute = ( selectedToken: Token, diff --git a/webapp/src/components/Modals/BuyWithCryptoModal/MintNameWithCryptoModal/MintNameWithCryptoModal.tsx b/webapp/src/components/Modals/BuyWithCryptoModal/MintNameWithCryptoModal/MintNameWithCryptoModal.tsx index 76d57c438..08b68a8f6 100644 --- a/webapp/src/components/Modals/BuyWithCryptoModal/MintNameWithCryptoModal/MintNameWithCryptoModal.tsx +++ b/webapp/src/components/Modals/BuyWithCryptoModal/MintNameWithCryptoModal/MintNameWithCryptoModal.tsx @@ -90,14 +90,8 @@ const MintNameWithCryptoModalHOC = (props: Props) => { ) const onGetGasCost: OnGetGasCost = useCallback( - (selectedToken, selectedChain, wallet, providerTokens) => - useNameMintingGasCost( - name, - selectedToken, - selectedChain, - wallet, - providerTokens - ), + (selectedToken, chainNativeToken, wallet) => + useNameMintingGasCost(name, selectedToken, chainNativeToken, wallet), [name] ) diff --git a/webapp/src/components/Modals/BuyWithCryptoModal/MintNftWithCryptoModal/MintNftWithCryptoModal.tsx b/webapp/src/components/Modals/BuyWithCryptoModal/MintNftWithCryptoModal/MintNftWithCryptoModal.tsx index 7b9496532..426ca0091 100644 --- a/webapp/src/components/Modals/BuyWithCryptoModal/MintNftWithCryptoModal/MintNftWithCryptoModal.tsx +++ b/webapp/src/components/Modals/BuyWithCryptoModal/MintNftWithCryptoModal/MintNftWithCryptoModal.tsx @@ -78,14 +78,8 @@ const MintNftWithCryptoModalHOC = (props: Props) => { [item] ) const onGetGasCost: OnGetGasCost = useCallback( - (selectedToken, selectedChain, wallet, providerTokens) => - useMintingNftGasCost( - item, - selectedToken, - selectedChain, - wallet, - providerTokens - ), + (selectedToken, chainNativeToken, wallet) => + useMintingNftGasCost(item, selectedToken, chainNativeToken, wallet), [item] ) diff --git a/webapp/src/components/Modals/BuyWithCryptoModal/hooks.spec.ts b/webapp/src/components/Modals/BuyWithCryptoModal/hooks.spec.ts index 672e13e34..15677af1f 100644 --- a/webapp/src/components/Modals/BuyWithCryptoModal/hooks.spec.ts +++ b/webapp/src/components/Modals/BuyWithCryptoModal/hooks.spec.ts @@ -1,20 +1,43 @@ -import { ChainId, Network } from '@dcl/schemas' +import { ChainId, Network, Order, Item } from '@dcl/schemas' import { BigNumber, ethers } from 'ethers' import { renderHook } from '@testing-library/react-hooks' import { NATIVE_TOKEN, Token } from 'decentraland-transactions/crossChain' import { Wallet } from 'decentraland-dapps/dist/modules/wallet' -import { useShouldUseCrossChainProvider, useNameMintingGasCost } from './hooks' -import { estimateNameMintingGas } from './utils' +import { + useShouldUseCrossChainProvider, + useNameMintingGasCost, + useTokenBalance, + useBuyNftGasCost, + useMintingNftGasCost +} from './hooks' +import { + estimateBuyNftGas, + estimateMintNftGas, + estimateNameMintingGas +} from './utils' import { waitFor } from '@testing-library/react' +import { NFT } from '../../../modules/nft/types' jest.mock('ethers', () => ({ ...jest.requireActual('ethers'), ethers: { ...jest.requireActual('ethers').ethers, + Contract: function(address: string, abi: string[], provider: any) { + return { + address, + abi, + provider, + balanceOf: () => Promise.resolve(BigNumber.from(3232)) + } + }, providers: { Web3Provider: function() { return { - getGasPrice: () => Promise.resolve(BigNumber.from(4)) + getGasPrice: () => Promise.resolve(BigNumber.from(4)), + send: (method: string) => + method === 'eth_getBalance' + ? Promise.resolve(BigNumber.from(3231)) + : Promise.resolve(undefined) } } } @@ -24,7 +47,9 @@ jest.mock('ethers', () => ({ jest.mock('./utils', () => { return { ...jest.requireActual('./utils'), - estimateNameMintingGas: jest.fn() + estimateNameMintingGas: jest.fn(), + estimateMintNftGas: jest.fn(), + estimateBuyNftGas: jest.fn() } }) @@ -43,7 +68,7 @@ describe('when using the should use cross chain provider hook', () => { describe('and the selected token is not mana', () => { beforeEach(() => { - selectedToken = { symbol: 'ETH', address: '0x1' } as Token + selectedToken = { symbol: 'ETH', chainId: '1', address: '0x1' } as Token }) it('should return true', () => { @@ -62,7 +87,7 @@ describe('when using the should use cross chain provider hook', () => { let assetNetwork: Network beforeEach(() => { - selectedToken = { symbol: 'MANA', address: '0x1' } as Token + selectedToken = { symbol: 'MANA', chainId: '1', address: '0x1' } as Token }) describe('and the asset network is not ethereum and the network for the selected chain is ethereum', () => { @@ -72,11 +97,7 @@ describe('when using the should use cross chain provider hook', () => { it('should return true', () => { const { result } = renderHook(() => - useShouldUseCrossChainProvider( - selectedToken, - ChainId.ETHEREUM_MAINNET, - assetNetwork - ) + useShouldUseCrossChainProvider(selectedToken, assetNetwork) ) expect(result.current).toBe(true) }) @@ -89,35 +110,28 @@ describe('when using the should use cross chain provider hook', () => { it('should return true', () => { const { result } = renderHook(() => - useShouldUseCrossChainProvider( - selectedToken, - ChainId.ETHEREUM_MAINNET, - assetNetwork - ) + useShouldUseCrossChainProvider(selectedToken, assetNetwork) ) expect(result.current).toBe(true) }) }) describe('and the asset network is ethereum', () => { - let selectedChain: ChainId - beforeEach(() => { assetNetwork = Network.ETHEREUM }) describe('and the network for the selected chain is not ethereum', () => { beforeEach(() => { - selectedChain = ChainId.MATIC_MAINNET + selectedToken = { + ...selectedToken, + chainId: ChainId.MATIC_MAINNET.toString() + } }) it('should return true', () => { const { result } = renderHook(() => - useShouldUseCrossChainProvider( - selectedToken, - selectedChain, - assetNetwork - ) + useShouldUseCrossChainProvider(selectedToken, assetNetwork) ) expect(result.current).toBe(true) }) @@ -125,16 +139,15 @@ describe('when using the should use cross chain provider hook', () => { describe('and the network for the selected chain is ethereum', () => { beforeEach(() => { - selectedChain = ChainId.ETHEREUM_MAINNET + selectedToken = { + ...selectedToken, + chainId: ChainId.ETHEREUM_MAINNET.toString() + } }) it('should return false', () => { const { result } = renderHook(() => - useShouldUseCrossChainProvider( - selectedToken, - selectedChain, - assetNetwork - ) + useShouldUseCrossChainProvider(selectedToken, assetNetwork) ) expect(result.current).toBe(false) }) @@ -142,24 +155,21 @@ describe('when using the should use cross chain provider hook', () => { }) describe('and the asset network is matic', () => { - let selectedChain: ChainId - beforeEach(() => { assetNetwork = Network.MATIC }) describe('and the network for the selected chain is not matic', () => { beforeEach(() => { - selectedChain = ChainId.ETHEREUM_MAINNET + selectedToken = { + ...selectedToken, + chainId: ChainId.ETHEREUM_MAINNET.toString() + } }) it('should return true', () => { const { result } = renderHook(() => - useShouldUseCrossChainProvider( - selectedToken, - selectedChain, - assetNetwork - ) + useShouldUseCrossChainProvider(selectedToken, assetNetwork) ) expect(result.current).toBe(true) }) @@ -167,16 +177,15 @@ describe('when using the should use cross chain provider hook', () => { describe('and the network for the selected chain is matic', () => { beforeEach(() => { - selectedChain = ChainId.MATIC_MAINNET + selectedToken = { + ...selectedToken, + chainId: ChainId.MATIC_MAINNET.toString() + } }) it('should return false', () => { const { result } = renderHook(() => - useShouldUseCrossChainProvider( - selectedToken, - selectedChain, - assetNetwork - ) + useShouldUseCrossChainProvider(selectedToken, assetNetwork) ) expect(result.current).toBe(false) }) @@ -185,31 +194,35 @@ describe('when using the should use cross chain provider hook', () => { }) }) -describe('when using the name minting as cost hook', () => { +describe('when using the name minting gas cost hook', () => { let selectedToken: Token let name: string - let selectedChain: ChainId let wallet: Wallet | null - let providerTokens: Token[] + let chainNativeToken: Token + + beforeEach(() => { + name = 'name' + chainNativeToken = { + symbol: 'ETH', + address: '0x1', + chainId: ChainId.ETHEREUM_MAINNET.toString(), + usdPrice: 10 + } as Token + }) describe("and there's no wallet set", () => { beforeEach(() => { - selectedToken = { symbol: 'MANA', address: '0x1' } as Token - name = 'name' - selectedChain = ChainId.ETHEREUM_MAINNET - providerTokens = [] + selectedToken = { + symbol: 'MANA', + address: '0x1', + chainId: ChainId.ETHEREUM_MAINNET.toString() + } as Token wallet = null }) it('should return an undefined cost and the loading flag as false', () => { const { result } = renderHook(() => - useNameMintingGasCost( - name, - selectedToken, - selectedChain, - wallet, - providerTokens - ) + useNameMintingGasCost(name, selectedToken, chainNativeToken, wallet) ) expect(result.current.gasCost).toBe(undefined) expect(result.current.isFetchingGasCost).toBe(false) @@ -218,10 +231,11 @@ describe('when using the name minting as cost hook', () => { describe("and the wallet's network is not the same as the asset's one", () => { beforeEach(() => { - selectedToken = { symbol: 'MANA', address: '0x1' } as Token - name = 'name' - selectedChain = ChainId.ETHEREUM_MAINNET - providerTokens = [] + selectedToken = { + symbol: 'MANA', + address: '0x1', + chainId: ChainId.ETHEREUM_MAINNET.toString() + } as Token wallet = { chainId: ChainId.MATIC_MAINNET, network: Network.MATIC @@ -230,13 +244,7 @@ describe('when using the name minting as cost hook', () => { it('should return an undefined cost and the loading flag as false', () => { const { result } = renderHook(() => - useNameMintingGasCost( - name, - selectedToken, - selectedChain, - wallet, - providerTokens - ) + useNameMintingGasCost(name, selectedToken, chainNativeToken, wallet) ) expect(result.current.gasCost).toBe(undefined) expect(result.current.isFetchingGasCost).toBe(false) @@ -245,10 +253,11 @@ describe('when using the name minting as cost hook', () => { describe('and combination of selected tokens and chains results in the need of using a cross chain provider', () => { beforeEach(() => { - selectedToken = { symbol: 'MANA', address: '0x1' } as Token - name = 'name' - selectedChain = ChainId.MATIC_MAINNET - providerTokens = [] + selectedToken = { + symbol: 'MANA', + address: '0x1', + chainId: ChainId.MATIC_MAINNET.toString() + } as Token wallet = { chainId: ChainId.ETHEREUM_MAINNET, network: Network.ETHEREUM @@ -257,13 +266,7 @@ describe('when using the name minting as cost hook', () => { it('should return an undefined cost and the loading flag as false', () => { const { result } = renderHook(() => - useNameMintingGasCost( - name, - selectedToken, - selectedChain, - wallet, - providerTokens - ) + useNameMintingGasCost(name, selectedToken, chainNativeToken, wallet) ) expect(result.current.gasCost).toBe(undefined) expect(result.current.isFetchingGasCost).toBe(false) @@ -278,9 +281,6 @@ describe('when using the name minting as cost hook', () => { chainId: ChainId.ETHEREUM_MAINNET.toString(), usdPrice: 10 } as Token - name = 'name' - selectedChain = ChainId.ETHEREUM_MAINNET - providerTokens = [selectedToken] wallet = { address: '0x1', chainId: ChainId.ETHEREUM_MAINNET, @@ -298,13 +298,7 @@ describe('when using the name minting as cost hook', () => { it('should return an undefined cost and the loading flag as false', async () => { const { result } = renderHook(() => - useNameMintingGasCost( - name, - selectedToken, - selectedChain, - wallet, - providerTokens - ) + useNameMintingGasCost(name, selectedToken, chainNativeToken, wallet) ) await waitFor(() => { expect(result.current.isFetchingGasCost).toBe(false) @@ -323,13 +317,301 @@ describe('when using the name minting as cost hook', () => { it('should return a total gas cost of 16, a USD gas cost of 160, the native token and the loading flag as false', async () => { const { result } = renderHook(() => - useNameMintingGasCost( - name, - selectedToken, - selectedChain, - wallet, - providerTokens - ) + useNameMintingGasCost(name, selectedToken, chainNativeToken, wallet) + ) + + await waitFor(() => { + expect(result.current.isFetchingGasCost).toBe(false) + }) + expect(result.current.gasCost?.total).toEqual( + ethers.utils.formatEther(BigNumber.from('16000000000000000000')) + ) + expect(result.current.gasCost?.token).toEqual(chainNativeToken) + expect(result.current.gasCost?.totalUSDPrice).toEqual(160) + }) + }) + }) +}) + +describe('when using the buy nft gas cost hook', () => { + let selectedToken: Token + let nft: NFT + let order: Order + let wallet: Wallet | null + let chainNativeToken: Token + + beforeEach(() => { + nft = { + id: 'aNftId' + } as NFT + order = { + id: 'anOrderId', + price: '1000000000000000000', + chainId: ChainId.ETHEREUM_MAINNET, + network: Network.ETHEREUM + } as Order + chainNativeToken = { + symbol: 'ETH', + address: '0x1', + chainId: ChainId.ETHEREUM_MAINNET.toString(), + usdPrice: 10 + } as Token + }) + + describe("and there's no wallet set", () => { + beforeEach(() => { + selectedToken = { + symbol: 'MANA', + address: '0x1', + chainId: ChainId.ETHEREUM_MAINNET.toString() + } as Token + + wallet = null + }) + + it('should return an undefined cost and the loading flag as false', () => { + const { result } = renderHook(() => + useBuyNftGasCost(nft, order, selectedToken, chainNativeToken, wallet) + ) + expect(result.current.gasCost).toBe(undefined) + expect(result.current.isFetchingGasCost).toBe(false) + }) + }) + + describe("and the wallet's network is not the same as the asset's one", () => { + beforeEach(() => { + selectedToken = { + symbol: 'MANA', + address: '0x1', + chainId: ChainId.ETHEREUM_MAINNET.toString() + } as Token + wallet = { + chainId: ChainId.MATIC_MAINNET, + network: Network.MATIC + } as Wallet + }) + + it('should return an undefined cost and the loading flag as false', () => { + const { result } = renderHook(() => + useBuyNftGasCost(nft, order, selectedToken, chainNativeToken, wallet) + ) + expect(result.current.gasCost).toBe(undefined) + expect(result.current.isFetchingGasCost).toBe(false) + }) + }) + + describe('and combination of selected tokens and chains results in the need of using a cross chain provider', () => { + beforeEach(() => { + selectedToken = { + symbol: 'MANA', + address: '0x1', + chainId: ChainId.MATIC_MAINNET.toString() + } as Token + wallet = { + chainId: ChainId.ETHEREUM_MAINNET, + network: Network.ETHEREUM + } as Wallet + }) + + it('should return an undefined cost and the loading flag as false', () => { + const { result } = renderHook(() => + useBuyNftGasCost(nft, order, selectedToken, chainNativeToken, wallet) + ) + expect(result.current.gasCost).toBe(undefined) + expect(result.current.isFetchingGasCost).toBe(false) + }) + }) + + describe('and all parameters are set to get the gas', () => { + beforeEach(() => { + selectedToken = { + symbol: 'MANA', + address: NATIVE_TOKEN, + chainId: ChainId.ETHEREUM_MAINNET.toString(), + usdPrice: 10 + } as Token + wallet = { + address: '0x1', + chainId: ChainId.ETHEREUM_MAINNET, + network: Network.ETHEREUM + } as Wallet + }) + + describe('and the transactions gas estimation fails', () => { + beforeEach(() => { + ;(estimateBuyNftGas as jest.Mock< + ReturnType, + Parameters + >).mockRejectedValueOnce(undefined) + }) + + it('should return an undefined cost and the loading flag as false', async () => { + const { result } = renderHook(() => + useBuyNftGasCost(nft, order, selectedToken, chainNativeToken, wallet) + ) + await waitFor(() => { + expect(result.current.isFetchingGasCost).toBe(false) + }) + expect(result.current.gasCost).toBe(undefined) + }) + }) + + describe('and the transactions gas estimation is successful', () => { + beforeEach(() => { + ;(estimateBuyNftGas as jest.Mock< + ReturnType, + Parameters + >).mockResolvedValueOnce(BigNumber.from('4000000000000000000')) + }) + + it('should return a total gas cost of 16, a USD gas cost of 160, the native token and the loading flag as false', async () => { + const { result } = renderHook(() => + useBuyNftGasCost(nft, order, selectedToken, chainNativeToken, wallet) + ) + + await waitFor(() => { + expect(result.current.isFetchingGasCost).toBe(false) + }) + expect(result.current.gasCost?.total).toEqual( + ethers.utils.formatEther(BigNumber.from('16000000000000000000')) + ) + expect(result.current.gasCost?.token).toEqual(chainNativeToken) + expect(result.current.gasCost?.totalUSDPrice).toEqual(160) + }) + }) + }) +}) + +describe('when using the minting nft gas cost hook', () => { + let selectedToken: Token + let item: Item + let wallet: Wallet | null + let chainNativeToken: Token + + beforeEach(() => { + item = { + id: 'aNftId', + price: '1000000000000000000', + chainId: ChainId.ETHEREUM_MAINNET, + network: Network.ETHEREUM + } as Item + chainNativeToken = { + symbol: 'ETH', + address: '0x1', + chainId: ChainId.ETHEREUM_MAINNET.toString(), + usdPrice: 10 + } as Token + }) + + describe("and there's no wallet set", () => { + beforeEach(() => { + selectedToken = { + symbol: 'MANA', + address: '0x1', + chainId: ChainId.ETHEREUM_MAINNET.toString() + } as Token + + wallet = null + }) + + it('should return an undefined cost and the loading flag as false', () => { + const { result } = renderHook(() => + useMintingNftGasCost(item, selectedToken, chainNativeToken, wallet) + ) + expect(result.current.gasCost).toBe(undefined) + expect(result.current.isFetchingGasCost).toBe(false) + }) + }) + + describe("and the wallet's network is not the same as the asset's one", () => { + beforeEach(() => { + selectedToken = { + symbol: 'MANA', + address: '0x1', + chainId: ChainId.ETHEREUM_MAINNET.toString() + } as Token + wallet = { + chainId: ChainId.MATIC_MAINNET, + network: Network.MATIC + } as Wallet + }) + + it('should return an undefined cost and the loading flag as false', () => { + const { result } = renderHook(() => + useMintingNftGasCost(item, selectedToken, chainNativeToken, wallet) + ) + expect(result.current.gasCost).toBe(undefined) + expect(result.current.isFetchingGasCost).toBe(false) + }) + }) + + describe('and combination of selected tokens and chains results in the need of using a cross chain provider', () => { + beforeEach(() => { + selectedToken = { + symbol: 'MANA', + address: '0x1', + chainId: ChainId.MATIC_MAINNET.toString() + } as Token + wallet = { + chainId: ChainId.ETHEREUM_MAINNET, + network: Network.ETHEREUM + } as Wallet + }) + + it('should return an undefined cost and the loading flag as false', () => { + const { result } = renderHook(() => + useMintingNftGasCost(item, selectedToken, chainNativeToken, wallet) + ) + expect(result.current.gasCost).toBe(undefined) + expect(result.current.isFetchingGasCost).toBe(false) + }) + }) + + describe('and all parameters are set to get the gas', () => { + beforeEach(() => { + selectedToken = { + symbol: 'MANA', + address: NATIVE_TOKEN, + chainId: ChainId.ETHEREUM_MAINNET.toString(), + usdPrice: 10 + } as Token + wallet = { + address: '0x1', + chainId: ChainId.ETHEREUM_MAINNET, + network: Network.ETHEREUM + } as Wallet + }) + + describe('and the transactions gas estimation fails', () => { + beforeEach(() => { + ;(estimateMintNftGas as jest.Mock< + ReturnType, + Parameters + >).mockRejectedValueOnce(undefined) + }) + + it('should return an undefined cost and the loading flag as false', async () => { + const { result } = renderHook(() => + useMintingNftGasCost(item, selectedToken, chainNativeToken, wallet) + ) + await waitFor(() => { + expect(result.current.isFetchingGasCost).toBe(false) + }) + expect(result.current.gasCost).toBe(undefined) + }) + }) + + describe('and the transactions gas estimation is successful', () => { + beforeEach(() => { + ;(estimateMintNftGas as jest.Mock< + ReturnType, + Parameters + >).mockResolvedValueOnce(BigNumber.from('4000000000000000000')) + }) + + it('should return a total gas cost of 16, a USD gas cost of 160, the native token and the loading flag as false', async () => { + const { result } = renderHook(() => + useMintingNftGasCost(item, selectedToken, chainNativeToken, wallet) ) await waitFor(() => { @@ -338,9 +620,104 @@ describe('when using the name minting as cost hook', () => { expect(result.current.gasCost?.total).toEqual( ethers.utils.formatEther(BigNumber.from('16000000000000000000')) ) - expect(result.current.gasCost?.token).toEqual(selectedToken) + expect(result.current.gasCost?.token).toEqual(chainNativeToken) expect(result.current.gasCost?.totalUSDPrice).toEqual(160) }) }) }) }) + +describe('when using the use token balance hook', () => { + let address: string | undefined + let selectedToken: Token + let selectedChainId: ChainId + + describe('and the address is not set', () => { + beforeEach(() => { + selectedToken = { + symbol: 'ETH', + address: NATIVE_TOKEN, + chainId: ChainId.ETHEREUM_MAINNET.toString() + } as Token + address = undefined + }) + + it('should return an undefined token balance and the loading flag as false', async () => { + const { result } = renderHook(() => + useTokenBalance(selectedToken, selectedChainId, address) + ) + await waitFor(() => { + expect(result.current.isFetchingBalance).toBe(false) + }) + expect(result.current.tokenBalance).toBe(undefined) + }) + }) + + describe('and the selected currency is MANA', () => { + beforeEach(() => { + selectedToken = { + symbol: 'MANA', + address: '0x3', + chainId: ChainId.ETHEREUM_MAINNET.toString() + } as Token + address = '0x1' + selectedChainId = ChainId.ETHEREUM_MAINNET + }) + + it('should return an undefined balance and the loading flag as false', async () => { + const { result } = renderHook(() => + useTokenBalance(selectedToken, selectedChainId, address) + ) + await waitFor(() => { + expect(result.current.isFetchingBalance).toBe(false) + }) + expect(result.current.tokenBalance).toBe(undefined) + }) + }) + + describe('and the selected token is the native one', () => { + beforeEach(() => { + selectedToken = { + symbol: 'ETH', + address: NATIVE_TOKEN, + chainId: ChainId.ETHEREUM_MAINNET.toString() + } as Token + address = '0x1' + selectedChainId = ChainId.ETHEREUM_MAINNET + }) + + it('should return the balance of the native token and the loading flag as false', async () => { + const { result } = renderHook(() => + useTokenBalance(selectedToken, selectedChainId, address) + ) + + await waitFor(() => { + expect(result.current.isFetchingBalance).toBe(false) + }) + expect(result.current.tokenBalance).toEqual(BigNumber.from(3231)) + }) + }) + + describe('and the selected token is not the native one and is not MANA', () => { + beforeEach(() => { + selectedToken = { + symbol: 'USDC', + address: '0x2', + chainId: ChainId.ETHEREUM_MAINNET.toString() + } as Token + address = '0x1' + selectedChainId = ChainId.ETHEREUM_MAINNET + }) + + it('should return the balance of the selected token and the loading flag as false', async () => { + const { result } = renderHook(() => + useTokenBalance(selectedToken, selectedChainId, address) + ) + + await waitFor(() => { + expect(result.current.isFetchingBalance).toBe(false) + }) + expect(result.current.tokenBalance).toEqual(BigNumber.from(3232)) + }) + }) +}) diff --git a/webapp/src/components/Modals/BuyWithCryptoModal/hooks.ts b/webapp/src/components/Modals/BuyWithCryptoModal/hooks.ts index c148b4921..9617a9fbb 100644 --- a/webapp/src/components/Modals/BuyWithCryptoModal/hooks.ts +++ b/webapp/src/components/Modals/BuyWithCryptoModal/hooks.ts @@ -15,31 +15,33 @@ import { import { NFT } from '../../../modules/nft/types' import * as events from '../../../utils/events' import { - estimateTransactionGas as estimateMintingOrBuyingTransactionGas, + estimateBuyNftGas, + estimateMintNftGas, estimateNameMintingGas, formatPrice, getShouldUseMetaTx } from './utils' -const NATIVE_TOKEN = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE' +export const NATIVE_TOKEN = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE' const ROUTE_FETCH_INTERVAL = 10000000 // 10 secs export const useShouldUseCrossChainProvider = ( selectedToken: Token, - selectedChain: ChainId, assetNetwork: Network ) => { return useMemo( () => !( (selectedToken.symbol === 'MANA' && - getNetwork(selectedChain) === Network.MATIC && + getNetwork(parseInt(selectedToken.chainId) as ChainId) === + Network.MATIC && assetNetwork === Network.MATIC) || // MANA selected and it's sending the tx from MATIC (selectedToken.symbol === 'MANA' && - getNetwork(selectedChain) === Network.ETHEREUM && + getNetwork(parseInt(selectedToken.chainId) as ChainId) === + Network.ETHEREUM && assetNetwork === Network.ETHEREUM) ), // MANA selected and it's connected to ETH and buying a L1 NFT - [assetNetwork, selectedChain, selectedToken] + [assetNetwork, selectedToken] ) } @@ -64,8 +66,6 @@ export const useTokenBalance = ( try { setIsFetchingBalance(true) if ( - selectedChain && - selectedToken && selectedToken.symbol !== 'MANA' && // mana balance is already available in the wallet address ) { @@ -78,20 +78,22 @@ export const useTokenBalance = ( address, 'latest' ]) - setSelectedTokenBalance(balanceWei) - - return - } - // else ERC20 - const tokenContract = new ethers.Contract( - selectedToken.address, - ['function balanceOf(address owner) view returns (uint256)'], - provider - ) - const balance: BigNumber = await tokenContract.balanceOf(address) - if (!cancel) { - setSelectedTokenBalance(balance) + if (!cancel) { + setSelectedTokenBalance(balanceWei) + } + } else { + // else ERC20 + const tokenContract = new ethers.Contract( + selectedToken.address, + ['function balanceOf(address owner) view returns (uint256)'], + provider + ) + const balance: BigNumber = await tokenContract.balanceOf(address) + + if (!cancel) { + setSelectedTokenBalance(balance) + } } } } catch (error) { @@ -123,7 +125,7 @@ export type GasCost = { const useGasCost = ( assetNetwork: Network, - providerTokens: Token[], + nativeChainToken: Token | undefined, selectedChain: ChainId, shouldUseCrossChainProvider: boolean, wallet: Wallet | undefined | null, @@ -141,17 +143,14 @@ const useGasCost = ( const gasPrice: BigNumber = await provider.getGasPrice() const estimation = await estimateTransactionGas() - if (estimation && providerTokens.length) { + if (estimation) { const total = estimation.mul(gasPrice) - const nativeToken = providerTokens.find( - t => +t.chainId === selectedChain && t.address === NATIVE_TOKEN - ) - const totalUSDPrice = nativeToken?.usdPrice - ? nativeToken.usdPrice * +ethers.utils.formatEther(total) + const totalUSDPrice = nativeChainToken?.usdPrice + ? nativeChainToken.usdPrice * +ethers.utils.formatEther(total) : undefined setGasCost({ - token: nativeToken, + token: nativeChainToken, total: ethers.utils.formatEther(total), totalUSDPrice }) @@ -174,7 +173,7 @@ const useGasCost = ( }, [ assetNetwork, estimateTransactionGas, - providerTokens, + nativeChainToken, selectedChain, shouldUseCrossChainProvider, wallet @@ -186,27 +185,27 @@ const useGasCost = ( export const useMintingNftGasCost = ( item: Item, selectedToken: Token, - selectedChain: ChainId, - wallet: Wallet | null, - providerTokens: Token[] + chainNativeToken: Token | undefined, + wallet: Wallet | null ): GasCost => { + const chainId = parseInt(selectedToken.chainId) as ChainId + const estimateGas = useCallback( () => wallet - ? estimateMintingOrBuyingTransactionGas(selectedChain, wallet, item) + ? estimateMintNftGas(chainId, wallet, item) : Promise.resolve(undefined), - [selectedChain, wallet, item] + [chainId, wallet, item] ) const shouldUseCrossChainProvider = useShouldUseCrossChainProvider( selectedToken, - selectedChain, item.network ) return useGasCost( item.network, - providerTokens, - selectedChain, + chainNativeToken, + chainId, shouldUseCrossChainProvider, wallet, estimateGas @@ -217,32 +216,27 @@ export const useBuyNftGasCost = ( nft: NFT, order: Order, selectedToken: Token, - selectedChain: ChainId, - wallet: Wallet | null, - providerTokens: Token[] + chainNativeToken: Token | undefined, + wallet: Wallet | null ): GasCost => { + const chainId = parseInt(selectedToken.chainId) as ChainId + const estimateGas = useCallback( () => wallet - ? estimateMintingOrBuyingTransactionGas( - selectedChain, - wallet, - nft, - order - ) + ? estimateBuyNftGas(chainId, wallet, nft, order) : Promise.resolve(undefined), - [selectedChain, wallet, order] + [chainId, wallet, order] ) const shouldUseCrossChainProvider = useShouldUseCrossChainProvider( selectedToken, - selectedChain, order.network ) return useGasCost( order.network, - providerTokens, - selectedChain, + chainNativeToken, + chainId, shouldUseCrossChainProvider, wallet, estimateGas @@ -252,28 +246,28 @@ export const useBuyNftGasCost = ( export const useNameMintingGasCost = ( name: string, selectedToken: Token, - selectedChain: ChainId, - wallet: Wallet | null, - providerTokens: Token[] + chainNativeToken: Token | undefined, + wallet: Wallet | null ) => { + const chainId = parseInt(selectedToken.chainId) as ChainId + const estimateGas = useCallback( () => wallet?.address - ? estimateNameMintingGas(name, selectedChain, wallet?.address) + ? estimateNameMintingGas(name, chainId, wallet?.address) : Promise.resolve(undefined), - [name, selectedChain, wallet?.address] + [name, chainId, wallet?.address] ) const shouldUseCrossChainProvider = useShouldUseCrossChainProvider( selectedToken, - selectedChain, Network.ETHEREUM ) return useGasCost( Network.ETHEREUM, - providerTokens, - selectedChain, + chainNativeToken, + chainId, shouldUseCrossChainProvider, wallet, estimateGas diff --git a/webapp/src/components/Modals/BuyWithCryptoModal/utils.ts b/webapp/src/components/Modals/BuyWithCryptoModal/utils.ts index ec78ce05e..a32b51ea0 100644 --- a/webapp/src/components/Modals/BuyWithCryptoModal/utils.ts +++ b/webapp/src/components/Modals/BuyWithCryptoModal/utils.ts @@ -1,11 +1,7 @@ -import { ChainId, Item, Network, Order } from '@dcl/schemas' +import { ChainId, Network, Order, Item } from '@dcl/schemas' +import { BigNumber, ethers } from 'ethers' import { Env } from '@dcl/ui-env' -import { - ChainData, - CrossChainProvider, - Route, - Token -} from 'decentraland-transactions/crossChain' +import { ChainData, Token } from 'decentraland-transactions/crossChain' import { Wallet } from 'decentraland-dapps/dist/modules/wallet/types' import { ContractName, @@ -14,10 +10,8 @@ import { } from 'decentraland-transactions' import { getNetwork } from '@dcl/schemas/dist/dapps/chain-id' import { getNetworkProvider } from 'decentraland-dapps/dist/lib/eth' -import { Asset } from '../../../modules/asset/types' import { config } from '../../../config' -import { BigNumber, ethers } from 'ethers' -import { isNFT } from '../../../modules/asset/utils' +import { NFT } from '../../../modules/nft/types' export const getShouldUseMetaTx = ( assetChainId: ChainId, @@ -152,7 +146,7 @@ export const DEFAULT_CHAINS = [ } }, { - chainId: ChainId.BSC_MAINNET, + chainId: ChainId.BSC_MAINNET.toString(), networkName: 'BNB Chain', nativeCurrency: { name: 'BNB', @@ -163,7 +157,7 @@ export const DEFAULT_CHAINS = [ } }, { - chainId: ChainId.FANTOM_MAINNET, + chainId: ChainId.FANTOM_MAINNET.toString(), networkName: 'Fantom', nativeCurrency: { name: 'FTM', @@ -207,69 +201,40 @@ export const getDefaultChains = () => { return DEFAULT_CHAINS } -export const getBuyNftRoute = ( - crossChainProvider: CrossChainProvider, - baseRouteConfig: any, - order: Order -): Promise => - crossChainProvider.getBuyNFTRoute({ - ...baseRouteConfig, - nft: { - collectionAddress: order.contractAddress, - tokenId: order.tokenId, - price: order.price - }, - toAmount: order.price, - toChain: order.chainId - }) - -export const getMintNFTRoute = ( - crossChainProvider: CrossChainProvider, - baseRouteConfig: any, +export const estimateMintNftGas = async ( + selectedChain: ChainId, + wallet: Wallet, asset: Item -): Promise => - crossChainProvider.getMintNFTRoute({ - ...baseRouteConfig, - item: { - collectionAddress: asset.contractAddress, - itemId: asset.itemId, - price: asset.price - }, - toAmount: asset.price, - toChain: asset.chainId - }) +): Promise => { + const networkProvider = await getNetworkProvider(selectedChain) + const provider = new ethers.providers.Web3Provider(networkProvider) -export const estimateTransactionGas = async ( + const contract = getContract(ContractName.CollectionStore, asset.chainId) + const c = new ethers.Contract(contract.address, contract.abi, provider) + return c.estimateGas.buy( + [[asset.contractAddress, [asset.itemId], [asset.price], [wallet.address]]], + { from: wallet.address } + ) +} + +export const estimateBuyNftGas = async ( selectedChain: ChainId, wallet: Wallet, - asset: Asset, - order?: Order + asset: NFT, + order: Order ) => { const networkProvider = await getNetworkProvider(selectedChain) const provider = new ethers.providers.Web3Provider(networkProvider) - let estimation: BigNumber | undefined - if (order && isNFT(asset)) { - const contractName = getContractName(order.marketplaceAddress) - const contract = getContract(contractName, order.chainId) - const c = new ethers.Contract(contract.address, contract.abi, provider) - estimation = await c.estimateGas.executeOrder( - asset.contractAddress, - asset.tokenId, - order.price, - { from: wallet.address } - ) - } else if (!isNFT(asset)) { - const contract = getContract(ContractName.CollectionStore, asset.chainId) - const c = new ethers.Contract(contract.address, contract.abi, provider) - estimation = await c.estimateGas.buy( - [ - [asset.contractAddress, [asset.itemId], [asset.price], [wallet.address]] - ], - { from: wallet.address } - ) - } - return estimation + const contractName = getContractName(order.marketplaceAddress) + const contract = getContract(contractName, order.chainId) + const c = new ethers.Contract(contract.address, contract.abi, provider) + return c.estimateGas.executeOrder( + asset.contractAddress, + asset.tokenId, + order.price, + { from: wallet.address } + ) } export const estimateNameMintingGas = async (