From 52e22b52f271fe2c0705b61b1b2a5b1f837f2468 Mon Sep 17 00:00:00 2001 From: gregs Date: Thu, 21 Nov 2024 14:13:00 -0300 Subject: [PATCH 01/95] portal --- src/App.tsx | 2 ++ src/components/AbsolutePortal.tsx | 10 ++++------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index d59b7ed7296..5833fb7b38c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -43,6 +43,7 @@ import { IS_ANDROID, IS_DEV } from '@/env'; import { prefetchDefaultFavorites } from '@/resources/favorites'; import Routes from '@/navigation/Routes'; import { BackendNetworks } from '@/components/BackendNetworks'; +import { AbsolutePortalRoot } from './components/AbsolutePortal'; if (IS_DEV) { reactNativeDisableYellowBox && LogBox.ignoreAllLogs(); @@ -76,6 +77,7 @@ function App({ walletReady }: AppProps) { + )} diff --git a/src/components/AbsolutePortal.tsx b/src/components/AbsolutePortal.tsx index b578234bd0a..c98b036cd70 100644 --- a/src/components/AbsolutePortal.tsx +++ b/src/components/AbsolutePortal.tsx @@ -32,17 +32,15 @@ export const AbsolutePortalRoot = () => { return () => unsubscribe(); }, []); - return ( - - {nodes} - - ); + return {nodes}; }; export const AbsolutePortal = ({ children }: PropsWithChildren) => { useEffect(() => { absolutePortal.addNode(children); - return () => absolutePortal.removeNode(children); + return () => { + absolutePortal.removeNode(children); + }; }, [children]); return null; From 31cb47763d47e4408ee62272c6c04c86728839ec Mon Sep 17 00:00:00 2001 From: gregs Date: Thu, 21 Nov 2024 14:13:36 -0300 Subject: [PATCH 02/95] trending tokens --- .eslintrc.js | 1 + src/languages/en_US.json | 24 ++ .../discover/components/TrendingTokens.tsx | 387 ++++++++++++++++++ 3 files changed, 412 insertions(+) create mode 100644 src/screens/discover/components/TrendingTokens.tsx diff --git a/.eslintrc.js b/.eslintrc.js index 6df1e6e1e52..f8cb2150123 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -39,5 +39,6 @@ module.exports = { ], 'jest/expect-expect': 'off', 'jest/no-disabled-tests': 'off', + 'no-nested-ternary': 'off', }, }; diff --git a/src/languages/en_US.json b/src/languages/en_US.json index e49299f792b..0d2e0fcb380 100644 --- a/src/languages/en_US.json +++ b/src/languages/en_US.json @@ -2974,6 +2974,30 @@ "back": "Back" } }, + "trending_tokens": { + "no_results": { + "title": "No results", + "body": "Try browsing a larger timeframe or a different network or category." + }, + "filters": { + "categories": { + "trending": "Trending", + "new": "New", + "farcaster": "Farcaster" + }, + "sort": { + "volume": "Volume", + "market_cap": "Market Cap", + "top_gainers": "Top Gainers", + "top_losers": "Top Losers" + }, + "time": { + "day": "24h", + "week": "1 Week", + "month": "1 Month" + } + } + }, "copy": "Copy", "paste": "Paste" } diff --git a/src/screens/discover/components/TrendingTokens.tsx b/src/screens/discover/components/TrendingTokens.tsx new file mode 100644 index 00000000000..a92fdbd012c --- /dev/null +++ b/src/screens/discover/components/TrendingTokens.tsx @@ -0,0 +1,387 @@ +import { ChainId } from '@/chains/types'; +import { ChainBadge } from '@/components/coin-icon'; +import { DropdownMenu } from '@/components/DropdownMenu'; +import { globalColors, Text, useBackgroundColor } from '@/design-system'; +import { useForegroundColor } from '@/design-system/color/useForegroundColor'; + +import chroma from 'chroma-js'; +import { useState } from 'react'; +import React, { View } from 'react-native'; +import FastImage from 'react-native-fast-image'; +import { Gesture, GestureDetector } from 'react-native-gesture-handler'; +import LinearGradient from 'react-native-linear-gradient'; +import Animated, { LinearTransition, runOnJS, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; +import { NetworkSelector } from './NetworkSwitcher'; +import * as i18n from '@/languages'; + +const TRANSLATIONS = i18n.l.cards.ens_search; + +const AnimatedLinearGradient = Animated.createAnimatedComponent(LinearGradient); + +function FilterButton({ icon, label, onPress }: { onPress?: VoidFunction; label: string; icon: string }) { + const pressed = useSharedValue(false); + + const tap = Gesture.Tap() + .onBegin(() => { + pressed.value = true; + if (onPress) runOnJS(onPress)(); + }) + .onFinalize(() => (pressed.value = false)); + + const animatedStyles = useAnimatedStyle(() => ({ + transform: [{ scale: withTiming(pressed.value ? 0.95 : 1, { duration: 100 }) }], + })); + + const backgroundColor = useBackgroundColor('fillTertiary'); + const borderColor = useBackgroundColor('fillSecondary'); + + const iconColor = useForegroundColor('labelQuaternary'); + + return ( + + + + {icon} + + + {label} + + + 􀆏 + + + + ); +} + +function CategoryFilterButton({ + selected, + onPress, + icon, + iconWidth = 16, + iconColor, + label, +}: { + onPress: VoidFunction; + selected: boolean; + icon: string; + iconColor: string; + iconWidth?: number; + label: string; +}) { + const backgroundColor = useBackgroundColor('fillTertiary'); + const borderColor = useBackgroundColor('fillSecondary'); + + const pressed = useSharedValue(false); + + const tap = Gesture.Tap() + .onBegin(() => { + pressed.value = true; + runOnJS(onPress)(); + }) + .onFinalize(() => (pressed.value = false)); + + const animatedStyles = useAnimatedStyle(() => ({ + transform: [{ scale: withTiming(pressed.value ? 0.95 : 1, { duration: 100 }) }], + })); + + return ( + + + + {icon} + + + {label} + + + + ); +} + +function FriendHolders() { + return ( + + + + + + + + mikedemarais{' '} + + and 2 others + + + + ); +} + +function TokenIcon({ uri, chainId }: { uri: string; chainId: ChainId }) { + return ( + + + {chainId !== ChainId.mainnet && } + + ); +} + +function TrendingTokenRow() { + const separatorColor = useForegroundColor('separator'); + + const percentChange24h = '3.40%'; + const percentChange1h = '8.82%'; + + const token = { + name: 'Uniswap', + symbol: 'UNI', + price: '$9.21', + }; + + const volume = '$1.8M'; + const marketCap = '$1.8M'; + + return ( + + + + + + + + + + + {token.name} + + + {token.symbol} + + + {token.price} + + + + + + + VOL + + + {volume} + + + + + | + + + + + MCAP + + + {marketCap} + + + + + + + + + 􀄨 + + + {percentChange24h} + + + + + 1H + + + {percentChange1h} + + + + + + + ); +} + +const t = i18n.l.trending_tokens; + +function NoResults() { + const backgroundColor = '#191A1C'; // useBackgroundColor('fillQuaternary'); + + return ( + + + + {i18n.t(t.no_results.title)} + + + {i18n.t(t.no_results.body)} + + + + + 􀙭 + + + + ); +} + +function NetworkFilter() { + const [isOpen, setOpen] = useState(false); + + return ( + <> + setOpen(true)} /> + {isOpen && setOpen(false)} onSelect={() => null} multiple />} + + ); +} + +const sortFilters = ['volume', 'market_cap', 'top_gainers', 'top_losers'] as const; +const timeFilters = ['day', 'week', 'month'] as const; +type TrendingTokensFilter = { + category: 'trending' | 'new' | 'farcaster'; + network: undefined | ChainId; + timeframe: (typeof timeFilters)[number]; + sort: (typeof sortFilters)[number] | undefined; +}; + +export function TrendingTokens() { + const [filter, setFilter] = useState({ + category: 'trending', + network: undefined, + timeframe: 'day', + sort: 'volume', + }); + const setCategory = (category: TrendingTokensFilter['category']) => setFilter(filter => ({ ...filter, category })); + return ( + + + + setCategory('trending')} + /> + setCategory('new')} + /> + setCategory('farcaster')} + /> + + + + + + ({ + actionTitle: i18n.t(t.filters.time[time]), + actionKey: time, + })), + }} + side="bottom" + onPressMenuItem={timeframe => setFilter(filter => ({ ...filter, timeframe }))} + > + + + + ({ + actionTitle: i18n.t(t.filters.sort[sort]), + actionKey: sort, + })), + }} + side="bottom" + onPressMenuItem={sort => + setFilter(filter => { + if (sort === filter.sort) return { ...filter, sort: undefined }; + return { ...filter, sort }; + }) + } + > + + + + + + + + + + + + + + + + + ); +} From adfaceebf30f185e106eaea54fa6618369d3073b Mon Sep 17 00:00:00 2001 From: gregs Date: Thu, 21 Nov 2024 14:14:26 -0300 Subject: [PATCH 03/95] network switcher --- src/chains/index.ts | 2 + .../discover/components/NetworkSwitcher.tsx | 577 ++++++++++++++++++ 2 files changed, 579 insertions(+) create mode 100644 src/screens/discover/components/NetworkSwitcher.tsx diff --git a/src/chains/index.ts b/src/chains/index.ts index ace5273f2de..92a95f6b3e5 100644 --- a/src/chains/index.ts +++ b/src/chains/index.ts @@ -15,6 +15,8 @@ const BACKEND_CHAINS = transformBackendNetworksToChains(backendNetworks.networks export const SUPPORTED_CHAINS: Chain[] = IS_TEST ? [...BACKEND_CHAINS, chainHardhat, chainHardhatOptimism] : BACKEND_CHAINS; +export const SUPPORTED_CHAIN_IDS_ALPHABETICAL = SUPPORTED_CHAINS.sort((a, b) => a.name.localeCompare(b.name)).map(c => c.id); + export const defaultChains: Record = SUPPORTED_CHAINS.reduce( (acc, chain) => { acc[chain.id] = chain; diff --git a/src/screens/discover/components/NetworkSwitcher.tsx b/src/screens/discover/components/NetworkSwitcher.tsx new file mode 100644 index 00000000000..f4f1b6f78eb --- /dev/null +++ b/src/screens/discover/components/NetworkSwitcher.tsx @@ -0,0 +1,577 @@ +import { getChainColorWorklet } from '@/__swaps__/utils/swaps'; +import { chainsLabel, SUPPORTED_CHAIN_IDS_ALPHABETICAL } from '@/chains'; +import { ChainId } from '@/chains/types'; +import { AbsolutePortal } from '@/components/AbsolutePortal'; +import { ChainImage } from '@/components/coin-icon/ChainImage'; +import { globalColors, Text, useBackgroundColor, useColorMode } from '@/design-system'; +import { useForegroundColor } from '@/design-system/color/useForegroundColor'; +import hiddenTokens from '@/redux/hiddenTokens'; +import { createRainbowStore } from '@/state/internal/createRainbowStore'; +import { nonceStore } from '@/state/nonces'; + +import chroma from 'chroma-js'; +import { useReducer, useState } from 'react'; +import React, { StyleSheet, View } from 'react-native'; +import { Gesture, GestureDetector, GestureStateChangeEvent, TapGestureHandlerEventPayload } from 'react-native-gesture-handler'; +import Animated, { + FadeIn, + FadeOut, + LinearTransition, + runOnJS, + SlideInDown, + SlideOutDown, + useAnimatedReaction, + useAnimatedStyle, + useDerivedValue, + useSharedValue, + withSpring, + withTiming, +} from 'react-native-reanimated'; + +function getMostUsedChains() { + const noncesByAddress = nonceStore.getState().nonces; + + const summedNoncesByChainId: Record = {}; + for (const addressNonces of Object.values(noncesByAddress)) { + for (const [chainId, { currentNonce }] of Object.entries(addressNonces)) { + summedNoncesByChainId[chainId] ??= 0; + summedNoncesByChainId[chainId] += currentNonce || 0; + } + } + + return Object.entries(summedNoncesByChainId) + .sort((a, b) => b[1] - a[1]) + .map(([chainId]) => parseInt(chainId)); +} + +const initialPinnedNetworks = getMostUsedChains().slice(0, 5); +const useNetworkSwitcherStore = createRainbowStore<{ pinnedNetworks: ChainId[]; unpinnedNetworks: ChainId[] }>( + (set, get) => ({ + pinnedNetworks: initialPinnedNetworks, + unpinnedNetworks: SUPPORTED_CHAIN_IDS_ALPHABETICAL.filter(chainId => !initialPinnedNetworks.includes(chainId)), + }) + // { + // storageKey: 'network-switcher', + // version: 1, + // } +); +const setNetworkSwitcherState = (s: { pinnedNetworks: ChainId[]; unpinnedNetworks: ChainId[] }) => { + useNetworkSwitcherStore.setState(s); +}; + +function EditButton({ text, onPress }: { text: string; onPress: VoidFunction }) { + const blue = useForegroundColor('blue'); + + const pressed = useSharedValue(false); + + const tap = Gesture.Tap() + .onBegin(() => { + pressed.value = true; + }) + .onFinalize(() => { + pressed.value = false; + runOnJS(onPress)(); + }); + + const animatedStyles = useAnimatedStyle(() => ({ + transform: [{ scale: withTiming(pressed.value ? 0.95 : 1, { duration: 100 }) }], + })); + + return ( + + + + {text} + + + + ); +} + +function NetworkOption({ + chainId, + selected, + onPress, +}: { + chainId: ChainId; + selected: boolean; + onPress?: (e: GestureStateChangeEvent) => void; +}) { + const name = chainsLabel[chainId]; + if (!name) throw new Error(`No chain name for chainId ${chainId}`); + + const { isDarkMode } = useColorMode(); + const surfaceSecondary = useBackgroundColor('fillSecondary'); + const chainColor = chroma(getChainColorWorklet(chainId, true)).alpha(0.16).hex(); + const backgroundColor = selected ? chainColor : isDarkMode ? globalColors.white10 : globalColors.grey20; + + const borderColor = selected ? chainColor : '#F5F8FF05'; + + const pressed = useSharedValue(false); + + const tap = Gesture.Tap() + .onBegin(e => { + pressed.value = true; + if (onPress) runOnJS(onPress)(e); + }) + .onFinalize(() => { + pressed.value = false; + }) + .enabled(!!onPress); + + const animatedStyles = useAnimatedStyle(() => ({ + transform: [{ scale: withTiming(pressed.value ? 0.95 : 1, { duration: 100 }) }], + })); + + return ( + + + + + {name} + + + + ); +} + +function ExpandNetworks({ + hiddenNetworksLength, + isExpanded, + toggleExpanded, +}: { + hiddenNetworksLength: number; + toggleExpanded: (expanded: boolean) => void; + isExpanded: boolean; +}) { + const pressed = useSharedValue(false); + + const tap = Gesture.Tap() + .onBegin(() => { + pressed.value = true; + }) + .onFinalize(() => { + pressed.value = false; + runOnJS(toggleExpanded)(!isExpanded); + }); + + const animatedStyles = useAnimatedStyle(() => ({ + transform: [{ scale: withTiming(pressed.value ? 0.95 : 1, { duration: 100 }) }], + })); + + return ( + + + {!isExpanded && ( + + + {hiddenNetworksLength} + + + )} + + {isExpanded ? 'Show Less' : 'More Networks'} + + + + {isExpanded ? '􀆇' : '􀆈'} + + + + + ); +} + +type Transform = { x: number; y: number; scale: number }; + +const ITEM_HEIGHT = 48; +const ITEM_WIDTH = 164.5; +const GAP = 12; +const HALF_GAP = 6; + +const styles = StyleSheet.create({ draggingItem: { zIndex: 2, position: 'absolute', height: ITEM_HEIGHT, width: ITEM_WIDTH } }); + +/* + + - what to do if user pins all networks (where to drag to unpin) + - how to display if user selects a network that is in the "more networks" (hidden) + + */ + +function NetworksGrid({ + editing, + selected, + unselect, + select, +}: { + editing: boolean; + selected: ChainId[]; + unselect: (chainId: ChainId) => void; + select: (chainId: ChainId) => void; +}) { + const networks = useSharedValue(useNetworkSwitcherStore()); + const pinnedNetworks = useDerivedValue(() => networks.value.pinnedNetworks); + const unpinnedNetworks = useDerivedValue(() => networks.value.unpinnedNetworks); + + const dragging = useSharedValue(null); + const draggingTransform = useSharedValue(null); + const dropping = useSharedValue(null); + const droppingTransform = useSharedValue(null); + + const unpinnedGridY = useSharedValue(NaN); + + // Sync back to store + useAnimatedReaction( + () => networks.value, + current => runOnJS(setNetworkSwitcherState)(current) + ); + + // Force rerender when dragging or dropping changes + const [, rerender] = useReducer(x => x + 1, 0); + useAnimatedReaction( + () => [dropping.value, dragging.value], + () => runOnJS(rerender)() + ); + + const positionIndex = (x: number, y: number) => { + 'worklet'; + const yOffset = y > unpinnedGridY.value ? unpinnedGridY.value + GAP : 0; + const column = x > ITEM_WIDTH + HALF_GAP ? 1 : 0; + const row = Math.floor((y - yOffset) / (ITEM_HEIGHT + HALF_GAP)); + const index = row * 2 + column; + return index < 0 ? 0 : index; // row can be negative if the dragged item is above the first row + }; + + const indexPosition = (index: number, isUnpinned: boolean) => { + 'worklet'; + const column = index % 2; + const row = Math.floor(index / 2); + const position = { x: column * (ITEM_WIDTH + GAP), y: row * (ITEM_HEIGHT + GAP) }; + const yOffset = isUnpinned ? unpinnedGridY.value + GAP : 0; + return { x: position.x, y: position.y + yOffset }; + }; + + const dragNetwork = Gesture.Pan() + .maxPointers(1) + .onBegin(e => { + 'worklet'; + + const isTargetUnpinned = e.y > unpinnedGridY.value; + if (isTargetUnpinned && pinnedNetworks.value.length === 1) return; + + const index = positionIndex(e.x, e.y); + const position = indexPosition(index, isTargetUnpinned); + + draggingTransform.value = { x: position.x, y: position.y, scale: 1 }; // initial position is the grid slot + draggingTransform.value = withSpring({ x: e.x - ITEM_WIDTH * 0.5, y: e.y - ITEM_HEIGHT * 0.5, scale: 1.05 }); // animate into the center of the pointer + + const targetArray = isTargetUnpinned ? unpinnedNetworks.value : pinnedNetworks.value; + const chainId = targetArray[index]; + dragging.value = chainId; + }) + .onChange(e => { + 'worklet'; + const chainId = dragging.value; + if (!chainId) return; + + draggingTransform.modify(item => { + if (!item) return item; + item.x = e.x - ITEM_WIDTH * 0.5; + item.y = e.y - ITEM_HEIGHT * 0.5; + return item; + }); + + const isDraggingOverUnpinned = e.y > unpinnedGridY.value; + + const targetArrayKey = isDraggingOverUnpinned ? 'unpinnedNetworks' : 'pinnedNetworks'; + const otherArrayKey = isDraggingOverUnpinned ? 'pinnedNetworks' : 'unpinnedNetworks'; + + const targetArray = networks.value[targetArrayKey]; + const targetIndex = Math.min(positionIndex(e.x, e.y), targetArray.length - 1); + const indexInTarget = targetArray.indexOf(chainId); + + if (indexInTarget === -1) { + networks.modify(v => { + // Pin/Unpin + v[otherArrayKey] = v[otherArrayKey].filter(id => id !== chainId); + v[targetArrayKey].splice(targetIndex, 0, chainId); + return v; + }); + } else if (indexInTarget !== targetIndex) { + // Reorder + networks.modify(v => { + const [movedChainId] = v[targetArrayKey].splice(indexInTarget, 1); + v[targetArrayKey].splice(targetIndex, 0, movedChainId); + return v; + }); + } + }) + .onFinalize(e => { + 'worklet'; + if (!dragging.value) return; + + const isDroppingInUnpinned = e.y > unpinnedGridY.value; + const targetArray = isDroppingInUnpinned ? pinnedNetworks.value : unpinnedNetworks.value; + + const index = Math.min(positionIndex(e.x, e.y), targetArray.length - 1); + const { x, y } = indexPosition(index, isDroppingInUnpinned); + + droppingTransform.value = draggingTransform.value; + droppingTransform.value = withSpring({ y, x, scale: 1 }, { mass: 0.6 }, completed => { + if (completed) dropping.value = null; + }); + dropping.value = dragging.value; + dragging.value = null; + }) + .enabled(editing); + + useAnimatedReaction( + () => unpinnedGridY.value, + (newY, prevY) => { + // the layout can recalculate after the drop started + if (!prevY || !droppingTransform.value || droppingTransform.value.y < prevY) return; + const { x, y, scale } = droppingTransform.value; + droppingTransform.value = withSpring({ x, y: y + (prevY - newY), scale }, { mass: 0.6 }, completed => { + if (completed) dropping.value = null; + }); + } + ); + + const draggingStyles = useAnimatedStyle(() => { + if (!draggingTransform.value) return {}; + return { + transform: [{ scale: draggingTransform.value.scale }], + left: draggingTransform.value.x, + top: draggingTransform.value.y, + }; + }); + + const droppingStyles = useAnimatedStyle(() => { + if (!droppingTransform.value) return {}; + return { + transform: [{ scale: droppingTransform.value.scale }], + left: droppingTransform.value.x, + top: droppingTransform.value.y, + }; + }); + + const [isExpanded, setExpanded] = useState(false); + + const toggleSelected = (chainId: ChainId) => { + if (selected.includes(chainId)) unselect(chainId); + else select(chainId); + }; + + // const mainGridNetworks = editing + // ? pinnedNetworks.value + // : [...pinnedNetworks.value, ...unpinnedNetworks.value.filter(chainId => selected.includes(chainId))]; + + return ( + + + {!!dropping.value && ( + + + + )} + + {!!dragging.value && ( + + + + )} + + + {pinnedNetworks.value.map(chainId => + chainId === dragging.value || chainId === dropping.value ? ( + + ) : ( + toggleSelected(chainId)} + /> + ) + )} + + + {editing ? ( + + + Drag to Rearrange + + + ) : ( + + )} + + {(editing || isExpanded) && ( + (unpinnedGridY.value = e.nativeEvent.layout.y)} + layout={LinearTransition} + entering={FadeIn.duration(250).delay(125)} + exiting={FadeOut.duration(50)} + style={{ flexDirection: 'row', gap: 12, flexWrap: 'wrap', paddingVertical: 12 }} + > + {unpinnedNetworks.value.length === 0 && ( + + + Drag here to unpin networks + + + )} + {unpinnedNetworks.value.map(chainId => + chainId === dragging.value || chainId === dropping.value ? ( + + ) : ( + toggleSelected(chainId)} + /> + ) + )} + + )} + + + ); +} + +export function NetworkSelector({ onClose, onSelect, multiple }: { onClose: VoidFunction; onSelect: VoidFunction; multiple?: boolean }) { + const backgroundColor = '#191A1C'; + const separatorSecondary = useForegroundColor('separatorSecondary'); + const separatorTertiary = useForegroundColor('separatorTertiary'); + const fill = useForegroundColor('fill'); + + const translationY = useSharedValue(0); + + const swipeToClose = Gesture.Pan() + .onChange(event => { + if (event.translationY < 0) return; + translationY.value = event.translationY; + }) + .onFinalize(() => { + if (translationY.value > 120) runOnJS(onClose)(); + else translationY.value = withSpring(0); + }); + + const [isEditing, setEditing] = useState(false); + + const animatedStyles = useAnimatedStyle(() => ({ + transform: [{ translateY: translationY.value }], + })); + + const [selected, setSelected] = useState([]); + const unselect = (chainId: ChainId) => setSelected(s => s.filter(id => id !== chainId)); + const select = (chainId: ChainId) => setSelected(s => (multiple ? [...s, chainId] : [chainId])); + + return ( + + + + + + + + + + + + + + {isEditing ? 'Edit' : 'Network'} + + + setEditing(s => !s)} /> + + + + + + + + + ); +} From 51fee696ac133ae17ed9f9551c02fa92abbb4e7a Mon Sep 17 00:00:00 2001 From: gregs Date: Thu, 21 Nov 2024 14:14:48 -0300 Subject: [PATCH 04/95] feat flag --- src/config/experimental.ts | 2 ++ src/screens/discover/components/DiscoverHome.tsx | 4 ++++ src/styles/colors.ts | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/config/experimental.ts b/src/config/experimental.ts index 0e81cb9d1df..72b8da36994 100644 --- a/src/config/experimental.ts +++ b/src/config/experimental.ts @@ -30,6 +30,7 @@ export const DEGEN_MODE = 'Degen Mode'; export const FEATURED_RESULTS = 'Featured Results'; export const CLAIMABLES = 'Claimables'; export const NFTS_ENABLED = 'Nfts Enabled'; +export const TRENDING_TOKENS = 'Trending Tokens'; /** * A developer setting that pushes log lines to an array in-memory so that @@ -68,6 +69,7 @@ export const defaultConfig: Record = { [FEATURED_RESULTS]: { settings: true, value: false }, [CLAIMABLES]: { settings: true, value: false }, [NFTS_ENABLED]: { settings: true, value: !!IS_TEST }, + [TRENDING_TOKENS]: { settings: true, value: false }, }; export const defaultConfigValues: Record = Object.fromEntries( diff --git a/src/screens/discover/components/DiscoverHome.tsx b/src/screens/discover/components/DiscoverHome.tsx index 7132c19c016..fc58f141d19 100644 --- a/src/screens/discover/components/DiscoverHome.tsx +++ b/src/screens/discover/components/DiscoverHome.tsx @@ -6,6 +6,7 @@ import useExperimentalFlag, { MINTS, NFT_OFFERS, FEATURED_RESULTS, + TRENDING_TOKENS, } from '@rainbow-me/config/experimentalHooks'; import { isTestnetChain } from '@/handlers/web3'; import { Inline, Inset, Stack, Box } from '@/design-system'; @@ -28,6 +29,7 @@ import { FeaturedResultStack } from '@/components/FeaturedResult/FeaturedResultS import Routes from '@/navigation/routesNames'; import { useNavigation } from '@/navigation'; import { DiscoverFeaturedResultsCard } from './DiscoverFeaturedResultsCard'; +import { TrendingTokens } from './TrendingTokens'; export const HORIZONTAL_PADDING = 20; @@ -42,6 +44,7 @@ export default function DiscoverHome() { const mintsEnabled = (useExperimentalFlag(MINTS) || mints_enabled) && !IS_TEST; const opRewardsLocalFlag = useExperimentalFlag(OP_REWARDS); const opRewardsRemoteFlag = op_rewards_enabled; + const trendingTokensEnabled = useExperimentalFlag(TRENDING_TOKENS); const testNetwork = isTestnetChain({ chainId }); const { navigate } = useNavigation(); const isProfilesEnabled = profilesEnabledLocalFlag && profilesEnabledRemoteFlag; @@ -67,6 +70,7 @@ export default function DiscoverHome() { {isProfilesEnabled && } + {trendingTokensEnabled && } {mintsEnabled && ( diff --git a/src/styles/colors.ts b/src/styles/colors.ts index b1bfe39c376..0eeb8b0f104 100644 --- a/src/styles/colors.ts +++ b/src/styles/colors.ts @@ -49,7 +49,7 @@ const darkModeColors = { skeleton: '#191B21', stackBackground: '#000000', surfacePrimary: '#000000', - white: '#12131A', + white: '#000', whiteLabel: '#FFFFFF', }; From 4573d00053924c3d0e49d3420ef88b672dc935e4 Mon Sep 17 00:00:00 2001 From: gregs Date: Mon, 25 Nov 2024 14:51:13 -0300 Subject: [PATCH 05/95] opss --- src/components/coin-icon/ChainImage.tsx | 18 +- src/languages/en_US.json | 1 + .../discover/components/NetworkSwitcher.tsx | 520 +++++++++++++----- .../discover/components/TrendingTokens.tsx | 45 +- 4 files changed, 434 insertions(+), 150 deletions(-) diff --git a/src/components/coin-icon/ChainImage.tsx b/src/components/coin-icon/ChainImage.tsx index 4db59d9c61f..6c63ddc5890 100644 --- a/src/components/coin-icon/ChainImage.tsx +++ b/src/components/coin-icon/ChainImage.tsx @@ -12,9 +12,17 @@ import AvalancheBadge from '@/assets/badges/avalanche.png'; import BlastBadge from '@/assets/badges/blast.png'; import DegenBadge from '@/assets/badges/degen.png'; import ApechainBadge from '@/assets/badges/apechain.png'; -import FastImage, { Source } from 'react-native-fast-image'; +import FastImage, { FastImageProps, Source } from 'react-native-fast-image'; -export function ChainImage({ chainId, size = 20 }: { chainId: ChainId | null | undefined; size?: number }) { +export function ChainImage({ + chainId, + size = 20, + style, +}: { + chainId: ChainId | null | undefined; + size?: number; + style?: FastImageProps['style']; +}) { const source = useMemo(() => { switch (chainId) { case ChainId.apechain: @@ -47,6 +55,10 @@ export function ChainImage({ chainId, size = 20 }: { chainId: ChainId | null | u if (!chainId) return null; return ( - + ); } diff --git a/src/languages/en_US.json b/src/languages/en_US.json index 0d2e0fcb380..ad8b7f0aaab 100644 --- a/src/languages/en_US.json +++ b/src/languages/en_US.json @@ -2986,6 +2986,7 @@ "farcaster": "Farcaster" }, "sort": { + "sort": "Sort", "volume": "Volume", "market_cap": "Market Cap", "top_gainers": "Top Gainers", diff --git a/src/screens/discover/components/NetworkSwitcher.tsx b/src/screens/discover/components/NetworkSwitcher.tsx index f4f1b6f78eb..10bb5df4eca 100644 --- a/src/screens/discover/components/NetworkSwitcher.tsx +++ b/src/screens/discover/components/NetworkSwitcher.tsx @@ -2,22 +2,28 @@ import { getChainColorWorklet } from '@/__swaps__/utils/swaps'; import { chainsLabel, SUPPORTED_CHAIN_IDS_ALPHABETICAL } from '@/chains'; import { ChainId } from '@/chains/types'; import { AbsolutePortal } from '@/components/AbsolutePortal'; +import { AnimatedBlurView } from '@/components/AnimatedComponents/AnimatedBlurView'; import { ChainImage } from '@/components/coin-icon/ChainImage'; -import { globalColors, Text, useBackgroundColor, useColorMode } from '@/design-system'; +import { DesignSystemProvider, globalColors, Separator, Text, useBackgroundColor, useColorMode } from '@/design-system'; import { useForegroundColor } from '@/design-system/color/useForegroundColor'; -import hiddenTokens from '@/redux/hiddenTokens'; import { createRainbowStore } from '@/state/internal/createRainbowStore'; import { nonceStore } from '@/state/nonces'; - +import { useTheme } from '@/theme'; +import MaskedView from '@react-native-masked-view/masked-view'; import chroma from 'chroma-js'; -import { useReducer, useState } from 'react'; -import React, { StyleSheet, View } from 'react-native'; +import { Component, forwardRef, useReducer, useState } from 'react'; +import React, { Pressable, View, ViewStyle } from 'react-native'; import { Gesture, GestureDetector, GestureStateChangeEvent, TapGestureHandlerEventPayload } from 'react-native-gesture-handler'; +import LinearGradient from 'react-native-linear-gradient'; import Animated, { + BounceIn, FadeIn, FadeOut, + FadeOutUp, LinearTransition, + makeMutable, runOnJS, + SharedValue, SlideInDown, SlideOutDown, useAnimatedReaction, @@ -27,6 +33,7 @@ import Animated, { withSpring, withTiming, } from 'react-native-reanimated'; +import Svg, { Path } from 'react-native-svg'; function getMostUsedChains() { const noncesByAddress = nonceStore.getState().nonces; @@ -45,17 +52,16 @@ function getMostUsedChains() { } const initialPinnedNetworks = getMostUsedChains().slice(0, 5); -const useNetworkSwitcherStore = createRainbowStore<{ pinnedNetworks: ChainId[]; unpinnedNetworks: ChainId[] }>( +const useNetworkSwitcherStore = createRainbowStore<{ pinnedNetworks: ChainId[] }>( (set, get) => ({ pinnedNetworks: initialPinnedNetworks, - unpinnedNetworks: SUPPORTED_CHAIN_IDS_ALPHABETICAL.filter(chainId => !initialPinnedNetworks.includes(chainId)), }) // { // storageKey: 'network-switcher', - // version: 1, + // // version: 0, // } ); -const setNetworkSwitcherState = (s: { pinnedNetworks: ChainId[]; unpinnedNetworks: ChainId[] }) => { +const setNetworkSwitcherState = (s: { pinnedNetworks: ChainId[] }) => { useNetworkSwitcherStore.setState(s); }; @@ -102,24 +108,93 @@ function EditButton({ text, onPress }: { text: string; onPress: VoidFunction }) ); } -function NetworkOption({ - chainId, +function ExpandNetworks({ + hiddenNetworksLength, + isExpanded, + toggleExpanded, +}: { + hiddenNetworksLength: number; + toggleExpanded: (expanded: boolean) => void; + isExpanded: boolean; +}) { + const pressed = useSharedValue(false); + + const tap = Gesture.Tap() + .onBegin(() => { + pressed.value = true; + }) + .onFinalize(() => { + pressed.value = false; + runOnJS(toggleExpanded)(!isExpanded); + }); + + const animatedStyles = useAnimatedStyle(() => ({ + transform: [{ scale: withTiming(pressed.value ? 0.95 : 1, { duration: 100 }) }], + })); + + return ( + + + {!isExpanded && ( + + + {hiddenNetworksLength} + + + )} + + {isExpanded ? 'Show Less' : 'More Networks'} + + + + {isExpanded ? '􀆇' : '􀆈'} + + + + + ); +} + +function AllNetworksOption({ selected, onPress, }: { - chainId: ChainId; selected: boolean; onPress?: (e: GestureStateChangeEvent) => void; }) { - const name = chainsLabel[chainId]; - if (!name) throw new Error(`No chain name for chainId ${chainId}`); - const { isDarkMode } = useColorMode(); - const surfaceSecondary = useBackgroundColor('fillSecondary'); - const chainColor = chroma(getChainColorWorklet(chainId, true)).alpha(0.16).hex(); - const backgroundColor = selected ? chainColor : isDarkMode ? globalColors.white10 : globalColors.grey20; + const surfacePrimary = useBackgroundColor('surfacePrimary'); + const networkSwitcherBackgroundColor = isDarkMode ? '#191A1C' : surfacePrimary; + + const blue = useForegroundColor('blue'); - const borderColor = selected ? chainColor : '#F5F8FF05'; + const backgroundColor = selected + ? chroma.scale([networkSwitcherBackgroundColor, blue])(0.16).hex() + : isDarkMode + ? globalColors.white10 + : '#F2F3F4'; + const borderColor = selected ? chroma(blue).alpha(0.16).hex() : '#F5F8FF05'; const pressed = useSharedValue(false); @@ -137,14 +212,22 @@ function NetworkOption({ transform: [{ scale: withTiming(pressed.value ? 0.95 : 1, { duration: 100 }) }], })); + const overlappingBadge = { + borderColor: backgroundColor, + borderWidth: 1.67, + borderRadius: 16, + marginLeft: -9, + width: 16 + 1.67 * 2, + height: 16 + 1.67 * 2, + }; + return ( - + + + + + + - {name} + All Networks ); } -function ExpandNetworks({ - hiddenNetworksLength, - isExpanded, - toggleExpanded, +function NetworkOption({ + chainId, + selected, + onPress, + style, }: { - hiddenNetworksLength: number; - toggleExpanded: (expanded: boolean) => void; - isExpanded: boolean; + chainId: ChainId; + selected: boolean; + onPress?: (e: GestureStateChangeEvent) => void; + style?: ViewStyle; }) { + const name = chainsLabel[chainId]; + if (!name) throw new Error(`: No chain name for chainId ${chainId}`); + + const { isDarkMode } = useColorMode(); + + const surfacePrimary = useBackgroundColor('surfacePrimary'); + const networkSwitcherBackgroundColor = isDarkMode ? '#191A1C' : surfacePrimary; + + const chainColor = getChainColorWorklet(chainId, true); + const backgroundColor = selected + ? chroma.scale([networkSwitcherBackgroundColor, chainColor])(0.16).hex() + : isDarkMode + ? globalColors.white10 + : globalColors.grey20; + + const borderColor = selected ? chroma(chainColor).alpha(0.16).hex() : '#F5F8FF05'; + const pressed = useSharedValue(false); const tap = Gesture.Tap() - .onBegin(() => { + .onBegin(e => { pressed.value = true; + if (onPress) runOnJS(onPress)(e); }) .onFinalize(() => { pressed.value = false; - runOnJS(toggleExpanded)(!isExpanded); - }); + }) + .enabled(!!onPress); const animatedStyles = useAnimatedStyle(() => ({ transform: [{ scale: withTiming(pressed.value ? 0.95 : 1, { duration: 100 }) }], @@ -192,42 +300,27 @@ function ExpandNetworks({ return ( - {!isExpanded && ( - - - {hiddenNetworksLength} - - - )} - - {isExpanded ? 'Show Less' : 'More Networks'} + + + {name} - - - {isExpanded ? '􀆇' : '􀆈'} - - ); @@ -240,8 +333,6 @@ const ITEM_WIDTH = 164.5; const GAP = 12; const HALF_GAP = 6; -const styles = StyleSheet.create({ draggingItem: { zIndex: 2, position: 'absolute', height: ITEM_HEIGHT, width: ITEM_WIDTH } }); - /* - what to do if user pins all networks (where to drag to unpin) @@ -249,6 +340,32 @@ const styles = StyleSheet.create({ draggingItem: { zIndex: 2, position: 'absolut */ +function DraggingItem({ + chainId, + selected, + transform, +}: { + chainId: ChainId | null; + transform: SharedValue; + selected: boolean; +}) { + const draggingStyles = useAnimatedStyle(() => { + if (!transform.value) return { opacity: 0 }; + return { + opacity: 1, + transform: [{ scale: transform.value.scale }], + left: transform.value.x, + top: transform.value.y, + }; + }); + + return ( + + {chainId && } + + ); +} + function NetworksGrid({ editing, selected, @@ -260,9 +377,10 @@ function NetworksGrid({ unselect: (chainId: ChainId) => void; select: (chainId: ChainId) => void; }) { - const networks = useSharedValue(useNetworkSwitcherStore()); - const pinnedNetworks = useDerivedValue(() => networks.value.pinnedNetworks); - const unpinnedNetworks = useDerivedValue(() => networks.value.unpinnedNetworks); + const pinnedNetworks = useSharedValue(useNetworkSwitcherStore(s => s.pinnedNetworks)); + const unpinnedNetworks = useDerivedValue(() => + SUPPORTED_CHAIN_IDS_ALPHABETICAL.filter(chainId => !pinnedNetworks.value.includes(chainId)) + ); const dragging = useSharedValue(null); const draggingTransform = useSharedValue(null); @@ -273,15 +391,21 @@ function NetworksGrid({ // Sync back to store useAnimatedReaction( - () => networks.value, - current => runOnJS(setNetworkSwitcherState)(current) + () => pinnedNetworks.value, + (pinnedNetworks, prev) => { + if (!prev) return; // no need to react on initial value + runOnJS(setNetworkSwitcherState)({ pinnedNetworks }); + } ); // Force rerender when dragging or dropping changes const [, rerender] = useReducer(x => x + 1, 0); useAnimatedReaction( () => [dropping.value, dragging.value], - () => runOnJS(rerender)() + () => { + console.log('force rerendering'); + runOnJS(rerender)(); + } ); const positionIndex = (x: number, y: number) => { @@ -308,17 +432,22 @@ function NetworksGrid({ 'worklet'; const isTargetUnpinned = e.y > unpinnedGridY.value; - if (isTargetUnpinned && pinnedNetworks.value.length === 1) return; + if (!isTargetUnpinned && pinnedNetworks.value.length === 1) return; const index = positionIndex(e.x, e.y); const position = indexPosition(index, isTargetUnpinned); draggingTransform.value = { x: position.x, y: position.y, scale: 1 }; // initial position is the grid slot - draggingTransform.value = withSpring({ x: e.x - ITEM_WIDTH * 0.5, y: e.y - ITEM_HEIGHT * 0.5, scale: 1.05 }); // animate into the center of the pointer const targetArray = isTargetUnpinned ? unpinnedNetworks.value : pinnedNetworks.value; const chainId = targetArray[index]; dragging.value = chainId; + + draggingTransform.value = withSpring({ + x: e.x - ITEM_WIDTH * 0.5, + y: e.y - ITEM_HEIGHT * 0.5, + scale: 1.05, + }); // animate into the center of the pointer }) .onChange(e => { 'worklet'; @@ -334,37 +463,51 @@ function NetworksGrid({ const isDraggingOverUnpinned = e.y > unpinnedGridY.value; - const targetArrayKey = isDraggingOverUnpinned ? 'unpinnedNetworks' : 'pinnedNetworks'; - const otherArrayKey = isDraggingOverUnpinned ? 'pinnedNetworks' : 'unpinnedNetworks'; + const currentIndexAtPinned = pinnedNetworks.value.indexOf(chainId); + const isPinned = currentIndexAtPinned !== -1; - const targetArray = networks.value[targetArrayKey]; - const targetIndex = Math.min(positionIndex(e.x, e.y), targetArray.length - 1); - const indexInTarget = targetArray.indexOf(chainId); + // We don't reorder unpinned networks + if (isDraggingOverUnpinned && !isPinned) return; - if (indexInTarget === -1) { - networks.modify(v => { - // Pin/Unpin - v[otherArrayKey] = v[otherArrayKey].filter(id => id !== chainId); - v[targetArrayKey].splice(targetIndex, 0, chainId); - return v; + // Unpin + if (isDraggingOverUnpinned && isPinned) { + pinnedNetworks.modify(networks => { + networks.splice(currentIndexAtPinned, 1); + return networks; }); - } else if (indexInTarget !== targetIndex) { - // Reorder - networks.modify(v => { - const [movedChainId] = v[targetArrayKey].splice(indexInTarget, 1); - v[targetArrayKey].splice(targetIndex, 0, movedChainId); - return v; + return; + } + + // Pin + if (!isDraggingOverUnpinned && !isPinned) { + pinnedNetworks.modify(networks => { + networks.push(chainId); + return networks; + }); + return; + } + + // Reorder + const newIndex = Math.min(positionIndex(e.x, e.y), pinnedNetworks.value.length - 1); + if (newIndex !== currentIndexAtPinned) { + pinnedNetworks.modify(networks => { + networks.splice(currentIndexAtPinned, 1); + networks.splice(newIndex, 0, chainId); + return networks; }); } }) .onFinalize(e => { 'worklet'; - if (!dragging.value) return; + const chainId = dragging.value; + if (!chainId) return; const isDroppingInUnpinned = e.y > unpinnedGridY.value; - const targetArray = isDroppingInUnpinned ? pinnedNetworks.value : unpinnedNetworks.value; - const index = Math.min(positionIndex(e.x, e.y), targetArray.length - 1); + const index = isDroppingInUnpinned + ? unpinnedNetworks.value.indexOf(chainId) + : Math.min(positionIndex(e.x, e.y), pinnedNetworks.value.length - 1); + const { x, y } = indexPosition(index, isDroppingInUnpinned); droppingTransform.value = draggingTransform.value; @@ -380,32 +523,14 @@ function NetworksGrid({ () => unpinnedGridY.value, (newY, prevY) => { // the layout can recalculate after the drop started - if (!prevY || !droppingTransform.value || droppingTransform.value.y < prevY) return; - const { x, y, scale } = droppingTransform.value; - droppingTransform.value = withSpring({ x, y: y + (prevY - newY), scale }, { mass: 0.6 }, completed => { + if (!prevY || !droppingTransform.value) return; + const { x, y } = droppingTransform.value; // TODO: does not work + droppingTransform.value = withSpring({ x, y: y + (prevY - newY), scale: 1 }, { mass: 0.6 }, completed => { if (completed) dropping.value = null; }); } ); - const draggingStyles = useAnimatedStyle(() => { - if (!draggingTransform.value) return {}; - return { - transform: [{ scale: draggingTransform.value.scale }], - left: draggingTransform.value.x, - top: draggingTransform.value.y, - }; - }); - - const droppingStyles = useAnimatedStyle(() => { - if (!droppingTransform.value) return {}; - return { - transform: [{ scale: droppingTransform.value.scale }], - left: droppingTransform.value.x, - top: droppingTransform.value.y, - }; - }); - const [isExpanded, setExpanded] = useState(false); const toggleSelected = (chainId: ChainId) => { @@ -413,30 +538,31 @@ function NetworksGrid({ else select(chainId); }; - // const mainGridNetworks = editing - // ? pinnedNetworks.value - // : [...pinnedNetworks.value, ...unpinnedNetworks.value.filter(chainId => selected.includes(chainId))]; + const pinnedGrid = editing + ? pinnedNetworks.value + : [...pinnedNetworks.value, ...unpinnedNetworks.value.filter(chainId => selected.includes(chainId))]; + + const unpinnedGrid = editing ? unpinnedNetworks.value : unpinnedNetworks.value.filter(chainId => !selected.includes(chainId)); return ( - {!!dropping.value && ( - - - - )} - - {!!dragging.value && ( - - - - )} + + - {pinnedNetworks.value.map(chainId => + {pinnedGrid.map(chainId => chainId === dragging.value || chainId === dropping.value ? ( ) : ( @@ -465,17 +591,16 @@ function NetworksGrid({ onLayout={e => (unpinnedGridY.value = e.nativeEvent.layout.y)} layout={LinearTransition} entering={FadeIn.duration(250).delay(125)} - exiting={FadeOut.duration(50)} style={{ flexDirection: 'row', gap: 12, flexWrap: 'wrap', paddingVertical: 12 }} > - {unpinnedNetworks.value.length === 0 && ( + {unpinnedGrid.length === 0 && ( Drag here to unpin networks )} - {unpinnedNetworks.value.map(chainId => + {unpinnedGrid.map(chainId => chainId === dragging.value || chainId === dropping.value ? ( ) : ( @@ -494,12 +619,115 @@ function NetworksGrid({ ); } +const useCustomizeNetworksBanner = createRainbowStore<{ + dismissedAt: number; // timestamp +}>(() => ({ dismissedAt: 0 }), { + storageKey: 'CustomizeNetworksBanner', + version: 0, +}); +const twoWeeks = 1000 * 60 * 60 * 24 * 7 * 2; +const dismissCustomizeNetworksBanner = () => { + const { dismissedAt } = useCustomizeNetworksBanner.getState(); + if (Date.now() - dismissedAt < twoWeeks) return; + useCustomizeNetworksBanner.setState({ dismissedAt: Date.now() }); +}; + +function CustomizeNetworksBanner() { + const blue = '#268FFF'; + + const dismissedAt = useCustomizeNetworksBanner(s => s.dismissedAt); + const isOpen = Date.now() - dismissedAt > twoWeeks; + + if (!isOpen) return null; + + return ( + + + + + + } + > + + + + + 􀍱 + + + + + Customize Your Networks + + + Tap the{' '} + + Edit + {' '} + button below to set up + + + + + 􀆄 + + + + + + + + ); +} + export function NetworkSelector({ onClose, onSelect, multiple }: { onClose: VoidFunction; onSelect: VoidFunction; multiple?: boolean }) { - const backgroundColor = '#191A1C'; + const { isDarkMode } = useTheme(); + const surfacePrimary = useBackgroundColor('surfacePrimary'); + const backgroundColor = isDarkMode ? '#191A1C' : surfacePrimary; const separatorSecondary = useForegroundColor('separatorSecondary'); const separatorTertiary = useForegroundColor('separatorTertiary'); const fill = useForegroundColor('fill'); + console.log('rerender NetworkSelector'); + const translationY = useSharedValue(0); const swipeToClose = Gesture.Pan() @@ -518,9 +746,17 @@ export function NetworkSelector({ onClose, onSelect, multiple }: { onClose: Void transform: [{ translateY: translationY.value }], })); - const [selected, setSelected] = useState([]); - const unselect = (chainId: ChainId) => setSelected(s => s.filter(id => id !== chainId)); - const select = (chainId: ChainId) => setSelected(s => (multiple ? [...s, chainId] : [chainId])); + const [selected, setSelected] = useState([]); + const unselect = (chainId: ChainId) => + setSelected(s => { + if (s === 'all') return []; + return s.filter(id => id !== chainId); + }); + const select = (chainId: ChainId) => + setSelected(s => { + if (s === 'all') return [chainId]; + return multiple ? [...s, chainId] : [chainId]; + }); return ( @@ -553,6 +789,7 @@ export function NetworkSelector({ onClose, onSelect, multiple }: { onClose: Void animatedStyles, ]} > + @@ -564,12 +801,25 @@ export function NetworkSelector({ onClose, onSelect, multiple }: { onClose: Void {isEditing ? 'Edit' : 'Network'} - setEditing(s => !s)} /> + { + dismissCustomizeNetworksBanner(); + setEditing(s => !s); + }} + /> - + {multiple && !isEditing && ( + <> + setSelected('all')} /> + + + )} + + diff --git a/src/screens/discover/components/TrendingTokens.tsx b/src/screens/discover/components/TrendingTokens.tsx index a92fdbd012c..d9f5d38bb52 100644 --- a/src/screens/discover/components/TrendingTokens.tsx +++ b/src/screens/discover/components/TrendingTokens.tsx @@ -13,8 +13,7 @@ import LinearGradient from 'react-native-linear-gradient'; import Animated, { LinearTransition, runOnJS, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; import { NetworkSelector } from './NetworkSwitcher'; import * as i18n from '@/languages'; - -const TRANSLATIONS = i18n.l.cards.ens_search; +import { useTheme } from '@/theme'; const AnimatedLinearGradient = Animated.createAnimatedComponent(LinearGradient); @@ -84,8 +83,11 @@ function CategoryFilterButton({ iconWidth?: number; label: string; }) { - const backgroundColor = useBackgroundColor('fillTertiary'); - const borderColor = useBackgroundColor('fillSecondary'); + const { isDarkMode } = useTheme(); + const fillTertiary = useBackgroundColor('fillTertiary'); + const fillSecondary = useBackgroundColor('fillSecondary'); + + const borderColor = selected && isDarkMode ? globalColors.white80 : fillSecondary; const pressed = useSharedValue(false); @@ -103,7 +105,7 @@ function CategoryFilterButton({ return ( - + @@ -258,7 +277,9 @@ function TrendingTokenRow() { const t = i18n.l.trending_tokens; function NoResults() { - const backgroundColor = '#191A1C'; // useBackgroundColor('fillQuaternary'); + const { isDarkMode } = useTheme(); + const fillQuaternary = useBackgroundColor('fillQuaternary'); + const backgroundColor = isDarkMode ? '#191A1C' : fillQuaternary; return ( @@ -349,7 +370,7 @@ export function TrendingTokens() { side="bottom" onPressMenuItem={timeframe => setFilter(filter => ({ ...filter, timeframe }))} > - + - + From 9ebc63da1765aeb7ae7b9d147bada88a58e33c2d Mon Sep 17 00:00:00 2001 From: gregs Date: Mon, 25 Nov 2024 15:46:07 -0300 Subject: [PATCH 06/95] i18n --- src/languages/en_US.json | 13 +++++ .../discover/components/NetworkSwitcher.tsx | 55 +++++++++---------- 2 files changed, 38 insertions(+), 30 deletions(-) diff --git a/src/languages/en_US.json b/src/languages/en_US.json index ad8b7f0aaab..d1af4f9caeb 100644 --- a/src/languages/en_US.json +++ b/src/languages/en_US.json @@ -2999,6 +2999,19 @@ } } }, + "network_switcher": { + "customize_networks_banner": { + "title": "Customize Networks", + "description": "Tap the edit button below to set up" + }, + "edit": "Edit", + "networks": "Networks", + "drag_to_rearrange": "Drag to rearrange", + "show_less": "Show less", + "show_more": "More Networks", + "all_networks": "All Networks" + }, + "done": "Done", "copy": "Copy", "paste": "Paste" } diff --git a/src/screens/discover/components/NetworkSwitcher.tsx b/src/screens/discover/components/NetworkSwitcher.tsx index 10bb5df4eca..5d52cfeec42 100644 --- a/src/screens/discover/components/NetworkSwitcher.tsx +++ b/src/screens/discover/components/NetworkSwitcher.tsx @@ -11,18 +11,17 @@ import { nonceStore } from '@/state/nonces'; import { useTheme } from '@/theme'; import MaskedView from '@react-native-masked-view/masked-view'; import chroma from 'chroma-js'; -import { Component, forwardRef, useReducer, useState } from 'react'; +import { useReducer, useState } from 'react'; import React, { Pressable, View, ViewStyle } from 'react-native'; import { Gesture, GestureDetector, GestureStateChangeEvent, TapGestureHandlerEventPayload } from 'react-native-gesture-handler'; import LinearGradient from 'react-native-linear-gradient'; import Animated, { - BounceIn, FadeIn, FadeOut, FadeOutUp, LinearTransition, - makeMutable, runOnJS, + SequencedTransition, SharedValue, SlideInDown, SlideOutDown, @@ -34,6 +33,9 @@ import Animated, { withTiming, } from 'react-native-reanimated'; import Svg, { Path } from 'react-native-svg'; +import * as i18n from '@/languages'; + +const t = i18n.l.network_switcher; function getMostUsedChains() { const noncesByAddress = nonceStore.getState().nonces; @@ -53,13 +55,13 @@ function getMostUsedChains() { const initialPinnedNetworks = getMostUsedChains().slice(0, 5); const useNetworkSwitcherStore = createRainbowStore<{ pinnedNetworks: ChainId[] }>( - (set, get) => ({ + () => ({ pinnedNetworks: initialPinnedNetworks, - }) - // { - // storageKey: 'network-switcher', - // // version: 0, - // } + }), + { + storageKey: 'network-switcher', + version: 0, + } ); const setNetworkSwitcherState = (s: { pinnedNetworks: ChainId[] }) => { useNetworkSwitcherStore.setState(s); @@ -164,7 +166,7 @@ function ExpandNetworks({ )} - {isExpanded ? 'Show Less' : 'More Networks'} + {isExpanded ? i18n.t(t.show_less) : i18n.t(t.show_more)} @@ -246,7 +248,7 @@ function AllNetworksOption({ - All Networks + {i18n.t(t.all_networks)} @@ -333,13 +335,6 @@ const ITEM_WIDTH = 164.5; const GAP = 12; const HALF_GAP = 6; -/* - - - what to do if user pins all networks (where to drag to unpin) - - how to display if user selects a network that is in the "more networks" (hidden) - - */ - function DraggingItem({ chainId, selected, @@ -513,6 +508,7 @@ function NetworksGrid({ droppingTransform.value = draggingTransform.value; droppingTransform.value = withSpring({ y, x, scale: 1 }, { mass: 0.6 }, completed => { if (completed) dropping.value = null; + else droppingTransform.value = { y, x, scale: 1 }; }); dropping.value = dragging.value; dragging.value = null; @@ -524,7 +520,7 @@ function NetworksGrid({ (newY, prevY) => { // the layout can recalculate after the drop started if (!prevY || !droppingTransform.value) return; - const { x, y } = droppingTransform.value; // TODO: does not work + const { x, y } = droppingTransform.value; droppingTransform.value = withSpring({ x, y: y + (prevY - newY), scale: 1 }, { mass: 0.6 }, completed => { if (completed) dropping.value = null; }); @@ -558,10 +554,7 @@ function NetworksGrid({ selected={!!dragging.value && selected.includes(dragging.value)} /> - + {pinnedGrid.map(chainId => chainId === dragging.value || chainId === dropping.value ? ( @@ -579,7 +572,7 @@ function NetworksGrid({ {editing ? ( - Drag to Rearrange + {i18n.t(t.drag_to_rearrange)} ) : ( @@ -589,7 +582,7 @@ function NetworksGrid({ {(editing || isExpanded) && ( (unpinnedGridY.value = e.nativeEvent.layout.y)} - layout={LinearTransition} + layout={SequencedTransition.duration(500)} entering={FadeIn.duration(250).delay(125)} style={{ flexDirection: 'row', gap: 12, flexWrap: 'wrap', paddingVertical: 12 }} > @@ -695,9 +688,13 @@ function CustomizeNetworksBanner() { - Customize Your Networks + {i18n.t(t.customize_networks_banner.title)} + {/* + is there a way to render a diferent component mid sentence? + like i18n.t(t.customize_networks_banner.description, { Edit: }) + */} Tap the{' '} Edit @@ -726,8 +723,6 @@ export function NetworkSelector({ onClose, onSelect, multiple }: { onClose: Void const separatorTertiary = useForegroundColor('separatorTertiary'); const fill = useForegroundColor('fill'); - console.log('rerender NetworkSelector'); - const translationY = useSharedValue(0); const swipeToClose = Gesture.Pan() @@ -798,11 +793,11 @@ export function NetworkSelector({ onClose, onSelect, multiple }: { onClose: Void - {isEditing ? 'Edit' : 'Network'} + {isEditing ? i18n.t(t.edit) : i18n.t(t.networks)} { dismissCustomizeNetworksBanner(); setEditing(s => !s); From 06557d9264c0e54ce21bd377b7e46f90f3554b1c Mon Sep 17 00:00:00 2001 From: gregs Date: Mon, 2 Dec 2024 08:19:23 -0300 Subject: [PATCH 07/95] performance --- src/chains/index.ts | 2 +- src/components/coin-icon/ChainImage.tsx | 30 +- .../discover/components/NetworkSwitcher.tsx | 1230 ++++++++--------- .../discover/components/TrendingTokens.tsx | 11 +- src/state/internal/createRainbowStore.ts | 7 + 5 files changed, 628 insertions(+), 652 deletions(-) diff --git a/src/chains/index.ts b/src/chains/index.ts index 92a95f6b3e5..7915cccdf51 100644 --- a/src/chains/index.ts +++ b/src/chains/index.ts @@ -15,7 +15,7 @@ const BACKEND_CHAINS = transformBackendNetworksToChains(backendNetworks.networks export const SUPPORTED_CHAINS: Chain[] = IS_TEST ? [...BACKEND_CHAINS, chainHardhat, chainHardhatOptimism] : BACKEND_CHAINS; -export const SUPPORTED_CHAIN_IDS_ALPHABETICAL = SUPPORTED_CHAINS.sort((a, b) => a.name.localeCompare(b.name)).map(c => c.id); +export const SUPPORTED_CHAIN_IDS_ALPHABETICAL: ChainId[] = SUPPORTED_CHAINS.sort((a, b) => a.name.localeCompare(b.name)).map(c => c.id); export const defaultChains: Record = SUPPORTED_CHAINS.reduce( (acc, chain) => { diff --git a/src/components/coin-icon/ChainImage.tsx b/src/components/coin-icon/ChainImage.tsx index 6c63ddc5890..ed7cb9301e2 100644 --- a/src/components/coin-icon/ChainImage.tsx +++ b/src/components/coin-icon/ChainImage.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import React, { forwardRef, useMemo } from 'react'; import { ChainId } from '@/chains/types'; import ArbitrumBadge from '@/assets/badges/arbitrum.png'; @@ -13,16 +13,20 @@ import BlastBadge from '@/assets/badges/blast.png'; import DegenBadge from '@/assets/badges/degen.png'; import ApechainBadge from '@/assets/badges/apechain.png'; import FastImage, { FastImageProps, Source } from 'react-native-fast-image'; +import Animated from 'react-native-reanimated'; -export function ChainImage({ - chainId, - size = 20, - style, -}: { - chainId: ChainId | null | undefined; - size?: number; - style?: FastImageProps['style']; -}) { +export const ChainImage = forwardRef(function ChainImage( + { + chainId, + size = 20, + style, + }: { + chainId: ChainId | null | undefined; + size?: number; + style?: FastImageProps['style']; + }, + ref +) { const source = useMemo(() => { switch (chainId) { case ChainId.apechain: @@ -56,9 +60,13 @@ export function ChainImage({ return ( ); -} +}); + +export const AnimatedChainImage = Animated.createAnimatedComponent(ChainImage); diff --git a/src/screens/discover/components/NetworkSwitcher.tsx b/src/screens/discover/components/NetworkSwitcher.tsx index 5d52cfeec42..bc8853ae335 100644 --- a/src/screens/discover/components/NetworkSwitcher.tsx +++ b/src/screens/discover/components/NetworkSwitcher.tsx @@ -1,19 +1,25 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ import { getChainColorWorklet } from '@/__swaps__/utils/swaps'; +import { analyticsV2 } from '@/analytics'; import { chainsLabel, SUPPORTED_CHAIN_IDS_ALPHABETICAL } from '@/chains'; import { ChainId } from '@/chains/types'; import { AbsolutePortal } from '@/components/AbsolutePortal'; import { AnimatedBlurView } from '@/components/AnimatedComponents/AnimatedBlurView'; -import { ChainImage } from '@/components/coin-icon/ChainImage'; -import { DesignSystemProvider, globalColors, Separator, Text, useBackgroundColor, useColorMode } from '@/design-system'; +import { ButtonPressAnimation } from '@/components/animations'; +import { SPRING_CONFIGS } from '@/components/animations/animationConfigs'; +import { AnimatedChainImage, ChainImage } from '@/components/coin-icon/ChainImage'; +import { AnimatedText, DesignSystemProvider, globalColors, Separator, Text, useBackgroundColor, useColorMode } from '@/design-system'; import { useForegroundColor } from '@/design-system/color/useForegroundColor'; +import * as i18n from '@/languages'; import { createRainbowStore } from '@/state/internal/createRainbowStore'; import { nonceStore } from '@/state/nonces'; import { useTheme } from '@/theme'; +import { DEVICE_WIDTH } from '@/utils/deviceUtils'; import MaskedView from '@react-native-masked-view/masked-view'; import chroma from 'chroma-js'; -import { useReducer, useState } from 'react'; -import React, { Pressable, View, ViewStyle } from 'react-native'; -import { Gesture, GestureDetector, GestureStateChangeEvent, TapGestureHandlerEventPayload } from 'react-native-gesture-handler'; +import { PropsWithChildren, ReactElement, useEffect } from 'react'; +import React, { Pressable, View } from 'react-native'; +import { Gesture, GestureDetector, State } from 'react-native-gesture-handler'; import LinearGradient from 'react-native-linear-gradient'; import Animated, { FadeIn, @@ -21,7 +27,6 @@ import Animated, { FadeOutUp, LinearTransition, runOnJS, - SequencedTransition, SharedValue, SlideInDown, SlideOutDown, @@ -29,11 +34,12 @@ import Animated, { useAnimatedStyle, useDerivedValue, useSharedValue, + withDelay, + withSequence, withSpring, withTiming, } from 'react-native-reanimated'; import Svg, { Path } from 'react-native-svg'; -import * as i18n from '@/languages'; const t = i18n.l.network_switcher; @@ -53,199 +59,274 @@ function getMostUsedChains() { .map(([chainId]) => parseInt(chainId)); } -const initialPinnedNetworks = getMostUsedChains().slice(0, 5); -const useNetworkSwitcherStore = createRainbowStore<{ pinnedNetworks: ChainId[] }>( - () => ({ - pinnedNetworks: initialPinnedNetworks, - }), - { - storageKey: 'network-switcher', - version: 0, - } -); -const setNetworkSwitcherState = (s: { pinnedNetworks: ChainId[] }) => { - useNetworkSwitcherStore.setState(s); +// const pinnedNetworks = getMostUsedChains().slice(0, 5); +const useNetworkSwitcherStore = createRainbowStore<{ + pinnedNetworks: ChainId[]; +}>(() => ({ pinnedNetworks: [] }), { + storageKey: 'network-switcher', + version: 0, + onRehydrateStorage(state) { + // if we are missing pinned networks, use the user most used chains + if (state.pinnedNetworks.length === 0) { + const mostUsedNetworks = getMostUsedChains(); + state.pinnedNetworks = mostUsedNetworks.slice(0, 5); + analyticsV2.identify({ mostUsedNetworks: mostUsedNetworks.filter(Boolean) }); + } + }, +}); +const setNetworkSwitcherPinned = (pinnedNetworks: ChainId[]) => { + useNetworkSwitcherStore.setState({ pinnedNetworks }); }; -function EditButton({ text, onPress }: { text: string; onPress: VoidFunction }) { - const blue = useForegroundColor('blue'); - - const pressed = useSharedValue(false); +const translations = { + edit: i18n.t(t.edit), + done: i18n.t(i18n.l.done), + networks: i18n.t(t.networks), + show_more: i18n.t(t.show_more), + show_less: i18n.t(t.show_less), + drag_to_rearrange: i18n.t(t.drag_to_rearrange), +}; - const tap = Gesture.Tap() - .onBegin(() => { - pressed.value = true; - }) - .onFinalize(() => { - pressed.value = false; - runOnJS(onPress)(); - }); +function EditButton({ editing }: { editing: SharedValue }) { + const blue = useForegroundColor('blue'); + const borderColor = chroma(blue).alpha(0.08).hex(); - const animatedStyles = useAnimatedStyle(() => ({ - transform: [{ scale: withTiming(pressed.value ? 0.95 : 1, { duration: 100 }) }], - })); + const text = useDerivedValue(() => (editing.value ? translations.done : translations.edit)); return ( - - - - {text} - - - + { + 'worklet'; + editing.value = !editing.value; + }} + scaleTo={0.95} + style={[ + { position: 'absolute', right: 0 }, + { paddingHorizontal: 10, height: 28, justifyContent: 'center' }, + { borderColor, borderWidth: 1.33, borderRadius: 14 }, + ]} + > + + {text} + + ); } -function ExpandNetworks({ - hiddenNetworksLength, - isExpanded, - toggleExpanded, -}: { - hiddenNetworksLength: number; - toggleExpanded: (expanded: boolean) => void; - isExpanded: boolean; -}) { - const pressed = useSharedValue(false); - - const tap = Gesture.Tap() - .onBegin(() => { - pressed.value = true; - }) - .onFinalize(() => { - pressed.value = false; - runOnJS(toggleExpanded)(!isExpanded); - }); +function Header({ editing }: { editing: SharedValue }) { + const separatorTertiary = useForegroundColor('separatorTertiary'); + const fill = useForegroundColor('fill'); - const animatedStyles = useAnimatedStyle(() => ({ - transform: [{ scale: withTiming(pressed.value ? 0.95 : 1, { duration: 100 }) }], - })); + const title = useDerivedValue(() => { + return editing.value ? translations.edit : translations.networks; + }); return ( - - - {!isExpanded && ( - - - {hiddenNetworksLength} - - - )} - - {isExpanded ? i18n.t(t.show_less) : i18n.t(t.show_more)} - - - - {isExpanded ? '􀆇' : '􀆈'} - - - - + + + + + + + + {title} + + + + + ); } -function AllNetworksOption({ - selected, - onPress, -}: { - selected: boolean; - onPress?: (e: GestureStateChangeEvent) => void; -}) { +const useCustomizeNetworksBanner = createRainbowStore<{ + dismissedAt: number; // timestamp +}>(() => ({ dismissedAt: 0 }), { + storageKey: 'CustomizeNetworksBanner', + version: 0, +}); +const twoWeeks = 1000 * 60 * 60 * 24 * 7 * 2; +const should_show_CustomizeNetworksBanner = (dismissedAt: number) => Date.now() - dismissedAt > twoWeeks; +const dismissCustomizeNetworksBanner = () => { + const { dismissedAt } = useCustomizeNetworksBanner.getState(); + if (should_show_CustomizeNetworksBanner(dismissedAt)) return; + useCustomizeNetworksBanner.setState({ dismissedAt: Date.now() }); +}; +const show_CustomizeNetworksBanner = should_show_CustomizeNetworksBanner(useCustomizeNetworksBanner.getState().dismissedAt); + +const CustomizeNetworksBanner = !show_CustomizeNetworksBanner + ? () => null + : function CustomizeNetworksBanner({ editing }: { editing: SharedValue }) { + useAnimatedReaction( + () => editing.value, + (editing, prev) => { + if (!prev && editing) runOnJS(dismissCustomizeNetworksBanner)(); + } + ); + + const dismissedAt = useCustomizeNetworksBanner(s => s.dismissedAt); + if (!should_show_CustomizeNetworksBanner(dismissedAt)) return null; + + const height = 75; + const blue = '#268FFF'; + + return ( + + + + + + } + > + + + + + 􀍱 + + + + + {i18n.t(t.customize_networks_banner.title)} + + + {/* + is there a way to render a diferent component mid sentence? + like i18n.t(t.customize_networks_banner.description, { Edit: }) + */} + Tap the{' '} + + Edit + {' '} + button below to set up + + + + + 􀆄 + + + + + + + + ); + }; + +const useNetworkOptionStyle = (isSelected: SharedValue, color: string) => { const { isDarkMode } = useColorMode(); + const surfacePrimary = useBackgroundColor('surfacePrimary'); const networkSwitcherBackgroundColor = isDarkMode ? '#191A1C' : surfacePrimary; - const blue = useForegroundColor('blue'); + const defaultStyle = { + backgroundColor: isDarkMode ? globalColors.white10 : globalColors.grey20, + borderColor: '#F5F8FF05', + }; + const selectedStyle = { + backgroundColor: chroma.scale([networkSwitcherBackgroundColor, color])(0.16).hex(), + borderColor: chroma(color).alpha(0.16).hex(), + }; - const backgroundColor = selected - ? chroma.scale([networkSwitcherBackgroundColor, blue])(0.16).hex() - : isDarkMode - ? globalColors.white10 - : '#F2F3F4'; - const borderColor = selected ? chroma(blue).alpha(0.16).hex() : '#F5F8FF05'; + const scale = useSharedValue(1); + useAnimatedReaction( + () => isSelected.value, + () => { + scale.value = withSequence(withTiming(0.95, { duration: 50 }), withTiming(1, { duration: 80 })); + } + ); - const pressed = useSharedValue(false); + const animatedStyle = useAnimatedStyle(() => { + const colors = isSelected.value ? selectedStyle : defaultStyle; + return { + backgroundColor: colors.backgroundColor, + borderColor: colors.borderColor, + transform: [{ scale: scale.value }], + }; + }); - const tap = Gesture.Tap() - .onBegin(e => { - pressed.value = true; - if (onPress) runOnJS(onPress)(e); - }) - .onFinalize(() => { - pressed.value = false; - }) - .enabled(!!onPress); + return { + animatedStyle, + selectedStyle, + defaultStyle, + }; +}; - const animatedStyles = useAnimatedStyle(() => ({ - transform: [{ scale: withTiming(pressed.value ? 0.95 : 1, { duration: 100 }) }], - })); +function AllNetworksOption({ selected }: { selected: SharedValue }) { + const blue = useForegroundColor('blue'); - const overlappingBadge = { - borderColor: backgroundColor, - borderWidth: 1.67, - borderRadius: 16, - marginLeft: -9, - width: 16 + 1.67 * 2, - height: 16 + 1.67 * 2, - }; + const isSelected = useDerivedValue(() => selected.value === 'all'); + const { animatedStyle, selectedStyle, defaultStyle } = useNetworkOptionStyle(isSelected, blue); + + const overlappingBadge = useAnimatedStyle(() => { + return { + borderColor: isSelected.value ? selectedStyle.borderColor : defaultStyle.borderColor, + borderWidth: 1.67, + borderRadius: 16, + marginLeft: -9, + width: 16 + 1.67 * 2, // size + borders + height: 16 + 1.67 * 2, + }; + }); + + const tapGesture = Gesture.Tap().onTouchesDown(() => { + if (selected.value === 'all') selected.value = []; + else selected.value = 'all'; + }); return ( - + - - - - + + + + {i18n.t(t.all_networks)} @@ -255,473 +336,376 @@ function AllNetworksOption({ ); } -function NetworkOption({ - chainId, - selected, - onPress, - style, -}: { - chainId: ChainId; - selected: boolean; - onPress?: (e: GestureStateChangeEvent) => void; - style?: ViewStyle; -}) { +function AllNetworksSection({ editing, selected }: { editing: SharedValue; selected: SharedValue }) { + const style = useAnimatedStyle(() => ({ + opacity: editing.value ? withTiming(0, { duration: 50 }) : withDelay(250, withTiming(1, { duration: 250 })), + height: withTiming( + editing.value ? 0 : ITEM_HEIGHT + 14, // 14 is the gap to the separator + { duration: 250 } + ), + marginTop: editing.value ? 0 : 14, + pointerEvents: editing.value ? 'none' : 'auto', + })); + return ( + + + + + ); +} + +function NetworkOption({ chainId, selected }: { chainId: ChainId; selected: SharedValue }) { const name = chainsLabel[chainId]; if (!name) throw new Error(`: No chain name for chainId ${chainId}`); - const { isDarkMode } = useColorMode(); - - const surfacePrimary = useBackgroundColor('surfacePrimary'); - const networkSwitcherBackgroundColor = isDarkMode ? '#191A1C' : surfacePrimary; - const chainColor = getChainColorWorklet(chainId, true); - const backgroundColor = selected - ? chroma.scale([networkSwitcherBackgroundColor, chainColor])(0.16).hex() - : isDarkMode - ? globalColors.white10 - : globalColors.grey20; - - const borderColor = selected ? chroma(chainColor).alpha(0.16).hex() : '#F5F8FF05'; - - const pressed = useSharedValue(false); - - const tap = Gesture.Tap() - .onBegin(e => { - pressed.value = true; - if (onPress) runOnJS(onPress)(e); - }) - .onFinalize(() => { - pressed.value = false; - }) - .enabled(!!onPress); - - const animatedStyles = useAnimatedStyle(() => ({ - transform: [{ scale: withTiming(pressed.value ? 0.95 : 1, { duration: 100 }) }], - })); + const isSelected = useDerivedValue(() => selected.value !== 'all' && selected.value.includes(chainId)); + const { animatedStyle } = useNetworkOptionStyle(isSelected, chainColor); return ( - - - - - {name} - - - + + + + {name} + + ); } -type Transform = { x: number; y: number; scale: number }; - -const ITEM_HEIGHT = 48; -const ITEM_WIDTH = 164.5; +const SHEET_OUTER_INSET = 8; +const SHEET_INNER_PADDING = 16; const GAP = 12; -const HALF_GAP = 6; +const ITEM_WIDTH = (DEVICE_WIDTH - SHEET_INNER_PADDING * 2 - SHEET_OUTER_INSET * 2 - GAP) / 2; +const ITEM_HEIGHT = 48; +const SEPARATOR_HEIGHT = 68; +const enum Section { + pinned, + unpinned, +} -function DraggingItem({ +function Draggable({ + children, + dragging, chainId, - selected, - transform, -}: { - chainId: ChainId | null; - transform: SharedValue; - selected: boolean; -}) { - const draggingStyles = useAnimatedStyle(() => { - if (!transform.value) return { opacity: 0 }; + networks, + sectionsOffsets, + isUnpinnedHidden, +}: PropsWithChildren<{ + chainId: ChainId; + dragging: SharedValue; + networks: SharedValue>; + sectionsOffsets: SharedValue>; + isUnpinnedHidden: SharedValue; +}>) { + const zIndex = useSharedValue(0); + useAnimatedReaction( + () => dragging.value?.chainId, + (current, prev) => { + if (current === prev) return; + if (current === chainId) zIndex.value = 2; + if (prev === chainId) zIndex.value = 1; + } + ); + + const draggableStyles = useAnimatedStyle(() => { + const section = networks.value[Section.pinned].includes(chainId) ? Section.pinned : Section.unpinned; + const itemIndex = networks.value[section].indexOf(chainId); + const slotPosition = positionFromIndex(itemIndex, sectionsOffsets.value[section]); + + const opacity = + section === Section.unpinned && isUnpinnedHidden.value ? withTiming(0, { duration: 150 }) : withDelay(150, withTiming(1)); + + const isBeingDragged = dragging.value?.chainId === chainId; + const position = isBeingDragged ? dragging.value!.position : slotPosition; + return { - opacity: 1, - transform: [{ scale: transform.value.scale }], - left: transform.value.x, - top: transform.value.y, + opacity, + zIndex: zIndex.value, + transform: [ + { scale: withSpring(isBeingDragged ? 1.05 : 1, SPRING_CONFIGS.springConfig) }, + { translateX: isBeingDragged ? position.x : withSpring(position.x, SPRING_CONFIGS.springConfig) }, + { translateY: isBeingDragged ? position.y : withSpring(position.y, SPRING_CONFIGS.springConfig) }, + ], }; }); - return ( - - {chainId && } - - ); + return {children}; } -function NetworksGrid({ +const indexFromPosition = (x: number, y: number, offset: { y: number }) => { + 'worklet'; + const yoffsets = y > offset.y ? offset.y : 0; + const column = x > ITEM_WIDTH + GAP / 2 ? 1 : 0; + const row = Math.floor((y - yoffsets) / (ITEM_HEIGHT + GAP / 2)); + const index = row * 2 + column; + return index < 0 ? 0 : index; // row can be negative if the dragged item is above the first row +}; + +const positionFromIndex = (index: number, offset: { y: number }) => { + 'worklet'; + const column = index % 2; + const row = Math.floor(index / 2); + const position = { x: column * (ITEM_WIDTH + GAP), y: row * (ITEM_HEIGHT + GAP) + offset.y }; + return position; +}; + +type Point = { x: number; y: number }; +type DraggingState = { + chainId: ChainId; + position: Point; +}; + +function SectionSeparator({ + y, editing, - selected, - unselect, - select, + expanded, + networks, }: { - editing: boolean; - selected: ChainId[]; - unselect: (chainId: ChainId) => void; - select: (chainId: ChainId) => void; + y: SharedValue; + editing: SharedValue; + expanded: SharedValue; + networks: SharedValue>; }) { - const pinnedNetworks = useSharedValue(useNetworkSwitcherStore(s => s.pinnedNetworks)); - const unpinnedNetworks = useDerivedValue(() => - SUPPORTED_CHAIN_IDS_ALPHABETICAL.filter(chainId => !pinnedNetworks.value.includes(chainId)) - ); + const pressed = useSharedValue(false); + const tapGesture = Gesture.Tap() + .onBegin(e => { + if (editing.value) e.state = State.FAILED; + else pressed.value = true; + }) + .onEnd(() => { + pressed.value = false; + expanded.value = !expanded.value; + }); - const dragging = useSharedValue(null); - const draggingTransform = useSharedValue(null); - const dropping = useSharedValue(null); - const droppingTransform = useSharedValue(null); + const separatorStyles = useAnimatedStyle(() => ({ + transform: [{ translateY: y.value }, { scale: withTiming(pressed.value ? 0.95 : 1) }], + })); - const unpinnedGridY = useSharedValue(NaN); + const text = useDerivedValue(() => { + if (editing.value) return translations.drag_to_rearrange; + return expanded.value ? translations.show_less : translations.show_more; + }); - // Sync back to store - useAnimatedReaction( - () => pinnedNetworks.value, - (pinnedNetworks, prev) => { - if (!prev) return; // no need to react on initial value - runOnJS(setNetworkSwitcherState)({ pinnedNetworks }); - } - ); + const unpinnedNetworksLength = useDerivedValue(() => networks.value[Section.unpinned].length.toString()); + const showMoreAmountStyle = useAnimatedStyle(() => ({ opacity: expanded.value || editing.value ? 0 : 1 })); + const showMoreOrLessIcon = useDerivedValue(() => (expanded.value ? '􀆇' : '􀆈')); + const showMoreOrLessIconStyle = useAnimatedStyle(() => ({ opacity: editing.value ? 0 : 1 })); - // Force rerender when dragging or dropping changes - const [, rerender] = useReducer(x => x + 1, 0); - useAnimatedReaction( - () => [dropping.value, dragging.value], - () => { - console.log('force rerendering'); - runOnJS(rerender)(); - } + return ( + + + + + {unpinnedNetworksLength} + + + + {text} + + + + {showMoreOrLessIcon} + + + + ); +} - const positionIndex = (x: number, y: number) => { - 'worklet'; - const yOffset = y > unpinnedGridY.value ? unpinnedGridY.value + GAP : 0; - const column = x > ITEM_WIDTH + HALF_GAP ? 1 : 0; - const row = Math.floor((y - yOffset) / (ITEM_HEIGHT + HALF_GAP)); - const index = row * 2 + column; - return index < 0 ? 0 : index; // row can be negative if the dragged item is above the first row - }; - - const indexPosition = (index: number, isUnpinned: boolean) => { - 'worklet'; - const column = index % 2; - const row = Math.floor(index / 2); - const position = { x: column * (ITEM_WIDTH + GAP), y: row * (ITEM_HEIGHT + GAP) }; - const yOffset = isUnpinned ? unpinnedGridY.value + GAP : 0; - return { x: position.x, y: position.y + yOffset }; - }; +function NetworksGrid({ editing, selected }: { editing: SharedValue; selected: SharedValue }) { + const initialPinned = useNetworkSwitcherStore.getState().pinnedNetworks; + const initialUnpinned = SUPPORTED_CHAIN_IDS_ALPHABETICAL.filter(chainId => !initialPinned.includes(chainId)); + const networks = useSharedValue({ [Section.pinned]: initialPinned, [Section.unpinned]: initialUnpinned }); - const dragNetwork = Gesture.Pan() - .maxPointers(1) - .onBegin(e => { - 'worklet'; - - const isTargetUnpinned = e.y > unpinnedGridY.value; - if (!isTargetUnpinned && pinnedNetworks.value.length === 1) return; + useEffect(() => { + // persists pinned networks when closing the sheet + // should be the only time this component is unmounted + return () => { + setNetworkSwitcherPinned(networks.value[Section.pinned]); + }; + }, [networks]); - const index = positionIndex(e.x, e.y); - const position = indexPosition(index, isTargetUnpinned); + const expanded = useSharedValue(false); + const isUnpinnedHidden = useDerivedValue(() => !expanded.value && !editing.value); - draggingTransform.value = { x: position.x, y: position.y, scale: 1 }; // initial position is the grid slot + const dragging = useSharedValue(null); - const targetArray = isTargetUnpinned ? unpinnedNetworks.value : pinnedNetworks.value; - const chainId = targetArray[index]; - dragging.value = chainId; + const pinnedHeight = useDerivedValue(() => Math.ceil(networks.value[Section.pinned].length / 2) * (ITEM_HEIGHT + GAP) - GAP); + const sectionsOffsets = useDerivedValue(() => ({ + [Section.pinned]: { y: 0 }, + [Section.unpinned]: { y: pinnedHeight.value + SEPARATOR_HEIGHT }, + })); + const containerStyle = useAnimatedStyle(() => { + const unpinnedHeight = isUnpinnedHidden.value + ? 0 + : Math.ceil(networks.value[Section.unpinned].length / 2) * (ITEM_HEIGHT + GAP) - GAP + 32; + const height = pinnedHeight.value + SEPARATOR_HEIGHT + unpinnedHeight; + return { height: withTiming(height) }; + }); - draggingTransform.value = withSpring({ - x: e.x - ITEM_WIDTH * 0.5, - y: e.y - ITEM_HEIGHT * 0.5, - scale: 1.05, - }); // animate into the center of the pointer + const dragNetwork = Gesture.Pan() + .maxPointers(1) + .onTouchesDown((e, s) => { + if (!editing.value) { + s.fail(); + return; + } + const touch = e.allTouches[0]; + const section = touch.y > sectionsOffsets.value[Section.unpinned].y ? Section.unpinned : Section.pinned; + const sectionOffset = sectionsOffsets.value[section]; + const index = indexFromPosition(touch.x, touch.y, sectionOffset); + const chainId = networks.value[section][index]; + const position = positionFromIndex(index, sectionOffset); + dragging.value = { chainId, position }; }) .onChange(e => { 'worklet'; - const chainId = dragging.value; - if (!chainId) return; - - draggingTransform.modify(item => { - if (!item) return item; - item.x = e.x - ITEM_WIDTH * 0.5; - item.y = e.y - ITEM_HEIGHT * 0.5; - return item; + if (!dragging.value) return; + const chainId = dragging.value.chainId; + + const section = e.y > sectionsOffsets.value[Section.unpinned].y ? Section.unpinned : Section.pinned; + const sectionArray = networks.value[section]; + + const currentIndex = sectionArray.indexOf(chainId); + const newIndex = Math.min(indexFromPosition(e.x, e.y, sectionsOffsets.value[section]), sectionArray.length - 1); + + networks.modify(networks => { + if (currentIndex === -1) { + // Pin/Unpin + if (section === Section.unpinned) networks[Section.pinned].splice(currentIndex, 1); + else networks[Section.pinned].splice(newIndex, 0, chainId); + networks[Section.unpinned] = SUPPORTED_CHAIN_IDS_ALPHABETICAL.filter(chainId => !networks[Section.pinned].includes(chainId)); + } else if (section === Section.pinned && newIndex !== currentIndex) { + // Reorder + networks[Section.pinned].splice(currentIndex, 1); + networks[Section.pinned].splice(newIndex, 0, chainId); + } + return networks; + }); + dragging.modify(dragging => { + if (!dragging) return dragging; + dragging.position.x += e.changeX; + dragging.position.y += e.changeY; + return dragging; }); - - const isDraggingOverUnpinned = e.y > unpinnedGridY.value; - - const currentIndexAtPinned = pinnedNetworks.value.indexOf(chainId); - const isPinned = currentIndexAtPinned !== -1; - - // We don't reorder unpinned networks - if (isDraggingOverUnpinned && !isPinned) return; - - // Unpin - if (isDraggingOverUnpinned && isPinned) { - pinnedNetworks.modify(networks => { - networks.splice(currentIndexAtPinned, 1); - return networks; - }); - return; - } - - // Pin - if (!isDraggingOverUnpinned && !isPinned) { - pinnedNetworks.modify(networks => { - networks.push(chainId); - return networks; - }); - return; - } - - // Reorder - const newIndex = Math.min(positionIndex(e.x, e.y), pinnedNetworks.value.length - 1); - if (newIndex !== currentIndexAtPinned) { - pinnedNetworks.modify(networks => { - networks.splice(currentIndexAtPinned, 1); - networks.splice(newIndex, 0, chainId); - return networks; - }); - } }) - .onFinalize(e => { + .onFinalize(() => { 'worklet'; - const chainId = dragging.value; - if (!chainId) return; - - const isDroppingInUnpinned = e.y > unpinnedGridY.value; - - const index = isDroppingInUnpinned - ? unpinnedNetworks.value.indexOf(chainId) - : Math.min(positionIndex(e.x, e.y), pinnedNetworks.value.length - 1); - - const { x, y } = indexPosition(index, isDroppingInUnpinned); - - droppingTransform.value = draggingTransform.value; - droppingTransform.value = withSpring({ y, x, scale: 1 }, { mass: 0.6 }, completed => { - if (completed) dropping.value = null; - else droppingTransform.value = { y, x, scale: 1 }; - }); - dropping.value = dragging.value; dragging.value = null; - }) - .enabled(editing); + }); - useAnimatedReaction( - () => unpinnedGridY.value, - (newY, prevY) => { - // the layout can recalculate after the drop started - if (!prevY || !droppingTransform.value) return; - const { x, y } = droppingTransform.value; - droppingTransform.value = withSpring({ x, y: y + (prevY - newY), scale: 1 }, { mass: 0.6 }, completed => { - if (completed) dropping.value = null; - }); + const tapNetwork = Gesture.Tap().onTouchesDown((e, s) => { + 'worklet'; + const touch = e.allTouches[0]; + if (editing.value) { + s.fail(); + return; } - ); - - const [isExpanded, setExpanded] = useState(false); - - const toggleSelected = (chainId: ChainId) => { - if (selected.includes(chainId)) unselect(chainId); - else select(chainId); - }; - - const pinnedGrid = editing - ? pinnedNetworks.value - : [...pinnedNetworks.value, ...unpinnedNetworks.value.filter(chainId => selected.includes(chainId))]; + const section = touch.y > sectionsOffsets.value[Section.unpinned].y ? Section.unpinned : Section.pinned; + const index = indexFromPosition(touch.x, touch.y, sectionsOffsets.value[section]); + const chainId = networks.value[section][index]; + + selected.modify(selected => { + if (selected === 'all') { + // @ts-expect-error I think something is wrong with reanimated types, not infering right here + selected = [chainId]; + return selected; + } + const selectedIndex = selected.indexOf(chainId); + if (selectedIndex !== -1) selected.splice(selectedIndex, 1); + else selected.push(chainId); + return selected; + }); + }); - const unpinnedGrid = editing ? unpinnedNetworks.value : unpinnedNetworks.value.filter(chainId => !selected.includes(chainId)); + const gridGesture = Gesture.Exclusive(dragNetwork, tapNetwork); return ( - - - - - - - {pinnedGrid.map(chainId => - chainId === dragging.value || chainId === dropping.value ? ( - - ) : ( - toggleSelected(chainId)} - /> - ) - )} - + + + {initialPinned.map(chainId => ( + + + + ))} - {editing ? ( - - - {i18n.t(t.drag_to_rearrange)} - - - ) : ( - - )} + - {(editing || isExpanded) && ( - (unpinnedGridY.value = e.nativeEvent.layout.y)} - layout={SequencedTransition.duration(500)} - entering={FadeIn.duration(250).delay(125)} - style={{ flexDirection: 'row', gap: 12, flexWrap: 'wrap', paddingVertical: 12 }} + {/* {initialUnpinned.length === 0 && ( + + + Drag here to unpin networks + + + )} */} + {initialUnpinned.map(chainId => ( + - {unpinnedGrid.length === 0 && ( - - - Drag here to unpin networks - - - )} - {unpinnedGrid.map(chainId => - chainId === dragging.value || chainId === dropping.value ? ( - - ) : ( - toggleSelected(chainId)} - /> - ) - )} - - )} - + + + ))} + ); } -const useCustomizeNetworksBanner = createRainbowStore<{ - dismissedAt: number; // timestamp -}>(() => ({ dismissedAt: 0 }), { - storageKey: 'CustomizeNetworksBanner', - version: 0, -}); -const twoWeeks = 1000 * 60 * 60 * 24 * 7 * 2; -const dismissCustomizeNetworksBanner = () => { - const { dismissedAt } = useCustomizeNetworksBanner.getState(); - if (Date.now() - dismissedAt < twoWeeks) return; - useCustomizeNetworksBanner.setState({ dismissedAt: Date.now() }); -}; - -function CustomizeNetworksBanner() { - const blue = '#268FFF'; - - const dismissedAt = useCustomizeNetworksBanner(s => s.dismissedAt); - const isOpen = Date.now() - dismissedAt > twoWeeks; - - if (!isOpen) return null; - +function SheetBackdrop({ onPress }: { onPress: VoidFunction }) { + const tapGesture = Gesture.Tap().onEnd(onPress); return ( - + - - - - } - > - - - - - 􀍱 - - - - - {i18n.t(t.customize_networks_banner.title)} - - - {/* - is there a way to render a diferent component mid sentence? - like i18n.t(t.customize_networks_banner.description, { Edit: }) - */} - Tap the{' '} - - Edit - {' '} - button below to set up - - - - - 􀆄 - - - - - - - + entering={FadeIn.delay(100)} + exiting={FadeOut} + style={{ flex: 1, position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: '#000000B2' }} + /> + ); } -export function NetworkSelector({ onClose, onSelect, multiple }: { onClose: VoidFunction; onSelect: VoidFunction; multiple?: boolean }) { +function Sheet({ children, header, onClose }: PropsWithChildren<{ header: ReactElement; onClose: VoidFunction }>) { const { isDarkMode } = useTheme(); const surfacePrimary = useBackgroundColor('surfacePrimary'); const backgroundColor = isDarkMode ? '#191A1C' : surfacePrimary; const separatorSecondary = useForegroundColor('separatorSecondary'); - const separatorTertiary = useForegroundColor('separatorTertiary'); - const fill = useForegroundColor('fill'); const translationY = useSharedValue(0); @@ -731,92 +715,60 @@ export function NetworkSelector({ onClose, onSelect, multiple }: { onClose: Void translationY.value = event.translationY; }) .onFinalize(() => { - if (translationY.value > 120) runOnJS(onClose)(); + if (translationY.value > 120) onClose(); else translationY.value = withSpring(0); }); - const [isEditing, setEditing] = useState(false); - - const animatedStyles = useAnimatedStyle(() => ({ - transform: [{ translateY: translationY.value }], - })); - - const [selected, setSelected] = useState([]); - const unselect = (chainId: ChainId) => - setSelected(s => { - if (s === 'all') return []; - return s.filter(id => id !== chainId); - }); - const select = (chainId: ChainId) => - setSelected(s => { - if (s === 'all') return [chainId]; - return multiple ? [...s, chainId] : [chainId]; - }); + const sheetStyle = useAnimatedStyle(() => ({ transform: [{ translateY: translationY.value }] })); return ( - - + + - - - - - - - - - - {isEditing ? i18n.t(t.edit) : i18n.t(t.networks)} - - - { - dismissCustomizeNetworksBanner(); - setEditing(s => !s); - }} - /> - - - - - {multiple && !isEditing && ( - <> - setSelected('all')} /> - - - )} - - + {header} + {children} ); } + +export function NetworkSelector({ + onClose, + multiple, +}: { + onClose: (selected: ChainId[] | 'all') => void; + onSelect: VoidFunction; + multiple?: boolean; +}) { + const editing = useSharedValue(false); + const selected = useSharedValue([]); + + const close = () => { + 'worklet'; + runOnJS(onClose)(selected.value); + }; + + return ( + } onClose={close}> + + + {multiple && } + + + + ); +} diff --git a/src/screens/discover/components/TrendingTokens.tsx b/src/screens/discover/components/TrendingTokens.tsx index d9f5d38bb52..033e603a287 100644 --- a/src/screens/discover/components/TrendingTokens.tsx +++ b/src/screens/discover/components/TrendingTokens.tsx @@ -306,7 +306,16 @@ function NetworkFilter() { return ( <> setOpen(true)} /> - {isOpen && setOpen(false)} onSelect={() => null} multiple />} + {isOpen && ( + { + console.log(selected); + setOpen(false); + }} + onSelect={() => null} + multiple + /> + )} ); } diff --git a/src/state/internal/createRainbowStore.ts b/src/state/internal/createRainbowStore.ts index df1a4d11df0..0f7a164109f 100644 --- a/src/state/internal/createRainbowStore.ts +++ b/src/state/internal/createRainbowStore.ts @@ -43,6 +43,12 @@ interface RainbowPersistConfig { * This function will be called when persisted state versions mismatch with the one specified here. */ migrate?: (persistedState: unknown, version: number) => S | Promise; + /** + * A function returning another (optional) function. + * The main function will be called before the state rehydration. + * The returned function will be called after the state rehydration or when an error occurred. + */ + onRehydrateStorage?: PersistOptions>['onRehydrateStorage']; } /** @@ -157,6 +163,7 @@ export function createRainbowStore( storage: persistStorage, version, migrate: persistConfig.migrate, + onRehydrateStorage: persistConfig.onRehydrateStorage, }) ) ); From 2366cee2a1e2ddb8538cc849c7b007525986358f Mon Sep 17 00:00:00 2001 From: gregs Date: Mon, 2 Dec 2024 08:36:59 -0300 Subject: [PATCH 08/95] ops --- src/analytics/userProperties.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/analytics/userProperties.ts b/src/analytics/userProperties.ts index 1ceb4c9325f..f5c59be4242 100644 --- a/src/analytics/userProperties.ts +++ b/src/analytics/userProperties.ts @@ -1,3 +1,4 @@ +import { ChainId } from '@/chains/types'; import { NativeCurrencyKey } from '@/entities'; import { Language } from '@/languages'; @@ -16,6 +17,9 @@ export interface UserProperties { hiddenCOins?: string[]; appIcon?: string; + // most used networks at the time the user first opens the network switcher + mostUsedNetworks?: ChainId[]; + // assets NFTs?: number; poaps?: number; From 394f6fc59bf682a28430328808e98b57bda7792f Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Fri, 6 Dec 2024 12:18:47 -0500 Subject: [PATCH 09/95] Wire up trending tokens UI (#6292) * implement gql query to get trending tokens for selected network and display them * fix dragging issue with no chainId and tapping empty space resetting network to all networks * add mock handler for navigating to swaps flow * fix lint * shuffle files and break out network switcher to be composable * refactor SwapCoinIcon to use a size prop instead of small, large, xlarge, etc. * add view token analytics event * add time tracking to discover screen * add tracking for if user has swapped a trending token * decouple network selector from trending tokens and add rest of analytics events * fix customize network banner not being dismissable * revert white color token change --- .../Swap/components/AnimatedSwapCoinIcon.tsx | 90 +-- .../screens/Swap/components/CoinRow.tsx | 2 +- .../screens/Swap/components/SwapCoinIcon.tsx | 103 +-- .../Swap/components/SwapInputAsset.tsx | 2 +- .../Swap/components/SwapOutputAsset.tsx | 2 +- .../screens/Swap/components/SwapSlider.tsx | 2 +- .../screens/Swap/providers/swap-provider.tsx | 12 +- src/analytics/event.ts | 43 ++ .../Discover}/DiscoverFeaturedResultsCard.tsx | 0 .../Discover}/DiscoverHome.tsx | 2 +- .../Discover}/DiscoverScreenContent.tsx | 8 +- .../Discover}/DiscoverScreenContext.tsx | 3 + .../Discover}/DiscoverSearch.tsx | 2 +- .../Discover}/DiscoverSearchContainer.tsx | 4 +- .../Discover}/DiscoverSearchInput.tsx | 2 +- src/components/Discover/TrendingTokens.tsx | 593 ++++++++++++++++++ .../Discover/useTrackDiscoverScreenTime.ts | 21 + .../components/NetworkSwitcher.tsx | 231 ++++--- .../asset/ChartExpandedState.js | 3 +- src/helpers/strings.ts | 56 ++ src/hooks/reanimated/useSyncSharedValue.ts | 8 +- src/languages/en_US.json | 1 + src/navigation/SwipeNavigator.tsx | 4 +- src/performance/tracking/index.ts | 2 +- .../tracking/types/PerformanceMetrics.ts | 1 + .../trendingTokens/trendingTokens.ts | 1 + src/screens/{discover => }/DiscoverScreen.tsx | 16 +- .../discover/components/TrendingTokens.tsx | 417 ------------ src/state/networkSwitcher/networkSwitcher.ts | 48 ++ src/state/swaps/swapsStore.ts | 6 +- src/state/trendingTokens/trendingTokens.ts | 46 ++ src/styles/colors.ts | 2 +- 32 files changed, 1021 insertions(+), 712 deletions(-) rename src/{screens/discover/components => components/Discover}/DiscoverFeaturedResultsCard.tsx (100%) rename src/{screens/discover/components => components/Discover}/DiscoverHome.tsx (98%) rename src/{screens/discover/components => components/Discover}/DiscoverScreenContent.tsx (76%) rename src/{screens/discover => components/Discover}/DiscoverScreenContext.tsx (96%) rename src/{screens/discover/components => components/Discover}/DiscoverSearch.tsx (99%) rename src/{screens/discover/components => components/Discover}/DiscoverSearchContainer.tsx (92%) rename src/{screens/discover/components => components/Discover}/DiscoverSearchInput.tsx (98%) create mode 100644 src/components/Discover/TrendingTokens.tsx create mode 100644 src/components/Discover/useTrackDiscoverScreenTime.ts rename src/{screens/discover => }/components/NetworkSwitcher.tsx (82%) rename src/screens/{discover => }/DiscoverScreen.tsx (95%) delete mode 100644 src/screens/discover/components/TrendingTokens.tsx create mode 100644 src/state/networkSwitcher/networkSwitcher.ts create mode 100644 src/state/trendingTokens/trendingTokens.ts diff --git a/src/__swaps__/screens/Swap/components/AnimatedSwapCoinIcon.tsx b/src/__swaps__/screens/Swap/components/AnimatedSwapCoinIcon.tsx index 2c8ba92b2ea..7d8ec66e8e1 100644 --- a/src/__swaps__/screens/Swap/components/AnimatedSwapCoinIcon.tsx +++ b/src/__swaps__/screens/Swap/components/AnimatedSwapCoinIcon.tsx @@ -14,37 +14,19 @@ import { IS_ANDROID, IS_IOS } from '@/env'; import { PIXEL_RATIO } from '@/utils/deviceUtils'; import { useSwapContext } from '../providers/swap-provider'; -const fallbackIconStyle = { - ...borders.buildCircleAsObject(32), - position: 'absolute' as ViewStyle['position'], -}; - -const largeFallbackIconStyle = { - ...borders.buildCircleAsObject(36), - position: 'absolute' as ViewStyle['position'], -}; - -const smallFallbackIconStyle = { - ...borders.buildCircleAsObject(16), - position: 'absolute' as ViewStyle['position'], -}; - export const AnimatedSwapCoinIcon = memo(function AnimatedSwapCoinIcon({ assetType, - large = true, - small, + size = 32, showBadge = true, }: { assetType: 'input' | 'output'; - large?: boolean; - small?: boolean; + size?: number; showBadge?: boolean; }) { const { isDarkMode, colors } = useTheme(); const { internalSelectedInputAsset, internalSelectedOutputAsset } = useSwapContext(); const asset = assetType === 'input' ? internalSelectedInputAsset : internalSelectedOutputAsset; - const size = small ? 16 : large ? 36 : 32; const didErrorForUniqueId = useSharedValue(undefined); @@ -91,15 +73,8 @@ export const AnimatedSwapCoinIcon = memo(function AnimatedSwapCoinIcon({ })); return ( - - + + {/* ⚠️ TODO: This works but we should figure out how to type this correctly to avoid this error */} {/* @ts-expect-error: Doesn't pick up that it's getting a source prop via animatedProps */} @@ -122,29 +97,14 @@ export const AnimatedSwapCoinIcon = memo(function AnimatedSwapCoinIcon({ /> - - + + @@ -153,28 +113,28 @@ export const AnimatedSwapCoinIcon = memo(function AnimatedSwapCoinIcon({ ); }); +const fallbackIconStyle = (size: number) => ({ + ...borders.buildCircleAsObject(size), + position: 'absolute' as ViewStyle['position'], +}); + +const coinIconFallbackStyle = (size: number) => ({ + borderRadius: size / 2, + height: size, + width: size, + overflow: 'visible' as const, +}); + +const containerStyle = (size: number) => ({ + elevation: 6, + height: size, + overflow: 'visible' as const, +}); + const sx = StyleSheet.create({ coinIcon: { overflow: 'hidden', }, - coinIconFallback: { - borderRadius: 16, - height: 32, - overflow: 'visible', - width: 32, - }, - coinIconFallbackLarge: { - borderRadius: 18, - height: 36, - overflow: 'visible', - width: 36, - }, - coinIconFallbackSmall: { - borderRadius: 8, - height: 16, - overflow: 'visible', - width: 16, - }, container: { elevation: 6, height: 32, diff --git a/src/__swaps__/screens/Swap/components/CoinRow.tsx b/src/__swaps__/screens/Swap/components/CoinRow.tsx index 6a1be01ed47..9cf1bbeb142 100644 --- a/src/__swaps__/screens/Swap/components/CoinRow.tsx +++ b/src/__swaps__/screens/Swap/components/CoinRow.tsx @@ -131,7 +131,7 @@ export function CoinRow({ isFavorite, onPress, output, uniqueId, testID, ...asse iconUrl={icon_url} address={address} mainnetAddress={mainnetAddress} - large + size={36} chainId={chainId} symbol={symbol || ''} color={colors?.primary} diff --git a/src/__swaps__/screens/Swap/components/SwapCoinIcon.tsx b/src/__swaps__/screens/Swap/components/SwapCoinIcon.tsx index d65c2af7408..9e56098952c 100644 --- a/src/__swaps__/screens/Swap/components/SwapCoinIcon.tsx +++ b/src/__swaps__/screens/Swap/components/SwapCoinIcon.tsx @@ -24,20 +24,10 @@ const fallbackTextStyles = { textAlign: 'center', }; -const fallbackIconStyle = { - ...borders.buildCircleAsObject(32), +const fallbackIconStyle = (size: number) => ({ + ...borders.buildCircleAsObject(size), position: 'absolute', -}; - -const largeFallbackIconStyle = { - ...borders.buildCircleAsObject(36), - position: 'absolute', -}; - -const smallFallbackIconStyle = { - ...borders.buildCircleAsObject(16), - position: 'absolute', -}; +}); /** * If mainnet asset is available, get the token under /ethereum/ (token) url. @@ -63,22 +53,22 @@ export const SwapCoinIcon = React.memo(function FeedCoinIcon({ iconUrl, disableShadow = true, forceDarkMode, - large, mainnetAddress, chainId, - small, symbol, + size = 32, + chainSize, }: { address: string; color?: string; iconUrl?: string; disableShadow?: boolean; forceDarkMode?: boolean; - large?: boolean; mainnetAddress?: string; chainId: ChainId; - small?: boolean; symbol: string; + size?: number; + chainSize?: number; }) { const theme = useTheme(); @@ -92,52 +82,52 @@ export const SwapCoinIcon = React.memo(function FeedCoinIcon({ const eth = isETH(resolvedAddress); return ( - + {eth ? ( - + ) : ( - + {() => ( )} )} - {chainId && chainId !== ChainId.mainnet && !small && ( + {chainId && chainId !== ChainId.mainnet && size > 16 && ( - + )} ); }); +const styles = { + container: (size: number) => ({ + elevation: 6, + height: size, + overflow: 'visible' as const, + }), + coinIcon: (size: number) => ({ + borderRadius: size / 2, + height: size, + width: size, + overflow: 'visible' as const, + }), +}; + const sx = StyleSheet.create({ badge: { bottom: -0, @@ -151,39 +141,6 @@ const sx = StyleSheet.create({ shadowRadius: 6, shadowOpacity: 0.2, }, - coinIconFallback: { - borderRadius: 16, - height: 32, - overflow: 'visible', - width: 32, - }, - coinIconFallbackLarge: { - borderRadius: 18, - height: 36, - overflow: 'visible', - width: 36, - }, - coinIconFallbackSmall: { - borderRadius: 8, - height: 16, - overflow: 'visible', - width: 16, - }, - container: { - elevation: 6, - height: 32, - overflow: 'visible', - }, - containerLarge: { - elevation: 6, - height: 36, - overflow: 'visible', - }, - containerSmall: { - elevation: 6, - height: 16, - overflow: 'visible', - }, reactCoinIconContainer: { alignItems: 'center', justifyContent: 'center', diff --git a/src/__swaps__/screens/Swap/components/SwapInputAsset.tsx b/src/__swaps__/screens/Swap/components/SwapInputAsset.tsx index af94a152e8a..23734d39ce8 100644 --- a/src/__swaps__/screens/Swap/components/SwapInputAsset.tsx +++ b/src/__swaps__/screens/Swap/components/SwapInputAsset.tsx @@ -96,7 +96,7 @@ function SwapInputAmount() { function SwapInputIcon() { return ( - + ); } diff --git a/src/__swaps__/screens/Swap/components/SwapOutputAsset.tsx b/src/__swaps__/screens/Swap/components/SwapOutputAsset.tsx index 1f17c801be5..71aada3d1d1 100644 --- a/src/__swaps__/screens/Swap/components/SwapOutputAsset.tsx +++ b/src/__swaps__/screens/Swap/components/SwapOutputAsset.tsx @@ -108,7 +108,7 @@ function SwapOutputAmount({ handleTapWhileDisabled }: { handleTapWhileDisabled: function SwapOutputIcon() { return ( - + ); } diff --git a/src/__swaps__/screens/Swap/components/SwapSlider.tsx b/src/__swaps__/screens/Swap/components/SwapSlider.tsx index 572f9eb6e80..90ac3ac724c 100644 --- a/src/__swaps__/screens/Swap/components/SwapSlider.tsx +++ b/src/__swaps__/screens/Swap/components/SwapSlider.tsx @@ -412,7 +412,7 @@ export const SwapSlider = ({ - + { const isBridge = swapsStore.getState().inputAsset?.mainnetAddress === swapsStore.getState().outputAsset?.mainnetAddress; const isDegenModeEnabled = swapsStore.getState().degenMode; const isSwappingToPopularAsset = swapsStore.getState().outputAsset?.sectionId === 'popular'; + const lastNavigatedTrendingToken = swapsStore.getState().lastNavigatedTrendingToken; + const isSwappingToTrendingAsset = + lastNavigatedTrendingToken === parameters.assetToBuy.uniqueId || lastNavigatedTrendingToken === parameters.assetToSell.uniqueId; const selectedGas = getSelectedGas(parameters.chainId); if (!selectedGas) { @@ -331,6 +332,7 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { tradeAmountUSD: parameters.quote.tradeAmountUSD, degenMode: isDegenModeEnabled, isSwappingToPopularAsset, + isSwappingToTrendingAsset, errorMessage, isHardwareWallet, }); @@ -403,6 +405,7 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { tradeAmountUSD: parameters.quote.tradeAmountUSD, degenMode: isDegenModeEnabled, isSwappingToPopularAsset, + isSwappingToTrendingAsset, isHardwareWallet, }); } catch (error) { @@ -417,6 +420,11 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { }, }); } + + // reset the last navigated trending token after a swap has taken place + swapsStore.setState({ + lastNavigatedTrendingToken: undefined, + }); }; const executeSwap = performanceTracking.getState().executeFn({ diff --git a/src/analytics/event.ts b/src/analytics/event.ts index 7d7edf3b143..84d91562be5 100644 --- a/src/analytics/event.ts +++ b/src/analytics/event.ts @@ -9,6 +9,7 @@ import { RequestSource } from '@/utils/requestNavigationHandlers'; import { CrosschainQuote, Quote, QuoteError } from '@rainbow-me/swaps'; import { AnyPerformanceLog, Screen } from '../state/performance/operations'; import { FavoritedSite } from '@/state/browser/favoriteDappsStore'; +import { TrendingTokens } from '@/resources/trendingTokens/trendingTokens'; /** * All events, used by `analytics.track()` @@ -167,6 +168,14 @@ export const event = { // token details tokenDetailsErc20: 'token_details.erc20', tokenDetailsNFT: 'token_details.nft', + + // trending tokens + viewTrendingToken: 'trending_tokens.view_trending_token', + viewRankedCategory: 'trending_tokens.view_ranked_category', + changeNetworkFilter: 'trending_tokens.change_network_filter', + changeTimeframeFilter: 'trending_tokens.change_timeframe_filter', + changeSortFilter: 'trending_tokens.change_sort_filter', + hasLinkedFarcaster: 'trending_tokens.has_linked_farcaster', } as const; type SwapEventParameters = { @@ -187,6 +196,7 @@ type SwapEventParameters = { tradeAmountUSD: number; degenMode: boolean; isSwappingToPopularAsset: boolean; + isSwappingToTrendingAsset: boolean; isHardwareWallet: boolean; }; @@ -694,4 +704,37 @@ export type EventProperties = { eventSentAfterMs: number; available_data: { description: boolean; image_url: boolean; floorPrice: boolean }; }; + + [event.viewTrendingToken]: { + address: TrendingTokens['trendingTokens']['data'][number]['address']; + chainId: TrendingTokens['trendingTokens']['data'][number]['chainId']; + symbol: TrendingTokens['trendingTokens']['data'][number]['symbol']; + name: TrendingTokens['trendingTokens']['data'][number]['name']; + highlightedFriends: number; + }; + + [event.viewRankedCategory]: { + category: string; + chainId: ChainId | undefined; + isLimited: boolean; + isEmpty: boolean; + }; + + [event.changeNetworkFilter]: { + chainId: ChainId | undefined; + }; + + [event.changeTimeframeFilter]: { + timeframe: string; + }; + + [event.changeSortFilter]: { + sort: string | undefined; + }; + + [event.hasLinkedFarcaster]: { + hasFarcaster: boolean; + personalizedTrending: boolean; + walletHash: string; + }; }; diff --git a/src/screens/discover/components/DiscoverFeaturedResultsCard.tsx b/src/components/Discover/DiscoverFeaturedResultsCard.tsx similarity index 100% rename from src/screens/discover/components/DiscoverFeaturedResultsCard.tsx rename to src/components/Discover/DiscoverFeaturedResultsCard.tsx diff --git a/src/screens/discover/components/DiscoverHome.tsx b/src/components/Discover/DiscoverHome.tsx similarity index 98% rename from src/screens/discover/components/DiscoverHome.tsx rename to src/components/Discover/DiscoverHome.tsx index fc58f141d19..d06f149fb7c 100644 --- a/src/screens/discover/components/DiscoverHome.tsx +++ b/src/components/Discover/DiscoverHome.tsx @@ -29,7 +29,7 @@ import { FeaturedResultStack } from '@/components/FeaturedResult/FeaturedResultS import Routes from '@/navigation/routesNames'; import { useNavigation } from '@/navigation'; import { DiscoverFeaturedResultsCard } from './DiscoverFeaturedResultsCard'; -import { TrendingTokens } from './TrendingTokens'; +import { TrendingTokens } from '@/components/Discover/TrendingTokens'; export const HORIZONTAL_PADDING = 20; diff --git a/src/screens/discover/components/DiscoverScreenContent.tsx b/src/components/Discover/DiscoverScreenContent.tsx similarity index 76% rename from src/screens/discover/components/DiscoverScreenContent.tsx rename to src/components/Discover/DiscoverScreenContent.tsx index 1e3a7650013..99271271ded 100644 --- a/src/screens/discover/components/DiscoverScreenContent.tsx +++ b/src/components/Discover/DiscoverScreenContent.tsx @@ -1,12 +1,12 @@ import React from 'react'; import { View } from 'react-native'; import { FlexItem, Page } from '@/components/layout'; -import DiscoverHome from './DiscoverHome'; -import DiscoverSearch from './DiscoverSearch'; -import DiscoverSearchContainer from './DiscoverSearchContainer'; +import DiscoverHome from '@/components/Discover/DiscoverHome'; +import DiscoverSearch from '@/components/Discover/DiscoverSearch'; +import DiscoverSearchContainer from '@/components/Discover/DiscoverSearchContainer'; import { Box, Inset } from '@/design-system'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { useDiscoverScreenContext } from '../DiscoverScreenContext'; +import { useDiscoverScreenContext } from '@/components/Discover/DiscoverScreenContext'; function Switcher({ children }: { children: React.ReactNode[] }) { const { isSearching } = useDiscoverScreenContext(); diff --git a/src/screens/discover/DiscoverScreenContext.tsx b/src/components/Discover/DiscoverScreenContext.tsx similarity index 96% rename from src/screens/discover/DiscoverScreenContext.tsx rename to src/components/Discover/DiscoverScreenContext.tsx index eb9b276443d..31a8e89106b 100644 --- a/src/screens/discover/DiscoverScreenContext.tsx +++ b/src/components/Discover/DiscoverScreenContext.tsx @@ -2,6 +2,7 @@ import { analytics } from '@/analytics'; import React, { createContext, Dispatch, SetStateAction, RefObject, useState, useRef, useCallback } from 'react'; import { SectionList, TextInput } from 'react-native'; import Animated from 'react-native-reanimated'; +import { useTrackDiscoverScreenTime } from './useTrackDiscoverScreenTime'; type DiscoverScreenContextType = { scrollViewRef: RefObject; @@ -80,6 +81,8 @@ const DiscoverScreenProvider = ({ children }: { children: React.ReactNode }) => setIsSearching(false); }, [searchQuery]); + useTrackDiscoverScreenTime(); + return ( { + pressed.value = true; + if (onPress) runOnJS(onPress)(); + }) + .onFinalize(() => (pressed.value = false)); + + const animatedStyles = useAnimatedStyle(() => ({ + transform: [{ scale: withTiming(pressed.value ? 0.95 : 1, { duration: 100 }) }], + })); + + const backgroundColor = useBackgroundColor('fillTertiary'); + const borderColor = useBackgroundColor('fillSecondary'); + + const iconColor = useForegroundColor('labelQuaternary'); + + return ( + + + {typeof icon === 'string' ? ( + + {icon} + + ) : ( + icon + )} + + {label} + + + 􀆏 + + + + ); +} + +function useTrendingTokensData() { + const { chainId, category, timeframe, sort } = useTrendingTokensStore(state => ({ + chainId: state.chainId, + category: state.category, + timeframe: state.timeframe, + sort: state.sort, + })); + + return useTrendingTokens({ + chainId, + // category, + // timeframe, + // sort, + }); +} + +function ReportAnalytics() { + const activeSwipeRoute = useNavigationStore(state => state.activeSwipeRoute); + const { category, chainId } = useTrendingTokensStore(state => ({ category: state.category, chainId: state.chainId })); + const { data, isLoading } = useTrendingTokensData(); + + useEffect(() => { + if (isLoading || activeSwipeRoute !== Routes.DISCOVER_SCREEN) return; + + const isEmpty = (data?.trendingTokens?.data?.length ?? 0) === 0; + const isLimited = !isEmpty && (data?.trendingTokens?.data?.length ?? 0) < 6; + + analyticsV2.track(analyticsV2.event.viewRankedCategory, { + category, + chainId, + isLimited, + isEmpty, + }); + }, [isLoading, activeSwipeRoute, data?.trendingTokens.data.length, category, chainId]); + + return null; +} + +function CategoryFilterButton({ + category, + icon, + iconWidth = 16, + iconColor, + label, +}: { + category: (typeof categories)[number]; + icon: string; + iconColor: string; + iconWidth?: number; + label: string; +}) { + const { isDarkMode } = useTheme(); + const fillTertiary = useBackgroundColor('fillTertiary'); + const fillSecondary = useBackgroundColor('fillSecondary'); + + const selected = useTrendingTokensStore(state => state.category === category); + + const borderColor = selected && isDarkMode ? globalColors.white80 : fillSecondary; + + const pressed = useSharedValue(false); + + const selectCategory = useCallback(() => { + useTrendingTokensStore.getState().setCategory(category); + }, [category]); + + const tap = Gesture.Tap() + .onBegin(() => { + pressed.value = true; + runOnJS(selectCategory)(); + }) + .onFinalize(() => (pressed.value = false)); + + const animatedStyles = useAnimatedStyle(() => ({ + transform: [{ scale: withTiming(pressed.value ? 0.95 : 1, { duration: 100 }) }], + })); + + return ( + + + + {icon} + + + {label} + + + + ); +} + +function FriendHolders() { + const backgroundColor = useBackgroundColor('surfacePrimary'); + return ( + + + + + + + + mikedemarais{' '} + + and 2 others + + + + ); +} + +function TrendingTokenLoadingRow() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} + +function TrendingTokenRow({ item }: { item: TrendingTokensType['trendingTokens']['data'][number] }) { + const separatorColor = useForegroundColor('separator'); + + const marketCap = useMemo( + () => formatNumber(item.market.market_cap?.value || 0, { useOrderSuffix: true, decimals: 1 }), + [item.market.market_cap?.value] + ); + + const isPositiveChange = useMemo( + () => item.market.price?.change_24h && item.market.price?.change_24h > 0, + [item.market.price?.change_24h] + ); + + const price = useMemo(() => `$${item.market.price?.value ?? '0'}`, [item.market.price?.value]); + + const volume = useMemo(() => formatNumber(item.market.volume_24h || 0, { useOrderSuffix: true, decimals: 1 }), [item.market.volume_24h]); + + const handleNavigateToToken = useCallback(() => { + analyticsV2.track(analyticsV2.event.viewTrendingToken, { + address: item.address, + chainId: item.chainId, + symbol: item.symbol, + name: item.name, + highlightedFriends: 0, // TODO: Once data is available from backend + }); + + swapsStore.setState({ + lastNavigatedTrendingToken: item.uniqueId, + }); + + Navigation.handleAction(Routes.EXPANDED_ASSET_SHEET, { + asset: item, + type: 'token', + }); + }, [item]); + + if (!item) return null; + + return ( + + + + + + + + + + + + {item.name} + + + {item.symbol} + + + {price} + + + + + + + VOL + + + {volume} + + + + + | + + + + + MCAP + + + {marketCap} + + + + + + + + + {isPositiveChange ? '􀄨' : '􀄩'} + + + {formatNumber(item.market.price?.change_24h || 0, { decimals: 2 })}% + + + + + 1H + + + {formatNumber(item.market.price?.change_24h || 0, { decimals: 2 })}% + + + + + + + + ); +} + +function NoResults() { + const { isDarkMode } = useTheme(); + const fillQuaternary = useBackgroundColor('fillQuaternary'); + const backgroundColor = isDarkMode ? '#191A1C' : fillQuaternary; + + return ( + + + + {i18n.t(t.no_results.title)} + + + {i18n.t(t.no_results.body)} + + + + + 􀙭 + + + + ); +} + +function NetworkFilter() { + const [isOpen, setOpen] = useState(false); + const selected = useSharedValue(undefined); + const { chainId, setChainId } = useTrendingTokensStore(state => ({ + chainId: state.chainId, + setChainId: state.setChainId, + })); + + const setSelected = useCallback( + (chainId: ChainId | undefined) => { + 'worklet'; + selected.value = chainId; + runOnJS(setChainId)(chainId); + }, + [selected, setChainId] + ); + + const label = useMemo(() => { + if (!chainId) return i18n.t(t.all); + return chainsLabel[chainId]; + }, [chainId]); + + const icon = useMemo(() => { + if (!chainId) return '􀤆'; + return ; + }, [chainId]); + + return ( + <> + setOpen(true)} /> + {isOpen && setOpen(false)} />} + + ); +} + +function TimeFilter() { + const timeframe = useTrendingTokensStore(state => state.timeframe); + + return ( + ({ + actionTitle: i18n.t(t.filters.time[time]), + actionKey: time, + })), + }} + side="bottom" + onPressMenuItem={timeframe => useTrendingTokensStore.getState().setTimeframe(timeframe)} + > + + + ); +} + +function SortFilter() { + const sort = useTrendingTokensStore(state => state.sort); + + return ( + ({ + actionTitle: i18n.t(t.filters.sort[sort]), + actionKey: sort, + })), + }} + side="bottom" + onPressMenuItem={selection => { + if (selection === sort) return useTrendingTokensStore.getState().setSort(undefined); + useTrendingTokensStore.getState().setSort(selection); + }} + > + + + ); +} + +function TrendingTokenData() { + const { data, isLoading } = useTrendingTokensData(); + if (isLoading) + return ( + + {Array.from({ length: 10 }).map((_, index) => ( + + ))} + + ); + + return ( + } + data={data?.trendingTokens.data} + renderItem={({ item }) => } + /> + ); +} + +export function TrendingTokens() { + return ( + + + + + + + + + + + + + + + + + + + ); +} diff --git a/src/components/Discover/useTrackDiscoverScreenTime.ts b/src/components/Discover/useTrackDiscoverScreenTime.ts new file mode 100644 index 00000000000..64e96a50fa1 --- /dev/null +++ b/src/components/Discover/useTrackDiscoverScreenTime.ts @@ -0,0 +1,21 @@ +import { useNavigationStore } from '@/state/navigation/navigationStore'; +import { useEffect } from 'react'; +import Routes from '@/navigation/routesNames'; +import { PerformanceTracking, currentlyTrackedMetrics } from '@/performance/tracking'; +import { PerformanceMetrics } from '@/performance/tracking/types/PerformanceMetrics'; + +export const useTrackDiscoverScreenTime = () => { + const activeSwipeRoute = useNavigationStore(state => state.activeSwipeRoute); + useEffect(() => { + const isOnDiscoverScreen = activeSwipeRoute === Routes.DISCOVER_SCREEN; + const data = currentlyTrackedMetrics.get(PerformanceMetrics.timeSpentOnDiscoverScreen); + + if (!isOnDiscoverScreen && data?.startTimestamp) { + PerformanceTracking.finishMeasuring(PerformanceMetrics.timeSpentOnDiscoverScreen); + } + + if (isOnDiscoverScreen) { + PerformanceTracking.startMeasuring(PerformanceMetrics.timeSpentOnDiscoverScreen); + } + }, [activeSwipeRoute]); +}; diff --git a/src/screens/discover/components/NetworkSwitcher.tsx b/src/components/NetworkSwitcher.tsx similarity index 82% rename from src/screens/discover/components/NetworkSwitcher.tsx rename to src/components/NetworkSwitcher.tsx index bc8853ae335..fe418511515 100644 --- a/src/screens/discover/components/NetworkSwitcher.tsx +++ b/src/components/NetworkSwitcher.tsx @@ -1,6 +1,5 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import { getChainColorWorklet } from '@/__swaps__/utils/swaps'; -import { analyticsV2 } from '@/analytics'; import { chainsLabel, SUPPORTED_CHAIN_IDS_ALPHABETICAL } from '@/chains'; import { ChainId } from '@/chains/types'; import { AbsolutePortal } from '@/components/AbsolutePortal'; @@ -11,15 +10,13 @@ import { AnimatedChainImage, ChainImage } from '@/components/coin-icon/ChainImag import { AnimatedText, DesignSystemProvider, globalColors, Separator, Text, useBackgroundColor, useColorMode } from '@/design-system'; import { useForegroundColor } from '@/design-system/color/useForegroundColor'; import * as i18n from '@/languages'; -import { createRainbowStore } from '@/state/internal/createRainbowStore'; -import { nonceStore } from '@/state/nonces'; import { useTheme } from '@/theme'; import { DEVICE_WIDTH } from '@/utils/deviceUtils'; import MaskedView from '@react-native-masked-view/masked-view'; import chroma from 'chroma-js'; import { PropsWithChildren, ReactElement, useEffect } from 'react'; import React, { Pressable, View } from 'react-native'; -import { Gesture, GestureDetector, State } from 'react-native-gesture-handler'; +import { Gesture, GestureDetector, State, TapGesture } from 'react-native-gesture-handler'; import LinearGradient from 'react-native-linear-gradient'; import Animated, { FadeIn, @@ -40,44 +37,16 @@ import Animated, { withTiming, } from 'react-native-reanimated'; import Svg, { Path } from 'react-native-svg'; +import { + customizeNetworksBannerStore, + dismissCustomizeNetworksBanner, + networkSwitcherStore, + shouldShowCustomizeNetworksBanner, + showCustomizeNetworksBanner, +} from '@/state/networkSwitcher/networkSwitcher'; const t = i18n.l.network_switcher; -function getMostUsedChains() { - const noncesByAddress = nonceStore.getState().nonces; - - const summedNoncesByChainId: Record = {}; - for (const addressNonces of Object.values(noncesByAddress)) { - for (const [chainId, { currentNonce }] of Object.entries(addressNonces)) { - summedNoncesByChainId[chainId] ??= 0; - summedNoncesByChainId[chainId] += currentNonce || 0; - } - } - - return Object.entries(summedNoncesByChainId) - .sort((a, b) => b[1] - a[1]) - .map(([chainId]) => parseInt(chainId)); -} - -// const pinnedNetworks = getMostUsedChains().slice(0, 5); -const useNetworkSwitcherStore = createRainbowStore<{ - pinnedNetworks: ChainId[]; -}>(() => ({ pinnedNetworks: [] }), { - storageKey: 'network-switcher', - version: 0, - onRehydrateStorage(state) { - // if we are missing pinned networks, use the user most used chains - if (state.pinnedNetworks.length === 0) { - const mostUsedNetworks = getMostUsedChains(); - state.pinnedNetworks = mostUsedNetworks.slice(0, 5); - analyticsV2.identify({ mostUsedNetworks: mostUsedNetworks.filter(Boolean) }); - } - }, -}); -const setNetworkSwitcherPinned = (pinnedNetworks: ChainId[]) => { - useNetworkSwitcherStore.setState({ pinnedNetworks }); -}; - const translations = { edit: i18n.t(t.edit), done: i18n.t(i18n.l.done), @@ -138,22 +107,7 @@ function Header({ editing }: { editing: SharedValue }) { ); } -const useCustomizeNetworksBanner = createRainbowStore<{ - dismissedAt: number; // timestamp -}>(() => ({ dismissedAt: 0 }), { - storageKey: 'CustomizeNetworksBanner', - version: 0, -}); -const twoWeeks = 1000 * 60 * 60 * 24 * 7 * 2; -const should_show_CustomizeNetworksBanner = (dismissedAt: number) => Date.now() - dismissedAt > twoWeeks; -const dismissCustomizeNetworksBanner = () => { - const { dismissedAt } = useCustomizeNetworksBanner.getState(); - if (should_show_CustomizeNetworksBanner(dismissedAt)) return; - useCustomizeNetworksBanner.setState({ dismissedAt: Date.now() }); -}; -const show_CustomizeNetworksBanner = should_show_CustomizeNetworksBanner(useCustomizeNetworksBanner.getState().dismissedAt); - -const CustomizeNetworksBanner = !show_CustomizeNetworksBanner +const CustomizeNetworksBanner = !showCustomizeNetworksBanner ? () => null : function CustomizeNetworksBanner({ editing }: { editing: SharedValue }) { useAnimatedReaction( @@ -163,8 +117,8 @@ const CustomizeNetworksBanner = !show_CustomizeNetworksBanner } ); - const dismissedAt = useCustomizeNetworksBanner(s => s.dismissedAt); - if (!should_show_CustomizeNetworksBanner(dismissedAt)) return null; + const dismissedAt = customizeNetworksBannerStore(s => s.dismissedAt); + if (!shouldShowCustomizeNetworksBanner(dismissedAt)) return null; const height = 75; const blue = '#268FFF'; @@ -285,10 +239,16 @@ const useNetworkOptionStyle = (isSelected: SharedValue, color: string) }; }; -function AllNetworksOption({ selected }: { selected: SharedValue }) { +function AllNetworksOption({ + selected, + setSelected, +}: { + selected: SharedValue; + setSelected: (chainId: ChainId | undefined) => void; +}) { const blue = useForegroundColor('blue'); - const isSelected = useDerivedValue(() => selected.value === 'all'); + const isSelected = useDerivedValue(() => selected.value === undefined); const { animatedStyle, selectedStyle, defaultStyle } = useNetworkOptionStyle(isSelected, blue); const overlappingBadge = useAnimatedStyle(() => { @@ -303,8 +263,8 @@ function AllNetworksOption({ selected }: { selected: SharedValue { - if (selected.value === 'all') selected.value = []; - else selected.value = 'all'; + 'worklet'; + setSelected(undefined); }); return ( @@ -336,7 +296,15 @@ function AllNetworksOption({ selected }: { selected: SharedValue; selected: SharedValue }) { +function AllNetworksSection({ + editing, + setSelected, + selected, +}: { + editing: SharedValue; + setSelected: (chainId: ChainId | undefined) => void; + selected: SharedValue; +}) { const style = useAnimatedStyle(() => ({ opacity: editing.value ? withTiming(0, { duration: 50 }) : withDelay(250, withTiming(1, { duration: 250 })), height: withTiming( @@ -348,18 +316,16 @@ function AllNetworksSection({ editing, selected }: { editing: SharedValue - + ); } -function NetworkOption({ chainId, selected }: { chainId: ChainId; selected: SharedValue }) { +function NetworkOption({ chainId, selected }: { chainId: ChainId; selected: SharedValue }) { const name = chainsLabel[chainId]; - if (!name) throw new Error(`: No chain name for chainId ${chainId}`); - const chainColor = getChainColorWorklet(chainId, true); - const isSelected = useDerivedValue(() => selected.value !== 'all' && selected.value.includes(chainId)); + const isSelected = useDerivedValue(() => selected.value === chainId); const { animatedStyle } = useNetworkOptionStyle(isSelected, chainColor); return ( @@ -468,25 +434,18 @@ function SectionSeparator({ editing, expanded, networks, + tapExpand, + pressedExpand, }: { y: SharedValue; editing: SharedValue; expanded: SharedValue; networks: SharedValue>; + tapExpand: TapGesture; + pressedExpand: SharedValue; }) { - const pressed = useSharedValue(false); - const tapGesture = Gesture.Tap() - .onBegin(e => { - if (editing.value) e.state = State.FAILED; - else pressed.value = true; - }) - .onEnd(() => { - pressed.value = false; - expanded.value = !expanded.value; - }); - const separatorStyles = useAnimatedStyle(() => ({ - transform: [{ translateY: y.value }, { scale: withTiming(pressed.value ? 0.95 : 1) }], + transform: [{ translateY: y.value }, { scale: withTiming(pressedExpand.value ? 0.95 : 1) }], })); const text = useDerivedValue(() => { @@ -500,7 +459,7 @@ function SectionSeparator({ const showMoreOrLessIconStyle = useAnimatedStyle(() => ({ opacity: editing.value ? 0 : 1 })); return ( - + ; selected: SharedValue }) { - const initialPinned = useNetworkSwitcherStore.getState().pinnedNetworks; +function NetworksGrid({ + editing, + setSelected, + selected, +}: { + editing: SharedValue; + setSelected: (chainId: ChainId | undefined) => void; + selected: SharedValue; +}) { + const initialPinned = networkSwitcherStore.getState().pinnedNetworks; const initialUnpinned = SUPPORTED_CHAIN_IDS_ALPHABETICAL.filter(chainId => !initialPinned.includes(chainId)); const networks = useSharedValue({ [Section.pinned]: initialPinned, [Section.unpinned]: initialUnpinned }); @@ -547,7 +514,7 @@ function NetworksGrid({ editing, selected }: { editing: SharedValue; se // persists pinned networks when closing the sheet // should be the only time this component is unmounted return () => { - setNetworkSwitcherPinned(networks.value[Section.pinned]); + networkSwitcherStore.setState({ pinnedNetworks: networks.value[Section.pinned] }); }; }, [networks]); @@ -581,13 +548,18 @@ function NetworksGrid({ editing, selected }: { editing: SharedValue; se const sectionOffset = sectionsOffsets.value[section]; const index = indexFromPosition(touch.x, touch.y, sectionOffset); const chainId = networks.value[section][index]; + if (!chainId) { + s.fail(); + return; + } + const position = positionFromIndex(index, sectionOffset); dragging.value = { chainId, position }; }) .onChange(e => { - 'worklet'; if (!dragging.value) return; const chainId = dragging.value.chainId; + if (!chainId) return; const section = e.y > sectionsOffsets.value[Section.unpinned].y ? Section.unpinned : Section.pinned; const sectionArray = networks.value[section]; @@ -616,33 +588,46 @@ function NetworksGrid({ editing, selected }: { editing: SharedValue; se }); }) .onFinalize(() => { - 'worklet'; dragging.value = null; }); - const tapNetwork = Gesture.Tap().onTouchesDown((e, s) => { - 'worklet'; - const touch = e.allTouches[0]; - if (editing.value) { - s.fail(); - return; - } - const section = touch.y > sectionsOffsets.value[Section.unpinned].y ? Section.unpinned : Section.pinned; - const index = indexFromPosition(touch.x, touch.y, sectionsOffsets.value[section]); - const chainId = networks.value[section][index]; - - selected.modify(selected => { - if (selected === 'all') { - // @ts-expect-error I think something is wrong with reanimated types, not infering right here - selected = [chainId]; - return selected; + const pressedExpand = useSharedValue(false); + + // TODO: Need to prevent this from firing the tapNetwork as well + const tapExpand = Gesture.Tap() + .onBegin(e => { + if (editing.value) { + e.state = State.FAILED; } - const selectedIndex = selected.indexOf(chainId); - if (selectedIndex !== -1) selected.splice(selectedIndex, 1); - else selected.push(chainId); - return selected; + pressedExpand.value = true; + }) + .onEnd(() => { + pressedExpand.value = false; + expanded.value = !expanded.value; + }); + + const tapNetwork = Gesture.Tap() + .onTouchesDown((e, s) => { + if (editing.value) { + s.fail(); + } + + const touches = e.allTouches[0]; + const section = touches.y > sectionsOffsets.value[Section.unpinned].y ? Section.unpinned : Section.pinned; + const index = indexFromPosition(touches.x, touches.y, sectionsOffsets.value[section]); + const chainId = networks.value[section][index]; + if (!chainId) { + s.fail(); + } + }) + .onEnd(e => { + const section = e.y > sectionsOffsets.value[Section.unpinned].y ? Section.unpinned : Section.pinned; + const index = indexFromPosition(e.x, e.y, sectionsOffsets.value[section]); + const chainId = networks.value[section][index]; + if (!chainId) return; + + setSelected(chainId); }); - }); const gridGesture = Gesture.Exclusive(dragNetwork, tapNetwork); @@ -662,7 +647,14 @@ function NetworksGrid({ editing, selected }: { editing: SharedValue; se ))} - + {/* {initialUnpinned.length === 0 && ( @@ -715,7 +707,7 @@ function Sheet({ children, header, onClose }: PropsWithChildren<{ header: ReactE translationY.value = event.translationY; }) .onFinalize(() => { - if (translationY.value > 120) onClose(); + if (translationY.value > 120) runOnJS(onClose)(); else translationY.value = withSpring(0); }); @@ -748,27 +740,20 @@ function Sheet({ children, header, onClose }: PropsWithChildren<{ header: ReactE export function NetworkSelector({ onClose, - multiple, + selected, + setSelected, }: { - onClose: (selected: ChainId[] | 'all') => void; - onSelect: VoidFunction; - multiple?: boolean; + onClose: VoidFunction; + selected: SharedValue; + setSelected: (chainId: ChainId | undefined) => void; }) { const editing = useSharedValue(false); - const selected = useSharedValue([]); - - const close = () => { - 'worklet'; - runOnJS(onClose)(selected.value); - }; return ( - } onClose={close}> + } onClose={onClose}> - - {multiple && } - - + + ); } diff --git a/src/components/expanded-state/asset/ChartExpandedState.js b/src/components/expanded-state/asset/ChartExpandedState.js index 136e3eeb4ce..74280105eaf 100644 --- a/src/components/expanded-state/asset/ChartExpandedState.js +++ b/src/components/expanded-state/asset/ChartExpandedState.js @@ -1,6 +1,6 @@ import { useRoute } from '@react-navigation/native'; import lang from 'i18n-js'; -import React, { useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; import { LayoutAnimation, View } from 'react-native'; import { getSoftMenuBarHeight } from 'react-native-extra-dimensions-android'; import { ModalContext } from '../../../react-native-cool-modals/NativeStackView'; @@ -23,7 +23,6 @@ import { useChartThrottledPoints, useDelayedValueWithLayoutAnimation, useDimensions, - useTimeout, } from '@/hooks'; import { useRemoteConfig } from '@/model/remoteConfig'; import { useNavigation } from '@/navigation'; diff --git a/src/helpers/strings.ts b/src/helpers/strings.ts index 4e7fd76ab91..a4151c0dc0b 100644 --- a/src/helpers/strings.ts +++ b/src/helpers/strings.ts @@ -10,3 +10,59 @@ export const containsEmoji = memoFn(str => { // @ts-expect-error ts-migrate(2571) FIXME: Object is of type 'unknown'. return !!str.match(ranges.join('|')); }); + +/* + * Return the given number as a formatted string. The default format is a plain + * integer with thousands-separator commas. The optional parameters facilitate + * other formats: + * - decimals = the number of decimals places to round to and show + * - valueIfNaN = the value to show for non-numeric input + * - style + * - '%': multiplies by 100 and appends a percent symbol + * - '$': prepends a dollar sign + * - useOrderSuffix = whether to use suffixes like k for 1,000, etc. + * - orderSuffixes = the list of suffixes to use + * - minOrder and maxOrder allow the order to be constrained. Examples: + * - minOrder = 1 means the k suffix should be used for numbers < 1,000 + * - maxOrder = 1 means the k suffix should be used for numbers >= 1,000,000 + */ +export function formatNumber( + number: string | number, + { + decimals = 0, + valueIfNaN = '', + style = '', + useOrderSuffix = false, + orderSuffixes = ['', 'K', 'M', 'B', 'T'], + minOrder = 0, + maxOrder = Infinity, + } = {} +) { + let x = parseFloat(`${number}`); + + if (isNaN(x)) return valueIfNaN; + + if (style === '%') x *= 100.0; + + let order; + if (!isFinite(x) || !useOrderSuffix) order = 0; + else if (minOrder === maxOrder) order = minOrder; + else { + const unboundedOrder = Math.floor(Math.log10(Math.abs(x)) / 3); + order = Math.max(0, minOrder, Math.min(unboundedOrder, maxOrder, orderSuffixes.length - 1)); + } + + const orderSuffix = orderSuffixes[order]; + if (order !== 0) x /= Math.pow(10, order * 3); + + return ( + (style === '$' ? '$' : '') + + x.toLocaleString('en-US', { + style: 'decimal', + minimumFractionDigits: decimals, + maximumFractionDigits: decimals, + }) + + orderSuffix + + (style === '%' ? '%' : '') + ); +} diff --git a/src/hooks/reanimated/useSyncSharedValue.ts b/src/hooks/reanimated/useSyncSharedValue.ts index f8c19c71a0c..c48f83a3643 100644 --- a/src/hooks/reanimated/useSyncSharedValue.ts +++ b/src/hooks/reanimated/useSyncSharedValue.ts @@ -9,14 +9,14 @@ interface BaseSyncParams { /** A boolean or shared value boolean that controls whether synchronization is paused. */ pauseSync?: DerivedValue | SharedValue | boolean; /** The JS state to be synchronized. */ - state: T | undefined; + state: T; } interface SharedToStateParams extends BaseSyncParams { /** The setter function for the JS state (only applicable when `syncDirection` is `'sharedValueToState'`). */ setState: (value: T) => void; /** The shared value to be synchronized. */ - sharedValue: DerivedValue | DerivedValue | SharedValue | SharedValue; + sharedValue: DerivedValue | SharedValue; /** The direction of synchronization. */ syncDirection: 'sharedValueToState'; } @@ -24,7 +24,7 @@ interface SharedToStateParams extends BaseSyncParams { interface StateToSharedParams extends BaseSyncParams { setState?: never; /** The shared value to be synchronized. */ - sharedValue: SharedValue | SharedValue; + sharedValue: SharedValue; /** The direction of synchronization. */ syncDirection: 'stateToSharedValue'; } @@ -73,7 +73,7 @@ export function useSyncSharedValue({ compareDepth = 'deep', pauseSync, setSta }, shouldSync => { if (shouldSync) { - if (syncDirection === 'sharedValueToState' && sharedValue.value !== undefined) { + if (syncDirection === 'sharedValueToState') { runOnJS(setState)(sharedValue.value); } else if (syncDirection === 'stateToSharedValue') { sharedValue.value = state; diff --git a/src/languages/en_US.json b/src/languages/en_US.json index 9236998ac25..9b75f4ad8bb 100644 --- a/src/languages/en_US.json +++ b/src/languages/en_US.json @@ -2990,6 +2990,7 @@ } }, "trending_tokens": { + "all": "All", "no_results": { "title": "No results", "body": "Try browsing a larger timeframe or a different network or category." diff --git a/src/navigation/SwipeNavigator.tsx b/src/navigation/SwipeNavigator.tsx index 33f84504828..781ca8932d4 100644 --- a/src/navigation/SwipeNavigator.tsx +++ b/src/navigation/SwipeNavigator.tsx @@ -15,7 +15,7 @@ import RecyclerListViewScrollToTopProvider, { useRecyclerListViewScrollToTopContext, } from '@/navigation/RecyclerListViewScrollToTopContext'; import DappBrowserScreen from '@/screens/dapp-browser/DappBrowserScreen'; -import { discoverOpenSearchFnRef } from '@/screens/discover/components/DiscoverSearchContainer'; +import { discoverOpenSearchFnRef } from '@/components/Discover/DiscoverSearchContainer'; import { PointsScreen } from '@/screens/points/PointsScreen'; import WalletScreen from '@/screens/WalletScreen'; import { useTheme } from '@/theme'; @@ -39,7 +39,7 @@ import { TIMING_CONFIGS } from '@/components/animations/animationConfigs'; import { useBrowserStore } from '@/state/browser/browserStore'; import { opacityWorklet } from '@/__swaps__/utils/swaps'; import ProfileScreen from '../screens/ProfileScreen'; -import DiscoverScreen, { discoverScrollToTopFnRef } from '../screens/discover/DiscoverScreen'; +import DiscoverScreen, { discoverScrollToTopFnRef } from '@/screens/DiscoverScreen'; import { ScrollPositionContext } from './ScrollPositionContext'; import SectionListScrollToTopProvider, { useSectionListScrollToTopContext } from './SectionListScrollToTopContext'; import Routes from './routesNames'; diff --git a/src/performance/tracking/index.ts b/src/performance/tracking/index.ts index 425faba867e..81b8fb5e73a 100644 --- a/src/performance/tracking/index.ts +++ b/src/performance/tracking/index.ts @@ -18,7 +18,7 @@ function logDurationIfAppropriate(metric: PerformanceMetricsType, durationInMs: } } -const currentlyTrackedMetrics = new Map(); +export const currentlyTrackedMetrics = new Map(); interface AdditionalParams extends Record { tag?: PerformanceTagsType; diff --git a/src/performance/tracking/types/PerformanceMetrics.ts b/src/performance/tracking/types/PerformanceMetrics.ts index 3baf050eb54..3d272e1e71b 100644 --- a/src/performance/tracking/types/PerformanceMetrics.ts +++ b/src/performance/tracking/types/PerformanceMetrics.ts @@ -11,6 +11,7 @@ export const PerformanceMetrics = { initializeWalletconnect: 'Performance WalletConnect Initialize Time', quoteFetching: 'Performance Quote Fetching Time', + timeSpentOnDiscoverScreen: 'Time spent on the Discover screen', } as const; export type PerformanceMetricsType = (typeof PerformanceMetrics)[keyof typeof PerformanceMetrics]; diff --git a/src/resources/trendingTokens/trendingTokens.ts b/src/resources/trendingTokens/trendingTokens.ts index 169a24ab6f4..06e6f623318 100644 --- a/src/resources/trendingTokens/trendingTokens.ts +++ b/src/resources/trendingTokens/trendingTokens.ts @@ -22,5 +22,6 @@ export function useTrendingTokens( ...config, staleTime: 60_000, // 1 minute cacheTime: 60_000 * 30, // 30 minutes + keepPreviousData: true, }); } diff --git a/src/screens/discover/DiscoverScreen.tsx b/src/screens/DiscoverScreen.tsx similarity index 95% rename from src/screens/discover/DiscoverScreen.tsx rename to src/screens/DiscoverScreen.tsx index 601066d6260..2a0d43df6a2 100644 --- a/src/screens/discover/DiscoverScreen.tsx +++ b/src/screens/DiscoverScreen.tsx @@ -4,7 +4,7 @@ import { useIsFocused } from '@react-navigation/native'; import { Box } from '@/design-system'; import { Page } from '@/components/layout'; import { Navbar } from '@/components/navbar/Navbar'; -import DiscoverScreenContent from './components/DiscoverScreenContent'; +import DiscoverScreenContent from '@/components/Discover/DiscoverScreenContent'; import { ButtonPressAnimation } from '@/components/animations'; import { ContactAvatar } from '@/components/contacts'; import ImageAvatar from '@/components/contacts/ImageAvatar'; @@ -14,7 +14,7 @@ import { useNavigation } from '@/navigation'; import { safeAreaInsetValues } from '@/utils'; import * as i18n from '@/languages'; import Animated, { useAnimatedScrollHandler, useSharedValue } from 'react-native-reanimated'; -import DiscoverScreenProvider, { useDiscoverScreenContext } from './DiscoverScreenContext'; +import DiscoverScreenProvider, { useDiscoverScreenContext } from '@/components/Discover/DiscoverScreenContext'; export let discoverScrollToTopFnRef: () => number | null = () => null; @@ -30,18 +30,18 @@ const Content = () => { navigate(Routes.CHANGE_WALLET_SHEET); }, [navigate]); - React.useEffect(() => { - if (isSearching && !isFocused) { - Keyboard.dismiss(); - } - }, [isFocused, isSearching]); - const scrollHandler = useAnimatedScrollHandler({ onScroll: event => { scrollY.value = event.contentOffset.y; }, }); + useEffect(() => { + if (isSearching && !isFocused) { + Keyboard.dismiss(); + } + }, [isFocused, isSearching]); + useEffect(() => { discoverScrollToTopFnRef = scrollToTop; }, [scrollToTop]); diff --git a/src/screens/discover/components/TrendingTokens.tsx b/src/screens/discover/components/TrendingTokens.tsx deleted file mode 100644 index 033e603a287..00000000000 --- a/src/screens/discover/components/TrendingTokens.tsx +++ /dev/null @@ -1,417 +0,0 @@ -import { ChainId } from '@/chains/types'; -import { ChainBadge } from '@/components/coin-icon'; -import { DropdownMenu } from '@/components/DropdownMenu'; -import { globalColors, Text, useBackgroundColor } from '@/design-system'; -import { useForegroundColor } from '@/design-system/color/useForegroundColor'; - -import chroma from 'chroma-js'; -import { useState } from 'react'; -import React, { View } from 'react-native'; -import FastImage from 'react-native-fast-image'; -import { Gesture, GestureDetector } from 'react-native-gesture-handler'; -import LinearGradient from 'react-native-linear-gradient'; -import Animated, { LinearTransition, runOnJS, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; -import { NetworkSelector } from './NetworkSwitcher'; -import * as i18n from '@/languages'; -import { useTheme } from '@/theme'; - -const AnimatedLinearGradient = Animated.createAnimatedComponent(LinearGradient); - -function FilterButton({ icon, label, onPress }: { onPress?: VoidFunction; label: string; icon: string }) { - const pressed = useSharedValue(false); - - const tap = Gesture.Tap() - .onBegin(() => { - pressed.value = true; - if (onPress) runOnJS(onPress)(); - }) - .onFinalize(() => (pressed.value = false)); - - const animatedStyles = useAnimatedStyle(() => ({ - transform: [{ scale: withTiming(pressed.value ? 0.95 : 1, { duration: 100 }) }], - })); - - const backgroundColor = useBackgroundColor('fillTertiary'); - const borderColor = useBackgroundColor('fillSecondary'); - - const iconColor = useForegroundColor('labelQuaternary'); - - return ( - - - - {icon} - - - {label} - - - 􀆏 - - - - ); -} - -function CategoryFilterButton({ - selected, - onPress, - icon, - iconWidth = 16, - iconColor, - label, -}: { - onPress: VoidFunction; - selected: boolean; - icon: string; - iconColor: string; - iconWidth?: number; - label: string; -}) { - const { isDarkMode } = useTheme(); - const fillTertiary = useBackgroundColor('fillTertiary'); - const fillSecondary = useBackgroundColor('fillSecondary'); - - const borderColor = selected && isDarkMode ? globalColors.white80 : fillSecondary; - - const pressed = useSharedValue(false); - - const tap = Gesture.Tap() - .onBegin(() => { - pressed.value = true; - runOnJS(onPress)(); - }) - .onFinalize(() => (pressed.value = false)); - - const animatedStyles = useAnimatedStyle(() => ({ - transform: [{ scale: withTiming(pressed.value ? 0.95 : 1, { duration: 100 }) }], - })); - - return ( - - - - {icon} - - - {label} - - - - ); -} - -function FriendHolders() { - const backgroundColor = useBackgroundColor('surfacePrimary'); - return ( - - - - - - - - mikedemarais{' '} - - and 2 others - - - - ); -} - -function TokenIcon({ uri, chainId }: { uri: string; chainId: ChainId }) { - return ( - - - {chainId !== ChainId.mainnet && } - - ); -} - -function TrendingTokenRow() { - const separatorColor = useForegroundColor('separator'); - - const percentChange24h = '3.40%'; - const percentChange1h = '8.82%'; - - const token = { - name: 'Uniswap', - symbol: 'UNI', - price: '$9.21', - }; - - const volume = '$1.8M'; - const marketCap = '$1.8M'; - - return ( - - - - - - - - - - - {token.name} - - - {token.symbol} - - - {token.price} - - - - - - - VOL - - - {volume} - - - - - | - - - - - MCAP - - - {marketCap} - - - - - - - - - 􀄨 - - - {percentChange24h} - - - - - 1H - - - {percentChange1h} - - - - - - - ); -} - -const t = i18n.l.trending_tokens; - -function NoResults() { - const { isDarkMode } = useTheme(); - const fillQuaternary = useBackgroundColor('fillQuaternary'); - const backgroundColor = isDarkMode ? '#191A1C' : fillQuaternary; - - return ( - - - - {i18n.t(t.no_results.title)} - - - {i18n.t(t.no_results.body)} - - - - - 􀙭 - - - - ); -} - -function NetworkFilter() { - const [isOpen, setOpen] = useState(false); - - return ( - <> - setOpen(true)} /> - {isOpen && ( - { - console.log(selected); - setOpen(false); - }} - onSelect={() => null} - multiple - /> - )} - - ); -} - -const sortFilters = ['volume', 'market_cap', 'top_gainers', 'top_losers'] as const; -const timeFilters = ['day', 'week', 'month'] as const; -type TrendingTokensFilter = { - category: 'trending' | 'new' | 'farcaster'; - network: undefined | ChainId; - timeframe: (typeof timeFilters)[number]; - sort: (typeof sortFilters)[number] | undefined; -}; - -export function TrendingTokens() { - const [filter, setFilter] = useState({ - category: 'trending', - network: undefined, - timeframe: 'day', - sort: 'volume', - }); - const setCategory = (category: TrendingTokensFilter['category']) => setFilter(filter => ({ ...filter, category })); - return ( - - - - setCategory('trending')} - /> - setCategory('new')} - /> - setCategory('farcaster')} - /> - - - - - - ({ - actionTitle: i18n.t(t.filters.time[time]), - actionKey: time, - })), - }} - side="bottom" - onPressMenuItem={timeframe => setFilter(filter => ({ ...filter, timeframe }))} - > - - - - ({ - actionTitle: i18n.t(t.filters.sort[sort]), - actionKey: sort, - })), - }} - side="bottom" - onPressMenuItem={sort => - setFilter(filter => { - if (sort === filter.sort) return { ...filter, sort: undefined }; - return { ...filter, sort }; - }) - } - > - - - - - - - - - - - - - - - - - ); -} diff --git a/src/state/networkSwitcher/networkSwitcher.ts b/src/state/networkSwitcher/networkSwitcher.ts new file mode 100644 index 00000000000..1ea781b242d --- /dev/null +++ b/src/state/networkSwitcher/networkSwitcher.ts @@ -0,0 +1,48 @@ +import { ChainId } from '@/chains/types'; +import { createRainbowStore } from '../internal/createRainbowStore'; +import { analyticsV2 } from '@/analytics'; +import { nonceStore } from '@/state/nonces'; + +function getMostUsedChains() { + const noncesByAddress = nonceStore.getState().nonces; + const summedNoncesByChainId: Record = {}; + for (const addressNonces of Object.values(noncesByAddress)) { + for (const [chainId, { currentNonce }] of Object.entries(addressNonces)) { + summedNoncesByChainId[chainId] ??= 0; + summedNoncesByChainId[chainId] += currentNonce || 0; + } + } + + return Object.entries(summedNoncesByChainId) + .sort((a, b) => b[1] - a[1]) + .map(([chainId]) => parseInt(chainId)); +} + +export const networkSwitcherStore = createRainbowStore<{ + pinnedNetworks: ChainId[]; +}>(() => ({ pinnedNetworks: getMostUsedChains().slice(0, 5) }), { + storageKey: 'network-switcher', + version: 0, + onRehydrateStorage(state) { + // if we are missing pinned networks, use the user most used chains + if (state.pinnedNetworks.length === 0) { + const mostUsedNetworks = getMostUsedChains(); + state.pinnedNetworks = mostUsedNetworks.slice(0, 5); + analyticsV2.identify({ mostUsedNetworks: mostUsedNetworks.filter(Boolean) }); + } + }, +}); + +export const customizeNetworksBannerStore = createRainbowStore<{ + dismissedAt: number; // timestamp +}>(() => ({ dismissedAt: 0 }), { + storageKey: 'CustomizeNetworksBanner', + version: 0, +}); + +const twoWeeks = 1000 * 60 * 60 * 24 * 7 * 2; +export const shouldShowCustomizeNetworksBanner = (dismissedAt: number) => Date.now() - dismissedAt > twoWeeks; +export const dismissCustomizeNetworksBanner = () => { + customizeNetworksBannerStore.setState({ dismissedAt: Date.now() }); +}; +export const showCustomizeNetworksBanner = shouldShowCustomizeNetworksBanner(customizeNetworksBannerStore.getState().dismissedAt); diff --git a/src/state/swaps/swapsStore.ts b/src/state/swaps/swapsStore.ts index 926b1037222..401d303fa9f 100644 --- a/src/state/swaps/swapsStore.ts +++ b/src/state/swaps/swapsStore.ts @@ -1,7 +1,7 @@ import { INITIAL_SLIDER_POSITION, MIN_FLASHBOTS_PRIORITY_FEE } from '@/__swaps__/screens/Swap/constants'; import { getCustomGasSettings, setCustomMaxPriorityFee } from '@/__swaps__/screens/Swap/hooks/useCustomGas'; import { getSelectedGasSpeed } from '@/__swaps__/screens/Swap/hooks/useSelectedGas'; -import { ExtendedAnimatedAssetWithColors, ParsedSearchAsset } from '@/__swaps__/types/assets'; +import { ExtendedAnimatedAssetWithColors, ParsedSearchAsset, UniqueId } from '@/__swaps__/types/assets'; import { ChainId } from '@/chains/types'; import { GasSpeed } from '@/__swaps__/types/gas'; import { RecentSwap } from '@/__swaps__/types/swap'; @@ -49,6 +49,8 @@ export interface SwapsState { // degen mode preferences preferredNetwork: ChainId | undefined; setPreferredNetwork: (preferredNetwork: ChainId | undefined) => void; + + lastNavigatedTrendingToken: UniqueId | undefined; } type StateWithTransforms = Omit, 'latestSwapAt' | 'recentSwaps'> & { @@ -188,6 +190,8 @@ export const swapsStore = createRainbowStore( latestSwapAt: new Map(latestSwapAt), }); }, + + lastNavigatedTrendingToken: undefined, }), { storageKey: 'swapsStore', diff --git a/src/state/trendingTokens/trendingTokens.ts b/src/state/trendingTokens/trendingTokens.ts new file mode 100644 index 00000000000..e99f10b6072 --- /dev/null +++ b/src/state/trendingTokens/trendingTokens.ts @@ -0,0 +1,46 @@ +import { ChainId } from '@/chains/types'; +import { createRainbowStore } from '../internal/createRainbowStore'; +import { makeMutable, SharedValue } from 'react-native-reanimated'; +import { analyticsV2 } from '@/analytics'; + +export const categories = ['trending', 'new', 'farcaster'] as const; +export const sortFilters = ['volume', 'market_cap', 'top_gainers', 'top_losers'] as const; +export const timeFilters = ['day', 'week', 'month'] as const; + +type TrendingTokensState = { + category: 'trending' | 'new' | 'farcaster'; + chainId: undefined | ChainId; + timeframe: (typeof timeFilters)[number]; + sort: (typeof sortFilters)[number] | undefined; + + setCategory: (category: TrendingTokensState['category']) => void; + setChainId: (chainId: TrendingTokensState['chainId']) => void; + setTimeframe: (timeframe: TrendingTokensState['timeframe']) => void; + setSort: (sort: TrendingTokensState['sort']) => void; +}; + +export const useTrendingTokensStore = createRainbowStore( + set => ({ + category: 'trending', + chainId: undefined, + timeframe: 'day', + sort: 'volume', + setCategory: category => set({ category }), + setChainId: chainId => { + analyticsV2.track(analyticsV2.event.changeNetworkFilter, { chainId }); + set({ chainId }); + }, + setTimeframe: timeframe => { + analyticsV2.track(analyticsV2.event.changeTimeframeFilter, { timeframe }); + set({ timeframe }); + }, + setSort: sort => { + analyticsV2.track(analyticsV2.event.changeSortFilter, { sort }); + set({ sort }); + }, + }), + { + storageKey: 'trending-tokens', + version: 0, + } +); diff --git a/src/styles/colors.ts b/src/styles/colors.ts index 0eeb8b0f104..b1bfe39c376 100644 --- a/src/styles/colors.ts +++ b/src/styles/colors.ts @@ -49,7 +49,7 @@ const darkModeColors = { skeleton: '#191B21', stackBackground: '#000000', surfacePrimary: '#000000', - white: '#000', + white: '#12131A', whiteLabel: '#FFFFFF', }; From 814eb1c7f4f4bbfe251a55895f6144a951659d77 Mon Sep 17 00:00:00 2001 From: gregs Date: Mon, 9 Dec 2024 14:21:12 -0300 Subject: [PATCH 10/95] lint? --- src/components/NetworkSwitcher.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/NetworkSwitcher.tsx b/src/components/NetworkSwitcher.tsx index fe418511515..c97236e6bb7 100644 --- a/src/components/NetworkSwitcher.tsx +++ b/src/components/NetworkSwitcher.tsx @@ -455,7 +455,7 @@ function SectionSeparator({ const unpinnedNetworksLength = useDerivedValue(() => networks.value[Section.unpinned].length.toString()); const showMoreAmountStyle = useAnimatedStyle(() => ({ opacity: expanded.value || editing.value ? 0 : 1 })); - const showMoreOrLessIcon = useDerivedValue(() => (expanded.value ? '􀆇' : '􀆈')); + const showMoreOrLessIcon = useDerivedValue(() => (expanded.value ? '􀆇' : '􀆈') as string); const showMoreOrLessIconStyle = useAnimatedStyle(() => ({ opacity: editing.value ? 0 : 1 })); return ( From 281900d7c781e051d1e1b6b9c49f592dbccd4774 Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Mon, 9 Dec 2024 17:01:09 -0500 Subject: [PATCH 11/95] add remote config flag for trending tokens --- src/components/Discover/DiscoverHome.tsx | 4 ++-- src/model/remoteConfig.ts | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/Discover/DiscoverHome.tsx b/src/components/Discover/DiscoverHome.tsx index d06f149fb7c..54cb5ed71f9 100644 --- a/src/components/Discover/DiscoverHome.tsx +++ b/src/components/Discover/DiscoverHome.tsx @@ -34,7 +34,7 @@ import { TrendingTokens } from '@/components/Discover/TrendingTokens'; export const HORIZONTAL_PADDING = 20; export default function DiscoverHome() { - const { profiles_enabled, mints_enabled, op_rewards_enabled, featured_results } = useRemoteConfig(); + const { profiles_enabled, mints_enabled, op_rewards_enabled, featured_results, trending_tokens_enabled } = useRemoteConfig(); const { chainId } = useAccountSettings(); const profilesEnabledLocalFlag = useExperimentalFlag(PROFILES); const profilesEnabledRemoteFlag = profiles_enabled; @@ -44,7 +44,7 @@ export default function DiscoverHome() { const mintsEnabled = (useExperimentalFlag(MINTS) || mints_enabled) && !IS_TEST; const opRewardsLocalFlag = useExperimentalFlag(OP_REWARDS); const opRewardsRemoteFlag = op_rewards_enabled; - const trendingTokensEnabled = useExperimentalFlag(TRENDING_TOKENS); + const trendingTokensEnabled = (useExperimentalFlag(TRENDING_TOKENS) || trending_tokens_enabled) && !IS_TEST; const testNetwork = isTestnetChain({ chainId }); const { navigate } = useNavigation(); const isProfilesEnabled = profilesEnabledLocalFlag && profilesEnabledRemoteFlag; diff --git a/src/model/remoteConfig.ts b/src/model/remoteConfig.ts index 3481092be7e..ab8eba1e2e8 100644 --- a/src/model/remoteConfig.ts +++ b/src/model/remoteConfig.ts @@ -52,6 +52,7 @@ export interface RainbowConfig extends Record dapp_browser: boolean; idfa_check_enabled: boolean; rewards_enabled: boolean; + trending_tokens_enabled: boolean; degen_mode: boolean; featured_results: boolean; @@ -123,6 +124,7 @@ export const DEFAULT_CONFIG: RainbowConfig = { dapp_browser: true, idfa_check_enabled: true, rewards_enabled: true, + trending_tokens_enabled: false, degen_mode: true, featured_results: true, @@ -181,6 +183,7 @@ export async function fetchRemoteConfig(): Promise { key === 'dapp_browser' || key === 'idfa_check_enabled' || key === 'rewards_enabled' || + key === 'trending_tokens_enabled' || key === 'degen_mode' || key === 'featured_results' || key === 'claimables' || From bb2247b31628589734a95a01fecd1479c937ab52 Mon Sep 17 00:00:00 2001 From: gregs Date: Wed, 11 Dec 2024 14:01:26 -0300 Subject: [PATCH 12/95] scroll & light mode --- src/components/Discover/TrendingTokens.tsx | 65 +++++++++++++--------- src/components/NetworkSwitcher.tsx | 4 +- 2 files changed, 42 insertions(+), 27 deletions(-) diff --git a/src/components/Discover/TrendingTokens.tsx b/src/components/Discover/TrendingTokens.tsx index caf8e659a6e..41d42e14f69 100644 --- a/src/components/Discover/TrendingTokens.tsx +++ b/src/components/Discover/TrendingTokens.tsx @@ -2,31 +2,31 @@ import { DropdownMenu } from '@/components/DropdownMenu'; import { globalColors, Text, useBackgroundColor } from '@/design-system'; import { useForegroundColor } from '@/design-system/color/useForegroundColor'; -import chroma from 'chroma-js'; -import { useCallback, useEffect, useMemo, useState } from 'react'; -import React, { FlatList, View } from 'react-native'; -import FastImage from 'react-native-fast-image'; -import { Gesture, GestureDetector } from 'react-native-gesture-handler'; -import LinearGradient from 'react-native-linear-gradient'; -import Animated, { LinearTransition, runOnJS, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; -import { NetworkSelector } from '@/components/NetworkSwitcher'; -import * as i18n from '@/languages'; -import { useTheme } from '@/theme'; -import { TrendingTokens as TrendingTokensType, useTrendingTokens } from '@/resources/trendingTokens/trendingTokens'; -import { SwapCoinIcon } from '@/__swaps__/screens/Swap/components/SwapCoinIcon'; import { GestureHandlerButton } from '@/__swaps__/screens/Swap/components/GestureHandlerButton'; -import Skeleton, { FakeAvatar, FakeText } from '@/components/skeleton/Skeleton'; -import { colors } from '@/styles'; -import { categories, sortFilters, timeFilters, useTrendingTokensStore } from '@/state/trendingTokens/trendingTokens'; +import { SwapCoinIcon } from '@/__swaps__/screens/Swap/components/SwapCoinIcon'; +import { analyticsV2 } from '@/analytics'; import { chainsLabel } from '@/chains'; +import { ChainId } from '@/chains/types'; import { ChainImage } from '@/components/coin-icon/ChainImage'; +import { NetworkSelector } from '@/components/NetworkSwitcher'; +import Skeleton, { FakeAvatar, FakeText } from '@/components/skeleton/Skeleton'; import { formatNumber } from '@/helpers/strings'; +import * as i18n from '@/languages'; import { Navigation } from '@/navigation'; import Routes from '@/navigation/routesNames'; -import { analyticsV2 } from '@/analytics'; -import { swapsStore } from '@/state/swaps/swapsStore'; -import { ChainId } from '@/chains/types'; +import { TrendingTokens as TrendingTokensType, useTrendingTokens } from '@/resources/trendingTokens/trendingTokens'; import { useNavigationStore } from '@/state/navigation/navigationStore'; +import { swapsStore } from '@/state/swaps/swapsStore'; +import { categories, sortFilters, timeFilters, useTrendingTokensStore } from '@/state/trendingTokens/trendingTokens'; +import { colors } from '@/styles'; +import { useTheme } from '@/theme'; +import chroma from 'chroma-js'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { FlatList, View } from 'react-native'; +import FastImage from 'react-native-fast-image'; +import { Gesture, GestureDetector } from 'react-native-gesture-handler'; +import LinearGradient from 'react-native-linear-gradient'; +import Animated, { LinearTransition, runOnJS, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; const t = i18n.l.trending_tokens; @@ -155,9 +155,11 @@ function CategoryFilterButton({ const tap = Gesture.Tap() .onBegin(() => { pressed.value = true; - runOnJS(selectCategory)(); }) - .onFinalize(() => (pressed.value = false)); + .onEnd(() => { + pressed.value = false; + runOnJS(selectCategory)(); + }); const animatedStyles = useAnimatedStyle(() => ({ transform: [{ scale: withTiming(pressed.value ? 0.95 : 1, { duration: 100 }) }], @@ -564,26 +566,37 @@ function TrendingTokenData() { } export function TrendingTokens() { + const padding = 20; return ( - + - - - + + + - + diff --git a/src/components/NetworkSwitcher.tsx b/src/components/NetworkSwitcher.tsx index c97236e6bb7..5415d59bcd2 100644 --- a/src/components/NetworkSwitcher.tsx +++ b/src/components/NetworkSwitcher.tsx @@ -458,6 +458,8 @@ function SectionSeparator({ const showMoreOrLessIcon = useDerivedValue(() => (expanded.value ? '􀆇' : '􀆈') as string); const showMoreOrLessIconStyle = useAnimatedStyle(() => ({ opacity: editing.value ? 0 : 1 })); + const { isDarkMode } = useTheme(); + return ( Date: Wed, 11 Dec 2024 14:06:32 -0300 Subject: [PATCH 13/95] better categories colors --- src/components/Discover/TrendingTokens.tsx | 24 ++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/components/Discover/TrendingTokens.tsx b/src/components/Discover/TrendingTokens.tsx index 41d42e14f69..39152d2b8bf 100644 --- a/src/components/Discover/TrendingTokens.tsx +++ b/src/components/Discover/TrendingTokens.tsx @@ -131,10 +131,12 @@ function CategoryFilterButton({ iconWidth = 16, iconColor, label, + highlightedBackgroundColor, }: { category: (typeof categories)[number]; icon: string; iconColor: string; + highlightedBackgroundColor: string; iconWidth?: number; label: string; }) { @@ -168,7 +170,7 @@ function CategoryFilterButton({ return ( - - + + From 2e5314e756b7a6a7411673105125fae5e123718c Mon Sep 17 00:00:00 2001 From: gregs Date: Wed, 11 Dec 2024 14:25:38 -0300 Subject: [PATCH 14/95] fix select all chain badges --- src/components/NetworkSwitcher.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/NetworkSwitcher.tsx b/src/components/NetworkSwitcher.tsx index 5415d59bcd2..a60ea1cb799 100644 --- a/src/components/NetworkSwitcher.tsx +++ b/src/components/NetworkSwitcher.tsx @@ -253,7 +253,7 @@ function AllNetworksOption({ const overlappingBadge = useAnimatedStyle(() => { return { - borderColor: isSelected.value ? selectedStyle.borderColor : defaultStyle.borderColor, + borderColor: isSelected.value ? selectedStyle.backgroundColor : defaultStyle.backgroundColor, borderWidth: 1.67, borderRadius: 16, marginLeft: -9, From 5f381c71aa0353e7cf25511cbc10bb6aed7923a3 Mon Sep 17 00:00:00 2001 From: gregs Date: Wed, 11 Dec 2024 19:03:40 -0300 Subject: [PATCH 15/95] polish ui --- src/components/Discover/TrendingTokens.tsx | 38 ++++++++++++---------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/components/Discover/TrendingTokens.tsx b/src/components/Discover/TrendingTokens.tsx index 39152d2b8bf..47c38278372 100644 --- a/src/components/Discover/TrendingTokens.tsx +++ b/src/components/Discover/TrendingTokens.tsx @@ -332,19 +332,10 @@ function TrendingTokenLoadingRow() { function TrendingTokenRow({ item }: { item: TrendingTokensType['trendingTokens']['data'][number] }) { const separatorColor = useForegroundColor('separator'); - const marketCap = useMemo( - () => formatNumber(item.market.market_cap?.value || 0, { useOrderSuffix: true, decimals: 1 }), - [item.market.market_cap?.value] - ); - - const isPositiveChange = useMemo( - () => item.market.price?.change_24h && item.market.price?.change_24h > 0, - [item.market.price?.change_24h] - ); - - const price = useMemo(() => `$${item.market.price?.value ?? '0'}`, [item.market.price?.value]); - - const volume = useMemo(() => formatNumber(item.market.volume_24h || 0, { useOrderSuffix: true, decimals: 1 }), [item.market.volume_24h]); + const isPositiveChange = item.market.price?.change_24h && item.market.price?.change_24h > 0; + const price = `$${item.market.price?.value ?? '0'}`; + const marketCap = `$` + formatNumber(item.market.market_cap?.value || 0, { useOrderSuffix: true, decimals: 1 }); + const volume = `$` + formatNumber(item.market.volume_24h || 0, { useOrderSuffix: true, decimals: 1 }); const handleNavigateToToken = useCallback(() => { analyticsV2.track(analyticsV2.event.viewTrendingToken, { @@ -369,7 +360,7 @@ function TrendingTokenRow({ item }: { item: TrendingTokensType['trendingTokens'] return ( - + - {formatNumber(item.market.price?.change_24h || 0, { decimals: 2 })}% + {formatNumber(item.market.price?.change_24h || 0, { decimals: 2, useOrderSuffix: true })}% @@ -436,7 +427,7 @@ function TrendingTokenRow({ item }: { item: TrendingTokensType['trendingTokens'] 1H - {formatNumber(item.market.price?.change_24h || 0, { decimals: 2 })}% + {formatNumber(item.market.price?.change_24h || 0, { decimals: 2, useOrderSuffix: true })}% @@ -528,6 +519,8 @@ function TimeFilter() { function SortFilter() { const sort = useTrendingTokensStore(state => state.sort); + const iconColor = useForegroundColor('labelQuaternary'); + return ( - + + 􀄬 + + } + /> ); } @@ -560,6 +560,8 @@ function TrendingTokenData() { return ( } data={data?.trendingTokens.data} renderItem={({ item }) => } @@ -595,7 +597,7 @@ export function TrendingTokens() { /> Date: Thu, 12 Dec 2024 18:19:33 -0300 Subject: [PATCH 16/95] better buttons --- src/components/Discover/TrendingTokens.tsx | 18 +++++++++--------- src/components/NetworkSwitcher.tsx | 6 ++++-- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/components/Discover/TrendingTokens.tsx b/src/components/Discover/TrendingTokens.tsx index 47c38278372..d7fd75e640d 100644 --- a/src/components/Discover/TrendingTokens.tsx +++ b/src/components/Discover/TrendingTokens.tsx @@ -2,7 +2,6 @@ import { DropdownMenu } from '@/components/DropdownMenu'; import { globalColors, Text, useBackgroundColor } from '@/design-system'; import { useForegroundColor } from '@/design-system/color/useForegroundColor'; -import { GestureHandlerButton } from '@/__swaps__/screens/Swap/components/GestureHandlerButton'; import { SwapCoinIcon } from '@/__swaps__/screens/Swap/components/SwapCoinIcon'; import { analyticsV2 } from '@/analytics'; import { chainsLabel } from '@/chains'; @@ -20,13 +19,13 @@ import { swapsStore } from '@/state/swaps/swapsStore'; import { categories, sortFilters, timeFilters, useTrendingTokensStore } from '@/state/trendingTokens/trendingTokens'; import { colors } from '@/styles'; import { useTheme } from '@/theme'; -import chroma from 'chroma-js'; import { useCallback, useEffect, useMemo, useState } from 'react'; import React, { FlatList, View } from 'react-native'; import FastImage from 'react-native-fast-image'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; import LinearGradient from 'react-native-linear-gradient'; import Animated, { LinearTransition, runOnJS, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; +import { ButtonPressAnimation } from '../animations'; const t = i18n.l.trending_tokens; @@ -332,10 +331,11 @@ function TrendingTokenLoadingRow() { function TrendingTokenRow({ item }: { item: TrendingTokensType['trendingTokens']['data'][number] }) { const separatorColor = useForegroundColor('separator'); + const priceValue = item.market.price?.value || 0; const isPositiveChange = item.market.price?.change_24h && item.market.price?.change_24h > 0; - const price = `$${item.market.price?.value ?? '0'}`; - const marketCap = `$` + formatNumber(item.market.market_cap?.value || 0, { useOrderSuffix: true, decimals: 1 }); - const volume = `$` + formatNumber(item.market.volume_24h || 0, { useOrderSuffix: true, decimals: 1 }); + const price = priceValue < 0.001 ? `< $0.001` : formatNumber(priceValue, { useOrderSuffix: true, decimals: 4, style: '$' }); + const marketCap = formatNumber(item.market.market_cap?.value || 0, { useOrderSuffix: true, decimals: 1, style: '$' }); + const volume = formatNumber(item.market.volume_24h || 0, { useOrderSuffix: true, decimals: 1, style: '$' }); const handleNavigateToToken = useCallback(() => { analyticsV2.track(analyticsV2.event.viewTrendingToken, { @@ -359,7 +359,7 @@ function TrendingTokenRow({ item }: { item: TrendingTokensType['trendingTokens'] if (!item) return null; return ( - + {item.name} - + {item.symbol} - + {price} @@ -434,7 +434,7 @@ function TrendingTokenRow({ item }: { item: TrendingTokensType['trendingTokens'] - + ); } diff --git a/src/components/NetworkSwitcher.tsx b/src/components/NetworkSwitcher.tsx index a60ea1cb799..1e72dc7cd10 100644 --- a/src/components/NetworkSwitcher.tsx +++ b/src/components/NetworkSwitcher.tsx @@ -218,8 +218,10 @@ const useNetworkOptionStyle = (isSelected: SharedValue, color: string) const scale = useSharedValue(1); useAnimatedReaction( () => isSelected.value, - () => { - scale.value = withSequence(withTiming(0.95, { duration: 50 }), withTiming(1, { duration: 80 })); + current => { + if (current === true) { + scale.value = withSequence(withTiming(0.95, { duration: 50 }), withTiming(1, { duration: 80 })); + } } ); From 201ed07ec1dd5b6acaecf2f9ef513de040dcb5c3 Mon Sep 17 00:00:00 2001 From: gregs Date: Fri, 13 Dec 2024 09:50:24 -0300 Subject: [PATCH 17/95] save --- src/analytics/event.ts | 10 +- src/components/Discover/TrendingTokens.tsx | 199 +++++++++--------- src/components/NetworkSwitcher.tsx | 162 +++++++------- src/graphql/queries/arc.graphql | 36 +++- src/helpers/strings.ts | 61 ++++++ src/hooks/useFarcasterAccountForWallets.ts | 67 ++++++ src/languages/en_US.json | 28 +-- src/navigation/types.ts | 3 +- src/redux/settings.ts | 7 +- src/resources/summary/summary.ts | 24 +++ .../trendingTokens/trendingTokens.ts | 103 ++++++++- src/state/networkSwitcher/networkSwitcher.ts | 1 - src/state/trendingTokens/trendingTokens.ts | 35 ++- src/walletConnect/sheets/AuthRequest.tsx | 2 +- 14 files changed, 518 insertions(+), 220 deletions(-) create mode 100644 src/hooks/useFarcasterAccountForWallets.ts diff --git a/src/analytics/event.ts b/src/analytics/event.ts index 3befb5245f9..ecbedd05886 100644 --- a/src/analytics/event.ts +++ b/src/analytics/event.ts @@ -9,7 +9,7 @@ import { RequestSource } from '@/utils/requestNavigationHandlers'; import { CrosschainQuote, Quote, QuoteError } from '@rainbow-me/swaps'; import { AnyPerformanceLog, Screen } from '../state/performance/operations'; import { FavoritedSite } from '@/state/browser/favoriteDappsStore'; -import { TrendingTokens } from '@/resources/trendingTokens/trendingTokens'; +import { TrendingToken } from '@/resources/trendingTokens/trendingTokens'; /** * All events, used by `analytics.track()` @@ -718,10 +718,10 @@ export type EventProperties = { }; [event.viewTrendingToken]: { - address: TrendingTokens['trendingTokens']['data'][number]['address']; - chainId: TrendingTokens['trendingTokens']['data'][number]['chainId']; - symbol: TrendingTokens['trendingTokens']['data'][number]['symbol']; - name: TrendingTokens['trendingTokens']['data'][number]['name']; + address: TrendingToken['address']; + chainId: TrendingToken['chainId']; + symbol: TrendingToken['symbol']; + name: TrendingToken['name']; highlightedFriends: number; }; diff --git a/src/components/Discover/TrendingTokens.tsx b/src/components/Discover/TrendingTokens.tsx index d7fd75e640d..6d3b2b9faee 100644 --- a/src/components/Discover/TrendingTokens.tsx +++ b/src/components/Discover/TrendingTokens.tsx @@ -9,15 +9,17 @@ import { ChainId } from '@/chains/types'; import { ChainImage } from '@/components/coin-icon/ChainImage'; import { NetworkSelector } from '@/components/NetworkSwitcher'; import Skeleton, { FakeAvatar, FakeText } from '@/components/skeleton/Skeleton'; -import { formatNumber } from '@/helpers/strings'; +import { TrendingCategory, TrendingSort } from '@/graphql/__generated__/arc'; +import { formatCurrency, formatNumber } from '@/helpers/strings'; import * as i18n from '@/languages'; import { Navigation } from '@/navigation'; import Routes from '@/navigation/routesNames'; -import { TrendingTokens as TrendingTokensType, useTrendingTokens } from '@/resources/trendingTokens/trendingTokens'; +import { FarcasterUser, TrendingToken, useTrendingTokens } from '@/resources/trendingTokens/trendingTokens'; import { useNavigationStore } from '@/state/navigation/navigationStore'; import { swapsStore } from '@/state/swaps/swapsStore'; -import { categories, sortFilters, timeFilters, useTrendingTokensStore } from '@/state/trendingTokens/trendingTokens'; +import { sortFilters, timeFilters, useTrendingTokensStore } from '@/state/trendingTokens/trendingTokens'; import { colors } from '@/styles'; +import { darkModeThemeColors } from '@/styles/colors'; import { useTheme } from '@/theme'; import { useCallback, useEffect, useMemo, useState } from 'react'; import React, { FlatList, View } from 'react-native'; @@ -26,6 +28,7 @@ import { Gesture, GestureDetector } from 'react-native-gesture-handler'; import LinearGradient from 'react-native-linear-gradient'; import Animated, { LinearTransition, runOnJS, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; import { ButtonPressAnimation } from '../animations'; +import { useFarcasterAccountForWallets } from '@/hooks/useFarcasterAccountForWallets'; const t = i18n.l.trending_tokens; @@ -94,24 +97,28 @@ function useTrendingTokensData() { sort: state.sort, })); + const walletAddress = useFarcasterAccountForWallets(); + return useTrendingTokens({ chainId, - // category, - // timeframe, - // sort, + category, + timeframe, + sortBy: sort, + sortDirection: undefined, + walletAddress: walletAddress, }); } function ReportAnalytics() { const activeSwipeRoute = useNavigationStore(state => state.activeSwipeRoute); const { category, chainId } = useTrendingTokensStore(state => ({ category: state.category, chainId: state.chainId })); - const { data, isLoading } = useTrendingTokensData(); + const { data: trendingTokens, isLoading } = useTrendingTokensData(); useEffect(() => { if (isLoading || activeSwipeRoute !== Routes.DISCOVER_SCREEN) return; - const isEmpty = (data?.trendingTokens?.data?.length ?? 0) === 0; - const isLimited = !isEmpty && (data?.trendingTokens?.data?.length ?? 0) < 6; + const isEmpty = (trendingTokens?.length ?? 0) === 0; + const isLimited = !isEmpty && (trendingTokens?.length ?? 0) < 6; analyticsV2.track(analyticsV2.event.viewRankedCategory, { category, @@ -119,7 +126,7 @@ function ReportAnalytics() { isLimited, isEmpty, }); - }, [isLoading, activeSwipeRoute, data?.trendingTokens.data.length, category, chainId]); + }, [isLoading, activeSwipeRoute, trendingTokens?.length, category, chainId]); return null; } @@ -132,7 +139,7 @@ function CategoryFilterButton({ label, highlightedBackgroundColor, }: { - category: (typeof categories)[number]; + category: TrendingCategory; icon: string; iconColor: string; highlightedBackgroundColor: string; @@ -196,56 +203,53 @@ function CategoryFilterButton({ ); } -function FriendHolders() { +function FriendPfp({ pfp_url }: { pfp_url: string }) { const backgroundColor = useBackgroundColor('surfacePrimary'); + return ( + + ); +} +function FriendHolders({ friends }: { friends: FarcasterUser[] }) { + if (friends.length === 0) return null; return ( - - + + {friends[1] && } - - mikedemarais{' '} - - and 2 others + + + {friends[0].username}{' '} - + {friends.length > 1 && ( + + {i18n.t(t.and_others, { count: friends.length - 1 })} + + )} + ); } function TrendingTokenLoadingRow() { + const backgroundColor = useBackgroundColor('surfacePrimary'); + const { isDarkMode } = useTheme(); return ( - + - + @@ -257,20 +261,8 @@ function TrendingTokenLoadingRow() { width: 12 + 2, borderRadius: 6, borderWidth: 1, - borderColor: colors.dark, - backgroundColor: colors.alpha(colors.dark, 0.8), - marginVertical: -1, - marginLeft: -6, - }} - /> - @@ -328,60 +320,59 @@ function TrendingTokenLoadingRow() { ); } -function TrendingTokenRow({ item }: { item: TrendingTokensType['trendingTokens']['data'][number] }) { +function TrendingTokenRow({ token }: { token: TrendingToken }) { const separatorColor = useForegroundColor('separator'); - const priceValue = item.market.price?.value || 0; - const isPositiveChange = item.market.price?.change_24h && item.market.price?.change_24h > 0; - const price = priceValue < 0.001 ? `< $0.001` : formatNumber(priceValue, { useOrderSuffix: true, decimals: 4, style: '$' }); - const marketCap = formatNumber(item.market.market_cap?.value || 0, { useOrderSuffix: true, decimals: 1, style: '$' }); - const volume = formatNumber(item.market.volume_24h || 0, { useOrderSuffix: true, decimals: 1, style: '$' }); + const isPositiveChange = token.priceChange > 0; + const price = formatCurrency(token.price); + const marketCap = formatNumber(token.marketCap, { useOrderSuffix: true, decimals: 1, style: '$' }); + const volume = formatNumber(token.volume, { useOrderSuffix: true, decimals: 1, style: '$' }); const handleNavigateToToken = useCallback(() => { analyticsV2.track(analyticsV2.event.viewTrendingToken, { - address: item.address, - chainId: item.chainId, - symbol: item.symbol, - name: item.name, - highlightedFriends: 0, // TODO: Once data is available from backend + address: token.address, + chainId: token.chainId, + symbol: token.symbol, + name: token.name, + highlightedFriends: token.highlightedFriends.length, }); swapsStore.setState({ - lastNavigatedTrendingToken: item.uniqueId, + lastNavigatedTrendingToken: token.uniqueId, }); Navigation.handleAction(Routes.EXPANDED_ASSET_SHEET, { - asset: item, + asset: token, type: 'token', }); - }, [item]); + }, [token]); - if (!item) return null; + if (!token) return null; return ( - + - {item.name} + {token.name} - {item.symbol} + {token.symbol} {price} @@ -419,7 +410,7 @@ function TrendingTokenRow({ item }: { item: TrendingTokensType['trendingTokens'] {isPositiveChange ? '􀄨' : '􀄩'} - {formatNumber(item.market.price?.change_24h || 0, { decimals: 2, useOrderSuffix: true })}% + {formatNumber(token.priceChange, { decimals: 2, useOrderSuffix: true })}% @@ -427,7 +418,7 @@ function TrendingTokenRow({ item }: { item: TrendingTokensType['trendingTokens'] 1H - {formatNumber(item.market.price?.change_24h || 0, { decimals: 2, useOrderSuffix: true })}% + {formatNumber(token.priceChange, { decimals: 2, useOrderSuffix: true })}% @@ -524,19 +515,21 @@ function SortFilter() { return ( ({ - actionTitle: i18n.t(t.filters.sort[sort]), - actionKey: sort, - })), + menuItems: sortFilters + .filter(s => s !== 'RECOMMENDED') + .map(sort => ({ + actionTitle: i18n.t(t.filters.sort[sort]), + actionKey: sort, + })), }} side="bottom" onPressMenuItem={selection => { - if (selection === sort) return useTrendingTokensStore.getState().setSort(undefined); + if (selection === sort) return useTrendingTokensStore.getState().setSort(TrendingSort.Recommended); useTrendingTokensStore.getState().setSort(selection); }} > 􀄬 @@ -548,7 +541,7 @@ function SortFilter() { } function TrendingTokenData() { - const { data, isLoading } = useTrendingTokensData(); + const { data: trendingTokens, isLoading } = useTrendingTokensData(); if (isLoading) return ( @@ -563,8 +556,8 @@ function TrendingTokenData() { style={{ marginHorizontal: -20 }} contentContainerStyle={{ paddingHorizontal: 20 }} ListEmptyComponent={} - data={data?.trendingTokens.data} - renderItem={({ item }) => } + data={trendingTokens} + renderItem={({ item }) => } /> ); } @@ -581,23 +574,23 @@ export function TrendingTokens() { style={{ marginHorizontal: -padding }} > }) { ); } -const CustomizeNetworksBanner = !showCustomizeNetworksBanner +const CustomizeNetworksBanner = !shouldShowCustomizeNetworksBanner(customizeNetworksBannerStore.getState().dismissedAt) ? () => null : function CustomizeNetworksBanner({ editing }: { editing: SharedValue }) { useAnimatedReaction( @@ -176,15 +175,11 @@ const CustomizeNetworksBanner = !showCustomizeNetworksBanner {i18n.t(t.customize_networks_banner.title)} - {/* - is there a way to render a diferent component mid sentence? - like i18n.t(t.customize_networks_banner.description, { Edit: }) - */} - Tap the{' '} + {i18n.t(t.customize_networks_banner.tap_the)}{' '} - Edit + {i18n.t(t.edit)} {' '} - button below to set up + {i18n.t(t.customize_networks_banner.button_to_set_up)} @@ -308,7 +303,7 @@ function AllNetworksSection({ selected: SharedValue; }) { const style = useAnimatedStyle(() => ({ - opacity: editing.value ? withTiming(0, { duration: 50 }) : withDelay(250, withTiming(1, { duration: 250 })), + opacity: editing.value ? withTiming(0, { duration: 200 }) : withDelay(200, withTiming(1, { duration: 200 })), height: withTiming( editing.value ? 0 : ITEM_HEIGHT + 14, // 14 is the gap to the separator { duration: 250 } @@ -356,6 +351,7 @@ const ITEM_HEIGHT = 48; const SEPARATOR_HEIGHT = 68; const enum Section { pinned, + separator, unpinned, } @@ -369,7 +365,7 @@ function Draggable({ }: PropsWithChildren<{ chainId: ChainId; dragging: SharedValue; - networks: SharedValue>; + networks: SharedValue>; sectionsOffsets: SharedValue>; isUnpinnedHidden: SharedValue; }>) { @@ -432,22 +428,35 @@ type DraggingState = { }; function SectionSeparator({ - y, + sectionsOffsets, editing, expanded, networks, - tapExpand, - pressedExpand, }: { - y: SharedValue; + sectionsOffsets: SharedValue>; editing: SharedValue; expanded: SharedValue; - networks: SharedValue>; - tapExpand: TapGesture; - pressedExpand: SharedValue; + networks: SharedValue>; }) { + const pressed = useSharedValue(false); + + const visible = useDerivedValue(() => { + return networks.value[Section.unpinned].length > 0 || editing.value; + }); + + const tapExpand = Gesture.Tap() + .onTouchesDown((e, s) => { + if (editing.value || !visible.value) return s.fail(); + pressed.value = true; + }) + .onEnd(() => { + pressed.value = false; + expanded.value = !expanded.value; + }); + const separatorStyles = useAnimatedStyle(() => ({ - transform: [{ translateY: y.value }, { scale: withTiming(pressedExpand.value ? 0.95 : 1) }], + opacity: visible.value ? 1 : 0, + transform: [{ translateY: sectionsOffsets.value[Section.separator].y }, { scale: withTiming(pressed.value ? 0.95 : 1) }], })); const text = useDerivedValue(() => { @@ -501,6 +510,40 @@ function SectionSeparator({ ); } +function EmptyUnpinnedPlaceholder({ + sectionsOffsets, + networks, + isUnpinnedHidden, +}: { + sectionsOffsets: SharedValue>; + networks: SharedValue>; + isUnpinnedHidden: SharedValue; +}) { + const styles = useAnimatedStyle(() => { + const isVisible = networks.value[Section.unpinned].length === 0 && !isUnpinnedHidden.value; + return { + opacity: isVisible ? withTiming(1, { duration: 800 }) : 0, + transform: [{ translateY: sectionsOffsets.value[Section.unpinned].y }], + }; + }); + const { isDarkMode } = useTheme(); + return ( + + + {i18n.t(t.drag_here_to_unpin)} + + + ); +} + function NetworksGrid({ editing, setSelected, @@ -527,18 +570,28 @@ function NetworksGrid({ const dragging = useSharedValue(null); - const pinnedHeight = useDerivedValue(() => Math.ceil(networks.value[Section.pinned].length / 2) * (ITEM_HEIGHT + GAP) - GAP); - const sectionsOffsets = useDerivedValue(() => ({ - [Section.pinned]: { y: 0 }, - [Section.unpinned]: { y: pinnedHeight.value + SEPARATOR_HEIGHT }, - })); - const containerStyle = useAnimatedStyle(() => { + const sectionsOffsets = useDerivedValue(() => { + const pinnedHeight = Math.ceil(networks.value[Section.pinned].length / 2) * (ITEM_HEIGHT + GAP) - GAP; + return { + [Section.pinned]: { y: 0 }, + [Section.separator]: { y: pinnedHeight }, + [Section.unpinned]: { y: pinnedHeight + SEPARATOR_HEIGHT }, + }; + }); + const containerHeight = useDerivedValue(() => { + const length = networks.value[Section.unpinned].length; + const paddingBottom = 32; const unpinnedHeight = isUnpinnedHidden.value - ? 0 - : Math.ceil(networks.value[Section.unpinned].length / 2) * (ITEM_HEIGHT + GAP) - GAP + 32; - const height = pinnedHeight.value + SEPARATOR_HEIGHT + unpinnedHeight; - return { height: withTiming(height) }; + ? length === 0 + ? -SEPARATOR_HEIGHT + paddingBottom + : 0 + : length === 0 + ? ITEM_HEIGHT + paddingBottom + : Math.ceil((length + 1) / 2) * (ITEM_HEIGHT + GAP) - GAP + paddingBottom; + const height = sectionsOffsets.value[Section.unpinned].y + unpinnedHeight; + return height; }); + const containerStyle = useAnimatedStyle(() => ({ height: containerHeight.value })); const dragNetwork = Gesture.Pan() .maxPointers(1) @@ -565,7 +618,7 @@ function NetworksGrid({ const chainId = dragging.value.chainId; if (!chainId) return; - const section = e.y > sectionsOffsets.value[Section.unpinned].y ? Section.unpinned : Section.pinned; + const section = e.y > sectionsOffsets.value[Section.unpinned].y - SEPARATOR_HEIGHT / 2 ? Section.unpinned : Section.pinned; const sectionArray = networks.value[section]; const currentIndex = sectionArray.indexOf(chainId); @@ -575,7 +628,7 @@ function NetworksGrid({ if (currentIndex === -1) { // Pin/Unpin if (section === Section.unpinned) networks[Section.pinned].splice(currentIndex, 1); - else networks[Section.pinned].splice(newIndex, 0, chainId); + else networks[Section.pinned].push(chainId); networks[Section.unpinned] = SUPPORTED_CHAIN_IDS_ALPHABETICAL.filter(chainId => !networks[Section.pinned].includes(chainId)); } else if (section === Section.pinned && newIndex !== currentIndex) { // Reorder @@ -595,34 +648,9 @@ function NetworksGrid({ dragging.value = null; }); - const pressedExpand = useSharedValue(false); - - // TODO: Need to prevent this from firing the tapNetwork as well - const tapExpand = Gesture.Tap() - .onBegin(e => { - if (editing.value) { - e.state = State.FAILED; - } - pressedExpand.value = true; - }) - .onEnd(() => { - pressedExpand.value = false; - expanded.value = !expanded.value; - }); - const tapNetwork = Gesture.Tap() .onTouchesDown((e, s) => { - if (editing.value) { - s.fail(); - } - - const touches = e.allTouches[0]; - const section = touches.y > sectionsOffsets.value[Section.unpinned].y ? Section.unpinned : Section.pinned; - const index = indexFromPosition(touches.x, touches.y, sectionsOffsets.value[section]); - const chainId = networks.value[section][index]; - if (!chainId) { - s.fail(); - } + if (editing.value) return s.fail(); }) .onEnd(e => { const section = e.y > sectionsOffsets.value[Section.unpinned].y ? Section.unpinned : Section.pinned; @@ -651,22 +679,10 @@ function NetworksGrid({ ))} - - - {/* {initialUnpinned.length === 0 && ( - - - Drag here to unpin networks - - - )} */} + + + + {initialUnpinned.map(chainId => ( str.toString().replace(/[0-9]/g, num => String.fromCharCode(0x2080 + +num)); + +/* + converts 6.9e-7 to 0.00000069 +*/ +const toDecimalString = (num: number): string => { + const [coefficient, exponent] = num.toExponential(20).split('e'); + const exp = parseInt(exponent); + const digits = coefficient.replace('.', '').replace(/0+$/, ''); + + if (exp >= 0) { + const position = exp + 1; + if (position >= digits.length) return digits + '0'.repeat(position - digits.length); + return digits.slice(0, position) + (digits.slice(position) && '.' + digits.slice(position)); + } + return '0.' + '0'.repeat(Math.abs(exp) - 1) + digits; +}; + +/* + formats a numeric string like 00000069 to 0₅69 +*/ +function formatFraction(fraction: string) { + const leadingZeros = fraction.match(/^[0]+/)?.[0].length || 0; + if (+fraction === 0) return '00'; + + const significantDigits = fraction.slice(leadingZeros, leadingZeros + 2); + if (+significantDigits === 0) return '00'; + + if (leadingZeros >= 4) return `0${toSubscript(leadingZeros - 1)}${significantDigits}`; + return significantDigits.replace(/0+$/, ''); +} + +export function formatCurrency( + value: string | number, + { valueIfNaN = '', currency = store.getState().settings.nativeCurrency }: CurrencyFormatterOptions = {} +): string { + const numericString = typeof value === 'number' ? toDecimalString(value) : String(value); + if (isNaN(+numericString)) return valueIfNaN; + + const currencySymbol = supportedNativeCurrencies[currency].symbol; + + const [whole, fraction = '00'] = numericString.split('.'); + + const formattedWhole = formatNumber(whole, { decimals: 0, useOrderSuffix: true }); + const formattedFraction = formatFraction(fraction); + + // if it ends with a non-numeric character, it's in compact notation like '1.2K' + if (isNaN(+formattedWhole[formattedWhole.length - 1])) return `${currencySymbol}${formattedWhole}`; + + return `${currencySymbol}${formattedWhole}.${formattedFraction}`; +} diff --git a/src/hooks/useFarcasterAccountForWallets.ts b/src/hooks/useFarcasterAccountForWallets.ts new file mode 100644 index 00000000000..b55f303aa0a --- /dev/null +++ b/src/hooks/useFarcasterAccountForWallets.ts @@ -0,0 +1,67 @@ +import { queryClient } from '@/react-query'; +import useWallets from './useWallets'; +import { useEffect, useMemo, useState } from 'react'; +import { addysSummaryQueryKey, useAddysSummary } from '@/resources/summary/summary'; +import { Address } from 'viem'; +import useAccountSettings from './useAccountSettings'; +import store from '@/redux/store'; +import { isEmpty } from 'lodash'; +import walletTypes from '@/helpers/walletTypes'; +import { isLowerCaseMatch } from '@/utils'; +import { AllRainbowWallets } from '@/model/wallet'; + +type SummaryData = ReturnType['data']; + +const getWalletForAddress = (wallets: AllRainbowWallets | null, address: string) => { + return Object.values(wallets || {}).find(wallet => wallet.addresses.some(addr => isLowerCaseMatch(addr.address, address))); +}; + +export const useFarcasterAccountForWallets = () => { + const [farcasterWalletAddress, setFarcasterWalletAddress] = useState
(); + const { accountAddress } = useAccountSettings(); + const { wallets } = useWallets(); + + const allAddresses = useMemo( + () => Object.values(wallets || {}).flatMap(wallet => (wallet.addresses || []).map(account => account.address as Address)), + [wallets] + ); + + useEffect(() => { + const summaryData = queryClient.getQueryData( + addysSummaryQueryKey({ + addresses: allAddresses, + currency: store.getState().settings.nativeCurrency, + }) + ); + const addresses = summaryData?.data.addresses; + + if (!addresses || isEmpty(addresses) || isEmpty(wallets)) { + setFarcasterWalletAddress(undefined); + return; + } + + const selectedAddressFid = addresses[accountAddress]?.meta?.farcaster?.fid; + if (selectedAddressFid && getWalletForAddress(wallets, accountAddress)?.type !== walletTypes.readOnly) { + setFarcasterWalletAddress(accountAddress); + return; + } + + const farcasterWalletAddress = Object.keys(addresses).find(addr => { + const address = addr as Address; + const faracsterId = summaryData?.data.addresses[address]?.meta?.farcaster?.fid; + if (faracsterId && getWalletForAddress(wallets, address)?.type !== walletTypes.readOnly) { + return address; + } + }) as Address | undefined; + + if (farcasterWalletAddress) { + setFarcasterWalletAddress(farcasterWalletAddress); + return; + } + setFarcasterWalletAddress(undefined); + }, [wallets, allAddresses, accountAddress]); + + console.log('farcasterWalletAddress', farcasterWalletAddress); + + return farcasterWalletAddress; +}; diff --git a/src/languages/en_US.json b/src/languages/en_US.json index e43e31f6062..ba65d43765b 100644 --- a/src/languages/en_US.json +++ b/src/languages/en_US.json @@ -2994,31 +2994,35 @@ "title": "No results", "body": "Try browsing a larger timeframe or a different network or category." }, + "and_others": "and %{count} others", "filters": { "categories": { - "trending": "Trending", - "new": "New", - "farcaster": "Farcaster" + "TRENDING": "Trending", + "NEW": "New", + "FARCASTER": "Farcaster" }, "sort": { - "sort": "Sort", - "volume": "Volume", - "market_cap": "Market Cap", - "top_gainers": "Top Gainers", - "top_losers": "Top Losers" + "RECOMMENDED": "Sort", + "VOLUME": "Volume", + "MARKET_CAP": "Market Cap", + "TOP_GAINERS": "Top Gainers", + "TOP_LOSERS": "Top Losers" }, "time": { - "day": "24h", - "week": "1 Week", - "month": "1 Month" + "H12": "12h", + "H24": "24h", + "D7": "1 Week", + "D3": "1 Month" } } }, "network_switcher": { "customize_networks_banner": { "title": "Customize Networks", - "description": "Tap the edit button below to set up" + "tap_the": "Tap the", + "button_to_set_up": "button below to set up" }, + "drag_here_to_unpin": "Drag here to unpin networks", "edit": "Edit", "networks": "Networks", "drag_to_rearrange": "Drag to rearrange", diff --git a/src/navigation/types.ts b/src/navigation/types.ts index cc2c8842ab5..409bc90740e 100644 --- a/src/navigation/types.ts +++ b/src/navigation/types.ts @@ -10,6 +10,7 @@ import { Claimable } from '@/resources/addys/claimables/types'; import { WalletconnectApprovalSheetRouteParams, WalletconnectResultType } from '@/walletConnect/types'; import { WalletConnectApprovalSheetType } from '@/helpers/walletConnectApprovalSheetTypes'; import { RainbowPosition } from '@/resources/defi/types'; +import { Address } from 'viem'; export type PartialNavigatorConfigOptions = Pick['Screen']>[0]>, 'options'>; @@ -31,7 +32,7 @@ export type RootStackParamList = { [Routes.CHANGE_WALLET_SHEET]: { watchOnly: boolean; currentAccountAddress: string; - onChangeWallet: (address: string) => void; + onChangeWallet: (address: Address) => void; }; [Routes.SPEED_UP_AND_CANCEL_BOTTOM_SHEET]: { accentColor?: string; diff --git a/src/redux/settings.ts b/src/redux/settings.ts index e7166c81985..7671a740d31 100644 --- a/src/redux/settings.ts +++ b/src/redux/settings.ts @@ -24,6 +24,7 @@ import { getProvider } from '@/handlers/web3'; import { AppState } from '@/redux/store'; import { logger, RainbowError } from '@/logger'; import { Network, ChainId } from '@/chains/types'; +import { Address } from 'viem'; // -- Constants ------------------------------------------------------------- // const SETTINGS_UPDATE_SETTINGS_ADDRESS = 'settings/SETTINGS_UPDATE_SETTINGS_ADDRESS'; @@ -41,7 +42,7 @@ const SETTINGS_UPDATE_ACCOUNT_SETTINGS_SUCCESS = 'settings/SETTINGS_UPDATE_ACCOU */ interface SettingsState { appIcon: string; - accountAddress: string; + accountAddress: Address; chainId: number; language: Language; nativeCurrency: NativeCurrencyKey; @@ -205,7 +206,7 @@ export const settingsChangeAppIcon = (appIcon: string) => (dispatch: Dispatch async (dispatch: Dispatch) => { dispatch({ - payload: accountAddress, + payload: accountAddress as Address, type: SETTINGS_UPDATE_SETTINGS_ADDRESS, }); }; @@ -254,7 +255,7 @@ export const settingsChangeNativeCurrency = // -- Reducer --------------------------------------------------------------- // export const INITIAL_STATE: SettingsState = { - accountAddress: '', + accountAddress: '' as Address, appIcon: 'og', chainId: 1, language: Language.EN_US, diff --git a/src/resources/summary/summary.ts b/src/resources/summary/summary.ts index 6f902a391f0..cd0ef1d8542 100644 --- a/src/resources/summary/summary.ts +++ b/src/resources/summary/summary.ts @@ -16,6 +16,30 @@ interface AddysSummary { data: { addresses: { [key: Address]: { + meta: { + farcaster?: { + object: string; + fid: number; + username: string; + display_name: string; + pfp_url: string; + custody_address: string; + profile: { + Bio: { + text: string; + }; + }; + follower_count: number; + following_count: number; + verifications: string[]; + verified_addresses: { + eth_addresses: string[]; + sol_addresses: string[]; + }; + verified_accounts: string[]; + power_badge: boolean; + }; + }; summary: { native_balance_by_symbol: { [key in 'ETH' | 'MATIC' | 'BNB' | 'AVAX']: { diff --git a/src/resources/trendingTokens/trendingTokens.ts b/src/resources/trendingTokens/trendingTokens.ts index 06e6f623318..b3f1f80f63b 100644 --- a/src/resources/trendingTokens/trendingTokens.ts +++ b/src/resources/trendingTokens/trendingTokens.ts @@ -1,27 +1,114 @@ import { QueryConfigWithSelect, createQueryKey } from '@/react-query'; import { useQuery } from '@tanstack/react-query'; import { arcClient } from '@/graphql'; +import { ChainId } from '@/chains/types'; +import { TrendingCategory, TrendingSort, TrendingTimeframe } from '@/state/trendingTokens/trendingTokens'; +import { Address } from 'viem'; +import { NativeCurrencyKey } from '@/entities'; +import store from '@/redux/store'; +import { SortDirection } from '@/graphql/__generated__/arc'; +import { UniqueId } from '@/__swaps__/types/assets'; -export type TrendingTokensVariables = Parameters['0']; -export type TrendingTokens = Awaited>; +export type FarcasterUser = { + username: string; + pfp_url: string; +}; +export type TrendingToken = { + uniqueId: UniqueId; + chainId: ChainId; + address: string; + name: string; + symbol: string; + decimals: number; + price: number; + priceChange: number; + marketCap: number; + volume: number; + highlightedFriends: FarcasterUser[]; + colors: { + primary: string; + }; + icon_url: string; +}; // /////////////////////////////////////////////// // Query Key -export const trendingTokensQueryKey = (props: TrendingTokensVariables) => createQueryKey('trending-tokens', props, { persisterVersion: 0 }); +export const trendingTokensQueryKey = (props: FetchTrendingTokensArgs) => createQueryKey('trending-tokens', props, { persisterVersion: 2 }); export type TrendingTokensQueryKey = ReturnType; +type FetchTrendingTokensArgs = { + chainId?: ChainId; + category: TrendingCategory; + sortBy: TrendingSort; + sortDirection: SortDirection | undefined; + timeframe: TrendingTimeframe; + walletAddress: Address | undefined; + currency?: NativeCurrencyKey; +}; + +async function fetchTrendingTokens({ + queryKey: [{ currency = store.getState().settings.nativeCurrency, category, sortBy, sortDirection, timeframe, walletAddress, chainId }], +}: { + queryKey: TrendingTokensQueryKey; +}) { + console.log('fetching trending tokens'); + const response = await arcClient.trendingTokens({ + category, + sortBy, + sortDirection, + timeframe, + walletAddress, + chainId, + currency: currency.toLowerCase(), + }); + const trendingTokens: TrendingToken[] = []; + + for (const token of response.trendingTokens.data) { + const { uniqueId, address, name, symbol, chainId, decimals, trending, market, icon_url, colors } = token; + const { bought_stats, sold_stats } = trending.swap_data; + const highlightedFriends = [...(bought_stats.farcaster_users || []), ...(sold_stats.farcaster_users || [])].reduce( + (friends, friend) => { + const { username, pfp_url } = friend; + if (username && pfp_url) friends.push({ username, pfp_url }); + return friends; + }, + [] as FarcasterUser[] + ); + + trendingTokens.push({ + uniqueId, + chainId: chainId as ChainId, + address, + name, + symbol, + decimals, + price: market.price?.value || 0, + priceChange: market.price?.change_24h || 0, + marketCap: market.market_cap?.value || 0, + volume: market.volume_24h || 0, + highlightedFriends, + icon_url, + colors: { + primary: colors.primary, + }, + }); + } + + return trendingTokens; +} + // /////////////////////////////////////////////// // Query Hook -export function useTrendingTokens( - props: TrendingTokensVariables, - config: QueryConfigWithSelect = {} +export function useTrendingTokens( + args: FetchTrendingTokensArgs, + config: QueryConfigWithSelect = {} ) { - return useQuery(trendingTokensQueryKey(props), () => arcClient.trendingTokens(props), { + return useQuery(trendingTokensQueryKey(args), fetchTrendingTokens, { ...config, staleTime: 60_000, // 1 minute cacheTime: 60_000 * 30, // 30 minutes - keepPreviousData: true, + // keepPreviousData: true, }); } diff --git a/src/state/networkSwitcher/networkSwitcher.ts b/src/state/networkSwitcher/networkSwitcher.ts index 1ea781b242d..12ebbd7d3f3 100644 --- a/src/state/networkSwitcher/networkSwitcher.ts +++ b/src/state/networkSwitcher/networkSwitcher.ts @@ -45,4 +45,3 @@ export const shouldShowCustomizeNetworksBanner = (dismissedAt: number) => Date.n export const dismissCustomizeNetworksBanner = () => { customizeNetworksBannerStore.setState({ dismissedAt: Date.now() }); }; -export const showCustomizeNetworksBanner = shouldShowCustomizeNetworksBanner(customizeNetworksBannerStore.getState().dismissedAt); diff --git a/src/state/trendingTokens/trendingTokens.ts b/src/state/trendingTokens/trendingTokens.ts index e99f10b6072..939e60c3e59 100644 --- a/src/state/trendingTokens/trendingTokens.ts +++ b/src/state/trendingTokens/trendingTokens.ts @@ -1,17 +1,30 @@ +import { analyticsV2 } from '@/analytics'; import { ChainId } from '@/chains/types'; import { createRainbowStore } from '../internal/createRainbowStore'; -import { makeMutable, SharedValue } from 'react-native-reanimated'; -import { analyticsV2 } from '@/analytics'; +import { + TrendingCategory as ArcTrendingCategory, + Timeframe as ArcTimeframe, + TrendingSort as ArcTrendingSort, +} from '@/graphql/__generated__/arc'; -export const categories = ['trending', 'new', 'farcaster'] as const; -export const sortFilters = ['volume', 'market_cap', 'top_gainers', 'top_losers'] as const; -export const timeFilters = ['day', 'week', 'month'] as const; +export const categories = [ArcTrendingCategory.Trending, ArcTrendingCategory.New, ArcTrendingCategory.Farcaster] as const; +export type TrendingCategory = (typeof categories)[number]; +export const sortFilters = [ + ArcTrendingSort.Recommended, + ArcTrendingSort.Volume, + ArcTrendingSort.MarketCap, + ArcTrendingSort.TopGainers, + ArcTrendingSort.TopLosers, +] as const; +export type TrendingSort = (typeof sortFilters)[number]; +export const timeFilters = [ArcTimeframe.H24, ArcTimeframe.D7, ArcTimeframe.D3] as const; +export type TrendingTimeframe = (typeof timeFilters)[number]; type TrendingTokensState = { - category: 'trending' | 'new' | 'farcaster'; + category: (typeof categories)[number]; chainId: undefined | ChainId; timeframe: (typeof timeFilters)[number]; - sort: (typeof sortFilters)[number] | undefined; + sort: (typeof sortFilters)[number]; setCategory: (category: TrendingTokensState['category']) => void; setChainId: (chainId: TrendingTokensState['chainId']) => void; @@ -21,10 +34,10 @@ type TrendingTokensState = { export const useTrendingTokensStore = createRainbowStore( set => ({ - category: 'trending', + category: ArcTrendingCategory.Trending, chainId: undefined, - timeframe: 'day', - sort: 'volume', + timeframe: ArcTimeframe.H24, + sort: ArcTrendingSort.Recommended, setCategory: category => set({ category }), setChainId: chainId => { analyticsV2.track(analyticsV2.event.changeNetworkFilter, { chainId }); @@ -41,6 +54,6 @@ export const useTrendingTokensStore = createRainbowStore( }), { storageKey: 'trending-tokens', - version: 0, + version: 1, } ); diff --git a/src/walletConnect/sheets/AuthRequest.tsx b/src/walletConnect/sheets/AuthRequest.tsx index 338acad39b4..724cae00de2 100644 --- a/src/walletConnect/sheets/AuthRequest.tsx +++ b/src/walletConnect/sheets/AuthRequest.tsx @@ -149,7 +149,7 @@ export function AuthRequest({ navigate(Routes.CHANGE_WALLET_SHEET, { watchOnly: true, currentAccountAddress: address, - onChangeWallet(address: string) { + onChangeWallet(address) { setAddress(address); goBack(); }, From e97f4147cd7f3a12d4bdbe4ca37c288153b5db65 Mon Sep 17 00:00:00 2001 From: gregs Date: Fri, 13 Dec 2024 10:00:24 -0300 Subject: [PATCH 18/95] align friends pfps --- src/components/Discover/TrendingTokens.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Discover/TrendingTokens.tsx b/src/components/Discover/TrendingTokens.tsx index 6d3b2b9faee..c6cd0eee127 100644 --- a/src/components/Discover/TrendingTokens.tsx +++ b/src/components/Discover/TrendingTokens.tsx @@ -224,7 +224,7 @@ function FriendHolders({ friends }: { friends: FarcasterUser[] }) { if (friends.length === 0) return null; return ( - + {friends[1] && } From eaec25d13c63eafd000a43e2f5477b7a0ce73ddf Mon Sep 17 00:00:00 2001 From: gregs Date: Fri, 13 Dec 2024 10:33:21 -0300 Subject: [PATCH 19/95] fix price change timeframes --- src/components/Discover/TrendingTokens.tsx | 13 ++++++------- src/graphql/queries/arc.graphql | 6 ++++++ src/resources/trendingTokens/trendingTokens.ts | 14 ++++++++++++-- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/components/Discover/TrendingTokens.tsx b/src/components/Discover/TrendingTokens.tsx index c6cd0eee127..84f15a40bcf 100644 --- a/src/components/Discover/TrendingTokens.tsx +++ b/src/components/Discover/TrendingTokens.tsx @@ -323,7 +323,6 @@ function TrendingTokenLoadingRow() { function TrendingTokenRow({ token }: { token: TrendingToken }) { const separatorColor = useForegroundColor('separator'); - const isPositiveChange = token.priceChange > 0; const price = formatCurrency(token.price); const marketCap = formatNumber(token.marketCap, { useOrderSuffix: true, decimals: 1, style: '$' }); const volume = formatNumber(token.volume, { useOrderSuffix: true, decimals: 1, style: '$' }); @@ -406,19 +405,19 @@ function TrendingTokenRow({ token }: { token: TrendingToken }) { - - {isPositiveChange ? '􀄨' : '􀄩'} + 0 ? 'green' : 'red'} size="11pt" weight="bold"> + {token.priceChange.timeframe > 0 ? '􀄨' : '􀄩'} - - {formatNumber(token.priceChange, { decimals: 2, useOrderSuffix: true })}% + 0 ? 'green' : 'red'} size="15pt" weight="bold"> + {formatNumber(token.priceChange.timeframe, { decimals: 2, useOrderSuffix: true })}% 1H - - {formatNumber(token.priceChange, { decimals: 2, useOrderSuffix: true })}% + 0 ? 'green' : 'red'} size="11pt" weight="bold"> + {formatNumber(token.priceChange.hr, { decimals: 2, useOrderSuffix: true })}% diff --git a/src/graphql/queries/arc.graphql b/src/graphql/queries/arc.graphql index 4c1e4a18285..23e87ec820f 100644 --- a/src/graphql/queries/arc.graphql +++ b/src/graphql/queries/arc.graphql @@ -595,6 +595,12 @@ query trendingTokens( } } } + pool_data { + h24_price_change + h6_price_change + h1_price_change + m5_price_change + } } bridging { bridgeable diff --git a/src/resources/trendingTokens/trendingTokens.ts b/src/resources/trendingTokens/trendingTokens.ts index b3f1f80f63b..dbc2397b5a9 100644 --- a/src/resources/trendingTokens/trendingTokens.ts +++ b/src/resources/trendingTokens/trendingTokens.ts @@ -21,7 +21,10 @@ export type TrendingToken = { symbol: string; decimals: number; price: number; - priceChange: number; + priceChange: { + hr: number; + timeframe: number; + }; marketCap: number; volume: number; highlightedFriends: FarcasterUser[]; @@ -84,7 +87,14 @@ async function fetchTrendingTokens({ symbol, decimals, price: market.price?.value || 0, - priceChange: market.price?.change_24h || 0, + priceChange: { + hr: trending.pool_data.h1_price_change || 0, + timeframe: { + H24: trending.pool_data.h24_price_change || 0, + D7: trending.pool_data.h24_price_change || 0, // TODO: WRONG TIMEFRAME DATA + D3: trending.pool_data.h24_price_change || 0, // TODO: WRONG TIMEFRAME DATA + }[timeframe], + }, marketCap: market.market_cap?.value || 0, volume: market.volume_24h || 0, highlightedFriends, From d0f6fcc4056a0278d27e07ecb6f120f10b6266a9 Mon Sep 17 00:00:00 2001 From: gregs Date: Fri, 13 Dec 2024 14:24:58 -0300 Subject: [PATCH 20/95] fix images --- src/components/Discover/TrendingTokens.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Discover/TrendingTokens.tsx b/src/components/Discover/TrendingTokens.tsx index 84f15a40bcf..155331fc5a3 100644 --- a/src/components/Discover/TrendingTokens.tsx +++ b/src/components/Discover/TrendingTokens.tsx @@ -23,12 +23,12 @@ import { darkModeThemeColors } from '@/styles/colors'; import { useTheme } from '@/theme'; import { useCallback, useEffect, useMemo, useState } from 'react'; import React, { FlatList, View } from 'react-native'; -import FastImage from 'react-native-fast-image'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; import LinearGradient from 'react-native-linear-gradient'; import Animated, { LinearTransition, runOnJS, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; import { ButtonPressAnimation } from '../animations'; import { useFarcasterAccountForWallets } from '@/hooks/useFarcasterAccountForWallets'; +import { ImgixImage } from '../images'; const t = i18n.l.trending_tokens; @@ -206,7 +206,7 @@ function CategoryFilterButton({ function FriendPfp({ pfp_url }: { pfp_url: string }) { const backgroundColor = useBackgroundColor('surfacePrimary'); return ( - Date: Fri, 13 Dec 2024 18:32:17 -0300 Subject: [PATCH 21/95] better currency formatting --- src/components/Discover/TrendingTokens.tsx | 19 +++++++++++++------ src/helpers/strings.ts | 4 ++-- .../trendingTokens/trendingTokens.ts | 9 ++------- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/components/Discover/TrendingTokens.tsx b/src/components/Discover/TrendingTokens.tsx index 155331fc5a3..a8bff3c775c 100644 --- a/src/components/Discover/TrendingTokens.tsx +++ b/src/components/Discover/TrendingTokens.tsx @@ -320,6 +320,11 @@ function TrendingTokenLoadingRow() { ); } +function getPriceChangeColor(priceChange: number) { + if (priceChange === 0) return 'labelTertiary'; + return priceChange > 0 ? 'green' : 'red'; +} + function TrendingTokenRow({ token }: { token: TrendingToken }) { const separatorColor = useForegroundColor('separator'); @@ -405,18 +410,20 @@ function TrendingTokenRow({ token }: { token: TrendingToken }) { - 0 ? 'green' : 'red'} size="11pt" weight="bold"> - {token.priceChange.timeframe > 0 ? '􀄨' : '􀄩'} - - 0 ? 'green' : 'red'} size="15pt" weight="bold"> - {formatNumber(token.priceChange.timeframe, { decimals: 2, useOrderSuffix: true })}% + {token.priceChange.day !== 0 && ( + + {token.priceChange.day > 0 ? '􀄨' : '􀄩'} + + )} + + {formatNumber(token.priceChange.day, { decimals: 2, useOrderSuffix: true })}% 1H - 0 ? 'green' : 'red'} size="11pt" weight="bold"> + {formatNumber(token.priceChange.hr, { decimals: 2, useOrderSuffix: true })}% diff --git a/src/helpers/strings.ts b/src/helpers/strings.ts index 2e13245a9f5..a1f3428feb1 100644 --- a/src/helpers/strings.ts +++ b/src/helpers/strings.ts @@ -104,8 +104,8 @@ function formatFraction(fraction: string) { const significantDigits = fraction.slice(leadingZeros, leadingZeros + 2); if (+significantDigits === 0) return '00'; - if (leadingZeros >= 4) return `0${toSubscript(leadingZeros - 1)}${significantDigits}`; - return significantDigits.replace(/0+$/, ''); + if (leadingZeros >= 4) return `0${toSubscript(leadingZeros)}${significantDigits}`; + return `${'0'.repeat(leadingZeros)}${significantDigits}`; } export function formatCurrency( diff --git a/src/resources/trendingTokens/trendingTokens.ts b/src/resources/trendingTokens/trendingTokens.ts index dbc2397b5a9..f9c475a6c4f 100644 --- a/src/resources/trendingTokens/trendingTokens.ts +++ b/src/resources/trendingTokens/trendingTokens.ts @@ -23,7 +23,7 @@ export type TrendingToken = { price: number; priceChange: { hr: number; - timeframe: number; + day: number; }; marketCap: number; volume: number; @@ -89,11 +89,7 @@ async function fetchTrendingTokens({ price: market.price?.value || 0, priceChange: { hr: trending.pool_data.h1_price_change || 0, - timeframe: { - H24: trending.pool_data.h24_price_change || 0, - D7: trending.pool_data.h24_price_change || 0, // TODO: WRONG TIMEFRAME DATA - D3: trending.pool_data.h24_price_change || 0, // TODO: WRONG TIMEFRAME DATA - }[timeframe], + day: trending.pool_data.h24_price_change || 0, }, marketCap: market.market_cap?.value || 0, volume: market.volume_24h || 0, @@ -119,6 +115,5 @@ export function useTrendingTokens( ...config, staleTime: 60_000, // 1 minute cacheTime: 60_000 * 30, // 30 minutes - // keepPreviousData: true, }); } From e2e2bec2211430aeb9c0982a09cb7b15e6a681cf Mon Sep 17 00:00:00 2001 From: gregs Date: Fri, 13 Dec 2024 18:39:10 -0300 Subject: [PATCH 22/95] fix comment --- src/helpers/strings.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/strings.ts b/src/helpers/strings.ts index a1f3428feb1..611376cd3d8 100644 --- a/src/helpers/strings.ts +++ b/src/helpers/strings.ts @@ -95,7 +95,7 @@ const toDecimalString = (num: number): string => { }; /* - formats a numeric string like 00000069 to 0₅69 + formats a numeric string like 0000069 to 0₅69 */ function formatFraction(fraction: string) { const leadingZeros = fraction.match(/^[0]+/)?.[0].length || 0; From d4ae43b21dcf0fe70112071b6d90e2f1b4589230 Mon Sep 17 00:00:00 2001 From: gregs Date: Fri, 13 Dec 2024 20:32:55 -0300 Subject: [PATCH 23/95] fix network button selecting the wrong one --- src/components/NetworkSwitcher.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/NetworkSwitcher.tsx b/src/components/NetworkSwitcher.tsx index a23cfe76fe6..f9611b63a9e 100644 --- a/src/components/NetworkSwitcher.tsx +++ b/src/components/NetworkSwitcher.tsx @@ -329,7 +329,7 @@ function NetworkOption({ chainId, selected }: { chainId: ChainId; selected: Shar { 'worklet'; const yoffsets = y > offset.y ? offset.y : 0; const column = x > ITEM_WIDTH + GAP / 2 ? 1 : 0; - const row = Math.floor((y - yoffsets) / (ITEM_HEIGHT + GAP / 2)); + const row = Math.floor((y - yoffsets) / (ITEM_HEIGHT + GAP)); const index = row * 2 + column; return index < 0 ? 0 : index; // row can be negative if the dragged item is above the first row }; @@ -604,8 +604,10 @@ function NetworksGrid({ const section = touch.y > sectionsOffsets.value[Section.unpinned].y ? Section.unpinned : Section.pinned; const sectionOffset = sectionsOffsets.value[section]; const index = indexFromPosition(touch.x, touch.y, sectionOffset); - const chainId = networks.value[section][index]; - if (!chainId) { + const sectionNetworks = networks.value[section]; + const chainId = sectionNetworks[index]; + + if (!chainId || (section === Section.pinned && sectionNetworks.length === 1)) { s.fail(); return; } From b3d44b3f72735c22f1a280f4738cbdbc76d11b25 Mon Sep 17 00:00:00 2001 From: Christopher Howard Date: Mon, 16 Dec 2024 17:46:32 -0500 Subject: [PATCH 24/95] fix: tt sort (#6337) --- src/components/Discover/TrendingTokens.tsx | 4 ++-- src/hooks/useFarcasterAccountForWallets.ts | 2 -- src/resources/trendingTokens/trendingTokens.ts | 1 - 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/components/Discover/TrendingTokens.tsx b/src/components/Discover/TrendingTokens.tsx index a8bff3c775c..d8564c5c3cb 100644 --- a/src/components/Discover/TrendingTokens.tsx +++ b/src/components/Discover/TrendingTokens.tsx @@ -9,7 +9,7 @@ import { ChainId } from '@/chains/types'; import { ChainImage } from '@/components/coin-icon/ChainImage'; import { NetworkSelector } from '@/components/NetworkSwitcher'; import Skeleton, { FakeAvatar, FakeText } from '@/components/skeleton/Skeleton'; -import { TrendingCategory, TrendingSort } from '@/graphql/__generated__/arc'; +import { SortDirection, TrendingCategory, TrendingSort } from '@/graphql/__generated__/arc'; import { formatCurrency, formatNumber } from '@/helpers/strings'; import * as i18n from '@/languages'; import { Navigation } from '@/navigation'; @@ -104,7 +104,7 @@ function useTrendingTokensData() { category, timeframe, sortBy: sort, - sortDirection: undefined, + sortDirection: SortDirection.Desc, walletAddress: walletAddress, }); } diff --git a/src/hooks/useFarcasterAccountForWallets.ts b/src/hooks/useFarcasterAccountForWallets.ts index b55f303aa0a..80873b867e3 100644 --- a/src/hooks/useFarcasterAccountForWallets.ts +++ b/src/hooks/useFarcasterAccountForWallets.ts @@ -61,7 +61,5 @@ export const useFarcasterAccountForWallets = () => { setFarcasterWalletAddress(undefined); }, [wallets, allAddresses, accountAddress]); - console.log('farcasterWalletAddress', farcasterWalletAddress); - return farcasterWalletAddress; }; diff --git a/src/resources/trendingTokens/trendingTokens.ts b/src/resources/trendingTokens/trendingTokens.ts index f9c475a6c4f..a816a799d5b 100644 --- a/src/resources/trendingTokens/trendingTokens.ts +++ b/src/resources/trendingTokens/trendingTokens.ts @@ -55,7 +55,6 @@ async function fetchTrendingTokens({ }: { queryKey: TrendingTokensQueryKey; }) { - console.log('fetching trending tokens'); const response = await arcClient.trendingTokens({ category, sortBy, From d4dcf1b8af96dc88594814a43ff104ec66c16361 Mon Sep 17 00:00:00 2001 From: Bruno Barbieri <1247834+brunobar79@users.noreply.github.com> Date: Mon, 16 Dec 2024 18:27:57 -0500 Subject: [PATCH 25/95] TT fixes (#6338) * remove arrows for price change * fix friend holders display * fix dupes * align and make friends looks better * remove spread * ops --------- Co-authored-by: gregs --- src/components/Discover/TrendingTokens.tsx | 27 ++++++++++++------- src/languages/en_US.json | 6 ++++- .../trendingTokens/trendingTokens.ts | 15 +++++------ 3 files changed, 28 insertions(+), 20 deletions(-) diff --git a/src/components/Discover/TrendingTokens.tsx b/src/components/Discover/TrendingTokens.tsx index d8564c5c3cb..549e22dc9b5 100644 --- a/src/components/Discover/TrendingTokens.tsx +++ b/src/components/Discover/TrendingTokens.tsx @@ -222,6 +222,9 @@ function FriendPfp({ pfp_url }: { pfp_url: string }) { } function FriendHolders({ friends }: { friends: FarcasterUser[] }) { if (friends.length === 0) return null; + const howManyOthers = Math.max(1, friends.length - 2); + const separator = howManyOthers === 1 && friends.length === 2 ? ` ${i18n.t(t.and)} ` : ', '; + return ( @@ -230,12 +233,21 @@ function FriendHolders({ friends }: { friends: FarcasterUser[] }) { - - {friends[0].username}{' '} + + {friends[0].username} + {friends[1] && ( + <> + + {separator} + + {friends[1].username} + + )} - {friends.length > 1 && ( + {friends.length > 2 && ( - {i18n.t(t.and_others, { count: friends.length - 1 })} + {' '} + {i18n.t('trending_tokens.and_others', { count: howManyOthers })} )} @@ -409,12 +421,7 @@ function TrendingTokenRow({ token }: { token: TrendingToken }) { - - {token.priceChange.day !== 0 && ( - - {token.priceChange.day > 0 ? '􀄨' : '􀄩'} - - )} + {formatNumber(token.priceChange.day, { decimals: 2, useOrderSuffix: true })}% diff --git a/src/languages/en_US.json b/src/languages/en_US.json index ba65d43765b..31c2b3ccbf2 100644 --- a/src/languages/en_US.json +++ b/src/languages/en_US.json @@ -2994,7 +2994,11 @@ "title": "No results", "body": "Try browsing a larger timeframe or a different network or category." }, - "and_others": "and %{count} others", + "and": "and", + "and_others": { + "one": "and %{count} other", + "other": "and %{count} others" + }, "filters": { "categories": { "TRENDING": "Trending", diff --git a/src/resources/trendingTokens/trendingTokens.ts b/src/resources/trendingTokens/trendingTokens.ts index a816a799d5b..4ea14c95c93 100644 --- a/src/resources/trendingTokens/trendingTokens.ts +++ b/src/resources/trendingTokens/trendingTokens.ts @@ -68,15 +68,12 @@ async function fetchTrendingTokens({ for (const token of response.trendingTokens.data) { const { uniqueId, address, name, symbol, chainId, decimals, trending, market, icon_url, colors } = token; - const { bought_stats, sold_stats } = trending.swap_data; - const highlightedFriends = [...(bought_stats.farcaster_users || []), ...(sold_stats.farcaster_users || [])].reduce( - (friends, friend) => { - const { username, pfp_url } = friend; - if (username && pfp_url) friends.push({ username, pfp_url }); - return friends; - }, - [] as FarcasterUser[] - ); + const { bought_stats } = trending.swap_data; + const highlightedFriends = (bought_stats.farcaster_users || []).reduce((friends, friend) => { + const { username, pfp_url } = friend; + if (username && pfp_url) friends.push({ username, pfp_url }); + return friends; + }, [] as FarcasterUser[]); trendingTokens.push({ uniqueId, From 8a1f3b7371b1bff1492a237af2f441503cb82c65 Mon Sep 17 00:00:00 2001 From: Christopher Howard Date: Tue, 17 Dec 2024 15:36:06 -0500 Subject: [PATCH 26/95] fix: tt -> swaps nav (#6343) --- src/components/sheet/sheet-action-buttons/SwapActionButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/sheet/sheet-action-buttons/SwapActionButton.tsx b/src/components/sheet/sheet-action-buttons/SwapActionButton.tsx index 8e38433a4ab..046467dd8a3 100644 --- a/src/components/sheet/sheet-action-buttons/SwapActionButton.tsx +++ b/src/components/sheet/sheet-action-buttons/SwapActionButton.tsx @@ -29,7 +29,7 @@ function SwapActionButton({ asset, color: givenColor, inputType, label, weight = const color = givenColor || colors.swapPurple; const goToSwap = useCallback(async () => { - const chainId = chainsIdByName[asset.network]; + const chainId = asset.chainId || chainsIdByName[asset.network]; const uniqueId = `${asset.address}_${chainId}`; const userAsset = userAssetsStore.getState().userAssets.get(uniqueId); From 5bb2a0f9cb58b955300111424cec7f5a045626a9 Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Wed, 11 Dec 2024 13:36:08 -0500 Subject: [PATCH 27/95] fix missing header height on the sheet (#6315) --- src/components/change-wallet/WalletList.tsx | 15 +++++++++------ src/screens/AddWalletSheet.tsx | 1 - src/screens/ChangeWalletSheet.tsx | 1 - 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/components/change-wallet/WalletList.tsx b/src/components/change-wallet/WalletList.tsx index a49b70ad6ef..ba23599e224 100644 --- a/src/components/change-wallet/WalletList.tsx +++ b/src/components/change-wallet/WalletList.tsx @@ -27,7 +27,7 @@ import { useTheme } from '@/theme'; import { DEVICE_HEIGHT } from '@/utils/deviceUtils'; const listTopPadding = 7.5; -const rowHeight = 59; +const listBottomPadding = 9.5; const transitionDuration = 75; const RowTypes = { @@ -66,7 +66,7 @@ const EmptyWalletList = styled(EmptyAssetList).attrs({ const WalletFlatList: FlatList = styled(FlatList).attrs(({ showDividers }: { showDividers: boolean }) => ({ contentContainerStyle: { - paddingBottom: showDividers ? 9.5 : 0, + paddingBottom: showDividers ? listBottomPadding : 0, paddingTop: listTopPadding, }, getItemLayout, @@ -118,6 +118,7 @@ const EditButtonLabel = styled(Text).attrs(({ theme: { colors }, editMode }: { t height: 40, }); +const HEADER_HEIGHT = 40; const FOOTER_HEIGHT = getExperimetalFlag(HARDWARE_WALLETS) ? 100 : 60; const LIST_PADDING_BOTTOM = 6; export const MAX_LIST_HEIGHT = DEVICE_HEIGHT - 220; @@ -125,8 +126,10 @@ const WALLET_ROW_HEIGHT = 59; const WATCH_ONLY_BOTTOM_PADDING = IS_ANDROID ? 20 : 0; const getWalletListHeight = (numWallets: number, watchOnly: boolean) => { - const baseHeight = !watchOnly ? FOOTER_HEIGHT + LIST_PADDING_BOTTOM : WATCH_ONLY_BOTTOM_PADDING; - const calculatedHeight = baseHeight + numWallets * (WALLET_ROW_HEIGHT + 6); + const baseHeight = !watchOnly ? FOOTER_HEIGHT + LIST_PADDING_BOTTOM + HEADER_HEIGHT : WATCH_ONLY_BOTTOM_PADDING; + const paddingBetweenRows = 6 * (numWallets - 1); + const rowHeight = WALLET_ROW_HEIGHT * numWallets; + const calculatedHeight = baseHeight + rowHeight + paddingBetweenRows; return Math.min(calculatedHeight, MAX_LIST_HEIGHT); }; @@ -181,7 +184,7 @@ export default function WalletList({ const row = { ...account, editMode, - height: rowHeight, + height: WALLET_ROW_HEIGHT, id: account.address, isOnlyAddress: filteredAccounts.length === 1, isReadOnly: wallet.type === WalletTypes.readOnly, @@ -264,7 +267,7 @@ export default function WalletList({ return ( - + {lang.t('wallet.label')} diff --git a/src/screens/AddWalletSheet.tsx b/src/screens/AddWalletSheet.tsx index 6021f7ad295..4b010ff3f16 100644 --- a/src/screens/AddWalletSheet.tsx +++ b/src/screens/AddWalletSheet.tsx @@ -18,7 +18,6 @@ import CreateNewWallet from '@/assets/CreateNewWallet.png'; import PairHairwareWallet from '@/assets/PairHardwareWallet.png'; import ImportSecretPhraseOrPrivateKey from '@/assets/ImportSecretPhraseOrPrivateKey.png'; import WatchWalletIcon from '@/assets/watchWallet.png'; -import { captureException } from '@sentry/react-native'; import { useDispatch } from 'react-redux'; import { backupUserDataIntoCloud, diff --git a/src/screens/ChangeWalletSheet.tsx b/src/screens/ChangeWalletSheet.tsx index 9d59a858a3b..e58c9dda0d1 100644 --- a/src/screens/ChangeWalletSheet.tsx +++ b/src/screens/ChangeWalletSheet.tsx @@ -19,7 +19,6 @@ import { useTheme } from '@/theme'; import { EthereumAddress } from '@/entities'; import { getNotificationSettingsForWalletWithAddress } from '@/notifications/settings/storage'; import { remotePromoSheetsStore } from '@/state/remotePromoSheets/remotePromoSheets'; -import { DebugLayout } from '@/design-system'; export type EditWalletContextMenuActions = { edit: (walletId: string, address: EthereumAddress) => void; From e67803ca73abcc15dcaecefe9c2a9010ba685c60 Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Wed, 11 Dec 2024 15:41:37 -0500 Subject: [PATCH 28/95] convert network accessors to functions (#6219) * convert network accessors to functions * backend networks store * wrote a couple worklet functions to get swaps working * fix other calls * Update package.json * Update ios/Podfile.lock * move all functions into backendNetworksStore and replace calls * refresh podlock * no clue what happened during merge * fix: APP-2021 * fix: APP-2022 * fix build and lint --- src/__swaps__/screens/Swap/Swap.tsx | 2 +- .../components/AnimatedChainImage.android.tsx | 2 +- .../components/AnimatedChainImage.ios.tsx | 2 +- .../screens/Swap/components/CoinRow.tsx | 11 +- .../screens/Swap/components/FlipButton.tsx | 2 +- .../screens/Swap/components/GasButton.tsx | 2 +- .../screens/Swap/components/GasPanel.tsx | 2 +- .../screens/Swap/components/ReviewPanel.tsx | 10 +- .../screens/Swap/components/SwapCoinIcon.tsx | 2 +- .../Swap/components/SwapOutputAsset.tsx | 2 +- .../components/TokenList/ChainSelection.tsx | 17 +- .../components/TokenList/TokenToBuyList.tsx | 2 +- .../screens/Swap/hooks/useCustomGas.ts | 2 +- .../screens/Swap/hooks/useEstimatedGasFee.ts | 2 +- .../Swap/hooks/useSearchCurrencyLists.ts | 2 +- .../screens/Swap/hooks/useSelectedGas.ts | 2 +- .../Swap/hooks/useSwapEstimatedGasLimit.ts | 2 +- .../Swap/hooks/useSwapInputsController.ts | 10 +- .../Swap/hooks/useSwapOutputQuotesDisabled.ts | 12 +- .../SyncSwapStateAndSharedValues.tsx | 2 +- .../screens/Swap/providers/swap-provider.tsx | 7 +- .../Swap/resources/_selectors/assets.ts | 2 +- .../Swap/resources/assets/userAssets.ts | 6 +- .../resources/assets/userAssetsByChain.ts | 2 +- .../Swap/resources/search/discovery.ts | 2 +- .../screens/Swap/resources/search/search.ts | 2 +- .../screens/Swap/resources/search/utils.ts | 2 +- src/__swaps__/types/assets.ts | 2 +- src/__swaps__/types/refraction.ts | 2 +- src/__swaps__/types/search.ts | 2 +- src/__swaps__/utils/assets.ts | 18 +- src/__swaps__/utils/gasUtils.ts | 2 +- src/__swaps__/utils/meteorology.ts | 3 +- src/__swaps__/utils/swaps.ts | 37 +- src/analytics/event.ts | 2 +- src/appIcons/appIcons.ts | 2 +- src/chains/index.ts | 216 ------ src/components/AddFundsInterstitial.js | 2 +- src/components/BackendNetworks.tsx | 7 +- src/components/ChainLogo.js | 2 +- .../control-panel/ControlPanel.tsx | 8 +- .../DappBrowser/handleProviderRequest.ts | 15 +- src/components/Discover/DiscoverSearch.tsx | 8 +- .../Discover/DiscoverSearchInput.tsx | 6 +- src/components/ExchangeTokenRow.tsx | 2 +- src/components/L2Disclaimer.js | 4 +- .../Transactions/TransactionDetailsCard.tsx | 12 +- .../Transactions/TransactionDetailsRow.tsx | 2 +- .../TransactionSimulatedEventRow.tsx | 6 +- .../TransactionSimulationCard.tsx | 6 +- .../RecyclerAssetList2/Claimable.tsx | 4 +- .../FastComponents/FastBalanceCoinRow.tsx | 2 +- .../FastComponents/FastCoinBadge.tsx | 2 +- .../FastCurrencySelectionRow.tsx | 2 +- src/components/cards/EthCard.tsx | 2 +- src/components/cards/NFTOffersCard/Offer.tsx | 6 +- src/components/cards/OpRewardsCard.tsx | 2 +- src/components/change-wallet/WalletList.tsx | 2 +- src/components/coin-icon/ChainBadge.js | 2 +- src/components/coin-icon/ChainIcon.js | 2 +- src/components/coin-icon/ChainImage.tsx | 4 +- src/components/coin-icon/EthCoinIcon.tsx | 2 +- src/components/coin-icon/RainbowCoinIcon.tsx | 2 +- src/components/coin-icon/TwoCoinsIcon.tsx | 2 +- .../coin-row/FastTransactionCoinRow.tsx | 2 +- .../context-menu-buttons/ChainContextMenu.tsx | 11 +- .../ens-profile/ActionButtons/MoreButton.tsx | 2 +- .../exchangeAssetRowContextMenuProps.ts | 18 +- .../expanded-state/AvailableNetworks.js | 6 +- .../expanded-state/AvailableNetworksv2.tsx | 19 +- .../UniqueTokenExpandedState.tsx | 2 +- .../asset/ChartExpandedState.js | 8 +- .../unique-token/NFTBriefTokenInfoRow.tsx | 2 +- .../UniqueTokenExpandedStateHeader.tsx | 2 +- src/components/gas/GasSpeedButton.tsx | 8 +- src/components/positions/PositionsCard.tsx | 6 +- .../check-fns/hasSwapTxn.ts | 4 +- .../sheet-action-buttons/SwapActionButton.tsx | 6 +- src/components/toasts/OfflineToast.js | 2 +- src/components/toasts/TestnetToast.js | 8 +- .../WalletConnectV2ListItem.tsx | 8 +- src/entities/tokens.ts | 2 +- src/entities/transactions/transaction.ts | 2 +- src/entities/uniqueAssets.ts | 2 +- src/featuresToUnlock/tokenGatedUtils.ts | 6 +- src/handlers/assets.ts | 6 +- src/handlers/deeplinks.ts | 5 +- src/handlers/ens.ts | 2 +- src/handlers/gasFees.ts | 7 +- src/handlers/localstorage/globalSettings.ts | 2 +- src/handlers/localstorage/removeWallet.ts | 2 +- src/handlers/swap.ts | 2 +- src/handlers/tokenSearch.ts | 5 +- src/handlers/web3.ts | 14 +- src/helpers/SharedValuesContext.tsx | 8 + src/helpers/ens.ts | 2 +- src/helpers/gas.ts | 2 +- src/helpers/networkInfo.ts | 2 +- src/helpers/signingWallet.ts | 2 +- src/helpers/validators.ts | 2 +- src/helpers/walletConnectNetworks.ts | 11 +- src/hooks/charts/useChartInfo.ts | 2 +- src/hooks/useAccountTransactions.ts | 11 +- src/hooks/useAdditionalAssetData.ts | 2 +- src/hooks/useAsset.ts | 2 +- src/hooks/useCalculateGasLimit.ts | 6 +- src/hooks/useENSRegistrationActionHandler.ts | 2 +- src/hooks/useENSRegistrationCosts.ts | 2 +- src/hooks/useENSRegistrationStepHandler.tsx | 2 +- src/hooks/useENSSearch.ts | 2 +- src/hooks/useGas.ts | 6 +- src/hooks/useHasEnoughBalance.ts | 2 +- src/hooks/useImportingWallet.ts | 2 +- src/hooks/useNonceForDisplay.ts | 2 +- src/hooks/useSearchCurrencyList.ts | 9 +- src/hooks/useTransactionSetup.ts | 2 +- src/hooks/useWalletSectionsData.ts | 2 +- src/hooks/useWatchPendingTxs.ts | 8 +- .../migratePinnedAndHiddenTokenUniqueIds.ts | 2 +- src/model/remoteConfig.ts | 16 +- src/model/wallet.ts | 2 +- src/navigation/config.tsx | 6 +- src/parsers/transactions.ts | 6 +- src/raps/actions/claimBridge.ts | 8 +- src/raps/actions/crosschainSwap.ts | 6 +- src/raps/actions/ens.ts | 2 +- src/raps/actions/swap.ts | 6 +- src/raps/actions/unlock.ts | 6 +- src/raps/execute.ts | 2 +- src/raps/references.ts | 2 +- src/raps/utils.ts | 2 +- src/redux/ensRegistration.ts | 2 +- src/redux/gas.ts | 18 +- src/redux/settings.ts | 4 +- src/redux/showcaseTokens.ts | 2 +- src/references/gasUnits.ts | 2 +- src/references/rainbow-token-list/index.ts | 2 +- src/references/testnet-assets-by-chain.ts | 2 +- src/resources/addys/claimables/query.ts | 4 +- src/resources/addys/claimables/types.ts | 2 +- src/resources/addys/claimables/utils.ts | 8 +- src/resources/addys/types.ts | 2 +- src/resources/assets/UserAssetsQuery.ts | 6 +- src/resources/assets/assets.ts | 6 +- src/resources/assets/externalAssetsQuery.ts | 2 +- src/resources/assets/hardhatAssets.ts | 6 +- src/resources/assets/types.ts | 2 +- src/resources/assets/useUserAsset.ts | 6 +- src/resources/defi/PositionsQuery.ts | 4 +- src/resources/defi/types.ts | 2 +- src/resources/defi/utils.ts | 3 +- src/resources/ens/ensAddressQuery.ts | 2 +- src/resources/favorites.ts | 8 +- src/resources/metadata/backendNetworks.ts | 10 +- src/resources/nfts/index.ts | 2 +- src/resources/nfts/simplehash/index.ts | 12 +- src/resources/nfts/simplehash/types.ts | 2 +- src/resources/nfts/simplehash/utils.ts | 6 +- src/resources/nfts/types.ts | 2 +- src/resources/nfts/utils.ts | 2 +- src/resources/reservoir/client.ts | 2 +- src/resources/reservoir/mints.ts | 2 +- src/resources/reservoir/utils.ts | 2 +- .../transactions/consolidatedTransactions.ts | 6 +- src/resources/transactions/transaction.ts | 6 +- .../transactions/transactionSimulation.ts | 2 +- .../AddCash/components/ProviderCard.tsx | 2 +- src/screens/AddCash/utils.ts | 2 +- src/screens/ENSConfirmRegisterSheet.tsx | 2 +- src/screens/ExplainSheet.js | 10 +- src/screens/MintsSheet/card/Card.tsx | 6 +- src/screens/NFTOffersSheet/OfferRow.tsx | 6 +- src/screens/NFTSingleOfferSheet/index.tsx | 24 +- src/screens/SendConfirmationSheet.tsx | 8 +- src/screens/SendSheet.tsx | 14 +- .../components/CurrencySection.tsx | 2 +- .../components/NetworkSection.tsx | 6 +- src/screens/SignTransactionSheet.tsx | 9 +- src/screens/SpeedUpAndCancelSheet.tsx | 2 +- src/screens/WalletConnectApprovalSheet.tsx | 16 +- src/screens/claimables/transaction/claim.ts | 3 +- .../components/ClaimCustomization.tsx | 11 +- .../transaction/components/GasDetails.tsx | 4 +- .../context/TransactionClaimableContext.tsx | 6 +- .../claimables/transaction/estimateGas.ts | 2 +- src/screens/claimables/transaction/types.ts | 2 +- src/screens/mints/MintSheet.tsx | 14 +- .../points/claim-flow/ClaimRewardsPanel.tsx | 10 +- .../points/components/LeaderboardRow.tsx | 2 +- src/screens/points/content/PointsContent.tsx | 2 +- .../points/contexts/PointsProfileContext.tsx | 2 +- src/screens/positions/SubPositionListItem.tsx | 4 +- .../TransactionDetailsValueAndFeeSection.tsx | 2 +- .../components/TransactionMasthead.tsx | 2 +- src/state/appSessions/index.test.ts | 2 +- src/state/appSessions/index.ts | 2 +- src/state/assets/userAssets.ts | 21 +- src/state/backendNetworks/backendNetworks.ts | 636 ++++++++++++++++++ .../backendNetworks}/types.ts | 0 .../backendNetworks/utils.ts} | 2 +- src/state/nonces/index.ts | 7 +- src/state/pendingTransactions/index.ts | 2 +- src/state/staleBalances/index.test.ts | 2 +- src/state/swaps/swapsStore.ts | 4 +- src/state/sync/UserAssetsSync.tsx | 2 +- src/storage/index.ts | 2 +- src/styles/colors.ts | 2 +- src/utils/ethereumUtils.ts | 35 +- src/utils/getUrlForTrustIconFallback.ts | 6 +- src/utils/requestNavigationHandlers.ts | 15 +- src/walletConnect/index.tsx | 11 +- src/walletConnect/types.ts | 2 +- 212 files changed, 1230 insertions(+), 694 deletions(-) delete mode 100644 src/chains/index.ts create mode 100644 src/state/backendNetworks/backendNetworks.ts rename src/{chains => state/backendNetworks}/types.ts (100%) rename src/{chains/utils/backendNetworks.ts => state/backendNetworks/utils.ts} (96%) diff --git a/src/__swaps__/screens/Swap/Swap.tsx b/src/__swaps__/screens/Swap/Swap.tsx index 0d5db8bb3eb..22d71dc3cc5 100644 --- a/src/__swaps__/screens/Swap/Swap.tsx +++ b/src/__swaps__/screens/Swap/Swap.tsx @@ -18,7 +18,7 @@ import { SwapInputAsset } from '@/__swaps__/screens/Swap/components/SwapInputAss import { SwapNavbar } from '@/__swaps__/screens/Swap/components/SwapNavbar'; import { SwapOutputAsset } from '@/__swaps__/screens/Swap/components/SwapOutputAsset'; import { SwapSheetGestureBlocker } from '@/__swaps__/screens/Swap/components/SwapSheetGestureBlocker'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { SwapAssetType } from '@/__swaps__/types/swap'; import { parseSearchAsset } from '@/__swaps__/utils/assets'; import { AbsolutePortalRoot } from '@/components/AbsolutePortal'; diff --git a/src/__swaps__/screens/Swap/components/AnimatedChainImage.android.tsx b/src/__swaps__/screens/Swap/components/AnimatedChainImage.android.tsx index 49d30869f71..19f39bac85c 100644 --- a/src/__swaps__/screens/Swap/components/AnimatedChainImage.android.tsx +++ b/src/__swaps__/screens/Swap/components/AnimatedChainImage.android.tsx @@ -14,7 +14,7 @@ const OptimismBadge = require('@/assets/badges/optimism.png'); const PolygonBadge = require('@/assets/badges/polygon.png'); const ZoraBadge = require('@/assets/badges/zora.png'); -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { globalColors } from '@/design-system'; import { PIXEL_RATIO } from '@/utils/deviceUtils'; import { useSwapsStore } from '@/state/swaps/swapsStore'; diff --git a/src/__swaps__/screens/Swap/components/AnimatedChainImage.ios.tsx b/src/__swaps__/screens/Swap/components/AnimatedChainImage.ios.tsx index eb48444a8a0..af14fd783e8 100644 --- a/src/__swaps__/screens/Swap/components/AnimatedChainImage.ios.tsx +++ b/src/__swaps__/screens/Swap/components/AnimatedChainImage.ios.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Image, StyleSheet, View } from 'react-native'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { useAnimatedProps, useDerivedValue } from 'react-native-reanimated'; import { AnimatedFasterImage } from '@/components/AnimatedComponents/AnimatedFasterImage'; import { DEFAULT_FASTER_IMAGE_CONFIG } from '@/components/images/ImgixImage'; diff --git a/src/__swaps__/screens/Swap/components/CoinRow.tsx b/src/__swaps__/screens/Swap/components/CoinRow.tsx index 9cf1bbeb142..dc1894bddf1 100644 --- a/src/__swaps__/screens/Swap/components/CoinRow.tsx +++ b/src/__swaps__/screens/Swap/components/CoinRow.tsx @@ -1,7 +1,7 @@ import { BalancePill } from '@/__swaps__/screens/Swap/components/BalancePill'; import { CoinRowButton } from '@/__swaps__/screens/Swap/components/CoinRowButton'; -import { AddressOrEth, ParsedSearchAsset } from '@/__swaps__/types/assets'; -import { ChainId } from '@/chains/types'; +import { AddressOrEth, ParsedSearchAsset, UniqueId } from '@/__swaps__/types/assets'; +import { ChainId } from '@/state/backendNetworks/types'; import { SearchAsset } from '@/__swaps__/types/search'; import { ButtonPressAnimation } from '@/components/animations'; import { ContextMenuButton } from '@/components/context-menu'; @@ -17,7 +17,7 @@ import React, { useCallback, useMemo } from 'react'; import { GestureResponderEvent } from 'react-native'; import { OnPressMenuItemEventObject } from 'react-native-ios-context-menu'; import { SwapCoinIcon } from './SwapCoinIcon'; -import { SUPPORTED_CHAIN_IDS } from '@/chains'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; export const COIN_ROW_WITH_PADDING_HEIGHT = 56; @@ -44,7 +44,7 @@ interface InputCoinRowProps { nativePriceChange?: string; onPress: (asset: ParsedSearchAsset | null) => void; output?: false | undefined; - uniqueId: string; + uniqueId: UniqueId; testID?: string; } @@ -178,7 +178,8 @@ export function CoinRow({ isFavorite, onPress, output, uniqueId, testID, ...asse } const InfoButton = ({ address, chainId }: { address: string; chainId: ChainId }) => { - const supportedChain = SUPPORTED_CHAIN_IDS.includes(chainId); + const getSupportedChainIds = useBackendNetworksStore(state => state.getSupportedChainIds); + const supportedChain = getSupportedChainIds().includes(chainId); const handleCopy = useCallback(() => { haptics.selection(); diff --git a/src/__swaps__/screens/Swap/components/FlipButton.tsx b/src/__swaps__/screens/Swap/components/FlipButton.tsx index 3baa0974095..b31fa07eb52 100644 --- a/src/__swaps__/screens/Swap/components/FlipButton.tsx +++ b/src/__swaps__/screens/Swap/components/FlipButton.tsx @@ -14,7 +14,7 @@ import { TIMING_CONFIGS } from '@/components/animations/animationConfigs'; import { SwapAssetType } from '@/__swaps__/types/swap'; import { GestureHandlerButton } from './GestureHandlerButton'; import { useSwapsStore } from '@/state/swaps/swapsStore'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; export const FlipButton = () => { const { isDarkMode } = useColorMode(); diff --git a/src/__swaps__/screens/Swap/components/GasButton.tsx b/src/__swaps__/screens/Swap/components/GasButton.tsx index 59cee69c319..eb5ccbd7638 100644 --- a/src/__swaps__/screens/Swap/components/GasButton.tsx +++ b/src/__swaps__/screens/Swap/components/GasButton.tsx @@ -22,7 +22,7 @@ import { NavigationSteps, useSwapContext } from '../providers/swap-provider'; import { EstimatedSwapGasFee, EstimatedSwapGasFeeSlot } from './EstimatedSwapGasFee'; import { GestureHandlerButton } from './GestureHandlerButton'; import { UnmountOnAnimatedReaction } from './UnmountOnAnimatedReaction'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; const { SWAP_GAS_ICONS } = gasUtils; const GAS_BUTTON_HIT_SLOP = 16; diff --git a/src/__swaps__/screens/Swap/components/GasPanel.tsx b/src/__swaps__/screens/Swap/components/GasPanel.tsx index e3f5d9f8569..b61169d347e 100644 --- a/src/__swaps__/screens/Swap/components/GasPanel.tsx +++ b/src/__swaps__/screens/Swap/components/GasPanel.tsx @@ -4,7 +4,7 @@ import Animated, { runOnJS, useAnimatedReaction, useAnimatedStyle, withDelay, wi import { THICK_BORDER_WIDTH } from '@/__swaps__/screens/Swap/constants'; import { NavigationSteps, useSwapContext } from '@/__swaps__/screens/Swap/providers/swap-provider'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { GasSpeed } from '@/__swaps__/types/gas'; import { gweiToWei, weiToGwei } from '@/parsers'; import { diff --git a/src/__swaps__/screens/Swap/components/ReviewPanel.tsx b/src/__swaps__/screens/Swap/components/ReviewPanel.tsx index 82cdbc2c5e9..3403463373f 100644 --- a/src/__swaps__/screens/Swap/components/ReviewPanel.tsx +++ b/src/__swaps__/screens/Swap/components/ReviewPanel.tsx @@ -41,8 +41,8 @@ import { useSelectedGasSpeed } from '../hooks/useSelectedGas'; import { NavigationSteps, useSwapContext } from '../providers/swap-provider'; import { EstimatedSwapGasFee, EstimatedSwapGasFeeSlot } from './EstimatedSwapGasFee'; import { UnmountOnAnimatedReaction } from './UnmountOnAnimatedReaction'; -import { chainsLabel, chainsNativeAsset } from '@/chains'; -import { ChainId } from '@/chains/types'; +import { getChainsLabelWorklet, useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; +import { ChainId } from '@/state/backendNetworks/types'; const UNKNOWN_LABEL = i18n.t(i18n.l.swap.unknown); const REVIEW_LABEL = i18n.t(i18n.l.expanded_state.swap_details.review); @@ -245,6 +245,7 @@ export const SlippageRow = () => { export function ReviewPanel() { const { navigate } = useNavigation(); const { isDarkMode } = useColorMode(); + const backendNetworks = useBackendNetworksStore(state => state.backendNetworksSharedValue); const { configProgress, lastTypedInput, internalSelectedInputAsset, internalSelectedOutputAsset, quote } = useSwapContext(); const labelTertiary = useForegroundColor('labelTertiary'); @@ -252,7 +253,9 @@ export function ReviewPanel() { const unknown = i18n.t(i18n.l.swap.unknown); - const chainName = useDerivedValue(() => chainsLabel[internalSelectedInputAsset.value?.chainId ?? ChainId.mainnet]); + const chainName = useDerivedValue( + () => getChainsLabelWorklet(backendNetworks)[internalSelectedInputAsset.value?.chainId ?? ChainId.mainnet] + ); const minReceivedOrMaxSoldLabel = useDerivedValue(() => { const isInputBasedTrade = lastTypedInput.value === 'inputAmount' || lastTypedInput.value === 'inputNativeValue'; @@ -281,6 +284,7 @@ export function ReviewPanel() { }); const openGasExplainer = useCallback(async () => { + const chainsNativeAsset = useBackendNetworksStore.getState().getChainsNativeAsset(); const nativeAsset = chainsNativeAsset[swapsStore.getState().inputAsset?.chainId ?? ChainId.mainnet]; navigate(Routes.EXPLAIN_SHEET, { chainId: swapsStore.getState().inputAsset?.chainId ?? ChainId.mainnet, diff --git a/src/__swaps__/screens/Swap/components/SwapCoinIcon.tsx b/src/__swaps__/screens/Swap/components/SwapCoinIcon.tsx index 9e56098952c..f5e15d41c8d 100644 --- a/src/__swaps__/screens/Swap/components/SwapCoinIcon.tsx +++ b/src/__swaps__/screens/Swap/components/SwapCoinIcon.tsx @@ -10,7 +10,7 @@ import { FallbackIcon as CoinIconTextFallback, isETH } from '@/utils'; import { FastFallbackCoinIconImage } from '@/components/asset-list/RecyclerAssetList2/FastComponents/FastFallbackCoinIconImage'; import Animated from 'react-native-reanimated'; import FastImage, { Source } from 'react-native-fast-image'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; // TODO: Delete this and replace with RainbowCoinIcon // ⚠️ When replacing this component with RainbowCoinIcon, make sure diff --git a/src/__swaps__/screens/Swap/components/SwapOutputAsset.tsx b/src/__swaps__/screens/Swap/components/SwapOutputAsset.tsx index 71aada3d1d1..93130066142 100644 --- a/src/__swaps__/screens/Swap/components/SwapOutputAsset.tsx +++ b/src/__swaps__/screens/Swap/components/SwapOutputAsset.tsx @@ -17,7 +17,7 @@ import { TokenList } from '@/__swaps__/screens/Swap/components/TokenList/TokenLi import { BASE_INPUT_WIDTH, INPUT_INNER_WIDTH, INPUT_PADDING, THICK_BORDER_WIDTH } from '@/__swaps__/screens/Swap/constants'; import { IS_ANDROID, IS_IOS } from '@/env'; import { useSwapContext } from '@/__swaps__/screens/Swap/providers/swap-provider'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import * as i18n from '@/languages'; import { useNavigation } from '@/navigation'; import Routes from '@/navigation/routesNames'; diff --git a/src/__swaps__/screens/Swap/components/TokenList/ChainSelection.tsx b/src/__swaps__/screens/Swap/components/TokenList/ChainSelection.tsx index 2fb3f5a389d..6e803ae4904 100644 --- a/src/__swaps__/screens/Swap/components/TokenList/ChainSelection.tsx +++ b/src/__swaps__/screens/Swap/components/TokenList/ChainSelection.tsx @@ -6,7 +6,7 @@ import { Text as RNText, StyleSheet } from 'react-native'; import Animated, { useDerivedValue, useSharedValue } from 'react-native-reanimated'; import { useSwapContext } from '@/__swaps__/screens/Swap/providers/swap-provider'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { opacity } from '@/__swaps__/utils/swaps'; import { analyticsV2 } from '@/analytics'; import { ChainImage } from '@/components/coin-icon/ChainImage'; @@ -18,7 +18,7 @@ import { userAssetsStore, useUserAssetsStore } from '@/state/assets/userAssets'; import { swapsStore } from '@/state/swaps/swapsStore'; import { showActionSheetWithOptions } from '@/utils'; import { OnPressMenuItemEventObject } from 'react-native-ios-context-menu'; -import { chainsLabel, chainsName } from '@/chains'; +import { getChainsLabelWorklet, getChainsNameWorklet, useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; type ChainSelectionProps = { allText?: string; @@ -29,6 +29,7 @@ export const ChainSelection = memo(function ChainSelection({ allText, output }: const { isDarkMode } = useColorMode(); const { accentColor: accountColor } = useAccountAccentColor(); const { selectedOutputChainId, setSelectedOutputChainId } = useSwapContext(); + const backendNetworks = useBackendNetworksStore(state => state.backendNetworksSharedValue); // chains sorted by balance on output, chains without balance hidden on input const { balanceSortedChainList, filter } = useUserAssetsStore(state => ({ @@ -47,11 +48,13 @@ export const ChainSelection = memo(function ChainSelection({ allText, output }: }, [accountColor, isDarkMode]); const chainName = useDerivedValue(() => { + const chainLabels = getChainsLabelWorklet(backendNetworks); + return output - ? chainsLabel[selectedOutputChainId.value] + ? chainLabels[selectedOutputChainId.value] : inputListFilter.value === 'all' ? allText - : chainsLabel[inputListFilter.value as ChainId]; + : chainLabels[inputListFilter.value as ChainId]; }); const handleSelectChain = useCallback( @@ -78,11 +81,11 @@ export const ChainSelection = memo(function ChainSelection({ allText, output }: const supportedChains = balanceSortedChainList.map(chainId => { return { actionKey: `${chainId}`, - actionTitle: chainsLabel[chainId], + actionTitle: getChainsLabelWorklet(backendNetworks)[chainId], icon: { iconType: 'ASSET', // NOTE: chainsName[chainId] for mainnet is 'mainnet' and we need it to be 'ethereum' - iconValue: chainId === ChainId.mainnet ? 'ethereumBadge' : `${chainsName[chainId]}BadgeNoShadow`, + iconValue: chainId === ChainId.mainnet ? 'ethereumBadge' : `${getChainsNameWorklet(backendNetworks)[chainId]}BadgeNoShadow`, }, }; }); @@ -101,7 +104,7 @@ export const ChainSelection = memo(function ChainSelection({ allText, output }: return { menuItems: supportedChains, }; - }, [balanceSortedChainList, output]); + }, [backendNetworks, balanceSortedChainList, output]); const onShowActionSheet = useCallback(() => { const chainTitles = menuConfig.menuItems.map(chain => chain.actionTitle); diff --git a/src/__swaps__/screens/Swap/components/TokenList/TokenToBuyList.tsx b/src/__swaps__/screens/Swap/components/TokenList/TokenToBuyList.tsx index 42bbfa26ff3..cf2fc29c7e2 100644 --- a/src/__swaps__/screens/Swap/components/TokenList/TokenToBuyList.tsx +++ b/src/__swaps__/screens/Swap/components/TokenList/TokenToBuyList.tsx @@ -3,7 +3,7 @@ import { COIN_ROW_WITH_PADDING_HEIGHT, CoinRow } from '@/__swaps__/screens/Swap/ import { ListEmpty } from '@/__swaps__/screens/Swap/components/TokenList/ListEmpty'; import { AssetToBuySectionId, useSearchCurrencyLists } from '@/__swaps__/screens/Swap/hooks/useSearchCurrencyLists'; import { useSwapContext } from '@/__swaps__/screens/Swap/providers/swap-provider'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { SearchAsset } from '@/__swaps__/types/search'; import { SwapAssetType } from '@/__swaps__/types/swap'; import { parseSearchAsset } from '@/__swaps__/utils/assets'; diff --git a/src/__swaps__/screens/Swap/hooks/useCustomGas.ts b/src/__swaps__/screens/Swap/hooks/useCustomGas.ts index 74559a1e1e3..00c0f0bbe22 100644 --- a/src/__swaps__/screens/Swap/hooks/useCustomGas.ts +++ b/src/__swaps__/screens/Swap/hooks/useCustomGas.ts @@ -1,4 +1,4 @@ -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { createRainbowStore } from '@/state/internal/createRainbowStore'; export type EIP1159GasSettings = { diff --git a/src/__swaps__/screens/Swap/hooks/useEstimatedGasFee.ts b/src/__swaps__/screens/Swap/hooks/useEstimatedGasFee.ts index fbfe9ca2cc6..d833657a12b 100644 --- a/src/__swaps__/screens/Swap/hooks/useEstimatedGasFee.ts +++ b/src/__swaps__/screens/Swap/hooks/useEstimatedGasFee.ts @@ -1,4 +1,4 @@ -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { weiToGwei } from '@/parsers'; import { convertAmountToNativeDisplayWorklet, formatNumber, multiply } from '@/helpers/utilities'; import { useNativeAsset } from '@/utils/ethereumUtils'; diff --git a/src/__swaps__/screens/Swap/hooks/useSearchCurrencyLists.ts b/src/__swaps__/screens/Swap/hooks/useSearchCurrencyLists.ts index 82e78bed66c..4033417ed62 100644 --- a/src/__swaps__/screens/Swap/hooks/useSearchCurrencyLists.ts +++ b/src/__swaps__/screens/Swap/hooks/useSearchCurrencyLists.ts @@ -1,5 +1,5 @@ import { TokenSearchResult, useTokenSearch } from '@/__swaps__/screens/Swap/resources/search/search'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { SearchAsset, TokenSearchAssetKey, TokenSearchThreshold } from '@/__swaps__/types/search'; import { addHexPrefix } from '@/handlers/web3'; import { isLowerCaseMatch, filterList } from '@/utils'; diff --git a/src/__swaps__/screens/Swap/hooks/useSelectedGas.ts b/src/__swaps__/screens/Swap/hooks/useSelectedGas.ts index 4fa9145a0b7..2323417d28b 100644 --- a/src/__swaps__/screens/Swap/hooks/useSelectedGas.ts +++ b/src/__swaps__/screens/Swap/hooks/useSelectedGas.ts @@ -1,4 +1,4 @@ -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { GasSpeed } from '@/__swaps__/types/gas'; import { getCachedGasSuggestions, useMeteorologySuggestions } from '@/__swaps__/utils/meteorology'; import { createRainbowStore } from '@/state/internal/createRainbowStore'; diff --git a/src/__swaps__/screens/Swap/hooks/useSwapEstimatedGasLimit.ts b/src/__swaps__/screens/Swap/hooks/useSwapEstimatedGasLimit.ts index d061ca5fe3f..1c77056a6ff 100644 --- a/src/__swaps__/screens/Swap/hooks/useSwapEstimatedGasLimit.ts +++ b/src/__swaps__/screens/Swap/hooks/useSwapEstimatedGasLimit.ts @@ -2,7 +2,7 @@ import { CrosschainQuote, Quote, QuoteError, SwapType } from '@rainbow-me/swaps' import { useQuery } from '@tanstack/react-query'; import { ParsedSearchAsset } from '@/__swaps__/types/assets'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { estimateUnlockAndCrosschainSwap } from '@/raps/actions/crosschainSwap'; import { estimateUnlockAndSwap } from '@/raps/actions/swap'; import { QueryConfigWithSelect, QueryFunctionArgs, QueryFunctionResult, createQueryKey } from '@/react-query'; diff --git a/src/__swaps__/screens/Swap/hooks/useSwapInputsController.ts b/src/__swaps__/screens/Swap/hooks/useSwapInputsController.ts index 0992827ce8e..4507731c7a0 100644 --- a/src/__swaps__/screens/Swap/hooks/useSwapInputsController.ts +++ b/src/__swaps__/screens/Swap/hooks/useSwapInputsController.ts @@ -1,7 +1,7 @@ import { divWorklet, equalWorklet, greaterThanWorklet, isNumberStringWorklet, mulWorklet, toFixedWorklet } from '@/safe-math/SafeMath'; import { SCRUBBER_WIDTH, SLIDER_WIDTH, snappySpringConfig } from '@/__swaps__/screens/Swap/constants'; import { ExtendedAnimatedAssetWithColors } from '@/__swaps__/types/assets'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { RequestNewQuoteParams, inputKeys, inputMethods, inputValuesType } from '@/__swaps__/types/swap'; import { valueBasedDecimalFormatter } from '@/__swaps__/utils/decimalFormatter'; import { getInputValuesForSliderPositionWorklet, updateInputValuesAfterFlip } from '@/__swaps__/utils/flipAssets'; @@ -686,7 +686,7 @@ export function useSwapInputsController({ () => ({ assetToBuyId: internalSelectedOutputAsset.value?.uniqueId, assetToSellId: internalSelectedInputAsset.value?.uniqueId, - assetToSellNetwork: internalSelectedInputAsset.value?.chainId, + assetToSellChainId: internalSelectedInputAsset.value?.chainId, }), (current, previous) => { const didInputAssetChange = current.assetToSellId !== previous?.assetToSellId; @@ -694,12 +694,12 @@ export function useSwapInputsController({ if (!didInputAssetChange && !didOutputAssetChange) return; - if (current.assetToSellNetwork !== previous?.assetToSellNetwork) { - const previousDefaultSlippage = getDefaultSlippageWorklet(previous?.assetToSellNetwork || ChainId.mainnet, REMOTE_CONFIG); + if (current.assetToSellChainId !== previous?.assetToSellChainId) { + const previousDefaultSlippage = getDefaultSlippageWorklet(previous?.assetToSellChainId || ChainId.mainnet, REMOTE_CONFIG); // If the user has not overridden the default slippage, update it if (slippage.value === previousDefaultSlippage) { - const newSlippage = getDefaultSlippageWorklet(current.assetToSellNetwork || ChainId.mainnet, REMOTE_CONFIG); + const newSlippage = getDefaultSlippageWorklet(current.assetToSellChainId || ChainId.mainnet, REMOTE_CONFIG); slippage.value = newSlippage; runOnJS(setSlippage)(newSlippage); } diff --git a/src/__swaps__/screens/Swap/hooks/useSwapOutputQuotesDisabled.ts b/src/__swaps__/screens/Swap/hooks/useSwapOutputQuotesDisabled.ts index 63649915485..cf3259ab0eb 100644 --- a/src/__swaps__/screens/Swap/hooks/useSwapOutputQuotesDisabled.ts +++ b/src/__swaps__/screens/Swap/hooks/useSwapOutputQuotesDisabled.ts @@ -1,6 +1,10 @@ import { SharedValue, useDerivedValue } from 'react-native-reanimated'; import { ExtendedAnimatedAssetWithColors } from '@/__swaps__/types/assets'; -import { supportedSwapExactOutputChainIds, supportedBridgeExactOutputChainIds } from '@/chains'; +import { + useBackendNetworksStore, + getSwapExactOutputSupportedChainIdsWorklet, + getBridgeExactOutputSupportedChainIdsWorklet, +} from '@/state/backendNetworks/backendNetworks'; export const useSwapOutputQuotesDisabled = ({ inputAsset, @@ -9,13 +13,15 @@ export const useSwapOutputQuotesDisabled = ({ inputAsset: SharedValue; outputAsset: SharedValue; }): SharedValue => { + const backendNetworks = useBackendNetworksStore(state => state.backendNetworksSharedValue); + const outputQuotesAreDisabled = useDerivedValue(() => { if (!inputAsset.value || !outputAsset.value) return false; if (inputAsset.value.chainId === outputAsset.value.chainId) { - return !supportedSwapExactOutputChainIds.includes(inputAsset.value.chainId); + return !getSwapExactOutputSupportedChainIdsWorklet(backendNetworks).includes(inputAsset.value.chainId); } else { - return !supportedBridgeExactOutputChainIds.includes(inputAsset.value.chainId); + return !getBridgeExactOutputSupportedChainIdsWorklet(backendNetworks).includes(inputAsset.value.chainId); } }); diff --git a/src/__swaps__/screens/Swap/providers/SyncSwapStateAndSharedValues.tsx b/src/__swaps__/screens/Swap/providers/SyncSwapStateAndSharedValues.tsx index 65476eb5fd6..beee83e8b3e 100644 --- a/src/__swaps__/screens/Swap/providers/SyncSwapStateAndSharedValues.tsx +++ b/src/__swaps__/screens/Swap/providers/SyncSwapStateAndSharedValues.tsx @@ -13,7 +13,7 @@ import { toScaledIntegerWorklet, } from '@/safe-math/SafeMath'; import { ExtendedAnimatedAssetWithColors } from '@/__swaps__/types/assets'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { ParsedAddressAsset } from '@/entities'; import { useUserNativeNetworkAsset } from '@/resources/assets/useUserAsset'; import { CrosschainQuote, Quote, QuoteError } from '@rainbow-me/swaps'; diff --git a/src/__swaps__/screens/Swap/providers/swap-provider.tsx b/src/__swaps__/screens/Swap/providers/swap-provider.tsx index ff02c775648..5cbb3638652 100644 --- a/src/__swaps__/screens/Swap/providers/swap-provider.tsx +++ b/src/__swaps__/screens/Swap/providers/swap-provider.tsx @@ -24,7 +24,7 @@ import { useSwapTextStyles } from '@/__swaps__/screens/Swap/hooks/useSwapTextSty import { SwapWarningType, useSwapWarning } from '@/__swaps__/screens/Swap/hooks/useSwapWarning'; import { userAssetsQueryKey as swapsUserAssetsQueryKey } from '@/__swaps__/screens/Swap/resources/assets/userAssets'; import { AddressOrEth, ExtendedAnimatedAssetWithColors, ParsedSearchAsset } from '@/__swaps__/types/assets'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { SwapAssetType, inputKeys } from '@/__swaps__/types/swap'; import { clamp, getDefaultSlippageWorklet, parseAssetAndExtend } from '@/__swaps__/utils/swaps'; import { analyticsV2 } from '@/analytics'; @@ -58,7 +58,7 @@ import { SyncGasStateToSharedValues, SyncQuoteSharedValuesToState } from './Sync import { performanceTracking, Screens, TimeToSignOperation } from '@/state/performance/performance'; import { getRemoteConfig } from '@/model/remoteConfig'; import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; -import { chainsNativeAsset } from '@/chains'; +import { useBackendNetworksStore, getChainsNativeAssetWorklet } from '@/state/backendNetworks/backendNetworks'; import { getSwapsNavigationParams } from '../navigateToSwaps'; import { LedgerSigner } from '@/handlers/LedgerSigner'; @@ -154,6 +154,7 @@ const getInitialSliderXPosition = ({ export const SwapProvider = ({ children }: SwapProviderProps) => { const { nativeCurrency } = useAccountSettings(); + const backendNetworks = useBackendNetworksStore(state => state.backendNetworksSharedValue); const initialValues = getSwapsNavigationParams(); const isFetching = useSharedValue(false); @@ -792,7 +793,7 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { } if (hasEnoughFundsForGas.value === false) { - const nativeCurrency = chainsNativeAsset[sellAsset?.chainId || ChainId.mainnet]; + const nativeCurrency = getChainsNativeAssetWorklet(backendNetworks)[sellAsset?.chainId || ChainId.mainnet]; return { label: `${insufficient} ${nativeCurrency.symbol}`, disabled: true, diff --git a/src/__swaps__/screens/Swap/resources/_selectors/assets.ts b/src/__swaps__/screens/Swap/resources/_selectors/assets.ts index 4ca4805c1c7..390045d369c 100644 --- a/src/__swaps__/screens/Swap/resources/_selectors/assets.ts +++ b/src/__swaps__/screens/Swap/resources/_selectors/assets.ts @@ -1,5 +1,5 @@ import { ParsedAssetsDict, ParsedAssetsDictByChain, ParsedUserAsset, UniqueId } from '@/__swaps__/types/assets'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { getAddressAndChainIdFromUniqueId } from '@/utils/ethereumUtils'; import { add } from '@/helpers/utilities'; diff --git a/src/__swaps__/screens/Swap/resources/assets/userAssets.ts b/src/__swaps__/screens/Swap/resources/assets/userAssets.ts index 47e59d36ed4..76e685a1545 100644 --- a/src/__swaps__/screens/Swap/resources/assets/userAssets.ts +++ b/src/__swaps__/screens/Swap/resources/assets/userAssets.ts @@ -8,14 +8,14 @@ import { RainbowError, logger } from '@/logger'; import { RainbowFetchClient } from '@/rainbow-fetch'; import { SupportedCurrencyKey } from '@/references'; import { ParsedAssetsDictByChain, ZerionAsset } from '@/__swaps__/types/assets'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { AddressAssetsReceivedMessage } from '@/__swaps__/types/refraction'; import { parseUserAsset } from '@/__swaps__/utils/assets'; import { greaterThan } from '@/helpers/utilities'; import { fetchUserAssetsByChain } from './userAssetsByChain'; import { fetchHardhatBalancesByChainId } from '@/resources/assets/hardhatAssets'; -import { SUPPORTED_CHAIN_IDS } from '@/chains'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; import store from '@/redux/store'; @@ -127,7 +127,7 @@ async function userAssetsQueryFunction({ const cachedUserAssets = (cache.find(userAssetsQueryKey({ address, currency, testnetMode }))?.state?.data || {}) as ParsedAssetsDictByChain; try { - const url = `/${SUPPORTED_CHAIN_IDS.join(',')}/${address}/assets`; + const url = `/${useBackendNetworksStore.getState().getSupportedChainIds().join(',')}/${address}/assets`; const res = await addysHttp.get(url, { params: { currency: currency.toLowerCase(), diff --git a/src/__swaps__/screens/Swap/resources/assets/userAssetsByChain.ts b/src/__swaps__/screens/Swap/resources/assets/userAssetsByChain.ts index 8a3260fa188..723334cce51 100644 --- a/src/__swaps__/screens/Swap/resources/assets/userAssetsByChain.ts +++ b/src/__swaps__/screens/Swap/resources/assets/userAssetsByChain.ts @@ -3,7 +3,7 @@ import { Address } from 'viem'; import { QueryConfigWithSelect, QueryFunctionArgs, QueryFunctionResult, createQueryKey, queryClient } from '@/react-query'; import { SupportedCurrencyKey } from '@/references'; import { ParsedAssetsDictByChain, ParsedUserAsset } from '@/__swaps__/types/assets'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { AddressAssetsReceivedMessage } from '@/__swaps__/types/refraction'; import { RainbowError, logger } from '@/logger'; diff --git a/src/__swaps__/screens/Swap/resources/search/discovery.ts b/src/__swaps__/screens/Swap/resources/search/discovery.ts index 5c8565facfd..ebb15d0f59b 100644 --- a/src/__swaps__/screens/Swap/resources/search/discovery.ts +++ b/src/__swaps__/screens/Swap/resources/search/discovery.ts @@ -1,4 +1,4 @@ -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { SearchAsset } from '@/__swaps__/types/search'; import { RainbowError, logger } from '@/logger'; import { RainbowFetchClient } from '@/rainbow-fetch'; diff --git a/src/__swaps__/screens/Swap/resources/search/search.ts b/src/__swaps__/screens/Swap/resources/search/search.ts index 7529cd32387..b270d55c6cd 100644 --- a/src/__swaps__/screens/Swap/resources/search/search.ts +++ b/src/__swaps__/screens/Swap/resources/search/search.ts @@ -1,5 +1,5 @@ /* eslint-disable no-nested-ternary */ -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { SearchAsset, TokenSearchAssetKey, TokenSearchListId, TokenSearchThreshold } from '@/__swaps__/types/search'; import { RainbowError, logger } from '@/logger'; import { RainbowFetchClient } from '@/rainbow-fetch'; diff --git a/src/__swaps__/screens/Swap/resources/search/utils.ts b/src/__swaps__/screens/Swap/resources/search/utils.ts index 288fa9302bc..975c0c3e8f4 100644 --- a/src/__swaps__/screens/Swap/resources/search/utils.ts +++ b/src/__swaps__/screens/Swap/resources/search/utils.ts @@ -1,4 +1,4 @@ -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { SearchAsset } from '@/__swaps__/types/search'; import { Address } from 'viem'; import { isNativeAsset } from '@/handlers/assets'; diff --git a/src/__swaps__/types/assets.ts b/src/__swaps__/types/assets.ts index f6bd292a44d..c6f7bcd84a5 100644 --- a/src/__swaps__/types/assets.ts +++ b/src/__swaps__/types/assets.ts @@ -1,7 +1,7 @@ import type { Address } from 'viem'; import { ETH_ADDRESS } from '@/references'; -import { ChainId, ChainName } from '@/chains/types'; +import { ChainId, ChainName } from '@/state/backendNetworks/types'; import { SearchAsset } from '@/__swaps__/types/search'; import { ResponseByTheme } from '../utils/swaps'; diff --git a/src/__swaps__/types/refraction.ts b/src/__swaps__/types/refraction.ts index 802b8fc0f33..674027f9324 100644 --- a/src/__swaps__/types/refraction.ts +++ b/src/__swaps__/types/refraction.ts @@ -1,5 +1,5 @@ import { ZerionAsset } from '@/__swaps__/types/assets'; -import { ChainId, ChainName } from '@/chains/types'; +import { ChainId, ChainName } from '@/state/backendNetworks/types'; import { PaginatedTransactionsApiResponse } from '@/entities'; /** diff --git a/src/__swaps__/types/search.ts b/src/__swaps__/types/search.ts index e108655d0d0..c11d1a6f920 100644 --- a/src/__swaps__/types/search.ts +++ b/src/__swaps__/types/search.ts @@ -1,7 +1,7 @@ import { Address } from 'viem'; import { AddressOrEth, AssetType, ParsedAsset, UniqueId } from '@/__swaps__/types/assets'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { AssetToBuySectionId } from '../screens/Swap/hooks/useSearchCurrencyLists'; export type TokenSearchAssetKey = keyof ParsedAsset; diff --git a/src/__swaps__/utils/assets.ts b/src/__swaps__/utils/assets.ts index 34a8cdf66fc..c2fa5dfd935 100644 --- a/src/__swaps__/utils/assets.ts +++ b/src/__swaps__/utils/assets.ts @@ -10,7 +10,7 @@ import { ZerionAsset, ZerionAssetPrice, } from '@/__swaps__/types/assets'; -import { ChainId, ChainName } from '@/chains/types'; +import { ChainId, ChainName } from '@/state/backendNetworks/types'; import * as i18n from '@/languages'; import { SearchAsset } from '@/__swaps__/types/search'; @@ -24,7 +24,7 @@ import { convertRawAmountToDecimalFormat, } from '@/helpers/utilities'; import { isLowerCaseMatch } from '@/utils'; -import { chainsIdByName, chainsName } from '@/chains'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; export const isSameAsset = (a1: Pick, a2: Pick) => +a1.chainId === +a2.chainId && isLowerCaseMatch(a1.address, a2.address); @@ -62,7 +62,10 @@ const getUniqueIdForAsset = ({ asset }: { asset: ZerionAsset | AssetApiResponse const address = asset.asset_code; const chainName = asset.network ?? ChainName.mainnet; const networks = 'networks' in asset ? asset.networks || {} : {}; - const chainId = ('chain_id' in asset && asset.chain_id) || chainsIdByName[chainName] || Number(Object.keys(networks)[0]); + const chainId = + ('chain_id' in asset && asset.chain_id) || + useBackendNetworksStore.getState().getChainsIdByName()[chainName] || + Number(Object.keys(networks)[0]); // ZerionAsset should be removed when we move fully away from websckets/refraction api const mainnetAddress = isZerionAsset(asset) @@ -76,7 +79,10 @@ export function parseAsset({ asset, currency }: { asset: ZerionAsset | AssetApiR const address = asset.asset_code; const chainName = asset.network ?? ChainName.mainnet; const networks = 'networks' in asset ? asset.networks || {} : {}; - const chainId = ('chain_id' in asset && asset.chain_id) || chainsIdByName[chainName] || Number(Object.keys(networks)[0]); + const chainId = + ('chain_id' in asset && asset.chain_id) || + useBackendNetworksStore.getState().getChainsIdByName()[chainName] || + Number(Object.keys(networks)[0]); // ZerionAsset should be removed when we move fully away from websckets/refraction api const mainnetAddress = isZerionAsset(asset) @@ -154,7 +160,7 @@ export function parseAssetMetadata({ const parsedAsset = { address, chainId, - chainName: chainsName[chainId], + chainName: useBackendNetworksStore.getState().getChainsName()[chainId], colors: asset?.colors, decimals: asset?.decimals, icon_url: asset?.iconUrl, @@ -272,7 +278,7 @@ export const parseSearchAsset = ({ isNativeAsset: isNativeAsset(searchAsset.address, searchAsset.chainId), address: searchAsset.address, chainId: searchAsset.chainId, - chainName: chainsName[searchAsset.chainId], + chainName: useBackendNetworksStore.getState().getChainsName()[searchAsset.chainId], native: { balance: userAsset?.native.balance || { amount: '0', diff --git a/src/__swaps__/utils/gasUtils.ts b/src/__swaps__/utils/gasUtils.ts index 40dfdeed543..7d8bce40c88 100644 --- a/src/__swaps__/utils/gasUtils.ts +++ b/src/__swaps__/utils/gasUtils.ts @@ -11,7 +11,7 @@ import * as i18n from '@/languages'; import { OVM_GAS_PRICE_ORACLE, gasUnits, supportedNativeCurrencies, optimismGasOracleAbi, SupportedCurrencyKey } from '@/references'; import { ParsedAsset } from '@/__swaps__/types/assets'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { BlocksToConfirmation, GasFeeLegacyParams, GasFeeParam, GasFeeParams, GasSpeed } from '@/__swaps__/types/gas'; import { gweiToWei, weiToGwei } from '@/parsers'; diff --git a/src/__swaps__/utils/meteorology.ts b/src/__swaps__/utils/meteorology.ts index 67181553eab..ba971109c4a 100644 --- a/src/__swaps__/utils/meteorology.ts +++ b/src/__swaps__/utils/meteorology.ts @@ -1,11 +1,10 @@ import { useQuery } from '@tanstack/react-query'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { rainbowMeteorologyGetData } from '@/handlers/gasFees'; import { abs, lessThan, subtract } from '@/helpers/utilities'; import { gweiToWei } from '@/parsers'; import { QueryConfig, QueryFunctionArgs, QueryFunctionResult, createQueryKey, queryClient } from '@/react-query'; -import { useSwapsStore } from '@/state/swaps/swapsStore'; import { useCallback } from 'react'; import { GasSettings } from '../screens/Swap/hooks/useCustomGas'; import { getSelectedGasSpeed, useGasSettings } from '../screens/Swap/hooks/useSelectedGas'; diff --git a/src/__swaps__/utils/swaps.ts b/src/__swaps__/utils/swaps.ts index 514c9d63240..4d9ec3982ea 100644 --- a/src/__swaps__/utils/swaps.ts +++ b/src/__swaps__/utils/swaps.ts @@ -9,12 +9,11 @@ import { SLIDER_WIDTH, STABLECOIN_MINIMUM_SIGNIFICANT_DECIMALS, } from '@/__swaps__/screens/Swap/constants'; -import { ChainId } from '@/chains/types'; import { globalColors } from '@/design-system'; import { ForegroundColor, palettes } from '@/design-system/color/palettes'; import { TokenColors } from '@/graphql/__generated__/metadata'; import * as i18n from '@/languages'; -import { RainbowConfig } from '@/model/remoteConfig'; +import { DEFAULT_CONFIG, RainbowConfig } from '@/model/remoteConfig'; import store from '@/redux/store'; import { supportedNativeCurrencies } from '@/references'; import { userAssetsStore } from '@/state/assets/userAssets'; @@ -38,7 +37,7 @@ import { ExtendedAnimatedAssetWithColors, ParsedSearchAsset } from '../types/ass import { inputKeys } from '../types/swap'; import { valueBasedDecimalFormatter } from './decimalFormatter'; import { convertAmountToRawAmount } from '@/helpers/utilities'; -import { chainsName } from '@/chains'; +import { ChainId } from '@/state/backendNetworks/types'; import { getUniqueId } from '@/utils/ethereumUtils'; // DO NOT REMOVE THESE COMMENTED ENV VARS @@ -346,20 +345,6 @@ export const opacityWorklet = (color: string, opacity: number) => { // // /---- END worklet utils ----/ // -export const DEFAULT_SLIPPAGE_BIPS = { - [ChainId.apechain]: 200, - [ChainId.arbitrum]: 200, - [ChainId.avalanche]: 200, - [ChainId.base]: 200, - [ChainId.blast]: 200, - [ChainId.bsc]: 200, - [ChainId.degen]: 200, - [ChainId.mainnet]: 100, - [ChainId.optimism]: 200, - [ChainId.polygon]: 200, - [ChainId.zora]: 200, -}; - export const slippageInBipsToString = (slippageInBips: number) => (slippageInBips / 100).toFixed(1); export const slippageInBipsToStringWorklet = (slippageInBips: number) => { @@ -368,18 +353,24 @@ export const slippageInBipsToStringWorklet = (slippageInBips: number) => { }; export const getDefaultSlippage = (chainId: ChainId, config: RainbowConfig) => { - return slippageInBipsToString( - // NOTE: JSON.parse doesn't type the result as a Record - (config.default_slippage_bips as unknown as Record)[chainsName[chainId]] || DEFAULT_SLIPPAGE_BIPS[chainId] + const amount = +( + (config.default_slippage_bips_chainId as unknown as { [key: number]: number })[chainId] || + DEFAULT_CONFIG.default_slippage_bips_chainId[chainId] || + 200 ); + + return slippageInBipsToString(amount); }; export const getDefaultSlippageWorklet = (chainId: ChainId, config: RainbowConfig) => { 'worklet'; - - return slippageInBipsToStringWorklet( - (config.default_slippage_bips as unknown as { [key: string]: number })[chainsName[chainId]] || DEFAULT_SLIPPAGE_BIPS[chainId] + const amount = +( + (config.default_slippage_bips_chainId as unknown as { [key: number]: number })[chainId] || + DEFAULT_CONFIG.default_slippage_bips_chainId[chainId] || + 200 ); + + return slippageInBipsToStringWorklet(amount); }; export type Colors = { diff --git a/src/analytics/event.ts b/src/analytics/event.ts index ecbedd05886..673e71a810d 100644 --- a/src/analytics/event.ts +++ b/src/analytics/event.ts @@ -1,5 +1,5 @@ import { AddressOrEth, ExtendedAnimatedAssetWithColors, ParsedSearchAsset } from '@/__swaps__/types/assets'; -import { ChainId, Network } from '@/chains/types'; +import { ChainId, Network } from '@/state/backendNetworks/types'; import { SwapAssetType } from '@/__swaps__/types/swap'; import { UnlockableAppIconKey } from '@/appIcons/appIcons'; import { CardType } from '@/components/cards/GenericCard'; diff --git a/src/appIcons/appIcons.ts b/src/appIcons/appIcons.ts index a4429006d58..97ae364095a 100644 --- a/src/appIcons/appIcons.ts +++ b/src/appIcons/appIcons.ts @@ -14,7 +14,7 @@ import AppIconPoolboy from '@/assets/appIconPoolboy.png'; import AppIconAdworld from '@/assets/appIconAdworld.png'; import AppIconFarcaster from '@/assets/appIconFarcaster.png'; import { TokenGateCheckerNetwork } from '@/featuresToUnlock/tokenGatedUtils'; -import { Network } from '@/chains/types'; +import { Network } from '@/state/backendNetworks/types'; // optimism app icon unlocking NFTs const OPTIMISTIC_EXPLORER_NFT_ADDRESS: EthereumAddress = '0x81b30ff521D1fEB67EDE32db726D95714eb00637'; diff --git a/src/chains/index.ts b/src/chains/index.ts deleted file mode 100644 index b8f45f8bbad..00000000000 --- a/src/chains/index.ts +++ /dev/null @@ -1,216 +0,0 @@ -import { Chain } from 'viem/chains'; -import { queryClient } from '@/react-query'; -import { backendNetworksQueryKey, BackendNetworksResponse } from '@/resources/metadata/backendNetworks'; -import { ChainId, BackendNetwork, BackendNetworkServices, chainHardhat, chainHardhatOptimism } from './types'; -import { transformBackendNetworksToChains } from './utils/backendNetworks'; -import { gasUtils } from '@/utils'; -import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; -import { IS_TEST } from '@/env'; -import buildTimeNetworks from '@/references/networks.json'; - -// NOTE: Prefer runtime data from backendNetworksQueryKey, but fallback to buildTimeNetworks if needed -const backendNetworks = queryClient.getQueryData(backendNetworksQueryKey()) ?? buildTimeNetworks; - -const BACKEND_CHAINS = transformBackendNetworksToChains(backendNetworks.networks); - -const DEFAULT_PRIVATE_MEMPOOL_TIMEOUT = 2 * 60 * 1_000; // 2 minutes - -export const SUPPORTED_CHAINS: Chain[] = IS_TEST ? [...BACKEND_CHAINS, chainHardhat, chainHardhatOptimism] : BACKEND_CHAINS; - -export const SUPPORTED_CHAIN_IDS_ALPHABETICAL: ChainId[] = SUPPORTED_CHAINS.sort((a, b) => a.name.localeCompare(b.name)).map(c => c.id); - -export const defaultChains: Record = SUPPORTED_CHAINS.reduce( - (acc, chain) => { - acc[chain.id] = chain; - return acc; - }, - {} as Record -); - -export const SUPPORTED_CHAIN_IDS = SUPPORTED_CHAINS.map(chain => chain.id); - -export const SUPPORTED_MAINNET_CHAINS: Chain[] = SUPPORTED_CHAINS.filter(chain => !chain.testnet); - -export const SUPPORTED_MAINNET_CHAIN_IDS: ChainId[] = SUPPORTED_MAINNET_CHAINS.map(chain => chain.id); - -export const needsL1SecurityFeeChains = backendNetworks.networks - .filter((backendNetwork: BackendNetwork) => backendNetwork.opStack) - .map((backendNetwork: BackendNetwork) => parseInt(backendNetwork.id, 10)); - -export const chainsNativeAsset: Record = backendNetworks.networks.reduce( - (acc, backendNetwork: BackendNetwork) => { - acc[parseInt(backendNetwork.id, 10)] = backendNetwork.nativeAsset; - return acc; - }, - {} as Record -); - -export const chainsLabel: Record = backendNetworks.networks.reduce( - (acc, backendNetwork: BackendNetwork) => { - acc[parseInt(backendNetwork.id, 10)] = backendNetwork.label; - return acc; - }, - {} as Record -); - -export const chainsPrivateMempoolTimeout: Record = backendNetworks.networks.reduce( - (acc, backendNetwork: BackendNetwork) => { - acc[parseInt(backendNetwork.id, 10)] = backendNetwork.privateMempoolTimeout || DEFAULT_PRIVATE_MEMPOOL_TIMEOUT; - return acc; - }, - {} as Record -); - -export const chainsName: Record = backendNetworks.networks.reduce( - (acc, backendNetwork: BackendNetwork) => { - acc[parseInt(backendNetwork.id, 10)] = backendNetwork.name; - return acc; - }, - {} as Record -); - -export const chainsIdByName: Record = backendNetworks.networks.reduce( - (acc, backendNetwork: BackendNetwork) => { - acc[backendNetwork.name] = parseInt(backendNetwork.id, 10); - return acc; - }, - {} as Record -); - -const defaultGasSpeeds = (chainId: ChainId) => { - switch (chainId) { - case ChainId.bsc: - case ChainId.goerli: - case ChainId.polygon: - return [gasUtils.NORMAL, gasUtils.FAST, gasUtils.URGENT]; - case ChainId.gnosis: - return [gasUtils.NORMAL]; - default: - return [gasUtils.NORMAL, gasUtils.FAST, gasUtils.URGENT, gasUtils.CUSTOM]; - } -}; - -export const chainsGasSpeeds: Record = backendNetworks.networks.reduce( - (acc, backendNetwork: BackendNetwork) => { - acc[parseInt(backendNetwork.id, 10)] = defaultGasSpeeds(parseInt(backendNetwork.id, 10)); - return acc; - }, - {} as Record -); - -const defaultPollingIntervals = (chainId: ChainId) => { - switch (chainId) { - case ChainId.polygon: - return 2_000; - case ChainId.arbitrum: - case ChainId.bsc: - return 3_000; - default: - return 5_000; - } -}; - -export const chainsSwapPollingInterval: Record = backendNetworks.networks.reduce( - (acc, backendNetwork: BackendNetwork) => { - acc[parseInt(backendNetwork.id, 10)] = defaultPollingIntervals(parseInt(backendNetwork.id, 10)); - return acc; - }, - {} as Record -); - -const defaultSimplehashNetworks = (chainId: ChainId) => { - switch (chainId) { - case ChainId.apechain: - return 'apechain'; - case ChainId.arbitrum: - return 'arbitrum'; - case ChainId.avalanche: - return 'avalanche'; - case ChainId.base: - return 'base'; - case ChainId.blast: - return 'blast'; - case ChainId.bsc: - return 'bsc'; - case ChainId.degen: - return 'degen'; - case ChainId.gnosis: - return 'gnosis'; - case ChainId.goerli: - return 'ethereum-goerli'; - case ChainId.mainnet: - return 'ethereum'; - case ChainId.optimism: - return 'optimism'; - case ChainId.polygon: - return 'polygon'; - case ChainId.zora: - return 'zora'; - default: - return ''; - } -}; - -export const chainsSimplehashNetwork: Record = backendNetworks.networks.reduce( - (acc, backendNetwork: BackendNetwork) => { - acc[parseInt(backendNetwork.id, 10)] = defaultSimplehashNetworks(parseInt(backendNetwork.id, 10)); - return acc; - }, - {} as Record -); - -const filterChainIdsByService = (servicePath: (services: BackendNetworkServices) => boolean): number[] => { - return backendNetworks.networks - .filter((network: BackendNetwork) => { - const services = network?.enabledServices; - return services && servicePath(services); - }) - .map((network: BackendNetwork) => parseInt(network.id, 10)); -}; - -export const meteorologySupportedChainIds = filterChainIdsByService(services => services.meteorology.enabled); - -export const supportedSwapChainIds = filterChainIdsByService(services => services.swap.enabled); - -export const supportedSwapExactOutputChainIds = filterChainIdsByService(services => services.swap.swapExactOutput); - -export const supportedBridgeExactOutputChainIds = filterChainIdsByService(services => services.swap.bridgeExactOutput); - -export const supportedNotificationsChainIds = filterChainIdsByService(services => services.notifications.enabled); - -export const supportedApprovalsChainIds = filterChainIdsByService(services => services.addys.approvals); - -export const supportedTransactionsChainIds = filterChainIdsByService(services => services.addys.transactions); - -export const supportedAssetsChainIds = filterChainIdsByService(services => services.addys.assets); - -export const supportedPositionsChainIds = filterChainIdsByService(services => services.addys.positions); - -export const supportedTokenSearchChainIds = filterChainIdsByService(services => services.tokenSearch.enabled); - -export const supportedNftChainIds = filterChainIdsByService(services => services.nftProxy.enabled); - -export const shouldDefaultToFastGasChainIds = [ChainId.mainnet, ChainId.polygon, ChainId.goerli]; - -const chainsGasUnits = backendNetworks.networks.reduce( - (acc, backendNetwork: BackendNetwork) => { - acc[parseInt(backendNetwork.id, 10)] = backendNetwork.gasUnits; - return acc; - }, - {} as Record -); - -export const getChainGasUnits = (chainId?: number) => { - return (chainId ? chainsGasUnits[chainId] : undefined) || chainsGasUnits[ChainId.mainnet]; -}; - -export const getChainDefaultRpc = (chainId: ChainId) => { - switch (chainId) { - case ChainId.mainnet: - return useConnectedToHardhatStore.getState().connectedToHardhat - ? chainHardhat.rpcUrls.default.http[0] - : defaultChains[ChainId.mainnet].rpcUrls.default.http[0]; - default: - return defaultChains[chainId].rpcUrls.default.http[0]; - } -}; diff --git a/src/components/AddFundsInterstitial.js b/src/components/AddFundsInterstitial.js index 01128c72803..4a7c315a0b6 100644 --- a/src/components/AddFundsInterstitial.js +++ b/src/components/AddFundsInterstitial.js @@ -19,7 +19,7 @@ import styled from '@/styled-thing'; import { padding, position } from '@/styles'; import ShadowStack from '@/react-native-shadow-stack'; import { useRoute } from '@react-navigation/native'; -import { Network } from '@/chains/types'; +import { Network } from '@/state/backendNetworks/types'; const ContainerWidth = 261; diff --git a/src/components/BackendNetworks.tsx b/src/components/BackendNetworks.tsx index 9ff6747ddd2..ba1d9e6337f 100644 --- a/src/components/BackendNetworks.tsx +++ b/src/components/BackendNetworks.tsx @@ -1,7 +1,12 @@ import { useBackendNetworks } from '@/resources/metadata/backendNetworks'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; export const BackendNetworks = () => { - useBackendNetworks(); + useBackendNetworks({ + onSuccess(data) { + useBackendNetworksStore.getState().setBackendNetworks(data); + }, + }); return null; }; diff --git a/src/components/ChainLogo.js b/src/components/ChainLogo.js index 04b56e1e2b4..3c7efd4c8ff 100644 --- a/src/components/ChainLogo.js +++ b/src/components/ChainLogo.js @@ -21,7 +21,7 @@ import BaseBadgeNoShadow from '../assets/badges/baseBadgeNoShadow.png'; import { Centered } from './layout'; import styled from '@/styled-thing'; import { position } from '@/styles'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; const ChainIcon = styled(FastImage)({ height: ({ size }) => size, diff --git a/src/components/DappBrowser/control-panel/ControlPanel.tsx b/src/components/DappBrowser/control-panel/ControlPanel.tsx index f70f9d9a1b0..99c24dd0597 100644 --- a/src/components/DappBrowser/control-panel/ControlPanel.tsx +++ b/src/components/DappBrowser/control-panel/ControlPanel.tsx @@ -58,8 +58,8 @@ import { addressSetSelected, walletsSetSelected } from '@/redux/wallets'; import { swapsStore } from '@/state/swaps/swapsStore'; import { userAssetsStore } from '@/state/assets/userAssets'; import { greaterThan } from '@/helpers/utilities'; -import { ChainId } from '@/chains/types'; -import { chainsLabel, defaultChains } from '@/chains'; +import { ChainId } from '@/state/backendNetworks/types'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; const PAGES = { HOME: 'home', @@ -182,12 +182,12 @@ export const ControlPanel = () => { const { testnetsEnabled } = store.getState().settings; const allNetworkItems = useMemo(() => { - return Object.values(defaultChains) + return Object.values(useBackendNetworksStore.getState().getDefaultChains()) .filter(({ testnet }) => testnetsEnabled || !testnet) .map(chain => { return { IconComponent: , - label: chainsLabel[chain.id], + label: useBackendNetworksStore.getState().getChainsLabel()[chain.id], secondaryLabel: i18n.t( isConnected && chain.id === currentChainId ? i18n.l.dapp_browser.control_panel.connected diff --git a/src/components/DappBrowser/handleProviderRequest.ts b/src/components/DappBrowser/handleProviderRequest.ts index c8466c853a4..c56b9e5cd17 100644 --- a/src/components/DappBrowser/handleProviderRequest.ts +++ b/src/components/DappBrowser/handleProviderRequest.ts @@ -11,8 +11,8 @@ import { Tab } from '@rainbow-me/provider/dist/references/messengers'; import { getDappMetadata } from '@/resources/metadata/dapp'; import { useAppSessionsStore } from '@/state/appSessions'; import { BigNumber } from '@ethersproject/bignumber'; -import { ChainId } from '@/chains/types'; -import { chainsNativeAsset, defaultChains, SUPPORTED_CHAIN_IDS } from '@/chains'; +import { ChainId } from '@/state/backendNetworks/types'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; export type ProviderRequestPayload = RequestArguments & { id: number; @@ -162,7 +162,10 @@ const messengerProviderRequestFn = async (messenger: Messenger, request: Provide const isSupportedChainId = (chainId: number | string) => { const numericChainId = BigNumber.from(chainId).toNumber(); - return !!SUPPORTED_CHAIN_IDS.find(chainId => chainId === numericChainId); + return !!useBackendNetworksStore + .getState() + .getSupportedChainIds() + .find(chainId => chainId === numericChainId); }; const getActiveSession = ({ host }: { host: string }): ActiveSession => { const hostSessions = useAppSessionsStore.getState().getActiveSession({ host }); @@ -265,7 +268,7 @@ export const handleProviderRequestApp = ({ messenger, data, meta }: { messenger: callbackOptions?: CallbackOptions; }): { chainAlreadyAdded: boolean } => { const { chainId } = proposedChain; - if (defaultChains[Number(chainId)]) { + if (useBackendNetworksStore.getState().getDefaultChains()[Number(chainId)]) { // TODO - Open add / switch ethereum chain return { chainAlreadyAdded: true }; } else { @@ -320,7 +323,7 @@ export const handleProviderRequestApp = ({ messenger, data, meta }: { messenger: callbackOptions?: CallbackOptions; }) => { const { chainId } = proposedChain; - const supportedChainId = SUPPORTED_CHAIN_IDS.includes(Number(chainId)); + const supportedChainId = useBackendNetworksStore.getState().getSupportedChainIds().includes(Number(chainId)); if (supportedChainId) { const host = getDappHost(callbackOptions?.sender.url) || ''; const activeSession = getActiveSession({ host }); @@ -343,7 +346,7 @@ export const handleProviderRequestApp = ({ messenger, data, meta }: { messenger: onSwitchEthereumChainSupported, getProvider, getActiveSession, - getChainNativeCurrency: chainId => chainsNativeAsset[chainId], + getChainNativeCurrency: chainId => useBackendNetworksStore.getState().getChainsNativeAsset()[chainId], }); // @ts-ignore diff --git a/src/components/Discover/DiscoverSearch.tsx b/src/components/Discover/DiscoverSearch.tsx index 1c3ded34286..92495a2100d 100644 --- a/src/components/Discover/DiscoverSearch.tsx +++ b/src/components/Discover/DiscoverSearch.tsx @@ -18,13 +18,13 @@ import { ethereumUtils, safeAreaInsetValues } from '@/utils'; import { getPoapAndOpenSheetWithQRHash, getPoapAndOpenSheetWithSecretWord } from '@/utils/poaps'; import { navigateToMintCollection } from '@/resources/reservoir/mints'; import { TAB_BAR_HEIGHT } from '@/navigation/SwipeNavigator'; -import { ChainId, Network } from '@/chains/types'; -import { chainsIdByName } from '@/chains'; import { navbarHeight } from '@/components/navbar/Navbar'; import { IS_TEST } from '@/env'; import { uniqBy } from 'lodash'; import { useTheme } from '@/theme'; import { EnrichedExchangeAsset } from '@/components/ExchangeAssetList'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; +import { ChainId, Network } from '@/state/backendNetworks/types'; export const SearchContainer = styled(Row)({ height: '100%', @@ -71,7 +71,7 @@ export default function DiscoverSearch() { const lastSearchQuery = usePrevious(searchQueryForSearch); const [ensResults, setEnsResults] = useState([]); - const { swapCurrencyList, swapCurrencyListLoading } = useSearchCurrencyList(searchQueryForSearch, ChainId.mainnet, true); + const { swapCurrencyList, swapCurrencyListLoading } = useSearchCurrencyList(searchQueryForSearch, ChainId.mainnet); const profilesEnabled = useExperimentalFlag(PROFILES); const marginBottom = TAB_BAR_HEIGHT + safeAreaInsetValues.bottom + 16; @@ -148,7 +148,7 @@ export default function DiscoverSearch() { const mintdotfunURL = seachQueryForMint.split('https://mint.fun/'); const query = mintdotfunURL[1]; const [networkName] = query.split('/'); - let chainId = chainsIdByName[networkName]; + let chainId = useBackendNetworksStore.getState().getChainsIdByName()[networkName]; if (!chainId) { switch (networkName) { case 'op': diff --git a/src/components/Discover/DiscoverSearchInput.tsx b/src/components/Discover/DiscoverSearchInput.tsx index 2089602cac6..f8dc62ae696 100644 --- a/src/components/Discover/DiscoverSearchInput.tsx +++ b/src/components/Discover/DiscoverSearchInput.tsx @@ -12,10 +12,10 @@ import { ImgixImage } from '@/components/images'; import styled from '@/styled-thing'; import { margin, padding } from '@/styles'; import { deviceUtils } from '@/utils'; -import { chainsName } from '@/chains'; import { ThemeContextProps } from '@/theme'; -import { ChainId } from '@/chains/types'; import { useDiscoverScreenContext } from '@/components/Discover/DiscoverScreenContext'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; +import { ChainId } from '@/state/backendNetworks/types'; const SearchHeight = 40; const SearchWidth = deviceUtils.dimensions.width - 30; @@ -151,7 +151,7 @@ const DiscoverSearchInput = ({ const placeholder = useMemo(() => { if (!currentChainId) return placeholderText; return lang.t('button.exchange_search_placeholder_network', { - network: chainsName[currentChainId], + network: useBackendNetworksStore.getState().getChainsName()[currentChainId], }); }, [currentChainId, placeholderText]); diff --git a/src/components/ExchangeTokenRow.tsx b/src/components/ExchangeTokenRow.tsx index 2b70fe2d35a..0e509debbe4 100644 --- a/src/components/ExchangeTokenRow.tsx +++ b/src/components/ExchangeTokenRow.tsx @@ -9,7 +9,7 @@ import { IS_IOS } from '@/env'; import { FavStar, Info } from '@/components/asset-list/RecyclerAssetList2/FastComponents/FastCurrencySelectionRow'; import { View } from 'react-native'; import RainbowCoinIcon from '@/components/coin-icon/RainbowCoinIcon'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { ParsedAddressAsset } from '@/entities'; interface ExchangeTokenRowProps { diff --git a/src/components/L2Disclaimer.js b/src/components/L2Disclaimer.js index 3e44506a096..38305bcb90a 100644 --- a/src/components/L2Disclaimer.js +++ b/src/components/L2Disclaimer.js @@ -10,7 +10,7 @@ import { darkModeThemeColors } from '@/styles/colors'; import * as lang from '@/languages'; import { isL2Chain } from '@/handlers/web3'; import { EthCoinIcon } from './coin-icon/EthCoinIcon'; -import { chainsName } from '@/chains'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; const L2Disclaimer = ({ chainId, @@ -57,7 +57,7 @@ const L2Disclaimer = ({ ? customText : lang.t(lang.l.expanded_state.asset.l2_disclaimer, { symbol, - network: chainsName[chainId], + network: useBackendNetworksStore.getState().getChainsName()[chainId], })} diff --git a/src/components/Transactions/TransactionDetailsCard.tsx b/src/components/Transactions/TransactionDetailsCard.tsx index 96d0af6092b..8e90144e879 100644 --- a/src/components/Transactions/TransactionDetailsCard.tsx +++ b/src/components/Transactions/TransactionDetailsCard.tsx @@ -7,7 +7,7 @@ import { TextColor } from '@/design-system/color/palettes'; import { abbreviations, ethereumUtils } from '@/utils'; import { TransactionSimulationMeta } from '@/graphql/__generated__/metadataPOST'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { TransactionDetailsRow } from '@/components/Transactions/TransactionDetailsRow'; import { FadedScrollCard } from '@/components/FadedScrollCard'; @@ -20,7 +20,7 @@ import { CARD_BORDER_WIDTH, EXPANDED_CARD_TOP_INSET, } from '@/components/Transactions/constants'; -import { chainsName } from '@/chains'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; interface TransactionDetailsCardProps { chainId: ChainId; @@ -93,7 +93,13 @@ export const TransactionDetailsCard = ({ - {} + { + + } {!!(meta?.to?.address || toAddress || showTransferToRow) && ( {i18n.t(i18n.l.walletconnect.simulation.simulation_card.messages.need_more_native, { symbol: walletBalance?.symbol, - network: chainsName[chainId], + network: useBackendNetworksStore.getState().getChainsName()[chainId], })} ) : ( diff --git a/src/components/asset-list/RecyclerAssetList2/Claimable.tsx b/src/components/asset-list/RecyclerAssetList2/Claimable.tsx index c9e0f1b28c6..7bd7de3e92b 100644 --- a/src/components/asset-list/RecyclerAssetList2/Claimable.tsx +++ b/src/components/asset-list/RecyclerAssetList2/Claimable.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import { Box, Inline, Stack, Text } from '@/design-system'; import { useAccountSettings } from '@/hooks'; import { useClaimables } from '@/resources/addys/claimables/query'; @@ -11,7 +11,7 @@ import { convertAmountAndPriceToNativeDisplay, convertAmountToNativeDisplayWorkl import { analyticsV2 } from '@/analytics'; import { ChainBadge } from '@/components/coin-icon'; import { useNativeAsset } from '@/utils/ethereumUtils'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { usePoints } from '@/resources/points'; const RAINBOW_ICON_URL = 'https://rainbowme-res.cloudinary.com/image/upload/v1694722625/dapps/rainbow-icon-large.png'; diff --git a/src/components/asset-list/RecyclerAssetList2/FastComponents/FastBalanceCoinRow.tsx b/src/components/asset-list/RecyclerAssetList2/FastComponents/FastBalanceCoinRow.tsx index 4aea3213bd9..62a58656b2e 100644 --- a/src/components/asset-list/RecyclerAssetList2/FastComponents/FastBalanceCoinRow.tsx +++ b/src/components/asset-list/RecyclerAssetList2/FastComponents/FastBalanceCoinRow.tsx @@ -12,7 +12,7 @@ import Routes from '@/navigation/routesNames'; import { borders, colors, padding, shadow } from '@/styles'; import RainbowCoinIcon from '@/components/coin-icon/RainbowCoinIcon'; import { NativeCurrencyKey } from '@/entities'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; interface CoinCheckButtonProps { isHidden: boolean; diff --git a/src/components/asset-list/RecyclerAssetList2/FastComponents/FastCoinBadge.tsx b/src/components/asset-list/RecyclerAssetList2/FastComponents/FastCoinBadge.tsx index 5eba92ba6fa..dd948ecd527 100644 --- a/src/components/asset-list/RecyclerAssetList2/FastComponents/FastCoinBadge.tsx +++ b/src/components/asset-list/RecyclerAssetList2/FastComponents/FastCoinBadge.tsx @@ -20,7 +20,7 @@ import DegenBadge from '@/assets/badges/degenBadge.png'; import DegenBadgeDark from '@/assets/badges/degenBadgeDark.png'; import ApechainBadge from '@/assets/badges/apechainBadge.png'; import ApechainBadgeDark from '@/assets/badges/apechainBadgeDark.png'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; interface FastChainBadgeProps { chainId: ChainId; diff --git a/src/components/asset-list/RecyclerAssetList2/FastComponents/FastCurrencySelectionRow.tsx b/src/components/asset-list/RecyclerAssetList2/FastComponents/FastCurrencySelectionRow.tsx index d9c41c51347..d122b9a622e 100644 --- a/src/components/asset-list/RecyclerAssetList2/FastComponents/FastCurrencySelectionRow.tsx +++ b/src/components/asset-list/RecyclerAssetList2/FastComponents/FastCurrencySelectionRow.tsx @@ -13,7 +13,7 @@ import { colors, fonts, fontWithWidth, getFontSize } from '@/styles'; import { deviceUtils } from '@/utils'; import RainbowCoinIcon from '@/components/coin-icon/RainbowCoinIcon'; import { useExternalToken } from '@/resources/assets/externalAssetsQuery'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; const SafeRadialGradient = (IS_TESTING === 'true' ? View : RadialGradient) as typeof RadialGradient; diff --git a/src/components/cards/EthCard.tsx b/src/components/cards/EthCard.tsx index 17719704d4e..b4504d62421 100644 --- a/src/components/cards/EthCard.tsx +++ b/src/components/cards/EthCard.tsx @@ -22,7 +22,7 @@ import * as i18n from '@/languages'; import { ButtonPressAnimationTouchEvent } from '@/components/animations/ButtonPressAnimation/types'; import { useExternalToken } from '@/resources/assets/externalAssetsQuery'; import assetTypes from '@/entities/assetTypes'; -import { Network, ChainId } from '@/chains/types'; +import { Network, ChainId } from '@/state/backendNetworks/types'; import { getUniqueId } from '@/utils/ethereumUtils'; import { EthCoinIcon } from '../coin-icon/EthCoinIcon'; diff --git a/src/components/cards/NFTOffersCard/Offer.tsx b/src/components/cards/NFTOffersCard/Offer.tsx index c9c0afd6f64..85452eab6c6 100644 --- a/src/components/cards/NFTOffersCard/Offer.tsx +++ b/src/components/cards/NFTOffersCard/Offer.tsx @@ -21,9 +21,9 @@ import Svg, { Path } from 'react-native-svg'; import RainbowCoinIcon from '@/components/coin-icon/RainbowCoinIcon'; import { useExternalToken } from '@/resources/assets/externalAssetsQuery'; import { useAccountSettings } from '@/hooks'; -import { Network } from '@/chains/types'; +import { Network } from '@/state/backendNetworks/types'; import { AddressOrEth } from '@/__swaps__/types/assets'; -import { chainsIdByName } from '@/chains'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; const TWO_HOURS_MS = 2 * 60 * 60 * 1000; export const CELL_HORIZONTAL_PADDING = 7; @@ -68,7 +68,7 @@ export const Offer = ({ offer }: { offer: NftOffer }) => { const { colorMode } = useColorMode(); const theme = useTheme(); const { nativeCurrency } = useAccountSettings(); - const offerChainId = chainsIdByName[offer.network as Network]; + const offerChainId = useBackendNetworksStore.getState().getChainsIdByName()[offer.network as Network]; const { data: externalAsset } = useExternalToken({ address: offer.paymentToken.address as AddressOrEth, chainId: offerChainId, diff --git a/src/components/cards/OpRewardsCard.tsx b/src/components/cards/OpRewardsCard.tsx index 9c6c1ca601b..3c1251010b3 100644 --- a/src/components/cards/OpRewardsCard.tsx +++ b/src/components/cards/OpRewardsCard.tsx @@ -8,7 +8,7 @@ import * as i18n from '@/languages'; import { useNavigation } from '@/navigation'; import Routes from '@/navigation/routesNames'; import { colors } from '@/styles'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; const GRADIENT: Gradient = { colors: ['#520907', '#B22824'], diff --git a/src/components/change-wallet/WalletList.tsx b/src/components/change-wallet/WalletList.tsx index ba23599e224..e3c858761f2 100644 --- a/src/components/change-wallet/WalletList.tsx +++ b/src/components/change-wallet/WalletList.tsx @@ -19,7 +19,7 @@ import { position } from '@/styles'; import { EditWalletContextMenuActions } from '@/screens/ChangeWalletSheet'; import { getExperimetalFlag, HARDWARE_WALLETS, useExperimentalFlag } from '@/config'; import { Inset, Stack } from '@/design-system'; -import { Network } from '@/chains/types'; +import { Network } from '@/state/backendNetworks/types'; import { SheetTitle } from '../sheet'; import ButtonPressAnimation from '@/components/animations/ButtonPressAnimation'; import { IS_ANDROID, IS_IOS } from '@/env'; diff --git a/src/components/coin-icon/ChainBadge.js b/src/components/coin-icon/ChainBadge.js index 7aa789def16..0eafa651841 100644 --- a/src/components/coin-icon/ChainBadge.js +++ b/src/components/coin-icon/ChainBadge.js @@ -44,7 +44,7 @@ import { Centered } from '../layout'; import styled from '@/styled-thing'; import { position as positions } from '@/styles'; import { ChainBadgeSizeConfigs } from '@/components/coin-icon/ChainBadgeSizeConfigs'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; const ChainIcon = styled(FastImage)({ height: ({ containerSize }) => containerSize, diff --git a/src/components/coin-icon/ChainIcon.js b/src/components/coin-icon/ChainIcon.js index 626ef5f53d7..2cf0988854a 100644 --- a/src/components/coin-icon/ChainIcon.js +++ b/src/components/coin-icon/ChainIcon.js @@ -10,7 +10,7 @@ import BscBadge from '../../assets/badges/bscBadge.png'; import BscBadgeDark from '../../assets/badges/bscBadgeDark.png'; import { Centered } from '../layout'; import styled from '@/styled-thing'; -import { Network } from '@/chains/types'; +import { Network } from '@/state/backendNetworks/types'; const sizeConfigs = { large: { diff --git a/src/components/coin-icon/ChainImage.tsx b/src/components/coin-icon/ChainImage.tsx index ed7cb9301e2..4cd54a33f34 100644 --- a/src/components/coin-icon/ChainImage.tsx +++ b/src/components/coin-icon/ChainImage.tsx @@ -1,5 +1,5 @@ -import React, { forwardRef, useMemo } from 'react'; -import { ChainId } from '@/chains/types'; +import React, { useMemo, forwardRef } from 'react'; +import { ChainId } from '@/state/backendNetworks/types'; import ArbitrumBadge from '@/assets/badges/arbitrum.png'; import BaseBadge from '@/assets/badges/base.png'; diff --git a/src/components/coin-icon/EthCoinIcon.tsx b/src/components/coin-icon/EthCoinIcon.tsx index a1b23be263f..3d9aa81a1c4 100644 --- a/src/components/coin-icon/EthCoinIcon.tsx +++ b/src/components/coin-icon/EthCoinIcon.tsx @@ -3,7 +3,7 @@ import { useTheme } from '@/theme'; import { useNativeAsset } from '@/utils/ethereumUtils'; import RainbowCoinIcon from './RainbowCoinIcon'; import { ETH_SYMBOL } from '@/references'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; type EthCoinIconProps = { size?: number; diff --git a/src/components/coin-icon/RainbowCoinIcon.tsx b/src/components/coin-icon/RainbowCoinIcon.tsx index f47cfb57a07..118c9445f42 100644 --- a/src/components/coin-icon/RainbowCoinIcon.tsx +++ b/src/components/coin-icon/RainbowCoinIcon.tsx @@ -7,7 +7,7 @@ import { FallbackIcon as CoinIconTextFallback } from '@/utils'; import { FastFallbackCoinIconImage } from '../asset-list/RecyclerAssetList2/FastComponents/FastFallbackCoinIconImage'; import { FastChainBadge } from '../asset-list/RecyclerAssetList2/FastComponents/FastCoinBadge'; import { TokenColors } from '@/graphql/__generated__/metadata'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; const fallbackTextStyles = { fontFamily: fonts.family.SFProRounded, diff --git a/src/components/coin-icon/TwoCoinsIcon.tsx b/src/components/coin-icon/TwoCoinsIcon.tsx index e84d15062cc..d70e91b6d75 100644 --- a/src/components/coin-icon/TwoCoinsIcon.tsx +++ b/src/components/coin-icon/TwoCoinsIcon.tsx @@ -4,7 +4,7 @@ import { ParsedAddressAsset } from '@/entities'; import { useTheme } from '@/theme'; import ChainBadge from './ChainBadge'; import RainbowCoinIcon from './RainbowCoinIcon'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; export function TwoCoinsIcon({ size = 45, diff --git a/src/components/coin-row/FastTransactionCoinRow.tsx b/src/components/coin-row/FastTransactionCoinRow.tsx index 588255a4409..a7764a02d04 100644 --- a/src/components/coin-row/FastTransactionCoinRow.tsx +++ b/src/components/coin-row/FastTransactionCoinRow.tsx @@ -10,7 +10,7 @@ import Routes from '@rainbow-me/routes'; import { ImgixImage } from '../images'; import { CardSize } from '../unique-token/CardSize'; import { ChainBadge } from '../coin-icon'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { address } from '@/utils/abbreviations'; import { convertAmountAndPriceToNativeDisplay, diff --git a/src/components/context-menu-buttons/ChainContextMenu.tsx b/src/components/context-menu-buttons/ChainContextMenu.tsx index 3492fbf5eb7..d323684cc12 100644 --- a/src/components/context-menu-buttons/ChainContextMenu.tsx +++ b/src/components/context-menu-buttons/ChainContextMenu.tsx @@ -5,8 +5,8 @@ import { Bleed, Box, Inline, Text, TextProps } from '@/design-system'; import * as i18n from '@/languages'; import { useUserAssetsStore } from '@/state/assets/userAssets'; import { showActionSheetWithOptions } from '@/utils'; -import { ChainId } from '@/chains/types'; -import { chainsLabel, chainsName } from '@/chains'; +import { ChainId } from '@/state/backendNetworks/types'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; interface DefaultButtonOptions { iconColor?: TextProps['color']; @@ -56,12 +56,13 @@ export const ChainContextMenu = ({ const menuConfig = useMemo(() => { const chainItems = balanceSortedChains.map(chainId => { + const chainName = useBackendNetworksStore.getState().getChainsName()[chainId]; return { actionKey: `${chainId}`, - actionTitle: chainsLabel[chainId], + actionTitle: useBackendNetworksStore.getState().getChainsLabel()[chainId], icon: { iconType: 'ASSET', - iconValue: chainId === ChainId.mainnet ? 'ethereumBadge' : `${chainsName[chainId]}BadgeNoShadow`, + iconValue: chainId === ChainId.mainnet ? 'ethereumBadge' : `${chainName}BadgeNoShadow`, }, }; }); @@ -111,7 +112,7 @@ export const ChainContextMenu = ({ const displayName = useMemo(() => { if (!selectedChainId) return allNetworksText; - const name = chainsLabel[selectedChainId]; + const name = useBackendNetworksStore.getState().getChainsLabel()[selectedChainId]; return name.endsWith(' Chain') ? name.slice(0, -6) : name; }, [allNetworksText, selectedChainId]); diff --git a/src/components/ens-profile/ActionButtons/MoreButton.tsx b/src/components/ens-profile/ActionButtons/MoreButton.tsx index 2d161ceddc5..af0a3c83e3a 100644 --- a/src/components/ens-profile/ActionButtons/MoreButton.tsx +++ b/src/components/ens-profile/ActionButtons/MoreButton.tsx @@ -11,7 +11,7 @@ import { RAINBOW_PROFILES_BASE_URL } from '@/references'; import Routes from '@/navigation/routesNames'; import { ethereumUtils } from '@/utils'; import { formatAddressForDisplay } from '@/utils/abbreviations'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; const ACTIONS = { ADD_CONTACT: 'add-contact', diff --git a/src/components/exchangeAssetRowContextMenuProps.ts b/src/components/exchangeAssetRowContextMenuProps.ts index e16a28ec471..cc0cb066edd 100644 --- a/src/components/exchangeAssetRowContextMenuProps.ts +++ b/src/components/exchangeAssetRowContextMenuProps.ts @@ -3,8 +3,8 @@ import { startCase } from 'lodash'; import { NativeSyntheticEvent } from 'react-native'; import { setClipboard } from '@/hooks/useClipboard'; import { abbreviations, ethereumUtils, haptics, showActionSheetWithOptions } from '@/utils'; -import { ChainId } from '@/chains/types'; -import { chainsIdByName } from '@/chains'; +import { ChainId } from '@/state/backendNetworks/types'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; const buildBlockExplorerAction = (chainId: ChainId) => { const blockExplorerText = lang.t('exchange.coin_row.view_on', { @@ -44,7 +44,7 @@ export default function contextMenuProps(item: any, onCopySwapDetailsText: (addr }; const onPressAndroid = () => { - const blockExplorerText = `View on ${startCase(ethereumUtils.getBlockExplorer({ chainId: chainsIdByName[item?.network] }))}`; + const blockExplorerText = `View on ${startCase(ethereumUtils.getBlockExplorer({ chainId: useBackendNetworksStore.getState().getChainsIdByName()[item?.network] }))}`; const androidContractActions = [lang.t('wallet.action.copy_contract_address'), blockExplorerText, lang.t('button.cancel')]; showActionSheetWithOptions( @@ -59,13 +59,16 @@ export default function contextMenuProps(item: any, onCopySwapDetailsText: (addr handleCopyContractAddress(item?.address); } if (idx === 1) { - ethereumUtils.openTokenEtherscanURL({ address: item?.address, chainId: chainsIdByName[item?.network] }); + ethereumUtils.openTokenEtherscanURL({ + address: item?.address, + chainId: useBackendNetworksStore.getState().getChainsIdByName()[item?.network], + }); } } ); }; - const blockExplorerAction = buildBlockExplorerAction(chainsIdByName[item?.network]); + const blockExplorerAction = buildBlockExplorerAction(useBackendNetworksStore.getState().getChainsIdByName()[item?.network]); const menuConfig = { menuItems: [ blockExplorerAction, @@ -81,7 +84,10 @@ export default function contextMenuProps(item: any, onCopySwapDetailsText: (addr if (actionKey === CoinRowActionsEnum.copyAddress) { handleCopyContractAddress(item?.address); } else if (actionKey === CoinRowActionsEnum.blockExplorer) { - ethereumUtils.openTokenEtherscanURL({ address: item?.address, chainId: chainsIdByName[item?.network] }); + ethereumUtils.openTokenEtherscanURL({ + address: item?.address, + chainId: useBackendNetworksStore.getState().getChainsIdByName()[item?.network], + }); } }; return { diff --git a/src/components/expanded-state/AvailableNetworks.js b/src/components/expanded-state/AvailableNetworks.js index 29ac98354ca..5b469892f3d 100644 --- a/src/components/expanded-state/AvailableNetworks.js +++ b/src/components/expanded-state/AvailableNetworks.js @@ -13,8 +13,8 @@ import { ChainBadge } from '../coin-icon'; import Divider from '@/components/Divider'; import { Text } from '../text'; import { EthCoinIcon } from '../coin-icon/EthCoinIcon'; -import { ChainId } from '@/chains/types'; -import { defaultChains } from '@/chains'; +import { ChainId } from '@/state/backendNetworks/types'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; const AvailableNetworksv1 = ({ asset, networks, hideDivider, marginBottom = 24, marginHorizontal = 19, prominent }) => { const { colors } = useTheme(); @@ -87,7 +87,7 @@ const AvailableNetworksv1 = ({ asset, networks, hideDivider, marginBottom = 24, availableNetworks: availableChainIds?.length, }) : lang.t('expanded_state.asset.available_network', { - availableNetwork: defaultChains[availableChainIds[0]]?.name, + availableNetwork: useBackendNetworksStore.getState().getChainsName()[availableChainIds[0]], })} diff --git a/src/components/expanded-state/AvailableNetworksv2.tsx b/src/components/expanded-state/AvailableNetworksv2.tsx index 0f07a751d1a..b597edf481a 100644 --- a/src/components/expanded-state/AvailableNetworksv2.tsx +++ b/src/components/expanded-state/AvailableNetworksv2.tsx @@ -21,8 +21,8 @@ import { parseSearchAsset } from '@/__swaps__/utils/assets'; import { AddressOrEth, AssetType } from '@/__swaps__/types/assets'; import { swapsStore } from '@/state/swaps/swapsStore'; import { InteractionManager } from 'react-native'; -import { ChainId } from '@/chains/types'; -import { chainsLabel, chainsName, defaultChains, supportedSwapChainIds } from '@/chains'; +import { ChainId } from '@/state/backendNetworks/types'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; const NOOP = () => null; @@ -68,6 +68,7 @@ const AvailableNetworksv2 = ({ goBack(); const uniqueId = `${newAsset.address}_${asset.chainId}`; + const chainsName = useBackendNetworksStore.getState().getChainsName(); const userAsset = userAssetsStore.getState().userAssets.get(uniqueId); const parsedAsset = parseSearchAsset({ @@ -134,15 +135,17 @@ const AvailableNetworksv2 = ({ convertAssetAndNavigate(availableChainIds[0]); }, [availableChainIds, convertAssetAndNavigate]); - const networkMenuItems = supportedSwapChainIds - .filter(chainId => chainId !== ChainId.mainnet && availableChainIds.includes(chainId)) - .map(chainId => defaultChains[chainId]) + const networkMenuItems = useBackendNetworksStore + .getState() + .getSupportedChainIds() + .filter(chainId => chainId !== ChainId.mainnet) + .map(chainId => useBackendNetworksStore.getState().getDefaultChains()[chainId]) .map(chain => ({ actionKey: `${chain.id}`, - actionTitle: chainsLabel[chain.id], + actionTitle: useBackendNetworksStore.getState().getChainsLabel()[chain.id], icon: { iconType: 'ASSET', - iconValue: `${chainsName[chain.id]}Badge${chain.id === ChainId.mainnet ? '' : 'NoShadow'}`, + iconValue: `${useBackendNetworksStore.getState().getChainsName()[chain.id]}Badge${chain.id === ChainId.mainnet ? '' : 'NoShadow'}`, }, })); @@ -204,7 +207,7 @@ const AvailableNetworksv2 = ({ availableNetworks: availableChainIds?.length, }) : lang.t('expanded_state.asset.available_networkv2', { - availableNetwork: chainsName[availableChainIds[0]], + availableNetwork: useBackendNetworksStore.getState().getChainsName()[availableChainIds[0]], })} diff --git a/src/components/expanded-state/UniqueTokenExpandedState.tsx b/src/components/expanded-state/UniqueTokenExpandedState.tsx index a16b843bfa5..a321d1d8ac4 100644 --- a/src/components/expanded-state/UniqueTokenExpandedState.tsx +++ b/src/components/expanded-state/UniqueTokenExpandedState.tsx @@ -65,7 +65,7 @@ import { buildRainbowUrl } from '@/utils/buildRainbowUrl'; import isHttpUrl from '@/helpers/isHttpUrl'; import { useNFTOffers } from '@/resources/reservoir/nftOffersQuery'; import { convertAmountToNativeDisplay } from '@/helpers/utilities'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { useTimeoutEffect } from '@/hooks/useTimeout'; import { analyticsV2 } from '@/analytics'; import { getAddressAndChainIdFromUniqueId } from '@/utils/ethereumUtils'; diff --git a/src/components/expanded-state/asset/ChartExpandedState.js b/src/components/expanded-state/asset/ChartExpandedState.js index 74280105eaf..3bf7aff28ff 100644 --- a/src/components/expanded-state/asset/ChartExpandedState.js +++ b/src/components/expanded-state/asset/ChartExpandedState.js @@ -36,8 +36,8 @@ import { Box } from '@/design-system'; import { useExternalToken } from '@/resources/assets/externalAssetsQuery'; import { bigNumberFormat } from '@/helpers/bigNumberFormat'; import { greaterThanOrEqualTo } from '@/helpers/utilities'; -import { chainsName, supportedSwapChainIds } from '@/chains'; -import { ChainId } from '@/chains/types'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; +import { ChainId } from '@/state/backendNetworks/types'; import { useTimeoutEffect } from '@/hooks/useTimeout'; import { analyticsV2 } from '@/analytics'; @@ -168,7 +168,7 @@ export default function ChartExpandedState({ asset }) { chainId: asset.chainId, network: asset.network, address: asset.address, - mainnetAddress: asset?.networks?.[chainsName[ChainId.mainnet]]?.address, + mainnetAddress: asset?.networks?.[useBackendNetworksStore.getState().getChainsName()[ChainId.mainnet]]?.address, } : asset; }, [asset, genericAsset, hasBalance]); @@ -245,7 +245,7 @@ export default function ChartExpandedState({ asset }) { const assetChainId = assetWithPrice.chainId; const { swagg_enabled, f2c_enabled } = useRemoteConfig(); - const swapEnabled = swagg_enabled && supportedSwapChainIds.includes(assetChainId); + const swapEnabled = swagg_enabled && useBackendNetworksStore.getState().getSwapSupportedChainIds().includes(assetChainId); const addCashEnabled = f2c_enabled; const format = useCallback( diff --git a/src/components/expanded-state/unique-token/NFTBriefTokenInfoRow.tsx b/src/components/expanded-state/unique-token/NFTBriefTokenInfoRow.tsx index c437402f328..269fc3a8191 100644 --- a/src/components/expanded-state/unique-token/NFTBriefTokenInfoRow.tsx +++ b/src/components/expanded-state/unique-token/NFTBriefTokenInfoRow.tsx @@ -13,7 +13,7 @@ import { UniqueAsset } from '@/entities'; import { fetchReservoirNFTFloorPrice } from '@/resources/nfts/utils'; import { handleReviewPromptAction } from '@/utils/reviewAlert'; import { ReviewPromptAction } from '@/storage/schema'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; const NONE = 'None'; diff --git a/src/components/expanded-state/unique-token/UniqueTokenExpandedStateHeader.tsx b/src/components/expanded-state/unique-token/UniqueTokenExpandedStateHeader.tsx index 990158f132b..7e5126d1b84 100644 --- a/src/components/expanded-state/unique-token/UniqueTokenExpandedStateHeader.tsx +++ b/src/components/expanded-state/unique-token/UniqueTokenExpandedStateHeader.tsx @@ -21,7 +21,7 @@ import isSVGImage from '@/utils/isSVG'; import { refreshNFTContractMetadata, reportNFT } from '@/resources/nfts/simplehash'; import { ContextCircleButton } from '@/components/context-menu'; import { IS_ANDROID, IS_IOS } from '@/env'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; const AssetActionsEnum = { copyTokenID: 'copyTokenID', diff --git a/src/components/gas/GasSpeedButton.tsx b/src/components/gas/GasSpeedButton.tsx index 2f176a7e3d2..724ae269c32 100644 --- a/src/components/gas/GasSpeedButton.tsx +++ b/src/components/gas/GasSpeedButton.tsx @@ -25,8 +25,8 @@ import { gasUtils } from '@/utils'; import { IS_ANDROID } from '@/env'; import { ContextMenu } from '../context-menu'; import { EthCoinIcon } from '../coin-icon/EthCoinIcon'; -import { ChainId } from '@/chains/types'; -import { chainsGasSpeeds, chainsNativeAsset } from '@/chains'; +import { ChainId } from '@/state/backendNetworks/types'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; import { ThemeContextProps, useTheme } from '@/theme'; import { ParsedAddressAsset } from '@/entities'; import { GasSpeed } from '@/__swaps__/types/gas'; @@ -302,7 +302,7 @@ const GasSpeedButton = ({ const openGasHelper = useCallback(async () => { Keyboard.dismiss(); - const nativeAsset = chainsNativeAsset[chainId]; + const nativeAsset = useBackendNetworksStore.getState().getChainsNativeAsset()[chainId]; navigate(Routes.EXPLAIN_SHEET, { chainId, type: 'gas', @@ -336,7 +336,7 @@ const GasSpeedButton = ({ const speedOptions = useMemo(() => { if (speeds) return speeds; - return chainsGasSpeeds[chainId]; + return useBackendNetworksStore.getState().getChainsGasSpeeds()[chainId]; }, [chainId, speeds]); const menuConfig = useMemo(() => { diff --git a/src/components/positions/PositionsCard.tsx b/src/components/positions/PositionsCard.tsx index 197d5583fdc..a77dd48a78a 100644 --- a/src/components/positions/PositionsCard.tsx +++ b/src/components/positions/PositionsCard.tsx @@ -13,12 +13,12 @@ import { event } from '@/analytics/event'; import { IS_ANDROID } from '@/env'; import { capitalize, uniqBy } from 'lodash'; import { RainbowBorrow, RainbowClaimable, RainbowDeposit, RainbowPosition, RainbowStake } from '@/resources/defi/types'; -import { Network } from '@/chains/types'; +import { Network } from '@/state/backendNetworks/types'; import RainbowCoinIcon from '../coin-icon/RainbowCoinIcon'; import { useAccountSettings } from '@/hooks'; import { useExternalToken } from '@/resources/assets/externalAssetsQuery'; import { AddressOrEth } from '@/__swaps__/types/assets'; -import { chainsIdByName } from '@/chains'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; type PositionCardProps = { position: RainbowPosition; @@ -33,7 +33,7 @@ type CoinStackToken = { function CoinIconForStack({ token }: { token: CoinStackToken }) { const theme = useTheme(); const { nativeCurrency } = useAccountSettings(); - const chainId = chainsIdByName[token.network]; + const chainId = useBackendNetworksStore.getState().getChainsIdByName()[token.network]; const { data: externalAsset } = useExternalToken({ address: token.address as AddressOrEth, chainId, currency: nativeCurrency }); return ( diff --git a/src/components/remote-promo-sheet/check-fns/hasSwapTxn.ts b/src/components/remote-promo-sheet/check-fns/hasSwapTxn.ts index 738e33cdf58..17c4d22b04d 100644 --- a/src/components/remote-promo-sheet/check-fns/hasSwapTxn.ts +++ b/src/components/remote-promo-sheet/check-fns/hasSwapTxn.ts @@ -1,5 +1,5 @@ import type { EthereumAddress, RainbowTransaction } from '@/entities'; -import { SUPPORTED_MAINNET_CHAIN_IDS } from '@/chains'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; import { queryClient } from '@/react-query/queryClient'; import store from '@/redux/store'; import { consolidatedTransactionsQueryKey } from '@/resources/transactions/consolidatedTransactions'; @@ -16,7 +16,7 @@ export const hasSwapTxn = async (): Promise => { const paginatedTransactionsKey = consolidatedTransactionsQueryKey({ address: accountAddress, currency: nativeCurrency, - chainIds: SUPPORTED_MAINNET_CHAIN_IDS, + chainIds: useBackendNetworksStore.getState().getSupportedMainnetChainIds(), }); const queryData = queryClient.getQueryData(paginatedTransactionsKey); const pages = queryData?.pages || []; diff --git a/src/components/sheet/sheet-action-buttons/SwapActionButton.tsx b/src/components/sheet/sheet-action-buttons/SwapActionButton.tsx index 046467dd8a3..9a7ea341748 100644 --- a/src/components/sheet/sheet-action-buttons/SwapActionButton.tsx +++ b/src/components/sheet/sheet-action-buttons/SwapActionButton.tsx @@ -11,8 +11,8 @@ import { SwapAssetType } from '@/__swaps__/types/swap'; import { swapsStore } from '@/state/swaps/swapsStore'; import { InteractionManager } from 'react-native'; import { AddressOrEth, AssetType, ParsedSearchAsset } from '@/__swaps__/types/assets'; -import { chainsIdByName, chainsName } from '@/chains'; import useNavigationForNonReadOnlyWallets from '@/hooks/useNavigationForNonReadOnlyWallets'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; type SwapActionButtonProps = { asset: RainbowToken; @@ -29,7 +29,9 @@ function SwapActionButton({ asset, color: givenColor, inputType, label, weight = const color = givenColor || colors.swapPurple; const goToSwap = useCallback(async () => { - const chainId = asset.chainId || chainsIdByName[asset.network]; + const chainsIdByName = useBackendNetworksStore.getState().getChainsIdByName(); + const chainsName = useBackendNetworksStore.getState().getChainsName(); + const chainId = chainsIdByName[asset.network]; const uniqueId = `${asset.address}_${chainId}`; const userAsset = userAssetsStore.getState().userAssets.get(uniqueId); diff --git a/src/components/toasts/OfflineToast.js b/src/components/toasts/OfflineToast.js index b86c3904823..053791ce854 100644 --- a/src/components/toasts/OfflineToast.js +++ b/src/components/toasts/OfflineToast.js @@ -2,7 +2,7 @@ import lang from 'i18n-js'; import React from 'react'; import Toast from './Toast'; import { useAccountSettings, useInternetStatus } from '@/hooks'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; const OfflineToast = () => { diff --git a/src/components/toasts/TestnetToast.js b/src/components/toasts/TestnetToast.js index b31eb9befea..5618d0706d4 100644 --- a/src/components/toasts/TestnetToast.js +++ b/src/components/toasts/TestnetToast.js @@ -3,15 +3,15 @@ import { Icon } from '../icons'; import { Nbsp, Text } from '../text'; import Toast from './Toast'; import { useInternetStatus } from '@/hooks'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; -import { chainsName, chainsNativeAsset } from '@/chains'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; const TestnetToast = ({ chainId }) => { const { connectedToHardhat } = useConnectedToHardhatStore(); const isConnected = useInternetStatus(); - const nativeAsset = chainsNativeAsset[chainId]; - const name = chainsName[chainId]; + const nativeAsset = useBackendNetworksStore.getState().getChainsNativeAsset()[chainId]; + const name = useBackendNetworksStore.getState().getChainsName()[chainId]; const color = isDarkMode ? nativeAsset.colors.primary : nativeAsset.colors.fallback || nativeAsset.colors.primary; const [visible, setVisible] = useState(chainId !== ChainId.mainnet); const [networkName, setNetworkName] = useState(name); diff --git a/src/components/walletconnect-list/WalletConnectV2ListItem.tsx b/src/components/walletconnect-list/WalletConnectV2ListItem.tsx index 56a9d22b4cf..ee1fdf35660 100644 --- a/src/components/walletconnect-list/WalletConnectV2ListItem.tsx +++ b/src/components/walletconnect-list/WalletConnectV2ListItem.tsx @@ -24,8 +24,8 @@ import { changeAccount, disconnectSession } from '@/walletConnect'; import { Box, Inline } from '@/design-system'; import ChainBadge from '@/components/coin-icon/ChainBadge'; import { EthCoinIcon } from '../coin-icon/EthCoinIcon'; -import { ChainId } from '@/chains/types'; -import { SUPPORTED_CHAIN_IDS } from '@/chains'; +import { ChainId } from '@/state/backendNetworks/types'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; const CONTAINER_PADDING = 15; const VENDOR_LOGO_ICON_SIZE = 50; @@ -78,7 +78,9 @@ export function WalletConnectV2ListItem({ session, reload }: { session: SessionT const chains = useMemo(() => namespaces?.eip155?.chains || [], [namespaces]); const chainIds = useMemo( () => - (chains?.map(chain => parseInt(chain.split(':')[1]))?.filter(chainId => SUPPORTED_CHAIN_IDS.includes(chainId)) ?? []) as ChainId[], + chains + ?.map(chain => parseInt(chain.split(':')[1])) + ?.filter(chainId => useBackendNetworksStore.getState().getSupportedChainIds().includes(chainId)) ?? [], [chains] ); diff --git a/src/entities/tokens.ts b/src/entities/tokens.ts index c8f2eddc099..bd67a05eaeb 100644 --- a/src/entities/tokens.ts +++ b/src/entities/tokens.ts @@ -1,6 +1,6 @@ import { EthereumAddress } from '.'; import { Chain } from '@wagmi/chains'; -import { Network, ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { TokenColors } from '@/graphql/__generated__/metadata'; export interface ZerionAssetPrice { diff --git a/src/entities/transactions/transaction.ts b/src/entities/transactions/transaction.ts index 28c1f35252a..bf3b4b0265b 100644 --- a/src/entities/transactions/transaction.ts +++ b/src/entities/transactions/transaction.ts @@ -7,7 +7,7 @@ import { SwapType } from '@rainbow-me/swaps'; import { SwapMetadata } from '@/raps/references'; import { UniqueAsset } from '../uniqueAssets'; import { ParsedAsset, AddysAsset } from '@/resources/assets/types'; -import { ChainId, Network } from '@/chains/types'; +import { ChainId, Network } from '@/state/backendNetworks/types'; import { TransactionResponse } from '@ethersproject/providers'; import { BytesLike } from '@ethersproject/bytes'; diff --git a/src/entities/uniqueAssets.ts b/src/entities/uniqueAssets.ts index 78f9f5ad4b7..9876d2e971a 100644 --- a/src/entities/uniqueAssets.ts +++ b/src/entities/uniqueAssets.ts @@ -1,4 +1,4 @@ -import { Network, ChainId } from '@/chains/types'; +import { ChainId, Network } from '@/state/backendNetworks/types'; import { AssetContract, AssetType } from '.'; interface UniqueAssetLastSale { diff --git a/src/featuresToUnlock/tokenGatedUtils.ts b/src/featuresToUnlock/tokenGatedUtils.ts index 014f178732a..2b4b532ff8b 100644 --- a/src/featuresToUnlock/tokenGatedUtils.ts +++ b/src/featuresToUnlock/tokenGatedUtils.ts @@ -2,8 +2,8 @@ import { Contract } from '@ethersproject/contracts'; import { EthereumAddress } from '@/entities'; import { getProvider } from '@/handlers/web3'; import { tokenGateCheckerAbi } from '@/references'; -import { Network } from '@/chains/types'; -import { chainsIdByName } from '@/chains'; +import { Network } from '@/state/backendNetworks/types'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; export type TokenGateCheckerNetwork = | Network.arbitrum @@ -29,7 +29,7 @@ export const checkIfWalletsOwnNft = async ( network: TokenGateCheckerNetwork, walletsToCheck: EthereumAddress[] ) => { - const p = await getProvider({ chainId: chainsIdByName[network] }); + const p = getProvider({ chainId: useBackendNetworksStore.getState().getChainsIdByName()[network] }); const contractInstance = new Contract(TOKEN_GATE_CHECKER_ADDRESS[network], tokenGateCheckerAbi, p); diff --git a/src/handlers/assets.ts b/src/handlers/assets.ts index 1fb241b4e6f..64c7cf65f6f 100644 --- a/src/handlers/assets.ts +++ b/src/handlers/assets.ts @@ -2,13 +2,13 @@ import { Contract } from '@ethersproject/contracts'; import { erc20ABI } from '@/references'; import { convertAmountToBalanceDisplay, convertRawAmountToDecimalFormat } from '@/helpers/utilities'; -import { ChainId } from '@/chains/types'; -import { chainsNativeAsset } from '@/chains'; +import { ChainId } from '@/state/backendNetworks/types'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; import { isLowerCaseMatch } from '@/utils'; import { AddressOrEth } from '@/__swaps__/types/assets'; export function isNativeAsset(address: AddressOrEth | string, chainId: ChainId) { - return isLowerCaseMatch(chainsNativeAsset[chainId].address, address); + return isLowerCaseMatch(useBackendNetworksStore.getState().getChainsNativeAsset()[chainId].address, address); } export async function getOnchainAssetBalance({ address, decimals, symbol }: any, userAddress: any, chainId: ChainId, provider: any) { diff --git a/src/handlers/deeplinks.ts b/src/handlers/deeplinks.ts index b5aee723e24..60f309df3b3 100644 --- a/src/handlers/deeplinks.ts +++ b/src/handlers/deeplinks.ts @@ -20,11 +20,11 @@ import { queryClient } from '@/react-query'; import { pointsReferralCodeQueryKey } from '@/resources/points'; import { useMobileWalletProtocolHost } from '@coinbase/mobile-wallet-protocol-host'; import { InitialRoute } from '@/navigation/initialRoute'; -import { ParsedSearchAsset, UniqueId } from '@/__swaps__/types/assets'; +import { ParsedSearchAsset } from '@/__swaps__/types/assets'; import { GasSpeed } from '@/__swaps__/types/gas'; import { parseSearchAsset } from '@/__swaps__/utils/assets'; -import { supportedSwapChainIds } from '@/chains'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; import { queryTokenSearch } from '@/__swaps__/screens/Swap/resources/search/search'; import { clamp } from '@/__swaps__/utils/swaps'; import { isAddress } from 'viem'; @@ -348,6 +348,7 @@ const querySwapAsset = async (uniqueId: string | undefined): Promise rainbowMeteorologyApi.get(`/meteorology/v1/gas/${chainsName[chainId]}`, {}); +export const rainbowMeteorologyGetData = (chainId: ChainId) => + rainbowMeteorologyApi.get(`/meteorology/v1/gas/${useBackendNetworksStore.getState().getChainsName()[chainId]}`, {}); diff --git a/src/handlers/localstorage/globalSettings.ts b/src/handlers/localstorage/globalSettings.ts index d5a6a1000ec..f112a7a2695 100644 --- a/src/handlers/localstorage/globalSettings.ts +++ b/src/handlers/localstorage/globalSettings.ts @@ -1,4 +1,4 @@ -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { getGlobal, saveGlobal } from './common'; import { NativeCurrencyKeys } from '@/entities'; import { Language } from '@/languages'; diff --git a/src/handlers/localstorage/removeWallet.ts b/src/handlers/localstorage/removeWallet.ts index 95ccf7ccc43..4559e5e5e8b 100644 --- a/src/handlers/localstorage/removeWallet.ts +++ b/src/handlers/localstorage/removeWallet.ts @@ -4,7 +4,7 @@ import { accountLocalKeys } from './accountLocal'; import { getKey } from './common'; import { logger, RainbowError } from '@/logger'; import { removeNotificationSettingsForWallet } from '@/notifications/settings'; -import { Network } from '@/chains/types'; +import { Network } from '@/state/backendNetworks/types'; export const removeWalletData = async (accountAddress: any) => { logger.debug('[localstorage/removeWallet]: removing wallet data', { accountAddress }); diff --git a/src/handlers/swap.ts b/src/handlers/swap.ts index 9a537286856..7d811afc0d4 100644 --- a/src/handlers/swap.ts +++ b/src/handlers/swap.ts @@ -12,7 +12,7 @@ import { add, convertRawAmountToDecimalFormat, divide, lessThan, multiply, subtr import { erc20ABI, ethUnits } from '@/references'; import { ethereumUtils } from '@/utils'; import { logger, RainbowError } from '@/logger'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; export enum Field { INPUT = 'INPUT', diff --git a/src/handlers/tokenSearch.ts b/src/handlers/tokenSearch.ts index e03d7452837..3ea1c0c0f4a 100644 --- a/src/handlers/tokenSearch.ts +++ b/src/handlers/tokenSearch.ts @@ -4,8 +4,8 @@ import { RainbowFetchClient } from '../rainbow-fetch'; import { TokenSearchThreshold, TokenSearchTokenListId } from '@/entities'; import { logger, RainbowError } from '@/logger'; import { RainbowToken, TokenSearchToken } from '@/entities/tokens'; -import { chainsName } from '@/chains'; -import { ChainId } from '@/chains/types'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; +import { ChainId } from '@/state/backendNetworks/types'; const ALL_VERIFIED_TOKENS_PARAM = '/?list=verifiedAssets'; @@ -19,6 +19,7 @@ const tokenSearchHttp = new RainbowFetchClient({ }); function parseTokenSearch(assets: TokenSearchToken[]): RainbowToken[] { + const chainsName = useBackendNetworksStore.getState().getChainsName(); return assets.map(token => { const networkKeys = Object.keys(token.networks); const chainId = Number(networkKeys[0]); diff --git a/src/handlers/web3.ts b/src/handlers/web3.ts index e6e4dde3f70..96f099109a2 100644 --- a/src/handlers/web3.ts +++ b/src/handlers/web3.ts @@ -24,9 +24,9 @@ import { import { ethereumUtils } from '@/utils'; import { logger, RainbowError } from '@/logger'; import { IS_IOS, RPC_PROXY_API_KEY, RPC_PROXY_BASE_URL } from '@/env'; -import { ChainId, chainHardhat } from '@/chains/types'; +import { ChainId, chainHardhat } from '@/state/backendNetworks/types'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; -import { defaultChains } from '@/chains'; export enum TokenStandard { ERC1155 = 'ERC1155', @@ -93,7 +93,8 @@ export type NewTransactionNonNullable = { * @return Whether or not the network is a L2 network. */ export const isL2Chain = ({ chainId = ChainId.mainnet }: { chainId?: ChainId }): boolean => { - return defaultChains[chainId].id !== ChainId.mainnet && !defaultChains[chainId].testnet; + const defaultChains = useBackendNetworksStore.getState().getDefaultChains(); + return defaultChains[chainId]?.id !== ChainId.mainnet && !defaultChains[chainId]?.testnet; }; /** @@ -102,7 +103,7 @@ export const isL2Chain = ({ chainId = ChainId.mainnet }: { chainId?: ChainId }): * @return Whether or not the network is a testnet. */ export const isTestnetChain = ({ chainId = ChainId.mainnet }: { chainId?: ChainId }): boolean => { - return !!defaultChains[chainId].testnet; + return !!useBackendNetworksStore.getState().getDefaultChains()[chainId]?.testnet; }; export const getCachedProviderForNetwork = (chainId: ChainId = ChainId.mainnet): StaticJsonRpcProvider | undefined => { @@ -118,8 +119,7 @@ export const getBatchedProvider = ({ chainId = ChainId.mainnet }: { chainId?: nu } const cachedProvider = chainsBatchProviders.get(chainId); - - const providerUrl = defaultChains[chainId]?.rpcUrls?.default?.http?.[0]; + const providerUrl = useBackendNetworksStore.getState().getDefaultChains()[chainId]?.rpcUrls?.default?.http?.[0]; if (cachedProvider && cachedProvider?.connection.url === providerUrl) { return cachedProvider; @@ -140,7 +140,7 @@ export const getProvider = ({ chainId = ChainId.mainnet }: { chainId?: number }) const cachedProvider = chainsProviders.get(chainId); - const providerUrl = defaultChains[chainId]?.rpcUrls?.default?.http?.[0]; + const providerUrl = useBackendNetworksStore.getState().getDefaultChains()[chainId]?.rpcUrls?.default?.http?.[0]; if (cachedProvider && cachedProvider?.connection.url === providerUrl) { return cachedProvider; diff --git a/src/helpers/SharedValuesContext.tsx b/src/helpers/SharedValuesContext.tsx index d7d2035c8db..08dee0b4544 100644 --- a/src/helpers/SharedValuesContext.tsx +++ b/src/helpers/SharedValuesContext.tsx @@ -22,3 +22,11 @@ export function SharedValuesProvider({ children }: PropsWithChildren) { ); return {children}; } + +export const useSharedValuesContext = () => { + const context = React.useContext(Context); + if (context === undefined) { + throw new Error('useSharedValuesContext must be used within a SharedValuesProvider'); + } + return context; +}; diff --git a/src/helpers/ens.ts b/src/helpers/ens.ts index d3611b2beb1..f4e09e55064 100644 --- a/src/helpers/ens.ts +++ b/src/helpers/ens.ts @@ -25,7 +25,7 @@ import { import { colors } from '@/styles'; import { labelhash } from '@/utils'; import { encodeContenthash, isValidContenthash } from '@/utils/contenthash'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; export const ENS_SECONDS_WAIT = 60; export const ENS_SECONDS_PADDING = 5; diff --git a/src/helpers/gas.ts b/src/helpers/gas.ts index e9d3f17939b..08c8756ed28 100644 --- a/src/helpers/gas.ts +++ b/src/helpers/gas.ts @@ -1,6 +1,6 @@ import { memoFn } from '../utils/memoFn'; import { gasUtils } from '@/utils'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { isL2Chain } from '@/handlers/web3'; const { GasTrends } = gasUtils; diff --git a/src/helpers/networkInfo.ts b/src/helpers/networkInfo.ts index cd8f6bcc139..4668c35288b 100644 --- a/src/helpers/networkInfo.ts +++ b/src/helpers/networkInfo.ts @@ -1,4 +1,4 @@ -import { Network } from '@/chains/types'; +import { Network } from '@/state/backendNetworks/types'; // TODO: networkInfo is DEPRECATED after the new network support changes const networkInfo = { diff --git a/src/helpers/signingWallet.ts b/src/helpers/signingWallet.ts index 769bb1cadf0..10532dd3e95 100644 --- a/src/helpers/signingWallet.ts +++ b/src/helpers/signingWallet.ts @@ -3,7 +3,7 @@ import { generateMnemonic } from 'bip39'; import { default as LibWallet } from 'ethereumjs-wallet'; import { RAINBOW_MASTER_KEY } from 'react-native-dotenv'; import { loadString, publicAccessControlOptions, saveString } from '../model/keychain'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { loadWallet } from '../model/wallet'; import { signingWalletAddress, signingWallet as signingWalletKeychain } from '../utils/keychainConstants'; import { EthereumAddress } from '@/entities'; diff --git a/src/helpers/validators.ts b/src/helpers/validators.ts index c2c5fce0942..e2ceccd5c0a 100644 --- a/src/helpers/validators.ts +++ b/src/helpers/validators.ts @@ -2,7 +2,7 @@ import { isValidAddress } from 'ethereumjs-util'; import { memoFn } from '../utils/memoFn'; import { getProvider, isHexStringIgnorePrefix, isValidMnemonic, resolveUnstoppableDomain } from '@/handlers/web3'; import { sanitizeSeedPhrase } from '@/utils/formatters'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; // Currently supported Top Level Domains from Unstoppable Domains const supportedUnstoppableDomains = ['888', 'bitcoin', 'blockchain', 'coin', 'crypto', 'dao', 'nft', 'wallet', 'x', 'zil']; diff --git a/src/helpers/walletConnectNetworks.ts b/src/helpers/walletConnectNetworks.ts index 69fb67ee2c1..662191d0eb4 100644 --- a/src/helpers/walletConnectNetworks.ts +++ b/src/helpers/walletConnectNetworks.ts @@ -1,14 +1,14 @@ import store from '@/redux/store'; import { showActionSheetWithOptions } from '@/utils'; import * as i18n from '@/languages'; -import { ChainId } from '@/chains/types'; -import { chainsLabel, defaultChains } from '@/chains'; +import { ChainId } from '@/state/backendNetworks/types'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; import { isL2Chain } from '@/handlers/web3'; import { MenuActionConfig } from 'react-native-ios-context-menu'; const androidNetworkActions = () => { const { testnetsEnabled } = store.getState().settings; - return Object.values(defaultChains) + return Object.values(useBackendNetworksStore.getState().getDefaultChains()) .filter(chain => testnetsEnabled || !chain.testnet) .map(chain => chain.id); }; @@ -18,11 +18,11 @@ export const NETWORK_MENU_ACTION_KEY_FILTER = 'switch-to-network-'; export const networksMenuItems: () => MenuActionConfig[] = () => { const { testnetsEnabled } = store.getState().settings; - return Object.values(defaultChains) + return Object.values(useBackendNetworksStore.getState().getDefaultChains()) .filter(chain => testnetsEnabled || !chain.testnet) .map(chain => ({ actionKey: `${NETWORK_MENU_ACTION_KEY_FILTER}${chain.id}`, - actionTitle: chainsLabel[chain.id], + actionTitle: useBackendNetworksStore.getState().getChainsLabel()[chain.id], icon: { iconType: 'ASSET', iconValue: `${isL2Chain({ chainId: chain.id }) ? `${chain.name}BadgeNoShadow` : 'ethereumBadge'}`, @@ -78,6 +78,7 @@ export const androidShowNetworksActionSheet = (callback: any) => { }, (idx: number) => { if (idx !== undefined) { + const defaultChains = useBackendNetworksStore.getState().getDefaultChains(); const networkActions = androidNetworkActions(); const chain = defaultChains[networkActions[idx]] || defaultChains[ChainId.mainnet]; callback({ chainId: chain.id }); diff --git a/src/hooks/charts/useChartInfo.ts b/src/hooks/charts/useChartInfo.ts index dd1c6f91ad3..09159ab38a5 100644 --- a/src/hooks/charts/useChartInfo.ts +++ b/src/hooks/charts/useChartInfo.ts @@ -5,7 +5,7 @@ import { metadataClient } from '@/graphql'; import { useQuery } from '@tanstack/react-query'; import { createQueryKey } from '@/react-query'; import { SupportedCurrencyKey } from '@/references'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; const chartTimes = ['hour', 'day', 'week', 'month', 'year'] as const; type ChartTime = (typeof chartTimes)[number]; diff --git a/src/hooks/useAccountTransactions.ts b/src/hooks/useAccountTransactions.ts index 7b3ee474611..3454249673c 100644 --- a/src/hooks/useAccountTransactions.ts +++ b/src/hooks/useAccountTransactions.ts @@ -8,8 +8,8 @@ import { useConsolidatedTransactions } from '@/resources/transactions/consolidat import { RainbowTransaction } from '@/entities'; import { pendingTransactionsStore } from '@/state/pendingTransactions'; import { getSortedWalletConnectRequests } from '@/state/walletConnectRequests'; -import { ChainId } from '@/chains/types'; -import { SUPPORTED_CHAIN_IDS } from '@/chains'; +import { ChainId } from '@/state/backendNetworks/types'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; export const NOE_PAGE = 30; @@ -44,7 +44,12 @@ export default function useAccountTransactions() { } return latestTxMap; }, - new Map(SUPPORTED_CHAIN_IDS.map(chainId => [chainId, null as RainbowTransaction | null])) + new Map( + useBackendNetworksStore + .getState() + .getSupportedChainIds() + .map(chainId => [chainId, null as RainbowTransaction | null]) + ) ); watchForPendingTransactionsReportedByRainbowBackend({ currentAddress: accountAddress, diff --git a/src/hooks/useAdditionalAssetData.ts b/src/hooks/useAdditionalAssetData.ts index 0a89867ccf8..271d75819df 100644 --- a/src/hooks/useAdditionalAssetData.ts +++ b/src/hooks/useAdditionalAssetData.ts @@ -2,7 +2,7 @@ import { useQuery } from '@tanstack/react-query'; import { NativeCurrencyKey } from '@/entities'; import { metadataClient } from '@/graphql'; import { Token } from '@/graphql/__generated__/metadata'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; // Types type TokenMetadata = Pick< diff --git a/src/hooks/useAsset.ts b/src/hooks/useAsset.ts index cea27883fff..663c50858a5 100644 --- a/src/hooks/useAsset.ts +++ b/src/hooks/useAsset.ts @@ -4,7 +4,7 @@ import { getUniqueId } from '@/utils/ethereumUtils'; import { useExternalToken } from '@/resources/assets/externalAssetsQuery'; import { useSelector } from 'react-redux'; import { AppState } from '@/redux/store'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { Address } from 'viem'; // To fetch an asset from account assets, diff --git a/src/hooks/useCalculateGasLimit.ts b/src/hooks/useCalculateGasLimit.ts index 34f6b89b92f..703d073a268 100644 --- a/src/hooks/useCalculateGasLimit.ts +++ b/src/hooks/useCalculateGasLimit.ts @@ -9,8 +9,8 @@ import { InteractionManager } from 'react-native'; import { GasFeeParamsBySpeed } from '@/entities'; import { StaticJsonRpcProvider } from '@ethersproject/providers'; import { useGas } from '@/hooks'; -import { ChainId } from '@/chains/types'; -import { needsL1SecurityFeeChains } from '@/chains'; +import { ChainId } from '@/state/backendNetworks/types'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; type CalculateGasLimitProps = { isMessageRequest: boolean; @@ -52,7 +52,7 @@ export const useCalculateGasLimit = ({ } finally { logger.debug('WC: Setting gas limit to', { gas: convertHexToString(gas) }, logger.DebugContext.walletconnect); - const needsL1SecurityFee = needsL1SecurityFeeChains.includes(chainId); + const needsL1SecurityFee = useBackendNetworksStore.getState().getNeedsL1SecurityFeeChains().includes(chainId); if (needsL1SecurityFee) { const l1GasFeeOptimism = await ethereumUtils.calculateL1FeeOptimism(txPayload, provider); updateTxFee(gas, null, l1GasFeeOptimism); diff --git a/src/hooks/useENSRegistrationActionHandler.ts b/src/hooks/useENSRegistrationActionHandler.ts index 95337c4d4b9..a61bf71ee92 100644 --- a/src/hooks/useENSRegistrationActionHandler.ts +++ b/src/hooks/useENSRegistrationActionHandler.ts @@ -25,7 +25,7 @@ import store from '@/redux/store'; import { performanceTracking, Screens, TimeToSignOperation } from '@/state/performance/performance'; import { noop } from 'lodash'; import { logger, RainbowError } from '@/logger'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { IS_IOS } from '@/env'; // Generic type for action functions diff --git a/src/hooks/useENSRegistrationCosts.ts b/src/hooks/useENSRegistrationCosts.ts index 95ce537981e..a2653f3c86d 100644 --- a/src/hooks/useENSRegistrationCosts.ts +++ b/src/hooks/useENSRegistrationCosts.ts @@ -27,7 +27,7 @@ import { import { add, addBuffer, addDisplay, fromWei, greaterThanOrEqualTo, multiply } from '@/helpers/utilities'; import { ethUnits, timeUnits } from '@/references'; import { ethereumUtils, gasUtils } from '@/utils'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; enum QUERY_KEYS { GET_COMMIT_GAS_LIMIT = 'GET_COMMIT_GAS_LIMIT', diff --git a/src/hooks/useENSRegistrationStepHandler.tsx b/src/hooks/useENSRegistrationStepHandler.tsx index 24b9ceea113..d99183c9bec 100644 --- a/src/hooks/useENSRegistrationStepHandler.tsx +++ b/src/hooks/useENSRegistrationStepHandler.tsx @@ -14,7 +14,7 @@ import { REGISTRATION_STEPS, } from '@/helpers/ens'; import { updateTransactionRegistrationParameters } from '@/redux/ensRegistration'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; const checkRegisterBlockTimestamp = async ({ diff --git a/src/hooks/useENSSearch.ts b/src/hooks/useENSSearch.ts index 5e9d207375d..6fc3e3451f7 100644 --- a/src/hooks/useENSSearch.ts +++ b/src/hooks/useENSSearch.ts @@ -6,7 +6,7 @@ import { fetchRegistrationDate } from '@/handlers/ens'; import { ENS_DOMAIN, formatRentPrice, getAvailable, getENSRegistrarControllerContract, getNameExpires, getRentPrice } from '@/helpers/ens'; import { timeUnits } from '@/references'; import { ethereumUtils, validateENS } from '@/utils'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; const formatTime = (timestamp: string, abbreviated = true) => { const style = abbreviated ? 'MMM d, y' : 'MMMM d, y'; diff --git a/src/hooks/useGas.ts b/src/hooks/useGas.ts index 65df100a784..3949702fe1a 100644 --- a/src/hooks/useGas.ts +++ b/src/hooks/useGas.ts @@ -30,9 +30,9 @@ import { fetchExternalToken, } from '@/resources/assets/externalAssetsQuery'; import useAccountSettings from './useAccountSettings'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { useQueries } from '@tanstack/react-query'; -import { chainsNativeAsset } from '@/chains'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; const checkSufficientGas = (txFee: LegacyGasFee | GasFee, chainId: ChainId, nativeAsset?: ParsedAddressAsset) => { const isLegacyGasNetwork = !(txFee as GasFee)?.maxFee; @@ -66,6 +66,8 @@ export default function useGas({ nativeAsset }: { nativeAsset?: ParsedAddressAss const dispatch = useDispatch(); const { nativeCurrency } = useAccountSettings(); + const chainsNativeAsset = useBackendNetworksStore.getState().getChainsNativeAsset(); + // keep native assets up to date for gas price calculations // NOTE: We only fetch the native asset for mainnet and chains that don't use ETH as their native token const chainsToFetch = Object.entries(chainsNativeAsset).filter( diff --git a/src/hooks/useHasEnoughBalance.ts b/src/hooks/useHasEnoughBalance.ts index 72da88cf43b..63ad3a81a1b 100644 --- a/src/hooks/useHasEnoughBalance.ts +++ b/src/hooks/useHasEnoughBalance.ts @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'; import { fromWei, greaterThanOrEqualTo } from '@/helpers/utilities'; import BigNumber from 'bignumber.js'; import { SelectedGasFee } from '@/entities'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; type WalletBalance = { amount: string | number; diff --git a/src/hooks/useImportingWallet.ts b/src/hooks/useImportingWallet.ts index e78fa43a2e8..d0f92e12a3a 100644 --- a/src/hooks/useImportingWallet.ts +++ b/src/hooks/useImportingWallet.ts @@ -31,7 +31,7 @@ import { handleReviewPromptAction } from '@/utils/reviewAlert'; import { ReviewPromptAction } from '@/storage/schema'; import { checkWalletsForBackupStatus } from '@/screens/SettingsSheet/utils'; import walletBackupTypes from '@/helpers/walletBackupTypes'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; export default function useImportingWallet({ showImportModal = true } = {}) { const { accountAddress } = useAccountSettings(); diff --git a/src/hooks/useNonceForDisplay.ts b/src/hooks/useNonceForDisplay.ts index 8308bc460e9..98376442019 100644 --- a/src/hooks/useNonceForDisplay.ts +++ b/src/hooks/useNonceForDisplay.ts @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react'; import { getNextNonce } from '@/state/nonces'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { logger, RainbowError } from '@/logger'; type UseNonceParams = { diff --git a/src/hooks/useSearchCurrencyList.ts b/src/hooks/useSearchCurrencyList.ts index f740de45000..6c414079be7 100644 --- a/src/hooks/useSearchCurrencyList.ts +++ b/src/hooks/useSearchCurrencyList.ts @@ -17,8 +17,8 @@ import { CROSSCHAIN_SWAPS, useExperimentalFlag } from '@/config'; import { IS_TEST } from '@/env'; import { useFavorites } from '@/resources/favorites'; import { getUniqueId } from '@/utils/ethereumUtils'; -import { chainsName } from '@/chains'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; type swapCurrencyListType = | 'verifiedAssets' @@ -70,7 +70,7 @@ const searchCurrencyList = async (searchParams: { } }; -const useSearchCurrencyList = (searchQuery: string, searchChainId = ChainId.mainnet, isDiscover = false) => { +const useSearchCurrencyList = (searchQuery: string, searchChainId = ChainId.mainnet) => { const previousChainId = usePrevious(searchChainId); const searching = useMemo(() => searchQuery !== '' || ChainId.mainnet !== searchChainId, [searchChainId, searchQuery]); @@ -189,7 +189,7 @@ const useSearchCurrencyList = (searchQuery: string, searchChainId = ChainId.main }, }, symbol, - network: chainsName[chainId], + network: useBackendNetworksStore.getState().getChainsName()[chainId], uniqueId, } as RainbowToken, ]; @@ -434,7 +434,6 @@ const useSearchCurrencyList = (searchQuery: string, searchChainId = ChainId.main const currentNetworkChainId = Number(chainId); if (currentNetworkChainId !== searchChainId) { // including goerli in our networks type is causing this type issue - // @ts-ignore const exactMatch = crosschainVerifiedAssets[currentNetworkChainId].find((asset: RainbowToken) => { const symbolMatch = isLowerCaseMatch(asset?.symbol, searchQuery); const nameMatch = isLowerCaseMatch(asset?.name, searchQuery); diff --git a/src/hooks/useTransactionSetup.ts b/src/hooks/useTransactionSetup.ts index 0eb1542e66e..28bdf4d4989 100644 --- a/src/hooks/useTransactionSetup.ts +++ b/src/hooks/useTransactionSetup.ts @@ -6,7 +6,7 @@ import { methodRegistryLookupAndParse } from '@/utils/methodRegistry'; import { analytics } from '@/analytics'; import { event } from '@/analytics/event'; import { RequestSource } from '@/utils/requestNavigationHandlers'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; type TransactionSetupParams = { chainId: ChainId; diff --git a/src/hooks/useWalletSectionsData.ts b/src/hooks/useWalletSectionsData.ts index d8e3f97e7e0..db186edc714 100644 --- a/src/hooks/useWalletSectionsData.ts +++ b/src/hooks/useWalletSectionsData.ts @@ -23,7 +23,7 @@ import { throttle } from 'lodash'; import { usePoints } from '@/resources/points'; import { convertAmountAndPriceToNativeDisplay, convertRawAmountToBalance } from '@/helpers/utilities'; import { useNativeAsset } from '@/utils/ethereumUtils'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; // user properties analytics for claimables that executes at max once every 2 min const throttledClaimablesAnalytics = throttle( diff --git a/src/hooks/useWatchPendingTxs.ts b/src/hooks/useWatchPendingTxs.ts index 5b90513d185..f939d6e408b 100644 --- a/src/hooks/useWatchPendingTxs.ts +++ b/src/hooks/useWatchPendingTxs.ts @@ -12,7 +12,7 @@ import { usePendingTransactionsStore } from '@/state/pendingTransactions'; import { Address } from 'viem'; import { staleBalancesStore } from '@/state/staleBalances'; import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; -import { SUPPORTED_MAINNET_CHAIN_IDS } from '@/chains'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; export const useWatchPendingTransactions = ({ address }: { address: string }) => { const { storePendingTransactions, setPendingTransactions } = usePendingTransactionsStore(state => ({ @@ -126,11 +126,13 @@ export const useWatchPendingTransactions = ({ address }: { address: string }) => queryKey: userAssetsQueryKey({ address, currency: nativeCurrency, connectedToHardhat }), }); + const supportedMainnetChainIds = useBackendNetworksStore.getState().getSupportedMainnetChainIds(); + await queryClient.refetchQueries({ queryKey: consolidatedTransactionsQueryKey({ address, currency: nativeCurrency, - chainIds: SUPPORTED_MAINNET_CHAIN_IDS, + chainIds: supportedMainnetChainIds, }), }); @@ -140,7 +142,7 @@ export const useWatchPendingTransactions = ({ address }: { address: string }) => queryKey: consolidatedTransactionsQueryKey({ address, currency: nativeCurrency, - chainIds: SUPPORTED_MAINNET_CHAIN_IDS, + chainIds: supportedMainnetChainIds, }), }); }, 2000); diff --git a/src/migrations/migrations/migratePinnedAndHiddenTokenUniqueIds.ts b/src/migrations/migrations/migratePinnedAndHiddenTokenUniqueIds.ts index abff230dbbb..20f8fe20986 100644 --- a/src/migrations/migrations/migratePinnedAndHiddenTokenUniqueIds.ts +++ b/src/migrations/migrations/migratePinnedAndHiddenTokenUniqueIds.ts @@ -1,7 +1,7 @@ import { BooleanMap } from '@/hooks/useCoinListEditOptions'; import { Migration, MigrationName } from '@/migrations/types'; import { loadAddress } from '@/model/wallet'; -import { Network } from '@/chains/types'; +import { Network } from '@/state/backendNetworks/types'; import { MMKV } from 'react-native-mmkv'; const mmkv = new MMKV(); diff --git a/src/model/remoteConfig.ts b/src/model/remoteConfig.ts index f709b4ea9a1..ff3acc7ea5a 100644 --- a/src/model/remoteConfig.ts +++ b/src/model/remoteConfig.ts @@ -6,6 +6,7 @@ import { useQuery } from '@tanstack/react-query'; export interface RainbowConfig extends Record { default_slippage_bips: string; + default_slippage_bips_chainId: string; f2c_enabled: boolean; op_nft_network: string; op_rewards_enabled: boolean; @@ -73,6 +74,19 @@ export const DEFAULT_CONFIG: RainbowConfig = { polygon: 200, zora: 200, }), + default_slippage_bips_chainId: JSON.stringify({ + '33139': 200, + '42161': 200, + '43114': 200, + '8453': 200, + '81457': 200, + '56': 200, + '666666666': 200, + '1': 100, + '10': 200, + '137': 200, + '7777777': 200, + }), f2c_enabled: true, op_nft_network: 'op-mainnet', op_rewards_enabled: false, @@ -138,7 +152,7 @@ export async function fetchRemoteConfig(): Promise { const parameters = remoteConfig().getAll(); Object.entries(parameters).forEach($ => { const [key, entry] = $; - if (key === 'default_slippage_bips') { + if (key === 'default_slippage_bips' || key === 'default_slippage_bips_chainId') { config[key] = JSON.parse(entry.asString()); } else if ( key === 'f2c_enabled' || diff --git a/src/model/wallet.ts b/src/model/wallet.ts index 1f7d833d014..da67e97fdbd 100644 --- a/src/model/wallet.ts +++ b/src/model/wallet.ts @@ -50,7 +50,7 @@ import { setHardwareTXError } from '@/navigation/HardwareWalletTxNavigator'; import { Signer } from '@ethersproject/abstract-signer'; import { sanitizeTypedData } from '@/utils/signingUtils'; import { ExecuteFnParamsWithoutFn, performanceTracking, Screen } from '@/state/performance/performance'; -import { Network } from '@/chains/types'; +import { Network } from '@/state/backendNetworks/types'; export type EthereumPrivateKey = string; type EthereumMnemonic = string; diff --git a/src/navigation/config.tsx b/src/navigation/config.tsx index 4de26c91662..5428d27e37b 100644 --- a/src/navigation/config.tsx +++ b/src/navigation/config.tsx @@ -28,8 +28,8 @@ import { Box } from '@/design-system'; import { IS_ANDROID } from '@/env'; import { SignTransactionSheetRouteProp } from '@/screens/SignTransactionSheet'; import { RequestSource } from '@/utils/requestNavigationHandlers'; -import { ChainId } from '@/chains/types'; -import { chainsName } from '@/chains'; +import { ChainId } from '@/state/backendNetworks/types'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; export const sharedCoolModalTopOffset = safeAreaInsetValues.top; @@ -491,7 +491,7 @@ export const ensAdditionalRecordsSheetConfig: PartialNavigatorConfigOptions = { }; export const explainSheetConfig: PartialNavigatorConfigOptions = { - options: ({ route: { params = { network: chainsName[ChainId.mainnet] } } }) => { + options: ({ route: { params = { network: useBackendNetworksStore.getState().getChainsName()[ChainId.mainnet] } } }) => { // @ts-ignore const explainerConfig = explainers(params.network)[params?.type]; return buildCoolModalConfig({ diff --git a/src/parsers/transactions.ts b/src/parsers/transactions.ts index 7430c03fa44..c0d65bf1f22 100644 --- a/src/parsers/transactions.ts +++ b/src/parsers/transactions.ts @@ -21,8 +21,8 @@ import { NewTransaction, RainbowTransactionFee } from '@/entities/transactions/t import { parseAddressAsset, parseAsset } from '@/resources/assets/assets'; import { ParsedAsset } from '@/resources/assets/types'; -import { ChainId } from '@/chains/types'; -import { chainsNativeAsset } from '@/chains'; +import { ChainId } from '@/state/backendNetworks/types'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; const TransactionOutTypes = [ 'burn', @@ -169,7 +169,7 @@ const getTransactionFee = ( return undefined; } - const chainNativeAsset = chainsNativeAsset[chainId]; + const chainNativeAsset = useBackendNetworksStore.getState().getChainsNativeAsset()[chainId]; const zerionFee = txn.fee; return { diff --git a/src/raps/actions/claimBridge.ts b/src/raps/actions/claimBridge.ts index 6b64cc4f28c..7b769b7817b 100644 --- a/src/raps/actions/claimBridge.ts +++ b/src/raps/actions/claimBridge.ts @@ -14,12 +14,12 @@ import { REFERRER_CLAIM } from '@/references'; import { addNewTransaction } from '@/state/pendingTransactions'; import ethereumUtils from '@/utils/ethereumUtils'; import { AddressZero } from '@ethersproject/constants'; -import { CrosschainQuote, QuoteError, SwapType, getClaimBridgeQuote } from '@rainbow-me/swaps'; +import { CrosschainQuote, QuoteError, getClaimBridgeQuote } from '@rainbow-me/swaps'; import { Address } from 'viem'; import { ActionProps } from '../references'; import { executeCrosschainSwap } from './crosschainSwap'; -import { ChainId } from '@/chains/types'; -import { chainsName } from '@/chains'; +import { ChainId } from '@/state/backendNetworks/types'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; // This action is used to bridge the claimed funds to another chain export async function claimBridge({ parameters, wallet, baseNonce }: ActionProps<'claimBridge'>) { @@ -158,6 +158,8 @@ export async function claimBridge({ parameters, wallet, baseNonce }: ActionProps throw new Error('[CLAIM-BRIDGE]: executeCrosschainSwap returned undefined'); } + const chainsName = useBackendNetworksStore.getState().getChainsName(); + const typedAssetToBuy: ParsedAddressAsset = { ...parameters.assetToBuy, network: chainsName[parameters.assetToBuy.chainId], diff --git a/src/raps/actions/crosschainSwap.ts b/src/raps/actions/crosschainSwap.ts index 728aa9040ff..d0dc68c59bb 100644 --- a/src/raps/actions/crosschainSwap.ts +++ b/src/raps/actions/crosschainSwap.ts @@ -6,7 +6,7 @@ import { add } from '@/helpers/utilities'; import { assetNeedsUnlocking, estimateApprove } from './unlock'; import { REFERRER, gasUnits, ReferrerType } from '@/references'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { NewTransaction, TransactionDirection, TransactionStatus, TxHash } from '@/entities'; import { addNewTransaction } from '@/state/pendingTransactions'; import { RainbowError, logger } from '@/logger'; @@ -26,7 +26,7 @@ import { AddysNetworkDetails, ParsedAsset } from '@/resources/assets/types'; import { ExtendedAnimatedAssetWithColors } from '@/__swaps__/types/assets'; import { Screens, TimeToSignOperation, performanceTracking } from '@/state/performance/performance'; import { swapsStore } from '@/state/swaps/swapsStore'; -import { chainsName } from '@/chains'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; const getCrosschainSwapDefaultGasLimit = (quote: CrosschainQuote) => quote?.routes?.[0]?.userTxs?.[0]?.gasFees?.gasLimit; @@ -242,6 +242,8 @@ export const crosschainSwap = async ({ } : parameters.assetToSell.price; + const chainsName = useBackendNetworksStore.getState().getChainsName(); + const assetToBuy = { ...parameters.assetToBuy, network: chainsName[parameters.assetToBuy.chainId], diff --git a/src/raps/actions/ens.ts b/src/raps/actions/ens.ts index d0466d77149..e4e315c98a4 100644 --- a/src/raps/actions/ens.ts +++ b/src/raps/actions/ens.ts @@ -12,7 +12,7 @@ import store from '@/redux/store'; import { logger, RainbowError } from '@/logger'; import { parseGasParamAmounts } from '@/parsers'; import { addNewTransaction } from '@/state/pendingTransactions'; -import { ChainId, Network } from '@/chains/types'; +import { ChainId, Network } from '@/state/backendNetworks/types'; import { createRegisterENSRap, createRenewENSRap, diff --git a/src/raps/actions/swap.ts b/src/raps/actions/swap.ts index ca01e138928..deeb8c5658d 100644 --- a/src/raps/actions/swap.ts +++ b/src/raps/actions/swap.ts @@ -16,7 +16,7 @@ import { estimateGasWithPadding, getProvider, toHex } from '@/handlers/web3'; import { Address } from 'viem'; import { metadataPOSTClient } from '@/graphql'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { NewTransaction, TxHash, TransactionStatus, TransactionDirection } from '@/entities'; import { add } from '@/helpers/utilities'; import { addNewTransaction } from '@/state/pendingTransactions'; @@ -41,7 +41,7 @@ import { AddysNetworkDetails, ParsedAsset } from '@/resources/assets/types'; import { ExtendedAnimatedAssetWithColors } from '@/__swaps__/types/assets'; import { Screens, TimeToSignOperation, performanceTracking } from '@/state/performance/performance'; import { swapsStore } from '@/state/swaps/swapsStore'; -import { chainsName } from '@/chains'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; const WRAP_GAS_PADDING = 1.002; @@ -374,6 +374,8 @@ export const swap = async ({ } : parameters.assetToSell.price; + const chainsName = useBackendNetworksStore.getState().getChainsName(); + const assetToBuy = { ...parameters.assetToBuy, network: chainsName[parameters.assetToBuy.chainId], diff --git a/src/raps/actions/unlock.ts b/src/raps/actions/unlock.ts index f12aa0c91b7..2ede72a1b7e 100644 --- a/src/raps/actions/unlock.ts +++ b/src/raps/actions/unlock.ts @@ -5,7 +5,7 @@ import { parseUnits } from '@ethersproject/units'; import { getProvider, toHex } from '@/handlers/web3'; import { Address, erc20Abi, erc721Abi } from 'viem'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { TransactionGasParams, TransactionLegacyGasParams } from '@/__swaps__/types/gas'; import { NewTransaction, TransactionStatus, TxHash } from '@/entities'; import { addNewTransaction } from '@/state/pendingTransactions'; @@ -19,7 +19,7 @@ import { ActionProps, RapActionResult } from '../references'; import { overrideWithFastSpeedIfNeeded } from './../utils'; import { TokenColors } from '@/graphql/__generated__/metadata'; import { ParsedAsset } from '@/resources/assets/types'; -import { chainsName } from '@/chains'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; export const getAssetRawAllowance = async ({ owner, @@ -266,6 +266,8 @@ export const unlock = async ({ if (!approval) throw new RainbowError('[raps/unlock]: error executeApprove'); + const chainsName = useBackendNetworksStore.getState().getChainsName(); + const transaction = { asset: { ...assetToUnlock, diff --git a/src/raps/execute.ts b/src/raps/execute.ts index bf9809da383..fa8fed28cab 100644 --- a/src/raps/execute.ts +++ b/src/raps/execute.ts @@ -2,7 +2,7 @@ /* eslint-disable no-async-promise-executor */ /* eslint-disable no-promise-executor-return */ import { Signer } from '@ethersproject/abstract-signer'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { RainbowError, logger } from '@/logger'; import { claim, swap, unlock } from './actions'; diff --git a/src/raps/references.ts b/src/raps/references.ts index 0184fbe1671..7c087f159b1 100644 --- a/src/raps/references.ts +++ b/src/raps/references.ts @@ -4,7 +4,7 @@ import { Address } from 'viem'; import { ParsedAsset } from '@/__swaps__/types/assets'; import { GasFeeParamsBySpeed, LegacyGasFeeParamsBySpeed, LegacyTransactionGasParamAmounts, TransactionGasParamAmounts } from '@/entities'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { TransactionClaimableTxPayload } from '@/screens/claimables/transaction/types'; export enum SwapModalField { diff --git a/src/raps/utils.ts b/src/raps/utils.ts index 75e36705419..f46513fc145 100644 --- a/src/raps/utils.ts +++ b/src/raps/utils.ts @@ -8,7 +8,7 @@ import { Chain, erc20Abi } from 'viem'; import { GasFeeParamsBySpeed, LegacyGasFeeParamsBySpeed, LegacyTransactionGasParamAmounts, TransactionGasParamAmounts } from '@/entities'; import { gasUtils } from '@/utils'; import { add, greaterThan, multiply } from '@/helpers/utilities'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { gasUnits } from '@/references'; import { toHexNoLeadingZeros } from '@/handlers/web3'; import { BigNumber } from '@ethersproject/bignumber'; diff --git a/src/redux/ensRegistration.ts b/src/redux/ensRegistration.ts index ea2158ec215..33a5b23c844 100644 --- a/src/redux/ensRegistration.ts +++ b/src/redux/ensRegistration.ts @@ -5,7 +5,7 @@ import { ENSRegistrations, ENSRegistrationState, Records, RegistrationParameters import { getLocalENSRegistrations, saveLocalENSRegistrations } from '@/handlers/localstorage/accountLocal'; import { ENS_RECORDS, REGISTRATION_MODES } from '@/helpers/ens'; import { omitFlatten } from '@/helpers/utilities'; -import { Network } from '@/chains/types'; +import { Network } from '@/state/backendNetworks/types'; const ENS_REGISTRATION_SET_CHANGED_RECORDS = 'ensRegistration/ENS_REGISTRATION_SET_CHANGED_RECORDS'; const ENS_REGISTRATION_SET_INITIAL_RECORDS = 'ensRegistration/ENS_REGISTRATION_SET_INITIAL_RECORDS'; diff --git a/src/redux/gas.ts b/src/redux/gas.ts index b0ced82c649..496bec67a67 100644 --- a/src/redux/gas.ts +++ b/src/redux/gas.ts @@ -36,9 +36,9 @@ import { } from '@/parsers'; import { ethUnits } from '@/references'; import { ethereumUtils, gasUtils } from '@/utils'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; -import { chainsNativeAsset, chainsSwapPollingInterval, meteorologySupportedChainIds, needsL1SecurityFeeChains } from '@/chains'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; import { MeteorologyLegacyResponse, MeteorologyResponse } from '@/entities/gas'; import { addBuffer } from '@/helpers/utilities'; @@ -124,6 +124,8 @@ const getUpdatedGasFeeParams = ( ) => { let nativeTokenPriceUnit = ethereumUtils.getPriceOfNativeAssetForNetwork({ chainId: ChainId.mainnet }); + const chainsNativeAsset = useBackendNetworksStore.getState().getChainsNativeAsset(); + // we want to fetch the specific chain native token if anything but ETH const networkNativeAsset = chainsNativeAsset[chainId]; if (networkNativeAsset.symbol.toLowerCase() !== 'eth') { @@ -191,6 +193,7 @@ export const gasUpdateToCustomGasFee = (gasParams: GasFeeParams) => async (dispa const _gasLimit = gasLimit || getDefaultGasLimit(chainId, defaultGasLimit); let nativeTokenPriceUnit = ethereumUtils.getPriceOfNativeAssetForNetwork({ chainId: ChainId.mainnet }); + const chainsNativeAsset = useBackendNetworksStore.getState().getChainsNativeAsset(); // we want to fetch the specific chain native token if anything but ETH const networkNativeAsset = chainsNativeAsset[chainId]; @@ -320,7 +323,7 @@ export const gasPricesStartPolling = const { nativeCurrency } = getState().settings; let dataIsReady = true; - const meteorologySupportsChainId = meteorologySupportedChainIds.includes(chainId); + const meteorologySupportsChainId = useBackendNetworksStore.getState().getMeteorologySupportedChainIds().includes(chainId); if (!meteorologySupportsChainId) { const adjustedGasFees = await getProviderGasPrices({ chainId }); if (!adjustedGasFees) return; @@ -356,7 +359,7 @@ export const gasPricesStartPolling = } else { try { // OP chains have an additional fee we need to load - if (needsL1SecurityFeeChains.includes(chainId)) { + if (useBackendNetworksStore.getState().getNeedsL1SecurityFeeChains().includes(chainId)) { dataIsReady = l1GasFeeOptimism !== null; } const meteorologyGasParams = await getMeteorologyGasParams(chainId); @@ -473,7 +476,7 @@ export const gasPricesStartPolling = } }; - const pollingInterval = chainsSwapPollingInterval[chainId]; + const pollingInterval = useBackendNetworksStore.getState().getChainsPollingInterval()[chainId]; watchGasPrices(chainId, pollingInterval); }; @@ -509,7 +512,10 @@ export const gasUpdateTxFee = const { defaultGasLimit, gasLimit, gasFeeParamsBySpeed, selectedGasFee, chainId, currentBlockParams } = getState().gas; const { nativeCurrency } = getState().settings; - if (isEmpty(gasFeeParamsBySpeed) || (needsL1SecurityFeeChains.includes(chainId) && l1GasFeeOptimism === null)) { + if ( + isEmpty(gasFeeParamsBySpeed) || + (useBackendNetworksStore.getState().getNeedsL1SecurityFeeChains().includes(chainId) && l1GasFeeOptimism === null) + ) { // if fee prices not ready, we need to store the gas limit for future calculations // the rest is as the initial state value if (updatedGasLimit) { diff --git a/src/redux/settings.ts b/src/redux/settings.ts index 7671a740d31..4535437ea7c 100644 --- a/src/redux/settings.ts +++ b/src/redux/settings.ts @@ -1,4 +1,4 @@ -// @ts-ignore +// @ts-expect-error - changeIcon has no declaration file import { changeIcon } from 'react-native-change-icon'; import lang from 'i18n-js'; import { Dispatch } from 'redux'; @@ -23,7 +23,7 @@ import { import { getProvider } from '@/handlers/web3'; import { AppState } from '@/redux/store'; import { logger, RainbowError } from '@/logger'; -import { Network, ChainId } from '@/chains/types'; +import { Network, ChainId } from '@/state/backendNetworks/types'; import { Address } from 'viem'; // -- Constants ------------------------------------------------------------- // diff --git a/src/redux/showcaseTokens.ts b/src/redux/showcaseTokens.ts index 062d292c68f..1ca4bf8db8e 100644 --- a/src/redux/showcaseTokens.ts +++ b/src/redux/showcaseTokens.ts @@ -5,7 +5,7 @@ import { getPreference } from '../model/preferences'; import { AppGetState } from './store'; import { getShowcaseTokens, getWebDataEnabled, saveShowcaseTokens, saveWebDataEnabled } from '@/handlers/localstorage/accountLocal'; import WalletTypes from '@/helpers/walletTypes'; -import { Network } from '@/chains/types'; +import { Network } from '@/state/backendNetworks/types'; // -- Constants --------------------------------------- // diff --git a/src/references/gasUnits.ts b/src/references/gasUnits.ts index 75be1c71e93..a9517837ca8 100644 --- a/src/references/gasUnits.ts +++ b/src/references/gasUnits.ts @@ -1,4 +1,4 @@ -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; export const gasUnits = { basic_approval: '55000', diff --git a/src/references/rainbow-token-list/index.ts b/src/references/rainbow-token-list/index.ts index cdda1dcae0f..b8ad0db4687 100644 --- a/src/references/rainbow-token-list/index.ts +++ b/src/references/rainbow-token-list/index.ts @@ -6,7 +6,7 @@ import RAINBOW_TOKEN_LIST_DATA from './rainbow-token-list.json'; import { RainbowToken } from '@/entities'; import { STORAGE_IDS } from '@/model/mmkv'; import { logger, RainbowError } from '@/logger'; -import { Network, ChainId } from '@/chains/types'; +import { Network, ChainId } from '@/state/backendNetworks/types'; export const rainbowListStorage = new MMKV({ id: STORAGE_IDS.RAINBOW_TOKEN_LIST, diff --git a/src/references/testnet-assets-by-chain.ts b/src/references/testnet-assets-by-chain.ts index 8f0eedac9ba..141a195f022 100644 --- a/src/references/testnet-assets-by-chain.ts +++ b/src/references/testnet-assets-by-chain.ts @@ -1,5 +1,5 @@ import { UniqueId, ZerionAsset } from '@/__swaps__/types/assets'; -import { ChainId, ChainName } from '@/chains/types'; +import { ChainId, ChainName } from '@/state/backendNetworks/types'; type ChainAssets = { [uniqueId: UniqueId]: { diff --git a/src/resources/addys/claimables/query.ts b/src/resources/addys/claimables/query.ts index 311da649a50..b1e3d14e87e 100644 --- a/src/resources/addys/claimables/query.ts +++ b/src/resources/addys/claimables/query.ts @@ -9,7 +9,7 @@ import { parseClaimables } from './utils'; import { useRemoteConfig } from '@/model/remoteConfig'; import { CLAIMABLES, useExperimentalFlag } from '@/config'; import { IS_TEST } from '@/env'; -import { SUPPORTED_CHAIN_IDS } from '@/chains'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; export const ADDYS_BASE_URL = 'https://addys.p.rainbow.me/v3'; @@ -41,7 +41,7 @@ type ClaimablesQueryKey = ReturnType; async function claimablesQueryFunction({ queryKey: [{ address, currency }] }: QueryFunctionArgs) { try { - const url = `/${SUPPORTED_CHAIN_IDS.join(',')}/${address}/claimables`; + const url = `/${useBackendNetworksStore.getState().getSupportedChainIds().join(',')}/${address}/claimables`; const { data } = await addysHttp.get(url, { params: { currency: currency.toLowerCase(), diff --git a/src/resources/addys/claimables/types.ts b/src/resources/addys/claimables/types.ts index 426ed6e4523..2004563c6de 100644 --- a/src/resources/addys/claimables/types.ts +++ b/src/resources/addys/claimables/types.ts @@ -1,6 +1,6 @@ import { Address } from 'viem'; import { AddysAsset, AddysConsolidatedError, AddysResponseStatus } from '../types'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { ParsedAddressAsset } from '@/entities'; interface Colors { diff --git a/src/resources/addys/claimables/utils.ts b/src/resources/addys/claimables/utils.ts index edc71b29578..94047252d47 100644 --- a/src/resources/addys/claimables/utils.ts +++ b/src/resources/addys/claimables/utils.ts @@ -1,9 +1,9 @@ import { NativeCurrencyKey } from '@/entities'; import { AddysClaimable, Claimable } from './types'; -import { convertRawAmountToBalance, convertRawAmountToNativeDisplay, greaterThan } from '@/helpers/utilities'; +import { convertRawAmountToBalance, convertRawAmountToNativeDisplay } from '@/helpers/utilities'; import { parseAsset } from '@/resources/assets/assets'; -import { Network } from '@/chains/types'; -import { chainsName } from '@/chains'; +import { Network } from '@/state/backendNetworks/types'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; export const parseClaimables = (claimables: AddysClaimable[], currency: NativeCurrencyKey): Claimable[] => { return claimables @@ -20,7 +20,7 @@ export const parseClaimables = (claimables: AddysClaimable[], currency: NativeCu address: claimable.asset.asset_code, asset: { ...claimable.asset, - network: chainsName[claimable.network] as Network, + network: useBackendNetworksStore.getState().getChainsName()[claimable.network] as Network, transferable: claimable.asset.transferable ?? false, }, }), diff --git a/src/resources/addys/types.ts b/src/resources/addys/types.ts index 6da5c2cc4d7..17d330b9927 100644 --- a/src/resources/addys/types.ts +++ b/src/resources/addys/types.ts @@ -1,4 +1,4 @@ -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { Address } from 'viem'; interface BridgeableNetwork { diff --git a/src/resources/assets/UserAssetsQuery.ts b/src/resources/assets/UserAssetsQuery.ts index e9c345ca8d2..5616163e903 100644 --- a/src/resources/assets/UserAssetsQuery.ts +++ b/src/resources/assets/UserAssetsQuery.ts @@ -9,9 +9,9 @@ import { useQuery } from '@tanstack/react-query'; import { filterPositionsData, parseAddressAsset } from './assets'; import { fetchHardhatBalances } from './hardhatAssets'; import { AddysAccountAssetsMeta, AddysAccountAssetsResponse, RainbowAddressAssets } from './types'; -import { Network } from '@/chains/types'; +import { Network } from '@/state/backendNetworks/types'; import { staleBalancesStore } from '@/state/staleBalances'; -import { SUPPORTED_MAINNET_CHAIN_IDS } from '@/chains'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; // /////////////////////////////////////////////// // Query Types @@ -80,7 +80,7 @@ async function userAssetsQueryFunction({ const { erroredChainIds, results } = await fetchAndParseUserAssetsForChainIds({ address, currency, - chainIds: SUPPORTED_MAINNET_CHAIN_IDS, + chainIds: useBackendNetworksStore.getState().getSupportedMainnetChainIds(), staleBalanceParam, }); let parsedSuccessResults = results; diff --git a/src/resources/assets/assets.ts b/src/resources/assets/assets.ts index f8c9eb9abfb..057963856bb 100644 --- a/src/resources/assets/assets.ts +++ b/src/resources/assets/assets.ts @@ -8,8 +8,8 @@ import { positionsQueryKey } from '@/resources/defi/PositionsQuery'; import { RainbowPositions } from '@/resources/defi/types'; import { AddysAddressAsset, AddysAsset, ParsedAsset, RainbowAddressAssets } from './types'; import { getUniqueId } from '@/utils/ethereumUtils'; -import { chainsIdByName } from '@/chains'; -import { ChainId } from '@/chains/types'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; +import { ChainId } from '@/state/backendNetworks/types'; export const filterPositionsData = ( address: string, @@ -34,7 +34,7 @@ export const filterPositionsData = ( export function parseAsset({ address, asset }: { address: string; asset: AddysAsset }): ParsedAsset { const network = asset?.network; - const chainId = chainsIdByName[network]; + const chainId = useBackendNetworksStore.getState().getChainsIdByName()[network]; const mainnetAddress = asset?.networks?.[ChainId.mainnet]?.address; const uniqueId = getUniqueId(address, chainId); diff --git a/src/resources/assets/externalAssetsQuery.ts b/src/resources/assets/externalAssetsQuery.ts index 03ef2f5e1cf..7abbddd523f 100644 --- a/src/resources/assets/externalAssetsQuery.ts +++ b/src/resources/assets/externalAssetsQuery.ts @@ -4,7 +4,7 @@ import { createQueryKey, queryClient, QueryConfig, QueryFunctionArgs, QueryFunct import { convertAmountAndPriceToNativeDisplay, convertAmountToPercentageDisplay } from '@/helpers/utilities'; import { NativeCurrencyKey } from '@/entities'; import { Token } from '@/graphql/__generated__/metadata'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { isNativeAsset } from '@/handlers/assets'; import { AddressOrEth } from '@/__swaps__/types/assets'; diff --git a/src/resources/assets/hardhatAssets.ts b/src/resources/assets/hardhatAssets.ts index a559414041c..409a3ee1a1c 100644 --- a/src/resources/assets/hardhatAssets.ts +++ b/src/resources/assets/hardhatAssets.ts @@ -8,8 +8,8 @@ import { logger, RainbowError } from '@/logger'; import { AddressOrEth, UniqueId, ZerionAsset } from '@/__swaps__/types/assets'; import { AddressZero } from '@ethersproject/constants'; import chainAssetsByChainId from '@/references/testnet-assets-by-chain'; -import { ChainId, ChainName, Network } from '@/chains/types'; -import { SUPPORTED_CHAIN_IDS } from '@/chains'; +import { ChainId, ChainName, Network } from '@/state/backendNetworks/types'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; const MAINNET_BALANCE_CHECKER = '0x4dcf4562268dd384fe814c00fad239f06c2a0c2b'; @@ -125,6 +125,6 @@ export const fetchHardhatBalancesByChainId = async ( return { assets: updatedAssets, - chainIdsInResponse: SUPPORTED_CHAIN_IDS, + chainIdsInResponse: useBackendNetworksStore.getState().getSupportedChainIds(), }; }; diff --git a/src/resources/assets/types.ts b/src/resources/assets/types.ts index 3a37f1a76b9..ad5c3a62285 100644 --- a/src/resources/assets/types.ts +++ b/src/resources/assets/types.ts @@ -1,6 +1,6 @@ import { NativeCurrencyKey, ParsedAddressAsset } from '@/entities'; import { TokenColors } from '@/graphql/__generated__/metadata'; -import { Network } from '@/chains/types'; +import { Network } from '@/state/backendNetworks/types'; export type AddysAccountAssetsResponse = { meta: AddysAccountAssetsMeta; diff --git a/src/resources/assets/useUserAsset.ts b/src/resources/assets/useUserAsset.ts index e6033701e58..4ee1dea9de1 100644 --- a/src/resources/assets/useUserAsset.ts +++ b/src/resources/assets/useUserAsset.ts @@ -1,10 +1,10 @@ -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { useAccountSettings } from '@/hooks'; import { useUserAssets } from '@/resources/assets/UserAssetsQuery'; import { selectUserAssetWithUniqueId } from '@/resources/assets/assetSelectors'; import { getUniqueId } from '@/utils/ethereumUtils'; import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; -import { chainsNativeAsset } from '@/chains'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; export function useUserAsset(uniqueId: string) { const { accountAddress, nativeCurrency } = useAccountSettings(); @@ -23,7 +23,7 @@ export function useUserAsset(uniqueId: string) { } export function useUserNativeNetworkAsset(chainId: ChainId) { - const nativeCurrency = chainsNativeAsset[chainId]; + const nativeCurrency = useBackendNetworksStore.getState().getChainsNativeAsset()[chainId]; const { address } = nativeCurrency; const uniqueId = getUniqueId(address, chainId); return useUserAsset(uniqueId); diff --git a/src/resources/defi/PositionsQuery.ts b/src/resources/defi/PositionsQuery.ts index fb8d7ef582c..f1052b39f01 100644 --- a/src/resources/defi/PositionsQuery.ts +++ b/src/resources/defi/PositionsQuery.ts @@ -7,12 +7,12 @@ import { rainbowFetch } from '@/rainbow-fetch'; import { ADDYS_API_KEY } from 'react-native-dotenv'; import { AddysPositionsResponse, PositionsArgs } from './types'; import { parsePositions } from './utils'; -import { SUPPORTED_CHAIN_IDS } from '@/chains'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; import { DEFI_POSITIONS, useExperimentalFlag } from '@/config'; import { IS_TEST } from '@/env'; export const buildPositionsUrl = (address: string) => { - const networkString = SUPPORTED_CHAIN_IDS.join(','); + const networkString = useBackendNetworksStore.getState().getSupportedChainIds().join(','); return `https://addys.p.rainbow.me/v3/${networkString}/${address}/positions`; }; diff --git a/src/resources/defi/types.ts b/src/resources/defi/types.ts index 7a26518bd21..8f8da356468 100644 --- a/src/resources/defi/types.ts +++ b/src/resources/defi/types.ts @@ -1,5 +1,5 @@ import { NativeCurrencyKey } from '@/entities'; -import { ChainId, Network } from '@/chains/types'; +import { Network, ChainId } from '@/state/backendNetworks/types'; import { AddysAsset } from '@/resources/addys/types'; export type AddysPositionsResponse = diff --git a/src/resources/defi/utils.ts b/src/resources/defi/utils.ts index 7399b3b953b..672a325c962 100644 --- a/src/resources/defi/utils.ts +++ b/src/resources/defi/utils.ts @@ -18,7 +18,7 @@ import { import { add, convertAmountToNativeDisplay, convertRawAmountToNativeDisplay, lessThan, subtract } from '@/helpers/utilities'; import { maybeSignUri } from '@/handlers/imgix'; import { ethereumUtils } from '@/utils'; -import { chainsIdByName } from '@/chains'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; const PROTOCOL_VERSION_REGEX = /-[vV]\d+$/; const LP_POOL_SYMBOL = 'LP-POOL'; @@ -394,6 +394,7 @@ export function parsePositions(data: AddysPositionsResponse, currency: NativeCur // these are tokens that would be represented twice if shown in the token list, such as a Sushiswap LP token const tokensToExcludeFromTokenList: string[] = []; + const chainsIdByName = useBackendNetworksStore.getState().getChainsIdByName(); positions.forEach(({ deposits }) => { deposits.forEach(({ asset }) => { diff --git a/src/resources/ens/ensAddressQuery.ts b/src/resources/ens/ensAddressQuery.ts index 717b2616a2a..a35a7d11a8e 100644 --- a/src/resources/ens/ensAddressQuery.ts +++ b/src/resources/ens/ensAddressQuery.ts @@ -2,7 +2,7 @@ import { useQuery } from '@tanstack/react-query'; import { createQueryKey, queryClient, QueryFunctionArgs } from '@/react-query'; import { getProvider } from '@/handlers/web3'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; // Set a default stale time of 10 seconds so we don't over-fetch // (query will serve cached data & invalidate after 10s). diff --git a/src/resources/favorites.ts b/src/resources/favorites.ts index f27044c7f5c..f6d897f84bb 100644 --- a/src/resources/favorites.ts +++ b/src/resources/favorites.ts @@ -1,7 +1,6 @@ import { AddressOrEth, UniqueId } from '@/__swaps__/types/assets'; -import { ChainId, Network } from '@/chains/types'; +import { ChainId, Network } from '@/state/backendNetworks/types'; import { getUniqueId } from '@/utils/ethereumUtils'; -import { chainsIdByName, chainsName } from '@/chains'; import { NativeCurrencyKeys, RainbowToken } from '@/entities'; import { createQueryKey, queryClient } from '@/react-query'; import { DAI_ADDRESS, ETH_ADDRESS, SOCKS_ADDRESS, WBTC_ADDRESS, WETH_ADDRESS } from '@/references'; @@ -9,6 +8,7 @@ import { promiseUtils } from '@/utils'; import { useQuery } from '@tanstack/react-query'; import { omit } from 'lodash'; import { externalTokenQueryKey, fetchExternalToken } from './assets/externalAssetsQuery'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; import { analyticsV2 } from '@/analytics'; export const favoritesQueryKey = createQueryKey('favorites', {}, { persisterVersion: 4 }); @@ -21,7 +21,7 @@ const DEFAULT_FAVORITES = [DAI_ADDRESS, ETH_ADDRESS, SOCKS_ADDRESS, WBTC_ADDRESS async function fetchMetadata(addresses: string[], chainId = ChainId.mainnet) { const favoritesMetadata: Record = {}; const newFavoritesMeta: Record = {}; - const network = chainsName[chainId]; + const network = useBackendNetworksStore.getState().getChainsName()[chainId]; // Map addresses to an array of promises returned by fetchExternalToken const fetchPromises: Promise[] = addresses.map(async address => { @@ -92,6 +92,8 @@ export async function refreshFavorites() { {} as Record ); + const chainsIdByName = useBackendNetworksStore.getState().getChainsIdByName(); + const updatedMetadataByNetwork = await Promise.all( Object.entries(favoritesByNetwork).map(async ([network, networkFavorites]) => fetchMetadata(networkFavorites, chainsIdByName[network as Network]) diff --git a/src/resources/metadata/backendNetworks.ts b/src/resources/metadata/backendNetworks.ts index 4e36f816300..623d6f5d23c 100644 --- a/src/resources/metadata/backendNetworks.ts +++ b/src/resources/metadata/backendNetworks.ts @@ -1,6 +1,6 @@ import { useQuery } from '@tanstack/react-query'; import { QueryConfigWithSelect, QueryFunctionArgs, createQueryKey, queryClient } from '@/react-query'; -import { BackendNetwork } from '@/chains/types'; +import { BackendNetwork } from '@/state/backendNetworks/types'; import { BACKEND_NETWORKS_QUERY } from './sharedQueries'; // /////////////////////////////////////////////// @@ -50,13 +50,13 @@ export async function backendNetworksQueryFunction({ // /////////////////////////////////////////////// // Query Hook -export function useBackendNetworks( - config: QueryConfigWithSelect = {} +export function useBackendNetworks( + config: QueryConfigWithSelect = {} ) { return useQuery(backendNetworksQueryKey(), backendNetworksQueryFunction, { ...config, - cacheTime: 1000 * 60 * 60 * 24, // 24 hours - staleTime: 1000 * 60 * 15, // 15 minutes + refetchInterval: 60_000, + staleTime: process.env.IS_TESTING === 'true' ? 0 : 1000, }); } diff --git a/src/resources/nfts/index.ts b/src/resources/nfts/index.ts index d5585f29197..2c448365e97 100644 --- a/src/resources/nfts/index.ts +++ b/src/resources/nfts/index.ts @@ -8,7 +8,7 @@ import { UniqueAsset } from '@/entities'; import { arcClient } from '@/graphql'; import { NftCollectionSortCriterion, SortDirection } from '@/graphql/__generated__/arc'; import { createSelector } from 'reselect'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; const NFTS_STALE_TIME = 600000; // 10 minutes const NFTS_CACHE_TIME_EXTERNAL = 3600000; // 1 hour diff --git a/src/resources/nfts/simplehash/index.ts b/src/resources/nfts/simplehash/index.ts index 83ffac0574b..dfa305925a8 100644 --- a/src/resources/nfts/simplehash/index.ts +++ b/src/resources/nfts/simplehash/index.ts @@ -3,8 +3,8 @@ import { RainbowFetchClient } from '@/rainbow-fetch'; import { SimpleHashListing, SimpleHashNFT, SimpleHashMarketplaceId } from '@/resources/nfts/simplehash/types'; import { UniqueAsset } from '@/entities'; import { RainbowError, logger } from '@/logger'; -import { ChainId } from '@/chains/types'; -import { chainsSimplehashNetwork } from '@/chains'; +import { ChainId } from '@/state/backendNetworks/types'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; export const START_CURSOR = 'start'; @@ -19,7 +19,7 @@ export async function fetchSimpleHashNFT( tokenId: string, chainId: Omit = ChainId.mainnet ): Promise { - const simplehashNetwork = chainsSimplehashNetwork[chainId as ChainId]; + const simplehashNetwork = useBackendNetworksStore.getState().getChainsSimplehashNetwork()[chainId as ChainId]; if (!simplehashNetwork) { logger.warn(`[simplehash]: no SimpleHash for chainId: ${chainId}`); @@ -44,7 +44,7 @@ export async function fetchSimpleHashNFTListing( // array of all eth listings on OpenSea for this token let listings: SimpleHashListing[] = []; let cursor = START_CURSOR; - const simplehashNetwork = chainsSimplehashNetwork[chainId as ChainId]; + const simplehashNetwork = useBackendNetworksStore.getState().getChainsSimplehashNetwork()[chainId as ChainId]; if (!simplehashNetwork) { logger.warn(`[simplehash]: no SimpleHash for chainId: ${chainId}`); @@ -83,7 +83,7 @@ export async function fetchSimpleHashNFTListing( * @param nft */ export async function refreshNFTContractMetadata(nft: UniqueAsset) { - const simplehashNetwork = chainsSimplehashNetwork[nft.isPoap ? ChainId.gnosis : nft.chainId]; + const simplehashNetwork = useBackendNetworksStore.getState().getChainsSimplehashNetwork()[nft.isPoap ? ChainId.gnosis : nft.chainId]; if (!simplehashNetwork) { logger.warn(`[simplehash]: no SimpleHash for chainId: ${nft.chainId}`); @@ -135,7 +135,7 @@ export async function refreshNFTContractMetadata(nft: UniqueAsset) { * @param nft */ export async function reportNFT(nft: UniqueAsset) { - const simplehashNetwork = chainsSimplehashNetwork[nft.isPoap ? ChainId.gnosis : nft.chainId]; + const simplehashNetwork = useBackendNetworksStore.getState().getChainsSimplehashNetwork()[nft.isPoap ? ChainId.gnosis : nft.chainId]; if (!simplehashNetwork) { logger.warn(`[simplehash]: no SimpleHash for chainId: ${nft.chainId}`); diff --git a/src/resources/nfts/simplehash/types.ts b/src/resources/nfts/simplehash/types.ts index 9c769c72e80..1aad59dba5b 100644 --- a/src/resources/nfts/simplehash/types.ts +++ b/src/resources/nfts/simplehash/types.ts @@ -1,4 +1,4 @@ -import { Network } from '@/chains/types'; +import { Network } from '@/state/backendNetworks/types'; /** * @see https://docs.simplehash.com/reference/sale-model diff --git a/src/resources/nfts/simplehash/utils.ts b/src/resources/nfts/simplehash/utils.ts index ab3c0fc3f56..0c62afa97db 100644 --- a/src/resources/nfts/simplehash/utils.ts +++ b/src/resources/nfts/simplehash/utils.ts @@ -20,8 +20,8 @@ import { deviceUtils } from '@/utils'; import { TokenStandard } from '@/handlers/web3'; import { handleNFTImages } from '@/utils/handleNFTImages'; import { SimpleHashNft } from '@/graphql/__generated__/arc'; -import { Network } from '@/chains/types'; -import { chainsIdByName } from '@/chains'; +import { Network } from '@/state/backendNetworks/types'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; const ENS_COLLECTION_NAME = 'ENS'; const SVG_MIME_TYPE = 'image/svg+xml'; @@ -60,6 +60,8 @@ export function simpleHashNFTToUniqueAsset(nft: SimpleHashNft, address: string): const ownerEntry = nft.owners?.find(o => o.owner_address === address); + const chainsIdByName = useBackendNetworksStore.getState().getChainsIdByName(); + return { animation_url: nft?.video_url ?? nft.audio_url ?? nft.model_url ?? nft.extra_metadata?.animation_original_url ?? undefined, asset_contract: { diff --git a/src/resources/nfts/types.ts b/src/resources/nfts/types.ts index 88459dce3af..6f77ce7614d 100644 --- a/src/resources/nfts/types.ts +++ b/src/resources/nfts/types.ts @@ -1,5 +1,5 @@ import { Asset, AssetContract, AssetType } from '@/entities'; -import { Network } from '@/chains/types'; +import { Network } from '@/state/backendNetworks/types'; import { UniqueTokenType } from '@/utils/uniqueTokens'; export enum NFTMarketplaceId { diff --git a/src/resources/nfts/utils.ts b/src/resources/nfts/utils.ts index 7b558cd849d..a8c806a4906 100644 --- a/src/resources/nfts/utils.ts +++ b/src/resources/nfts/utils.ts @@ -5,7 +5,7 @@ import { RainbowError, logger } from '@/logger'; import { handleSignificantDecimals } from '@/helpers/utilities'; import { IS_PROD } from '@/env'; import { RESERVOIR_API_KEY_DEV, RESERVOIR_API_KEY_PROD } from 'react-native-dotenv'; -import { Network } from '@/chains/types'; +import { Network } from '@/state/backendNetworks/types'; const SUPPORTED_NETWORKS = [Network.mainnet, Network.polygon, Network.bsc, Network.arbitrum, Network.optimism, Network.base, Network.zora]; diff --git a/src/resources/reservoir/client.ts b/src/resources/reservoir/client.ts index cb3021e5258..43b74b03c1c 100644 --- a/src/resources/reservoir/client.ts +++ b/src/resources/reservoir/client.ts @@ -1,7 +1,7 @@ import { createClient } from '@reservoir0x/reservoir-sdk'; import { IS_PROD } from '@/env'; import { RESERVOIR_API_KEY_PROD, RESERVOIR_API_KEY_DEV } from 'react-native-dotenv'; -import { ChainId, Network } from '@/chains/types'; +import { ChainId, Network } from '@/state/backendNetworks/types'; const RESERVOIR_API_KEY = IS_PROD ? RESERVOIR_API_KEY_PROD : RESERVOIR_API_KEY_DEV; diff --git a/src/resources/reservoir/mints.ts b/src/resources/reservoir/mints.ts index 7f28aaca8cc..361786328b8 100644 --- a/src/resources/reservoir/mints.ts +++ b/src/resources/reservoir/mints.ts @@ -6,7 +6,7 @@ import { logger } from '@/logger'; import { WrappedAlert as Alert } from '@/helpers/alert'; import * as lang from '@/languages'; import { BigNumberish } from '@ethersproject/bignumber'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; const showAlert = () => { Alert.alert( diff --git a/src/resources/reservoir/utils.ts b/src/resources/reservoir/utils.ts index 69145106e9d..e1991297161 100644 --- a/src/resources/reservoir/utils.ts +++ b/src/resources/reservoir/utils.ts @@ -1,4 +1,4 @@ -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; const RAINBOW_FEE_ADDRESS_MAINNET = '0x69d6d375de8c7ade7e44446df97f49e661fdad7d'; const RAINBOW_FEE_ADDRESS_POLYGON = '0xfb9af3db5e19c4165f413f53fe3bbe6226834548'; diff --git a/src/resources/transactions/consolidatedTransactions.ts b/src/resources/transactions/consolidatedTransactions.ts index 616af75da8e..a749e2f72d0 100644 --- a/src/resources/transactions/consolidatedTransactions.ts +++ b/src/resources/transactions/consolidatedTransactions.ts @@ -5,7 +5,7 @@ import { RainbowError, logger } from '@/logger'; import { rainbowFetch } from '@/rainbow-fetch'; import { ADDYS_API_KEY } from 'react-native-dotenv'; import { parseTransaction } from '@/parsers/transactions'; -import { chainsIdByName, SUPPORTED_MAINNET_CHAIN_IDS } from '@/chains'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; const CONSOLIDATED_TRANSACTIONS_INTERVAL = 30000; const CONSOLIDATED_TRANSACTIONS_TIMEOUT = 20000; @@ -106,6 +106,8 @@ async function parseConsolidatedTransactions( ): Promise { const data = message?.payload?.transactions || []; + const chainsIdByName = useBackendNetworksStore.getState().getChainsIdByName(); + const parsedTransactionPromises = data.map((tx: TransactionApiResponse) => parseTransaction(tx, currency, chainsIdByName[tx.network])); // Filter out undefined values immediately @@ -125,7 +127,7 @@ export function useConsolidatedTransactions( consolidatedTransactionsQueryKey({ address, currency, - chainIds: SUPPORTED_MAINNET_CHAIN_IDS, + chainIds: useBackendNetworksStore.getState().getSupportedMainnetChainIds(), }), consolidatedTransactionsQueryFunction, { diff --git a/src/resources/transactions/transaction.ts b/src/resources/transactions/transaction.ts index a320b3d9eef..8280e63975d 100644 --- a/src/resources/transactions/transaction.ts +++ b/src/resources/transactions/transaction.ts @@ -7,8 +7,8 @@ import { rainbowFetch } from '@/rainbow-fetch'; import { ADDYS_API_KEY } from 'react-native-dotenv'; import { parseTransaction } from '@/parsers/transactions'; import { RainbowError, logger } from '@/logger'; -import { ChainId } from '@/chains/types'; -import { SUPPORTED_MAINNET_CHAIN_IDS } from '@/chains'; +import { ChainId } from '@/state/backendNetworks/types'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; export type ConsolidatedTransactionsResult = QueryFunctionResult; export type PaginatedTransactions = { pages: ConsolidatedTransactionsResult[] }; @@ -81,7 +81,7 @@ export function useBackendTransaction({ hash, chainId }: BackendTransactionArgs) const paginatedTransactionsKey = consolidatedTransactionsQueryKey({ address: accountAddress, currency: nativeCurrency, - chainIds: SUPPORTED_MAINNET_CHAIN_IDS, + chainIds: useBackendNetworksStore.getState().getSupportedMainnetChainIds(), }); const params: TransactionArgs = { diff --git a/src/resources/transactions/transactionSimulation.ts b/src/resources/transactions/transactionSimulation.ts index e9bb37df9ec..2f26fa2ec3e 100644 --- a/src/resources/transactions/transactionSimulation.ts +++ b/src/resources/transactions/transactionSimulation.ts @@ -5,7 +5,7 @@ import { metadataPOSTClient } from '@/graphql'; import { TransactionErrorType, TransactionScanResultType, TransactionSimulationResult } from '@/graphql/__generated__/metadataPOST'; import { isNil } from 'lodash'; import { RequestData } from '@/walletConnect/types'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; type SimulationArgs = { address: string; diff --git a/src/screens/AddCash/components/ProviderCard.tsx b/src/screens/AddCash/components/ProviderCard.tsx index 7ff2e135dae..9cc19003cd2 100644 --- a/src/screens/AddCash/components/ProviderCard.tsx +++ b/src/screens/AddCash/components/ProviderCard.tsx @@ -16,7 +16,7 @@ import { convertAPINetworkToInternalChainIds } from '@/screens/AddCash/utils'; import { ProviderConfig, CalloutType, PaymentMethod } from '@/screens/AddCash/types'; import * as i18n from '@/languages'; import { EthCoinIcon } from '@/components/coin-icon/EthCoinIcon'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; type PaymentMethodConfig = { name: string; diff --git a/src/screens/AddCash/utils.ts b/src/screens/AddCash/utils.ts index 1cc7a37c6f7..4d334137128 100644 --- a/src/screens/AddCash/utils.ts +++ b/src/screens/AddCash/utils.ts @@ -1,4 +1,4 @@ -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { Network as APINetwork } from '@/screens/AddCash/types'; export function convertAPINetworkToInternalChainIds(network: APINetwork): ChainId | undefined { diff --git a/src/screens/ENSConfirmRegisterSheet.tsx b/src/screens/ENSConfirmRegisterSheet.tsx index 600d160b929..2a1105933f4 100644 --- a/src/screens/ENSConfirmRegisterSheet.tsx +++ b/src/screens/ENSConfirmRegisterSheet.tsx @@ -39,7 +39,7 @@ import { usePersistentDominantColorFromImage } from '@/hooks/usePersistentDomina import { handleReviewPromptAction } from '@/utils/reviewAlert'; import { ReviewPromptAction } from '@/storage/schema'; import { ActionTypes } from '@/hooks/useENSRegistrationActionHandler'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; export const ENSConfirmRegisterSheetHeight = 600; export const ENSConfirmRenewSheetHeight = 560; diff --git a/src/screens/ExplainSheet.js b/src/screens/ExplainSheet.js index 622b9d05213..d8945923c99 100644 --- a/src/screens/ExplainSheet.js +++ b/src/screens/ExplainSheet.js @@ -22,8 +22,8 @@ import { isL2Chain } from '@/handlers/web3'; import { IS_ANDROID } from '@/env'; import { EthCoinIcon } from '@/components/coin-icon/EthCoinIcon'; import RainbowCoinIcon from '@/components/coin-icon/RainbowCoinIcon'; -import { ChainId } from '@/chains/types'; -import { chainsLabel } from '@/chains'; +import { ChainId } from '@/state/backendNetworks/types'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; const { GAS_TRENDS } = gasUtils; export const ExplainSheetHeight = android ? 454 : 434; @@ -76,7 +76,7 @@ const SENDING_FUNDS_TO_CONTRACT = lang.t('explain.sending_to_contract.text'); const FLOOR_PRICE_EXPLAINER = lang.t('explain.floor_price.text'); const networkExplainer = ({ emoji = '⛽️', chainId, ...props }) => { - const chainName = chainsLabel[chainId]; + const chainName = useBackendNetworksStore.getState().getChainsLabel()[chainId]; let title = lang.t(`explain.default_network_explainer.title`, { chainName }); let text = lang.t(`explain.default_network_explainer.text`, { chainName }); @@ -107,6 +107,7 @@ const networkExplainer = ({ emoji = '⛽️', chainId, ...props }) => { const gasExplainer = network => lang.t('explain.gas.text', { networkName: network }); const availableNetworksExplainer = (tokenSymbol, chainIds) => { + const chainsLabel = useBackendNetworksStore.getState().getChainsLabel(); const readableNetworks = chainIds?.map(chainId => chainsLabel[chainId])?.join(', '); return lang.t('explain.available_networks.text', { @@ -171,6 +172,9 @@ export const explainers = (params, theme) => { const chainId = params?.chainId; const fromChainId = params?.fromChainId; const toChainId = params?.toChainId; + + const chainsLabel = useBackendNetworksStore.getState().getChainsLabel(); + return { op_rewards_airdrop_timing: { emoji: '📦', diff --git a/src/screens/MintsSheet/card/Card.tsx b/src/screens/MintsSheet/card/Card.tsx index c0317d7fc0f..a268ad94090 100644 --- a/src/screens/MintsSheet/card/Card.tsx +++ b/src/screens/MintsSheet/card/Card.tsx @@ -12,8 +12,8 @@ import * as i18n from '@/languages'; import ChainBadge from '@/components/coin-icon/ChainBadge'; import { navigateToMintCollection } from '@/resources/reservoir/mints'; import { EthCoinIcon } from '@/components/coin-icon/EthCoinIcon'; -import { ChainId } from '@/chains/types'; -import { chainsNativeAsset } from '@/chains'; +import { ChainId } from '@/state/backendNetworks/types'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; export const NUM_NFTS = 3; @@ -28,7 +28,7 @@ export function Card({ collection }: { collection: MintableCollection }) { const separatorTertiary = useForegroundColor('separatorTertiary'); const price = convertRawAmountToRoundedDecimal(collection.mintStatus.price, 18, 6); - const currencySymbol = chainsNativeAsset[collection.chainId].symbol; + const currencySymbol = useBackendNetworksStore.getState().getChainsNativeAsset()[collection.chainId].symbol; const isFree = !price; // update elapsed time every minute if it's less than an hour diff --git a/src/screens/NFTOffersSheet/OfferRow.tsx b/src/screens/NFTOffersSheet/OfferRow.tsx index 9707e780a89..7983c7edd33 100644 --- a/src/screens/NFTOffersSheet/OfferRow.tsx +++ b/src/screens/NFTOffersSheet/OfferRow.tsx @@ -14,11 +14,11 @@ import { CardSize } from '@/components/unique-token/CardSize'; import { View } from 'react-native'; import Svg, { Path } from 'react-native-svg'; import { useExternalToken } from '@/resources/assets/externalAssetsQuery'; -import { Network } from '@/chains/types'; +import { Network } from '@/state/backendNetworks/types'; import { useAccountSettings } from '@/hooks'; import RainbowCoinIcon from '@/components/coin-icon/RainbowCoinIcon'; import { AddressOrEth } from '@/__swaps__/types/assets'; -import { chainsIdByName } from '@/chains'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; const NFT_SIZE = 50; const MARKETPLACE_ORB_SIZE = 18; @@ -99,7 +99,7 @@ export const OfferRow = ({ offer }: { offer: NftOffer }) => { const { colorMode } = useColorMode(); const theme = useTheme(); const bgColor = useBackgroundColor('surfaceSecondaryElevated'); - const chainId = chainsIdByName[offer.network as Network]; + const chainId = useBackendNetworksStore.getState().getChainsIdByName()[offer.network as Network]; const { data: externalAsset } = useExternalToken({ address: offer.paymentToken.address as AddressOrEth, chainId, diff --git a/src/screens/NFTSingleOfferSheet/index.tsx b/src/screens/NFTSingleOfferSheet/index.tsx index fec9ccad360..89dc64f2fa6 100644 --- a/src/screens/NFTSingleOfferSheet/index.tsx +++ b/src/screens/NFTSingleOfferSheet/index.tsx @@ -39,7 +39,7 @@ import { createWalletClient, http } from 'viem'; import { RainbowError, logger } from '@/logger'; import { useTheme } from '@/theme'; -import { Network, ChainId } from '@/chains/types'; +import { Network, ChainId } from '@/state/backendNetworks/types'; import { CardSize } from '@/components/unique-token/CardSize'; import { queryClient } from '@/react-query'; import { nftOffersQueryKey } from '@/resources/reservoir/nftOffersQuery'; @@ -48,11 +48,11 @@ import { useExternalToken } from '@/resources/assets/externalAssetsQuery'; import RainbowCoinIcon from '@/components/coin-icon/RainbowCoinIcon'; import { addNewTransaction } from '@/state/pendingTransactions'; import { getUniqueId } from '@/utils/ethereumUtils'; -import { chainsIdByName, chainsNativeAsset, defaultChains, getChainDefaultRpc } from '@/chains'; import { getNextNonce } from '@/state/nonces'; import { metadataPOSTClient } from '@/graphql'; import { ethUnits } from '@/references'; import { Transaction } from '@/graphql/__generated__/metadataPOST'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; const NFT_IMAGE_HEIGHT = 160; const TWO_HOURS_MS = 2 * 60 * 60 * 1000; @@ -94,7 +94,7 @@ export function NFTSingleOfferSheet() { } = useLegacyNFTs({ address: accountAddress }); const { offer } = params as { offer: NftOffer }; - const offerChainId = chainsIdByName[offer.network as Network]; + const offerChainId = useBackendNetworksStore.getState().getChainsIdByName()[offer.network as Network]; const { data: externalAsset } = useExternalToken({ address: offer.paymentToken.address, chainId: offerChainId, @@ -164,8 +164,6 @@ export function NFTSingleOfferSheet() { const feesPercentage = Math.floor(offer.feesPercentage * 10) / 10; const royaltiesPercentage = Math.floor(offer.royaltiesPercentage * 10) / 10; - const chain = defaultChains[offerChainId]; - useEffect(() => { setParams({ longFormHeight: height }); }, [height, setParams]); @@ -184,8 +182,8 @@ export function NFTSingleOfferSheet() { const signer = createWalletClient({ // @ts-ignore account: accountAddress, - chain, - transport: http(getChainDefaultRpc(offerChainId)), + chain: useBackendNetworksStore.getState().getDefaultChains()[offerChainId], + transport: http(useBackendNetworksStore.getState().getChainDefaultRpc(offerChainId)), }); getClient()?.actions.acceptOffer({ items: [ @@ -243,7 +241,7 @@ export function NFTSingleOfferSheet() { } catch { logger.error(new RainbowError('[NFTSingleOfferSheet]: Failed to estimate gas')); } - }, [accountAddress, feeParam, offerChainId, offer, updateTxFee]); + }, [accountAddress, feeParam, offer.nft.contractAddress, offer.nft.tokenId, offerChainId, updateTxFee]); // estimate gas useEffect(() => { @@ -254,7 +252,7 @@ export function NFTSingleOfferSheet() { return () => { stopPollingGasFees(); }; - }, [estimateGas, isExpired, isReadOnlyWallet, offer.network, offerChainId, startPollingGasFees, stopPollingGasFees, updateTxFee]); + }, [estimateGas, isExpired, isReadOnlyWallet, offerChainId, startPollingGasFees, stopPollingGasFees]); const acceptOffer = useCallback(async () => { logger.debug(`[NFTSingleOfferSheet]: Initiating sale of NFT ${offer.nft.contractAddress}:${offer.nft.tokenId}`); @@ -284,8 +282,8 @@ export function NFTSingleOfferSheet() { const signer = createWalletClient({ account, - chain, - transport: http(getChainDefaultRpc(offerChainId)), + chain: useBackendNetworksStore.getState().getDefaultChains()[offerChainId], + transport: http(useBackendNetworksStore.getState().getChainDefaultRpc(offerChainId)), }); const nonce = await getNextNonce({ address: accountAddress, chainId: offerChainId }); try { @@ -425,13 +423,13 @@ export function NFTSingleOfferSheet() { } finally { setIsAccepting(false); } - }, [offer, rainbowFeeDecimal, accountAddress, chain, offerChainId, feeParam, navigate, nft]); + }, [offer, rainbowFeeDecimal, accountAddress, offerChainId, feeParam, navigate, nft]); let buttonLabel = ''; if (!isAccepting) { if (insufficientEth) { buttonLabel = lang.t('button.confirm_exchange.insufficient_token', { - tokenName: chainsNativeAsset[offerChainId].symbol, + tokenName: useBackendNetworksStore.getState().getChainsNativeAsset()[offerChainId].symbol, }); } else { buttonLabel = i18n.t(i18n.l.nft_offers.single_offer_sheet.hold_to_sell); diff --git a/src/screens/SendConfirmationSheet.tsx b/src/screens/SendConfirmationSheet.tsx index 6edead99ea4..b150c5cbe9a 100644 --- a/src/screens/SendConfirmationSheet.tsx +++ b/src/screens/SendConfirmationSheet.tsx @@ -61,8 +61,8 @@ import { IS_ANDROID, IS_IOS } from '@/env'; import { useConsolidatedTransactions } from '@/resources/transactions/consolidatedTransactions'; import RainbowCoinIcon from '@/components/coin-icon/RainbowCoinIcon'; import { performanceTracking, TimeToSignOperation, Screens } from '@/state/performance/performance'; -import { ChainId } from '@/chains/types'; -import { chainsLabel } from '@/chains'; +import { ChainId } from '@/state/backendNetworks/types'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; const Container = styled(Centered).attrs({ direction: 'column', @@ -133,7 +133,7 @@ export function getDefaultCheckboxes({ checked: false, id: 'has-wallet-that-supports', label: lang.t('wallet.transaction.checkboxes.has_a_wallet_that_supports', { - networkName: chainsLabel[chainId], + networkName: useBackendNetworksStore.getState().getChainsLabel()[chainId], }), }, ]; @@ -602,7 +602,7 @@ export const SendConfirmationSheet = () => { onPress={handleL2DisclaimerPress} prominent customText={i18n.t(i18n.l.expanded_state.asset.l2_disclaimer_send, { - network: chainsLabel[asset.chainId], + network: useBackendNetworksStore.getState().getChainsLabel()[asset.chainId], })} symbol={asset.symbol} /> diff --git a/src/screens/SendSheet.tsx b/src/screens/SendSheet.tsx index eca3f922fa2..1369c2ec051 100644 --- a/src/screens/SendSheet.tsx +++ b/src/screens/SendSheet.tsx @@ -63,8 +63,8 @@ import { getNextNonce } from '@/state/nonces'; import { usePersistentDominantColorFromImage } from '@/hooks/usePersistentDominantColorFromImage'; import { performanceTracking, Screens, TimeToSignOperation } from '@/state/performance/performance'; import { REGISTRATION_STEPS } from '@/helpers/ens'; -import { ChainId } from '@/chains/types'; -import { chainsName, chainsNativeAsset, needsL1SecurityFeeChains } from '@/chains'; +import { ChainId } from '@/state/backendNetworks/types'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; import { RootStackParamList } from '@/navigation/types'; import { ThemeContextProps, useTheme } from '@/theme'; import { StaticJsonRpcProvider } from '@ethersproject/providers'; @@ -418,7 +418,7 @@ export default function SendSheet() { }); if (!wallet) return; - const currentChainIdNetwork = chainsName[currentChainId ?? ChainId.mainnet]; + const currentChainIdNetwork = useBackendNetworksStore.getState().getChainsName()[currentChainId ?? ChainId.mainnet]; const validTransaction = isValidAddress && amountDetails.isSufficientBalance && isSufficientGas && isValidGas; if (!selectedGasFee?.gasFee?.estimatedFee || !validTransaction) { @@ -450,7 +450,7 @@ export default function SendSheet() { ); if (updatedGasLimit && !lessThan(updatedGasLimit, gasLimit)) { - if (needsL1SecurityFeeChains.includes(currentChainId)) { + if (useBackendNetworksStore.getState().getNeedsL1SecurityFeeChains().includes(currentChainId)) { updateTxFeeForOptimism(updatedGasLimit); } else { updateTxFee(updatedGasLimit, null); @@ -672,14 +672,14 @@ export default function SendSheet() { !selectedGasFee || isEmpty(selectedGasFee?.gasFee) || !toAddress || - (needsL1SecurityFeeChains.includes(currentChainId) && l1GasFeeOptimism === null) + (useBackendNetworksStore.getState().getNeedsL1SecurityFeeChains().includes(currentChainId) && l1GasFeeOptimism === null) ) { label = lang.t('button.confirm_exchange.loading'); disabled = true; } else if (!isZeroAssetAmount && !isSufficientGas) { disabled = true; label = lang.t('button.confirm_exchange.insufficient_token', { - tokenName: chainsNativeAsset[currentChainId || ChainId.mainnet].symbol, + tokenName: useBackendNetworksStore.getState().getChainsNativeAsset()[currentChainId || ChainId.mainnet].symbol, }); } else if (!isValidGas) { disabled = true; @@ -856,7 +856,7 @@ export default function SendSheet() { currentChainId ) .then(async gasLimit => { - if (gasLimit && needsL1SecurityFeeChains.includes(currentChainId)) { + if (gasLimit && useBackendNetworksStore.getState().getNeedsL1SecurityFeeChains().includes(currentChainId)) { updateTxFeeForOptimism(gasLimit); } else { updateTxFee(gasLimit, null); diff --git a/src/screens/SettingsSheet/components/CurrencySection.tsx b/src/screens/SettingsSheet/components/CurrencySection.tsx index 337d3cd4c47..c523cebb00c 100644 --- a/src/screens/SettingsSheet/components/CurrencySection.tsx +++ b/src/screens/SettingsSheet/components/CurrencySection.tsx @@ -10,7 +10,7 @@ import { ETH_ADDRESS, WBTC_ADDRESS, emojis, supportedNativeCurrencies } from '@/ import { useExternalToken } from '@/resources/assets/externalAssetsQuery'; import RainbowCoinIcon from '@/components/coin-icon/RainbowCoinIcon'; import { useTheme } from '@/theme'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; const emojiData = Object.entries(emojis).map(([emoji, { name }]) => [name, emoji]); diff --git a/src/screens/SettingsSheet/components/NetworkSection.tsx b/src/screens/SettingsSheet/components/NetworkSection.tsx index df337be22ec..fb3ead755fe 100644 --- a/src/screens/SettingsSheet/components/NetworkSection.tsx +++ b/src/screens/SettingsSheet/components/NetworkSection.tsx @@ -8,8 +8,8 @@ import { analytics } from '@/analytics'; import { Separator, Stack } from '@/design-system'; import { useAccountSettings, useLoadAccountData } from '@/hooks'; import { settingsUpdateNetwork } from '@/redux/settings'; -import { ChainId } from '@/chains/types'; -import { defaultChains } from '@/chains'; +import { ChainId } from '@/state/backendNetworks/types'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; import { isL2Chain } from '@/handlers/web3'; interface NetworkSectionProps { @@ -33,7 +33,7 @@ const NetworkSection = ({ inDevSection }: NetworkSectionProps) => { ); const renderNetworkList = useCallback(() => { - return Object.values(defaultChains) + return Object.values(useBackendNetworksStore.getState().getDefaultChains()) .filter(({ id }) => !isL2Chain({ chainId: id })) .map(({ name, id, testnet }) => ( { } catch (e) { logger.error(new RainbowError(`[SignTransactionSheet]: Error while ${sendInsteadOfSign ? 'sending' : 'signing'} transaction`)); } + const chainsName = useBackendNetworksStore.getState().getChainsName(); if (response?.result) { const signResult = response.result as string; @@ -519,7 +520,7 @@ export const SignTransactionSheet = () => { dappName: transactionDetails?.dappName, dappUrl: transactionDetails?.dappUrl, isHardwareWallet: accountInfo.isHardwareWallet, - network: chainsName[chainId] as Network, + network: useBackendNetworksStore.getState().getChainsName()[chainId] as Network, }); onSuccessCallback?.(response.result); @@ -743,7 +744,7 @@ export const SignTransactionSheet = () => { {`${walletBalance?.display} ${i18n.t(i18n.l.walletconnect.simulation.profile_section.on_network, { - network: defaultChains[chainId]?.name, + network: useBackendNetworksStore.getState().getChainsName()[chainId], })}`} diff --git a/src/screens/SpeedUpAndCancelSheet.tsx b/src/screens/SpeedUpAndCancelSheet.tsx index 8b0ac0cdfd9..dcdb9fd3a54 100644 --- a/src/screens/SpeedUpAndCancelSheet.tsx +++ b/src/screens/SpeedUpAndCancelSheet.tsx @@ -30,7 +30,7 @@ import { gasUtils, safeAreaInsetValues } from '@/utils'; import * as i18n from '@/languages'; import { updateTransaction } from '@/state/pendingTransactions'; import { logger, RainbowError } from '@/logger'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { ThemeContextProps, useTheme } from '@/theme'; import { BigNumberish } from '@ethersproject/bignumber'; import { RootStackParamList } from '@/navigation/types'; diff --git a/src/screens/WalletConnectApprovalSheet.tsx b/src/screens/WalletConnectApprovalSheet.tsx index 9c3ff6295cf..e979e6f3ed3 100644 --- a/src/screens/WalletConnectApprovalSheet.tsx +++ b/src/screens/WalletConnectApprovalSheet.tsx @@ -29,8 +29,8 @@ import { DAppStatus } from '@/graphql/__generated__/metadata'; import { InfoAlert } from '@/components/info-alert/info-alert'; import { EthCoinIcon } from '@/components/coin-icon/EthCoinIcon'; import { findWalletWithAccount } from '@/helpers/findWalletWithAccount'; -import { ChainId } from '@/chains/types'; -import { chainsLabel, chainsNativeAsset, defaultChains } from '@/chains'; +import { ChainId } from '@/state/backendNetworks/types'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; import { ThemeContextProps, useTheme } from '@/theme'; import { noop } from 'lodash'; import { RootStackParamList } from '@/navigation/types'; @@ -132,7 +132,7 @@ const NetworkPill = ({ chainIds }: { chainIds: ChainId[] }) => { - {chainsLabel[availableNetworkChainIds[0]]} + {useBackendNetworksStore.getState().getChainsLabel()[availableNetworkChainIds[0]]} @@ -229,8 +229,8 @@ export function WalletConnectApprovalSheet() { * v2. */ const approvalNetworkInfo = useMemo(() => { - const chain = defaultChains[approvalChainId || ChainId.mainnet]; - const nativeAsset = chainsNativeAsset[chain.id]; + const chain = useBackendNetworksStore.getState().getDefaultChains()[approvalChainId || ChainId.mainnet]; + const nativeAsset = useBackendNetworksStore.getState().getChainsNativeAsset()[chain.id]; return { chainId: chain.id, color: isDarkMode ? nativeAsset.colors.primary : nativeAsset.colors.fallback || nativeAsset.colors.primary, @@ -365,7 +365,9 @@ export function WalletConnectApprovalSheet() { {`${ - type === WalletConnectApprovalSheetType.connect ? approvalNetworkInfo.name : chainsLabel[chainId] + type === WalletConnectApprovalSheetType.connect + ? approvalNetworkInfo.name + : useBackendNetworksStore.getState().getChainsLabel()[chainId] } ${type === WalletConnectApprovalSheetType.connect && menuItems.length > 1 ? '􀁰' : ''}`} @@ -406,7 +408,7 @@ export function WalletConnectApprovalSheet() { {type === WalletConnectApprovalSheetType.connect ? lang.t(lang.l.walletconnect.wants_to_connect) : lang.t(lang.l.walletconnect.wants_to_connect_to_network, { - network: chainsLabel[chainId], + network: useBackendNetworksStore.getState().getChainsLabel()[chainId], })} diff --git a/src/screens/claimables/transaction/claim.ts b/src/screens/claimables/transaction/claim.ts index 1008c27e116..b2ebd3aabf0 100644 --- a/src/screens/claimables/transaction/claim.ts +++ b/src/screens/claimables/transaction/claim.ts @@ -1,4 +1,4 @@ -import { chainsName } from '@/chains'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; import { NewTransaction, TransactionStatus } from '@/entities'; import { TokenColors } from '@/graphql/__generated__/metadata'; import { getProvider } from '@/handlers/web3'; @@ -21,6 +21,7 @@ export async function executeClaim({ wallet: Signer; }) { const provider = getProvider({ chainId: claimTx.chainId }); + const chainsName = useBackendNetworksStore.getState().getChainsName(); const result = await sendTransaction({ transaction: claimTx, existingWallet: wallet, provider }); diff --git a/src/screens/claimables/transaction/components/ClaimCustomization.tsx b/src/screens/claimables/transaction/components/ClaimCustomization.tsx index c26ae2df8d5..6a43623e0d2 100644 --- a/src/screens/claimables/transaction/components/ClaimCustomization.tsx +++ b/src/screens/claimables/transaction/components/ClaimCustomization.tsx @@ -1,8 +1,8 @@ import { Box, Text } from '@/design-system'; import { haptics, showActionSheetWithOptions } from '@/utils'; import React, { useCallback, useMemo, useState } from 'react'; -import { ChainId } from '@/chains/types'; -import { chainsLabel, chainsName, chainsNativeAsset } from '@/chains'; +import { ChainId } from '@/state/backendNetworks/types'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; import { useUserAssetsStore } from '@/state/assets/userAssets'; import { ETH_SYMBOL, USDC_ADDRESS } from '@/references'; import { DropdownMenu } from '../../shared/components/DropdownMenu'; @@ -30,6 +30,9 @@ export function ClaimCustomization() { const [isInitialState, setIsInitialState] = useState(true); + const chainsLabel = useBackendNetworksStore.getState().getChainsLabel(); + const chainsName = useBackendNetworksStore.getState().getChainsName(); + const { data: usdcSearchData } = useTokenSearch( { keys: ['address'], @@ -50,7 +53,7 @@ export function ClaimCustomization() { const nativeTokens: TokenMap = useMemo( () => balanceSortedChainList.reduce((nativeTokenDict, chainId) => { - const nativeToken = chainsNativeAsset[chainId]; + const nativeToken = useBackendNetworksStore.getState().getChainsNativeAsset()[chainId]; if (nativeToken) { if (!nativeTokenDict[nativeToken.symbol]) { nativeTokenDict[nativeToken.symbol] = { @@ -210,7 +213,7 @@ export function ClaimCustomization() { return { menuItems, }; - }, [balanceSortedChainList, isInitialState, outputChainId, outputToken]); + }, [balanceSortedChainList, chainsLabel, chainsName, isInitialState, outputChainId, outputToken]); const handleTokenSelection = useCallback( ({ nativeEvent: { actionKey } }: Omit) => { diff --git a/src/screens/claimables/transaction/components/GasDetails.tsx b/src/screens/claimables/transaction/components/GasDetails.tsx index dfc61b50480..e8fdc99ba98 100644 --- a/src/screens/claimables/transaction/components/GasDetails.tsx +++ b/src/screens/claimables/transaction/components/GasDetails.tsx @@ -2,7 +2,7 @@ import { Box, Inline, Text } from '@/design-system'; import React, { useEffect } from 'react'; import * as i18n from '@/languages'; import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; -import { chainsLabel } from '@/chains'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; import { useTransactionClaimableContext } from '../context/TransactionClaimableContext'; export function GasDetails() { @@ -52,7 +52,7 @@ export function GasDetails() { {i18n.t(i18n.l.claimables.panel.amount_to_claim_on_network, { amount: gasFeeDisplay, - network: chainsLabel[chainId], + network: useBackendNetworksStore.getState().getChainsLabel()[chainId], })} diff --git a/src/screens/claimables/transaction/context/TransactionClaimableContext.tsx b/src/screens/claimables/transaction/context/TransactionClaimableContext.tsx index 4503bce6f22..63c1ce771be 100644 --- a/src/screens/claimables/transaction/context/TransactionClaimableContext.tsx +++ b/src/screens/claimables/transaction/context/TransactionClaimableContext.tsx @@ -1,5 +1,5 @@ import React, { Dispatch, SetStateAction, createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { TokenToReceive } from '../types'; import { CrosschainQuote, ETH_ADDRESS, getCrosschainQuote, getQuote, Quote, QuoteParams } from '@rainbow-me/swaps'; import { Claimable, TransactionClaimable } from '@/resources/addys/claimables/types'; @@ -39,7 +39,7 @@ import { analyticsV2 } from '@/analytics'; import { getDefaultSlippageWorklet } from '@/__swaps__/utils/swaps'; import { getRemoteConfig } from '@/model/remoteConfig'; import { estimateClaimUnlockSwapGasLimit } from '../estimateGas'; -import { chainsNativeAsset } from '@/chains'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; import showWalletErrorAlert from '@/helpers/support'; enum ErrorMessages { @@ -251,7 +251,7 @@ export function TransactionClaimableContextProvider({ const gasFeeWei = calculateGasFeeWorklet(gasSettings, gasLimit); - const nativeAsset = chainsNativeAsset[claimable.chainId]; + const nativeAsset = useBackendNetworksStore.getState().getChainsNativeAsset()[claimable.chainId]; const gasFeeNativeToken = formatUnits(safeBigInt(gasFeeWei), nativeAsset.decimals); const userBalance = userNativeNetworkAsset?.balance?.amount || '0'; diff --git a/src/screens/claimables/transaction/estimateGas.ts b/src/screens/claimables/transaction/estimateGas.ts index 4933b2d6f15..08daef9b1a0 100644 --- a/src/screens/claimables/transaction/estimateGas.ts +++ b/src/screens/claimables/transaction/estimateGas.ts @@ -2,7 +2,7 @@ import { CrosschainQuote, Quote } from '@rainbow-me/swaps'; import { getProvider } from '@/handlers/web3'; import { Address } from 'viem'; import { metadataPOSTClient } from '@/graphql'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { add, convertAmountToRawAmount, greaterThan } from '@/helpers/utilities'; import { populateSwap } from '@/raps/utils'; import { estimateApprove, getAssetRawAllowance, populateApprove } from '@/raps/actions/unlock'; diff --git a/src/screens/claimables/transaction/types.ts b/src/screens/claimables/transaction/types.ts index 343a170b7aa..2bcc62fe3d5 100644 --- a/src/screens/claimables/transaction/types.ts +++ b/src/screens/claimables/transaction/types.ts @@ -1,4 +1,4 @@ -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { TransactionRequest } from '@ethersproject/providers'; // supports legacy and new gas types diff --git a/src/screens/mints/MintSheet.tsx b/src/screens/mints/MintSheet.tsx index 5060821490e..d78217c93c8 100644 --- a/src/screens/mints/MintSheet.tsx +++ b/src/screens/mints/MintSheet.tsx @@ -52,11 +52,11 @@ import { IS_ANDROID, IS_IOS } from '@/env'; import { EthCoinIcon } from '@/components/coin-icon/EthCoinIcon'; import { addNewTransaction } from '@/state/pendingTransactions'; import { getUniqueId } from '@/utils/ethereumUtils'; -import { chainsName, defaultChains, getChainDefaultRpc } from '@/chains'; import { getNextNonce } from '@/state/nonces'; import { metadataPOSTClient } from '@/graphql'; import { Transaction } from '@/graphql/__generated__/metadataPOST'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; const NFT_IMAGE_HEIGHT = 250; // inset * 2 -> 28 *2 @@ -252,10 +252,11 @@ const MintSheet = () => { // estimate gas limit useEffect(() => { const estimateMintGas = async () => { + const defaultChains = useBackendNetworksStore.getState().getDefaultChains(); const signer = createWalletClient({ account: accountAddress, chain: defaultChains[chainId], - transport: http(getChainDefaultRpc(chainId)), + transport: http(useBackendNetworksStore.getState().getChainDefaultRpc(chainId)), }); try { await getClient()?.actions.mintToken({ @@ -360,11 +361,11 @@ const MintSheet = () => { const privateKey = await loadPrivateKey(accountAddress, false); // @ts-ignore const account = privateKeyToAccount(privateKey); - const chain = defaultChains[chainId]; + const chain = useBackendNetworksStore.getState().getDefaultChains()[chainId]; const signer = createWalletClient({ account, chain, - transport: http(getChainDefaultRpc(chainId)), + transport: http(useBackendNetworksStore.getState().getChainDefaultRpc(chainId)), }); const feeAddress = getRainbowFeeAddress(chainId); @@ -381,6 +382,7 @@ const MintSheet = () => { wallet: signer!, chainId, onProgress: (steps: Execute['steps']) => { + const chainsName = useBackendNetworksStore.getState().getChainsName(); steps.forEach(step => { if (step.error) { logger.error(new RainbowError(`[MintSheet]: Error minting NFT: ${step.error}`)); @@ -708,7 +710,7 @@ const MintSheet = () => { )} - {`${defaultChains[chainId].name}`} + {`${useBackendNetworksStore.getState().getDefaultChains()[chainId].name}`} diff --git a/src/screens/points/claim-flow/ClaimRewardsPanel.tsx b/src/screens/points/claim-flow/ClaimRewardsPanel.tsx index e20ad7acd22..de63895bd34 100644 --- a/src/screens/points/claim-flow/ClaimRewardsPanel.tsx +++ b/src/screens/points/claim-flow/ClaimRewardsPanel.tsx @@ -5,7 +5,7 @@ import { Bleed, Box, Text, TextShadow, globalColors, useBackgroundColor, useColo import * as i18n from '@/languages'; import { ListHeader, ListPanel, Panel, TapToDismiss, controlPanelStyles } from '@/components/SmoothPager/ListPanel'; import { ChainImage } from '@/components/coin-icon/ChainImage'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import ethereumUtils, { useNativeAsset } from '@/utils/ethereumUtils'; import { useAccountAccentColor, useAccountProfile, useAccountSettings, useWallets } from '@/hooks'; import { safeAreaInsetValues, watchingAlert } from '@/utils'; @@ -34,7 +34,7 @@ import { useMeteorologySuggestions } from '@/__swaps__/utils/meteorology'; import { AnimatedSpinner } from '@/components/animations/AnimatedSpinner'; import { RainbowError, logger } from '@/logger'; import { RewardsActionButton } from '../components/RewardsActionButton'; -import { chainsLabel, chainsName } from '@/chains'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; type ClaimStatus = 'idle' | 'claiming' | 'success' | PointsErrorType | 'error' | 'bridge-error'; type ClaimNetwork = '10' | '8453' | '7777777'; @@ -93,7 +93,7 @@ export const ClaimRewardsPanel = () => { const NETWORK_LIST_ITEMS = CLAIM_NETWORKS.map(chainId => { return { IconComponent: , - label: chainsLabel[chainId], + label: useBackendNetworksStore.getState().getChainsLabel()[chainId], uniqueId: chainId.toString(), selected: false, }; @@ -210,6 +210,7 @@ const ClaimingRewards = ({ nonce: number | null; }>({ mutationFn: async () => { + const chainsName = useBackendNetworksStore.getState().getChainsName(); // Fetch the native asset from the origin chain const opEth_ = await ethereumUtils.getNativeAssetForNetwork({ chainId: ChainId.optimism }); const opEth = { @@ -338,6 +339,7 @@ const ClaimingRewards = ({ }, [claimStatus]); const panelTitle = useMemo(() => { + const chainsLabel = useBackendNetworksStore.getState().getChainsLabel(); switch (claimStatus) { case 'idle': return i18n.t(i18n.l.points.points.claim_on_network, { @@ -495,7 +497,7 @@ const ClaimingRewards = ({ {i18n.t(i18n.l.points.points.bridge_error_explainer, { - network: chainId ? chainsLabel[chainId] : '', + network: chainId ? useBackendNetworksStore.getState().getChainsLabel()[chainId] : '', })} diff --git a/src/screens/points/components/LeaderboardRow.tsx b/src/screens/points/components/LeaderboardRow.tsx index 8d2d451ba3e..fff29344f13 100644 --- a/src/screens/points/components/LeaderboardRow.tsx +++ b/src/screens/points/components/LeaderboardRow.tsx @@ -18,7 +18,7 @@ import { useTheme } from '@/theme'; import LinearGradient from 'react-native-linear-gradient'; import { ButtonPressAnimation } from '@/components/animations'; import { noop } from 'lodash'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; const ACTIONS = { ADD_CONTACT: 'add-contact', diff --git a/src/screens/points/content/PointsContent.tsx b/src/screens/points/content/PointsContent.tsx index 5bb4eed4e05..295336fc9ff 100644 --- a/src/screens/points/content/PointsContent.tsx +++ b/src/screens/points/content/PointsContent.tsx @@ -62,7 +62,7 @@ import { format, intervalToDuration, isToday } from 'date-fns'; import { useRemoteConfig } from '@/model/remoteConfig'; import { ETH_REWARDS, useExperimentalFlag } from '@/config'; import { RewardsActionButton } from '../components/RewardsActionButton'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; const InfoCards = ({ points }: { points: GetPointsDataForWalletQuery | undefined }) => { const labelSecondary = useForegroundColor('labelSecondary'); diff --git a/src/screens/points/contexts/PointsProfileContext.tsx b/src/screens/points/contexts/PointsProfileContext.tsx index d54395db617..543062caec5 100644 --- a/src/screens/points/contexts/PointsProfileContext.tsx +++ b/src/screens/points/contexts/PointsProfileContext.tsx @@ -16,7 +16,7 @@ import { useNavigation } from '@/navigation'; import { getProvider } from '@/handlers/web3'; import { analyticsV2 } from '@/analytics'; import { delay } from '@/utils/delay'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; type PointsProfileContext = { step: RainbowPointsFlowSteps; diff --git a/src/screens/positions/SubPositionListItem.tsx b/src/screens/positions/SubPositionListItem.tsx index 9511a1fd4f5..b7835e89df5 100644 --- a/src/screens/positions/SubPositionListItem.tsx +++ b/src/screens/positions/SubPositionListItem.tsx @@ -10,7 +10,7 @@ import { NativeDisplay, PositionAsset } from '@/resources/defi/types'; import { useExternalToken } from '@/resources/assets/externalAssetsQuery'; import { useAccountSettings } from '@/hooks'; import RainbowCoinIcon from '@/components/coin-icon/RainbowCoinIcon'; -import { chainsIdByName } from '@/chains'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; type Props = { asset: PositionAsset; @@ -24,7 +24,7 @@ type Props = { export const SubPositionListItem: React.FC = ({ asset, apy, quantity, native, positionColor, dappVersion }) => { const theme = useTheme(); const { nativeCurrency } = useAccountSettings(); - const chainId = chainsIdByName[asset.network]; + const chainId = useBackendNetworksStore.getState().getChainsIdByName()[asset.network]; const { data: externalAsset } = useExternalToken({ address: asset.asset_code, chainId, currency: nativeCurrency }); const separatorSecondary = useForegroundColor('separatorSecondary'); diff --git a/src/screens/transaction-details/components/TransactionDetailsValueAndFeeSection.tsx b/src/screens/transaction-details/components/TransactionDetailsValueAndFeeSection.tsx index c6993a28b6d..dfde595df20 100644 --- a/src/screens/transaction-details/components/TransactionDetailsValueAndFeeSection.tsx +++ b/src/screens/transaction-details/components/TransactionDetailsValueAndFeeSection.tsx @@ -15,7 +15,7 @@ import ImgixImage from '@/components/images/ImgixImage'; import { View } from 'react-native'; import ChainBadge from '@/components/coin-icon/ChainBadge'; import { checkForPendingSwap } from '@/helpers/checkForPendingSwap'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; type Props = { transaction: RainbowTransaction; diff --git a/src/screens/transaction-details/components/TransactionMasthead.tsx b/src/screens/transaction-details/components/TransactionMasthead.tsx index f431fd0e3ea..0de04d2616a 100644 --- a/src/screens/transaction-details/components/TransactionMasthead.tsx +++ b/src/screens/transaction-details/components/TransactionMasthead.tsx @@ -34,7 +34,7 @@ import ImageAvatar from '@/components/contacts/ImageAvatar'; import RainbowCoinIcon from '@/components/coin-icon/RainbowCoinIcon'; import * as lang from '@/languages'; import { checkForPendingSwap } from '@/helpers/checkForPendingSwap'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; const TransactionMastheadHeight = android ? 153 : 135; diff --git a/src/state/appSessions/index.test.ts b/src/state/appSessions/index.test.ts index 71ca5d15697..2cfd0c8399a 100644 --- a/src/state/appSessions/index.test.ts +++ b/src/state/appSessions/index.test.ts @@ -1,4 +1,4 @@ -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { useAppSessionsStore } from '.'; const UNISWAP_HOST = 'uniswap.org'; diff --git a/src/state/appSessions/index.ts b/src/state/appSessions/index.ts index 02128c94c47..ee5400779ca 100644 --- a/src/state/appSessions/index.ts +++ b/src/state/appSessions/index.ts @@ -1,6 +1,6 @@ import { Address } from 'viem'; -import { Network, ChainId } from '@/chains/types'; +import { Network, ChainId } from '@/state/backendNetworks/types'; import { createRainbowStore } from '../internal/createRainbowStore'; const chainsIdByNetwork: Record = { diff --git a/src/state/assets/userAssets.ts b/src/state/assets/userAssets.ts index 35548509b29..2109601e25d 100644 --- a/src/state/assets/userAssets.ts +++ b/src/state/assets/userAssets.ts @@ -6,8 +6,8 @@ import { supportedNativeCurrencies } from '@/references'; import { createRainbowStore } from '@/state/internal/createRainbowStore'; import { useStore } from 'zustand'; import { swapsStore } from '@/state/swaps/swapsStore'; -import { ChainId } from '@/chains/types'; -import { SUPPORTED_CHAIN_IDS } from '@/chains'; +import { ChainId } from '@/state/backendNetworks/types'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; import { useSelector } from 'react-redux'; const SEARCH_CACHE_MAX_ENTRIES = 50; @@ -19,7 +19,7 @@ const getDefaultCacheKeys = (): Set => { const queryKeysToPreserve = new Set(); queryKeysToPreserve.add('all'); - for (const chainId of SUPPORTED_CHAIN_IDS) { + for (const chainId of useBackendNetworksStore.getState().getSupportedChainIds()) { queryKeysToPreserve.add(`${chainId}`); } return queryKeysToPreserve; @@ -317,12 +317,15 @@ export const createUserAssetsStore = (address: Address | string) => }); // Ensure all supported chains are in the map with a fallback value of 0 - SUPPORTED_CHAIN_IDS.forEach(chainId => { - if (!unsortedChainBalances.has(chainId)) { - unsortedChainBalances.set(chainId, 0); - idsByChain.set(chainId, []); - } - }); + useBackendNetworksStore + .getState() + .getSupportedChainIds() + .forEach(chainId => { + if (!unsortedChainBalances.has(chainId)) { + unsortedChainBalances.set(chainId, 0); + idsByChain.set(chainId, []); + } + }); // Sort the existing map by balance in descending order const sortedEntries = Array.from(unsortedChainBalances.entries()).sort(([, balanceA], [, balanceB]) => balanceB - balanceA); diff --git a/src/state/backendNetworks/backendNetworks.ts b/src/state/backendNetworks/backendNetworks.ts new file mode 100644 index 00000000000..17da5798f27 --- /dev/null +++ b/src/state/backendNetworks/backendNetworks.ts @@ -0,0 +1,636 @@ +import { makeMutable, SharedValue } from 'react-native-reanimated'; +import { queryClient } from '@/react-query'; +import buildTimeNetworks from '@/references/networks.json'; +import { backendNetworksQueryKey, BackendNetworksResponse } from '@/resources/metadata/backendNetworks'; +import { createRainbowStore } from '@/state/internal/createRainbowStore'; +import { Chain } from 'viem/chains'; +import { transformBackendNetworksToChains } from '@/state/backendNetworks/utils'; +import { IS_TEST } from '@/env'; +import { BackendNetwork, BackendNetworkServices, chainHardhat, chainHardhatOptimism, ChainId } from '@/state/backendNetworks/types'; +import { GasSpeed } from '@/__swaps__/types/gas'; +import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; + +const INITIAL_BACKEND_NETWORKS = queryClient.getQueryData(backendNetworksQueryKey()) ?? buildTimeNetworks; +const DEFAULT_PRIVATE_MEMPOOL_TIMEOUT = 2 * 60 * 1_000; // 2 minutes + +export interface BackendNetworksState { + backendNetworks: BackendNetworksResponse; + backendNetworksSharedValue: SharedValue; + + getBackendChains: () => Chain[]; + getSupportedChains: () => Chain[]; + + getDefaultChains: () => Record; + getSupportedChainIds: () => ChainId[]; + getSupportedMainnetChains: () => Chain[]; + getSupportedMainnetChainIds: () => ChainId[]; + getNeedsL1SecurityFeeChains: () => ChainId[]; + getChainsNativeAsset: () => Record; + getChainsLabel: () => Record; + getChainsPrivateMempoolTimeout: () => Record; + getChainsName: () => Record; + getChainsIdByName: () => Record; + + defaultGasSpeeds: (chainId: ChainId) => GasSpeed[]; + + getChainsGasSpeeds: () => Record; + defaultPollingInterval: (chainId: ChainId) => number; + getChainsPollingInterval: () => Record; + + defaultSimplehashNetwork: (chainId: ChainId) => string; + getChainsSimplehashNetwork: () => Record; + filterChainIdsByService: (servicePath: (services: BackendNetworkServices) => boolean) => ChainId[]; + + getMeteorologySupportedChainIds: () => ChainId[]; + getSwapSupportedChainIds: () => ChainId[]; + getSwapExactOutputSupportedChainIds: () => ChainId[]; + getBridgeExactOutputSupportedChainIds: () => ChainId[]; + getNotificationsSupportedChainIds: () => ChainId[]; + getApprovalsSupportedChainIds: () => ChainId[]; + getTransactionsSupportedChainIds: () => ChainId[]; + getSupportedAssetsChainIds: () => ChainId[]; + getSupportedPositionsChainIds: () => ChainId[]; + getTokenSearchSupportedChainIds: () => ChainId[]; + getNftSupportedChainIds: () => ChainId[]; + getFlashbotsSupportedChainIds: () => ChainId[]; + getShouldDefaultToFastGasChainIds: () => ChainId[]; + + getChainGasUnits: (chainId?: ChainId) => BackendNetwork['gasUnits']; + getChainDefaultRpc: (chainId: ChainId) => string; + + setBackendNetworks: (backendNetworks: BackendNetworksResponse) => void; +} + +export const useBackendNetworksStore = createRainbowStore((set, get) => ({ + backendNetworks: INITIAL_BACKEND_NETWORKS, + backendNetworksSharedValue: makeMutable(INITIAL_BACKEND_NETWORKS), + + getBackendChains: () => { + const { backendNetworks } = get(); + return transformBackendNetworksToChains(backendNetworks.networks); + }, + + getSupportedChains: () => { + const backendChains = get().getBackendChains(); + return IS_TEST ? [...backendChains, chainHardhat, chainHardhatOptimism] : backendChains; + }, + + getDefaultChains: () => { + const supportedChains = get().getSupportedChains(); + return supportedChains.reduce( + (acc, chain) => { + acc[chain.id] = chain; + return acc; + }, + {} as Record + ); + }, + + getSupportedChainIds: () => { + const supportedChains = get().getSupportedChains(); + return supportedChains.map(chain => chain.id); + }, + + getSupportedMainnetChains: () => { + const supportedChains = get().getSupportedChains(); + return supportedChains.filter(chain => !chain.testnet); + }, + + getSupportedMainnetChainIds: () => { + const supportedMainnetChains = get().getSupportedMainnetChains(); + return supportedMainnetChains.map(chain => chain.id); + }, + + getNeedsL1SecurityFeeChains: () => { + const backendNetworks = get().backendNetworks; + return backendNetworks.networks + .filter((backendNetwork: BackendNetwork) => backendNetwork.opStack) + .map((backendNetwork: BackendNetwork) => parseInt(backendNetwork.id, 10)); + }, + + getChainsNativeAsset: () => { + const backendNetworks = get().backendNetworks; + return backendNetworks.networks.reduce( + (acc, backendNetwork) => { + acc[parseInt(backendNetwork.id, 10)] = backendNetwork.nativeAsset; + return acc; + }, + {} as Record + ); + }, + + getChainsLabel: () => { + const backendNetworks = get().backendNetworks; + return backendNetworks.networks.reduce( + (acc, backendNetwork) => { + acc[parseInt(backendNetwork.id, 10)] = backendNetwork.label; + return acc; + }, + {} as Record + ); + }, + + getChainsPrivateMempoolTimeout: () => { + const backendNetworks = get().backendNetworks; + return backendNetworks.networks.reduce( + (acc, backendNetwork) => { + acc[parseInt(backendNetwork.id, 10)] = backendNetwork.privateMempoolTimeout || DEFAULT_PRIVATE_MEMPOOL_TIMEOUT; + return acc; + }, + {} as Record + ); + }, + + getChainsName: () => { + const backendNetworks = get().backendNetworks; + return backendNetworks.networks.reduce( + (acc, backendNetwork) => { + acc[parseInt(backendNetwork.id, 10)] = backendNetwork.name; + return acc; + }, + {} as Record + ); + }, + + getChainsIdByName: () => { + const backendNetworks = get().backendNetworks; + return backendNetworks.networks.reduce( + (acc, backendNetwork) => { + acc[backendNetwork.name] = parseInt(backendNetwork.id, 10); + return acc; + }, + {} as Record + ); + }, + + // TODO: This should come from the backend at some point + defaultGasSpeeds: chainId => { + switch (chainId) { + case ChainId.bsc: + case ChainId.goerli: + case ChainId.polygon: + return [GasSpeed.NORMAL, GasSpeed.FAST, GasSpeed.URGENT]; + case ChainId.gnosis: + return [GasSpeed.NORMAL]; + default: + return [GasSpeed.NORMAL, GasSpeed.FAST, GasSpeed.URGENT, GasSpeed.CUSTOM]; + } + }, + + getChainsGasSpeeds: () => { + const backendNetworks = get().backendNetworks; + return backendNetworks.networks.reduce( + (acc, backendNetwork) => { + acc[parseInt(backendNetwork.id, 10)] = get().defaultGasSpeeds(parseInt(backendNetwork.id, 10)); + return acc; + }, + {} as Record + ); + }, + + defaultPollingInterval: chainId => { + switch (chainId) { + case ChainId.polygon: + return 2_000; + case ChainId.arbitrum: + case ChainId.bsc: + return 3_000; + default: + return 5_000; + } + }, + + getChainsPollingInterval: () => { + const backendNetworks = get().backendNetworks; + return backendNetworks.networks.reduce( + (acc, backendNetwork) => { + acc[parseInt(backendNetwork.id, 10)] = get().defaultPollingInterval(parseInt(backendNetwork.id, 10)); + return acc; + }, + {} as Record + ); + }, + + // TODO: This should come from the backend at some point + defaultSimplehashNetwork: chainId => { + switch (chainId) { + case ChainId.apechain: + return 'apechain'; + case ChainId.arbitrum: + return 'arbitrum'; + case ChainId.avalanche: + return 'avalanche'; + case ChainId.base: + return 'base'; + case ChainId.blast: + return 'blast'; + case ChainId.bsc: + return 'bsc'; + case ChainId.degen: + return 'degen'; + case ChainId.gnosis: + return 'gnosis'; + case ChainId.goerli: + return 'ethereum-goerli'; + case ChainId.mainnet: + return 'ethereum'; + case ChainId.optimism: + return 'optimism'; + case ChainId.polygon: + return 'polygon'; + case ChainId.zora: + return 'zora'; + default: + return ''; + } + }, + + getChainsSimplehashNetwork: () => { + const backendNetworks = get().backendNetworks; + return backendNetworks.networks.reduce( + (acc, backendNetwork) => { + acc[parseInt(backendNetwork.id, 10)] = get().defaultSimplehashNetwork(parseInt(backendNetwork.id, 10)); + return acc; + }, + {} as Record + ); + }, + + filterChainIdsByService: servicePath => { + const backendNetworks = get().backendNetworks; + return backendNetworks.networks.filter(network => servicePath(network.enabledServices)).map(network => parseInt(network.id, 10)); + }, + + getMeteorologySupportedChainIds: () => { + return get().filterChainIdsByService(services => services.meteorology.enabled); + }, + + getSwapSupportedChainIds: () => { + return get().filterChainIdsByService(services => services.swap.enabled); + }, + + getSwapExactOutputSupportedChainIds: () => { + return get().filterChainIdsByService(services => services.swap.swapExactOutput); + }, + + getBridgeExactOutputSupportedChainIds: () => { + return get().filterChainIdsByService(services => services.swap.bridgeExactOutput); + }, + + getNotificationsSupportedChainIds: () => { + return get().filterChainIdsByService(services => services.notifications.enabled); + }, + + getApprovalsSupportedChainIds: () => { + return get().filterChainIdsByService(services => services.addys.approvals); + }, + + getTransactionsSupportedChainIds: () => { + return get().filterChainIdsByService(services => services.addys.transactions); + }, + + getSupportedAssetsChainIds: () => { + return get().filterChainIdsByService(services => services.addys.assets); + }, + + getSupportedPositionsChainIds: () => { + return get().filterChainIdsByService(services => services.addys.positions); + }, + + getTokenSearchSupportedChainIds: () => { + return get().filterChainIdsByService(services => services.tokenSearch.enabled); + }, + + getNftSupportedChainIds: () => { + return get().filterChainIdsByService(services => services.nftProxy.enabled); + }, + + getFlashbotsSupportedChainIds: () => { + return [ChainId.mainnet]; + }, + + getShouldDefaultToFastGasChainIds: () => { + return [ChainId.mainnet, ChainId.polygon, ChainId.goerli]; + }, + + getChainGasUnits: chainId => { + const backendNetworks = get().backendNetworks; + const chainsGasUnits = backendNetworks.networks.reduce( + (acc, backendNetwork: BackendNetwork) => { + acc[parseInt(backendNetwork.id, 10)] = backendNetwork.gasUnits; + return acc; + }, + {} as Record + ); + + return (chainId ? chainsGasUnits[chainId] : undefined) || chainsGasUnits[ChainId.mainnet]; + }, + + getChainDefaultRpc: chainId => { + const defaultChains = get().getDefaultChains(); + switch (chainId) { + case ChainId.mainnet: + return useConnectedToHardhatStore.getState().connectedToHardhat + ? chainHardhat.rpcUrls.default.http[0] + : defaultChains[ChainId.mainnet].rpcUrls.default.http[0]; + default: + return defaultChains[chainId].rpcUrls.default.http[0]; + } + }, + + setBackendNetworks: backendNetworks => + set(state => { + state.backendNetworksSharedValue.value = backendNetworks; + return { + ...state, + backendNetworks: backendNetworks, + }; + }), +})); + +// ------ WORKLET FUNCTIONS ------ + +export const getBackendChainsWorklet = (backendNetworks: SharedValue) => { + 'worklet'; + return transformBackendNetworksToChains(backendNetworks.value.networks); +}; + +export const getSupportedChainsWorklet = (backendNetworks: SharedValue) => { + 'worklet'; + const backendChains = getBackendChainsWorklet(backendNetworks); + return IS_TEST ? [...backendChains, chainHardhat, chainHardhatOptimism] : backendChains; +}; + +export const getDefaultChainsWorklet = (backendNetworks: SharedValue) => { + 'worklet'; + const supportedChains = getSupportedChainsWorklet(backendNetworks); + return supportedChains.reduce( + (acc, chain) => { + acc[chain.id] = chain; + return acc; + }, + {} as Record + ); +}; + +export const getSupportedChainIdsWorklet = (backendNetworks: SharedValue) => { + 'worklet'; + const supportedChains = getSupportedChainsWorklet(backendNetworks); + return supportedChains.map(chain => chain.id); +}; + +export const getSupportedMainnetChainsWorklet = (backendNetworks: SharedValue) => { + 'worklet'; + const supportedChains = getSupportedChainsWorklet(backendNetworks); + return supportedChains.filter(chain => !chain.testnet); +}; + +export const getSupportedMainnetChainIdsWorklet = (backendNetworks: SharedValue) => { + 'worklet'; + const supportedMainnetChains = getSupportedMainnetChainsWorklet(backendNetworks); + return supportedMainnetChains.map(chain => chain.id); +}; + +export const getNeedsL1SecurityFeeChainsWorklet = (backendNetworks: SharedValue) => { + 'worklet'; + return backendNetworks.value.networks + .filter((backendNetwork: BackendNetwork) => backendNetwork.opStack) + .map((backendNetwork: BackendNetwork) => parseInt(backendNetwork.id, 10)); +}; + +export const getChainsNativeAssetWorklet = (backendNetworks: SharedValue) => { + 'worklet'; + return backendNetworks.value.networks.reduce( + (acc, backendNetwork) => { + acc[parseInt(backendNetwork.id, 10)] = backendNetwork.nativeAsset; + return acc; + }, + {} as Record + ); +}; + +export const getChainsLabelWorklet = (backendNetworks: SharedValue) => { + 'worklet'; + return backendNetworks.value.networks.reduce( + (acc, backendNetwork: BackendNetwork) => { + acc[parseInt(backendNetwork.id, 10)] = backendNetwork.label; + return acc; + }, + {} as Record + ); +}; + +export const getChainsNameWorklet = (backendNetworks: SharedValue) => { + 'worklet'; + return backendNetworks.value.networks.reduce( + (acc, backendNetwork: BackendNetwork) => { + acc[parseInt(backendNetwork.id, 10)] = backendNetwork.name; + return acc; + }, + {} as Record + ); +}; + +export const getChainsIdByNameWorklet = (backendNetworks: SharedValue) => { + 'worklet'; + return backendNetworks.value.networks.reduce( + (acc, backendNetwork) => { + acc[backendNetwork.name] = parseInt(backendNetwork.id, 10); + return acc; + }, + {} as Record + ); +}; + +export const defaultGasSpeedsWorklet = (chainId: ChainId) => { + 'worklet'; + switch (chainId) { + case ChainId.bsc: + case ChainId.goerli: + case ChainId.polygon: + return [GasSpeed.NORMAL, GasSpeed.FAST, GasSpeed.URGENT]; + case ChainId.gnosis: + return [GasSpeed.NORMAL]; + default: + return [GasSpeed.NORMAL, GasSpeed.FAST, GasSpeed.URGENT, GasSpeed.CUSTOM]; + } +}; + +export const getChainsGasSpeedsWorklet = (backendNetworks: SharedValue) => { + 'worklet'; + return backendNetworks.value.networks.reduce( + (acc, backendNetwork) => { + acc[parseInt(backendNetwork.id, 10)] = defaultGasSpeedsWorklet(parseInt(backendNetwork.id, 10)); + return acc; + }, + {} as Record + ); +}; + +export const defaultPollingIntervalWorklet = (chainId: ChainId) => { + 'worklet'; + switch (chainId) { + case ChainId.polygon: + return 2_000; + case ChainId.arbitrum: + case ChainId.bsc: + return 3_000; + default: + return 5_000; + } +}; + +export const getChainsPollingIntervalWorklet = (backendNetworks: SharedValue) => { + 'worklet'; + return backendNetworks.value.networks.reduce( + (acc, backendNetwork) => { + acc[parseInt(backendNetwork.id, 10)] = defaultPollingIntervalWorklet(parseInt(backendNetwork.id, 10)); + return acc; + }, + {} as Record + ); +}; + +export const defaultSimplehashNetworkWorklet = (chainId: ChainId) => { + 'worklet'; + switch (chainId) { + case ChainId.apechain: + return 'apechain'; + case ChainId.arbitrum: + return 'arbitrum'; + case ChainId.avalanche: + return 'avalanche'; + case ChainId.base: + return 'base'; + case ChainId.blast: + return 'blast'; + case ChainId.bsc: + return 'bsc'; + case ChainId.degen: + return 'degen'; + case ChainId.gnosis: + return 'gnosis'; + case ChainId.goerli: + return 'ethereum-goerli'; + case ChainId.mainnet: + return 'ethereum'; + case ChainId.optimism: + return 'optimism'; + case ChainId.polygon: + return 'polygon'; + case ChainId.zora: + return 'zora'; + default: + return ''; + } +}; + +export const getChainsSimplehashNetworkWorklet = (backendNetworks: SharedValue) => { + 'worklet'; + return backendNetworks.value.networks.reduce( + (acc, backendNetwork) => { + acc[parseInt(backendNetwork.id, 10)] = defaultSimplehashNetworkWorklet(parseInt(backendNetwork.id, 10)); + return acc; + }, + {} as Record + ); +}; + +export const filterChainIdsByServiceWorklet = ( + backendNetworks: SharedValue, + servicePath: (services: BackendNetworkServices) => boolean +) => { + 'worklet'; + return backendNetworks.value.networks.filter(network => servicePath(network.enabledServices)).map(network => parseInt(network.id, 10)); +}; + +export const getMeteorologySupportedChainIdsWorklet = (backendNetworks: SharedValue) => { + 'worklet'; + return filterChainIdsByServiceWorklet(backendNetworks, services => services.meteorology.enabled); +}; + +export const getSwapSupportedChainIdsWorklet = (backendNetworks: SharedValue) => { + 'worklet'; + return filterChainIdsByServiceWorklet(backendNetworks, services => services.swap.enabled); +}; + +export const getSwapExactOutputSupportedChainIdsWorklet = (backendNetworks: SharedValue) => { + 'worklet'; + return filterChainIdsByServiceWorklet(backendNetworks, services => services.swap.swapExactOutput); +}; + +export const getBridgeExactOutputSupportedChainIdsWorklet = (backendNetworks: SharedValue) => { + 'worklet'; + return filterChainIdsByServiceWorklet(backendNetworks, services => services.swap.bridgeExactOutput); +}; + +export const getNotificationsSupportedChainIdsWorklet = (backendNetworks: SharedValue) => { + 'worklet'; + return filterChainIdsByServiceWorklet(backendNetworks, services => services.notifications.enabled); +}; + +export const getApprovalsSupportedChainIdsWorklet = (backendNetworks: SharedValue) => { + 'worklet'; + return filterChainIdsByServiceWorklet(backendNetworks, services => services.addys.approvals); +}; + +export const getTransactionsSupportedChainIdsWorklet = (backendNetworks: SharedValue) => { + 'worklet'; + return filterChainIdsByServiceWorklet(backendNetworks, services => services.addys.transactions); +}; + +export const getSupportedAssetsChainIdsWorklet = (backendNetworks: SharedValue) => { + 'worklet'; + return filterChainIdsByServiceWorklet(backendNetworks, services => services.addys.assets); +}; + +export const getSupportedPositionsChainIdsWorklet = (backendNetworks: SharedValue) => { + 'worklet'; + return filterChainIdsByServiceWorklet(backendNetworks, services => services.addys.positions); +}; + +export const getTokenSearchSupportedChainIdsWorklet = (backendNetworks: SharedValue) => { + 'worklet'; + return filterChainIdsByServiceWorklet(backendNetworks, services => services.tokenSearch.enabled); +}; + +export const getNftSupportedChainIdsWorklet = (backendNetworks: SharedValue) => { + 'worklet'; + return filterChainIdsByServiceWorklet(backendNetworks, services => services.nftProxy.enabled); +}; + +export const getFlashbotsSupportedChainIdsWorklet = (_?: SharedValue) => { + 'worklet'; + return [ChainId.mainnet]; +}; + +export const getShouldDefaultToFastGasChainIdsWorklet = (_?: SharedValue) => { + 'worklet'; + return [ChainId.mainnet, ChainId.polygon, ChainId.goerli]; +}; + +export const getChainGasUnitsWorklet = (backendNetworks: SharedValue, chainId?: ChainId) => { + 'worklet'; + const chainsGasUnits = backendNetworks.value.networks.reduce( + (acc, backendNetwork: BackendNetwork) => { + acc[parseInt(backendNetwork.id, 10)] = backendNetwork.gasUnits; + return acc; + }, + {} as Record + ); + + return (chainId ? chainsGasUnits[chainId] : undefined) || chainsGasUnits[ChainId.mainnet]; +}; + +export const getChainDefaultRpcWorklet = (backendNetworks: SharedValue, chainId: ChainId) => { + 'worklet'; + const defaultChains = getDefaultChainsWorklet(backendNetworks); + switch (chainId) { + case ChainId.mainnet: + return useConnectedToHardhatStore.getState().connectedToHardhat + ? 'http://127.0.0.1:8545' + : defaultChains[ChainId.mainnet].rpcUrls.default.http[0]; + default: + return defaultChains[chainId].rpcUrls.default.http[0]; + } +}; diff --git a/src/chains/types.ts b/src/state/backendNetworks/types.ts similarity index 100% rename from src/chains/types.ts rename to src/state/backendNetworks/types.ts diff --git a/src/chains/utils/backendNetworks.ts b/src/state/backendNetworks/utils.ts similarity index 96% rename from src/chains/utils/backendNetworks.ts rename to src/state/backendNetworks/utils.ts index 0082b40abd0..2242f3042c1 100644 --- a/src/chains/utils/backendNetworks.ts +++ b/src/state/backendNetworks/utils.ts @@ -2,7 +2,7 @@ import { Chain } from 'viem'; import { mainnet } from 'viem/chains'; import { IS_DEV, RPC_PROXY_API_KEY } from '@/env'; -import { BackendNetwork } from '../types'; +import { BackendNetwork } from './types'; const proxyBackendNetworkRpcEndpoint = (endpoint: string) => { return `${endpoint}${RPC_PROXY_API_KEY}`; diff --git a/src/state/nonces/index.ts b/src/state/nonces/index.ts index 8296f42ed86..caa6de76c95 100644 --- a/src/state/nonces/index.ts +++ b/src/state/nonces/index.ts @@ -1,9 +1,9 @@ import create from 'zustand'; import { createStore } from '../internal/createStore'; import { RainbowTransaction } from '@/entities/transactions'; -import { Network, ChainId } from '@/chains/types'; +import { Network, ChainId } from '@/state/backendNetworks/types'; import { getBatchedProvider } from '@/handlers/web3'; -import { chainsIdByName, chainsPrivateMempoolTimeout } from '@/chains'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; import { pendingTransactionsStore } from '@/state/pendingTransactions'; type NonceData = { @@ -23,7 +23,7 @@ export async function getNextNonce({ address, chainId }: { address: string; chai const localNonceData = getNonce({ address, chainId }); const localNonce = localNonceData?.currentNonce || 0; const provider = getBatchedProvider({ chainId }); - const privateMempoolTimeout = chainsPrivateMempoolTimeout[chainId]; + const privateMempoolTimeout = useBackendNetworksStore.getState().getChainsPrivateMempoolTimeout()[chainId]; const pendingTxCountRequest = provider.getTransactionCount(address, 'pending'); const latestTxCountRequest = provider.getTransactionCount(address, 'latest'); @@ -109,6 +109,7 @@ export const nonceStore = createStore>( version: 1, migrate: (persistedState: unknown, version: number) => { if (version === 0) { + const chainsIdByName = useBackendNetworksStore.getState().getChainsIdByName(); const oldState = persistedState as CurrentNonceState; const newNonces: CurrentNonceState['nonces'] = {}; for (const [address, networkNonces] of Object.entries(oldState.nonces)) { diff --git a/src/state/pendingTransactions/index.ts b/src/state/pendingTransactions/index.ts index 44b484f2cbd..a09a7bb006b 100644 --- a/src/state/pendingTransactions/index.ts +++ b/src/state/pendingTransactions/index.ts @@ -3,7 +3,7 @@ import { createStore } from '../internal/createStore'; import create from 'zustand'; import { convertNewTransactionToRainbowTransaction } from '@/parsers/transactions'; import { nonceStore } from '../nonces'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; export interface PendingTransactionsState { pendingTransactions: Record; diff --git a/src/state/staleBalances/index.test.ts b/src/state/staleBalances/index.test.ts index 8631c572bd7..7ac5bbaa270 100644 --- a/src/state/staleBalances/index.test.ts +++ b/src/state/staleBalances/index.test.ts @@ -3,7 +3,7 @@ import { Address } from 'viem'; import { staleBalancesStore } from '.'; import { DAI_ADDRESS } from '@/references'; import { ETH_ADDRESS } from '@rainbow-me/swaps'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; const TEST_ADDRESS_1 = '0xFOO'; const TEST_ADDRESS_2 = '0xBAR'; diff --git a/src/state/swaps/swapsStore.ts b/src/state/swaps/swapsStore.ts index 6fe5c54f53f..c7a030072c9 100644 --- a/src/state/swaps/swapsStore.ts +++ b/src/state/swaps/swapsStore.ts @@ -1,8 +1,8 @@ import { INITIAL_SLIDER_POSITION } from '@/__swaps__/screens/Swap/constants'; -import { ExtendedAnimatedAssetWithColors, ParsedSearchAsset, UniqueId } from '@/__swaps__/types/assets'; +import { ExtendedAnimatedAssetWithColors, ParsedSearchAsset } from '@/__swaps__/types/assets'; +import { ChainId } from '@/state/backendNetworks/types'; import { RecentSwap } from '@/__swaps__/types/swap'; import { getDefaultSlippage } from '@/__swaps__/utils/swaps'; -import { ChainId } from '@/chains/types'; import { RainbowError, logger } from '@/logger'; import { getRemoteConfig } from '@/model/remoteConfig'; import { createRainbowStore } from '@/state/internal/createRainbowStore'; diff --git a/src/state/sync/UserAssetsSync.tsx b/src/state/sync/UserAssetsSync.tsx index 639d9cbc38a..e8722cddfce 100644 --- a/src/state/sync/UserAssetsSync.tsx +++ b/src/state/sync/UserAssetsSync.tsx @@ -4,7 +4,7 @@ import { useSwapsStore } from '@/state/swaps/swapsStore'; import { selectUserAssetsList, selectorFilterByUserChains } from '@/__swaps__/screens/Swap/resources/_selectors/assets'; import { ParsedSearchAsset } from '@/__swaps__/types/assets'; import { useUserAssets } from '@/__swaps__/screens/Swap/resources/assets'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; export const UserAssetsSync = function UserAssetsSync() { const { accountAddress, nativeCurrency: currentCurrency } = useAccountSettings(); diff --git a/src/storage/index.ts b/src/storage/index.ts index 1014ce03dbf..db4e2d88ca1 100644 --- a/src/storage/index.ts +++ b/src/storage/index.ts @@ -3,7 +3,7 @@ import { MMKV } from 'react-native-mmkv'; import { Account, Cards, Campaigns, Device, Review, WatchedWalletCohort } from '@/storage/schema'; import { EthereumAddress, RainbowTransaction } from '@/entities'; import { SecureStorage } from '@coinbase/mobile-wallet-protocol-host'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; /** * Generic storage class. DO NOT use this directly. Instead, use the exported diff --git a/src/styles/colors.ts b/src/styles/colors.ts index b1bfe39c376..0cdac9387af 100644 --- a/src/styles/colors.ts +++ b/src/styles/colors.ts @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import { globalColors } from '@/design-system'; import currentColors from '../theme/currentColors'; import { memoFn } from '../utils/memoFn'; -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; export type Colors = ReturnType; diff --git a/src/utils/ethereumUtils.ts b/src/utils/ethereumUtils.ts index daf8cbb0d66..7c095eb3676 100644 --- a/src/utils/ethereumUtils.ts +++ b/src/utils/ethereumUtils.ts @@ -39,9 +39,9 @@ import { fetchExternalToken, useExternalToken, } from '@/resources/assets/externalAssetsQuery'; -import { ChainId, Network } from '@/chains/types'; +import { ChainId, Network } from '@/state/backendNetworks/types'; import { AddressOrEth } from '@/__swaps__/types/assets'; -import { chainsIdByName, chainsName, chainsNativeAsset, defaultChains, getChainGasUnits } from '@/chains'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; /** @@ -75,7 +75,7 @@ export const getAddressAndChainIdFromUniqueId = (uniqueId: string): { address: A const networkOrChainId = parts[1]; // if the second part is a string, it's probably a network if (isNaN(Number(networkOrChainId))) { - const chainId = chainsIdByName[networkOrChainId] || ChainId.mainnet; // Default to mainnet if unknown + const chainId = useBackendNetworksStore.getState().getChainsIdByName()[networkOrChainId] || ChainId.mainnet; // Default to mainnet if unknown return { address, chainId }; } @@ -83,7 +83,7 @@ export const getAddressAndChainIdFromUniqueId = (uniqueId: string): { address: A }; const getNetworkNativeAsset = ({ chainId }: { chainId: ChainId }) => { - const nativeAssetAddress = chainsNativeAsset[chainId].address; + const nativeAssetAddress = useBackendNetworksStore.getState().getChainsNativeAsset()[chainId].address; const nativeAssetUniqueId = getUniqueId(nativeAssetAddress, chainId); return getAccountAsset(nativeAssetUniqueId); }; @@ -102,7 +102,7 @@ export const getNativeAssetForNetwork = async ({ // If the asset is on a different wallet, or not available in this wallet if (differentWallet || !nativeAsset) { - const chainNativeAsset = chainsNativeAsset[chainId]; + const chainNativeAsset = useBackendNetworksStore.getState().getChainsNativeAsset()[chainId]; const mainnetAddress = chainNativeAsset?.address || ETH_ADDRESS; const nativeAssetAddress = chainNativeAsset.address as AddressOrEth; @@ -117,7 +117,7 @@ export const getNativeAssetForNetwork = async ({ // @ts-ignore nativeAsset = { ...externalAsset, - network: chainsName[chainId], + network: useBackendNetworksStore.getState().getChainsName()[chainId], uniqueId: getUniqueId(chainNativeAsset.address, chainId), address: chainNativeAsset.address, decimals: chainNativeAsset.decimals, @@ -211,7 +211,7 @@ const getAssetPrice = ( export const useNativeAsset = ({ chainId }: { chainId: ChainId }) => { const { nativeCurrency } = store.getState().settings; - const address = (chainsNativeAsset[chainId]?.address || ETH_ADDRESS) as AddressOrEth; + const address = (useBackendNetworksStore.getState().getChainsNativeAsset()[chainId]?.address || ETH_ADDRESS) as AddressOrEth; const { data: nativeAsset } = useExternalToken({ address, @@ -223,6 +223,7 @@ export const useNativeAsset = ({ chainId }: { chainId: ChainId }) => { }; const getPriceOfNativeAssetForNetwork = ({ chainId }: { chainId: ChainId }) => { + const chainsNativeAsset = useBackendNetworksStore.getState().getChainsNativeAsset(); const address = (chainsNativeAsset[chainId]?.address || ETH_ADDRESS) as AddressOrEth; return getAssetPrice({ address, chainId }); }; @@ -297,8 +298,8 @@ const getDataString = (func: string, arrVals: string[]) => { */ function getEtherscanHostForNetwork({ chainId }: { chainId: ChainId }): string { const base_host = 'etherscan.io'; - const blockExplorer = defaultChains[chainId]?.blockExplorers?.default?.url; - const network = chainsName[chainId]; + const blockExplorer = useBackendNetworksStore.getState().getDefaultChains()[chainId]?.blockExplorers?.default?.url; + const network = useBackendNetworksStore.getState().getChainsName()[chainId]; if (network && isTestnetChain({ chainId })) { return `${network}.${base_host}`; @@ -373,29 +374,29 @@ export const getFirstTransactionTimestamp = async (address: EthereumAddress): Pr }; function getBlockExplorer({ chainId }: { chainId: ChainId }) { - return defaultChains[chainId]?.blockExplorers?.default.name || 'etherscan'; + return useBackendNetworksStore.getState().getDefaultChains()[chainId]?.blockExplorers?.default.name || 'etherscan'; } function openAddressInBlockExplorer({ address, chainId }: { address: EthereumAddress; chainId: ChainId }) { - const explorer = defaultChains[chainId]?.blockExplorers?.default?.url; + const explorer = useBackendNetworksStore.getState().getDefaultChains()[chainId]?.blockExplorers?.default?.url; Linking.openURL(`${explorer}/address/${address}`); } function openTokenEtherscanURL({ address, chainId }: { address: EthereumAddress; chainId: ChainId }) { if (!isString(address)) return; - const explorer = defaultChains[chainId]?.blockExplorers?.default?.url; + const explorer = useBackendNetworksStore.getState().getDefaultChains()[chainId]?.blockExplorers?.default?.url; Linking.openURL(`${explorer}/token/${address}`); } function openNftInBlockExplorer({ contractAddress, tokenId, chainId }: { contractAddress: string; tokenId: string; chainId: ChainId }) { - const explorer = defaultChains[chainId]?.blockExplorers?.default?.url; + const explorer = useBackendNetworksStore.getState().getDefaultChains()[chainId]?.blockExplorers?.default?.url; Linking.openURL(`${explorer}/token/${contractAddress}?a=${tokenId}`); } function openTransactionInBlockExplorer({ hash, chainId }: { hash: string; chainId: ChainId }) { const normalizedHash = hash.replace(/-.*/g, ''); if (!isString(hash)) return; - const explorer = defaultChains[chainId]?.blockExplorers?.default?.url; + const explorer = useBackendNetworksStore.getState().getDefaultChains()[chainId]?.blockExplorers?.default?.url; Linking.openURL(`${explorer}/tx/${normalizedHash}`); } @@ -411,14 +412,14 @@ async function parseEthereumUrl(data: string) { const functionName = ethUrl.function_name; let asset = null; const chainId = (ethUrl.chain_id as ChainId) || ChainId.mainnet; - const network = chainsName[chainId]; + const network = useBackendNetworksStore.getState().getChainsName()[chainId]; let address: any = null; let nativeAmount: any = null; const { nativeCurrency } = store.getState().settings; if (!functionName) { // Send native asset - const chainId = chainsIdByName[network]; + const chainId = useBackendNetworksStore.getState().getChainsIdByName()[network]; asset = getNetworkNativeAsset({ chainId }); // @ts-ignore @@ -506,7 +507,7 @@ const calculateL1FeeOptimism = async ( }; const getBasicSwapGasLimit = (chainId: ChainId) => { - return Number(getChainGasUnits(chainId).basic.swap); + return Number(useBackendNetworksStore.getState().getChainGasUnits(chainId).basic.swap); }; export default { diff --git a/src/utils/getUrlForTrustIconFallback.ts b/src/utils/getUrlForTrustIconFallback.ts index c9757d3080d..53509e2b121 100644 --- a/src/utils/getUrlForTrustIconFallback.ts +++ b/src/utils/getUrlForTrustIconFallback.ts @@ -1,6 +1,6 @@ -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { EthereumAddress } from '@/entities'; -import { chainsName } from '@/chains'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; export default function getUrlForTrustIconFallback(address: EthereumAddress, chainId: ChainId): string | null { if (!address) return null; @@ -13,7 +13,7 @@ export default function getUrlForTrustIconFallback(address: EthereumAddress, cha networkPath = 'smartchain'; break; default: - networkPath = chainsName[chainId]; + networkPath = useBackendNetworksStore.getState().getChainsName()[chainId]; } return `https://rainbowme-res.cloudinary.com/image/upload/assets/${networkPath}/${address}.png`; } diff --git a/src/utils/requestNavigationHandlers.ts b/src/utils/requestNavigationHandlers.ts index 26d07de7559..0c3d1d21622 100644 --- a/src/utils/requestNavigationHandlers.ts +++ b/src/utils/requestNavigationHandlers.ts @@ -32,8 +32,8 @@ import { noop } from 'lodash'; import { toUtf8String } from '@ethersproject/strings'; import { BigNumber } from '@ethersproject/bignumber'; import { Address } from 'viem'; -import { ChainId } from '@/chains/types'; -import { chainsName, SUPPORTED_MAINNET_CHAIN_IDS } from '@/chains'; +import { ChainId } from '@/state/backendNetworks/types'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; import { MobileWalletProtocolUserErrors } from '@/components/MobileWalletProtocolListener'; import { hideWalletConnectToast } from '@/components/toasts/WalletConnectToast'; import { removeWalletConnectRequest } from '@/state/walletConnectRequests'; @@ -122,7 +122,7 @@ export const handleMobileWalletProtocolRequest = async ({ const routeParams: WalletconnectApprovalSheetRouteParams = { receivedTimestamp, meta: { - chainIds: SUPPORTED_MAINNET_CHAIN_IDS, + chainIds: useBackendNetworksStore.getState().getSupportedMainnetChainIds(), dappName: dappMetadata?.appName || dappMetadata?.appUrl || action.appName || action.appIconUrl || action.appId || '', dappUrl: dappMetadata?.appUrl || action.appId || '', imageUrl: maybeSignUri(dappMetadata?.iconUrl || action.appIconUrl), @@ -168,7 +168,10 @@ export const handleMobileWalletProtocolRequest = async ({ } if (action.method === 'wallet_switchEthereumChain') { - const isSupportedChain = SUPPORTED_MAINNET_CHAIN_IDS.includes(BigNumber.from(action.params.chainId).toNumber()); + const isSupportedChain = useBackendNetworksStore + .getState() + .getSupportedMainnetChainIds() + .includes(BigNumber.from(action.params.chainId).toNumber()); if (!isSupportedChain) { await rejectAction(action, { message: 'Unsupported chain', @@ -300,7 +303,7 @@ export const handleDappBrowserConnectionPrompt = ( const routeParams: WalletconnectApprovalSheetRouteParams = { receivedTimestamp, meta: { - chainIds: SUPPORTED_MAINNET_CHAIN_IDS, + chainIds: useBackendNetworksStore.getState().getSupportedMainnetChainIds(), dappName: dappData?.dappName || dappData.dappUrl, dappUrl: dappData.dappUrl, imageUrl: maybeSignUri(dappData.imageUrl), @@ -400,7 +403,7 @@ export const handleDappBrowserRequest = async (request: Omit { const chainId = request?.walletConnectV2RequestValues?.chainId; if (!chainId) return; - const network = chainsName[chainId]; + const network = useBackendNetworksStore.getState().getChainsName()[chainId]; const address = request?.walletConnectV2RequestValues?.address; const onSuccess = async (result: string) => { diff --git a/src/walletConnect/index.tsx b/src/walletConnect/index.tsx index c5600cfb845..81894116634 100644 --- a/src/walletConnect/index.tsx +++ b/src/walletConnect/index.tsx @@ -47,8 +47,8 @@ import { DAppStatus } from '@/graphql/__generated__/metadata'; import { handleWalletConnectRequest } from '@/utils/requestNavigationHandlers'; import { PerformanceMetrics } from '@/performance/tracking/types/PerformanceMetrics'; import { PerformanceTracking } from '@/performance/tracking'; -import { ChainId } from '@/chains/types'; -import { SUPPORTED_CHAIN_IDS } from '@/chains'; +import { ChainId } from '@/state/backendNetworks/types'; +import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; import { hideWalletConnectToast } from '@/components/toasts/WalletConnectToast'; const SUPPORTED_SESSION_EVENTS = ['chainChanged', 'accountsChanged']; @@ -451,7 +451,8 @@ export async function onSessionProposal(proposal: WalletKitTypes.SessionProposal // we already checked for eip155 namespace above const chainIds = chains?.map(chain => parseInt(chain.split('eip155:')[1])); - const supportedChainIds = chainIds.filter(chainId => SUPPORTED_CHAIN_IDS.includes(chainId)); + const supportedChainIds = useBackendNetworksStore.getState().getSupportedChainIds(); + const chainIdsToUse = chainIds.filter(chainId => supportedChainIds.includes(chainId)); const peerMeta = proposer.metadata; const metadata = await fetchDappMetadata({ url: peerMeta.url, status: true }); @@ -462,7 +463,7 @@ export async function onSessionProposal(proposal: WalletKitTypes.SessionProposal const routeParams: WalletconnectApprovalSheetRouteParams = { receivedTimestamp, meta: { - chainIds: supportedChainIds, + chainIds: chainIdsToUse, dappName, dappScheme: 'unused in WC v2', // only used for deeplinks from WC v1 dappUrl: peerMeta.url || lang.t(lang.l.walletconnect.unknown_url), @@ -492,7 +493,7 @@ export async function onSessionProposal(proposal: WalletKitTypes.SessionProposal const supportedEvents = requiredNamespaces?.eip155?.events || SUPPORTED_SESSION_EVENTS; /** @see https://chainagnostic.org/CAIPs/caip-2 */ - const caip2ChainIds = SUPPORTED_CHAIN_IDS.map(id => `eip155:${id}`); + const caip2ChainIds = supportedChainIds.map(id => `eip155:${id}`); const namespaces = getApprovedNamespaces({ proposal: proposal.params, supportedNamespaces: { diff --git a/src/walletConnect/types.ts b/src/walletConnect/types.ts index 18a8a2a7237..239265a1220 100644 --- a/src/walletConnect/types.ts +++ b/src/walletConnect/types.ts @@ -1,4 +1,4 @@ -import { ChainId } from '@/chains/types'; +import { ChainId } from '@/state/backendNetworks/types'; import { Address } from 'viem'; import { SignClientTypes, Verify } from '@walletconnect/types'; import { RequestSource } from '@/utils/requestNavigationHandlers'; From 668bbb159dba9a3ad71bae44c9adc4b3857257f4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:22:41 -0500 Subject: [PATCH 29/95] Bump nanoid from 3.3.7 to 3.3.8 in /src/design-system/docs (#6320) Bumps [nanoid](https://github.com/ai/nanoid) from 3.3.7 to 3.3.8. - [Release notes](https://github.com/ai/nanoid/releases) - [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md) - [Commits](https://github.com/ai/nanoid/compare/3.3.7...3.3.8) --- updated-dependencies: - dependency-name: nanoid dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- src/design-system/docs/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/design-system/docs/yarn.lock b/src/design-system/docs/yarn.lock index f300bad06e2..c135098c36e 100644 --- a/src/design-system/docs/yarn.lock +++ b/src/design-system/docs/yarn.lock @@ -7333,11 +7333,11 @@ __metadata: linkType: hard "nanoid@npm:^3.3.4, nanoid@npm:^3.3.7": - version: 3.3.7 - resolution: "nanoid@npm:3.3.7" + version: 3.3.8 + resolution: "nanoid@npm:3.3.8" bin: nanoid: bin/nanoid.cjs - checksum: 10c0/e3fb661aa083454f40500473bb69eedb85dc160e763150b9a2c567c7e9ff560ce028a9f833123b618a6ea742e311138b591910e795614a629029e86e180660f3 + checksum: 10c0/4b1bb29f6cfebf3be3bc4ad1f1296fb0a10a3043a79f34fbffe75d1621b4318319211cd420549459018ea3592f0d2f159247a6f874911d6d26eaaadda2478120 languageName: node linkType: hard From a97f60d1b576d4cead4da3b2ca6d75e626c0435d Mon Sep 17 00:00:00 2001 From: Jin Date: Wed, 11 Dec 2024 17:19:29 -0500 Subject: [PATCH 30/95] Update default currentNonce value to -1 (use case: fresh wallet or freshly imported wallet) (#6324) * Set currentNonce default to -1 if not stored (use case: fresh wallet or freshly imported wallet) * Update pending txn logic to treat default of -1 for currentNonce when it does not exist yet --- src/state/nonces/index.ts | 2 +- src/state/pendingTransactions/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/state/nonces/index.ts b/src/state/nonces/index.ts index caa6de76c95..d49d40e9c89 100644 --- a/src/state/nonces/index.ts +++ b/src/state/nonces/index.ts @@ -21,7 +21,7 @@ type UpdateNonceArgs = NonceData & GetNonceArgs; export async function getNextNonce({ address, chainId }: { address: string; chainId: ChainId }): Promise { const { getNonce } = nonceStore.getState(); const localNonceData = getNonce({ address, chainId }); - const localNonce = localNonceData?.currentNonce || 0; + const localNonce = localNonceData?.currentNonce || -1; const provider = getBatchedProvider({ chainId }); const privateMempoolTimeout = useBackendNetworksStore.getState().getChainsPrivateMempoolTimeout()[chainId]; diff --git a/src/state/pendingTransactions/index.ts b/src/state/pendingTransactions/index.ts index a09a7bb006b..a53be91df02 100644 --- a/src/state/pendingTransactions/index.ts +++ b/src/state/pendingTransactions/index.ts @@ -84,7 +84,7 @@ export const addNewTransaction = ({ const parsedTransaction = convertNewTransactionToRainbowTransaction(transaction); addPendingTransaction({ address, pendingTransaction: parsedTransaction }); const localNonceData = getNonce({ address, chainId }); - const localNonce = localNonceData?.currentNonce || 0; + const localNonce = localNonceData?.currentNonce || -1; if (transaction.nonce > localNonce) { setNonce({ address, From 9dc68367d263ea2af91d4cbe51b2261102d9c7ba Mon Sep 17 00:00:00 2001 From: Jin Date: Thu, 12 Dec 2024 14:27:45 -0500 Subject: [PATCH 31/95] Replace node ack retries with a small delay (#6326) --- src/raps/execute.ts | 30 ++++-------------------------- 1 file changed, 4 insertions(+), 26 deletions(-) diff --git a/src/raps/execute.ts b/src/raps/execute.ts index fa8fed28cab..08502a53aaf 100644 --- a/src/raps/execute.ts +++ b/src/raps/execute.ts @@ -117,30 +117,7 @@ function getRapFullName(actions: RapAction[]) { const delay = (ms: number) => new Promise(res => setTimeout(res, ms)); -const NODE_ACK_MAX_TRIES = 10; - -const waitForNodeAck = async (hash: string, provider: Signer['provider'], tries = 0): Promise => { - try { - const tx = await provider?.getTransaction(hash); - - // This means the node is aware of the tx, we're good to go - if ((tx && tx.blockNumber === null) || (tx && tx?.blockNumber && tx?.blockNumber > 0)) { - return; - } - - // Wait for 1 second and try again - if (tries < NODE_ACK_MAX_TRIES) { - await delay(1000); - return waitForNodeAck(hash, provider, tries + 1); - } - } catch (e) { - // Wait for 1 second and try again - if (tries < NODE_ACK_MAX_TRIES) { - await delay(1000); - return waitForNodeAck(hash, provider, tries + 1); - } - } -}; +const NODE_ACK_DELAY = 500; export const walletExecuteRap = async ( wallet: Signer, @@ -177,12 +154,13 @@ export const walletExecuteRap = async ( }; const { baseNonce, errorMessage: error, hash: firstHash } = await executeAction(actionParams); - const shouldWaitForNodeAck = parameters.chainId !== ChainId.mainnet; + const shouldDelayForNodeAck = parameters.chainId !== ChainId.mainnet; if (typeof baseNonce === 'number') { let latestHash = firstHash; for (let index = 1; index < actions.length; index++) { - latestHash && shouldWaitForNodeAck && (await waitForNodeAck(latestHash, wallet.provider)); + latestHash && shouldDelayForNodeAck && (await delay(NODE_ACK_DELAY)); + const action = actions[index]; const actionParams = { action, From 9066a2ad994e638d12b8cfa1755fb0bc83480fa3 Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Thu, 12 Dec 2024 16:14:27 -0500 Subject: [PATCH 32/95] fix undefined size on TextShadow component when switching themes (#6329) --- src/screens/points/content/PointsContent.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/screens/points/content/PointsContent.tsx b/src/screens/points/content/PointsContent.tsx index 295336fc9ff..282e24ba181 100644 --- a/src/screens/points/content/PointsContent.tsx +++ b/src/screens/points/content/PointsContent.tsx @@ -476,9 +476,11 @@ const NextDistributionCountdown = ({ nextDistribution }: { nextDistribution: Dat const minuteStr = minutes ? `${minutes}m` : ''; return ( - - {`${dayStr} ${hourStr} ${minuteStr}`.trim()} - + + + {`${dayStr} ${hourStr} ${minuteStr}`.trim()} + + ); }; @@ -529,9 +531,7 @@ const NextDropCard = memo(function NextDropCard({ nextDistribution }: { nextDist overflow: 'hidden', }} > - - - + From abf5db5f281f803d7efcdfaf5d3986c082942001 Mon Sep 17 00:00:00 2001 From: Wayne Cheng <677680+welps@users.noreply.github.com> Date: Thu, 12 Dec 2024 17:05:37 -0500 Subject: [PATCH 33/95] chore: update swaps sdk + use new getWrappedAssetAddress method (#6327) --- package.json | 2 +- src/raps/actions/swap.ts | 8 ++++---- yarn.lock | 10 +++++----- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 20c652428f4..2a2e505bc1a 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "@notifee/react-native": "7.8.2", "@rainbow-me/provider": "0.1.1", "@rainbow-me/react-native-animated-number": "0.0.2", - "@rainbow-me/swaps": "0.28.0", + "@rainbow-me/swaps": "0.30.1", "@react-native-async-storage/async-storage": "1.23.1", "@react-native-camera-roll/camera-roll": "7.7.0", "@react-native-clipboard/clipboard": "1.13.2", diff --git a/src/raps/actions/swap.ts b/src/raps/actions/swap.ts index deeb8c5658d..7bcf02a9173 100644 --- a/src/raps/actions/swap.ts +++ b/src/raps/actions/swap.ts @@ -3,11 +3,11 @@ import { Transaction } from '@ethersproject/transactions'; import { CrosschainQuote, Quote, - ChainId as SwapChainId, SwapType, fillQuote, getQuoteExecutionDetails, getRainbowRouterContractAddress, + getWrappedAssetAddress, getWrappedAssetMethod, unwrapNativeAsset, wrapNativeAsset, @@ -138,7 +138,7 @@ export const estimateSwapGasLimit = async ({ from: quote.from, value: isWrapNativeAsset ? quote.buyAmount.toString() : '0', }, - getWrappedAssetMethod(isWrapNativeAsset ? 'deposit' : 'withdraw', provider, chainId as unknown as SwapChainId), + getWrappedAssetMethod(isWrapNativeAsset ? 'deposit' : 'withdraw', provider, getWrappedAssetAddress(quote)), isWrapNativeAsset ? [] : [quote.buyAmount.toString()], provider, WRAP_GAS_PADDING @@ -285,10 +285,10 @@ export const executeSwap = async ({ // Wrap Eth if (quote.swapType === SwapType.wrap) { - return wrapNativeAsset(quote.buyAmount, wallet, chainId as unknown as SwapChainId, transactionParams); + return wrapNativeAsset(quote.buyAmount, wallet, getWrappedAssetAddress(quote), transactionParams); // Unwrap Weth } else if (quote.swapType === SwapType.unwrap) { - return unwrapNativeAsset(quote.sellAmount, wallet, chainId as unknown as SwapChainId, transactionParams); + return unwrapNativeAsset(quote.sellAmount, wallet, getWrappedAssetAddress(quote), transactionParams); // Swap } else { return fillQuote(quote, transactionParams, wallet, permit, chainId as number, REFERRER); diff --git a/yarn.lock b/yarn.lock index 004c5ca45b8..2926ab36f27 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5512,9 +5512,9 @@ __metadata: languageName: node linkType: hard -"@rainbow-me/swaps@npm:0.28.0": - version: 0.28.0 - resolution: "@rainbow-me/swaps@npm:0.28.0" +"@rainbow-me/swaps@npm:0.30.1": + version: 0.30.1 + resolution: "@rainbow-me/swaps@npm:0.30.1" dependencies: "@ethereumjs/util": "npm:9.0.0" "@ethersproject/abi": "npm:5.7.0" @@ -5529,7 +5529,7 @@ __metadata: "@ethersproject/transactions": "npm:5.7.0" "@ethersproject/wallet": "npm:5.7.0" "@metamask/eth-sig-util": "npm:7.0.0" - checksum: 10c0/a5c8cd8325ceb7552ad7442a815b1a5afa9ddc9f7487a732704b414fcd94e718286951c02a46440d6252020bdd191bacc38f59d61deec6933d78fb787d1602c9 + checksum: 10c0/40f6824393986527bdd41b56129e5189c0089f74ca4a4e860aea5bb706c5acc4ad2458cf5a63edead5d45d61bde0c40251b536c640aa2a6f3b2d026c0161fcf9 languageName: node linkType: hard @@ -9129,7 +9129,7 @@ __metadata: "@notifee/react-native": "npm:7.8.2" "@rainbow-me/provider": "npm:0.1.1" "@rainbow-me/react-native-animated-number": "npm:0.0.2" - "@rainbow-me/swaps": "npm:0.28.0" + "@rainbow-me/swaps": "npm:0.30.1" "@react-native-async-storage/async-storage": "npm:1.23.1" "@react-native-camera-roll/camera-roll": "npm:7.7.0" "@react-native-clipboard/clipboard": "npm:1.13.2" From 2f81bceac917149d6b212f16318e9e65b572c973 Mon Sep 17 00:00:00 2001 From: Bruno Barbieri <1247834+brunobar79@users.noreply.github.com> Date: Fri, 13 Dec 2024 15:25:58 -0500 Subject: [PATCH 34/95] NFT checker 2.0 (#6293) --- android/app/src/main/AndroidManifest.xml | 13 + .../main/res/mipmap-anydpi-v26/redacted.xml | 5 + .../res/mipmap-anydpi-v26/redacted_round.xml | 5 + .../main/res/mipmap-hdpi/legacy_redacted.png | Bin 0 -> 5575 bytes .../res/mipmap-hdpi/redacted_background.png | Bin 0 -> 3825 bytes .../res/mipmap-hdpi/redacted_foreground.png | Bin 0 -> 15841 bytes .../main/res/mipmap-hdpi/redacted_round.png | Bin 0 -> 6292 bytes .../main/res/mipmap-ldpi/legacy_redacted.png | Bin 0 -> 2156 bytes .../res/mipmap-ldpi/redacted_background.png | Bin 0 -> 1365 bytes .../res/mipmap-ldpi/redacted_foreground.png | Bin 0 -> 5267 bytes .../main/res/mipmap-ldpi/redacted_round.png | Bin 0 -> 2490 bytes .../main/res/mipmap-mdpi/legacy_redacted.png | Bin 0 -> 3092 bytes .../res/mipmap-mdpi/redacted_background.png | Bin 0 -> 1817 bytes .../res/mipmap-mdpi/redacted_foreground.png | Bin 0 -> 8299 bytes .../main/res/mipmap-mdpi/redacted_round.png | Bin 0 -> 3525 bytes .../main/res/mipmap-xhdpi/legacy_redacted.png | Bin 0 -> 8596 bytes .../res/mipmap-xhdpi/redacted_background.png | Bin 0 -> 5664 bytes .../res/mipmap-xhdpi/redacted_foreground.png | Bin 0 -> 23852 bytes .../main/res/mipmap-xhdpi/redacted_round.png | Bin 0 -> 9528 bytes .../res/mipmap-xxhdpi/legacy_redacted.png | Bin 0 -> 15521 bytes .../res/mipmap-xxhdpi/redacted_background.png | Bin 0 -> 24979 bytes .../res/mipmap-xxhdpi/redacted_foreground.png | Bin 0 -> 44672 bytes .../main/res/mipmap-xxhdpi/redacted_round.png | Bin 0 -> 17194 bytes .../res/mipmap-xxxhdpi/legacy_redacted.png | Bin 0 -> 24476 bytes .../mipmap-xxxhdpi/redacted_background.png | Bin 0 -> 37628 bytes .../mipmap-xxxhdpi/redacted_foreground.png | Bin 0 -> 64749 bytes .../res/mipmap-xxxhdpi/redacted_round.png | Bin 0 -> 26674 bytes ios/AppIcons/redacted@2x.png | Bin 0 -> 13326 bytes ios/AppIcons/redacted@3x.png | Bin 0 -> 24676 bytes ios/Podfile.lock | 2 +- ios/Rainbow.xcodeproj/project.pbxproj | 354 +++++++++--------- ios/Rainbow/Info.plist | 9 + src/appIcons/appIcons.ts | 25 +- src/assets/appIconRedacted.png | Bin 0 -> 14969 bytes src/assets/appIconRedacted@2x.png | Bin 0 -> 42535 bytes src/assets/appIconRedacted@3x.png | Bin 0 -> 84234 bytes src/featuresToUnlock/tokenGatedUtils.ts | 63 +++- .../unlockableAppIconCheck.ts | 26 +- src/languages/en_US.json | 2 + src/references/token-gate-checker-abi.json | 22 +- 40 files changed, 327 insertions(+), 199 deletions(-) create mode 100644 android/app/src/main/res/mipmap-anydpi-v26/redacted.xml create mode 100644 android/app/src/main/res/mipmap-anydpi-v26/redacted_round.xml create mode 100644 android/app/src/main/res/mipmap-hdpi/legacy_redacted.png create mode 100644 android/app/src/main/res/mipmap-hdpi/redacted_background.png create mode 100644 android/app/src/main/res/mipmap-hdpi/redacted_foreground.png create mode 100644 android/app/src/main/res/mipmap-hdpi/redacted_round.png create mode 100644 android/app/src/main/res/mipmap-ldpi/legacy_redacted.png create mode 100644 android/app/src/main/res/mipmap-ldpi/redacted_background.png create mode 100644 android/app/src/main/res/mipmap-ldpi/redacted_foreground.png create mode 100644 android/app/src/main/res/mipmap-ldpi/redacted_round.png create mode 100644 android/app/src/main/res/mipmap-mdpi/legacy_redacted.png create mode 100644 android/app/src/main/res/mipmap-mdpi/redacted_background.png create mode 100644 android/app/src/main/res/mipmap-mdpi/redacted_foreground.png create mode 100644 android/app/src/main/res/mipmap-mdpi/redacted_round.png create mode 100644 android/app/src/main/res/mipmap-xhdpi/legacy_redacted.png create mode 100644 android/app/src/main/res/mipmap-xhdpi/redacted_background.png create mode 100644 android/app/src/main/res/mipmap-xhdpi/redacted_foreground.png create mode 100644 android/app/src/main/res/mipmap-xhdpi/redacted_round.png create mode 100644 android/app/src/main/res/mipmap-xxhdpi/legacy_redacted.png create mode 100644 android/app/src/main/res/mipmap-xxhdpi/redacted_background.png create mode 100644 android/app/src/main/res/mipmap-xxhdpi/redacted_foreground.png create mode 100644 android/app/src/main/res/mipmap-xxhdpi/redacted_round.png create mode 100644 android/app/src/main/res/mipmap-xxxhdpi/legacy_redacted.png create mode 100644 android/app/src/main/res/mipmap-xxxhdpi/redacted_background.png create mode 100644 android/app/src/main/res/mipmap-xxxhdpi/redacted_foreground.png create mode 100644 android/app/src/main/res/mipmap-xxxhdpi/redacted_round.png create mode 100644 ios/AppIcons/redacted@2x.png create mode 100644 ios/AppIcons/redacted@3x.png create mode 100644 src/assets/appIconRedacted.png create mode 100644 src/assets/appIconRedacted@2x.png create mode 100644 src/assets/appIconRedacted@3x.png diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 5fa3c8649c4..0554e98e1eb 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -259,6 +259,19 @@ + + + + + + diff --git a/android/app/src/main/res/mipmap-anydpi-v26/redacted.xml b/android/app/src/main/res/mipmap-anydpi-v26/redacted.xml new file mode 100644 index 00000000000..082c50eb1c0 --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/redacted.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android/app/src/main/res/mipmap-anydpi-v26/redacted_round.xml b/android/app/src/main/res/mipmap-anydpi-v26/redacted_round.xml new file mode 100644 index 00000000000..082c50eb1c0 --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/redacted_round.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/legacy_redacted.png b/android/app/src/main/res/mipmap-hdpi/legacy_redacted.png new file mode 100644 index 0000000000000000000000000000000000000000..b8048596a840dc8a3250c127bbb893daeb5034ba GIT binary patch literal 5575 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 VSgVNJ4D$d0002ovPDHLkV1hRKm;(R+ literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-hdpi/redacted_background.png b/android/app/src/main/res/mipmap-hdpi/redacted_background.png new file mode 100644 index 0000000000000000000000000000000000000000..6b5cf4349b35ca79ecc60d9f142c0a8ffb5d7fb7 GIT binary patch literal 3825 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 KMNUMnLSTX*UHm%$ literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-ldpi/legacy_redacted.png b/android/app/src/main/res/mipmap-ldpi/legacy_redacted.png new file mode 100644 index 0000000000000000000000000000000000000000..42cb39db7ba643ea5c0f5afebd6194df860913d5 GIT binary patch literal 2156 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 XSbW=c#6tS!00000NkvXXu0mjf2Y`{3 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-ldpi/redacted_foreground.png b/android/app/src/main/res/mipmap-ldpi/redacted_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..3c6bfca090fdc8b5202cf15d26830e63302aef56 GIT binary patch literal 5267 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 ZU@7E-e*JMR%TNFS002ovPDHLkV1jGnWq1Gp literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-ldpi/redacted_round.png b/android/app/src/main/res/mipmap-ldpi/redacted_round.png new file mode 100644 index 0000000000000000000000000000000000000000..c01d0478328bede24243da285040126c442a4e75 GIT binary patch literal 2490 zcmV;r2}SmaP)U>h*?d$b`aT5T$j!+ zmv){4Q-A$k)t||l8vWnYL5&#};E{=yZHo!sB_1(oDdMW&yvxUe(=MMoo|R;r_?B4G z3TlS9Ni3&3^n3@Gie;^}gV^4m)mN9r9O_ZbVG|}8icnF31O?%L{L_vm;#3HD6?Va!XbBie+=k*;rwF^@ErnnmGiG7&g~k7pMfj= z2Nx-7yi*22%K!if07*naRCod1m;-cUTN{R-Z=Z9525r-him`ER+qUuBwr$(CZCkyz zrgw^|l%`3OoU`{g$ZFRb``5G9dyFT3cld8Z*h`v^|8ypZYtH#()J7Qq@%(~`D1;Rm z0SX6PsQL6yEHzHA7U5z=4`(L`(xJ-tosq8t5A9c>+$kSu3&ekeI&MZV>Csu5L56# zJOLcyYR4NJVVuJghZv>FKE}IWdKR}_aV%G#xq_EI;TW#Ecm;)go&X1~mQD8zqj2dq z&yc5jwK!)C4$r~-AWwTRL6o)hwJ4P&^i^6YMLAsALwiHKiHHD2)PaH%hj9>P4CgLs zV*b3@#F7DPd-J^bE#GBDqh-OIG^P9`6%9}aD#m!q)xaCK5OU%!78ECnFIAqLeT<9e z_;k+q`0<&R@do%s{&3b$90L;}^*^~C|Be!#s}Q#qD?*4*(hC_pb~(PYc%FxdkWIxo z_w<998I6qwL7n=d_0VYae&NmJLtT-xGC{_Ut$rKXAfXR|280|rdmVqdZI*U_K6#$$vEd&v$H~_3DqRIoMI)=*;UmvQF zGRKo{oP`)jCt5f$%<%p0yLs7(C-CGS|B540DazvUHw~G4!~(LpS#;GwE+2B~x+D3~ z?Z0D2N+=Q0S+w+(>u}ED5I_MyEdU0x^;fT6;i61@o3tF3fp7Xp*XrJH2?E?jV^y40$IkcV= zk3N{sKG;q7_#;At0s8849qpWBPs1`^0VMjX;{rLu- zwDM>=#{>Ew`71ZhP4eg7Jl`J4;#}=Z6a}S*2pvEHK&U}%#WP-w${VPG5k-JGewo*_ ztmA@3v#5)w36pT&nvmbMIf}o|@|@VgjQDXL92p~Sgazq35`K)IKJqwq!w++Qi%;iR zp4UF&^SiM|tSS~^3q)T7ym1Sm`0{5iRp;&p3^nis46ra>XOK&$$(JJr3I%Lwn6sNKYDn*aa+ literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-mdpi/legacy_redacted.png b/android/app/src/main/res/mipmap-mdpi/legacy_redacted.png new file mode 100644 index 0000000000000000000000000000000000000000..31893c73a716017cc9c6f80d832f05493705467f GIT binary patch literal 3092 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-mdpi/redacted_round.png b/android/app/src/main/res/mipmap-mdpi/redacted_round.png new file mode 100644 index 0000000000000000000000000000000000000000..d7974bd9c1dbe6758ca3cb8430aa03a149d250d7 GIT binary patch literal 3525 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xhdpi/legacy_redacted.png b/android/app/src/main/res/mipmap-xhdpi/legacy_redacted.png new file mode 100644 index 0000000000000000000000000000000000000000..a2e4a357bd2e83668f2d757d08960bf234db9fc0 GIT binary patch literal 8596 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�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 Txj07LpI?Bc8d9}Z*(&Tm86#wG literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xhdpi/redacted_foreground.png b/android/app/src/main/res/mipmap-xhdpi/redacted_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..a7d739d8c9bbd58c936deac5f1d4f0973e5fa314 GIT binary patch literal 23852 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

8byy=t-T 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 Wz>4tE5pE0s0000U>h*?d$b`aT5T$j!+ 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