From 4cdc78321fe2059adef26334915dff0543e2101d Mon Sep 17 00:00:00 2001 From: brdy <41711440+BrodyHughes@users.noreply.github.com> Date: Sun, 25 Aug 2024 13:46:56 -0500 Subject: [PATCH] Revert "Revert "Brody/swap v2 e2e (#5915)" (#5987)" (#5988) * Revert "Revert "Brody/swap v2 e2e (#5915)" (#5987)" This reverts commit fdcdd09dcf416da82e50be95035fa91a960d7a41. * fix test * Revert "fix test" This reverts commit a7f5b1ccdfe7f5b334bf9c014873607d5b26e2f4. * fix * fix test * typecast to fix lint * matthew :D * test this * Revert "test this" This reverts commit ab6b11dcb3844d1b2439902596b2da9c6143a19c. * fix lint * fix user assets not working on swaps e2e * . * oop * lint * lel our @/references folder is fked * fix some things * fix user assets in testnet mode * fix up e2e * readd memo * . * revert change to swipeUntilVisible * re add very long delay * remove react memo --------- Co-authored-by: Matthew Wall --- e2e/3_homeScreen.spec.ts | 14 +- e2e/9_swaps.spec.ts | 144 ++++++++++++++++++ e2e/helpers.ts | 34 ++++- package.json | 7 +- .../screens/Swap/components/CoinRow.tsx | 6 +- .../Swap/components/SwapActionButton.tsx | 7 +- .../Swap/components/SwapBackground.tsx | 11 +- .../Swap/components/SwapBottomPanel.tsx | 1 + .../Swap/components/SwapInputAsset.tsx | 12 +- .../Swap/components/SwapOutputAsset.tsx | 1 + .../components/TokenList/TokenToBuyList.tsx | 7 +- src/__swaps__/screens/Swap/constants.ts | 32 ++-- .../screens/Swap/hooks/useAssetsToSell.ts | 6 +- .../Swap/hooks/useSwapInputsController.ts | 1 - .../Swap/resources/assets/userAssets.ts | 31 +++- src/__swaps__/types/chains.ts | 4 + src/components/activity-list/ActivityList.js | 1 - src/components/animations/animationConfigs.ts | 44 +++--- .../FastComponents/FastBalanceCoinRow.tsx | 2 +- src/config/experimental.ts | 2 +- src/design-system/components/Box/Box.tsx | 15 +- src/handlers/swap.ts | 4 +- src/navigation/SwipeNavigator.tsx | 8 +- src/references/chain-assets.json | 19 ++- src/references/index.ts | 29 ++-- src/references/testnet-assets-by-chain.ts | 70 +++++++++ src/resources/assets/hardhatAssets.ts | 79 +++++++++- src/state/assets/userAssets.ts | 10 +- src/state/sync/UserAssetsSync.tsx | 6 +- 29 files changed, 501 insertions(+), 106 deletions(-) create mode 100644 e2e/9_swaps.spec.ts create mode 100644 src/references/testnet-assets-by-chain.ts diff --git a/e2e/3_homeScreen.spec.ts b/e2e/3_homeScreen.spec.ts index c090519ded0..94fea2546f0 100644 --- a/e2e/3_homeScreen.spec.ts +++ b/e2e/3_homeScreen.spec.ts @@ -5,8 +5,9 @@ import { checkIfExists, checkIfExistsByText, swipe, - waitAndTap, afterAllcleanApp, + tap, + delayTime, } from './helpers'; const RAINBOW_TEST_WALLET = 'rainbowtestwallet.eth'; @@ -41,19 +42,20 @@ describe('Home Screen', () => { }); it('tapping "Swap" opens the swap screen', async () => { - await waitAndTap('swap-button'); + await tap('swap-button'); + await delayTime('long'); await checkIfExists('swap-screen'); - await swipe('swap-screen', 'down', 'slow'); + await swipe('swap-screen', 'down', 'fast'); }); it('tapping "Send" opens the send screen', async () => { - await waitAndTap('send-button'); + await tap('send-button'); await checkIfVisible('send-asset-form-field'); - await swipe('send-asset-form-field', 'down'); + await swipe('send-asset-form-field', 'down', 'fast'); }); it('tapping "Copy" shows copy address toast', async () => { - await waitAndTap('receive-button'); + await tap('receive-button'); await checkIfVisible('address-copied-toast'); }); }); diff --git a/e2e/9_swaps.spec.ts b/e2e/9_swaps.spec.ts new file mode 100644 index 00000000000..f899fbdc68f --- /dev/null +++ b/e2e/9_swaps.spec.ts @@ -0,0 +1,144 @@ +/* + * // Other tests to consider: + * - Flip assets + * - exchange button onPress + * - disable button states once https://github.com/rainbow-me/rainbow/pull/5785 gets merged + * - swap execution + * - token search (both from userAssets and output token list) + * - custom gas panel + * - flashbots + * - slippage + * - explainer sheets + * - switching wallets inside of swap screen + */ + +import { + importWalletFlow, + sendETHtoTestWallet, + checkIfVisible, + beforeAllcleanApp, + afterAllcleanApp, + fetchElementAttributes, + tap, + tapByText, + delayTime, + swipeUntilVisible, + tapAndLongPressByText, + tapAndLongPress, + swipe, +} from './helpers'; + +import { expect } from '@jest/globals'; +import { WALLET_VARS } from './testVariables'; + +describe('Swap Sheet Interaction Flow', () => { + beforeAll(async () => { + await beforeAllcleanApp({ hardhat: true }); + }); + afterAll(async () => { + await afterAllcleanApp({ hardhat: true }); + }); + + it('Import a wallet and go to welcome', async () => { + await importWalletFlow(WALLET_VARS.EMPTY_WALLET.PK); + }); + + it('Should send ETH to test wallet', async () => { + // send 20 eth + await sendETHtoTestWallet(); + }); + + it('Should show Hardhat Toast after pressing Connect To Hardhat', async () => { + await tap('dev-button-hardhat'); + await checkIfVisible('testnet-toast-Hardhat'); + + // doesn't work atm + // validate it has the expected funds of 20 eth + // const attributes = await fetchElementAttributes('fast-coin-info'); + // expect(attributes.label).toContain('Ethereum'); + // expect(attributes.label).toContain('20'); + }); + + it('Should open swap screen with 50% inputAmount for inputAsset', async () => { + await device.disableSynchronization(); + await tap('swap-button'); + await delayTime('long'); + + await swipeUntilVisible('token-to-buy-dai-1', 'token-to-buy-list', 'up', 100); + await swipe('token-to-buy-list', 'up', 'slow', 0.1); + + await tap('token-to-buy-dai-1'); + await delayTime('medium'); + const swapInput = await fetchElementAttributes('swap-asset-input'); + + expect(swapInput.label).toContain('ETH'); + expect(swapInput.label).toContain('10'); + }); + + it('Should be able to go to review and execute a swap', async () => { + await tap('swap-bottom-action-button'); + const inputAssetActionButton = await fetchElementAttributes('swap-input-asset-action-button'); + const outputAssetActionButton = await fetchElementAttributes('swap-output-asset-action-button'); + const holdToSwapButton = await fetchElementAttributes('swap-bottom-action-button'); + + expect(inputAssetActionButton.label).toBe('ETH 􀆏'); + expect(outputAssetActionButton.label).toBe('DAI 􀆏'); + expect(holdToSwapButton.label).toBe('􀎽 Hold to Swap'); + + await tapAndLongPress('swap-bottom-action-button', 1500); + + // TODO: This doesn't work so need to figure this out eventually... + // await checkIfVisible('profile-screen'); + }); + + it.skip('Should be able to verify swap is happening', async () => { + // await delayTime('very-long'); + // const activityListElements = await fetchElementAttributes('wallet-activity-list'); + // expect(activityListElements.label).toContain('ETH'); + // expect(activityListElements.label).toContain('DAI'); + // await tapByText('Swapping'); + // await delayTime('long'); + // const transactionSheet = await checkIfVisible('transaction-details-sheet'); + // expect(transactionSheet).toBeTruthy(); + }); + + it.skip('Should open swap screen from ProfileActionRowButton with largest user asset', async () => { + /** + * tap swap button + * wait for Swap header to be visible + * grab highest user asset balance from userAssetsStore + * expect inputAsset.uniqueId === highest user asset uniqueId + */ + }); + + it.skip('Should open swap screen from asset chart with that asset selected', async () => { + /** + * tap any user asset (store const uniqueId here) + * wait for Swap header to be visible + * expect inputAsset.uniqueId === const uniqueId ^^ + */ + }); + + it.skip('Should open swap screen from dapp browser control panel with largest user asset', async () => { + /** + * tap swap button + * wait for Swap header to be visible + * grab highest user asset balance from userAssetsStore + * expect inputAsset.uniqueId === highest user asset uniqueId + */ + }); + + it.skip('Should not be able to type in output amount if cross-chain quote', async () => { + /** + * tap swap button + * wait for Swap header to be visible + * select different chain in output list chain selector + * select any asset in output token list + * focus output amount + * attempt to type any number in the SwapNumberPad + * attempt to remove a character as well + * + * ^^ expect both of those to not change the outputAmount + */ + }); +}); diff --git a/e2e/helpers.ts b/e2e/helpers.ts index 55344f6052c..dd82d8f2754 100644 --- a/e2e/helpers.ts +++ b/e2e/helpers.ts @@ -4,8 +4,9 @@ import { JsonRpcProvider } from '@ethersproject/providers'; import { Wallet } from '@ethersproject/wallet'; import { expect, device, element, by, waitFor } from 'detox'; import { parseEther } from '@ethersproject/units'; +import { IosElementAttributes, AndroidElementAttributes } from 'detox/detox'; -const TESTING_WALLET = '0x3Cb462CDC5F809aeD0558FBEe151eD5dC3D3f608'; +const TESTING_WALLET = '0x3637f053D542E6D00Eee42D656dD7C59Fa33a62F'; const DEFAULT_TIMEOUT = 20_000; const android = device.getPlatform() === 'android'; @@ -70,6 +71,16 @@ export async function tap(elementId: string | RegExp) { } } +interface CustomElementAttributes { + elements: Array; +} + +type ElementAttributes = IosElementAttributes & AndroidElementAttributes & CustomElementAttributes; + +export const fetchElementAttributes = async (testId: string): Promise => { + return (await element(by.id(testId)).getAttributes()) as ElementAttributes; +}; + export async function waitAndTap(elementId: string | RegExp, timeout = DEFAULT_TIMEOUT) { await delayTime('medium'); try { @@ -188,17 +199,17 @@ export async function clearField(elementId: string | RegExp) { } } -export async function tapAndLongPress(elementId: string | RegExp) { +export async function tapAndLongPress(elementId: string | RegExp, duration?: number) { try { - return await element(by.id(elementId)).longPress(); + return await element(by.id(elementId)).longPress(duration); } catch (error) { throw new Error(`Error long-pressing element by id "${elementId}": ${error}`); } } -export async function tapAndLongPressByText(text: string | RegExp) { +export async function tapAndLongPressByText(text: string | RegExp, duration?: number) { try { - return await element(by.text(text)).longPress(); + return await element(by.text(text)).longPress(duration); } catch (error) { throw new Error(`Error long-pressing element by text "${text}": ${error}`); } @@ -263,15 +274,22 @@ export async function scrollTo(scrollviewId: string | RegExp, edge: Direction) { } } -export async function swipeUntilVisible(elementId: string | RegExp, scrollViewId: string, direction: Direction, pctVisible = 75) { +export async function swipeUntilVisible( + elementId: string | RegExp, + scrollViewId: string, + direction: Direction, + percentageVisible?: number +) { let stop = false; + while (!stop) { try { await waitFor(element(by.id(elementId))) - .toBeVisible(pctVisible) + .toBeVisible(percentageVisible) .withTimeout(500); + stop = true; - } catch { + } catch (e) { await swipe(scrollViewId, direction, 'slow', 0.2); } } diff --git a/package.json b/package.json index 3dddbc815ca..698efeb2060 100644 --- a/package.json +++ b/package.json @@ -23,9 +23,10 @@ "nuke": "./scripts/nuke.sh", "detox:android": "detox build -c android.emu.debug && detox test -c android.emu.debug --loglevel verbose", "detox:android:release": "detox build -c android.emu.release && detox test -c android.emu.release", - "detox:ios:tests": "detox test -c ios.sim.debug --maxWorkers 3 -- --bail 1", - "detox:ios": "detox build -c ios.sim.debug | xcpretty --color && yarn detox:ios:tests", - "detox:ios:release": "detox build -c ios.sim.release && detox test -c ios.sim.release --maxWorkers 3 -- --bail 1", + "detox:ios:build": "detox build -c ios.sim.debug | xcpretty --color ", + "detox:ios:tests": "detox test -c ios.sim.debug --maxWorkers 2 -- --bail 1", + "detox:ios": "yarn detox:ios:build && yarn detox:ios:tests", + "detox:ios:release": "detox build -c ios.sim.release && detox test -c ios.sim.release --maxWorkers 2 -- --bail 1", "ds:install": "cd src/design-system/docs && yarn install", "ds": "cd src/design-system/docs && yarn dev", "fast": "yarn install && yarn setup && yarn install-pods-fast", diff --git a/src/__swaps__/screens/Swap/components/CoinRow.tsx b/src/__swaps__/screens/Swap/components/CoinRow.tsx index 02e439c90db..0728370c7aa 100644 --- a/src/__swaps__/screens/Swap/components/CoinRow.tsx +++ b/src/__swaps__/screens/Swap/components/CoinRow.tsx @@ -52,6 +52,7 @@ interface InputCoinRowProps { onPress: (asset: ParsedSearchAsset | null) => void; output?: false | undefined; uniqueId: string; + testID?: string; } type PartialAsset = Pick; @@ -62,11 +63,12 @@ interface OutputCoinRowProps extends PartialAsset { output: true; nativePriceChange?: string; isTrending?: boolean; + testID?: string; } type CoinRowProps = InputCoinRowProps | OutputCoinRowProps; -export function CoinRow({ isFavorite, onPress, output, uniqueId, ...assetProps }: CoinRowProps) { +export function CoinRow({ isFavorite, onPress, output, uniqueId, testID, ...assetProps }: CoinRowProps) { const inputAsset = userAssetsStore(state => (output ? undefined : state.getUserAsset(uniqueId))); const outputAsset = output ? (assetProps as PartialAsset) : undefined; @@ -116,7 +118,7 @@ export function CoinRow({ isFavorite, onPress, output, uniqueId, ...assetProps } if (!address || !chainId) return null; return ( - + diff --git a/src/__swaps__/screens/Swap/components/SwapActionButton.tsx b/src/__swaps__/screens/Swap/components/SwapActionButton.tsx index e407de019ab..a6a8d913966 100644 --- a/src/__swaps__/screens/Swap/components/SwapActionButton.tsx +++ b/src/__swaps__/screens/Swap/components/SwapActionButton.tsx @@ -34,6 +34,7 @@ function SwapButton({ disabled, opacity, children, + testID, }: { asset: DerivedValue; borderRadius?: number; @@ -47,6 +48,7 @@ function SwapButton({ disabled?: DerivedValue; opacity?: DerivedValue; children?: React.ReactNode; + testID?: string; }) { const { isDarkMode } = useColorMode(); const fallbackColor = useForegroundColor('label'); @@ -110,6 +112,7 @@ function SwapButton({ return ( ; @@ -248,6 +252,7 @@ export const SwapActionButton = ({ style?: ViewStyle; disabled?: DerivedValue; opacity?: DerivedValue; + testID?: string; }) => { const disabledWrapper = useAnimatedStyle(() => { return { @@ -268,7 +273,7 @@ export const SwapActionButton = ({ style={[hugContent && feedActionButtonStyles.buttonWrapper, style]} > {/* eslint-disable-next-line react/jsx-props-no-spreading */} - + {holdProgress && } diff --git a/src/__swaps__/screens/Swap/components/SwapBackground.tsx b/src/__swaps__/screens/Swap/components/SwapBackground.tsx index 8fc56cf4f0e..594b33f61bc 100644 --- a/src/__swaps__/screens/Swap/components/SwapBackground.tsx +++ b/src/__swaps__/screens/Swap/components/SwapBackground.tsx @@ -1,11 +1,11 @@ import { Canvas, Rect, LinearGradient, vec, Paint } from '@shopify/react-native-skia'; import React from 'react'; -import { StyleSheet } from 'react-native'; +import { StyleSheet, View } from 'react-native'; import { useDerivedValue, withTiming } from 'react-native-reanimated'; import { ScreenCornerRadius } from 'react-native-screen-corner-radius'; import { TIMING_CONFIGS } from '@/components/animations/animationConfigs'; import { useColorMode } from '@/design-system'; -import { IS_ANDROID } from '@/env'; +import { IS_ANDROID, IS_TEST } from '@/env'; import { useSwapContext } from '@/__swaps__/screens/Swap/providers/swap-provider'; import { getColorValueForThemeWorklet, getTintedBackgroundColor } from '@/__swaps__/utils/swaps'; import { DEVICE_HEIGHT, DEVICE_WIDTH } from '@/utils/deviceUtils'; @@ -18,12 +18,15 @@ export const SwapBackground = () => { const { internalSelectedInputAsset, internalSelectedOutputAsset } = useSwapContext(); const animatedTopColor = useDerivedValue(() => { + if (IS_TEST) return getColorValueForThemeWorklet(DEFAULT_BACKGROUND_COLOR, isDarkMode, true); return withTiming( getColorValueForThemeWorklet(internalSelectedInputAsset.value?.tintedBackgroundColor || DEFAULT_BACKGROUND_COLOR, isDarkMode, true), TIMING_CONFIGS.slowFadeConfig ); }); + const animatedBottomColor = useDerivedValue(() => { + if (IS_TEST) return getColorValueForThemeWorklet(DEFAULT_BACKGROUND_COLOR, isDarkMode, true); return withTiming( getColorValueForThemeWorklet(internalSelectedOutputAsset.value?.tintedBackgroundColor || DEFAULT_BACKGROUND_COLOR, isDarkMode, true), TIMING_CONFIGS.slowFadeConfig @@ -34,6 +37,10 @@ export const SwapBackground = () => { return [animatedTopColor.value, animatedBottomColor.value]; }); + if (IS_TEST) { + return ; + } + return ( diff --git a/src/__swaps__/screens/Swap/components/SwapBottomPanel.tsx b/src/__swaps__/screens/Swap/components/SwapBottomPanel.tsx index d41f5d28733..87298a06eb8 100644 --- a/src/__swaps__/screens/Swap/components/SwapBottomPanel.tsx +++ b/src/__swaps__/screens/Swap/components/SwapBottomPanel.tsx @@ -98,6 +98,7 @@ export function SwapBottomPanel() { } style={styles.inputTextMask}> - + {SwapInputController.formattedInputAmount} @@ -123,7 +131,7 @@ export function SwapInputAsset() { return ( - + diff --git a/src/__swaps__/screens/Swap/components/SwapOutputAsset.tsx b/src/__swaps__/screens/Swap/components/SwapOutputAsset.tsx index ac2dc4a4aba..74ffb1cc923 100644 --- a/src/__swaps__/screens/Swap/components/SwapOutputAsset.tsx +++ b/src/__swaps__/screens/Swap/components/SwapOutputAsset.tsx @@ -39,6 +39,7 @@ function SwapOutputActionButton() { return ( { if (isLoading) return null; + const getFormattedTestId = (name: string, chainId: ChainId) => { + return `token-to-buy-${name}-${chainId}`.toLowerCase().replace(/\s+/g, '-'); + }; + return ( - + } @@ -160,6 +164,7 @@ export const TokenToBuyList = () => { } return ( (config: T): T => { + if (!IS_TEST) return config; + return { + ...config, + duration: 0, + } as T; +}; + +export const buttonPressConfig = disableForTestingEnvironment({ duration: 160, easing: Easing.bezier(0.25, 0.46, 0.45, 0.94) }); +export const caretConfig = disableForTestingEnvironment({ duration: 300, easing: Easing.bezier(0.87, 0, 0.13, 1) }); +export const fadeConfig = disableForTestingEnvironment({ duration: 200, easing: Easing.bezier(0.22, 1, 0.36, 1) }); +export const pulsingConfig = disableForTestingEnvironment({ duration: 1000, easing: Easing.bezier(0.37, 0, 0.63, 1) }); +export const sliderConfig = disableForTestingEnvironment({ damping: 40, mass: 1.25, stiffness: 450 }); +export const slowFadeConfig = disableForTestingEnvironment({ duration: 300, easing: Easing.bezier(0.22, 1, 0.36, 1) }); +export const snappySpringConfig = disableForTestingEnvironment({ damping: 100, mass: 0.8, stiffness: 275 }); +export const snappierSpringConfig = disableForTestingEnvironment({ damping: 42, mass: 0.8, stiffness: 800 }); +export const springConfig = disableForTestingEnvironment({ damping: 100, mass: 1.2, stiffness: 750 }); // // /---- END animation configs ----/ // diff --git a/src/__swaps__/screens/Swap/hooks/useAssetsToSell.ts b/src/__swaps__/screens/Swap/hooks/useAssetsToSell.ts index 13f6d54e02d..a2d181c9a08 100644 --- a/src/__swaps__/screens/Swap/hooks/useAssetsToSell.ts +++ b/src/__swaps__/screens/Swap/hooks/useAssetsToSell.ts @@ -10,6 +10,7 @@ import { useUserAssets } from '@/__swaps__/screens/Swap/resources/assets'; import { ParsedAssetsDictByChain, ParsedSearchAsset, UserAssetFilter } from '@/__swaps__/types/assets'; import { useAccountSettings, useDebounce } from '@/hooks'; import { userAssetsStore } from '@/state/assets/userAssets'; +import { getIsHardhatConnected } from '@/handlers/web3'; const sortBy = (by: UserAssetFilter) => { switch (by) { @@ -21,7 +22,9 @@ const sortBy = (by: UserAssetFilter) => { }; export const useAssetsToSell = () => { - const { accountAddress: currentAddress, nativeCurrency: currentCurrency } = useAccountSettings(); + const { accountAddress: currentAddress, nativeCurrency: currentCurrency, network: currentNetwork } = useAccountSettings(); + + const connectedToHardhat = getIsHardhatConnected(); const filter = userAssetsStore(state => state.filter); const searchQuery = userAssetsStore(state => state.inputSearchQuery); @@ -32,6 +35,7 @@ export const useAssetsToSell = () => { { address: currentAddress as Address, currency: currentCurrency, + testnetMode: connectedToHardhat, }, { select: data => diff --git a/src/__swaps__/screens/Swap/hooks/useSwapInputsController.ts b/src/__swaps__/screens/Swap/hooks/useSwapInputsController.ts index 2d1944a544b..6aa89017e0a 100644 --- a/src/__swaps__/screens/Swap/hooks/useSwapInputsController.ts +++ b/src/__swaps__/screens/Swap/hooks/useSwapInputsController.ts @@ -872,7 +872,6 @@ export function useSwapInputsController({ } } ); - return { debouncedFetchQuote, formattedInputAmount, diff --git a/src/__swaps__/screens/Swap/resources/assets/userAssets.ts b/src/__swaps__/screens/Swap/resources/assets/userAssets.ts index 1408eec1fb1..d6d689682e2 100644 --- a/src/__swaps__/screens/Swap/resources/assets/userAssets.ts +++ b/src/__swaps__/screens/Swap/resources/assets/userAssets.ts @@ -15,6 +15,7 @@ import { parseUserAsset } from '@/__swaps__/utils/assets'; import { greaterThan } from '@/__swaps__/utils/numbers'; import { fetchUserAssetsByChain } from './userAssetsByChain'; +import { fetchHardhatBalances, fetchHardhatBalancesByChainId } from '@/resources/assets/hardhatAssets'; const addysHttp = new RainbowFetchClient({ baseURL: 'https://addys.p.rainbow.me/v3', @@ -81,10 +82,32 @@ export const userAssetsSetQueryData = ({ address, currency, userAssets, testnetM queryClient.setQueryData(userAssetsQueryKey({ address, currency, testnetMode }), userAssets); }; -async function userAssetsQueryFunction({ queryKey: [{ address, currency, testnetMode }] }: QueryFunctionArgs) { +async function userAssetsQueryFunction({ + queryKey: [{ address, currency, testnetMode }], +}: QueryFunctionArgs): Promise { if (!address) { return {}; } + if (testnetMode) { + const { assets, chainIdsInResponse } = await fetchHardhatBalancesByChainId(address); + const parsedAssets: Array<{ + asset: ZerionAsset; + quantity: string; + small_balances: boolean; + }> = Object.values(assets).map(asset => ({ + asset: asset.asset, + quantity: asset.quantity, + small_balances: false, + })); + + const parsedAssetsDict = await parseUserAssets({ + assets: parsedAssets, + chainIds: chainIdsInResponse, + currency, + }); + + return parsedAssetsDict; + } const cache = queryClient.getQueryCache(); const cachedUserAssets = (cache.find(userAssetsQueryKey({ address, currency, testnetMode }))?.state?.data || {}) as ParsedAssetsDictByChain; @@ -208,12 +231,10 @@ export async function parseUserAssets({ // Query Hook export function useUserAssets( - { address, currency }: UserAssetsArgs, + { address, currency, testnetMode }: UserAssetsArgs, config: QueryConfigWithSelect = {} ) { - const isHardhatConnected = getIsHardhatConnected(); - - return useQuery(userAssetsQueryKey({ address, currency, testnetMode: isHardhatConnected }), userAssetsQueryFunction, { + return useQuery(userAssetsQueryKey({ address, currency, testnetMode }), userAssetsQueryFunction, { ...config, refetchInterval: USER_ASSETS_REFETCH_INTERVAL, staleTime: process.env.IS_TESTING === 'true' ? 0 : 1000, diff --git a/src/__swaps__/types/chains.ts b/src/__swaps__/types/chains.ts index 98105ad5628..ff59ca43b05 100644 --- a/src/__swaps__/types/chains.ts +++ b/src/__swaps__/types/chains.ts @@ -47,6 +47,7 @@ export enum ChainName { celo = 'celo', degen = 'degen', gnosis = 'gnosis', + goerli = 'goerli', linea = 'linea', manta = 'manta', optimism = 'optimism', @@ -117,6 +118,7 @@ export const chainNameToIdMapping: { [ChainName.celo]: ChainId.celo, [ChainName.degen]: ChainId.degen, [ChainName.gnosis]: ChainId.gnosis, + [ChainName.goerli]: chain.goerli.id, [ChainName.linea]: ChainId.linea, [ChainName.manta]: ChainId.manta, [ChainName.optimism]: ChainId.optimism, @@ -156,6 +158,7 @@ export const chainIdToNameMapping: { [ChainId.celo]: ChainName.celo, [ChainId.degen]: ChainName.degen, [ChainId.gnosis]: ChainName.gnosis, + [chain.goerli.id]: ChainName.goerli, [ChainId.linea]: ChainName.linea, [ChainId.manta]: ChainName.manta, [ChainId.optimism]: ChainName.optimism, @@ -197,6 +200,7 @@ export const ChainNameDisplay = { [ChainId.scroll]: chain.scroll.name, [ChainId.zora]: 'Zora', [ChainId.mainnet]: 'Ethereum', + [chain.goerli.id]: 'Goerli', [ChainId.hardhat]: 'Hardhat', [ChainId.hardhatOptimism]: chainHardhatOptimism.name, [ChainId.sepolia]: chain.sepolia.name, diff --git a/src/components/activity-list/ActivityList.js b/src/components/activity-list/ActivityList.js index d829d4fa8dc..b57e625fe12 100644 --- a/src/components/activity-list/ActivityList.js +++ b/src/components/activity-list/ActivityList.js @@ -106,7 +106,6 @@ const ActivityList = ({ if (!ref) return; setScrollToTopRef(ref); }; - if (network === networkTypes.mainnet) { return ( >(configs: T): T { return configs; } + function createTimingConfigs>(configs: T): T { return configs; } +type AnyConfig = WithSpringConfig | WithTimingConfig; + +export const disableForTestingEnvironment = (config: T): T => { + if (!IS_TEST) return config; + return { + ...config, + duration: 0, + } as T; +}; + // /---- 🍎 Spring Animations 🍎 ----/ // -// const springAnimations = createSpringConfigs({ - browserTabTransition: { dampingRatio: 0.82, duration: 800 }, - keyboardConfig: { damping: 500, mass: 3, stiffness: 1000 }, - sliderConfig: { damping: 40, mass: 1.25, stiffness: 450 }, - slowSpring: { damping: 500, mass: 3, stiffness: 800 }, - snappierSpringConfig: { damping: 42, mass: 0.8, stiffness: 800 }, - snappySpringConfig: { damping: 100, mass: 0.8, stiffness: 275 }, - springConfig: { damping: 100, mass: 1.2, stiffness: 750 }, + browserTabTransition: disableForTestingEnvironment({ dampingRatio: 0.82, duration: 800 }), + keyboardConfig: disableForTestingEnvironment({ damping: 500, mass: 3, stiffness: 1000 }), + sliderConfig: disableForTestingEnvironment({ damping: 40, mass: 1.25, stiffness: 450 }), + slowSpring: disableForTestingEnvironment({ damping: 500, mass: 3, stiffness: 800 }), + snappierSpringConfig: disableForTestingEnvironment({ damping: 42, mass: 0.8, stiffness: 800 }), + snappySpringConfig: disableForTestingEnvironment({ damping: 100, mass: 0.8, stiffness: 275 }), + springConfig: disableForTestingEnvironment({ damping: 100, mass: 1.2, stiffness: 750 }), }); export const SPRING_CONFIGS: Record = springAnimations; -// // /---- END ----/ // // /---- ⏱️ Timing Animations ⏱️ ----/ // -// const timingAnimations = createTimingConfigs({ - buttonPressConfig: { duration: 160, easing: Easing.bezier(0.25, 0.46, 0.45, 0.94) }, - fadeConfig: { duration: 200, easing: Easing.bezier(0.22, 1, 0.36, 1) }, - fastFadeConfig: { duration: 100, easing: Easing.bezier(0.22, 1, 0.36, 1) }, - slowFadeConfig: { duration: 300, easing: Easing.bezier(0.22, 1, 0.36, 1) }, - slowerFadeConfig: { duration: 400, easing: Easing.bezier(0.22, 1, 0.36, 1) }, - slowestFadeConfig: { duration: 500, easing: Easing.bezier(0.22, 1, 0.36, 1) }, - tabPressConfig: { duration: 800, easing: Easing.bezier(0.22, 1, 0.36, 1) }, + buttonPressConfig: disableForTestingEnvironment({ duration: 160, easing: Easing.bezier(0.25, 0.46, 0.45, 0.94) }), + fadeConfig: disableForTestingEnvironment({ duration: 200, easing: Easing.bezier(0.22, 1, 0.36, 1) }), + fastFadeConfig: disableForTestingEnvironment({ duration: 100, easing: Easing.bezier(0.22, 1, 0.36, 1) }), + slowFadeConfig: disableForTestingEnvironment({ duration: 300, easing: Easing.bezier(0.22, 1, 0.36, 1) }), + slowerFadeConfig: disableForTestingEnvironment({ duration: 400, easing: Easing.bezier(0.22, 1, 0.36, 1) }), + slowestFadeConfig: disableForTestingEnvironment({ duration: 500, easing: Easing.bezier(0.22, 1, 0.36, 1) }), + tabPressConfig: disableForTestingEnvironment({ duration: 800, easing: Easing.bezier(0.22, 1, 0.36, 1) }), }); export const TIMING_CONFIGS: Record = timingAnimations; -// // /---- END ----/ // diff --git a/src/components/asset-list/RecyclerAssetList2/FastComponents/FastBalanceCoinRow.tsx b/src/components/asset-list/RecyclerAssetList2/FastComponents/FastBalanceCoinRow.tsx index 30b8915ed54..adb20230da8 100644 --- a/src/components/asset-list/RecyclerAssetList2/FastComponents/FastBalanceCoinRow.tsx +++ b/src/components/asset-list/RecyclerAssetList2/FastComponents/FastBalanceCoinRow.tsx @@ -94,7 +94,7 @@ const MemoizedBalanceCoinRow = React.memo( const chainId = ethereumUtils.getChainIdFromNetwork(item?.network); return ( - + diff --git a/src/config/experimental.ts b/src/config/experimental.ts index 77c87bd58a4..c9d422f38c1 100644 --- a/src/config/experimental.ts +++ b/src/config/experimental.ts @@ -61,7 +61,7 @@ export const defaultConfig: Record = { [REMOTE_CARDS]: { settings: true, value: false }, [POINTS_NOTIFICATIONS_TOGGLE]: { settings: true, value: false }, [DAPP_BROWSER]: { settings: true, value: !!IS_TEST }, - [SWAPS_V2]: { settings: true, value: false }, + [SWAPS_V2]: { settings: true, value: !!IS_TEST }, [ETH_REWARDS]: { settings: true, value: false }, [DEGEN_MODE]: { settings: true, value: false }, }; diff --git a/src/design-system/components/Box/Box.tsx b/src/design-system/components/Box/Box.tsx index 26287386771..3d051af2a34 100644 --- a/src/design-system/components/Box/Box.tsx +++ b/src/design-system/components/Box/Box.tsx @@ -10,6 +10,10 @@ import { BackgroundProvider, BackgroundProviderProps } from '../BackgroundProvid import { Border, BorderProps } from '../Border/Border'; import { ApplyShadow } from '../private/ApplyShadow/ApplyShadow'; import type * as Polymorphic from './polymorphic'; +import { IS_TEST } from '@/env'; +import LinearGradient from 'react-native-linear-gradient'; + +const COMPONENTS_TO_OVERRIDE_IN_TEST_MODE = [LinearGradient]; const positions = ['absolute'] as const; type Position = (typeof positions)[number]; @@ -174,7 +178,8 @@ export const Box = forwardRef(function Box( const width = typeof widthProp === 'number' ? widthProp : resolveToken(widths, widthProp); const height = typeof heightProp === 'number' ? heightProp : resolveToken(heights, heightProp); - const isView = Component === View || Component === Animated.View; + const ComponentToUse = IS_TEST && COMPONENTS_TO_OVERRIDE_IN_TEST_MODE.some(_C => Component instanceof _C) ? View : Component; + const isView = ComponentToUse === View || ComponentToUse === Animated.View; const shadowStylesExist = !!styleProp && @@ -273,7 +278,7 @@ export const Box = forwardRef(function Box( {({ backgroundColor, backgroundStyle }) => ( - + {children} {borderColor || borderWidth ? ( ) : null} - + )} ) : ( - + {children} {borderColor || borderWidth ? ( ) : null} - + ); }) as PolymorphicBox; diff --git a/src/handlers/swap.ts b/src/handlers/swap.ts index c1dc6f021bd..51c4b9de402 100644 --- a/src/handlers/swap.ts +++ b/src/handlers/swap.ts @@ -205,7 +205,7 @@ export const isUnwrapNative = ({ buyTokenAddress: string; }) => { return ( - sellTokenAddress.toLowerCase() === WRAPPED_ASSET[chainId].toLowerCase() && + sellTokenAddress.toLowerCase() === WRAPPED_ASSET[chainId]?.toLowerCase() && buyTokenAddress.toLowerCase() === ETH_ADDRESS_AGGREGATORS.toLowerCase() ); }; @@ -221,7 +221,7 @@ export const isWrapNative = ({ }) => { return ( sellTokenAddress.toLowerCase() === ETH_ADDRESS_AGGREGATORS.toLowerCase() && - buyTokenAddress.toLowerCase() === WRAPPED_ASSET[chainId].toLowerCase() + buyTokenAddress.toLowerCase() === WRAPPED_ASSET[chainId]?.toLowerCase() ); }; diff --git a/src/navigation/SwipeNavigator.tsx b/src/navigation/SwipeNavigator.tsx index b3f89f1ec1e..45fd589fcc6 100644 --- a/src/navigation/SwipeNavigator.tsx +++ b/src/navigation/SwipeNavigator.tsx @@ -92,7 +92,13 @@ const ActivityTabIcon = React.memo( }, [pendingCount]); return pendingCount > 0 ? ( - + diff --git a/src/references/chain-assets.json b/src/references/chain-assets.json index 44548600043..766268a1054 100644 --- a/src/references/chain-assets.json +++ b/src/references/chain-assets.json @@ -3,14 +3,22 @@ { "asset": { "asset_code": "eth", + "mainnet_address": "eth", "colors": { "fallback": "#E8EAF5", "primary": "#808088" }, + "chainId": 5, "decimals": 18, "icon_url": "https://s3.amazonaws.com/icons.assets/ETH.png", - "name": "Ether", + "name": "Goerli", "network": "goerli", + "networks": { + "1": { + "address": "eth", + "decimals": 18 + } + }, "price": { "changed_at": 1582568575, "relative_change_24h": -4.586615622469276, @@ -29,10 +37,17 @@ "fallback": "#E8EAF5", "primary": "#808088" }, + "chainId": 1, "decimals": 18, "icon_url": "https://s3.amazonaws.com/icons.assets/ETH.png", - "name": "Ether", + "name": "Ethereum", "network": "mainnet", + "networks": { + "1": { + "address": "eth", + "decimals": 18 + } + }, "price": { "changed_at": 1582568575, "relative_change_24h": -4.586615622469276, diff --git a/src/references/index.ts b/src/references/index.ts index b4b7cc6686a..a692c06120e 100644 --- a/src/references/index.ts +++ b/src/references/index.ts @@ -229,21 +229,10 @@ export const SUPPORTED_MAINNET_CHAINS: Chain[] = [mainnet, polygon, optimism, ar name: ChainNameDisplay[chain.id], })); -export const SUPPORTED_CHAINS = ({ testnetMode = false }: { testnetMode?: boolean }): Chain[] => - [ - // In default order of appearance - mainnet, - base, - optimism, - arbitrum, - polygon, - zora, - blast, - degen, - avalanche, - bsc, +export const SUPPORTED_CHAINS = ({ testnetMode = false }: { testnetMode?: boolean }): Chain[] => { + const mainnetChains: Chain[] = [mainnet, base, optimism, arbitrum, polygon, zora, blast, degen, avalanche, bsc]; - // Testnets + const testnetChains: Chain[] = [ goerli, holesky, sepolia, @@ -255,12 +244,12 @@ export const SUPPORTED_CHAINS = ({ testnetMode = false }: { testnetMode?: boolea zoraSepolia, avalancheFuji, bscTestnet, - ].reduce((chainList, chain) => { - if (testnetMode || !chain.testnet) { - chainList.push({ ...chain, name: ChainNameDisplay[chain.id] }); - } - return chainList; - }, [] as Chain[]); + ]; + + const allChains = mainnetChains.concat(testnetMode ? testnetChains : []); + + return allChains.map(chain => ({ ...chain, name: ChainNameDisplay[chain.id] ?? chain.name })); +}; export const SUPPORTED_CHAIN_IDS = ({ testnetMode = false }: { testnetMode?: boolean }): ChainId[] => SUPPORTED_CHAINS({ testnetMode }).map(chain => chain.id); diff --git a/src/references/testnet-assets-by-chain.ts b/src/references/testnet-assets-by-chain.ts new file mode 100644 index 00000000000..eb56ab9f147 --- /dev/null +++ b/src/references/testnet-assets-by-chain.ts @@ -0,0 +1,70 @@ +import { UniqueId, ZerionAsset } from '@/__swaps__/types/assets'; +import { ChainName } from '@/__swaps__/types/chains'; +import { Network } from '@/helpers'; + +type ChainAssets = { + [uniqueId: UniqueId]: { + asset: ZerionAsset; + quantity: string; + }; +}; + +// NOTE: Don't import `ETH_ADDRESS` as it's resolving to undefined... +export const chainAssets: Partial> = { + [Network.goerli]: { + eth_5: { + asset: { + asset_code: 'eth', + mainnet_address: 'eth', + colors: { + fallback: '#E8EAF5', + primary: '#808088', + }, + implementations: {}, + bridging: { + bridgeable: true, + networks: {}, // TODO: Add bridgeable networks + }, + decimals: 18, + icon_url: 'https://s3.amazonaws.com/icons.assets/ETH.png', + name: 'Goerli', + network: ChainName.goerli, + price: { + relative_change_24h: -4.586615622469276, + value: 2590.2, + }, + symbol: 'ETH', + }, + quantity: '0', + }, + }, + [Network.mainnet]: { + eth_1: { + asset: { + asset_code: 'eth', + mainnet_address: 'eth', + colors: { + fallback: '#E8EAF5', + primary: '#808088', + }, + decimals: 18, + icon_url: 'https://s3.amazonaws.com/icons.assets/ETH.png', + name: 'Ethereum', + network: ChainName.mainnet, + implementations: {}, + bridging: { + bridgeable: true, + networks: {}, // TODO: Add bridgeable networks + }, + price: { + relative_change_24h: -4.586615622469276, + value: 2590.2, + }, + symbol: 'ETH', + }, + quantity: '0', + }, + }, +}; + +export default chainAssets; diff --git a/src/resources/assets/hardhatAssets.ts b/src/resources/assets/hardhatAssets.ts index 4dde7452bca..63ba4a2b21f 100644 --- a/src/resources/assets/hardhatAssets.ts +++ b/src/resources/assets/hardhatAssets.ts @@ -3,12 +3,14 @@ import { keyBy, mapValues } from 'lodash'; import { Network } from '@/helpers/networkTypes'; import { web3Provider } from '@/handlers/web3'; // TODO JIN import { getNetworkObj } from '@/networks'; -import { balanceCheckerContractAbi, chainAssets, ETH_ADDRESS } from '@/references'; +import { balanceCheckerContractAbi, chainAssets, ETH_ADDRESS, SUPPORTED_CHAIN_IDS } from '@/references'; import { parseAddressAsset } from './assets'; import { RainbowAddressAssets } from './types'; import { logger, RainbowError } from '@/logger'; - -const ETHEREUM_ADDRESS_FOR_BALANCE_CONTRACT = '0x0000000000000000000000000000000000000000'; +import { AddressOrEth, UniqueId, ZerionAsset } from '@/__swaps__/types/assets'; +import { ChainId, ChainName } from '@/__swaps__/types/chains'; +import { AddressZero } from '@ethersproject/constants'; +import chainAssetsByChainId from '@/references/testnet-assets-by-chain'; const fetchHardhatBalancesWithBalanceChecker = async ( tokens: string[], @@ -23,7 +25,7 @@ const fetchHardhatBalancesWithBalanceChecker = async ( } = {}; tokens.forEach((tokenAddr, tokenIdx) => { const balance = values[tokenIdx]; - const assetCode = tokenAddr === ETHEREUM_ADDRESS_FOR_BALANCE_CONTRACT ? ETH_ADDRESS : tokenAddr; + const assetCode = tokenAddr === AddressZero ? ETH_ADDRESS : tokenAddr; balances[assetCode] = balance.toString(); }); return balances; @@ -33,11 +35,18 @@ const fetchHardhatBalancesWithBalanceChecker = async ( } }; +/** + * @deprecated - to be removed once rest of the app is converted to new userAssetsStore + * Fetches the balances of the hardhat assets for the given account address and network. + * @param accountAddress - The address of the account to fetch the balances for. + * @param network - The network to fetch the balances for. + * @returns The balances of the hardhat assets for the given account address and network. + */ export const fetchHardhatBalances = async (accountAddress: string, network: Network = Network.mainnet): Promise => { - const chainAssetsMap = keyBy(chainAssets[network as keyof typeof chainAssets], ({ asset }) => `${asset.asset_code}_${asset.network}`); + const chainAssetsMap = keyBy(chainAssets[network as keyof typeof chainAssets], ({ asset }) => `${asset.asset_code}_${asset.chainId}`); const tokenAddresses = Object.values(chainAssetsMap).map(({ asset: { asset_code } }) => - asset_code === ETH_ADDRESS ? ETHEREUM_ADDRESS_FOR_BALANCE_CONTRACT : asset_code.toLowerCase() + asset_code === ETH_ADDRESS ? AddressZero : asset_code.toLowerCase() ); const balances = await fetchHardhatBalancesWithBalanceChecker(tokenAddresses, accountAddress, network); if (!balances) return {}; @@ -55,3 +64,61 @@ export const fetchHardhatBalances = async (accountAddress: string, network: Netw }); return updatedAssets; }; + +export const fetchHardhatBalancesByChainId = async ( + accountAddress: string, + network: Network = Network.mainnet +): Promise<{ + assets: { + [uniqueId: UniqueId]: { + asset: ZerionAsset; + quantity: string; + }; + }; + chainIdsInResponse: ChainId[]; +}> => { + const chainAssetsMap = chainAssetsByChainId[network as keyof typeof chainAssets] || {}; + const tokenAddresses = Object.values(chainAssetsMap).map(({ asset }) => + asset.asset_code === ETH_ADDRESS ? AddressZero : asset.asset_code.toLowerCase() + ); + + const balances = await fetchHardhatBalancesWithBalanceChecker(tokenAddresses, accountAddress, network); + if (!balances) + return { + assets: {}, + chainIdsInResponse: [], + }; + + const updatedAssets = Object.entries(chainAssetsMap).reduce( + (acc, [uniqueId, chainAsset]) => { + const assetCode = chainAsset.asset.asset_code || ETH_ADDRESS; + const quantity = balances[assetCode.toLowerCase()] || '0'; + + const asset: ZerionAsset = { + ...chainAsset.asset, + asset_code: assetCode as AddressOrEth, + mainnet_address: (chainAsset.asset.mainnet_address as AddressOrEth) || (assetCode as AddressOrEth), + network: (chainAsset.asset.network as ChainName) || ChainName.mainnet, + bridging: chainAsset.asset.bridging || { + bridgeable: false, + networks: {}, + }, + implementations: chainAsset.asset.implementations || {}, + name: chainAsset.asset.name || 'Unknown Token', + symbol: chainAsset.asset.symbol || 'UNKNOWN', + decimals: chainAsset.asset.decimals || 18, + icon_url: chainAsset.asset.icon_url || '', + price: chainAsset.asset.price || { value: 0, relative_change_24h: 0 }, + }; + + acc[uniqueId] = { asset, quantity }; + return acc; + }, + {} as { [uniqueId: UniqueId]: { asset: ZerionAsset; quantity: string } } + ); + + return { + assets: updatedAssets, + chainIdsInResponse: SUPPORTED_CHAIN_IDS({ testnetMode: true }), + }; +}; diff --git a/src/state/assets/userAssets.ts b/src/state/assets/userAssets.ts index 91b3efd0f49..87dbe97b7c0 100644 --- a/src/state/assets/userAssets.ts +++ b/src/state/assets/userAssets.ts @@ -1,11 +1,12 @@ +import { ParsedSearchAsset, UniqueId, UserAssetFilter } from '@/__swaps__/types/assets'; +import { ChainId } from '@/__swaps__/types/chains'; +import { getIsHardhatConnected } from '@/handlers/web3'; import { Address } from 'viem'; import { RainbowError, logger } from '@/logger'; import store from '@/redux/store'; import { ETH_ADDRESS, SUPPORTED_CHAIN_IDS, supportedNativeCurrencies } from '@/references'; import { createRainbowStore } from '@/state/internal/createRainbowStore'; -import { ParsedSearchAsset, UniqueId, UserAssetFilter } from '@/__swaps__/types/assets'; -import { ChainId } from '@/__swaps__/types/chains'; -import { swapsStore } from '../swaps/swapsStore'; +import { swapsStore } from '@/state/swaps/swapsStore'; const SEARCH_CACHE_MAX_ENTRIES = 50; @@ -179,7 +180,6 @@ export const userAssetsStore = createRainbowStore( return filteredIds; } }, - getHighestValueEth: () => { const preferredNetwork = swapsStore.getState().preferredNetwork; const assets = get().userAssets; @@ -278,7 +278,7 @@ export const userAssetsStore = createRainbowStore( }); // Ensure all supported chains are in the map with a fallback value of 0 - SUPPORTED_CHAIN_IDS({ testnetMode: false }).forEach(chainId => { + SUPPORTED_CHAIN_IDS({ testnetMode: getIsHardhatConnected() }).forEach(chainId => { if (!unsortedChainBalances.has(chainId)) { unsortedChainBalances.set(chainId, 0); idsByChain.set(chainId, []); diff --git a/src/state/sync/UserAssetsSync.tsx b/src/state/sync/UserAssetsSync.tsx index f4573bb7834..8d547f2f8eb 100644 --- a/src/state/sync/UserAssetsSync.tsx +++ b/src/state/sync/UserAssetsSync.tsx @@ -6,9 +6,10 @@ import { useSwapsStore } from '@/state/swaps/swapsStore'; import { selectUserAssetsList, selectorFilterByUserChains } from '@/__swaps__/screens/Swap/resources/_selectors/assets'; import { ParsedSearchAsset } from '@/__swaps__/types/assets'; import { ChainId } from '@/__swaps__/types/chains'; +import { getIsHardhatConnected } from '@/handlers/web3'; import { useUserAssets } from '@/__swaps__/screens/Swap/resources/assets'; -export const UserAssetsSync = memo(function UserAssetsSync() { +export const UserAssetsSync = function UserAssetsSync() { const { accountAddress: currentAddress, nativeCurrency: currentCurrency } = useAccountSettings(); const userAssetsWalletAddress = userAssetsStore(state => state.associatedWalletAddress); @@ -18,6 +19,7 @@ export const UserAssetsSync = memo(function UserAssetsSync() { { address: currentAddress as Address, currency: currentCurrency, + testnetMode: getIsHardhatConnected(), }, { enabled: !isSwapsOpen || userAssetsWalletAddress !== currentAddress, @@ -41,4 +43,4 @@ export const UserAssetsSync = memo(function UserAssetsSync() { ); return null; -}); +};