import { render, renderHook } from '@testing-library/react-native' import BigNumber from 'bignumber.js' import React from 'react' import { Text, View } from 'react-native' import { Provider } from 'react-redux' import { getFeatureGate, getMultichainFeatures } from 'src/statsig' import { StatsigFeatureGates } from 'src/statsig/types' import { useAmountAsUsd, useCashInTokens, useCashOutTokens, useLocalToTokenAmount, useSwappableTokens, useTokenInfo, useTokenPricesAreStale, useTokensInfo, useTokenToLocalAmount, } from 'src/tokens/hooks' import { TokenBalance } from 'src/tokens/slice' import { NetworkId } from 'src/transactions/types' import { createMockStore } from 'test/utils' import { mockAccount, mockCeloTokenId, mockCeurTokenId, mockCrealTokenId, mockCusdTokenId, mockPoofTokenId, mockTokenBalances, mockUSDCTokenId, } from 'test/values' jest.mock('src/statsig') beforeEach(() => { jest.clearAllMocks() jest.mocked(getFeatureGate).mockReturnValue(true) jest.mocked(getMultichainFeatures).mockReturnValue({ showCico: [NetworkId['celo-alfajores']], showSend: [NetworkId['celo-alfajores']], showSwap: [NetworkId['celo-alfajores']], showBalances: [NetworkId['celo-alfajores']], }) }) const tokenAddressWithPriceAndBalance = '0x001' const tokenIdWithPriceAndBalance = `celo-alfajores:${tokenAddressWithPriceAndBalance}` const tokenAddressWithoutBalance = '0x002' const tokenIdWithoutBalance = `celo-alfajores:${tokenAddressWithoutBalance}` const ethTokenId = 'ethereum-sepolia:native' function TestComponent({ tokenId }: { tokenId: string }) { const tokenAmount = useLocalToTokenAmount(new BigNumber(1), tokenId) const localAmount = useTokenToLocalAmount(new BigNumber(1), tokenId) const usdAmount = useAmountAsUsd(new BigNumber(1), tokenId) const tokenPricesAreStale = useTokenPricesAreStale([NetworkId['celo-alfajores']]) return ( {tokenAmount?.toNumber()} {localAmount?.toNumber()} {usdAmount?.toNumber()} {tokenPricesAreStale} ) } function TokenHookTestComponent({ hook }: { hook: () => TokenBalance[] }) { const tokens = hook() return {tokens.map((token) => token.tokenId)} } const store = (usdToLocalRate: string | null, priceFetchedAt: number) => createMockStore({ tokens: { tokenBalances: { [tokenIdWithPriceAndBalance]: { address: tokenAddressWithPriceAndBalance, tokenId: tokenIdWithPriceAndBalance, networkId: NetworkId['celo-alfajores'], symbol: 'T1', balance: '0', priceUsd: '5', priceFetchedAt, }, [tokenIdWithoutBalance]: { address: tokenAddressWithoutBalance, tokenId: tokenIdWithoutBalance, networkId: NetworkId['celo-alfajores'], symbol: 'T2', priceUsd: '5', balance: null, priceFetchedAt, }, }, }, localCurrency: { usdToLocalRate, }, }) const storeWithMultipleNetworkTokens = (walletAddress?: string) => createMockStore({ web3: { account: walletAddress ?? mockAccount, }, tokens: { tokenBalances: { ...mockTokenBalances, [mockCrealTokenId]: { ...mockTokenBalances[mockCrealTokenId], balance: '1', minimumAppVersionToSwap: '1.0.0', }, [mockCeloTokenId]: { ...mockTokenBalances[mockCeloTokenId], balance: '1', isSwappable: true, }, [ethTokenId]: { tokenId: ethTokenId, symbol: 'ETH', balance: '10', priceUsd: '5', networkId: NetworkId['ethereum-sepolia'], priceFetchedAt: Date.now(), isCashInEligible: true, isCashOutEligible: true, minimumAppVersionToSwap: '0.0.1', }, }, }, positions: { positions: [ { type: 'app-token' as const, networkId: NetworkId['celo-alfajores'], tokenId: 'celo-alfajores:0xa', address: '0xa', priceUsd: '60', balance: '3', displayProps: { title: 'Title', }, tokens: [ { networkId: NetworkId['celo-alfajores'], tokenId: 'celo-alfajores:0xb', balance: '1', priceUsd: '30', }, { networkId: NetworkId['celo-alfajores'], tokenId: 'celo-alfajores:0xc', balance: '2', priceUsd: '20', }, ], }, ], }, }) describe('token to fiat exchanges', () => { it('maps correctly if all the info is available', async () => { const { getByTestId } = render( ) const tokenAmount = getByTestId('tokenAmount') expect(tokenAmount.props.children).toEqual(0.1) const localAmount = getByTestId('localAmount') expect(localAmount.props.children).toEqual(10) const usdAmount = getByTestId('usdAmount') expect(usdAmount.props.children).toEqual(5) const pricesStale = getByTestId('pricesStale') expect(pricesStale.props.children).toEqual(false) }) it('returns undefined if there is no balance set', async () => { const { getByTestId } = render( ) const tokenAmount = getByTestId('tokenAmount') expect(tokenAmount.props.children).toBeUndefined() const localAmount = getByTestId('localAmount') expect(localAmount.props.children).toBeUndefined() const usdAmount = getByTestId('usdAmount') expect(usdAmount.props.children).toBeUndefined() const pricesStale = getByTestId('pricesStale') expect(pricesStale.props.children).toEqual(false) }) it('returns undefined if there is no exchange rate', async () => { const { getByTestId } = render( ) const tokenAmount = getByTestId('tokenAmount') expect(tokenAmount.props.children).toBeUndefined() const localAmount = getByTestId('localAmount') expect(localAmount.props.children).toBeUndefined() // USD amount doesn't use the exchange rate const usdAmount = getByTestId('usdAmount') expect(usdAmount.props.children).toEqual(5) const pricesStale = getByTestId('pricesStale') expect(pricesStale.props.children).toEqual(false) }) it('returns undefined if the token doesnt exist', async () => { const { getByTestId } = render( ) const tokenAmount = getByTestId('tokenAmount') expect(tokenAmount.props.children).toBeUndefined() const localAmount = getByTestId('localAmount') expect(localAmount.props.children).toBeUndefined() const usdAmount = getByTestId('usdAmount') expect(usdAmount.props.children).toBeUndefined() const pricesStale = getByTestId('pricesStale') expect(pricesStale.props.children).toEqual(false) }) it('shows prices are stale', async () => { const { getByTestId } = render( ) const pricesStale = getByTestId('pricesStale') expect(pricesStale.props.children).toEqual(true) }) }) describe('useSwappableTokens', () => { it('returns sorted swappable tokens for the non-holdout group', () => { jest .mocked(getFeatureGate) .mockImplementation( (featureGate) => featureGate !== StatsigFeatureGates.SHUFFLE_SWAP_TOKENS_ORDER ) const { result } = renderHook(() => useSwappableTokens(), { wrapper: (component) => ( {component?.children ? component.children : component} ), }) expect(result.current.swappableToTokens.map((token) => token.tokenId)).toEqual([ mockCeloTokenId, ]) expect(result.current.swappableFromTokens.map((token) => token.tokenId)).toEqual([ mockCeloTokenId, mockPoofTokenId, mockCrealTokenId, ]) expect(result.current.areSwapTokensShuffled).toBe(false) }) it('returns sorted tokens with balance for multiple networks for the non-holdout group', () => { jest .mocked(getFeatureGate) .mockImplementation( (featureGate) => featureGate !== StatsigFeatureGates.SHUFFLE_SWAP_TOKENS_ORDER ) jest.mocked(getMultichainFeatures).mockReturnValueOnce({ showSwap: [NetworkId['celo-alfajores'], NetworkId['ethereum-sepolia']], }) const { result } = renderHook(() => useSwappableTokens(), { wrapper: (component) => ( {component?.children ? component.children : component} ), }) expect(result.current.swappableToTokens.map((token) => token.tokenId)).toEqual([ ethTokenId, mockCeloTokenId, ]) expect(result.current.swappableFromTokens.map((token) => token.tokenId)).toEqual([ ethTokenId, mockCeloTokenId, mockPoofTokenId, mockCrealTokenId, ]) }) it('returns deterministically shuffled tokens for each user in the holdout group', () => { jest.mocked(getMultichainFeatures).mockReturnValue({ showSwap: [NetworkId['celo-alfajores'], NetworkId['ethereum-sepolia']], }) const expectedToTokens1 = [mockCeloTokenId, ethTokenId] const expectedFromTokens1 = [mockPoofTokenId, mockCeloTokenId, ethTokenId, mockCrealTokenId] const expectedToTokens2 = [ethTokenId, mockCeloTokenId] const expectedFromTokens2 = [mockCrealTokenId, ethTokenId, mockPoofTokenId, mockCeloTokenId] const { result: result1 } = renderHook(() => useSwappableTokens(), { wrapper: (component) => ( {component?.children ? component.children : component} ), }) const { result: result2 } = renderHook(() => useSwappableTokens(), { wrapper: (component) => ( {component?.children ? component.children : component} ), }) expect(result1.current.swappableToTokens.map((token) => token.tokenId)).toEqual( expectedToTokens1 ) expect(result1.current.swappableFromTokens.map((token) => token.tokenId)).toEqual( expectedFromTokens1 ) expect(result1.current.areSwapTokensShuffled).toBe(true) expect(result2.current.swappableToTokens.map((token) => token.tokenId)).toEqual( expectedToTokens2 ) expect(result2.current.swappableFromTokens.map((token) => token.tokenId)).toEqual( expectedFromTokens2 ) expect(result2.current.areSwapTokensShuffled).toBe(true) }) }) describe('useCashInTokens', () => { it('returns tokens eligible for cash in', () => { const { getByTestId } = render( ) expect(getByTestId('tokenIDs').props.children).toEqual([ mockCeurTokenId, mockCusdTokenId, mockCeloTokenId, mockCrealTokenId, ]) }) it('returns tokens eligible for cash in for multiple networks', () => { jest.mocked(getMultichainFeatures).mockReturnValueOnce({ showCico: [NetworkId['celo-alfajores'], NetworkId['ethereum-sepolia']], }) const { getByTestId } = render( ) expect(getByTestId('tokenIDs').props.children).toEqual([ mockCeurTokenId, mockCusdTokenId, mockCeloTokenId, mockCrealTokenId, ethTokenId, mockUSDCTokenId, ]) }) }) describe('useCashOutTokens', () => { it('returns tokens for eligible for cash out', () => { const { getByTestId } = render( ) expect(getByTestId('tokenIDs').props.children).toEqual([mockCeloTokenId]) }) it('returns tokens eligible for cash out for multiple networks', () => { jest.mocked(getMultichainFeatures).mockReturnValueOnce({ showCico: [NetworkId['celo-alfajores'], NetworkId['ethereum-sepolia']], }) const { getByTestId } = render( ) expect(getByTestId('tokenIDs').props.children).toEqual([mockCeloTokenId, ethTokenId]) }) }) describe('useTokenInfo', () => { it('returns the token when it exists', () => { const { result } = renderHook(() => useTokenInfo(mockCeloTokenId), { wrapper: (component) => ( {component?.children ? component.children : component} ), }) expect(result.current?.tokenId).toEqual(mockCeloTokenId) }) it('returns position tokens when they exist', () => { const { result } = renderHook(() => useTokenInfo('celo-alfajores:0xb'), { wrapper: (component) => ( {component?.children ? component.children : component} ), }) expect(result.current?.tokenId).toEqual('celo-alfajores:0xb') }) it('returns undefined if the tokenId is not found', () => { const { result } = renderHook(() => useTokenInfo(undefined), { wrapper: (component) => ( {component?.children ? component.children : component} ), }) expect(result.current).toBeUndefined() }) }) describe('useTokensInfo', () => { it('returns the token when it exists', () => { const { result } = renderHook(() => useTokensInfo([mockCeloTokenId, mockUSDCTokenId]), { wrapper: (component) => ( {component?.children ? component.children : component} ), }) expect(result.current[0]?.tokenId).toEqual(mockCeloTokenId) expect(result.current[1]?.tokenId).toEqual(mockUSDCTokenId) }) it('returns position tokens when they exist', () => { const { result } = renderHook( () => useTokensInfo(['celo-alfajores:0xb', 'celo-alfajores:0xc']), { wrapper: (component) => ( {component?.children ? component.children : component} ), } ) expect(result.current[0]?.tokenId).toEqual('celo-alfajores:0xb') expect(result.current[1]?.tokenId).toEqual('celo-alfajores:0xc') }) it('returns empty array if the tokenId is not found', () => { const { result } = renderHook(() => useTokensInfo(['iDoNotExist']), { wrapper: (component) => ( {component?.children ? component.children : component} ), }) expect(result.current).toEqual([]) }) })