diff --git a/.gitignore b/.gitignore index 1c63e3e0263..c1e4e271eca 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,6 @@ packages/uniswap/src/i18n/locales/source/*_old.json # Vercel .vercel + +# CodeTours Extension +.tours/* diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 00000000000..f70773659eb --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +* @uniswap/web-admins diff --git a/RELEASE b/RELEASE index e2a5e9c726e..e7980143d31 100644 --- a/RELEASE +++ b/RELEASE @@ -1,22 +1,75 @@ -### Lots of new updates! +IPFS hash of the deployment: +- CIDv0: `QmXSmkbZBfMGwWiC7fzw8oDCxvmfGENb7ahoaZVWsstaTF` +- CIDv1: `bafybeiehjkqxfubc7qylb2q67pnqzri4j5kqxcllryecaq7nuqntzzjrfy` -### Bridging +The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org). -You can now swap your ETH, USDC, and more across 8+ networks! Try it by pressing the banner on your homepage. +You can also access the Uniswap Interface from an IPFS gateway. +**BEWARE**: The Uniswap interface uses [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) to remember your settings, such as which tokens you have imported. +**You should always use an IPFS gateway that enforces origin separation**, or our hosted deployment of the latest release at [app.uniswap.org](https://app.uniswap.org). +Your Uniswap settings are never remembered across different URLs. -### Faster Onboarding +IPFS gateways: +- https://bafybeiehjkqxfubc7qylb2q67pnqzri4j5kqxcllryecaq7nuqntzzjrfy.ipfs.dweb.link/ +- https://bafybeiehjkqxfubc7qylb2q67pnqzri4j5kqxcllryecaq7nuqntzzjrfy.ipfs.cf-ipfs.com/ +- [ipfs://QmXSmkbZBfMGwWiC7fzw8oDCxvmfGENb7ahoaZVWsstaTF/](ipfs://QmXSmkbZBfMGwWiC7fzw8oDCxvmfGENb7ahoaZVWsstaTF/) -New users can create a wallet lightning fast. +## 5.57.0 (2024-11-07) -### Multichain Explore -Users can now see all 12 chains we support on the Explore page, and can also filter by a specific chain. +### Features -### Worldchain +* **web:** [v4] add v4 data to explore graphs (#13174) 3d2b961 +* **web:** add hook functionality and update the UI (#13415) 4dfcd67 +* **web:** Add liq button on pools explore page (#13400) d116542 +* **web:** add loading states to all LP actions buttons (#13408) 9aa091b +* **web:** adding dynamic fee tier option to fee tier search modal (#13478) 7276ef7 +* **web:** animate fee tier dropdown (#13542) 13f32e2 +* **web:** design fixes on PosDP (#13543) 5f93dfc +* **web:** handle one-sided liq input in increase modal (#13370) 4fa66d4 +* **web:** redirect to positions list after creating a position (#13444) 160adc6 +* **web:** refetch position queries when pending LP txs change (#13483) 70ae19d +* **web:** remove thai from supported languages and translations (#13528) 36fe674 +* **web:** update explore table header styles (#13386) e272167 +* **web:** updating fee tiers search modal to include the default fee tiers (#13464) aa0bfcf +* **web:** use the new rest endpoint on the mini portfolio pools tab (#13521) 555de82 -Users now have access to all ur regular features for this new chain. -### Other changes +### Bug Fixes + +* **web:** Add error message for missing trade routes (#13327) 004265a +* **web:** add insuffient balance error state for the button (#13447) eb4ea2d +* **web:** allow testnets to be backend supported chains (#13424) b968f05 +* **web:** better default fee tier selection (#13526) 5976b4b +* **web:** change view position to view positions (#13511) bcee0e3 +* **web:** cypress tests (#13402) f78f58b +* **web:** fee tier fixes (#13240) 16ad294 +* **web:** filter to testnets when in testnet mode (#13356) b1a67be +* **web:** fix token sorting for v2 positions (#13504) 7c460ff +* **web:** fixing uniswap context provider for web (#13566) c7511a4 +* **web:** hide the collect button if there are no fees to collect (#13459) 9a98813 +* **web:** improve LiquidityChartRangeInput behavior when inverting tokens (#13550) 523eb00 +* **web:** landing page redirection when connected (#13425) 6b5993e +* **web:** position cards text sizing (#13307) e406dd1 +* **web:** remove confusing subgraph naming from v3 tick query (#13436) f1d7d14 +* **web:** Remove default mainnet chain ID in NetworkFilter (#13272) 4993213 +* **web:** stop using multicall for Position NFT data fetching (#13414) 5d9db63 +* **web:** udpate scroll behavior on explore page (#12277) bb14b4d +* **web:** update creating pool banner to be persistent (#13456) 94b7801 +* **web:** update loading/error states in LiquidityChartRangeInput (#13498) a5f6b0e +* **web:** update url when switching protocol versions in create (#13422) f3edc84 +* **web:** use DropdownSelector for position filters (#13544) dc63bdd +* **web:** use NATIVE in url to add liquidity (#13262) 48aa927 +* **web:** version dropdown ordering, clickable Sidebar in create flow (#13551) a86ee47 + + +### Continuous Integration + +* **web:** update sitemaps d3686aa + + +### Tests + +* **web:** cron job run web e2e tests (#13482) 8eda2da + -- Better redirect handling on fiat onramp -- Various bug fixes and performance improvements diff --git a/VERSION b/VERSION index 0940e3ab6bb..05a48a4b65a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -mobile/1.38 \ No newline at end of file +web/5.57.0 \ No newline at end of file diff --git a/apps/extension/package.json b/apps/extension/package.json index c4d26e5b391..147c594534d 100644 --- a/apps/extension/package.json +++ b/apps/extension/package.json @@ -15,7 +15,7 @@ "@tamagui/core": "1.108.4", "@types/uuid": "9.0.1", "@uniswap/analytics-events": "2.38.0", - "@uniswap/uniswapx-sdk": "^2.1.0-beta.14", + "@uniswap/uniswapx-sdk": "2.1.0-beta.18", "@uniswap/universal-router-sdk": "4.5.2", "@uniswap/v3-sdk": "3.18.1", "@uniswap/v4-sdk": "1.10.3", diff --git a/apps/extension/src/app/OnboardingApp.tsx b/apps/extension/src/app/OnboardingApp.tsx index 398de94fcef..3b46ad40ceb 100644 --- a/apps/extension/src/app/OnboardingApp.tsx +++ b/apps/extension/src/app/OnboardingApp.tsx @@ -39,6 +39,7 @@ import { initExtensionAnalytics } from 'src/app/utils/analytics' import { checksIfSupportsSidePanel } from 'src/app/utils/chrome' import { PrimaryAppInstanceDebuggerLazy } from 'src/store/PrimaryAppInstanceDebuggerLazy' import { getReduxPersistor, getReduxStore } from 'src/store/store' +import { BlankUrlProvider } from 'uniswap/src/contexts/UrlContext' import { LocalizationContextProvider } from 'uniswap/src/features/language/LocalizationContext' import Trace from 'uniswap/src/features/telemetry/Trace' import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants' @@ -192,12 +193,14 @@ export default function OnboardingApp(): JSX.Element { - - - - - - + + + + + + + + diff --git a/apps/extension/src/app/PopupApp.tsx b/apps/extension/src/app/PopupApp.tsx index dcccdb561d1..b4ababc497e 100644 --- a/apps/extension/src/app/PopupApp.tsx +++ b/apps/extension/src/app/PopupApp.tsx @@ -17,6 +17,7 @@ import { getReduxPersistor, getReduxStore } from 'src/store/store' import { Button, Flex, Image, Text } from 'ui/src' import { CHROME_LOGO, UNISWAP_LOGO } from 'ui/src/assets' import { iconSizes, spacing } from 'ui/src/theme' +import { BlankUrlProvider } from 'uniswap/src/contexts/UrlContext' import { LocalizationContextProvider } from 'uniswap/src/features/language/LocalizationContext' import { syncAppWithDeviceLanguage } from 'uniswap/src/features/settings/slice' import Trace from 'uniswap/src/features/telemetry/Trace' @@ -132,14 +133,16 @@ export default function PopupApp(): JSX.Element { - - - - - - - - + + + + + + + + + + diff --git a/apps/extension/src/app/SidebarApp.tsx b/apps/extension/src/app/SidebarApp.tsx index 8540e616c78..c70497567b1 100644 --- a/apps/extension/src/app/SidebarApp.tsx +++ b/apps/extension/src/app/SidebarApp.tsx @@ -39,6 +39,7 @@ import { import { BackgroundToSidePanelRequestType } from 'src/background/messagePassing/types/requests' import { PrimaryAppInstanceDebuggerLazy } from 'src/store/PrimaryAppInstanceDebuggerLazy' import { getReduxPersistor, getReduxStore } from 'src/store/store' +import { BlankUrlProvider } from 'uniswap/src/contexts/UrlContext' import { LocalizationContextProvider } from 'uniswap/src/features/language/LocalizationContext' import { syncAppWithDeviceLanguage } from 'uniswap/src/features/settings/slice' import Trace from 'uniswap/src/features/telemetry/Trace' @@ -262,15 +263,17 @@ export default function SidebarApp(): JSX.Element { - - - - - - - - - + + + + + + + + + + + diff --git a/apps/extension/src/app/StatsigProvider.tsx b/apps/extension/src/app/StatsigProvider.tsx index ec02dc83a04..ce4166a40a7 100644 --- a/apps/extension/src/app/StatsigProvider.tsx +++ b/apps/extension/src/app/StatsigProvider.tsx @@ -4,7 +4,6 @@ import { getStatsigEnvironmentTier } from 'src/app/version' import Statsig from 'statsig-js' // Use JS package for browser import { uniswapUrls } from 'uniswap/src/constants/urls' import { DUMMY_STATSIG_SDK_KEY, StatsigCustomAppValue } from 'uniswap/src/features/gating/constants' -import { FeatureFlags, getFeatureFlagName } from 'uniswap/src/features/gating/flags' import { StatsigOptions, StatsigProvider, StatsigUser } from 'uniswap/src/features/gating/sdk/statsig' import { getUniqueId } from 'utilities/src/device/getUniqueId' import { useAsyncData } from 'utilities/src/react/hooks' @@ -51,10 +50,7 @@ export function ExtensionStatsigProvider({ disableErrorLogging: true, initCompletionCallback: () => { setInitFinished(true) - const datadogEnabled = Statsig.checkGate(getFeatureFlagName(FeatureFlags.Datadog)) - if (datadogEnabled) { - initializeDatadog(appName).catch(() => undefined) - } + initializeDatadog(appName).catch(() => undefined) }, } diff --git a/apps/extension/src/app/components/Trace/TraceUserProperties.tsx b/apps/extension/src/app/components/Trace/TraceUserProperties.tsx index 99a70235839..6f22bc88469 100644 --- a/apps/extension/src/app/components/Trace/TraceUserProperties.tsx +++ b/apps/extension/src/app/components/Trace/TraceUserProperties.tsx @@ -1,12 +1,9 @@ import { useEffect } from 'react' import { useColorScheme } from 'react-native' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks' import { useAppFiatCurrencyInfo } from 'uniswap/src/features/fiatCurrency/hooks' import { useCurrentLanguage } from 'uniswap/src/features/language/hooks' -import { - useEnabledChains, - useHideSmallBalancesSetting, - useHideSpamTokensSetting, -} from 'uniswap/src/features/settings/hooks' +import { useHideSmallBalancesSetting, useHideSpamTokensSetting } from 'uniswap/src/features/settings/hooks' import { ExtensionUserPropertyName, setUserProperty } from 'uniswap/src/features/telemetry/user' // eslint-disable-next-line no-restricted-imports import { analytics } from 'utilities/src/telemetry/analytics/analytics' diff --git a/apps/extension/src/app/datadog.ts b/apps/extension/src/app/datadog.ts index 6abe6f281ec..235444a5f3f 100644 --- a/apps/extension/src/app/datadog.ts +++ b/apps/extension/src/app/datadog.ts @@ -31,6 +31,9 @@ export async function initializeDatadog(appName: string): Promise { // otherwise DataDog will ignore error events event.view.url = event.view.url.replace(/^chrome-extension:\/\/[a-z]{32}\//i, '') if (event.error && event.type === 'error') { + if (event.error.source === 'console') { + return false + } Object.defineProperty(event.error, 'stack', { value: event.error.stack?.replace(/chrome-extension:\/\/[a-z]{32}/gi, ''), writable: false, diff --git a/apps/extension/src/app/features/accounts/AccountItem.tsx b/apps/extension/src/app/features/accounts/AccountItem.tsx index 981f0bfc875..7535624a80e 100644 --- a/apps/extension/src/app/features/accounts/AccountItem.tsx +++ b/apps/extension/src/app/features/accounts/AccountItem.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next' import { useDispatch } from 'react-redux' import { EditLabelModal } from 'src/app/features/accounts/EditLabelModal' import { removeAllDappConnectionsForAccount } from 'src/app/features/dapp/actions' -import { ContextMenu, Flex, MenuContentItem, Text, TouchableArea } from 'ui/src' +import { Flex, Text, TouchableArea } from 'ui/src' import { CopySheets, Edit, Ellipsis, TrashFilled } from 'ui/src/components/icons' import { iconSizes } from 'ui/src/theme' import { WarningModal } from 'uniswap/src/components/modals/WarningModal/WarningModal' @@ -17,6 +17,8 @@ import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { setClipboard } from 'uniswap/src/utils/clipboard' import { NumberType } from 'utilities/src/format/types' import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' +import { ContextMenu } from 'wallet/src/components/menu/ContextMenu' +import { MenuContentItem } from 'wallet/src/components/menu/types' import { EditAccountAction, editAccountActions } from 'wallet/src/features/wallet/accounts/editAccountSaga' import { useActiveAccountWithThrow, useDisplayName, useSignerAccounts } from 'wallet/src/features/wallet/hooks' import { DisplayNameType } from 'wallet/src/features/wallet/types' diff --git a/apps/extension/src/app/features/accounts/AccountSwitcherScreen.tsx b/apps/extension/src/app/features/accounts/AccountSwitcherScreen.tsx index 5fdaea1c8dc..603dda4e9da 100644 --- a/apps/extension/src/app/features/accounts/AccountSwitcherScreen.tsx +++ b/apps/extension/src/app/features/accounts/AccountSwitcherScreen.tsx @@ -14,7 +14,7 @@ import { PopupName, openPopup } from 'src/app/features/popups/slice' import { AppRoutes, RemoveRecoveryPhraseRoutes, SettingsRoutes, UnitagClaimRoutes } from 'src/app/navigation/constants' import { navigate } from 'src/app/navigation/state' import { focusOrCreateUnitagTab } from 'src/app/navigation/utils' -import { Button, Flex, MenuContent, MenuContentItem, Popover, ScrollView, Text, useSporeColors } from 'ui/src' +import { Button, Flex, Popover, ScrollView, Text, useSporeColors } from 'ui/src' import { WalletFilled, X } from 'ui/src/components/icons' import { spacing } from 'ui/src/theme' import { WarningModal } from 'uniswap/src/components/modals/WarningModal/WarningModal' @@ -31,6 +31,8 @@ import { logger } from 'utilities/src/logger/logger' import { sleep } from 'utilities/src/time/timing' import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' import { PlusCircle } from 'wallet/src/components/icons/PlusCircle' +import { MenuContent } from 'wallet/src/components/menu/MenuContent' +import { MenuContentItem } from 'wallet/src/components/menu/types' import { useAccountList } from 'wallet/src/features/accounts/hooks' import { createOnboardingAccount } from 'wallet/src/features/onboarding/createOnboardingAccount' import { BackupType, SignerMnemonicAccount } from 'wallet/src/features/wallet/accounts/types' diff --git a/apps/extension/src/app/features/accounts/EditLabelModal.tsx b/apps/extension/src/app/features/accounts/EditLabelModal.tsx index 8b0cf5ab8c9..d08f6332a22 100644 --- a/apps/extension/src/app/features/accounts/EditLabelModal.tsx +++ b/apps/extension/src/app/features/accounts/EditLabelModal.tsx @@ -12,10 +12,10 @@ import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { ModalName } from 'uniswap/src/features/telemetry/constants' import { OnboardingCardLoggingName } from 'uniswap/src/features/telemetry/types' +import { UNITAG_SUFFIX_NO_LEADING_DOT } from 'uniswap/src/features/unitags/constants' import { shortenAddress } from 'utilities/src/addresses' import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon' import { CardType, IntroCard, IntroCardGraphicType } from 'wallet/src/components/introCards/IntroCard' -import { UNITAG_SUFFIX_NO_LEADING_DOT } from 'wallet/src/features/unitags/constants' import { useCanActiveAddressClaimUnitag } from 'wallet/src/features/unitags/hooks' import { EditAccountAction, editAccountActions } from 'wallet/src/features/wallet/accounts/editAccountSaga' import { useDisplayName } from 'wallet/src/features/wallet/hooks' @@ -37,7 +37,7 @@ export function EditLabelModal({ isOpen, address, onClose }: EditLabelModalProps const [inputText, setInputText] = useState(defaultText) const [isfocused, setIsFocused] = useState(false) - const { canClaimUnitag } = useCanActiveAddressClaimUnitag() + const { canClaimUnitag } = useCanActiveAddressClaimUnitag(address) const unitagsClaimEnabled = useFeatureFlag(FeatureFlags.ExtensionClaimUnitag) const onConfirm = useCallback(async () => { diff --git a/apps/extension/src/app/features/dapp/DappContext.tsx b/apps/extension/src/app/features/dapp/DappContext.tsx index a3c2e4af8c1..3e24d9babd3 100644 --- a/apps/extension/src/app/features/dapp/DappContext.tsx +++ b/apps/extension/src/app/features/dapp/DappContext.tsx @@ -7,7 +7,7 @@ import { isConnectedAccount } from 'src/app/features/dapp/utils' import { closePopup, PopupName } from 'src/app/features/popups/slice' import { backgroundToSidePanelMessageChannel } from 'src/background/messagePassing/messageChannels' import { BackgroundToSidePanelRequestType } from 'src/background/messagePassing/types/requests' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { extractBaseUrl } from 'utilities/src/format/urls' import { useActiveAccountAddress } from 'wallet/src/features/wallet/hooks' diff --git a/apps/extension/src/app/features/dapp/actions.ts b/apps/extension/src/app/features/dapp/actions.ts index 41b0f1c9e74..2f3875d7ca9 100644 --- a/apps/extension/src/app/features/dapp/actions.ts +++ b/apps/extension/src/app/features/dapp/actions.ts @@ -6,8 +6,8 @@ import { ExtensionToDappRequestType, UpdateConnectionRequest, } from 'src/background/messagePassing/types/requests' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { chainIdToHexadecimalString } from 'uniswap/src/features/chains/utils' -import { UniverseChainId } from 'uniswap/src/types/chains' import { Account } from 'wallet/src/features/wallet/accounts/types' import { getProviderSync } from 'wallet/src/features/wallet/context' diff --git a/apps/extension/src/app/features/dapp/changeChain.test.ts b/apps/extension/src/app/features/dapp/changeChain.test.ts index 2e8b0f5269b..c61423749aa 100644 --- a/apps/extension/src/app/features/dapp/changeChain.test.ts +++ b/apps/extension/src/app/features/dapp/changeChain.test.ts @@ -3,10 +3,10 @@ import { providerErrors, serializeError } from '@metamask/rpc-errors' import { changeChain } from 'src/app/features/dapp/changeChain' import { dappStore } from 'src/app/features/dapp/store' import { DappResponseType } from 'src/app/features/dappRequests/types/DappRequestTypes' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { chainIdToHexadecimalString } from 'uniswap/src/features/chains/utils' import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' -import { UniverseChainId } from 'uniswap/src/types/chains' // Mock dependencies jest.mock('@ethersproject/providers') diff --git a/apps/extension/src/app/features/dapp/changeChain.ts b/apps/extension/src/app/features/dapp/changeChain.ts index dd8b91a22fa..0543f61f121 100644 --- a/apps/extension/src/app/features/dapp/changeChain.ts +++ b/apps/extension/src/app/features/dapp/changeChain.ts @@ -6,10 +6,10 @@ import { DappResponseType, ErrorResponse, } from 'src/app/features/dappRequests/types/DappRequestTypes' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { chainIdToHexadecimalString } from 'uniswap/src/features/chains/utils' import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' -import { UniverseChainId } from 'uniswap/src/types/chains' export function changeChain({ activeConnectedAddress, diff --git a/apps/extension/src/app/features/dapp/hooks.test.ts b/apps/extension/src/app/features/dapp/hooks.test.ts index c1edbe5114c..966118ec94b 100644 --- a/apps/extension/src/app/features/dapp/hooks.test.ts +++ b/apps/extension/src/app/features/dapp/hooks.test.ts @@ -6,8 +6,8 @@ import { } from 'src/app/features/dapp/hooks' import { DappState, dappStore } from 'src/app/features/dapp/store' import { act, renderHook, waitFor } from 'src/test/test-utils' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { SAMPLE_SEED_ADDRESS_1, SAMPLE_SEED_ADDRESS_3 } from 'uniswap/src/test/fixtures' -import { UniverseChainId } from 'uniswap/src/types/chains' import { ACCOUNT, ACCOUNT2, ACCOUNT3 } from 'wallet/src/test/fixtures' const SAMPLE_DAPP = 'http://example.com' diff --git a/apps/extension/src/app/features/dapp/hooks.ts b/apps/extension/src/app/features/dapp/hooks.ts index 562b2736e10..d36bc229d7a 100644 --- a/apps/extension/src/app/features/dapp/hooks.ts +++ b/apps/extension/src/app/features/dapp/hooks.ts @@ -1,6 +1,6 @@ import { useEffect, useReducer, useState } from 'react' import { DappInfo, DappStoreEvent, dappStore } from 'src/app/features/dapp/store' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { Account } from 'wallet/src/features/wallet/accounts/types' import { useActiveAccountAddress } from 'wallet/src/features/wallet/hooks' diff --git a/apps/extension/src/app/features/dapp/store.ts b/apps/extension/src/app/features/dapp/store.ts index a325c894b6a..289d0561f99 100644 --- a/apps/extension/src/app/features/dapp/store.ts +++ b/apps/extension/src/app/features/dapp/store.ts @@ -1,7 +1,7 @@ import { cloneDeep } from '@apollo/client/utilities' import EventEmitter from 'eventemitter3' import { getOrderedConnectedAddresses, isConnectedAccount } from 'src/app/features/dapp/utils' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { Account } from 'wallet/src/features/wallet/accounts/types' const STATE_STORAGE_KEY = 'dappState' diff --git a/apps/extension/src/app/features/dappRequests/DappRequestContent.tsx b/apps/extension/src/app/features/dappRequests/DappRequestContent.tsx index 472574a11d4..cb77db4f247 100644 --- a/apps/extension/src/app/features/dappRequests/DappRequestContent.tsx +++ b/apps/extension/src/app/features/dappRequests/DappRequestContent.tsx @@ -6,12 +6,12 @@ import { DappRequestStoreItem } from 'src/app/features/dappRequests/slice' import { DappRequestType } from 'src/app/features/dappRequests/types/DappRequestTypes' import { Anchor, AnimatePresence, Button, Flex, Text, UniversalImage, UniversalImageResizeMode, styled } from 'ui/src' import { borderRadii, iconSizes } from 'ui/src/theme' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { GasFeeResult } from 'uniswap/src/features/gas/types' import { hasSufficientFundsIncludingGas } from 'uniswap/src/features/gas/utils' import { useOnChainNativeCurrencyBalance } from 'uniswap/src/features/portfolio/api' -import { useEnabledChains } from 'uniswap/src/features/settings/hooks' import { TransactionTypeInfo } from 'uniswap/src/features/transactions/types/transactionDetails' -import { UniverseChainId } from 'uniswap/src/types/chains' import { extractNameFromUrl } from 'utilities/src/format/extractNameFromUrl' import { formatDappURL } from 'utilities/src/format/urls' import { logger } from 'utilities/src/logger/logger' diff --git a/apps/extension/src/app/features/dappRequests/accounts.ts b/apps/extension/src/app/features/dappRequests/accounts.ts index 890bcefdea4..167ca0a386c 100644 --- a/apps/extension/src/app/features/dappRequests/accounts.ts +++ b/apps/extension/src/app/features/dappRequests/accounts.ts @@ -15,13 +15,13 @@ import { } from 'src/app/features/dappRequests/types/DappRequestTypes' import { dappResponseMessageChannel } from 'src/background/messagePassing/messageChannels' import { call, put, select } from 'typed-redux-saga' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { chainIdToHexadecimalString } from 'uniswap/src/features/chains/utils' import { pushNotification } from 'uniswap/src/features/notifications/slice' import { AppNotificationType } from 'uniswap/src/features/notifications/types' import { getEnabledChainIdsSaga } from 'uniswap/src/features/settings/saga' import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' -import { UniverseChainId } from 'uniswap/src/types/chains' import { extractBaseUrl } from 'utilities/src/format/urls' import { getProvider } from 'wallet/src/features/wallet/context' import { selectActiveAccount } from 'wallet/src/features/wallet/selectors' diff --git a/apps/extension/src/app/features/dappRequests/getChainId.ts b/apps/extension/src/app/features/dappRequests/getChainId.ts index 066f8210125..ec773de71cb 100644 --- a/apps/extension/src/app/features/dappRequests/getChainId.ts +++ b/apps/extension/src/app/features/dappRequests/getChainId.ts @@ -7,8 +7,8 @@ import { } from 'src/app/features/dappRequests/types/DappRequestTypes' import { dappResponseMessageChannel } from 'src/background/messagePassing/messageChannels' import { call } from 'typed-redux-saga' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { chainIdToHexadecimalString } from 'uniswap/src/features/chains/utils' -import { UniverseChainId } from 'uniswap/src/types/chains' // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export function* getChainId(request: GetChainIdRequest, { id }: SenderTabInfo, dappInfo: DappInfo) { diff --git a/apps/extension/src/app/features/dappRequests/requestContent/EthSend/FallbackEthSend/FallbackEthSend.tsx b/apps/extension/src/app/features/dappRequests/requestContent/EthSend/FallbackEthSend/FallbackEthSend.tsx index 3470b2ad851..d713cd3fab9 100644 --- a/apps/extension/src/app/features/dappRequests/requestContent/EthSend/FallbackEthSend/FallbackEthSend.tsx +++ b/apps/extension/src/app/features/dappRequests/requestContent/EthSend/FallbackEthSend/FallbackEthSend.tsx @@ -1,9 +1,9 @@ -import { BigNumber } from 'ethers' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { useDappLastChainId } from 'src/app/features/dapp/hooks' import { DappRequestContent } from 'src/app/features/dappRequests/DappRequestContent' import { useDappRequestQueueContext } from 'src/app/features/dappRequests/DappRequestQueueContext' +import { isNonZeroBigNumber } from 'src/app/features/dappRequests/requestContent/EthSend/Swap/utils' import { SendTransactionRequest } from 'src/app/features/dappRequests/types/DappRequestTypes' import { useCopyToClipboard } from 'src/app/hooks/useOnCopyToClipboard' import { Anchor, Flex, Text, TouchableArea } from 'ui/src' @@ -55,6 +55,7 @@ export function FallbackEthSendRequestContent({ ) const { parsedTransactionData } = useNoYoloParser(dappRequest.transaction, chainId) const transactionCurrencies = useTransactionCurrencies({ chainId, to: toAddress, parsedTransactionData }) + const showSpendingEthDetails = isNonZeroBigNumber(sending) && chainId return ( - {sending && !BigNumber.from(sending).eq(0) && chainId && ( - - )} + {showSpendingEthDetails && } {transactionCurrencies?.map((currencyInfo, i) => ( + typeof obj === 'object' && !!obj && 'hex' in obj && typeof (obj as BigNumberParam).hex === 'string' + +// We have to type this as unknown because BigNumberSchema is any (as defined in apps/extension/src/app/features/dappRequests/types/EthersTypes.ts) +function isZeroBigNumberParam(bigNumberObj: unknown): boolean { + // We treat an undefined or badly formatted param as zero + if (!bigNumberObj || !isBigNumberParam(bigNumberObj)) { + return true + } + + return !isNonZeroBigNumber(bigNumberObj.hex) +} diff --git a/apps/extension/src/app/features/dappRequests/requestContent/WrapContent.tsx b/apps/extension/src/app/features/dappRequests/requestContent/WrapContent.tsx index 957eee3531f..addd049e60f 100644 --- a/apps/extension/src/app/features/dappRequests/requestContent/WrapContent.tsx +++ b/apps/extension/src/app/features/dappRequests/requestContent/WrapContent.tsx @@ -4,8 +4,8 @@ import { useDappLastChainId } from 'src/app/features/dapp/hooks' import { DappRequestStoreItem } from 'src/app/features/dappRequests/slice' import { SendTransactionRequest } from 'src/app/features/dappRequests/types/DappRequestTypes' import { Flex, Text } from 'ui/src' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks' import { useGasFeeFormattedAmounts, useTransactionGasFee } from 'uniswap/src/features/gas/hooks' -import { useEnabledChains } from 'uniswap/src/features/settings/hooks' import { useActiveAccountAddressWithThrow, useDisplayName } from 'wallet/src/features/wallet/hooks' export const WrapTransactionDetails = ({ diff --git a/apps/extension/src/app/features/for/utils.ts b/apps/extension/src/app/features/for/utils.ts index a86e419c81f..bc4dcf64588 100644 --- a/apps/extension/src/app/features/for/utils.ts +++ b/apps/extension/src/app/features/for/utils.ts @@ -2,11 +2,11 @@ import { SharedEventName } from '@uniswap/analytics-events' import { useDappContext } from 'src/app/features/dapp/DappContext' import { useDappLastChainId } from 'src/app/features/dapp/hooks' import { focusOrCreateUniswapInterfaceTab } from 'src/app/navigation/utils' -import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' import { uniswapUrls } from 'uniswap/src/constants/urls' +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { ElementNameType } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' -import { UniverseChainId } from 'uniswap/src/types/chains' import { ExtensionScreens } from 'uniswap/src/types/screens/extension' import { logger } from 'utilities/src/logger/logger' @@ -24,7 +24,7 @@ export function useInterfaceBuyNavigator(element?: ElementNameType): () => void } export function navigateToInterfaceFiatOnRamp(chainId?: UniverseChainId): void { - const chainParam = chainId ? `?chain=${UNIVERSE_CHAIN_INFO[chainId].urlParam}` : '' + const chainParam = chainId ? `?chain=${getChainInfo(chainId).urlParam}` : '' focusOrCreateUniswapInterfaceTab({ url: `${uniswapUrls.webInterfaceBuyUrl}${chainParam}`, }).catch((err) => diff --git a/apps/extension/src/app/features/home/PortfolioActionButtons.tsx b/apps/extension/src/app/features/home/PortfolioActionButtons.tsx index f2ad5a07d6f..a5eec5fe7dc 100644 --- a/apps/extension/src/app/features/home/PortfolioActionButtons.tsx +++ b/apps/extension/src/app/features/home/PortfolioActionButtons.tsx @@ -6,7 +6,7 @@ import { AppRoutes } from 'src/app/navigation/constants' import { navigate } from 'src/app/navigation/state' import { Flex, Text, getTokenValue, useMedia } from 'ui/src' import { ArrowDownCircle, Buy, CoinConvert, SendAction } from 'ui/src/components/icons' -import { useEnabledChains } from 'uniswap/src/features/settings/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks' import { ElementName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { TestnetModeModal } from 'uniswap/src/features/testnets/TestnetModeModal' diff --git a/apps/extension/src/app/features/home/PortfolioHeader.tsx b/apps/extension/src/app/features/home/PortfolioHeader.tsx index d0046ac1f52..00aa8e8b346 100644 --- a/apps/extension/src/app/features/home/PortfolioHeader.tsx +++ b/apps/extension/src/app/features/home/PortfolioHeader.tsx @@ -15,12 +15,12 @@ import { animationPresets } from 'ui/src/animations' import { CopyAlt, Globe, RotatableChevron, Settings } from 'ui/src/components/icons' import { borderRadii, iconSizes } from 'ui/src/theme' import { useAvatar } from 'uniswap/src/features/address/avatar' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { pushNotification } from 'uniswap/src/features/notifications/slice' import { AppNotificationType, CopyNotificationType } from 'uniswap/src/features/notifications/types' import { ElementName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { TestID } from 'uniswap/src/test/fixtures/testIDs' -import { UniverseChainId } from 'uniswap/src/types/chains' import { ExtensionScreens } from 'uniswap/src/types/screens/extension' import { sanitizeAddressText, shortenAddress } from 'uniswap/src/utils/addresses' import { setClipboard } from 'uniswap/src/utils/clipboard' diff --git a/apps/extension/src/app/features/home/SwitchNetworksModal.tsx b/apps/extension/src/app/features/home/SwitchNetworksModal.tsx index ab3d2c17890..22893cf2cd8 100644 --- a/apps/extension/src/app/features/home/SwitchNetworksModal.tsx +++ b/apps/extension/src/app/features/home/SwitchNetworksModal.tsx @@ -9,13 +9,13 @@ import { Check, Power } from 'ui/src/components/icons' import { usePreventOverflowBelowFold } from 'ui/src/hooks/usePreventOverflowBelowFold' import { iconSizes } from 'ui/src/theme' import { NetworkLogo } from 'uniswap/src/components/CurrencyLogo/NetworkLogo' -import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { getChainLabel } from 'uniswap/src/features/chains/utils' import { pushNotification } from 'uniswap/src/features/notifications/slice' import { AppNotificationType } from 'uniswap/src/features/notifications/types' -import { useEnabledChains } from 'uniswap/src/features/settings/hooks' import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' -import { UniverseChainId } from 'uniswap/src/types/chains' import { extractUrlHost } from 'utilities/src/format/urls' import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks' @@ -89,7 +89,7 @@ export function SwitchNetworksModal(): JSX.Element { - {UNIVERSE_CHAIN_INFO[chain]?.label} + {getChainLabel(chain)} {activeChain === chain ? ( diff --git a/apps/extension/src/app/features/home/TokenBalanceList.tsx b/apps/extension/src/app/features/home/TokenBalanceList.tsx index 43f27745c3e..f6925d0b4eb 100644 --- a/apps/extension/src/app/features/home/TokenBalanceList.tsx +++ b/apps/extension/src/app/features/home/TokenBalanceList.tsx @@ -4,17 +4,18 @@ import { useTranslation } from 'react-i18next' import { useInterfaceBuyNavigator } from 'src/app/features/for/utils' import { AppRoutes } from 'src/app/navigation/constants' import { navigate } from 'src/app/navigation/state' -import { AnimatePresence, ContextMenu, Flex, Loader } from 'ui/src' +import { AnimatePresence, Flex, Loader } from 'ui/src' import { ShieldCheck } from 'ui/src/components/icons' import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard' import { InfoLinkModal } from 'uniswap/src/components/modals/InfoLinkModal' import { uniswapUrls } from 'uniswap/src/constants/urls' import { SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks' import { PortfolioBalance } from 'uniswap/src/features/dataApi/types' -import { useEnabledChains } from 'uniswap/src/features/settings/hooks' import { ElementName, ModalName, SectionName, WalletEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { InformationBanner } from 'wallet/src/components/banners/InformationBanner' +import { ContextMenu } from 'wallet/src/components/menu/ContextMenu' import { useWalletNavigation } from 'wallet/src/contexts/WalletNavigationContext' import { isNonPollingRequestInFlight } from 'wallet/src/data/utils' import { HiddenTokensRow } from 'wallet/src/features/portfolio/HiddenTokensRow' @@ -211,7 +212,12 @@ const TokenBalanceItemRow = memo(function TokenBalanceItemRow({ item }: { item: return ( - + ) }) diff --git a/apps/extension/src/app/features/lockScreen/Locked.tsx b/apps/extension/src/app/features/lockScreen/Locked.tsx index 810a6fcdc77..976c57b5fe2 100644 --- a/apps/extension/src/app/features/lockScreen/Locked.tsx +++ b/apps/extension/src/app/features/lockScreen/Locked.tsx @@ -89,7 +89,7 @@ export function Locked(): JSX.Element { // The standard onboarding open logic triggers but doesn't update the path because the generic one doesn't have a path specified. await openRecoveryTab() - await dispatch( + dispatch( editAccountActions.trigger({ type: EditAccountAction.Remove, accounts: associatedAccounts, diff --git a/apps/extension/src/app/features/onboarding/ClaimUnitagScreen.tsx b/apps/extension/src/app/features/onboarding/ClaimUnitagScreen.tsx index 18c19d99e23..b11f133f81b 100644 --- a/apps/extension/src/app/features/onboarding/ClaimUnitagScreen.tsx +++ b/apps/extension/src/app/features/onboarding/ClaimUnitagScreen.tsx @@ -50,7 +50,7 @@ export function ClaimUnitagScreen(): JSX.Element { onBack={handleBack} onSkip={goToNextStep} > - + { setSelectedRecipient(newRecipient) diff --git a/apps/extension/src/app/features/send/SendFormScreen/SendFormScreen.tsx b/apps/extension/src/app/features/send/SendFormScreen/SendFormScreen.tsx index b05f5f768d8..712190ce426 100644 --- a/apps/extension/src/app/features/send/SendFormScreen/SendFormScreen.tsx +++ b/apps/extension/src/app/features/send/SendFormScreen/SendFormScreen.tsx @@ -3,6 +3,7 @@ import { RecipientPanel } from 'src/app/features/send/SendFormScreen/RecipientPa import { ReviewButton } from 'src/app/features/send/SendFormScreen/ReviewButton' import { Flex, Separator, useSporeColors } from 'ui/src' import { Modal } from 'uniswap/src/components/modals/Modal' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import Trace from 'uniswap/src/features/telemetry/Trace' import { ModalName, SectionName } from 'uniswap/src/features/telemetry/constants' import { InsufficientNativeTokenWarning } from 'uniswap/src/features/transactions/InsufficientNativeTokenWarning/InsufficientNativeTokenWarning' @@ -14,7 +15,6 @@ import { useUSDTokenUpdater } from 'uniswap/src/features/transactions/hooks/useU import { BlockedAddressWarning } from 'uniswap/src/features/transactions/modals/BlockedAddressWarning' import { useUSDCValue } from 'uniswap/src/features/transactions/swap/hooks/useUSDCPrice' import { useIsBlocked } from 'uniswap/src/features/trm/hooks' -import { UniverseChainId } from 'uniswap/src/types/chains' import { CurrencyField } from 'uniswap/src/types/currency' import { createTransactionId } from 'uniswap/src/utils/createTransactionId' import { useSendContext } from 'wallet/src/features/transactions/contexts/SendContext' diff --git a/apps/extension/src/app/features/settings/SettingsManageConnectionsScreen/SettingsManageConnectionsScreen.tsx b/apps/extension/src/app/features/settings/SettingsManageConnectionsScreen/SettingsManageConnectionsScreen.tsx index 3b59c486275..5d5c03863f5 100644 --- a/apps/extension/src/app/features/settings/SettingsManageConnectionsScreen/SettingsManageConnectionsScreen.tsx +++ b/apps/extension/src/app/features/settings/SettingsManageConnectionsScreen/SettingsManageConnectionsScreen.tsx @@ -2,10 +2,9 @@ import { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useDispatch } from 'react-redux' import { ScreenHeader } from 'src/app/components/layout/ScreenHeader' -import { removeDappConnection } from 'src/app/features/dapp/actions' +import { removeAllDappConnectionsForAccount, removeDappConnection } from 'src/app/features/dapp/actions' import { useAllDappConnectionsForActiveAccount } from 'src/app/features/dapp/hooks' import { dappStore } from 'src/app/features/dapp/store' -import { EllipsisDropdown } from 'src/app/features/settings/SettingsManageConnectionsScreen/internal/EllipsisDropdown' import { NoDappConnections } from 'src/app/features/settings/SettingsManageConnectionsScreen/internal/NoDappConnections' import { Flex, Text, TouchableArea, UniversalImage, useSporeColors } from 'ui/src' import { MinusCircle } from 'ui/src/components/icons' @@ -19,6 +18,7 @@ import { ExtensionScreens } from 'uniswap/src/types/screens/extension' import { extractNameFromUrl } from 'utilities/src/format/extractNameFromUrl' import { extractUrlHost } from 'utilities/src/format/urls' import { DappIconPlaceholder } from 'wallet/src/components/WalletConnect/DappIconPlaceholder' +import { DappEllipsisDropdown } from 'wallet/src/components/settings/DappEllipsisDropdown/DappEllipsisDropdown' import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks' const MIN_SCREEN_WIDTH = breakpoints.xxs @@ -112,7 +112,11 @@ export function SettingsManageConnectionsScreen(): JSX.Element { return ( : undefined} + rightColumn={ + hasConnections ? ( + + ) : undefined + } title={t('settings.setting.wallet.connections.title')} /> diff --git a/apps/extension/src/app/features/settings/SettingsRecoveryPhraseScreen/SettingsRecoveryPhrase.tsx b/apps/extension/src/app/features/settings/SettingsRecoveryPhraseScreen/SettingsRecoveryPhrase.tsx index 654fb831a27..f5f0f169599 100644 --- a/apps/extension/src/app/features/settings/SettingsRecoveryPhraseScreen/SettingsRecoveryPhrase.tsx +++ b/apps/extension/src/app/features/settings/SettingsRecoveryPhraseScreen/SettingsRecoveryPhrase.tsx @@ -37,7 +37,7 @@ export function SettingsRecoveryPhrase({ {children} - + + ) +} diff --git a/apps/web/src/components/ChainConnectivityWarning.tsx b/apps/web/src/components/ChainConnectivityWarning.tsx index 9e5460cb77f..a6531da5ac9 100644 --- a/apps/web/src/components/ChainConnectivityWarning.tsx +++ b/apps/web/src/components/ChainConnectivityWarning.tsx @@ -1,4 +1,3 @@ -import { AVERAGE_L1_BLOCK_TIME, getChain } from 'constants/chains' import { useAccount } from 'hooks/useAccount' import useCurrentBlockTimestamp from 'hooks/useCurrentBlockTimestamp' import { useIsLandingPage } from 'hooks/useIsLandingPage' @@ -8,9 +7,11 @@ import styled from 'lib/styled-components' import { useMemo } from 'react' import { AlertTriangle } from 'react-feather' import { ExternalLink } from 'theme/components' -import { DEFAULT_MS_BEFORE_WARNING, UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' +import { DEFAULT_MS_BEFORE_WARNING, getChainInfo } from 'uniswap/src/features/chains/chainInfo' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { AVERAGE_L1_BLOCK_TIME_MS } from 'uniswap/src/features/transactions/swap/hooks/usePollingIntervalByChain' import { Trans } from 'uniswap/src/i18n' -import { UniverseChainId } from 'uniswap/src/types/chains' const BodyRow = styled.div` color: ${({ theme }) => theme.neutral1}; @@ -53,22 +54,22 @@ const Wrapper = styled.div` export function ChainConnectivityWarning() { const { chainId } = useAccount() - const info = getChain({ chainId, withFallback: true }) + const { defaultChainId } = useEnabledChains() + const info = getChainInfo(chainId ?? defaultChainId) const label = info.label const isNftPage = useIsNftPage() const isLandingPage = useIsLandingPage() const waitMsBeforeWarning = useMemo( - () => (chainId ? UNIVERSE_CHAIN_INFO[chainId]?.blockWaitMsBeforeWarning : undefined) ?? DEFAULT_MS_BEFORE_WARNING, + () => (chainId ? getChainInfo(chainId)?.blockWaitMsBeforeWarning : undefined) ?? DEFAULT_MS_BEFORE_WARNING, [chainId], ) - const machineTime = useMachineTimeMs(AVERAGE_L1_BLOCK_TIME) + const machineTime = useMachineTimeMs(AVERAGE_L1_BLOCK_TIME_MS) const blockTime = useCurrentBlockTimestamp( useMemo( () => ({ - blocksPerFetch: - /* 5m / 12s = */ 25 * (chainId ? UNIVERSE_CHAIN_INFO[chainId].blockPerMainnetEpochForChainId : 1), + blocksPerFetch: /* 5m / 12s = */ 25 * (chainId ? getChainInfo(chainId).blockPerMainnetEpochForChainId : 1), }), [chainId], ), diff --git a/apps/web/src/components/Charts/ChartHeader.tsx b/apps/web/src/components/Charts/ChartHeader.tsx index 3206ace161e..6901215a52e 100644 --- a/apps/web/src/components/Charts/ChartHeader.tsx +++ b/apps/web/src/components/Charts/ChartHeader.tsx @@ -8,8 +8,6 @@ import { EllipsisTamaguiStyle } from 'theme/components' import { ThemedText } from 'theme/components/text' import { Flex, Text, styled } from 'ui/src' import { PriceSource } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { NumberType, useFormatter } from 'utils/formatNumbers' export type ChartHeaderProtocolInfo = { protocol: PriceSource; value?: number } @@ -22,7 +20,7 @@ const ProtocolLegendWrapper = styled(Flex, { gap: '$gap12', pointerEvents: 'none', variants: { - isMultichainExploreEnabled: { + hover: { true: { right: 'unset', p: '$spacing8', @@ -41,49 +39,28 @@ const ProtocolLegendWrapper = styled(Flex, { function ProtocolLegend({ protocolData }: { protocolData?: ChartHeaderProtocolInfo[] }) { const { formatFiatPrice } = useFormatter() const theme = useTheme() - const isMultichainExploreEnabled = useFeatureFlag(FeatureFlags.MultichainExplore) return ( - + {protocolData ?.map(({ value, protocol }) => { - const display = value - ? formatFiatPrice({ price: value, type: NumberType.ChartFiatValue }) - : isMultichainExploreEnabled - ? null - : getProtocolName(protocol) + const display = value ? formatFiatPrice({ price: value, type: NumberType.ChartFiatValue }) : null return ( !!display && ( - - {isMultichainExploreEnabled ? ( - - {getProtocolName(protocol)} - - ) : ( - - {display} - - )} + + + {getProtocolName(protocol)} + + - {isMultichainExploreEnabled && ( - - {display} - - )} + + {display} + ) ) @@ -141,7 +118,6 @@ export function ChartHeader({ additionalFields, }: ChartHeaderProps) { const isHovered = !!time - const isMultichainExploreEnabled = useFeatureFlag(FeatureFlags.MultichainExplore) return ( - {((isHovered && protocolData) || !isMultichainExploreEnabled) && } + {isHovered && protocolData && } ) } diff --git a/apps/web/src/components/Charts/ChartModel.tsx b/apps/web/src/components/Charts/ChartModel.tsx index bfa95bf8cda..d4d4f2fe762 100644 --- a/apps/web/src/components/Charts/ChartModel.tsx +++ b/apps/web/src/components/Charts/ChartModel.tsx @@ -2,7 +2,6 @@ import { PROTOCOL_LEGEND_ELEMENT_ID, SeriesDataItemType } from 'components/Chart import { formatTickMarks } from 'components/Charts/utils' import { MissingDataBars } from 'components/Table/icons' import { useScreenSize } from 'hooks/screenSize/useScreenSize' -import { useActiveLocale } from 'hooks/useActiveLocale' import { useOnClickOutside } from 'hooks/useOnClickOutside' import { atom } from 'jotai' import { useUpdateAtom } from 'jotai/utils' @@ -21,6 +20,7 @@ import { import { ReactElement, useEffect, useMemo, useRef, useState } from 'react' import { ThemedText } from 'theme/components' import { Flex, TamaguiElement, assertWebElement, styled } from 'ui/src' +import { useCurrentLocale } from 'uniswap/src/features/language/hooks' import { Trans } from 'uniswap/src/i18n' import { useFormatter } from 'utils/formatNumbers' import { v4 as uuidv4 } from 'uuid' @@ -265,7 +265,7 @@ export function Chart, TDataType e const [crosshairData, setCrosshairData] = useState(undefined) const format = useFormatter() const theme = useTheme() - const locale = useActiveLocale() + const locale = useCurrentLocale() const { md: isLargeScreen } = useScreenSize() const modelParams = useMemo( () => ({ ...params, format, theme, locale, isLargeScreen, onCrosshairMove: setCrosshairData }), diff --git a/apps/web/src/components/Charts/LiquidityChart/index.tsx b/apps/web/src/components/Charts/LiquidityChart/index.tsx index 8ee96b70ed9..967027e04b8 100644 --- a/apps/web/src/components/Charts/LiquidityChart/index.tsx +++ b/apps/web/src/components/Charts/LiquidityChart/index.tsx @@ -12,7 +12,7 @@ import { TickProcessed, usePoolActiveLiquidity } from 'hooks/usePoolTickData' import JSBI from 'jsbi' import { ISeriesApi, UTCTimestamp } from 'lightweight-charts' import { useEffect, useState } from 'react' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { NumberType, useFormatter } from 'utils/formatNumbers' interface LiquidityBarChartModelParams extends ChartModelParams, LiquidityBarProps {} diff --git a/apps/web/src/components/Charts/SparklineChart/index.tsx b/apps/web/src/components/Charts/SparklineChart/index.tsx index b12f22e4c54..15554814d55 100644 --- a/apps/web/src/components/Charts/SparklineChart/index.tsx +++ b/apps/web/src/components/Charts/SparklineChart/index.tsx @@ -1,16 +1,16 @@ import { getPriceBounds } from 'components/Charts/PriceChart/utils' import LineChart from 'components/Charts/SparklineChart/LineChart' import { LoadingBubble } from 'components/Tokens/loading' -import { getChainFromChainUrlParam } from 'constants/chains' import { NATIVE_CHAIN_ID } from 'constants/tokens' import { curveCardinal, scaleLinear } from 'd3' -import { SparklineMap, TopToken } from 'graphql/data/TopTokens' +import { SparklineMap, TopToken } from 'graphql/data/types' import { PricePoint } from 'graphql/data/util' import styled, { useTheme } from 'lib/styled-components' import { memo } from 'react' import { TokenStat } from 'state/explore/types' -import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' import { addressesAreEquivalent } from 'utils/addressesAreEquivalent' +import { getChainIdFromChainUrlParam } from 'utils/chainParams' const LoadingContainer = styled.div` height: 100%; @@ -38,8 +38,8 @@ interface SparklineChartProps { function _SparklineChart({ width, height, tokenData, pricePercentChange, sparklineMap }: SparklineChartProps) { const theme = useTheme() // for sparkline - const chainId = getChainFromChainUrlParam(tokenData?.chain.toLowerCase())?.id - const chainInfo = chainId && UNIVERSE_CHAIN_INFO[chainId] + const chainId = getChainIdFromChainUrlParam(tokenData?.chain.toLowerCase()) + const chainInfo = chainId && getChainInfo(chainId) const isNative = addressesAreEquivalent(tokenData?.address, chainInfo?.wrappedNativeCurrency.address) const pricePoints = tokenData?.address ? sparklineMap[isNative ? NATIVE_CHAIN_ID : tokenData.address.toLowerCase()] diff --git a/apps/web/src/components/Charts/StackedLineChart/stacked-area-series/renderer.ts b/apps/web/src/components/Charts/StackedLineChart/stacked-area-series/renderer.ts index beca9f26631..ef270c9168c 100644 --- a/apps/web/src/components/Charts/StackedLineChart/stacked-area-series/renderer.ts +++ b/apps/web/src/components/Charts/StackedLineChart/stacked-area-series/renderer.ts @@ -72,29 +72,28 @@ export class StackedAreaSeriesRenderer implements } }) const zeroY = priceToCoordinate(0) ?? 0 + const colorsCount = options.colors.length + const isV4EverywhereEnabled = options.colors.length === 3 const { linesMeshed, hoverInfo } = this._createLinePaths( bars, this._data.visibleRange, renderingScope, zeroY * renderingScope.verticalPixelRatio, options.hoveredLogicalIndex, + isV4EverywhereEnabled, ) - const fullLinesMeshed = linesMeshed.slice(0, 3) - const highlightLinesMeshed = options.hoveredLogicalIndex ? linesMeshed.slice(3) : [] + const fullLinesMeshed = linesMeshed.slice(0, colorsCount + 1) + const highlightLinesMeshed = options.hoveredLogicalIndex ? linesMeshed.slice(colorsCount + 1) : [] const areaPaths = this._createAreas(fullLinesMeshed) - const colorsCount = options.colors.length const isHovered = options.hoveredLogicalIndex && options.hoveredLogicalIndex !== -1 - const isMultichainExploreEnabled = !!options.gradients areaPaths.forEach((areaPath, index) => { // Modification: determine area fill opacity based on number of lines and hover state if (areaPaths.length === 1) { ctx.globalAlpha = 0.12 // single-line charts have low opacity fill - } else if (!isMultichainExploreEnabled) { - ctx.globalAlpha = isHovered ? 0.24 : 1 } const gradient = options.gradients @@ -109,51 +108,47 @@ export class StackedAreaSeriesRenderer implements ctx.fill(areaPath) }) - ctx.lineWidth = options.lineWidth * (isMultichainExploreEnabled ? 1 : renderingScope.verticalPixelRatio) + ctx.lineWidth = options.lineWidth ctx.lineJoin = 'round' fullLinesMeshed.toReversed().forEach((linePath, index) => { - const unreversedIndex = fullLinesMeshed.length - index - const color = options.colors[unreversedIndex % colorsCount] + const color = options.colors[colorsCount - (index + 1)] ctx.strokeStyle = color ctx.fillStyle = color - ctx.globalAlpha = isHovered && isMultichainExploreEnabled ? 0.24 : 1 + ctx.globalAlpha = isHovered ? 0.24 : 1 // Bottom line is just the x-axis, which should not be drawn if (index !== fullLinesMeshed.length - 1) { // Line rendering: ctx.beginPath() - ctx.strokeStyle = color ctx.stroke(linePath.path) - } - - // Modification: Draws a glyph where lines intersect with the crosshair - const hoverY = hoverInfo.points[index - 1] - // Reset the global alpha to 1 after filling in the area under the graph and before drawing the glyph - ctx.globalAlpha = 1 - - // Glyph rendering: - ctx.globalCompositeOperation = 'destination-out' // This mode allows removing a portion of the drawn line from the canvas - ctx.beginPath() - ctx.arc(hoverInfo.x, hoverY, 5 * renderingScope.verticalPixelRatio, 0, 2 * Math.PI) - ctx.fill() // Cuts a hole out of the line where part of the glyph should be rendered + // Modification: Draws a glyph where lines intersect with the crosshair + const hoverY = hoverInfo.points.toReversed()[index] + // Reset the global alpha to 1 after filling in the area under the graph and before drawing the glyph + ctx.globalAlpha = 1 - ctx.globalCompositeOperation = 'source-over' // Resets to default mode + // Glyph rendering: + ctx.globalCompositeOperation = 'destination-out' // This mode allows removing a portion of the drawn line from the canvas + ctx.beginPath() + ctx.arc(hoverInfo.x, hoverY, 5 * renderingScope.verticalPixelRatio, 0, 2 * Math.PI) + ctx.fill() // Cuts a hole out of the line where part of the glyph should be rendered - ctx.beginPath() + ctx.globalCompositeOperation = 'source-over' // Resets to default mode + ctx.beginPath() - ctx.arc(hoverInfo.x, hoverY, 3 * renderingScope.verticalPixelRatio, 0, 2 * Math.PI) - ctx.fill() // Draws innermost portion of glyph + ctx.arc(hoverInfo.x, hoverY, 3 * renderingScope.verticalPixelRatio, 0, 2 * Math.PI) + ctx.fill() // Draws innermost portion of glyph - ctx.globalAlpha = 0.2 - ctx.beginPath() - ctx.arc(hoverInfo.x, hoverY, 8 * renderingScope.verticalPixelRatio, 0, 2 * Math.PI) - ctx.fill() // Draws middle portion of glyph + ctx.globalAlpha = 0.2 + ctx.beginPath() + ctx.arc(hoverInfo.x, hoverY, 8 * renderingScope.verticalPixelRatio, 0, 2 * Math.PI) + ctx.fill() // Draws middle portion of glyph - ctx.globalAlpha = 0.3 - ctx.beginPath() - ctx.arc(hoverInfo.x, hoverY, 12 * renderingScope.verticalPixelRatio, 0, 2 * Math.PI) - ctx.fill() // Draws outer portion of glyph + ctx.globalAlpha = 0.3 + ctx.beginPath() + ctx.arc(hoverInfo.x, hoverY, 12 * renderingScope.verticalPixelRatio, 0, 2 * Math.PI) + ctx.fill() // Draws outer portion of glyph + } ctx.globalAlpha = 1 }) @@ -163,19 +158,14 @@ export class StackedAreaSeriesRenderer implements ctx.globalAlpha = 1 return } - const unreversedIndex = fullLinesMeshed.length - index - const color = options.colors[unreversedIndex % colorsCount] + const color = options.colors[colorsCount - (index + 1)] ctx.strokeStyle = color ctx.fillStyle = color ctx.globalAlpha = 1 - // Bottom line is just the x-axis, which should not be drawn - if (index !== fullLinesMeshed.length - 1) { - // Line rendering: - ctx.beginPath() - ctx.strokeStyle = color - ctx.stroke(linePath.path) - } + // Line rendering: + ctx.beginPath() + ctx.stroke(linePath.path) }) } @@ -186,17 +176,22 @@ export class StackedAreaSeriesRenderer implements renderingScope: BitmapCoordinatesRenderingScope, zeroY: number, hoveredIndex?: number | null, + isV4EverywhereEnabled?: boolean, ) { const { horizontalPixelRatio, verticalPixelRatio } = renderingScope - const oddLines: LinePathData[] = [] - const evenLines: LinePathData[] = [] - const oddHighlightLines: LinePathData[] = [] - const evenHighlightLines: LinePathData[] = [] + const v2Lines: LinePathData[] = [] + const v3Lines: LinePathData[] = [] + const v4Lines: LinePathData[] = [] + const v2HighlightLines: LinePathData[] = [] + const v3HighlightLines: LinePathData[] = [] + const v4HighlightLines: LinePathData[] = [] + let firstBar = true // Modification: tracks and returns coordinates of where a glyph should be rendered for each line when a crosshair is drawn const hoverInfo = { points: new Array(), x: 0 } + const numLines = isV4EverywhereEnabled ? 3 : 2 // Modification: updated loop to include one point above and below the visible range to ensure the line is drawn to edges of chart for (let i = visibleRange.from - 1; i < visibleRange.to + 1; i++) { if (i >= bars.length || i < 0) { @@ -206,8 +201,8 @@ export class StackedAreaSeriesRenderer implements const stack = bars[i] let lineIndex = 0 stack.ys.forEach((yMedia, index) => { - if (index % 2 !== 0) { - return // only doing odd at the moment + if (index % numLines !== 0) { + return } const x = stack.x * horizontalPixelRatio @@ -219,34 +214,86 @@ export class StackedAreaSeriesRenderer implements } if (firstBar) { - oddLines[lineIndex] = { + v2Lines[lineIndex] = { path: new Path2D(), first: { x, y }, last: { x, y }, } - oddLines[lineIndex].path.moveTo(x, y) + v2Lines[lineIndex].path.moveTo(x, y) } else { - oddLines[lineIndex].path.lineTo(x, y) - oddLines[lineIndex].last.x = x - oddLines[lineIndex].last.y = y + v2Lines[lineIndex].path.lineTo(x, y) + v2Lines[lineIndex].last.x = x + v2Lines[lineIndex].last.y = y } if (firstBar && hoveredIndex && i <= hoveredIndex) { - oddHighlightLines[lineIndex] = { + v2HighlightLines[lineIndex] = { + path: new Path2D(), + first: { x, y }, + last: { x, y }, + } + v2HighlightLines[lineIndex].path.moveTo(x, y) + } else if (hoveredIndex && i <= hoveredIndex) { + v2HighlightLines[lineIndex].path.lineTo(x, y) + v2HighlightLines[lineIndex].last.x = x + v2HighlightLines[lineIndex].last.y = y + } + lineIndex += 1 + }) + firstBar = false + } + firstBar = true + // Modification: updated loop to include one point above and below the visible range to ensure the line is drawn to edges of chart + for (let i = visibleRange.to + 1; i >= visibleRange.from - 1; i--) { + if (i >= bars.length || i < 0) { + continue + } + const stack = bars[i] + let lineIndex = 0 + stack.ys.forEach((yMedia, index) => { + if (index % numLines !== 1) { + return + } + + const x = stack.x * horizontalPixelRatio + const y = yMedia * verticalPixelRatio + + if (i === hoveredIndex) { + hoverInfo.points[index] = y + hoverInfo.x = x + } + + if (firstBar) { + v3Lines[lineIndex] = { + path: new Path2D(), + first: { x, y }, + last: { x, y }, + } + v3Lines[lineIndex].path.moveTo(x, y) + } else { + v3Lines[lineIndex].path.lineTo(x, y) + v3Lines[lineIndex].last.x = x + v3Lines[lineIndex].last.y = y + } + + if (v3HighlightLines.length <= lineIndex && hoveredIndex && i <= hoveredIndex) { + v3HighlightLines[lineIndex] = { path: new Path2D(), first: { x, y }, last: { x, y }, } - oddHighlightLines[lineIndex].path.moveTo(x, y) + v3HighlightLines[lineIndex].path.moveTo(x, y) } else if (hoveredIndex && i <= hoveredIndex) { - oddHighlightLines[lineIndex].path.lineTo(x, y) - oddHighlightLines[lineIndex].last.x = x - oddHighlightLines[lineIndex].last.y = y + v3HighlightLines[lineIndex].path.lineTo(x, y) + v3HighlightLines[lineIndex].last.x = x + v3HighlightLines[lineIndex].last.y = y } + lineIndex += 1 }) firstBar = false } firstBar = true + // Modification: updated loop to include one point above and below the visible range to ensure the line is drawn to edges of chart for (let i = visibleRange.to + 1; i >= visibleRange.from - 1; i--) { if (i >= bars.length || i < 0) { @@ -255,8 +302,8 @@ export class StackedAreaSeriesRenderer implements const stack = bars[i] let lineIndex = 0 stack.ys.forEach((yMedia, index) => { - if (index % 2 === 0) { - return // only doing even at the moment + if (index % numLines !== 2 || !isV4EverywhereEnabled) { + return } const x = stack.x * horizontalPixelRatio @@ -268,29 +315,29 @@ export class StackedAreaSeriesRenderer implements } if (firstBar) { - evenLines[lineIndex] = { + v4Lines[lineIndex] = { path: new Path2D(), first: { x, y }, last: { x, y }, } - evenLines[lineIndex].path.moveTo(x, y) + v4Lines[lineIndex].path.moveTo(x, y) } else { - evenLines[lineIndex].path.lineTo(x, y) - evenLines[lineIndex].last.x = x - evenLines[lineIndex].last.y = y + v4Lines[lineIndex].path.lineTo(x, y) + v4Lines[lineIndex].last.x = x + v4Lines[lineIndex].last.y = y } - if (evenHighlightLines.length <= lineIndex && hoveredIndex && i <= hoveredIndex) { - evenHighlightLines[lineIndex] = { + if (v4HighlightLines.length <= lineIndex && hoveredIndex && i <= hoveredIndex) { + v4HighlightLines[lineIndex] = { path: new Path2D(), first: { x, y }, last: { x, y }, } - evenHighlightLines[lineIndex].path.moveTo(x, y) + v4HighlightLines[lineIndex].path.moveTo(x, y) } else if (hoveredIndex && i <= hoveredIndex) { - evenHighlightLines[lineIndex].path.lineTo(x, y) - evenHighlightLines[lineIndex].last.x = x - evenHighlightLines[lineIndex].last.y = y + v4HighlightLines[lineIndex].path.lineTo(x, y) + v4HighlightLines[lineIndex].last.x = x + v4HighlightLines[lineIndex].last.y = y } lineIndex += 1 @@ -300,20 +347,24 @@ export class StackedAreaSeriesRenderer implements const baseLine = { path: new Path2D(), - first: { x: oddLines[0].last.x, y: zeroY }, - last: { x: oddLines[0].first.x, y: zeroY }, + first: { x: v2Lines[0].last.x, y: zeroY }, + last: { x: v2Lines[0].first.x, y: zeroY }, } - baseLine.path.moveTo(oddLines[0].last.x, zeroY) - baseLine.path.lineTo(oddLines[0].first.x, zeroY) + baseLine.path.moveTo(v2Lines[0].last.x, zeroY) + baseLine.path.lineTo(v2Lines[0].first.x, zeroY) const linesMeshed: LinePathData[] = [baseLine] - for (let i = 0; i < oddLines.length; i++) { - linesMeshed.push(oddLines[i]) - if (i < evenLines.length) { - linesMeshed.push(evenLines[i]) + for (let i = 0; i < v2Lines.length; i++) { + linesMeshed.push(v2Lines[i]) + if (i < v3Lines.length) { + linesMeshed.push(v3Lines[i]) + } + if (i < v4Lines.length && isV4EverywhereEnabled) { + linesMeshed.push(v4Lines[i]) } if (hoveredIndex) { - linesMeshed.push(oddHighlightLines[i]) - linesMeshed.push(evenHighlightLines[i]) + linesMeshed.push(v2HighlightLines[i]) + linesMeshed.push(v3HighlightLines[i]) + isV4EverywhereEnabled && linesMeshed.push(v4HighlightLines[i]) } } @@ -324,10 +375,13 @@ export class StackedAreaSeriesRenderer implements _createAreas(linesMeshed: LinePathData[]): Path2D[] { const areas: Path2D[] = [] for (let i = 1; i < linesMeshed.length; i++) { - const areaPath = new Path2D(linesMeshed[i - 1].path) + // The first area must reference the base line, aka index 0 + // All other areas need to reference the first area to fully fill the bottom of the graph + const baseReferenceIndex = Math.min(i - 1, 1) + const areaPath = new Path2D(linesMeshed[baseReferenceIndex].path) areaPath.lineTo(linesMeshed[i].first.x, linesMeshed[i].first.y) areaPath.addPath(linesMeshed[i].path) - areaPath.lineTo(linesMeshed[i - 1].first.x, linesMeshed[i - 1].first.y) + areaPath.lineTo(linesMeshed[baseReferenceIndex].first.x, linesMeshed[baseReferenceIndex].first.y) areaPath.closePath() areas.push(areaPath) } diff --git a/apps/web/src/components/Charts/VolumeChart/CustomVolumeChartModel.tsx b/apps/web/src/components/Charts/VolumeChart/CustomVolumeChartModel.tsx index fb1c359563e..df171ed5924 100644 --- a/apps/web/src/components/Charts/VolumeChart/CustomVolumeChartModel.tsx +++ b/apps/web/src/components/Charts/VolumeChart/CustomVolumeChartModel.tsx @@ -9,7 +9,6 @@ export type CustomVolumeChartModelParams = { colors: string[] headerHeight: number useThinCrosshair?: boolean - isMultichainExploreEnabled?: boolean background?: string } @@ -26,7 +25,6 @@ export class CustomVolumeChartModel exten this.series = this.api.addCustomSeries( new CustomHistogramSeries({ colors: params.colors, - isMultichainExploreEnabled: params.isMultichainExploreEnabled, background: params.background, }), ) diff --git a/apps/web/src/components/Charts/VolumeChart/custom-histogram-series.tsx b/apps/web/src/components/Charts/VolumeChart/custom-histogram-series.tsx index 53e04c96173..9fd4870124d 100644 --- a/apps/web/src/components/Charts/VolumeChart/custom-histogram-series.tsx +++ b/apps/web/src/components/Charts/VolumeChart/custom-histogram-series.tsx @@ -23,13 +23,11 @@ export class CustomHistogramSeries { _renderer: CustomHistogramSeriesRenderer _colors: string[] - _isMultichainExploreEnabled?: boolean _background?: string constructor(props: CustomHistogramProps) { this._renderer = new CustomHistogramSeriesRenderer(props) this._colors = props.colors - this._isMultichainExploreEnabled = props.isMultichainExploreEnabled this._background = props.background } diff --git a/apps/web/src/components/Charts/VolumeChart/renderer.tsx b/apps/web/src/components/Charts/VolumeChart/renderer.tsx index 6c38d88911c..1540dfd57a2 100644 --- a/apps/web/src/components/Charts/VolumeChart/renderer.tsx +++ b/apps/web/src/components/Charts/VolumeChart/renderer.tsx @@ -57,7 +57,6 @@ function cumulativeBuildUp(data: StackedHistogramData): number[] { export interface CustomHistogramProps { colors: string[] - isMultichainExploreEnabled?: boolean background?: string } @@ -65,12 +64,10 @@ export class CustomHistogramSeriesRenderer im _data: PaneRendererCustomData | null = null _options: CustomHistogramSeriesOptions | null = null _colors: string[] - _isMultichainExploreEnabled?: boolean _background?: string constructor(props: CustomHistogramProps) { this._colors = props.colors - this._isMultichainExploreEnabled = props.isMultichainExploreEnabled this._background = props.background } @@ -134,18 +131,11 @@ export class CustomHistogramSeriesRenderer im const totalBox = positionsBox(zeroY, stack.ys[stack.ys.length - 1], renderingScope.verticalPixelRatio) ctx.beginPath() - const isMultichainExploreEnabled = this._isMultichainExploreEnabled if (this._background) { ctx.fillStyle = this._background } - ctx.roundRect( - column.left + margin, - totalBox.position, - width - margin, - totalBox.length, - isMultichainExploreEnabled ? 4 : 8, - ) + ctx.roundRect(column.left + margin, totalBox.position, width - margin, totalBox.length, 4) ctx.fill() // Modification: draw the stack's boxes atop the total volume bar, resulting in the top and bottom boxes being rounded @@ -157,9 +147,9 @@ export class CustomHistogramSeriesRenderer im const color = this._colors[this._colors.length - 1 - index] // color v2, then v3 const stackBoxPositions = positionsBox(previousY, y, renderingScope.verticalPixelRatio) ctx.fillStyle = color - ctx.globalAlpha = isStackedHistogram && !isHovered && isMultichainExploreEnabled ? 0.24 : 1 + ctx.globalAlpha = isStackedHistogram && !isHovered ? 0.24 : 1 ctx.fillRect(column.left + margin, stackBoxPositions.position, width - margin, stackBoxPositions.length) - if (isStackedHistogram && isMultichainExploreEnabled && !isHovered) { + if (isStackedHistogram && !isHovered) { ctx.globalAlpha = 1 ctx.fillStyle = color ctx.fillRect(column.left + margin, stackBoxPositions.position, width - margin, 2) diff --git a/apps/web/src/components/Charts/hooks.ts b/apps/web/src/components/Charts/hooks.ts index ac8d67af507..f18df0b3055 100644 --- a/apps/web/src/components/Charts/hooks.ts +++ b/apps/web/src/components/Charts/hooks.ts @@ -1,9 +1,9 @@ -import { useActiveLocale } from 'hooks/useActiveLocale' import { UTCTimestamp } from 'lightweight-charts' import { useCallback } from 'react' +import { useCurrentLocale } from 'uniswap/src/features/language/hooks' export function useHeaderDateFormatter() { - const locale = useActiveLocale() + const locale = useCurrentLocale() return useCallback( (time?: UTCTimestamp) => { if (!time) { diff --git a/apps/web/src/components/ConfirmSwapModal/Pending.test.tsx b/apps/web/src/components/ConfirmSwapModal/Pending.test.tsx index 3a86fa4384b..3843bb534c1 100644 --- a/apps/web/src/components/ConfirmSwapModal/Pending.test.tsx +++ b/apps/web/src/components/ConfirmSwapModal/Pending.test.tsx @@ -13,7 +13,7 @@ import { mocked } from 'test-utils/mocked' import { render, screen } from 'test-utils/render' import { UniswapXOrderStatus } from 'types/uniswapx' import { TransactionStatus } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' jest.mock('state/transactions/hooks', () => ({ ...jest.requireActual('state/transactions/hooks'), diff --git a/apps/web/src/components/ConfirmSwapModal/Pending.tsx b/apps/web/src/components/ConfirmSwapModal/Pending.tsx index 596117e17c8..8f41dd424a2 100644 --- a/apps/web/src/components/ConfirmSwapModal/Pending.tsx +++ b/apps/web/src/components/ConfirmSwapModal/Pending.tsx @@ -24,8 +24,8 @@ import { ThemedText } from 'theme/components/text' import { UniswapXOrderStatus } from 'types/uniswapx' import { uniswapUrls } from 'uniswap/src/constants/urls' import { TransactionStatus } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { Trans, t } from 'uniswap/src/i18n' -import { UniverseChainId } from 'uniswap/src/types/chains' import { ExplorerDataType, getExplorerLink } from 'uniswap/src/utils/linking' const Container = styled(ColumnCenter)` diff --git a/apps/web/src/components/CurrencyInputPanel/SwapCurrencyInputPanel.tsx b/apps/web/src/components/CurrencyInputPanel/SwapCurrencyInputPanel.tsx index 6f2f903d893..beda25f7a4f 100644 --- a/apps/web/src/components/CurrencyInputPanel/SwapCurrencyInputPanel.tsx +++ b/apps/web/src/components/CurrencyInputPanel/SwapCurrencyInputPanel.tsx @@ -13,7 +13,6 @@ import CurrencySearchModal from 'components/SearchModal/CurrencySearchModal' import Tooltip from 'components/Tooltip' import { AutoColumn } from 'components/deprecated/Column' import { RowBetween, RowFixed } from 'components/deprecated/Row' -import { useIsSupportedChainId } from 'constants/chains' import { PrefetchBalancesWrapper } from 'graphql/data/apollo/AdaptiveTokenBalancesProvider' import { useAccount } from 'hooks/useAccount' import styled, { useTheme } from 'lib/styled-components' @@ -26,6 +25,7 @@ import { useSwapAndLimitContext } from 'state/swap/useSwapContext' import { ThemedText } from 'theme/components' import { flexColumnNoWrap, flexRowNoWrap } from 'theme/styles' import { AnimatePresence, Flex, Text } from 'ui/src' +import { useIsSupportedChainId } from 'uniswap/src/features/chains/hooks' import Trace from 'uniswap/src/features/telemetry/Trace' import { Trans } from 'uniswap/src/i18n' import { CurrencyField } from 'uniswap/src/types/currency' diff --git a/apps/web/src/components/CurrencyInputPanel/index.tsx b/apps/web/src/components/CurrencyInputPanel/index.tsx index 6f2f7b090d7..2e53de4b695 100644 --- a/apps/web/src/components/CurrencyInputPanel/index.tsx +++ b/apps/web/src/components/CurrencyInputPanel/index.tsx @@ -10,7 +10,6 @@ import { DoubleCurrencyLogo } from 'components/Logo/DoubleLogo' import { Input as NumericalInput } from 'components/NumericalInput' import CurrencySearchModal from 'components/SearchModal/CurrencySearchModal' import { RowBetween, RowFixed } from 'components/deprecated/Row' -import { useIsSupportedChainId } from 'constants/chains' import { PrefetchBalancesWrapper } from 'graphql/data/apollo/AdaptiveTokenBalancesProvider' import { useAccount } from 'hooks/useAccount' import styled, { useTheme } from 'lib/styled-components' @@ -20,6 +19,7 @@ import { useCurrencyBalance } from 'state/connection/hooks' import { BREAKPOINTS } from 'theme' import { ThemedText } from 'theme/components' import { flexColumnNoWrap, flexRowNoWrap } from 'theme/styles' +import { useIsSupportedChainId } from 'uniswap/src/features/chains/hooks' import Trace from 'uniswap/src/features/telemetry/Trace' import { Trans, useTranslation } from 'uniswap/src/i18n' import { CurrencyField } from 'uniswap/src/types/currency' diff --git a/apps/web/src/components/CurrencyInputPanel/utils.test.ts b/apps/web/src/components/CurrencyInputPanel/utils.test.ts index cf3cdb3f780..710645366c9 100644 --- a/apps/web/src/components/CurrencyInputPanel/utils.test.ts +++ b/apps/web/src/components/CurrencyInputPanel/utils.test.ts @@ -1,7 +1,7 @@ import { Token } from '@uniswap/sdk-core' import { formatCurrencySymbol } from 'components/CurrencyInputPanel/utils' import { DAI } from 'uniswap/src/constants/tokens' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' describe('formatCurrencySymbol', () => { it('should return undefined if currency is undefined', () => { diff --git a/apps/web/src/components/FeatureFlagModal/FeatureFlagModal.tsx b/apps/web/src/components/FeatureFlagModal/FeatureFlagModal.tsx index 8f0f71ded6b..01ab0991415 100644 --- a/apps/web/src/components/FeatureFlagModal/FeatureFlagModal.tsx +++ b/apps/web/src/components/FeatureFlagModal/FeatureFlagModal.tsx @@ -9,11 +9,11 @@ import { X } from 'react-feather' import { useCloseModal, useModalIsOpen } from 'state/application/hooks' import { ApplicationModal } from 'state/application/reducer' import { BREAKPOINTS } from 'theme' +import { SUPPORTED_CHAIN_IDS } from 'uniswap/src/features/chains/types' import { DynamicConfigKeys, DynamicConfigs, QuickRouteChainsConfigKey } from 'uniswap/src/features/gating/configs' import { FeatureFlags, getFeatureFlagName } from 'uniswap/src/features/gating/flags' import { useFeatureFlagWithExposureLoggingDisabled } from 'uniswap/src/features/gating/hooks' import { Statsig } from 'uniswap/src/features/gating/sdk/statsig' -import { SUPPORTED_CHAIN_IDS } from 'uniswap/src/types/chains' const Wrapper = styled(Column)` padding: 20px 16px; @@ -219,13 +219,11 @@ export default function FeatureFlagModal() { - - - - - - - - , - supportedChains: COMBINED_CHAIN_IDS, + supportedChains: ALL_CHAIN_IDS, }, [FeeAmount.LOW_200]: { label: '0.02', @@ -27,16 +27,16 @@ export const FEE_AMOUNT_DETAIL: Record< [FeeAmount.LOW]: { label: '0.05', description: , - supportedChains: COMBINED_CHAIN_IDS, + supportedChains: ALL_CHAIN_IDS, }, [FeeAmount.MEDIUM]: { label: '0.3', description: , - supportedChains: COMBINED_CHAIN_IDS, + supportedChains: ALL_CHAIN_IDS, }, [FeeAmount.HIGH]: { label: '1', description: , - supportedChains: COMBINED_CHAIN_IDS, + supportedChains: ALL_CHAIN_IDS, }, } diff --git a/apps/web/src/components/FiatOnrampModal/index.tsx b/apps/web/src/components/FiatOnrampModal/index.tsx index 6a273e2c81a..bbc2953da3c 100644 --- a/apps/web/src/components/FiatOnrampModal/index.tsx +++ b/apps/web/src/components/FiatOnrampModal/index.tsx @@ -1,9 +1,7 @@ import Circle from 'assets/images/blue-loader.svg' import { MOONPAY_SUPPORTED_CURRENCY_CODES } from 'components/FiatOnrampModal/constants' import { getDefaultCurrencyCode, parsePathParts } from 'components/FiatOnrampModal/utils' -import { getChain, getChainFromChainUrlParam, getChainUrlParam } from 'constants/chains' import { useAccount } from 'hooks/useAccount' -import useParsedQueryString from 'hooks/useParsedQueryString' import styled, { useTheme } from 'lib/styled-components' import { useCallback, useEffect, useState } from 'react' import { useHref } from 'react-router-dom' @@ -12,9 +10,11 @@ import { ApplicationModal } from 'state/application/reducer' import { CustomLightSpinner, ThemedText } from 'theme/components' import { useIsDarkMode } from 'theme/components/ThemeToggle' import { AdaptiveWebModal } from 'ui/src' +import { useUrlContext } from 'uniswap/src/contexts/UrlContext' +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' import { Trans } from 'uniswap/src/i18n' - import { logger } from 'utilities/src/logger/logger' +import { getChainIdFromChainUrlParam } from 'utils/chainParams' const MOONPAY_DARK_BACKGROUND = '#1c1c1e' const Wrapper = styled.div<{ isDarkMode: boolean }>` @@ -77,11 +77,13 @@ export default function FiatOnrampModal() { const closeModal = useCloseModal() const fiatOnrampModalOpen = useModalIsOpen(ApplicationModal.FIAT_ONRAMP) - const { chain, tokenAddress } = parsePathParts(location.pathname) + const { chainId, tokenAddress } = parsePathParts(location.pathname) + const chainInfo = chainId ? getChainInfo(chainId) : undefined + const { useParsedQueryString } = useUrlContext() const parsedChainName = useParsedQueryString().chain - const queryChain = - typeof parsedChainName === 'string' ? getChainFromChainUrlParam(getChainUrlParam(parsedChainName)) : undefined - const accountChainInfo = getChain({ chainId: account.chainId }) + const queryChainId = typeof parsedChainName === 'string' ? getChainIdFromChainUrlParam(parsedChainName) : undefined + const queryChainInfo = queryChainId ? getChainInfo(queryChainId) : undefined + const accountChainInfo = account.chainId ? getChainInfo(account.chainId) : undefined const [signedIframeUrl, setSignedIframeUrl] = useState(null) const [error, setError] = useState(null) @@ -109,7 +111,7 @@ export default function FiatOnrampModal() { colorCode: theme.accent1, defaultCurrencyCode: getDefaultCurrencyCode( tokenAddress, - chain?.backendChain.chain ?? queryChain?.backendChain.chain ?? accountChainInfo?.backendChain.chain, + chainInfo?.backendChain.chain ?? queryChainInfo?.backendChain.chain ?? accountChainInfo?.backendChain.chain, ), redirectUrl: swapUrl, walletAddresses: JSON.stringify( @@ -135,9 +137,9 @@ export default function FiatOnrampModal() { account.address, account.isConnected, accountChainInfo?.backendChain.chain, - chain?.backendChain.chain, + chainInfo?.backendChain.chain, isDarkMode, - queryChain?.backendChain.chain, + queryChainInfo?.backendChain.chain, swapUrl, theme.accent1, tokenAddress, diff --git a/apps/web/src/components/FiatOnrampModal/utils.test.ts b/apps/web/src/components/FiatOnrampModal/utils.test.ts index 015844357be..ead2021ec65 100644 --- a/apps/web/src/components/FiatOnrampModal/utils.test.ts +++ b/apps/web/src/components/FiatOnrampModal/utils.test.ts @@ -1,7 +1,6 @@ import { WETH9 } from '@uniswap/sdk-core' import { getDefaultCurrencyCode, parsePathParts } from 'components/FiatOnrampModal/utils' import { NATIVE_CHAIN_ID } from 'constants/tokens' -import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' import { MATIC_MAINNET, USDC_ARBITRUM, @@ -13,7 +12,7 @@ import { WETH_POLYGON, } from 'uniswap/src/constants/tokens' import { Chain } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' describe('getDefaultCurrencyCode', () => { it('NATIVE/arbitrum should return the correct currency code', () => { @@ -66,15 +65,15 @@ describe('getDefaultCurrencyCode', () => { describe('parseLocation', () => { it('should parse the URL correctly', () => { expect(parsePathParts('/tokens/ethereum/0x2260fac5e5542a773aa44fbcfedf7c193bc2c599')).toEqual({ - chain: UNIVERSE_CHAIN_INFO[UniverseChainId.Mainnet], + chainId: UniverseChainId.Mainnet, tokenAddress: '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599', }) expect(parsePathParts('tokens/ethereum/0x2260fac5e5542a773aa44fbcfedf7c193bc2c599')).toEqual({ - chain: UNIVERSE_CHAIN_INFO[UniverseChainId.Mainnet], + chainId: UniverseChainId.Mainnet, tokenAddress: '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599', }) expect(parsePathParts('/swap')).toEqual({ - chain: undefined, + chainId: undefined, tokenAddress: undefined, }) }) diff --git a/apps/web/src/components/FiatOnrampModal/utils.ts b/apps/web/src/components/FiatOnrampModal/utils.ts index 21cfd71d1d1..b59a26a70aa 100644 --- a/apps/web/src/components/FiatOnrampModal/utils.ts +++ b/apps/web/src/components/FiatOnrampModal/utils.ts @@ -1,6 +1,5 @@ import { WETH9 } from '@uniswap/sdk-core' import { MoonpaySupportedCurrencyCode } from 'components/FiatOnrampModal/constants' -import { getChainFromChainUrlParam, getChainUrlParam, InterfaceGqlChain } from 'constants/chains' import { MATIC_MAINNET, USDC_ARBITRUM, @@ -13,7 +12,8 @@ import { WETH_POLYGON, } from 'uniswap/src/constants/tokens' import { Chain } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { GqlChainId, UniverseChainId } from 'uniswap/src/features/chains/types' +import { getChainIdFromChainUrlParam } from 'utils/chainParams' type MoonpaySupportedChain = Chain.Ethereum | Chain.Polygon | Chain.Arbitrum | Chain.Optimism | Chain.Base const moonPaySupportedChains = [Chain.Ethereum, Chain.Polygon, Chain.Arbitrum, Chain.Optimism, Chain.Base] @@ -53,7 +53,7 @@ const CURRENCY_CODES: { }, } -export function getDefaultCurrencyCode(address?: string, gqlChain?: InterfaceGqlChain): MoonpaySupportedCurrencyCode { +export function getDefaultCurrencyCode(address?: string, gqlChain?: GqlChainId): MoonpaySupportedCurrencyCode { if (!gqlChain) { return 'eth' } @@ -74,8 +74,8 @@ export function getDefaultCurrencyCode(address?: string, gqlChain?: InterfaceGql export function parsePathParts(pathname: string) { const pathParts = pathname.split('/') // Matches the /tokens// path. - const chainSlug = getChainUrlParam(pathParts.length > 2 ? pathParts[pathParts.length - 2] : undefined) - const chain = getChainFromChainUrlParam(chainSlug) + const chainSlug = pathParts.length > 2 ? pathParts[pathParts.length - 2] : undefined + const chainId = getChainIdFromChainUrlParam(chainSlug) const tokenAddress = pathParts.length > 2 ? pathParts[pathParts.length - 1] : undefined - return { chain, tokenAddress } + return { chainId, tokenAddress } } diff --git a/apps/web/src/components/IncreaseLiquidity/IncreaseLiquidityContext.tsx b/apps/web/src/components/IncreaseLiquidity/IncreaseLiquidityContext.tsx index 06413cb4cd2..3dae25a9ff9 100644 --- a/apps/web/src/components/IncreaseLiquidity/IncreaseLiquidityContext.tsx +++ b/apps/web/src/components/IncreaseLiquidity/IncreaseLiquidityContext.tsx @@ -1,5 +1,5 @@ import { useDerivedIncreaseLiquidityInfo } from 'components/IncreaseLiquidity/hooks' -import { useModalLiquidityPositionInfo } from 'components/Liquidity/hooks' +import { useModalLiquidityInitialState } from 'components/Liquidity/hooks' import { DepositInfo, PositionInfo } from 'components/Liquidity/types' import { Dispatch, PropsWithChildren, SetStateAction, createContext, useContext, useMemo, useState } from 'react' import { PositionField } from 'types/position' @@ -19,11 +19,18 @@ const DEFAULT_INCREASE_LIQUIDITY_STATE = { exactField: PositionField.TOKEN0, } +// This increase-specific context needs to recalculate deposit0Disabled and deposit1Disabled, +// which are derived from price range inputs in the regular create flow. +export type IncreaseLiquidityDerivedInfo = DepositInfo & { + deposit0Disabled?: boolean + deposit1Disabled?: boolean +} + interface IncreaseLiquidityContextType { step: IncreaseLiquidityStep setStep: Dispatch> increaseLiquidityState: IncreaseLiquidityState - derivedIncreaseLiquidityInfo: DepositInfo + derivedIncreaseLiquidityInfo: IncreaseLiquidityDerivedInfo setIncreaseLiquidityState: Dispatch> } @@ -40,7 +47,7 @@ export function useIncreaseLiquidityContext() { } export function IncreaseLiquidityContextProvider({ children }: PropsWithChildren) { - const positionInfo = useModalLiquidityPositionInfo() + const positionInfo = useModalLiquidityInitialState() const [step, setStep] = useState(IncreaseLiquidityStep.Input) diff --git a/apps/web/src/components/IncreaseLiquidity/IncreaseLiquidityReview.tsx b/apps/web/src/components/IncreaseLiquidity/IncreaseLiquidityReview.tsx index d94cdc5ebd5..8c3a3fc653d 100644 --- a/apps/web/src/components/IncreaseLiquidity/IncreaseLiquidityReview.tsx +++ b/apps/web/src/components/IncreaseLiquidity/IncreaseLiquidityReview.tsx @@ -61,6 +61,12 @@ export function IncreaseLiquidityReview({ onClose }: { onClose: () => void }) { setCurrentStep(undefined) } + const onSuccess = () => { + setSteps([]) + setCurrentStep(undefined) + onClose() + } + const onIncreaseLiquidity = () => { const isValidTx = isValidLiquidityTxContext(txInfo) if (!account || account?.type !== AccountType.SignerMnemonic || !isValidTx) { @@ -75,7 +81,7 @@ export function IncreaseLiquidityReview({ onClose }: { onClose: () => void }) { liquidityTxContext: txInfo, setCurrentStep, setSteps, - onSuccess: onClose, + onSuccess, onFailure, }), ) diff --git a/apps/web/src/components/IncreaseLiquidity/IncreaseLiquidityTxContext.tsx b/apps/web/src/components/IncreaseLiquidity/IncreaseLiquidityTxContext.tsx index bd49364c03a..2927d96ae92 100644 --- a/apps/web/src/components/IncreaseLiquidity/IncreaseLiquidityTxContext.tsx +++ b/apps/web/src/components/IncreaseLiquidity/IncreaseLiquidityTxContext.tsx @@ -2,7 +2,7 @@ import { ProtocolVersion } from '@uniswap/client-pools/dist/pools/v1/types_pb' import { Currency, CurrencyAmount } from '@uniswap/sdk-core' import { useIncreaseLiquidityContext } from 'components/IncreaseLiquidity/IncreaseLiquidityContext' -import { useModalLiquidityPositionInfo } from 'components/Liquidity/hooks' +import { useModalLiquidityInitialState } from 'components/Liquidity/hooks' import { getProtocolItems } from 'components/Liquidity/utils' import { ZERO_ADDRESS } from 'constants/misc' import { PropsWithChildren, createContext, useContext, useMemo } from 'react' @@ -23,7 +23,7 @@ interface IncreasePositionContextType { const IncreaseLiquidityTxContext = createContext(undefined) export function IncreaseLiquidityTxContextProvider({ children }: PropsWithChildren): JSX.Element { - const positionInfo = useModalLiquidityPositionInfo() + const positionInfo = useModalLiquidityInitialState() const { derivedIncreaseLiquidityInfo } = useIncreaseLiquidityContext() const { currencyAmounts } = derivedIncreaseLiquidityInfo diff --git a/apps/web/src/components/IncreaseLiquidity/hooks.tsx b/apps/web/src/components/IncreaseLiquidity/hooks.tsx index 91e9161ff40..613a5f0444a 100644 --- a/apps/web/src/components/IncreaseLiquidity/hooks.tsx +++ b/apps/web/src/components/IncreaseLiquidity/hooks.tsx @@ -1,12 +1,14 @@ // eslint-disable-next-line no-restricted-imports import { ProtocolVersion } from '@uniswap/client-pools/dist/pools/v1/types_pb' -import { IncreaseLiquidityState } from 'components/IncreaseLiquidity/IncreaseLiquidityContext' -import { DepositInfo } from 'components/Liquidity/types' +import { + IncreaseLiquidityDerivedInfo, + IncreaseLiquidityState, +} from 'components/IncreaseLiquidity/IncreaseLiquidityContext' import { useAccount } from 'hooks/useAccount' import { UseDepositInfoProps, useDepositInfo } from 'pages/Pool/Positions/create/hooks' import { useMemo } from 'react' -export function useDerivedIncreaseLiquidityInfo(state: IncreaseLiquidityState): DepositInfo { +export function useDerivedIncreaseLiquidityInfo(state: IncreaseLiquidityState): IncreaseLiquidityDerivedInfo { const account = useAccount() const { position: positionInfo, exactAmount, exactField } = state @@ -29,6 +31,8 @@ export function useDerivedIncreaseLiquidityInfo(state: IncreaseLiquidityState): token1, exactField, exactAmount, + deposit0Disabled: false, + deposit1Disabled: false, } } @@ -36,6 +40,9 @@ export function useDerivedIncreaseLiquidityInfo(state: IncreaseLiquidityState): const tickLower = tickLowerStr ? parseInt(tickLowerStr) : undefined const tickUpper = tickUpperStr ? parseInt(tickUpperStr) : undefined + const deposit0Disabled = Boolean(tickUpper && positionInfo.pool && positionInfo.pool.tickCurrent >= tickUpper) + const deposit1Disabled = Boolean(tickLower && positionInfo.pool && positionInfo.pool.tickCurrent <= tickLower) + if (positionInfo.version === ProtocolVersion.V3) { return { protocolVersion: ProtocolVersion.V3, @@ -47,6 +54,8 @@ export function useDerivedIncreaseLiquidityInfo(state: IncreaseLiquidityState): token1, exactField, exactAmount, + deposit0Disabled, + deposit1Disabled, } } @@ -61,6 +70,8 @@ export function useDerivedIncreaseLiquidityInfo(state: IncreaseLiquidityState): token1: currency1, exactField, exactAmount, + deposit0Disabled, + deposit1Disabled, } } @@ -70,5 +81,14 @@ export function useDerivedIncreaseLiquidityInfo(state: IncreaseLiquidityState): } }, [account.address, exactAmount, exactField, positionInfo, currency0, currency1, token0, token1]) - return useDepositInfo(depositInfoProps) + const depositInfo = useDepositInfo(depositInfoProps) + + return useMemo( + () => ({ + ...depositInfo, + deposit0Disabled: depositInfoProps.deposit0Disabled, + deposit1Disabled: depositInfoProps.deposit1Disabled, + }), + [depositInfo, depositInfoProps.deposit0Disabled, depositInfoProps.deposit1Disabled], + ) } diff --git a/apps/web/src/components/Liquidity/FeeTierSearchModal.tsx b/apps/web/src/components/Liquidity/FeeTierSearchModal.tsx index 92f7b93a004..7d824e47cd2 100644 --- a/apps/web/src/components/Liquidity/FeeTierSearchModal.tsx +++ b/apps/web/src/components/Liquidity/FeeTierSearchModal.tsx @@ -10,16 +10,18 @@ import { useEffect, useState } from 'react' // eslint-disable-next-line @typescript-eslint/no-restricted-imports import styled from 'styled-components' import { ClickableTamaguiStyle, CloseIcon } from 'theme/components' -import { Button, Flex, Input, Text } from 'ui/src' +import { Button, Flex, Text } from 'ui/src' import { BackArrow } from 'ui/src/components/icons/BackArrow' import { CheckCircleFilled } from 'ui/src/components/icons/CheckCircleFilled' import { Plus } from 'ui/src/components/icons/Plus' import { Search } from 'ui/src/components/icons/Search' +import { AmountInput, numericInputRegex } from 'uniswap/src/components/CurrencyInputPanel/AmountInput' import { Modal } from 'uniswap/src/components/modals/Modal' +import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext' import { ModalName } from 'uniswap/src/features/telemetry/constants' import { useTranslation } from 'uniswap/src/i18n' import useResizeObserver from 'use-resize-observer' -import { NumberType, useFormatter } from 'utils/formatNumbers' +import { NumberType } from 'utilities/src/format/types' const FeeTierPercentInput = styled(StyledPercentInput)` flex-grow: 0; @@ -30,7 +32,7 @@ const FeeTierPercentInput = styled(StyledPercentInput)` export function FeeTierSearchModal() { const { chainId } = useAccount() const { - positionState: { fee: selectedFee, protocolVersion }, + positionState: { fee: selectedFee, protocolVersion, hook }, derivedPositionInfo, setPositionState, feeTierSearchModalOpen, @@ -45,13 +47,21 @@ export function FeeTierSearchModal() { const [searchValue, setSearchValue] = useState('') const [createFeeValue, setCreateFeeValue] = useState('') const [createModeEnabled, setCreateModeEnabled] = useState(false) - const { formatPercent, formatNumberOrString } = useFormatter() + const { formatNumberOrString, formatPercent } = useLocalizationContext() const [autoDecrementing, setAutoDecrementing] = useState(false) const [autoIncrementing, setAutoIncrementing] = useState(false) const [holdDuration, setHoldDuration] = useState(0) const hiddenObserver = useResizeObserver() - const feeTierData = useAllFeeTierPoolData({ chainId, protocolVersion, currencies: derivedPositionInfo.currencies }) + const withDynamicFeeTier = Boolean(hook) + const { feeTierData, hasExistingFeeTiers } = useAllFeeTierPoolData({ + chainId, + protocolVersion, + currencies: derivedPositionInfo.currencies, + withDynamicFeeTier, + }) + + const showCreateModal = !withDynamicFeeTier && (createModeEnabled || !hasExistingFeeTiers) useEffect(() => { let interval: NodeJS.Timeout @@ -78,11 +88,17 @@ export function FeeTierSearchModal() { setCreateFeeValue((prev) => { let newValue = parseFloat(prev) if (autoDecrementing) { + if (!prev || prev === '') { + return '0' + } newValue -= 0.01 if (newValue < 0) { return '0' } } else if (autoIncrementing) { + if (!prev || prev === '') { + return '0.01' + } newValue += 0.01 if (newValue > 100) { return '100' @@ -116,13 +132,13 @@ export function FeeTierSearchModal() { )} - - {createModeEnabled ? t('fee.tier.create') : t('fee.tier.select')} + + {showCreateModal ? t('fee.tier.create') : t('fee.tier.select')} - {createModeEnabled ? ( + {showCreateModal ? ( {t('fee.tier.create.description')} @@ -248,24 +264,59 @@ export function FeeTierSearchModal() { ) : ( <> - + - { - setSearchValue(event.target.value) + placeholderTextColor="$neutral3" + onChangeText={(value) => { + if (value === '.') { + setSearchValue('0.') + return + } + // Prevent two decimals + if (value.indexOf('.') !== -1 && value.indexOf('.', value.indexOf('.') + 1) !== -1) { + return + } + // Prevent addition of non-numeric characters to the end of the string + if (!numericInputRegex.test(value)) { + setSearchValue(value.slice(0, -1)) + return + } + + const newValue = parseFloat(value) + if (newValue > 100) { + setSearchValue('100') + return + } + + setSearchValue(newValue >= 0 ? value : '') }} - value={searchValue} /> {Object.values(feeTierData) - .filter((data) => data.formattedFee.includes(searchValue) || searchValue.includes(data.id)) + .filter((data) => data.formattedFee.includes(searchValue) || (data.id && searchValue.includes(data.id))) .map((pool) => ( ({ ...prevState, fee: { - feeAmount: pool.fee, - tickSpacing: calculateTickSpacingFromFeeAmount(pool.fee), + feeAmount: pool.fee.feeAmount, + tickSpacing: pool.fee.tickSpacing, }, })) onClose() @@ -291,14 +342,24 @@ export function FeeTierSearchModal() { {pool.formattedFee} - {formatNumberOrString({ input: pool.totalLiquidityUsd, type: NumberType.ChartFiatValue })} + {pool.totalLiquidityUsd === 0 + ? '0' + : formatNumberOrString({ + value: pool.totalLiquidityUsd, + type: NumberType.FiatTokenStats, + })}{' '} + {t('common.totalValueLocked')} - {t('fee.tier.percent.select', { percentage: formatPercent(pool.percentage) })} + {pool.created + ? t('fee.tier.percent.select', { percentage: formatPercent(pool.percentage.toFixed()) }) + : t('common.notCreated.label')} - {pool.fee === selectedFee.feeAmount && } + {pool.fee.feeAmount === selectedFee.feeAmount && ( + + )} ))} diff --git a/apps/web/src/components/Liquidity/HookModal.tsx b/apps/web/src/components/Liquidity/HookModal.tsx new file mode 100644 index 00000000000..a04130e93c9 --- /dev/null +++ b/apps/web/src/components/Liquidity/HookModal.tsx @@ -0,0 +1,200 @@ +import { FlagWarning, getFlagsFromContractAddress, getFlagWarning } from 'components/Liquidity/utils' +import { GetHelpHeader } from 'components/Modal/GetHelpHeader' +import { useMemo, useState } from 'react' +import { CopyHelper } from 'theme/components' +import { Button, Checkbox, Flex, HeightAnimator, Separator, Text, TouchableArea } from 'ui/src' +import { AlertTriangleFilled } from 'ui/src/components/icons/AlertTriangleFilled' +import { ContractInteraction } from 'ui/src/components/icons/ContractInteraction' +import { DocumentList } from 'ui/src/components/icons/DocumentList' +import { Page } from 'ui/src/components/icons/Page' +import { RotatableChevron } from 'ui/src/components/icons/RotatableChevron' +import { Modal } from 'uniswap/src/components/modals/Modal' +import { LearnMoreLink } from 'uniswap/src/components/text/LearnMoreLink' +import { uniswapUrls } from 'uniswap/src/constants/urls' +import { ModalName } from 'uniswap/src/features/telemetry/constants' +import { useTranslation } from 'uniswap/src/i18n' +import { shortenAddress } from 'uniswap/src/utils/addresses' + +function HookWarnings({ flags, hasDangerous }: { flags: FlagWarning[]; hasDangerous: boolean }) { + const { t } = useTranslation() + + const [expandedProperties, setExpandedProperties] = useState(hasDangerous) + + const toggleExpandedProperties = () => { + setExpandedProperties((state) => !state) + } + + if (!flags.length) { + return null + } + + return ( + <> + + + + + + + {t('position.addingHook.viewProperties')} + + + + + + {expandedProperties && ( + + {flags.map(({ Icon, name, info, dangerous }) => ( + + + + + {name} + + + + + {info} + + + + ))} + + )} + + ) +} + +export function HookModal({ + isOpen, + onClose, + onClearHook, + onContinue, + address, +}: { + address: Address + isOpen: boolean + onClose: () => void + onClearHook: () => void + onContinue: () => void +}) { + const { t } = useTranslation() + const [disclaimerChecked, setDisclaimerChecked] = useState(false) + + const handleClearHook = () => { + onClearHook() + onClose() + } + + const onDisclaimerChecked = () => { + setDisclaimerChecked((state) => !state) + } + + const { flags, hasDangerous } = useMemo(() => { + if (!address) { + return { + flags: [], + hasDangerous: false, + } + } + + let hasDangerous = false + const flagInfos: Record = {} + getFlagsFromContractAddress(address).forEach((flag) => { + const warning = getFlagWarning(flag, t) + + if (warning?.dangerous) { + hasDangerous = true + } + + if (warning?.name) { + flagInfos[warning.name] = warning + } + }) + + return { + flags: Object.values(flagInfos), + hasDangerous, + } + }, [address, t]) + + const canContinue = !hasDangerous || (hasDangerous && disclaimerChecked) + const handleContinue = () => { + if (canContinue) { + onContinue() + onClose() + } + } + + if (!address) { + return null + } + + // TODO(WEB-5289): match entrance/exit animations with the currency selector + return ( + + + + + + + {hasDangerous ? ( + + ) : ( + + )} + + + {hasDangerous ? t('position.hook.warningHeader') : t('position.addingHook')} + + + {hasDangerous ? t('position.hook.warningInfo') : t('position.addingHook.disclaimer')} + + + + + + + + + + {t('common.text.contract')} + + + + + {shortenAddress(address)} + + + + + + + {hasDangerous && ( + + + + {t('position.hook.disclaimer')} + + + )} + + + + + + + + + ) +} diff --git a/apps/web/src/components/Liquidity/LiquidityPositionAmountsTile.tsx b/apps/web/src/components/Liquidity/LiquidityPositionAmountsTile.tsx index 6164863c8d5..fda7b7ac5d4 100644 --- a/apps/web/src/components/Liquidity/LiquidityPositionAmountsTile.tsx +++ b/apps/web/src/components/Liquidity/LiquidityPositionAmountsTile.tsx @@ -26,21 +26,21 @@ export function LiquidityPositionAmountsTile({ - + {currency0Amount.currency.symbol} - + {formatCurrencyAmount({ value: currency0Amount })} - {fiatValue0 && ( - + {fiatValue0?.greaterThan(0) && ( + ({formatCurrencyAmount({ value: fiatValue0, type: NumberType.FiatTokenPrice })}) )} {totalFiatValue?.greaterThan(0) && fiatValue0 && ( - + {formatPercent(new Percent(fiatValue0.quotient, totalFiatValue.quotient).toFixed(6))} )} @@ -49,21 +49,21 @@ export function LiquidityPositionAmountsTile({ - + {currency1Amount.currency.symbol} - + {formatCurrencyAmount({ value: currency1Amount })} - {fiatValue1 && ( - + {fiatValue1?.greaterThan(0) && ( + ({formatCurrencyAmount({ value: fiatValue1, type: NumberType.FiatTokenPrice })}) )} {totalFiatValue?.greaterThan(0) && fiatValue1 && ( - + {formatPercent(new Percent(fiatValue1.quotient, totalFiatValue.quotient).toFixed(6))} )} diff --git a/apps/web/src/components/Liquidity/LiquidityPositionCard.tsx b/apps/web/src/components/Liquidity/LiquidityPositionCard.tsx index c1aa79e4170..82aa08ac85f 100644 --- a/apps/web/src/components/Liquidity/LiquidityPositionCard.tsx +++ b/apps/web/src/components/Liquidity/LiquidityPositionCard.tsx @@ -1,4 +1,5 @@ // eslint-disable-next-line no-restricted-imports +import { PositionStatus } from '@uniswap/client-pools/dist/pools/v1/types_pb' import { LiquidityPositionFeeStats } from 'components/Liquidity/LiquidityPositionFeeStats' import { LiquidityPositionInfo } from 'components/Liquidity/LiquidityPositionInfo' import { useV3OrV4PositionDerivedInfo } from 'components/Liquidity/hooks' @@ -20,7 +21,7 @@ export function LiquidityPositionCard({ liquidityPosition, ...rest }: { liquidit fiatValue0 && fiatValue1 ? formatCurrencyAmount({ value: fiatValue0.add(fiatValue1), - type: NumberType.FiatTokenPrice, + type: NumberType.FiatStandard, }) : undefined const v2FormattedUsdValue = @@ -32,7 +33,8 @@ export function LiquidityPositionCard({ liquidityPosition, ...rest }: { liquidit fiatFeeValue0 && fiatFeeValue1 ? formatCurrencyAmount({ value: fiatFeeValue0.add(fiatFeeValue1), - type: NumberType.FiatTokenPrice, + type: + liquidityPosition.status === PositionStatus.CLOSED ? NumberType.FiatStandard : NumberType.FiatTokenPrice, }) : undefined @@ -59,6 +61,7 @@ export function LiquidityPositionCard({ liquidityPosition, ...rest }: { liquidit feeTier={liquidityPosition.feeTier?.toString()} tickLower={liquidityPosition.tickLower} tickUpper={liquidityPosition.tickUpper} + version={liquidityPosition.version} // TODO (WEB-4920): add total APR and fee APR /> diff --git a/apps/web/src/components/Liquidity/LiquidityPositionFeeStats.tsx b/apps/web/src/components/Liquidity/LiquidityPositionFeeStats.tsx index f8f719e0cd1..b2c5ada5c40 100644 --- a/apps/web/src/components/Liquidity/LiquidityPositionFeeStats.tsx +++ b/apps/web/src/components/Liquidity/LiquidityPositionFeeStats.tsx @@ -1,3 +1,5 @@ +// eslint-disable-next-line no-restricted-imports +import { ProtocolVersion } from '@uniswap/client-pools/dist/pools/v1/types_pb' import { useGetRangeDisplay } from 'components/Liquidity/hooks' import { PriceOrdering } from 'components/PositionListItem' import { MouseoverTooltip } from 'components/Tooltip' @@ -17,11 +19,12 @@ interface LiquidityPositionFeeStatsProps { tickLower?: string tickUpper?: string showReverseButton?: boolean + version: ProtocolVersion } const PrimaryText = styled(Text, { color: '$neutral1', - variant: 'body1', + variant: 'body2', }) const SecondaryText = styled(Text, { @@ -36,6 +39,7 @@ export function LiquidityPositionFeeStats({ tickLower, tickUpper, feeTier, + version, showReverseButton = true, }: LiquidityPositionFeeStatsProps) { const { t } = useTranslation() @@ -50,7 +54,7 @@ export function LiquidityPositionFeeStats({ }) return ( - + {formattedUsdValue ? ( {formattedUsdValue} @@ -59,16 +63,16 @@ export function LiquidityPositionFeeStats({ - )} - - {t('pool.position')} - - - - {formattedUsdFees || t('common.unavailable')} - - {t('common.fees')} - + {t('pool.position')} + {version !== ProtocolVersion.V2 && ( + + {formattedUsdFees || t('common.unavailable')} + + {t('common.fees')} + + + )} {/* TODO: add APR once its been calculated. */} {priceOrdering.priceLower && priceOrdering.priceUpper ? ( @@ -78,17 +82,17 @@ export function LiquidityPositionFeeStats({ - + {minPrice} {tokenASymbol} / {tokenBSymbol} - + - + {maxPrice} {tokenASymbol} / {tokenBSymbol} - + {showReverseButton && ( @@ -103,9 +107,7 @@ export function LiquidityPositionFeeStats({ ) : ( - - {t('common.fullRange')} - + {t('common.fullRange')} )} diff --git a/apps/web/src/components/Liquidity/LiquidityPositionInfo.test.tsx b/apps/web/src/components/Liquidity/LiquidityPositionInfo.test.tsx index 8fd4a9b43dc..df08d21660f 100644 --- a/apps/web/src/components/Liquidity/LiquidityPositionInfo.test.tsx +++ b/apps/web/src/components/Liquidity/LiquidityPositionInfo.test.tsx @@ -10,6 +10,7 @@ jest.mock('components/Liquidity/utils') describe('LiquidityPositionInfo', () => { it('should render in range', () => { const positionInfo: PositionInfo = { + chainId: TEST_TOKEN_1.chainId, currency0Amount: toCurrencyAmount(TEST_TOKEN_1, 1), currency1Amount: toCurrencyAmount(TEST_TOKEN_2, 1), status: PositionStatus.IN_RANGE, @@ -23,6 +24,7 @@ describe('LiquidityPositionInfo', () => { it('should render out of range', () => { const positionInfo: PositionInfo = { + chainId: TEST_TOKEN_1.chainId, currency0Amount: toCurrencyAmount(TEST_TOKEN_1, 1), currency1Amount: toCurrencyAmount(TEST_TOKEN_2, 1), status: PositionStatus.OUT_OF_RANGE, @@ -36,6 +38,7 @@ describe('LiquidityPositionInfo', () => { it('should render closed', () => { const positionInfo: PositionInfo = { + chainId: TEST_TOKEN_1.chainId, currency0Amount: toCurrencyAmount(TEST_TOKEN_1, 1), currency1Amount: toCurrencyAmount(TEST_TOKEN_2, 1), status: PositionStatus.CLOSED, diff --git a/apps/web/src/components/Liquidity/LiquidityPositionInfo.tsx b/apps/web/src/components/Liquidity/LiquidityPositionInfo.tsx index d3f16ed6417..0d9873a0902 100644 --- a/apps/web/src/components/Liquidity/LiquidityPositionInfo.tsx +++ b/apps/web/src/components/Liquidity/LiquidityPositionInfo.tsx @@ -1,11 +1,9 @@ -import { BadgeData, LiquidityPositionInfoBadges } from 'components/Liquidity/LiquidityPositionInfoBadges' +import { LiquidityPositionInfoBadges } from 'components/Liquidity/LiquidityPositionInfoBadges' import { LiquidityPositionStatusIndicator } from 'components/Liquidity/LiquidityPositionStatusIndicator' import { PositionInfo } from 'components/Liquidity/types' import { getProtocolVersionLabel } from 'components/Liquidity/utils' import { DoubleCurrencyAndChainLogo } from 'components/Logo/DoubleLogo' -import { ZERO_ADDRESS } from 'constants/misc' import { Flex, Text } from 'ui/src' -import { DocumentList } from 'ui/src/components/icons/DocumentList' interface LiquidityPositionInfoProps { positionInfo: PositionInfo @@ -23,22 +21,11 @@ export function LiquidityPositionInfo({ positionInfo }: LiquidityPositionInfoPro /> - + {currency0Amount?.currency.symbol} / {currency1Amount?.currency.symbol} - } - : undefined, - feeTier ? { label: `${Number(feeTier) / 10000}%` } : undefined, - ].filter(Boolean) as BadgeData[] - } - /> + diff --git a/apps/web/src/components/Liquidity/LiquidityPositionInfoBadges.test.tsx b/apps/web/src/components/Liquidity/LiquidityPositionInfoBadges.test.tsx index 178b9c00a05..242b04981bd 100644 --- a/apps/web/src/components/Liquidity/LiquidityPositionInfoBadges.test.tsx +++ b/apps/web/src/components/Liquidity/LiquidityPositionInfoBadges.test.tsx @@ -1,22 +1,20 @@ import { LiquidityPositionInfoBadges } from 'components/Liquidity/LiquidityPositionInfoBadges' import { render } from 'test-utils/render' -const testBadgeData = [{ label: 'test', copyable: true }, { label: 'test2' }] - describe('LiquidityPositionInfoBadges', () => { it('should render with default size', () => { - const { getByText } = render() - expect(getByText('test')).toBeInTheDocument() + const { getByText } = render() + expect(getByText('2')).toBeInTheDocument() }) it('should render with small size', () => { - const { getByText } = render() - expect(getByText('test')).toBeInTheDocument() + const { getByText } = render() + expect(getByText('2')).toBeInTheDocument() }) it('should render with multiple badges', () => { - const { getByText } = render() - expect(getByText('test')).toBeInTheDocument() - expect(getByText('test2')).toBeInTheDocument() + const { getByText } = render() + expect(getByText('2')).toBeInTheDocument() + expect(getByText('0.01%')).toBeInTheDocument() }) }) diff --git a/apps/web/src/components/Liquidity/LiquidityPositionInfoBadges.tsx b/apps/web/src/components/Liquidity/LiquidityPositionInfoBadges.tsx index 1f8258f977c..1b7fc734108 100644 --- a/apps/web/src/components/Liquidity/LiquidityPositionInfoBadges.tsx +++ b/apps/web/src/components/Liquidity/LiquidityPositionInfoBadges.tsx @@ -1,5 +1,11 @@ +import { FeeAmount } from '@uniswap/v3-sdk' +import { isDynamicFeeTierAmount } from 'components/Liquidity/utils' +import { ZERO_ADDRESS } from 'constants/misc' +import { useMemo } from 'react' import { CopyHelper } from 'theme/components' import { styled, Text } from 'ui/src' +import { DocumentList } from 'ui/src/components/icons/DocumentList' +import { useTranslation } from 'uniswap/src/i18n/useTranslation' import { isAddress, shortenAddress } from 'utilities/src/addresses' export const PositionInfoBadge = styled(Text, { @@ -42,19 +48,39 @@ function getPlacement(index: number, length: number): 'start' | 'middle' | 'end' return length === 1 ? 'only' : index === 0 ? 'start' : index === length - 1 ? 'end' : 'middle' } -export interface BadgeData { +interface BadgeData { label: string copyable?: boolean icon?: JSX.Element } export function LiquidityPositionInfoBadges({ - badges, + versionLabel, + v4hook, + feeTier, size = 'default', }: { - badges: BadgeData[] + versionLabel?: string + v4hook?: string + feeTier?: string | FeeAmount size: 'small' | 'default' }): JSX.Element { + const { t } = useTranslation() + + const badges = useMemo(() => { + return [ + versionLabel ? { label: versionLabel } : undefined, + v4hook && v4hook !== ZERO_ADDRESS + ? { label: v4hook, copyable: true, icon: } + : undefined, + feeTier + ? isDynamicFeeTierAmount(feeTier) + ? { label: t('common.dynamic') } + : { label: `${Number(feeTier) / 10000}%` } + : undefined, + ].filter(Boolean) as BadgeData[] + }, [versionLabel, v4hook, feeTier, t]) + return ( <> {badges.map(({ label, copyable, icon }, index) => { diff --git a/apps/web/src/components/Liquidity/LiquidityPositionPriceRangeTile.tsx b/apps/web/src/components/Liquidity/LiquidityPositionPriceRangeTile.tsx index 0c76bfd7412..6a0f5cb9e72 100644 --- a/apps/web/src/components/Liquidity/LiquidityPositionPriceRangeTile.tsx +++ b/apps/web/src/components/Liquidity/LiquidityPositionPriceRangeTile.tsx @@ -1,7 +1,5 @@ // eslint-disable-next-line no-restricted-imports -import { PositionStatus } from '@uniswap/client-pools/dist/pools/v1/types_pb' import { Currency, Price } from '@uniswap/sdk-core' -import { LiquidityPositionStatusIndicator } from 'components/Liquidity/LiquidityPositionStatusIndicator' import { useGetRangeDisplay } from 'components/Liquidity/hooks' import { PriceOrdering } from 'components/PositionListItem' import { useMemo, useState } from 'react' @@ -19,7 +17,6 @@ const InnerTile = styled(Flex, { }) interface LiquidityPositionPriceRangeTileProps { - status?: PositionStatus priceOrdering: PriceOrdering token0CurrentPrice: Price token1CurrentPrice: Price @@ -29,7 +26,6 @@ interface LiquidityPositionPriceRangeTileProps { } export function LiquidityPositionPriceRangeTile({ - status, priceOrdering, token0CurrentPrice, token1CurrentPrice, @@ -77,9 +73,9 @@ export function LiquidityPositionPriceRangeTile({ - {status && } { @@ -87,7 +83,7 @@ export function LiquidityPositionPriceRangeTile({ }} /> - + diff --git a/apps/web/src/components/Liquidity/LiquidityPositionStatusIndicator.tsx b/apps/web/src/components/Liquidity/LiquidityPositionStatusIndicator.tsx index d132d262fd8..3b98d1762bf 100644 --- a/apps/web/src/components/Liquidity/LiquidityPositionStatusIndicator.tsx +++ b/apps/web/src/components/Liquidity/LiquidityPositionStatusIndicator.tsx @@ -26,8 +26,9 @@ export function LiquidityPositionStatusIndicator({ status }: { status: PositionS if (!config) { return null } + return ( - + diff --git a/apps/web/src/components/Liquidity/constants.ts b/apps/web/src/components/Liquidity/constants.ts new file mode 100644 index 00000000000..f810b86a374 --- /dev/null +++ b/apps/web/src/components/Liquidity/constants.ts @@ -0,0 +1,29 @@ +import { FeeAmount, TICK_SPACINGS } from '@uniswap/v3-sdk' +import { UniverseChainId } from 'uniswap/src/features/chains/types' + +interface FeeDataWithChain { + feeData: { + feeAmount: FeeAmount + tickSpacing: number + } + supportedChainIds?: UniverseChainId[] +} + +export const defaultFeeTiers: Record = { + [FeeAmount.LOWEST]: { feeData: { feeAmount: FeeAmount.LOWEST, tickSpacing: TICK_SPACINGS[FeeAmount.LOWEST] } }, + [FeeAmount.LOW_200]: { + feeData: { feeAmount: FeeAmount.LOW_200, tickSpacing: TICK_SPACINGS[FeeAmount.LOW_200] }, + supportedChainIds: [UniverseChainId.Base], + }, + [FeeAmount.LOW_300]: { + feeData: { feeAmount: FeeAmount.LOW_300, tickSpacing: TICK_SPACINGS[FeeAmount.LOW_300] }, + supportedChainIds: [UniverseChainId.Base], + }, + [FeeAmount.LOW_400]: { + feeData: { feeAmount: FeeAmount.LOW_400, tickSpacing: TICK_SPACINGS[FeeAmount.LOW_400] }, + supportedChainIds: [UniverseChainId.Base], + }, + [FeeAmount.LOW]: { feeData: { feeAmount: FeeAmount.LOW, tickSpacing: TICK_SPACINGS[FeeAmount.LOW] } }, + [FeeAmount.MEDIUM]: { feeData: { feeAmount: FeeAmount.MEDIUM, tickSpacing: TICK_SPACINGS[FeeAmount.MEDIUM] } }, + [FeeAmount.HIGH]: { feeData: { feeAmount: FeeAmount.HIGH, tickSpacing: TICK_SPACINGS[FeeAmount.HIGH] } }, +} as const diff --git a/apps/web/src/components/Liquidity/hooks.ts b/apps/web/src/components/Liquidity/hooks.ts index db2f6db3739..cb6e32a8b43 100644 --- a/apps/web/src/components/Liquidity/hooks.ts +++ b/apps/web/src/components/Liquidity/hooks.ts @@ -2,28 +2,27 @@ import { BigNumber } from '@ethersproject/bignumber' // eslint-disable-next-line no-restricted-imports import { ProtocolVersion } from '@uniswap/client-pools/dist/pools/v1/types_pb' import { Currency, CurrencyAmount, Percent, Price } from '@uniswap/sdk-core' -import { PositionInfo } from 'components/Liquidity/types' -import { calculateInvertedValues, parseV3FeeTier } from 'components/Liquidity/utils' +import { FeeTierData, PositionInfo } from 'components/Liquidity/types' +import { + calculateInvertedValues, + getDefaultFeeTiersForChainWithDynamicFeeTier, + mergeFeeTiers, + parseV3FeeTier, +} from 'components/Liquidity/utils' import { PriceOrdering, getPriceOrderingFromPositionForUI } from 'components/PositionListItem' import useIsTickAtLimit from 'hooks/useIsTickAtLimit' import JSBI from 'jsbi' import { OptionalCurrency } from 'pages/Pool/Positions/create/types' -import { getCurrencyAddressWithWrap, getSortedCurrenciesTuple } from 'pages/Pool/Positions/create/utils' +import { getCurrencyAddressForTradingApi, getSortedCurrenciesTupleWithWrap } from 'pages/Pool/Positions/create/utils' import { useMemo } from 'react' +import { LiquidityModalInitialState } from 'state/application/reducer' import { useAppSelector } from 'state/hooks' import { Bound } from 'state/mint/v3/actions' import { useGetPoolsByTokens } from 'uniswap/src/data/rest/getPools' import { useUSDCPrice } from 'uniswap/src/features/transactions/swap/hooks/useUSDCPrice' +import { useTranslation } from 'uniswap/src/i18n' import { NumberType, useFormatter } from 'utils/formatNumbers' -type FeeTierData = { - id: string - fee: number - formattedFee: string - totalLiquidityUsd: number - percentage: Percent -} - /** * @returns map of fee tier (in hundredths of bips) to more data about the Pool * @@ -32,24 +31,27 @@ export function useAllFeeTierPoolData({ chainId, protocolVersion, currencies, + withDynamicFeeTier = false, }: { chainId?: number protocolVersion: ProtocolVersion currencies: [OptionalCurrency, OptionalCurrency] -}): Record { + withDynamicFeeTier?: boolean +}): { feeTierData: Record; hasExistingFeeTiers: boolean } { + const { t } = useTranslation() const { formatPercent } = useFormatter() - const sortedCurrencies = getSortedCurrenciesTuple(currencies[0], currencies[1]) - const token0Address = getCurrencyAddressWithWrap(sortedCurrencies[0], protocolVersion) - const token1Address = getCurrencyAddressWithWrap(sortedCurrencies[1], protocolVersion) + const sortedCurrencies = getSortedCurrenciesTupleWithWrap(currencies[0], currencies[1], protocolVersion) + const { data: poolData } = useGetPoolsByTokens( { chainId, protocolVersions: [protocolVersion], - token0: token0Address, - token1: token1Address, + token0: getCurrencyAddressForTradingApi(sortedCurrencies[0]), + token1: getCurrencyAddressForTradingApi(sortedCurrencies[1]), }, Boolean(chainId && sortedCurrencies?.[0] && sortedCurrencies?.[1]), ) + return useMemo(() => { const liquiditySum = poolData?.pools.reduce( (sum, pool) => BigNumber.from(pool.totalLiquidityUsd.split('.')?.[0] ?? '0').add(sum), @@ -70,16 +72,31 @@ export function useAllFeeTierPoolData({ } else { feeTierData[feeTier] = { id: pool.poolId, - fee: pool.fee, + fee: { + feeAmount: pool.fee, + tickSpacing: pool.tickSpacing, + }, formattedFee: formatPercent(new Percent(pool.fee, 1000000)), totalLiquidityUsd: totalLiquidityUsdTruncated, percentage, - } + created: true, + } satisfies FeeTierData } } } - return feeTierData - }, [poolData, sortedCurrencies, formatPercent]) + + return { + feeTierData: mergeFeeTiers( + feeTierData, + Object.values( + getDefaultFeeTiersForChainWithDynamicFeeTier({ chainId, dynamicFeeTierEnabled: withDynamicFeeTier }), + ), + formatPercent, + t('fee.dynamic'), + ), + hasExistingFeeTiers: Object.values(feeTierData).length > 0, + } + }, [poolData, sortedCurrencies, chainId, withDynamicFeeTier, formatPercent, t]) } /** @@ -239,7 +256,7 @@ export function usePositionCurrentPrice(positionInfo?: PositionInfo) { /** * Parses the Positions API object from the modal state and returns the relevant information for the modals. */ -export function useModalLiquidityPositionInfo(): PositionInfo | undefined { +export function useModalLiquidityInitialState(): LiquidityModalInitialState | undefined { const modalState = useAppSelector((state) => state.application.openModal) return modalState?.initialState } diff --git a/apps/web/src/components/Liquidity/types.ts b/apps/web/src/components/Liquidity/types.ts index 4e21dd0311a..b68498e93c8 100644 --- a/apps/web/src/components/Liquidity/types.ts +++ b/apps/web/src/components/Liquidity/types.ts @@ -1,11 +1,13 @@ // eslint-disable-next-line no-restricted-imports import { PositionStatus, ProtocolVersion } from '@uniswap/client-pools/dist/pools/v1/types_pb' -import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core' +import { Currency, CurrencyAmount, Percent, Token } from '@uniswap/sdk-core' import { Pair } from '@uniswap/v2-sdk' import { FeeAmount, Pool as V3Pool, Position as V3Position } from '@uniswap/v3-sdk' import { Pool as V4Pool, Position as V4Position } from '@uniswap/v4-sdk' +import { FeeData } from 'pages/Pool/Positions/create/types' import { Dispatch, ReactNode, SetStateAction } from 'react' import { PositionField } from 'types/position' +import { UniverseChainId } from 'uniswap/src/features/chains/types' export interface DepositState { exactField: PositionField @@ -31,6 +33,7 @@ interface BasePositionInfo { version: ProtocolVersion currency0Amount: CurrencyAmount currency1Amount: CurrencyAmount + chainId: UniverseChainId tokenId?: string tickLower?: string tickUpper?: string @@ -55,6 +58,7 @@ export type V3PositionInfo = BasePositionInfo & { version: ProtocolVersion.V3 tokenId: string pool?: V3Pool + poolId?: string feeTier?: FeeAmount position?: V3Position v4hook: undefined @@ -64,9 +68,19 @@ type V4PositionInfo = BasePositionInfo & { version: ProtocolVersion.V4 tokenId: string pool?: V4Pool + poolId?: string position?: V4Position feeTier?: string v4hook?: string } export type PositionInfo = V2PairInfo | V3PositionInfo | V4PositionInfo + +export type FeeTierData = { + id?: string + fee: FeeData + formattedFee: string + totalLiquidityUsd: number + percentage: Percent + created: boolean +} diff --git a/apps/web/src/components/Liquidity/utils.test.ts b/apps/web/src/components/Liquidity/utils.test.ts new file mode 100644 index 00000000000..4c1f5817fc8 --- /dev/null +++ b/apps/web/src/components/Liquidity/utils.test.ts @@ -0,0 +1,66 @@ +import { HookFlag, getFlagsFromContractAddress } from 'components/Liquidity/utils' + +describe('getFlagsFromContractAddress', () => { + it('should return an empty array for an address with no flags', () => { + const address = '0x1234567890123456789012345678901234560000' + expect(getFlagsFromContractAddress(address)).toEqual([]) + }) + + it('should correctly identify a single flag', () => { + const address = '0x1234567890123456789012345678901234560200' + expect(getFlagsFromContractAddress(address)).toEqual([HookFlag.BeforeRemoveLiquidity]) + }) + + it('should correctly identify multiple flags', () => { + const address = '0x1234567890123456789012345678901234567FFF' + expect(getFlagsFromContractAddress(address)).toEqual([ + HookFlag.BeforeRemoveLiquidity, + HookFlag.AfterRemoveLiquidity, + HookFlag.BeforeAddLiquidity, + HookFlag.AfterAddLiquidity, + HookFlag.BeforeSwap, + HookFlag.AfterSwap, + HookFlag.BeforeDonate, + HookFlag.AfterDonate, + HookFlag.BeforeSwapReturnsDelta, + HookFlag.AfterSwapReturnsDelta, + HookFlag.AfterAddLiquidityReturnsDelta, + HookFlag.AfterRemoveLiquidityReturnsDelta, + ]) + }) + + it('should correctly identify a mix of flags (case 1)', () => { + const address = '0x123456789012345678901234567890123456789A' + expect(getFlagsFromContractAddress(address)).toEqual([ + HookFlag.BeforeAddLiquidity, + HookFlag.BeforeSwap, + HookFlag.AfterDonate, + HookFlag.BeforeSwapReturnsDelta, + HookFlag.AfterAddLiquidityReturnsDelta, + ]) + }) + + it('should correctly identify a mix of flags (case 2)', () => { + const address = '0x12345678901234567890123456789012345678C0' + expect(getFlagsFromContractAddress(address)).toEqual([ + HookFlag.BeforeAddLiquidity, + HookFlag.BeforeSwap, + HookFlag.AfterSwap, + ]) + }) + + it('should correctly identify a mix of flags (case 3)', () => { + const address = '0x123456789012345678901234567890123456780B' + expect(getFlagsFromContractAddress(address)).toEqual([ + HookFlag.BeforeAddLiquidity, + HookFlag.BeforeSwapReturnsDelta, + HookFlag.AfterAddLiquidityReturnsDelta, + HookFlag.AfterRemoveLiquidityReturnsDelta, + ]) + }) + + it('should correctly identify a mix of flags (case 4)', () => { + const address = '0x0000000000000000000000000000000000002400' + expect(getFlagsFromContractAddress(address)).toEqual([HookFlag.AfterAddLiquidity]) + }) +}) diff --git a/apps/web/src/components/Liquidity/utils.tsx b/apps/web/src/components/Liquidity/utils.tsx index c861704bc89..7544c9db494 100644 --- a/apps/web/src/components/Liquidity/utils.tsx +++ b/apps/web/src/components/Liquidity/utils.tsx @@ -9,15 +9,23 @@ import { Position as RestPosition, Token as RestToken, } from '@uniswap/client-pools/dist/pools/v1/types_pb' -import { Currency, CurrencyAmount, Price, Token } from '@uniswap/sdk-core' +import { Currency, CurrencyAmount, Percent, Price, Token } from '@uniswap/sdk-core' import { Pair } from '@uniswap/v2-sdk' import { FeeAmount, Pool as V3Pool, Position as V3Position } from '@uniswap/v3-sdk' import { Pool as V4Pool, Position as V4Position } from '@uniswap/v4-sdk' -import { PositionInfo } from 'components/Liquidity/types' +import { defaultFeeTiers } from 'components/Liquidity/constants' +import { FeeTierData, PositionInfo } from 'components/Liquidity/types' import { ZERO_ADDRESS } from 'constants/misc' +import { DYNAMIC_FEE_DATA, DynamicFeeData, FeeData } from 'pages/Pool/Positions/create/types' +import { GeneratedIcon } from 'ui/src' +import { Flag } from 'ui/src/components/icons/Flag' +import { Pools } from 'ui/src/components/icons/Pools' +import { SwapCoin } from 'ui/src/components/icons/SwapCoin' import { AppTFunction } from 'ui/src/i18n/types' import { nativeOnChain } from 'uniswap/src/constants/tokens' import { ProtocolItems } from 'uniswap/src/data/tradingApi/__generated__' +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' +import { UniverseChainId } from 'uniswap/src/features/chains/types' export function getProtocolVersionLabel(version: ProtocolVersion): string | undefined { switch (version) { @@ -68,6 +76,16 @@ export function parseProtocolVersion(version: string | undefined): ProtocolVersi } } +export function getPositionUrl(position: PositionInfo): string { + const chainInfo = getChainInfo(position.chainId) + if (position.version === ProtocolVersion.V2) { + return `/positions/v2/${chainInfo.urlParam}/${position.liquidityToken.address}` + } else if (position.version === ProtocolVersion.V3) { + return `/positions/v3/${chainInfo.urlParam}/${position.tokenId}` + } + return `/positions/v4/${chainInfo.urlParam}/${position.tokenId}` +} + export function parseV3FeeTier(feeTier: string | undefined): FeeAmount | undefined { const parsedFee = parseInt(feeTier || '') @@ -223,6 +241,7 @@ export function parseRestPosition(position?: RestPosition): PositionInfo | undef version: ProtocolVersion.V2, pair, liquidityToken, + chainId: token0.chainId, currency0Amount: CurrencyAmount.fromRawAmount(token0, v2PairPosition.liquidity0), currency1Amount: CurrencyAmount.fromRawAmount(token1, v2PairPosition.liquidity1), totalSupply: CurrencyAmount.fromRawAmount(liquidityToken, v2PairPosition.totalSupply), @@ -253,7 +272,9 @@ export function parseRestPosition(position?: RestPosition): PositionInfo | undef status: position.status, feeTier: parseV3FeeTier(v3Position.feeTier), version: ProtocolVersion.V3, + chainId: token0.chainId, pool, + poolId: position.position.value.poolId, position: sdkPosition, tickLower: v3Position.tickLower, tickUpper: v3Position.tickUpper, @@ -285,19 +306,22 @@ export function parseRestPosition(position?: RestPosition): PositionInfo | undef tickUpper: Number(v4Position.tickUpper), }) : undefined + const poolId = V4Pool.getPoolId(token0, token1, Number(v4Position.feeTier), Number(v4Position.tickSpacing), hook) return { status: position.status, - feeTier: v4Position?.feeTier, + feeTier: v4Position.feeTier, version: ProtocolVersion.V4, position: sdkPosition, + chainId: token0.chainId, pool, + poolId, v4hook: hook, tokenId: v4Position.tokenId, - tickLower: v4Position?.tickLower, - tickUpper: v4Position?.tickUpper, - tickSpacing: Number(v4Position?.tickSpacing), - currency0Amount: CurrencyAmount.fromRawAmount(token0, v4Position?.amount0 ?? 0), - currency1Amount: CurrencyAmount.fromRawAmount(token1, v4Position?.amount1 ?? 0), + tickLower: v4Position.tickLower, + tickUpper: v4Position.tickUpper, + tickSpacing: Number(v4Position.tickSpacing), + currency0Amount: CurrencyAmount.fromRawAmount(token0, v4Position.amount0 ?? 0), + currency1Amount: CurrencyAmount.fromRawAmount(token1, v4Position.amount1 ?? 0), token0UncollectedFees: v4Position.token0UncollectedFees, token1UncollectedFees: v4Position.token1UncollectedFees, liquidity: v4Position.liquidity, @@ -352,3 +376,220 @@ export function calculateInvertedPrice({ price, invert }: { price?: Price (parseInt(relevantBits, 2) & bitPosition) !== 0) + .map(([flag]) => flag as HookFlag) + + return activeFlags +} + +export interface FlagWarning { + Icon: GeneratedIcon + name: string + info: string + dangerous: boolean +} + +export function getFlagWarning(flag: HookFlag, t: AppTFunction): FlagWarning | undefined { + switch (flag) { + case HookFlag.BeforeSwap: + case HookFlag.BeforeSwapReturnsDelta: + return { + Icon: SwapCoin, + name: t('common.swap'), + info: t('position.hook.swapWarning'), + dangerous: false, + } + case HookFlag.BeforeAddLiquidity: + case HookFlag.AfterAddLiquidity: + return { + Icon: Pools, + name: t('common.addLiquidity'), + info: t('position.hook.liquidityWarning'), + dangerous: false, + } + case HookFlag.BeforeRemoveLiquidity: + case HookFlag.AfterRemoveLiquidity: + return { + Icon: Flag, + name: t('common.flag'), + info: t('position.hook.removeWarning'), + dangerous: true, + } + default: + return undefined + } +} + +export function mergeFeeTiers( + feeTiers: Record, + feeData: FeeData[], + formatPercent: (percent: Percent | undefined) => string, + formattedDynamicFeeTier: string, +): Record { + const result: Record = {} + for (const feeTier of feeData) { + result[feeTier.feeAmount] = { + fee: feeTier, + formattedFee: isDynamicFeeTier(feeTier) + ? formattedDynamicFeeTier + : formatPercent(new Percent(feeTier.feeAmount, 1000000)), + totalLiquidityUsd: 0, + percentage: new Percent(0, 100), + created: false, + } satisfies FeeTierData + } + + return { ...result, ...feeTiers } +} + +function getDefaultFeeTiersForChain( + chainId?: UniverseChainId, +): Record { + const feeData = Object.values(defaultFeeTiers) + .filter((feeTier) => !feeTier.supportedChainIds || (chainId && feeTier.supportedChainIds.includes(chainId))) + .map((feeTier) => feeTier.feeData) + + return feeData.reduce( + (acc, fee) => { + acc[fee.feeAmount] = fee + return acc + }, + {} as Record, + ) +} + +export function getDefaultFeeTiersForChainWithDynamicFeeTier({ + chainId, + dynamicFeeTierEnabled, +}: { + chainId?: UniverseChainId + dynamicFeeTierEnabled: boolean +}) { + if (!dynamicFeeTierEnabled) { + return getDefaultFeeTiersForChain(chainId) + } + + return { ...getDefaultFeeTiersForChain(chainId), [DYNAMIC_FEE_DATA.feeAmount]: DYNAMIC_FEE_DATA } +} + +export function getDefaultFeeTiersWithData({ + chainId, + feeTierData, + t, +}: { + chainId?: UniverseChainId + feeTierData: Record + t: AppTFunction +}) { + const defaultFeeTiersForChain = getDefaultFeeTiersForChain(chainId) + + const feeTiers = [ + { + tier: FeeAmount.LOWEST, + value: defaultFeeTiersForChain[FeeAmount.LOWEST], + title: t(`fee.bestForVeryStable`), + selectionPercent: feeTierData[FeeAmount.LOWEST]?.percentage, + }, + { + tier: FeeAmount.LOW_200, + value: defaultFeeTiersForChain[FeeAmount.LOW_200], + title: '', + selectionPercent: feeTierData[FeeAmount.LOW_200]?.percentage, + }, + { + tier: FeeAmount.LOW_300, + value: defaultFeeTiersForChain[FeeAmount.LOW_300], + title: '', + selectionPercent: feeTierData[FeeAmount.LOW_300]?.percentage, + }, + { + tier: FeeAmount.LOW_400, + value: defaultFeeTiersForChain[FeeAmount.LOW_400], + title: '', + selectionPercent: feeTierData[FeeAmount.LOW_400]?.percentage, + }, + { + tier: FeeAmount.LOW, + value: defaultFeeTiersForChain[FeeAmount.LOW], + title: t(`fee.bestForStablePairs`), + selectionPercent: feeTierData[FeeAmount.LOW]?.percentage, + }, + { + tier: FeeAmount.MEDIUM, + value: defaultFeeTiersForChain[FeeAmount.MEDIUM], + title: t(`fee.bestForMost`), + selectionPercent: feeTierData[FeeAmount.MEDIUM]?.percentage, + }, + { + tier: FeeAmount.HIGH, + value: defaultFeeTiersForChain[FeeAmount.HIGH], + title: t(`fee.bestForExotic`), + selectionPercent: feeTierData[FeeAmount.HIGH]?.percentage, + }, + ] as const + + return feeTiers.filter((feeTier) => Object.keys(feeTierData).includes(feeTier.tier.toString())) +} + +export function isDynamicFeeTier(feeData: FeeData): feeData is DynamicFeeData { + return feeData.feeAmount === DYNAMIC_FEE_DATA.feeAmount +} + +export function isDynamicFeeTierAmount( + feeAmount: string | number | undefined, +): feeAmount is DynamicFeeData['feeAmount'] { + if (!feeAmount) { + return false + } + + const feeAmountNumber = Number(feeAmount) + if (isNaN(feeAmountNumber)) { + return false + } + + return feeAmountNumber === DYNAMIC_FEE_DATA.feeAmount +} diff --git a/apps/web/src/components/LiquidityChartRangeInput/index.tsx b/apps/web/src/components/LiquidityChartRangeInput/index.tsx index c767386cd05..1ac29ef9ce4 100644 --- a/apps/web/src/components/LiquidityChartRangeInput/index.tsx +++ b/apps/web/src/components/LiquidityChartRangeInput/index.tsx @@ -1,7 +1,6 @@ import { Currency, Price } from '@uniswap/sdk-core' import { FeeAmount } from '@uniswap/v3-sdk' import { AutoColumn, ColumnCenter } from 'components/deprecated/Column' -import Loader from 'components/Icons/LoadingSpinner' import { Chart } from 'components/LiquidityChartRangeInput/Chart' import { useDensityChartData } from 'components/LiquidityChartRangeInput/hooks' import { ZoomLevels } from 'components/LiquidityChartRangeInput/types' @@ -13,6 +12,7 @@ import { BarChart2, CloudOff, Inbox } from 'react-feather' import { batch } from 'react-redux' import { Bound } from 'state/mint/v3/actions' import { ThemedText } from 'theme/components' +import { Flex, Shine } from 'ui/src' import { Trans } from 'uniswap/src/i18n' import { useFormatter } from 'utils/formatNumbers' @@ -63,6 +63,22 @@ function InfoBox({ message, icon }: { message?: ReactNode; icon: ReactNode }) { ) } +function LoadingBar({ height }: { height: string }) { + return +} + +function LoadingBars() { + return ( + + + {[10, 20, 45, 70, 80, 55, 30, 15].map((h) => ( + + ))} + + + ) +} + export default function LiquidityChartRangeInput({ currencyA, currencyB, @@ -161,14 +177,14 @@ export default function LiquidityChartRangeInput({ [formatDelta, isSorted, price, ticksAtLimit], ) - const isUninitialized = !currencyA || !currencyB || (formattedData === undefined && !isLoading) + const isUninitialized = !currencyA || !currencyB || (formattedData === undefined && !isLoading && !error) return ( {isUninitialized ? ( } icon={} /> ) : isLoading ? ( - } /> + ) : error ? ( } diff --git a/apps/web/src/components/Logo/ChainLogo.tsx b/apps/web/src/components/Logo/ChainLogo.tsx index 1b71c227bb6..e1b34be1cb1 100644 --- a/apps/web/src/components/Logo/ChainLogo.tsx +++ b/apps/web/src/components/Logo/ChainLogo.tsx @@ -1,4 +1,3 @@ -import { getChain, useIsSupportedChainId } from 'constants/chains' import { CSSProperties } from 'react' import { useIsDarkMode } from 'theme/components/ThemeToggle' import { @@ -16,7 +15,9 @@ import { ZKSYNC_LOGO, ZORA_LOGO, } from 'ui/src/assets' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' +import { useIsSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' type ChainUI = { symbol: string; bgColor: string; textColor: string } @@ -162,7 +163,7 @@ export function ChainLogo({ if (!isSupportedChain) { return null } - const { label } = getChain({ chainId }) + const { label } = getChainInfo(chainId) const { symbol } = getChainUI(chainId, darkMode) const iconSize = fillContainer ? '100%' : size + 'px' diff --git a/apps/web/src/components/Logo/DoubleLogo.tsx b/apps/web/src/components/Logo/DoubleLogo.tsx index db542ebecf9..4353f8777e2 100644 --- a/apps/web/src/components/Logo/DoubleLogo.tsx +++ b/apps/web/src/components/Logo/DoubleLogo.tsx @@ -5,7 +5,7 @@ import { useCurrencyInfo } from 'hooks/Tokens' import styled, { css } from 'lib/styled-components' import { memo, useState } from 'react' import { useColorSchemeFromSeed } from 'ui/src' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' const MissingImageLogo = styled.div<{ $size?: string; $textColor: string; $backgroundColor: string }>` --size: ${({ $size }) => $size}; diff --git a/apps/web/src/components/Logo/QueryTokenLogo.tsx b/apps/web/src/components/Logo/QueryTokenLogo.tsx index e244d129fb0..46c8879f65c 100644 --- a/apps/web/src/components/Logo/QueryTokenLogo.tsx +++ b/apps/web/src/components/Logo/QueryTokenLogo.tsx @@ -1,23 +1,22 @@ import { PortfolioLogo } from 'components/AccountDrawer/MiniPortfolio/PortfolioLogo' import { AssetLogoBaseProps } from 'components/Logo/AssetLogo' -import { getChainFromChainUrlParam } from 'constants/chains' import { NATIVE_CHAIN_ID } from 'constants/tokens' import { GqlSearchToken } from 'graphql/data/SearchTokens' import { TokenQueryData } from 'graphql/data/Token' -import { TopToken } from 'graphql/data/TopTokens' +import { TopToken } from 'graphql/data/types' import { gqlToCurrency } from 'graphql/data/util' import useNativeCurrency from 'lib/hooks/useNativeCurrency' import { useMemo } from 'react' import { TokenStat } from 'state/explore/types' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { getChainIdFromChainUrlParam } from 'utils/chainParams' export default function QueryTokenLogo( props: AssetLogoBaseProps & { token?: TopToken | TokenQueryData | GqlSearchToken | TokenStat }, ) { - const chain = getChainFromChainUrlParam(props.token?.chain.toLowerCase()) - const chainId = chain?.id ?? UniverseChainId.Mainnet + const chainId = getChainIdFromChainUrlParam(props.token?.chain.toLowerCase()) ?? UniverseChainId.Mainnet const isNative = props.token?.address === NATIVE_CHAIN_ID const isTokenStat = !!props.token && 'volume' in props.token diff --git a/apps/web/src/components/NavBar/ChainSelector/ChainSelectorRow.test.tsx b/apps/web/src/components/NavBar/ChainSelector/ChainSelectorRow.test.tsx index 6b293d814c9..acc6ce0632b 100644 --- a/apps/web/src/components/NavBar/ChainSelector/ChainSelectorRow.test.tsx +++ b/apps/web/src/components/NavBar/ChainSelector/ChainSelectorRow.test.tsx @@ -1,6 +1,6 @@ import ChainSelectorRow from 'components/NavBar/ChainSelector/ChainSelectorRow' import { render } from 'test-utils/render' -import { SUPPORTED_CHAIN_IDS, UniverseChainId } from 'uniswap/src/types/chains' +import { SUPPORTED_CHAIN_IDS, UniverseChainId } from 'uniswap/src/features/chains/types' describe('ChainSelectorRow', () => { SUPPORTED_CHAIN_IDS.forEach((chainId) => { diff --git a/apps/web/src/components/NavBar/ChainSelector/ChainSelectorRow.tsx b/apps/web/src/components/NavBar/ChainSelector/ChainSelectorRow.tsx index da5c0021d11..fd8778a086d 100644 --- a/apps/web/src/components/NavBar/ChainSelector/ChainSelectorRow.tsx +++ b/apps/web/src/components/NavBar/ChainSelector/ChainSelectorRow.tsx @@ -1,13 +1,14 @@ import Loader from 'components/Icons/LoadingSpinner' import { ChainLogo } from 'components/Logo/ChainLogo' -import { getChain, useSupportedChainId } from 'constants/chains' import styled, { useTheme } from 'lib/styled-components' import { Check } from 'react-feather' import { useSwapAndLimitContext } from 'state/swap/useSwapContext' +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' +import { useSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import Trace from 'uniswap/src/features/telemetry/Trace' import { SectionName } from 'uniswap/src/features/telemetry/constants' import { Trans } from 'uniswap/src/i18n' -import { UniverseChainId } from 'uniswap/src/types/chains' const LOGO_SIZE = 20 @@ -63,10 +64,10 @@ interface ChainSelectorRowProps { export default function ChainSelectorRow({ disabled, targetChain, onSelectChain, isPending }: ChainSelectorRowProps) { const theme = useTheme() const { chainId } = useSwapAndLimitContext() - const supportedChain = useSupportedChainId(targetChain) + const supportedChainId = useSupportedChainId(targetChain) const active = chainId === targetChain - const chainInfo = getChain({ chainId: supportedChain }) + const chainInfo = supportedChainId ? getChainInfo(supportedChainId) : undefined const label = chainInfo?.label return ( diff --git a/apps/web/src/components/NavBar/ChainSelector/__snapshots__/ChainSelectorRow.test.tsx.snap b/apps/web/src/components/NavBar/ChainSelector/__snapshots__/ChainSelectorRow.test.tsx.snap index d6427200e1c..fbf6e7ea95b 100644 --- a/apps/web/src/components/NavBar/ChainSelector/__snapshots__/ChainSelectorRow.test.tsx.snap +++ b/apps/web/src/components/NavBar/ChainSelector/__snapshots__/ChainSelectorRow.test.tsx.snap @@ -498,6 +498,13 @@ exports[`ChainSelectorRow should match snapshot for chainId 480 1`] = ` } .c1 { + grid-column: 2; + grid-row: 1; + font-size: 16px; + font-weight: 485; +} + +.c2 { grid-column: 3; grid-row: 1; display: -webkit-box; @@ -524,10 +531,23 @@ exports[`ChainSelectorRow should match snapshot for chainId 480 1`] = ` > diff --git a/apps/web/src/components/NavBar/ChainSelector/index.tsx b/apps/web/src/components/NavBar/ChainSelector/index.tsx index eb795ac6d92..56834943f29 100644 --- a/apps/web/src/components/NavBar/ChainSelector/index.tsx +++ b/apps/web/src/components/NavBar/ChainSelector/index.tsx @@ -1,4 +1,3 @@ -import { CHAIN_IDS_TO_NAMES, useIsSupportedChainIdCallback } from 'constants/chains' import { useAccount } from 'hooks/useAccount' import useSelectChain from 'hooks/useSelectChain' import { useCallback, useRef } from 'react' @@ -6,8 +5,9 @@ import { useSearchParams } from 'react-router-dom' import { useSwapAndLimitContext } from 'state/swap/useSwapContext' import { Flex, Popover } from 'ui/src' import { NetworkFilter } from 'uniswap/src/components/network/NetworkFilter' -import { useEnabledChains } from 'uniswap/src/features/settings/hooks' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' +import { useEnabledChains, useIsSupportedChainIdCallback } from 'uniswap/src/features/chains/hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' type ChainSelectorProps = { hideArrow?: boolean @@ -34,7 +34,7 @@ export const ChainSelector = ({ hideArrow }: ChainSelectorProps) => { searchParams.delete('outputCurrency') searchParams.delete('value') searchParams.delete('field') - targetChainId && searchParams.set('chain', CHAIN_IDS_TO_NAMES[targetChainId]) + targetChainId && searchParams.set('chain', getChainInfo(targetChainId).interfaceName) setSearchParams(searchParams) popoverRef.current?.close() diff --git a/apps/web/src/components/NavBar/PreferencesMenu/Preferences.tsx b/apps/web/src/components/NavBar/PreferencesMenu/Preferences.tsx index 3d3d4597dca..4eae8e4d40e 100644 --- a/apps/web/src/components/NavBar/PreferencesMenu/Preferences.tsx +++ b/apps/web/src/components/NavBar/PreferencesMenu/Preferences.tsx @@ -1,12 +1,11 @@ import { PreferencesHeader } from 'components/NavBar/PreferencesMenu/Header' import { PreferencesView } from 'components/NavBar/PreferencesMenu/shared' -import { useActiveLocalCurrency } from 'hooks/useActiveLocalCurrency' -import { useActiveLanguage } from 'hooks/useActiveLocale' import styled, { useTheme } from 'lib/styled-components' import { ChevronRight } from 'react-feather' import { ThemeSelector } from 'theme/components/ThemeToggle' import { Text } from 'ui/src' -import { useLanguageInfo } from 'uniswap/src/features/language/hooks' +import { useAppFiatCurrency } from 'uniswap/src/features/fiatCurrency/hooks' +import { useCurrentLanguage, useLanguageInfo } from 'uniswap/src/features/language/hooks' import { Trans, t } from 'uniswap/src/i18n' const Pref = styled.div` @@ -58,8 +57,8 @@ export function PreferenceSettings({ setSettingsView: (view: PreferencesView) => void showHeader?: boolean }) { - const activeLocalCurrency = useActiveLocalCurrency() - const activeLanguage = useActiveLanguage() + const activeLocalCurrency = useAppFiatCurrency() + const activeLanguage = useCurrentLanguage() const languageInfo = useLanguageInfo(activeLanguage) const items: SettingItem[] = [ diff --git a/apps/web/src/components/NavBar/SearchBar/RecentlySearchedAssets.ts b/apps/web/src/components/NavBar/SearchBar/RecentlySearchedAssets.ts index 0990d8016f1..f7caf9eef69 100644 --- a/apps/web/src/components/NavBar/SearchBar/RecentlySearchedAssets.ts +++ b/apps/web/src/components/NavBar/SearchBar/RecentlySearchedAssets.ts @@ -1,16 +1,16 @@ -import { chainIdToBackendChain } from 'constants/chains' import { NATIVE_CHAIN_ID } from 'constants/tokens' import { GqlSearchToken } from 'graphql/data/SearchTokens' import { GenieCollection } from 'nft/types' import { useMemo } from 'react' import { useSelector } from 'react-redux' -import { MAX_RECENT_SEARCH_RESULTS } from 'uniswap/src/components/TokenSelector/hooks' +import { MAX_RECENT_SEARCH_RESULTS } from 'uniswap/src/components/TokenSelector/constants' import { nativeOnChain } from 'uniswap/src/constants/tokens' import { Chain, NftCollection, useRecentlySearchedAssetsQuery, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { toGraphQLChain } from 'uniswap/src/features/chains/utils' import { SearchResult, isNFTCollectionSearchResult, @@ -18,7 +18,6 @@ import { } from 'uniswap/src/features/search/SearchResult' import { selectSearchHistory } from 'uniswap/src/features/search/selectSearchHistory' import { isNativeCurrencyAddress } from 'uniswap/src/utils/currencyId' -import { logger } from 'utilities/src/logger/logger' export type InterfaceRemoteSearchHistoryItem = GqlSearchToken | GenieCollection @@ -31,7 +30,7 @@ export function useRecentlySearchedAssets(): { data?: InterfaceRemoteSearchHisto collectionAddresses: shortenedHistory.filter(isNFTCollectionSearchResult).map((asset) => asset.address), contracts: shortenedHistory.filter(isTokenSearchResult).map((token) => ({ address: token.address ?? undefined, - chain: chainIdToBackendChain({ chainId: token.chainId }), + chain: toGraphQLChain(token.chainId), })), }, skip: shortenedHistory.length === 0, @@ -101,17 +100,7 @@ function generateInterfaceHistoryItem( // Handle native assets if (isNativeCurrencyAddress(asset.chainId, asset.address)) { // Handles special case where wMATIC data needs to be used for MATIC - const chain = chainIdToBackendChain({ chainId: asset.chainId }) - if (!chain) { - logger.error(new Error('Invalid chain retrieved from Search Token/Collection Query'), { - tags: { - file: 'RecentlySearchedAssets', - function: 'useRecentlySearchedAssets', - }, - extra: { asset }, - }) - return undefined - } + const chain = toGraphQLChain(asset.chainId) const native = nativeOnChain(asset.chainId) const queryAddress = asset.address ?? getNativeQueryAddress(chain) const result = resultsMap[queryAddress] diff --git a/apps/web/src/components/NavBar/SearchBar/SearchBarDropdown.tsx b/apps/web/src/components/NavBar/SearchBar/SearchBarDropdown.tsx index a47d32eef28..69b51a03464 100644 --- a/apps/web/src/components/NavBar/SearchBar/SearchBarDropdown.tsx +++ b/apps/web/src/components/NavBar/SearchBar/SearchBarDropdown.tsx @@ -9,7 +9,6 @@ import { SkeletonRow, SuggestionRow } from 'components/NavBar/SearchBar/Suggesti import QuestionHelper from 'components/QuestionHelper' import { SuspendConditionally } from 'components/Suspense/SuspendConditionally' import { SuspenseWithPreviousRenderAsFallback } from 'components/Suspense/SuspenseWithPreviousRenderAsFallback' -import { BACKEND_NOT_YET_SUPPORTED_CHAIN_IDS } from 'constants/chains' import { GqlSearchToken } from 'graphql/data/SearchTokens' import useTrendingTokens from 'graphql/data/TrendingTokens' import { useTrendingCollections } from 'graphql/data/nft/TrendingCollections' @@ -23,15 +22,16 @@ import { Clock, TrendingUp } from 'react-feather' import { useLocation } from 'react-router-dom' import { ThemedText } from 'theme/components' import { Flex, Text, useScrollbarStyles } from 'ui/src' -import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' import { HistoryDuration, SafetyLevel, Token, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { isBackendSupportedChainId } from 'uniswap/src/features/chains/utils' import { InterfaceSearchResultSelectionProperties } from 'uniswap/src/features/telemetry/types' import { Trans } from 'uniswap/src/i18n' -import { UniverseChainId } from 'uniswap/src/types/chains' import { useTrace } from 'utilities/src/telemetry/trace/TraceContext' interface SearchBarDropdownSectionProps { @@ -122,8 +122,7 @@ interface SearchBarDropdownProps { export function SearchBarDropdown(props: SearchBarDropdownProps) { const { isLoading } = props const account = useAccount() - const showChainComingSoonBadge = - account.chainId && BACKEND_NOT_YET_SUPPORTED_CHAIN_IDS.includes(account.chainId) && !isLoading + const showChainComingSoonBadge = account.chainId && !isBackendSupportedChainId(account.chainId) && !isLoading const scrollBarStyles = useScrollbarStyles() return ( @@ -366,8 +365,6 @@ function SearchBarDropdownContents({ } function ComingSoonText({ chainId }: { chainId: UniverseChainId }) { - const chainName = UNIVERSE_CHAIN_INFO[chainId]?.name - return BACKEND_NOT_YET_SUPPORTED_CHAIN_IDS.includes(chainId) ? ( - - ) : null + const chainName = getChainInfo(chainId).name + return !isBackendSupportedChainId(chainId) ? : null } diff --git a/apps/web/src/components/NavBar/SearchBar/SuggestionRow.tsx b/apps/web/src/components/NavBar/SearchBar/SuggestionRow.tsx index c268316741c..198fc067c43 100644 --- a/apps/web/src/components/NavBar/SearchBar/SuggestionRow.tsx +++ b/apps/web/src/components/NavBar/SearchBar/SuggestionRow.tsx @@ -7,7 +7,8 @@ import Column from 'components/deprecated/Column' import { useTokenWarning } from 'constants/deprecatedTokenSafety' import { NATIVE_CHAIN_ID } from 'constants/tokens' import { GqlSearchToken } from 'graphql/data/SearchTokens' -import { getTokenDetailsURL, supportedChainIdFromGQLChain } from 'graphql/data/util' +import { gqlTokenToCurrencyInfo } from 'graphql/data/types' +import { getTokenDetailsURL } from 'graphql/data/util' import styled, { css } from 'lib/styled-components' import { searchGenieCollectionToTokenSearchResult, searchTokenToTokenSearchResult } from 'lib/utils/searchBar' import { GenieCollection } from 'nft/types' @@ -17,12 +18,17 @@ import { Link, useNavigate } from 'react-router-dom' import { EllipsisStyle, ThemedText } from 'theme/components' import { Flex } from 'ui/src' import { Verified } from 'ui/src/components/icons/Verified' -import { TokenStandard } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import WarningIcon from 'uniswap/src/components/warnings/WarningIcon' +import { Token, TokenStandard } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { fromGraphQLChain } from 'uniswap/src/features/chains/utils' +import { FeatureFlags } from 'uniswap/src/features/gating/flags' +import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { addToSearchHistory } from 'uniswap/src/features/search/searchHistorySlice' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { InterfaceSearchResultSelectionProperties } from 'uniswap/src/features/telemetry/types' +import { getTokenWarningSeverity } from 'uniswap/src/features/tokens/safetyUtils' import { Trans, useTranslation } from 'uniswap/src/i18n' -import { UniverseChainId } from 'uniswap/src/types/chains' import { shortenAddress } from 'uniswap/src/utils/addresses' import { NumberType, useFormatter } from 'utils/formatNumbers' @@ -105,17 +111,23 @@ export function SuggestionRow({ const navigate = useNavigate() const { formatFiatPrice, formatDelta, formatNumberOrString } = useFormatter() const [brokenCollectionImage, setBrokenCollectionImage] = useState(false) + const { defaultChainId } = useEnabledChains() + + const tokenProtectionEnabled = useFeatureFlag(FeatureFlags.TokenProtection) const warning = useTokenWarning( isToken ? suggestion.address : undefined, - isToken ? supportedChainIdFromGQLChain(suggestion.chain) : UniverseChainId.Mainnet, + isToken ? fromGraphQLChain(suggestion.chain) ?? undefined : defaultChainId, ) + const tokenWarningSeverity = isToken + ? getTokenWarningSeverity(gqlTokenToCurrencyInfo(suggestion as Token)) // casting GqlSearchToken to Token + : undefined const handleClick = useCallback(() => { const address = !suggestion.address && suggestion.standard === TokenStandard.Native ? NATIVE_CHAIN_ID : suggestion.address if (isToken && address) { - const chainId = supportedChainIdFromGQLChain(suggestion.chain) + const chainId = fromGraphQLChain(suggestion.chain) if (chainId) { const searchResult = searchTokenToTokenSearchResult({ ...suggestion, address, chainId }) dispatch(addToSearchHistory({ searchResult })) @@ -180,9 +192,23 @@ export function SuggestionRow({ /> )} - + {suggestion.name} - {isToken ? : suggestion.isVerified && } + {isToken ? ( + tokenProtectionEnabled ? ( + + ) : ( + + ) + ) : ( + suggestion.isVerified && + )} diff --git a/apps/web/src/components/NavBar/Tabs/TabsContent.tsx b/apps/web/src/components/NavBar/Tabs/TabsContent.tsx index 4f9979e68eb..680c403b417 100644 --- a/apps/web/src/components/NavBar/Tabs/TabsContent.tsx +++ b/apps/web/src/components/NavBar/Tabs/TabsContent.tsx @@ -25,7 +25,6 @@ export type TabsItem = MenuItem & { export const useTabsContent = (props?: { includeNftsLink?: boolean }): TabsSection[] => { const { t } = useTranslation() - const isMultichainExploreEnabled = useFeatureFlag(FeatureFlags.MultichainExplore) const isV4EverywhereEnabled = useFeatureFlag(FeatureFlags.V4Everywhere) const { pathname } = useLocation() const theme = useTheme() @@ -77,7 +76,7 @@ export const useTabsContent = (props?: { includeNftsLink?: boolean }): TabsSecti { label: t('common.transactions'), quickKey: 'X', - href: `/explore/transactions${isMultichainExploreEnabled ? '/ethereum' : ''}`, + href: '/explore/transactions/ethereum', internal: true, }, { label: t('common.nfts'), quickKey: 'N', href: '/nfts', internal: true }, @@ -85,11 +84,11 @@ export const useTabsContent = (props?: { includeNftsLink?: boolean }): TabsSecti }, { title: t('common.pool'), - href: '/pool', + href: isV4EverywhereEnabled ? '/positions' : '/pool', isActive: pathname.startsWith('/pool'), items: [ { - label: t('nav.tabs.viewPosition'), + label: t('nav.tabs.viewPositions'), quickKey: 'V', href: isV4EverywhereEnabled ? '/positions' : '/pool', internal: true, diff --git a/apps/web/src/components/NavBar/index.tsx b/apps/web/src/components/NavBar/index.tsx index 7d3396bd814..6d5e28bc2ac 100644 --- a/apps/web/src/components/NavBar/index.tsx +++ b/apps/web/src/components/NavBar/index.tsx @@ -23,9 +23,7 @@ import { useProfilePageState } from 'nft/hooks' import { ProfilePageStateType } from 'nft/types' import { BREAKPOINTS } from 'theme' import { Z_INDEX } from 'theme/zIndex' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlagWithLoading } from 'uniswap/src/features/gating/hooks' -import { useEnabledChains } from 'uniswap/src/features/settings/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks' import { INTERFACE_NAV_HEIGHT } from 'uniswap/src/theme/heights' const Nav = styled.nav` @@ -78,20 +76,12 @@ function useShouldHideChainSelector() { const isSwapPage = useIsSwapPage() const isLimitPage = useIsLimitPage() const isExplorePage = useIsExplorePage() - const { value: multichainExploreFlagEnabled, isLoading: isMultichainExploreFlagLoading } = useFeatureFlagWithLoading( - FeatureFlags.MultichainExplore, - ) const baseHiddenPages = isNftPage - const multichainHiddenPages = isLandingPage || isSendPage || isSwapPage || isLimitPage || baseHiddenPages - const multichainExploreHiddenPages = multichainHiddenPages || isExplorePage - - const hideChainSelector = - multichainExploreFlagEnabled || isMultichainExploreFlagLoading - ? multichainExploreHiddenPages - : multichainHiddenPages + const multichainHiddenPages = + isLandingPage || isSendPage || isSwapPage || isLimitPage || baseHiddenPages || isExplorePage - return hideChainSelector + return multichainHiddenPages } export default function Navbar() { diff --git a/apps/web/src/components/NumericalInput.tsx b/apps/web/src/components/NumericalInput.tsx index f0465d80899..acb01720f67 100644 --- a/apps/web/src/components/NumericalInput.tsx +++ b/apps/web/src/components/NumericalInput.tsx @@ -2,8 +2,8 @@ import { loadingOpacityMixin } from 'components/Loader/styled' import styled from 'lib/styled-components' import React, { forwardRef } from 'react' import { Locale } from 'uniswap/src/features/language/constants' +import { useCurrentLocale } from 'uniswap/src/features/language/hooks' import { escapeRegExp } from 'utils/escapeRegExp' -import { useFormatterLocales } from 'utils/formatNumbers' export const StyledInput = styled.input<{ error?: boolean; fontSize?: string; align?: string; disabled?: boolean }>` color: ${({ error, theme }) => (error ? theme.critical : theme.neutral1)}; @@ -68,7 +68,7 @@ export function isInputGreaterThanDecimals(value: string, maxDecimals?: number): const Input = forwardRef( ({ value, onUserInput, placeholder, prependSymbol, maxDecimals, testId, ...rest }: InputProps, ref) => { - const { formatterLocale } = useFormatterLocales() + const locale = useCurrentLocale() const enforcer = (nextUserInput: string) => { if (nextUserInput === '' || inputRegex.test(escapeRegExp(nextUserInput))) { @@ -81,7 +81,7 @@ const Input = forwardRef( } const formatValueWithLocale = (value: string | number) => { - const [searchValue, replaceValue] = localeUsesComma(formatterLocale) ? [/\./g, ','] : [/,/g, '.'] + const [searchValue, replaceValue] = localeUsesComma(locale) ? [/\./g, ','] : [/,/g, '.'] return value.toString().replace(searchValue, replaceValue) } diff --git a/apps/web/src/components/PercentInput.tsx b/apps/web/src/components/PercentInput.tsx index 0fcb8716092..52bf95f47fc 100644 --- a/apps/web/src/components/PercentInput.tsx +++ b/apps/web/src/components/PercentInput.tsx @@ -3,13 +3,13 @@ import { NumericalInputFontStyle } from 'pages/Swap/common/shared' import React, { forwardRef } from 'react' // eslint-disable-next-line @typescript-eslint/no-restricted-imports import styled from 'styled-components' -import { useFormatterLocales } from 'utils/formatNumbers' +import { useCurrentLocale } from 'uniswap/src/features/language/hooks' const inputRegex = RegExp(`^\\d*(\\.\\d{0,2})?$`) const PercentInput = forwardRef( ({ value, onUserInput, placeholder, testId, ...rest }: InputProps, ref) => { - const { formatterLocale } = useFormatterLocales() + const locale = useCurrentLocale() const enforcer = (nextUserInput: string) => { const sanitizedInput = nextUserInput.replace(/,/g, '.') // Normalize the input @@ -19,7 +19,7 @@ const PercentInput = forwardRef( } const formatValueWithLocale = (value: string | number) => { - const [searchValue, replaceValue] = localeUsesComma(formatterLocale) ? [/\./g, ','] : [/,/g, '.'] + const [searchValue, replaceValue] = localeUsesComma(locale) ? [/\./g, ','] : [/,/g, '.'] return value.toString().replace(searchValue, replaceValue) } diff --git a/apps/web/src/components/PoolProgressIndicator/PoolProgressIndicator.tsx b/apps/web/src/components/PoolProgressIndicator/PoolProgressIndicator.tsx index 641d3b53b81..35fbc772f68 100644 --- a/apps/web/src/components/PoolProgressIndicator/PoolProgressIndicator.tsx +++ b/apps/web/src/components/PoolProgressIndicator/PoolProgressIndicator.tsx @@ -1,9 +1,13 @@ import { Fragment } from 'react' +import { ClickableTamaguiStyle } from 'theme/components' import { Flex, FlexProps, Text } from 'ui/src' import { Trans } from 'uniswap/src/i18n' import { assert } from 'utilities/src/errors' -export function PoolProgressIndicator({ steps, ...rest }: { steps: { label: string; active: boolean }[] } & FlexProps) { +export function PoolProgressIndicator({ + steps, + ...rest +}: { steps: { label: string; active: boolean; onPress?: () => void }[] } & FlexProps) { assert(steps.length > 0, 'PoolProgressIndicator: steps must have at least one step') return ( {steps.map((step, index) => ( - + theme.transition.duration.medium}; ` -const Badge = styled(ThemedText.LabelMicro)` - background: ${({ theme }) => theme.surface2}; - padding: 2px 6px; - border-radius: 4px; -` - const IconBubble = styled(LoadingBubble)` width: 32px; height: 32px; @@ -68,7 +64,8 @@ interface PoolDetailsBreadcrumbProps { } export function PoolDetailsBreadcrumb({ chainId, poolAddress, token0, token1, loading }: PoolDetailsBreadcrumbProps) { - const chainName = chainIdToBackendChain({ chainId, withFallback: true }) + const { defaultChainId } = useEnabledChains() + const chainName = toGraphQLChain(chainId ?? defaultChainId) const exploreOrigin = `/explore/${chainName.toLowerCase()}` const poolsOrigin = `/explore/pools/${chainName.toLowerCase()}` @@ -122,6 +119,8 @@ const PoolDetailsTitle = ({ toggleReversed: React.DispatchWithoutAction }) => { const { formatPercent } = useFormatter() + const { defaultChainId } = useEnabledChains() + const graphQLChain = toGraphQLChain(chainId ?? defaultChainId) const feePercent = feeTier && formatPercent(new Percent(feeTier, BIPS_BASE * 100)) return ( @@ -130,7 +129,7 @@ const PoolDetailsTitle = ({ {token0?.symbol} @@ -139,15 +138,24 @@ const PoolDetailsTitle = ({ {token1?.symbol} - {protocolVersion === ProtocolVersion.V2 && v2} - {!!feePercent && {feePercent}} + + + {protocolVersion?.toLowerCase()} + + {/* TODO(WEB-5364): add hook badge when data available, it should have a hover state and link out to the explorer */} + {!!feePercent && ( + + {feePercent} + + )} + { beforeEach(() => { diff --git a/apps/web/src/components/Pools/PoolDetails/PoolDetailsLink.tsx b/apps/web/src/components/Pools/PoolDetails/PoolDetailsLink.tsx index d7456ec8928..a2e0a9f7144 100644 --- a/apps/web/src/components/Pools/PoolDetails/PoolDetailsLink.tsx +++ b/apps/web/src/components/Pools/PoolDetails/PoolDetailsLink.tsx @@ -5,7 +5,6 @@ import { DoubleCurrencyAndChainLogo } from 'components/Logo/DoubleLogo' import { DetailBubble, SmallDetailBubble } from 'components/Pools/PoolDetails/shared' import Tooltip, { TooltipSize } from 'components/Tooltip' import Row from 'components/deprecated/Row' -import { chainIdToBackendChain } from 'constants/chains' import { NATIVE_CHAIN_ID } from 'constants/tokens' import { getTokenDetailsURL, gqlToCurrency } from 'graphql/data/util' import useCopyClipboard from 'hooks/useCopyClipboard' @@ -16,8 +15,10 @@ import { useNavigate } from 'react-router-dom' import { BREAKPOINTS } from 'theme' import { ClickableStyle, EllipsisStyle, ExternalLink, ThemedText } from 'theme/components' import { Token } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { toGraphQLChain } from 'uniswap/src/features/chains/utils' import { Trans, t } from 'uniswap/src/i18n' -import { UniverseChainId } from 'uniswap/src/types/chains' import { ExplorerDataType, getExplorerLink } from 'uniswap/src/utils/linking' import { isAddress, shortenAddress } from 'utilities/src/addresses' @@ -107,7 +108,8 @@ export function PoolDetailsLink({ address, chainId, tokens, loading }: PoolDetai ) const navigate = useNavigate() - const chainName = chainIdToBackendChain({ chainId, withFallback: true }) + const { defaultChainId } = useEnabledChains() + const chainName = toGraphQLChain(chainId ?? defaultChainId) const handleTokenTextClick = useCallback(() => { if (!isPool) { navigate(getTokenDetailsURL({ address: tokens[0]?.address, chain: chainName })) diff --git a/apps/web/src/components/Pools/PoolDetails/PoolDetailsStats.tsx b/apps/web/src/components/Pools/PoolDetails/PoolDetailsStats.tsx index 2e781da625b..44b07e10bad 100644 --- a/apps/web/src/components/Pools/PoolDetails/PoolDetailsStats.tsx +++ b/apps/web/src/components/Pools/PoolDetails/PoolDetailsStats.tsx @@ -5,7 +5,6 @@ import { DeltaArrow } from 'components/Tokens/TokenDetails/Delta' import { LoadingBubble } from 'components/Tokens/loading' import Column from 'components/deprecated/Column' import Row from 'components/deprecated/Row' -import { chainIdToBackendChain } from 'constants/chains' import { NATIVE_CHAIN_ID } from 'constants/tokens' import { PoolData } from 'graphql/data/pools/usePoolData' import { getTokenDetailsURL, unwrapToken } from 'graphql/data/util' @@ -19,8 +18,10 @@ import { BREAKPOINTS } from 'theme' import { ClickableStyle, ThemedText } from 'theme/components' import { nativeOnChain } from 'uniswap/src/constants/tokens' import { Token } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { toGraphQLChain } from 'uniswap/src/features/chains/utils' import { Trans } from 'uniswap/src/i18n' -import { UniverseChainId } from 'uniswap/src/types/chains' import { NumberType, useFormatter } from 'utils/formatNumbers' const HeaderText = styled(Text)` @@ -133,6 +134,7 @@ const PoolBalanceTokenNames = ({ token, chainId }: { token: TokenFullData; chain const unwrappedToken = chainId ? unwrapToken(chainId, token) : token const isNative = unwrappedToken?.address === NATIVE_CHAIN_ID const currency = isNative && chainId ? nativeOnChain(chainId) : token.currency + const { defaultChainId } = useEnabledChains() return ( {!screenIsNotLarge && } @@ -144,7 +146,7 @@ const PoolBalanceTokenNames = ({ token, chainId }: { token: TokenFullData; chain {screenIsNotLarge && ( diff --git a/apps/web/src/components/Pools/PoolDetails/PoolDetailsStatsButtons.test.tsx b/apps/web/src/components/Pools/PoolDetails/PoolDetailsStatsButtons.test.tsx index b4bb448ad68..8e0285fec30 100644 --- a/apps/web/src/components/Pools/PoolDetails/PoolDetailsStatsButtons.test.tsx +++ b/apps/web/src/components/Pools/PoolDetails/PoolDetailsStatsButtons.test.tsx @@ -10,8 +10,8 @@ import { mocked } from 'test-utils/mocked' import { useMultiChainPositionsReturnValue, validBEPoolToken0, validBEPoolToken1 } from 'test-utils/pools/fixtures' import { act, render, screen } from 'test-utils/render' import { useUniswapContext } from 'uniswap/src/contexts/UniswapContext' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { dismissTokenWarning } from 'uniswap/src/features/tokens/slice/slice' -import { UniverseChainId } from 'uniswap/src/types/chains' jest.mock('components/AccountDrawer/MiniPortfolio/Pools/useMultiChainPositions') diff --git a/apps/web/src/components/Pools/PoolDetails/PoolDetailsStatsButtons.tsx b/apps/web/src/components/Pools/PoolDetails/PoolDetailsStatsButtons.tsx index e5267373ee3..13e1a3826b5 100644 --- a/apps/web/src/components/Pools/PoolDetails/PoolDetailsStatsButtons.tsx +++ b/apps/web/src/components/Pools/PoolDetails/PoolDetailsStatsButtons.tsx @@ -6,9 +6,9 @@ import Column from 'components/deprecated/Column' import Row from 'components/deprecated/Row' import { SwapWrapperOuter } from 'components/swap/styled' import { LoadingBubble } from 'components/Tokens/loading' -import TokenSafetyMessage from 'components/TokenSafety/TokenSafetyMessage' -import { chainIdToBackendChain } from 'constants/chains' +import TokenSafetyMessage from 'components/TokenSafety/DeprecatedTokenSafetyMessage' import { getPriorityWarning, StrongWarning, useTokenWarning } from 'constants/deprecatedTokenSafety' +import { NATIVE_CHAIN_ID } from 'constants/tokens' import { useTokenBalancesQuery } from 'graphql/data/apollo/AdaptiveTokenBalancesProvider' import { gqlToCurrency } from 'graphql/data/util' import { useScreenSize } from 'hooks/screenSize/useScreenSize' @@ -16,7 +16,7 @@ import { useAccount } from 'hooks/useAccount' import { useSwitchChain } from 'hooks/useSwitchChain' import styled from 'lib/styled-components' import { Swap } from 'pages/Swap' -import { useMemo, useReducer } from 'react' +import { useCallback, useMemo, useReducer, useState } from 'react' import { Plus, X } from 'react-feather' import { useLocation, useNavigate } from 'react-router-dom' import { BREAKPOINTS } from 'theme' @@ -25,9 +25,17 @@ import { opacify } from 'theme/utils' import { Z_INDEX } from 'theme/zIndex' import { ArrowUpDown } from 'ui/src/components/icons/ArrowUpDown' import { Token } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { toGraphQLChain } from 'uniswap/src/features/chains/utils' +import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' +import { FeatureFlags } from 'uniswap/src/features/gating/flags' +import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' +import { TokenWarningCard } from 'uniswap/src/features/tokens/TokenWarningCard' +import TokenWarningModal from 'uniswap/src/features/tokens/TokenWarningModal' +import { useCurrencyInfo } from 'uniswap/src/features/tokens/useCurrencyInfo' import { Trans } from 'uniswap/src/i18n' -import { UniverseChainId } from 'uniswap/src/types/chains' -import { currencyId } from 'utils/currencyId' +import { currencyId } from 'uniswap/src/utils/currencyId' import { NumberType, useFormatter } from 'utils/formatNumbers' const PoolDetailsStatsButtonsRow = styled(Row)` @@ -154,16 +162,19 @@ export function PoolDetailsStatsButtons({ chainId, token0, token1, feeTier, load const position = userOwnedPositions && findMatchingPosition(userOwnedPositions, token0, token1, feeTier) const tokenId = position?.details.tokenId const switchChain = useSwitchChain() + const { defaultChainId } = useEnabledChains() const navigate = useNavigate() const location = useLocation() const currency0 = token0 && gqlToCurrency(token0) const currency1 = token1 && gqlToCurrency(token1) + const currencyInfo0 = useCurrencyInfo(currency0 && currencyId(currency0)) + const currencyInfo1 = useCurrencyInfo(currency1 && currencyId(currency1)) // Mobile Balance Data const { data: balanceQuery } = useTokenBalancesQuery() const { balance0, balance1, balance0Fiat, balance1Fiat } = useMemo(() => { const filteredBalances = balanceQuery?.portfolios?.[0]?.tokenBalances?.filter( - (tokenBalance) => tokenBalance?.token?.chain === chainIdToBackendChain({ chainId, withFallback: true }), + (tokenBalance) => tokenBalance?.token?.chain === toGraphQLChain(chainId ?? defaultChainId), ) const tokenBalance0 = filteredBalances?.find((tokenBalance) => tokenBalance?.token?.address === token0?.address) const tokenBalance1 = filteredBalances?.find((tokenBalance) => tokenBalance?.token?.address === token1?.address) @@ -173,7 +184,7 @@ export function PoolDetailsStatsButtons({ chainId, token0, token1, feeTier, load balance0Fiat: tokenBalance0?.denominatedValue?.value ?? 0, balance1Fiat: tokenBalance1?.denominatedValue?.value ?? 0, } - }, [balanceQuery?.portfolios, chainId, token0?.address, token1?.address]) + }, [balanceQuery?.portfolios, chainId, defaultChainId, token0?.address, token1?.address]) const { formatNumber } = useFormatter() const formattedBalance0 = formatNumber({ input: balance0, @@ -194,7 +205,9 @@ export function PoolDetailsStatsButtons({ chainId, token0, token1, feeTier, load if (account.chainId !== chainId && chainId) { await switchChain(chainId) } - navigate(`/add/${currencyId(currency0)}/${currencyId(currency1)}/${feeTier}${tokenId ? `/${tokenId}` : ''}`, { + const currency0Address = currency0.isNative ? NATIVE_CHAIN_ID : currency0.address + const currency1Address = currency1.isNative ? NATIVE_CHAIN_ID : currency1.address + navigate(`/add/${currency0Address}/${currency1Address}/${feeTier}${tokenId ? `/${tokenId}` : ''}`, { state: { from: location.pathname }, }) } @@ -207,6 +220,15 @@ export function PoolDetailsStatsButtons({ chainId, token0, token1, feeTier, load const token1Warning = useTokenWarning(token1?.address, chainId) const priorityWarning = getPriorityWarning(token0Warning, token1Warning) + const tokenProtectionEnabled = useFeatureFlag(FeatureFlags.TokenProtection) + const [showWarningModal, setShowWarningModal] = useState(false) + const closeWarningModal = useCallback(() => setShowWarningModal(false), []) + const [warningModalCurrencyInfo, setWarningModalCurrencyInfo] = useState>() + const onWarningCardCtaPressed = useCallback((currencyInfo: Maybe) => { + setWarningModalCurrencyInfo(currencyInfo) + setShowWarningModal(true) + }, []) + if (loading || !currency0 || !currency1) { return ( @@ -282,13 +304,30 @@ export function PoolDetailsStatsButtons({ chainId, token0, token1, feeTier, load compact disableTokenInputs={chainId !== account.chainId} /> - {Boolean(priorityWarning) && ( - + {tokenProtectionEnabled ? ( + <> + onWarningCardCtaPressed(currencyInfo0)} /> + onWarningCardCtaPressed(currencyInfo1)} /> + {warningModalCurrencyInfo && ( + // Intentionally duplicative with the TokenWarningModal in the swap component; this one only displays when user clicks "i" Info button on the TokenWarningCard + + )} + + ) : ( + Boolean(priorityWarning) && ( + + ) )} !s, false) const filterAnchorRef = useRef(null) @@ -83,7 +84,7 @@ export function PoolDetailsTransactionsTable({ const { transactions, loading, loadMore, error } = usePoolTransactions( poolAddress, - chain.id, + chainId, filter, token0, protocolVersion, @@ -113,7 +114,7 @@ export function PoolDetailsTransactionsTable({ > ), @@ -266,7 +267,7 @@ export function PoolDetailsTransactionsTable({ justifyContent="flex-end" grow > - + {shortenAddress(makerAddress.getValue?.(), 0)} @@ -275,7 +276,7 @@ export function PoolDetailsTransactionsTable({ ] }, [ activeLocalCurrency, - chain.id, + chainId, filter, filterModalIsOpen, formatFiatPrice, diff --git a/apps/web/src/components/Pools/PoolDetails/__snapshots__/PoolDetailsHeader.test.tsx.snap b/apps/web/src/components/Pools/PoolDetails/__snapshots__/PoolDetailsHeader.test.tsx.snap index fef3b3d892b..8a5bd4f0403 100644 --- a/apps/web/src/components/Pools/PoolDetails/__snapshots__/PoolDetailsHeader.test.tsx.snap +++ b/apps/web/src/components/Pools/PoolDetails/__snapshots__/PoolDetailsHeader.test.tsx.snap @@ -284,7 +284,7 @@ exports[`PoolDetailsHeader renders header text correctly 1`] = ` min-width: 0; } -.c12 { +.c10 { box-sizing: border-box; margin: 0; min-width: 0; @@ -311,7 +311,7 @@ exports[`PoolDetailsHeader renders header text correctly 1`] = ` gap: 12px; } -.c13 { +.c11 { width: -webkit-max-content; width: -moz-max-content; width: max-content; @@ -331,7 +331,7 @@ exports[`PoolDetailsHeader renders header text correctly 1`] = ` gap: 8px; } -.c14 { +.c12 { display: inline-block; height: inherit; } @@ -344,14 +344,6 @@ exports[`PoolDetailsHeader renders header text correctly 1`] = ` letter-spacing: -0.01em; } -.c10 { - color: #7D7D7D; - -webkit-letter-spacing: -0.01em; - -moz-letter-spacing: -0.01em; - -ms-letter-spacing: -0.01em; - letter-spacing: -0.01em; -} - .c4 { display: -webkit-box; display: -webkit-flex; @@ -411,12 +403,6 @@ exports[`PoolDetailsHeader renders header text correctly 1`] = ` animation-duration: 250ms; } -.c11 { - background: #F9F9F9; - padding: 2px 6px; - border-radius: 4px; -} - .c6 { display: -webkit-box; display: -webkit-flex; @@ -523,9 +509,18 @@ exports[`PoolDetailsHeader renders header text correctly 1`] = `
- 0.05% + + + 0.05% +
diff --git a/apps/web/src/components/Popups/PopupContent.tsx b/apps/web/src/components/Popups/PopupContent.tsx index 08c6cf3825d..11ca59e7b8b 100644 --- a/apps/web/src/components/Popups/PopupContent.tsx +++ b/apps/web/src/components/Popups/PopupContent.tsx @@ -11,7 +11,6 @@ import AlertTriangleFilled from 'components/Icons/AlertTriangleFilled' import { LoaderV3 } from 'components/Icons/LoadingSpinner' import Column, { AutoColumn } from 'components/deprecated/Column' import { AutoRow } from 'components/deprecated/Row' -import { useIsSupportedChainId } from 'constants/chains' import styled from 'lib/styled-components' import { X } from 'react-feather' import { useOrder } from 'state/signatures/hooks' @@ -19,10 +18,11 @@ import { useTransaction } from 'state/transactions/hooks' import { EllipsisStyle, ThemedText } from 'theme/components' import { Flex, useSporeColors } from 'ui/src' import { BridgeIcon } from 'uniswap/src/components/CurrencyLogo/SplitLogo' -import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' import { TransactionStatus } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' +import { useIsSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { Trans } from 'uniswap/src/i18n' -import { UniverseChainId } from 'uniswap/src/types/chains' import { ExplorerDataType, getExplorerLink } from 'uniswap/src/utils/linking' import { useFormatter } from 'utils/formatNumbers' @@ -73,7 +73,7 @@ const PopupAlertTriangle = styled(AlertTriangleFilled)` export function FailedNetworkSwitchPopup({ chainId, onClose }: { chainId: UniverseChainId; onClose: () => void }) { const isSupportedChain = useIsSupportedChainId(chainId) - const chainInfo = isSupportedChain ? UNIVERSE_CHAIN_INFO[chainId] : undefined + const chainInfo = isSupportedChain ? getChainInfo(chainId) : undefined if (!chainInfo) { return null diff --git a/apps/web/src/components/Popups/PopupItem.tsx b/apps/web/src/components/Popups/PopupItem.tsx index 347d79f87f5..9eebd20c3a3 100644 --- a/apps/web/src/components/Popups/PopupItem.tsx +++ b/apps/web/src/components/Popups/PopupItem.tsx @@ -4,7 +4,6 @@ import { UniswapXOrderPopupContent, } from 'components/Popups/PopupContent' import { ToastRegularSimple } from 'components/Popups/ToastRegularSimple' -import { useSupportedChainId } from 'constants/chains' import { useAccount } from 'hooks/useAccount' import { useEffect } from 'react' import { useRemovePopup } from 'state/application/hooks' @@ -12,9 +11,10 @@ import { PopupContent, PopupType } from 'state/application/reducer' import { Flex, Text } from 'ui/src' import { Shuffle } from 'ui/src/components/icons/Shuffle' import { NetworkLogo } from 'uniswap/src/components/CurrencyLogo/NetworkLogo' -import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' +import { useSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { t } from 'uniswap/src/i18n' -import { UniverseChainId } from 'uniswap/src/types/chains' import { SwapTab } from 'uniswap/src/types/screens/interface' export default function PopupItem({ @@ -79,7 +79,7 @@ export default function PopupItem({ } function getSwitchNetworkTitle(action: SwapTab, chainId: UniverseChainId) { - const { label } = UNIVERSE_CHAIN_INFO[chainId] + const { label } = getChainInfo(chainId) switch (action) { case SwapTab.Swap: @@ -98,8 +98,8 @@ function BridgeToast({ inputChainId: UniverseChainId outputChainId: UniverseChainId }): JSX.Element { - const originChain = UNIVERSE_CHAIN_INFO[inputChainId] - const targetChain = UNIVERSE_CHAIN_INFO[outputChainId] + const originChain = getChainInfo(inputChainId) + const targetChain = getChainInfo(outputChainId) return ( diff --git a/apps/web/src/components/PositionCard/index.tsx b/apps/web/src/components/PositionCard/index.tsx index e02c2023f0b..077b1115636 100644 --- a/apps/web/src/components/PositionCard/index.tsx +++ b/apps/web/src/components/PositionCard/index.tsx @@ -8,7 +8,6 @@ import { AutoColumn } from 'components/deprecated/Column' import { AutoRow, RowBetween, RowFixed } from 'components/deprecated/Row' import { CardNoise } from 'components/earn/styled' import { Dots } from 'components/swap/styled' -import { chainIdToBackendChain } from 'constants/chains' import { BIG_INT_ZERO } from 'constants/misc' import { useAccount } from 'hooks/useAccount' import { useColor } from 'hooks/useColor' @@ -22,6 +21,8 @@ import { Link } from 'react-router-dom' import { Text } from 'rebass' import { useTokenBalance } from 'state/connection/hooks' import { StyledInternalLink, ThemedText } from 'theme/components' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { toGraphQLChain } from 'uniswap/src/features/chains/utils' import { Trans } from 'uniswap/src/i18n' import { currencyId } from 'utils/currencyId' import { unwrappedToken } from 'utils/unwrappedToken' @@ -157,6 +158,8 @@ export function MinimalPositionCard({ pair, showUnwrapped = false, border }: Pos export default function FullPositionCard({ pair, border, stakedBalance }: PositionCardProps) { const account = useAccount() + const { defaultChainId } = useEnabledChains() + const currency0 = unwrappedToken(pair.token0) const currency1 = unwrappedToken(pair.token1) @@ -293,7 +296,7 @@ export default function FullPositionCard({ pair, border, stakedBalance }: Positi diff --git a/apps/web/src/components/PositionListItem/PositionListItem.test.tsx b/apps/web/src/components/PositionListItem/PositionListItem.test.tsx index 50c1219f374..c0333210e75 100644 --- a/apps/web/src/components/PositionListItem/PositionListItem.test.tsx +++ b/apps/web/src/components/PositionListItem/PositionListItem.test.tsx @@ -7,7 +7,7 @@ import { PoolState, usePool } from 'hooks/usePools' import { mocked } from 'test-utils/mocked' import { render } from 'test-utils/render' import { USDC_MAINNET } from 'uniswap/src/constants/tokens' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' jest.mock('components/Logo/DoubleLogo') jest.mock('hooks/Tokens') diff --git a/apps/web/src/components/PositionPreview.tsx b/apps/web/src/components/PositionPreview.tsx index 16798088d63..935dba80417 100644 --- a/apps/web/src/components/PositionPreview.tsx +++ b/apps/web/src/components/PositionPreview.tsx @@ -9,7 +9,7 @@ import { AutoColumn } from 'components/deprecated/Column' import { RowBetween, RowFixed } from 'components/deprecated/Row' import { Break } from 'components/earn/styled' import JSBI from 'jsbi' -import { BlastRebasingAlert } from 'pages/AddLiquidity/blastAlerts' +import { BlastRebasingAlert } from 'pages/AddLiquidityV3/blastAlerts' import { ReactNode, useCallback, useState } from 'react' import { Bound } from 'state/mint/v3/actions' import { ThemedText } from 'theme/components' diff --git a/apps/web/src/components/RemoveLiquidity/RemoveLiquidityModalContext.tsx b/apps/web/src/components/RemoveLiquidity/RemoveLiquidityModalContext.tsx index 0f74baafcab..5dec68189fa 100644 --- a/apps/web/src/components/RemoveLiquidity/RemoveLiquidityModalContext.tsx +++ b/apps/web/src/components/RemoveLiquidity/RemoveLiquidityModalContext.tsx @@ -1,4 +1,4 @@ -import { useModalLiquidityPositionInfo } from 'components/Liquidity/hooks' +import { useModalLiquidityInitialState } from 'components/Liquidity/hooks' import { PositionInfo } from 'components/Liquidity/types' import { Dispatch, PropsWithChildren, SetStateAction, createContext, useContext, useState } from 'react' @@ -27,7 +27,7 @@ const RemoveLiquidityModalContext = createContext({ export function RemoveLiquidityModalContextProvider({ children }: PropsWithChildren): JSX.Element { const [step, setStep] = useState(DecreaseLiquidityStep.Input) const [percent, setPercent] = useState('') - const positionInfo = useModalLiquidityPositionInfo() + const positionInfo = useModalLiquidityInitialState() const percentInvalid = percent === '0' || percent === '' || !percent return ( @@ -37,7 +37,7 @@ export function RemoveLiquidityModalContextProvider({ children }: PropsWithChild ) } -export function useLiquidityModalContext() { +export function useRemoveLiquidityModalContext() { const removeModalContext = useContext(RemoveLiquidityModalContext) if (removeModalContext === undefined) { diff --git a/apps/web/src/components/RemoveLiquidity/RemoveLiquidityReview.tsx b/apps/web/src/components/RemoveLiquidity/RemoveLiquidityReview.tsx index ed33411ebc1..48148862d3f 100644 --- a/apps/web/src/components/RemoveLiquidity/RemoveLiquidityReview.tsx +++ b/apps/web/src/components/RemoveLiquidity/RemoveLiquidityReview.tsx @@ -1,10 +1,12 @@ +// eslint-disable-next-line no-restricted-imports +import { ProtocolVersion } from '@uniswap/client-pools/dist/pools/v1/types_pb' import { TokenInfo } from 'components/Liquidity/TokenInfo' import { useGetPoolTokenPercentage, usePositionCurrentPrice, useV3OrV4PositionDerivedInfo, } from 'components/Liquidity/hooks' -import { useLiquidityModalContext } from 'components/RemoveLiquidity/RemoveLiquidityModalContext' +import { useRemoveLiquidityModalContext } from 'components/RemoveLiquidity/RemoveLiquidityModalContext' import { useRemoveLiquidityTxContext } from 'components/RemoveLiquidity/RemoveLiquidityTxContext' import { DetailLineItem } from 'components/swap/DetailLineItem' import { useCurrencyInfo } from 'hooks/Tokens' @@ -29,7 +31,7 @@ import { NumberType } from 'utilities/src/format/types' export function RemoveLiquidityReview({ onClose }: { onClose: () => void }) { const { t } = useTranslation() const [steps, setSteps] = useState([]) - const { percent, positionInfo } = useLiquidityModalContext() + const { percent, positionInfo } = useRemoveLiquidityModalContext() const removeLiquidityTxContext = useRemoveLiquidityTxContext() const { formatCurrencyAmount, formatPercent } = useLocalizationContext() const [currentStep, setCurrentStep] = useState<{ step: TransactionStep; accepted: boolean } | undefined>() @@ -42,6 +44,12 @@ export function RemoveLiquidityReview({ onClose }: { onClose: () => void }) { const { txContext, gasFeeEstimateUSD } = removeLiquidityTxContext + const onSuccess = () => { + setSteps([]) + setCurrentStep(undefined) + onClose() + } + const onFailure = () => { setCurrentStep(undefined) } @@ -59,7 +67,7 @@ export function RemoveLiquidityReview({ onClose }: { onClose: () => void }) { liquidityTxContext: txContext, setCurrentStep, setSteps, - onSuccess: onClose, + onSuccess, onFailure, }), ) @@ -103,37 +111,39 @@ export function RemoveLiquidityReview({ onClose }: { onClose: () => void }) { currencyAmount={currency1AmountToRemove} currencyUSDAmount={currency1FiatAmount?.multiply(percent).divide(100)} /> - - - Includes accrued fees: - - - - - - {currency0Amount.currency.symbol} fees + {positionInfo.version !== ProtocolVersion.V2 && ( + + + {t('fee.accrued')} + + + + + + {currency0Amount.currency.symbol} fees + + + {formatCurrencyAmount({ value: feeValue0 })}{' '} + + ({formatCurrencyAmount({ value: fiatFeeValue0, type: NumberType.FiatTokenPrice })}) + + - - {formatCurrencyAmount({ value: feeValue0 })}{' '} - - ({formatCurrencyAmount({ value: fiatFeeValue0, type: NumberType.FiatTokenPrice })}) - - - - - - - {currency1Amount.currency.symbol} fees - - - {formatCurrencyAmount({ value: feeValue1 })}{' '} - - ({formatCurrencyAmount({ value: fiatFeeValue1, type: NumberType.FiatTokenPrice })}) - + + + + {currency1Amount.currency.symbol} fees + + + {formatCurrencyAmount({ value: feeValue1 })}{' '} + + ({formatCurrencyAmount({ value: fiatFeeValue1, type: NumberType.FiatTokenPrice })}) + + - + )} {currentStep ? ( diff --git a/apps/web/src/components/RemoveLiquidity/RemoveLiquidityTxContext.tsx b/apps/web/src/components/RemoveLiquidity/RemoveLiquidityTxContext.tsx index 8a5138daa34..0de27743910 100644 --- a/apps/web/src/components/RemoveLiquidity/RemoveLiquidityTxContext.tsx +++ b/apps/web/src/components/RemoveLiquidity/RemoveLiquidityTxContext.tsx @@ -1,5 +1,5 @@ import { Currency, CurrencyAmount } from '@uniswap/sdk-core' -import { useLiquidityModalContext } from 'components/RemoveLiquidity/RemoveLiquidityModalContext' +import { useRemoveLiquidityModalContext } from 'components/RemoveLiquidity/RemoveLiquidityModalContext' import { useRemoveLiquidityTxAndGasInfo } from 'components/RemoveLiquidity/hooks' import { PropsWithChildren, createContext, useContext, useEffect, useMemo } from 'react' import { useAccountMeta } from 'uniswap/src/contexts/UniswapContext' @@ -23,7 +23,7 @@ const RemoveLiquidityTxContext = createContext ({ + useEnabledChains: jest.fn(), + useIsSupportedChainId: jest.fn(), +})) jest.mock('hooks/useAccount') describe('Settings Tab', () => { @@ -16,6 +21,12 @@ describe('Settings Tab', () => { mocked(useAccount).mockReturnValue({ chainId: UniverseChainId.Mainnet, } as unknown as ReturnType) + mocked(useEnabledChains).mockReturnValue({ + isTestnetModeEnabled: false, + chains: [], + gqlChains: [], + defaultChainId: UniverseChainId.Mainnet, + }) mocked(useIsSupportedChainId).mockReturnValue(true) }) diff --git a/apps/web/src/components/Settings/index.tsx b/apps/web/src/components/Settings/index.tsx index 5acb587d56a..7ea5b60ff3c 100644 --- a/apps/web/src/components/Settings/index.tsx +++ b/apps/web/src/components/Settings/index.tsx @@ -8,9 +8,9 @@ import MenuButton from 'components/Settings/MenuButton' import MultipleRoutingOptions from 'components/Settings/MultipleRoutingOptions' import RouterPreferenceSettings from 'components/Settings/RouterPreferenceSettings' import TransactionDeadlineSettings from 'components/Settings/TransactionDeadlineSettings' -import { useIsSupportedChainId, useIsUniswapXSupportedChain } from 'constants/chains' import { useIsMobile } from 'hooks/screenSize/useIsMobile' import useDisableScrolling from 'hooks/useDisableScrolling' +import { useIsUniswapXSupportedChain } from 'hooks/useIsUniswapXSupportedChain' import { useOnClickOutside } from 'hooks/useOnClickOutside' import styled from 'lib/styled-components' import { Portal } from 'nft/components/common/Portal' @@ -22,6 +22,7 @@ import { InterfaceTrade } from 'state/routing/types' import { isUniswapXTrade } from 'state/routing/utils' import { Divider, ThemedText } from 'theme/components' import { Z_INDEX } from 'theme/zIndex' +import { useIsSupportedChainId } from 'uniswap/src/features/chains/hooks' import { isL2ChainId } from 'uniswap/src/features/chains/utils' import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' diff --git a/apps/web/src/components/SwapBottomCard.tsx b/apps/web/src/components/SwapBottomCard.tsx index 56173e22da0..4c54f88a910 100644 --- a/apps/web/src/components/SwapBottomCard.tsx +++ b/apps/web/src/components/SwapBottomCard.tsx @@ -1,5 +1,4 @@ import { getChainUI } from 'components/Logo/ChainLogo' -import { getChain, useIsSupportedChainId } from 'constants/chains' import { useIsSendPage } from 'hooks/useIsSendPage' import { useIsSwapPage } from 'hooks/useIsSwapPage' import { useCallback } from 'react' @@ -19,10 +18,10 @@ import { useUniswapContext } from 'uniswap/src/contexts/UniswapContext' import { selectHasViewedBridgingBanner } from 'uniswap/src/features/behaviorHistory/selectors' import { setHasViewedBridgingBanner } from 'uniswap/src/features/behaviorHistory/slice' import { useIsBridgingChain, useNumBridgingChains } from 'uniswap/src/features/bridging/hooks/chains' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' +import { useIsSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { useTranslation } from 'uniswap/src/i18n' -import { UniverseChainId } from 'uniswap/src/types/chains' import { ONE_SECOND_MS } from 'utilities/src/time/time' export function SwapBottomCard() { @@ -36,8 +35,7 @@ export function SwapBottomCard() { const isSupportedChain = useIsSupportedChainId(chainId) const hasViewedBridgingBanner = useSelector(selectHasViewedBridgingBanner) - const bridgingEnabled = useFeatureFlag(FeatureFlags.Bridging) - const isBridgingSupported = useIsBridgingChain(chainId ?? UniverseChainId.Mainnet) + const isBridgingSupportedChain = useIsBridgingChain(chainId ?? UniverseChainId.Mainnet) const numBridgingChains = useNumBridgingChains() const handleBridgingDismiss = useCallback( (shouldNavigate: boolean) => { @@ -61,9 +59,8 @@ export function SwapBottomCard() { return null } - const shouldShowBridgingBanner = bridgingEnabled && !hasViewedBridgingBanner && isBridgingSupported - - const shouldShowLegacyTreatment = !bridgingEnabled + const isBridgingBannerChain = chainId === null || chainId === UniverseChainId.Mainnet || isBridgingSupportedChain + const shouldShowBridgingBanner = !hasViewedBridgingBanner && isBridgingBannerChain if (shouldShowBridgingBanner) { return ( @@ -84,7 +81,7 @@ export function SwapBottomCard() { />
) - } else if (shouldShowLegacyTreatment || !isBridgingSupported) { + } else if (!isBridgingSupportedChain) { return } else { return null @@ -96,7 +93,7 @@ function NetworkAlert({ chainId }: { chainId: UniverseChainId }) { const { t } = useTranslation() const { symbol, bgColor, textColor } = getChainUI(chainId, darkMode) - const chainInfo = getChain({ chainId }) + const chainInfo = getChainInfo(chainId) return chainInfo.bridge ? ( diff --git a/apps/web/src/components/SwitchLocaleLink.tsx b/apps/web/src/components/SwitchLocaleLink.tsx index de6c8158e21..0e870c47f0b 100644 --- a/apps/web/src/components/SwitchLocaleLink.tsx +++ b/apps/web/src/components/SwitchLocaleLink.tsx @@ -1,11 +1,10 @@ -import { navigatorLocale, useActiveLocale } from 'hooks/useActiveLocale' import { useLocationLinkProps } from 'hooks/useLocationLinkProps' import { useMemo } from 'react' import { useAppDispatch } from 'state/hooks' import { StyledInternalLink } from 'theme/components' import { Text } from 'ui/src' import { DEFAULT_LOCALE, Language, Locale, mapLocaleToLanguage } from 'uniswap/src/features/language/constants' -import { useLanguageInfo } from 'uniswap/src/features/language/hooks' +import { navigatorLocale, useCurrentLocale, useLanguageInfo } from 'uniswap/src/features/language/hooks' import { setCurrentLanguage } from 'uniswap/src/features/settings/slice' import { Trans } from 'uniswap/src/i18n' @@ -23,7 +22,7 @@ const useTargetLocale = (activeLocale: Locale) => { } export function SwitchLocaleLink() { - const activeLocale = useActiveLocale() + const activeLocale = useCurrentLocale() const targetLocale = useTargetLocale(activeLocale) const targetLanguageInfo = useLanguageInfo(targetLocale ? mapLocaleToLanguage[targetLocale] : Language.English) const dispatch = useAppDispatch() diff --git a/apps/web/src/components/Table/styled.tsx b/apps/web/src/components/Table/styled.tsx index a74146c7198..a9de95e74c7 100644 --- a/apps/web/src/components/Table/styled.tsx +++ b/apps/web/src/components/Table/styled.tsx @@ -3,9 +3,8 @@ import { ButtonLight } from 'components/Button/buttons' import { useAbbreviatedTimeString } from 'components/Table/utils' import { MouseoverTooltip, TooltipSize } from 'components/Tooltip' import { NATIVE_CHAIN_ID } from 'constants/tokens' -import { OrderDirection, getTokenDetailsURL, supportedChainIdFromGQLChain, unwrapToken } from 'graphql/data/util' +import { OrderDirection, getTokenDetailsURL, unwrapToken } from 'graphql/data/util' import { useCurrency } from 'hooks/Tokens' -import { useActiveLocale } from 'hooks/useActiveLocale' import deprecatedStyled from 'lib/styled-components' import { PropsWithChildren } from 'react' import { ArrowDown, CornerLeftUp, ExternalLink as ExternalLinkIcon } from 'react-feather' @@ -14,8 +13,10 @@ import { ClickableStyle, ClickableTamaguiStyle, EllipsisTamaguiStyle, ThemedText import { Z_INDEX } from 'theme/zIndex' import { Anchor, Flex, Text, View, styled } from 'ui/src' import { Token } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { fromGraphQLChain } from 'uniswap/src/features/chains/utils' +import { useCurrentLocale } from 'uniswap/src/features/language/hooks' import { useTranslation } from 'uniswap/src/i18n' -import { UniverseChainId } from 'uniswap/src/types/chains' export const SHOW_RETURN_TO_TOP_OFFSET = 500 export const LOAD_MORE_BOTTOM_OFFSET = 50 @@ -260,7 +261,7 @@ const StyledExternalLinkIcon = deprecatedStyled(ExternalLinkIcon)` * @returns JSX.Element containing the formatted timestamp */ export const TimestampCell = ({ timestamp, link }: { timestamp: number; link: string }) => { - const locale = useActiveLocale() + const locale = useCurrentLocale() const options: Intl.DateTimeFormatOptions = { year: '2-digit', month: '2-digit', @@ -298,7 +299,8 @@ const TokenSymbolText = styled(Text, { */ export const TokenLinkCell = ({ token }: { token: Token }) => { const { t } = useTranslation() - const chainId = supportedChainIdFromGQLChain(token.chain) ?? UniverseChainId.Mainnet + const { defaultChainId } = useEnabledChains() + const chainId = fromGraphQLChain(token.chain) ?? defaultChainId const unwrappedToken = unwrapToken(chainId, token) const isNative = unwrappedToken.address === NATIVE_CHAIN_ID const nativeCurrency = useCurrency(NATIVE_CHAIN_ID, chainId) diff --git a/apps/web/src/components/TokenSafety/TokenSafetyMessage.tsx b/apps/web/src/components/TokenSafety/DeprecatedTokenSafetyMessage.tsx similarity index 97% rename from apps/web/src/components/TokenSafety/TokenSafetyMessage.tsx rename to apps/web/src/components/TokenSafety/DeprecatedTokenSafetyMessage.tsx index 147738a452a..d1f628ebb1e 100644 --- a/apps/web/src/components/TokenSafety/TokenSafetyMessage.tsx +++ b/apps/web/src/components/TokenSafety/DeprecatedTokenSafetyMessage.tsx @@ -47,6 +47,7 @@ type TokenSafetyMessageProps = { tokenSymbol?: string } +/** @deprecated Use TokenWarningCard from packages/uniswap instead */ export default function TokenSafetyMessage({ warning, tokenAddress, diff --git a/apps/web/src/components/TokenSafety/TokenSafetyIcon.tsx b/apps/web/src/components/TokenSafety/TokenSafetyIcon.tsx index 41748470fc5..9eae29bae6c 100644 --- a/apps/web/src/components/TokenSafety/TokenSafetyIcon.tsx +++ b/apps/web/src/components/TokenSafety/TokenSafetyIcon.tsx @@ -8,6 +8,7 @@ const WarningContainer = styled(Flex, { justifyContent: 'center', }) +/** @deprecated use WarningIcon from packages/uniswap instead */ export default function TokenSafetyIcon({ warning }: { warning?: Warning }) { const colors = useSporeColors() switch (warning?.level) { diff --git a/apps/web/src/components/TokenSafety/index.tsx b/apps/web/src/components/TokenSafety/index.tsx index 68cd68f3241..6a26bdc9dc4 100644 --- a/apps/web/src/components/TokenSafety/index.tsx +++ b/apps/web/src/components/TokenSafety/index.tsx @@ -14,7 +14,7 @@ import { import styled from 'lib/styled-components' import { Text } from 'rebass' import { ButtonText, ExternalLink } from 'theme/components' -import { ExplorerView } from 'uniswap/src/features/address/ExplorerView' +import { TokenAddressView } from 'uniswap/src/features/address/TokenAddressView' import { ModalName } from 'uniswap/src/features/telemetry/constants' import { useDismissedTokenWarnings } from 'uniswap/src/features/tokens/slice/hooks' import { Trans } from 'uniswap/src/i18n' @@ -156,11 +156,11 @@ export default function TokenSafety({ // Logic for only showing the 'unsupported' warning if one is supported and other isn't if (token0 && token0Warning && (token0Unsupported || !(token1Warning && token1Unsupported))) { logos.push() - urls.push() + urls.push() } if (token1 && token1Warning && (token1Unsupported || !(token0Warning && token0Unsupported))) { logos.push() - urls.push() + urls.push() } const plural = logos.length > 1 diff --git a/apps/web/src/components/Tokens/TokenDetails/BalanceSummary.tsx b/apps/web/src/components/Tokens/TokenDetails/BalanceSummary.tsx index 8c4a3d620f1..22b9f9b0a06 100644 --- a/apps/web/src/components/Tokens/TokenDetails/BalanceSummary.tsx +++ b/apps/web/src/components/Tokens/TokenDetails/BalanceSummary.tsx @@ -10,8 +10,9 @@ import { useNavigate } from 'react-router-dom' import { BREAKPOINTS } from 'theme' import { ThemedText } from 'theme/components' import { Chain } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { Trans } from 'uniswap/src/i18n' -import { UniverseChainId } from 'uniswap/src/types/chains' import { NumberType, useFormatter } from 'utils/formatNumbers' const BalancesCard = styled.div` @@ -131,6 +132,7 @@ const OtherChainsBalanceSummary = ({ hasPageChainBalance: boolean }) => { const navigate = useNavigate() + const { defaultChainId } = useEnabledChains() if (!otherChainBalances.length) { return null @@ -148,7 +150,7 @@ const OtherChainsBalanceSummary = ({ )} {otherChainBalances.map((balance) => { const currency = balance.token && gqlToCurrency(balance.token) - const chainId = (balance.token && supportedChainIdFromGQLChain(balance.token.chain)) ?? UniverseChainId.Mainnet + const chainId = (balance.token && supportedChainIdFromGQLChain(balance.token.chain)) ?? defaultChainId return ( @@ -82,7 +82,7 @@ export default function InvalidTokenDetails({ diff --git a/apps/web/src/components/Tokens/TokenDetails/Skeleton.test.tsx b/apps/web/src/components/Tokens/TokenDetails/Skeleton.test.tsx index 82055b5d03c..e3644cc7ead 100644 --- a/apps/web/src/components/Tokens/TokenDetails/Skeleton.test.tsx +++ b/apps/web/src/components/Tokens/TokenDetails/Skeleton.test.tsx @@ -1,7 +1,7 @@ import { getLoadingTitle, TokenDetailsPageSkeleton } from 'components/Tokens/TokenDetails/Skeleton' import { render } from 'test-utils/render' import { USDC_MAINNET } from 'uniswap/src/constants/tokens' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' describe('TDP Skeleton', () => { it('should render correctly', () => { diff --git a/apps/web/src/components/Tokens/TokenDetails/Skeleton.tsx b/apps/web/src/components/Tokens/TokenDetails/Skeleton.tsx index 5feb16f65d6..4713415f2aa 100644 --- a/apps/web/src/components/Tokens/TokenDetails/Skeleton.tsx +++ b/apps/web/src/components/Tokens/TokenDetails/Skeleton.tsx @@ -8,9 +8,7 @@ import { StatPair, StatWrapper, StatsWrapper } from 'components/Tokens/TokenDeta import { Hr } from 'components/Tokens/TokenDetails/shared' import { LoadingBubble } from 'components/Tokens/loading' import { SwapSkeleton } from 'components/swap/SwapSkeleton' -import { useChainFromUrlParam } from 'constants/chains' import { NATIVE_CHAIN_ID } from 'constants/tokens' -import { getSupportedGraphQlChain } from 'graphql/data/util' import { useCurrency } from 'hooks/Tokens' import deprecatedStyled from 'lib/styled-components' import { ReactNode } from 'react' @@ -19,8 +17,11 @@ import { useParams } from 'react-router-dom' import { ClickableTamaguiStyle } from 'theme/components' import { capitalize } from 'tsafe' import { Anchor, Flex, Text, TextProps, styled } from 'ui/src' +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { Trans } from 'uniswap/src/i18n' import { ExplorerDataType, getExplorerLink } from 'uniswap/src/utils/linking' +import { useChainIdFromUrlParam } from 'utils/chainParams' const SWAP_COMPONENT_WIDTH = 360 @@ -268,17 +269,17 @@ function LoadingStats() { /* Loading State: row component with loading bubbles */ function TokenDetailsSkeleton() { - const chain = getSupportedGraphQlChain(useChainFromUrlParam(), { fallbackToEthereum: true }) + const { id: chainId, urlParam } = getChainInfo(useChainIdFromUrlParam() ?? UniverseChainId.Mainnet) const { tokenAddress } = useParams<{ tokenAddress?: string }>() - const token = useCurrency(tokenAddress === NATIVE_CHAIN_ID ? 'ETH' : tokenAddress, chain.id) + const token = useCurrency(tokenAddress === NATIVE_CHAIN_ID ? 'ETH' : tokenAddress, chainId) return ( - + - + @@ -313,7 +314,7 @@ function TokenDetailsSkeleton() { {tokenAddress && ( - {getLoadingTitle(token, tokenAddress, chain.id, chain.urlParam)} + {getLoadingTitle(token, tokenAddress, chainId, urlParam)} )} diff --git a/apps/web/src/components/Tokens/TokenDetails/StatsSection.tsx b/apps/web/src/components/Tokens/TokenDetails/StatsSection.tsx index 2b750967050..0ad811b8704 100644 --- a/apps/web/src/components/Tokens/TokenDetails/StatsSection.tsx +++ b/apps/web/src/components/Tokens/TokenDetails/StatsSection.tsx @@ -2,15 +2,15 @@ import { HEADER_DESCRIPTIONS } from 'components/Tokens/TokenTable' import { UNSUPPORTED_METADATA_CHAINS } from 'components/Tokens/constants' import { TokenSortMethod } from 'components/Tokens/state' import { MouseoverTooltip } from 'components/Tooltip' -import { useIsSupportedChainId } from 'constants/chains' import { TokenQueryData } from 'graphql/data/Token' import styled from 'lib/styled-components' import { ReactNode } from 'react' import { ExternalLink, ThemedText } from 'theme/components' import { textFadeIn } from 'theme/styles' -import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' +import { useIsSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { Trans } from 'uniswap/src/i18n' -import { UniverseChainId } from 'uniswap/src/types/chains' import { NumberType, useFormatter } from 'utils/formatNumbers' export const StatWrapper = styled.div` @@ -92,9 +92,7 @@ type StatsSectionProps = { export default function StatsSection(props: StatsSectionProps) { const { chainId, address, tokenQueryData } = props const isSupportedChain = useIsSupportedChainId(chainId) - const { label, infoLink } = isSupportedChain - ? UNIVERSE_CHAIN_INFO[chainId] - : { label: undefined, infoLink: undefined } + const { label, infoLink } = isSupportedChain ? getChainInfo(chainId) : { label: undefined, infoLink: undefined } const tokenMarketInfo = tokenQueryData?.market const tokenProjectMarketInfo = tokenQueryData?.project?.markets?.[0] // aggregated market price from CoinGecko diff --git a/apps/web/src/components/Tokens/TokenDetails/TokenDescription.tsx b/apps/web/src/components/Tokens/TokenDetails/TokenDescription.tsx index a46fef79834..b670e144532 100644 --- a/apps/web/src/components/Tokens/TokenDetails/TokenDescription.tsx +++ b/apps/web/src/components/Tokens/TokenDetails/TokenDescription.tsx @@ -12,8 +12,8 @@ import { useCallback, useReducer } from 'react' import { Copy } from 'react-feather' import { ClickableTamaguiStyle, EllipsisTamaguiStyle, ExternalLink, ThemedText } from 'theme/components' import { Flex, Paragraph, styled, Text } from 'ui/src' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { t, Trans } from 'uniswap/src/i18n' -import { UniverseChainId } from 'uniswap/src/types/chains' import { ExplorerDataType, getExplorerLink } from 'uniswap/src/utils/linking' import { shortenAddress } from 'utilities/src/addresses' import { useFormatter } from 'utils/formatNumbers' diff --git a/apps/web/src/components/Tokens/TokenDetails/TokenDetailsHeader.tsx b/apps/web/src/components/Tokens/TokenDetails/TokenDetailsHeader.tsx index 45148295b41..d120462dc26 100644 --- a/apps/web/src/components/Tokens/TokenDetails/TokenDetailsHeader.tsx +++ b/apps/web/src/components/Tokens/TokenDetails/TokenDetailsHeader.tsx @@ -17,8 +17,8 @@ import { EllipsisTamaguiStyle } from 'theme/components' import { Flex, Text, WebBottomSheet, useMedia, useSporeColors } from 'ui/src' import { Check } from 'ui/src/components/icons/Check' import { iconSizes } from 'ui/src/theme' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { useTranslation } from 'uniswap/src/i18n' -import { UniverseChainId } from 'uniswap/src/types/chains' import { ExplorerDataType, getExplorerLink } from 'uniswap/src/utils/linking' import { isMobileWeb } from 'utilities/src/platform' diff --git a/apps/web/src/components/Tokens/TokenDetails/index.tsx b/apps/web/src/components/Tokens/TokenDetails/index.tsx index 12e3c0df714..67d0ca9d3da 100644 --- a/apps/web/src/components/Tokens/TokenDetails/index.tsx +++ b/apps/web/src/components/Tokens/TokenDetails/index.tsx @@ -2,7 +2,7 @@ import { InterfacePageName } from '@uniswap/analytics-events' import { Currency } from '@uniswap/sdk-core' import { BreadcrumbNavContainer, BreadcrumbNavLink, CurrentPageBreadcrumb } from 'components/BreadcrumbNav' import { MobileBottomBar, TDPActionTabs } from 'components/NavBar/MobileBottomBar' -import TokenSafetyMessage from 'components/TokenSafety/TokenSafetyMessage' +import TokenSafetyMessage from 'components/TokenSafety/DeprecatedTokenSafetyMessage' import { ActivitySection } from 'components/Tokens/TokenDetails/ActivitySection' import BalanceSummary, { PageChainBalanceSummary } from 'components/Tokens/TokenDetails/BalanceSummary' import ChartSection from 'components/Tokens/TokenDetails/ChartSection' @@ -11,23 +11,30 @@ import StatsSection from 'components/Tokens/TokenDetails/StatsSection' import { TokenDescription } from 'components/Tokens/TokenDetails/TokenDescription' import { TokenDetailsHeader } from 'components/Tokens/TokenDetails/TokenDetailsHeader' import { Hr } from 'components/Tokens/TokenDetails/shared' -import { CHAIN_ID_TO_BACKEND_NAME, isSupportedChainId } from 'constants/chains' import { NATIVE_CHAIN_ID } from 'constants/tokens' import { getTokenDetailsURL } from 'graphql/data/util' import { useCurrency } from 'hooks/Tokens' import { useScreenSize } from 'hooks/screenSize/useScreenSize' -import useParsedQueryString from 'hooks/useParsedQueryString' import { ScrollDirection, useScroll } from 'hooks/useScroll' import deprecatedStyled from 'lib/styled-components' import { Swap } from 'pages/Swap' import { useTDPContext } from 'pages/TokenDetails/TDPContext' -import { PropsWithChildren, useCallback, useMemo } from 'react' +import { PropsWithChildren, useCallback, useMemo, useState } from 'react' import { ChevronRight } from 'react-feather' import { useNavigate } from 'react-router-dom' import { CurrencyState } from 'state/swap/types' import { Flex, useIsTouchDevice } from 'ui/src' +import { useUrlContext } from 'uniswap/src/contexts/UrlContext' +import { isUniverseChainId } from 'uniswap/src/features/chains/types' +import { toGraphQLChain } from 'uniswap/src/features/chains/utils' +import { FeatureFlags } from 'uniswap/src/features/gating/flags' +import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import Trace from 'uniswap/src/features/telemetry/Trace' +import { TokenWarningCard } from 'uniswap/src/features/tokens/TokenWarningCard' +import TokenWarningModal from 'uniswap/src/features/tokens/TokenWarningModal' +import { useCurrencyInfo } from 'uniswap/src/features/tokens/useCurrencyInfo' import { Trans } from 'uniswap/src/i18n' +import { currencyId } from 'uniswap/src/utils/currencyId' import { addressesAreEquivalent } from 'utils/addressesAreEquivalent' import { getInitialLogoUrl } from 'utils/getInitialLogoURL' @@ -69,6 +76,7 @@ function getCurrencyURLAddress(currency?: Currency): string { function useSwapInitialInputCurrency() { const { currency } = useTDPContext() + const { useParsedQueryString } = useUrlContext() const parsedQs = useParsedQueryString() const inputTokenAddress = useMemo(() => { @@ -80,8 +88,11 @@ function useSwapInitialInputCurrency() { function TDPSwapComponent() { const { address, currency, currencyChainId, warning } = useTDPContext() + const tokenProtectionEnabled = useFeatureFlag(FeatureFlags.TokenProtection) const navigate = useNavigate() + const currencyInfo = useCurrencyInfo(currencyId(currency)) + const handleCurrencyChange = useCallback( (tokens: CurrencyState) => { const inputCurrencyURLAddress = getCurrencyURLAddress(tokens.inputCurrency) @@ -107,10 +118,7 @@ function TDPSwapComponent() { const url = getTokenDetailsURL({ // The function falls back to "NATIVE" if the address is null address: newDefaultToken.isNative ? null : newDefaultToken.address, - chain: - CHAIN_ID_TO_BACKEND_NAME[ - isSupportedChainId(newDefaultToken.chainId) ? newDefaultToken.chainId : currencyChainId - ], + chain: toGraphQLChain(isUniverseChainId(newDefaultToken.chainId) ? newDefaultToken.chainId : currencyChainId), inputAddress: // If only one token was selected before we navigate, then it was the default token and it's being replaced. // On the new page, the *new* default token becomes the output, and we don't have another option to set as the input token. @@ -124,6 +132,9 @@ function TDPSwapComponent() { // Other token to prefill the swap form with const initialInputCurrency = useSwapInitialInputCurrency() + const [showWarningModal, setShowWarningModal] = useState(false) + const closeWarningModal = useCallback(() => setShowWarningModal(false), []) + return ( <> - {warning && } + {tokenProtectionEnabled ? ( + <> + setShowWarningModal(true)} /> + {currencyInfo && ( + // Intentionally duplicative with the TokenWarningModal in the swap component; this one only displays when user clicks "i" Info button on the TokenWarningCard + + )} + + ) : ( + warning && + )} ) } diff --git a/apps/web/src/components/Tokens/TokenDetails/tables/TokenDetailsPoolsTable.test.tsx b/apps/web/src/components/Tokens/TokenDetails/tables/TokenDetailsPoolsTable.test.tsx index b1d1b3233d1..7caf33a42ee 100644 --- a/apps/web/src/components/Tokens/TokenDetails/tables/TokenDetailsPoolsTable.test.tsx +++ b/apps/web/src/components/Tokens/TokenDetails/tables/TokenDetailsPoolsTable.test.tsx @@ -9,7 +9,7 @@ import { mocked } from 'test-utils/mocked' import { validBEPoolToken0, validBEPoolToken1, validParams } from 'test-utils/pools/fixtures' import { render, screen } from 'test-utils/render' import { ProtocolVersion } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' jest.mock('graphql/data/pools/usePoolsFromTokenAddress') jest.mock('react-router-dom', () => ({ diff --git a/apps/web/src/components/Tokens/TokenDetails/tables/TokenDetailsPoolsTable.tsx b/apps/web/src/components/Tokens/TokenDetails/tables/TokenDetailsPoolsTable.tsx index de2790a1fce..05b794b2407 100644 --- a/apps/web/src/components/Tokens/TokenDetails/tables/TokenDetailsPoolsTable.tsx +++ b/apps/web/src/components/Tokens/TokenDetails/tables/TokenDetailsPoolsTable.tsx @@ -7,7 +7,7 @@ import { PoolSortFields } from 'graphql/data/pools/useTopPools' import { OrderDirection } from 'graphql/data/util' import { useAtomValue, useResetAtom } from 'jotai/utils' import { useEffect, useMemo } from 'react' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' const HIDDEN_COLUMNS = [PoolSortFields.VolOverTvl] diff --git a/apps/web/src/components/Tokens/TokenDetails/tables/TransactionsTable.tsx b/apps/web/src/components/Tokens/TokenDetails/tables/TransactionsTable.tsx index 47577dfd613..43781e8ac75 100644 --- a/apps/web/src/components/Tokens/TokenDetails/tables/TransactionsTable.tsx +++ b/apps/web/src/components/Tokens/TokenDetails/tables/TransactionsTable.tsx @@ -15,13 +15,13 @@ import { import { useUpdateManualOutage } from 'featureFlags/flags/outageBanner' import { TokenTransactionType, useTokenTransactions } from 'graphql/data/useTokenTransactions' import { OrderDirection, unwrapToken } from 'graphql/data/util' -import { useActiveLocalCurrency } from 'hooks/useActiveLocalCurrency' import { useMemo, useReducer, useRef, useState } from 'react' import { EllipsisTamaguiStyle } from 'theme/components' import { Flex, Text, styled } from 'ui/src' import { Token as GQLToken } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { useAppFiatCurrency } from 'uniswap/src/features/fiatCurrency/hooks' import { Trans } from 'uniswap/src/i18n' -import { UniverseChainId } from 'uniswap/src/types/chains' import { ExplorerDataType, getExplorerLink } from 'uniswap/src/utils/linking' import { shortenAddress } from 'utilities/src/addresses' import { useFormatter } from 'utils/formatNumbers' @@ -56,7 +56,7 @@ interface SwapLeg { } export function TransactionsTable({ chainId, referenceToken }: { chainId: UniverseChainId; referenceToken: Token }) { - const activeLocalCurrency = useActiveLocalCurrency() + const activeLocalCurrency = useAppFiatCurrency() const { formatNumber, formatFiatPrice } = useFormatter() const [filterModalIsOpen, toggleFilterModal] = useReducer((s) => !s, false) const filterAnchorRef = useRef(null) diff --git a/apps/web/src/components/Tokens/TokenDetails/tables/__snapshots__/TokenDetailsPoolsTable.test.tsx.snap b/apps/web/src/components/Tokens/TokenDetails/tables/__snapshots__/TokenDetailsPoolsTable.test.tsx.snap index 10efb1ca8bb..c51b03407d4 100644 --- a/apps/web/src/components/Tokens/TokenDetails/tables/__snapshots__/TokenDetailsPoolsTable.test.tsx.snap +++ b/apps/web/src/components/Tokens/TokenDetails/tables/__snapshots__/TokenDetailsPoolsTable.test.tsx.snap @@ -304,12 +304,22 @@ exports[`TDPPoolTable renders data filled state 1`] = ` > USDC/ETH - - 1% - + + v3 + + + 1% + + diff --git a/apps/web/src/components/Tokens/TokenTable/NetworkFilter.tsx b/apps/web/src/components/Tokens/TokenTable/NetworkFilter.tsx index 23c25849529..014dc936317 100644 --- a/apps/web/src/components/Tokens/TokenTable/NetworkFilter.tsx +++ b/apps/web/src/components/Tokens/TokenTable/NetworkFilter.tsx @@ -3,29 +3,22 @@ import Badge from 'components/Badge/Badge' import { DropdownSelector, InternalMenuItem } from 'components/DropdownSelector' import { ChainLogo } from 'components/Logo/ChainLogo' import { AllNetworksIcon } from 'components/Tokens/TokenTable/icons' -import { - BACKEND_NOT_YET_SUPPORTED_CHAIN_IDS, - BACKEND_SUPPORTED_CHAINS, - InterfaceGqlChain, - useChainFromUrlParam, - useIsSupportedChainIdCallback, -} from 'constants/chains' -import { getSupportedGraphQlChain, supportedChainIdFromGQLChain } from 'graphql/data/util' import deprecatedStyled, { useTheme } from 'lib/styled-components' import { ExploreTab } from 'pages/Explore' import { useExploreParams } from 'pages/Explore/redirects' -import { Dispatch, SetStateAction, memo, useState } from 'react' +import { Dispatch, SetStateAction, memo, useCallback, useState } from 'react' import { Check } from 'react-feather' import { useNavigate } from 'react-router-dom' import { EllipsisTamaguiStyle } from 'theme/components' import { Flex, FlexProps, ScrollView, Text, styled } from 'ui/src' -import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' +import { useEnabledChains, useIsSupportedChainIdCallback } from 'uniswap/src/features/chains/hooks' +import { ALL_CHAIN_IDS, GqlChainId, UniverseChainId, UniverseChainInfo } from 'uniswap/src/features/chains/types' +import { isBackendSupportedChainId, isTestnetChain, toGraphQLChain } from 'uniswap/src/features/chains/utils' import Trace from 'uniswap/src/features/telemetry/Trace' import { ModalName } from 'uniswap/src/features/telemetry/constants' import { useTranslation } from 'uniswap/src/i18n' -import { UniverseChainId, UniverseChainInfo } from 'uniswap/src/types/chains' +import { useChainIdFromUrlParam } from 'utils/chainParams' const NetworkLabel = styled(Flex, { flexDirection: 'row', @@ -54,14 +47,33 @@ const StyledDropdown = { export default function TableNetworkFilter() { const [isMenuOpen, toggleMenu] = useState(false) const isSupportedChainCallback = useIsSupportedChainIdCallback() - const isMultichainExploreEnabled = useFeatureFlag(FeatureFlags.MultichainExplore) + const { isTestnetModeEnabled } = useEnabledChains() const exploreParams = useExploreParams() - const currentChain = getSupportedGraphQlChain(useChainFromUrlParam(), { - fallbackToEthereum: !isMultichainExploreEnabled, - }) + const currentChainId = useChainIdFromUrlParam() const tab = exploreParams.tab + const tableNetworkItemRenderer = useCallback( + (chainId: UniverseChainId) => { + if (!isSupportedChainCallback(chainId)) { + return null + } + const chainInfo = getChainInfo(chainId) + const supported = isBackendSupportedChainId(chainId) + return ( + + ) + }, + [isSupportedChainCallback, tab], + ) + return (
@@ -70,46 +82,28 @@ export default function TableNetworkFilter() { toggleOpen={toggleMenu} menuLabel={ - {!currentChain ? ( + {!currentChainId ? ( ) : ( - + )} } internalMenuItems={ - {isMultichainExploreEnabled && ( - + + {/* non-testnet backend supported chains */} + {ALL_CHAIN_IDS.filter(isBackendSupportedChainId) + .filter((c) => !isTestnetChain(c)) + .map(tableNetworkItemRenderer)} + {/* Testnet backend supported chains */} + {isTestnetModeEnabled + ? ALL_CHAIN_IDS.filter(isBackendSupportedChainId).filter(isTestnetChain).map(tableNetworkItemRenderer) + : null} + {/* Unsupported non-testnet backend supported chains */} + {ALL_CHAIN_IDS.filter((c) => !isBackendSupportedChainId(c) && !isTestnetChain(c)).map( + tableNetworkItemRenderer, )} - {BACKEND_SUPPORTED_CHAINS.map((network) => { - const chainId = supportedChainIdFromGQLChain(network) - const isSupportedChain = isSupportedChainCallback(chainId) - const chainInfo = isSupportedChain ? UNIVERSE_CHAIN_INFO[chainId] : undefined - return chainInfo ? ( - - ) : null - })} - {BACKEND_NOT_YET_SUPPORTED_CHAIN_IDS.map((network) => { - const isSupportedChain = isSupportedChainCallback(network) - const chainInfo = isSupportedChain ? UNIVERSE_CHAIN_INFO[network] : undefined - return chainInfo ? ( - - ) : null - })} } buttonStyle={{ height: 40 }} @@ -127,7 +121,7 @@ const TableNetworkItem = memo(function TableNetworkItem({ tab, unsupported, }: { - display: 'All networks' | InterfaceGqlChain + display: 'All networks' | GqlChainId chainInfo?: UniverseChainInfo toggleMenu: Dispatch> tab?: ExploreTab @@ -136,17 +130,15 @@ const TableNetworkItem = memo(function TableNetworkItem({ const navigate = useNavigate() const theme = useTheme() const { t } = useTranslation() - const isMultichainExploreEnabled = useFeatureFlag(FeatureFlags.MultichainExplore) const chainId = chainInfo?.id const exploreParams = useExploreParams() - const currentChain = getSupportedGraphQlChain( - useChainFromUrlParam(), - isMultichainExploreEnabled ? undefined : { fallbackToEthereum: true }, - ) - const isAllNetworks = display === 'All networks' && isMultichainExploreEnabled + const urlChainId = useChainIdFromUrlParam() + const currentChainInfo = urlChainId ? getChainInfo(urlChainId) : undefined + + const isAllNetworks = display === 'All networks' const isCurrentChain = isAllNetworks - ? !currentChain - : currentChain?.backendChain.chain === display && exploreParams.chainName + ? !currentChainInfo + : currentChainInfo?.backendChain.chain === display && exploreParams.chainName return ( { - return isRestExploreEnabled - ? { - tokens: restTopTokens?.slice(0, page * TABLE_PAGE_SIZE), - tokenSortRank: restTokenSortRank, - loading: restIsLoading, - sparklines: restSparklines, - error: restError, - } - : { - tokens: gqlTokens, - tokenSortRank: gqlTokenSortRank, - loading: gqlLoadingTokens, - sparklines: gqlSparklines, - error: gqlError, - } - }, [ - isRestExploreEnabled, - restTopTokens, - page, - restTokenSortRank, - restIsLoading, - restSparklines, - restError, - gqlTokens, - gqlTokenSortRank, - gqlLoadingTokens, - gqlSparklines, - gqlError, - ]) - return ( ) @@ -206,6 +160,7 @@ function TokenTable({ loadMore?: ({ onComplete }: { onComplete?: () => void }) => void }) { const { formatFiatPrice, formatNumber, formatDelta } = useFormatter() + const { defaultChainId } = useEnabledChains() const sortAscending = useAtomValue(sortAscendingAtom) const orderDirection = sortAscending ? OrderDirection.Asc : OrderDirection.Desc const sortMethod = useAtomValue(sortMethodAtom) @@ -221,14 +176,14 @@ function TokenTable({ : token?.pricePercentChange1Hour?.value const delta1d = isGqlToken ? token?.market?.pricePercentChange1Day?.value : token?.pricePercentChange1Day?.value const tokenSortIndex = tokenSortRank[token?.address ?? NATIVE_CHAIN_ID] - const chainId = getChainFromChainUrlParam(token?.chain.toLowerCase())?.id + const chainId = getChainIdFromChainUrlParam(token?.chain.toLowerCase()) const unwrappedToken = chainId ? unwrapToken(chainId, token) : token return { index: tokenSortIndex, tokenDescription: , price: isGqlToken ? token?.market?.price?.value ?? 0 : giveExploreStatDefaultValue(token?.price?.value), - testId: `token-table-row-${token?.address}`, + testId: `token-table-row-${unwrappedToken?.address ?? NATIVE_CHAIN_ID}`, percentChange1hr: ( <> @@ -266,7 +221,7 @@ function TokenTable({ ), link: getTokenDetailsURL({ address: unwrappedToken?.address, - chain: chainIdToBackendChain({ chainId, withFallback: true }), + chain: toGraphQLChain(chainId ?? defaultChainId), }), analytics: { elementName: InterfaceElementName.TOKENS_TABLE_ROW, @@ -284,7 +239,7 @@ function TokenTable({ linkState: { preloadedLogoSrc: isGqlToken ? token?.project?.logoUrl : token?.logo }, } }) ?? [], - [filterString, formatDelta, sparklines, timePeriod, tokenSortRank, tokens], + [defaultChainId, filterString, formatDelta, sparklines, timePeriod, tokenSortRank, tokens], ) const showLoadingSkeleton = loading || !!error diff --git a/apps/web/src/components/Tokens/constants.ts b/apps/web/src/components/Tokens/constants.ts index 3b8f4500cf5..fa0c5e3d082 100644 --- a/apps/web/src/components/Tokens/constants.ts +++ b/apps/web/src/components/Tokens/constants.ts @@ -1,6 +1,6 @@ // Breakpoints specifically for the token pages -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' // TODO(WEB-2968): Deprecate these in the new .info project export const MAX_WIDTH_MEDIA_BREAKPOINT = '1200px' diff --git a/apps/web/src/components/TopLevelModals/LaunchModal.tsx b/apps/web/src/components/TopLevelModals/LaunchModal.tsx index 177a6e9f067..779c5a45075 100644 --- a/apps/web/src/components/TopLevelModals/LaunchModal.tsx +++ b/apps/web/src/components/TopLevelModals/LaunchModal.tsx @@ -81,12 +81,19 @@ export function LaunchModal({ - - diff --git a/apps/web/src/components/TopLevelModals/index.tsx b/apps/web/src/components/TopLevelModals/index.tsx index 1a4b6be722e..78ca7094bab 100644 --- a/apps/web/src/components/TopLevelModals/index.tsx +++ b/apps/web/src/components/TopLevelModals/index.tsx @@ -17,6 +17,7 @@ import useAccountRiskCheck from 'hooks/useAccountRiskCheck' import Bag from 'nft/components/bag/Bag' import TransactionCompleteModal from 'nft/components/collection/TransactionCompleteModal' import { IncreaseLiquidityModal } from 'pages/IncreaseLiquidity/IncreaseLiquidityModal' +import { ClaimFeeModal } from 'pages/Pool/Positions/ClaimFeeModal' import { RemoveLiquidityModal } from 'pages/RemoveLiquidity/RemoveLiquidityModal' import { useCloseModal, useModalIsOpen, useToggleModal } from 'state/application/hooks' import { ApplicationModal } from 'state/application/reducer' @@ -32,6 +33,7 @@ export default function TopLevelModals() { const blockedAccountModalOpen = useModalIsOpen(ApplicationModal.BLOCKED_ACCOUNT) const isAddLiquidityModalOpen = useModalIsOpen(ModalName.AddLiquidity) const isRemoveLiquidityModalOpen = useModalIsOpen(ModalName.RemoveLiquidity) + const isClaimFeeModalOpen = useModalIsOpen(ModalName.ClaimFee) const isTestnetModeModalOpen = useModalIsOpen(ModalName.TestnetMode) const closeTestnetModeModal = useCloseModal(ModalName.TestnetMode) @@ -65,6 +67,7 @@ export default function TopLevelModals() { {isAddLiquidityModalOpen && } {isRemoveLiquidityModalOpen && } + {isClaimFeeModalOpen && } ) } diff --git a/apps/web/src/components/TransactionConfirmationModal/index.tsx b/apps/web/src/components/TransactionConfirmationModal/index.tsx index e81ef32ca7b..aeff239ef2c 100644 --- a/apps/web/src/components/TransactionConfirmationModal/index.tsx +++ b/apps/web/src/components/TransactionConfirmationModal/index.tsx @@ -9,7 +9,6 @@ import Modal from 'components/Modal' import AnimatedConfirmation from 'components/TransactionConfirmationModal/AnimatedConfirmation' import { AutoColumn, ColumnCenter } from 'components/deprecated/Column' import Row, { RowBetween, RowFixed } from 'components/deprecated/Row' -import { useIsSupportedChainId } from 'constants/chains' import { useCurrencyInfo } from 'hooks/Tokens' import { useAccount } from 'hooks/useAccount' import styled, { useTheme } from 'lib/styled-components' @@ -18,11 +17,12 @@ import { AlertCircle, ArrowUpCircle, CheckCircle } from 'react-feather' import { useTransaction } from 'state/transactions/hooks' import { isConfirmedTx } from 'state/transactions/utils' import { CloseIcon, CustomLightSpinner, ExternalLink, ThemedText } from 'theme/components' -import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' import { TransactionStatus } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' +import { useIsSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { isL2ChainId } from 'uniswap/src/features/chains/utils' import { Trans } from 'uniswap/src/i18n' -import { UniverseChainId } from 'uniswap/src/types/chains' import { ExplorerDataType, getExplorerLink } from 'uniswap/src/utils/linking' const Wrapper = styled.div` @@ -236,7 +236,7 @@ function L2Content({ const secondsToConfirm = confirmed && transaction.confirmedTime ? (transaction.confirmedTime - transaction.addedTime) / 1000 : undefined - const info = UNIVERSE_CHAIN_INFO[chainId] + const info = getChainInfo(chainId) return ( diff --git a/apps/web/src/components/Web3Provider/WebUniswapContext.tsx b/apps/web/src/components/Web3Provider/WebUniswapContext.tsx index a0b1cdab3a1..909c2b31141 100644 --- a/apps/web/src/components/Web3Provider/WebUniswapContext.tsx +++ b/apps/web/src/components/Web3Provider/WebUniswapContext.tsx @@ -7,32 +7,38 @@ import { PropsWithChildren, useCallback, useMemo } from 'react' import { useNavigate } from 'react-router-dom' import { UniswapProvider } from 'uniswap/src/contexts/UniswapContext' import { AccountMeta, AccountType } from 'uniswap/src/features/accounts/types' +import { Connector } from 'wagmi' // Adapts useEthersProvider to fit uniswap context hook shape function useWebProvider(chainId: number) { return useEthersProvider({ chainId }) } -function useWagmiAccount(): AccountMeta | undefined { +function useWagmiAccount(): { account?: AccountMeta; connector?: Connector } { const account = useAccount() return useMemo(() => { if (!account.address) { - return undefined + return { + account: undefined, + connector: account.connector, + } } return { - address: account.address, - type: AccountType.SignerMnemonic, + account: { + address: account.address, + type: AccountType.SignerMnemonic, + }, + connector: account.connector, } - }, [account.address]) + }, [account.address, account.connector]) } // Abstracts web-specific transaction flow objects for usage in cross-platform flows in the `uniswap` package. export function WebUniswapProvider({ children }: PropsWithChildren) { - const account = useWagmiAccount() + const { account, connector } = useWagmiAccount() const signer = useEthersSigner() - const { connector } = useAccount() const showSwapNetworkNotification = useShowSwapNetworkNotification() const navigate = useNavigate() const navigateToFiatOnRamp = useCallback(() => navigate(`/buy`, { replace: true }), [navigate]) diff --git a/apps/web/src/components/Web3Provider/index.tsx b/apps/web/src/components/Web3Provider/index.tsx index f5bb1f7f5ff..063ed5988bf 100644 --- a/apps/web/src/components/Web3Provider/index.tsx +++ b/apps/web/src/components/Web3Provider/index.tsx @@ -4,7 +4,6 @@ import { CustomUserProperties, InterfaceEventName, WalletConnectionResult } from import { recentConnectorIdAtom } from 'components/Web3Provider/constants' import { queryClient, wagmiConfig } from 'components/Web3Provider/wagmiConfig' import { walletTypeToAmplitudeWalletType } from 'components/Web3Provider/walletConnect' -import { useIsSupportedChainId } from 'constants/chains' import { RPC_PROVIDERS } from 'constants/providers' import { useAccount } from 'hooks/useAccount' import { ConnectionProvider } from 'hooks/useConnect' @@ -14,6 +13,7 @@ import { useUpdateAtom } from 'jotai/utils' import { ReactNode, useEffect } from 'react' import { useLocation } from 'react-router-dom' import { useConnectedWallets } from 'state/wallets/hooks' +import { useIsSupportedChainId } from 'uniswap/src/features/chains/hooks' import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' @@ -28,17 +28,14 @@ export default function Web3Provider({ children }: { children: ReactNode }) { return ( - - - {children} - + {children} ) } /** A component to run hooks under the Web3ReactProvider context. */ -function Updater() { +export function Web3ProviderUpdater() { const account = useAccount() const provider = useEthersWeb3Provider() diff --git a/apps/web/src/components/Web3Provider/wagmiConfig.ts b/apps/web/src/components/Web3Provider/wagmiConfig.ts index 9e02df6d2ab..73c116df417 100644 --- a/apps/web/src/components/Web3Provider/wagmiConfig.ts +++ b/apps/web/src/components/Web3Provider/wagmiConfig.ts @@ -2,9 +2,9 @@ import { QueryClient } from '@tanstack/react-query' import { injectedWithFallback } from 'components/Web3Provider/injectedWithFallback' import { WC_PARAMS, uniswapWalletConnect } from 'components/Web3Provider/walletConnect' import { UNISWAP_LOGO } from 'ui/src/assets' -import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' import { UNISWAP_WEB_URL } from 'uniswap/src/constants/urls' -import { COMBINED_CHAIN_IDS, UniverseChainId } from 'uniswap/src/types/chains' +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' +import { ALL_CHAIN_IDS, UniverseChainId } from 'uniswap/src/features/chains/types' import { createClient } from 'viem' import { createConfig, http } from 'wagmi' import { connect } from 'wagmi/actions' @@ -17,10 +17,7 @@ declare module 'wagmi' { } export const wagmiConfig = createConfig({ - chains: [ - UNIVERSE_CHAIN_INFO[UniverseChainId.Mainnet], - ...COMBINED_CHAIN_IDS.map((chainId) => UNIVERSE_CHAIN_INFO[chainId]), - ], + chains: [getChainInfo(UniverseChainId.Mainnet), ...ALL_CHAIN_IDS.map(getChainInfo)], connectors: [ injectedWithFallback(), walletConnect(WC_PARAMS), @@ -40,7 +37,7 @@ export const wagmiConfig = createConfig({ chain, batch: { multicall: true }, pollingInterval: 12_000, - transport: http(chain.rpcUrls.appOnly.http[0]), + transport: http(chain.rpcUrls.interface.http[0]), }) }, }) diff --git a/apps/web/src/components/swap/GasBreakdownTooltip.tsx b/apps/web/src/components/swap/GasBreakdownTooltip.tsx index a0ef2ff1ebd..1a8345780aa 100644 --- a/apps/web/src/components/swap/GasBreakdownTooltip.tsx +++ b/apps/web/src/components/swap/GasBreakdownTooltip.tsx @@ -2,7 +2,6 @@ import { Currency } from '@uniswap/sdk-core' import UniswapXRouterLabel, { UniswapXGradient } from 'components/RouterLabel/UniswapXRouterLabel' import { AutoColumn } from 'components/deprecated/Column' import Row from 'components/deprecated/Row' -import { chainIdToBackendChain, useSupportedChainId } from 'constants/chains' import styled from 'lib/styled-components' import { ReactNode } from 'react' import { InterfaceTrade } from 'state/routing/types' @@ -10,6 +9,8 @@ import { isPreviewTrade, isUniswapXTrade } from 'state/routing/utils' import { Divider, ExternalLink, ThemedText } from 'theme/components' import { nativeOnChain } from 'uniswap/src/constants/tokens' import { uniswapUrls } from 'uniswap/src/constants/urls' +import { useEnabledChains, useSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { toGraphQLChain } from 'uniswap/src/features/chains/utils' import { Trans } from 'uniswap/src/i18n' import { NumberType, useFormatter } from 'utils/formatNumbers' @@ -84,7 +85,8 @@ export function GasBreakdownTooltip({ trade }: GasBreakdownTooltipProps) { function NetworkCostDescription({ native }: { native: Currency }) { const supportedChain = useSupportedChainId(native.chainId) - const chainName = chainIdToBackendChain({ chainId: supportedChain, withFallback: true }) + const { defaultChainId } = useEnabledChains() + const chainName = toGraphQLChain(supportedChain ?? defaultChainId) return ( diff --git a/apps/web/src/components/swap/GasEstimateTooltip.tsx b/apps/web/src/components/swap/GasEstimateTooltip.tsx index ad80dc1d731..450c87add97 100644 --- a/apps/web/src/components/swap/GasEstimateTooltip.tsx +++ b/apps/web/src/components/swap/GasEstimateTooltip.tsx @@ -5,12 +5,12 @@ import { UniswapXGradient, UniswapXRouterIcon } from 'components/RouterLabel/Uni import { MouseoverTooltip, TooltipSize } from 'components/Tooltip' import Row, { RowFixed } from 'components/deprecated/Row' import { GasBreakdownTooltip } from 'components/swap/GasBreakdownTooltip' -import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains' import styled from 'lib/styled-components' import { SubmittableTrade } from 'state/routing/types' import { isUniswapXTrade } from 'state/routing/utils' import { useSwapAndLimitContext } from 'state/swap/useSwapContext' import { ThemedText } from 'theme/components' +import { chainSupportsGasEstimates } from 'uniswap/src/features/chains/utils' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { NumberType, useFormatter } from 'utils/formatNumbers' @@ -27,7 +27,7 @@ export default function GasEstimateTooltip({ trade, loading }: { trade?: Submitt const { chainId } = useSwapAndLimitContext() const { formatNumber } = useFormatter() - if (!trade || !chainId || !SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId)) { + if (!trade || !chainId || !chainSupportsGasEstimates(chainId)) { return null } diff --git a/apps/web/src/components/swap/SwapHeader.test.tsx b/apps/web/src/components/swap/SwapHeader.test.tsx index 19ce3d9769e..1429e1f69bb 100644 --- a/apps/web/src/components/swap/SwapHeader.test.tsx +++ b/apps/web/src/components/swap/SwapHeader.test.tsx @@ -2,7 +2,7 @@ import SwapHeader from 'components/swap/SwapHeader' import { Dispatch, PropsWithChildren, SetStateAction } from 'react' import { CurrencyState, EMPTY_DERIVED_SWAP_INFO, SwapAndLimitContext, SwapContext } from 'state/swap/types' import { act, render, screen } from 'test-utils/render' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { CurrencyField } from 'uniswap/src/types/currency' import { SwapTab } from 'uniswap/src/types/screens/interface' diff --git a/apps/web/src/components/swap/SwapLineItem.tsx b/apps/web/src/components/swap/SwapLineItem.tsx index 3691bef360c..bafbcbc3ab8 100644 --- a/apps/web/src/components/swap/SwapLineItem.tsx +++ b/apps/web/src/components/swap/SwapLineItem.tsx @@ -9,7 +9,6 @@ import { GasBreakdownTooltip, UniswapXDescription } from 'components/swap/GasBre import GasEstimateTooltip from 'components/swap/GasEstimateTooltip' import { RoutingTooltip, SwapRoute } from 'components/swap/SwapRoute' import TradePrice from 'components/swap/TradePrice' -import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains' import { useUSDPrice } from 'hooks/useUSDPrice' import styled, { DefaultTheme } from 'lib/styled-components' import React, { ReactNode, useEffect, useState } from 'react' @@ -19,6 +18,7 @@ import { isLimitTrade, isPreviewTrade, isUniswapXTrade, isUniswapXTradeType } fr import { useUserSlippageTolerance } from 'state/user/hooks' import { SlippageTolerance } from 'state/user/types' import { ExternalLink, ThemedText } from 'theme/components' +import { chainSupportsGasEstimates } from 'uniswap/src/features/chains/utils' import { Trans, t } from 'uniswap/src/i18n' import { NumberType, useFormatter } from 'utils/formatNumbers' import { getPriceImpactColor } from 'utils/prices' @@ -157,7 +157,7 @@ function useLineItem(props: SwapLineItemProps): LineItemData | undefined { tooltipSize: isUniswapX ? TooltipSize.Small : TooltipSize.Large, } case SwapLineItemType.NETWORK_COST: - if (!SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId)) { + if (!chainSupportsGasEstimates(chainId)) { return undefined } return { diff --git a/apps/web/src/components/swap/SwapRoute.tsx b/apps/web/src/components/swap/SwapRoute.tsx index ee1a649f82d..f082e4319ef 100644 --- a/apps/web/src/components/swap/SwapRoute.tsx +++ b/apps/web/src/components/swap/SwapRoute.tsx @@ -2,20 +2,20 @@ import RouterLabel from 'components/RouterLabel' import Column from 'components/deprecated/Column' import { RowBetween } from 'components/deprecated/Row' import { UniswapXDescription } from 'components/swap/GasBreakdownTooltip' -import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains' import { ClassicTrade, SubmittableTrade } from 'state/routing/types' import { isClassicTrade } from 'state/routing/utils' import { Separator, ThemedText } from 'theme/components' import RoutingDiagram from 'uniswap/src/components/RoutingDiagram/RoutingDiagram' +import { chainSupportsGasEstimates } from 'uniswap/src/features/chains/utils' import { Trans } from 'uniswap/src/i18n' import { NumberType, useFormatter } from 'utils/formatNumbers' import getRoutingDiagramEntries from 'utils/getRoutingDiagramEntries' // TODO(WEB-2022) -// Can `trade.gasUseEstimateUSD` be defined when `chainId` is not in `SUPPORTED_GAS_ESTIMATE_CHAIN_IDS`? +// Can `trade.gasUseEstimateUSD` be defined when `chainId` doesn't support gas estimates? function useGasPrice({ gasUseEstimateUSD, inputAmount }: ClassicTrade) { const { formatNumber } = useFormatter() - if (!gasUseEstimateUSD || !SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(inputAmount.currency.chainId)) { + if (!gasUseEstimateUSD || !chainSupportsGasEstimates(inputAmount.currency.chainId)) { return undefined } diff --git a/apps/web/src/components/swap/UnsupportedCurrencyFooter.tsx b/apps/web/src/components/swap/UnsupportedCurrencyFooter.tsx index f28f8d8b0a9..f010a603c15 100644 --- a/apps/web/src/components/swap/UnsupportedCurrencyFooter.tsx +++ b/apps/web/src/components/swap/UnsupportedCurrencyFooter.tsx @@ -13,8 +13,8 @@ import { CloseIcon, ExternalLink, ThemedText } from 'theme/components' import { Z_INDEX } from 'theme/zIndex' import { Text } from 'ui/src' import { SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { Trans } from 'uniswap/src/i18n' -import { UniverseChainId } from 'uniswap/src/types/chains' import { ExplorerDataType, getExplorerLink } from 'uniswap/src/utils/linking' const DetailsFooter = styled.div<{ show: boolean }>` diff --git a/apps/web/src/connection/web3reactShim.ts b/apps/web/src/connection/web3reactShim.ts index 52ca2978221..d56b9d2d144 100644 --- a/apps/web/src/connection/web3reactShim.ts +++ b/apps/web/src/connection/web3reactShim.ts @@ -1,7 +1,7 @@ import { useAccount } from 'hooks/useAccount' import { useEthersProvider } from 'hooks/useEthersProvider' import { useMemo } from 'react' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' // eslint-disable-next-line import/no-unused-modules -- shim is used via a build alias in craco.config.cjs export function useWeb3React() { diff --git a/apps/web/src/constants/chains.test.ts b/apps/web/src/constants/chains.test.ts index 083879b10e7..19c387c7dda 100644 --- a/apps/web/src/constants/chains.test.ts +++ b/apps/web/src/constants/chains.test.ts @@ -1,112 +1,20 @@ -import { - BACKEND_NOT_YET_SUPPORTED_CHAIN_IDS, - BACKEND_SUPPORTED_CHAINS, - CHAIN_IDS_TO_NAMES, - CHAIN_ID_TO_BACKEND_NAME, - CHAIN_NAME_TO_CHAIN_ID, - ChainSlug, - INFURA_PREFIX_TO_CHAIN_ID, - InterfaceGqlChain, - SUPPORTED_GAS_ESTIMATE_CHAIN_IDS, - TESTNET_CHAIN_IDS, - UX_SUPPORTED_GQL_CHAINS, - getChainFromChainUrlParam, - getChainPriority, -} from 'constants/chains' -import { GQL_MAINNET_CHAINS, GQL_TESTNET_CHAINS, UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' import { Chain } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { SUPPORTED_CHAIN_IDS, UniverseChainId } from 'uniswap/src/types/chains' +import { ALL_GQL_CHAINS, GQL_MAINNET_CHAINS } from 'uniswap/src/features/chains/chainInfo' +import { GqlChainId, UniverseChainId } from 'uniswap/src/features/chains/types' +import { toGraphQLChain } from 'uniswap/src/features/chains/utils' +import { getChainIdFromChainUrlParam } from 'utils/chainParams' -// Define an array of test cases with chainId and expected priority -const chainPriorityTestCases: [UniverseChainId, number][] = [ - [UniverseChainId.Mainnet, 0], - [UniverseChainId.Sepolia, 0], - [UniverseChainId.ArbitrumOne, 1], - [UniverseChainId.Optimism, 2], - [UniverseChainId.Polygon, 3], - [UniverseChainId.Base, 4], - [UniverseChainId.Bnb, 5], - [UniverseChainId.Avalanche, 6], - [UniverseChainId.Celo, 7], - [UniverseChainId.Blast, 8], - [UniverseChainId.Zora, 9], - [UniverseChainId.Zksync, 10], -] - -test.each(chainPriorityTestCases)( - 'getChainPriority returns expected priority for a given ChainId %O', - (chainId: UniverseChainId, expectedPriority: number) => { - const priority = getChainPriority(chainId) - expect(priority).toBe(expectedPriority) - }, -) - -const chainIdNames: { [chainId in UniverseChainId]: string } = { - [UniverseChainId.Mainnet]: 'mainnet', - [UniverseChainId.Sepolia]: 'sepolia', - [UniverseChainId.Polygon]: 'polygon', - [UniverseChainId.Celo]: 'celo', - [UniverseChainId.ArbitrumOne]: 'arbitrum', - [UniverseChainId.Optimism]: 'optimism', - [UniverseChainId.Bnb]: 'bnb', - [UniverseChainId.Avalanche]: 'avalanche', - [UniverseChainId.Base]: 'base', - [UniverseChainId.Blast]: 'blast', - [UniverseChainId.WorldChain]: 'worldchain', - [UniverseChainId.Zora]: 'zora', - [UniverseChainId.Zksync]: 'zksync', - [UniverseChainId.AstrochainSepolia]: 'astrochain', -} as const - -test.each(Object.keys(chainIdNames).map((key) => parseInt(key) as UniverseChainId))( - 'CHAIN_IDS_TO_NAMES generates the correct chainIds', - (chainId: UniverseChainId) => { - const name = CHAIN_IDS_TO_NAMES[chainId] - expect(name).toBe(chainIdNames[chainId]) - }, -) - -const supportedGasEstimateChains = [ - UniverseChainId.Mainnet, - UniverseChainId.Polygon, - UniverseChainId.Celo, - UniverseChainId.Optimism, - UniverseChainId.ArbitrumOne, - UniverseChainId.Bnb, - UniverseChainId.Avalanche, - UniverseChainId.Base, - UniverseChainId.Blast, - UniverseChainId.Zora, -] as const - -test.each(supportedGasEstimateChains)( - 'SUPPORTED_GAS_ESTIMATE_CHAIN_IDS generates the correct chainIds', - (chainId: UniverseChainId) => { - expect(SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId)).toBe(true) - expect(SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.length).toEqual(supportedGasEstimateChains.length) - }, -) - -const testnetChainIds = [UniverseChainId.Sepolia, UniverseChainId.AstrochainSepolia] as const - -test.each(testnetChainIds)('TESTNET_CHAIN_IDS generates the correct chainIds', (chainId: UniverseChainId) => { - expect(TESTNET_CHAIN_IDS.includes(chainId)).toBe(true) - expect(TESTNET_CHAIN_IDS.length).toEqual(testnetChainIds.length) -}) - -const uxSupportedGQLChains = [...GQL_MAINNET_CHAINS, ...GQL_TESTNET_CHAINS] as const - -test.each(GQL_MAINNET_CHAINS)('GQL_MAINNET_CHAINS generates the correct chains', (chain: InterfaceGqlChain) => { +test.each(GQL_MAINNET_CHAINS)('GQL_MAINNET_CHAINS generates the correct chains', (chain: GqlChainId) => { expect(GQL_MAINNET_CHAINS.includes(chain)).toBe(true) expect(GQL_MAINNET_CHAINS.length).toEqual(GQL_MAINNET_CHAINS.length) }) -test.each(uxSupportedGQLChains)('UX_SUPPORTED_GQL_CHAINS generates the correct chains', (chain: InterfaceGqlChain) => { - expect(UX_SUPPORTED_GQL_CHAINS.includes(chain)).toBe(true) - expect(UX_SUPPORTED_GQL_CHAINS.length).toEqual(uxSupportedGQLChains.length) +test.each(ALL_GQL_CHAINS)('UX_SUPPORTED_GQL_CHAINS generates the correct chains', (chain: GqlChainId) => { + expect(ALL_GQL_CHAINS.includes(chain)).toBe(true) + expect(ALL_GQL_CHAINS.length).toEqual(ALL_GQL_CHAINS.length) }) -const chainIdToBackendName: { [key: number]: InterfaceGqlChain } = { +const chainIdToBackendName: { [key: number]: GqlChainId } = { [UniverseChainId.Mainnet]: Chain.Ethereum, [UniverseChainId.Sepolia]: Chain.EthereumSepolia, [UniverseChainId.Polygon]: Chain.Polygon, @@ -123,133 +31,29 @@ const chainIdToBackendName: { [key: number]: InterfaceGqlChain } = { test.each(Object.keys(chainIdToBackendName).map((key) => parseInt(key) as UniverseChainId))( 'CHAIN_IDS_TO_BACKEND_NAME generates the correct chains', (chainId: UniverseChainId) => { - const name = CHAIN_ID_TO_BACKEND_NAME[chainId] + const name = toGraphQLChain(chainId) expect(name).toBe(chainIdToBackendName[chainId]) }, ) -const chainToChainId = { - [Chain.Ethereum]: UniverseChainId.Mainnet, - [Chain.EthereumSepolia]: UniverseChainId.Sepolia, - [Chain.Polygon]: UniverseChainId.Polygon, - [Chain.Celo]: UniverseChainId.Celo, - [Chain.Optimism]: UniverseChainId.Optimism, - [Chain.Arbitrum]: UniverseChainId.ArbitrumOne, - [Chain.Bnb]: UniverseChainId.Bnb, - [Chain.Avalanche]: UniverseChainId.Avalanche, - [Chain.Base]: UniverseChainId.Base, - [Chain.Blast]: UniverseChainId.Blast, - [Chain.Worldchain]: UniverseChainId.WorldChain, - [Chain.Zora]: UniverseChainId.Zora, - [Chain.Zksync]: UniverseChainId.Zksync, -} as const - -test.each(Object.keys(chainToChainId).map((key) => key as InterfaceGqlChain))( - 'CHAIN_NAME_TO_CHAIN_ID generates the correct chains', - (chain) => { - const chainId = CHAIN_NAME_TO_CHAIN_ID[chain] - expect(chainId).toBe(chainToChainId[chain as Exclude]) // will remove this after AstrochainSepolia is added - }, -) - -const backendSupportedChains = [ - Chain.Ethereum, - Chain.Arbitrum, - Chain.Optimism, - Chain.Polygon, - Chain.Base, - Chain.Bnb, - Chain.Celo, - Chain.Blast, - Chain.Avalanche, - Chain.Worldchain, - Chain.Zksync, - Chain.Zora, -] as const - -test.each(backendSupportedChains)( - 'BACKEND_SUPPORTED_CHAINS generates the correct chains', - (chain: InterfaceGqlChain) => { - expect(BACKEND_SUPPORTED_CHAINS.includes(chain)).toBe(true) - expect(BACKEND_SUPPORTED_CHAINS.length).toEqual(backendSupportedChains.length) - }, -) - -const backendNotyetSupportedChainIds = [] as const - -test('BACKEND_NOT_YET_SUPPORTED_CHAIN_IDS array is empty', () => { - expect(backendNotyetSupportedChainIds).toEqual(BACKEND_NOT_YET_SUPPORTED_CHAIN_IDS) -}) - -const infuraPrefixToChainId: { [prefix: string]: UniverseChainId } = { - mainnet: UniverseChainId.Mainnet, - sepolia: UniverseChainId.Sepolia, - 'optimism-mainnet': UniverseChainId.Optimism, - 'arbitrum-mainnet': UniverseChainId.ArbitrumOne, - 'polygon-mainnet': UniverseChainId.Polygon, - 'avalanche-mainnet': UniverseChainId.Avalanche, - 'base-mainnet': UniverseChainId.Base, - 'blast-mainnet': UniverseChainId.Blast, -} - -test.each(Object.keys(infuraPrefixToChainId))('INFURA_PREFIX_TO_CHAIN_ID generates the correct chains', (chainName) => { - const chain = INFURA_PREFIX_TO_CHAIN_ID[chainName] - expect(chain).toEqual(infuraPrefixToChainId[chainName]) - expect(Object.keys(infuraPrefixToChainId).length).toEqual(Object.keys(infuraPrefixToChainId).length) -}) - -function getBlocksPerMainnetEpochForChainId(chainId: number | undefined): number { - // Average block times were pulled from https://dune.com/jacobdcastro/avg-block-times on 2024-03-14, - // and corroborated with that chain's documentation/explorer. - // Blocks per mainnet epoch is computed as `Math.floor(12s / AVG_BLOCK_TIME)` and hard-coded. - switch (chainId) { - case UniverseChainId.ArbitrumOne: - return 46 - case UniverseChainId.Optimism: - return 6 - case UniverseChainId.Polygon: - return 5 - case UniverseChainId.Base: - return 6 - case UniverseChainId.Bnb: - return 4 - case UniverseChainId.Avalanche: - return 6 - case UniverseChainId.Celo: - return 2 - case UniverseChainId.Zksync: - return 12 - default: - return 1 - } -} - -test.each(SUPPORTED_CHAIN_IDS)( - 'CHAIN_INFO maps the correct blocks per mainnet epoch for chainId', - (chainId: UniverseChainId) => { - const block = UNIVERSE_CHAIN_INFO[chainId].blockPerMainnetEpochForChainId - expect(block).toEqual(getBlocksPerMainnetEpochForChainId(chainId)) - }, -) - describe('getChainFromChainUrlParam', () => { it('should return true for valid chain slug', () => { const validChainName = 'ethereum' - expect(getChainFromChainUrlParam(validChainName)?.id).toBe(UniverseChainId.Mainnet) + expect(getChainIdFromChainUrlParam(validChainName)).toBe(UniverseChainId.Mainnet) }) it('should return false for undefined chain slug', () => { const undefinedChainName = undefined - expect(getChainFromChainUrlParam(undefinedChainName)?.id).toBe(undefined) + expect(getChainIdFromChainUrlParam(undefinedChainName)).toBe(undefined) }) it('should return false for invalid chain slug', () => { const invalidChainName = 'invalidchain' - expect(getChainFromChainUrlParam(invalidChainName as ChainSlug)?.id).toBe(undefined) + expect(getChainIdFromChainUrlParam(invalidChainName)).toBe(undefined) }) it('should return false for a misconfigured chain slug', () => { const invalidChainName = 'eThErEuM' - expect(getChainFromChainUrlParam(invalidChainName as ChainSlug)?.id).toBe(undefined) + expect(getChainIdFromChainUrlParam(invalidChainName)).toBe(undefined) }) }) diff --git a/apps/web/src/constants/chains.ts b/apps/web/src/constants/chains.ts deleted file mode 100644 index 5c263661f53..00000000000 --- a/apps/web/src/constants/chains.ts +++ /dev/null @@ -1,193 +0,0 @@ -/* eslint-disable rulesdir/no-undefined-or */ -import { Currency, V2_ROUTER_ADDRESSES } from '@uniswap/sdk-core' -import ms from 'ms' -import { useCallback } from 'react' -import { useParams } from 'react-router-dom' -import { GQL_MAINNET_CHAINS, GQL_TESTNET_CHAINS, UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' -import { Chain as BackendChainId } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { useFeatureFlaggedChainIds } from 'uniswap/src/features/chains/utils' -import { ArbitrumXV2ExperimentGroup, Experiments } from 'uniswap/src/features/gating/experiments' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useExperimentGroupName, useFeatureFlag } from 'uniswap/src/features/gating/hooks' -import { COMBINED_CHAIN_IDS, UniverseChainId, UniverseChainInfo } from 'uniswap/src/types/chains' - -export const AVERAGE_L1_BLOCK_TIME = ms(`12s`) - -export function isSupportedChainId(chainId?: number | UniverseChainId | null): chainId is UniverseChainId { - return !!chainId && COMBINED_CHAIN_IDS.includes(chainId as UniverseChainId) -} - -export function useIsSupportedChainId(chainId?: number | UniverseChainId): chainId is UniverseChainId { - const featureFlaggedChains = useFeatureFlaggedChainIds() - - const chainIsNotEnabled = !featureFlaggedChains.includes(chainId as UniverseChainId) - return chainIsNotEnabled ? false : isSupportedChainId(chainId) -} - -export function useIsSupportedChainIdCallback() { - const featureFlaggedChains = useFeatureFlaggedChainIds() - - return useCallback( - (chainId?: number | UniverseChainId): chainId is UniverseChainId => { - const chainIsNotEnabled = !featureFlaggedChains.includes(chainId as UniverseChainId) - return chainIsNotEnabled ? false : isSupportedChainId(chainId) - }, - [featureFlaggedChains], - ) -} - -export function useSupportedChainId(chainId?: number): UniverseChainId | undefined { - const featureFlaggedChains = useFeatureFlaggedChainIds() - if (!chainId || COMBINED_CHAIN_IDS.indexOf(chainId) === -1) { - return undefined - } - - const chainDisabled = !featureFlaggedChains.includes(chainId as UniverseChainId) - return chainDisabled ? undefined : (chainId as UniverseChainId) -} - -export type InterfaceGqlChain = Exclude - -export type ChainSlug = UniverseChainInfo['urlParam'] -export const isChainUrlParam = (str?: string): str is ChainSlug => - !!str && Object.values(UNIVERSE_CHAIN_INFO).some((chain) => chain.urlParam === str) -export const getChainUrlParam = (str?: string): ChainSlug | undefined => (isChainUrlParam(str) ? str : undefined) - -export function getChain(options: { chainId: UniverseChainId }): UniverseChainInfo -export function getChain(options: { chainId?: UniverseChainId; withFallback: true }): UniverseChainInfo -export function getChain(options: { chainId?: UniverseChainId; withFallback?: boolean }): UniverseChainInfo | undefined -export function getChain({ - chainId, - withFallback, -}: { - chainId?: UniverseChainId - withFallback?: boolean -}): UniverseChainInfo | undefined { - return chainId - ? UNIVERSE_CHAIN_INFO[chainId] - : withFallback - ? UNIVERSE_CHAIN_INFO[UniverseChainId.Mainnet] - : undefined -} - -export const CHAIN_IDS_TO_NAMES = Object.fromEntries( - Object.entries(UNIVERSE_CHAIN_INFO).map(([key, value]) => [key, value.interfaceName]), -) as { [chainId in UniverseChainId]: string } - -export const UX_SUPPORTED_GQL_CHAINS = [...GQL_MAINNET_CHAINS, ...GQL_TESTNET_CHAINS] - -export const CHAIN_ID_TO_BACKEND_NAME = Object.fromEntries( - Object.entries(UNIVERSE_CHAIN_INFO).map(([key, value]) => [key, value.backendChain.chain]), -) as { [chainId in UniverseChainId]: InterfaceGqlChain } - -export function chainIdToBackendChain(options: { chainId: UniverseChainId }): InterfaceGqlChain -export function chainIdToBackendChain(options: { chainId?: UniverseChainId; withFallback: true }): InterfaceGqlChain -export function chainIdToBackendChain(options: { - chainId?: UniverseChainId - withFallback?: boolean -}): InterfaceGqlChain | undefined -export function chainIdToBackendChain({ - chainId, - withFallback, -}: { - chainId?: UniverseChainId - withFallback?: boolean -}): InterfaceGqlChain | undefined { - return chainId - ? CHAIN_ID_TO_BACKEND_NAME[chainId] - : withFallback - ? CHAIN_ID_TO_BACKEND_NAME[UniverseChainId.Mainnet] - : undefined -} - -export const CHAIN_NAME_TO_CHAIN_ID = Object.fromEntries( - Object.entries(UNIVERSE_CHAIN_INFO) - .filter(([, value]) => !value.backendChain.isSecondaryChain) - .map(([key, value]) => [value.backendChain.chain, parseInt(key) as UniverseChainId]), -) as { [chain in InterfaceGqlChain]: UniverseChainId } - -export const SUPPORTED_GAS_ESTIMATE_CHAIN_IDS = Object.keys(UNIVERSE_CHAIN_INFO) - .filter((key) => UNIVERSE_CHAIN_INFO[parseInt(key) as UniverseChainId].supportsGasEstimates) - .map((key) => parseInt(key) as UniverseChainId) - -export const PRODUCTION_CHAIN_IDS: UniverseChainId[] = Object.values(UNIVERSE_CHAIN_INFO) - .filter((chain) => !chain.testnet) - .map((chain) => chain.id) - -export const TESTNET_CHAIN_IDS = Object.keys(UNIVERSE_CHAIN_INFO) - .filter((key) => UNIVERSE_CHAIN_INFO[parseInt(key) as UniverseChainId].testnet) - .map((key) => parseInt(key) as UniverseChainId) - -/** - * @deprecated when v2 pools are enabled on chains supported through sdk-core - */ -export const SUPPORTED_V2POOL_CHAIN_IDS = Object.keys(V2_ROUTER_ADDRESSES).map((chainId) => parseInt(chainId)) - -export const BACKEND_SUPPORTED_CHAINS = Object.keys(UNIVERSE_CHAIN_INFO) - .filter((key) => { - const chainId = parseInt(key) as UniverseChainId - return ( - UNIVERSE_CHAIN_INFO[chainId].backendChain.backendSupported && - !UNIVERSE_CHAIN_INFO[chainId].backendChain.isSecondaryChain && - !UNIVERSE_CHAIN_INFO[chainId].testnet - ) - }) - .map((key) => UNIVERSE_CHAIN_INFO[parseInt(key) as UniverseChainId].backendChain.chain as InterfaceGqlChain) - -export const BACKEND_NOT_YET_SUPPORTED_CHAIN_IDS = GQL_MAINNET_CHAINS.filter( - (chain) => !BACKEND_SUPPORTED_CHAINS.includes(chain), -).map((chain) => CHAIN_NAME_TO_CHAIN_ID[chain]) as [UniverseChainId] - -export const INFURA_PREFIX_TO_CHAIN_ID: { [prefix: string]: UniverseChainId } = Object.fromEntries( - Object.entries(UNIVERSE_CHAIN_INFO) - .filter(([, value]) => !!value.infuraPrefix) - .map(([key, value]) => [value.infuraPrefix, parseInt(key) as UniverseChainId]), -) - -/** - * Get the priority of a chainId based on its relevance to the user. - * @param {ChainId} chainId - The chainId to determine the priority for. - * @returns {number} The priority of the chainId, the lower the priority, the earlier it should be displayed, with base of MAINNET=0. - */ -export function getChainPriority(chainId: UniverseChainId): number { - if (isSupportedChainId(chainId)) { - return UNIVERSE_CHAIN_INFO[chainId].chainPriority - } - - return Infinity -} - -export function useIsUniswapXSupportedChain(chainId?: number) { - const xv2ArbitrumEnabled = - useExperimentGroupName(Experiments.ArbitrumXV2OpenOrders) === ArbitrumXV2ExperimentGroup.Test - const isPriorityOrdersEnabled = useFeatureFlag(FeatureFlags.UniswapXPriorityOrders) - - return ( - chainId === UniverseChainId.Mainnet || - (xv2ArbitrumEnabled && chainId === UniverseChainId.ArbitrumOne) || - (isPriorityOrdersEnabled && chainId === UniverseChainId.Base) // UniswapX priority orders are only available on Base for now - ) -} - -export function isStablecoin(currency?: Currency): boolean { - if (!currency) { - return false - } - - return getChain({ chainId: currency.chainId as UniverseChainId }).stablecoins.some((stablecoin) => - stablecoin.equals(currency), - ) -} - -export function getChainFromChainUrlParam(chainUrlParam?: ChainSlug): UniverseChainInfo | undefined { - return chainUrlParam !== undefined - ? Object.values(UNIVERSE_CHAIN_INFO).find((chain) => chainUrlParam === chain.urlParam) - : undefined -} - -export function useChainFromUrlParam(): UniverseChainInfo | undefined { - const chainName = useParams<{ chainName?: string }>().chainName - // In the case where /explore/:chainName is used, the chainName is passed as a tab param - const tab = useParams<{ tab?: string }>().tab - return getChainFromChainUrlParam(getChainUrlParam(chainName ?? tab)) -} diff --git a/apps/web/src/constants/deprecatedTokenSafety.tsx b/apps/web/src/constants/deprecatedTokenSafety.tsx index 3ea0eaa1d25..c9a76a838bc 100644 --- a/apps/web/src/constants/deprecatedTokenSafety.tsx +++ b/apps/web/src/constants/deprecatedTokenSafety.tsx @@ -5,8 +5,8 @@ */ import { useCurrencyInfo } from 'hooks/Tokens' import { SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { Trans, t } from 'uniswap/src/i18n' -import { UniverseChainId } from 'uniswap/src/types/chains' export const TOKEN_SAFETY_ARTICLE = 'https://support.uniswap.org/hc/en-us/articles/8723118437133' diff --git a/apps/web/src/constants/providers.ts b/apps/web/src/constants/providers.ts index 8aeaa777396..7f2359ed25e 100644 --- a/apps/web/src/constants/providers.ts +++ b/apps/web/src/constants/providers.ts @@ -1,15 +1,12 @@ -import { CHAIN_IDS_TO_NAMES } from 'constants/chains' import AppJsonRpcProvider from 'rpc/AppJsonRpcProvider' import ConfiguredJsonRpcProvider from 'rpc/ConfiguredJsonRpcProvider' -import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' -import { SUPPORTED_CHAIN_IDS, UniverseChainId } from 'uniswap/src/types/chains' +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' +import { SUPPORTED_CHAIN_IDS, UniverseChainId } from 'uniswap/src/features/chains/types' function getAppProvider(chainId: UniverseChainId) { - const info = UNIVERSE_CHAIN_INFO[chainId] + const info = getChainInfo(chainId) return new AppJsonRpcProvider( - info.rpcUrls.appOnly.http.map( - (url) => new ConfiguredJsonRpcProvider(url, { chainId, name: CHAIN_IDS_TO_NAMES[chainId] }), - ), + info.rpcUrls.interface.http.map((url) => new ConfiguredJsonRpcProvider(url, { chainId, name: info.interfaceName })), ) } diff --git a/apps/web/src/constants/routing.test.ts b/apps/web/src/constants/routing.test.ts index 44d686c6142..94583f60e08 100644 --- a/apps/web/src/constants/routing.test.ts +++ b/apps/web/src/constants/routing.test.ts @@ -1,5 +1,5 @@ import { COMMON_BASES } from 'uniswap/src/constants/routing' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' describe('Routing', () => { describe('COMMON_BASES', () => { diff --git a/apps/web/src/constants/routing.ts b/apps/web/src/constants/routing.ts index 9e07460a298..e678c6336e1 100644 --- a/apps/web/src/constants/routing.ts +++ b/apps/web/src/constants/routing.ts @@ -16,7 +16,7 @@ import { WETH_AVALANCHE, WRAPPED_NATIVE_CURRENCY, } from 'uniswap/src/constants/tokens' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' type ChainTokenList = { readonly [chainId: number]: Token[] diff --git a/apps/web/src/featureFlags/dynamicConfig/quickRouteChains.ts b/apps/web/src/featureFlags/dynamicConfig/quickRouteChains.ts index 4ee3118beea..02ddefb7ddb 100644 --- a/apps/web/src/featureFlags/dynamicConfig/quickRouteChains.ts +++ b/apps/web/src/featureFlags/dynamicConfig/quickRouteChains.ts @@ -1,6 +1,6 @@ +import { SUPPORTED_CHAIN_IDS, UniverseChainId } from 'uniswap/src/features/chains/types' import { DynamicConfigs, QuickRouteChainsConfigKey } from 'uniswap/src/features/gating/configs' import { useDynamicConfigValue } from 'uniswap/src/features/gating/hooks' -import { SUPPORTED_CHAIN_IDS, UniverseChainId } from 'uniswap/src/types/chains' import { logger } from 'utilities/src/logger/logger' export function useQuickRouteChains(): UniverseChainId[] { diff --git a/apps/web/src/featureFlags/flags/outageBanner.ts b/apps/web/src/featureFlags/flags/outageBanner.ts index 77ba15a4d48..e9f5b7cd3e2 100644 --- a/apps/web/src/featureFlags/flags/outageBanner.ts +++ b/apps/web/src/featureFlags/flags/outageBanner.ts @@ -1,9 +1,9 @@ import { ApolloError } from '@apollo/client' import { atomWithReset, useResetAtom, useUpdateAtom } from 'jotai/utils' import { ProtocolVersion } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' -import { UniverseChainId } from 'uniswap/src/types/chains' export type ChainOutageData = { chainId: UniverseChainId diff --git a/apps/web/src/featureFlags/useFeatureFlagUrlOverrides.tsx b/apps/web/src/featureFlags/useFeatureFlagUrlOverrides.tsx index 1e54108df99..8101771179a 100644 --- a/apps/web/src/featureFlags/useFeatureFlagUrlOverrides.tsx +++ b/apps/web/src/featureFlags/useFeatureFlagUrlOverrides.tsx @@ -1,12 +1,13 @@ -import useParsedQueryString from 'hooks/useParsedQueryString' import { useContext, useEffect } from 'react' +import { useUrlContext } from 'uniswap/src/contexts/UrlContext' import { Statsig, StatsigContext } from 'uniswap/src/features/gating/sdk/statsig' import { isProdEnv } from 'utilities/src/environment/env' export function useFeatureFlagUrlOverrides() { + const { useParsedQueryString } = useUrlContext() const parsedQs = useParsedQueryString() const statsigContext = useContext(StatsigContext) - const isProduction = isProdEnv() + const isProduction = isProdEnv() && window.location.hostname !== 'localhost' useEffect(() => { // Override on diff --git a/apps/web/src/graphql/data/RecentTokenTransfers.ts b/apps/web/src/graphql/data/RecentTokenTransfers.ts index b2231c8de1c..2a5d8aa5f3f 100644 --- a/apps/web/src/graphql/data/RecentTokenTransfers.ts +++ b/apps/web/src/graphql/data/RecentTokenTransfers.ts @@ -5,7 +5,7 @@ import { TransactionType, useRecentTokenTransfersQuery, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { useEnabledChains } from 'uniswap/src/features/settings/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks' export function useRecentTokenTransfers(address?: string) { const { gqlChains } = useEnabledChains() diff --git a/apps/web/src/graphql/data/SearchTokens.ts b/apps/web/src/graphql/data/SearchTokens.ts index 186415e17cd..743ede5bb11 100644 --- a/apps/web/src/graphql/data/SearchTokens.ts +++ b/apps/web/src/graphql/data/SearchTokens.ts @@ -1,15 +1,14 @@ -import { BACKEND_SUPPORTED_CHAINS } from 'constants/chains' import { useMemo } from 'react' import { - Chain, SearchTokensWebQuery, Token, useSearchTokensWebQuery, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { isBackendSupportedChain } from 'uniswap/src/features/chains/utils' // Filters out results that are undefined, or where the token's chain is not supported in explore. function isExploreSupportedToken(token: GqlSearchToken | undefined): token is Token { - return token !== undefined && (BACKEND_SUPPORTED_CHAINS as ReadonlyArray).includes(token.chain) + return token !== undefined && isBackendSupportedChain(token.chain) } export function useSearchTokens(searchQuery: string = '') { diff --git a/apps/web/src/graphql/data/TopTokens.ts b/apps/web/src/graphql/data/TopTokens.ts deleted file mode 100644 index 68a41be69b2..00000000000 --- a/apps/web/src/graphql/data/TopTokens.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { ApolloError } from '@apollo/client' -import { - exploreSearchStringAtom, - filterTimeAtom, - sortAscendingAtom, - sortMethodAtom, - TokenSortMethod, -} from 'components/Tokens/state' -import { - isPricePoint, - PollingInterval, - PricePoint, - supportedChainIdFromGQLChain, - toHistoryDuration, - unwrapToken, - usePollQueryWhileMounted, -} from 'graphql/data/util' -import useIsWindowVisible from 'hooks/useIsWindowVisible' -import { useAtomValue } from 'jotai/utils' -import { useMemo } from 'react' -import { - Chain, - TopTokens100Query, - useTopTokens100Query, - useTopTokensSparklineQuery, -} from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' - -const TokenSortMethods = { - [TokenSortMethod.PRICE]: (a: TopToken, b: TopToken) => - (b?.market?.price?.value ?? 0) - (a?.market?.price?.value ?? 0), - [TokenSortMethod.DAY_CHANGE]: (a: TopToken, b: TopToken) => - (b?.market?.pricePercentChange1Day?.value ?? 0) - (a?.market?.pricePercentChange1Day?.value ?? 0), - [TokenSortMethod.HOUR_CHANGE]: (a: TopToken, b: TopToken) => - (b?.market?.pricePercentChange1Hour?.value ?? 0) - (a?.market?.pricePercentChange1Hour?.value ?? 0), - [TokenSortMethod.VOLUME]: (a: TopToken, b: TopToken) => - (b?.market?.volume?.value ?? 0) - (a?.market?.volume?.value ?? 0), - [TokenSortMethod.FULLY_DILUTED_VALUATION]: (a: TopToken, b: TopToken) => - (b?.project?.markets?.[0]?.fullyDilutedValuation?.value ?? 0) - - (a?.project?.markets?.[0]?.fullyDilutedValuation?.value ?? 0), -} - -function useSortedTokens(tokens: TopTokens100Query['topTokens']) { - const sortMethod = useAtomValue(sortMethodAtom) - const sortAscending = useAtomValue(sortAscendingAtom) - - return useMemo(() => { - if (!tokens) { - return undefined - } - const tokenArray = Array.from(tokens).sort(TokenSortMethods[sortMethod]) - - return sortAscending ? tokenArray.reverse() : tokenArray - }, [tokens, sortMethod, sortAscending]) -} - -function useFilteredTokens(tokens: TopTokens100Query['topTokens']) { - const filterString = useAtomValue(exploreSearchStringAtom) - - const lowercaseFilterString = useMemo(() => filterString.toLowerCase(), [filterString]) - - return useMemo(() => { - if (!tokens) { - return undefined - } - let returnTokens = tokens - if (lowercaseFilterString) { - returnTokens = returnTokens?.filter((token) => { - const addressIncludesFilterString = token?.address?.toLowerCase().includes(lowercaseFilterString) - const projectNameIncludesFilterString = token?.project?.name?.toLowerCase().includes(lowercaseFilterString) - const nameIncludesFilterString = token?.name?.toLowerCase().includes(lowercaseFilterString) - const symbolIncludesFilterString = token?.symbol?.toLowerCase().includes(lowercaseFilterString) - return ( - projectNameIncludesFilterString || - nameIncludesFilterString || - symbolIncludesFilterString || - addressIncludesFilterString - ) - }) - } - return returnTokens - }, [tokens, lowercaseFilterString]) -} - -export type SparklineMap = { [key: string]: PricePoint[] | undefined } -export type TopToken = NonNullable['topTokens']>[number] - -interface UseTopTokensReturnValue { - tokens?: readonly TopToken[] - tokenSortRank: Record - loadingTokens: boolean - sparklines: SparklineMap - error?: ApolloError -} - -export function useTopTokens(chain: Chain, skip?: boolean): UseTopTokensReturnValue { - const chainId = supportedChainIdFromGQLChain(chain) - const duration = toHistoryDuration(useAtomValue(filterTimeAtom)) - const isWindowVisible = useIsWindowVisible() - - const { data: sparklineQuery } = usePollQueryWhileMounted( - useTopTokensSparklineQuery({ - variables: { duration, chain }, - skip: !isWindowVisible || skip, - }), - PollingInterval.Slow, - ) - - const sparklines = useMemo(() => { - const unwrappedTokens = chainId && sparklineQuery?.topTokens?.map((topToken) => unwrapToken(chainId, topToken)) - const map: SparklineMap = {} - unwrappedTokens?.forEach((current) => { - if (current?.address !== undefined) { - map[current.address] = current?.market?.priceHistory?.filter(isPricePoint) as PricePoint[] - } - }) - return map - }, [chainId, sparklineQuery?.topTokens]) - - const { - data, - loading: loadingTokens, - error, - } = usePollQueryWhileMounted( - useTopTokens100Query({ - variables: { duration, chain }, - skip: !isWindowVisible || skip, - }), - PollingInterval.Fast, - ) - - const unwrappedTokens = useMemo( - () => chainId && data?.topTokens?.map((token) => unwrapToken(chainId, token)), - [chainId, data], - ) - const sortedTokens = useSortedTokens(unwrappedTokens) - const tokenSortRank = useMemo( - () => - sortedTokens?.reduce((acc, cur, i) => { - if (!cur?.address) { - return acc - } - return { - ...acc, - [cur.address]: i + 1, - } - }, {}) ?? {}, - [sortedTokens], - ) - const filteredTokens = useFilteredTokens(sortedTokens) - return useMemo( - () => ({ tokens: filteredTokens, tokenSortRank, loadingTokens, sparklines, error }), - [filteredTokens, tokenSortRank, loadingTokens, sparklines, error], - ) -} diff --git a/apps/web/src/graphql/data/TrendingTokens.ts b/apps/web/src/graphql/data/TrendingTokens.ts index e5e28a858ff..2a71563288e 100644 --- a/apps/web/src/graphql/data/TrendingTokens.ts +++ b/apps/web/src/graphql/data/TrendingTokens.ts @@ -1,11 +1,13 @@ -import { chainIdToBackendChain } from 'constants/chains' import { unwrapToken } from 'graphql/data/util' import { useMemo } from 'react' import { useTrendingTokensQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { toGraphQLChain } from 'uniswap/src/features/chains/utils' export default function useTrendingTokens(chainId?: UniverseChainId) { - const chain = chainIdToBackendChain({ chainId, withFallback: true }) + const { defaultChainId } = useEnabledChains() + const chain = toGraphQLChain(chainId ?? defaultChainId) const { data, loading } = useTrendingTokensQuery({ variables: { chain } }) return useMemo( diff --git a/apps/web/src/graphql/data/apollo/AssetActivityProvider.tsx b/apps/web/src/graphql/data/apollo/AssetActivityProvider.tsx index 555b52c92a0..0fe57edba21 100644 --- a/apps/web/src/graphql/data/apollo/AssetActivityProvider.tsx +++ b/apps/web/src/graphql/data/apollo/AssetActivityProvider.tsx @@ -22,9 +22,9 @@ import { useActivityWebLazyQuery, useOnAssetActivitySubscription, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks' import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' -import { useEnabledChains } from 'uniswap/src/features/settings/hooks' import { logger } from 'utilities/src/logger/logger' import { useInterval } from 'utilities/src/time/timing' import { v4 as uuidV4 } from 'uuid' diff --git a/apps/web/src/graphql/data/apollo/TokenBalancesProvider.test.tsx b/apps/web/src/graphql/data/apollo/TokenBalancesProvider.test.tsx index b4f02e14ec4..4134d5a09ec 100644 --- a/apps/web/src/graphql/data/apollo/TokenBalancesProvider.test.tsx +++ b/apps/web/src/graphql/data/apollo/TokenBalancesProvider.test.tsx @@ -1,4 +1,4 @@ -import { fireEvent, screen } from '@testing-library/react' +import { fireEvent, screen, waitFor } from '@testing-library/react' import { PrefetchBalancesWrapper, useTokenBalancesQuery } from 'graphql/data/apollo/AdaptiveTokenBalancesProvider' import { useAccount } from 'hooks/useAccount' import { mocked } from 'test-utils/mocked' @@ -7,6 +7,8 @@ import { useOnAssetActivitySubscription } from 'uniswap/src/data/graphql/uniswap import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' +// TODO(WEB-5370): Remove this delay + waitFor once we've integrated wallet's refetch logic +jest.setTimeout(10000) const mockLazyFetch = jest.fn() const mockBalanceQueryResponse = [ mockLazyFetch, @@ -45,17 +47,17 @@ describe('TokenBalancesProvider', () => { mocked(useAccount).mockReturnValue({ address: '0xaddress1', chainId: 1 } as any) }) - it('TokenBalancesProvider should not fetch balances without calls to useOnAssetActivitySubscription', () => { + it('TokenBalancesProvider should not fetch balances without calls to useOnAssetActivitySubscription', async () => { render(
) - expect(mockLazyFetch).toHaveBeenCalledTimes(0) + await waitFor(() => expect(mockLazyFetch).toHaveBeenCalledTimes(0), { timeout: 3500 }) }) describe('useTokenBalancesQuery', () => { - it('should only refetch balances when stale', () => { + it('should only refetch balances when stale', async () => { const { rerender, unmount } = renderHook(() => useTokenBalancesQuery()) // Rendering useTokenBalancesQuery should trigger a fetch - expect(mockLazyFetch).toHaveBeenCalledTimes(1) + await waitFor(() => expect(mockLazyFetch).toHaveBeenCalledTimes(1), { timeout: 3500 }) // Rerender to clear staleness rerender() @@ -63,31 +65,31 @@ describe('TokenBalancesProvider', () => { // Receiving a new value from subscription should trigger a fetch while useTokenBalancesQuery hooks are mounted triggerSubscriptionUpdate() rerender() - expect(mockLazyFetch).toHaveBeenCalledTimes(2) + await waitFor(() => expect(mockLazyFetch).toHaveBeenCalledTimes(2), { timeout: 3500 }) // Unmounting the hooks should not trigger any fetches unmount() - expect(mockLazyFetch).toHaveBeenCalledTimes(2) + await waitFor(() => expect(mockLazyFetch).toHaveBeenCalledTimes(2), { timeout: 3500 }) // Receiving a new value from subscription should NOT trigger a fetch if no useTokenBalancesQuery hooks are mounted triggerSubscriptionUpdate() - expect(mockLazyFetch).toHaveBeenCalledTimes(2) + await waitFor(() => expect(mockLazyFetch).toHaveBeenCalledTimes(2), { timeout: 3500 }) }) - it('should use cached balances across multiple hook calls', () => { + it('should use cached balances across multiple hook calls', async () => { renderHook(() => ({ hook1: useTokenBalancesQuery(), hook2: useTokenBalancesQuery(), })) // Rendering useTokenBalancesQuery twice should only trigger one fetch - expect(mockLazyFetch).toHaveBeenCalledTimes(1) + await waitFor(() => expect(mockLazyFetch).toHaveBeenCalledTimes(1), { timeout: 3500 }) }) - it('should refetch when account changes', () => { + it('should refetch when account changes', async () => { const { rerender } = renderHook(() => useTokenBalancesQuery()) - expect(mockLazyFetch).toHaveBeenCalledTimes(1) + await waitFor(() => expect(mockLazyFetch).toHaveBeenCalledTimes(1), { timeout: 3500 }) // Rerender to clear staleness rerender() @@ -96,12 +98,12 @@ describe('TokenBalancesProvider', () => { mocked(useAccount).mockReturnValue({ address: '0xaddress2', chainId: 1 } as any) rerender() - expect(mockLazyFetch).toHaveBeenCalledTimes(2) + await waitFor(() => expect(mockLazyFetch).toHaveBeenCalledTimes(2), { timeout: 3500 }) }) }) describe('PrefetchBalancesWrapper', () => { - it('should fetch balances when a PrefetchBalancesWrapper is hovered', () => { + it('should fetch balances when a PrefetchBalancesWrapper is hovered', async () => { const { rerender } = render(
hi
@@ -117,17 +119,17 @@ describe('TokenBalancesProvider', () => { ) // Should not fetch balances before hover - expect(mockLazyFetch).toHaveBeenCalledTimes(0) + await waitFor(() => expect(mockLazyFetch).toHaveBeenCalledTimes(0), { timeout: 3500 }) // Hovering component should trigger a fetch fireEvent.mouseEnter(wrappedComponent) fireEvent.mouseLeave(wrappedComponent) - expect(mockLazyFetch).toHaveBeenCalledTimes(1) + await waitFor(() => expect(mockLazyFetch).toHaveBeenCalledTimes(1), { timeout: 4000 }) // Subsequent hover should not trigger a fetch fireEvent.mouseEnter(wrappedComponent) fireEvent.mouseLeave(wrappedComponent) - expect(mockLazyFetch).toHaveBeenCalledTimes(1) + await waitFor(() => expect(mockLazyFetch).toHaveBeenCalledTimes(1), { timeout: 4000 }) // Subsequent hover should trigger a fetch if the subscription has updated triggerSubscriptionUpdate() @@ -136,10 +138,10 @@ describe('TokenBalancesProvider', () => {
hi
, ) - expect(mockLazyFetch).toHaveBeenCalledTimes(1) + await waitFor(() => expect(mockLazyFetch).toHaveBeenCalledTimes(1), { timeout: 4000 }) fireEvent.mouseEnter(wrappedComponent) fireEvent.mouseLeave(wrappedComponent) - expect(mockLazyFetch).toHaveBeenCalledTimes(2) + await waitFor(() => expect(mockLazyFetch).toHaveBeenCalledTimes(2), { timeout: 4000 }) }) }) }) diff --git a/apps/web/src/graphql/data/apollo/TokenBalancesProvider.tsx b/apps/web/src/graphql/data/apollo/TokenBalancesProvider.tsx index 2747a311fc6..92af65a096d 100644 --- a/apps/web/src/graphql/data/apollo/TokenBalancesProvider.tsx +++ b/apps/web/src/graphql/data/apollo/TokenBalancesProvider.tsx @@ -8,14 +8,11 @@ import { SwapOrderStatus, usePortfolioBalancesLazyQuery, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' -import { - useEnabledChains, - useHideSmallBalancesSetting, - useHideSpamTokensSetting, -} from 'uniswap/src/features/settings/hooks' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { useHideSmallBalancesSetting, useHideSpamTokensSetting } from 'uniswap/src/features/settings/hooks' import { SUBSCRIPTION_CHAINIDS } from 'utilities/src/apollo/constants' import { usePrevious } from 'utilities/src/react/hooks' @@ -102,22 +99,37 @@ export function TokenBalancesProvider({ children }: PropsWithChildren) { if (!account.address) { return } - lazyFetch({ - variables: { - ownerAddress: account.address, - chains: gqlChains, - valueModifiers: [ - { - ownerAddress: account.address, - includeSpamTokens: valueModifiers.includeSpamTokens, - includeSmallBalances: valueModifiers.includeSmallBalances, - tokenExcludeOverrides: [], - tokenIncludeOverrides: [], - }, - ], + // adds a 3 second delay to account for dependency latency after an account update + // TODO(WEB-5370): Remove this delay once we've integrated wallet's refetch logic + setTimeout( + () => { + account.address && + lazyFetch({ + variables: { + ownerAddress: account.address, + chains: gqlChains, + valueModifiers: [ + { + ownerAddress: account.address, + includeSpamTokens: valueModifiers.includeSpamTokens, + includeSmallBalances: valueModifiers.includeSmallBalances, + tokenExcludeOverrides: [], + tokenIncludeOverrides: [], + }, + ], + }, + }) }, - }) - }, [account.address, lazyFetch, valueModifiers, gqlChains]) + hasAccountUpdate ? 3000 : 0, + ) + }, [ + account.address, + hasAccountUpdate, + lazyFetch, + gqlChains, + valueModifiers.includeSpamTokens, + valueModifiers.includeSmallBalances, + ]) return ( , Record>, +): Reference | StoreObject { + if (existing && !incoming) { + return existing + } + return mergeObjects(existing, incoming) +} diff --git a/apps/web/src/graphql/data/nft/NftUniversalRouterAddress.ts b/apps/web/src/graphql/data/nft/NftUniversalRouterAddress.ts index 535e10cb760..c414120ec55 100644 --- a/apps/web/src/graphql/data/nft/NftUniversalRouterAddress.ts +++ b/apps/web/src/graphql/data/nft/NftUniversalRouterAddress.ts @@ -1,6 +1,6 @@ import { UNIVERSAL_ROUTER_ADDRESS, UniversalRouterVersion } from '@uniswap/universal-router-sdk' import { useNftUniversalRouterAddressQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' export function getURAddress(chainId?: UniverseChainId, nftURAddress?: string): string | undefined { if (!chainId) { diff --git a/apps/web/src/graphql/data/pools/usePoolData.ts b/apps/web/src/graphql/data/pools/usePoolData.ts index a9d77524bd4..4481699b7ce 100644 --- a/apps/web/src/graphql/data/pools/usePoolData.ts +++ b/apps/web/src/graphql/data/pools/usePoolData.ts @@ -1,4 +1,3 @@ -import { chainIdToBackendChain } from 'constants/chains' import { V2_BIPS } from 'graphql/data/pools/useTopPools' import ms from 'ms' import { useMemo } from 'react' @@ -8,7 +7,9 @@ import { useV2PairQuery, useV3PoolQuery, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { toGraphQLChain } from 'uniswap/src/features/chains/utils' export interface PoolData { // basic pool info @@ -74,12 +75,14 @@ export function usePoolData( error: boolean data?: PoolData } { + const { defaultChainId } = useEnabledChains() + const variables = { chain: toGraphQLChain(chainId ?? defaultChainId), address: poolAddress } const { loading: loadingV3, error: errorV3, data: dataV3, } = useV3PoolQuery({ - variables: { chain: chainIdToBackendChain({ chainId, withFallback: true }), address: poolAddress }, + variables, errorPolicy: 'all', }) const { @@ -87,7 +90,7 @@ export function usePoolData( error: errorV2, data: dataV2, } = useV2PairQuery({ - variables: { chain: chainIdToBackendChain({ chainId, withFallback: true }), address: poolAddress }, + variables, skip: !chainId, errorPolicy: 'all', }) diff --git a/apps/web/src/graphql/data/pools/usePoolTransactions.ts b/apps/web/src/graphql/data/pools/usePoolTransactions.ts index 0b6ff5f5917..9e217b7b093 100644 --- a/apps/web/src/graphql/data/pools/usePoolTransactions.ts +++ b/apps/web/src/graphql/data/pools/usePoolTransactions.ts @@ -1,4 +1,3 @@ -import { chainIdToBackendChain } from 'constants/chains' import { NATIVE_CHAIN_ID } from 'constants/tokens' import { useCallback, useMemo, useRef } from 'react' import { WRAPPED_NATIVE_CURRENCY } from 'uniswap/src/constants/tokens' @@ -11,7 +10,9 @@ import { useV2PairTransactionsQuery, useV3PoolTransactionsQuery, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { toGraphQLChain } from 'uniswap/src/features/chains/utils' export enum PoolTableTransactionType { BUY = 'Buy', @@ -56,13 +57,15 @@ export function usePoolTransactions( protocolVersion: ProtocolVersion = ProtocolVersion.V3, first = PoolTransactionDefaultQuerySize, ) { + const { defaultChainId } = useEnabledChains() + const variables = { first, chain: toGraphQLChain(chainId ?? defaultChainId), address } const { loading: loadingV3, error: errorV3, data: dataV3, fetchMore: fetchMoreV3, } = useV3PoolTransactionsQuery({ - variables: { first, chain: chainIdToBackendChain({ chainId, withFallback: true }), address }, + variables, skip: protocolVersion !== ProtocolVersion.V3, }) const { @@ -71,7 +74,7 @@ export function usePoolTransactions( data: dataV2, fetchMore: fetchMoreV2, } = useV2PairTransactionsQuery({ - variables: { first, chain: chainIdToBackendChain({ chainId, withFallback: true }), address }, + variables, skip: !chainId || protocolVersion !== ProtocolVersion.V2, }) const loadingMore = useRef(false) diff --git a/apps/web/src/graphql/data/pools/usePoolsFromTokenAddress.ts b/apps/web/src/graphql/data/pools/usePoolsFromTokenAddress.ts index d2c872e9a72..9172d7cce04 100644 --- a/apps/web/src/graphql/data/pools/usePoolsFromTokenAddress.ts +++ b/apps/web/src/graphql/data/pools/usePoolsFromTokenAddress.ts @@ -1,4 +1,3 @@ -import { chainIdToBackendChain } from 'constants/chains' import { PoolTableSortState, TablePool, @@ -12,7 +11,9 @@ import { useTopV2PairsQuery, useTopV3PoolsQuery, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { toGraphQLChain } from 'uniswap/src/features/chains/utils' const DEFAULT_QUERY_SIZE = 20 @@ -21,6 +22,8 @@ export function usePoolsFromTokenAddress( sortState: PoolTableSortState, chainId?: UniverseChainId, ) { + const { defaultChainId } = useEnabledChains() + const chain = toGraphQLChain(chainId ?? defaultChainId) const { loading: loadingV3, error: errorV3, @@ -30,7 +33,7 @@ export function usePoolsFromTokenAddress( variables: { first: DEFAULT_QUERY_SIZE, tokenAddress, - chain: chainIdToBackendChain({ chainId, withFallback: true }), + chain, }, }) @@ -43,7 +46,7 @@ export function usePoolsFromTokenAddress( variables: { first: DEFAULT_QUERY_SIZE, tokenAddress, - chain: chainIdToBackendChain({ chainId, withFallback: true }), + chain, }, skip: !chainId, }) diff --git a/apps/web/src/graphql/data/pools/useTopPools.ts b/apps/web/src/graphql/data/pools/useTopPools.ts index 5fc3cbd0929..10456cbde86 100644 --- a/apps/web/src/graphql/data/pools/useTopPools.ts +++ b/apps/web/src/graphql/data/pools/useTopPools.ts @@ -1,20 +1,7 @@ import { Percent } from '@uniswap/sdk-core' -import { exploreSearchStringAtom } from 'components/Tokens/state' -import { chainIdToBackendChain } from 'constants/chains' import { OrderDirection } from 'graphql/data/util' -import useIsWindowVisible from 'hooks/useIsWindowVisible' -import { useAtomValue } from 'jotai/utils' -import { useMemo } from 'react' import { BIPS_BASE } from 'uniswap/src/constants/misc' -import { - ProtocolVersion, - Token, - useTopV2PairsQuery, - useTopV3PoolsQuery, -} from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { ProtocolVersion, Token } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' export function sortPools(pools: TablePool[], sortState: PoolTableSortState) { return pools.sort((a, b) => { @@ -93,92 +80,3 @@ export type PoolTableSortState = { sortBy: PoolSortFields sortDirection: OrderDirection } - -function useFilteredPools(pools: TablePool[]) { - const filterString = useAtomValue(exploreSearchStringAtom) - - const lowercaseFilterString = useMemo(() => filterString.toLowerCase(), [filterString]) - - return useMemo( - () => - pools.filter((pool) => { - const addressIncludesFilterString = pool.hash.toLowerCase().includes(lowercaseFilterString) - const token0IncludesFilterString = pool.token0?.symbol?.toLowerCase().includes(lowercaseFilterString) - const token1IncludesFilterString = pool.token1?.symbol?.toLowerCase().includes(lowercaseFilterString) - const token0HashIncludesFilterString = pool.token0?.address?.toLowerCase().includes(lowercaseFilterString) - const token1HashIncludesFilterString = pool.token1?.address?.toLowerCase().includes(lowercaseFilterString) - const poolName = `${pool.token0?.symbol}/${pool.token1?.symbol}`.toLowerCase() - const poolNameIncludesFilterString = poolName.includes(lowercaseFilterString) - return ( - token0IncludesFilterString || - token1IncludesFilterString || - addressIncludesFilterString || - token0HashIncludesFilterString || - token1HashIncludesFilterString || - poolNameIncludesFilterString - ) - }), - [lowercaseFilterString, pools], - ) -} - -export function useTopPools(sortState: PoolTableSortState, chainId?: UniverseChainId) { - const isWindowVisible = useIsWindowVisible() - const isRestExploreEnabled = useFeatureFlag(FeatureFlags.RestExplore) - const { - loading: loadingV3, - error: errorV3, - data: dataV3, - } = useTopV3PoolsQuery({ - variables: { first: 100, chain: chainIdToBackendChain({ chainId, withFallback: true }) }, - skip: !isWindowVisible || isRestExploreEnabled, - }) - const { - loading: loadingV2, - error: errorV2, - data: dataV2, - } = useTopV2PairsQuery({ - variables: { first: 100, chain: chainIdToBackendChain({ chainId, withFallback: true }) }, - skip: !isWindowVisible || !chainId || isRestExploreEnabled, - }) - const loading = loadingV3 || loadingV2 - - const unfilteredPools = useMemo(() => { - // TODO(WEB-4818): add v4 pools here - const topV3Pools: TablePool[] = - dataV3?.topV3Pools?.map((pool) => { - return { - hash: pool.address, - token0: pool.token0, - token1: pool.token1, - tvl: pool.totalLiquidity?.value, - volume24h: pool.volume24h?.value, - volumeWeek: pool.volumeWeek?.value, - apr: calculateApr(pool.volume24h?.value, pool.totalLiquidity?.value, pool.feeTier), - volOverTvl: calculate1DVolOverTvl(pool.volume24h?.value, pool.totalLiquidity?.value), - feeTier: pool.feeTier, - protocolVersion: pool.protocolVersion, - } as TablePool - }) ?? [] - const topV2Pairs: TablePool[] = - dataV2?.topV2Pairs?.map((pool) => { - return { - hash: pool.address, - token0: pool.token0, - token1: pool.token1, - tvl: pool.totalLiquidity?.value, - volume24h: pool.volume24h?.value, - volumeWeek: pool.volumeWeek?.value, - volOverTvl: calculate1DVolOverTvl(pool.volume24h?.value, pool.totalLiquidity?.value), - apr: calculateApr(pool.volume24h?.value, pool.totalLiquidity?.value, V2_BIPS), - feeTier: V2_BIPS, - protocolVersion: pool.protocolVersion, - } as TablePool - }) ?? [] - - return sortPools([...topV3Pools, ...topV2Pairs], sortState) - }, [dataV2?.topV2Pairs, dataV3?.topV3Pools, sortState]) - - const filteredPools = useFilteredPools(unfilteredPools).slice(0, 100) - return { topPools: filteredPools, loading, errorV3, errorV2 } -} diff --git a/apps/web/src/graphql/data/protocolStats.ts b/apps/web/src/graphql/data/protocolStats.ts deleted file mode 100644 index 7195dcfb580..00000000000 --- a/apps/web/src/graphql/data/protocolStats.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { StackedLineData } from 'components/Charts/StackedLineChart' -import { StackedHistogramData } from 'components/Charts/VolumeChart/renderer' -import { ChartType } from 'components/Charts/utils' -import { ChartQueryResult, checkDataQuality } from 'components/Tokens/TokenDetails/ChartSection/util' -import useIsWindowVisible from 'hooks/useIsWindowVisible' -import { UTCTimestamp } from 'lightweight-charts' -import { useMemo } from 'react' -import { - Chain, - HistoryDuration, - PriceSource, - ProtocolVersion, - TimestampedAmount, - useDailyProtocolTvlQuery, - useHistoricalProtocolVolumeQuery, -} from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' - -function mapDataByTimestamp( - v2Data?: readonly TimestampedAmount[], - v3Data?: readonly TimestampedAmount[], -): Record> { - const dataByTime: Record> = {} - v2Data?.forEach((v2Point) => { - const timestamp = v2Point.timestamp - dataByTime[timestamp] = { [ProtocolVersion.V2]: v2Point.value, [ProtocolVersion.V3]: 0, [ProtocolVersion.V4]: 0 } - }) - v3Data?.forEach((v3Point) => { - const timestamp = v3Point.timestamp - if (!dataByTime[timestamp]) { - dataByTime[timestamp] = { [ProtocolVersion.V4]: 0, [ProtocolVersion.V2]: 0, [ProtocolVersion.V3]: v3Point.value } - } else { - dataByTime[timestamp][ProtocolVersion.V3] = v3Point.value - } - }) - return dataByTime -} - -export function useHistoricalProtocolVolume( - chain: Chain, - duration: HistoryDuration, -): ChartQueryResult { - const isWindowVisible = useIsWindowVisible() - const isRestExploreEnabled = useFeatureFlag(FeatureFlags.RestExplore) - const { data: queryData, loading } = useHistoricalProtocolVolumeQuery({ - variables: { chain, duration }, - skip: !isWindowVisible || isRestExploreEnabled, - }) - - return useMemo(() => { - const dataByTime = mapDataByTimestamp(queryData?.v2HistoricalProtocolVolume, queryData?.v3HistoricalProtocolVolume) - - const entries = Object.entries(dataByTime).reduce((acc, [timestamp, values]) => { - acc.push({ - time: Number(timestamp) as UTCTimestamp, - values: { - [PriceSource.SubgraphV2]: values[ProtocolVersion.V2], - [PriceSource.SubgraphV3]: values[ProtocolVersion.V3], - [PriceSource.SubgraphV4]: values[ProtocolVersion.V4], - }, - }) - return acc - }, [] as StackedHistogramData[]) - - const dataQuality = checkDataQuality(entries, ChartType.VOLUME, duration) - return { chartType: ChartType.VOLUME, entries, loading, dataQuality } - }, [duration, loading, queryData?.v2HistoricalProtocolVolume, queryData?.v3HistoricalProtocolVolume]) -} - -export function useDailyProtocolTVL(chain: Chain): ChartQueryResult { - const isWindowVisible = useIsWindowVisible() - const isRestExploreEnabled = useFeatureFlag(FeatureFlags.RestExplore) - const { data: queryData, loading } = useDailyProtocolTvlQuery({ - variables: { chain }, - skip: !isWindowVisible || isRestExploreEnabled, - }) - - return useMemo(() => { - const dataByTime = mapDataByTimestamp(queryData?.v2DailyProtocolTvl, queryData?.v3DailyProtocolTvl) - const entries = Object.entries(dataByTime).map(([timestamp, values]) => ({ - time: Number(timestamp), - values: [values[ProtocolVersion.V2], values[ProtocolVersion.V3]], - })) as StackedLineData[] - - const dataQuality = checkDataQuality(entries, ChartType.TVL, HistoryDuration.Year) - return { chartType: ChartType.TVL, entries, loading, dataQuality } - }, [loading, queryData?.v2DailyProtocolTvl, queryData?.v3DailyProtocolTvl]) -} diff --git a/apps/web/src/graphql/data/types.test.ts b/apps/web/src/graphql/data/types.test.ts index 2ab76cdabb2..821da42c39f 100644 --- a/apps/web/src/graphql/data/types.test.ts +++ b/apps/web/src/graphql/data/types.test.ts @@ -7,8 +7,8 @@ import { Token, TokenStandard, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { removeSafetyInfo } from 'uniswap/src/test/fixtures' -import { UniverseChainId } from 'uniswap/src/types/chains' const MAINNET_NATIVE_GQL_TOKEN = { __typename: 'Token', diff --git a/apps/web/src/graphql/data/types.ts b/apps/web/src/graphql/data/types.ts index c09d0ba8e13..e8daca792c5 100644 --- a/apps/web/src/graphql/data/types.ts +++ b/apps/web/src/graphql/data/types.ts @@ -1,16 +1,16 @@ -import { isSupportedChainId } from 'constants/chains' -import { fiatOnRampToCurrency, gqlToCurrency } from 'graphql/data/util' +import { PricePoint, fiatOnRampToCurrency, gqlToCurrency } from 'graphql/data/util' import { COMMON_BASES, buildPartialCurrencyInfo } from 'uniswap/src/constants/routing' import { USDC_OPTIMISM } from 'uniswap/src/constants/tokens' import { Token as GqlToken, ProtectionResult, SafetyLevel, + TopTokens100Query, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { UniverseChainId, isUniverseChainId } from 'uniswap/src/features/chains/types' import { CurrencyInfo, TokenList } from 'uniswap/src/features/dataApi/types' import { buildCurrencyInfo, getCurrencySafetyInfo } from 'uniswap/src/features/dataApi/utils' import { FORSupportedToken } from 'uniswap/src/features/fiatOnRamp/types' -import { UniverseChainId } from 'uniswap/src/types/chains' import { isSameAddress } from 'utilities/src/addresses' import { currencyId } from 'utils/currencyId' @@ -40,7 +40,7 @@ export function gqlTokenToCurrencyInfo(token?: GqlToken): CurrencyInfo | undefin } export function meldSupportedCurrencyToCurrencyInfo(forCurrency: FORSupportedToken): CurrencyInfo | undefined { - if (!isSupportedChainId(Number(forCurrency.chainId))) { + if (!isUniverseChainId(Number(forCurrency.chainId))) { return undefined } @@ -82,3 +82,6 @@ export function meldSupportedCurrencyToCurrencyInfo(forCurrency: FORSupportedTok isSpam: false, }) } + +export type SparklineMap = { [key: string]: PricePoint[] | undefined } +export type TopToken = NonNullable['topTokens']>[number] diff --git a/apps/web/src/graphql/data/useTokenTransactions.ts b/apps/web/src/graphql/data/useTokenTransactions.ts index debe193563d..7ca9ce7fe4b 100644 --- a/apps/web/src/graphql/data/useTokenTransactions.ts +++ b/apps/web/src/graphql/data/useTokenTransactions.ts @@ -1,4 +1,3 @@ -import { chainIdToBackendChain } from 'constants/chains' import { useCallback, useMemo, useRef } from 'react' import { Chain, @@ -7,7 +6,9 @@ import { useV2TokenTransactionsQuery, useV3TokenTransactionsQuery, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { toGraphQLChain } from 'uniswap/src/features/chains/utils' export enum TokenTransactionType { BUY = 'Buy', @@ -21,6 +22,7 @@ export function useTokenTransactions( chainId: UniverseChainId, filter: TokenTransactionType[] = [TokenTransactionType.BUY, TokenTransactionType.SELL], ) { + const { defaultChainId } = useEnabledChains() const { data: dataV3, loading: loadingV3, @@ -29,7 +31,7 @@ export function useTokenTransactions( } = useV3TokenTransactionsQuery({ variables: { address: address.toLowerCase(), - chain: chainIdToBackendChain({ chainId, withFallback: true }), + chain: toGraphQLChain(chainId ?? defaultChainId), first: TokenTransactionDefaultQuerySize, }, }) @@ -42,7 +44,7 @@ export function useTokenTransactions( variables: { address: address.toLowerCase(), first: TokenTransactionDefaultQuerySize, - chain: chainIdToBackendChain({ chainId }), + chain: toGraphQLChain(chainId), }, }) const loadingMoreV3 = useRef(false) diff --git a/apps/web/src/graphql/data/util.test.tsx b/apps/web/src/graphql/data/util.test.tsx index 15c076a7944..bf8ef3953b6 100644 --- a/apps/web/src/graphql/data/util.test.tsx +++ b/apps/web/src/graphql/data/util.test.tsx @@ -1,13 +1,14 @@ -import { isSupportedGQLChain, supportedChainIdFromGQLChain } from 'graphql/data/util' +import { supportedChainIdFromGQLChain } from 'graphql/data/util' import { Chain } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { isBackendSupportedChain } from 'uniswap/src/features/chains/utils' describe('fromGraphQLChain', () => { it('should return the corresponding chain ID for supported chains', () => { expect(supportedChainIdFromGQLChain(Chain.Ethereum)).toBe(UniverseChainId.Mainnet) for (const chain of Object.values(Chain)) { - if (!isSupportedGQLChain(chain)) { + if (!isBackendSupportedChain(chain)) { continue } expect(supportedChainIdFromGQLChain(chain)).not.toBe(undefined) @@ -17,12 +18,11 @@ describe('fromGraphQLChain', () => { it('should return undefined for unsupported chains', () => { expect(supportedChainIdFromGQLChain(Chain.UnknownChain)).toBe(undefined) - for (const chain of Object.values(Chain)) { - if (isSupportedGQLChain(chain)) { - continue - } - expect(supportedChainIdFromGQLChain(chain)).toBe(undefined) - } + Object.values(Chain) + .filter((c) => !isBackendSupportedChain(c)) + .forEach((chain) => { + expect(supportedChainIdFromGQLChain(chain)).toBe(undefined) + }) }) it('should not crash when a new BE chain is added', () => { @@ -32,7 +32,7 @@ describe('fromGraphQLChain', () => { const ExpandedChainList = [...Object.values(Chain), NewChain.NewChain as unknown as Chain] for (const chain of ExpandedChainList) { - if (isSupportedGQLChain(chain)) { + if (isBackendSupportedChain(chain)) { continue } expect(supportedChainIdFromGQLChain(chain)).toBe(undefined) diff --git a/apps/web/src/graphql/data/util.tsx b/apps/web/src/graphql/data/util.tsx index 1957b9fe1ed..52be5573f8b 100644 --- a/apps/web/src/graphql/data/util.tsx +++ b/apps/web/src/graphql/data/util.tsx @@ -1,25 +1,13 @@ -import { OperationVariables, QueryResult } from '@apollo/client' import { DeepPartial } from '@apollo/client/utilities' +import { BigNumber } from '@ethersproject/bignumber' import { DataTag, DefaultError, QueryKey, UndefinedInitialDataOptions, queryOptions } from '@tanstack/react-query' import { Currency, Token } from '@uniswap/sdk-core' -import { - AVERAGE_L1_BLOCK_TIME, - BACKEND_SUPPORTED_CHAINS, - CHAIN_NAME_TO_CHAIN_ID, - InterfaceGqlChain, - UX_SUPPORTED_GQL_CHAINS, - chainIdToBackendChain, - getChainFromChainUrlParam, - isSupportedChainId, -} from 'constants/chains' import { NATIVE_CHAIN_ID } from 'constants/tokens' import { DefaultTheme } from 'lib/styled-components' import ms from 'ms' import { ExploreTab } from 'pages/Explore' -import { useEffect } from 'react' import { TokenStat } from 'state/explore/types' import { ThemeColors } from 'theme/colors' -import { GQL_MAINNET_CHAINS, UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' import { WRAPPED_NATIVE_CURRENCY, nativeOnChain } from 'uniswap/src/constants/tokens' import { Chain, @@ -29,32 +17,26 @@ import { PriceSource, TokenStandard, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { GQL_MAINNET_CHAINS, getChainInfo } from 'uniswap/src/features/chains/chainInfo' +import { GqlChainId, UniverseChainId, isUniverseChainId } from 'uniswap/src/features/chains/types' +import { + fromGraphQLChain, + isBackendSupportedChain, + toGraphQLChain, + toSupportedChainId, +} from 'uniswap/src/features/chains/utils' import { FORSupportedToken } from 'uniswap/src/features/fiatOnRamp/types' -import { UniverseChainId, UniverseChainInfo } from 'uniswap/src/types/chains' +import { AVERAGE_L1_BLOCK_TIME_MS } from 'uniswap/src/features/transactions/swap/hooks/usePollingIntervalByChain' +import { getChainIdFromChainUrlParam } from 'utils/chainParams' import { getNativeTokenDBAddress } from 'utils/nativeTokens' export enum PollingInterval { Slow = ms(`5m`), Normal = ms(`1m`), - Fast = AVERAGE_L1_BLOCK_TIME, + Fast = AVERAGE_L1_BLOCK_TIME_MS, LightningMcQueen = ms(`3s`), // approx block interval for polygon } -// Polls a query only when the current component is mounted, as useQuery's pollInterval prop will continue to poll after unmount -export function usePollQueryWhileMounted( - queryResult: QueryResult, - interval: PollingInterval, -) { - const { startPolling, stopPolling } = queryResult - - useEffect(() => { - startPolling(interval) - return stopPolling - }, [interval, startPolling, stopPolling]) - - return queryResult -} - export enum TimePeriod { HOUR = 'H', DAY = 'D', @@ -81,16 +63,13 @@ export function toHistoryDuration(timePeriod: TimePeriod): HistoryDuration { export type PricePoint = { timestamp: number; value: number } -export function isPricePoint(p: PricePoint | undefined): p is PricePoint { - return p !== undefined -} - export function isGqlSupportedChain(chainId?: UniverseChainId) { - return !!chainId && GQL_MAINNET_CHAINS.includes(UNIVERSE_CHAIN_INFO[chainId].backendChain.chain) + return !!chainId && GQL_MAINNET_CHAINS.includes(getChainInfo(chainId).backendChain.chain) } -export function toContractInput(currency: Currency): ContractInput { - const chain = chainIdToBackendChain({ chainId: currency.chainId as UniverseChainId }) +export function toContractInput(currency: Currency, fallback: UniverseChainId): ContractInput { + const supportedChainId = toSupportedChainId(currency.chainId) + const chain = toGraphQLChain(supportedChainId ?? fallback) return { chain, address: currency.isToken ? currency.address : getNativeTokenDBAddress(chain) } } @@ -98,7 +77,7 @@ export function gqlToCurrency(token: DeepPartial): Currenc if (!token.chain) { return undefined } - const chainId = getChainFromChainUrlParam(token.chain.toLowerCase())?.id + const chainId = getChainIdFromChainUrlParam(token.chain.toLowerCase()) if (!chainId) { return undefined } @@ -111,12 +90,15 @@ export function gqlToCurrency(token: DeepPartial): Currenc token.decimals ?? 18, token.symbol ?? undefined, token.name ?? token.project?.name ?? undefined, + undefined, + token.feeData?.buyFeeBps ? BigNumber.from(token.feeData.buyFeeBps) : undefined, + token.feeData?.sellFeeBps ? BigNumber.from(token.feeData.sellFeeBps) : undefined, ) } } export function fiatOnRampToCurrency(forCurrency: FORSupportedToken): Currency | undefined { - if (!isSupportedChainId(Number(forCurrency.chainId))) { + if (!isUniverseChainId(Number(forCurrency.chainId))) { return undefined } const supportedChainId = Number(forCurrency.chainId) as UniverseChainId @@ -129,35 +111,10 @@ export function fiatOnRampToCurrency(forCurrency: FORSupportedToken): Currency | } } -export function getSupportedGraphQlChain( - chain: UniverseChainInfo | undefined, - options: { fallbackToEthereum: true }, -): UniverseChainInfo -export function getSupportedGraphQlChain( - chain: UniverseChainInfo | undefined, - options?: { fallbackToEthereum?: boolean }, -): UniverseChainInfo | undefined -export function getSupportedGraphQlChain( - chain: UniverseChainInfo | undefined, - options?: { fallbackToEthereum?: boolean }, -): UniverseChainInfo | undefined { - const fallbackChain = options?.fallbackToEthereum ? UNIVERSE_CHAIN_INFO[UniverseChainId.Mainnet] : undefined - return chain?.backendChain.backendSupported ? chain : fallbackChain -} - -export function isSupportedGQLChain(chain: Chain): chain is InterfaceGqlChain { - const chains: ReadonlyArray = UX_SUPPORTED_GQL_CHAINS - return chains.includes(chain) -} - -export function supportedChainIdFromGQLChain(chain: InterfaceGqlChain): UniverseChainId +export function supportedChainIdFromGQLChain(chain: GqlChainId): UniverseChainId export function supportedChainIdFromGQLChain(chain: Chain): UniverseChainId | undefined export function supportedChainIdFromGQLChain(chain: Chain): UniverseChainId | undefined { - return isSupportedGQLChain(chain) ? CHAIN_NAME_TO_CHAIN_ID[chain] : undefined -} - -export function isBackendSupportedChain(chain: Chain): chain is InterfaceGqlChain { - return (BACKEND_SUPPORTED_CHAINS as ReadonlyArray).includes(chain) + return isBackendSupportedChain(chain) ? fromGraphQLChain(chain) ?? undefined : undefined } export function getTokenExploreURL({ tab, chain }: { tab: ExploreTab; chain?: Chain }) { @@ -231,8 +188,8 @@ const PROTOCOL_META: { [source in PriceSource]: ProtocolMeta } = { }, [PriceSource.SubgraphV4]: { name: 'v4', - color: 'accent1', // TODO(WEB-4618): update the colors when they are available - gradient: { start: 'rgba(252, 116, 254, 0.20)', end: 'rgba(252, 116, 254, 0.00)' }, + color: 'chain_137', + gradient: { start: 'rgba(96, 123, 238, 0.20)', end: 'rgba(55, 70, 136, 0.00)' }, }, /* [PriceSource.UniswapX]: { name: 'UniswapX', color: purple } */ } diff --git a/apps/web/src/hooks/Tokens.test.ts b/apps/web/src/hooks/Tokens.test.ts index 3c5edc0f3be..84c8fe70daa 100644 --- a/apps/web/src/hooks/Tokens.test.ts +++ b/apps/web/src/hooks/Tokens.test.ts @@ -3,11 +3,11 @@ import { NATIVE_CHAIN_ID } from 'constants/tokens' import { useCurrencyInfo } from 'hooks/Tokens' import { TEST_TOKEN_1 } from 'test-utils/constants' import { renderHook } from 'test-utils/render' -import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' import { DAI } from 'uniswap/src/constants/tokens' +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { NativeCurrency } from 'uniswap/src/features/tokens/NativeCurrency' import { useCurrencyInfo as useUniswapCurrencyInfo } from 'uniswap/src/features/tokens/useCurrencyInfo' -import { UniverseChainId } from 'uniswap/src/types/chains' jest.mock('uniswap/src/features/tokens/useCurrencyInfo', () => ({ useCurrencyInfo: jest.fn(), @@ -50,7 +50,7 @@ describe('useCurrencyInfo', () => { renderHook(() => useCurrencyInfo('ETH', UniverseChainId.Mainnet)) expect(useUniswapCurrencyInfo).toHaveBeenCalledWith( - `${UniverseChainId.Mainnet}-${UNIVERSE_CHAIN_INFO[UniverseChainId.Mainnet].nativeCurrency.address}`, + `${UniverseChainId.Mainnet}-${getChainInfo(UniverseChainId.Mainnet).nativeCurrency.address}`, { skip: undefined }, ) }) @@ -59,7 +59,7 @@ describe('useCurrencyInfo', () => { renderHook(() => useCurrencyInfo(undefined, UniverseChainId.Mainnet)) expect(useUniswapCurrencyInfo).toHaveBeenCalledWith( - `${UniverseChainId.Mainnet}-${UNIVERSE_CHAIN_INFO[UniverseChainId.Mainnet].nativeCurrency.address}`, + `${UniverseChainId.Mainnet}-${getChainInfo(UniverseChainId.Mainnet).nativeCurrency.address}`, { skip: undefined }, ) }) @@ -69,7 +69,7 @@ describe('useCurrencyInfo', () => { renderHook(() => useCurrencyInfo(currency)) expect(useUniswapCurrencyInfo).toHaveBeenCalledWith( - `${UniverseChainId.Mainnet}-${UNIVERSE_CHAIN_INFO[UniverseChainId.Mainnet].nativeCurrency.address}`, + `${UniverseChainId.Mainnet}-${getChainInfo(UniverseChainId.Mainnet).nativeCurrency.address}`, { skip: undefined }, ) }) diff --git a/apps/web/src/hooks/Tokens.ts b/apps/web/src/hooks/Tokens.ts index 7310db70a77..185a35bdcaf 100644 --- a/apps/web/src/hooks/Tokens.ts +++ b/apps/web/src/hooks/Tokens.ts @@ -1,12 +1,11 @@ import { Currency, Token } from '@uniswap/sdk-core' -import { useSupportedChainId } from 'constants/chains' import { NATIVE_CHAIN_ID } from 'constants/tokens' import { useAccount } from 'hooks/useAccount' import { useMemo } from 'react' -import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' import { COMMON_BASES } from 'uniswap/src/constants/routing' -import { UniverseChainId } from 'uniswap/src/types/chains' - +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' +import { useSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' import { useCurrencyInfo as useUniswapCurrencyInfo } from 'uniswap/src/features/tokens/useCurrencyInfo' import { buildCurrencyId } from 'uniswap/src/utils/currencyId' @@ -32,9 +31,8 @@ export function useCurrencyInfo( const { chainId: connectedChainId } = useAccount() const chainIdWithFallback = (typeof addressOrCurrency === 'string' ? chainId : addressOrCurrency?.chainId) ?? connectedChainId - const nativeAddressWithFallback = - UNIVERSE_CHAIN_INFO[chainIdWithFallback as UniverseChainId]?.nativeCurrency.address ?? - UNIVERSE_CHAIN_INFO[UniverseChainId.Mainnet]?.nativeCurrency.address + const supportedChainId = useSupportedChainId(chainIdWithFallback) + const nativeAddressWithFallback = getChainInfo(supportedChainId ?? UniverseChainId.Mainnet).nativeCurrency.address const isNative = useMemo(() => checkIsNative(addressOrCurrency), [addressOrCurrency]) const address = useMemo( @@ -42,8 +40,6 @@ export function useCurrencyInfo( [isNative, nativeAddressWithFallback, addressOrCurrency], ) - const supportedChainId = useSupportedChainId(chainIdWithFallback) - const addressWithFallback = isNative || !address ? nativeAddressWithFallback : address const currencyId = buildCurrencyId(supportedChainId ?? UniverseChainId.Mainnet, addressWithFallback) @@ -70,7 +66,7 @@ export function useCurrencyInfo( }, [addressOrCurrency, currencyInfo, chainIdWithFallback, isNative, address, skip]) } -const checkIsNative = (addressOrCurrency?: string | Currency): boolean => { +export const checkIsNative = (addressOrCurrency?: string | Currency): boolean => { return typeof addressOrCurrency === 'string' ? [NATIVE_CHAIN_ID, 'native', 'eth'].includes(addressOrCurrency.toLowerCase()) : addressOrCurrency?.isNative ?? false diff --git a/apps/web/src/hooks/useAccount.ts b/apps/web/src/hooks/useAccount.ts index f89896332bf..17a89eeedfe 100644 --- a/apps/web/src/hooks/useAccount.ts +++ b/apps/web/src/hooks/useAccount.ts @@ -1,7 +1,7 @@ /* eslint-disable rulesdir/no-undefined-or */ -import { useSupportedChainId } from 'constants/chains' import { useMemo } from 'react' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { useSupportedChainIdWithConnector } from 'uniswap/src/features/chains/hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' // eslint-disable-next-line @typescript-eslint/no-restricted-imports import { UseAccountReturnType as UseAccountReturnTypeWagmi, useAccount as useAccountWagmi, useChainId } from 'wagmi' @@ -16,7 +16,7 @@ type UseAccountReturnType = ReplaceChainId export function useAccount(): UseAccountReturnType { const { chainId, ...rest } = useAccountWagmi() const fallbackChainId = useChainId() - const supportedChainId = useSupportedChainId(chainId ?? fallbackChainId) + const supportedChainId = useSupportedChainIdWithConnector(chainId ?? fallbackChainId, rest.connector) return useMemo( () => ({ diff --git a/apps/web/src/hooks/useActiveLocalCurrency.ts b/apps/web/src/hooks/useActiveLocalCurrency.ts deleted file mode 100644 index 0a59ceb3438..00000000000 --- a/apps/web/src/hooks/useActiveLocalCurrency.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { useActiveLocale } from 'hooks/useActiveLocale' -import useParsedQueryString from 'hooks/useParsedQueryString' -import { useMemo } from 'react' -import { FiatCurrency, ORDERED_CURRENCIES } from 'uniswap/src/features/fiatCurrency/constants' -import { useAppFiatCurrency } from 'uniswap/src/features/fiatCurrency/hooks' -import { getFiatCurrencyComponents } from 'utils/formatNumbers' - -function useUrlLocalCurrency() { - const parsed = useParsedQueryString() - const parsedLocalCurrency = parsed.cur - - if (typeof parsedLocalCurrency !== 'string') { - return undefined - } - - const lowerCaseSupportedLocalCurrency = parsedLocalCurrency.toLowerCase() - return ORDERED_CURRENCIES.find((localCurrency) => localCurrency.toLowerCase() === lowerCaseSupportedLocalCurrency) -} - -export function useActiveLocalCurrency(): FiatCurrency { - const activeLocalCurrency = useAppFiatCurrency() - const urlLocalCurrency = useUrlLocalCurrency() - - return useMemo(() => urlLocalCurrency ?? activeLocalCurrency, [activeLocalCurrency, urlLocalCurrency]) -} - -export function useActiveLocalCurrencyComponents() { - const activeLocale = useActiveLocale() - const activeLocalCurrency = useActiveLocalCurrency() - - return useMemo( - () => getFiatCurrencyComponents(activeLocale, activeLocalCurrency), - [activeLocalCurrency, activeLocale], - ) -} diff --git a/apps/web/src/hooks/useActiveLocale.ts b/apps/web/src/hooks/useActiveLocale.ts deleted file mode 100644 index 2d5811118f1..00000000000 --- a/apps/web/src/hooks/useActiveLocale.ts +++ /dev/null @@ -1,67 +0,0 @@ -import useParsedQueryString from 'hooks/useParsedQueryString' -import { useMemo } from 'react' -import store from 'state' -import { - DEFAULT_LOCALE, - Language, - Locale, - WEB_SUPPORTED_LANGUAGES, - mapLocaleToLanguage, -} from 'uniswap/src/features/language/constants' -import { getLocale, useCurrentLocale } from 'uniswap/src/features/language/hooks' - -/** - * Given a locale string (e.g. from user agent), return the best match for corresponding Locale enum object - * @param maybeSupportedLocale the fuzzy locale identifier - */ -export function parseLocale(maybeSupportedLocale: unknown): Locale | undefined { - if (typeof maybeSupportedLocale !== 'string') { - return undefined - } - const lowerMaybeSupportedLocale = maybeSupportedLocale.toLowerCase() - return WEB_SUPPORTED_LANGUAGES.map((lang) => getLocale(lang)).find( - (locale) => - locale.toLowerCase() === lowerMaybeSupportedLocale || locale.split('-')[0] === lowerMaybeSupportedLocale, - ) -} - -/** - * Returns the supported locale read from the user agent (navigator) - */ -export function navigatorLocale(): Locale | undefined { - if (!navigator.language) { - return undefined - } - - const [language, region] = navigator.language.split('-') - - if (region) { - return parseLocale(`${language}-${region.toUpperCase()}`) ?? parseLocale(language) - } - - return parseLocale(language) -} - -export function storeLocale(): Locale | undefined { - const storeLanguage = store.getState().userSettings.currentLanguage - return getLocale(storeLanguage) -} - -function useUrlLocale() { - const parsed = useParsedQueryString() - return parseLocale(parsed.lng) -} - -/** - * Returns the currently active locale, from a combination of user agent, query string, and user settings stored in redux - */ -export function useActiveLocale(): Locale { - const urlLocale = useUrlLocale() - const userLocale = useCurrentLocale() - return useMemo(() => urlLocale ?? userLocale ?? navigatorLocale() ?? DEFAULT_LOCALE, [urlLocale, userLocale]) -} - -export function useActiveLanguage(): Language { - const locale = useActiveLocale() - return mapLocaleToLanguage[locale] -} diff --git a/apps/web/src/hooks/useAutoSlippageTolerance.ts b/apps/web/src/hooks/useAutoSlippageTolerance.ts index d5916473179..49cd03c4f65 100644 --- a/apps/web/src/hooks/useAutoSlippageTolerance.ts +++ b/apps/web/src/hooks/useAutoSlippageTolerance.ts @@ -2,7 +2,6 @@ import { MixedRoute, partitionMixedRouteByProtocol, Protocol, Trade } from '@uni import { Currency, CurrencyAmount, Percent, TradeType } from '@uniswap/sdk-core' import { Pair } from '@uniswap/v2-sdk' import { Pool } from '@uniswap/v3-sdk' -import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains' import { useAccount } from 'hooks/useAccount' import useGasPrice from 'hooks/useGasPrice' import { useStablecoinAmountFromFiatValue } from 'hooks/useStablecoinPrice' @@ -11,7 +10,7 @@ import JSBI from 'jsbi' import useNativeCurrency from 'lib/hooks/useNativeCurrency' import { useMemo } from 'react' import { ClassicTrade } from 'state/routing/types' -import { isL2ChainId } from 'uniswap/src/features/chains/utils' +import { chainSupportsGasEstimates, isL2ChainId } from 'uniswap/src/features/chains/utils' import { logger } from 'utilities/src/logger/logger' const DEFAULT_AUTO_SLIPPAGE = new Percent(5, 1000) // 0.5% @@ -81,7 +80,7 @@ export default function useClassicAutoSlippageTolerance(trade?: ClassicTrade): P const outputDollarValue = useStablecoinAmountFromFiatValue(outputUSD.data) // Prefer the USD estimate, if it is supported. - const supportsGasEstimate = useMemo(() => chainId && SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId), [chainId]) + const supportsGasEstimate = useMemo(() => chainId && chainSupportsGasEstimates(chainId), [chainId]) const gasEstimateUSD = useStablecoinAmountFromFiatValue(supportsGasEstimate ? trade?.gasUseEstimateUSD : undefined) ?? null @@ -108,9 +107,7 @@ export default function useClassicAutoSlippageTolerance(trade?: ClassicTrade): P // NOTE - dont use gas estimate for L2s yet - need to verify accuracy // if not, use local heuristic const dollarCostToUse = - chainId && SUPPORTED_GAS_ESTIMATE_CHAIN_IDS.includes(chainId) && gasEstimateUSD - ? gasEstimateUSD - : gasCostStablecoinAmount + chainId && chainSupportsGasEstimates(chainId) && gasEstimateUSD ? gasEstimateUSD : gasCostStablecoinAmount if (outputDollarValue && dollarCostToUse) { // optimize for highest possible slippage without getting MEV'd diff --git a/apps/web/src/hooks/useContract.ts b/apps/web/src/hooks/useContract.ts index 386454c38ab..bc32103d1fc 100644 --- a/apps/web/src/hooks/useContract.ts +++ b/apps/web/src/hooks/useContract.ts @@ -2,6 +2,7 @@ import { Contract } from '@ethersproject/contracts' import { InterfaceEventName } from '@uniswap/analytics-events' import { ARGENT_WALLET_DETECTOR_ADDRESS, + CHAIN_TO_ADDRESSES_MAP, ENS_REGISTRAR_ADDRESSES, MULTICALL_ADDRESSES, NONFUNGIBLE_POSITION_MANAGER_ADDRESSES, @@ -37,8 +38,8 @@ import { NonfungiblePositionManager, UniswapInterfaceMulticall } from 'uniswap/s import { V3Migrator } from 'uniswap/src/abis/types/v3/V3Migrator' import WETH_ABI from 'uniswap/src/abis/weth.json' import { WRAPPED_NATIVE_CURRENCY } from 'uniswap/src/constants/tokens' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' -import { UniverseChainId } from 'uniswap/src/types/chains' import { getContract } from 'utilities/src/contracts/getContract' import { logger } from 'utilities/src/logger/logger' @@ -204,3 +205,36 @@ export function useV3NFTPositionManagerContract( }, [account.isConnected, chainIdToUse, contract, withSignerIfPossible]) return contract } + +/** + * NOTE: the return type of this contract and the ABI used are just a generic ERC721, + * so you can only use this to call tokenURI or other Position NFT related functions. + */ +export function useV4NFTPositionManagerContract( + withSignerIfPossible?: boolean, + chainId?: UniverseChainId, +): Erc721 | null { + const account = useAccount() + const chainIdToUse = chainId ?? account.chainId + + const contract = useContract( + chainIdToUse ? CHAIN_TO_ADDRESSES_MAP[chainIdToUse].v4PositionManagerAddress : undefined, + NFTPositionManagerABI, + withSignerIfPossible, + chainIdToUse, + ) + useEffect(() => { + if (contract && account.isConnected) { + sendAnalyticsEvent(InterfaceEventName.WALLET_PROVIDER_USED, { + source: 'useV4NFTPositionManagerContract', + contract: { + name: 'V4NonfungiblePositionManager', + address: contract.address, + withSignerIfPossible, + chainId: chainIdToUse, + }, + }) + } + }, [account.isConnected, chainIdToUse, contract, withSignerIfPossible]) + return contract +} diff --git a/apps/web/src/hooks/useERC20Permit.ts b/apps/web/src/hooks/useERC20Permit.ts index e17057a1271..ca1f27739f4 100644 --- a/apps/web/src/hooks/useERC20Permit.ts +++ b/apps/web/src/hooks/useERC20Permit.ts @@ -9,7 +9,7 @@ import JSBI from 'jsbi' import { useSingleCallResult } from 'lib/hooks/multicall' import { useMemo, useState } from 'react' import { DAI, UNI, USDC_MAINNET } from 'uniswap/src/constants/tokens' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' export enum PermitType { AMOUNT = 1, diff --git a/apps/web/src/hooks/useEthersProvider.ts b/apps/web/src/hooks/useEthersProvider.ts index 0fe25fa89ea..b2c0f099b3d 100644 --- a/apps/web/src/hooks/useEthersProvider.ts +++ b/apps/web/src/hooks/useEthersProvider.ts @@ -1,7 +1,7 @@ import { Web3Provider } from '@ethersproject/providers' import { useAccount } from 'hooks/useAccount' import { useMemo } from 'react' -import { UniverseChainInfo } from 'uniswap/src/types/chains' +import { UniverseChainInfo } from 'uniswap/src/features/chains/types' import type { Client, Transport } from 'viem' import { useClient, useConnectorClient } from 'wagmi' diff --git a/apps/web/src/hooks/useEthersSigner.ts b/apps/web/src/hooks/useEthersSigner.ts index 593966a50a6..e58c9271593 100644 --- a/apps/web/src/hooks/useEthersSigner.ts +++ b/apps/web/src/hooks/useEthersSigner.ts @@ -1,6 +1,6 @@ import { Web3Provider } from '@ethersproject/providers' import { useMemo } from 'react' -import { UniverseChainInfo } from 'uniswap/src/types/chains' +import { UniverseChainInfo } from 'uniswap/src/features/chains/types' import type { Account, Client, Transport } from 'viem' import { useConnectorClient } from 'wagmi' diff --git a/apps/web/src/hooks/useFeeTierDistribution.ts b/apps/web/src/hooks/useFeeTierDistribution.ts index 5e4ca6ba970..862d4794ba9 100644 --- a/apps/web/src/hooks/useFeeTierDistribution.ts +++ b/apps/web/src/hooks/useFeeTierDistribution.ts @@ -1,6 +1,5 @@ import { Currency, Token } from '@uniswap/sdk-core' import { FeeAmount } from '@uniswap/v3-sdk' -import { chainIdToBackendChain } from 'constants/chains' import { PoolState, usePool } from 'hooks/usePools' import ms from 'ms' import { useMemo } from 'react' @@ -8,6 +7,8 @@ import { useFeeTierDistributionQuery, useIsV3SubgraphStaleQuery, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { toGraphQLChain } from 'uniswap/src/features/chains/utils' import { logger } from 'utilities/src/logger/logger' interface FeeTierDistribution { @@ -97,7 +98,8 @@ export function useFeeTierDistribution( } function usePoolTVL(token0: Token | undefined, token1: Token | undefined) { - const chain = chainIdToBackendChain({ chainId: token0?.chainId, withFallback: true }) + const { defaultChainId } = useEnabledChains() + const chain = toGraphQLChain(token0?.chainId ?? defaultChainId) const { loading, error, data } = useFeeTierDistributionQuery({ variables: { chain, diff --git a/apps/web/src/hooks/useFetchListCallback.ts b/apps/web/src/hooks/useFetchListCallback.ts index f9f993fbc03..8b865762912 100644 --- a/apps/web/src/hooks/useFetchListCallback.ts +++ b/apps/web/src/hooks/useFetchListCallback.ts @@ -6,7 +6,7 @@ import resolveENSContentHash from 'lib/utils/resolveENSContentHash' import { useCallback } from 'react' import { useAppDispatch } from 'state/hooks' import { fetchTokenList } from 'state/lists/actions' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { logger } from 'utilities/src/logger/logger' export function useFetchListCallback(): (listUrl: string, skipValidation?: boolean) => Promise { diff --git a/apps/web/src/hooks/useFilterPossiblyMaliciousPositions.ts b/apps/web/src/hooks/useFilterPossiblyMaliciousPositions.ts index 148c40d577a..9566af6d4a8 100644 --- a/apps/web/src/hooks/useFilterPossiblyMaliciousPositions.ts +++ b/apps/web/src/hooks/useFilterPossiblyMaliciousPositions.ts @@ -1,5 +1,4 @@ import { useQueries } from '@tanstack/react-query' -import { chainIdToBackendChain } from 'constants/chains' import { apolloClient } from 'graphql/data/apollo/client' import { gqlTokenToCurrencyInfo } from 'graphql/data/types' import { apolloQueryOptions } from 'graphql/data/util' @@ -13,7 +12,9 @@ import { TokenDocument, TokenQuery, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { toGraphQLChain } from 'uniswap/src/features/chains/utils' import { hasURL } from 'utils/urlChecks' function getUniqueAddressesFromPositions(positions: PositionDetails[]): string[] { @@ -22,7 +23,7 @@ function getUniqueAddressesFromPositions(positions: PositionDetails[]): string[] ) } -function getPositionCurrencyInfosQueryOptions(position: PositionDetails, chainId?: UniverseChainId) { +function getPositionCurrencyInfosQueryOptions(position: PositionDetails, chainId: UniverseChainId) { return apolloQueryOptions({ queryKey: ['positionCurrencyInfo', position], queryFn: async () => { @@ -31,7 +32,7 @@ function getPositionCurrencyInfosQueryOptions(position: PositionDetails, chainId query: TokenDocument, variables: { address: position.token0, - chain: chainIdToBackendChain({ chainId }), + chain: toGraphQLChain(chainId), }, fetchPolicy: 'cache-first', }), @@ -39,7 +40,7 @@ function getPositionCurrencyInfosQueryOptions(position: PositionDetails, chainId query: TokenDocument, variables: { address: position.token1, - chain: chainIdToBackendChain({ chainId }), + chain: toGraphQLChain(chainId), }, fetchPolicy: 'cache-first', }), @@ -67,10 +68,11 @@ function getPositionCurrencyInfosQueryOptions(position: PositionDetails, chainId */ export function useFilterPossiblyMaliciousPositions(positions: PositionDetails[]): PositionDetails[] { const { chainId } = useAccount() + const { defaultChainId } = useEnabledChains() const nonListPositionTokenAddresses = useMemo(() => getUniqueAddressesFromPositions(positions), [positions]) const positionCurrencyInfos = useQueries({ - queries: positions.map((position) => getPositionCurrencyInfosQueryOptions(position, chainId)), + queries: positions.map((position) => getPositionCurrencyInfosQueryOptions(position, chainId ?? defaultChainId)), }) const symbolCallStates = useTokenContractsConstant(nonListPositionTokenAddresses, 'symbol') diff --git a/apps/web/src/hooks/useGlobalChainSwitch.ts b/apps/web/src/hooks/useGlobalChainSwitch.ts index 48193f1cdc5..2e381f0b666 100644 --- a/apps/web/src/hooks/useGlobalChainSwitch.ts +++ b/apps/web/src/hooks/useGlobalChainSwitch.ts @@ -1,8 +1,9 @@ -import { chainIdToBackendChain, useIsSupportedChainId } from 'constants/chains' import { useAccount } from 'hooks/useAccount' import { useEffect } from 'react' import { useAppSelector } from 'state/hooks' import { Chain } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { useIsSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { toGraphQLChain } from 'uniswap/src/features/chains/utils' export const useOnGlobalChainSwitch = (callback: (chainId: number, chain?: Chain) => void) => { const { chainId } = useAccount() @@ -10,7 +11,7 @@ export const useOnGlobalChainSwitch = (callback: (chainId: number, chain?: Chain const switchingChain = useAppSelector((state) => state.wallets.switchingChain) useEffect(() => { if (isSupportedChain && chainId === switchingChain) { - const chainName = chainIdToBackendChain({ chainId }) + const chainName = toGraphQLChain(chainId) callback(chainId, chainName) } }, [callback, chainId, isSupportedChain, switchingChain]) diff --git a/apps/web/src/hooks/useIsUniswapXSupportedChain.ts b/apps/web/src/hooks/useIsUniswapXSupportedChain.ts new file mode 100644 index 00000000000..b856d785eea --- /dev/null +++ b/apps/web/src/hooks/useIsUniswapXSupportedChain.ts @@ -0,0 +1,17 @@ +/* eslint-disable rulesdir/no-undefined-or */ +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { ArbitrumXV2ExperimentGroup, Experiments } from 'uniswap/src/features/gating/experiments' +import { FeatureFlags } from 'uniswap/src/features/gating/flags' +import { useExperimentGroupName, useFeatureFlag } from 'uniswap/src/features/gating/hooks' + +export function useIsUniswapXSupportedChain(chainId?: number) { + const xv2ArbitrumEnabled = + useExperimentGroupName(Experiments.ArbitrumXV2OpenOrders) === ArbitrumXV2ExperimentGroup.Test + const isPriorityOrdersEnabled = useFeatureFlag(FeatureFlags.UniswapXPriorityOrders) + + return ( + chainId === UniverseChainId.Mainnet || + (xv2ArbitrumEnabled && chainId === UniverseChainId.ArbitrumOne) || + (isPriorityOrdersEnabled && chainId === UniverseChainId.Base) // UniswapX priority orders are only available on Base for now + ) +} diff --git a/apps/web/src/hooks/useLocalCurrencyLinkProps.ts b/apps/web/src/hooks/useLocalCurrencyLinkProps.ts index 06c1dac3cf4..536e370b870 100644 --- a/apps/web/src/hooks/useLocalCurrencyLinkProps.ts +++ b/apps/web/src/hooks/useLocalCurrencyLinkProps.ts @@ -1,11 +1,11 @@ -import { useActiveLocalCurrency } from 'hooks/useActiveLocalCurrency' -import useParsedQueryString from 'hooks/useParsedQueryString' import { stringify } from 'qs' import { useMemo } from 'react' import { useDispatch } from 'react-redux' import type { To } from 'react-router-dom' import { useLocation } from 'react-router-dom' +import { useUrlContext } from 'uniswap/src/contexts/UrlContext' import { FiatCurrency } from 'uniswap/src/features/fiatCurrency/constants' +import { useAppFiatCurrency } from 'uniswap/src/features/fiatCurrency/hooks' import { setCurrentFiatCurrency } from 'uniswap/src/features/settings/slice' import { InterfaceEventNameLocal } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' @@ -16,8 +16,9 @@ export function useLocalCurrencyLinkProps(localCurrency?: FiatCurrency): { } { const dispatch = useDispatch() const location = useLocation() + const { useParsedQueryString } = useUrlContext() const qs = useParsedQueryString() - const activeLocalCurrency = useActiveLocalCurrency() + const activeLocalCurrency = useAppFiatCurrency() return useMemo( () => diff --git a/apps/web/src/hooks/useLocationLinkProps.ts b/apps/web/src/hooks/useLocationLinkProps.ts index bd4048703fb..87267e2e4ec 100644 --- a/apps/web/src/hooks/useLocationLinkProps.ts +++ b/apps/web/src/hooks/useLocationLinkProps.ts @@ -1,14 +1,15 @@ -import useParsedQueryString from 'hooks/useParsedQueryString' import { stringify } from 'qs' import { useMemo } from 'react' import type { To } from 'react-router-dom' import { useLocation } from 'react-router-dom' +import { useUrlContext } from 'uniswap/src/contexts/UrlContext' import { Locale } from 'uniswap/src/features/language/constants' export function useLocationLinkProps(locale: Locale | null): { to?: To } { const location = useLocation() + const { useParsedQueryString } = useUrlContext() const qs = useParsedQueryString() return useMemo( diff --git a/apps/web/src/hooks/useNetworkSupportsV2.ts b/apps/web/src/hooks/useNetworkSupportsV2.ts index 8a3cbcf1ea6..cbdd11b82e3 100644 --- a/apps/web/src/hooks/useNetworkSupportsV2.ts +++ b/apps/web/src/hooks/useNetworkSupportsV2.ts @@ -1,11 +1,13 @@ -import { SUPPORTED_V2POOL_CHAIN_IDS } from 'constants/chains' +import { V2_ROUTER_ADDRESSES } from '@uniswap/sdk-core' import { useAccount } from 'hooks/useAccount' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' + +/** + * @deprecated when v2 pools are enabled on chains supported through sdk-core + */ +const SUPPORTED_V2POOL_CHAIN_IDS = Object.keys(V2_ROUTER_ADDRESSES).map((chainId) => parseInt(chainId)) export function useNetworkSupportsV2() { const { chainId } = useAccount() - const isV2EverywhereEnabled = useFeatureFlag(FeatureFlags.V2Everywhere) - return chainId && isV2EverywhereEnabled && SUPPORTED_V2POOL_CHAIN_IDS.includes(chainId) + return chainId && SUPPORTED_V2POOL_CHAIN_IDS.includes(chainId) } diff --git a/apps/web/src/hooks/useParsedQueryString.ts b/apps/web/src/hooks/useParsedQueryString.ts deleted file mode 100644 index 3abd77ced3d..00000000000 --- a/apps/web/src/hooks/useParsedQueryString.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { parse, ParsedQs } from 'qs' -import { useMemo } from 'react' -import { useLocation } from 'react-router-dom' - -export default function useParsedQueryString(): ParsedQs { - const { search } = useLocation() - return useMemo(() => { - const hash = window.location.hash - const query = search || hash.substr(hash.indexOf('?')) - - return query && query.length > 1 ? parse(query, { parseArrays: false, ignoreQueryPrefix: true }) : {} - }, [search]) -} diff --git a/apps/web/src/hooks/usePermit2Allowance.ts b/apps/web/src/hooks/usePermit2Allowance.ts index 1c67d540a7a..3e41344231c 100644 --- a/apps/web/src/hooks/usePermit2Allowance.ts +++ b/apps/web/src/hooks/usePermit2Allowance.ts @@ -1,6 +1,5 @@ import { permit2Address } from '@uniswap/permit2-sdk' import { CurrencyAmount, Token } from '@uniswap/sdk-core' -import { AVERAGE_L1_BLOCK_TIME } from 'constants/chains' import { useAccount } from 'hooks/useAccount' import { PermitSignature, usePermitAllowance, useUpdatePermitAllowance } from 'hooks/usePermitAllowance' import { useRevokeTokenAllowance, useTokenAllowance, useUpdateTokenAllowance } from 'hooks/useTokenAllowance' @@ -8,6 +7,7 @@ import useInterval from 'lib/hooks/useInterval' import { useCallback, useEffect, useMemo, useState } from 'react' import { TradeFillType } from 'state/routing/types' import { useHasPendingApproval, useHasPendingRevocation, useTransactionAdder } from 'state/transactions/hooks' +import { AVERAGE_L1_BLOCK_TIME_MS } from 'uniswap/src/features/transactions/swap/hooks/usePollingIntervalByChain' enum ApprovalState { PENDING, @@ -91,10 +91,10 @@ export default function usePermit2Allowance( // Signature and PermitAllowance will expire, so they should be rechecked at an interval. // Calculate now such that the signature will still be valid for the submitting block. - const [now, setNow] = useState(Date.now() + AVERAGE_L1_BLOCK_TIME) + const [now, setNow] = useState(Date.now() + AVERAGE_L1_BLOCK_TIME_MS) useInterval( - useCallback(() => setNow((Date.now() + AVERAGE_L1_BLOCK_TIME) / 1000), []), - AVERAGE_L1_BLOCK_TIME, + useCallback(() => setNow((Date.now() + AVERAGE_L1_BLOCK_TIME_MS) / 1000), []), + AVERAGE_L1_BLOCK_TIME_MS, ) const [signature, setSignature] = useState() diff --git a/apps/web/src/hooks/usePoolTickData.ts b/apps/web/src/hooks/usePoolTickData.ts index b48f4b73050..7bdd9da8154 100644 --- a/apps/web/src/hooks/usePoolTickData.ts +++ b/apps/web/src/hooks/usePoolTickData.ts @@ -1,6 +1,5 @@ import { Currency, Price, Token, V3_CORE_FACTORY_ADDRESSES } from '@uniswap/sdk-core' import { FeeAmount, Pool, TICK_SPACINGS, tickToPrice } from '@uniswap/v3-sdk' -import { chainIdToBackendChain, useSupportedChainId } from 'constants/chains' import { TickData, Ticks } from 'graphql/data/AllV3TicksQuery' import { useAccount } from 'hooks/useAccount' import { PoolState, usePoolMultichain } from 'hooks/usePools' @@ -8,7 +7,9 @@ import JSBI from 'jsbi' import ms from 'ms' import { useEffect, useMemo, useState } from 'react' import { useAllV3TicksQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { useEnabledChains, useSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { toGraphQLChain } from 'uniswap/src/features/chains/utils' import { logger } from 'utilities/src/logger/logger' import computeSurroundingTicks from 'utils/computeSurroundingTicks' @@ -27,13 +28,15 @@ const getActiveTick = (tickCurrent: number | undefined, feeAmount: FeeAmount | u tickCurrent && feeAmount ? Math.floor(tickCurrent / TICK_SPACINGS[feeAmount]) * TICK_SPACINGS[feeAmount] : undefined const MAX_TICK_FETCH_VALUE = 1000 -function useTicksFromSubgraph( +function usePaginatedTickQuery( currencyA: Currency | undefined, currencyB: Currency | undefined, feeAmount: FeeAmount | undefined, skip = 0, chainId: UniverseChainId, ) { + const { defaultChainId } = useEnabledChains() + const poolAddress = currencyA && currencyB && feeAmount ? Pool.getAddress( @@ -49,7 +52,7 @@ function useTicksFromSubgraph( return useAllV3TicksQuery({ variables: { address: poolAddress?.toLowerCase() ?? '', - chain: chainIdToBackendChain({ chainId: supportedChainId, withFallback: true }), + chain: toGraphQLChain(supportedChainId ?? defaultChainId), skip, first: MAX_TICK_FETCH_VALUE, }, @@ -70,13 +73,17 @@ function useAllV3Ticks( ticks?: TickData[] } { const [skipNumber, setSkipNumber] = useState(0) - const [subgraphTickData, setSubgraphTickData] = useState([]) - const { data, error, loading: isLoading } = useTicksFromSubgraph(currencyA, currencyB, feeAmount, skipNumber, chainId) + const [tickData, setTickData] = useState([]) + const { + data, + error, + loading: isLoading, + } = usePaginatedTickQuery(currencyA, currencyB, feeAmount, skipNumber, chainId) const ticks: Ticks = data?.v3Pool?.ticks as Ticks useEffect(() => { if (ticks?.length) { - setSubgraphTickData((tickData) => [...tickData, ...ticks]) + setTickData((tickData) => [...tickData, ...ticks]) if (ticks?.length === MAX_TICK_FETCH_VALUE) { setSkipNumber((skipNumber) => skipNumber + MAX_TICK_FETCH_VALUE) } @@ -86,7 +93,7 @@ function useAllV3Ticks( return { isLoading: isLoading || ticks?.length === MAX_TICK_FETCH_VALUE, error, - ticks: subgraphTickData, + ticks: tickData, } } diff --git a/apps/web/src/hooks/usePools.ts b/apps/web/src/hooks/usePools.ts index a1106824f9b..894a459b16b 100644 --- a/apps/web/src/hooks/usePools.ts +++ b/apps/web/src/hooks/usePools.ts @@ -9,8 +9,8 @@ import { useMultipleContractSingleData } from 'lib/hooks/multicall' import { useEffect, useMemo, useRef } from 'react' import { IUniswapV3PoolStateInterface } from 'uniswap/src/abis/types/v3/IUniswapV3PoolState' import { UniswapV3Pool } from 'uniswap/src/abis/types/v3/UniswapV3Pool' -import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { logger } from 'utilities/src/logger/logger' const POOL_STATE_INTERFACE = new Interface(IUniswapV3PoolStateJSON.abi) as IUniswapV3PoolStateInterface @@ -51,7 +51,7 @@ export class PoolCache { tokenA, tokenB, fee, - chainId: UNIVERSE_CHAIN_INFO[chainId].sdkId, + chainId: getChainInfo(chainId).sdkId, }), } this.addresses.unshift(address) diff --git a/apps/web/src/hooks/usePositionTokenURI.ts b/apps/web/src/hooks/usePositionTokenURI.ts index 9cc44095956..d0f8ee5ff7f 100644 --- a/apps/web/src/hooks/usePositionTokenURI.ts +++ b/apps/web/src/hooks/usePositionTokenURI.ts @@ -1,11 +1,13 @@ import { BigNumber } from '@ethersproject/bignumber' -import { useV3NFTPositionManagerContract } from 'hooks/useContract' -import { useEthersProvider } from 'hooks/useEthersProvider' +import { useQuery } from '@tanstack/react-query' +// eslint-disable-next-line no-restricted-imports +import { ProtocolVersion } from '@uniswap/client-pools/dist/pools/v1/types_pb' +import { useV3NFTPositionManagerContract, useV4NFTPositionManagerContract } from 'hooks/useContract' import JSBI from 'jsbi' -import { NEVER_RELOAD } from 'lib/hooks/multicall' -import multicall from 'lib/state/multicall' import { useMemo } from 'react' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { Erc721 } from 'uniswap/src/abis/types/Erc721' +import { NonfungiblePositionManager } from 'uniswap/src/abis/types/v3/NonfungiblePositionManager' +import { UniverseChainId } from 'uniswap/src/features/chains/types' type TokenId = number | JSBI | BigNumber @@ -30,49 +32,53 @@ type UsePositionTokenURIResult = loading: true } +function useNFTPositionManagerContract( + version: ProtocolVersion, + chainId?: UniverseChainId, +): NonfungiblePositionManager | Erc721 | null { + const v3Contract = useV3NFTPositionManagerContract(false, chainId) + const v4Contract = useV4NFTPositionManagerContract(false, chainId) + return version === ProtocolVersion.V3 ? v3Contract : v4Contract +} + export function usePositionTokenURI( tokenId: TokenId | undefined, chainId?: UniverseChainId, + version?: ProtocolVersion, ): UsePositionTokenURIResult { - const contract = useV3NFTPositionManagerContract(false, chainId) - const inputs = useMemo( - () => [tokenId instanceof BigNumber ? tokenId.toHexString() : tokenId?.toString(16)], - [tokenId], - ) - const latestBlock = useEthersProvider({ chainId })?.blockNumber - const { result, error, loading, valid } = multicall.hooks.useSingleCallResult( - chainId, - latestBlock, - contract, - 'tokenURI', - inputs, - { - ...NEVER_RELOAD, - gasRequired: 3_000_000, + const contract = useNFTPositionManagerContract(version ?? ProtocolVersion.V3, chainId) + const { data, isLoading, error } = useQuery({ + queryKey: ['PositionTokenURI', tokenId, chainId, version], + queryFn: async () => { + const input = tokenId instanceof BigNumber ? tokenId.toHexString() : tokenId?.toString(16) + if (!input) { + return null + } + return await contract?.tokenURI(input) }, - ) + }) return useMemo(() => { - if (error || !valid || !tokenId) { + if (error || !tokenId) { return { valid: false, loading: false, } } - if (loading) { + if (isLoading) { return { valid: true, loading: true, } } - if (!result) { + if (!data) { return { valid: false, loading: false, } } - const [tokenURI] = result as [string] - if (!tokenURI || !tokenURI.startsWith(STARTS_WITH)) { + + if (!data || !data.startsWith(STARTS_WITH)) { return { valid: false, loading: false, @@ -80,7 +86,7 @@ export function usePositionTokenURI( } try { - const json = JSON.parse(atob(tokenURI.slice(STARTS_WITH.length))) + const json = JSON.parse(atob(data.slice(STARTS_WITH.length))) return { valid: true, @@ -90,5 +96,5 @@ export function usePositionTokenURI( } catch (error) { return { valid: false, loading: false } } - }, [error, loading, result, tokenId, valid]) + }, [error, isLoading, data, tokenId]) } diff --git a/apps/web/src/hooks/useSelectChain.ts b/apps/web/src/hooks/useSelectChain.ts index 488ec4567c5..0e83e02d937 100644 --- a/apps/web/src/hooks/useSelectChain.ts +++ b/apps/web/src/hooks/useSelectChain.ts @@ -2,7 +2,7 @@ import { useSwitchChain } from 'hooks/useSwitchChain' import { useCallback } from 'react' import { useDispatch } from 'react-redux' import { PopupType, addPopup, removePopup } from 'state/application/reducer' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { logger } from 'utilities/src/logger/logger' import { UserRejectedRequestError } from 'viem' diff --git a/apps/web/src/hooks/useSendCallback.ts b/apps/web/src/hooks/useSendCallback.ts index 2fa5df2621c..8158d511b70 100644 --- a/apps/web/src/hooks/useSendCallback.ts +++ b/apps/web/src/hooks/useSendCallback.ts @@ -1,7 +1,6 @@ import { TransactionRequest } from '@ethersproject/abstract-provider' import { InterfaceEventName } from '@uniswap/analytics-events' import { Currency, CurrencyAmount } from '@uniswap/sdk-core' -import { useSupportedChainId } from 'constants/chains' import { useAccount } from 'hooks/useAccount' import { useEthersProvider } from 'hooks/useEthersProvider' import { useSwitchChain } from 'hooks/useSwitchChain' @@ -10,6 +9,7 @@ import { useCallback, useRef } from 'react' import { useTransactionAdder } from 'state/transactions/hooks' import { SendTransactionInfo, TransactionType } from 'state/transactions/types' import { trace } from 'tracing/trace' +import { useSupportedChainId } from 'uniswap/src/features/chains/hooks' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { currencyId } from 'utils/currencyId' import { UserRejectedRequestError, toReadableError } from 'utils/errors' diff --git a/apps/web/src/hooks/useSocksBalance.ts b/apps/web/src/hooks/useSocksBalance.ts index f2913bab0b8..373720d209c 100644 --- a/apps/web/src/hooks/useSocksBalance.ts +++ b/apps/web/src/hooks/useSocksBalance.ts @@ -2,7 +2,7 @@ import { SOCKS_CONTROLLER_ADDRESSES, Token } from '@uniswap/sdk-core' import { useAccount } from 'hooks/useAccount' import { useTokenBalance } from 'lib/hooks/useCurrencyBalance' import { useMemo } from 'react' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' // technically a 721, not an ERC20, but suffices for our purposes const SOCKS = new Token(UniverseChainId.Mainnet, SOCKS_CONTROLLER_ADDRESSES[UniverseChainId.Mainnet], 0) diff --git a/apps/web/src/hooks/useStablecoinPrice.ts b/apps/web/src/hooks/useStablecoinPrice.ts index 882425a1405..831055f13c5 100644 --- a/apps/web/src/hooks/useStablecoinPrice.ts +++ b/apps/web/src/hooks/useStablecoinPrice.ts @@ -1,10 +1,11 @@ import { Currency, CurrencyAmount, Price, Token, TradeType } from '@uniswap/sdk-core' -import { getChain, useSupportedChainId } from 'constants/chains' import { useAccount } from 'hooks/useAccount' import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount' import { useMemo, useRef } from 'react' import { ClassicTrade, INTERNAL_ROUTER_PREFERENCE_PRICE, TradeState } from 'state/routing/types' import { useRoutingAPITrade } from 'state/routing/useRoutingAPITrade' +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' +import { useSupportedChainId } from 'uniswap/src/features/chains/hooks' /** * Returns the price in USDC of the input currency @@ -15,7 +16,7 @@ export default function useStablecoinPrice(currency?: Currency): { state: TradeState } { const chainId = useSupportedChainId(currency?.chainId) - const amountOut = chainId ? getChain({ chainId }).spotPriceStablecoinAmount : undefined + const amountOut = chainId ? getChainInfo(chainId).spotPriceStablecoinAmount : undefined const stablecoin = amountOut?.currency const { trade, state } = useRoutingAPITrade( @@ -79,9 +80,7 @@ export function useStablecoinValue(currencyAmount: CurrencyAmount | un export function useStablecoinAmountFromFiatValue(fiatValue: number | null | undefined) { const { chainId } = useAccount() const supportedChainId = useSupportedChainId(chainId) - const stablecoin = supportedChainId - ? getChain({ chainId: supportedChainId }).spotPriceStablecoinAmount.currency - : undefined + const stablecoin = supportedChainId ? getChainInfo(supportedChainId).spotPriceStablecoinAmount.currency : undefined return useMemo(() => { if (fiatValue === null || fiatValue === undefined || !chainId || !stablecoin) { diff --git a/apps/web/src/hooks/useSwapCallback.tsx b/apps/web/src/hooks/useSwapCallback.tsx index 88224e9ac94..ea1d715bf2d 100644 --- a/apps/web/src/hooks/useSwapCallback.tsx +++ b/apps/web/src/hooks/useSwapCallback.tsx @@ -1,7 +1,6 @@ import { Percent, TradeType } from '@uniswap/sdk-core' import { FlatFeeOptions } from '@uniswap/universal-router-sdk' import { FeeOptions } from '@uniswap/v3-sdk' -import { useSupportedChainId } from 'constants/chains' import { BigNumber } from 'ethers/lib/ethers' import { useAccount } from 'hooks/useAccount' import { PermitSignature } from 'hooks/usePermitAllowance' @@ -20,7 +19,8 @@ import { ExactOutputSwapTransactionInfo, TransactionType, } from 'state/transactions/types' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { useSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { currencyId } from 'utils/currencyId' export type SwapResult = Awaited>> diff --git a/apps/web/src/hooks/useSwapTaxes.ts b/apps/web/src/hooks/useSwapTaxes.ts index 29d4375b724..09155f7ea55 100644 --- a/apps/web/src/hooks/useSwapTaxes.ts +++ b/apps/web/src/hooks/useSwapTaxes.ts @@ -7,8 +7,8 @@ import { useContract } from 'hooks/useContract' import { useEffect, useState } from 'react' import FOT_DETECTOR_ABI from 'uniswap/src/abis/fee-on-transfer-detector.json' import { FeeOnTransferDetector } from 'uniswap/src/abis/types' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' -import { UniverseChainId } from 'uniswap/src/types/chains' import { logger } from 'utilities/src/logger/logger' // TODO(WEB-4058): Move all of these contract addresses into the top-level wagmi config diff --git a/apps/web/src/hooks/useSwitchChain.ts b/apps/web/src/hooks/useSwitchChain.ts index 2b136ae7b12..ac343b7ad05 100644 --- a/apps/web/src/hooks/useSwitchChain.ts +++ b/apps/web/src/hooks/useSwitchChain.ts @@ -1,10 +1,10 @@ -import { useIsSupportedChainIdCallback } from 'constants/chains' import { useAccount } from 'hooks/useAccount' import { useCallback } from 'react' import { useDispatch } from 'react-redux' import { endSwitchingChain, startSwitchingChain } from 'state/wallets/reducer' import { trace } from 'tracing/trace' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { useIsSupportedChainIdCallback } from 'uniswap/src/features/chains/hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { useSwitchChain as useSwitchChainWagmi } from 'wagmi' export function useSwitchChain() { diff --git a/apps/web/src/hooks/useTokenBalances.ts b/apps/web/src/hooks/useTokenBalances.ts index cc244adf8a3..ee10382e314 100644 --- a/apps/web/src/hooks/useTokenBalances.ts +++ b/apps/web/src/hooks/useTokenBalances.ts @@ -7,7 +7,7 @@ import { QuickTokenBalancePartsFragment, useQuickTokenBalancesWebQuery, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { useEnabledChains } from 'uniswap/src/features/settings/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks' import { currencyKeyFromGraphQL } from 'utils/currencyKey' /** diff --git a/apps/web/src/hooks/useUSDPrice.ts b/apps/web/src/hooks/useUSDPrice.ts index 69e1dc5eddd..8e9c4470e14 100644 --- a/apps/web/src/hooks/useUSDPrice.ts +++ b/apps/web/src/hooks/useUSDPrice.ts @@ -1,6 +1,5 @@ import { NetworkStatus } from '@apollo/client' import { Currency, CurrencyAmount, Price, TradeType } from '@uniswap/sdk-core' -import { chainIdToBackendChain, useIsSupportedChainId, useSupportedChainId } from 'constants/chains' import { PollingInterval } from 'graphql/data/util' import useIsWindowVisible from 'hooks/useIsWindowVisible' import useStablecoinPrice from 'hooks/useStablecoinPrice' @@ -9,7 +8,9 @@ import { ClassicTrade, INTERNAL_ROUTER_PREFERENCE_PRICE, TradeState } from 'stat import { useRoutingAPITrade } from 'state/routing/useRoutingAPITrade' import { nativeOnChain } from 'uniswap/src/constants/tokens' import { Chain, useTokenSpotPriceQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { useEnabledChains, useIsSupportedChainId, useSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { toGraphQLChain } from 'uniswap/src/features/chains/utils' import { getNativeTokenDBAddress } from 'utils/nativeTokens' // ETH amounts used when calculating spot price for a given currency. @@ -71,7 +72,8 @@ export function useUSDPrice( } { const currency = currencyAmount?.currency ?? prefetchCurrency const chainId = useSupportedChainId(currency?.chainId) - const chain = chainIdToBackendChain({ chainId }) + const { defaultChainId } = useEnabledChains() + const chain = toGraphQLChain(chainId ?? defaultChainId) // skip all pricing requests if the window is not focused const isWindowVisible = useIsWindowVisible() diff --git a/apps/web/src/hooks/useUSDTokenUpdater.ts b/apps/web/src/hooks/useUSDTokenUpdater.ts index afd86b8ad1f..2e21baee27e 100644 --- a/apps/web/src/hooks/useUSDTokenUpdater.ts +++ b/apps/web/src/hooks/useUSDTokenUpdater.ts @@ -1,9 +1,10 @@ import { Currency } from '@uniswap/sdk-core' -import { getChain, useSupportedChainId } from 'constants/chains' import useStablecoinPrice from 'hooks/useStablecoinPrice' import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount' import { useMemo } from 'react' import { TradeState } from 'state/routing/types' +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' +import { useSupportedChainId } from 'uniswap/src/features/chains/hooks' import { NumberType, useFormatter } from 'utils/formatNumbers' const NUM_DECIMALS_USD = 2 @@ -30,10 +31,7 @@ export function useUSDTokenUpdater( if (isFiatInput) { const exactAmountUSD = (parseFloat(exactAmount || '0') / conversionRate).toFixed(NUM_DECIMALS_USD) const stablecoinAmount = supportedChainId - ? tryParseCurrencyAmount( - exactAmountUSD, - getChain({ chainId: supportedChainId }).spotPriceStablecoinAmount.currency, - ) + ? tryParseCurrencyAmount(exactAmountUSD, getChainInfo(supportedChainId).spotPriceStablecoinAmount.currency) : undefined const currencyAmount = stablecoinAmount ? price?.invert().quote(stablecoinAmount) : undefined diff --git a/apps/web/src/i18n/LanguageProvider.tsx b/apps/web/src/i18n/LanguageProvider.tsx index 2f3a5c65d26..7d2aa689db4 100644 --- a/apps/web/src/i18n/LanguageProvider.tsx +++ b/apps/web/src/i18n/LanguageProvider.tsx @@ -1,14 +1,19 @@ -import { navigatorLocale, parseLocale, storeLocale, useActiveLocale } from 'hooks/useActiveLocale' import { ReactNode, useEffect } from 'react' +import store from 'state' import { useAppDispatch } from 'state/hooks' -import { DEFAULT_LOCALE, mapLocaleToLanguage } from 'uniswap/src/features/language/constants' -import { useCurrentLocale } from 'uniswap/src/features/language/hooks' +import { DEFAULT_LOCALE, Locale, mapLocaleToLanguage } from 'uniswap/src/features/language/constants' +import { getLocale, navigatorLocale, parseLocale, useCurrentLocale } from 'uniswap/src/features/language/hooks' import { setCurrentLanguage } from 'uniswap/src/features/settings/slice' import { changeLanguage } from 'uniswap/src/i18n' +function getStoreLocale(): Locale | undefined { + const storeLanguage = store.getState().userSettings.currentLanguage + return getLocale(storeLanguage) +} + function setupInitialLanguage() { const lngQuery = typeof window !== 'undefined' ? new URL(window.location.href).searchParams.get('lng') : '' - const initialLocale = parseLocale(lngQuery) ?? storeLocale() ?? navigatorLocale() ?? DEFAULT_LOCALE + const initialLocale = parseLocale(lngQuery) ?? getStoreLocale() ?? navigatorLocale() ?? DEFAULT_LOCALE changeLanguage(initialLocale) } @@ -17,10 +22,8 @@ if (process.env.NODE_ENV !== 'test') { } export function LanguageProvider({ children }: { children: ReactNode }): JSX.Element { - const activeLocale = useActiveLocale() - const userLocale = useCurrentLocale() const dispatch = useAppDispatch() - const locale = userLocale || activeLocale + const locale = useCurrentLocale() useEffect(() => { changeLanguage(locale) diff --git a/apps/web/src/index.tsx b/apps/web/src/index.tsx index f7b1908aa38..4fcf67e419d 100644 --- a/apps/web/src/index.tsx +++ b/apps/web/src/index.tsx @@ -6,7 +6,7 @@ import { ApolloProvider } from '@apollo/client' import { PortalProvider } from '@tamagui/portal' import { QueryClientProvider } from '@tanstack/react-query' import { MiniKit } from '@worldcoin/minikit-js' -import Web3Provider from 'components/Web3Provider' +import Web3Provider, { Web3ProviderUpdater } from 'components/Web3Provider' import { WebUniswapProvider } from 'components/Web3Provider/WebUniswapContext' import { AssetActivityProvider } from 'graphql/data/apollo/AssetActivityProvider' import { TokenBalancesProvider } from 'graphql/data/apollo/TokenBalancesProvider' @@ -33,6 +33,7 @@ import RadialGradientByChainUpdater from 'theme/components/RadialGradientByChain import { SystemThemeUpdater, ThemeColorMetaUpdater } from 'theme/components/ThemeToggle' import { TamaguiProvider } from 'theme/tamaguiProvider' import { getEnvName } from 'tracing/env' +import { ReactRouterUrlProvider } from 'uniswap/src/contexts/UrlContext' import { SharedQueryClient } from 'uniswap/src/data/apiClients/SharedQueryClient' import { DUMMY_STATSIG_SDK_KEY } from 'uniswap/src/features/gating/constants' import { LocalizationContextProvider } from 'uniswap/src/features/language/LocalizationContext' @@ -62,6 +63,7 @@ function Updaters() { + ) } @@ -116,39 +118,41 @@ const Router = isBrowserRouterEnabled() ? BrowserRouter : HashRouter createRoot(container).render( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + , ) diff --git a/apps/web/src/lib/hooks/multicall.ts b/apps/web/src/lib/hooks/multicall.ts index d2004e74852..ea6c16185d4 100644 --- a/apps/web/src/lib/hooks/multicall.ts +++ b/apps/web/src/lib/hooks/multicall.ts @@ -2,7 +2,7 @@ import { useMainnetBlockNumber } from 'lib/hooks/useBlockNumber' import { useCallContext } from 'lib/hooks/useCallContext' import multicall from 'lib/state/multicall' import { SkipFirst } from 'types/tuple' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' export { NEVER_RELOAD } from '@uniswap/redux-multicall' // re-export for convenience export type { CallStateResult } from '@uniswap/redux-multicall' // re-export for convenience diff --git a/apps/web/src/lib/hooks/routing/clientSideSmartOrderRouter.ts b/apps/web/src/lib/hooks/routing/clientSideSmartOrderRouter.ts index 6165603ab55..eaec1b186d4 100644 --- a/apps/web/src/lib/hooks/routing/clientSideSmartOrderRouter.ts +++ b/apps/web/src/lib/hooks/routing/clientSideSmartOrderRouter.ts @@ -2,13 +2,12 @@ import { BigintIsh, CurrencyAmount, Token, TradeType } from '@uniswap/sdk-core' // This file is lazy-loaded, so the import of smart-order-router is intentional. // eslint-disable-next-line @typescript-eslint/no-restricted-imports import { AlphaRouter, AlphaRouterConfig } from '@uniswap/smart-order-router' -import { getChain, isSupportedChainId } from 'constants/chains' import { RPC_PROVIDERS } from 'constants/providers' import JSBI from 'jsbi' import { GetQuoteArgs, QuoteResult, QuoteState, SwapRouterNativeAssets } from 'state/routing/types' -import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' import { nativeOnChain } from 'uniswap/src/constants/tokens' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' +import { UniverseChainId, isUniverseChainId } from 'uniswap/src/features/chains/types' import { transformSwapRouteToGetQuoteResult } from 'utils/transformSwapRouteToGetQuoteResult' const routers = new Map() @@ -18,9 +17,9 @@ export function getRouter(chainId: UniverseChainId): AlphaRouter { return router } - if (isSupportedChainId(chainId) && getChain({ chainId }).supportsClientSideRouting) { + if (isUniverseChainId(chainId) && getChainInfo(chainId).supportsInterfaceClientSideRouting) { const provider = RPC_PROVIDERS[chainId as UniverseChainId] - const router = new AlphaRouter({ chainId: UNIVERSE_CHAIN_INFO[chainId].sdkId, provider }) + const router = new AlphaRouter({ chainId: getChainInfo(chainId).sdkId, provider }) routers.set(chainId, router) return router } diff --git a/apps/web/src/lib/hooks/routing/useRoutingAPIArguments.ts b/apps/web/src/lib/hooks/routing/useRoutingAPIArguments.ts index 5a393c7fa8c..32d540f5d0e 100644 --- a/apps/web/src/lib/hooks/routing/useRoutingAPIArguments.ts +++ b/apps/web/src/lib/hooks/routing/useRoutingAPIArguments.ts @@ -1,10 +1,11 @@ import { SkipToken, skipToken } from '@reduxjs/toolkit/query/react' import { Protocol } from '@uniswap/router-sdk' import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core' -import { useIsUniswapXSupportedChain } from 'constants/chains' +import { useIsUniswapXSupportedChain } from 'hooks/useIsUniswapXSupportedChain' import { useMemo } from 'react' import { GetQuoteArgs, INTERNAL_ROUTER_PREFERENCE_PRICE, RouterPreference } from 'state/routing/types' import { currencyAddressForSwapQuote } from 'state/routing/utils' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { ArbitrumXV2ExperimentGroup, ArbitrumXV2OpenOrderProperties, @@ -12,7 +13,6 @@ import { } from 'uniswap/src/features/gating/experiments' import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { useExperimentGroupName, useExperimentValue, useFeatureFlag } from 'uniswap/src/features/gating/hooks' -import { UniverseChainId } from 'uniswap/src/types/chains' /** * Returns query arguments for the Routing API query or undefined if the diff --git a/apps/web/src/lib/hooks/useBlockNumber.tsx b/apps/web/src/lib/hooks/useBlockNumber.tsx index 1fc46e36d18..916e5e84a33 100644 --- a/apps/web/src/lib/hooks/useBlockNumber.tsx +++ b/apps/web/src/lib/hooks/useBlockNumber.tsx @@ -7,7 +7,7 @@ import useIsWindowVisible from 'hooks/useIsWindowVisible' import { atom } from 'jotai' import { useAtomValue } from 'jotai/utils' import { PropsWithChildren, createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' // MulticallUpdater is outside of the SwapAndLimitContext but we still want to use the swap context chainId for swap-related multicalls export const multicallUpdaterSwapChainIdAtom = atom(undefined) diff --git a/apps/web/src/lib/hooks/useNativeCurrency.ts b/apps/web/src/lib/hooks/useNativeCurrency.ts index 943b3d2c510..c918db4aae8 100644 --- a/apps/web/src/lib/hooks/useNativeCurrency.ts +++ b/apps/web/src/lib/hooks/useNativeCurrency.ts @@ -1,7 +1,7 @@ import { NativeCurrency, Token } from '@uniswap/sdk-core' import { useMemo } from 'react' import { nativeOnChain } from 'uniswap/src/constants/tokens' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' export default function useNativeCurrency(chainId: UniverseChainId | null | undefined): NativeCurrency | Token { return useMemo( diff --git a/apps/web/src/lib/hooks/useTokenList/sorting.test.ts b/apps/web/src/lib/hooks/useTokenList/sorting.test.ts index ebed74f3cc1..c45dd10941d 100644 --- a/apps/web/src/lib/hooks/useTokenList/sorting.test.ts +++ b/apps/web/src/lib/hooks/useTokenList/sorting.test.ts @@ -8,7 +8,7 @@ import { TokenBalance, TokenStandard, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' const nativeToken: Token = { id: 'native-token', diff --git a/apps/web/src/lib/hooks/useTokenList/sorting.ts b/apps/web/src/lib/hooks/useTokenList/sorting.ts index 0cdbda776e6..924e713ebb4 100644 --- a/apps/web/src/lib/hooks/useTokenList/sorting.ts +++ b/apps/web/src/lib/hooks/useTokenList/sorting.ts @@ -2,7 +2,7 @@ import { Token } from '@uniswap/sdk-core' import { PortfolioBalance } from 'graphql/data/portfolios' import { supportedChainIdFromGQLChain } from 'graphql/data/util' import { nativeOnChain } from 'uniswap/src/constants/tokens' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { currencyKey } from 'utils/currencyKey' import { SplitOptions, splitHiddenTokens } from 'utils/splitHiddenTokens' diff --git a/apps/web/src/lib/state/multicall.tsx b/apps/web/src/lib/state/multicall.tsx index 4a26d3f87f5..d9612be4a80 100644 --- a/apps/web/src/lib/state/multicall.tsx +++ b/apps/web/src/lib/state/multicall.tsx @@ -4,8 +4,8 @@ import { useInterfaceMulticall, useMainnetInterfaceMulticall } from 'hooks/useCo import { useAtomValue } from 'jotai/utils' import useBlockNumber, { multicallUpdaterSwapChainIdAtom, useMainnetBlockNumber } from 'lib/hooks/useBlockNumber' import { useMemo } from 'react' -import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' +import { UniverseChainId } from 'uniswap/src/features/chains/types' const multicall = createMulticall() @@ -20,7 +20,7 @@ export function MulticallUpdater() { const latestBlockNumber = useBlockNumber() const contract = useInterfaceMulticall(chainId) const listenerOptions: ListenerOptions = useMemo( - () => ({ blocksPerFetch: chainId ? UNIVERSE_CHAIN_INFO[chainId].blockPerMainnetEpochForChainId : 1 }), + () => ({ blocksPerFetch: chainId ? getChainInfo(chainId).blockPerMainnetEpochForChainId : 1 }), [chainId], ) diff --git a/apps/web/src/lib/utils/formatLocaleNumber.test.ts b/apps/web/src/lib/utils/formatLocaleNumber.test.ts index 93e929537fa..d626436091b 100644 --- a/apps/web/src/lib/utils/formatLocaleNumber.test.ts +++ b/apps/web/src/lib/utils/formatLocaleNumber.test.ts @@ -13,7 +13,6 @@ function expectedOutput(l: Locale): string { case 'sw-TZ': case 'zh-Hans': case 'ur-PK': - case 'th-TH': case 'es-US': case 'es-419': case 'ms-MY': diff --git a/apps/web/src/lib/utils/searchBar.test.ts b/apps/web/src/lib/utils/searchBar.test.ts index db0a3f92784..ed1077e8649 100644 --- a/apps/web/src/lib/utils/searchBar.test.ts +++ b/apps/web/src/lib/utils/searchBar.test.ts @@ -3,17 +3,20 @@ import { searchTokenToTokenSearchResult } from 'lib/utils/searchBar' import { getNativeAddress } from 'uniswap/src/constants/addresses' import { Chain, + ProtectionResult, SafetyLevel, TokenStandard, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { SearchResultType } from 'uniswap/src/features/search/SearchResult' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { TokenList } from 'uniswap/src/features/dataApi/types' +import { getCurrencySafetyInfo } from 'uniswap/src/features/dataApi/utils' +import { SearchResultType, TokenSearchResult } from 'uniswap/src/features/search/SearchResult' describe('searchBar', () => { describe('searchTokenToTokenSearchResult', () => { describe(`${NATIVE_CHAIN_ID}`, () => { it('accepts a searchToken and returns a TokenSearchResult', () => { - const ethSearchResult = { + const ethSearchResult: TokenSearchResult = { type: SearchResultType.Token, chainId: UniverseChainId.Mainnet, address: null, @@ -21,6 +24,8 @@ describe('searchBar', () => { symbol: 'ETH', name: 'Ethereum', safetyLevel: SafetyLevel.Verified, + safetyInfo: getCurrencySafetyInfo(SafetyLevel.Verified, { attackTypes: [], result: ProtectionResult.Benign }), + feeData: null, } expect( @@ -39,24 +44,10 @@ describe('searchBar', () => { logoUrl: 'eth-logo.png', safetyLevel: SafetyLevel.Verified, }, - }), - ).toEqual(ethSearchResult) - - expect( - searchTokenToTokenSearchResult({ - decimals: 18, - name: 'Ethereum', - chain: Chain.Ethereum, - // This is not a mistake, sometimes the standard for ETH is ERC20 - // in search results. - standard: TokenStandard.Erc20, - address: NATIVE_CHAIN_ID, - symbol: 'ETH', - chainId: UniverseChainId.Mainnet, - // @ts-ignore - project: { - logoUrl: 'eth-logo.png', - safetyLevel: SafetyLevel.Verified, + feeData: undefined, + protectionInfo: { + attackTypes: [], + result: ProtectionResult.Benign, }, }), ).toEqual(ethSearchResult) @@ -75,6 +66,11 @@ describe('searchBar', () => { logoUrl: 'matic-logo.png', safetyLevel: SafetyLevel.Verified, }, + feeData: undefined, + protectionInfo: { + attackTypes: [], + result: ProtectionResult.Benign, + }, }), ).toEqual({ type: SearchResultType.Token, @@ -84,12 +80,18 @@ describe('searchBar', () => { symbol: 'MATIC', name: 'Polygon', safetyLevel: SafetyLevel.Verified, - }) + feeData: null, + safetyInfo: { + tokenList: TokenList.Default, + attackType: undefined, + protectionResult: ProtectionResult.Benign, + }, + } as TokenSearchResult) }) }) describe(`${TokenStandard.Erc20}`, () => { it('accepts a searchToken and returns a TokenSearchResult', () => { - const tokenSearchResult = { + const tokenSearchResult: TokenSearchResult = { type: SearchResultType.Token, chainId: 1, address: '0x123', @@ -97,6 +99,12 @@ describe('searchBar', () => { symbol: 'ABC', name: 'ABC Token', safetyLevel: SafetyLevel.Verified, + feeData: null, + safetyInfo: { + tokenList: TokenList.Default, + attackType: undefined, + protectionResult: ProtectionResult.Benign, + }, } expect( @@ -113,6 +121,11 @@ describe('searchBar', () => { logoUrl: 'token-logo.png', safetyLevel: SafetyLevel.Verified, }, + feeData: undefined, + protectionInfo: { + attackTypes: [], + result: ProtectionResult.Benign, + }, }), ).toEqual(tokenSearchResult) }) diff --git a/apps/web/src/lib/utils/searchBar.ts b/apps/web/src/lib/utils/searchBar.ts index 16cc51a8bf7..f2a0a13888b 100644 --- a/apps/web/src/lib/utils/searchBar.ts +++ b/apps/web/src/lib/utils/searchBar.ts @@ -1,12 +1,13 @@ import { GqlSearchToken } from 'graphql/data/SearchTokens' import { GenieCollection } from 'nft/types' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { getCurrencySafetyInfo } from 'uniswap/src/features/dataApi/utils' import { NFTCollectionSearchResult, SearchResultType, TokenSearchResult, } from 'uniswap/src/features/search/SearchResult' import { tokenAddressOrNativeAddress } from 'uniswap/src/features/search/utils' -import { UniverseChainId } from 'uniswap/src/types/chains' /** * Organizes the number of Token and NFT results to be shown to a user depending on if they're in the NFT or Token experience @@ -38,6 +39,8 @@ export const searchTokenToTokenSearchResult = ( name: searchToken.name ?? null, logoUrl: searchToken.project?.logoUrl ?? null, safetyLevel: searchToken.project?.safetyLevel ?? null, + safetyInfo: getCurrencySafetyInfo(searchToken.project?.safetyLevel, searchToken.protectionInfo), + feeData: searchToken.feeData ?? null, } } diff --git a/apps/web/src/nft/components/bag/BagFooter.test.tsx b/apps/web/src/nft/components/bag/BagFooter.test.tsx index cbcd22d0c67..d123f8e5ca5 100644 --- a/apps/web/src/nft/components/bag/BagFooter.test.tsx +++ b/apps/web/src/nft/components/bag/BagFooter.test.tsx @@ -20,7 +20,7 @@ import { TEST_TOKEN_1, TEST_TRADE_EXACT_INPUT, USE_CONNECTED_ACCOUNT, toCurrency import { mocked } from 'test-utils/mocked' import { render, screen } from 'test-utils/render' import { nativeOnChain } from 'uniswap/src/constants/tokens' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' jest.mock('hooks/useAccount', () => ({ useAccount: jest.fn(), diff --git a/apps/web/src/nft/components/bag/BagFooter.tsx b/apps/web/src/nft/components/bag/BagFooter.tsx index b5c2d910c09..278d943522b 100644 --- a/apps/web/src/nft/components/bag/BagFooter.tsx +++ b/apps/web/src/nft/components/bag/BagFooter.tsx @@ -10,7 +10,6 @@ import { LoadingBubble } from 'components/Tokens/loading' import { MouseoverTooltip } from 'components/Tooltip' import Column from 'components/deprecated/Column' import Row from 'components/deprecated/Row' -import { useIsSupportedChainId } from 'constants/chains' import { NATIVE_CHAIN_ID } from 'constants/tokens' import { getURAddress, useNftUniversalRouterAddress } from 'graphql/data/nft/NftUniversalRouterAddress' import { useCurrency } from 'hooks/Tokens' @@ -36,10 +35,11 @@ import { PropsWithChildren, useEffect, useMemo, useState } from 'react' import { AlertTriangle, ChevronDown } from 'react-feather' import { InterfaceTrade, TradeFillType, TradeState } from 'state/routing/types' import { ThemedText } from 'theme/components' +import { useIsSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import Trace from 'uniswap/src/features/telemetry/Trace' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { Trans, t } from 'uniswap/src/i18n' -import { UniverseChainId } from 'uniswap/src/types/chains' import { NumberType, useFormatter } from 'utils/formatNumbers' const FooterContainer = styled.div` diff --git a/apps/web/src/nft/components/collection/ActivityCells.tsx b/apps/web/src/nft/components/collection/ActivityCells.tsx index f397d185a5b..3d77db63ba7 100644 --- a/apps/web/src/nft/components/collection/ActivityCells.tsx +++ b/apps/web/src/nft/components/collection/ActivityCells.tsx @@ -32,9 +32,9 @@ import { NftMarketplace, OrderStatus, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { Trans } from 'uniswap/src/i18n' -import { UniverseChainId } from 'uniswap/src/types/chains' import { ExplorerDataType, getExplorerLink } from 'uniswap/src/utils/linking' import { shortenAddress } from 'utilities/src/addresses' import { useTrace } from 'utilities/src/telemetry/trace/TraceContext' diff --git a/apps/web/src/nft/components/collection/Filters.tsx b/apps/web/src/nft/components/collection/Filters.tsx index 22e5b9ea400..766dccae5f2 100644 --- a/apps/web/src/nft/components/collection/Filters.tsx +++ b/apps/web/src/nft/components/collection/Filters.tsx @@ -37,7 +37,12 @@ export const Filters = ({ traitsByGroup }: { traitsByGroup: Record Buy now - + {isMobileWeb && } diff --git a/apps/web/src/nft/components/explore/TrendingCollections.tsx b/apps/web/src/nft/components/explore/TrendingCollections.tsx index ac2165c8986..010d4e67ae6 100644 --- a/apps/web/src/nft/components/explore/TrendingCollections.tsx +++ b/apps/web/src/nft/components/explore/TrendingCollections.tsx @@ -7,7 +7,7 @@ import { CollectionTableColumn, Denomination, TimePeriod, VolumeType } from 'nft import { useMemo, useState } from 'react' import { ThemedText } from 'theme/components' import { HistoryDuration } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { useFormatterLocales } from 'utils/formatNumbers' +import { useAppFiatCurrency } from 'uniswap/src/features/fiatCurrency/hooks' const timeOptions: { label: string; value: TimePeriod }[] = [ { label: '1D', value: TimePeriod.OneDay }, @@ -85,7 +85,7 @@ function convertTimePeriodToHistoryDuration(timePeriod: TimePeriod): HistoryDura } const TrendingCollections = () => { - const { formatterLocalCurrency } = useFormatterLocales() + const currentCurrency = useAppFiatCurrency() const [timePeriod, setTimePeriod] = useState(TimePeriod.OneDay) const [isEthToggled, setEthToggled] = useState(true) @@ -155,7 +155,7 @@ const TrendingCollections = () => { - {formatterLocalCurrency} + {currentCurrency} diff --git a/apps/web/src/nft/hooks/useUsdPrice.ts b/apps/web/src/nft/hooks/useUsdPrice.ts index a1b30d11dc6..6606cb48fa9 100644 --- a/apps/web/src/nft/hooks/useUsdPrice.ts +++ b/apps/web/src/nft/hooks/useUsdPrice.ts @@ -3,7 +3,7 @@ import { useUSDPrice } from 'hooks/useUSDPrice' import useNativeCurrency from 'lib/hooks/useNativeCurrency' import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount' import { GenieAsset } from 'nft/types' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' export const useNativeUsdPrice = (chainId: number = UniverseChainId.Mainnet): number => { const nativeCurrency = useNativeCurrency(chainId) diff --git a/apps/web/src/nft/utils/tokenRoutes.ts b/apps/web/src/nft/utils/tokenRoutes.ts index e9a8b070575..e2a2c094e9b 100644 --- a/apps/web/src/nft/utils/tokenRoutes.ts +++ b/apps/web/src/nft/utils/tokenRoutes.ts @@ -4,12 +4,13 @@ import { Pair } from '@uniswap/v2-sdk' import { Pool as V3Pool } from '@uniswap/v3-sdk' import { Pool as V4Pool } from '@uniswap/v4-sdk' import { ClassicTrade } from 'state/routing/types' -import { DEFAULT_NATIVE_ADDRESS, UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' import { TokenAmountInput, TokenTradeRouteInput, TradePoolInput, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { DEFAULT_NATIVE_ADDRESS, getChainInfo } from 'uniswap/src/features/chains/chainInfo' +import { isUniverseChainId } from 'uniswap/src/features/chains/types' interface SwapAmounts { inputAmount: CurrencyAmount @@ -56,9 +57,9 @@ function buildTradeRouteInputAmounts(swapAmounts: SwapAmounts): TradeTokenInputA function buildPool(pool: Pair | V3Pool | V4Pool): TradePoolInput { const isPool = 'fee' in pool - const chainIdInUniverseChainInfo = pool.chainId in UNIVERSE_CHAIN_INFO - const nativeCurrencyAddress = chainIdInUniverseChainInfo - ? UNIVERSE_CHAIN_INFO[pool.chainId as keyof typeof UNIVERSE_CHAIN_INFO].nativeCurrency.address + const knownChainId = isUniverseChainId(pool.chainId) + const nativeCurrencyAddress = knownChainId + ? getChainInfo(pool.chainId).nativeCurrency.address : DEFAULT_NATIVE_ADDRESS return { diff --git a/apps/web/src/pages/AddLiquidity/redirects.tsx b/apps/web/src/pages/AddLiquidity/redirects.tsx deleted file mode 100644 index b4cca59eb0f..00000000000 --- a/apps/web/src/pages/AddLiquidity/redirects.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { useAccount } from 'hooks/useAccount' -import AddLiquidity from 'pages/AddLiquidity/index' -import { Navigate, useParams } from 'react-router-dom' -import { WRAPPED_NATIVE_CURRENCY } from 'uniswap/src/constants/tokens' - -export default function AddLiquidityWithTokenRedirects() { - const { currencyIdA, currencyIdB } = useParams<{ currencyIdA: string; currencyIdB: string; feeAmount?: string }>() - - const { chainId } = useAccount() - - // prevent weth + eth - const isETHOrWETHA = - currencyIdA === 'ETH' || (chainId !== undefined && currencyIdA === WRAPPED_NATIVE_CURRENCY[chainId]?.address) - const isETHOrWETHB = - currencyIdB === 'ETH' || (chainId !== undefined && currencyIdB === WRAPPED_NATIVE_CURRENCY[chainId]?.address) - - if ( - currencyIdA && - currencyIdB && - (currencyIdA.toLowerCase() === currencyIdB.toLowerCase() || (isETHOrWETHA && isETHOrWETHB)) - ) { - return - } - return -} diff --git a/apps/web/src/pages/AddLiquidityV2/redirects.tsx b/apps/web/src/pages/AddLiquidityV2/redirects.tsx index cc62e8e7758..cbd9bcd18e3 100644 --- a/apps/web/src/pages/AddLiquidityV2/redirects.tsx +++ b/apps/web/src/pages/AddLiquidityV2/redirects.tsx @@ -1,9 +1,15 @@ import AddLiquidityV2 from 'pages/AddLiquidityV2/index' import { Navigate, useParams } from 'react-router-dom' +import { FeatureFlags } from 'uniswap/src/features/gating/flags' +import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' export default function AddLiquidityV2WithTokenRedirects() { + const isV4EverywhereEnabled = useFeatureFlag(FeatureFlags.V4Everywhere) const { currencyIdA, currencyIdB } = useParams<{ currencyIdA: string; currencyIdB: string }>() - + if (isV4EverywhereEnabled) { + // TODO(WEB-5361): update this to enable prefilling form from URL currencyIdA and currencyIdB + return + } if (currencyIdA && currencyIdB && currencyIdA.toLowerCase() === currencyIdB.toLowerCase()) { return } diff --git a/apps/web/src/pages/AddLiquidity/Review.tsx b/apps/web/src/pages/AddLiquidityV3/Review.tsx similarity index 100% rename from apps/web/src/pages/AddLiquidity/Review.tsx rename to apps/web/src/pages/AddLiquidityV3/Review.tsx diff --git a/apps/web/src/pages/AddLiquidity/blastAlerts.tsx b/apps/web/src/pages/AddLiquidityV3/blastAlerts.tsx similarity index 100% rename from apps/web/src/pages/AddLiquidity/blastAlerts.tsx rename to apps/web/src/pages/AddLiquidityV3/blastAlerts.tsx diff --git a/apps/web/src/pages/AddLiquidity/index.tsx b/apps/web/src/pages/AddLiquidityV3/index.tsx similarity index 98% rename from apps/web/src/pages/AddLiquidity/index.tsx rename to apps/web/src/pages/AddLiquidityV3/index.tsx index 4d2e00c077c..3601f2c132f 100644 --- a/apps/web/src/pages/AddLiquidity/index.tsx +++ b/apps/web/src/pages/AddLiquidityV3/index.tsx @@ -35,7 +35,6 @@ import { TokenTaxV3Warning } from 'components/addLiquidity/TokenTaxV3Warning' import { AutoColumn } from 'components/deprecated/Column' import Row, { RowBetween, RowFixed } from 'components/deprecated/Row' import UnsupportedCurrencyFooter from 'components/swap/UnsupportedCurrencyFooter' -import { isSupportedChainId, useIsSupportedChainId } from 'constants/chains' import { ZERO_PERCENT } from 'constants/misc' import { useCurrency } from 'hooks/Tokens' import { useAccount } from 'hooks/useAccount' @@ -54,8 +53,8 @@ import { useV3PositionFromTokenId } from 'hooks/useV3Positions' import { atomWithStorage, useAtomValue, useUpdateAtom } from 'jotai/utils' import { useSingleCallResult } from 'lib/hooks/multicall' import styled, { useTheme } from 'lib/styled-components' -import { Review } from 'pages/AddLiquidity/Review' -import { BlastRebasingAlert, BlastRebasingModal } from 'pages/AddLiquidity/blastAlerts' +import { Review } from 'pages/AddLiquidityV3/Review' +import { BlastRebasingAlert, BlastRebasingModal } from 'pages/AddLiquidityV3/blastAlerts' import { DynamicSection, MediumOnly, @@ -63,7 +62,7 @@ import { ScrollablePage, StyledInput, Wrapper, -} from 'pages/AddLiquidity/styled' +} from 'pages/AddLiquidityV3/styled' import { BodyWrapper } from 'pages/App/AppBody' import { PositionPageUnsupportedContent } from 'pages/LegacyPool/PositionPage' import { Dots } from 'pages/LegacyPool/styled' @@ -83,12 +82,13 @@ import { TransactionInfo, TransactionType } from 'state/transactions/types' import { useUserSlippageToleranceWithDefault } from 'state/user/hooks' import { ThemedText } from 'theme/components' import { Text } from 'ui/src' -import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' import { WRAPPED_NATIVE_CURRENCY } from 'uniswap/src/constants/tokens' +import { useIsSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { UniverseChainId, isUniverseChainId } from 'uniswap/src/features/chains/types' +import { getChainLabel } from 'uniswap/src/features/chains/utils' import Trace from 'uniswap/src/features/telemetry/Trace' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { Trans, t } from 'uniswap/src/i18n' -import { UniverseChainId } from 'uniswap/src/types/chains' import { logger } from 'utilities/src/logger/logger' import { useTrace } from 'utilities/src/telemetry/trace/TraceContext' import { addressesAreEquivalent } from 'utils/addressesAreEquivalent' @@ -674,9 +674,7 @@ function AddLiquidity() { quoteCurrency?.symbol && baseCurrency?.symbol ? `${quoteCurrency.symbol}/${baseCurrency.symbol}` : quoteCurrency?.symbol ?? baseCurrency?.symbol ?? 'pools', - chain: - UNIVERSE_CHAIN_INFO[isSupportedChainId(account.chainId) ? account.chainId : UniverseChainId.Mainnet] - .label, + chain: getChainLabel(isUniverseChainId(account.chainId) ? account.chainId : UniverseChainId.Mainnet), })} diff --git a/apps/web/src/pages/AddLiquidityV3/redirects.tsx b/apps/web/src/pages/AddLiquidityV3/redirects.tsx new file mode 100644 index 00000000000..3d6fa744754 --- /dev/null +++ b/apps/web/src/pages/AddLiquidityV3/redirects.tsx @@ -0,0 +1,34 @@ +import { checkIsNative } from 'hooks/Tokens' +import { useAccount } from 'hooks/useAccount' +import AddLiquidity from 'pages/AddLiquidityV3/index' +import { Navigate, useParams } from 'react-router-dom' +import { WRAPPED_NATIVE_CURRENCY } from 'uniswap/src/constants/tokens' +import { FeatureFlags } from 'uniswap/src/features/gating/flags' +import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' + +export default function AddLiquidityV3WithTokenRedirects() { + const isV4EverywhereEnabled = useFeatureFlag(FeatureFlags.V4Everywhere) + const { currencyIdA, currencyIdB } = useParams<{ currencyIdA: string; currencyIdB: string; feeAmount?: string }>() + + const { chainId } = useAccount() + + if (isV4EverywhereEnabled) { + // TODO(WEB-5361): update this to enable prefilling form from URL currencyIdA and currencyIdB + return + } + + // prevent weth + eth + const isETHOrWETHA = + checkIsNative(currencyIdA) || (chainId !== undefined && currencyIdA === WRAPPED_NATIVE_CURRENCY[chainId]?.address) + const isETHOrWETHB = + checkIsNative(currencyIdB) || (chainId !== undefined && currencyIdB === WRAPPED_NATIVE_CURRENCY[chainId]?.address) + + if ( + currencyIdA && + currencyIdB && + (currencyIdA.toLowerCase() === currencyIdB.toLowerCase() || (isETHOrWETHA && isETHOrWETHB)) + ) { + return + } + return +} diff --git a/apps/web/src/pages/AddLiquidity/styled.tsx b/apps/web/src/pages/AddLiquidityV3/styled.tsx similarity index 100% rename from apps/web/src/pages/AddLiquidity/styled.tsx rename to apps/web/src/pages/AddLiquidityV3/styled.tsx diff --git a/apps/web/src/pages/Explore/charts/ExploreChartsSection.tsx b/apps/web/src/pages/Explore/charts/ExploreChartsSection.tsx index 0a4756e2a62..16d1db78414 100644 --- a/apps/web/src/pages/Explore/charts/ExploreChartsSection.tsx +++ b/apps/web/src/pages/Explore/charts/ExploreChartsSection.tsx @@ -9,9 +9,7 @@ import { getCumulativeSum, getCumulativeVolume, getVolumeProtocolInfo } from 'co import { ChartType } from 'components/Charts/utils' import { DataQuality } from 'components/Tokens/TokenDetails/ChartSection/util' import { MAX_WIDTH_MEDIA_BREAKPOINT } from 'components/Tokens/constants' -import { chainIdToBackendChain, useChainFromUrlParam } from 'constants/chains' -import { useDailyProtocolTVL, useHistoricalProtocolVolume } from 'graphql/data/protocolStats' -import { TimePeriod, getProtocolColor, getProtocolGradient, getSupportedGraphQlChain } from 'graphql/data/util' +import { TimePeriod, getProtocolColor, getProtocolGradient } from 'graphql/data/util' import { useScreenSize } from 'hooks/screenSize/useScreenSize' import { useAtomValue } from 'jotai/utils' import { useTheme } from 'lib/styled-components' @@ -24,13 +22,13 @@ import { EllipsisTamaguiStyle } from 'theme/components' import { Flex, SegmentedControl, Text, styled } from 'ui/src' import { HistoryDuration, PriceSource } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag, useFeatureFlagWithLoading } from 'uniswap/src/features/gating/hooks' +import { useFeatureFlagWithLoading } from 'uniswap/src/features/gating/hooks' import { Trans } from 'uniswap/src/i18n' -import { UniverseChainId } from 'uniswap/src/types/chains' import { NumberType, useFormatter } from 'utils/formatNumbers' const EXPLORE_CHART_HEIGHT_PX = 368 -const EXPLORE_PRICE_SOURCES = [PriceSource.SubgraphV2, PriceSource.SubgraphV3] +const EXPLORE_PRICE_SOURCES_V3 = [PriceSource.SubgraphV2, PriceSource.SubgraphV3] +const EXPLORE_PRICE_SOURCES_V4 = [PriceSource.SubgraphV2, PriceSource.SubgraphV3, PriceSource.SubgraphV4] const TIME_SELECTOR_OPTIONS = [{ value: TimePeriod.DAY }, { value: TimePeriod.WEEK }, { value: TimePeriod.MONTH }] @@ -70,14 +68,15 @@ const SectionTitle = styled(Text, { lineHeight: 24, }) -function VolumeChartSection({ chainId }: { chainId: UniverseChainId }) { +function VolumeChartSection() { const [timePeriod, setTimePeriod] = useState(TimePeriod.DAY) const theme = useTheme() const isSmallScreen = !useScreenSize()['sm'] - const { value: isMultichainExploreEnabledLoaded, isLoading: isMultichainExploreLoading } = useFeatureFlagWithLoading( - FeatureFlags.MultichainExplore, + const { value: isV4EverywhereEnabledLoaded, isLoading: isV4EverywhereLoading } = useFeatureFlagWithLoading( + FeatureFlags.V4Everywhere, ) - const isMultichainExploreEnabled = isMultichainExploreEnabledLoaded || isMultichainExploreLoading + const isV4EverywhereEnabled = isV4EverywhereEnabledLoaded || isV4EverywhereLoading + const EXPLORE_PRICE_SOURCES = isV4EverywhereEnabled ? EXPLORE_PRICE_SOURCES_V4 : EXPLORE_PRICE_SOURCES_V3 const refitChartContent = useAtomValue(refitChartContentAtom) function timeGranularityToHistoryDuration(timePeriod: TimePeriod): HistoryDuration { @@ -94,43 +93,36 @@ function VolumeChartSection({ chainId }: { chainId: UniverseChainId }) { } } - const { - entries: gqlEntries, - loading: gqlLoading, - dataQuality: gqlDataQuality, - } = useHistoricalProtocolVolume( - chainIdToBackendChain({ chainId, withFallback: true }), + const { entries, loading, dataQuality } = useRestHistoricalProtocolVolume( isSmallScreen ? HistoryDuration.Month : timeGranularityToHistoryDuration(timePeriod), ) - const { - entries: restEntries, - loading: restLoading, - dataQuality: restDataQuality, - } = useRestHistoricalProtocolVolume( - isSmallScreen ? HistoryDuration.Month : timeGranularityToHistoryDuration(timePeriod), + const protocolColors = useMemo( + () => + isV4EverywhereEnabled + ? [ + getProtocolColor(PriceSource.SubgraphV4, theme), + getProtocolColor(PriceSource.SubgraphV3, theme), + getProtocolColor(PriceSource.SubgraphV2, theme), + ] + : [getProtocolColor(PriceSource.SubgraphV3, theme), getProtocolColor(PriceSource.SubgraphV2, theme)], + [isV4EverywhereEnabled, theme], ) - const isRestExploreEnabled = useFeatureFlag(FeatureFlags.RestExplore) - const { entries, loading, dataQuality } = isRestExploreEnabled - ? { entries: restEntries, loading: restLoading, dataQuality: restDataQuality } - : { entries: gqlEntries, loading: gqlLoading, dataQuality: gqlDataQuality } const params = useMemo<{ data: StackedHistogramData[] - colors: [string, string] + colors: string[] useThinCrosshair: boolean headerHeight: number - isMultichainExploreEnabled: boolean background: string }>( () => ({ data: entries, - colors: [theme.accent1, theme.accent3], - headerHeight: isMultichainExploreEnabled ? 0 : 80, + colors: protocolColors, + headerHeight: 0, stale: dataQuality === DataQuality.STALE, - useThinCrosshair: isMultichainExploreEnabled, - isMultichainExploreEnabled, + useThinCrosshair: true, background: theme.background, }), - [entries, theme.accent1, theme.accent3, theme.background, isMultichainExploreEnabled, dataQuality], + [entries, protocolColors, dataQuality, theme.background], ) const cumulativeVolume = useMemo(() => getCumulativeVolume(entries), [entries]) @@ -194,30 +186,23 @@ function VolumeChartSection({ chainId }: { chainId: UniverseChainId }) { ) } -function TVLChartSection({ chainId }: { chainId: UniverseChainId }) { +function TVLChartSection() { const theme = useTheme() - const isMultichainExploreEnabled = useFeatureFlag(FeatureFlags.MultichainExplore) - const { - entries: gqlEntries, - loading: gqlLoading, - dataQuality: gqlDataQuality, - } = useDailyProtocolTVL(chainIdToBackendChain({ chainId })) - const { entries: restEntries, loading: restLoading, dataQuality: restDataQuality } = useRestDailyProtocolTVL() - const isRestExploreEnabled = useFeatureFlag(FeatureFlags.RestExplore) - const { entries, loading, dataQuality } = isRestExploreEnabled - ? { entries: restEntries, loading: restLoading, dataQuality: restDataQuality } - : { entries: gqlEntries, loading: gqlLoading, dataQuality: gqlDataQuality } + const { value: isV4EverywhereEnabledLoaded, isLoading: isV4EverywhereLoading } = useFeatureFlagWithLoading( + FeatureFlags.V4Everywhere, + ) + const isV4EverywhereEnabled = isV4EverywhereEnabledLoaded || isV4EverywhereLoading + const EXPLORE_PRICE_SOURCES = isV4EverywhereEnabled ? EXPLORE_PRICE_SOURCES_V4 : EXPLORE_PRICE_SOURCES_V3 + const { entries, loading, dataQuality } = useRestDailyProtocolTVL() const lastEntry = entries[entries.length - 1] const params = useMemo( () => ({ data: entries, colors: EXPLORE_PRICE_SOURCES?.map((source) => getProtocolColor(source, theme)) ?? [theme.accent1], - gradients: isMultichainExploreEnabled - ? EXPLORE_PRICE_SOURCES?.map((source) => getProtocolGradient(source)) - : undefined, + gradients: EXPLORE_PRICE_SOURCES?.map((source) => getProtocolGradient(source)), }), - [entries, isMultichainExploreEnabled, theme], + [EXPLORE_PRICE_SOURCES, entries, theme], ) const isSmallScreen = !useScreenSize()['sm'] @@ -273,12 +258,10 @@ function MinimalStatDisplay({ title, value, time }: { title: ReactNode; value: n } export function ExploreChartsSection() { - const chain = getSupportedGraphQlChain(useChainFromUrlParam(), { fallbackToEthereum: true }) - return ( - - + + ) } diff --git a/apps/web/src/pages/Explore/index.tsx b/apps/web/src/pages/Explore/index.tsx index 918e5f69447..7aa1b82146d 100644 --- a/apps/web/src/pages/Explore/index.tsx +++ b/apps/web/src/pages/Explore/index.tsx @@ -6,9 +6,8 @@ import SearchBar from 'components/Tokens/TokenTable/SearchBar' import VolumeTimeFrameSelector from 'components/Tokens/TokenTable/VolumeTimeFrameSelector' import { MAX_WIDTH_MEDIA_BREAKPOINT } from 'components/Tokens/constants' import { MouseoverTooltip, TooltipSize } from 'components/Tooltip' -import { useChainFromUrlParam } from 'constants/chains' import { manualChainOutageAtom } from 'featureFlags/flags/outageBanner' -import { getTokenExploreURL, isBackendSupportedChain } from 'graphql/data/util' +import { getTokenExploreURL } from 'graphql/data/util' import { useOnGlobalChainSwitch } from 'hooks/useGlobalChainSwitch' import { useResetAtom } from 'jotai/utils' import { ExploreChartsSection } from 'pages/Explore/charts/ExploreChartsSection' @@ -18,13 +17,15 @@ import { NamedExoticComponent, useCallback, useEffect, useMemo, useRef, useState import { useNavigate } from 'react-router-dom' import { ExploreContextProvider } from 'state/explore' import { TamaguiClickableStyle } from 'theme/components' -import { Flex, Text, styled as tamaguiStyled } from 'ui/src' -import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' +import { Button, Flex, Text, styled as tamaguiStyled } from 'ui/src' +import { Plus } from 'ui/src/components/icons/Plus' +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' +import { isBackendSupportedChain } from 'uniswap/src/features/chains/utils' import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import Trace from 'uniswap/src/features/telemetry/Trace' -import { Trans } from 'uniswap/src/i18n' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { Trans, t } from 'uniswap/src/i18n' +import { useChainIdFromUrlParam } from 'utils/chainParams' export enum ExploreTab { Tokens = 'tokens', @@ -62,8 +63,7 @@ const Pages: Array = [ const HeaderTab = tamaguiStyled(Text, { ...TamaguiClickableStyle, - fontSize: 28, - fontWeight: '$book', + variant: 'heading3', userSelect: 'none', color: '$neutral2', variants: { @@ -93,7 +93,7 @@ const HeaderTab = tamaguiStyled(Text, { const Explore = ({ initialTab }: { initialTab?: ExploreTab }) => { const tabNavRef = useRef(null) const resetManualOutage = useResetAtom(manualChainOutageAtom) - const isMultichainExploreEnabled = useFeatureFlag(FeatureFlags.MultichainExplore) + const v4EverywhereEnabled = useFeatureFlag(FeatureFlags.V4Everywhere) const initialKey: number = useMemo(() => { const key = initialTab && Pages.findIndex((page) => page.key === initialTab) @@ -109,9 +109,7 @@ const Explore = ({ initialTab }: { initialTab?: ExploreTab }) => { const offsetTop = tabNavRef.current.getBoundingClientRect().top + window.scrollY window.scrollTo({ top: offsetTop - 90, behavior: 'smooth' }) } - // scroll to tab navbar on initial page mount only - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) + }, [initialTab]) const [currentTab, setCurrentTab] = useState(initialKey) @@ -119,12 +117,10 @@ const Explore = ({ initialTab }: { initialTab?: ExploreTab }) => { const { tab: tabName } = useExploreParams() const tab = tabName ?? ExploreTab.Tokens - const chainWithoutFallback = useChainFromUrlParam() - const chain = useMemo(() => { - return isMultichainExploreEnabled - ? chainWithoutFallback - : chainWithoutFallback ?? UNIVERSE_CHAIN_INFO[UniverseChainId.Mainnet] - }, [chainWithoutFallback, isMultichainExploreEnabled]) + const urlChainId = useChainIdFromUrlParam() + const chainInfo = useMemo(() => { + return urlChainId ? getChainInfo(urlChainId) : undefined + }, [urlChainId]) useEffect(() => { const tabIndex = Pages.findIndex((page) => page.key === tab) if (tabIndex !== -1) { @@ -149,8 +145,12 @@ const Explore = ({ initialTab }: { initialTab?: ExploreTab }) => { ) return ( - - + + { > {Pages.map(({ title, loggingElementName, key }, index) => { // disable Transactions tab if no chain is selected - const disabled = isMultichainExploreEnabled && key === ExploreTab.Transactions && !chain - const url = getTokenExploreURL({ tab: key, chain: chain?.backendChain.chain }) + const disabled = key === ExploreTab.Transactions && !chainInfo return ( { disabled={!disabled} > !disabled && navigate(url)} + onPress={() => { + // Update tab and only replace url when navigating from the header nav + !disabled && setCurrentTab(index) + }} active={currentTab === index} disabled={disabled} key={key} @@ -211,6 +213,22 @@ const Explore = ({ initialTab }: { initialTab?: ExploreTab }) => { })} + {currentKey === ExploreTab.Pools && v4EverywhereEnabled && ( + + )} {currentKey === ExploreTab.Tokens && } {currentKey !== ExploreTab.Transactions && } diff --git a/apps/web/src/pages/Explore/tables/RecentTransactions.tsx b/apps/web/src/pages/Explore/tables/RecentTransactions.tsx index 24bf84000ca..67536218825 100644 --- a/apps/web/src/pages/Explore/tables/RecentTransactions.tsx +++ b/apps/web/src/pages/Explore/tables/RecentTransactions.tsx @@ -11,20 +11,22 @@ import { TimestampCell, TokenLinkCell, } from 'components/Table/styled' -import { useChainFromUrlParam } from 'constants/chains' import { useUpdateManualOutage } from 'featureFlags/flags/outageBanner' import { BETypeToTransactionType, TransactionType, useAllTransactions } from 'graphql/data/useAllTransactions' -import { OrderDirection, getSupportedGraphQlChain } from 'graphql/data/util' -import { useActiveLocalCurrency } from 'hooks/useActiveLocalCurrency' +import { OrderDirection } from 'graphql/data/util' import { memo, useMemo, useReducer, useRef, useState } from 'react' import { Flex, Text, styled } from 'ui/src' import { PoolTransaction, PoolTransactionType, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { useAppFiatCurrency } from 'uniswap/src/features/fiatCurrency/hooks' import { Trans } from 'uniswap/src/i18n' import { ExplorerDataType, getExplorerLink } from 'uniswap/src/utils/linking' import { shortenAddress } from 'utilities/src/addresses' +import { useChainIdFromUrlParam } from 'utils/chainParams' import { useFormatter } from 'utils/formatNumbers' const TableRow = styled(Flex, { @@ -34,7 +36,7 @@ const TableRow = styled(Flex, { }) const RecentTransactions = memo(function RecentTransactions() { - const activeLocalCurrency = useActiveLocalCurrency() + const activeLocalCurrency = useAppFiatCurrency() const { formatNumber, formatFiatPrice } = useFormatter() const [filterModalIsOpen, toggleFilterModal] = useReducer((s) => !s, false) const filterAnchorRef = useRef(null) @@ -43,16 +45,16 @@ const RecentTransactions = memo(function RecentTransactions() { TransactionType.REMOVE, TransactionType.ADD, ]) - const chain = getSupportedGraphQlChain(useChainFromUrlParam(), { fallbackToEthereum: true }) + const chainInfo = getChainInfo(useChainIdFromUrlParam() ?? UniverseChainId.Mainnet) - const { transactions, loading, loadMore, errorV2, errorV3 } = useAllTransactions(chain.backendChain.chain, filter) + const { transactions, loading, loadMore, errorV2, errorV3 } = useAllTransactions(chainInfo.backendChain.chain, filter) const combinedError = errorV2 && errorV3 - ? new ApolloError({ errorMessage: `Could not retrieve V2 and V3 Transactions for chain: ${chain.id}` }) + ? new ApolloError({ errorMessage: `Could not retrieve V2 and V3 Transactions for chain: ${chainInfo.id}` }) : undefined const allDataStillLoading = loading && !transactions.length const showLoadingSkeleton = allDataStillLoading || !!combinedError - useUpdateManualOutage({ chainId: chain.id, errorV3, errorV2 }) + useUpdateManualOutage({ chainId: chainInfo.id, errorV3, errorV2 }) // TODO(WEB-3236): once GQL BE Transaction query is supported add usd, token0 amount, and token1 amount sort support const columns = useMemo(() => { const columnHelper = createColumnHelper() @@ -73,7 +75,7 @@ const RecentTransactions = memo(function RecentTransactions() { ), @@ -205,14 +207,16 @@ const RecentTransactions = memo(function RecentTransactions() { ), cell: (makerAddress) => ( - + {shortenAddress(makerAddress.getValue?.())} ), }), ] - }, [activeLocalCurrency, chain.id, filter, filterModalIsOpen, formatFiatPrice, formatNumber, showLoadingSkeleton]) + }, [activeLocalCurrency, chainInfo.id, filter, filterModalIsOpen, formatFiatPrice, formatNumber, showLoadingSkeleton]) return ( { - setAddLiquidityState((prev) => ({ + setIncreaseLiquidityState((prev) => ({ ...prev, exactField: field, exactAmount: newValue, @@ -40,7 +44,7 @@ export function IncreaseLiquidityForm() { } const handleOnSetMax = (field: PositionField, amount: string) => { - setAddLiquidityState((prev) => ({ + setIncreaseLiquidityState((prev) => ({ ...prev, exactField: field, exactAmount: amount, @@ -48,13 +52,18 @@ export function IncreaseLiquidityForm() { } // TODO(WEB-4978): account for gas in this calculation once we have the gasfee + const insufficientToken0Balance = + currencyBalances?.TOKEN0 && currencyAmounts?.TOKEN0?.greaterThan(currencyBalances.TOKEN0) + const insufficientToken1Balance = + currencyBalances?.TOKEN1 && currencyAmounts?.TOKEN1?.greaterThan(currencyBalances.TOKEN1) + const disableContinue = !currencyAmounts?.TOKEN0 || !currencyBalances?.TOKEN0 || - currencyAmounts.TOKEN0.greaterThan(currencyBalances.TOKEN0) || + insufficientToken0Balance || !currencyAmounts?.TOKEN1 || !currencyBalances.TOKEN1 || - currencyAmounts?.TOKEN1?.greaterThan(currencyBalances.TOKEN1) + insufficientToken1Balance const handleOnContinue = () => { if (!disableContinue) { @@ -62,6 +71,15 @@ export function IncreaseLiquidityForm() { } } + const errorText = + insufficientToken0Balance && insufficientToken1Balance + ? t('common.insufficientBalance.error') + : insufficientToken0Balance || insufficientToken1Balance + ? t('common.insufficientTokenBalance.error', { + tokenSymbol: insufficientToken0Balance ? token0.symbol : token1.symbol, + }) + : undefined + return ( <> @@ -75,6 +93,8 @@ export function IncreaseLiquidityForm() { currencyBalances={currencyBalances} onUserInput={handleUserInput} onSetMax={handleOnSetMax} + deposit0Disabled={deposit0Disabled} + deposit1Disabled={deposit1Disabled} /> - + + {errorText || t('common.add.label')} + ) } diff --git a/apps/web/src/pages/Landing/assets/approvedTokens.ts b/apps/web/src/pages/Landing/assets/approvedTokens.ts index cd9332b988b..305b6e182f5 100644 --- a/apps/web/src/pages/Landing/assets/approvedTokens.ts +++ b/apps/web/src/pages/Landing/assets/approvedTokens.ts @@ -88,13 +88,14 @@ export const approvedERC20: InteractiveToken[] = [ 'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x0F5D2fB29fb7d3CFeE444a200298f468908cC942/logo.png', }, { - name: 'Matic', - symbol: 'MATIC', + name: 'Polygon', + symbol: 'POL', address: '0x0000000000000000000000000000000000001010', chain: Chain.Polygon, standard: TokenStandard.ERC20, color: '#833ADD', - logoUrl: 'https://app.uniswap.org/static/media/matic-token-icon.efed2ee4e843195b44bf68ffc7439403.svg', + logoUrl: + 'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/polygon/assets/0x0000000000000000000000000000000000001010/logo.png', }, { name: 'Moss Carbon Credit', diff --git a/apps/web/src/pages/Landing/components/cards/WebappCard.tsx b/apps/web/src/pages/Landing/components/cards/WebappCard.tsx index 00e1e03dc41..c1ada6ae257 100644 --- a/apps/web/src/pages/Landing/components/cards/WebappCard.tsx +++ b/apps/web/src/pages/Landing/components/cards/WebappCard.tsx @@ -1,6 +1,5 @@ import { PortfolioLogo } from 'components/AccountDrawer/MiniPortfolio/PortfolioLogo' import { DeltaArrow } from 'components/Tokens/TokenDetails/Delta' -import { chainIdToBackendChain } from 'constants/chains' import { NATIVE_CHAIN_ID } from 'constants/tokens' import { getTokenDetailsURL } from 'graphql/data/util' import { useCurrency } from 'hooks/Tokens' @@ -12,8 +11,9 @@ import { useNavigate } from 'react-router-dom' import { Flex, Text } from 'ui/src' import { LDO, UNI, USDC_BASE } from 'uniswap/src/constants/tokens' import { useTokenPromoQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { toGraphQLChain } from 'uniswap/src/features/chains/utils' import { t } from 'uniswap/src/i18n' -import { UniverseChainId } from 'uniswap/src/types/chains' import { NumberType, useFormatter } from 'utils/formatNumbers' const primary = '#2ABDFF' @@ -45,7 +45,7 @@ function Token({ chainId, address }: { chainId: UniverseChainId; address: string const tokenPromoQuery = useTokenPromoQuery({ variables: { address: currency?.wrapped.address, - chain: chainIdToBackendChain({ chainId }), + chain: toGraphQLChain(chainId), }, }) const price = tokenPromoQuery.data?.token?.market?.price?.value ?? 0 @@ -68,7 +68,7 @@ function Token({ chainId, address }: { chainId: UniverseChainId; address: string navigate( getTokenDetailsURL({ address: address === 'ETH' ? NATIVE_CHAIN_ID : address, - chain: chainIdToBackendChain({ chainId }), + chain: toGraphQLChain(chainId), }), ) }} diff --git a/apps/web/src/pages/Landing/index.tsx b/apps/web/src/pages/Landing/index.tsx index e04641f1b50..2dd11f913e5 100644 --- a/apps/web/src/pages/Landing/index.tsx +++ b/apps/web/src/pages/Landing/index.tsx @@ -25,8 +25,20 @@ export default function Landing() { const accountDrawer = useAccountDrawer() const prevAccount = usePrevious(account.address) const redirectOnConnect = useRef(false) + + const isInitialRender = useRef(true) + // Smoothly redirect to swap page if user connects while on landing page useEffect(() => { + // Skip logic on the first render because prevAccount will always be undefined on the first render + // and we don't want to redirect on the first render because that mean's we're possibly coming from + // another page in the app. + // We need to wait until future renders to check if the user connected while on the landing page. + if (isInitialRender.current) { + isInitialRender.current = false + return undefined + } + if (accountDrawer.isOpen && account.address && !prevAccount) { redirectOnConnect.current = true setTransition(true) diff --git a/apps/web/src/pages/Landing/sections/Hero.tsx b/apps/web/src/pages/Landing/sections/Hero.tsx index 5343500d8cf..6c6d85016fa 100644 --- a/apps/web/src/pages/Landing/sections/Hero.tsx +++ b/apps/web/src/pages/Landing/sections/Hero.tsx @@ -9,10 +9,10 @@ import { ChevronDown } from 'react-feather' import { useNavigate } from 'react-router-dom' import { serializeSwapStateToURLParameters } from 'state/swap/hooks' import { Flex, Text } from 'ui/src' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { SwapRedirectFn } from 'uniswap/src/features/transactions/TransactionModal/TransactionModalContext' import { Trans, useTranslation } from 'uniswap/src/i18n' import { INTERFACE_NAV_HEIGHT } from 'uniswap/src/theme/heights' -import { UniverseChainId } from 'uniswap/src/types/chains' interface HeroProps { scrollToRef: () => void diff --git a/apps/web/src/pages/LegacyPool/CTACards.tsx b/apps/web/src/pages/LegacyPool/CTACards.tsx index faf24bec1c5..d73bc91475b 100644 --- a/apps/web/src/pages/LegacyPool/CTACards.tsx +++ b/apps/web/src/pages/LegacyPool/CTACards.tsx @@ -1,11 +1,11 @@ import { AutoColumn } from 'components/deprecated/Column' -import { useSupportedChainId } from 'constants/chains' import { useAccount } from 'hooks/useAccount' import styled, { css } from 'lib/styled-components' import { ExternalLink, StyledInternalLink, ThemedText } from 'theme/components' -import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' +import { useSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { Trans } from 'uniswap/src/i18n' -import { UniverseChainId } from 'uniswap/src/types/chains' const CTASection = styled.section` display: grid; @@ -71,7 +71,7 @@ const ResponsiveColumn = styled(AutoColumn)` export default function CTACards() { const { chainId } = useAccount() - const chain = UNIVERSE_CHAIN_INFO[useSupportedChainId(chainId) ?? UniverseChainId.Mainnet] + const chain = getChainInfo(useSupportedChainId(chainId) ?? UniverseChainId.Mainnet) return ( diff --git a/apps/web/src/pages/LegacyPool/PositionPage.tsx b/apps/web/src/pages/LegacyPool/PositionPage.tsx index 4e8ef046bb3..50b90911bbf 100644 --- a/apps/web/src/pages/LegacyPool/PositionPage.tsx +++ b/apps/web/src/pages/LegacyPool/PositionPage.tsx @@ -18,7 +18,6 @@ import TransactionConfirmationModal, { ConfirmationModalContent } from 'componen import { AutoColumn } from 'components/deprecated/Column' import { RowBetween, RowFixed } from 'components/deprecated/Row' import { Dots } from 'components/swap/styled' -import { chainIdToBackendChain, useIsSupportedChainId, useSupportedChainId } from 'constants/chains' import { getPoolDetailsURL, getTokenDetailsURL, isGqlSupportedChain } from 'graphql/data/util' import { useToken } from 'hooks/Tokens' import { useAccount } from 'hooks/useAccount' @@ -42,10 +41,12 @@ import { useIsTransactionPending, useTransactionAdder } from 'state/transactions import { TransactionType } from 'state/transactions/types' import { ClickableStyle, ExternalLink, HideExtraSmall, HideSmall, StyledRouterLink, ThemedText } from 'theme/components' import { Switch, Text } from 'ui/src' +import { useEnabledChains, useIsSupportedChainId, useSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { toGraphQLChain } from 'uniswap/src/features/chains/utils' import Trace from 'uniswap/src/features/telemetry/Trace' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { Trans, t } from 'uniswap/src/i18n' -import { UniverseChainId } from 'uniswap/src/types/chains' import { ExplorerDataType, getExplorerLink } from 'uniswap/src/utils/linking' import { logger } from 'utilities/src/logger/logger' import { calculateGasMargin } from 'utils/calculateGasMargin' @@ -204,7 +205,7 @@ const TokenLink = ({ chainId, address, }: PropsWithChildren<{ chainId: UniverseChainId; address: string }>) => { - const tokenLink = getTokenDetailsURL({ address, chain: chainIdToBackendChain({ chainId }) }) + const tokenLink = getTokenDetailsURL({ address, chain: toGraphQLChain(chainId) }) return {children} } @@ -333,6 +334,8 @@ function PositionPageContent() { const theme = useTheme() const { formatCurrencyAmount, formatDelta, formatTickPrice } = useFormatter() + const { defaultChainId } = useEnabledChains() + const parsedTokenId = parseTokenId(tokenIdFromUrl) const { loading, position: positionDetails } = useV3PositionFromTokenId(parsedTokenId) @@ -653,10 +656,7 @@ function PositionPageContent() { diff --git a/apps/web/src/pages/LegacyPool/index.test.tsx b/apps/web/src/pages/LegacyPool/index.test.tsx index 0a385491591..9ee92ace678 100644 --- a/apps/web/src/pages/LegacyPool/index.test.tsx +++ b/apps/web/src/pages/LegacyPool/index.test.tsx @@ -1,15 +1,31 @@ -import { useIsSupportedChainId } from 'constants/chains' import { useFilterPossiblyMaliciousPositions } from 'hooks/useFilterPossiblyMaliciousPositions' import { useV3Positions } from 'hooks/useV3Positions' import Pool from 'pages/LegacyPool' import { mocked } from 'test-utils/mocked' import { render, screen } from 'test-utils/render' +import { useEnabledChains, useIsSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' -jest.mock('constants/chains') +jest.mock('uniswap/src/features/chains/hooks', () => ({ + useEnabledChains: jest.fn(), + useSupportedChainId: jest.fn(), + useSupportedChainIdWithConnector: jest.fn(), + useIsSupportedChainId: jest.fn(), +})) jest.mock('hooks/useV3Positions') jest.mock('hooks/useFilterPossiblyMaliciousPositions') describe('networks', () => { + beforeEach(() => { + mocked(useEnabledChains).mockReturnValue({ + isTestnetModeEnabled: false, + chains: [], + gqlChains: [], + defaultChainId: UniverseChainId.Mainnet, + }) + mocked(useIsSupportedChainId).mockReturnValue(true) + }) + it('renders error card when unsupported chain is selected', async () => { mocked(useIsSupportedChainId).mockReturnValue(false) mocked(useV3Positions).mockReturnValue({ loading: false, positions: undefined }) diff --git a/apps/web/src/pages/LegacyPool/index.tsx b/apps/web/src/pages/LegacyPool/index.tsx index 83948e0edec..bc06c770b61 100644 --- a/apps/web/src/pages/LegacyPool/index.tsx +++ b/apps/web/src/pages/LegacyPool/index.tsx @@ -1,11 +1,10 @@ import { InterfaceElementName, InterfaceEventName, InterfacePageName } from '@uniswap/analytics-events' import { useAccountDrawer } from 'components/AccountDrawer/MiniPortfolio/hooks' import { ButtonPrimary, ButtonText } from 'components/Button/buttons' -import { AutoColumn } from 'components/deprecated/Column' import { DropdownSelector } from 'components/DropdownSelector' import PositionList from 'components/PositionList' import { SwitchLocaleLink } from 'components/SwitchLocaleLink' -import { useIsSupportedChainId } from 'constants/chains' +import { AutoColumn } from 'components/deprecated/Column' import { useAccount } from 'hooks/useAccount' import { useFilterPossiblyMaliciousPositions } from 'hooks/useFilterPossiblyMaliciousPositions' import { useNetworkSupportsV2 } from 'hooks/useNetworkSupportsV2' @@ -20,8 +19,9 @@ import { Link } from 'react-router-dom' import { useUserHideClosedPositions } from 'state/user/hooks' import { HideSmall, ThemedText } from 'theme/components' import { PositionDetails } from 'types/position' -import { Anchor, Flex, styled, Text } from 'ui/src' +import { Anchor, Flex, Text, styled } from 'ui/src' import { ProtocolVersion } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { useIsSupportedChainId } from 'uniswap/src/features/chains/hooks' import Trace from 'uniswap/src/features/telemetry/Trace' import { t, useTranslation } from 'uniswap/src/i18n' diff --git a/apps/web/src/pages/LegacyPool/redirects.tsx b/apps/web/src/pages/LegacyPool/redirects.tsx new file mode 100644 index 00000000000..facfe3f1e87 --- /dev/null +++ b/apps/web/src/pages/LegacyPool/redirects.tsx @@ -0,0 +1,58 @@ +import LegacyPool from 'pages/LegacyPool' +import LegacyPositionPage from 'pages/LegacyPool/PositionPage' +import LegacyPoolV2 from 'pages/LegacyPool/v2' +import PoolFinder from 'pages/PoolFinder' +import { Navigate, useParams, useSearchParams } from 'react-router-dom' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { toGraphQLChain } from 'uniswap/src/features/chains/utils' +import { FeatureFlags } from 'uniswap/src/features/gating/flags' +import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' +import { searchParamToBackendName } from 'utils/chainParams' +import { useAccount } from 'wagmi' + +// /pool +export function LegacyPoolRedirects() { + const isV4EverywhereEnabled = useFeatureFlag(FeatureFlags.V4Everywhere) + + if (isV4EverywhereEnabled) { + return + } + return +} + +// /pool/v2 +export function LegacyPoolV2Redirects() { + const isV4EverywhereEnabled = useFeatureFlag(FeatureFlags.V4Everywhere) + + if (isV4EverywhereEnabled) { + return + } + return +} + +// /pool/v2/find +export function PoolFinderRedirects() { + const isV4EverywhereEnabled = useFeatureFlag(FeatureFlags.V4Everywhere) + + if (isV4EverywhereEnabled) { + return + } + return +} + +// /pool/:tokenId?chain=... +export function LegacyPositionPageRedirects() { + const isV4EverywhereEnabled = useFeatureFlag(FeatureFlags.V4Everywhere) + const { tokenId } = useParams<{ tokenId: string }>() + const [searchParams] = useSearchParams() + const { chainId: connectedChainId } = useAccount() + const { defaultChainId } = useEnabledChains() + + if (isV4EverywhereEnabled) { + const chainName = + searchParamToBackendName(searchParams.get('chain'))?.toLowerCase() ?? + toGraphQLChain(connectedChainId ?? defaultChainId).toLowerCase() + return + } + return +} diff --git a/apps/web/src/pages/MigrateV2/MigrateV2Pair.tsx b/apps/web/src/pages/MigrateV2/MigrateV2Pair.tsx index 1087d6d1045..2e264cbf7c4 100644 --- a/apps/web/src/pages/MigrateV2/MigrateV2Pair.tsx +++ b/apps/web/src/pages/MigrateV2/MigrateV2Pair.tsx @@ -44,9 +44,9 @@ import { useUserSlippageToleranceWithDefault } from 'state/user/hooks' import { BackArrowLink, ExternalLink, ThemedText } from 'theme/components' import { Text } from 'ui/src' import { WRAPPED_NATIVE_CURRENCY } from 'uniswap/src/constants/tokens' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { Trans, t } from 'uniswap/src/i18n' -import { UniverseChainId } from 'uniswap/src/types/chains' import { ExplorerDataType, getExplorerLink } from 'uniswap/src/utils/linking' import { isAddress } from 'utilities/src/addresses' import { logger } from 'utilities/src/logger/logger' diff --git a/apps/web/src/pages/MigrateV3/index.tsx b/apps/web/src/pages/MigrateV3/index.tsx index 5d459e15557..6659ce54a9f 100644 --- a/apps/web/src/pages/MigrateV3/index.tsx +++ b/apps/web/src/pages/MigrateV3/index.tsx @@ -8,7 +8,6 @@ import { PositionInfo } from 'components/Liquidity/types' import { parseRestPosition } from 'components/Liquidity/utils' import { LoadingRows } from 'components/Loader/styled' import { PoolProgressIndicator } from 'components/PoolProgressIndicator/PoolProgressIndicator' -import { useChainFromUrlParam } from 'constants/chains' import useSelectChain from 'hooks/useSelectChain' import { MigrateV3PositionTxContextProvider, useMigrateV3TxContext } from 'pages/MigrateV3/MigrateV3LiquidityTxContext' import { @@ -16,11 +15,17 @@ import { DepositContextProvider, PriceRangeContextProvider, } from 'pages/Pool/Positions/create/ContextProviders' -import { useCreatePositionContext } from 'pages/Pool/Positions/create/CreatePositionContext' +import { + DEFAULT_DEPOSIT_STATE, + DEFAULT_PRICE_RANGE_STATE_POOL_EXISTS, + useCreatePositionContext, + useDepositContext, + usePriceRangeContext, +} from 'pages/Pool/Positions/create/CreatePositionContext' import { EditSelectTokensStep } from 'pages/Pool/Positions/create/EditStep' import { SelectPriceRangeStep } from 'pages/Pool/Positions/create/RangeSelectionStep' import { SelectTokensStep } from 'pages/Pool/Positions/create/SelectTokenStep' -import { PositionFlowStep } from 'pages/Pool/Positions/create/types' +import { DEFAULT_POSITION_STATE, PositionFlowStep } from 'pages/Pool/Positions/create/types' import { LoadingRow } from 'pages/Pool/Positions/shared' import { useMemo, useState } from 'react' import { ChevronRight } from 'react-feather' @@ -29,7 +34,7 @@ import { Navigate, useParams } from 'react-router-dom' import { liquiditySaga } from 'state/sagas/liquidity/liquiditySaga' import { ClickableTamaguiStyle } from 'theme/components' import { PositionField } from 'types/position' -import { Flex, Main, Text, styled } from 'ui/src' +import { Flex, Main, Text, styled, useMedia } from 'ui/src' import { ArrowDown } from 'ui/src/components/icons/ArrowDown' import { RotateLeft } from 'ui/src/components/icons/RotateLeft' import { ProgressIndicator } from 'uniswap/src/components/ConfirmSwapModal/ProgressIndicator' @@ -44,6 +49,7 @@ import { isValidLiquidityTxContext } from 'uniswap/src/features/transactions/liq import { useUSDCValue } from 'uniswap/src/features/transactions/swap/hooks/useUSDCPrice' import { TransactionStep } from 'uniswap/src/features/transactions/swap/types/steps' import { Trans, useTranslation } from 'uniswap/src/i18n' +import { useChainIdFromUrlParam } from 'utils/chainParams' import { useAccount } from 'wagmi' const BodyWrapper = styled(Main, { @@ -59,10 +65,14 @@ const BodyWrapper = styled(Main, { }) function MigrateV3Inner({ positionInfo }: { positionInfo: PositionInfo }) { - const { positionId } = useParams<{ positionId: string }>() + const { chainName, tokenId } = useParams<{ tokenId: string; chainName: string }>() + const { t } = useTranslation() - const { step, setStep } = useCreatePositionContext() + const { positionState, setPositionState, setStep, step } = useCreatePositionContext() + const { protocolVersion } = positionState + const { setPriceRangeState } = usePriceRangeContext() + const { setDepositState } = useDepositContext() const { value: v4Enabled, isLoading: isV4GateLoading } = useFeatureFlagWithLoading(FeatureFlags.V4Everywhere) const [transactionSteps, setTransactionSteps] = useState([]) @@ -74,6 +84,7 @@ function MigrateV3Inner({ positionInfo }: { positionInfo: PositionInfo }) { const account = useAccountMeta() const dispatch = useDispatch() const { txInfo } = useMigrateV3TxContext() + const media = useMedia() const onClose = () => { setCurrentTransactionStep(undefined) @@ -92,33 +103,20 @@ function MigrateV3Inner({ positionInfo }: { positionInfo: PositionInfo }) { } return ( - - - {/* nav breadcrumbs */} - - - - - - - {currency0Amount.currency.symbol} / {currency1Amount.currency.symbol} - - - - - - - - - - - {/* TODO: replace with Spore button once available */} + <> + + + + + + + {currency0Amount.currency.symbol} / {currency1Amount.currency.symbol} + + + + + + { + setPositionState({ + ...DEFAULT_POSITION_STATE, + protocolVersion, + currencyInputs: { + [PositionField.TOKEN0]: currency0Amount.currency, + [PositionField.TOKEN1]: currency1Amount.currency, + }, + }) + setPriceRangeState(DEFAULT_PRICE_RANGE_STATE_POOL_EXISTS) + setDepositState(DEFAULT_DEPOSIT_STATE) setStep(PositionFlowStep.SELECT_TOKENS_AND_FEE_TIER) }} > @@ -139,50 +147,64 @@ function MigrateV3Inner({ positionInfo }: { positionInfo: PositionInfo }) { - - - - + + {!media.xl && ( + + + + )} + + + + + + + + {step === PositionFlowStep.SELECT_TOKENS_AND_FEE_TIER ? ( + { + setStep(PositionFlowStep.PRICE_RANGE) + }} + /> + ) : ( + + )} + {step === PositionFlowStep.PRICE_RANGE && ( + { + const isValidTx = isValidLiquidityTxContext(txInfo) + if (!account || account?.type !== AccountType.SignerMnemonic || !isValidTx) { + return + } + dispatch( + liquiditySaga.actions.trigger({ + selectChain, + startChainId, + account, + liquidityTxContext: txInfo, + setCurrentStep: setCurrentTransactionStep, + setSteps: setTransactionSteps, + onSuccess: onClose, + onFailure: onClose, + }), + ) + }} + /> + )} - - {step === PositionFlowStep.SELECT_TOKENS_AND_FEE_TIER ? ( - { - setStep(PositionFlowStep.PRICE_RANGE) - }} - /> - ) : ( - - )} - {step === PositionFlowStep.PRICE_RANGE && ( - { - const isValidTx = isValidLiquidityTxContext(txInfo) - if (!account || account?.type !== AccountType.SignerMnemonic || !isValidTx) { - return - } - dispatch( - liquiditySaga.actions.trigger({ - selectChain, - startChainId, - account, - liquidityTxContext: txInfo, - setCurrentStep: setCurrentTransactionStep, - setSteps: setTransactionSteps, - onSuccess: onClose, - onFailure: onClose, - }), - ) - }} - /> - )} + - + ) } @@ -208,7 +230,7 @@ function MigrateV3Inner({ positionInfo }: { positionInfo: PositionInfo }) { */ export default function MigrateV3() { const { tokenId } = useParams<{ tokenId: string }>() - const chainInfo = useChainFromUrlParam() + const chainId = useChainIdFromUrlParam() const account = useAccount() const { data, isLoading: positionLoading } = useGetPositionQuery( account.address @@ -216,7 +238,7 @@ export default function MigrateV3() { owner: account.address, protocolVersion: ProtocolVersion.V3, tokenId, - chainId: chainInfo?.id ?? account.chainId, + chainId: chainId ?? account.chainId, } : undefined, ) diff --git a/apps/web/src/pages/NotFound/index.tsx b/apps/web/src/pages/NotFound/index.tsx index 30f1e4109e4..036364107b3 100644 --- a/apps/web/src/pages/NotFound/index.tsx +++ b/apps/web/src/pages/NotFound/index.tsx @@ -4,6 +4,7 @@ import lightImage from 'assets/images/404-page-light.png' import { SmallButtonPrimary } from 'components/Button/buttons' import { useIsMobile } from 'hooks/screenSize/useIsMobile' import styled from 'lib/styled-components' +import { ReactNode } from 'react' import { Link } from 'react-router-dom' import { ThemedText } from 'theme/components' import { useIsDarkMode } from 'theme/components/ThemeToggle' @@ -37,7 +38,13 @@ const PageWrapper = styled(Container)` } ` -export default function NotFound() { +interface NotFoundProps { + title?: ReactNode + subtitle?: ReactNode + actionButton?: ReactNode +} + +export default function NotFound({ title, subtitle, actionButton }: NotFoundProps) { const isDarkMode = useIsDarkMode() const isMobile = useIsMobile() @@ -49,16 +56,20 @@ export default function NotFound() {
- 404 - - - + {title ?? 404} + {subtitle ?? ( + + + + )}
- - - + {actionButton ?? ( + + + + )}
) diff --git a/apps/web/src/pages/Pool/Positions/ClaimFeeModal.tsx b/apps/web/src/pages/Pool/Positions/ClaimFeeModal.tsx index 1fa75cbf8c8..d2b51ff5da3 100644 --- a/apps/web/src/pages/Pool/Positions/ClaimFeeModal.tsx +++ b/apps/web/src/pages/Pool/Positions/ClaimFeeModal.tsx @@ -1,7 +1,7 @@ /* eslint-disable-next-line no-restricted-imports */ import { ProtocolVersion } from '@uniswap/client-pools/dist/pools/v1/types_pb' -import { Currency, CurrencyAmount } from '@uniswap/sdk-core' -import { PositionInfo } from 'components/Liquidity/types' +import { LoaderButton } from 'components/Button/LoaderButton' +import { useModalLiquidityInitialState, useV3OrV4PositionDerivedInfo } from 'components/Liquidity/hooks' import { getProtocolItems } from 'components/Liquidity/utils' import { GetHelpHeader } from 'components/Modal/GetHelpHeader' import { ZERO_ADDRESS } from 'constants/misc' @@ -9,11 +9,12 @@ import { useCurrencyInfo } from 'hooks/Tokens' import { useAccount } from 'hooks/useAccount' import { useEthersSigner } from 'hooks/useEthersSigner' import { useMemo } from 'react' +import { useCloseModal } from 'state/application/hooks' import { PopupType, addPopup } from 'state/application/reducer' import { useAppDispatch } from 'state/hooks' import { useTransactionAdder } from 'state/transactions/hooks' import { TransactionType } from 'state/transactions/types' -import { Button, Flex, Text } from 'ui/src' +import { Flex, Text } from 'ui/src' import { iconSizes } from 'ui/src/theme' import { CurrencyLogo } from 'uniswap/src/components/CurrencyLogo/CurrencyLogo' import { Modal } from 'uniswap/src/components/modals/Modal' @@ -26,36 +27,29 @@ import { useTranslation } from 'uniswap/src/i18n' import { NumberType } from 'utilities/src/format/types' import { currencyId } from 'utils/currencyId' -type ClaimFeeModalProps = { - positionInfo: PositionInfo - isOpen: boolean - onClose: () => void - token0Fees?: CurrencyAmount - token1Fees?: CurrencyAmount - token0FeesUsd?: CurrencyAmount - token1FeesUsd?: CurrencyAmount - collectAsWETH: boolean -} - -export function ClaimFeeModal({ - positionInfo, - onClose, - isOpen, - token0Fees, - token1Fees, - token0FeesUsd, - token1FeesUsd, - collectAsWETH, -}: ClaimFeeModalProps) { +export function ClaimFeeModal() { const { t } = useTranslation() const { formatCurrencyAmount } = useLocalizationContext() + const positionInfo = useModalLiquidityInitialState() + const onClose = useCloseModal(ModalName.ClaimFee) + const { + feeValue0: token0Fees, + feeValue1: token1Fees, + fiatFeeValue0: token0FeesUsd, + fiatFeeValue1: token1FeesUsd, + } = useV3OrV4PositionDerivedInfo(positionInfo) + const currencyInfo0 = useCurrencyInfo(token0Fees?.currency) const currencyInfo1 = useCurrencyInfo(token1Fees?.currency) const account = useAccount() const dispatch = useAppDispatch() const addTransaction = useTransactionAdder() - const claimLpFeesParams = useMemo((): ClaimLPFeesRequest => { + const claimLpFeesParams = useMemo(() => { + if (!positionInfo) { + return undefined + } + return { protocol: getProtocolItems(positionInfo.version), tokenId: positionInfo.tokenId ? Number(positionInfo.tokenId) : undefined, @@ -80,16 +74,18 @@ export function ClaimFeeModal({ positionInfo.version !== ProtocolVersion.V4 ? token0Fees?.quotient.toString() : undefined, expectedTokenOwed1RawAmount: positionInfo.version !== ProtocolVersion.V4 ? token1Fees?.quotient.toString() : undefined, - collectAsWETH: positionInfo.version !== ProtocolVersion.V4 ? collectAsWETH : undefined, - } - }, [account.address, positionInfo, token0Fees, token1Fees, collectAsWETH]) + collectAsWETH: positionInfo.version !== ProtocolVersion.V4 ? positionInfo.collectAsWeth : undefined, + } satisfies ClaimLPFeesRequest + }, [account.address, positionInfo, token0Fees, token1Fees]) - const { data } = useClaimLpFeesCalldataQuery({ params: claimLpFeesParams, enabled: isOpen }) + const { data, isLoading: calldataLoading } = useClaimLpFeesCalldataQuery({ + params: claimLpFeesParams, + }) const signer = useEthersSigner() return ( - + )} - + + {t('common.collect.button')} + + ) diff --git a/apps/web/src/pages/Pool/Positions/PositionPage.tsx b/apps/web/src/pages/Pool/Positions/PositionPage.tsx index d39f9ee1073..58fcd1ee256 100644 --- a/apps/web/src/pages/Pool/Positions/PositionPage.tsx +++ b/apps/web/src/pages/Pool/Positions/PositionPage.tsx @@ -9,22 +9,25 @@ import { PositionNFT } from 'components/Liquidity/PositionNFT' import { useV3OrV4PositionDerivedInfo } from 'components/Liquidity/hooks' import { parseRestPosition } from 'components/Liquidity/utils' import { LoadingFullscreen, LoadingRows } from 'components/Loader/styled' -import { useChainFromUrlParam } from 'constants/chains' +import { ZERO_ADDRESS } from 'constants/misc' import { usePositionTokenURI } from 'hooks/usePositionTokenURI' -import { ClaimFeeModal } from 'pages/Pool/Positions/ClaimFeeModal' -import { LoadingRow, useRefetchOnLpModalClose } from 'pages/Pool/Positions/shared' +import NotFound from 'pages/NotFound' +import { LoadingRow } from 'pages/Pool/Positions/shared' import { useMemo, useState } from 'react' import { ChevronRight } from 'react-feather' import { Navigate, useLocation, useNavigate, useParams } from 'react-router-dom' import { setOpenModal } from 'state/application/reducer' import { useAppDispatch } from 'state/hooks' +import { usePendingLPTransactionsChangeListener } from 'state/transactions/hooks' import { ClickableTamaguiStyle } from 'theme/components' -import { Flex, Main, Switch, Text, styled } from 'ui/src' +import { Button, Flex, Main, Switch, Text, styled } from 'ui/src' import { useGetPositionQuery } from 'uniswap/src/data/rest/getPosition' +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { useFeatureFlagWithLoading } from 'uniswap/src/features/gating/hooks' import { ModalName } from 'uniswap/src/features/telemetry/constants' -import { Trans } from 'uniswap/src/i18n' +import { Trans, useTranslation } from 'uniswap/src/i18n' +import { useChainIdFromUrlParam } from 'utils/chainParams' import { NumberType, useFormatter } from 'utils/formatNumbers' import { useAccount } from 'wagmi' @@ -35,6 +38,7 @@ const BodyWrapper = styled(Main, { gap: '$spacing32', mx: 'auto', width: '100%', + maxWidth: 768, zIndex: '$default', p: '$spacing24', }) @@ -62,42 +66,51 @@ export const HeaderButton = styled(Flex, { } as const, }) +function parseTokenId(tokenId: string | undefined): BigNumber | undefined { + if (!tokenId) { + return undefined + } + try { + return BigNumber.from(tokenId) + } catch (error) { + return undefined + } +} + export default function PositionPage() { - const { tokenId } = useParams<{ tokenId: string }>() - const chainInfo = useChainFromUrlParam() + const { tokenId: tokenIdFromUrl } = useParams<{ tokenId: string }>() + const tokenId = parseTokenId(tokenIdFromUrl) + const chainId = useChainIdFromUrlParam() + const chainInfo = chainId ? getChainInfo(chainId) : undefined const account = useAccount() const { pathname } = useLocation() const { data, isLoading: positionLoading, refetch, - } = useGetPositionQuery( - account.address - ? { - owner: account.address, - protocolVersion: pathname.includes('v3') - ? ProtocolVersion.V3 - : pathname.includes('v4') - ? ProtocolVersion.V4 - : ProtocolVersion.UNSPECIFIED, - tokenId, - chainId: chainInfo?.id ?? account.chainId, - } - : undefined, - ) + } = useGetPositionQuery({ + owner: account?.address ?? ZERO_ADDRESS, + protocolVersion: pathname.includes('v3') + ? ProtocolVersion.V3 + : pathname.includes('v4') + ? ProtocolVersion.V4 + : ProtocolVersion.UNSPECIFIED, + tokenId: tokenIdFromUrl, + chainId: chainId ?? account.chainId, + }) const position = data?.position const positionInfo = useMemo(() => parseRestPosition(position), [position]) - const metadata = usePositionTokenURI(tokenId ? BigNumber.from(tokenId) : undefined, chainInfo?.id) + const metadata = usePositionTokenURI(tokenId, chainInfo?.id, positionInfo?.version) - useRefetchOnLpModalClose(refetch) + usePendingLPTransactionsChangeListener(refetch) const dispatch = useAppDispatch() const [collectAsWeth, setCollectAsWeth] = useState(false) - const [claimFeeModalOpen, setClaimFeeModalOpen] = useState(false) const { value: v4Enabled, isLoading } = useFeatureFlagWithLoading(FeatureFlags.V4Everywhere) const { formatCurrencyAmount } = useFormatter() const navigate = useNavigate() + const { t } = useTranslation() const { currency0Amount, currency1Amount, status } = positionInfo ?? {} const { @@ -116,7 +129,7 @@ export default function PositionPage() { return } - if (positionLoading || !position || !positionInfo || !currency0Amount || !currency1Amount) { + if (positionLoading) { return ( @@ -136,6 +149,26 @@ export default function PositionPage() { ) } + if (!position || !positionInfo || !currency0Amount || !currency1Amount) { + return ( + {t('position.notFound')}} + subtitle={ + + + {t('position.notFound.description')} + + + } + actionButton={} + /> + ) + } + + const hasFees = feeValue0?.greaterThan(0) || feeValue1?.greaterThan(0) || false + + // TODO (WEB-4920): hide action buttons if position owner is not connected wallet. + return ( @@ -146,7 +179,12 @@ export default function PositionPage() { - + {status !== PositionStatus.CLOSED && ( @@ -154,7 +192,7 @@ export default function PositionPage() { { - navigate(`/migrate/v3/${chainInfo?.urlParam}/${tokenId}`) + navigate(`/migrate/v3/${chainInfo?.urlParam}/${tokenIdFromUrl}`) }} > @@ -186,7 +224,7 @@ export default function PositionPage() { )} - + {'result' in metadata ? ( @@ -221,18 +259,24 @@ export default function PositionPage() { - { - setClaimFeeModalOpen(true) - }} - > - - - - + {hasFees && ( + { + if (hasFees) { + dispatch( + setOpenModal({ name: ModalName.ClaimFee, initialState: { ...positionInfo, collectAsWeth } }), + ) + } + }} + > + + + + + )} - + {fiatFeeValue0 && fiatFeeValue1 ? formatCurrencyAmount({ amount: fiatFeeValue0.add(fiatFeeValue1), @@ -249,7 +293,7 @@ export default function PositionPage() { /> )} {positionInfo.version !== ProtocolVersion.V4 && ( - + @@ -267,7 +311,6 @@ export default function PositionPage() { {priceOrdering && token0CurrentPrice && token1CurrentPrice && ( )} - setClaimFeeModalOpen(false)} - token0Fees={feeValue0} - token1Fees={feeValue1} - token0FeesUsd={fiatFeeValue0} - token1FeesUsd={fiatFeeValue1} - collectAsWETH={collectAsWeth} - /> ) } diff --git a/apps/web/src/pages/Pool/Positions/PositionsHeader.tsx b/apps/web/src/pages/Pool/Positions/PositionsHeader.tsx index c2bec807410..9ebde53657d 100644 --- a/apps/web/src/pages/Pool/Positions/PositionsHeader.tsx +++ b/apps/web/src/pages/Pool/Positions/PositionsHeader.tsx @@ -1,22 +1,22 @@ // eslint-disable-next-line no-restricted-imports import { PositionStatus, ProtocolVersion } from '@uniswap/client-pools/dist/pools/v1/types_pb' +import { DropdownSelector } from 'components/DropdownSelector' import { getProtocolStatusLabel, getProtocolVersionLabel } from 'components/Liquidity/utils' import { useAccount } from 'hooks/useAccount' -import { useMemo } from 'react' +import { useMemo, useState } from 'react' import { useNavigate } from 'react-router-dom' import { ClickableTamaguiStyle } from 'theme/components' import { Flex, LabeledCheckbox, Text } from 'ui/src' import { Plus } from 'ui/src/components/icons/Plus' import { RotatableChevron } from 'ui/src/components/icons/RotatableChevron' import { SortHorizontalLines } from 'ui/src/components/icons/SortHorizontalLines' -import { ActionSheetDropdown } from 'uniswap/src/components/dropdowns/ActionSheetDropdown' import { NetworkFilter } from 'uniswap/src/components/network/NetworkFilter' -import { useEnabledChains } from 'uniswap/src/features/settings/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { Trans, useTranslation } from 'uniswap/src/i18n' -import { UniverseChainId } from 'uniswap/src/types/chains' type PositionsHeaderProps = { - showFilters: boolean + showFilters?: boolean selectedChain: UniverseChainId | null selectedVersions?: ProtocolVersion[] selectedStatus?: PositionStatus[] @@ -26,7 +26,7 @@ type PositionsHeaderProps = { } export function PositionsHeader({ - showFilters, + showFilters = true, selectedChain, selectedVersions, selectedStatus, @@ -41,59 +41,41 @@ export function PositionsHeader({ const filterOptions = useMemo(() => { const statusOptions = [PositionStatus.IN_RANGE, PositionStatus.OUT_OF_RANGE, PositionStatus.CLOSED].map( - (status) => ({ - key: `PositionsHeader-status-${status}`, - onPress: () => null, - render: () => ( - - { - onStatusChange(status) - }} - /> - - ), - }), - ) - const versionOptions = [ProtocolVersion.V2, ProtocolVersion.V3, ProtocolVersion.V4].map((version) => ({ - key: `PositionsHeader-version-${version}`, - onPress: () => null, - render: () => ( - - { - onVersionChange(version) - }} - /> - + (status) => ( + { + onStatusChange(status) + }} + /> ), - })) + ) + const versionOptions = [ProtocolVersion.V4, ProtocolVersion.V3, ProtocolVersion.V2].map((version) => ( + { + onVersionChange(version) + }} + /> + )) return [ - { - key: 'PositionsHeader-status-section-title', - onPress: () => null, - render: () => ( - - {t('common.status')} - - ), - }, + + {t('common.status')} + , ...statusOptions, - { - key: 'PositionsHeader-version-section-title', - onPress: () => null, - render: () => ( - - {t('common.version')} - - ), - }, + + {t('common.version')} + , ...versionOptions, ] }, [onStatusChange, onVersionChange, selectedStatus, selectedVersions, t]) @@ -102,23 +84,27 @@ export function PositionsHeader({ () => [ProtocolVersion.V2, ProtocolVersion.V3, ProtocolVersion.V4].map((version) => { const protocolVersionLabel = getProtocolVersionLabel(version)?.toLowerCase() - return { - key: `PositionsHeader-create-${protocolVersionLabel}`, - onPress: () => { - navigate(`/positions/create/${protocolVersionLabel}`) - }, - render: () => ( - - - - - - ), - } + return ( + { + navigate(`/positions/create/${protocolVersionLabel}`) + }} + > + + + + + ) }), [navigate], ) + const [createDropdownOpen, setCreateDropdownOpen] = useState(false) + const [filterDropdownOpen, setFilterDropdownOpen] = useState(false) + return ( {t('pool.positions.title')} @@ -142,42 +128,11 @@ export function PositionsHeader({ {t('common.new')} - - - - - - - {showFilters && ( - <> - + - + - + } + buttonStyle={{ + borderWidth: 0, + p: 0, + }} + dropdownStyle={{ + width: 160, + }} + internalMenuItems={<>{createOptions}} + hideChevron={true} + isOpen={createDropdownOpen} + toggleOpen={() => { + setCreateDropdownOpen((prev) => !prev) + }} + /> + + {showFilters && ( + + { + setFilterDropdownOpen((prev) => !prev) + }} + menuLabel={ + + + + } + internalMenuItems={<>{filterOptions}} + hideChevron={true} + dropdownStyle={{ + width: 160, + }} + buttonStyle={{ + borderWidth: 0, + p: 0, + }} + /> - + )} )} diff --git a/apps/web/src/pages/Pool/Positions/V2PositionPage.tsx b/apps/web/src/pages/Pool/Positions/V2PositionPage.tsx index 2a39d54289e..a2b6dc0c36f 100644 --- a/apps/web/src/pages/Pool/Positions/V2PositionPage.tsx +++ b/apps/web/src/pages/Pool/Positions/V2PositionPage.tsx @@ -4,25 +4,28 @@ import { BreadcrumbNavContainer, BreadcrumbNavLink } from 'components/Breadcrumb import { LiquidityPositionInfo } from 'components/Liquidity/LiquidityPositionInfo' import { useGetPoolTokenPercentage } from 'components/Liquidity/hooks' import { parseRestPosition } from 'components/Liquidity/utils' -import { LoadingRows } from 'components/Loader/styled' import { DoubleCurrencyAndChainLogo } from 'components/Logo/DoubleLogo' -import { useChainFromUrlParam } from 'constants/chains' +import { ZERO_ADDRESS } from 'constants/misc' +import { LoadingRows } from 'pages/LegacyPool/styled' +import NotFound from 'pages/NotFound' import { HeaderButton } from 'pages/Pool/Positions/PositionPage' -import { LoadingRow, useRefetchOnLpModalClose } from 'pages/Pool/Positions/shared' +import { LoadingRow } from 'pages/Pool/Positions/shared' import { useMemo } from 'react' import { ChevronRight } from 'react-feather' import { Navigate, useNavigate, useParams } from 'react-router-dom' import { setOpenModal } from 'state/application/reducer' import { useAppDispatch } from 'state/hooks' -import { Flex, Main, Text, styled } from 'ui/src' +import { usePendingLPTransactionsChangeListener } from 'state/transactions/hooks' +import { Button, Flex, Main, Text, styled } from 'ui/src' import { useGetPositionQuery } from 'uniswap/src/data/rest/getPosition' import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { useFeatureFlagWithLoading } from 'uniswap/src/features/gating/hooks' import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext' import { ModalName } from 'uniswap/src/features/telemetry/constants' import { useUSDCValue } from 'uniswap/src/features/transactions/swap/hooks/useUSDCPrice' -import { Trans } from 'uniswap/src/i18n' +import { Trans, useTranslation } from 'uniswap/src/i18n' import { NumberType } from 'utilities/src/format/types' +import { useChainIdFromUrlParam } from 'utils/chainParams' import { useAccount } from 'wagmi' const BodyWrapper = styled(Main, { @@ -38,30 +41,27 @@ const BodyWrapper = styled(Main, { export default function V2PositionPage() { const { pairAddress } = useParams<{ pairAddress: string }>() - const chainInfo = useChainFromUrlParam() + const chainId = useChainIdFromUrlParam() const account = useAccount() const { data, isLoading: positionLoading, refetch, - } = useGetPositionQuery( - account.address - ? { - owner: account.address, - protocolVersion: ProtocolVersion.V2, - pairAddress, - chainId: chainInfo?.id ?? account.chainId, - } - : undefined, - ) + } = useGetPositionQuery({ + owner: account?.address ?? ZERO_ADDRESS, + protocolVersion: ProtocolVersion.V2, + pairAddress, + chainId: chainId ?? account.chainId, + }) const position = data?.position const positionInfo = useMemo(() => parseRestPosition(position), [position]) const dispatch = useAppDispatch() const navigate = useNavigate() const { formatCurrencyAmount, formatPercent } = useLocalizationContext() + const { t } = useTranslation() - useRefetchOnLpModalClose(refetch) + usePendingLPTransactionsChangeListener(refetch) const { value: v4Enabled, isLoading } = useFeatureFlagWithLoading(FeatureFlags.V4Everywhere) @@ -75,7 +75,7 @@ export default function V2PositionPage() { return } - if (positionLoading || !positionInfo || !liquidityAmount || !currency0Amount || !currency1Amount) { + if (positionLoading) { return ( @@ -96,9 +96,25 @@ export default function V2PositionPage() { ) } + if (!positionInfo || !liquidityAmount || !currency0Amount || !currency1Amount) { + return ( + {t('position.notFound')}} + subtitle={ + + + {t('position.notFound.description')} + + + } + actionButton={} + /> + ) + } + return ( - + @@ -109,7 +125,7 @@ export default function V2PositionPage() { {status === PositionStatus.IN_RANGE && ( - + { diff --git a/apps/web/src/pages/Pool/Positions/create/AddHook.tsx b/apps/web/src/pages/Pool/Positions/create/AddHook.tsx index 6119ca59ca9..b4644735b96 100644 --- a/apps/web/src/pages/Pool/Positions/create/AddHook.tsx +++ b/apps/web/src/pages/Pool/Positions/create/AddHook.tsx @@ -1,58 +1,189 @@ +import { HookModal } from 'components/Liquidity/HookModal' +import { isDynamicFeeTier } from 'components/Liquidity/utils' +import { useCreatePositionContext } from 'pages/Pool/Positions/create/CreatePositionContext' import { AdvancedButton } from 'pages/Pool/Positions/create/shared' -import { useState } from 'react' -import { Button } from 'ui/src' +import { DEFAULT_POSITION_STATE } from 'pages/Pool/Positions/create/types' +import { useCallback, useRef, useState } from 'react' +import { Button, Text, TouchableArea, styled } from 'ui/src' import { DocumentList } from 'ui/src/components/icons/DocumentList' import { X } from 'ui/src/components/icons/X' import { Flex } from 'ui/src/components/layout/Flex' import { fonts } from 'ui/src/theme' import { TextInput } from 'uniswap/src/components/input/TextInput' import { useTranslation } from 'uniswap/src/i18n' +import { getValidAddress, shortenAddress } from 'uniswap/src/utils/addresses' +import { useOnClickOutside } from 'utilities/src/react/hooks' + +const MenuFlyout = styled(Flex, { + animation: 'fastHeavy', + enterStyle: { top: 30, opacity: 0 }, + exitStyle: { top: 30, opacity: 0 }, + width: 'calc(100% - 48px)', + backgroundColor: '$surface2', + borderColor: '$surface3', + borderWidth: 1, + borderRadius: '$rounded12', + position: 'absolute', + top: 40, + zIndex: 100, + p: '$padding16', + opacity: 1, + shadowColor: '$shadowColor', + shadowOffset: { width: 0, height: 25 }, + shadowOpacity: 0.2, + shadowRadius: 50, +}) + +function AutocompleteFlyout({ address, handleSelectAddress }: { address: string; handleSelectAddress: () => void }) { + const { t } = useTranslation() + const validAddress = getValidAddress(address) + + return ( + + {validAddress ? ( + + {address} + + ) : ( + + {t('position.addingHook.invalidAddress')} + + )} + + ) +} export function AddHook() { const { t } = useTranslation() + const [isFocusing, setFocus] = useState(false) + const handleFocus = useCallback((focus: boolean) => setFocus(focus), []) + + const inputWrapperNode = useRef(null) + useOnClickOutside(inputWrapperNode, isFocusing ? () => handleFocus(false) : undefined) + const [hookInputEnabled, setHookInputEnabled] = useState(false) - const [hookAddress, setHookAddress] = useState('') + const [hookModalOpen, setHookModalOpen] = useState(false) - const handleToggleHookInput = () => { - setHookInputEnabled((prev) => !prev) - setHookAddress('') + const [hookValue, setHookValue] = useState('') + const { + positionState: { hook, fee }, + setPositionState, + } = useCreatePositionContext() + + const onSelectHook = (value: string | undefined) => { + setPositionState((state) => ({ + ...state, + hook: value, + })) + } + + const onClearHook = () => { + if (isDynamicFeeTier(fee)) { + setPositionState((state) => ({ + ...state, + fee: DEFAULT_POSITION_STATE.fee, + })) + } + + setHookInputEnabled(false) + setHookValue('') + onSelectHook(undefined) } if (hookInputEnabled) { + const showFlyout = isFocusing && hookValue + return ( - - setHookAddress(text)} - /> - - + <> + {hookModalOpen && ( + // intentionally only render this when the value is true to ensure that the address is valid. + setHookModalOpen(false)} + onClearHook={() => { + setHookInputEnabled(false) + setHookValue('') + }} + onContinue={() => onSelectHook(hookValue)} + /> + )} + {hook ? ( + + { + setHookInputEnabled(true) + onSelectHook(undefined) + }} + > + + + {shortenAddress(hook)} + + + + + {t('common.clear')} + + + + ) : ( + + handleFocus(true)} + /> + + {showFlyout && ( + setHookModalOpen(true)} /> + )} + + )} + ) } @@ -60,7 +191,7 @@ export function AddHook() { setHookInputEnabled(true)} tooltipText={t('position.addHook.tooltip')} /> ) diff --git a/apps/web/src/pages/Pool/Positions/create/ContextProviders.tsx b/apps/web/src/pages/Pool/Positions/create/ContextProviders.tsx index a1583f09293..168c8a2d0a8 100644 --- a/apps/web/src/pages/Pool/Positions/create/ContextProviders.tsx +++ b/apps/web/src/pages/Pool/Positions/create/ContextProviders.tsx @@ -1,11 +1,12 @@ -// eslint-disable-next-line no-restricted-imports import { FeeTierSearchModal } from 'components/Liquidity/FeeTierSearchModal' import { DepositState } from 'components/Liquidity/types' +import { useAccount } from 'hooks/useAccount' import { CreatePositionContext, CreateTxContext, DEFAULT_DEPOSIT_STATE, - DEFAULT_PRICE_RANGE_STATE, + DEFAULT_PRICE_RANGE_STATE_CREATING_POOL, + DEFAULT_PRICE_RANGE_STATE_POOL_EXISTS, DepositContext, PriceRangeContext, useCreatePositionContext, @@ -28,10 +29,14 @@ import { generateCreateCalldataQueryParams, generateCreatePositionTxRequest, } from 'pages/Pool/Positions/create/utils' -import { useMemo, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' +import { PositionField } from 'types/position' +import { nativeOnChain } from 'uniswap/src/constants/tokens' import { useAccountMeta } from 'uniswap/src/contexts/UniswapContext' import { useCheckLpApprovalQuery } from 'uniswap/src/data/apiClients/tradingApi/useCheckLpApprovalQuery' import { useCreateLpPositionCalldataQuery } from 'uniswap/src/data/apiClients/tradingApi/useCreateLpPositionCalldataQuery' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { usePrevious } from 'utilities/src/react/hooks' import { ONE_SECOND_MS } from 'utilities/src/time/time' export function CreatePositionContextProvider({ @@ -45,7 +50,20 @@ export function CreatePositionContextProvider({ const [step, setStep] = useState(PositionFlowStep.SELECT_TOKENS_AND_FEE_TIER) const derivedPositionInfo = useDerivedPositionInfo(positionState) const [feeTierSearchModalOpen, setFeeTierSearchModalOpen] = useState(false) - const [createPoolInfoDismissed, setCreatePoolInfoDismissed] = useState(false) + + const account = useAccount() + const prevChainId = usePrevious(account.chainId) + useEffect(() => { + if (prevChainId && prevChainId !== account.chainId) { + setPositionState((prevState) => ({ + ...prevState, + currencyInputs: { + [PositionField.TOKEN0]: nativeOnChain(account.chainId ?? UniverseChainId.Mainnet), + }, + })) + setStep(PositionFlowStep.SELECT_TOKENS_AND_FEE_TIER) + } + }, [account.chainId, prevChainId]) return ( {children} @@ -68,7 +84,24 @@ export function CreatePositionContextProvider({ } export function PriceRangeContextProvider({ children }: { children: React.ReactNode }) { - const [priceRangeState, setPriceRangeState] = useState(DEFAULT_PRICE_RANGE_STATE) + const { derivedPositionInfo } = useCreatePositionContext() + const [priceRangeState, setPriceRangeState] = useState(DEFAULT_PRICE_RANGE_STATE_CREATING_POOL) + + useEffect(() => { + // creatingPoolOrPair is calculated in the previous step of the create flow, so + // it's safe to reset PriceRangeState to defaults when it changes. + setPriceRangeState( + derivedPositionInfo.creatingPoolOrPair + ? DEFAULT_PRICE_RANGE_STATE_CREATING_POOL + : DEFAULT_PRICE_RANGE_STATE_POOL_EXISTS, + ) + }, [derivedPositionInfo.creatingPoolOrPair]) + + useEffect(() => { + // When the price is inverted, reset the state so that LiquidityChartRangeInput redraws the default range. + setPriceRangeState((prevState) => ({ ...prevState, fullRange: false, minPrice: undefined, maxPrice: undefined })) + }, [priceRangeState.priceInverted]) + const derivedPriceRangeInfo = useDerivedPriceRangeInfo(priceRangeState) return ( diff --git a/apps/web/src/pages/Pool/Positions/create/CreatePosition.tsx b/apps/web/src/pages/Pool/Positions/create/CreatePosition.tsx index 51c39ad9fd9..78a532accf9 100644 --- a/apps/web/src/pages/Pool/Positions/create/CreatePosition.tsx +++ b/apps/web/src/pages/Pool/Positions/create/CreatePosition.tsx @@ -12,7 +12,7 @@ import { } from 'pages/Pool/Positions/create/ContextProviders' import { DEFAULT_DEPOSIT_STATE, - DEFAULT_PRICE_RANGE_STATE, + DEFAULT_PRICE_RANGE_STATE_POOL_EXISTS, useCreatePositionContext, useDepositContext, usePriceRangeContext, @@ -21,22 +21,51 @@ import { DepositStep } from 'pages/Pool/Positions/create/Deposit' import { EditRangeSelectionStep, EditSelectTokensStep } from 'pages/Pool/Positions/create/EditStep' import { SelectPriceRangeStep, SelectPriceRangeStepV2 } from 'pages/Pool/Positions/create/RangeSelectionStep' import { SelectTokensStep } from 'pages/Pool/Positions/create/SelectTokenStep' +import { Container } from 'pages/Pool/Positions/create/shared' import { DEFAULT_POSITION_STATE, PositionFlowStep } from 'pages/Pool/Positions/create/types' import { useCallback, useMemo } from 'react' import { ChevronRight } from 'react-feather' -import { Navigate, useParams } from 'react-router-dom' +import { Navigate, useNavigate, useParams } from 'react-router-dom' import { PositionField } from 'types/position' -import { Button, Flex, Text } from 'ui/src' +import { Button, Flex, Text, useMedia } from 'ui/src' +import { InfoCircleFilled } from 'ui/src/components/icons/InfoCircleFilled' import { RotatableChevron } from 'ui/src/components/icons/RotatableChevron' import { RotateLeft } from 'ui/src/components/icons/RotateLeft' import { Settings } from 'ui/src/components/icons/Settings' import { iconSizes } from 'ui/src/theme/iconSizes' import { ActionSheetDropdown } from 'uniswap/src/components/dropdowns/ActionSheetDropdown' import { nativeOnChain } from 'uniswap/src/constants/tokens' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { useFeatureFlagWithLoading } from 'uniswap/src/features/gating/hooks' import { Trans, useTranslation } from 'uniswap/src/i18n' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { usePrevious } from 'utilities/src/react/hooks' + +function CreatingPoolInfo() { + const { derivedPositionInfo } = useCreatePositionContext() + + const previouslyCreatingPoolOrPair = usePrevious(derivedPositionInfo.creatingPoolOrPair) + + const shouldShowDisabled = previouslyCreatingPoolOrPair && derivedPositionInfo.poolOrPairLoading + + if (!shouldShowDisabled && !derivedPositionInfo.creatingPoolOrPair) { + return null + } + + return ( + + + + + + + + + + + + ) +} function CreatePositionInner() { const { @@ -59,27 +88,35 @@ function CreatePositionInner() { } }, [creatingPoolOrPair, setStep, step, v2Selected]) - return ( - - {step === PositionFlowStep.SELECT_TOKENS_AND_FEE_TIER ? ( + if (step === PositionFlowStep.SELECT_TOKENS_AND_FEE_TIER) { + return ( + <> - ) : step === PositionFlowStep.PRICE_RANGE ? ( - <> - - {v2Selected ? ( - - ) : ( - - )} - - ) : ( - <> - - {!v2Selected && } - - - )} - + + + ) + } + + if (step === PositionFlowStep.PRICE_RANGE) { + return ( + <> + + {v2Selected ? ( + + ) : ( + + )} + + + ) + } + + return ( + <> + + {!v2Selected && } + + ) } @@ -89,43 +126,40 @@ const Sidebar = () => { positionState: { protocolVersion }, derivedPositionInfo: { creatingPoolOrPair }, step, + setStep, } = useCreatePositionContext() const PoolProgressSteps = useMemo(() => { + const createStep = (label: string, stepEnum: PositionFlowStep) => ({ + label, + active: step === stepEnum, + // This relies on the ordering of PositionFlowStep enum values matching the actual order in the form. + onPress: stepEnum < step ? () => setStep(stepEnum) : undefined, + }) + if (protocolVersion === ProtocolVersion.V2) { if (creatingPoolOrPair) { return [ - { label: t(`position.step.select`), active: step === PositionFlowStep.SELECT_TOKENS_AND_FEE_TIER }, - { label: t(`position.step.price`), active: step === PositionFlowStep.PRICE_RANGE }, - { label: t(`position.step.deposit`), active: step == PositionFlowStep.DEPOSIT }, + createStep(t(`position.step.select`), PositionFlowStep.SELECT_TOKENS_AND_FEE_TIER), + createStep(t('position.step.price'), PositionFlowStep.PRICE_RANGE), + createStep(t('position.step.deposit'), PositionFlowStep.DEPOSIT), ] } - return [ - { label: t(`position.step.select`), active: step === PositionFlowStep.SELECT_TOKENS_AND_FEE_TIER }, - { label: t(`position.step.deposit`), active: step == PositionFlowStep.DEPOSIT }, + createStep(t('position.step.select'), PositionFlowStep.SELECT_TOKENS_AND_FEE_TIER), + createStep(t('position.step.deposit'), PositionFlowStep.DEPOSIT), ] } return [ - { label: t(`position.step.select`), active: step === PositionFlowStep.SELECT_TOKENS_AND_FEE_TIER }, - { label: t(`position.step.range`), active: step === PositionFlowStep.PRICE_RANGE }, - { label: t(`position.step.deposit`), active: step == PositionFlowStep.DEPOSIT }, + createStep(t('position.step.select'), PositionFlowStep.SELECT_TOKENS_AND_FEE_TIER), + createStep(t('position.step.range'), PositionFlowStep.PRICE_RANGE), + createStep(t('position.step.deposit'), PositionFlowStep.DEPOSIT), ] - }, [creatingPoolOrPair, protocolVersion, step, t]) + }, [creatingPoolOrPair, protocolVersion, setStep, step, t]) return ( - - - - - - - - - - - + ) @@ -136,6 +170,7 @@ const Toolbar = () => { const { protocolVersion } = positionState const { priceRangeState, setPriceRangeState } = usePriceRangeContext() const { depositState, setDepositState } = useDepositContext() + const navigate = useNavigate() const isFormUnchanged = useMemo(() => { // Check if all form fields (except protocol version) are set to their default values @@ -143,34 +178,39 @@ const Toolbar = () => { positionState.currencyInputs === DEFAULT_POSITION_STATE.currencyInputs && positionState.fee === DEFAULT_POSITION_STATE.fee && positionState.hook === DEFAULT_POSITION_STATE.hook && - priceRangeState === DEFAULT_PRICE_RANGE_STATE && + priceRangeState.initialPrice === DEFAULT_PRICE_RANGE_STATE_POOL_EXISTS.initialPrice && depositState === DEFAULT_DEPOSIT_STATE ) }, [positionState.currencyInputs, positionState.fee, positionState.hook, priceRangeState, depositState]) const handleReset = useCallback(() => { setPositionState({ ...DEFAULT_POSITION_STATE, protocolVersion }) - setPriceRangeState(DEFAULT_PRICE_RANGE_STATE) + setPriceRangeState(DEFAULT_PRICE_RANGE_STATE_POOL_EXISTS) setDepositState(DEFAULT_DEPOSIT_STATE) setStep(PositionFlowStep.SELECT_TOKENS_AND_FEE_TIER) }, [protocolVersion, setDepositState, setPositionState, setPriceRangeState, setStep]) const handleVersionChange = useCallback( (version: ProtocolVersion) => { + const versionUrl = getProtocolVersionLabel(version)?.toLowerCase() + if (versionUrl) { + navigate(`/positions/create/${versionUrl}`) + } + setPositionState((prevState) => ({ ...DEFAULT_POSITION_STATE, currencyInputs: prevState.currencyInputs, protocolVersion: version, })) - setPriceRangeState(DEFAULT_PRICE_RANGE_STATE) + setPriceRangeState(DEFAULT_PRICE_RANGE_STATE_POOL_EXISTS) setStep(PositionFlowStep.SELECT_TOKENS_AND_FEE_TIER) }, - [setPositionState, setPriceRangeState, setStep], + [setPositionState, setPriceRangeState, setStep, navigate], ) const versionOptions = useMemo( () => - [ProtocolVersion.V2, ProtocolVersion.V3, ProtocolVersion.V4] + [ProtocolVersion.V4, ProtocolVersion.V3, ProtocolVersion.V2] .filter((version) => version != protocolVersion) .map((version) => ({ key: `version-${version}`, @@ -190,7 +230,7 @@ const Toolbar = () => { ) return ( - + diff --git a/apps/web/src/pages/Pool/Positions/create/Deposit.tsx b/apps/web/src/pages/Pool/Positions/create/Deposit.tsx index f70cc57a398..c9219d9faad 100644 --- a/apps/web/src/pages/Pool/Positions/create/Deposit.tsx +++ b/apps/web/src/pages/Pool/Positions/create/Deposit.tsx @@ -1,6 +1,8 @@ +import { LoaderButton } from 'components/Button/LoaderButton' import { DepositInputForm } from 'components/Liquidity/DepositInputForm' import { useCreatePositionContext, + useCreateTxContext, useDepositContext, usePriceRangeContext, } from 'pages/Pool/Positions/create/CreatePositionContext' @@ -8,7 +10,7 @@ import { CreatePositionModal } from 'pages/Pool/Positions/create/CreatePositionM import { Container } from 'pages/Pool/Positions/create/shared' import { useCallback, useState } from 'react' import { PositionField } from 'types/position' -import { Button, Flex, FlexProps, Text } from 'ui/src' +import { Flex, FlexProps, Text } from 'ui/src' import { Trans } from 'uniswap/src/i18n' export const DepositStep = ({ ...rest }: FlexProps) => { @@ -18,6 +20,7 @@ export const DepositStep = ({ ...rest }: FlexProps) => { setDepositState, derivedDepositInfo: { formattedAmounts, currencyAmounts, currencyAmountsUSDValue, currencyBalances, error }, } = useDepositContext() + const txContext = useCreateTxContext() const [isReviewModalOpen, setIsReviewModalOpen] = useState(false) const handleUserInput = (field: PositionField, newValue: string) => { @@ -75,11 +78,19 @@ export const DepositStep = ({ ...rest }: FlexProps) => { deposit0Disabled={deposit0Disabled} deposit1Disabled={deposit1Disabled} /> - + setIsReviewModalOpen(false)} /> diff --git a/apps/web/src/pages/Pool/Positions/create/EditStep.tsx b/apps/web/src/pages/Pool/Positions/create/EditStep.tsx index 1695ddb812f..a9971aa6e45 100644 --- a/apps/web/src/pages/Pool/Positions/create/EditStep.tsx +++ b/apps/web/src/pages/Pool/Positions/create/EditStep.tsx @@ -1,6 +1,14 @@ // eslint-disable-next-line no-restricted-imports +import { LiquidityPositionInfoBadges } from 'components/Liquidity/LiquidityPositionInfoBadges' +import { getProtocolVersionLabel } from 'components/Liquidity/utils' import { DoubleCurrencyLogo } from 'components/Logo/DoubleLogo' -import { useCreatePositionContext, usePriceRangeContext } from 'pages/Pool/Positions/create/CreatePositionContext' +import { + DEFAULT_DEPOSIT_STATE, + DEFAULT_PRICE_RANGE_STATE_POOL_EXISTS, + useCreatePositionContext, + useDepositContext, + usePriceRangeContext, +} from 'pages/Pool/Positions/create/CreatePositionContext' import { Container, formatPrices } from 'pages/Pool/Positions/create/shared' import { PositionFlowStep } from 'pages/Pool/Positions/create/types' import { getInvertedTuple } from 'pages/Pool/Positions/create/utils' @@ -26,15 +34,19 @@ const EditStep = ({ children, onClick, ...rest }: { children: JSX.Element; onCli } export const EditSelectTokensStep = (props?: FlexProps) => { - const { - setStep, - derivedPositionInfo: { currencies }, - } = useCreatePositionContext() + const { setStep, derivedPositionInfo, positionState } = useCreatePositionContext() + const { setPriceRangeState } = usePriceRangeContext() + const { setDepositState } = useDepositContext() + const { currencies, protocolVersion } = derivedPositionInfo + const { fee, hook } = positionState const [token0, token1] = currencies + const versionLabel = getProtocolVersionLabel(protocolVersion) const handleEdit = useCallback(() => { + setPriceRangeState(DEFAULT_PRICE_RANGE_STATE_POOL_EXISTS) + setDepositState(DEFAULT_DEPOSIT_STATE) setStep(PositionFlowStep.SELECT_TOKENS_AND_FEE_TIER) - }, [setStep]) + }, [setDepositState, setPriceRangeState, setStep]) return ( @@ -45,6 +57,9 @@ export const EditSelectTokensStep = (props?: FlexProps) => { / {token1?.symbol} + + + ) @@ -59,13 +74,15 @@ export const EditRangeSelectionStep = (props?: FlexProps) => { priceRangeState: { priceInverted }, derivedPriceRangeInfo, } = usePriceRangeContext() + const { setDepositState } = useDepositContext() const { formatNumberOrString } = useLocalizationContext() const [baseCurrency, quoteCurrency] = getInvertedTuple(currencies, priceInverted) const handleEdit = useCallback(() => { + setDepositState(DEFAULT_DEPOSIT_STATE) setStep(PositionFlowStep.PRICE_RANGE) - }, [setStep]) + }, [setDepositState, setStep]) const formattedPrices = useMemo(() => { return formatPrices(derivedPriceRangeInfo, formatNumberOrString) diff --git a/apps/web/src/pages/Pool/Positions/create/RangeSelectionStep.tsx b/apps/web/src/pages/Pool/Positions/create/RangeSelectionStep.tsx index 026aa1c5bd6..32a72d01315 100644 --- a/apps/web/src/pages/Pool/Positions/create/RangeSelectionStep.tsx +++ b/apps/web/src/pages/Pool/Positions/create/RangeSelectionStep.tsx @@ -4,7 +4,7 @@ import { Currency, Price } from '@uniswap/sdk-core' import { calculateInvertedPrice } from 'components/Liquidity/utils' import LiquidityChartRangeInput from 'components/LiquidityChartRangeInput' import { useCreatePositionContext, usePriceRangeContext } from 'pages/Pool/Positions/create/CreatePositionContext' -import { Container, CreatingPoolInfo } from 'pages/Pool/Positions/create/shared' +import { Container } from 'pages/Pool/Positions/create/shared' import { getInvertedTuple } from 'pages/Pool/Positions/create/utils' import { useCallback, useMemo, useState } from 'react' import { Minus, Plus } from 'react-feather' @@ -169,11 +169,13 @@ function RangeInput({ input, decrement, increment, + showIncrementButtons = true, }: { value: string input: RangeSelectionInput decrement: () => string increment: () => string + showIncrementButtons?: boolean }) { const colors = useSporeColors() const { t } = useTranslation() @@ -257,14 +259,22 @@ function RangeInput({ /> - - - - + {showIncrementButtons && ( + + + + + )} ) } @@ -273,7 +283,6 @@ export const SelectPriceRangeStepV2 = ({ onContinue, ...rest }: { onContinue: () return ( - - {isShowMoreFeeTiersEnabled && ( - - {feeTiers.map((feeTier) => ( - + + + {feeTiers.map((feeTier) => ( + + ))} + + {protocolVersion === ProtocolVersion.V4 && ( + { + setFeeTierSearchModalOpen(true) + }} /> - ))} + )} - )} - {protocolVersion === ProtocolVersion.V4 && ( - { - setFeeTierSearchModalOpen(true) - }} - /> - )} + )} - - + 0 ? poolData.pools[0] : undefined - const pairsQueryEnabled = pairEnabledProtocolVersion(protocolVersion) && validCurrencyInput - const pairAddress = useMemo(() => { - return pairsQueryEnabled && validCurrencyInput - ? computePairAddress({ - factoryAddress: V2_FACTORY_ADDRESSES[sortedCurrencies[0].chainId], - tokenA: getCurrencyWithWrap(sortedCurrencies[0], protocolVersion), - tokenB: getCurrencyWithWrap(sortedCurrencies[1], protocolVersion), - }) - : undefined - }, [pairsQueryEnabled, protocolVersion, sortedCurrencies, validCurrencyInput]) - - const { data: pairData, isFetched: pairIsFetched } = useGetPair( + const { pairsQueryEnabled, pairAddress, sortedTokens } = useMemo(() => { + if (!pairEnabledProtocolVersion(protocolVersion)) { + return { + pairsQueryEnabled: false, + } as const + } + + const sortedTokens = getSortedCurrenciesTuple( + getCurrencyWithWrap(sortedCurrencies[0], protocolVersion), + getCurrencyWithWrap(sortedCurrencies[1], protocolVersion), + ) + + if (!validateCurrencyInput(sortedTokens)) { + return { + pairsQueryEnabled: false, + } as const + } + + return { + pairsQueryEnabled: true, + pairAddress: computePairAddress({ + factoryAddress: V2_FACTORY_ADDRESSES[sortedTokens[0].chainId], + tokenA: sortedTokens[0], + tokenB: sortedTokens[1], + }), + sortedTokens, + } as const + }, [protocolVersion, sortedCurrencies]) + + const { + data: pairData, + isFetched: pairIsFetched, + isLoading: pairIsLoading, + } = useGetPair( { chainId: chainId ?? (UniverseChainId.Mainnet as number), pairAddress, @@ -102,8 +124,8 @@ export function useDerivedPositionInfo(state: PositionState): CreatePositionInfo const pair = pairsQueryEnabled ? getPairFromRest({ pair: pairData?.pair, - token0: getCurrencyWithWrap(sortedCurrencies[0], protocolVersion), - token1: getCurrencyWithWrap(sortedCurrencies[1], protocolVersion), + token0: sortedTokens[0], + token1: sortedTokens[1], }) : undefined @@ -138,6 +160,7 @@ export function useDerivedPositionInfo(state: PositionState): CreatePositionInfo protocolVersion, pair, creatingPoolOrPair, + poolOrPairLoading: pairIsLoading, } satisfies CreateV2PositionInfo } @@ -152,6 +175,7 @@ export function useDerivedPositionInfo(state: PositionState): CreatePositionInfo protocolVersion, }), creatingPoolOrPair, + poolOrPairLoading: poolIsLoading, } satisfies CreateV3PositionInfo } @@ -166,8 +190,20 @@ export function useDerivedPositionInfo(state: PositionState): CreatePositionInfo hooks: pool?.hooks?.address || '', }), creatingPoolOrPair, + poolOrPairLoading: poolIsLoading, } satisfies CreateV4PositionInfo - }, [TOKEN0, TOKEN1, protocolVersion, pool, sortedCurrencies, creatingPoolOrPair, pair, poolData?.pools]) + }, [ + TOKEN0, + TOKEN1, + protocolVersion, + pool, + sortedCurrencies, + creatingPoolOrPair, + poolIsLoading, + pair, + pairIsLoading, + poolData?.pools, + ]) } export function useDerivedPriceRangeInfo(state: PriceRangeState): PriceRangeInfo { @@ -368,7 +404,14 @@ export function useDepositInfo(state: UseDepositInfoProps): DepositInfo { return t('common.noAmount.error') } - if (currency0Amount && token0Balance?.lessThan(currency0Amount)) { + const insufficientToken0Balance = currency0Amount && token0Balance?.lessThan(currency0Amount) + const insufficientToken1Balance = currency1Amount && token1Balance?.lessThan(currency1Amount) + + if (insufficientToken0Balance && insufficientToken1Balance) { + return + } + + if (insufficientToken0Balance) { return ( - - - - - - - - - - setCreatePoolInfoDismissed(true)}> - - - - ) -} - export function formatPrices( derivedPriceRangeInfo: PriceRangeInfo, formatter: (input: FormatNumberOrStringInput) => string, diff --git a/apps/web/src/pages/Pool/Positions/create/types.ts b/apps/web/src/pages/Pool/Positions/create/types.ts index 643c08f1447..1653c707ab8 100644 --- a/apps/web/src/pages/Pool/Positions/create/types.ts +++ b/apps/web/src/pages/Pool/Positions/create/types.ts @@ -12,6 +12,17 @@ export type FeeData = { tickSpacing: number } +const DYNAMIC_FEE_AMOUNT = 8388608 + +export type DynamicFeeData = FeeData & { + feeAmount: typeof DYNAMIC_FEE_AMOUNT +} + +export const DYNAMIC_FEE_DATA = { + feeAmount: DYNAMIC_FEE_AMOUNT, + tickSpacing: 60, +} as const satisfies DynamicFeeData + export enum PositionFlowStep { SELECT_TOKENS_AND_FEE_TIER, PRICE_RANGE, @@ -37,6 +48,7 @@ type BaseCreatePositionInfo = { protocolVersion: ProtocolVersion currencies: [OptionalCurrency, OptionalCurrency] creatingPoolOrPair?: boolean + poolOrPairLoading?: boolean } export type CreateV4PositionInfo = BaseCreatePositionInfo & { @@ -64,8 +76,6 @@ export type CreatePositionContextType = { derivedPositionInfo: CreatePositionInfo feeTierSearchModalOpen: boolean setFeeTierSearchModalOpen: Dispatch> - createPoolInfoDismissed: boolean - setCreatePoolInfoDismissed: Dispatch> } export interface PriceRangeState { diff --git a/apps/web/src/pages/Pool/Positions/create/utils.tsx b/apps/web/src/pages/Pool/Positions/create/utils.tsx index 5f7ebe5a527..80121966096 100644 --- a/apps/web/src/pages/Pool/Positions/create/utils.tsx +++ b/apps/web/src/pages/Pool/Positions/create/utils.tsx @@ -48,6 +48,7 @@ import { areCurrenciesEqual } from 'uniswap/src/utils/currencyId' import { getTickToPrice, getV4TickToPrice } from 'utils/getTickToPrice' type OptionalToken = Token | undefined +export function getSortedCurrenciesTuple(a: Token, b: Token): [Token, Token] export function getSortedCurrenciesTuple(a: OptionalToken, b: OptionalToken): [OptionalToken, OptionalToken] export function getSortedCurrenciesTuple(a: OptionalCurrency, b: OptionalCurrency): [OptionalCurrency, OptionalCurrency] export function getSortedCurrenciesTuple( @@ -65,6 +66,14 @@ export function getSortedCurrenciesTuple( return a.sortsBefore(b) ? [a, b] : [b, a] } +export function getSortedCurrenciesTupleWithWrap( + a: OptionalCurrency, + b: OptionalCurrency, + protocolVersion: ProtocolVersion, +): [OptionalCurrency, OptionalCurrency] { + return getSortedCurrenciesTuple(getCurrencyWithWrap(a, protocolVersion), getCurrencyWithWrap(b, protocolVersion)) +} + export function getCurrencyWithWrap(currency: Currency, protocolVersion: ProtocolVersion.V2 | ProtocolVersion.V3): Token export function getCurrencyWithWrap( currency: OptionalCurrency, @@ -107,9 +116,9 @@ export function getCurrencyAddressWithWrap( return currency?.wrapped.address } -function getCurrencyAddressForTradingApi(currency: Currency): string -function getCurrencyAddressForTradingApi(currency: OptionalCurrency): string | undefined -function getCurrencyAddressForTradingApi(currency: OptionalCurrency): string | undefined { +export function getCurrencyAddressForTradingApi(currency: Currency): string +export function getCurrencyAddressForTradingApi(currency: OptionalCurrency): string +export function getCurrencyAddressForTradingApi(currency: OptionalCurrency): string { return currency?.isToken ? currency.address : ZERO_ADDRESS } @@ -127,6 +136,10 @@ export function protocolShouldCalculateTaxes(protocolVersion: ProtocolVersion): return protocolVersion === ProtocolVersion.V3 } +export function validateCurrencyInput(currencies: [OptionalToken, OptionalToken]): currencies is [Token, Token] +export function validateCurrencyInput( + currencies: [OptionalCurrency, OptionalCurrency], +): currencies is [Currency, Currency] export function validateCurrencyInput( currencies: [OptionalCurrency, OptionalCurrency], ): currencies is [Currency, Currency] { @@ -317,8 +330,8 @@ function createMockV3Pool({ const wrappedPrice = new Price( price.baseCurrency.wrapped, price.quoteCurrency.wrapped, - price.numerator.toString(), - price.denominator.toString(), + price.denominator, + price.numerator, ) const invertedPrice = wrappedPrice.baseCurrency.sortsBefore(wrappedPrice.quoteCurrency) @@ -335,12 +348,14 @@ function createMockV4Pool({ baseToken, quoteToken, fee, + hook, price, invalidPrice, }: { baseToken?: Currency quoteToken?: Currency fee: FeeData + hook?: string price?: Price invalidPrice?: boolean }): V4Pool | undefined { @@ -355,7 +370,7 @@ function createMockV4Pool({ quoteToken, fee.feeAmount, fee.tickSpacing, - ZERO_ADDRESS, + hook ?? ZERO_ADDRESS, currentSqrt, JSBI.BigInt(0), currentTick, @@ -377,8 +392,8 @@ function createMockPair({ if (baseToken && quoteToken && price) { return new Pair( - CurrencyAmount.fromRawAmount(baseToken, price.denominator), - CurrencyAmount.fromRawAmount(quoteToken, price.numerator), + CurrencyAmount.fromRawAmount(baseToken, price.numerator), + CurrencyAmount.fromRawAmount(quoteToken, price.denominator), ) } else { return undefined @@ -533,13 +548,14 @@ export function getV3PriceRangeInfo({ getCurrencyWithWrap(baseCurrency, protocolVersion), getCurrencyWithWrap(quoteCurrency, protocolVersion), ] - const baseInitialPriceCurrency = state.initialPriceInverted - ? getCurrencyWithWrap(currencies[0], protocolVersion) - : getCurrencyWithWrap(currencies[1], protocolVersion) + const initialPriceTokens = getInvertedTuple( + [getCurrencyWithWrap(currencies[0], protocolVersion), getCurrencyWithWrap(currencies[1], protocolVersion)], + state.initialPriceInverted, + ) const price = derivedPositionInfo.creatingPoolOrPair ? getInitialPrice({ - baseCurrency: baseInitialPriceCurrency, + baseCurrency: initialPriceTokens[0], sortedCurrencies: sortedTokens, initialPrice: state.initialPrice, }) @@ -690,7 +706,7 @@ export function getV4PriceRangeInfo({ positionState: PositionState derivedPositionInfo: CreateV4PositionInfo }): V4PriceRangeInfo { - const { fee } = positionState + const { fee, hook } = positionState const { protocolVersion, currencies, pool } = derivedPositionInfo const sortedCurrencies = getSortedCurrenciesTuple(currencies[0], currencies[1]) @@ -714,6 +730,7 @@ export function getV4PriceRangeInfo({ baseToken: baseCurrency, quoteToken: quoteCurrency, fee, + hook, price, invalidPrice, }) diff --git a/apps/web/src/pages/Pool/Positions/index.tsx b/apps/web/src/pages/Pool/Positions/index.tsx index 286cb4a4d0f..be8947a11ec 100644 --- a/apps/web/src/pages/Pool/Positions/index.tsx +++ b/apps/web/src/pages/Pool/Positions/index.tsx @@ -1,11 +1,11 @@ /* eslint-disable-next-line no-restricted-imports */ import { PositionStatus, ProtocolVersion } from '@uniswap/client-pools/dist/pools/v1/types_pb' +import { useAccountDrawer } from 'components/AccountDrawer/MiniPortfolio/hooks' import { Pool } from 'components/Icons/Pool' import { LiquidityPositionCard } from 'components/Liquidity/LiquidityPositionCard' import { PositionInfo } from 'components/Liquidity/types' -import { parseRestPosition } from 'components/Liquidity/utils' +import { getPositionUrl, parseRestPosition } from 'components/Liquidity/utils' import { LoadingRows } from 'components/Loader/styled' -import { getChain } from 'constants/chains' import { useAccount } from 'hooks/useAccount' import { useAtom } from 'jotai' import { atomWithStorage } from 'jotai/utils' @@ -14,13 +14,16 @@ import { LoadingRow } from 'pages/Pool/Positions/shared' import { useCallback, useMemo, useState } from 'react' import { ChevronLeft, ChevronRight } from 'react-feather' import { useNavigate } from 'react-router-dom' +import { usePendingLPTransactionsChangeListener } from 'state/transactions/hooks' import { ClickableTamaguiStyle } from 'theme/components' import { Button, Flex, Text, useSporeColors } from 'ui/src' +import { InfoCircleFilled } from 'ui/src/components/icons/InfoCircleFilled' +import { X } from 'ui/src/components/icons/X' import { iconSizes } from 'ui/src/theme' import { useGetPositionsQuery } from 'uniswap/src/data/rest/getPositions' -import { useTranslation } from 'uniswap/src/i18n' -import { UniverseChainId } from 'uniswap/src/types/chains' -import { logger } from 'utilities/src/logger/logger' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { Trans, useTranslation } from 'uniswap/src/i18n' const PAGE_SIZE = 8 @@ -28,6 +31,7 @@ function EmptyPositionsView({ isConnected }: { isConnected: boolean }) { const colors = useSporeColors() const { t } = useTranslation() const navigate = useNavigate() + const accountDrawer = useAccountDrawer() return ( { + accountDrawer.toggle() + } + } > @@ -71,38 +83,31 @@ const statusFilterAtom = atomWithStorage('positions-status-fil export default function Positions() { const [chainFilter, setChainFilter] = useAtom(chainFilterAtom) + const { chains: currentModeChains } = useEnabledChains() const [versionFilter, setVersionFilter] = useAtom(versionFilterAtom) const [statusFilter, setStatusFilter] = useAtom(statusFilterAtom) + const [closedCTADismissed, setClosedCTADismissed] = useState(false) const navigate = useNavigate() const account = useAccount() const { address, isConnected } = account const [currentPage, setCurrentPage] = useState(0) - const { data, isLoading: positionsLoading } = useGetPositionsQuery( + const { data, isPlaceholderData, refetch } = useGetPositionsQuery( { address, - chainIds: chainFilter ? [chainFilter] : undefined, + chainIds: chainFilter ? [chainFilter] : currentModeChains, positionStatuses: statusFilter, protocolVersions: versionFilter, }, !isConnected, ) + usePendingLPTransactionsChangeListener(refetch) + const onNavigateToPosition = useCallback( (position: PositionInfo) => { - const chainInfo = getChain({ chainId: position.currency0Amount.currency.chainId }) - if (position.version === ProtocolVersion.V2) { - navigate(`/positions/v2/${chainInfo.urlParam}/${position.liquidityToken.address}`) - } else if (position.version === ProtocolVersion.V3) { - navigate(`/positions/v3/${chainInfo.urlParam}/${position.tokenId}`) - } else if (position.version === ProtocolVersion.V4) { - navigate(`/positions/v4/${chainInfo.urlParam}/${position.tokenId}`) - } else { - logger.error('Invalid position', { - tags: { file: 'Positions/index.tsx', function: 'onPress' }, - }) - } + navigate(getPositionUrl(position)) }, [navigate], ) @@ -116,7 +121,7 @@ export default function Positions() { return ( 0} + showFilters={account.isConnected} selectedChain={chainFilter} selectedVersions={versionFilter} selectedStatus={statusFilter} @@ -138,9 +143,9 @@ export default function Positions() { } }} /> - {!positionsLoading ? ( + {data || !account.address ? ( currentPageItems.length > 0 ? ( - + {currentPageItems.map((position, index) => { return ( position && ( @@ -157,8 +162,7 @@ export default function Positions() { ) : ( ) - ) : null} - {!data && positionsLoading && ( + ) : ( @@ -173,6 +177,33 @@ export default function Positions() { )} + {!statusFilter.includes(PositionStatus.CLOSED) && !closedCTADismissed && account.address && ( + + + + + + + + + + + + + setClosedCTADismissed(true)} cursor="pointer"> + + + + )} {!!pageCount && pageCount > 1 && data?.positions && ( void) { - const isAddLiquidityModalOpen = useModalIsOpen(ModalName.AddLiquidity) - const isRemoveLiquidityModalOpen = useModalIsOpen(ModalName.RemoveLiquidity) - const addLiquidityModalOpenPrev = usePrevious(isAddLiquidityModalOpen) - const removeLiquidityModalOpenPrev = usePrevious(isRemoveLiquidityModalOpen) - - useEffect(() => { - if (addLiquidityModalOpenPrev && !isAddLiquidityModalOpen) { - refetch() - } - if (removeLiquidityModalOpenPrev && !isRemoveLiquidityModalOpen) { - refetch() - } - }, [ - addLiquidityModalOpenPrev, - removeLiquidityModalOpenPrev, - isAddLiquidityModalOpen, - isRemoveLiquidityModalOpen, - refetch, - ]) -} diff --git a/apps/web/src/pages/Pool/index.tsx b/apps/web/src/pages/Pool/index.tsx index 91c0511ae15..1ca54b1aa92 100644 --- a/apps/web/src/pages/Pool/index.tsx +++ b/apps/web/src/pages/Pool/index.tsx @@ -48,7 +48,7 @@ export default function Pool() { - + {t('liquidity.learnMoreLabel')} () - const chainInfo = getSupportedGraphQlChain(useChainFromUrlParam()) + const urlChain = useChainIdFromUrlParam() + const chainInfo = urlChain ? getChainInfo(urlChain) : undefined const { data: poolData, loading } = usePoolData(poolAddress?.toLowerCase() ?? '', chainInfo?.id) const [isReversed, toggleReversed] = useReducer((x) => !x, false) const unwrappedTokens = getUnwrappedPoolToken(poolData, chainInfo?.id) diff --git a/apps/web/src/pages/RemoveLiquidity/RemoveLiquidityForm.tsx b/apps/web/src/pages/RemoveLiquidity/RemoveLiquidityForm.tsx index a2d1d27c124..d66a46985d7 100644 --- a/apps/web/src/pages/RemoveLiquidity/RemoveLiquidityForm.tsx +++ b/apps/web/src/pages/RemoveLiquidity/RemoveLiquidityForm.tsx @@ -1,11 +1,15 @@ +import { LoaderButton } from 'components/Button/LoaderButton' import { LiquidityModalDetailRows } from 'components/Liquidity/LiquidityModalDetailRows' import { LiquidityPositionInfo } from 'components/Liquidity/LiquidityPositionInfo' import { StyledPercentInput } from 'components/PercentInput' -import { DecreaseLiquidityStep, useLiquidityModalContext } from 'components/RemoveLiquidity/RemoveLiquidityModalContext' +import { + DecreaseLiquidityStep, + useRemoveLiquidityModalContext, +} from 'components/RemoveLiquidity/RemoveLiquidityModalContext' import { useRemoveLiquidityTxContext } from 'components/RemoveLiquidity/RemoveLiquidityTxContext' import { ClickablePill } from 'pages/Swap/Buy/PredefinedAmount' import { NumericalInputMimic, NumericalInputSymbolContainer, NumericalInputWrapper } from 'pages/Swap/common/shared' -import { Button, Flex, Text, useSporeColors } from 'ui/src' +import { Flex, Text, useSporeColors } from 'ui/src' import { Trans, useTranslation } from 'uniswap/src/i18n' import useResizeObserver from 'use-resize-observer' @@ -14,7 +18,7 @@ export function RemoveLiquidityForm() { const { t } = useTranslation() const colors = useSporeColors() - const { percent, positionInfo, setPercent, setStep, percentInvalid } = useLiquidityModalContext() + const { percent, positionInfo, setPercent, setStep, percentInvalid } = useRemoveLiquidityModalContext() const removeLiquidityTxContext = useRemoveLiquidityTxContext() const { gasFeeEstimateUSD, txContext } = removeLiquidityTxContext @@ -80,17 +84,18 @@ export function RemoveLiquidityForm() { currency1Amount={currency1Amount} networkCost={gasFeeEstimateUSD} /> - + ) } diff --git a/apps/web/src/pages/RemoveLiquidity/RemoveLiquidityModal.tsx b/apps/web/src/pages/RemoveLiquidity/RemoveLiquidityModal.tsx index d53098a2251..2fb964bc615 100644 --- a/apps/web/src/pages/RemoveLiquidity/RemoveLiquidityModal.tsx +++ b/apps/web/src/pages/RemoveLiquidity/RemoveLiquidityModal.tsx @@ -3,7 +3,7 @@ import { LiquidityModalHeader } from 'components/Liquidity/LiquidityModalHeader' import { DecreaseLiquidityStep, RemoveLiquidityModalContextProvider, - useLiquidityModalContext, + useRemoveLiquidityModalContext, } from 'components/RemoveLiquidity/RemoveLiquidityModalContext' import { RemoveLiquidityReview } from 'components/RemoveLiquidity/RemoveLiquidityReview' import { RemoveLiquidityTxContextProvider } from 'components/RemoveLiquidity/RemoveLiquidityTxContext' @@ -17,7 +17,7 @@ import { useTranslation } from 'uniswap/src/i18n' function RemoveLiquidityModalInner() { const closeModal = useCloseModal(ModalName.RemoveLiquidity) const { t } = useTranslation() - const { step, setStep } = useLiquidityModalContext() + const { step, setStep } = useRemoveLiquidityModalContext() let modalContent switch (step) { diff --git a/apps/web/src/pages/RemoveLiquidity/index.tsx b/apps/web/src/pages/RemoveLiquidity/V2.tsx similarity index 97% rename from apps/web/src/pages/RemoveLiquidity/index.tsx rename to apps/web/src/pages/RemoveLiquidity/V2.tsx index f1f91aeec0e..69a356bf2cd 100644 --- a/apps/web/src/pages/RemoveLiquidity/index.tsx +++ b/apps/web/src/pages/RemoveLiquidity/V2.tsx @@ -24,7 +24,6 @@ import { V2Unsupported } from 'components/V2Unsupported' import { AutoColumn, ColumnCenter } from 'components/deprecated/Column' import Row, { RowBetween, RowFixed } from 'components/deprecated/Row' import { Dots } from 'components/swap/styled' -import { useIsSupportedChainId } from 'constants/chains' import { useCurrency } from 'hooks/Tokens' import { useAccount } from 'hooks/useAccount' import { ApprovalState, useApproveCallback } from 'hooks/useApproveCallback' @@ -40,7 +39,7 @@ import { PositionPageUnsupportedContent } from 'pages/LegacyPool/PositionPage' import { ClickableText, MaxButton, Wrapper } from 'pages/LegacyPool/styled' import { useCallback, useMemo, useState } from 'react' import { ArrowDown, Plus } from 'react-feather' -import { useNavigate, useParams } from 'react-router-dom' +import { Navigate, useNavigate, useParams } from 'react-router-dom' import { Field } from 'state/burn/actions' import { useBurnActionHandlers, useBurnState, useDerivedBurnInfo } from 'state/burn/hooks' import { useTransactionAdder } from 'state/transactions/hooks' @@ -49,6 +48,10 @@ import { useUserSlippageToleranceWithDefault } from 'state/user/hooks' import { StyledInternalLink, ThemedText } from 'theme/components' import { Text } from 'ui/src' import { WRAPPED_NATIVE_CURRENCY } from 'uniswap/src/constants/tokens' +import { useEnabledChains, useIsSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { toGraphQLChain } from 'uniswap/src/features/chains/utils' +import { FeatureFlags } from 'uniswap/src/features/gating/flags' +import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import Trace from 'uniswap/src/features/telemetry/Trace' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { Trans } from 'uniswap/src/i18n' @@ -60,11 +63,20 @@ import { currencyId } from 'utils/currencyId' const DEFAULT_REMOVE_LIQUIDITY_SLIPPAGE_TOLERANCE = new Percent(50, 10_000) -export default function RemoveLiquidityWrapper() { +export default function RemoveLiquidityV2() { const { chainId } = useAccount() + const isV4EverywhereEnabled = useFeatureFlag(FeatureFlags.V4Everywhere) const isSupportedChain = useIsSupportedChainId(chainId) const { currencyIdA, currencyIdB } = useParams<{ currencyIdA: string; currencyIdB: string }>() const [currencyA, currencyB] = [useCurrency(currencyIdA) ?? undefined, useCurrency(currencyIdB) ?? undefined] + const { defaultChainId } = useEnabledChains() + + if (isV4EverywhereEnabled) { + // TODO(WEB-5361): prefill poolId from legacy URL /remove/ETH/0x123 + const chainName = toGraphQLChain(chainId ?? defaultChainId).toLowerCase() + return + } + if (isSupportedChain && currencyA !== currencyB) { return } else { diff --git a/apps/web/src/pages/RemoveLiquidity/V3.tsx b/apps/web/src/pages/RemoveLiquidity/V3.tsx index ce89b465cef..8a03414f301 100644 --- a/apps/web/src/pages/RemoveLiquidity/V3.tsx +++ b/apps/web/src/pages/RemoveLiquidity/V3.tsx @@ -15,7 +15,6 @@ import TransactionConfirmationModal, { ConfirmationModalContent } from 'componen import { AutoColumn } from 'components/deprecated/Column' import { AutoRow, RowBetween, RowFixed } from 'components/deprecated/Row' import { Break } from 'components/earn/styled' -import { useIsSupportedChainId } from 'constants/chains' import { useAccount } from 'hooks/useAccount' import { useV3NFTPositionManagerContract } from 'hooks/useContract' import useDebouncedChangeHandler from 'hooks/useDebouncedChangeHandler' @@ -36,6 +35,10 @@ import { useUserSlippageToleranceWithDefault } from 'state/user/hooks' import { ThemedText } from 'theme/components' import { Switch, Text } from 'ui/src' import { WRAPPED_NATIVE_CURRENCY } from 'uniswap/src/constants/tokens' +import { useEnabledChains, useIsSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { toGraphQLChain } from 'uniswap/src/features/chains/utils' +import { FeatureFlags } from 'uniswap/src/features/gating/flags' +import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { Trans } from 'uniswap/src/i18n' import { logger } from 'utilities/src/logger/logger' @@ -50,6 +53,7 @@ const DEFAULT_REMOVE_V3_LIQUIDITY_SLIPPAGE_TOLERANCE = new Percent(50, 10_000) // redirect invalid tokenIds export default function RemoveLiquidityV3() { const { chainId } = useAccount() + const { defaultChainId } = useEnabledChains() const isSupportedChain = useIsSupportedChainId(chainId) const { tokenId } = useParams<{ tokenId: string }>() const location = useLocation() @@ -62,6 +66,11 @@ export default function RemoveLiquidityV3() { }, [tokenId]) const { position, loading } = useV3PositionFromTokenId(parsedTokenId ?? undefined) + const isV4EverywhereEnabled = useFeatureFlag(FeatureFlags.V4Everywhere) + if (isV4EverywhereEnabled) { + const chainName = toGraphQLChain(chainId ?? defaultChainId).toLowerCase() + return + } if (parsedTokenId === null || parsedTokenId.eq(0)) { return } diff --git a/apps/web/src/pages/RouteDefinitions.tsx b/apps/web/src/pages/RouteDefinitions.tsx index a510c44b2f5..8e736d1278b 100644 --- a/apps/web/src/pages/RouteDefinitions.tsx +++ b/apps/web/src/pages/RouteDefinitions.tsx @@ -15,7 +15,7 @@ const NftExplore = lazy(() => import('nft/pages/explore')) const Collection = lazy(() => import('nft/pages/collection')) const Profile = lazy(() => import('nft/pages/profile')) const Asset = lazy(() => import('nft/pages/asset/Asset')) -const AddLiquidityWithTokenRedirects = lazy(() => import('pages/AddLiquidity/redirects')) +const AddLiquidityV3WithTokenRedirects = lazy(() => import('pages/AddLiquidityV3/redirects')) const AddLiquidityV2WithTokenRedirects = lazy(() => import('pages/AddLiquidityV2/redirects')) const RedirectExplore = lazy(() => import('pages/Explore/redirects')) const MigrateV2 = lazy(() => import('pages/MigrateV2')) @@ -23,14 +23,22 @@ const MigrateV2Pair = lazy(() => import('pages/MigrateV2/MigrateV2Pair')) const MigrateV3 = lazy(() => import('pages/MigrateV3')) const NotFound = lazy(() => import('pages/NotFound')) const Pool = lazy(() => import('pages/Pool')) -const LegacyPool = lazy(() => import('pages/LegacyPool')) -const LegacyPositionPage = lazy(() => import('pages/LegacyPool/PositionPage')) +const LegacyPoolRedirects = lazy(() => + import('pages/LegacyPool/redirects').then((module) => ({ default: module.LegacyPoolRedirects })), +) +const PoolFinderRedirects = lazy(() => + import('pages/LegacyPool/redirects').then((module) => ({ default: module.PoolFinderRedirects })), +) +const LegacyPoolV2Redirects = lazy(() => + import('pages/LegacyPool/redirects').then((module) => ({ default: module.LegacyPoolV2Redirects })), +) +const LegacyPositionPageRedirects = lazy(() => + import('pages/LegacyPool/redirects').then((module) => ({ default: module.LegacyPositionPageRedirects })), +) const PositionPage = lazy(() => import('pages/Pool/Positions/PositionPage')) const V2PositionPage = lazy(() => import('pages/Pool/Positions/V2PositionPage')) -const LegacyPoolV2 = lazy(() => import('pages/LegacyPool/v2')) const PoolDetails = lazy(() => import('pages/PoolDetails')) -const PoolFinder = lazy(() => import('pages/PoolFinder')) -const RemoveLiquidity = lazy(() => import('pages/RemoveLiquidity')) +const RemoveLiquidityV2 = lazy(() => import('pages/RemoveLiquidity/V2')) const RemoveLiquidityV3 = lazy(() => import('pages/RemoveLiquidity/V3')) const TokenDetails = lazy(() => import('pages/TokenDetails')) @@ -248,49 +256,49 @@ export const routes: RouteDefinition[] = [ // Legacy pool routes createRouteDefinition({ path: '/pool', - getElement: () => , + getElement: () => , getTitle: getPositionPageTitle, getDescription: getPositionPageDescription, }), createRouteDefinition({ path: '/pool/v2/find', - getElement: () => , - getTitle: () => t('title.importLiquidityv2'), - getDescription: () => t('title.useImportTool'), + getElement: () => , + getTitle: getPositionPageDescription, + getDescription: getPositionPageDescription, }), createRouteDefinition({ path: '/pool/v2', - getElement: () => , + getElement: () => , getTitle: getPositionPageTitle, getDescription: getPositionPageDescription, }), createRouteDefinition({ path: '/pool/:tokenId', - getElement: () => , + getElement: () => , getTitle: getPositionPageTitle, getDescription: getPositionPageDescription, }), createRouteDefinition({ path: '/pools/v2/find', - getElement: () => , - getTitle: () => t('title.importLiquidityv2'), - getDescription: () => t('title.useImportTool'), + getElement: () => , + getTitle: getPositionPageTitle, + getDescription: getPositionPageDescription, }), createRouteDefinition({ path: '/pools/v2', - getElement: () => , + getElement: () => , getTitle: getPositionPageTitle, getDescription: getPositionPageDescription, }), createRouteDefinition({ path: '/pools', - getElement: () => , + getElement: () => , getTitle: getPositionPageTitle, getDescription: getPositionPageDescription, }), createRouteDefinition({ path: '/pools/:tokenId', - getElement: () => , + getElement: () => , getTitle: getPositionPageTitle, getDescription: getPositionPageDescription, }), @@ -309,13 +317,13 @@ export const routes: RouteDefinition[] = [ ':currencyIdA/:currencyIdB/:feeAmount', ':currencyIdA/:currencyIdB/:feeAmount/:tokenId', ], - getElement: () => , + getElement: () => , getTitle: getAddLiquidityPageTitle, getDescription: () => StaticTitlesAndDescriptions.AddLiquidityDescription, }), createRouteDefinition({ path: '/remove/v2/:currencyIdA/:currencyIdB', - getElement: () => , + getElement: () => , getTitle: () => t('title.removeLiquidityv2'), getDescription: () => t('title.removeTokensv2'), }), diff --git a/apps/web/src/pages/Swap/Buy/BuyForm.tsx b/apps/web/src/pages/Swap/Buy/BuyForm.tsx index 788f48a6c3f..f6ac17c821b 100644 --- a/apps/web/src/pages/Swap/Buy/BuyForm.tsx +++ b/apps/web/src/pages/Swap/Buy/BuyForm.tsx @@ -1,6 +1,4 @@ import { useAccount } from 'hooks/useAccount' -import { useActiveLocalCurrencyComponents } from 'hooks/useActiveLocalCurrency' -import useParsedQueryString from 'hooks/useParsedQueryString' import { BuyFormButton } from 'pages/Swap/Buy/BuyFormButton' import { BuyFormContextProvider, ethCurrencyInfo, useBuyFormContext } from 'pages/Swap/Buy/BuyFormContext' import { ChooseProviderModal } from 'pages/Swap/Buy/ChooseProviderModal' @@ -16,6 +14,9 @@ import { } from 'pages/Swap/common/shared' import { useEffect } from 'react' import { Flex, Text, styled } from 'ui/src' +import { useUrlContext } from 'uniswap/src/contexts/UrlContext' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { useAppFiatCurrency, useFiatCurrencyComponents } from 'uniswap/src/features/fiatCurrency/hooks' import { FiatOnRampCountryPicker } from 'uniswap/src/features/fiatOnRamp/FiatOnRampCountryPicker' import { SelectTokenButton } from 'uniswap/src/features/fiatOnRamp/SelectTokenButton' import { useFiatOnRampAggregatorGetCountryQuery } from 'uniswap/src/features/fiatOnRamp/api' @@ -23,7 +24,6 @@ import Trace from 'uniswap/src/features/telemetry/Trace' import { FiatOnRampEventName, InterfacePageNameLocal } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { useTranslation } from 'uniswap/src/i18n' -import { UniverseChainId } from 'uniswap/src/types/chains' import useResizeObserver from 'use-resize-observer' import { useFormatter } from 'utils/formatNumbers' @@ -56,7 +56,8 @@ function BuyFormInner({ disabled }: BuyFormProps) { const account = useAccount() const { t } = useTranslation() const { convertToFiatAmount } = useFormatter() - const { symbol: fiatSymbol } = useActiveLocalCurrencyComponents() + const fiatCurrency = useAppFiatCurrency() + const { symbol: fiatSymbol } = useFiatCurrencyComponents(fiatCurrency) const { buyFormState, setBuyFormState, derivedBuyFormInfo } = useBuyFormContext() const { inputAmount, selectedCountry, quoteCurrency, currencyModalOpen, countryModalOpen, providerModalOpen } = @@ -82,6 +83,7 @@ function BuyFormInner({ disabled }: BuyFormProps) { } }, [buyFormState.selectedCountry, countryResult, selectedCountry, setBuyFormState]) + const { useParsedQueryString } = useUrlContext() const parsedQs = useParsedQueryString() useEffect(() => { const quoteCurrencyCode = parsedQs.quoteCurrencyCode diff --git a/apps/web/src/pages/Swap/Buy/BuyFormButton.tsx b/apps/web/src/pages/Swap/Buy/BuyFormButton.tsx index c12c63400fe..7f2c789e943 100644 --- a/apps/web/src/pages/Swap/Buy/BuyFormButton.tsx +++ b/apps/web/src/pages/Swap/Buy/BuyFormButton.tsx @@ -1,9 +1,9 @@ import { useAccountDrawer } from 'components/AccountDrawer/MiniPortfolio/hooks' +import { LoaderButton } from 'components/Button/LoaderButton' import { ButtonLight } from 'components/Button/buttons' import { ConnectWalletButtonText } from 'components/NavBar/accountCTAsExperimentUtils' import { useBuyFormContext } from 'pages/Swap/Buy/BuyFormContext' -import { Button, Flex, SpinningLoader, Text, WidthAnimator } from 'ui/src' -import { iconSizes } from 'ui/src/theme' +import { Button, Text } from 'ui/src' import { useTranslation } from 'uniswap/src/i18n' import { useAccount } from 'wagmi' @@ -46,25 +46,17 @@ export function BuyFormButton({ forceDisabled }: BuyFormButtonProps) { } return ( - + + {t('common.button.continue')} + + ) } diff --git a/apps/web/src/pages/Swap/Buy/BuyFormContext.tsx b/apps/web/src/pages/Swap/Buy/BuyFormContext.tsx index 8f75a95209b..46bcf138686 100644 --- a/apps/web/src/pages/Swap/Buy/BuyFormContext.tsx +++ b/apps/web/src/pages/Swap/Buy/BuyFormContext.tsx @@ -5,6 +5,7 @@ import { formatFiatOnRampFiatAmount } from 'pages/Swap/Buy/shared' import { Dispatch, PropsWithChildren, SetStateAction, createContext, useContext, useMemo, useState } from 'react' import { buildPartialCurrencyInfo } from 'uniswap/src/constants/routing' import { nativeOnChain } from 'uniswap/src/constants/tokens' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { useFiatOnRampAggregatorCountryListQuery, useFiatOnRampAggregatorCryptoQuoteQuery, @@ -15,6 +16,7 @@ import { FORSupportedCountriesResponse, FiatCurrencyInfo, FiatOnRampCurrency, + RampDirection, } from 'uniswap/src/features/fiatOnRamp/types' import { InvalidRequestAmountTooLow, @@ -23,7 +25,6 @@ import { isInvalidRequestAmountTooLow, } from 'uniswap/src/features/fiatOnRamp/utils' import { t } from 'uniswap/src/i18n' -import { UniverseChainId } from 'uniswap/src/types/chains' import { useAccount } from 'wagmi' class BuyFormError extends Error { @@ -102,7 +103,9 @@ function useDerivedBuyFormInfo(state: BuyFormState): BuyInfo { const { meldSupportedFiatCurrency, notAvailableInThisRegion } = useMeldFiatCurrencyInfo(state.selectedCountry) - const { data: countryOptionsResult } = useFiatOnRampAggregatorCountryListQuery() + const { data: countryOptionsResult } = useFiatOnRampAggregatorCountryListQuery({ + rampDirection: RampDirection.ONRAMP, + }) const supportedTokens = useFiatOnRampSupportedTokens(meldSupportedFiatCurrency, state.selectedCountry?.countryCode) const { diff --git a/apps/web/src/pages/Swap/Buy/hooks.ts b/apps/web/src/pages/Swap/Buy/hooks.ts index ec8eba69460..ff1e9291d2e 100644 --- a/apps/web/src/pages/Swap/Buy/hooks.ts +++ b/apps/web/src/pages/Swap/Buy/hooks.ts @@ -1,8 +1,10 @@ import { meldSupportedCurrencyToCurrencyInfo } from 'graphql/data/types' -import { useActiveLocalCurrency } from 'hooks/useActiveLocalCurrency' -import { useActiveLocale } from 'hooks/useActiveLocale' import { useMemo } from 'react' -import { getFiatCurrencyName } from 'uniswap/src/features/fiatCurrency/hooks' +import { + getFiatCurrencyName, + useAppFiatCurrency, + useFiatCurrencyComponents, +} from 'uniswap/src/features/fiatCurrency/hooks' import { useFiatOnRampAggregatorSupportedFiatCurrenciesQuery, useFiatOnRampAggregatorSupportedTokensQuery, @@ -34,8 +36,8 @@ export function useMeldFiatCurrencyInfo(selectedCountry?: FORCountry): FiatOnRam countryCode: selectedCountry?.countryCode ?? 'US', }) - const activeLocale = useActiveLocale() - const activeLocalCurrency = useActiveLocalCurrency() + const activeLocalCurrency = useAppFiatCurrency() + const fiatCurrencyComponents = useFiatCurrencyComponents(activeLocalCurrency) const { t } = useTranslation() const appFiatCurrencySupported = @@ -44,16 +46,15 @@ export function useMeldFiatCurrencyInfo(selectedCountry?: FORCountry): FiatOnRam (currency): boolean => activeLocalCurrency.toLowerCase() === currency.fiatCurrencyCode.toLowerCase(), ) const meldSupportedFiatCurrency: FiatCurrencyInfo = useMemo(() => { - const activeLocalCurrencyComponents = getFiatCurrencyComponents(activeLocale, activeLocalCurrency) const { name, shortName } = getFiatCurrencyName(t, activeLocalCurrency) const activeLocalCurrencyFiatCurrencyInfo: FiatCurrencyInfo = { - ...activeLocalCurrencyComponents, + ...fiatCurrencyComponents, name, shortName, code: activeLocalCurrency, } return appFiatCurrencySupported ? activeLocalCurrencyFiatCurrencyInfo : fallbackCurrencyInfo - }, [activeLocalCurrency, activeLocale, appFiatCurrencySupported, t]) + }, [activeLocalCurrency, appFiatCurrencySupported, fiatCurrencyComponents, t]) return { meldSupportedFiatCurrency, diff --git a/apps/web/src/pages/Swap/Limit/LimitForm.tsx b/apps/web/src/pages/Swap/Limit/LimitForm.tsx index fe90706ce94..a56ee65b735 100644 --- a/apps/web/src/pages/Swap/Limit/LimitForm.tsx +++ b/apps/web/src/pages/Swap/Limit/LimitForm.tsx @@ -16,9 +16,9 @@ import { ConnectWalletButtonText } from 'components/NavBar/accountCTAsExperiment import Column from 'components/deprecated/Column' import Row from 'components/deprecated/Row' import { ArrowContainer, ArrowWrapper, SwapSection } from 'components/swap/styled' -import { getChain, useIsSupportedChainId, useIsUniswapXSupportedChain } from 'constants/chains' import { ZERO_PERCENT } from 'constants/misc' import { useAccount } from 'hooks/useAccount' +import { useIsUniswapXSupportedChain } from 'hooks/useIsUniswapXSupportedChain' import usePermit2Allowance, { AllowanceState } from 'hooks/usePermit2Allowance' import { SwapResult, useSwapCallback } from 'hooks/useSwapCallback' import { useUSDPrice } from 'hooks/useUSDPrice' @@ -39,6 +39,8 @@ import { Anchor, Text, styled as tamaguiStyled } from 'ui/src' import { AlertTriangleFilled } from 'ui/src/components/icons/AlertTriangleFilled' import { nativeOnChain } from 'uniswap/src/constants/tokens' import { uniswapUrls } from 'uniswap/src/constants/urls' +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' +import { useIsSupportedChainId } from 'uniswap/src/features/chains/hooks' import { Locale } from 'uniswap/src/features/language/constants' import Trace from 'uniswap/src/features/telemetry/Trace' import { ElementName, InterfacePageNameLocal } from 'uniswap/src/features/telemetry/constants' @@ -206,7 +208,7 @@ function LimitForm({ onCurrencyChange }: LimitFormProps) { useEffect(() => { if (!outputCurrency && isSupportedChain) { - const stablecoinCurrency = getChain({ chainId }).spotPriceStablecoinAmount.currency + const stablecoinCurrency = getChainInfo(chainId).spotPriceStablecoinAmount.currency onSelectCurrency( 'outputCurrency', inputCurrency?.equals(stablecoinCurrency) ? nativeOnChain(chainId) : stablecoinCurrency, @@ -220,7 +222,7 @@ function LimitForm({ onCurrencyChange }: LimitFormProps) { ? [inputCurrency, outputCurrency] : [outputCurrency, inputCurrency] if (nativeCurrency.wrapped.equals(nonNativeCurrency)) { - onSelectCurrency('outputCurrency', getChain({ chainId }).spotPriceStablecoinAmount.currency) + onSelectCurrency('outputCurrency', getChainInfo(chainId).spotPriceStablecoinAmount.currency) } } // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/apps/web/src/pages/Swap/Send/SendCurrencyInputForm.tsx b/apps/web/src/pages/Swap/Send/SendCurrencyInputForm.tsx index d3025def6d3..066445b5a62 100644 --- a/apps/web/src/pages/Swap/Send/SendCurrencyInputForm.tsx +++ b/apps/web/src/pages/Swap/Send/SendCurrencyInputForm.tsx @@ -8,9 +8,7 @@ import { isInputGreaterThanDecimals } from 'components/NumericalInput' import CurrencySearchModal from 'components/SearchModal/CurrencySearchModal' import Column from 'components/deprecated/Column' import Row, { RowBetween } from 'components/deprecated/Row' -import { getChain, useSupportedChainId } from 'constants/chains' import { PrefetchBalancesWrapper } from 'graphql/data/apollo/AdaptiveTokenBalancesProvider' -import { useActiveLocalCurrency, useActiveLocalCurrencyComponents } from 'hooks/useActiveLocalCurrency' import { useUSDPrice } from 'hooks/useUSDPrice' import styled, { css } from 'lib/styled-components' import { @@ -28,10 +26,12 @@ import { useSwapAndLimitContext } from 'state/swap/useSwapContext' import { ClickableStyle, ThemedText } from 'theme/components' import { Text } from 'ui/src' import { ArrowUpDown } from 'ui/src/components/icons/ArrowUpDown' -import { useEnabledChains } from 'uniswap/src/features/settings/hooks' +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' +import { useEnabledChains, useSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { useAppFiatCurrency, useFiatCurrencyComponents } from 'uniswap/src/features/fiatCurrency/hooks' import Trace from 'uniswap/src/features/telemetry/Trace' import { Trans } from 'uniswap/src/i18n' -import { UniverseChainId } from 'uniswap/src/types/chains' import useResizeObserver from 'use-resize-observer' import { NumberType, useFormatter } from 'utils/formatNumbers' import { maxAmountSpend } from 'utils/maxAmountSpend' @@ -114,7 +114,7 @@ const AlternateCurrencyDisplayRow = styled(Row)<{ $disabled: boolean }>` const AlternateCurrencyDisplay = ({ disabled, onToggle }: { disabled: boolean; onToggle: () => void }) => { const { formatConvertedFiatNumberOrString, formatNumberOrString } = useFormatter() - const activeCurrency = useActiveLocalCurrency() + const activeCurrency = useAppFiatCurrency() const { sendState, derivedSendInfo } = useSendContext() const { inputCurrency, inputInFiat } = sendState @@ -188,10 +188,12 @@ export default function SendCurrencyInputForm({ onCurrencyChange?: (selected: CurrencyState) => void }) { const { chainId } = useSwapAndLimitContext() - const supportedChain = useSupportedChainId(chainId) + const { defaultChainId } = useEnabledChains() + const supportedChainId = useSupportedChainId(chainId) const { isTestnetModeEnabled } = useEnabledChains() const { formatCurrencyAmount } = useFormatter() - const { symbol: fiatSymbol } = useActiveLocalCurrencyComponents() + const appFiatCurrency = useAppFiatCurrency() + const { symbol: fiatSymbol } = useFiatCurrencyComponents(appFiatCurrency) const { formatNumber } = useFormatter() const { sendState, setSendState, derivedSendInfo } = useSendContext() @@ -202,8 +204,8 @@ export default function SendCurrencyInputForm({ const [tokenSelectorOpen, setTokenSelectorOpen] = useState(false) const fiatCurrency = useMemo( - () => getChain({ chainId: supportedChain, withFallback: true }).spotPriceStablecoinAmount.currency, - [supportedChain], + () => getChainInfo(supportedChainId ?? defaultChainId).spotPriceStablecoinAmount.currency, + [defaultChainId, supportedChainId], ) const fiatCurrencyEqualsTransferCurrency = !!inputCurrency && fiatCurrency.equals(inputCurrency) diff --git a/apps/web/src/pages/Swap/Send/SendForm.tsx b/apps/web/src/pages/Swap/Send/SendForm.tsx index 17a3e7ec3f0..fea263ef01b 100644 --- a/apps/web/src/pages/Swap/Send/SendForm.tsx +++ b/apps/web/src/pages/Swap/Send/SendForm.tsx @@ -3,7 +3,6 @@ import { useAccountDrawer } from 'components/AccountDrawer/MiniPortfolio/hooks' import { ButtonLight, ButtonPrimary } from 'components/Button/buttons' import { ConnectWalletButtonText } from 'components/NavBar/accountCTAsExperimentUtils' import Column from 'components/deprecated/Column' -import { useIsSupportedChainId } from 'constants/chains' import { useAccount } from 'hooks/useAccount' import { useGroupedRecentTransfers } from 'hooks/useGroupedRecentTransfers' import useSelectChain from 'hooks/useSelectChain' @@ -17,7 +16,8 @@ import { useCallback, useEffect, useMemo, useState } from 'react' import { SendContextProvider, useSendContext } from 'state/send/SendContext' import { CurrencyState } from 'state/swap/types' import { useSwapAndLimitContext } from 'state/swap/useSwapContext' -import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' +import { useIsSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { getChainLabel } from 'uniswap/src/features/chains/utils' import Trace from 'uniswap/src/features/telemetry/Trace' import { InterfacePageNameLocal } from 'uniswap/src/features/telemetry/constants' import { Trans } from 'uniswap/src/i18n' @@ -205,7 +205,7 @@ function SendFormInner({ disableTokenInputs = false, onCurrencyChange }: SendFor await selectChain(initialChainId)}> ) : ( diff --git a/apps/web/src/pages/Swap/Send/SendReviewModal.tsx b/apps/web/src/pages/Swap/Send/SendReviewModal.tsx index 028298625fa..ba40223f522 100644 --- a/apps/web/src/pages/Swap/Send/SendReviewModal.tsx +++ b/apps/web/src/pages/Swap/Send/SendReviewModal.tsx @@ -15,9 +15,9 @@ import { useSwapAndLimitContext } from 'state/swap/useSwapContext' import { Separator, ThemedText } from 'theme/components' import { capitalize } from 'tsafe' import { Unitag } from 'ui/src/components/icons/Unitag' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import Trace from 'uniswap/src/features/telemetry/Trace' import { Trans, useTranslation } from 'uniswap/src/i18n' -import { UniverseChainId } from 'uniswap/src/types/chains' import { shortenAddress } from 'utilities/src/addresses' import { NumberType, useFormatter } from 'utils/formatNumbers' diff --git a/apps/web/src/pages/Swap/SwapForm.tsx b/apps/web/src/pages/Swap/SwapForm.tsx index ed575db0efa..23135029a71 100644 --- a/apps/web/src/pages/Swap/SwapForm.tsx +++ b/apps/web/src/pages/Swap/SwapForm.tsx @@ -21,7 +21,6 @@ import PriceImpactModal from 'components/swap/PriceImpactModal' import SwapDetailsDropdown from 'components/swap/SwapDetailsDropdown' import confirmPriceImpactWithoutFee from 'components/swap/confirmPriceImpactWithoutFee' import { ArrowContainer, ArrowWrapper, OutputSwapSection, SwapSection } from 'components/swap/styled' -import { useIsSupportedChainId, useSupportedChainId } from 'constants/chains' import { useCurrencyInfo } from 'hooks/Tokens' import { useAccount } from 'hooks/useAccount' import { useIsLandingPage } from 'hooks/useIsLandingPage' @@ -49,15 +48,16 @@ import { CurrencyState } from 'state/swap/types' import { useSwapAndLimitContext, useSwapContext } from 'state/swap/useSwapContext' import { ExternalLink, ThemedText } from 'theme/components' import { Text } from 'ui/src' -import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' import { SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' +import { useIsSupportedChainId, useSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { getChainLabel } from 'uniswap/src/features/chains/utils' import Trace from 'uniswap/src/features/telemetry/Trace' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { maybeLogFirstSwapAction } from 'uniswap/src/features/transactions/swap/utils/maybeLogFirstSwapAction' import { WrapType } from 'uniswap/src/features/transactions/types/wrap' import { Trans } from 'uniswap/src/i18n' -import { UniverseChainId } from 'uniswap/src/types/chains' import { CurrencyField } from 'uniswap/src/types/currency' import { logger } from 'utilities/src/logger/logger' import { useTrace } from 'utilities/src/telemetry/trace/TraceContext' @@ -98,15 +98,22 @@ export function SwapForm({ setDismissTokenWarning(true) }, []) - // dismiss warning if all imported tokens are in active lists - const urlTokensNotInDefault = useMemo(() => { - return prefilledInputCurrencyInfo || prefilledOutputCurrencyInfo - ? [prefilledInputCurrencyInfo, prefilledOutputCurrencyInfo] - .filter((token): token is CurrencyInfo => { - return (token?.currency.isToken && token.safetyLevel !== SafetyLevel.Verified) ?? false - }) - .map((token: CurrencyInfo) => token.currency as Token) - : [] + // dismiss warning if prefilled tokens don't have warnings + const prefilledTokensWithWarnings: { field: CurrencyField; token: Token }[] = useMemo(() => { + const tokens = [] + if ( + prefilledInputCurrencyInfo?.currency.isToken && + prefilledInputCurrencyInfo.safetyLevel !== SafetyLevel.Verified + ) { + tokens.push({ field: CurrencyField.INPUT, token: prefilledInputCurrencyInfo.currency as Token }) + } + if ( + prefilledOutputCurrencyInfo?.currency.isToken && + prefilledOutputCurrencyInfo.safetyLevel !== SafetyLevel.Verified + ) { + tokens.push({ field: CurrencyField.OUTPUT, token: prefilledOutputCurrencyInfo.currency as Token }) + } + return tokens }, [prefilledInputCurrencyInfo, prefilledOutputCurrencyInfo]) const theme = useTheme() @@ -505,23 +512,25 @@ export function SwapForm({ return ( <> - 0 && !dismissTokenWarning} - token0={urlTokensNotInDefault[0]} - token1={urlTokensNotInDefault[1]} - onAcknowledge={handleConfirmTokenWarning} - onReject={() => { - setDismissTokenWarning(true) - onCurrencySelection(CurrencyField.INPUT, undefined) - onCurrencySelection(CurrencyField.OUTPUT, undefined) - }} - closeModalOnly={() => { - setDismissTokenWarning(true) - }} - onToken0BlockAcknowledged={() => onCurrencySelection(CurrencyField.INPUT, undefined)} - onToken1BlockAcknowledged={() => onCurrencySelection(CurrencyField.OUTPUT, undefined)} - showCancel={true} - /> + {prefilledTokensWithWarnings.length >= 1 && ( + = 1 && !dismissTokenWarning} + token0={prefilledTokensWithWarnings[0].token} + token1={prefilledTokensWithWarnings[1]?.token} + onAcknowledge={handleConfirmTokenWarning} + onReject={() => { + setDismissTokenWarning(true) + onCurrencySelection(CurrencyField.INPUT, undefined) + onCurrencySelection(CurrencyField.OUTPUT, undefined) + }} + closeModalOnly={() => { + setDismissTokenWarning(true) + }} + onToken0BlockAcknowledged={() => onCurrencySelection(prefilledTokensWithWarnings[0].field, undefined)} + onToken1BlockAcknowledged={() => onCurrencySelection(prefilledTokensWithWarnings[1].field, undefined)} + showCancel={true} + /> + )} {trade && showConfirm && ( @@ -691,7 +700,7 @@ export function SwapForm({ await selectChain(initialChainId)}> ) : showWrap ? ( diff --git a/apps/web/src/pages/Swap/index.tsx b/apps/web/src/pages/Swap/index.tsx index 79ddfb5460c..b7afc883c0f 100644 --- a/apps/web/src/pages/Swap/index.tsx +++ b/apps/web/src/pages/Swap/index.tsx @@ -27,10 +27,10 @@ import { AppTFunction } from 'ui/src/i18n/types' import { zIndices } from 'ui/src/theme' import { useUniswapContext } from 'uniswap/src/contexts/UniswapContext' import { SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' -import { useEnabledChains } from 'uniswap/src/features/settings/hooks' import Trace from 'uniswap/src/features/telemetry/Trace' import { InterfaceEventNameLocal } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' @@ -46,7 +46,6 @@ import { useSwapPrefilledState } from 'uniswap/src/features/transactions/swap/ho import { Deadline } from 'uniswap/src/features/transactions/swap/settings/configs/Deadline' import { currencyToAsset } from 'uniswap/src/features/transactions/swap/utils/asset' import { useTranslation } from 'uniswap/src/i18n' -import { UniverseChainId } from 'uniswap/src/types/chains' import { CurrencyField } from 'uniswap/src/types/currency' import { SwapTab } from 'uniswap/src/types/screens/interface' import { currencyId } from 'uniswap/src/utils/currencyId' @@ -184,7 +183,6 @@ export function Swap({ swapRedirectCallback={swapRedirectCallback} prefilledState={prefilledState} /> - @@ -309,58 +307,61 @@ function UniversalSwapFlow({ const prefilledOutputCurrencyInfo = useCurrencyInfo(initialOutputCurrency ? currencyId(initialOutputCurrency) : '') const [dismissTokenWarning, setDismissTokenWarning] = useState(false) const closeTokenWarning = useCallback(() => setDismissTokenWarning(true), [setDismissTokenWarning]) - const urlTokensNotInDefault = useMemo( - () => - prefilledInputCurrencyInfo || prefilledOutputCurrencyInfo - ? // dismiss warning if all imported tokens are in active lists - [prefilledInputCurrencyInfo, prefilledOutputCurrencyInfo] - .filter( - (token): token is CurrencyInfo => - (token?.currency.isToken && token.safetyLevel !== SafetyLevel.Verified) ?? false, - ) - .map((token: CurrencyInfo) => token.currency as Token) - : [], - [prefilledInputCurrencyInfo, prefilledOutputCurrencyInfo], - ) - + const prefilledTokensWithWarnings: { field: CurrencyField; token: Token }[] = useMemo(() => { + const tokens = [] + if ( + prefilledInputCurrencyInfo?.currency.isToken && + prefilledInputCurrencyInfo.safetyLevel !== SafetyLevel.Verified + ) { + tokens.push({ field: CurrencyField.INPUT, token: prefilledInputCurrencyInfo.currency as Token }) + } + if ( + prefilledOutputCurrencyInfo?.currency.isToken && + prefilledOutputCurrencyInfo.safetyLevel !== SafetyLevel.Verified + ) { + tokens.push({ field: CurrencyField.OUTPUT, token: prefilledOutputCurrencyInfo.currency as Token }) + } + return tokens + }, [prefilledInputCurrencyInfo, prefilledOutputCurrencyInfo]) const { updateSwapForm } = useSwapFormContext() + const onTokenBlockAcknowledged = useCallback( + (field: CurrencyField) => { + updateSwapForm({ [field]: undefined, selectingCurrencyField: undefined }) + onCurrencyChange?.({ [field === CurrencyField.INPUT ? 'inputCurrency' : 'outputCurrency']: undefined }) + }, + [updateSwapForm, onCurrencyChange], + ) return ( <> - 0 && !dismissTokenWarning} - token0={urlTokensNotInDefault[0]} - token1={urlTokensNotInDefault[1]} - onAcknowledge={closeTokenWarning} - onReject={() => { - closeTokenWarning() - updateSwapForm({ - [CurrencyField.INPUT]: undefined, - [CurrencyField.OUTPUT]: undefined, - selectingCurrencyField: undefined, - }) - onCurrencyChange?.({ - inputCurrency: undefined, - outputCurrency: undefined, - }) - }} - closeModalOnly={closeTokenWarning} - onToken0BlockAcknowledged={() => { - updateSwapForm({ - [CurrencyField.INPUT]: undefined, - selectingCurrencyField: undefined, - }) - onCurrencyChange?.({ inputCurrency: undefined }) - }} - onToken1BlockAcknowledged={() => { - updateSwapForm({ - [CurrencyField.OUTPUT]: undefined, - selectingCurrencyField: undefined, - }) - onCurrencyChange?.({ outputCurrency: undefined }) - }} - showCancel={true} - /> + {prefilledTokensWithWarnings.length >= 1 && ( + = 1 && !dismissTokenWarning} + token0={prefilledTokensWithWarnings[0].token} + token1={prefilledTokensWithWarnings[1]?.token} + onAcknowledge={closeTokenWarning} + onReject={() => { + closeTokenWarning() + updateSwapForm({ + [CurrencyField.INPUT]: undefined, + [CurrencyField.OUTPUT]: undefined, + selectingCurrencyField: undefined, + }) + onCurrencyChange?.({ + inputCurrency: undefined, + outputCurrency: undefined, + }) + }} + closeModalOnly={closeTokenWarning} + onToken0BlockAcknowledged={() => + prefilledTokensWithWarnings.length >= 1 && onTokenBlockAcknowledged(prefilledTokensWithWarnings[0].field) + } + onToken1BlockAcknowledged={() => + prefilledTokensWithWarnings.length == 2 && onTokenBlockAcknowledged(prefilledTokensWithWarnings[1].field) + } + showCancel={true} + /> + )} {!hideHeader && ( @@ -374,16 +375,19 @@ function UniversalSwapFlow({ )} {currentTab === SwapTab.Swap && ( - + + + + )} {currentTab === SwapTab.Limit && } {currentTab === SwapTab.Send && ( diff --git a/apps/web/src/pages/TokenDetails/TDPContext.tsx b/apps/web/src/pages/TokenDetails/TDPContext.tsx index 1ecdad7a8fd..35270288107 100644 --- a/apps/web/src/pages/TokenDetails/TDPContext.tsx +++ b/apps/web/src/pages/TokenDetails/TDPContext.tsx @@ -1,19 +1,18 @@ import { QueryResult } from '@apollo/client' import { Currency } from '@uniswap/sdk-core' import { TDPChartState } from 'components/Tokens/TokenDetails/ChartSection' -import { InterfaceGqlChain } from 'constants/chains' import { Warning } from 'constants/deprecatedTokenSafety' import { PortfolioBalance } from 'graphql/data/portfolios' import { PropsWithChildren, createContext, useContext } from 'react' import { Chain, Exact, TokenWebQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { GqlChainId, UniverseChainId } from 'uniswap/src/features/chains/types' export type MultiChainMap = { [chain in Chain]?: { address?: string; balance?: PortfolioBalance } | undefined } type BaseTDPContext = { - currencyChain: InterfaceGqlChain + currencyChain: GqlChainId /** Equivalent to `currency.chainId`, typed as `ChainId` instead of `number` */ currencyChainId: UniverseChainId diff --git a/apps/web/src/pages/TokenDetails/index.tsx b/apps/web/src/pages/TokenDetails/index.tsx index b7d2b20b366..26c1ace8a17 100644 --- a/apps/web/src/pages/TokenDetails/index.tsx +++ b/apps/web/src/pages/TokenDetails/index.tsx @@ -2,11 +2,10 @@ import TokenDetails from 'components/Tokens/TokenDetails' import { useCreateTDPChartState } from 'components/Tokens/TokenDetails/ChartSection' import InvalidTokenDetails from 'components/Tokens/TokenDetails/InvalidTokenDetails' import { TokenDetailsPageSkeleton } from 'components/Tokens/TokenDetails/Skeleton' -import { useChainFromUrlParam } from 'constants/chains' import { useTokenWarning } from 'constants/deprecatedTokenSafety' import { NATIVE_CHAIN_ID, UNKNOWN_TOKEN_SYMBOL } from 'constants/tokens' import { useTokenBalancesQuery } from 'graphql/data/apollo/AdaptiveTokenBalancesProvider' -import { getSupportedGraphQlChain, gqlToCurrency } from 'graphql/data/util' +import { gqlToCurrency } from 'graphql/data/util' import { useCurrency } from 'hooks/Tokens' import { useAccount } from 'hooks/useAccount' import { useSrcColor } from 'hooks/useColor' @@ -21,8 +20,10 @@ import { formatTokenMetatagTitleName } from 'shared-cloud/metatags' import { ThemeProvider } from 'theme' import { nativeOnChain } from 'uniswap/src/constants/tokens' import { useTokenWebQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { isAddress } from 'utilities/src/addresses' +import { useChainIdFromUrlParam } from 'utils/chainParams' import { getNativeTokenDBAddress } from 'utils/nativeTokens' function useOnChainToken(address: string | undefined, chainId: UniverseChainId, skip: boolean) { @@ -93,7 +94,8 @@ function useCreateTDPContext(): PendingTDPContext | LoadedTDPContext { if (!tokenAddress) { throw new Error('Invalid token details route: token address URL param is undefined') } - const currencyChainInfo = getSupportedGraphQlChain(useChainFromUrlParam(), { fallbackToEthereum: true }) + + const currencyChainInfo = getChainInfo(useChainIdFromUrlParam() ?? UniverseChainId.Mainnet) const isNative = tokenAddress === NATIVE_CHAIN_ID diff --git a/apps/web/src/pages/TokenDetails/utils.ts b/apps/web/src/pages/TokenDetails/utils.ts index bb49ccf0ef7..b4c39513710 100644 --- a/apps/web/src/pages/TokenDetails/utils.ts +++ b/apps/web/src/pages/TokenDetails/utils.ts @@ -1,7 +1,7 @@ import { Currency } from '@uniswap/sdk-core' -import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { getChainLabel } from 'uniswap/src/features/chains/utils' import { t } from 'uniswap/src/i18n' -import { UniverseChainId } from 'uniswap/src/types/chains' export const getTokenPageTitle = (currency?: Currency, chainId?: UniverseChainId) => { const tokenName = currency?.name @@ -11,7 +11,7 @@ export const getTokenPageTitle = (currency?: Currency, chainId?: UniverseChainId return baseTitle } - const chainSuffix = chainId && chainId !== UniverseChainId.Mainnet ? ` on ${UNIVERSE_CHAIN_INFO[chainId].label}` : '' + const chainSuffix = chainId && chainId !== UniverseChainId.Mainnet ? ` on ${getChainLabel(chainId)}` : '' if (!tokenName && tokenSymbol) { return `${tokenSymbol}${chainSuffix}: ${baseTitle}` } @@ -26,7 +26,7 @@ export const getTokenPageDescription = (currency?: Currency, chainId?: UniverseC currency?.name && currency?.symbol ? `${currency?.name} (${currency?.symbol})` : currency?.name ?? currency?.symbol ?? 'tokens' - const chainSuffix = chainId && chainId !== UniverseChainId.Mainnet ? ` on ${UNIVERSE_CHAIN_INFO[chainId].label}` : '' + const chainSuffix = chainId && chainId !== UniverseChainId.Mainnet ? ` on ${getChainLabel(chainId)}` : '' return `Buy, sell, and swap ${tokenPageName}${chainSuffix}. Real-time prices, charts, transaction data, and more.` } diff --git a/apps/web/src/pages/getExploreTitle.ts b/apps/web/src/pages/getExploreTitle.ts index 10546cf3d45..a5dc9a35896 100644 --- a/apps/web/src/pages/getExploreTitle.ts +++ b/apps/web/src/pages/getExploreTitle.ts @@ -1,15 +1,15 @@ -import { ChainSlug, isChainUrlParam } from 'constants/chains' import { ExploreTab } from 'pages/Explore' import { capitalize } from 'tsafe/capitalize' import { t } from 'uniswap/src/i18n' import { logger } from 'utilities/src/logger/logger' +import { isChainUrlParam } from 'utils/chainParams' export const getExploreTitle = (path?: string) => { const parts = path?.split('/').filter((part) => part !== '') const tabsToFind: string[] = [ExploreTab.Pools, ExploreTab.Tokens, ExploreTab.Transactions] const tab = parts?.find((part) => tabsToFind.includes(part)) ?? ExploreTab.Tokens - const networkPart: ChainSlug = parts?.find(isChainUrlParam) ?? 'ethereum' + const networkPart: string = parts?.find(isChainUrlParam) ?? 'ethereum' const network = capitalize(networkPart) switch (tab) { @@ -38,7 +38,7 @@ export const getExploreTitle = (path?: string) => { export const getExploreDescription = (path?: string) => { const parts = path?.split('/').filter((part) => part !== '') - const network: ChainSlug = parts?.find(isChainUrlParam) ?? 'ethereum' + const network: string = parts?.find(isChainUrlParam) ?? 'ethereum' return t(`web.explore.description`, { network: capitalize(network), diff --git a/apps/web/src/pages/getPositionPageTitle.ts b/apps/web/src/pages/getPositionPageTitle.ts index b0621d47c1c..e3e7fc7dc35 100644 --- a/apps/web/src/pages/getPositionPageTitle.ts +++ b/apps/web/src/pages/getPositionPageTitle.ts @@ -3,26 +3,29 @@ import { t } from 'uniswap/src/i18n' export const getPositionPageTitle = (path?: string) => { const parts = path?.split('/').filter((part) => part !== '') const isV2 = parts?.find((part) => part === 'v2') + const isV3 = parts?.find((part) => part === 'v3') return t(`liquidityPool.positions.page.version.title`, { - version: isV2 ? ' (v2)' : '', + version: isV2 ? ' (v2)' : isV3 ? ' (v3)' : '', }) } export const getPositionPageDescription = (path?: string) => { const parts = path?.split('/').filter((part) => part !== '') const isV2 = parts?.find((part) => part === 'v2') + const isV3 = parts?.find((part) => part === 'v3') return t(`liquidityPool.positions.page.version.description`, { - version: isV2 ? 'v2' : 'v3', + version: isV2 ? 'v2' : isV3 ? 'v3' : 'v4', }) } export const getAddLiquidityPageTitle = (path?: string) => { const parts = path?.split('/').filter((part) => part !== '') const isV2 = parts?.find((part) => part === 'v2') + const isV3 = parts?.find((part) => part === 'v3') return t('liquidityPool.page.title', { - version: isV2 ? ' (v2)' : '', + version: isV2 ? ' (v2)' : isV3 ? ' (v3)' : '', }) } diff --git a/apps/web/src/pages/paths.test.ts b/apps/web/src/pages/paths.test.ts index 4fd81072cb3..ba131cd538d 100644 --- a/apps/web/src/pages/paths.test.ts +++ b/apps/web/src/pages/paths.test.ts @@ -67,22 +67,28 @@ describe('getExploreTitle', () => { }) describe('positionPage static titles and descriptions', () => { - it('should return the correct title for v3 pools', () => { - expect(getPositionPageTitle('/pools')).toBe('Manage pool liquidity on Uniswap') - }) - - it('should return the correct title for v2 pools', () => { - expect(getPositionPageTitle('/pools/v2')).toBe('Manage pool liquidity (v2) on Uniswap') + it('should return the correct title & description for v4 positions page', () => { + const v4PositionsPageUrl = '/positions/v4/optimism/512372' + expect(getPositionPageTitle(v4PositionsPageUrl)).toBe('Manage pool liquidity on Uniswap') + expect(getPositionPageDescription(v4PositionsPageUrl)).toBe( + 'View your active v4 liquidity positions. Add new positions.', + ) }) - it('should return the correct description for v3 pools', () => { - expect(getPositionPageDescription('/pool/512372?chain=optimism')).toBe( + it('should return the correct title & description for v3 positions page', () => { + const v3PositionsPageUrl = '/positions/v3/optimism/512372' + expect(getPositionPageTitle(v3PositionsPageUrl)).toBe('Manage pool liquidity (v3) on Uniswap') + expect(getPositionPageDescription(v3PositionsPageUrl)).toBe( 'View your active v3 liquidity positions. Add new positions.', ) }) - it('should return the correct description for v2 pools', () => { - expect(getPositionPageDescription('/pool/v2')).toBe('View your active v2 liquidity positions. Add new positions.') + it('should return the correct title & description for v2 positions page', () => { + const v2PositionsPageUrl = '/positions/v2/ethereum/0x004375Dff511095CC5A197A54140a24eFEF3A416' + expect(getPositionPageTitle(v2PositionsPageUrl)).toBe('Manage pool liquidity (v2) on Uniswap') + expect(getPositionPageDescription(v2PositionsPageUrl)).toBe( + 'View your active v2 liquidity positions. Add new positions.', + ) }) it('should return the correct title for Add Liquidity pages', () => { diff --git a/apps/web/src/rpc/AppJsonRpcProvider.ts b/apps/web/src/rpc/AppJsonRpcProvider.ts index f62d8094920..f62d5f657cc 100644 --- a/apps/web/src/rpc/AppJsonRpcProvider.ts +++ b/apps/web/src/rpc/AppJsonRpcProvider.ts @@ -1,6 +1,6 @@ import { JsonRpcProvider } from '@ethersproject/providers' -import { AVERAGE_L1_BLOCK_TIME } from 'constants/chains' import ConfiguredJsonRpcProvider from 'rpc/ConfiguredJsonRpcProvider' +import { AVERAGE_L1_BLOCK_TIME_MS } from 'uniswap/src/features/transactions/swap/hooks/usePollingIntervalByChain' import { logger } from 'utilities/src/logger/logger' /** @@ -66,7 +66,7 @@ export default class AppJsonRpcProvider extends ConfiguredJsonRpcProvider { constructor( providers: JsonRpcProvider[], - { minimumBackoffTime = AVERAGE_L1_BLOCK_TIME }: AppJsonRpcProviderOptions = {}, + { minimumBackoffTime = AVERAGE_L1_BLOCK_TIME_MS }: AppJsonRpcProviderOptions = {}, ) { if (providers.length === 0) { throw new Error('Missing providers for AppJsonRpcProvider') diff --git a/apps/web/src/rpc/ConfiguredJsonRpcProvider.ts b/apps/web/src/rpc/ConfiguredJsonRpcProvider.ts index e521dc84d6c..53b0f664e8b 100644 --- a/apps/web/src/rpc/ConfiguredJsonRpcProvider.ts +++ b/apps/web/src/rpc/ConfiguredJsonRpcProvider.ts @@ -1,14 +1,14 @@ import { Networkish } from '@ethersproject/networks' import { StaticJsonRpcProvider } from '@ethersproject/providers' -import { AVERAGE_L1_BLOCK_TIME } from 'constants/chains' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { AVERAGE_L1_BLOCK_TIME_MS } from 'uniswap/src/features/transactions/swap/hooks/usePollingIntervalByChain' export default class ConfiguredJsonRpcProvider extends StaticJsonRpcProvider { constructor( url: string | undefined, // Including networkish allows ethers to skip the initial detectNetwork call. networkish: Networkish & { chainId: UniverseChainId }, - pollingInterval = AVERAGE_L1_BLOCK_TIME, + pollingInterval = AVERAGE_L1_BLOCK_TIME_MS, ) { super(url, networkish) diff --git a/apps/web/src/state/activity/polling/bridge.ts b/apps/web/src/state/activity/polling/bridge.ts index cd376abdeed..898ac9143ae 100644 --- a/apps/web/src/state/activity/polling/bridge.ts +++ b/apps/web/src/state/activity/polling/bridge.ts @@ -12,8 +12,8 @@ import { isPendingTx } from 'state/transactions/utils' import { fetchSwaps } from 'uniswap/src/data/apiClients/tradingApi/TradingApiClient' import { TransactionStatus } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { SwapStatus } from 'uniswap/src/data/tradingApi/__generated__' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { toTradingApiSupportedChainId } from 'uniswap/src/features/transactions/swap/utils/tradingApi' -import { UniverseChainId } from 'uniswap/src/types/chains' import { logger } from 'utilities/src/logger/logger' const MIN_BRIDGE_WAIT_TIME = ms('2s') diff --git a/apps/web/src/state/activity/polling/retry.ts b/apps/web/src/state/activity/polling/retry.ts index 9b3b5fefedb..5172ca8e934 100644 --- a/apps/web/src/state/activity/polling/retry.ts +++ b/apps/web/src/state/activity/polling/retry.ts @@ -1,4 +1,4 @@ -import { RetryOptions } from 'uniswap/src/types/chains' +import { RetryOptions } from 'uniswap/src/features/chains/types' function wait(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)) diff --git a/apps/web/src/state/activity/polling/transactions.ts b/apps/web/src/state/activity/polling/transactions.ts index 3157eeef353..7a2f61db235 100644 --- a/apps/web/src/state/activity/polling/transactions.ts +++ b/apps/web/src/state/activity/polling/transactions.ts @@ -1,6 +1,5 @@ import { NEVER_RELOAD } from '@uniswap/redux-multicall' import { useWeb3React } from '@web3-react/core' -import { getChain } from 'constants/chains' import { useAccount } from 'hooks/useAccount' import useCurrentBlockTimestamp from 'hooks/useCurrentBlockTimestamp' import useBlockNumber from 'lib/hooks/useBlockNumber' @@ -14,9 +13,10 @@ import { checkedTransaction } from 'state/transactions/reducer' import { PendingTransactionDetails } from 'state/transactions/types' import { isPendingTx } from 'state/transactions/utils' import { TransactionStatus } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' +import { RetryOptions, UniverseChainId } from 'uniswap/src/features/chains/types' import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' -import { RetryOptions, UniverseChainId } from 'uniswap/src/types/chains' import { SUBSCRIPTION_CHAINIDS } from 'utilities/src/apollo/constants' interface Transaction { @@ -93,8 +93,7 @@ export function usePollPendingTransactions(onActivityUpdate: OnActivityUpdate) { if (!provider || !account.chainId) { throw new Error('No provider or chainId') } - const retryOptions = - getChain({ chainId: account.chainId })?.pendingTransactionsRetryOptions ?? DEFAULT_RETRY_OPTIONS + const retryOptions = getChainInfo(account.chainId)?.pendingTransactionsRetryOptions ?? DEFAULT_RETRY_OPTIONS return retry( () => provider.getTransactionReceipt(tx.hash).then(async (receipt) => { diff --git a/apps/web/src/state/activity/subscription.ts b/apps/web/src/state/activity/subscription.ts index 5a5f4a9242b..69260e43d5a 100644 --- a/apps/web/src/state/activity/subscription.ts +++ b/apps/web/src/state/activity/subscription.ts @@ -14,7 +14,7 @@ import { TransactionDirection, TransactionStatus, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' export function useOnAssetActivity(onActivityUpdate: OnActivityUpdate) { const onOrderActivity = useOnOrderActivity(onActivityUpdate) diff --git a/apps/web/src/state/activity/types.ts b/apps/web/src/state/activity/types.ts index 08028f85fe4..24034dc5baf 100644 --- a/apps/web/src/state/activity/types.ts +++ b/apps/web/src/state/activity/types.ts @@ -1,6 +1,6 @@ import { FilledUniswapXOrderDetails, SignatureDetails, UnfilledUniswapXOrderDetails } from 'state/signatures/types' import { ConfirmedTransactionDetails, TransactionDetails } from 'state/transactions/types' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' interface BaseUpdate { type: string diff --git a/apps/web/src/state/application/reducer.ts b/apps/web/src/state/application/reducer.ts index ca4e255621c..58c55c31b1b 100644 --- a/apps/web/src/state/application/reducer.ts +++ b/apps/web/src/state/application/reducer.ts @@ -1,8 +1,8 @@ import { createSlice, nanoid, PayloadAction } from '@reduxjs/toolkit' import { PositionInfo } from 'components/Liquidity/types' import { DEFAULT_TXN_DISMISS_MS } from 'constants/misc' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { ModalName, ModalNameType } from 'uniswap/src/features/telemetry/constants' -import { UniverseChainId } from 'uniswap/src/types/chains' import { SwapTab } from 'uniswap/src/types/screens/interface' export enum PopupType { @@ -58,20 +58,28 @@ export enum ApplicationModal { GET_THE_APP, } +export type LiquidityModalInitialState = PositionInfo & { collectAsWeth?: boolean } + type AddLiquidityModalParams = { name: typeof ModalName.AddLiquidity - initialState: PositionInfo + initialState: LiquidityModalInitialState } type RemoveLiquidityModalParams = { name: typeof ModalName.RemoveLiquidity - initialState: PositionInfo + initialState: LiquidityModalInitialState +} + +type ClaimFeeModalParams = { + name: typeof ModalName.ClaimFee + initialState: LiquidityModalInitialState } export type OpenModalParams = | { name: ModalNameType | ApplicationModal; initialState?: undefined } | AddLiquidityModalParams | RemoveLiquidityModalParams + | ClaimFeeModalParams export type CloseModalParams = ModalNameType | ApplicationModal diff --git a/apps/web/src/state/application/updater.ts b/apps/web/src/state/application/updater.ts index 6131e3098dc..e0e260c01c7 100644 --- a/apps/web/src/state/application/updater.ts +++ b/apps/web/src/state/application/updater.ts @@ -1,11 +1,11 @@ import { useWeb3React } from '@web3-react/core' -import { useSupportedChainId } from 'constants/chains' import { useAccount } from 'hooks/useAccount' import useDebounce from 'hooks/useDebounce' import useIsWindowVisible from 'hooks/useIsWindowVisible' import { useEffect, useState } from 'react' import { updateChainId } from 'state/application/reducer' import { useAppDispatch } from 'state/hooks' +import { useSupportedChainId } from 'uniswap/src/features/chains/hooks' export default function Updater(): null { const account = useAccount() diff --git a/apps/web/src/state/explore/index.tsx b/apps/web/src/state/explore/index.tsx index 584ca3f8ad0..b3679efa901 100644 --- a/apps/web/src/state/explore/index.tsx +++ b/apps/web/src/state/explore/index.tsx @@ -4,7 +4,7 @@ import { createContext, useMemo } from 'react' import { ALL_NETWORKS_ARG } from 'uniswap/src/data/rest/base' import { useExploreStatsQuery } from 'uniswap/src/data/rest/exploreStats' import { useProtocolStatsQuery } from 'uniswap/src/data/rest/protocolStats' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' interface QueryResult { data?: T diff --git a/apps/web/src/state/explore/protocolStats.ts b/apps/web/src/state/explore/protocolStats.ts index 1885a947ff9..4080cda7188 100644 --- a/apps/web/src/state/explore/protocolStats.ts +++ b/apps/web/src/state/explore/protocolStats.ts @@ -8,24 +8,35 @@ import { UTCTimestamp } from 'lightweight-charts' import { useContext, useMemo } from 'react' import { ExploreContext } from 'state/explore' import { HistoryDuration } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { FeatureFlags } from 'uniswap/src/features/gating/flags' +import { useFeatureFlagWithLoading } from 'uniswap/src/features/gating/hooks' function mapDataByTimestamp( v2Data?: TimestampedAmount[], v3Data?: TimestampedAmount[], + v4Data?: TimestampedAmount[], ): Record> { const dataByTime: Record> = {} v2Data?.forEach((v2Point) => { const timestamp = Number(v2Point.timestamp) - dataByTime[timestamp] = { ['v2']: Number(v2Point.value), ['v3']: 0 } + dataByTime[timestamp] = { ['v2']: Number(v2Point.value), ['v3']: 0, ['v4']: 0 } }) v3Data?.forEach((v3Point) => { const timestamp = Number(v3Point.timestamp) if (!dataByTime[timestamp]) { - dataByTime[timestamp] = { ['v2']: 0, ['v3']: Number(v3Point.value) } + dataByTime[timestamp] = { ['v2']: 0, ['v3']: Number(v3Point.value), ['v4']: 0 } } else { dataByTime[timestamp]['v3'] = Number(v3Point.value) } }) + v4Data?.forEach((v4Point) => { + const timestamp = Number(v4Point.timestamp) + if (!dataByTime[timestamp]) { + dataByTime[timestamp] = { ['v2']: 0, ['v3']: 0, ['v4']: Number(v4Point.value) } + } else { + dataByTime[timestamp]['v4'] = Number(v4Point.value) + } + }) return dataByTime } @@ -33,25 +44,34 @@ export function useHistoricalProtocolVolume(duration: HistoryDuration) { const { protocolStats: { data, isLoading }, } = useContext(ExploreContext) + + const { value: isV4EverywhereEnabledLoaded, isLoading: isV4EverywhereLoading } = useFeatureFlagWithLoading( + FeatureFlags.V4Everywhere, + ) + const isV4EverywhereEnabled = isV4EverywhereEnabledLoaded || isV4EverywhereLoading + let v4Data: TimestampedAmount[] | undefined let v3Data: TimestampedAmount[] | undefined let v2Data: TimestampedAmount[] | undefined switch (duration) { case HistoryDuration.Max: v2Data = data?.historicalProtocolVolume?.Max?.v2 v3Data = data?.historicalProtocolVolume?.Max?.v3 + v4Data = data?.historicalProtocolVolume?.Max?.v4 break case HistoryDuration.Year: v2Data = data?.historicalProtocolVolume?.Year?.v2 v3Data = data?.historicalProtocolVolume?.Year?.v3 + v4Data = data?.historicalProtocolVolume?.Year?.v4 break default: v2Data = data?.historicalProtocolVolume?.Month?.v2 v3Data = data?.historicalProtocolVolume?.Month?.v3 + v4Data = data?.historicalProtocolVolume?.Month?.v4 break } return useMemo(() => { - const dataByTime = mapDataByTimestamp(v2Data, v3Data) + const dataByTime = mapDataByTimestamp(v2Data, v3Data, isV4EverywhereEnabled ? v4Data : undefined) const entries = Object.entries(dataByTime).reduce((acc, [timestamp, values]) => { acc.push({ @@ -59,7 +79,7 @@ export function useHistoricalProtocolVolume(duration: HistoryDuration) { values: { ['SUBGRAPH_V2']: values['v2'], ['SUBGRAPH_V3']: values['v3'], - ['SUBGRAPH_V4']: values['v4'], + ['SUBGRAPH_V4']: isV4EverywhereEnabled ? values['v4'] : undefined, }, }) return acc @@ -67,24 +87,30 @@ export function useHistoricalProtocolVolume(duration: HistoryDuration) { const dataQuality = checkDataQuality(entries, ChartType.VOLUME, duration) return { chartType: ChartType.VOLUME, entries, loading: isLoading, dataQuality } - }, [duration, isLoading, v2Data, v3Data]) + }, [duration, isLoading, isV4EverywhereEnabled, v2Data, v3Data, v4Data]) } export function useDailyProtocolTVL() { const { protocolStats: { data, isLoading }, } = useContext(ExploreContext) + + const { value: isV4EverywhereEnabledLoaded, isLoading: isV4EverywhereLoading } = useFeatureFlagWithLoading( + FeatureFlags.V4Everywhere, + ) + const isV4EverywhereEnabled = isV4EverywhereEnabledLoaded || isV4EverywhereLoading + const v4Data = data?.dailyProtocolTvl?.v4 const v3Data = data?.dailyProtocolTvl?.v3 const v2Data = data?.dailyProtocolTvl?.v2 return useMemo(() => { - const dataByTime = mapDataByTimestamp(v2Data, v3Data) + const dataByTime = mapDataByTimestamp(v2Data, v3Data, isV4EverywhereEnabled ? v4Data : undefined) const entries = Object.entries(dataByTime).map(([timestamp, values]) => ({ time: Number(timestamp), - values: [values['v2'], values['v3']], + values: isV4EverywhereEnabled ? [values['v2'], values['v3'], values['v4']] : [values['v2'], values['v3']], })) as StackedLineData[] const dataQuality = checkDataQuality(entries, ChartType.TVL, HistoryDuration.Year) return { chartType: ChartType.TVL, entries, loading: isLoading, dataQuality } - }, [isLoading, v2Data, v3Data]) + }, [isLoading, isV4EverywhereEnabled, v2Data, v3Data, v4Data]) } diff --git a/apps/web/src/state/explore/topTokens.ts b/apps/web/src/state/explore/topTokens.ts index 3e09a0d4ee5..b5b029fdf6b 100644 --- a/apps/web/src/state/explore/topTokens.ts +++ b/apps/web/src/state/explore/topTokens.ts @@ -7,14 +7,14 @@ import { sortAscendingAtom, sortMethodAtom, } from 'components/Tokens/state' -import { getChainFromChainUrlParam } from 'constants/chains' import { NATIVE_CHAIN_ID } from 'constants/tokens' -import { SparklineMap } from 'graphql/data/TopTokens' +import { SparklineMap } from 'graphql/data/types' import { PricePoint, TimePeriod, unwrapToken } from 'graphql/data/util' import { useAtomValue } from 'jotai/utils' import { useContext, useMemo } from 'react' import { ExploreContext, giveExploreStatDefaultValue } from 'state/explore' import { TokenStat } from 'state/explore/types' +import { getChainIdFromChainUrlParam } from 'utils/chainParams' const TokenSortMethods = { [TokenSortMethod.PRICE]: (a: TokenStat, b: TokenStat) => @@ -145,7 +145,7 @@ export function useTopTokens() { const filteredTokens = useFilteredTokens(sortedTokenStats)?.slice(0, MAX_TOP_TOKENS) const sparklines = useMemo(() => { const unwrappedTokens = filteredTokens?.map((tokenStat) => { - const chainId = getChainFromChainUrlParam(tokenStat?.chain.toLowerCase())?.id + const chainId = getChainIdFromChainUrlParam(tokenStat?.chain.toLowerCase()) return chainId ? unwrapToken(chainId, tokenStat) : undefined }) const map: SparklineMap = {} diff --git a/apps/web/src/state/explore/types.ts b/apps/web/src/state/explore/types.ts index 42166933e31..65effb5e3ec 100644 --- a/apps/web/src/state/explore/types.ts +++ b/apps/web/src/state/explore/types.ts @@ -1,6 +1,7 @@ // eslint-disable-next-line no-restricted-imports import { Amount, PoolStats, TokenStats } from '@uniswap/client-explore/dist/uniswap/explore/v1/service_pb' import { Percent } from '@uniswap/sdk-core' +import { FeeData } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' type PricePoint = { timestamp: number; value: number } @@ -8,6 +9,7 @@ export interface TokenStat extends Omit { volume?: Amount priceHistory?: PricePoint[] + feeData?: FeeData } type PoolStatWithoutMethods = Omit< diff --git a/apps/web/src/state/index.ts b/apps/web/src/state/index.ts index 4dfd196a592..a0e1c63352c 100644 --- a/apps/web/src/state/index.ts +++ b/apps/web/src/state/index.ts @@ -10,7 +10,7 @@ import { quickRouteApi } from 'state/routing/quickRouteSlice' import { routingApi } from 'state/routing/slice' import { rootWebSaga } from 'state/sagas/root' import { InterfaceState, interfacePersistedStateList, interfaceReducer } from 'state/webReducer' -import { fiatOnRampAggregatorApi } from 'uniswap/src/features/fiatOnRamp/api' +import { getFiatOnRampAggregatorApi } from 'uniswap/src/features/fiatOnRamp/api' import { isDevEnv, isTestEnv } from 'utilities/src/environment/env' const persistConfig: PersistConfig = { @@ -66,7 +66,7 @@ export function createDefaultStore() { }) .concat(routingApi.middleware) .concat(quickRouteApi.middleware) - .concat(fiatOnRampAggregatorApi.middleware) + .concat(getFiatOnRampAggregatorApi().middleware) .concat(sagaMiddleware), }) sagaMiddleware.run(rootWebSaga) diff --git a/apps/web/src/state/limit/hooks.ts b/apps/web/src/state/limit/hooks.ts index 492f31d31d3..84cd1b5c65e 100644 --- a/apps/web/src/state/limit/hooks.ts +++ b/apps/web/src/state/limit/hooks.ts @@ -1,5 +1,4 @@ import { Currency, CurrencyAmount, Price, TradeType } from '@uniswap/sdk-core' -import { isStablecoin } from 'constants/chains' import { useAccount } from 'hooks/useAccount' import JSBI from 'jsbi' import { useCurrencyBalances } from 'lib/hooks/useCurrencyBalance' @@ -13,6 +12,8 @@ import { useRoutingAPITrade } from 'state/routing/useRoutingAPITrade' import { getUSDCostPerGas, isClassicTrade } from 'state/routing/utils' import { useSwapAndLimitContext } from 'state/swap/useSwapContext' import { nativeOnChain } from 'uniswap/src/constants/tokens' +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' +import { isUniverseChainId } from 'uniswap/src/features/chains/types' import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { CurrencyField } from 'uniswap/src/types/currency' @@ -26,6 +27,14 @@ export type LimitInfo = { fee?: SwapFeeInfo } +function isStablecoin(currency?: Currency): boolean { + return ( + currency !== undefined && + isUniverseChainId(currency.chainId) && + getChainInfo(currency.chainId).stablecoins.some((stablecoin) => stablecoin.equals(currency)) + ) +} + // By default, inputCurrency is base currency and outputCurrency is quote currency // If only one of these tokens is a stablecoin, prefer the denomination (quote currency) to be the stablecoin // TODO(limits): Also add preference for ETH, BTC to be default diff --git a/apps/web/src/state/migrations/16.test.ts b/apps/web/src/state/migrations/16.test.ts index ac243ef79cb..b1dc32da78e 100644 --- a/apps/web/src/state/migrations/16.test.ts +++ b/apps/web/src/state/migrations/16.test.ts @@ -16,7 +16,7 @@ import { migration7 } from 'state/migrations/7' import { migration8 } from 'state/migrations/8' import { migration9 } from 'state/migrations/9' import { DAI_ARBITRUM_ONE, USDC } from 'uniswap/src/constants/tokens' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { serializeToken } from 'uniswap/src/utils/currency' const tokenMap = { diff --git a/apps/web/src/state/migrations/3.test.ts b/apps/web/src/state/migrations/3.test.ts index 8558181b10e..9e63931c491 100644 --- a/apps/web/src/state/migrations/3.test.ts +++ b/apps/web/src/state/migrations/3.test.ts @@ -5,7 +5,7 @@ import { migration2 } from 'state/migrations/2' import { PersistAppStateV3, migration3 } from 'state/migrations/3' import { RouterPreference } from 'state/routing/types' import { SlippageTolerance } from 'state/user/types' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' const previousState: PersistAppStateV3 = { user: { diff --git a/apps/web/src/state/migrations/3.ts b/apps/web/src/state/migrations/3.ts index 7d5b422981a..7908fa6d74d 100644 --- a/apps/web/src/state/migrations/3.ts +++ b/apps/web/src/state/migrations/3.ts @@ -1,7 +1,7 @@ import { Token } from '@uniswap/sdk-core' import { PersistState } from 'redux-persist' import { PreV16UserState } from 'state/migrations/oldTypes' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { serializeToken } from 'uniswap/src/utils/currency' export type PersistAppStateV3 = { diff --git a/apps/web/src/state/reducerTypeTest.ts b/apps/web/src/state/reducerTypeTest.ts index 53629adfcff..761d0692239 100644 --- a/apps/web/src/state/reducerTypeTest.ts +++ b/apps/web/src/state/reducerTypeTest.ts @@ -26,15 +26,17 @@ import { Wallet } from 'state/wallets/types' import { InterfaceState } from 'state/webReducer' import { Equals, assert } from 'tsafe' import { UniswapBehaviorHistoryState } from 'uniswap/src/features/behaviorHistory/slice' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { FavoritesState } from 'uniswap/src/features/favorites/slice' -import { fiatOnRampAggregatorApi } from 'uniswap/src/features/fiatOnRamp/api' +import { getFiatOnRampAggregatorApi } from 'uniswap/src/features/fiatOnRamp/api' import { NotificationState } from 'uniswap/src/features/notifications/slice' import { SearchHistoryState } from 'uniswap/src/features/search/searchHistorySlice' import { UserSettingsState } from 'uniswap/src/features/settings/slice' import { TimingState } from 'uniswap/src/features/timing/slice' import { TokensState } from 'uniswap/src/features/tokens/slice/slice' import { TransactionsState } from 'uniswap/src/features/transactions/slice' -import { UniverseChainId } from 'uniswap/src/types/chains' + +const forAggregatorApi = getFiatOnRampAggregatorApi() /** * WARNING: @@ -74,7 +76,7 @@ type ExpectedAppState = CombinedState<{ readonly [quickRouteApi.reducerPath]: ReturnType // Uniswap State - readonly [fiatOnRampAggregatorApi.reducerPath]: ReturnType + readonly [forAggregatorApi.reducerPath]: ReturnType readonly uniswapBehaviorHistory: UniswapBehaviorHistoryState readonly favorites: FavoritesState readonly notifications: NotificationState diff --git a/apps/web/src/state/routing/gas.ts b/apps/web/src/state/routing/gas.ts index 40bd57f7bd7..4f7e9299bbf 100644 --- a/apps/web/src/state/routing/gas.ts +++ b/apps/web/src/state/routing/gas.ts @@ -6,8 +6,8 @@ import ERC20_ABI from 'uniswap/src/abis/erc20.json' import { Erc20, Weth } from 'uniswap/src/abis/types' import WETH_ABI from 'uniswap/src/abis/weth.json' import { WRAPPED_NATIVE_CURRENCY } from 'uniswap/src/constants/tokens' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { WRAP_FALLBACK_GAS_LIMIT_IN_GWEI } from 'uniswap/src/features/transactions/swap/hooks/useTransactionRequestInfo' -import { UniverseChainId } from 'uniswap/src/types/chains' import { getContract } from 'utilities/src/contracts/getContract' // TODO(UniswapX): add fallback gas limits per chain? l2s have higher costs diff --git a/apps/web/src/state/routing/types.ts b/apps/web/src/state/routing/types.ts index 26dce1726ae..98c7e1e9350 100644 --- a/apps/web/src/state/routing/types.ts +++ b/apps/web/src/state/routing/types.ts @@ -17,7 +17,7 @@ import { Route as V2Route } from '@uniswap/v2-sdk' import { Route as V3Route } from '@uniswap/v3-sdk' import { ZERO_PERCENT } from 'constants/misc' import { BigNumber } from 'ethers/lib/ethers' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' export enum TradeState { LOADING = 'loading', diff --git a/apps/web/src/state/routing/usePreviewTrade.ts b/apps/web/src/state/routing/usePreviewTrade.ts index 34fb0d1bf9d..1afdfb0b9ab 100644 --- a/apps/web/src/state/routing/usePreviewTrade.ts +++ b/apps/web/src/state/routing/usePreviewTrade.ts @@ -8,9 +8,9 @@ import { useMemo } from 'react' import { useGetQuickRouteQuery, useGetQuickRouteQueryState } from 'state/routing/quickRouteSlice' import { GetQuickQuoteArgs, PreviewTrade, QuoteState, TradeState } from 'state/routing/types' import { currencyAddressForSwapQuote } from 'state/routing/utils' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' -import { UniverseChainId } from 'uniswap/src/types/chains' const TRADE_NOT_FOUND = { state: TradeState.NO_ROUTE_FOUND, trade: undefined } as const const TRADE_LOADING = { state: TradeState.LOADING, trade: undefined } as const diff --git a/apps/web/src/state/routing/useRoutingAPITrade.test.ts b/apps/web/src/state/routing/useRoutingAPITrade.test.ts index 9bdd8566c05..2d5e22a328a 100644 --- a/apps/web/src/state/routing/useRoutingAPITrade.test.ts +++ b/apps/web/src/state/routing/useRoutingAPITrade.test.ts @@ -1,7 +1,6 @@ import { skipToken } from '@reduxjs/toolkit/query/react' import { renderHook } from '@testing-library/react' import { CurrencyAmount, TradeType } from '@uniswap/sdk-core' -import { AVERAGE_L1_BLOCK_TIME } from 'constants/chains' import useIsWindowVisible from 'hooks/useIsWindowVisible' import ms from 'ms' import { useGetQuoteQuery, useGetQuoteQueryState } from 'state/routing/slice' @@ -13,6 +12,7 @@ import { ETH_MAINNET } from 'test-utils/constants' import { mocked } from 'test-utils/mocked' import { USDC_MAINNET } from 'uniswap/src/constants/tokens' import { useExperimentValue } from 'uniswap/src/features/gating/hooks' +import { AVERAGE_L1_BLOCK_TIME_MS } from 'uniswap/src/features/transactions/swap/hooks/usePollingIntervalByChain' const USDCAmount = CurrencyAmount.fromRawAmount(USDC_MAINNET, '10000') @@ -94,7 +94,7 @@ describe('#useRoutingAPITrade ExactIn', () => { ) expect(useGetQuoteQuery).toHaveBeenCalledWith(skipToken, { - pollingInterval: AVERAGE_L1_BLOCK_TIME, + pollingInterval: AVERAGE_L1_BLOCK_TIME_MS, refetchOnMountOrArgChange: 2 * 60, }) expect(result.current?.trade).toEqual(undefined) @@ -106,7 +106,7 @@ describe('#useRoutingAPITrade ExactIn', () => { renderHook(() => useRoutingAPITrade(false, TradeType.EXACT_INPUT, USDCAmount, ETH_MAINNET, RouterPreference.API)) expect(useGetQuoteQuery).toHaveBeenCalledWith(MOCK_ARGS, { - pollingInterval: AVERAGE_L1_BLOCK_TIME, + pollingInterval: AVERAGE_L1_BLOCK_TIME_MS, refetchOnMountOrArgChange: 2 * 60, }) }) diff --git a/apps/web/src/state/routing/useRoutingAPITrade.ts b/apps/web/src/state/routing/useRoutingAPITrade.ts index 5839f5ef9aa..e7ac0d45076 100644 --- a/apps/web/src/state/routing/useRoutingAPITrade.ts +++ b/apps/web/src/state/routing/useRoutingAPITrade.ts @@ -1,7 +1,6 @@ import { skipToken } from '@reduxjs/toolkit/query/react' import { Protocol } from '@uniswap/router-sdk' import { Currency, CurrencyAmount, Percent, TradeType } from '@uniswap/sdk-core' -import { AVERAGE_L1_BLOCK_TIME } from 'constants/chains' import useIsWindowVisible from 'hooks/useIsWindowVisible' import { useRoutingAPIArguments } from 'lib/hooks/routing/useRoutingAPIArguments' import ms from 'ms' @@ -16,6 +15,7 @@ import { SubmittableTrade, TradeState, } from 'state/routing/types' +import { AVERAGE_L1_BLOCK_TIME_MS } from 'uniswap/src/features/transactions/swap/hooks/usePollingIntervalByChain' const TRADE_NOT_FOUND = { state: TradeState.NO_ROUTE_FOUND, trade: undefined, currentData: undefined } as const const TRADE_LOADING = { state: TradeState.LOADING, trade: undefined, currentData: undefined } as const @@ -98,7 +98,7 @@ export function useRoutingAPITrade( const { isError, data: tradeResult, error, currentData } = useGetQuoteQueryState(queryArgs) useGetQuoteQuery(skipFetch || !isWindowVisible ? skipToken : queryArgs, { // Price-fetching is informational and costly, so it's done less frequently. - pollingInterval: routerPreference === INTERNAL_ROUTER_PREFERENCE_PRICE ? ms(`1m`) : AVERAGE_L1_BLOCK_TIME, + pollingInterval: routerPreference === INTERNAL_ROUTER_PREFERENCE_PRICE ? ms(`1m`) : AVERAGE_L1_BLOCK_TIME_MS, // If latest quote from cache was fetched > 2m ago, instantly repoll for another instead of waiting for next poll period refetchOnMountOrArgChange: 2 * 60, }) diff --git a/apps/web/src/state/routing/utils.test.ts b/apps/web/src/state/routing/utils.test.ts index 0c299c074f7..25845f3d0dd 100644 --- a/apps/web/src/state/routing/utils.test.ts +++ b/apps/web/src/state/routing/utils.test.ts @@ -2,7 +2,7 @@ import { Currency, Token, TradeType } from '@uniswap/sdk-core' import { GetQuoteArgs, PoolType, RouterPreference, TokenInRoute } from 'state/routing/types' import { computeRoutes } from 'state/routing/utils' import { nativeOnChain } from 'uniswap/src/constants/tokens' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' const USDC = new Token(1, '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 6, 'USDC', undefined, false) const USDC_IN_ROUTE = toTokenInRoute(USDC) diff --git a/apps/web/src/state/sagas/transactions/uniswapx.ts b/apps/web/src/state/sagas/transactions/uniswapx.ts index 2dec0dd1cc6..86dcf3ea3b7 100644 --- a/apps/web/src/state/sagas/transactions/uniswapx.ts +++ b/apps/web/src/state/sagas/transactions/uniswapx.ts @@ -13,13 +13,13 @@ import { call, put } from 'typed-redux-saga' import { UniswapXOrderStatus } from 'types/uniswapx' import { submitOrder } from 'uniswap/src/data/apiClients/tradingApi/TradingApiClient' import { Routing } from 'uniswap/src/data/tradingApi/__generated__' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { InterfaceEventNameLocal } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { HandledTransactionInterrupt } from 'uniswap/src/features/transactions/errors' import { getBaseTradeAnalyticsProperties } from 'uniswap/src/features/transactions/swap/analytics' import { UniswapXSignatureStep } from 'uniswap/src/features/transactions/swap/types/steps' import { UniswapXTrade } from 'uniswap/src/features/transactions/swap/types/trade' -import { UniverseChainId } from 'uniswap/src/types/chains' import { percentFromFloat } from 'utilities/src/format/percent' interface HandleUniswapXSignatureStepParams extends HandleSignatureStepParams { diff --git a/apps/web/src/state/send/SendContext.tsx b/apps/web/src/state/send/SendContext.tsx index 153ae2c40a6..e6cc1e24d4c 100644 --- a/apps/web/src/state/send/SendContext.tsx +++ b/apps/web/src/state/send/SendContext.tsx @@ -11,7 +11,7 @@ import { } from 'react' import { RecipientData, SendInfo, useDerivedSendInfo } from 'state/send/hooks' import { useSwapAndLimitContext } from 'state/swap/useSwapContext' -import { useEnabledChains } from 'uniswap/src/features/settings/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks' export type SendState = { readonly exactAmountToken?: string diff --git a/apps/web/src/state/signatures/hooks.ts b/apps/web/src/state/signatures/hooks.ts index be2b78c1e63..7f311e4e287 100644 --- a/apps/web/src/state/signatures/hooks.ts +++ b/apps/web/src/state/signatures/hooks.ts @@ -11,7 +11,7 @@ import { UniswapXOrderDetails, } from 'state/signatures/types' import { UniswapXOrderStatus } from 'types/uniswapx' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' export function useAllSignatures(): { [id: string]: SignatureDetails } { const account = useAccount() diff --git a/apps/web/src/state/signatures/types.ts b/apps/web/src/state/signatures/types.ts index 0464566922b..9e068ec6677 100644 --- a/apps/web/src/state/signatures/types.ts +++ b/apps/web/src/state/signatures/types.ts @@ -5,7 +5,7 @@ import { AssetActivityPartsFragment, SwapOrderDetailsPartsFragment, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { Prettify } from 'viem/chains' export type OrderActivity = AssetActivityPartsFragment & { details: SwapOrderDetailsPartsFragment } diff --git a/apps/web/src/state/stake/hooks.tsx b/apps/web/src/state/stake/hooks.tsx index d74f51620c9..332ecd6a77a 100644 --- a/apps/web/src/state/stake/hooks.tsx +++ b/apps/web/src/state/stake/hooks.tsx @@ -8,7 +8,7 @@ import JSBI from 'jsbi' import { NEVER_RELOAD, useMultipleContractSingleData } from 'lib/hooks/multicall' import { useMemo } from 'react' import { DAI, UNI, USDC_MAINNET, USDT, WBTC, WRAPPED_NATIVE_CURRENCY } from 'uniswap/src/constants/tokens' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { logger } from 'utilities/src/logger/logger' const STAKING_REWARDS_INTERFACE = new Interface(StakingRewardsJSON.abi) diff --git a/apps/web/src/state/swap/SwapContext.test.tsx b/apps/web/src/state/swap/SwapContext.test.tsx index 130350be02b..95fb381932e 100644 --- a/apps/web/src/state/swap/SwapContext.test.tsx +++ b/apps/web/src/state/swap/SwapContext.test.tsx @@ -5,7 +5,7 @@ import { SwapAndLimitContext, SwapInfo } from 'state/swap/types' import { useSwapAndLimitContext, useSwapContext } from 'state/swap/useSwapContext' import { render, screen } from 'test-utils/render' import { nativeOnChain } from 'uniswap/src/constants/tokens' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { CurrencyField } from 'uniswap/src/types/currency' import { SwapTab } from 'uniswap/src/types/screens/interface' diff --git a/apps/web/src/state/swap/SwapContext.tsx b/apps/web/src/state/swap/SwapContext.tsx index 5b1be57efd9..0931f9cb0ec 100644 --- a/apps/web/src/state/swap/SwapContext.tsx +++ b/apps/web/src/state/swap/SwapContext.tsx @@ -8,8 +8,8 @@ import { PropsWithChildren, useEffect, useMemo, useState } from 'react' import { useDerivedSwapInfo } from 'state/swap/hooks' import { CurrencyState, SwapAndLimitContext, SwapContext, SwapState, initialSwapState } from 'state/swap/types' import { useSwapAndLimitContext } from 'state/swap/useSwapContext' -import { useEnabledChains } from 'uniswap/src/features/settings/hooks' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { CurrencyField } from 'uniswap/src/types/currency' import { SwapTab } from 'uniswap/src/types/screens/interface' import { areCurrenciesEqual } from 'uniswap/src/utils/currencyId' diff --git a/apps/web/src/state/swap/hooks.test.ts b/apps/web/src/state/swap/hooks.test.tsx similarity index 84% rename from apps/web/src/state/swap/hooks.test.ts rename to apps/web/src/state/swap/hooks.test.tsx index 0c34748fb36..8154c7eb9a2 100644 --- a/apps/web/src/state/swap/hooks.test.ts +++ b/apps/web/src/state/swap/hooks.test.tsx @@ -1,10 +1,12 @@ import { UNI_ADDRESSES } from '@uniswap/sdk-core' import { parse } from 'qs' +import { ReactNode } from 'react' import { queryParametersToCurrencyState, useInitialCurrencyState } from 'state/swap/hooks' import { ETH_MAINNET } from 'test-utils/constants' import { renderHook, waitFor } from 'test-utils/render' import { UNI, nativeOnChain } from 'uniswap/src/constants/tokens' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UrlContext } from 'uniswap/src/contexts/UrlContext' +import { UniverseChainId } from 'uniswap/src/features/chains/types' jest.mock('uniswap/src/features/gating/hooks', () => { return { @@ -12,6 +14,15 @@ jest.mock('uniswap/src/features/gating/hooks', () => { } }) +function mockQueryStringInUrlProvider( + qs: Record, +): ({ children }: { children?: ReactNode }) => JSX.Element { + function MockedProvider({ children }: { children?: ReactNode }): JSX.Element { + return qs }}>{children} + } + return MockedProvider +} + describe('hooks', () => { describe('#queryParametersToCurrencyState', () => { test('ETH to DAI on mainnet', () => { @@ -86,8 +97,8 @@ describe('hooks', () => { describe('#useInitialCurrencyState', () => { describe('Disconnected wallet', () => { test('optimism output UNI', () => { - jest.mock('hooks/useParsedQueryString', () => ({ - useParsedQueryString: () => ({ + jest.mock('uniswap/src/contexts/UrlContext', () => ({ + ReactRouterUrlProvider: mockQueryStringInUrlProvider({ inputCurrency: undefined, outputCurrency: UNI_ADDRESSES[UniverseChainId.Optimism], chainId: 10, @@ -108,9 +119,9 @@ describe('hooks', () => { }) test('optimism input ETH, output UNI', () => { - jest.mock('hooks/useParsedQueryString', () => ({ - useParsedQueryString: () => ({ - inputCurrency: 'ETH', + jest.mock('uniswap/src/contexts/UrlContext', () => ({ + ReactRouterUrlProvider: mockQueryStringInUrlProvider({ + inputCurrency: undefined, outputCurrency: UNI_ADDRESSES[UniverseChainId.Optimism], chainId: 10, }), @@ -130,8 +141,8 @@ describe('hooks', () => { }) test('optimism input ETH, output UNI, input value, field output', () => { - jest.mock('hooks/useParsedQueryString', () => ({ - useParsedQueryString: () => ({ + jest.mock('uniswap/src/contexts/UrlContext', () => ({ + ReactRouterUrlProvider: mockQueryStringInUrlProvider({ inputCurrency: 'ETH', outputCurrency: UNI_ADDRESSES[UniverseChainId.Optimism], chainId: 10, @@ -156,8 +167,8 @@ describe('hooks', () => { }) test('empty query should default to ETH mainnet', () => { - jest.mock('hooks/useParsedQueryString', () => ({ - useParsedQueryString: () => ({ + jest.mock('uniswap/src/contexts/UrlContext', () => ({ + ReactRouterUrlProvider: mockQueryStringInUrlProvider({ inputCurrency: undefined, outputCurrency: undefined, chainId: undefined, @@ -211,8 +222,8 @@ describe('hooks', () => { })) test('optimism output UNI', () => { - jest.mock('hooks/useParsedQueryString', () => ({ - useParsedQueryString: () => ({ + jest.mock('uniswap/src/contexts/UrlContext', () => ({ + ReactRouterUrlProvider: mockQueryStringInUrlProvider({ inputCurrency: undefined, outputCurrency: UNI_ADDRESSES[UniverseChainId.Optimism], chainId: 10, @@ -233,8 +244,8 @@ describe('hooks', () => { }) test('mainnet', () => { - jest.mock('hooks/useParsedQueryString', () => ({ - useParsedQueryString: () => ({ + jest.mock('uniswap/src/contexts/UrlContext', () => ({ + ReactRouterUrlProvider: mockQueryStringInUrlProvider({ inputCurrency: undefined, outputCurrency: undefined, chainId: 1, @@ -255,8 +266,8 @@ describe('hooks', () => { }) test('optimism input ETH, output UNI', () => { - jest.mock('hooks/useParsedQueryString', () => ({ - useParsedQueryString: () => ({ + jest.mock('uniswap/src/contexts/UrlContext', () => ({ + ReactRouterUrlProvider: mockQueryStringInUrlProvider({ inputCurrency: 'ETH', outputCurrency: UNI_ADDRESSES[UniverseChainId.Optimism], chainId: 10, @@ -277,8 +288,8 @@ describe('hooks', () => { }) test('empty query should show highest balance native token', () => { - jest.mock('hooks/useParsedQueryString', () => ({ - useParsedQueryString: () => ({ + jest.mock('uniswap/src/contexts/UrlContext', () => ({ + ReactRouterUrlProvider: mockQueryStringInUrlProvider({ inputCurrency: undefined, outputCurrency: undefined, chainId: undefined, diff --git a/apps/web/src/state/swap/hooks.tsx b/apps/web/src/state/swap/hooks.tsx index 2659797deaa..0bb36c931bf 100644 --- a/apps/web/src/state/swap/hooks.tsx +++ b/apps/web/src/state/swap/hooks.tsx @@ -1,12 +1,10 @@ import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core' import { ConnectWalletButtonText } from 'components/NavBar/accountCTAsExperimentUtils' -import { CHAIN_IDS_TO_NAMES, useSupportedChainId } from 'constants/chains' import { NATIVE_CHAIN_ID } from 'constants/tokens' import { useCurrency, useCurrencyInfo } from 'hooks/Tokens' import { useAccount } from 'hooks/useAccount' import useAutoSlippageTolerance from 'hooks/useAutoSlippageTolerance' import { useDebouncedTrade } from 'hooks/useDebouncedTrade' -import useParsedQueryString from 'hooks/useParsedQueryString' import { useSwapTaxes } from 'hooks/useSwapTaxes' import { useUSDPrice } from 'hooks/useUSDPrice' import useNativeCurrency from 'lib/hooks/useNativeCurrency' @@ -19,14 +17,16 @@ import { isClassicTrade, isSubmittableTrade, isUniswapXTrade } from 'state/routi import { CurrencyState, SerializedCurrencyState, SwapInfo, SwapState } from 'state/swap/types' import { useSwapAndLimitContext, useSwapContext } from 'state/swap/useSwapContext' import { useUserSlippageToleranceWithDefault } from 'state/user/hooks' +import { useUrlContext } from 'uniswap/src/contexts/UrlContext' +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' +import { useEnabledChains, useSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { UniverseChainId, isUniverseChainId } from 'uniswap/src/features/chains/types' import { useTokenProjects } from 'uniswap/src/features/dataApi/tokenProjects' -import { useEnabledChains } from 'uniswap/src/features/settings/hooks' import { Trans } from 'uniswap/src/i18n' -import { UniverseChainId } from 'uniswap/src/types/chains' import { CurrencyField } from 'uniswap/src/types/currency' import { areCurrencyIdsEqual, currencyId } from 'uniswap/src/utils/currencyId' import { isAddress } from 'utilities/src/addresses' -import { ParsedChainIdKey, getParsedChainId } from 'utils/chains' +import { getParsedChainId } from 'utils/chainParams' export function useSwapActionHandlers(): { onCurrencySelection: (field: CurrencyField, currency?: Currency) => void @@ -337,10 +337,15 @@ export function serializeSwapStateToURLParameters( const { inputCurrency, outputCurrency, typedValue, independentField, chainId } = state const params = new URLSearchParams() - params.set('chain', CHAIN_IDS_TO_NAMES[chainId]) + params.set('chain', getChainInfo(chainId).interfaceName) - if (outputCurrency && inputCurrency && outputCurrency.chainId !== inputCurrency.chainId) { - params.set('outputChain', CHAIN_IDS_TO_NAMES[outputCurrency.chainId as UniverseChainId]) + if ( + outputCurrency && + inputCurrency && + outputCurrency.chainId !== inputCurrency.chainId && + isUniverseChainId(outputCurrency.chainId) + ) { + params.set('outputChain', getChainInfo(outputCurrency.chainId).interfaceName) } if (inputCurrency) { @@ -365,7 +370,7 @@ export function serializeSwapStateToURLParameters( export function queryParametersToCurrencyState(parsedQs: ParsedQs): SerializedCurrencyState { const chainId = getParsedChainId(parsedQs) - const outputChainId = getParsedChainId(parsedQs, ParsedChainIdKey.OUTPUT) + const outputChainId = getParsedChainId(parsedQs, CurrencyField.OUTPUT) const inputCurrencyId = parseCurrencyFromURLParameter(parsedQs.inputCurrency ?? parsedQs.inputcurrency) const parsedOutputCurrencyId = parseCurrencyFromURLParameter(parsedQs.outputCurrency ?? parsedQs.outputcurrency) const outputCurrencyId = @@ -401,6 +406,7 @@ export function useInitialCurrencyState(): { const { chainId, setIsUserSelectedToken } = useSwapAndLimitContext() const { defaultChainId } = useEnabledChains() + const { useParsedQueryString } = useUrlContext() const parsedQs = useParsedQueryString() const parsedCurrencyState = useMemo(() => { return queryParametersToCurrencyState(parsedQs) diff --git a/apps/web/src/state/swap/types.ts b/apps/web/src/state/swap/types.ts index 76721e52194..98aee5aec5f 100644 --- a/apps/web/src/state/swap/types.ts +++ b/apps/web/src/state/swap/types.ts @@ -1,7 +1,7 @@ import { Currency, CurrencyAmount, Percent } from '@uniswap/sdk-core' import { Dispatch, ReactNode, SetStateAction, createContext } from 'react' import { InterfaceTrade, RouterPreference, TradeState } from 'state/routing/types' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { CurrencyField } from 'uniswap/src/types/currency' import { SwapTab } from 'uniswap/src/types/screens/interface' diff --git a/apps/web/src/state/transactions/hooks.test.tsx b/apps/web/src/state/transactions/hooks.test.tsx index 985f2e402c7..4abe8e6a8e3 100644 --- a/apps/web/src/state/transactions/hooks.test.tsx +++ b/apps/web/src/state/transactions/hooks.test.tsx @@ -15,7 +15,7 @@ import { mocked } from 'test-utils/mocked' import { act, renderHook } from 'test-utils/render' import { USDC_MAINNET } from 'uniswap/src/constants/tokens' import { TransactionStatus } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' const PERMIT2_ADDRESS_MAINNET = permit2Address(UniverseChainId.Mainnet) diff --git a/apps/web/src/state/transactions/hooks.tsx b/apps/web/src/state/transactions/hooks.tsx index 779a6f9bf5b..b58708cdc9d 100644 --- a/apps/web/src/state/transactions/hooks.tsx +++ b/apps/web/src/state/transactions/hooks.tsx @@ -3,7 +3,7 @@ import type { TransactionResponse } from '@ethersproject/providers' import { Token } from '@uniswap/sdk-core' import { useAccount } from 'hooks/useAccount' import { SwapResult } from 'hooks/useSwapCallback' -import { useCallback, useMemo } from 'react' +import { useCallback, useEffect, useMemo } from 'react' import { useAppDispatch, useAppSelector } from 'state/hooks' import { TradeFillType } from 'state/routing/types' import { addTransaction, cancelTransaction, removeTransaction } from 'state/transactions/reducer' @@ -15,7 +15,7 @@ import { } from 'state/transactions/types' import { isConfirmedTx, isPendingTx } from 'state/transactions/utils' import { TransactionStatus } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { COMBINED_CHAIN_IDS, UniverseChainId } from 'uniswap/src/types/chains' +import { ALL_CHAIN_IDS, UniverseChainId } from 'uniswap/src/features/chains/types' // helper that can take a ethers library transaction response and add it to the list of transactions export function useTransactionAdder(): ( @@ -75,7 +75,7 @@ export function useMultichainTransactions(): [TransactionDetails, UniverseChainI return useMemo( () => - COMBINED_CHAIN_IDS.flatMap((chainId) => + ALL_CHAIN_IDS.flatMap((chainId) => state[chainId] ? Object.values(state[chainId]).map((tx): [TransactionDetails, UniverseChainId] => [tx, chainId]) : [], @@ -179,3 +179,30 @@ export function usePendingTransactions(): PendingTransactionDetails[] { [account.address, allTransactions], ) } + +function usePendingLPTransactions(): PendingTransactionDetails[] { + const allTransactions = useAllTransactions() + const account = useAccount() + + return useMemo( + () => + Object.values(allTransactions).filter( + (tx): tx is PendingTransactionDetails => + tx.from === account.address && + isPendingTx(tx) && + [ + TransactionType.INCREASE_LIQUIDITY, + TransactionType.DECREASE_LIQUIDITY, + TransactionType.COLLECT_FEES, + ].includes(tx.info.type), + ), + [account.address, allTransactions], + ) +} + +export function usePendingLPTransactionsChangeListener(callback: () => void) { + const pendingLPTransactions = usePendingLPTransactions() + useEffect(() => { + callback() + }, [pendingLPTransactions, callback]) +} diff --git a/apps/web/src/state/transactions/reducer.test.ts b/apps/web/src/state/transactions/reducer.test.ts index 5333023473e..180277e52ee 100644 --- a/apps/web/src/state/transactions/reducer.test.ts +++ b/apps/web/src/state/transactions/reducer.test.ts @@ -10,7 +10,7 @@ import reducer, { } from 'state/transactions/reducer' import { ConfirmedTransactionDetails, PendingTransactionDetails, TransactionType } from 'state/transactions/types' import { TransactionStatus } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' describe('transaction reducer', () => { let store: Store diff --git a/apps/web/src/state/transactions/reducer.ts b/apps/web/src/state/transactions/reducer.ts index c94dc3adbd3..4988c9355fb 100644 --- a/apps/web/src/state/transactions/reducer.ts +++ b/apps/web/src/state/transactions/reducer.ts @@ -6,7 +6,7 @@ import { TransactionType, } from 'state/transactions/types' import { TransactionStatus } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' // TODO(WEB-2053): update this to be a map of account -> chainId -> txHash -> TransactionDetails // to simplify usage, once we're able to invalidate localstorage diff --git a/apps/web/src/state/transactions/types.ts b/apps/web/src/state/transactions/types.ts index e205b383799..fd4db944f25 100644 --- a/apps/web/src/state/transactions/types.ts +++ b/apps/web/src/state/transactions/types.ts @@ -5,7 +5,7 @@ import { TransactionDetailsPartsFragment, TransactionStatus, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' export type TransactionActivity = AssetActivityPartsFragment & { details: TransactionDetailsPartsFragment } diff --git a/apps/web/src/state/user/hooks.tsx b/apps/web/src/state/user/hooks.tsx index c1eb36228da..e349d1dc242 100644 --- a/apps/web/src/state/user/hooks.tsx +++ b/apps/web/src/state/user/hooks.tsx @@ -1,6 +1,5 @@ import { Percent, Token, V2_FACTORY_ADDRESSES } from '@uniswap/sdk-core' import { Pair, computePairAddress } from '@uniswap/v2-sdk' -import { chainIdToBackendChain, useSupportedChainId } from 'constants/chains' import { L2_DEADLINE_FROM_NOW } from 'constants/misc' import { BASES_TO_TRACK_LIQUIDITY_FOR, PINNED_PAIRS } from 'constants/routing' import { gqlToCurrency } from 'graphql/data/util' @@ -18,11 +17,11 @@ import { } from 'state/user/reducer' import { SerializedPair, SlippageTolerance } from 'state/user/types' import { - Chain, TokenSortableField, useTopTokensQuery, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { isL2ChainId } from 'uniswap/src/features/chains/utils' +import { useEnabledChains, useSupportedChainId } from 'uniswap/src/features/chains/hooks' +import { isL2ChainId, toGraphQLChain } from 'uniswap/src/features/chains/utils' import { deserializeToken, serializeToken } from 'uniswap/src/utils/currency' export function useRouterPreference(): [RouterPreference, (routerPreference: RouterPreference) => void] { @@ -173,12 +172,13 @@ export function toV2LiquidityToken([tokenA, tokenB]: [Token, Token]): Token { */ export function useTrackedTokenPairs(): [Token, Token][] { const { chainId } = useAccount() + const { defaultChainId } = useEnabledChains() const supportedChainId = useSupportedChainId(chainId) // TODO(WEB-4001): use an "all tokens" query for better LP detection const { data: popularTokens } = useTopTokensQuery({ variables: { - chain: supportedChainId ? chainIdToBackendChain({ chainId: supportedChainId }) : Chain.Ethereum, + chain: toGraphQLChain(supportedChainId ?? defaultChainId), orderBy: TokenSortableField.Popularity, page: 1, pageSize: 100, diff --git a/apps/web/src/state/wallets/reducer.ts b/apps/web/src/state/wallets/reducer.ts index 00de61abdc3..a0ca57ec5c3 100644 --- a/apps/web/src/state/wallets/reducer.ts +++ b/apps/web/src/state/wallets/reducer.ts @@ -1,7 +1,7 @@ import { createSlice } from '@reduxjs/toolkit' import { shallowEqual } from 'react-redux' import { Wallet } from 'state/wallets/types' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' export interface ConnectedWalletsState { // Used to track wallets that have been connected by the user in current session, and remove them when deliberately disconnected. diff --git a/apps/web/src/test-utils/bundle-size-test.ts b/apps/web/src/test-utils/bundle-size-test.ts index c1d3f7b18f4..dd407e5a58f 100644 --- a/apps/web/src/test-utils/bundle-size-test.ts +++ b/apps/web/src/test-utils/bundle-size-test.ts @@ -32,8 +32,8 @@ const entryGzipSize = report.reduce( 0, ) -// somewhat arbitrary, based on current size (10/18/2024) -const limit = 2_327_000 +// somewhat arbitrary, based on current size (10/30/2024) +const limit = 2_350_000 if (entryGzipSize > limit) { console.error(`Bundle size has grown too big! Entry JS size is ${entryGzipSize}, over the limit of ${limit}.`) diff --git a/apps/web/src/test-utils/constants.ts b/apps/web/src/test-utils/constants.ts index af733ab278b..8ee14dade17 100644 --- a/apps/web/src/test-utils/constants.ts +++ b/apps/web/src/test-utils/constants.ts @@ -21,10 +21,10 @@ import { nativeOnChain, } from 'uniswap/src/constants/tokens' import { SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' import { FORCountry } from 'uniswap/src/features/fiatOnRamp/types' import { benignSafetyInfo } from 'uniswap/src/test/fixtures' -import { UniverseChainId } from 'uniswap/src/types/chains' import { LimitsExpiry } from 'uniswap/src/types/limits' import { UseAccountReturnType } from 'wagmi' diff --git a/apps/web/src/test-utils/pools/fixtures.ts b/apps/web/src/test-utils/pools/fixtures.ts index 2eeee1a0a5d..be43f6c8721 100644 --- a/apps/web/src/test-utils/pools/fixtures.ts +++ b/apps/web/src/test-utils/pools/fixtures.ts @@ -2,13 +2,14 @@ import { BigNumber } from '@ethersproject/bignumber' import { Currency, WETH9 } from '@uniswap/sdk-core' import { FeeAmount, Pool, Position } from '@uniswap/v3-sdk' import { PoolData } from 'graphql/data/pools/usePoolData' +import { PoolStat } from 'state/explore/types' import { USDC_MAINNET } from 'uniswap/src/constants/tokens' import { Token } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' export const validParams = { poolAddress: '0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640', chainName: 'ethereum' } -export const validBEPoolToken0 = { +const validPoolToken0 = { id: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', symbol: 'USDC', @@ -25,7 +26,10 @@ export const validBEPoolToken0 = { url: 'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', }, }, -} as Token +} + +export const validBEPoolToken0 = validPoolToken0 as Token +export const validRestPoolToken0 = validPoolToken0 as unknown as PoolStat['token0'] export const validUSDCCurrency = { isNative: false, @@ -42,7 +46,7 @@ export const validUSDCCurrency = { wrapped: validBEPoolToken0, } as unknown as Currency -export const validBEPoolToken1 = { +const validPoolToken1 = { symbol: 'WETH', name: 'Wrapped Ether', derivedETH: '1', @@ -59,7 +63,10 @@ export const validBEPoolToken1 = { url: 'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', }, }, -} as Token +} + +export const validBEPoolToken1 = validPoolToken1 as Token +export const validRestPoolToken1 = validPoolToken1 as unknown as PoolStat['token0'] export const owner = '0xf5b6bb25f5beaea03dd014c6ef9fa9f3926bf36c' diff --git a/apps/web/src/test-utils/render.tsx b/apps/web/src/test-utils/render.tsx index 36572a97e06..40c6ca31b60 100644 --- a/apps/web/src/test-utils/render.tsx +++ b/apps/web/src/test-utils/render.tsx @@ -3,7 +3,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { queries } from '@testing-library/dom' import { RenderOptions, render } from '@testing-library/react' import { RenderHookOptions, WrapperComponent, renderHook } from '@testing-library/react-hooks' -import Web3Provider from 'components/Web3Provider' +import Web3Provider, { Web3ProviderUpdater } from 'components/Web3Provider' import { AssetActivityProvider } from 'graphql/data/apollo/AssetActivityProvider' import { TokenBalancesProvider } from 'graphql/data/apollo/TokenBalancesProvider' import { BlockNumberContext } from 'lib/hooks/useBlockNumber' @@ -14,6 +14,7 @@ import { BrowserRouter } from 'react-router-dom' import store from 'state' import { ThemeProvider } from 'theme' import { TamaguiProvider } from 'theme/tamaguiProvider' +import { ReactRouterUrlProvider } from 'uniswap/src/contexts/UrlContext' import { UnitagUpdaterContextProvider } from 'uniswap/src/features/unitags/context' const queryClient = new QueryClient() @@ -33,13 +34,18 @@ const WithProviders = ({ children }: { children?: ReactNode }) => { - - - - {children} - - - + + + + + + + {children} + + + + + diff --git a/apps/web/src/test-utils/tokens/mocks.ts b/apps/web/src/test-utils/tokens/mocks.ts index f3498e8f7c2..88d986fd3e2 100644 --- a/apps/web/src/test-utils/tokens/mocks.ts +++ b/apps/web/src/test-utils/tokens/mocks.ts @@ -26,7 +26,7 @@ import { import { mocked } from 'test-utils/mocked' import { COMMON_BASES } from 'uniswap/src/constants/routing' import { DAI, DAI_ARBITRUM_ONE, USDC_ARBITRUM, USDC_MAINNET, USDT, WBTC } from 'uniswap/src/constants/tokens' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { isSameAddress } from 'utilities/src/addresses' beforeEach(() => { diff --git a/apps/web/src/theme/components/RadialGradientByChainUpdater.ts b/apps/web/src/theme/components/RadialGradientByChainUpdater.ts index 2926c649883..6fd4d369bab 100644 --- a/apps/web/src/theme/components/RadialGradientByChainUpdater.ts +++ b/apps/web/src/theme/components/RadialGradientByChainUpdater.ts @@ -3,7 +3,7 @@ import { useIsNftPage } from 'hooks/useIsNftPage' import { useEffect } from 'react' import { darkTheme, lightTheme } from 'theme/colors' import { useDarkModeManager } from 'theme/components/ThemeToggle' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' const initialStyles = { width: '200vw', diff --git a/apps/web/src/tracing/request.ts b/apps/web/src/tracing/request.ts index 8aec6f9d900..c08a75b4067 100644 --- a/apps/web/src/tracing/request.ts +++ b/apps/web/src/tracing/request.ts @@ -1,7 +1,7 @@ -import { INFURA_PREFIX_TO_CHAIN_ID, chainIdToBackendChain } from 'constants/chains' import { isTracing, trace } from 'tracing/trace' import { TraceContext } from 'tracing/types' import { Chain } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { getChainIdByInfuraPrefix, toGraphQLChain } from 'uniswap/src/features/chains/utils' export function patchFetch(api: Pick) { const apiFetch = api.fetch @@ -95,8 +95,11 @@ export function getTraceContext(url: URL, init?: RequestInit, force = false): Tr try { const body = JSON.parse(Buffer.from(init?.body as Uint8Array).toString()) method = body.method - const chainId = INFURA_PREFIX_TO_CHAIN_ID[url.host.split('.')[0]] - chain = chainId && chainIdToBackendChain({ chainId }) + + const chainId = getChainIdByInfuraPrefix(url.host.split('.')[0]) + if (chainId) { + chain = toGraphQLChain(chainId) + } } catch { // ignore the error } diff --git a/apps/web/src/utils/chainParams.ts b/apps/web/src/utils/chainParams.ts new file mode 100644 index 00000000000..713278fd499 --- /dev/null +++ b/apps/web/src/utils/chainParams.ts @@ -0,0 +1,46 @@ +import { ParsedQs } from 'qs' +import { useParams } from 'react-router-dom' +// eslint-disable-next-line no-restricted-imports +import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/features/chains/chainInfo' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { CurrencyField } from 'uniswap/src/types/currency' + +// i.e. ?chain=mainnet -> ethereum +export function searchParamToBackendName(interfaceName: string | null): string | undefined { + if (interfaceName === null) { + return undefined + } + + const chain = Object.values(UNIVERSE_CHAIN_INFO).find((item) => item.interfaceName === interfaceName) + return chain ? chain.urlParam : undefined +} + +export function isChainUrlParam(str: string): boolean { + return !!str && Object.values(UNIVERSE_CHAIN_INFO).some((chain) => chain.urlParam === str) +} + +export function getChainIdFromChainUrlParam(chainUrlParam?: string): UniverseChainId | undefined { + return chainUrlParam !== undefined + ? Object.values(UNIVERSE_CHAIN_INFO).find((chain) => chainUrlParam === chain.urlParam)?.id + : undefined +} + +export function useChainIdFromUrlParam(): UniverseChainId | undefined { + const chainName = useParams<{ chainName?: string }>().chainName + // In the case where /explore/:chainName is used, the chainName is passed as a tab param + const tab = useParams<{ tab?: string }>().tab + return getChainIdFromChainUrlParam(chainName ?? tab) +} + +export function getParsedChainId( + parsedQs?: ParsedQs, + key: CurrencyField = CurrencyField.INPUT, +): UniverseChainId | undefined { + const chain = key === CurrencyField.INPUT ? parsedQs?.chain : parsedQs?.outputChain + if (!chain || typeof chain !== 'string') { + return undefined + } + + const chainInfo = Object.values(UNIVERSE_CHAIN_INFO).find((i) => i.interfaceName === chain) + return chainInfo?.id +} diff --git a/apps/web/src/utils/chains.tsx b/apps/web/src/utils/chains.tsx deleted file mode 100644 index a6db9e43d37..00000000000 --- a/apps/web/src/utils/chains.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { CHAIN_IDS_TO_NAMES } from 'constants/chains' -import { ParsedQs } from 'qs' - -function getChainIdFromName(name: string) { - const entry = Object.entries(CHAIN_IDS_TO_NAMES).find(([, n]) => n === name) - const chainId = entry?.[0] - return chainId ? parseInt(chainId) : undefined -} - -export enum ParsedChainIdKey { - INPUT = 'input', - OUTPUT = 'output', -} - -export function getParsedChainId(parsedQs?: ParsedQs, key: ParsedChainIdKey = ParsedChainIdKey.INPUT) { - const chain = key === ParsedChainIdKey.INPUT ? parsedQs?.chain : parsedQs?.outputChain - if (!chain || typeof chain !== 'string') { - return undefined - } - - return getChainIdFromName(chain) -} diff --git a/apps/web/src/utils/currencyId.ts b/apps/web/src/utils/currencyId.ts index 34ac1386b03..1088c452466 100644 --- a/apps/web/src/utils/currencyId.ts +++ b/apps/web/src/utils/currencyId.ts @@ -1,5 +1,6 @@ import { Currency } from '@uniswap/sdk-core' +/** @deprecated confusing since currencyId from packages/uniswap is formatted as `chainId-address` */ export function currencyId(currency?: Currency): string { if (currency?.isNative) { return 'ETH' diff --git a/apps/web/src/utils/currencyKey.ts b/apps/web/src/utils/currencyKey.ts index 1caeb79f474..03a86913b6f 100644 --- a/apps/web/src/utils/currencyKey.ts +++ b/apps/web/src/utils/currencyKey.ts @@ -1,8 +1,8 @@ import { Currency } from '@uniswap/sdk-core' import { NATIVE_CHAIN_ID } from 'constants/tokens' -import { supportedChainIdFromGQLChain } from 'graphql/data/util' import { Chain, TokenStandard } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { fromGraphQLChain } from 'uniswap/src/features/chains/utils' export type CurrencyKey = string @@ -20,7 +20,7 @@ export function currencyKeyFromGraphQL(contract: { chain: Chain standard?: TokenStandard }): CurrencyKey { - const chainId = supportedChainIdFromGQLChain(contract.chain) + const chainId = fromGraphQLChain(contract.chain) const address = contract.standard === TokenStandard.Native ? NATIVE_CHAIN_ID : contract.address if (!address) { throw new Error('Non-native token missing address') diff --git a/apps/web/src/utils/formatNumbers.test.ts b/apps/web/src/utils/formatNumbers.test.ts index a0061539023..1243ed4d5da 100644 --- a/apps/web/src/utils/formatNumbers.test.ts +++ b/apps/web/src/utils/formatNumbers.test.ts @@ -1,17 +1,15 @@ import { renderHook } from '@testing-library/react' import { CurrencyAmount, Percent } from '@uniswap/sdk-core' -import { useActiveLocalCurrency } from 'hooks/useActiveLocalCurrency' -import { useActiveLocale } from 'hooks/useActiveLocale' import { mocked } from 'test-utils/mocked' import { USDC_MAINNET } from 'uniswap/src/constants/tokens' import { Currency } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { DEFAULT_LOCAL_CURRENCY, FiatCurrency } from 'uniswap/src/features/fiatCurrency/constants' import { useAppFiatCurrency } from 'uniswap/src/features/fiatCurrency/hooks' import { Locale } from 'uniswap/src/features/language/constants' +import { useCurrentLocale } from 'uniswap/src/features/language/hooks' import { NumberType, useFormatter } from 'utils/formatNumbers' -jest.mock('hooks/useActiveLocale') -jest.mock('hooks/useActiveLocalCurrency') +jest.mock('uniswap/src/features/language/hooks') jest.mock('uniswap/src/features/fiatCurrency/hooks') describe('formatNumber', () => { @@ -28,7 +26,7 @@ describe('formatNumber', () => { }) it('formats token reference numbers correctly with Dutch locale', () => { - mocked(useActiveLocale).mockReturnValue(Locale.DutchNetherlands) + mocked(useCurrentLocale).mockReturnValue(Locale.DutchNetherlands) const { formatNumber } = renderHook(() => useFormatter()).result.current expect(formatNumber({ input: 1234567000000000, type: NumberType.TokenNonTx })).toBe('>999\xa0bln.') @@ -62,7 +60,7 @@ describe('formatNumber', () => { }) it('formats token transaction numbers correctly with russian locale', () => { - mocked(useActiveLocale).mockReturnValue(Locale.RussianRussia) + mocked(useCurrentLocale).mockReturnValue(Locale.RussianRussia) const { formatNumber } = renderHook(() => useFormatter()).result.current expect(formatNumber({ input: 1234567.8901, type: NumberType.TokenTx })).toBe('1\xa0234\xa0567,89') @@ -98,8 +96,8 @@ describe('formatNumber', () => { }) it('formats fiat estimates on token details pages correctly with french locale and euro currency', () => { - mocked(useActiveLocale).mockReturnValue(Locale.FrenchFrance) - mocked(useActiveLocalCurrency).mockReturnValue(FiatCurrency.Euro) + mocked(useCurrentLocale).mockReturnValue(Locale.FrenchFrance) + mocked(useAppFiatCurrency).mockReturnValue(FiatCurrency.Euro) const { formatNumber } = renderHook(() => useFormatter()).result.current expect(formatNumber({ input: 1234567.891, type: NumberType.FiatTokenDetails })).toBe('1,23\xa0M\xa0€') @@ -131,8 +129,8 @@ describe('formatNumber', () => { }) it('formats fiat estimates for tokens correctly with spanish locale and yen currency', () => { - mocked(useActiveLocale).mockReturnValue(Locale.SpanishSpain) - mocked(useActiveLocalCurrency).mockReturnValue(FiatCurrency.JapaneseYen) + mocked(useCurrentLocale).mockReturnValue(Locale.SpanishSpain) + mocked(useAppFiatCurrency).mockReturnValue(FiatCurrency.JapaneseYen) const { formatNumber } = renderHook(() => useFormatter()).result.current expect(formatNumber({ input: 1234567.891, type: NumberType.FiatTokenPrice })).toBe('1,23\xa0M¥') @@ -163,8 +161,8 @@ describe('formatNumber', () => { }) it('formats fiat estimates for token stats correctly with japenese locale and cad currency', () => { - mocked(useActiveLocale).mockReturnValue(Locale.JapaneseJapan) - mocked(useActiveLocalCurrency).mockReturnValue(FiatCurrency.CanadianDollar) + mocked(useCurrentLocale).mockReturnValue(Locale.JapaneseJapan) + mocked(useAppFiatCurrency).mockReturnValue(FiatCurrency.CanadianDollar) const { formatNumber } = renderHook(() => useFormatter()).result.current expect(formatNumber({ input: 1234576, type: NumberType.FiatTokenStats })).toBe('CA$123.5万') @@ -186,8 +184,8 @@ describe('formatNumber', () => { }) it('formats gas prices correctly with portugese locale and thai baht currency', () => { - mocked(useActiveLocale).mockReturnValue(Locale.PortugueseBrazil) - mocked(useActiveLocalCurrency).mockReturnValue(FiatCurrency.ThaiBaht) + mocked(useCurrentLocale).mockReturnValue(Locale.PortugueseBrazil) + mocked(useAppFiatCurrency).mockReturnValue(FiatCurrency.ThaiBaht) const { formatNumber } = renderHook(() => useFormatter()).result.current expect(formatNumber({ input: 1234567.891, type: NumberType.FiatGasPrice })).toBe('฿\xa01,23\xa0mi') @@ -206,7 +204,7 @@ describe('formatNumber', () => { }) it('formats token quantities prices correctly with nigerian naira currency', () => { - mocked(useActiveLocalCurrency).mockReturnValue(FiatCurrency.NigerianNaira) + mocked(useAppFiatCurrency).mockReturnValue(FiatCurrency.NigerianNaira) const { formatNumber } = renderHook(() => useFormatter()).result.current expect(formatNumber({ input: 1234567.891, type: NumberType.FiatTokenQuantity })).toBe('₦1.23M') @@ -237,7 +235,7 @@ describe('formatNumber', () => { }) it('formats Swap text input/output numbers correctly with Korean locale', () => { - mocked(useActiveLocale).mockReturnValue(Locale.KoreanKorea) + mocked(useCurrentLocale).mockReturnValue(Locale.KoreanKorea) const { formatNumber } = renderHook(() => useFormatter()).result.current expect(formatNumber({ input: 1234567.8901, type: NumberType.SwapTradeAmount })).toBe('1234570') @@ -283,8 +281,8 @@ describe('formatNumber', () => { }) it('formats NFT numbers correctly with brazilian portuguese locale and brazilian real currency', () => { - mocked(useActiveLocale).mockReturnValue(Locale.PortugueseBrazil) - mocked(useActiveLocalCurrency).mockReturnValue(FiatCurrency.BrazilianReal) + mocked(useCurrentLocale).mockReturnValue(Locale.PortugueseBrazil) + mocked(useAppFiatCurrency).mockReturnValue(FiatCurrency.BrazilianReal) const { formatNumber } = renderHook(() => useFormatter()).result.current expect(formatNumber({ input: 1234567000000000, type: NumberType.NFTTokenFloorPrice })).toBe('>999\xa0tri') @@ -327,8 +325,8 @@ describe('formatUSDPrice', () => { }) it('format fiat price correctly in euros with french locale', () => { - mocked(useActiveLocalCurrency).mockReturnValue(FiatCurrency.Euro) - mocked(useActiveLocale).mockReturnValue(Locale.FrenchFrance) + mocked(useAppFiatCurrency).mockReturnValue(FiatCurrency.Euro) + mocked(useCurrentLocale).mockReturnValue(Locale.FrenchFrance) const { formatFiatPrice } = renderHook(() => useFormatter()).result.current expect(formatFiatPrice({ price: 0.000000009876 })).toBe('<0,00000001\xa0€') @@ -363,7 +361,7 @@ describe('formatPercent', () => { }) it('correctly formats a percent with french locale', () => { - mocked(useActiveLocale).mockReturnValue(Locale.FrenchFrance) + mocked(useCurrentLocale).mockReturnValue(Locale.FrenchFrance) const { formatPercent } = renderHook(() => useFormatter()).result.current expect(formatPercent(new Percent(1, 100000))).toBe('0,001%') @@ -383,7 +381,7 @@ describe('formatReviewSwapCurrencyAmount', () => { }) it('should use TokenTx formatting under a default length with french locales', () => { - mocked(useActiveLocale).mockReturnValue(Locale.FrenchFrance) + mocked(useCurrentLocale).mockReturnValue(Locale.FrenchFrance) const { formatReviewSwapCurrencyAmount } = renderHook(() => useFormatter()).result.current const currencyAmount = CurrencyAmount.fromRawAmount(USDC_MAINNET, '2000000000') // 2,000 USDC @@ -398,7 +396,7 @@ describe('formatReviewSwapCurrencyAmount', () => { }) it('should use SwapTradeAmount formatting over the default length with french locales', () => { - mocked(useActiveLocale).mockReturnValue(Locale.FrenchFrance) + mocked(useCurrentLocale).mockReturnValue(Locale.FrenchFrance) const { formatReviewSwapCurrencyAmount } = renderHook(() => useFormatter()).result.current const currencyAmount = CurrencyAmount.fromRawAmount(USDC_MAINNET, '2000000000000') // 2,000,000 USDC @@ -424,7 +422,7 @@ describe('formatDelta', () => { }) it('correctly formats a percent with 2 decimal places in french locale', () => { - mocked(useActiveLocale).mockReturnValue(Locale.FrenchFrance) + mocked(useCurrentLocale).mockReturnValue(Locale.FrenchFrance) const { formatDelta } = renderHook(() => useFormatter()).result.current expect(formatDelta(0)).toBe('0,00%') diff --git a/apps/web/src/utils/formatNumbers.ts b/apps/web/src/utils/formatNumbers.ts index c781b96e537..dc0101c5cc9 100644 --- a/apps/web/src/utils/formatNumbers.ts +++ b/apps/web/src/utils/formatNumbers.ts @@ -1,16 +1,15 @@ import { formatEther as ethersFormatEther } from '@ethersproject/units' import { Currency, CurrencyAmount, Percent, Price } from '@uniswap/sdk-core' import { getCurrencySymbolDisplayType } from 'constants/localCurrencies' -import { useActiveLocalCurrency } from 'hooks/useActiveLocalCurrency' -import { useActiveLocale } from 'hooks/useActiveLocale' import usePrevious from 'hooks/usePrevious' import { useCallback, useMemo } from 'react' import { Bound } from 'state/mint/v3/actions' import { DEFAULT_LOCAL_CURRENCY, FiatCurrency } from 'uniswap/src/features/fiatCurrency/constants' +import { useAppFiatCurrency } from 'uniswap/src/features/fiatCurrency/hooks' import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext' import { DEFAULT_LOCALE, Locale } from 'uniswap/src/features/language/constants' +import { useCurrentLocale } from 'uniswap/src/features/language/hooks' -type Nullish = T | null | undefined type NumberFormatOptions = Intl.NumberFormatOptions // Number formatting follows the standards laid out in this spec: @@ -507,7 +506,7 @@ function getFormatterRule(input: number, type: FormatterType, conversionRate?: n } interface FormatNumberOptions { - input: Nullish + input: Maybe type?: FormatterType placeholder?: string locale?: Locale @@ -561,7 +560,7 @@ function formatNumber({ } interface FormatCurrencyAmountOptions { - amount: Nullish> + amount: Maybe> type?: FormatterType placeholder?: string locale?: Locale @@ -599,7 +598,7 @@ function formatPercent(percent: Percent | undefined, locale: Locale = DEFAULT_LO } // Used to format floats representing percent change with fixed decimal places -function formatDelta(delta: Nullish, locale: Locale = DEFAULT_LOCALE) { +function formatDelta(delta: Maybe, locale: Locale = DEFAULT_LOCALE) { if (delta === null || delta === undefined || delta === Infinity || isNaN(delta)) { return '-' } @@ -612,7 +611,7 @@ function formatDelta(delta: Nullish, locale: Locale = DEFAULT_LOCALE) { } interface FormatPriceOptions { - price: Nullish> + price: Maybe> type?: FormatterType locale?: Locale localCurrency?: FiatCurrency @@ -672,7 +671,7 @@ function formatTickPrice({ } interface FormatNumberOrStringOptions { - input: Nullish + input: Maybe type: FormatterType locale?: Locale localCurrency?: FiatCurrency @@ -696,7 +695,7 @@ function formatNumberOrString({ } interface FormatEtherOptions { - input: Nullish + input: Maybe type: FormatterType locale?: Locale localCurrency?: FiatCurrency @@ -710,7 +709,7 @@ function formatEther({ input, type, locale, localCurrency }: FormatEtherOptions) } interface FormatFiatPriceOptions { - price: Nullish + price: Maybe type?: FormatterType locale?: Locale localCurrency?: FiatCurrency @@ -737,78 +736,6 @@ function formatReviewSwapCurrencyAmount(amount: CurrencyAmount, locale return formattedAmount } -// TODO: https://linear.app/uniswap/issue/WEB-3495/import-useasyncdata-from-mobile -type FiatCurrencyComponents = { - groupingSeparator: string - decimalSeparator: string - symbol: string - fullSymbol: string // Some currencies have whitespace in between number and currency - symbolAtFront: boolean // All currencies are at front or back except CVE, which we won't ever support -} - -/** - * Helper function to return components of a currency value for a specific locale - * E.g. comma, period, or space for separating thousands - */ -export function getFiatCurrencyComponents( - locale = DEFAULT_LOCALE, - localCurrency = DEFAULT_LOCAL_CURRENCY, -): FiatCurrencyComponents { - const format = new Intl.NumberFormat(locale, { - ...TWO_DECIMALS_CURRENCY, - currency: localCurrency, - currencyDisplay: getCurrencySymbolDisplayType(localCurrency), - }) - - // See MDN for official docs https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/formatToParts - // Returns something like [{"type":"currency","value":"$"},{"type":"integer","value":"1"}] - const parts = format.formatToParts(1000000.0) // This number should provide both types of separators - let groupingSeparator = ',' - let decimalSeparator = '.' - let symbol = '' - let fullSymbol = '' - let symbolAtFront = true - - parts.forEach((part, index) => { - if (part.type === 'group') { - groupingSeparator = part.value - } else if (part.type === 'decimal') { - decimalSeparator = part.value - } else if (part.type === 'currency') { - symbol = part.value - fullSymbol = symbol - - symbolAtFront = index === 0 - const nextPart = symbolAtFront ? parts[index + 1] : parts[index - 1] - // Check for additional characters between symbol and number, like whitespace - if (nextPart?.type === 'literal') { - fullSymbol = symbolAtFront ? symbol + nextPart.value : nextPart.value + symbol - } - } - }) - - return { - groupingSeparator, - decimalSeparator, - symbol, - fullSymbol, - symbolAtFront, - } -} - -export function useFormatterLocales(): { - formatterLocale: Locale - formatterLocalCurrency: FiatCurrency -} { - const activeLocale = useActiveLocale() - const activeLocalCurrency = useActiveLocalCurrency() - - return { - formatterLocale: activeLocale, - formatterLocalCurrency: activeLocalCurrency, - } -} - function handleFallbackCurrency( selectedCurrency: FiatCurrency, previousSelectedCurrency: FiatCurrency | undefined, @@ -827,16 +754,17 @@ function handleFallbackCurrency( // Constructs an object that injects the correct locale and local currency into each of the above formatter functions. export function useFormatter() { - const { formatterLocale, formatterLocalCurrency } = useFormatterLocales() + const activeLocale = useCurrentLocale() + const activeLocalCurrency = useAppFiatCurrency() const { convertFiatAmount, conversionRate: localCurrencyConversionRate } = useLocalizationContext() - const previousSelectedCurrency = usePrevious(formatterLocalCurrency) + const previousSelectedCurrency = usePrevious(activeLocalCurrency) const previousConversionRate = usePrevious(localCurrencyConversionRate) const shouldFallbackToPrevious = !localCurrencyConversionRate const shouldFallbackToUSD = !localCurrencyConversionRate const currencyToFormatWith = handleFallbackCurrency( - formatterLocalCurrency, + activeLocalCurrency, previousSelectedCurrency, previousConversionRate, shouldFallbackToUSD, @@ -851,102 +779,102 @@ export function useFormatter() { (options: Omit) => formatNumber({ ...options, - locale: formatterLocale, + locale: activeLocale, localCurrency: currencyToFormatWith, conversionRate: localCurrencyConversionRateToFormatWith, }), - [currencyToFormatWith, formatterLocale, localCurrencyConversionRateToFormatWith], + [currencyToFormatWith, activeLocale, localCurrencyConversionRateToFormatWith], ) const formatCurrencyAmountWithLocales = useCallback( (options: Omit) => formatCurrencyAmount({ ...options, - locale: formatterLocale, + locale: activeLocale, localCurrency: currencyToFormatWith, conversionRate: localCurrencyConversionRateToFormatWith, }), - [currencyToFormatWith, formatterLocale, localCurrencyConversionRateToFormatWith], + [currencyToFormatWith, activeLocale, localCurrencyConversionRateToFormatWith], ) const formatPriceWithLocales = useCallback( (options: Omit) => formatPrice({ ...options, - locale: formatterLocale, + locale: activeLocale, localCurrency: currencyToFormatWith, conversionRate: localCurrencyConversionRateToFormatWith, }), - [currencyToFormatWith, formatterLocale, localCurrencyConversionRateToFormatWith], + [currencyToFormatWith, activeLocale, localCurrencyConversionRateToFormatWith], ) const formatReviewSwapCurrencyAmountWithLocales = useCallback( - (amount: CurrencyAmount) => formatReviewSwapCurrencyAmount(amount, formatterLocale), - [formatterLocale], + (amount: CurrencyAmount) => formatReviewSwapCurrencyAmount(amount, activeLocale), + [activeLocale], ) const formatTickPriceWithLocales = useCallback( (options: Omit) => formatTickPrice({ ...options, - locale: formatterLocale, + locale: activeLocale, localCurrency: currencyToFormatWith, conversionRate: localCurrencyConversionRateToFormatWith, }), - [currencyToFormatWith, formatterLocale, localCurrencyConversionRateToFormatWith], + [currencyToFormatWith, activeLocale, localCurrencyConversionRateToFormatWith], ) const formatNumberOrStringWithLocales = useCallback( (options: Omit) => formatNumberOrString({ ...options, - locale: formatterLocale, + locale: activeLocale, localCurrency: currencyToFormatWith, conversionRate: localCurrencyConversionRateToFormatWith, }), - [currencyToFormatWith, formatterLocale, localCurrencyConversionRateToFormatWith], + [currencyToFormatWith, activeLocale, localCurrencyConversionRateToFormatWith], ) const formatFiatPriceWithLocales = useCallback( (options: Omit) => formatFiatPrice({ ...options, - locale: formatterLocale, + locale: activeLocale, localCurrency: currencyToFormatWith, conversionRate: localCurrencyConversionRateToFormatWith, }), - [currencyToFormatWith, formatterLocale, localCurrencyConversionRateToFormatWith], + [currencyToFormatWith, activeLocale, localCurrencyConversionRateToFormatWith], ) const formatDeltaWithLocales = useCallback( - (percent: Nullish) => formatDelta(percent, formatterLocale), - [formatterLocale], + (percent: Maybe) => formatDelta(percent, activeLocale), + [activeLocale], ) const formatPercentWithLocales = useCallback( - (percent: Percent | undefined) => formatPercent(percent, formatterLocale), - [formatterLocale], + (percent: Percent | undefined) => formatPercent(percent, activeLocale), + [activeLocale], ) const formatEtherwithLocales = useCallback( (options: Omit) => formatEther({ ...options, - locale: formatterLocale, + locale: activeLocale, localCurrency: currencyToFormatWith, }), - [currencyToFormatWith, formatterLocale], + [currencyToFormatWith, activeLocale], ) const formatConvertedFiatNumberOrString = useCallback( (options: Omit) => formatNumberOrString({ ...options, - locale: formatterLocale, + locale: activeLocale, localCurrency: currencyToFormatWith, conversionRate: undefined, }), - [currencyToFormatWith, formatterLocale], + [currencyToFormatWith, activeLocale], ) return useMemo( diff --git a/apps/web/src/utils/getInitialLogoURL.ts b/apps/web/src/utils/getInitialLogoURL.ts index 5178ab95085..544e65e05d9 100644 --- a/apps/web/src/utils/getInitialLogoURL.ts +++ b/apps/web/src/utils/getInitialLogoURL.ts @@ -1,6 +1,7 @@ -import { getChain, isSupportedChainId } from 'constants/chains' import { CELO_LOGO } from 'ui/src/assets' import { isCelo, nativeOnChain } from 'uniswap/src/constants/tokens' +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' +import { isUniverseChainId } from 'uniswap/src/features/chains/types' import { isAddress } from 'utilities/src/addresses' export function getInitialLogoUrl( @@ -9,9 +10,7 @@ export function getInitialLogoUrl( isNative?: boolean, backupImg?: string | null, ) { - const networkName = isSupportedChainId(chainId) - ? getChain({ chainId }).assetRepoNetworkName ?? 'ethereum' - : 'ethereum' + const networkName = isUniverseChainId(chainId) ? getChainInfo(chainId).assetRepoNetworkName ?? 'ethereum' : 'ethereum' const checksummedAddress = isAddress(address) if (chainId && isCelo(chainId) && address === nativeOnChain(chainId).wrapped.address) { diff --git a/apps/web/src/utils/getSupportedChainIdsFromWalletConnectSession.ts b/apps/web/src/utils/getSupportedChainIdsFromWalletConnectSession.ts index 62e85f67502..bdf026145da 100644 --- a/apps/web/src/utils/getSupportedChainIdsFromWalletConnectSession.ts +++ b/apps/web/src/utils/getSupportedChainIdsFromWalletConnectSession.ts @@ -1,5 +1,5 @@ import type { SessionTypes } from '@walletconnect/types' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' // Helper function to extract chainId from string in format 'eip155:{chainId}' function getChainIdFromFormattedString(item: string): number | null { diff --git a/apps/web/src/utils/nativeTokens.ts b/apps/web/src/utils/nativeTokens.ts index a9e69861e6a..aed156c492e 100644 --- a/apps/web/src/utils/nativeTokens.ts +++ b/apps/web/src/utils/nativeTokens.ts @@ -1,6 +1,6 @@ -import { getChain } from 'constants/chains' import { supportedChainIdFromGQLChain } from 'graphql/data/util' import { Chain } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' export function getNativeTokenDBAddress(chain: Chain): string | undefined { const pageChainId = supportedChainIdFromGQLChain(chain) @@ -8,5 +8,5 @@ export function getNativeTokenDBAddress(chain: Chain): string | undefined { return undefined } - return getChain({ chainId: pageChainId }).backendChain.nativeTokenBackendAddress + return getChainInfo(pageChainId).backendChain.nativeTokenBackendAddress } diff --git a/apps/web/src/utils/transfer.test.ts b/apps/web/src/utils/transfer.test.ts index b6f9e52e97f..d35024aefca 100644 --- a/apps/web/src/utils/transfer.test.ts +++ b/apps/web/src/utils/transfer.test.ts @@ -2,7 +2,7 @@ import { ExternalProvider, JsonRpcProvider, JsonRpcSigner, Web3Provider } from ' import { CurrencyAmount } from '@uniswap/sdk-core' import { act, renderHook } from 'test-utils/render' import { DAI, nativeOnChain } from 'uniswap/src/constants/tokens' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { useCreateTransferTransaction } from 'utils/transfer' type Mutable = { -readonly [P in keyof T]: T[P] } diff --git a/apps/web/src/utils/transfer.ts b/apps/web/src/utils/transfer.ts index 620e950fc5c..c72f2d1bfaa 100644 --- a/apps/web/src/utils/transfer.ts +++ b/apps/web/src/utils/transfer.ts @@ -5,7 +5,7 @@ import { useWeb3React } from '@web3-react/core' import { useCallback } from 'react' import ERC20_ABI from 'uniswap/src/abis/erc20.json' import { Erc20 } from 'uniswap/src/abis/types' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { getContract } from 'utilities/src/contracts/getContract' import { logger } from 'utilities/src/logger/logger' import { useAsyncData } from 'utilities/src/react/hooks' diff --git a/config/jest-presets/jest/setup.js b/config/jest-presets/jest/setup.js index 8ebf9d11221..166379a9f5c 100644 --- a/config/jest-presets/jest/setup.js +++ b/config/jest-presets/jest/setup.js @@ -89,14 +89,6 @@ jest.mock('@walletconnect/utils', () => ({ buildApprovedNamespaces: jest.fn(), })) -// Mock Sentry crash reporting -jest.mock('@sentry/react-native', () => ({ - init: () => jest.fn(), - wrap: (val) => val, - ReactNavigationInstrumentation: jest.fn(), - ReactNativeTracing: jest.fn(), -})) - jest.mock('react-native-appsflyer', () => { return { initSdk: jest.fn(), diff --git a/dangerfile.ts b/dangerfile.ts index 306feaa902b..456c8234998 100644 --- a/dangerfile.ts +++ b/dangerfile.ts @@ -33,6 +33,29 @@ async function getLinesAddedByFile(files: string[], { exclude = [] }: { exclude? ) } +function checkGeneralizedHookFiles() { + const touchedFiles = danger.git.modified_files.concat(danger.git.created_files) + + touchedFiles.forEach((file) => { + const isGeneralHookFile = file.endsWith('hooks.ts') || file.endsWith('hooks.tsx') + const isNonSpecificHookFile = file.includes('/hooks/') && !file.includes('/use') + + const sharedWarningExampleExplanation = `e.g. \`hooks/useXXX.ts{x}\`. This helps in development for discovery and navigation purposes.` + + if (isGeneralHookFile) { + warn( + `\`${file}\` should be split out into a \`hooks/*\` folder with a hook per file, ${sharedWarningExampleExplanation}`, + ) + } + + if (isNonSpecificHookFile) { + warn( + `\`${file}\` should only have one exported hook per file and be named accordingly, ${sharedWarningExampleExplanation}`, + ) + } + }) +} + // Put any files here that we explicitly want to ignore! const IGNORED_SPLIT_RULE_FILES: string[] = [] @@ -240,6 +263,9 @@ if (envChanged) { // Check native and web file splits checkSplitFiles() +// Check hook file pattern +checkGeneralizedHookFiles() + // Run checks on added changes processAddChanges() diff --git a/package.json b/package.json index c1a3ff439f9..962e1ddb2f3 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "@uniswap/v2-sdk": "4.6.1", "@uniswap/router-sdk": "1.14.3", "@apollo/client": "3.10.4", - "@uniswap/sdk-core": "5.8.4", + "@uniswap/sdk-core": "5.9.0", "@react-navigation/routers": "6.1.9", "@react-navigation/core": "6.2.2", "@sideway/formula": "3.0.1", @@ -61,7 +61,6 @@ "json5": "2.2.2", "expo/uuid": "3.4.0", "is-core-module": "2.13.0", - "qs": "6.11.0", "react": "18.2.0", "react-dom": "18.2.0", "react-native-fast-image@8.6.3": "patch:react-native-fast-image@npm%3A8.6.3#./.yarn/patches/react-native-fast-image-npm-8.6.3-03ee2d23c0.patch", diff --git a/packages/eslint-config/__snapshots__/preset.test.ts.snap b/packages/eslint-config/__snapshots__/preset.test.ts.snap index 3002605fd37..6b830d8e97b 100644 --- a/packages/eslint-config/__snapshots__/preset.test.ts.snap +++ b/packages/eslint-config/__snapshots__/preset.test.ts.snap @@ -902,6 +902,13 @@ exports[`should have a correct configuration for a React file 1`] = ` "message": "Use specific imports (e.g. \`import isEqual from 'lodash/isEqual'\`) to avoid pulling in all of lodash to web to keep bundle size down!", "name": "lodash", }, + { + "importNames": [ + "UNIVERSE_CHAIN_INFO", + ], + "message": "Use useChainInfo or helpers in packages/uniswap/src/features/chains/utils.ts when possible!", + "name": "uniswap/src/features/chains/chainInfo", + }, { "message": "Please import from '@ethersproject/module' directly to support tree-shaking.", "name": "ethers", diff --git a/packages/eslint-config/native.js b/packages/eslint-config/native.js index 9fa81917c5c..1ede3402523 100644 --- a/packages/eslint-config/native.js +++ b/packages/eslint-config/native.js @@ -312,7 +312,7 @@ module.exports = { }, // enforce saga imports from typed-redux-saga { - files: ['./**/*.ts'], + files: ['./**/*.ts', './**/*.tsx'], excludedFiles: ['./**/*.test.ts', './**/*.test.tsx'], rules: { '@jambit/typed-redux-saga/use-typed-effects': 'error', diff --git a/packages/eslint-config/restrictedImports.js b/packages/eslint-config/restrictedImports.js index f20e410a43b..0cf57336582 100644 --- a/packages/eslint-config/restrictedImports.js +++ b/packages/eslint-config/restrictedImports.js @@ -51,7 +51,12 @@ exports.shared = { { name: 'lodash', message: 'Use specific imports (e.g. `import isEqual from \'lodash/isEqual\'`) to avoid pulling in all of lodash to web to keep bundle size down!', - } + }, + { + name: 'uniswap/src/features/chains/chainInfo', + importNames: ['UNIVERSE_CHAIN_INFO'], + message: 'Use useChainInfo or helpers in packages/uniswap/src/features/chains/utils.ts when possible!', + }, ], patterns: [ { diff --git a/packages/ui/README.md b/packages/ui/README.md index fcb905b9438..0ef0b56aee9 100644 --- a/packages/ui/README.md +++ b/packages/ui/README.md @@ -1,30 +1,21 @@ -# Uniswap UI +# `ui` Package -This package holds a component library and themes that can be used across both mobile and web contexts. Below is instructions for both use and development of this package. +This package holds a component library and themes that can be used across all apps. -## Library Usage +## UI Package Philosophy -### Core components +The `ui` package contains all low level components that are shared between apps. It should *not* contain components that are specific to any one app or Uniswap business logic. Each component should be guided by the following principles: -Many base components are available in the UI library. Below key use cases are detailed, but many more are available. +- All components should be compatible with all platforms. +- Wrap as many implementation details as possible, including any direct exports from Tamagui. +- Export only what’s needed from `ui/src` or another allowlisted path. +- Only include components that will be used beyond a single feature. -While some are customized `tamagui` elements or even fully custom elements, many are simple or direct exposure of `tamagui` elements. If you would like to use any `tamagui` elements, please add them through this library to ensure a layer of abstraction between tamagui and our usage when possible. +Components that are shared between all applications but encode Uniswap business logic should most likely be placed in the `uniswap` package! -#### Flex +## Icons and Logos -The `Flex` component is the core organizational element of the UI library, acting as a base of a flexbox styling approach. Shortcuts are available for `row`, `grow`, `shrink`, `fill`, and `centered`. All other styling props can be added directly as needed. - -#### Text - -The `Text` element is the core element for displaying text through the app. The `variant` prop takes in the text variant from our design system (e.g `heading1`, `body2`, etc.) that is desired. All other text styling props can be added directly as needed. - -#### Button - -The `Button` element is used consistently to ensure action buttons maintain similar action and style, utilizing `size` and `theme` properties to enforce consistent usage. Even when not using either, this component ensures consistent haptics and other implementation details. - -#### Icons and Logos - -Icons and Logos are made available off `ui/src/components/icons` and `ui/src/components/logos`. These files are generated from placing the file in `packages/ui/src/assets/icons` or `packages/ui/src/assets/logos/svg` and running the generate command(s): +Icons and logos are placed in `ui/src/components/{icons|logos}`. These files are generated from placing the file in `packages/ui/src/assets/icons` or `packages/ui/src/assets/logos/svg` and running the generate command(s): ```bash # Generate all icons @@ -37,6 +28,22 @@ When adding an SVG, please ensure you replace color references as needed with `c Custom icons that take props can be added to the same icons import by adding the file in `packages/ui/src/components/icons/index.ts`. +## Core Components + +Many base components are available in the UI library. While some are customized `tamagui` elements or even fully custom elements, many are simple or direct exposure of `tamagui` elements. If you would like to use any `tamagui` elements, please add them through this library to ensure a layer of abstraction between tamagui and our usage when possible. + +Below are summaries of the most commonly used elements. + +### Flex + +The `Flex` component is the core organizational element of the UI library, acting as a base of a flexbox styling approach. Shortcuts are available for `row`, `grow`, `shrink`, `fill`, and `centered`. All other styling props can be added directly as needed. + +### Text + +The `Text` element is the core element for displaying text through the app. The `variant` prop takes in the text variant from our design system (e.g `heading1`, `body2`, etc.) that is desired. All other text styling props can be added directly as needed. + +## Theme and Platform + ### Theming Theming is applied through two primary methods: @@ -69,9 +76,7 @@ To account for screen size references, components take in props to adapt custom When components cannot take in these props or a value needs to be resued multiple times, the `useMedia` hook allows these same breakpoint values to be defined programmatically. -### Other notable usage - -#### Accessing colors +### Colors We've made a hook `useSporeColors()` which gives you access to the current theme, and you can access the values off it as follows: @@ -86,7 +91,7 @@ function MyComponent() { } ``` -##### When to use `.get()` vs `.val` +#### When to use `.get()` vs `.val` After some discussion we've come to prefer `.val` by default. This will always return the raw string color (in our case hex color) on all platforms, whereas `.get()` returns either an Object (iOS) or a string, which can cause issues when used with things like animations, or external components. @@ -102,65 +107,3 @@ In summary: - On iOS, it returns an object like `{ dynamic: { light: '#fff', dark: '#000' } }` - On the web, it returns a CSS variable string, `var(--colorName)` - You can call `.get('web')` to only optimize for web, or `.get('ios')` to only optimize for ios. - -#### When should you use `styled()` vs inline props - -When possible, usage of `styled` should be limited to use within the `ui` package or direct styling only repetition of . In general try to use the following rules: - -1. If it's used only once, always prefer inline for simplicity's sake. It avoids having to name things and avoids having to jump between files or places in code to understand styling. - -2. If it's used more than once, try to create a component that abstracts properties to the minimum needed. This new component should live as close to the usage as possible, so if used only within a single file, keep it within that file. You should only use `styled` when said component is being used more than once, otherwise defer to inline styles. - -3. If it's used across multiple apps or has a strong potential to be, consider extracting it into a shared package. To decide where to place it, see "What code should be placed in the package?" - -## Library Design - -### What code should be placed in the package? - -The `ui` package should be where we put all low level interface components that are shared between apps (or will likely be shared). It should *not* contain components that are specific to any one app, or which touch app-level data in any way. - -A good rule of thumb is: if it deals with app-specific state (eg anything thats stored in redux) or has a very complex interface, then: - -- If it's specific to just one app, put it in that app. -- Else if it's shared between multiple apps, put it in a shared package above the UI package, e.g `packages/{package}`. - -Otherwise, if it's simpler, put it in `packages/ui`. - -Think of `packages/ui` as our low level and more pure building blocks for interface, and `packages/{package}` as our higher level shared components that deal with complex dependencies or app-specific state. - -### Optimization - -The Tamagui optimizing compiler will extract CSS and do tree-flattening optimizations for all of `ui`, and for all usages of components from `ui` in the apps. - -Where it will bail out of optimization is if you define a new `styled()` component outside of `ui` and then use that component. - -But this is fine! It will still be pretty fast. - -Also, in the future we can turn `enableDynamicEvaluation` which enables the compiler to optimize those one-off `styled()` definitions within apps to also extract CSS and flatten. Its still a bit of a beta feature so no rush to turn it on, and likely it's fast enough that we don't need to worry about it. - -### useStyle, useProps, and usePropsAndStyle - -These three are useful more advanced patterns to get styles or props from Tamagui form into plain objects and [are documented on the Tamagui site](https://tamagui.dev/docs/core/exports#useprops). - -In short: - -- `useProps`: takes in props returns props as-is just with media queries resolved and shorthands expanded (not transformed so tokens like $color stay as $color) -- `useStyle`: takes in props returns only styles (media queries, shorthands, tokens, etc resolved) -- `usePropsAndStyle`: splits the props and styles apart for you, fully resolves everything - -Also the `forComponent` pattern is useful and works with all of them: - -```tsx -const CustomText = styled(Text, { - variants: { - large: { - true: { - fontSize: 100 - } - } - } -}) - -useStyle({ large: true }, { forComponent: CustomText } }) -// returns { fontSize: 100 } -``` diff --git a/packages/ui/src/animations/components/HeightAnimator.tsx b/packages/ui/src/animations/components/HeightAnimator.tsx index 23d9455dba9..28d8eb288d1 100644 --- a/packages/ui/src/animations/components/HeightAnimator.tsx +++ b/packages/ui/src/animations/components/HeightAnimator.tsx @@ -1,36 +1,29 @@ -import { useCallback, useEffect, useState } from 'react' +import { useState } from 'react' import { View, useEvent } from 'tamagui' export const HeightAnimator = View.styleable<{ open?: boolean }>((props, ref) => { const { open = true, children, ...rest } = props - const [height, setHeight] = useState(0) - const [hide, setHide] = useState(false) - - const close = useCallback(() => { - setHeight(0) - setTimeout(() => { - setHide(true) - }, 300) - }, [setHeight, setHide]) - - useEffect(() => { - if (open) { - setHide(false) - } else { - close() - } - }, [open, setHide, close]) + const [visibleHeight, setVisibleHeight] = useState(0) const onLayout = useEvent(({ nativeEvent }) => { if (nativeEvent.layout.height) { - setHeight(nativeEvent.layout.height) + setVisibleHeight(nativeEvent.layout.height) } }) return ( - + - {!hide && children} + {children} ) diff --git a/packages/ui/src/animations/components/WidthAnimator.tsx b/packages/ui/src/animations/components/WidthAnimator.tsx index c5676d223b0..21ffb340130 100644 --- a/packages/ui/src/animations/components/WidthAnimator.tsx +++ b/packages/ui/src/animations/components/WidthAnimator.tsx @@ -1,29 +1,13 @@ -import { useCallback, useEffect, useState } from 'react' +import { useState } from 'react' import { View, useEvent } from 'tamagui' export const WidthAnimator = View.styleable<{ open?: boolean; height: number }>((props, ref) => { const { open = true, height, children, ...rest } = props - const [width, setWidth] = useState(0) - const [hide, setHide] = useState(false) - - const close = useCallback(() => { - setWidth(0) - setTimeout(() => { - setHide(true) - }, 300) - }, [setWidth, setHide]) - - useEffect(() => { - if (open) { - setHide(false) - } else { - close() - } - }, [open, setHide, close]) + const [visibleWidth, setVisibleWidth] = useState(0) const onLayout = useEvent(({ nativeEvent }) => { if (nativeEvent.layout.width) { - setWidth(nativeEvent.layout.width) + setVisibleWidth(nativeEvent.layout.width) } }) @@ -36,11 +20,11 @@ export const WidthAnimator = View.styleable<{ open?: boolean; height: number }>( exitStyle={{ opacity: 0 }} height={height} overflow="hidden" - width={width} + width={open ? visibleWidth : 0} {...rest} > - {!hide && children} + {children} ) diff --git a/packages/ui/src/assets/backgrounds/android/notifications-dark.png b/packages/ui/src/assets/backgrounds/android/notifications-dark.png index ade6e0f3d92..5ce57622275 100644 Binary files a/packages/ui/src/assets/backgrounds/android/notifications-dark.png and b/packages/ui/src/assets/backgrounds/android/notifications-dark.png differ diff --git a/packages/ui/src/assets/backgrounds/android/notifications-light.png b/packages/ui/src/assets/backgrounds/android/notifications-light.png index 5768534ab4a..19b4c1c1553 100644 Binary files a/packages/ui/src/assets/backgrounds/android/notifications-light.png and b/packages/ui/src/assets/backgrounds/android/notifications-light.png differ diff --git a/packages/ui/src/assets/backgrounds/android/security-background-dark.png b/packages/ui/src/assets/backgrounds/android/security-background-dark.png index 6f97db70dc4..54938d6df4d 100644 Binary files a/packages/ui/src/assets/backgrounds/android/security-background-dark.png and b/packages/ui/src/assets/backgrounds/android/security-background-dark.png differ diff --git a/packages/ui/src/assets/backgrounds/android/security-background-light.png b/packages/ui/src/assets/backgrounds/android/security-background-light.png index 9b224e7607c..0479fcef6f1 100644 Binary files a/packages/ui/src/assets/backgrounds/android/security-background-light.png and b/packages/ui/src/assets/backgrounds/android/security-background-light.png differ diff --git a/packages/ui/src/assets/backgrounds/ios/notifications-dark.png b/packages/ui/src/assets/backgrounds/ios/notifications-dark.png index c1b0f40ff91..3aa12b550a2 100644 Binary files a/packages/ui/src/assets/backgrounds/ios/notifications-dark.png and b/packages/ui/src/assets/backgrounds/ios/notifications-dark.png differ diff --git a/packages/ui/src/assets/backgrounds/ios/notifications-light.png b/packages/ui/src/assets/backgrounds/ios/notifications-light.png index d4b5b7ec347..bc751e18d02 100644 Binary files a/packages/ui/src/assets/backgrounds/ios/notifications-light.png and b/packages/ui/src/assets/backgrounds/ios/notifications-light.png differ diff --git a/packages/ui/src/assets/backgrounds/ios/security-background-dark.png b/packages/ui/src/assets/backgrounds/ios/security-background-dark.png index b297a2c9e57..f384c519588 100644 Binary files a/packages/ui/src/assets/backgrounds/ios/security-background-dark.png and b/packages/ui/src/assets/backgrounds/ios/security-background-dark.png differ diff --git a/packages/ui/src/assets/backgrounds/ios/security-background-light.png b/packages/ui/src/assets/backgrounds/ios/security-background-light.png index c1bf7e5f760..ebc72439f59 100644 Binary files a/packages/ui/src/assets/backgrounds/ios/security-background-light.png and b/packages/ui/src/assets/backgrounds/ios/security-background-light.png differ diff --git a/packages/ui/src/assets/icons/flag.svg b/packages/ui/src/assets/icons/flag.svg new file mode 100644 index 00000000000..54d68c5b83c --- /dev/null +++ b/packages/ui/src/assets/icons/flag.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/ui/src/assets/icons/page.svg b/packages/ui/src/assets/icons/page.svg new file mode 100644 index 00000000000..75e607f01e5 --- /dev/null +++ b/packages/ui/src/assets/icons/page.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/ui/src/assets/icons/pools.svg b/packages/ui/src/assets/icons/pools.svg new file mode 100644 index 00000000000..4e25e8860c5 --- /dev/null +++ b/packages/ui/src/assets/icons/pools.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/ui/src/assets/icons/swap-coin.svg b/packages/ui/src/assets/icons/swap-coin.svg new file mode 100644 index 00000000000..a6fabfdbe69 --- /dev/null +++ b/packages/ui/src/assets/icons/swap-coin.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/ui/src/components/InlineCard/InlineCard.tsx b/packages/ui/src/components/InlineCard/InlineCard.tsx index 50975471ce5..78994bbc2e1 100644 --- a/packages/ui/src/components/InlineCard/InlineCard.tsx +++ b/packages/ui/src/components/InlineCard/InlineCard.tsx @@ -10,7 +10,7 @@ type InlineCardProps = { color: ColorTokens description: string | JSX.Element iconBackgroundColor?: ColorTokens - heading?: string + heading?: string | JSX.Element CtaButtonIcon?: GeneratedIcon | ((props: IconProps) => JSX.Element) onPressCtaButton?: () => void } @@ -43,11 +43,14 @@ export function InlineCard({ description ) - const headingElement = heading ? ( - - {heading} - - ) : null + const headingElement = + typeof heading === 'string' ? ( + + {heading} + + ) : ( + heading + ) return ( diff --git a/packages/ui/src/components/button/Button.tsx b/packages/ui/src/components/button/Button.tsx index 8a9c0f1af33..a0bcb76748b 100644 --- a/packages/ui/src/components/button/Button.tsx +++ b/packages/ui/src/components/button/Button.tsx @@ -18,7 +18,7 @@ import type { IconProps } from 'ui/src/components/factories/createIcon' import { HapticFeedbackStyle } from 'ui/src/utils/haptics/helpers' import { useHapticFeedback } from 'ui/src/utils/haptics/useHapticFeedback' -type ButtonSize = 'small' | 'medium' | 'large' +export type ButtonSize = 'small' | 'medium' | 'large' const ButtonNestingContext = createContext(false) diff --git a/packages/ui/src/components/checkbox/Checkbox.tsx b/packages/ui/src/components/checkbox/Checkbox.tsx index a857e1f93b4..8bf50814fac 100644 --- a/packages/ui/src/components/checkbox/Checkbox.tsx +++ b/packages/ui/src/components/checkbox/Checkbox.tsx @@ -7,9 +7,10 @@ import { getTokenValue, } from 'tamagui' import { Check } from 'ui/src/components/icons' -import { Flex } from 'ui/src/components/layout' +import { Flex, FlexProps } from 'ui/src/components/layout' import { SporeComponentVariant } from 'ui/src/components/types' import { IconSizeTokens } from 'ui/src/theme' +import { isTestEnv } from 'utilities/src/environment/env' import { v4 as uuid } from 'uuid' type CheckboxSizes = { @@ -41,6 +42,8 @@ type CheckboxProps = { size?: CheckboxSizeTokens } & Omit +const animationProp = isTestEnv() ? undefined : ({ animation: 'simple' } satisfies FlexProps['animation']) + /** * Spore Checkbox * @@ -61,13 +64,14 @@ export function Checkbox({ checked, variant = 'default', size = '$icon.20', ...r // This outer ring is only shown when the button is focused. { @@ -41,8 +45,8 @@ export function LabeledCheckbox({ ) return ( - - + + {checkboxPosition === 'start' && } {text && ( diff --git a/packages/ui/src/components/icons/Flag.tsx b/packages/ui/src/components/icons/Flag.tsx new file mode 100644 index 00000000000..b7f2fc1e634 --- /dev/null +++ b/packages/ui/src/components/icons/Flag.tsx @@ -0,0 +1,20 @@ +import { G, Path, Svg } from 'react-native-svg' + +// eslint-disable-next-line no-relative-import-paths/no-relative-import-paths +import { createIcon } from '../factories/createIcon' + +export const [Flag, AnimatedFlag] = createIcon({ + name: 'Flag', + getIcon: (props) => ( + + + + + + ), + defaultFill: '#FF5F52', +}) diff --git a/packages/ui/src/components/icons/Page.tsx b/packages/ui/src/components/icons/Page.tsx new file mode 100644 index 00000000000..8e0259189b4 --- /dev/null +++ b/packages/ui/src/components/icons/Page.tsx @@ -0,0 +1,20 @@ +import { G, Path, Svg } from 'react-native-svg' + +// eslint-disable-next-line no-relative-import-paths/no-relative-import-paths +import { createIcon } from '../factories/createIcon' + +export const [Page, AnimatedPage] = createIcon({ + name: 'Page', + getIcon: (props) => ( + + + + + + ), + defaultFill: '#9B9B9B', +}) diff --git a/packages/ui/src/components/icons/Pools.tsx b/packages/ui/src/components/icons/Pools.tsx new file mode 100644 index 00000000000..e946c414873 --- /dev/null +++ b/packages/ui/src/components/icons/Pools.tsx @@ -0,0 +1,20 @@ +import { G, Path, Svg } from 'react-native-svg' + +// eslint-disable-next-line no-relative-import-paths/no-relative-import-paths +import { createIcon } from '../factories/createIcon' + +export const [Pools, AnimatedPools] = createIcon({ + name: 'Pools', + getIcon: (props) => ( + + + + + + ), + defaultFill: '#9B9B9B', +}) diff --git a/packages/ui/src/components/icons/SwapCoin.tsx b/packages/ui/src/components/icons/SwapCoin.tsx new file mode 100644 index 00000000000..bbe98655ac7 --- /dev/null +++ b/packages/ui/src/components/icons/SwapCoin.tsx @@ -0,0 +1,20 @@ +import { G, Path, Svg } from 'react-native-svg' + +// eslint-disable-next-line no-relative-import-paths/no-relative-import-paths +import { createIcon } from '../factories/createIcon' + +export const [SwapCoin, AnimatedSwapCoin] = createIcon({ + name: 'SwapCoin', + getIcon: (props) => ( + + + + + + ), + defaultFill: '#9B9B9B', +}) diff --git a/packages/ui/src/components/icons/exported.ts b/packages/ui/src/components/icons/exported.ts index c5bd1c54434..37b1ffe219c 100644 --- a/packages/ui/src/components/icons/exported.ts +++ b/packages/ui/src/components/icons/exported.ts @@ -86,6 +86,7 @@ export * from './Feedback' export * from './FileListCheck' export * from './FileListLock' export * from './Fingerprint' +export * from './Flag' export * from './Flashbots' export * from './Gallery' export * from './Gas' @@ -132,6 +133,7 @@ export * from './NoTokens' export * from './NoTransactions' export * from './OctagonExclamation' export * from './OrderRouting' +export * from './Page' export * from './PaperStack' export * from './PapersText' export * from './Paste' @@ -148,6 +150,7 @@ export * from './Pin' export * from './Plus' export * from './PlusCircle' export * from './PlusSquare' +export * from './Pools' export * from './Power' export * from './Profile' export * from './ProfileFilled' @@ -194,6 +197,7 @@ export * from './StickyNoteTextSquare' export * from './Sun' export * from './Swap' export * from './SwapArrow' +export * from './SwapCoin' export * from './SwirlyArrowDown' export * from './Testnets' export * from './TextEdit' diff --git a/packages/ui/src/components/menu/MenuContent.tsx b/packages/ui/src/components/menu/MenuContent.tsx deleted file mode 100644 index 768629915c2..00000000000 --- a/packages/ui/src/components/menu/MenuContent.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { BaseSyntheticEvent, useCallback } from 'react' -import { Flex, FlexProps } from 'ui/src/components/layout' -import { MenuContentItem } from 'ui/src/components/menu/types' -import { Text } from 'ui/src/components/text' -import { TouchableArea } from 'ui/src/components/touchable' -import { useIsDarkMode } from 'ui/src/hooks/useIsDarkMode' - -type MenuContentProps = { - onClose?: () => void - items: MenuContentItem[] -} - -export function MenuContent({ items, onClose, ...rest }: MenuContentProps & FlexProps): JSX.Element { - const isDarkMode = useIsDarkMode() - - const handleOnPress = useCallback( - (e: BaseSyntheticEvent, onPress: (e: BaseSyntheticEvent) => void): void => { - onPress(e) - onClose?.() - }, - [onClose], - ) - - return ( - - {items.map(({ label, onPress, Icon, textProps, iconProps, destructive, ...touchableProps }, index) => ( - handleOnPress(e, onPress)} - {...touchableProps} - > - - - {label} - - {Icon && } - - - ))} - - ) -} diff --git a/packages/ui/src/components/swipeablecards/ClickableWithinGesture.web.tsx b/packages/ui/src/components/swipeablecards/ClickableWithinGesture.web.tsx index 55cc6fefbe9..d0f0fb0eff7 100644 --- a/packages/ui/src/components/swipeablecards/ClickableWithinGesture.web.tsx +++ b/packages/ui/src/components/swipeablecards/ClickableWithinGesture.web.tsx @@ -8,5 +8,9 @@ export function ClickableWithinGesture({ onPress, children }: ClickableWithinGes onPress?.() } - return {children} + return ( + + {children} + + ) } diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 117a07076b0..fa39fd1cbaf 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -64,9 +64,6 @@ export * from './components/checkbox' export type { GeneratedIcon, IconProps } from './components/factories/createIcon' export * from './components/input/utils' export { Flex, Inset, Separator, flexStyles, type FlexProps } from './components/layout' -export { ContextMenu } from './components/menu/ContextMenu' -export { MenuContent } from './components/menu/MenuContent' -export type { MenuContentItem } from './components/menu/types' export { AdaptiveWebModal, WebBottomSheet } from './components/modal/AdaptiveWebModal' export * from './components/radio/Radio' export { ClickableWithinGesture } from './components/swipeablecards/ClickableWithinGesture' diff --git a/packages/ui/src/loading/Loader.tsx b/packages/ui/src/loading/Loader.tsx index a2df5b30274..49b423380fe 100644 --- a/packages/ui/src/loading/Loader.tsx +++ b/packages/ui/src/loading/Loader.tsx @@ -24,6 +24,13 @@ const Transaction = memo(function _Transaction({ repeat = 1 }: { repeat?: number ) }) +/** + * Loader used for search results e.g. search, recipient etc... + */ +const SearchResult = memo(function _SearchResult({ repeat = 1 }: { repeat?: number }): JSX.Element { + return +}) + const TransferInstitution = memo(function _TransferInstitution({ itemsCount, iconSize, @@ -130,6 +137,7 @@ export const Loader = { Box, NFT, Image, + SearchResult, Token, TransferInstitution, Transaction, diff --git a/packages/uniswap/package.json b/packages/uniswap/package.json index 9a4e3d3c69e..871eb62f59b 100644 --- a/packages/uniswap/package.json +++ b/packages/uniswap/package.json @@ -45,8 +45,8 @@ "@uniswap/client-pools": "0.0.8", "@uniswap/permit2-sdk": "1.3.0", "@uniswap/router-sdk": "1.14.3", - "@uniswap/sdk-core": "5.8.4", - "@uniswap/uniswapx-sdk": "^2.1.0-beta.14", + "@uniswap/sdk-core": "5.9.0", + "@uniswap/uniswapx-sdk": "2.1.0-beta.18", "@uniswap/v2-sdk": "4.6.1", "@uniswap/v3-sdk": "3.18.1", "@uniswap/v4-sdk": "1.10.3", @@ -66,6 +66,7 @@ "jsbi": "3.2.5", "lodash": "4.17.21", "openapi-typescript-codegen": "0.27.0", + "qs": "6.11.0", "react": "18.2.0", "react-i18next": "14.1.0", "react-native": "0.73.6", @@ -78,6 +79,7 @@ "react-native-restart": "0.0.27", "react-native-svg": "15.1.0", "react-redux": "8.0.5", + "react-router-dom": "6.10.0", "react-test-renderer": "18.2.0", "react-virtualized-auto-sizer": "1.0.20", "react-window": "1.8.9", diff --git a/packages/uniswap/src/assets/chainLogos.tsx b/packages/uniswap/src/assets/chainLogos.tsx index b0db87dbc90..0e4a2c84464 100644 --- a/packages/uniswap/src/assets/chainLogos.tsx +++ b/packages/uniswap/src/assets/chainLogos.tsx @@ -7,7 +7,7 @@ import { OpEtherscanLogoDark } from 'ui/src/components/logos/OpEtherscanLogoDark import { OpEtherscanLogoLight } from 'ui/src/components/logos/OpEtherscanLogoLight' import { PolygonscanLogoDark } from 'ui/src/components/logos/PolygonscanLogoDark' import { PolygonscanLogoLight } from 'ui/src/components/logos/PolygonscanLogoLight' -import { UniverseChainId, UniverseChainLogoInfo } from 'uniswap/src/types/chains' +import { UniverseChainId, UniverseChainLogoInfo } from 'uniswap/src/features/chains/types' // Keeping this separate from UNIVERSE_CHAIN_INFO to avoid import issues on extension content script export const UNIVERSE_CHAIN_LOGO = { diff --git a/packages/uniswap/src/components/ConfirmSwapModal/steps/Approve.tsx b/packages/uniswap/src/components/ConfirmSwapModal/steps/Approve.tsx index 9b0d12a1d91..0ebf7ed6ce0 100644 --- a/packages/uniswap/src/components/ConfirmSwapModal/steps/Approve.tsx +++ b/packages/uniswap/src/components/ConfirmSwapModal/steps/Approve.tsx @@ -12,7 +12,7 @@ export function TokenApprovalTransactionStepRow({ status, }: StepRowProps): JSX.Element { const { t } = useTranslation() - const { token } = step + const { token, pair } = step const symbol = token.symbol const title = { @@ -26,6 +26,7 @@ export function TokenApprovalTransactionStepRow({ { interface StepRowSkeletonProps { /** If passed, the step row icon will be the currency logo. */ currency?: Currency + /** If passed, the step row icon will be the split currency logo. */ + pair?: [Currency, Currency] /** Icon to display if there is no currency to be displayed for this step. */ icon?: JSX.Element /** Color to display for the ripple effect around the icon or currency logo. This will default to a currency logo extracted color, if currency is defined. */ @@ -29,13 +32,22 @@ interface StepRowSkeletonProps { } export function StepRowSkeleton(props: StepRowSkeletonProps): JSX.Element { - const { currency, icon, secondsRemaining, title, learnMore, status, rippleColor } = props + const { currency, icon, secondsRemaining, title, learnMore, status, rippleColor, pair } = props const colors = useSporeColors() const currencyInfo = useCurrencyInfo(currency ? currencyId(currency) : undefined) + + // For V2 liquidity positions the user is generated a unique token which is + // the actual token they are approving, but since this token doesn't have + // a logo we use the SplitLogo component to display the pair logos instead. + const currency0Id = pair?.[0] ? currencyId(pair[0]) : undefined + const currency1Id = pair?.[1] ? currencyId(pair[1]) : undefined + const currency0Info = useCurrencyInfo(currency0Id) + const currency1Info = useCurrencyInfo(currency1Id) + const { tokenColor } = useExtractedTokenColor( - currencyInfo?.logoUrl, - currency?.symbol, + currency0Info ? currency0Info.logoUrl : currencyInfo?.logoUrl, + currency0Info ? currency0Info.currency.symbol : currency?.symbol, /*background=*/ colors.surface1.val, /*default=*/ colors.neutral3.val, ) @@ -46,7 +58,16 @@ export function StepRowSkeleton(props: StepRowSkeletonProps): JSX.Element { - {icon ?? } + {currency0Info && currency1Info ? ( + + ) : ( + icon ?? + )} diff --git a/packages/uniswap/src/components/CurrencyInputPanel/CurrencyInputPanel.tsx b/packages/uniswap/src/components/CurrencyInputPanel/CurrencyInputPanel.tsx index 797f899953b..11437ec3a3e 100644 --- a/packages/uniswap/src/components/CurrencyInputPanel/CurrencyInputPanel.tsx +++ b/packages/uniswap/src/components/CurrencyInputPanel/CurrencyInputPanel.tsx @@ -26,12 +26,12 @@ import { MaxAmountButton } from 'uniswap/src/components/CurrencyInputPanel/MaxAm import { SelectTokenButton } from 'uniswap/src/components/CurrencyInputPanel/SelectTokenButton' import { MAX_FIAT_INPUT_DECIMALS } from 'uniswap/src/constants/transactions' import { useAccountMeta } from 'uniswap/src/contexts/UniswapContext' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' import { useAppFiatCurrencyInfo } from 'uniswap/src/features/fiatCurrency/hooks' import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext' -import { useEnabledChains } from 'uniswap/src/features/settings/hooks' import { useTokenAndFiatDisplayAmounts } from 'uniswap/src/features/transactions/hooks/useTokenAndFiatDisplayAmounts' import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { CurrencyField } from 'uniswap/src/types/currency' @@ -296,7 +296,7 @@ export const CurrencyInputPanel = memo( autoFocus={autoFocus ?? focus} backgroundColor="$transparent" borderWidth={0} - color={color} + color={showInsufficientBalanceWarning ? '$statusCritical' : color} disabled={disabled || !currencyInfo} flex={1} focusable={!disabled && Boolean(currencyInfo)} @@ -313,6 +313,7 @@ export const CurrencyInputPanel = memo( overflow="visible" placeholder="0" placeholderTextColor={colors.neutral3.val} + borderRadius={0} px="$none" py="$none" returnKeyType={showSoftInputOnFocus ? 'done' : undefined} @@ -326,7 +327,7 @@ export const CurrencyInputPanel = memo( ) : ( - + 0 diff --git a/packages/uniswap/src/components/CurrencyLogo/NetworkLogo.test.tsx b/packages/uniswap/src/components/CurrencyLogo/NetworkLogo.test.tsx index 335b5d30faf..694876d6de3 100644 --- a/packages/uniswap/src/components/CurrencyLogo/NetworkLogo.test.tsx +++ b/packages/uniswap/src/components/CurrencyLogo/NetworkLogo.test.tsx @@ -1,25 +1,26 @@ import { NetworkLogo, TransactionSummaryNetworkLogo } from 'uniswap/src/components/CurrencyLogo/NetworkLogo' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { render } from 'uniswap/src/test/test-utils' -import { UniverseChainId } from 'uniswap/src/types/chains' -jest.mock('uniswap/src/constants/chains', () => { - const actualChains = jest.requireActual('uniswap/src/constants/chains') - - const mockedChainInfo = { - ...actualChains.UNIVERSE_CHAIN_INFO, - ['chainWithoutLogo']: { - logo: undefined, // no logo - for testing - }, - } +jest.mock('uniswap/src/features/chains/chainInfo', () => { + const actualChains = jest.requireActual('uniswap/src/features/chains/chainInfo') return { ...actualChains, - UNIVERSE_CHAIN_INFO: mockedChainInfo, + getChainInfo: (chainId: unknown): unknown => { + if (chainId === 'chainWithoutLogo') { + return { + logo: undefined, // no logo - for testing + } + } else { + return actualChains.UNIVERSE_CHAIN_INFO[chainId as UniverseChainId] + } + }, } }) describe('NetworkLogo', () => { - const ACTUAL_CHAIN_INFO = jest.requireActual('uniswap/src/constants/chains').UNIVERSE_CHAIN_INFO + const ACTUAL_CHAIN_INFO = jest.requireActual('uniswap/src/features/chains/chainInfo').UNIVERSE_CHAIN_INFO it('renders without error', () => { const tree = render() diff --git a/packages/uniswap/src/components/CurrencyLogo/NetworkLogo.tsx b/packages/uniswap/src/components/CurrencyLogo/NetworkLogo.tsx index d93f0aada55..1d66d6e2703 100644 --- a/packages/uniswap/src/components/CurrencyLogo/NetworkLogo.tsx +++ b/packages/uniswap/src/components/CurrencyLogo/NetworkLogo.tsx @@ -2,8 +2,8 @@ import React from 'react' import { Flex, FlexProps, Image, useSporeColors } from 'ui/src' import { ALL_NETWORKS_LOGO } from 'ui/src/assets' import { iconSizes } from 'ui/src/theme' -import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' +import { UniverseChainId } from 'uniswap/src/features/chains/types' export const SQUIRCLE_BORDER_RADIUS_RATIO = 0.3 @@ -49,7 +49,7 @@ function _NetworkLogo({ ) } - const logo = UNIVERSE_CHAIN_INFO[chainId].logo + const logo = getChainInfo(chainId).logo const imageSize = size - borderWidth * 2 // this prevents the border from cutting off the logo return logo ? ( diff --git a/packages/uniswap/src/components/CurrencyLogo/SplitLogo.test.tsx b/packages/uniswap/src/components/CurrencyLogo/SplitLogo.test.tsx index d47fdb43d2b..10327d18d80 100644 --- a/packages/uniswap/src/components/CurrencyLogo/SplitLogo.test.tsx +++ b/packages/uniswap/src/components/CurrencyLogo/SplitLogo.test.tsx @@ -1,7 +1,7 @@ import { SplitLogo } from 'uniswap/src/components/CurrencyLogo/SplitLogo' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { DAI_CURRENCY_INFO, ETH_CURRENCY_INFO, daiCurrencyInfo, ethCurrencyInfo } from 'uniswap/src/test/fixtures' import { render, within } from 'uniswap/src/test/test-utils' -import { UniverseChainId } from 'uniswap/src/types/chains' jest.mock('ui/src/components/UniversalImage/internal/PlainImage', () => ({ ...jest.requireActual('ui/src/components/UniversalImage/internal/PlainImage.web'), diff --git a/packages/uniswap/src/components/CurrencyLogo/SplitLogo.tsx b/packages/uniswap/src/components/CurrencyLogo/SplitLogo.tsx index 9f64ce009e4..d51a9982184 100644 --- a/packages/uniswap/src/components/CurrencyLogo/SplitLogo.tsx +++ b/packages/uniswap/src/components/CurrencyLogo/SplitLogo.tsx @@ -4,8 +4,8 @@ import { Shuffle } from 'ui/src/components/icons/Shuffle' import { iconSizes } from 'ui/src/theme' import { CurrencyLogo, STATUS_RATIO } from 'uniswap/src/components/CurrencyLogo/CurrencyLogo' import { TransactionSummaryNetworkLogo } from 'uniswap/src/components/CurrencyLogo/NetworkLogo' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' -import { UniverseChainId } from 'uniswap/src/types/chains' interface Props { inputCurrencyInfo: Maybe diff --git a/packages/uniswap/src/components/CurrencyLogo/TokenLogo.test.tsx b/packages/uniswap/src/components/CurrencyLogo/TokenLogo.test.tsx index 11071e9b847..0c96504fab3 100644 --- a/packages/uniswap/src/components/CurrencyLogo/TokenLogo.test.tsx +++ b/packages/uniswap/src/components/CurrencyLogo/TokenLogo.test.tsx @@ -1,6 +1,6 @@ import { TokenLogo } from 'uniswap/src/components/CurrencyLogo/TokenLogo' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { render } from 'uniswap/src/test/test-utils' -import { UniverseChainId } from 'uniswap/src/types/chains' // This test expects the invalid image URLs to fail to load, so // we silence the error logs to keep the test output clean. diff --git a/packages/uniswap/src/components/CurrencyLogo/TokenLogo.tsx b/packages/uniswap/src/components/CurrencyLogo/TokenLogo.tsx index a87ce585430..84554330652 100644 --- a/packages/uniswap/src/components/CurrencyLogo/TokenLogo.tsx +++ b/packages/uniswap/src/components/CurrencyLogo/TokenLogo.tsx @@ -3,8 +3,8 @@ import { Flex, Text, UniversalImage, useColorSchemeFromSeed, useSporeColors } fr import { iconSizes, validColor } from 'ui/src/theme' import { STATUS_RATIO } from 'uniswap/src/components/CurrencyLogo/CurrencyLogo' import { NetworkLogo } from 'uniswap/src/components/CurrencyLogo/NetworkLogo' -import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { isTestnetChain } from 'uniswap/src/features/chains/utils' import { isMobileApp } from 'utilities/src/platform' interface TokenLogoProps { @@ -34,7 +34,7 @@ export const TokenLogo = memo(function _TokenLogo({ const colors = useSporeColors() const { foreground, background } = useColorSchemeFromSeed(name ?? symbol ?? '') - const isTestnetToken = UNIVERSE_CHAIN_INFO[chainId as UniverseChainId]?.testnet + const isTestnetToken = chainId && isTestnetChain(chainId) const borderWidth = isTestnetToken ? size / TESTNET_BORDER_DIVISOR : 0 const showNetworkLogo = !hideNetworkLogo && chainId && chainId !== UniverseChainId.Mainnet diff --git a/packages/uniswap/src/components/InlineWarningCard/InlineWarningCard.tsx b/packages/uniswap/src/components/InlineWarningCard/InlineWarningCard.tsx index 83db0f8244d..3574764863a 100644 --- a/packages/uniswap/src/components/InlineWarningCard/InlineWarningCard.tsx +++ b/packages/uniswap/src/components/InlineWarningCard/InlineWarningCard.tsx @@ -16,6 +16,8 @@ type InlineWarningCardProps = { checked?: boolean setChecked?: (checked: boolean) => void hideCtaIcon?: boolean + headingTestId?: string + descriptionTestId?: string } export function InlineWarningCard({ @@ -28,7 +30,9 @@ export function InlineWarningCard({ checked, setChecked, hideCtaIcon, -}: InlineWarningCardProps): JSX.Element { + headingTestId, + descriptionTestId, +}: InlineWarningCardProps): JSX.Element | null { const tokenProtectionEnabled = useFeatureFlag(FeatureFlags.TokenProtection) const [checkedFallback, setCheckedFallback] = useState(false) const { color, textColor, backgroundColor } = getWarningIconColors(severity) @@ -43,6 +47,11 @@ export function InlineWarningCard({ } } + if (severity === WarningSeverity.None || !WarningIcon) { + // !WarningIcon for typecheck; should only be null if WarningSeverity == None + return null + } + const checkboxElement = checkboxLabel ? ( - + {description} {checkboxElement} } - heading={heading} + heading={ + heading && ( + + {heading} + + ) + } iconBackgroundColor={heroIcon ? backgroundColor : undefined} iconColor={color} onPressCtaButton={onPressCtaButton} diff --git a/packages/uniswap/src/components/TokenSelector/TokenSelector.tsx b/packages/uniswap/src/components/TokenSelector/TokenSelector.tsx index d12d884b38a..8e5459d435d 100644 --- a/packages/uniswap/src/components/TokenSelector/TokenSelector.tsx +++ b/packages/uniswap/src/components/TokenSelector/TokenSelector.tsx @@ -7,29 +7,29 @@ import { Flex, Text, TouchableArea, isWeb, useMedia, useScrollbarStyles, useSpor import { InfoCircleFilled } from 'ui/src/components/icons/InfoCircleFilled' import { X } from 'ui/src/components/icons/X' import { zIndices } from 'ui/src/theme' -import { TokenSelectorEmptySearchList } from 'uniswap/src/components/TokenSelector/TokenSelectorEmptySearchList' -import { TokenSelectorSearchResultsList } from 'uniswap/src/components/TokenSelector/TokenSelectorSearchResultsList' -import { TokenSelectorSendList } from 'uniswap/src/components/TokenSelector/TokenSelectorSendList' -import { TokenSelectorSwapInputList } from 'uniswap/src/components/TokenSelector/TokenSelectorSwapInputList' -import { TokenSelectorSwapOutputList } from 'uniswap/src/components/TokenSelector/TokenSelectorSwapOutputList' -import { flowToModalName } from 'uniswap/src/components/TokenSelector/flowToModalName' -import { useFilterCallbacks } from 'uniswap/src/components/TokenSelector/hooks' +import { useFilterCallbacks } from 'uniswap/src/components/TokenSelector/hooks/useFilterCallbacks' +import { TokenSelectorEmptySearchList } from 'uniswap/src/components/TokenSelector/lists/TokenSelectorEmptySearchList' +import { TokenSelectorSearchResultsList } from 'uniswap/src/components/TokenSelector/lists/TokenSelectorSearchResultsList' +import { TokenSelectorSendList } from 'uniswap/src/components/TokenSelector/lists/TokenSelectorSendList' +import { TokenSelectorSwapInputList } from 'uniswap/src/components/TokenSelector/lists/TokenSelectorSwapInputList' +import { TokenSelectorSwapOutputList } from 'uniswap/src/components/TokenSelector/lists/TokenSelectorSwapOutputList' import { TokenOptionSection, TokenSection, TokenSelectorFlow } from 'uniswap/src/components/TokenSelector/types' +import { flowToModalName } from 'uniswap/src/components/TokenSelector/utils' import PasteButton from 'uniswap/src/components/buttons/PasteButton' import { useBottomSheetContext } from 'uniswap/src/components/modals/BottomSheetContext' import { Modal } from 'uniswap/src/components/modals/Modal' import { NetworkFilter } from 'uniswap/src/components/network/NetworkFilter' import { useUniswapContext } from 'uniswap/src/contexts/UniswapContext' import { TradeableAsset } from 'uniswap/src/entities/assets' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' import { SearchContext } from 'uniswap/src/features/search/SearchContext' import { SearchTextInput } from 'uniswap/src/features/search/SearchTextInput' -import { useEnabledChains } from 'uniswap/src/features/settings/hooks' import Trace from 'uniswap/src/features/telemetry/Trace' import { ElementName, ModalName, SectionName, UniswapEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import useIsKeyboardOpen from 'uniswap/src/hooks/useIsKeyboardOpen' -import { UniverseChainId } from 'uniswap/src/types/chains' import { CurrencyField } from 'uniswap/src/types/currency' import { getClipboard } from 'uniswap/src/utils/clipboard' import { currencyAddress } from 'uniswap/src/utils/currencyId' @@ -146,6 +146,7 @@ export function TokenSelectorContent({ position: searchContext.position, suggestion_count: searchContext.suggestionCount, query: searchContext.query, + tokenSection: section.sectionKey, }) const isBridgePair = section.sectionKey === TokenOptionSection.BridgingTokens diff --git a/packages/uniswap/src/components/TokenSelector/TokenSelectorList.tsx b/packages/uniswap/src/components/TokenSelector/TokenSelectorList.tsx index f2cc7b9a43b..a2868925ed5 100644 --- a/packages/uniswap/src/components/TokenSelector/TokenSelectorList.tsx +++ b/packages/uniswap/src/components/TokenSelector/TokenSelectorList.tsx @@ -3,20 +3,20 @@ import { useTranslation } from 'react-i18next' import { AnimateTransition, Flex, Loader, Skeleton, Text } from 'ui/src' import { fonts } from 'ui/src/theme' import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard' -import { HorizontalTokenList } from 'uniswap/src/components/TokenSelector/HorizontalTokenList/HorizontalTokenList' -import { TokenOptionItem } from 'uniswap/src/components/TokenSelector/TokenOptionItem' +import { ITEM_SECTION_HEADER_ROW_HEIGHT } from 'uniswap/src/components/TokenSelector/constants' +import { TokenOptionItem } from 'uniswap/src/components/TokenSelector/items/TokenOptionItem' +import { SectionHeader, TokenSectionHeaderProps } from 'uniswap/src/components/TokenSelector/items/TokenSectionHeader' +import { HorizontalTokenList } from 'uniswap/src/components/TokenSelector/lists/HorizontalTokenList/HorizontalTokenList' import { TokenSectionBaseList, TokenSectionBaseListRef, -} from 'uniswap/src/components/TokenSelector/TokenSectionBaseList' -import { ITEM_SECTION_HEADER_ROW_HEIGHT } from 'uniswap/src/components/TokenSelector/TokenSectionBaseList.web' -import { SectionHeader, TokenSectionHeaderProps } from 'uniswap/src/components/TokenSelector/TokenSectionHeader' +} from 'uniswap/src/components/TokenSelector/lists/TokenSectionBaseList/TokenSectionBaseList' import { OnSelectCurrency, TokenOption, TokenSection } from 'uniswap/src/components/TokenSelector/types' import { useBottomSheetFocusHook } from 'uniswap/src/components/modals/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext' -import { useEnabledChains } from 'uniswap/src/features/settings/hooks' import { useDismissedTokenWarnings } from 'uniswap/src/features/tokens/slice/hooks' -import { UniverseChainId } from 'uniswap/src/types/chains' import { CurrencyId } from 'uniswap/src/types/currency' import { NumberType } from 'utilities/src/format/types' @@ -49,9 +49,7 @@ const TokenOptionItemWrapper = memo(function _TokenOptionItemWrapper({ const { isTestnetModeEnabled } = useEnabledChains() - const { tokenWarningDismissed, onDismissTokenWarning: dismissWarningCallback } = useDismissedTokenWarnings( - tokenOption.currencyInfo.currency, - ) + const { tokenWarningDismissed } = useDismissedTokenWarnings(tokenOption.currencyInfo.currency) const tokenBalance = formatNumberOrString({ value: tokenOption.quantity, @@ -66,7 +64,6 @@ const TokenOptionItemWrapper = memo(function _TokenOptionItemWrapper({ return ( = { includeMatches: true, diff --git a/packages/uniswap/src/components/TokenSelector/flowToModalName.tsx b/packages/uniswap/src/components/TokenSelector/flowToModalName.tsx deleted file mode 100644 index 97f795aa74e..00000000000 --- a/packages/uniswap/src/components/TokenSelector/flowToModalName.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { TokenSelectorFlow } from 'uniswap/src/components/TokenSelector/types' -import { ModalName, ModalNameType } from 'uniswap/src/features/telemetry/constants' - -export function flowToModalName(flow: TokenSelectorFlow): ModalNameType | undefined { - switch (flow) { - case TokenSelectorFlow.Swap: - return ModalName.Swap - case TokenSelectorFlow.Send: - return ModalName.Send - default: - return undefined - } -} diff --git a/packages/uniswap/src/components/TokenSelector/hooks.test.ts b/packages/uniswap/src/components/TokenSelector/hooks.test.ts index 779e1935677..9c0fea2916b 100644 --- a/packages/uniswap/src/components/TokenSelector/hooks.test.ts +++ b/packages/uniswap/src/components/TokenSelector/hooks.test.ts @@ -2,21 +2,20 @@ import { ApolloError } from '@apollo/client' import { toIncludeSameMembers } from 'jest-extended' import { PreloadedState } from 'redux' -import { - useAllCommonBaseCurrencies, - useCommonTokensOptionsWithFallback, - useCurrencyInfosToTokenOptions, - useFavoriteCurrencies, - useFavoriteTokensOptions, - useFilterCallbacks, - usePopularTokensOptions, - usePortfolioBalancesForAddressById, - usePortfolioTokenOptions, -} from 'uniswap/src/components/TokenSelector/hooks' +import { useAllCommonBaseCurrencies } from 'uniswap/src/components/TokenSelector/hooks/useAllCommonBaseCurrencies' +import { useCommonTokensOptionsWithFallback } from 'uniswap/src/components/TokenSelector/hooks/useCommonTokensOptionsWithFallback' +import { useCurrencyInfosToTokenOptions } from 'uniswap/src/components/TokenSelector/hooks/useCurrencyInfosToTokenOptions' +import { useFavoriteCurrencies } from 'uniswap/src/components/TokenSelector/hooks/useFavoriteCurrencies' +import { useFavoriteTokensOptions } from 'uniswap/src/components/TokenSelector/hooks/useFavoriteTokensOptions' +import { useFilterCallbacks } from 'uniswap/src/components/TokenSelector/hooks/useFilterCallbacks' +import { usePopularTokensOptions } from 'uniswap/src/components/TokenSelector/hooks/usePopularTokensOptions' +import { usePortfolioBalancesForAddressById } from 'uniswap/src/components/TokenSelector/hooks/usePortfolioBalancesForAddressById' +import { usePortfolioTokenOptions } from 'uniswap/src/components/TokenSelector/hooks/usePortfolioTokenOptions' import { TokenSelectorFlow } from 'uniswap/src/components/TokenSelector/types' import { createEmptyBalanceOption } from 'uniswap/src/components/TokenSelector/utils' import { BRIDGED_BASE_ADDRESSES } from 'uniswap/src/constants/addresses' -import { Chain } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { Chain, SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { fromGraphQLChain } from 'uniswap/src/features/chains/utils' import { tokenProjectToCurrencyInfos } from 'uniswap/src/features/dataApi/utils' import { UniswapState } from 'uniswap/src/state/uniswapReducer' @@ -39,7 +38,6 @@ import { } from 'uniswap/src/test/fixtures' import { act, renderHook, waitFor } from 'uniswap/src/test/test-utils' import { createArray, queryResolvers } from 'uniswap/src/test/utils' -import { UniverseChainId } from 'uniswap/src/types/chains' import { portfolioBalancesById } from 'uniswap/src/utils/balances' import { buildCurrencyId } from 'uniswap/src/utils/currencyId' @@ -49,12 +47,12 @@ jest.mock('uniswap/src/features/telemetry/send') const eth = ethToken() const dai = daiToken() -const usdc = usdcBaseToken() +const usdc_base = usdcBaseToken() const ethBalance = tokenBalance({ token: eth }) const daiBalance = tokenBalance({ token: dai }) -const usdcBalance = tokenBalance({ token: usdc }) -const favoriteTokens = [eth, dai, usdc] -const favoriteTokenBalances = [ethBalance, daiBalance, usdcBalance] +const usdcBaseBalance = tokenBalance({ token: usdc_base }) +const favoriteTokens = [eth, dai, usdc_base] +const favoriteTokenBalances = [ethBalance, daiBalance, usdcBaseBalance] const favoriteCurrencyIds = favoriteTokens.map((t) => buildCurrencyId(fromGraphQLChain(t.chain) ?? UniverseChainId.Mainnet, t.address), @@ -139,11 +137,12 @@ describe(useAllCommonBaseCurrencies, () => { describe(useFavoriteCurrencies, () => { const project = tokenProject({ // Add some more tokens to check if favorite tokens are filtered properly - tokens: [usdcArbitrumToken(), usdcBaseToken(), ...favoriteTokens, usdcToken()], + tokens: [usdcArbitrumToken(), usdcToken(), ...favoriteTokens], + safetyLevel: SafetyLevel.Verified, }) const projectWithFavoritesOnly = tokenProject({ - ...project, tokens: favoriteTokens, + safetyLevel: SafetyLevel.Verified, }) const cases = [ @@ -616,8 +615,8 @@ describe(usePopularTokensOptions, () => { }) describe(useCommonTokensOptionsWithFallback, () => { - const tokens = [eth, dai, usdc] - const tokenBalances = [ethBalance, daiBalance, usdcBalance] + const tokens = [eth, dai, usdc_base] + const tokenBalances = [ethBalance, daiBalance, usdcBaseBalance] const cases = [ { diff --git a/packages/uniswap/src/components/TokenSelector/hooks.tsx b/packages/uniswap/src/components/TokenSelector/hooks.tsx deleted file mode 100644 index 51c135c1544..00000000000 --- a/packages/uniswap/src/components/TokenSelector/hooks.tsx +++ /dev/null @@ -1,672 +0,0 @@ -/* eslint-disable max-lines */ -import { ApolloError } from '@apollo/client/errors' -import { useCallback, useEffect, useMemo, useState } from 'react' -import { useTranslation } from 'react-i18next' -import { useDispatch, useSelector } from 'react-redux' -import { Text, TouchableArea } from 'ui/src' -import { filter } from 'uniswap/src/components/TokenSelector/filter' -import { flowToModalName } from 'uniswap/src/components/TokenSelector/flowToModalName' -import { - TokenOption, - TokenOptionSection, - TokenSection, - TokenSelectorFlow, -} from 'uniswap/src/components/TokenSelector/types' -import { - createEmptyBalanceOption, - formatSearchResults, - mergeSearchResultsWithBridgingTokens, - useTokenOptionsSection, -} from 'uniswap/src/components/TokenSelector/utils' -import { BRIDGED_BASE_ADDRESSES, getNativeAddress } from 'uniswap/src/constants/addresses' -import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' -import { COMMON_BASES } from 'uniswap/src/constants/routing' -import { USDC, USDT, WBTC } from 'uniswap/src/constants/tokens' -import { SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { GqlResult } from 'uniswap/src/data/types' -import { TradeableAsset } from 'uniswap/src/entities/assets' -import { useBridgingTokensOptions } from 'uniswap/src/features/bridging/hooks/tokens' -import { - sortPortfolioBalances, - usePortfolioBalances, - useTokenBalancesGroupedByVisibility, -} from 'uniswap/src/features/dataApi/balances' -import { useSearchTokens } from 'uniswap/src/features/dataApi/searchTokens' -import { useTokenProjects } from 'uniswap/src/features/dataApi/tokenProjects' -import { usePopularTokens as usePopularTokensGql } from 'uniswap/src/features/dataApi/topTokens' -import { CurrencyInfo, PortfolioBalance } from 'uniswap/src/features/dataApi/types' -import { - buildCurrency, - buildCurrencyInfo, - gqlTokenToCurrencyInfo, - usePersistedError, -} from 'uniswap/src/features/dataApi/utils' -import { selectFavoriteTokens } from 'uniswap/src/features/favorites/selectors' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' -import { SearchResultType, TokenSearchResult } from 'uniswap/src/features/search/SearchResult' -import { addToSearchHistory, clearSearchHistory } from 'uniswap/src/features/search/searchHistorySlice' -import { selectSearchHistory } from 'uniswap/src/features/search/selectSearchHistory' -import { tokenAddressOrNativeAddress } from 'uniswap/src/features/search/utils' -import { useEnabledChains } from 'uniswap/src/features/settings/hooks' -import { WalletEventName } from 'uniswap/src/features/telemetry/constants' -import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' -import { usePopularTokens } from 'uniswap/src/features/tokens/hooks' -import { UniverseChainId } from 'uniswap/src/types/chains' -import { areAddressesEqual } from 'uniswap/src/utils/addresses' -import { buildNativeCurrencyId, buildWrappedNativeCurrencyId, currencyId } from 'uniswap/src/utils/currencyId' - -const getNativeCurrencyNames = (chains: UniverseChainId[]): { chainId: UniverseChainId; name: string }[] => - chains - .map((chainId) => { - return UNIVERSE_CHAIN_INFO[chainId].testnet - ? false - : { - chainId, - name: UNIVERSE_CHAIN_INFO[chainId].nativeCurrency.name.toLowerCase(), - } - }) - .filter(Boolean) as { chainId: UniverseChainId; name: string }[] - -// Use Mainnet base token addresses since TokenProjects query returns each token -// on each network -const baseCurrencyIds = [ - buildNativeCurrencyId(UniverseChainId.Mainnet), - buildNativeCurrencyId(UniverseChainId.Polygon), - buildNativeCurrencyId(UniverseChainId.Bnb), - buildNativeCurrencyId(UniverseChainId.Celo), - buildNativeCurrencyId(UniverseChainId.Avalanche), - currencyId(USDC), - currencyId(USDT), - currencyId(WBTC), - buildWrappedNativeCurrencyId(UniverseChainId.Mainnet), -] - -export function currencyInfosToTokenOptions( - currencyInfos: Array | undefined, -): TokenOption[] | undefined { - return currencyInfos - ?.filter((cI): cI is CurrencyInfo => Boolean(cI)) - .map((currencyInfo) => ({ - currencyInfo, - quantity: null, - balanceUSD: undefined, - })) -} - -export function searchResultToCurrencyInfo({ - chainId, - address, - symbol, - name, - logoUrl, - safetyLevel, - safetyInfo, -}: TokenSearchResult): CurrencyInfo | null { - const currency = buildCurrency({ - chainId: chainId as UniverseChainId, - address, - decimals: 0, // this does not matter in a context of CurrencyInfo here, as we do not provide any balance - symbol, - name, - }) - - if (!currency) { - return null - } - - return buildCurrencyInfo({ - currency, - currencyId: currencyId(currency), - logoUrl, - safetyLevel: safetyLevel ?? SafetyLevel.StrongWarning, - // defaulting to not spam, as user has searched and chosen this token before - isSpam: false, - safetyInfo, - }) -} - -export function useAllCommonBaseCurrencies(): GqlResult { - const { isTestnetModeEnabled } = useEnabledChains() - return useCurrencies(isTestnetModeEnabled ? [] : baseCurrencyIds) -} - -export function useCurrencies(currencyIds: string[]): GqlResult { - const { data: baseCurrencyInfos, loading, error, refetch } = useTokenProjects(currencyIds) - const persistedError = usePersistedError(loading, error instanceof ApolloError ? error : undefined) - - // TokenProjects returns tokens on every network, so filter out native assets that have a - // bridged version on other networks - const filteredBaseCurrencyInfos = useMemo(() => { - return baseCurrencyInfos?.filter((currencyInfo) => { - if (currencyInfo.currency.isNative) { - return true - } - - const { address } = currencyInfo.currency - const bridgedAsset = BRIDGED_BASE_ADDRESSES.find((bridgedAddress) => areAddressesEqual(bridgedAddress, address)) - - if (!bridgedAsset) { - return true - } - - return false - }) - }, [baseCurrencyInfos]) - - return { data: filteredBaseCurrencyInfos, loading, error: persistedError, refetch } -} - -export function usePortfolioBalancesForAddressById( - address: Address | undefined, -): GqlResult | undefined> { - const { - data: portfolioBalancesById, - error, - refetch, - loading, - } = usePortfolioBalances({ - address, - fetchPolicy: 'cache-first', // we want to avoid re-renders when token selector is opening - }) - - return { - data: portfolioBalancesById, - error, - refetch, - loading, - } -} - -export function useFavoriteCurrencies(): GqlResult { - const favoriteCurrencyIds = useSelector(selectFavoriteTokens) - const { data: favoriteTokensOnAllChains, loading, error, refetch } = useTokenProjects(favoriteCurrencyIds) - - const persistedError = usePersistedError(loading, error instanceof ApolloError ? error : undefined) - - // useTokenProjects returns each token on Arbitrum, Optimism, Polygon, - // so we need to filter out the tokens which user has actually favorited - const favoriteTokens = useMemo( - () => - favoriteTokensOnAllChains && - favoriteCurrencyIds - .map((_currencyId) => { - return favoriteTokensOnAllChains.find((token) => token.currencyId === _currencyId) - }) - .filter((token: CurrencyInfo | undefined): token is CurrencyInfo => { - return !!token - }), - [favoriteCurrencyIds, favoriteTokensOnAllChains], - ) - - return { data: favoriteTokens, loading, error: persistedError, refetch } -} - -export function useAddToSearchHistory(): { registerSearch: (currencyInfo: CurrencyInfo) => void } { - const dispatch = useDispatch() - - const registerSearch = (currencyInfo: CurrencyInfo): void => { - dispatch( - addToSearchHistory({ - searchResult: currencyInfoToTokenSearchResult(currencyInfo), - }), - ) - } - - return { registerSearch } -} - -function currencyInfoToTokenSearchResult(currencyInfo: CurrencyInfo): TokenSearchResult { - const address = currencyInfo.currency.isToken - ? currencyInfo.currency.address - : getNativeAddress(currencyInfo.currency.chainId) - - return { - type: SearchResultType.Token, - chainId: currencyInfo.currency.chainId, - address: tokenAddressOrNativeAddress(address, currencyInfo.currency.chainId), - name: currencyInfo.currency.name ?? null, - symbol: currencyInfo.currency.symbol ?? '', - logoUrl: currencyInfo.logoUrl ?? null, - safetyLevel: currencyInfo.safetyLevel ?? null, - safetyInfo: currencyInfo.safetyInfo, - } -} - -export function useFavoriteTokensOptions( - address: Address | undefined, - chainFilter: UniverseChainId | null, -): GqlResult { - const { - data: portfolioBalancesById, - error: portfolioBalancesByIdError, - refetch: portfolioBalancesByIdRefetch, - loading: loadingPorfolioBalancesById, - } = usePortfolioBalancesForAddressById(address) - - const { - data: favoriteCurrencies, - error: favoriteCurrenciesError, - refetch: refetchFavoriteCurrencies, - loading: loadingFavoriteCurrencies, - } = useFavoriteCurrencies() - - const favoriteTokenOptions = useCurrencyInfosToTokenOptions({ - currencyInfos: favoriteCurrencies, - portfolioBalancesById, - sortAlphabetically: true, - }) - - const refetch = useCallback(() => { - portfolioBalancesByIdRefetch?.() - refetchFavoriteCurrencies?.() - }, [portfolioBalancesByIdRefetch, refetchFavoriteCurrencies]) - - const error = - (!portfolioBalancesById && portfolioBalancesByIdError) || (!favoriteCurrencies && favoriteCurrenciesError) - - const filteredFavoriteTokenOptions = useMemo( - () => favoriteTokenOptions && filter(favoriteTokenOptions, chainFilter), - [chainFilter, favoriteTokenOptions], - ) - - return { - data: filteredFavoriteTokenOptions, - refetch, - error: error || undefined, - loading: loadingPorfolioBalancesById || loadingFavoriteCurrencies, - } -} - -function ClearAll({ onPress }: { onPress: () => void }): JSX.Element { - const { t } = useTranslation() - return ( - - - {t('tokens.selector.button.clear')} - - - ) -} - -export function useTokenSectionsForEmptySearch(chainFilter: UniverseChainId | null): GqlResult { - const dispatch = useDispatch() - - const { popularTokens, loading } = usePopularTokens() - - const recentlySearchedTokenOptions = useRecentlySearchedTokens(chainFilter) - - // it's a dependency of useMemo => useCallback - const onPressClearSearchHistory = useCallback((): void => { - dispatch(clearSearchHistory()) - }, [dispatch]) - - const recentSection = useTokenOptionsSection({ - sectionKey: TokenOptionSection.RecentTokens, - tokenOptions: recentlySearchedTokenOptions, - endElement: , - }) - - const popularSection = useTokenOptionsSection({ - sectionKey: TokenOptionSection.PopularTokens, - tokenOptions: currencyInfosToTokenOptions(popularTokens?.map(gqlTokenToCurrencyInfo)), - }) - const sections = useMemo(() => [...(recentSection ?? []), ...(popularSection ?? [])], [popularSection, recentSection]) - - return useMemo( - () => ({ - data: sections, - loading, - }), - [loading, sections], - ) -} - -export function useCurrencyInfosToTokenOptions({ - currencyInfos, - portfolioBalancesById, - sortAlphabetically, -}: { - currencyInfos?: CurrencyInfo[] - sortAlphabetically?: boolean - portfolioBalancesById?: Record -}): TokenOption[] | undefined { - // we use useMemo here to avoid recalculation of internals when function params are the same, - // but the component, where this hook is used is re-rendered - return useMemo(() => { - if (!currencyInfos) { - return undefined - } - const sortedCurrencyInfos = sortAlphabetically - ? [...currencyInfos].sort((a, b) => { - if (a.currency.name && b.currency.name) { - return a.currency.name.localeCompare(b.currency.name) - } - return 0 - }) - : currencyInfos - - return sortedCurrencyInfos.map( - (currencyInfo) => portfolioBalancesById?.[currencyInfo.currencyId] ?? createEmptyBalanceOption(currencyInfo), - ) - }, [currencyInfos, portfolioBalancesById, sortAlphabetically]) -} - -export function useCommonTokensOptions( - address: Address | undefined, - chainFilter: UniverseChainId | null, -): GqlResult { - const { - data: portfolioBalancesById, - error: portfolioBalancesByIdError, - refetch: portfolioBalancesByIdRefetch, - loading: loadingPorfolioBalancesById, - } = usePortfolioBalancesForAddressById(address) - - const { - data: commonBaseCurrencies, - error: commonBaseCurrenciesError, - refetch: refetchCommonBaseCurrencies, - loading: loadingCommonBaseCurrencies, - } = useAllCommonBaseCurrencies() - - const commonBaseTokenOptions = useCurrencyInfosToTokenOptions({ - currencyInfos: commonBaseCurrencies, - portfolioBalancesById, - }) - - const refetch = useCallback(() => { - portfolioBalancesByIdRefetch?.() - refetchCommonBaseCurrencies?.() - }, [portfolioBalancesByIdRefetch, refetchCommonBaseCurrencies]) - - const error = - (!portfolioBalancesById && portfolioBalancesByIdError) || (!commonBaseCurrencies && commonBaseCurrenciesError) - - const filteredCommonBaseTokenOptions = useMemo( - () => commonBaseTokenOptions && filter(commonBaseTokenOptions, chainFilter), - [chainFilter, commonBaseTokenOptions], - ) - - return { - data: filteredCommonBaseTokenOptions, - refetch, - error: error || undefined, - loading: loadingPorfolioBalancesById || loadingCommonBaseCurrencies, - } -} - -export function useCommonTokensOptionsWithFallback( - address: Address | undefined, - chainFilter: UniverseChainId | null, -): GqlResult { - const { data, error, refetch, loading } = useCommonTokensOptions(address, chainFilter) - const commonBases = chainFilter ? currencyInfosToTokenOptions(COMMON_BASES[chainFilter]) : undefined - - const shouldFallback = !loading && data?.length === 0 && commonBases?.length - - return { - data: shouldFallback ? commonBases : data, - error: shouldFallback ? undefined : error, - refetch, - loading, - } -} - -export function usePopularTokensOptions( - address: Address | undefined, - chainFilter: UniverseChainId, -): GqlResult { - const { - data: portfolioBalancesById, - error: portfolioBalancesByIdError, - refetch: portfolioBalancesByIdRefetch, - loading: loadingPorfolioBalancesById, - } = usePortfolioBalancesForAddressById(address) - - const { - data: popularTokens, - error: popularTokensError, - refetch: refetchPopularTokens, - loading: loadingPopularTokens, - } = usePopularTokensGql(chainFilter) - - const popularTokenOptions = useCurrencyInfosToTokenOptions({ - currencyInfos: popularTokens, - portfolioBalancesById, - sortAlphabetically: true, - }) - - const refetch = useCallback(() => { - portfolioBalancesByIdRefetch?.() - refetchPopularTokens?.() - }, [portfolioBalancesByIdRefetch, refetchPopularTokens]) - - const error = (!portfolioBalancesById && portfolioBalancesByIdError) || (!popularTokenOptions && popularTokensError) - - return { - data: popularTokenOptions, - refetch, - error: error || undefined, - loading: loadingPorfolioBalancesById || loadingPopularTokens, - } -} - -export function usePortfolioTokenOptions( - address: Address | undefined, - chainFilter: UniverseChainId | null, - searchFilter?: string, -): GqlResult { - const { data: portfolioBalancesById, error, refetch, loading } = usePortfolioBalancesForAddressById(address) - const { isTestnetModeEnabled } = useEnabledChains() - - const { shownTokens } = useTokenBalancesGroupedByVisibility({ - balancesById: portfolioBalancesById, - }) - - const portfolioBalances = useMemo( - () => (shownTokens ? sortPortfolioBalances({ balances: shownTokens, isTestnetModeEnabled }) : undefined), - [shownTokens, isTestnetModeEnabled], - ) - - const filteredPortfolioBalances = useMemo( - () => portfolioBalances && filter(portfolioBalances, chainFilter, searchFilter), - [chainFilter, portfolioBalances, searchFilter], - ) - - return { - data: filteredPortfolioBalances, - error, - refetch, - loading, - } -} - -export function useFilterCallbacks( - chainId: UniverseChainId | null, - flow: TokenSelectorFlow, -): { - chainFilter: UniverseChainId | null - parsedChainFilter: UniverseChainId | null - searchFilter: string | null - parsedSearchFilter: string | null - onChangeChainFilter: (newChainFilter: UniverseChainId | null) => void - onClearSearchFilter: () => void - onChangeText: (newSearchFilter: string) => void -} { - const [chainFilter, setChainFilter] = useState(chainId) - const [parsedChainFilter, setParsedChainFilter] = useState(null) - const [searchFilter, setSearchFilter] = useState(null) - const [parsedSearchFilter, setParsedSearchFilter] = useState(null) - - const { chains: enabledChains } = useEnabledChains() - - // Parses the user input to determine if the user is searching for a chain + token - // i.e "eth dai" - // parsedChainFilter: 1 - // parsedSearchFilter: "dai" - useEffect(() => { - const splitSearch = searchFilter?.split(' ') - const maybeChainName = splitSearch?.[0]?.toLowerCase() - - const chainMatch = getNativeCurrencyNames(enabledChains).find((currency) => - currency.name.startsWith(maybeChainName ?? ''), - ) - const search = splitSearch?.slice(1).join(' ') - - if (!chainFilter && chainMatch && search) { - setParsedChainFilter(chainMatch.chainId) - setParsedSearchFilter(search) - } else { - setParsedChainFilter(null) - setParsedSearchFilter(null) - } - }, [searchFilter, chainFilter, enabledChains]) - - useEffect(() => { - setChainFilter(chainId) - }, [chainId]) - - const onChangeChainFilter = useCallback( - (newChainFilter: typeof chainFilter) => { - setChainFilter(newChainFilter) - sendAnalyticsEvent(WalletEventName.NetworkFilterSelected, { - chain: newChainFilter ?? 'All', - modal: flowToModalName(flow), - }) - }, - [flow], - ) - - const onClearSearchFilter = useCallback(() => { - setSearchFilter(null) - }, []) - - const onChangeText = useCallback((newSearchFilter: string) => setSearchFilter(newSearchFilter), [setSearchFilter]) - - return { - chainFilter, - parsedChainFilter, - searchFilter, - parsedSearchFilter, - onChangeChainFilter, - onClearSearchFilter, - onChangeText, - } -} - -export const MAX_RECENT_SEARCH_RESULTS = 4 - -export function useRecentlySearchedTokens(chainFilter: UniverseChainId | null): TokenOption[] | undefined { - const searchHistory = useSelector(selectSearchHistory) - - return useMemo( - () => - currencyInfosToTokenOptions( - searchHistory - .filter((searchResult): searchResult is TokenSearchResult => searchResult.type === SearchResultType.Token) - .filter((searchResult) => (chainFilter ? searchResult.chainId === chainFilter : true)) - .slice(0, MAX_RECENT_SEARCH_RESULTS) - .map(searchResultToCurrencyInfo), - ), - [chainFilter, searchHistory], - ) -} - -export function useTokenSectionsForSearchResults( - address: string | undefined, - chainFilter: UniverseChainId | null, - searchFilter: string | null, - isBalancesOnlySearch: boolean, - input: TradeableAsset | undefined, -): GqlResult { - const { t } = useTranslation() - const isBridgingEnabled = useFeatureFlag(FeatureFlags.Bridging) - - const { - data: portfolioBalancesById, - error: portfolioBalancesByIdError, - refetch: refetchPortfolioBalances, - loading: portfolioBalancesByIdLoading, - } = usePortfolioBalancesForAddressById(address) - - const { - data: portfolioTokenOptions, - error: portfolioTokenOptionsError, - refetch: refetchPortfolioTokenOptions, - loading: portfolioTokenOptionsLoading, - } = usePortfolioTokenOptions(address, chainFilter, searchFilter ?? undefined) - - // Bridging tokens are only shown if input is provided - const { - data: bridgingTokenOptions, - error: bridgingTokenOptionsError, - refetch: refetchBridgingTokenOptions, - loading: bridgingTokenOptionsLoading, - } = useBridgingTokensOptions({ input, walletAddress: address, chainFilter }) - - // Only call search endpoint if isBalancesOnlySearch is false - const { - data: searchResultCurrencies, - error: searchTokensError, - refetch: refetchSearchTokens, - loading: searchTokensLoading, - } = useSearchTokens(searchFilter, chainFilter, /*skip*/ isBalancesOnlySearch) - - const searchResults = useMemo(() => { - return formatSearchResults(searchResultCurrencies, portfolioBalancesById, searchFilter) - }, [searchResultCurrencies, portfolioBalancesById, searchFilter]) - - const loading = - portfolioTokenOptionsLoading || - portfolioBalancesByIdLoading || - (!isBalancesOnlySearch && searchTokensLoading) || - bridgingTokenOptionsLoading - - const searchResultsSections = useTokenOptionsSection({ - sectionKey: TokenOptionSection.SearchResults, - // Use local search when only searching balances - tokenOptions: isBalancesOnlySearch ? portfolioTokenOptions : searchResults, - }) - - // If there are bridging options, we need to extract them from the search results and then prepend them as a new section above. - // The remaining non-bridging search results will be shown in a section with a different name - const networkName = chainFilter ? UNIVERSE_CHAIN_INFO[chainFilter].label : undefined - const searchResultsSectionHeader = networkName - ? t('tokens.selector.section.otherSearchResults', { network: networkName }) - : undefined - const sections = isBridgingEnabled - ? mergeSearchResultsWithBridgingTokens(searchResultsSections, bridgingTokenOptions, searchResultsSectionHeader) - : searchResultsSections - - const error = - (!bridgingTokenOptions && bridgingTokenOptionsError) || - (!portfolioBalancesById && portfolioBalancesByIdError) || - (!portfolioTokenOptions && portfolioTokenOptionsError) || - (!isBalancesOnlySearch && !searchResults && searchTokensError) - - const refetchAll = useCallback(() => { - refetchPortfolioBalances?.() - refetchSearchTokens?.() - refetchPortfolioTokenOptions?.() - if (isBridgingEnabled) { - refetchBridgingTokenOptions?.() - } - }, [ - isBridgingEnabled, - refetchBridgingTokenOptions, - refetchPortfolioBalances, - refetchPortfolioTokenOptions, - refetchSearchTokens, - ]) - - return useMemo( - () => ({ - data: sections, - loading, - error: error || undefined, - refetch: refetchAll, - }), - [error, loading, refetchAll, sections], - ) -} diff --git a/packages/uniswap/src/components/TokenSelector/hooks/useAddToSearchHistory.ts b/packages/uniswap/src/components/TokenSelector/hooks/useAddToSearchHistory.ts new file mode 100644 index 00000000000..784a2c27af1 --- /dev/null +++ b/packages/uniswap/src/components/TokenSelector/hooks/useAddToSearchHistory.ts @@ -0,0 +1,43 @@ +import { useDispatch } from 'react-redux' +import { getNativeAddress } from 'uniswap/src/constants/addresses' +import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' +import { SearchResultType, TokenSearchResult } from 'uniswap/src/features/search/SearchResult' +import { addToSearchHistory } from 'uniswap/src/features/search/searchHistorySlice' +import { tokenAddressOrNativeAddress } from 'uniswap/src/features/search/utils' + +export function useAddToSearchHistory(): { registerSearch: (currencyInfo: CurrencyInfo) => void } { + const dispatch = useDispatch() + + const registerSearch = (currencyInfo: CurrencyInfo): void => { + dispatch( + addToSearchHistory({ + searchResult: currencyInfoToTokenSearchResult(currencyInfo), + }), + ) + } + + return { registerSearch } +} + +function currencyInfoToTokenSearchResult(currencyInfo: CurrencyInfo): TokenSearchResult { + const address = currencyInfo.currency.isToken + ? currencyInfo.currency.address + : getNativeAddress(currencyInfo.currency.chainId) + + return { + type: SearchResultType.Token, + chainId: currencyInfo.currency.chainId, + address: tokenAddressOrNativeAddress(address, currencyInfo.currency.chainId), + name: currencyInfo.currency.name ?? null, + symbol: currencyInfo.currency.symbol ?? '', + logoUrl: currencyInfo.logoUrl ?? null, + safetyLevel: currencyInfo.safetyLevel ?? null, + safetyInfo: currencyInfo.safetyInfo, + feeData: currencyInfo.currency.isToken + ? { + buyFeeBps: currencyInfo.currency.buyFeeBps?.gt(0) ? currencyInfo.currency.buyFeeBps.toString() : undefined, + sellFeeBps: currencyInfo.currency.sellFeeBps?.gt(0) ? currencyInfo.currency.sellFeeBps.toString() : undefined, + } + : null, + } +} diff --git a/packages/uniswap/src/components/TokenSelector/hooks/useAllCommonBaseCurrencies.ts b/packages/uniswap/src/components/TokenSelector/hooks/useAllCommonBaseCurrencies.ts new file mode 100644 index 00000000000..60b1595d286 --- /dev/null +++ b/packages/uniswap/src/components/TokenSelector/hooks/useAllCommonBaseCurrencies.ts @@ -0,0 +1,26 @@ +import { useCurrencies } from 'uniswap/src/components/TokenSelector/hooks/useCurrencies' +import { USDC, USDT, WBTC } from 'uniswap/src/constants/tokens' +import { GqlResult } from 'uniswap/src/data/types' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' +import { buildNativeCurrencyId, buildWrappedNativeCurrencyId, currencyId } from 'uniswap/src/utils/currencyId' + +// Use Mainnet base token addresses since TokenProjects query returns each token +// on each network +const baseCurrencyIds = [ + buildNativeCurrencyId(UniverseChainId.Mainnet), + buildNativeCurrencyId(UniverseChainId.Polygon), + buildNativeCurrencyId(UniverseChainId.Bnb), + buildNativeCurrencyId(UniverseChainId.Celo), + buildNativeCurrencyId(UniverseChainId.Avalanche), + currencyId(USDC), + currencyId(USDT), + currencyId(WBTC), + buildWrappedNativeCurrencyId(UniverseChainId.Mainnet), +] + +export function useAllCommonBaseCurrencies(): GqlResult { + const { isTestnetModeEnabled } = useEnabledChains() + return useCurrencies(isTestnetModeEnabled ? [] : baseCurrencyIds) +} diff --git a/packages/uniswap/src/components/TokenSelector/hooks/useCommonTokensOptions.ts b/packages/uniswap/src/components/TokenSelector/hooks/useCommonTokensOptions.ts new file mode 100644 index 00000000000..8e9a71b6e1d --- /dev/null +++ b/packages/uniswap/src/components/TokenSelector/hooks/useCommonTokensOptions.ts @@ -0,0 +1,52 @@ +import { useCallback, useMemo } from 'react' +import { filter } from 'uniswap/src/components/TokenSelector/filter' +import { useAllCommonBaseCurrencies } from 'uniswap/src/components/TokenSelector/hooks/useAllCommonBaseCurrencies' +import { useCurrencyInfosToTokenOptions } from 'uniswap/src/components/TokenSelector/hooks/useCurrencyInfosToTokenOptions' +import { usePortfolioBalancesForAddressById } from 'uniswap/src/components/TokenSelector/hooks/usePortfolioBalancesForAddressById' +import { TokenOption } from 'uniswap/src/components/TokenSelector/types' +import { GqlResult } from 'uniswap/src/data/types' +import { UniverseChainId } from 'uniswap/src/features/chains/types' + +export function useCommonTokensOptions( + address: Address | undefined, + chainFilter: UniverseChainId | null, +): GqlResult { + const { + data: portfolioBalancesById, + error: portfolioBalancesByIdError, + refetch: portfolioBalancesByIdRefetch, + loading: loadingPorfolioBalancesById, + } = usePortfolioBalancesForAddressById(address) + + const { + data: commonBaseCurrencies, + error: commonBaseCurrenciesError, + refetch: refetchCommonBaseCurrencies, + loading: loadingCommonBaseCurrencies, + } = useAllCommonBaseCurrencies() + + const commonBaseTokenOptions = useCurrencyInfosToTokenOptions({ + currencyInfos: commonBaseCurrencies, + portfolioBalancesById, + }) + + const refetch = useCallback(() => { + portfolioBalancesByIdRefetch?.() + refetchCommonBaseCurrencies?.() + }, [portfolioBalancesByIdRefetch, refetchCommonBaseCurrencies]) + + const error = + (!portfolioBalancesById && portfolioBalancesByIdError) || (!commonBaseCurrencies && commonBaseCurrenciesError) + + const filteredCommonBaseTokenOptions = useMemo( + () => commonBaseTokenOptions && filter(commonBaseTokenOptions, chainFilter), + [chainFilter, commonBaseTokenOptions], + ) + + return { + data: filteredCommonBaseTokenOptions, + refetch, + error: error || undefined, + loading: loadingPorfolioBalancesById || loadingCommonBaseCurrencies, + } +} diff --git a/packages/uniswap/src/components/TokenSelector/hooks/useCommonTokensOptionsWithFallback.ts b/packages/uniswap/src/components/TokenSelector/hooks/useCommonTokensOptionsWithFallback.ts new file mode 100644 index 00000000000..6a717f60012 --- /dev/null +++ b/packages/uniswap/src/components/TokenSelector/hooks/useCommonTokensOptionsWithFallback.ts @@ -0,0 +1,23 @@ +import { useCommonTokensOptions } from 'uniswap/src/components/TokenSelector/hooks/useCommonTokensOptions' +import { currencyInfosToTokenOptions } from 'uniswap/src/components/TokenSelector/hooks/useCurrencyInfosToTokenOptions' +import { TokenOption } from 'uniswap/src/components/TokenSelector/types' +import { COMMON_BASES } from 'uniswap/src/constants/routing' +import { GqlResult } from 'uniswap/src/data/types' +import { UniverseChainId } from 'uniswap/src/features/chains/types' + +export function useCommonTokensOptionsWithFallback( + address: Address | undefined, + chainFilter: UniverseChainId | null, +): GqlResult { + const { data, error, refetch, loading } = useCommonTokensOptions(address, chainFilter) + const commonBases = chainFilter ? currencyInfosToTokenOptions(COMMON_BASES[chainFilter]) : undefined + + const shouldFallback = !loading && data?.length === 0 && commonBases?.length + + return { + data: shouldFallback ? commonBases : data, + error: shouldFallback ? undefined : error, + refetch, + loading, + } +} diff --git a/packages/uniswap/src/components/TokenSelector/hooks/useCurrencies.ts b/packages/uniswap/src/components/TokenSelector/hooks/useCurrencies.ts new file mode 100644 index 00000000000..9291f3545d0 --- /dev/null +++ b/packages/uniswap/src/components/TokenSelector/hooks/useCurrencies.ts @@ -0,0 +1,34 @@ +import { ApolloError } from '@apollo/client' +import { useMemo } from 'react' +import { BRIDGED_BASE_ADDRESSES } from 'uniswap/src/constants/addresses' +import { GqlResult } from 'uniswap/src/data/types' +import { useTokenProjects } from 'uniswap/src/features/dataApi/tokenProjects' +import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' +import { usePersistedError } from 'uniswap/src/features/dataApi/utils' +import { areAddressesEqual } from 'uniswap/src/utils/addresses' + +export function useCurrencies(currencyIds: string[]): GqlResult { + const { data: baseCurrencyInfos, loading, error, refetch } = useTokenProjects(currencyIds) + const persistedError = usePersistedError(loading, error instanceof ApolloError ? error : undefined) + + // TokenProjects returns tokens on every network, so filter out native assets that have a + // bridged version on other networks + const filteredBaseCurrencyInfos = useMemo(() => { + return baseCurrencyInfos?.filter((currencyInfo) => { + if (currencyInfo.currency.isNative) { + return true + } + + const { address } = currencyInfo.currency + const bridgedAsset = BRIDGED_BASE_ADDRESSES.find((bridgedAddress) => areAddressesEqual(bridgedAddress, address)) + + if (!bridgedAsset) { + return true + } + + return false + }) + }, [baseCurrencyInfos]) + + return { data: filteredBaseCurrencyInfos, loading, error: persistedError, refetch } +} diff --git a/packages/uniswap/src/components/TokenSelector/hooks/useCurrencyInfosToTokenOptions.ts b/packages/uniswap/src/components/TokenSelector/hooks/useCurrencyInfosToTokenOptions.ts new file mode 100644 index 00000000000..56ae6e1fb5a --- /dev/null +++ b/packages/uniswap/src/components/TokenSelector/hooks/useCurrencyInfosToTokenOptions.ts @@ -0,0 +1,78 @@ +import { ApolloError } from '@apollo/client' +import { useMemo } from 'react' +import { TokenOption } from 'uniswap/src/components/TokenSelector/types' +import { createEmptyBalanceOption } from 'uniswap/src/components/TokenSelector/utils' +import { BRIDGED_BASE_ADDRESSES } from 'uniswap/src/constants/addresses' +import { GqlResult } from 'uniswap/src/data/types' +import { useTokenProjects } from 'uniswap/src/features/dataApi/tokenProjects' +import { CurrencyInfo, PortfolioBalance } from 'uniswap/src/features/dataApi/types' +import { usePersistedError } from 'uniswap/src/features/dataApi/utils' +import { areAddressesEqual } from 'uniswap/src/utils/addresses' + +export function useCurrencies(currencyIds: string[]): GqlResult { + const { data: baseCurrencyInfos, loading, error, refetch } = useTokenProjects(currencyIds) + const persistedError = usePersistedError(loading, error instanceof ApolloError ? error : undefined) + + // TokenProjects returns tokens on every network, so filter out native assets that have a + // bridged version on other networks + const filteredBaseCurrencyInfos = useMemo(() => { + return baseCurrencyInfos?.filter((currencyInfo) => { + if (currencyInfo.currency.isNative) { + return true + } + + const { address } = currencyInfo.currency + const bridgedAsset = BRIDGED_BASE_ADDRESSES.find((bridgedAddress) => areAddressesEqual(bridgedAddress, address)) + + if (!bridgedAsset) { + return true + } + + return false + }) + }, [baseCurrencyInfos]) + + return { data: filteredBaseCurrencyInfos, loading, error: persistedError, refetch } +} + +export function currencyInfosToTokenOptions( + currencyInfos: Array | undefined, +): TokenOption[] | undefined { + return currencyInfos + ?.filter((cI): cI is CurrencyInfo => Boolean(cI)) + .map((currencyInfo) => ({ + currencyInfo, + quantity: null, + balanceUSD: undefined, + })) +} + +export function useCurrencyInfosToTokenOptions({ + currencyInfos, + portfolioBalancesById, + sortAlphabetically, +}: { + currencyInfos?: CurrencyInfo[] + sortAlphabetically?: boolean + portfolioBalancesById?: Record +}): TokenOption[] | undefined { + // we use useMemo here to avoid recalculation of internals when function params are the same, + // but the component, where this hook is used is re-rendered + return useMemo(() => { + if (!currencyInfos) { + return undefined + } + const sortedCurrencyInfos = sortAlphabetically + ? [...currencyInfos].sort((a, b) => { + if (a.currency.name && b.currency.name) { + return a.currency.name.localeCompare(b.currency.name) + } + return 0 + }) + : currencyInfos + + return sortedCurrencyInfos.map( + (currencyInfo) => portfolioBalancesById?.[currencyInfo.currencyId] ?? createEmptyBalanceOption(currencyInfo), + ) + }, [currencyInfos, portfolioBalancesById, sortAlphabetically]) +} diff --git a/packages/uniswap/src/components/TokenSelector/hooks/useFavoriteCurrencies.ts b/packages/uniswap/src/components/TokenSelector/hooks/useFavoriteCurrencies.ts new file mode 100644 index 00000000000..f0536614a87 --- /dev/null +++ b/packages/uniswap/src/components/TokenSelector/hooks/useFavoriteCurrencies.ts @@ -0,0 +1,32 @@ +import { ApolloError } from '@apollo/client' +import { useMemo } from 'react' +import { useSelector } from 'react-redux' +import { GqlResult } from 'uniswap/src/data/types' +import { useTokenProjects } from 'uniswap/src/features/dataApi/tokenProjects' +import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' +import { usePersistedError } from 'uniswap/src/features/dataApi/utils' +import { selectFavoriteTokens } from 'uniswap/src/features/favorites/selectors' + +export function useFavoriteCurrencies(): GqlResult { + const favoriteCurrencyIds = useSelector(selectFavoriteTokens) + const { data: favoriteTokensOnAllChains, loading, error, refetch } = useTokenProjects(favoriteCurrencyIds) + + const persistedError = usePersistedError(loading, error instanceof ApolloError ? error : undefined) + + // useTokenProjects returns each token on Arbitrum, Optimism, Polygon, + // so we need to filter out the tokens which user has actually favorited + const favoriteTokens = useMemo( + () => + favoriteTokensOnAllChains && + favoriteCurrencyIds + .map((_currencyId) => { + return favoriteTokensOnAllChains.find((token) => token.currencyId === _currencyId) + }) + .filter((token: CurrencyInfo | undefined): token is CurrencyInfo => { + return !!token + }), + [favoriteCurrencyIds, favoriteTokensOnAllChains], + ) + + return { data: favoriteTokens, loading, error: persistedError, refetch } +} diff --git a/packages/uniswap/src/components/TokenSelector/hooks/useFavoriteTokensOptions.ts b/packages/uniswap/src/components/TokenSelector/hooks/useFavoriteTokensOptions.ts new file mode 100644 index 00000000000..57bfa55f451 --- /dev/null +++ b/packages/uniswap/src/components/TokenSelector/hooks/useFavoriteTokensOptions.ts @@ -0,0 +1,53 @@ +import { useCallback, useMemo } from 'react' +import { filter } from 'uniswap/src/components/TokenSelector/filter' +import { useCurrencyInfosToTokenOptions } from 'uniswap/src/components/TokenSelector/hooks/useCurrencyInfosToTokenOptions' +import { useFavoriteCurrencies } from 'uniswap/src/components/TokenSelector/hooks/useFavoriteCurrencies' +import { usePortfolioBalancesForAddressById } from 'uniswap/src/components/TokenSelector/hooks/usePortfolioBalancesForAddressById' +import { TokenOption } from 'uniswap/src/components/TokenSelector/types' +import { GqlResult } from 'uniswap/src/data/types' +import { UniverseChainId } from 'uniswap/src/features/chains/types' + +export function useFavoriteTokensOptions( + address: Address | undefined, + chainFilter: UniverseChainId | null, +): GqlResult { + const { + data: portfolioBalancesById, + error: portfolioBalancesByIdError, + refetch: portfolioBalancesByIdRefetch, + loading: loadingPorfolioBalancesById, + } = usePortfolioBalancesForAddressById(address) + + const { + data: favoriteCurrencies, + error: favoriteCurrenciesError, + refetch: refetchFavoriteCurrencies, + loading: loadingFavoriteCurrencies, + } = useFavoriteCurrencies() + + const favoriteTokenOptions = useCurrencyInfosToTokenOptions({ + currencyInfos: favoriteCurrencies, + portfolioBalancesById, + sortAlphabetically: true, + }) + + const refetch = useCallback(() => { + portfolioBalancesByIdRefetch?.() + refetchFavoriteCurrencies?.() + }, [portfolioBalancesByIdRefetch, refetchFavoriteCurrencies]) + + const error = + (!portfolioBalancesById && portfolioBalancesByIdError) || (!favoriteCurrencies && favoriteCurrenciesError) + + const filteredFavoriteTokenOptions = useMemo( + () => favoriteTokenOptions && filter(favoriteTokenOptions, chainFilter), + [chainFilter, favoriteTokenOptions], + ) + + return { + data: filteredFavoriteTokenOptions, + refetch, + error: error || undefined, + loading: loadingPorfolioBalancesById || loadingFavoriteCurrencies, + } +} diff --git a/packages/uniswap/src/components/TokenSelector/hooks/useFilterCallbacks.ts b/packages/uniswap/src/components/TokenSelector/hooks/useFilterCallbacks.ts new file mode 100644 index 00000000000..0b4f4729200 --- /dev/null +++ b/packages/uniswap/src/components/TokenSelector/hooks/useFilterCallbacks.ts @@ -0,0 +1,94 @@ +import { useCallback, useEffect, useState } from 'react' +import { TokenSelectorFlow } from 'uniswap/src/components/TokenSelector/types' +import { flowToModalName } from 'uniswap/src/components/TokenSelector/utils' +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { isTestnetChain } from 'uniswap/src/features/chains/utils' +import { WalletEventName } from 'uniswap/src/features/telemetry/constants' +import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' + +export function useFilterCallbacks( + chainId: UniverseChainId | null, + flow: TokenSelectorFlow, +): { + chainFilter: UniverseChainId | null + parsedChainFilter: UniverseChainId | null + searchFilter: string | null + parsedSearchFilter: string | null + onChangeChainFilter: (newChainFilter: UniverseChainId | null) => void + onClearSearchFilter: () => void + onChangeText: (newSearchFilter: string) => void +} { + const [chainFilter, setChainFilter] = useState(chainId) + const [parsedChainFilter, setParsedChainFilter] = useState(null) + const [searchFilter, setSearchFilter] = useState(null) + const [parsedSearchFilter, setParsedSearchFilter] = useState(null) + + const { chains: enabledChains } = useEnabledChains() + + // Parses the user input to determine if the user is searching for a chain + token + // i.e "eth dai" + // parsedChainFilter: 1 + // parsedSearchFilter: "dai" + useEffect(() => { + const splitSearch = searchFilter?.split(' ') + const maybeChainName = splitSearch?.[0]?.toLowerCase() + + const chainMatch = getNativeCurrencyNames(enabledChains).find((currency) => + currency.name.startsWith(maybeChainName ?? ''), + ) + const search = splitSearch?.slice(1).join(' ') + + if (!chainFilter && chainMatch && search) { + setParsedChainFilter(chainMatch.chainId) + setParsedSearchFilter(search) + } else { + setParsedChainFilter(null) + setParsedSearchFilter(null) + } + }, [searchFilter, chainFilter, enabledChains]) + + useEffect(() => { + setChainFilter(chainId) + }, [chainId]) + + const onChangeChainFilter = useCallback( + (newChainFilter: typeof chainFilter) => { + setChainFilter(newChainFilter) + sendAnalyticsEvent(WalletEventName.NetworkFilterSelected, { + chain: newChainFilter ?? 'All', + modal: flowToModalName(flow), + }) + }, + [flow], + ) + + const onClearSearchFilter = useCallback(() => { + setSearchFilter(null) + }, []) + + const onChangeText = useCallback((newSearchFilter: string) => setSearchFilter(newSearchFilter), [setSearchFilter]) + + return { + chainFilter, + parsedChainFilter, + searchFilter, + parsedSearchFilter, + onChangeChainFilter, + onClearSearchFilter, + onChangeText, + } +} + +const getNativeCurrencyNames = (chains: UniverseChainId[]): { chainId: UniverseChainId; name: string }[] => + chains + .map((chainId) => { + return isTestnetChain(chainId) + ? false + : { + chainId, + name: getChainInfo(chainId).nativeCurrency.name.toLowerCase(), + } + }) + .filter(Boolean) as { chainId: UniverseChainId; name: string }[] diff --git a/packages/uniswap/src/components/TokenSelector/hooks/usePopularTokensOptions.ts b/packages/uniswap/src/components/TokenSelector/hooks/usePopularTokensOptions.ts new file mode 100644 index 00000000000..7833ef9f724 --- /dev/null +++ b/packages/uniswap/src/components/TokenSelector/hooks/usePopularTokensOptions.ts @@ -0,0 +1,46 @@ +import { useCallback } from 'react' +import { useCurrencyInfosToTokenOptions } from 'uniswap/src/components/TokenSelector/hooks/useCurrencyInfosToTokenOptions' +import { usePortfolioBalancesForAddressById } from 'uniswap/src/components/TokenSelector/hooks/usePortfolioBalancesForAddressById' +import { TokenOption } from 'uniswap/src/components/TokenSelector/types' +import { GqlResult } from 'uniswap/src/data/types' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { usePopularTokens as usePopularTokensGql } from 'uniswap/src/features/dataApi/topTokens' + +export function usePopularTokensOptions( + address: Address | undefined, + chainFilter: UniverseChainId, +): GqlResult { + const { + data: portfolioBalancesById, + error: portfolioBalancesByIdError, + refetch: portfolioBalancesByIdRefetch, + loading: loadingPorfolioBalancesById, + } = usePortfolioBalancesForAddressById(address) + + const { + data: popularTokens, + error: popularTokensError, + refetch: refetchPopularTokens, + loading: loadingPopularTokens, + } = usePopularTokensGql(chainFilter) + + const popularTokenOptions = useCurrencyInfosToTokenOptions({ + currencyInfos: popularTokens, + portfolioBalancesById, + sortAlphabetically: true, + }) + + const refetch = useCallback(() => { + portfolioBalancesByIdRefetch?.() + refetchPopularTokens?.() + }, [portfolioBalancesByIdRefetch, refetchPopularTokens]) + + const error = (!portfolioBalancesById && portfolioBalancesByIdError) || (!popularTokenOptions && popularTokensError) + + return { + data: popularTokenOptions, + refetch, + error: error || undefined, + loading: loadingPorfolioBalancesById || loadingPopularTokens, + } +} diff --git a/packages/uniswap/src/components/TokenSelector/hooks/usePortfolioBalancesForAddressById.ts b/packages/uniswap/src/components/TokenSelector/hooks/usePortfolioBalancesForAddressById.ts new file mode 100644 index 00000000000..3376de3b2d8 --- /dev/null +++ b/packages/uniswap/src/components/TokenSelector/hooks/usePortfolioBalancesForAddressById.ts @@ -0,0 +1,24 @@ +import { GqlResult } from 'uniswap/src/data/types' +import { usePortfolioBalances } from 'uniswap/src/features/dataApi/balances' +import { PortfolioBalance } from 'uniswap/src/features/dataApi/types' + +export function usePortfolioBalancesForAddressById( + address: Address | undefined, +): GqlResult | undefined> { + const { + data: portfolioBalancesById, + error, + refetch, + loading, + } = usePortfolioBalances({ + address, + fetchPolicy: 'cache-first', // we want to avoid re-renders when token selector is opening + }) + + return { + data: portfolioBalancesById, + error, + refetch, + loading, + } +} diff --git a/packages/uniswap/src/components/TokenSelector/hooks/usePortfolioTokenOptions.ts b/packages/uniswap/src/components/TokenSelector/hooks/usePortfolioTokenOptions.ts new file mode 100644 index 00000000000..2b8b2c110f4 --- /dev/null +++ b/packages/uniswap/src/components/TokenSelector/hooks/usePortfolioTokenOptions.ts @@ -0,0 +1,38 @@ +import { useMemo } from 'react' +import { filter } from 'uniswap/src/components/TokenSelector/filter' +import { usePortfolioBalancesForAddressById } from 'uniswap/src/components/TokenSelector/hooks/usePortfolioBalancesForAddressById' +import { TokenOption } from 'uniswap/src/components/TokenSelector/types' +import { GqlResult } from 'uniswap/src/data/types' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { sortPortfolioBalances, useTokenBalancesGroupedByVisibility } from 'uniswap/src/features/dataApi/balances' + +export function usePortfolioTokenOptions( + address: Address | undefined, + chainFilter: UniverseChainId | null, + searchFilter?: string, +): GqlResult { + const { data: portfolioBalancesById, error, refetch, loading } = usePortfolioBalancesForAddressById(address) + const { isTestnetModeEnabled } = useEnabledChains() + + const { shownTokens } = useTokenBalancesGroupedByVisibility({ + balancesById: portfolioBalancesById, + }) + + const portfolioBalances = useMemo( + () => (shownTokens ? sortPortfolioBalances({ balances: shownTokens, isTestnetModeEnabled }) : undefined), + [shownTokens, isTestnetModeEnabled], + ) + + const filteredPortfolioBalances = useMemo( + () => portfolioBalances && filter(portfolioBalances, chainFilter, searchFilter), + [chainFilter, portfolioBalances, searchFilter], + ) + + return { + data: filteredPortfolioBalances, + error, + refetch, + loading, + } +} diff --git a/packages/uniswap/src/components/TokenSelector/hooks/useRecentlySearchedTokens.ts b/packages/uniswap/src/components/TokenSelector/hooks/useRecentlySearchedTokens.ts new file mode 100644 index 00000000000..1f2f05dae13 --- /dev/null +++ b/packages/uniswap/src/components/TokenSelector/hooks/useRecentlySearchedTokens.ts @@ -0,0 +1,63 @@ +import { useMemo } from 'react' +import { useSelector } from 'react-redux' +import { MAX_RECENT_SEARCH_RESULTS } from 'uniswap/src/components/TokenSelector/constants' +import { currencyInfosToTokenOptions } from 'uniswap/src/components/TokenSelector/hooks/useCurrencyInfosToTokenOptions' +import { TokenOption } from 'uniswap/src/components/TokenSelector/types' +import { SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' +import { buildCurrency, buildCurrencyInfo } from 'uniswap/src/features/dataApi/utils' +import { SearchResultType, TokenSearchResult } from 'uniswap/src/features/search/SearchResult' +import { selectSearchHistory } from 'uniswap/src/features/search/selectSearchHistory' +import { currencyId } from 'uniswap/src/utils/currencyId' + +export function useRecentlySearchedTokens(chainFilter: UniverseChainId | null): TokenOption[] | undefined { + const searchHistory = useSelector(selectSearchHistory) + + return useMemo( + () => + currencyInfosToTokenOptions( + searchHistory + .filter((searchResult): searchResult is TokenSearchResult => searchResult.type === SearchResultType.Token) + .filter((searchResult) => (chainFilter ? searchResult.chainId === chainFilter : true)) + .slice(0, MAX_RECENT_SEARCH_RESULTS) + .map(searchResultToCurrencyInfo), + ), + [chainFilter, searchHistory], + ) +} + +function searchResultToCurrencyInfo({ + chainId, + address, + symbol, + name, + logoUrl, + safetyLevel, + safetyInfo, + feeData, +}: TokenSearchResult): CurrencyInfo | null { + const currency = buildCurrency({ + chainId: chainId as UniverseChainId, + address, + decimals: 0, // this does not matter in a context of CurrencyInfo here, as we do not provide any balance + symbol, + name, + buyFeeBps: feeData?.buyFeeBps, + sellFeeBps: feeData?.sellFeeBps, + }) + + if (!currency) { + return null + } + + return buildCurrencyInfo({ + currency, + currencyId: currencyId(currency), + logoUrl, + safetyLevel: safetyLevel ?? SafetyLevel.StrongWarning, + // defaulting to not spam, as user has searched and chosen this token before + isSpam: false, + safetyInfo, + }) +} diff --git a/packages/uniswap/src/components/TokenSelector/hooks/useTokenSectionsForEmptySearch.tsx b/packages/uniswap/src/components/TokenSelector/hooks/useTokenSectionsForEmptySearch.tsx new file mode 100644 index 00000000000..925e5486a71 --- /dev/null +++ b/packages/uniswap/src/components/TokenSelector/hooks/useTokenSectionsForEmptySearch.tsx @@ -0,0 +1,57 @@ +import { useCallback, useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { useDispatch } from 'react-redux' +import { Text, TouchableArea } from 'ui/src' +import { currencyInfosToTokenOptions } from 'uniswap/src/components/TokenSelector/hooks/useCurrencyInfosToTokenOptions' +import { useRecentlySearchedTokens } from 'uniswap/src/components/TokenSelector/hooks/useRecentlySearchedTokens' +import { TokenOptionSection, TokenSection } from 'uniswap/src/components/TokenSelector/types' +import { useTokenOptionsSection } from 'uniswap/src/components/TokenSelector/utils' +import { GqlResult } from 'uniswap/src/data/types' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { gqlTokenToCurrencyInfo } from 'uniswap/src/features/dataApi/utils' +import { clearSearchHistory } from 'uniswap/src/features/search/searchHistorySlice' +import { usePopularTokens } from 'uniswap/src/features/tokens/hooks' + +function ClearAll({ onPress }: { onPress: () => void }): JSX.Element { + const { t } = useTranslation() + return ( + + + {t('tokens.selector.button.clear')} + + + ) +} + +export function useTokenSectionsForEmptySearch(chainFilter: UniverseChainId | null): GqlResult { + const dispatch = useDispatch() + + const { popularTokens, loading } = usePopularTokens() + + const recentlySearchedTokenOptions = useRecentlySearchedTokens(chainFilter) + + // it's a dependency of useMemo => useCallback + const onPressClearSearchHistory = useCallback((): void => { + dispatch(clearSearchHistory()) + }, [dispatch]) + + const recentSection = useTokenOptionsSection({ + sectionKey: TokenOptionSection.RecentTokens, + tokenOptions: recentlySearchedTokenOptions, + endElement: , + }) + + const popularSection = useTokenOptionsSection({ + sectionKey: TokenOptionSection.PopularTokens, + tokenOptions: currencyInfosToTokenOptions(popularTokens?.map(gqlTokenToCurrencyInfo)), + }) + const sections = useMemo(() => [...(recentSection ?? []), ...(popularSection ?? [])], [popularSection, recentSection]) + + return useMemo( + () => ({ + data: sections, + loading, + }), + [loading, sections], + ) +} diff --git a/packages/uniswap/src/components/TokenSelector/hooks/useTokenSectionsForSearchResults.ts b/packages/uniswap/src/components/TokenSelector/hooks/useTokenSectionsForSearchResults.ts new file mode 100644 index 00000000000..fa92072f5a9 --- /dev/null +++ b/packages/uniswap/src/components/TokenSelector/hooks/useTokenSectionsForSearchResults.ts @@ -0,0 +1,107 @@ +import { useCallback, useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { usePortfolioBalancesForAddressById } from 'uniswap/src/components/TokenSelector/hooks/usePortfolioBalancesForAddressById' +import { usePortfolioTokenOptions } from 'uniswap/src/components/TokenSelector/hooks/usePortfolioTokenOptions' +import { TokenOptionSection, TokenSection } from 'uniswap/src/components/TokenSelector/types' +import { + formatSearchResults, + mergeSearchResultsWithBridgingTokens, + useTokenOptionsSection, +} from 'uniswap/src/components/TokenSelector/utils' +import { GqlResult } from 'uniswap/src/data/types' +import { TradeableAsset } from 'uniswap/src/entities/assets' +import { useBridgingTokensOptions } from 'uniswap/src/features/bridging/hooks/tokens' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { getChainLabel } from 'uniswap/src/features/chains/utils' +import { useSearchTokens } from 'uniswap/src/features/dataApi/searchTokens' + +export function useTokenSectionsForSearchResults( + address: string | undefined, + chainFilter: UniverseChainId | null, + searchFilter: string | null, + isBalancesOnlySearch: boolean, + input: TradeableAsset | undefined, +): GqlResult { + const { t } = useTranslation() + + const { + data: portfolioBalancesById, + error: portfolioBalancesByIdError, + refetch: refetchPortfolioBalances, + loading: portfolioBalancesByIdLoading, + } = usePortfolioBalancesForAddressById(address) + + const { + data: portfolioTokenOptions, + error: portfolioTokenOptionsError, + refetch: refetchPortfolioTokenOptions, + loading: portfolioTokenOptionsLoading, + } = usePortfolioTokenOptions(address, chainFilter, searchFilter ?? undefined) + + // Bridging tokens are only shown if input is provided + const { + data: bridgingTokenOptions, + error: bridgingTokenOptionsError, + refetch: refetchBridgingTokenOptions, + loading: bridgingTokenOptionsLoading, + } = useBridgingTokensOptions({ input, walletAddress: address, chainFilter }) + + // Only call search endpoint if isBalancesOnlySearch is false + const { + data: searchResultCurrencies, + error: searchTokensError, + refetch: refetchSearchTokens, + loading: searchTokensLoading, + } = useSearchTokens(searchFilter, chainFilter, /*skip*/ isBalancesOnlySearch) + + const searchResults = useMemo(() => { + return formatSearchResults(searchResultCurrencies, portfolioBalancesById, searchFilter) + }, [searchResultCurrencies, portfolioBalancesById, searchFilter]) + + const loading = + portfolioTokenOptionsLoading || + portfolioBalancesByIdLoading || + (!isBalancesOnlySearch && searchTokensLoading) || + bridgingTokenOptionsLoading + + const searchResultsSections = useTokenOptionsSection({ + sectionKey: TokenOptionSection.SearchResults, + // Use local search when only searching balances + tokenOptions: isBalancesOnlySearch ? portfolioTokenOptions : searchResults, + }) + + // If there are bridging options, we need to extract them from the search results and then prepend them as a new section above. + // The remaining non-bridging search results will be shown in a section with a different name + const networkName = chainFilter ? getChainLabel(chainFilter) : undefined + const searchResultsSectionHeader = networkName + ? t('tokens.selector.section.otherSearchResults', { network: networkName }) + : undefined + const sections = mergeSearchResultsWithBridgingTokens( + searchResultsSections, + bridgingTokenOptions, + searchResultsSectionHeader, + ) + + const error = + (!bridgingTokenOptions && bridgingTokenOptionsError) || + (!portfolioBalancesById && portfolioBalancesByIdError) || + (!portfolioTokenOptions && portfolioTokenOptionsError) || + (!isBalancesOnlySearch && !searchResults && searchTokensError) + + const refetchAll = useCallback(() => { + refetchPortfolioBalances?.() + refetchSearchTokens?.() + refetchPortfolioTokenOptions?.() + refetchBridgingTokenOptions?.() + }, [refetchBridgingTokenOptions, refetchPortfolioBalances, refetchPortfolioTokenOptions, refetchSearchTokens]) + + return useMemo( + () => ({ + data: sections, + loading, + error: error || undefined, + refetch: refetchAll, + }), + [error, loading, refetchAll, sections], + ) +} diff --git a/packages/uniswap/src/components/TokenSelector/SuggestedToken.tsx b/packages/uniswap/src/components/TokenSelector/items/SuggestedToken.tsx similarity index 95% rename from packages/uniswap/src/components/TokenSelector/SuggestedToken.tsx rename to packages/uniswap/src/components/TokenSelector/items/SuggestedToken.tsx index b4b8204c149..0e1165e88cf 100644 --- a/packages/uniswap/src/components/TokenSelector/SuggestedToken.tsx +++ b/packages/uniswap/src/components/TokenSelector/items/SuggestedToken.tsx @@ -6,7 +6,7 @@ import { Pill } from 'uniswap/src/components/pill/Pill' import { OnSelectCurrency, TokenOption, TokenSection } from 'uniswap/src/components/TokenSelector/types' import { getSymbolDisplayText } from 'uniswap/src/utils/currency' -function _SuggestedToken({ +function _TokenPill({ onSelectCurrency, token, index, @@ -58,4 +58,4 @@ function _SuggestedToken({ ) } -export const SuggestedToken = memo(_SuggestedToken) +export const TokenPill = memo(_TokenPill) diff --git a/packages/uniswap/src/components/TokenSelector/TokenCard.tsx b/packages/uniswap/src/components/TokenSelector/items/TokenCard.tsx similarity index 100% rename from packages/uniswap/src/components/TokenSelector/TokenCard.tsx rename to packages/uniswap/src/components/TokenSelector/items/TokenCard.tsx diff --git a/packages/uniswap/src/components/TokenSelector/TokenOptionItem.tsx b/packages/uniswap/src/components/TokenSelector/items/TokenOptionItem.tsx similarity index 90% rename from packages/uniswap/src/components/TokenSelector/TokenOptionItem.tsx rename to packages/uniswap/src/components/TokenSelector/items/TokenOptionItem.tsx index e3f04ba356a..8859e377616 100644 --- a/packages/uniswap/src/components/TokenSelector/TokenOptionItem.tsx +++ b/packages/uniswap/src/components/TokenSelector/items/TokenOptionItem.tsx @@ -6,7 +6,7 @@ import { TokenLogo } from 'uniswap/src/components/CurrencyLogo/TokenLogo' import { TokenOption } from 'uniswap/src/components/TokenSelector/types' import { WarningSeverity } from 'uniswap/src/components/modals/WarningModal/types' import WarningIcon from 'uniswap/src/components/warnings/WarningIcon' -import { getWarningIconColorOverride } from 'uniswap/src/components/warnings/utils' +import { getWarningIconColors } from 'uniswap/src/components/warnings/utils' import { SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { CurrencyInfo, TokenList } from 'uniswap/src/features/dataApi/types' import { FeatureFlags } from 'uniswap/src/features/gating/flags' @@ -24,7 +24,6 @@ interface OptionProps { onPress: () => void showTokenAddress?: boolean tokenWarningDismissed: boolean - dismissWarningCallback: () => void quantity: number | null // TODO(WEB-4731): Remove isKeyboardOpen dependency isKeyboardOpen?: boolean @@ -36,15 +35,12 @@ interface OptionProps { } function getTokenWarningDetails(currencyInfo: CurrencyInfo): { - severity: WarningSeverity | undefined - isWarningSevere: boolean + severity: WarningSeverity isNonDefaultList: boolean isBlocked: boolean } { const { safetyLevel, safetyInfo } = currencyInfo const severity = getTokenWarningSeverity(currencyInfo) - const isWarningSevere = - severity === WarningSeverity.Blocked || severity === WarningSeverity.High || severity === WarningSeverity.Medium const isNonDefaultList = safetyLevel === SafetyLevel.MediumWarning || safetyLevel === SafetyLevel.StrongWarning || @@ -52,7 +48,6 @@ function getTokenWarningDetails(currencyInfo: CurrencyInfo): { const isBlocked = severity === WarningSeverity.Blocked || safetyLevel === SafetyLevel.Blocked return { severity, - isWarningSevere, isNonDefaultList, isBlocked, } @@ -64,7 +59,6 @@ function _TokenOptionItem({ onPress, showTokenAddress, tokenWarningDismissed, - dismissWarningCallback, balance, quantity, quantityFormatted, @@ -76,11 +70,12 @@ function _TokenOptionItem({ const [showWarningModal, setShowWarningModal] = useState(false) const tokenProtectionEnabled = useFeatureFlag(FeatureFlags.TokenProtection) - const { severity, isBlocked, isNonDefaultList, isWarningSevere } = getTokenWarningDetails(currencyInfo) - const warningIconColor = getWarningIconColorOverride(severity) + const { severity, isBlocked, isNonDefaultList } = getTokenWarningDetails(currencyInfo) + // in token selector, we only show the warning icon if token is >=Medium severity + const { colorSecondary: warningIconColor } = getWarningIconColors(severity) const shouldShowWarningModalOnPress = !tokenProtectionEnabled ? isBlocked || (isNonDefaultList && !tokenWarningDismissed) - : isWarningSevere && !tokenWarningDismissed + : isBlocked || (severity !== WarningSeverity.None && !tokenWarningDismissed) const handleShowWarningModal = useCallback((): void => { dismissNativeKeyboard() @@ -105,10 +100,9 @@ function _TokenOptionItem({ }, [showWarnings, shouldShowWarningModalOnPress, onPress, isKeyboardOpen, handleShowWarningModal]) const onAcceptTokenWarning = useCallback(() => { - dismissWarningCallback() setShowWarningModal(false) onPress() - }, [dismissWarningCallback, onPress]) + }, [onPress]) return ( <> diff --git a/packages/uniswap/src/components/TokenSelector/TokenSectionHeader.tsx b/packages/uniswap/src/components/TokenSelector/items/TokenSectionHeader.tsx similarity index 100% rename from packages/uniswap/src/components/TokenSelector/TokenSectionHeader.tsx rename to packages/uniswap/src/components/TokenSelector/items/TokenSectionHeader.tsx diff --git a/packages/uniswap/src/components/TokenSelector/HorizontalTokenList/HorizontalTokenList.native.tsx b/packages/uniswap/src/components/TokenSelector/lists/HorizontalTokenList/HorizontalTokenList.native.tsx similarity index 59% rename from packages/uniswap/src/components/TokenSelector/HorizontalTokenList/HorizontalTokenList.native.tsx rename to packages/uniswap/src/components/TokenSelector/lists/HorizontalTokenList/HorizontalTokenList.native.tsx index 3d7a6d3b357..e8fbea14027 100644 --- a/packages/uniswap/src/components/TokenSelector/HorizontalTokenList/HorizontalTokenList.native.tsx +++ b/packages/uniswap/src/components/TokenSelector/lists/HorizontalTokenList/HorizontalTokenList.native.tsx @@ -2,10 +2,8 @@ import { memo, useCallback } from 'react' import { FlatList } from 'react-native-gesture-handler' import { Flex } from 'ui/src' import { spacing } from 'ui/src/theme' -import { HorizontalTokenListProps } from 'uniswap/src/components/TokenSelector/HorizontalTokenList/HorizontalTokenList' -import { SuggestedToken } from 'uniswap/src/components/TokenSelector/SuggestedToken' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' +import { TokenPill } from 'uniswap/src/components/TokenSelector/items/SuggestedToken' +import { HorizontalTokenListProps } from 'uniswap/src/components/TokenSelector/lists/HorizontalTokenList/HorizontalTokenList' export const HorizontalTokenList = memo(function _HorizontalTokenList({ tokens: suggestedTokens, @@ -13,26 +11,8 @@ export const HorizontalTokenList = memo(function _HorizontalTokenList({ index, section, }: HorizontalTokenListProps): JSX.Element { - const isBridgingEnabled = useFeatureFlag(FeatureFlags.Bridging) - const itemSeparatorComponent = useCallback(() => , []) - if (!isBridgingEnabled) { - return ( - - {suggestedTokens.map((token) => ( - - ))} - - ) - } - return ( token.currencyInfo.currencyId} ItemSeparatorComponent={itemSeparatorComponent} renderItem={({ item: token }) => ( - { - const isBridgingEnabled = useFeatureFlag(FeatureFlags.Bridging) const { defaultChainId, isTestnetModeEnabled } = useEnabledChains() const { @@ -169,7 +164,7 @@ function useTokenSectionsForSwapOutput({ return [ ...(suggestedSection ?? []), - ...(isBridgingEnabled ? bridgingSection ?? [] : []), + ...(bridgingSection ?? []), ...(portfolioSection ?? []), ...(recentSection ?? []), // TODO(WEB-3061): Favorited wallets/tokens @@ -182,7 +177,6 @@ function useTokenSectionsForSwapOutput({ portfolioSection, popularSection, suggestedSection, - isBridgingEnabled, bridgingSection, recentSection, favoriteSection, diff --git a/packages/uniswap/src/components/TokenSelector/types.ts b/packages/uniswap/src/components/TokenSelector/types.ts index 7c5e4860b7e..2adcc416b6a 100644 --- a/packages/uniswap/src/components/TokenSelector/types.ts +++ b/packages/uniswap/src/components/TokenSelector/types.ts @@ -1,6 +1,6 @@ import { TradeableAsset } from 'uniswap/src/entities/assets' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' -import { UniverseChainId } from 'uniswap/src/types/chains' import { FiatNumberType } from 'utilities/src/format/types' export type TokenOption = { diff --git a/packages/uniswap/src/components/TokenSelector/utils.tsx b/packages/uniswap/src/components/TokenSelector/utils.tsx index 391d92d482a..75961041790 100644 --- a/packages/uniswap/src/components/TokenSelector/utils.tsx +++ b/packages/uniswap/src/components/TokenSelector/utils.tsx @@ -1,9 +1,15 @@ import { useMemo } from 'react' -import { TokenOption, TokenOptionSection, TokenSection } from 'uniswap/src/components/TokenSelector/types' +import { + TokenOption, + TokenOptionSection, + TokenSection, + TokenSelectorFlow, +} from 'uniswap/src/components/TokenSelector/types' import { tradingApiSwappableTokenToCurrencyInfo } from 'uniswap/src/data/apiClients/tradingApi/utils/tradingApiSwappableTokenToCurrencyInfo' import { SafetyLevel as GqlSafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { GetSwappableTokensResponse, SafetyLevel } from 'uniswap/src/data/tradingApi/__generated__' import { CurrencyInfo, PortfolioBalance } from 'uniswap/src/features/dataApi/types' +import { ModalName, ModalNameType } from 'uniswap/src/features/telemetry/constants' import { areCurrencyIdsEqual } from 'uniswap/src/utils/currencyId' import { differenceWith } from 'utilities/src/primitives/array' @@ -210,3 +216,14 @@ export function isSwapListLoading( ): boolean { return loading && (!portfolioSection || !popularSection) } + +export function flowToModalName(flow: TokenSelectorFlow): ModalNameType | undefined { + switch (flow) { + case TokenSelectorFlow.Swap: + return ModalName.Swap + case TokenSelectorFlow.Send: + return ModalName.Send + default: + return undefined + } +} diff --git a/packages/uniswap/src/components/banners/TestnetModeBanner.tsx b/packages/uniswap/src/components/banners/TestnetModeBanner.tsx index c567effd69c..4139a7b9f39 100644 --- a/packages/uniswap/src/components/banners/TestnetModeBanner.tsx +++ b/packages/uniswap/src/components/banners/TestnetModeBanner.tsx @@ -3,7 +3,9 @@ import { Flex, FlexProps, Text, isWeb } from 'ui/src' import { Wrench } from 'ui/src/components/icons/Wrench' // eslint-disable-next-line no-restricted-imports import { useDeviceInsets } from 'ui/src/hooks/useDeviceInsets' -import { TESTNET_MODE_BANNER_HEIGHT, useEnabledChains } from 'uniswap/src/features/settings/hooks' +import { zIndices } from 'ui/src/theme' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { TESTNET_MODE_BANNER_HEIGHT } from 'uniswap/src/features/settings/hooks' import { isInterface, isMobileApp } from 'utilities/src/platform' export function TestnetModeBanner(props: FlexProps): JSX.Element | null { @@ -22,7 +24,7 @@ export function TestnetModeBanner(props: FlexProps): JSX.Element | null { centered top={top} position={isMobileApp ? 'absolute' : 'relative'} - zIndex="$sticky" + zIndex={zIndices.fixed} width={isInterface ? 'auto' : '100%'} p="$padding12" gap="$gap8" diff --git a/packages/uniswap/src/components/dropdowns/ActionSheetDropdown.tsx b/packages/uniswap/src/components/dropdowns/ActionSheetDropdown.tsx index b40195649ed..02d751d6893 100644 --- a/packages/uniswap/src/components/dropdowns/ActionSheetDropdown.tsx +++ b/packages/uniswap/src/components/dropdowns/ActionSheetDropdown.tsx @@ -20,7 +20,7 @@ import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard' import { Scrollbar } from 'uniswap/src/components/misc/Scrollbar' import { MenuItemProp } from 'uniswap/src/components/modals/ActionSheetModal' import { useAppInsets } from 'uniswap/src/hooks/useAppInsets' -import { isAndroid, isInterface, isTouchable } from 'utilities/src/platform' +import { isAndroid, isInterface, isMobileApp, isTouchable } from 'utilities/src/platform' const DEFAULT_MIN_WIDTH = 225 @@ -178,11 +178,15 @@ const ActionSheetBackdropWithContent = memo(function ActionSheetBackdropWithCont closeOnSelect: boolean }): JSX.Element { /* - We need to add key to Portal, becuase of a bug in tamagui. + We need to add key to Portal on mobile, becuase of a bug in tamagui. Remove when https://linear.app/uniswap/issue/WALL-4817/tamaguis-portal-stops-reacting-to-re-renders is done */ + const key = useMemo( + () => (isMobileApp ? Math.random() : undefined), // eslint-disable-next-line react-hooks/exhaustive-deps + [closeDropdown, styles, isOpen, toggleMeasurements, contentProps, closeOnSelect], + ) return ( - + {isOpen && toggleMeasurements && ( <> diff --git a/packages/uniswap/src/components/gas/NetworkFee.test.tsx b/packages/uniswap/src/components/gas/NetworkFee.test.tsx index f3b4e55f13d..1835f62b16c 100644 --- a/packages/uniswap/src/components/gas/NetworkFee.test.tsx +++ b/packages/uniswap/src/components/gas/NetworkFee.test.tsx @@ -1,6 +1,6 @@ import { NetworkFee } from 'uniswap/src/components/gas/NetworkFee' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { render } from 'uniswap/src/test/test-utils' -import { UniverseChainId } from 'uniswap/src/types/chains' jest.mock('uniswap/src/features/gas/hooks', () => { return { diff --git a/packages/uniswap/src/components/gas/NetworkFee.tsx b/packages/uniswap/src/components/gas/NetworkFee.tsx index 9a545bd26a6..1de2bca5c0a 100644 --- a/packages/uniswap/src/components/gas/NetworkFee.tsx +++ b/packages/uniswap/src/components/gas/NetworkFee.tsx @@ -5,6 +5,7 @@ import { UniswapX } from 'ui/src/components/icons/UniswapX' import { iconSizes } from 'ui/src/theme' import { NetworkLogo } from 'uniswap/src/components/CurrencyLogo/NetworkLogo' import { IndicativeLoadingWrapper } from 'uniswap/src/components/misc/IndicativeLoadingWrapper' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { useFormattedUniswapXGasFeeInfo, useGasFeeFormattedAmounts, @@ -13,7 +14,6 @@ import { import { GasFeeResult } from 'uniswap/src/features/gas/types' import { NetworkFeeWarning } from 'uniswap/src/features/transactions/swap/modals/NetworkFeeWarning' import { UniswapXGasBreakdown } from 'uniswap/src/features/transactions/swap/types/swapTxAndGasInfo' -import { UniverseChainId } from 'uniswap/src/types/chains' import { isInterface } from 'utilities/src/platform' export function NetworkFee({ diff --git a/packages/uniswap/src/components/modals/Modal.native.tsx b/packages/uniswap/src/components/modals/Modal.native.tsx index 6baae6d9731..5506a8300a7 100644 --- a/packages/uniswap/src/components/modals/Modal.native.tsx +++ b/packages/uniswap/src/components/modals/Modal.native.tsx @@ -13,7 +13,7 @@ import { BackHandler, StyleProp, StyleSheet, ViewStyle } from 'react-native' import Animated, { Extrapolate, interpolate, useAnimatedStyle, useSharedValue } from 'react-native-reanimated' import { Flex, useIsDarkMode, useMedia, useSporeColors } from 'ui/src' import { useDeviceDimensions } from 'ui/src/hooks/useDeviceDimensions' -import { borderRadii, spacing } from 'ui/src/theme' +import { borderRadii, spacing, zIndices } from 'ui/src/theme' import { BottomSheetContextProvider } from 'uniswap/src/components/modals/BottomSheetContext' import { HandleBar } from 'uniswap/src/components/modals/HandleBar' import { ModalProps } from 'uniswap/src/components/modals/ModalProps' @@ -93,6 +93,7 @@ function BottomSheetModalContents({ // probably it requires usage of extendOnKeyboardVisible = false, hideScrim = false, + isBehindFixedBanners = false, }: ModalProps): JSX.Element { const dimensions = useDeviceDimensions() const insets = useAppInsets() @@ -132,13 +133,14 @@ function BottomSheetModalContents({ (props: BottomSheetBackdropProps) => ( ), - [blurredBackground, hideScrim, isDismissible], + [blurredBackground, hideScrim, isDismissible, isBehindFixedBanners], ) const renderHandleBar = useCallback( @@ -251,6 +253,7 @@ function BottomSheetModalContents({ {...background} {...backdrop} ref={modalRef} + containerStyle={[!isBehindFixedBanners && { zIndex: zIndices.modal }]} animatedPosition={animatedPosition} backgroundStyle={backgroundStyle} containerComponent={containerComponent} diff --git a/packages/uniswap/src/components/modals/ModalProps.tsx b/packages/uniswap/src/components/modals/ModalProps.tsx index a821aedaae6..42251b15302 100644 --- a/packages/uniswap/src/components/modals/ModalProps.tsx +++ b/packages/uniswap/src/components/modals/ModalProps.tsx @@ -28,6 +28,8 @@ export type ModalProps = PropsWithChildren<{ extendOnKeyboardVisible?: boolean // defaults to `true` isModalOpen?: boolean + // created to allow testnet mode banner to be displayed on mobile + isBehindFixedBanners?: boolean // TODO MOB-2526 refactor Modal to more platform-agnostic alignment?: 'center' | 'top' diff --git a/packages/uniswap/src/components/modals/WarningModal/getAlertColor.ts b/packages/uniswap/src/components/modals/WarningModal/getAlertColor.ts index 7af2208e529..d451c544405 100644 --- a/packages/uniswap/src/components/modals/WarningModal/getAlertColor.ts +++ b/packages/uniswap/src/components/modals/WarningModal/getAlertColor.ts @@ -5,36 +5,42 @@ export function getAlertColor(severity?: WarningSeverity): WarningColor { case WarningSeverity.None: return { text: '$neutral2', + headerText: '$neutral1', background: '$neutral2', buttonTheme: 'secondary', } case WarningSeverity.Low: return { text: '$neutral2', + headerText: '$neutral1', background: '$surface2', buttonTheme: 'tertiary', } case WarningSeverity.High: return { text: '$statusCritical', + headerText: '$statusCritical', background: '$DEP_accentCriticalSoft', buttonTheme: 'detrimental', } case WarningSeverity.Medium: return { text: '$DEP_accentWarning', + headerText: '$DEP_accentWarning', background: '$DEP_accentWarningSoft', buttonTheme: 'warning', } case WarningSeverity.Blocked: return { text: '$neutral1', + headerText: '$neutral1', background: '$surface3', buttonTheme: 'secondary', } default: return { text: '$neutral2', + headerText: '$neutral1', background: '$transparent', buttonTheme: 'tertiary', } diff --git a/packages/uniswap/src/components/modals/WarningModal/types.ts b/packages/uniswap/src/components/modals/WarningModal/types.ts index 79c0419eae9..8569acc8c2a 100644 --- a/packages/uniswap/src/components/modals/WarningModal/types.ts +++ b/packages/uniswap/src/components/modals/WarningModal/types.ts @@ -12,6 +12,7 @@ export enum WarningSeverity { export type WarningColor = { text: ColorTokens + headerText: ColorTokens background: ColorTokens buttonTheme: ThemeNames } diff --git a/packages/uniswap/src/components/network/NetworkFilter.test.tsx b/packages/uniswap/src/components/network/NetworkFilter.test.tsx index a3b04333504..ef83f21c460 100644 --- a/packages/uniswap/src/components/network/NetworkFilter.test.tsx +++ b/packages/uniswap/src/components/network/NetworkFilter.test.tsx @@ -2,7 +2,7 @@ import { NetworkFilter } from 'uniswap/src/components/network/NetworkFilter' import { render } from 'uniswap/src/test/test-utils' import ReactDOM from 'react-dom' -import { SUPPORTED_CHAIN_IDS } from 'uniswap/src/types/chains' +import { SUPPORTED_CHAIN_IDS } from 'uniswap/src/features/chains/types' ReactDOM.createPortal = jest.fn((element) => { return element as React.ReactPortal diff --git a/packages/uniswap/src/components/network/NetworkFilter.tsx b/packages/uniswap/src/components/network/NetworkFilter.tsx index 9010d2789c9..a43996f703d 100644 --- a/packages/uniswap/src/components/network/NetworkFilter.tsx +++ b/packages/uniswap/src/components/network/NetworkFilter.tsx @@ -10,8 +10,8 @@ import { ActionSheetDropdownStyleProps, } from 'uniswap/src/components/dropdowns/ActionSheetDropdown' import { useNetworkOptions } from 'uniswap/src/components/network/hooks' -import { useEnabledChains } from 'uniswap/src/features/settings/hooks' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { isMobileApp } from 'utilities/src/platform' const ELLIPSIS = 'ellipsis' diff --git a/packages/uniswap/src/components/network/NetworkLogos.tsx b/packages/uniswap/src/components/network/NetworkLogos.tsx index 1ee55ab5bfc..fce9631cf56 100644 --- a/packages/uniswap/src/components/network/NetworkLogos.tsx +++ b/packages/uniswap/src/components/network/NetworkLogos.tsx @@ -16,10 +16,10 @@ import { X } from 'ui/src/components/icons/X' import { borderRadii, iconSizes, zIndices } from 'ui/src/theme' import { Modal } from 'uniswap/src/components/modals/Modal' import { LearnMoreLink } from 'uniswap/src/components/text/LearnMoreLink' -import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' import { uniswapUrls } from 'uniswap/src/constants/urls' +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { ModalName } from 'uniswap/src/features/telemetry/constants' -import { UniverseChainId } from 'uniswap/src/types/chains' import { isInterface } from 'utilities/src/platform' export type NetworkLogosProps = { @@ -38,7 +38,7 @@ export function NetworkLogos({ chains }: NetworkLogosProps): JSX.Element { () => ( {chains.map((chain) => { - const { label, logo } = UNIVERSE_CHAIN_INFO[chain] + const { label, logo } = getChainInfo(chain) return ( { it('renders a NetworkPill without image', () => { diff --git a/packages/uniswap/src/components/network/NetworkPill.tsx b/packages/uniswap/src/components/network/NetworkPill.tsx index f3662eb8653..347a0e5cbcf 100644 --- a/packages/uniswap/src/components/network/NetworkPill.tsx +++ b/packages/uniswap/src/components/network/NetworkPill.tsx @@ -2,8 +2,8 @@ import { ComponentProps } from 'react' import { iconSizes } from 'ui/src/theme' import { NetworkLogo } from 'uniswap/src/components/CurrencyLogo/NetworkLogo' import { Pill } from 'uniswap/src/components/pill/Pill' -import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { getChainLabel } from 'uniswap/src/features/chains/utils' import { useNetworkColors } from 'uniswap/src/utils/colors' export type NetworkPillProps = { @@ -22,7 +22,7 @@ export function NetworkPill({ iconSize = iconSizes.icon16, ...rest }: NetworkPillProps): JSX.Element { - const info = UNIVERSE_CHAIN_INFO[chainId] + const label = getChainLabel(chainId) const colors = useNetworkColors(chainId) return ( @@ -31,7 +31,7 @@ export function NetworkPill({ customBorderColor={showBorder ? colors.foreground : 'transparent'} foregroundColor={colors.foreground} icon={showIcon ? : null} - label={info.label} + label={label} {...rest} /> ) diff --git a/packages/uniswap/src/components/network/hooks.tsx b/packages/uniswap/src/components/network/hooks.tsx index f5ef96af578..e767db97dde 100644 --- a/packages/uniswap/src/components/network/hooks.tsx +++ b/packages/uniswap/src/components/network/hooks.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react' import { NetworkOption } from 'uniswap/src/components/network/NetworkOption' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { ElementName } from 'uniswap/src/features/telemetry/constants' -import { UniverseChainId } from 'uniswap/src/types/chains' export function useNetworkOptions({ onPress, diff --git a/packages/uniswap/src/components/text/LearnMoreLink.tsx b/packages/uniswap/src/components/text/LearnMoreLink.tsx index 38cba388e2e..840f7bfcc45 100644 --- a/packages/uniswap/src/components/text/LearnMoreLink.tsx +++ b/packages/uniswap/src/components/text/LearnMoreLink.tsx @@ -10,15 +10,17 @@ export const LearnMoreLink = ({ url, textVariant = 'buttonLabel2', textColor = '$accent1', + centered = false, }: { url: string textVariant?: TextProps['variant'] textColor?: TextProps['color'] + centered?: boolean }): JSX.Element => { const { t } = useTranslation() return ( => onPressLearnMore(url)}> - + {t('common.button.learn')} diff --git a/packages/uniswap/src/components/warnings/WarningIcon.tsx b/packages/uniswap/src/components/warnings/WarningIcon.tsx index 248c274dcbd..5a0483243ee 100644 --- a/packages/uniswap/src/components/warnings/WarningIcon.tsx +++ b/packages/uniswap/src/components/warnings/WarningIcon.tsx @@ -11,6 +11,7 @@ import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' interface Props { // TODO (WALL-4626): remove SafetyLevel entirely + /** @deprecated use severity instead */ safetyLevel?: Maybe severity?: WarningSeverity // To override the normally associated safetyLevel<->color mapping @@ -30,7 +31,7 @@ export default function WarningIcon({ const { color: defaultIconColor, backgroundColor } = getWarningIconColors(severityToUse) const color = strokeColorOverride ?? defaultIconColor const Icon = getWarningIcon(severityToUse, tokenProtectionEnabled) - const icon = + const icon = Icon ? : null return heroIcon ? ( {icon} diff --git a/packages/uniswap/src/components/warnings/utils.ts b/packages/uniswap/src/components/warnings/utils.ts index b38d30e183e..24b1668d54a 100644 --- a/packages/uniswap/src/components/warnings/utils.ts +++ b/packages/uniswap/src/components/warnings/utils.ts @@ -21,7 +21,11 @@ export function safetyLevelToWarningSeverity(safetyLevel: Maybe): W } } -export function getWarningIcon(severity?: WarningSeverity, tokenProtectionEnabled: boolean = false): GeneratedIcon { +// eslint-disable-next-line consistent-return +export function getWarningIcon( + severity: WarningSeverity, + tokenProtectionEnabled: boolean = false, +): GeneratedIcon | null { switch (severity) { case WarningSeverity.High: return tokenProtectionEnabled ? OctagonExclamation : AlertTriangleFilled @@ -30,15 +34,16 @@ export function getWarningIcon(severity?: WarningSeverity, tokenProtectionEnable case WarningSeverity.Blocked: return Blocked case WarningSeverity.Low: - case WarningSeverity.None: return InfoCircleFilled - default: - return AlertTriangleFilled + case WarningSeverity.None: + return null } } export function getWarningIconColors(severity?: WarningSeverity): { color: ColorTokens + /** `colorSecondary` used instead of `color` in certain places, such as token selector & mobile search */ + colorSecondary: ColorTokens | undefined backgroundColor: ColorTokens textColor: ColorTokens } { @@ -46,21 +51,30 @@ export function getWarningIconColors(severity?: WarningSeverity): { case WarningSeverity.High: return { color: '$statusCritical', + colorSecondary: '$statusCritical', backgroundColor: '$DEP_accentCriticalSoft', textColor: '$statusCritical', } case WarningSeverity.Medium: return { color: '$DEP_accentWarning', + colorSecondary: '$neutral2', backgroundColor: '$DEP_accentWarningSoft', textColor: '$DEP_accentWarning', } case WarningSeverity.Blocked: + return { + color: '$neutral2', + colorSecondary: '$neutral2', + backgroundColor: '$surface3', + textColor: '$neutral1', + } case WarningSeverity.Low: case WarningSeverity.None: default: return { color: '$neutral2', + colorSecondary: undefined, backgroundColor: '$surface3', textColor: '$neutral1', } @@ -85,17 +99,3 @@ export function getWarningButtonProps(severity?: WarningSeverity): { theme: Them } } } - -export function getWarningIconColorOverride(severity?: WarningSeverity): ColorTokens | undefined { - switch (severity) { - case WarningSeverity.High: - return '$statusCritical' - case WarningSeverity.Medium: - case WarningSeverity.Blocked: - return '$neutral2' - case WarningSeverity.Low: - case WarningSeverity.None: - default: - return undefined - } -} diff --git a/packages/uniswap/src/constants/addresses.ts b/packages/uniswap/src/constants/addresses.ts index aa1ed39412e..2726043abee 100644 --- a/packages/uniswap/src/constants/addresses.ts +++ b/packages/uniswap/src/constants/addresses.ts @@ -1,5 +1,5 @@ -import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' +import { UniverseChainId } from 'uniswap/src/features/chains/types' const POL_MAINNET_ADDRESS = '0x455e53cbb86018ac2b8092fdcd39d8444affc3f6' const MATIC_MAINNET_ADDRESS = '0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0' @@ -24,9 +24,9 @@ export const BRIDGED_BASE_ADDRESSES = [ ] export function getNativeAddress(chainId: UniverseChainId): string { - return UNIVERSE_CHAIN_INFO[chainId].nativeCurrency.address + return getChainInfo(chainId).nativeCurrency.address } export function getWrappedNativeAddress(chainId: UniverseChainId): string { - return UNIVERSE_CHAIN_INFO[chainId].wrappedNativeCurrency.address + return getChainInfo(chainId).wrappedNativeCurrency.address } diff --git a/packages/uniswap/src/constants/routing.ts b/packages/uniswap/src/constants/routing.ts index ee46a590468..2b7b5227106 100644 --- a/packages/uniswap/src/constants/routing.ts +++ b/packages/uniswap/src/constants/routing.ts @@ -2,7 +2,6 @@ import { Currency, Token, WETH9 } from '@uniswap/sdk-core' // eslint-disable-next-line no-restricted-imports import type { ImageSourcePropType } from 'react-native' import { CELO_LOGO, ETH_LOGO } from 'ui/src/assets' -import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' import { ARB, BTC_BSC, @@ -49,9 +48,10 @@ import { nativeOnChain, } from 'uniswap/src/constants/tokens' import { SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' import { buildCurrencyInfo } from 'uniswap/src/features/dataApi/utils' -import { UniverseChainId } from 'uniswap/src/types/chains' import { isSameAddress } from 'utilities/src/addresses' type ChainCurrencyList = { @@ -176,11 +176,11 @@ function getNativeLogoURI(chainId: UniverseChainId = UniverseChainId.Mainnet): I return ETH_LOGO as ImageSourcePropType } - return UNIVERSE_CHAIN_INFO[chainId].nativeCurrency.logo ?? (ETH_LOGO as ImageSourcePropType) + return getChainInfo(chainId).nativeCurrency.logo ?? (ETH_LOGO as ImageSourcePropType) } function getTokenLogoURI(chainId: UniverseChainId, address: string): ImageSourcePropType | string | undefined { - const chainInfo = UNIVERSE_CHAIN_INFO[chainId] + const chainInfo = getChainInfo(chainId) const networkName = chainInfo?.assetRepoNetworkName if (isCelo(chainId) && isSameAddress(address, nativeOnChain(chainId).wrapped.address)) { diff --git a/packages/uniswap/src/constants/tokens.ts b/packages/uniswap/src/constants/tokens.ts index 51615dbb170..d524d1db346 100644 --- a/packages/uniswap/src/constants/tokens.ts +++ b/packages/uniswap/src/constants/tokens.ts @@ -1,7 +1,7 @@ /* eslint-disable max-lines */ import { Currency, NativeCurrency, Token, UNI_ADDRESSES, WETH9 } from '@uniswap/sdk-core' import invariant from 'tiny-invariant' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' export const USDC_SEPOLIA = new Token( UniverseChainId.Sepolia, diff --git a/packages/uniswap/src/constants/urls.ts b/packages/uniswap/src/constants/urls.ts index 266bfc80522..d3a6d0f0d36 100644 --- a/packages/uniswap/src/constants/urls.ts +++ b/packages/uniswap/src/constants/urls.ts @@ -83,7 +83,8 @@ export const uniswapUrls = { // Feature service URL's unitagsApiUrl: `${getCloudflareApiBaseUrl(TrafficFlows.Unitags)}/v2/unitags`, scantasticApiUrl: `${getCloudflareApiBaseUrl(TrafficFlows.Scantastic)}/v2/scantastic`, - fiatOnRampApiUrl: `${getCloudflareApiBaseUrl(TrafficFlows.FOR)}/v2/fiat-on-ramp`, + fiatOnRampApiUrl: `${getCloudflareApiBaseUrl(TrafficFlows.FOR)}/v2/fiat-on-ramp`, // TODO: WALL-5189 - remove this once we finish migrating away from original FOR endpoint service + forApiUrl: `${getCloudflareApiBaseUrl(TrafficFlows.FOR)}/v2/FOR.v1.FORService`, tradingApiUrl: getCloudflareApiBaseUrl(TrafficFlows.TradingApi), // API Paths diff --git a/packages/uniswap/src/contexts/UniswapContext.tsx b/packages/uniswap/src/contexts/UniswapContext.tsx index 3485d243cfa..6780480d904 100644 --- a/packages/uniswap/src/contexts/UniswapContext.tsx +++ b/packages/uniswap/src/contexts/UniswapContext.tsx @@ -2,8 +2,8 @@ import { JsonRpcProvider } from '@ethersproject/providers' import { Signer } from 'ethers/lib/ethers' import { createContext, PropsWithChildren, useContext, useMemo, useState } from 'react' import { AccountMeta } from 'uniswap/src/features/accounts/types' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { FiatOnRampCurrency } from 'uniswap/src/features/fiatOnRamp/types' -import { UniverseChainId } from 'uniswap/src/types/chains' import { Connector } from 'wagmi' /** Stores objects/utils that exist on all platforms, abstracting away app-level specifics for each, in order to allow usage in cross-platform code. */ diff --git a/packages/uniswap/src/contexts/UrlContext.tsx b/packages/uniswap/src/contexts/UrlContext.tsx new file mode 100644 index 00000000000..406a5d8bc4e --- /dev/null +++ b/packages/uniswap/src/contexts/UrlContext.tsx @@ -0,0 +1,43 @@ +import { ParsedQs, parse } from 'qs' +import { ReactNode, createContext, useContext, useMemo } from 'react' +import { useLocation } from 'react-router-dom' + +interface UrlContext { + useParsedQueryString: () => ParsedQs +} + +export const UrlContext = createContext(null) + +function useParsedQueryString(): ParsedQs { + const { search } = useLocation() + return useMemo(() => { + const hash = window.location.hash + const query = search || hash.substr(hash.indexOf('?')) + + return query && query.length > 1 ? parse(query, { parseArrays: false, ignoreQueryPrefix: true }) : {} + }, [search]) +} + +export function ReactRouterUrlProvider({ children }: { children: ReactNode | undefined }): JSX.Element { + return {children} +} + +export function BlankUrlProvider({ children }: { children: ReactNode | undefined }): JSX.Element { + const value = useMemo(() => { + return { + useParsedQueryString: (): ParsedQs => { + return {} + }, + } + }, []) + return {children} +} + +export function useUrlContext(): UrlContext { + const context = useContext(UrlContext) + if (!context) { + throw new Error('useUrlContext must be used within a UrlProvider') + } + + return context +} diff --git a/packages/uniswap/src/data/balances/utils.tsx b/packages/uniswap/src/data/balances/utils.tsx index 03f08491fd0..177bd8224e7 100644 --- a/packages/uniswap/src/data/balances/utils.tsx +++ b/packages/uniswap/src/data/balances/utils.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react' import { PortfolioBalancesQueryResult } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { useEnabledChains } from 'uniswap/src/features/settings/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks' import { logger } from 'utilities/src/logger/logger' /** diff --git a/packages/uniswap/src/data/graphql/uniswap-data-api/queries.graphql b/packages/uniswap/src/data/graphql/uniswap-data-api/queries.graphql index 9c413f63ce3..5512e544159 100644 --- a/packages/uniswap/src/data/graphql/uniswap-data-api/queries.graphql +++ b/packages/uniswap/src/data/graphql/uniswap-data-api/queries.graphql @@ -302,6 +302,27 @@ query NftsTab( } } +# We use this fragment to optimize how we render each row in the Tokens tab. +# We should keep it small, only include the fields we need to render that row, +# and avoid including fields that might change too often. +fragment TokenBalanceMainParts on TokenBalance { + ...TokenBalanceQuantityParts + denominatedValue { + currency + value + } + tokenProjectMarket { + relativeChange24: pricePercentChange(duration: DAY) { + value + } + } +} + +fragment TokenBalanceQuantityParts on TokenBalance { + id + quantity +} + query PortfolioBalances( $ownerAddress: String! $valueModifiers: [PortfolioValueModifier!] @@ -328,14 +349,8 @@ query PortfolioBalances( # Individual portfolio token balances tokenBalances { - id - quantity + ...TokenBalanceMainParts isHidden - denominatedValue { - id - currency - value - } token { id address @@ -361,11 +376,6 @@ query PortfolioBalances( attackTypes } } - tokenProjectMarket { - relativeChange24: pricePercentChange(duration: DAY) { - value - } - } } } } @@ -593,6 +603,14 @@ query TokenProjects($contracts: [ContractInput!]!) { address decimals symbol + feeData { + buyFeeBps + sellFeeBps + } + protectionInfo { + result + attackTypes + } } } } @@ -952,6 +970,10 @@ query SearchTokens($searchQuery: String!, $chains: [Chain!]!) { logoUrl safetyLevel } + feeData { + buyFeeBps + sellFeeBps + } protectionInfo { result attackTypes @@ -984,6 +1006,10 @@ query ExploreSearch( result attackTypes } + feeData { + buyFeeBps + sellFeeBps + } } nftCollections(filter: $nftCollectionsFilter, first: 4) { edges { diff --git a/packages/uniswap/src/data/graphql/uniswap-data-api/web/RecentlySearchedAssets.graphql b/packages/uniswap/src/data/graphql/uniswap-data-api/web/RecentlySearchedAssets.graphql index 972f691e1c2..fb2454733de 100644 --- a/packages/uniswap/src/data/graphql/uniswap-data-api/web/RecentlySearchedAssets.graphql +++ b/packages/uniswap/src/data/graphql/uniswap-data-api/web/RecentlySearchedAssets.graphql @@ -32,6 +32,14 @@ query RecentlySearchedAssets( standard address symbol + feeData { + buyFeeBps + sellFeeBps + } + protectionInfo { + attackTypes + result + } market(currency: USD) { id price { diff --git a/packages/uniswap/src/data/graphql/uniswap-data-api/web/SimpleToken.graphql b/packages/uniswap/src/data/graphql/uniswap-data-api/web/SimpleToken.graphql index f6f9301ed8d..2ed43374f65 100644 --- a/packages/uniswap/src/data/graphql/uniswap-data-api/web/SimpleToken.graphql +++ b/packages/uniswap/src/data/graphql/uniswap-data-api/web/SimpleToken.graphql @@ -17,4 +17,8 @@ fragment SimpleTokenDetails on Token { buyFeeBps sellFeeBps } + protectionInfo { + attackTypes + result + } } diff --git a/packages/uniswap/src/data/rest/getPair.ts b/packages/uniswap/src/data/rest/getPair.ts index 2732b4fc278..9ef90a52455 100644 --- a/packages/uniswap/src/data/rest/getPair.ts +++ b/packages/uniswap/src/data/rest/getPair.ts @@ -5,11 +5,11 @@ import { useQuery } from '@connectrpc/connect-query' import { UseQueryResult } from '@tanstack/react-query' import { getPair } from '@uniswap/client-pools/dist/pools/v1/api-PoolsService_connectquery' import { GetPairRequest, GetPairResponse } from '@uniswap/client-pools/dist/pools/v1/api_pb' -import { getPositionsTestTransport } from 'uniswap/src/data/rest/getPositions' +import { uniswapGetTransport } from 'uniswap/src/data/rest/base' export function useGetPair( input?: PartialMessage, enabled = true, ): UseQueryResult { - return useQuery(getPair, input, { transport: getPositionsTestTransport, enabled, retry: false }) + return useQuery(getPair, input, { transport: uniswapGetTransport, enabled, retry: false }) } diff --git a/packages/uniswap/src/data/rest/getPools.ts b/packages/uniswap/src/data/rest/getPools.ts index 3e233199605..228e7b5570d 100644 --- a/packages/uniswap/src/data/rest/getPools.ts +++ b/packages/uniswap/src/data/rest/getPools.ts @@ -5,11 +5,11 @@ import { useQuery } from '@connectrpc/connect-query' import { UseQueryResult } from '@tanstack/react-query' import { listPools } from '@uniswap/client-pools/dist/pools/v1/api-PoolsService_connectquery' import { ListPoolsRequest, ListPoolsResponse } from '@uniswap/client-pools/dist/pools/v1/api_pb' -import { getPositionsTestTransport } from 'uniswap/src/data/rest/getPositions' +import { uniswapGetTransport } from 'uniswap/src/data/rest/base' export function useGetPoolsByTokens( input?: PartialMessage, enabled = true, ): UseQueryResult { - return useQuery(listPools, input, { transport: getPositionsTestTransport, enabled }) + return useQuery(listPools, input, { transport: uniswapGetTransport, enabled }) } diff --git a/packages/uniswap/src/data/rest/getPosition.ts b/packages/uniswap/src/data/rest/getPosition.ts index 96207d3c30f..1b8e9a69d16 100644 --- a/packages/uniswap/src/data/rest/getPosition.ts +++ b/packages/uniswap/src/data/rest/getPosition.ts @@ -5,10 +5,10 @@ import { useQuery } from '@connectrpc/connect-query' import { UseQueryResult } from '@tanstack/react-query' import { getPosition } from '@uniswap/client-pools/dist/pools/v1/api-PoolsService_connectquery' import { GetPositionRequest, GetPositionResponse } from '@uniswap/client-pools/dist/pools/v1/api_pb' -import { getPositionsTestTransport } from 'uniswap/src/data/rest/getPositions' +import { uniswapGetTransport } from 'uniswap/src/data/rest/base' export function useGetPositionQuery( input?: PartialMessage, ): UseQueryResult { - return useQuery(getPosition, input, { transport: getPositionsTestTransport, enabled: !!input }) + return useQuery(getPosition, input, { transport: uniswapGetTransport, enabled: !!input }) } diff --git a/packages/uniswap/src/data/rest/getPositions.ts b/packages/uniswap/src/data/rest/getPositions.ts index bf842c09384..017e6b136da 100644 --- a/packages/uniswap/src/data/rest/getPositions.ts +++ b/packages/uniswap/src/data/rest/getPositions.ts @@ -2,18 +2,18 @@ import { PartialMessage } from '@bufbuild/protobuf' import { ConnectError } from '@connectrpc/connect' import { useQuery } from '@connectrpc/connect-query' -import { createConnectTransport } from '@connectrpc/connect-web' -import { UseQueryResult } from '@tanstack/react-query' +import { UseQueryResult, keepPreviousData } from '@tanstack/react-query' import { listPositions } from '@uniswap/client-pools/dist/pools/v1/api-PoolsService_connectquery' import { ListPositionsRequest, ListPositionsResponse } from '@uniswap/client-pools/dist/pools/v1/api_pb' - -export const getPositionsTestTransport = createConnectTransport({ - baseUrl: 'https://9bxqhlmige.execute-api.us-east-2.amazonaws.com', // TODO: replace with the prod url and update in csp.json as well -}) +import { uniswapGetTransport } from 'uniswap/src/data/rest/base' export function useGetPositionsQuery( input?: PartialMessage, disabled?: boolean, ): UseQueryResult { - return useQuery(listPositions, input, { transport: getPositionsTestTransport, enabled: !!input && !disabled }) + return useQuery(listPositions, input, { + transport: uniswapGetTransport, + enabled: !!input && !disabled, + placeholderData: keepPreviousData, + }) } diff --git a/packages/uniswap/src/data/tradingApi/api.json b/packages/uniswap/src/data/tradingApi/api.json index 6cf5558aaf3..58ae1472b72 100644 --- a/packages/uniswap/src/data/tradingApi/api.json +++ b/packages/uniswap/src/data/tradingApi/api.json @@ -1 +1 @@ -{"openapi":"3.0.0","servers":[{"description":"Uniswap trading APIs Beta","url":"https://beta.trade-api.gateway.uniswap.org/v1"},{"description":"Uniswap trading APIs","url":"https://trade-api.gateway.uniswap.org/v1"}],"info":{"version":"1.0.0","title":"Token Trading","description":"Uniswap trading APIs for fungible tokens."},"paths":{"/check_approval":{"post":{"tags":["Approval"],"summary":"Check if token approval is required","description":"Checks if the swapper has the required approval. If the swapper does not have the required approval, then the response will include the transaction to approve the token. If the swapper has the required approval, then the response will be empty. If the parameter `includeGasInfo` is set to `true`, then the response will include the gas fee for the approval transaction.","operationId":"check_approval","security":[{"apiKey":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApprovalRequest"}}}},"responses":{"200":{"$ref":"#/components/responses/ApprovalSuccess200"},"400":{"$ref":"#/components/responses/BadRequest400"},"401":{"$ref":"#/components/responses/ApprovalUnauthorized401"},"404":{"$ref":"#/components/responses/ApprovalNotFound404"},"419":{"$ref":"#/components/responses/RateLimitedErr429"},"500":{"$ref":"#/components/responses/InternalErr500"},"504":{"$ref":"#/components/responses/Timeout504"}}}},"/quote":{"post":{"tags":["Quote"],"summary":"Get a quote","description":"Get a quote according to the provided configuration. Optionally adds a fee to the quote according to the API key being used. The fee is **ALWAYS** taken from the output token. If there is a fee and the trade is `EXACT_INPUT`, then the output amount will **NOT** include the fee subtraction. For `EXACT_INPUT` swaps, use `portionBips` to calculate the fee from the quoted amount. If there is a fee and the trade is `EXACT_OUTPUT`, then the input amount will **NOT** include the fee addition to account for the fee. For `EXACT_OUTPUT` swaps, use `portionAmount` to get the fee. \n \n We also support Wrapping and Unwrapping of native tokens on their respective chains. Wrapping and Unwrapping only works for when `routingPreference` is `CLASSIC`, `BEST_PRICE`, or `BEST_PRICE_V2`. We do not support `UNISWAPX` or `UNISWAPX_V2` for these actions.","operationId":"aggregator_quote","security":[{"apiKey":[]}],"parameters":[{"$ref":"#/components/parameters/universalRouterVersionHeader"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuoteRequest"}}}},"responses":{"200":{"$ref":"#/components/responses/QuoteSuccess200"},"400":{"$ref":"#/components/responses/BadRequest400"},"401":{"$ref":"#/components/responses/Unauthorized401"},"404":{"$ref":"#/components/responses/QuoteNotFound404"},"419":{"$ref":"#/components/responses/RateLimitedErr429"},"500":{"$ref":"#/components/responses/InternalErr500"},"504":{"$ref":"#/components/responses/Timeout504"}}}},"/order":{"post":{"tags":["Order"],"summary":"Create a gasless order","description":"Submits a new gasless encoded order. The order will be validated and if valid, will be submitted to the filler network. The network will try to fill the order at the quoted `startAmount`, and if not, the amount will start decaying until the `endAmount` is reached. While the order is within `decayEndTime`, the `orderStatus` is `open`. If the order does not get filled after the `decayEndTime` has passed, that is reflected in the `expired` `orderStatus`. then The order will be filled at the best price possible. Once the order is filled, `orderStatus` becomes `filled`.","operationId":"post_order","security":[{"apiKey":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderRequest"}}}},"responses":{"201":{"$ref":"#/components/responses/OrderSuccess201"},"400":{"$ref":"#/components/responses/BadRequest400"},"401":{"$ref":"#/components/responses/Unauthorized401"},"419":{"$ref":"#/components/responses/RateLimitedErr429"},"500":{"$ref":"#/components/responses/InternalErr500"},"504":{"$ref":"#/components/responses/Timeout504"}}}},"/orders":{"get":{"tags":["Order"],"summary":"Get gasless orders","description":"Retrieve gasless orders filtered by query param(s). Some fields on the order can be used as query param.","operationId":"get_order","security":[{"apiKey":[]}],"parameters":[{"$ref":"#/components/parameters/orderTypeParam"},{"$ref":"#/components/parameters/orderIdParam"},{"$ref":"#/components/parameters/orderIdsParam"},{"$ref":"#/components/parameters/limitParam"},{"$ref":"#/components/parameters/orderStatusParam"},{"$ref":"#/components/parameters/swapperParam"},{"$ref":"#/components/parameters/sortKeyParam"},{"$ref":"#/components/parameters/sortParam"},{"$ref":"#/components/parameters/fillerParam"},{"$ref":"#/components/parameters/cursorParam"}],"responses":{"200":{"$ref":"#/components/responses/OrdersSuccess200"},"400":{"$ref":"#/components/responses/OrdersBadRequest400"},"404":{"$ref":"#/components/responses/OrdersNotFound404"},"419":{"$ref":"#/components/responses/RateLimitedErr429"},"500":{"$ref":"#/components/responses/InternalErr500"},"504":{"$ref":"#/components/responses/Timeout504"}}}},"/swap":{"post":{"tags":["Swap"],"summary":"Create swap calldata","description":"Create the calldata for a swap transaction (including wrap/unwrap) against the Uniswap Protocols. If the `quote` parameter includes the fee parameters, then the calldata will include the fee disbursement. The gas estimates will be **more precise** when the the response calldata would be valid if submitted on-chain.","operationId":"create_swap_transaction","security":[{"apiKey":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSwapRequest"}}}},"parameters":[{"$ref":"#/components/parameters/universalRouterVersionHeader"}],"responses":{"200":{"$ref":"#/components/responses/CreateSwapSuccess200"},"400":{"$ref":"#/components/responses/BadRequest400"},"401":{"$ref":"#/components/responses/SwapUnauthorized401"},"404":{"$ref":"#/components/responses/SwapNotFound404"},"419":{"$ref":"#/components/responses/RateLimitedErr429"},"500":{"$ref":"#/components/responses/InternalErr500"},"504":{"$ref":"#/components/responses/Timeout504"}}}},"/swaps":{"get":{"tags":["Swap"],"summary":"Get swaps status","description":"Get the status of a swap or bridge transactions.","operationId":"get_swaps","security":[{"apiKey":[]}],"parameters":[{"$ref":"#/components/parameters/transactionHashesParam"},{"$ref":"#/components/parameters/chainIdParam"}],"responses":{"200":{"$ref":"#/components/responses/GetSwapsSuccess200"},"400":{"$ref":"#/components/responses/BadRequest400"},"404":{"$ref":"#/components/responses/SwapNotFound404"},"419":{"$ref":"#/components/responses/RateLimitedErr429"},"500":{"$ref":"#/components/responses/InternalErr500"},"504":{"$ref":"#/components/responses/Timeout504"}}}},"/indicative_quote":{"post":{"tags":["IndicativeQuote"],"summary":"Get an indicative quote","description":"Get an indicative quote according to the provided configuration. The quote will not include a fee.","operationId":"indicative_quote","security":[{"apiKey":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IndicativeQuoteRequest"}}}},"responses":{"200":{"$ref":"#/components/responses/IndicativeQuoteSuccess200"},"400":{"$ref":"#/components/responses/BadRequest400"},"404":{"$ref":"#/components/responses/QuoteNotFound404"},"419":{"$ref":"#/components/responses/RateLimitedErr429"},"500":{"$ref":"#/components/responses/InternalErr500"},"504":{"$ref":"#/components/responses/Timeout504"}}}},"/send":{"post":{"tags":["Send"],"summary":"Create send calldata","description":"Create the calldata for a send transaction.","operationId":"create_send","security":[{"apiKey":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSendRequest"}}}},"responses":{"200":{"$ref":"#/components/responses/CreateSendSuccess200"},"400":{"$ref":"#/components/responses/BadRequest400"},"401":{"$ref":"#/components/responses/Unauthorized401"},"404":{"$ref":"#/components/responses/SendNotFound404"},"429":{"$ref":"#/components/responses/RateLimitedErr429"},"500":{"$ref":"#/components/responses/InternalErr500"},"504":{"$ref":"#/components/responses/Timeout504"}}}},"/swappable_tokens":{"get":{"tags":["SwappableTokens"],"summary":"Get swappable tokens","description":"Get the swappable tokens for the given configuration. Either tokenIn (with tokenInChainId or (tokenInChainId and tokenOutChainId)) or tokenOut (with tokenOutChainId or (tokenOutChainId and tokenInChainId)) must be provided but not both.","operationId":"get_swappable_tokens","security":[{"apiKey":[]}],"parameters":[{"$ref":"#/components/parameters/tokenInParam"},{"$ref":"#/components/parameters/tokenOutParam"},{"$ref":"#/components/parameters/bridgeTokenInChainIdParam"},{"$ref":"#/components/parameters/bridgeTokenOutChainIdParam"}],"responses":{"200":{"$ref":"#/components/responses/GetSwappableTokensSuccess200"},"400":{"$ref":"#/components/responses/BadRequest400"},"401":{"$ref":"#/components/responses/Unauthorized401"},"404":{"$ref":"#/components/responses/QuoteNotFound404"},"429":{"$ref":"#/components/responses/RateLimitedErr429"},"500":{"$ref":"#/components/responses/InternalErr500"},"504":{"$ref":"#/components/responses/Timeout504"}}}},"/limit_order_quote":{"post":{"tags":["LimitOrderQuote"],"summary":"Get a limit order quote","description":"Get a quote for a limit order according to the provided configuration.","operationId":"get_limit_order_quote","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LimitOrderQuoteRequest"}}}},"responses":{"200":{"$ref":"#/components/responses/LimitOrderQuoteSuccess200"},"400":{"$ref":"#/components/responses/BadRequest400"},"401":{"$ref":"#/components/responses/Unauthorized401"},"404":{"$ref":"#/components/responses/QuoteNotFound404"},"419":{"$ref":"#/components/responses/RateLimitedErr429"},"500":{"$ref":"#/components/responses/InternalErr500"},"504":{"$ref":"#/components/responses/Timeout504"}}}},"/lp/approve":{"post":{"tags":["Liquidity"],"summary":"Check if tokens and permits need to be approved to add liquidity","description":"Checks if the wallet address has the required approvals. If the wallet address does not have the required approval, then the response will include the transactions to approve the tokens. If the wallet address has the required approval, then the response will be empty for the corresponding tokens. If the parameter `simulateTransaction` is set to `true`, then the response will include the gas fees for the approval transactions.","operationId":"check_approval_lp","security":[{"apiKey":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckApprovalLPRequest"}}}},"responses":{"200":{"$ref":"#/components/responses/CheckApprovalLPSuccess200"},"400":{"$ref":"#/components/responses/BadRequest400"},"401":{"$ref":"#/components/responses/ApprovalUnauthorized401"},"404":{"$ref":"#/components/responses/ApprovalNotFound404"},"419":{"$ref":"#/components/responses/RateLimitedErr429"},"500":{"$ref":"#/components/responses/InternalErr500"},"504":{"$ref":"#/components/responses/Timeout504"}}}},"/lp/create":{"post":{"tags":["Liquidity"],"summary":"Create pool and position calldata","description":"Create pool and position calldata. If the pool is not yet created, then the response will include the transaction to create the new pool with the initial price. If the pool is already created, then the response will not have the transaction to create the pool. The response will also have the transaction to create the position for the corresponding pool. If the parameter `simulateTransaction` is set to `true`, then the response will include the gas fees for the creation transactions.","operationId":"create_lp_position","security":[{"apiKey":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateLPPositionRequest"}}}},"responses":{"200":{"$ref":"#/components/responses/CreateLPPositionSuccess200"},"400":{"$ref":"#/components/responses/BadRequest400"},"401":{"$ref":"#/components/responses/ApprovalUnauthorized401"},"404":{"$ref":"#/components/responses/LPNotFound404"},"419":{"$ref":"#/components/responses/RateLimitedErr429"},"500":{"$ref":"#/components/responses/InternalErr500"},"504":{"$ref":"#/components/responses/Timeout504"}}}},"/lp/increase":{"post":{"tags":["Liquidity"],"summary":"Increase LP position calldata","description":"The response will also have the transaction to increase the position for the corresponding pool. If the parameter `simulateTransaction` is set to `true`, then the response will include the gas fees for the increase transaction.","operationId":"increase_lp_position","security":[{"apiKey":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IncreaseLPPositionRequest"}}}},"responses":{"200":{"$ref":"#/components/responses/IncreaseLPPositionSuccess200"},"400":{"$ref":"#/components/responses/BadRequest400"},"401":{"$ref":"#/components/responses/ApprovalUnauthorized401"},"404":{"$ref":"#/components/responses/LPNotFound404"},"419":{"$ref":"#/components/responses/RateLimitedErr429"},"500":{"$ref":"#/components/responses/InternalErr500"},"504":{"$ref":"#/components/responses/Timeout504"}}}},"/lp/decrease":{"post":{"tags":["Liquidity"],"summary":"Decrease LP position calldata","description":"The response will also have the transaction to decrease the position for the corresponding pool. If the parameter `simulateTransaction` is set to `true`, then the response will include the gas fees for the decrease transaction.","operationId":"decrease_lp_position","security":[{"apiKey":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DecreaseLPPositionRequest"}}}},"responses":{"200":{"$ref":"#/components/responses/DecreaseLPPositionSuccess200"},"400":{"$ref":"#/components/responses/BadRequest400"},"401":{"$ref":"#/components/responses/ApprovalUnauthorized401"},"404":{"$ref":"#/components/responses/LPNotFound404"},"419":{"$ref":"#/components/responses/RateLimitedErr429"},"500":{"$ref":"#/components/responses/InternalErr500"},"504":{"$ref":"#/components/responses/Timeout504"}}}},"/lp/claim":{"post":{"tags":["Liquidity"],"summary":"Claim LP fees calldata","description":"The response will also have the transaction to claim the fees for an LP position for the corresponding pool. If the parameter `simulateTransaction` is set to `true`, then the response will include the gas fees for the claim transaction.","operationId":"claim_lp_fees","security":[{"apiKey":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClaimLPFeesRequest"}}}},"responses":{"200":{"$ref":"#/components/responses/ClaimLPFeesSuccess200"},"400":{"$ref":"#/components/responses/BadRequest400"},"401":{"$ref":"#/components/responses/ApprovalUnauthorized401"},"404":{"$ref":"#/components/responses/LPNotFound404"},"419":{"$ref":"#/components/responses/RateLimitedErr429"},"500":{"$ref":"#/components/responses/InternalErr500"},"504":{"$ref":"#/components/responses/Timeout504"}}}},"/lp/migrate":{"post":{"tags":["Liquidity"],"summary":"Migrate LP position calldata","description":"The response will also have the transaction to migrate the position for the corresponding pool. If the parameter `simulateTransaction` is set to `true`, then the response will include the gas fees for the migrate transaction.","operationId":"migrate_lp_position","security":[{"apiKey":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MigrateLPPositionRequest"}}}},"responses":{"200":{"$ref":"#/components/responses/MigrateLPPositionSuccess200"},"400":{"$ref":"#/components/responses/BadRequest400"},"401":{"$ref":"#/components/responses/ApprovalUnauthorized401"},"404":{"$ref":"#/components/responses/LPNotFound404"},"419":{"$ref":"#/components/responses/RateLimitedErr429"},"500":{"$ref":"#/components/responses/InternalErr500"},"504":{"$ref":"#/components/responses/Timeout504"}}}}},"components":{"responses":{"OrdersSuccess200":{"description":"The request orders matching the query parameters.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetOrdersResponse"}}}},"OrderSuccess201":{"description":"Encoded order submitted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderResponse"}}}},"QuoteSuccess200":{"description":"Quote request successful.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuoteResponse"}}}},"LimitOrderQuoteSuccess200":{"description":"Limit Order Quote request successful.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LimitOrderQuoteResponse"}}}},"CheckApprovalLPSuccess200":{"description":"Approve LP successful.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckApprovalLPResponse"}}}},"ApprovalSuccess200":{"description":"Check approval successful.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApprovalResponse"}}}},"CreateSendSuccess200":{"description":"Create send successful.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSendResponse"}}}},"CreateSwapSuccess200":{"description":"Create swap successful.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSwapResponse"}}}},"GetSwapsSuccess200":{"description":"Get swap successful.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetSwapsResponse"}}}},"GetSwappableTokensSuccess200":{"description":"Get swappable tokens successful.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetSwappableTokensResponse"}}}},"CreateLPPositionSuccess200":{"description":"Create LP Position successful.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateLPPositionResponse"}}}},"IncreaseLPPositionSuccess200":{"description":"Create LP Position successful.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IncreaseLPPositionResponse"}}}},"DecreaseLPPositionSuccess200":{"description":"Decrease LP Position successful.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DecreaseLPPositionResponse"}}}},"ClaimLPFeesSuccess200":{"description":"Claim LP Fees successful.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClaimLPFeesResponse"}}}},"MigrateLPPositionSuccess200":{"description":"Migrate LP Position successful.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MigrateLPPositionResponse"}}}},"BadRequest400":{"description":"RequestValidationError, Bad Input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err400"}}}},"ApprovalUnauthorized401":{"description":"UnauthorizedError eg. Account is blocked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err401"}}}},"ApprovalNotFound404":{"description":"ResourceNotFound eg. Token allowance not found or Gas info not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err404"}}}},"Unauthorized401":{"description":"UnauthorizedError eg. Account is blocked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err401"}}}},"QuoteNotFound404":{"description":"ResourceNotFound eg. No quotes available or Gas fee/price not available","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err404"}}}},"SendNotFound404":{"description":"ResourceNotFound eg. Gas fee not available","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err404"}}}},"SwapBadRequest400":{"description":"RequestValidationError, Bad Input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err400"}}}},"SwapUnauthorized401":{"description":"UnauthorizedError eg. Account is blocked or Fee is not enabled.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err401"}}}},"SwapNotFound404":{"description":"ResourceNotFound eg. No quotes available or Gas fee/price not available","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err404"}}}},"OrdersNotFound404":{"description":"Orders not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err404"}}}},"LPNotFound404":{"description":"ResourceNotFound eg. Cant Find LP Position.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err404"}}}},"OrdersBadRequest400":{"description":"RequestValidationError eg. Token allowance not valid or Insufficient Funds.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err400"}}}},"RateLimitedErr429":{"description":"Ratelimited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err429"}}}},"InternalErr500":{"description":"Unexpected error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err500"}}}},"Timeout504":{"description":"Request duration limit reached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err504"}}}},"IndicativeQuoteSuccess200":{"description":"Indicative quote request successful.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IndicativeQuoteResponse"}}}}},"schemas":{"NullablePermit":{"allOf":[{"$ref":"#/components/schemas/Permit"},{"type":"object","nullable":true}]},"TokenAmount":{"type":"string"},"SwapStatus":{"type":"string","enum":["PENDING","SUCCESS","NOT_FOUND","FAILED","EXPIRED"]},"GetSwapsResponse":{"type":"object","properties":{"requestId":{"$ref":"#/components/schemas/RequestId"},"swaps":{"type":"array","items":{"type":"object","properties":{"swapType":{"$ref":"#/components/schemas/Routing"},"status":{"$ref":"#/components/schemas/SwapStatus"},"txHash":{"type":"string"},"swapId":{"type":"number"}}}}},"required":["requestId","status"]},"GetSwappableTokensResponse":{"type":"object","properties":{"requestId":{"$ref":"#/components/schemas/RequestId"},"tokens":{"type":"array","items":{"type":"object","properties":{"address":{"$ref":"#/components/schemas/Address"},"chainId":{"$ref":"#/components/schemas/ChainId"},"name":{"type":"string"},"symbol":{"type":"string"},"project":{"$ref":"#/components/schemas/TokenProject"},"isSpam":{"type":"boolean"},"decimals":{"type":"number"}},"required":["address","chainId","name","symbol","project","decimals"]}}},"required":["requestId","tokens"]},"CreateSwapRequest":{"type":"object","description":"The parameters **signature** and **permitData** should only be included if *permitData* was returned from **/quote**.","properties":{"quote":{"oneOf":[{"$ref":"#/components/schemas/ClassicQuote"},{"$ref":"#/components/schemas/WrapUnwrapQuote"},{"$ref":"#/components/schemas/BridgeQuote"}]},"signature":{"type":"string","description":"The signed permit."},"includeGasInfo":{"type":"boolean","default":false,"deprecated":true,"description":"Use `refreshGasPrice` instead."},"refreshGasPrice":{"type":"boolean","default":false,"description":"If true, the gas price will be re-fetched from the network."},"simulateTransaction":{"type":"boolean","default":false,"description":"If true, the transaction will be simulated. If the simulation results on an onchain error, endpoint will return an error."},"permitData":{"allOf":[{"$ref":"#/components/schemas/Permit"}]},"safetyMode":{"$ref":"#/components/schemas/SwapSafetyMode"},"deadline":{"type":"integer","description":"The deadline for the swap in unix timestamp format. If the deadline is not defined OR in the past then the default deadline is 30 minutes."},"urgency":{"$ref":"#/components/schemas/Urgency"}},"required":["quote"]},"CreateSendRequest":{"type":"object","properties":{"sender":{"$ref":"#/components/schemas/Address"},"recipient":{"$ref":"#/components/schemas/Address"},"token":{"$ref":"#/components/schemas/Address"},"amount":{"$ref":"#/components/schemas/TokenAmount"},"chainId":{"$ref":"#/components/schemas/ChainId"},"urgency":{"$ref":"#/components/schemas/Urgency"}},"required":["sender","recipient","token","amount"]},"UniversalRouterVersion":{"type":"string","enum":["1.2","2.0"],"default":"1.2"},"Address":{"type":"string","pattern":"^(0x)?[0-9a-fA-F]{40}$"},"Position":{"type":"object","properties":{"pool":{"$ref":"#/components/schemas/Pool"},"tickLower":{"type":"number"},"tickUpper":{"type":"number"}},"required":["pool"]},"Pool":{"type":"object","properties":{"token0":{"$ref":"#/components/schemas/Address"},"token1":{"$ref":"#/components/schemas/Address"},"fee":{"type":"number"},"tickSpacing":{"type":"number"},"hooks":{"$ref":"#/components/schemas/Address"}},"required":["token0","token1"]},"ClassicGasUseEstimateUSD":{"description":"The gas fee you would pay if you opted for a CLASSIC swap over a Uniswap X order in terms of USD.","type":"string"},"CreateSwapResponse":{"type":"object","properties":{"requestId":{"$ref":"#/components/schemas/RequestId"},"swap":{"$ref":"#/components/schemas/TransactionRequest"},"gasFee":{"type":"string"}},"required":["requestId","swap"]},"CreateSendResponse":{"type":"object","properties":{"requestId":{"$ref":"#/components/schemas/RequestId"},"send":{"$ref":"#/components/schemas/TransactionRequest"},"gasFee":{"type":"string"},"gasFeeUSD":{"type":"number"}},"required":["requestId","send"]},"QuoteResponse":{"type":"object","properties":{"requestId":{"$ref":"#/components/schemas/RequestId"},"quote":{"$ref":"#/components/schemas/Quote"},"routing":{"$ref":"#/components/schemas/Routing"},"permitData":{"$ref":"#/components/schemas/NullablePermit"}},"required":["routing","quote","permitData","requestId"]},"LimitOrderQuoteResponse":{"type":"object","properties":{"requestId":{"$ref":"#/components/schemas/RequestId"},"quote":{"$ref":"#/components/schemas/DutchQuote"},"routing":{"type":"string","enum":["LIMIT_ORDER"]},"permitData":{"$ref":"#/components/schemas/NullablePermit"}},"required":["routing","quote","permitData","requestId"]},"QuoteRequest":{"type":"object","properties":{"type":{"$ref":"#/components/schemas/TradeType"},"amount":{"type":"string"},"tokenInChainId":{"$ref":"#/components/schemas/ChainId"},"tokenOutChainId":{"$ref":"#/components/schemas/ChainId"},"tokenIn":{"type":"string"},"tokenOut":{"type":"string"},"swapper":{"$ref":"#/components/schemas/Address"},"slippageTolerance":{"description":"For **Classic** swaps, the slippage tolerance is the maximum amount the price can change between the time the transaction is submitted and the time it is executed. The slippage tolerance is represented as a percentage of the total value of the swap. \n\n Slippage tolerance works differently in **DutchLimit** swaps, it does not set a limit on the Spread in an order. See [here](https://uniswap-docs.readme.io/reference/faqs#why-do-the-uniswapx-quotes-have-more-slippage-than-the-tolerance-i-set) for more information. \n\n **NOTE**: slippage is in terms of trade type. If the trade type is `EXACT_INPUT`, then the slippage is in terms of the output token. If the trade type is `EXACT_OUTPUT`, then the slippage is in terms of the input token.","type":"number"},"autoSlippage":{"$ref":"#/components/schemas/AutoSlippage"},"routingPreference":{"$ref":"#/components/schemas/RoutingPreference"},"protocols":{"$ref":"#/components/schemas/Protocols"},"spreadOptimization":{"$ref":"#/components/schemas/SpreadOptimization"},"urgency":{"$ref":"#/components/schemas/Urgency"}},"required":["type","amount","tokenInChainId","tokenOutChainId","tokenIn","tokenOut","swapper"]},"LimitOrderQuoteRequest":{"type":"object","properties":{"swapper":{"$ref":"#/components/schemas/Address"},"limitPrice":{"type":"string"},"amount":{"type":"string"},"orderDeadline":{"type":"number"},"type":{"$ref":"#/components/schemas/TradeType"},"tokenIn":{"type":"string"},"tokenOut":{"type":"string"},"tokenInChainId":{"$ref":"#/components/schemas/ChainId"},"tokenOutChainId":{"$ref":"#/components/schemas/ChainId"}},"required":["swapper","type","amount","tokenIn","tokenOut","tokenInChainId","tokenOutChainId"]},"GetOrdersResponse":{"type":"object","properties":{"requestId":{"$ref":"#/components/schemas/RequestId"},"orders":{"type":"array","items":{"$ref":"#/components/schemas/UniswapXOrder"}},"cursor":{"type":"string"}},"required":["orders","requestId"]},"OrderResponse":{"type":"object","properties":{"requestId":{"$ref":"#/components/schemas/RequestId"},"orderId":{"type":"string"},"orderStatus":{"$ref":"#/components/schemas/OrderStatus"}},"required":["requestId","orderId","orderStatus"]},"OrderRequest":{"type":"object","properties":{"signature":{"type":"string","description":"The signed permit."},"quote":{"oneOf":[{"$ref":"#/components/schemas/DutchQuote"},{"$ref":"#/components/schemas/DutchQuoteV2"},{"$ref":"#/components/schemas/PriorityQuote"}]},"routing":{"$ref":"#/components/schemas/Routing"}},"required":["signature","quote"]},"Urgency":{"type":"string","enum":["normal","fast","urgent"],"description":"The urgency determines the urgency of the transaction. The default value is `urgent`.","default":"urgent"},"Protocols":{"type":"array","items":{"$ref":"#/components/schemas/ProtocolItems"},"description":"The protocols to use for the swap/order. If the `protocols` field is defined, then you can only set the `routingPreference` to `BEST_PRICE`"},"Err400":{"type":"object","properties":{"errorCode":{"default":"RequestValidationError","type":"string"},"detail":{"type":"string"}}},"Err401":{"type":"object","properties":{"errorCode":{"default":"UnauthorizedError","type":"string"},"detail":{"type":"string"}}},"Err404":{"type":"object","properties":{"errorCode":{"enum":["ResourceNotFound","QuoteAmountTooLowError"],"type":"string"},"detail":{"type":"string"}}},"Err429":{"type":"object","properties":{"errorCode":{"default":"Ratelimited","type":"string"},"detail":{"type":"string"}}},"Err500":{"type":"object","properties":{"errorCode":{"default":"InternalServerError","type":"string"},"detail":{"type":"string"}}},"Err504":{"type":"object","properties":{"errorCode":{"default":"Timeout","type":"string"},"detail":{"type":"string"}}},"ChainId":{"type":"number","enum":[1,10,56,137,8453,42161,81457,43114,42220,7777777,324,11155111,1301,480]},"OrderInput":{"type":"object","properties":{"token":{"type":"string"},"startAmount":{"type":"string"},"endAmount":{"type":"string"}},"required":["token"]},"OrderOutput":{"type":"object","properties":{"token":{"type":"string"},"startAmount":{"type":"string"},"endAmount":{"type":"string"},"isFeeOutput":{"type":"boolean"},"recipient":{"type":"string"}},"required":["token"]},"CosignerData":{"type":"object","properties":{"decayStartTime":{"type":"number"},"decayEndTime":{"type":"number"},"exclusiveFiller":{"type":"string"},"inputOverride":{"type":"string"},"outputOverrides":{"type":"array","items":{"type":"string"}}}},"SettledAmount":{"type":"object","properties":{"tokenOut":{"$ref":"#/components/schemas/Address"},"amountOut":{"type":"string"},"tokenIn":{"$ref":"#/components/schemas/Address"},"amountIn":{"type":"string"}}},"OrderType":{"type":"string","enum":["DutchLimit","Dutch","Dutch_V2"]},"OrderTypeQuery":{"type":"string","enum":["Dutch","Dutch_V2","Dutch_V1_V2","Limit","Priority"]},"UniswapXOrder":{"type":"object","properties":{"type":{"$ref":"#/components/schemas/OrderType"},"encodedOrder":{"type":"string"},"signature":{"type":"string"},"nonce":{"type":"string"},"orderStatus":{"$ref":"#/components/schemas/OrderStatus"},"orderId":{"type":"string"},"chainId":{"$ref":"#/components/schemas/ChainId"},"quoteId":{"type":"string"},"swapper":{"type":"string"},"txHash":{"type":"string"},"input":{"$ref":"#/components/schemas/OrderInput"},"outputs":{"type":"array","items":{"$ref":"#/components/schemas/OrderOutput"}},"settledAmounts":{"type":"array","items":{"$ref":"#/components/schemas/SettledAmount"}},"cosignature":{"type":"string"},"cosignerData":{"$ref":"#/components/schemas/CosignerData"}},"required":["encodedOrder","signature","nonce","orderId","orderStatus","chainId","type"]},"SortKey":{"type":"string","enum":["createdAt"]},"OrderId":{"type":"string"},"OrderIds":{"type":"string"},"OrderStatus":{"type":"string","enum":["open","expired","error","cancelled","filled","unverified","insufficient-funds"]},"Permit":{"type":"object","properties":{"domain":{"type":"object"},"values":{"type":"object"},"types":{"type":"object"}}},"TokenProject":{"type":"object","properties":{"logo":{"$ref":"#/components/schemas/TokenProjectLogo","nullable":true},"safetyLevel":{"$ref":"#/components/schemas/SafetyLevel"},"isSpam":{"type":"boolean"}},"required":["logo","safetyLevel","isSpam"]},"TokenProjectLogo":{"type":"object","properties":{"url":{"type":"string"}},"required":["url"]},"DutchInput":{"type":"object","properties":{"startAmount":{"type":"string"},"endAmount":{"type":"string"},"token":{"type":"string"}},"required":["startAmount","endAmount","type"]},"DutchOutput":{"type":"object","properties":{"startAmount":{"type":"string"},"endAmount":{"type":"string"},"token":{"type":"string"},"recipient":{"type":"string"}},"required":["startAmount","endAmount","token","recipient"]},"DutchOrderInfo":{"type":"object","properties":{"chainId":{"$ref":"#/components/schemas/ChainId"},"nonce":{"type":"string"},"reactor":{"type":"string"},"swapper":{"type":"string"},"deadline":{"type":"number"},"additionalValidationContract":{"type":"string"},"additionalValidationData":{"type":"string"},"decayStartTime":{"type":"number"},"decayEndTime":{"type":"number"},"exclusiveFiller":{"type":"string"},"exclusivityOverrideBps":{"type":"string"},"input":{"$ref":"#/components/schemas/DutchInput"},"outputs":{"type":"array","items":{"$ref":"#/components/schemas/DutchOutput"}}},"required":["chainId","nonce","reactor","swapper","deadline","validationContract","validationData","startTime","endTime","exclusiveFiller","exclusivityOverrideBps","input","outputs"]},"DutchOrderInfoV2":{"type":"object","properties":{"chainId":{"$ref":"#/components/schemas/ChainId"},"nonce":{"type":"string"},"reactor":{"type":"string"},"swapper":{"type":"string"},"deadline":{"type":"number"},"additionalValidationContract":{"type":"string"},"additionalValidationData":{"type":"string"},"input":{"$ref":"#/components/schemas/DutchInput"},"outputs":{"type":"array","items":{"$ref":"#/components/schemas/DutchOutput"}},"cosigner":{"$ref":"#/components/schemas/Address"}},"required":["chainId","nonce","reactor","swapper","deadline","validationContract","validationData","startTime","endTime","exclusiveFiller","exclusivityOverrideBps","input","outputs"]},"DutchQuote":{"type":"object","properties":{"encodedOrder":{"type":"string"},"orderId":{"type":"string"},"orderInfo":{"$ref":"#/components/schemas/DutchOrderInfo"},"portionBips":{"type":"number"},"portionAmount":{"type":"string"},"portionRecipient":{"$ref":"#/components/schemas/Address"},"quoteId":{"type":"string"},"slippageTolerance":{"type":"number"},"classicGasUseEstimateUSD":{"$ref":"#/components/schemas/ClassicGasUseEstimateUSD"}},"required":["encodedOrder","orderInfo","orderId"]},"DutchQuoteV2":{"type":"object","properties":{"encodedOrder":{"type":"string"},"orderId":{"type":"string"},"orderInfo":{"$ref":"#/components/schemas/DutchOrderInfoV2"},"portionBips":{"type":"number"},"portionAmount":{"type":"string"},"portionRecipient":{"$ref":"#/components/schemas/Address"},"quoteId":{"type":"string"},"slippageTolerance":{"type":"number"},"deadlineBufferSecs":{"type":"number"},"classicGasUseEstimateUSD":{"$ref":"#/components/schemas/ClassicGasUseEstimateUSD"}},"required":["encodedOrder","orderInfo","orderId"]},"PriorityInput":{"type":"object","properties":{"amount":{"type":"string"},"token":{"type":"string"},"mpsPerPriorityFeeWei":{"type":"string"}},"required":["amount","token","mpsPerPriorityFeeWei"]},"PriorityOutput":{"type":"object","properties":{"amount":{"type":"string"},"token":{"type":"string"},"recipient":{"type":"string"},"mpsPerPriorityFeeWei":{"type":"string"}},"required":["amount","token","recipient","mpsPerPriorityFeeWei"]},"PriorityOrderInfo":{"type":"object","properties":{"chainId":{"$ref":"#/components/schemas/ChainId"},"nonce":{"type":"string"},"reactor":{"type":"string"},"swapper":{"type":"string"},"deadline":{"type":"number"},"additionalValidationContract":{"type":"string"},"additionalValidationData":{"type":"string"},"auctionStartBlock":{"type":"string"},"baselinePriorityFeeWei":{"type":"string"},"input":{"$ref":"#/components/schemas/PriorityInput"},"outputs":{"type":"array","items":{"$ref":"#/components/schemas/PriorityOutput"}},"cosigner":{"$ref":"#/components/schemas/Address"}},"required":["chainId","nonce","reactor","swapper","deadline","validationContract","validationData","auctionStartBlock","baselinePriorityFeeWei","input","outputs","cosigner"]},"PriorityQuote":{"type":"object","properties":{"encodedOrder":{"type":"string"},"orderId":{"type":"string"},"orderInfo":{"$ref":"#/components/schemas/PriorityOrderInfo"},"portionBips":{"type":"number"},"portionAmount":{"type":"string"},"portionRecipient":{"$ref":"#/components/schemas/Address"},"quoteId":{"type":"string"},"slippageTolerance":{"type":"number"},"deadlineBufferSecs":{"type":"number"},"classicGasUseEstimateUSD":{"$ref":"#/components/schemas/ClassicGasUseEstimateUSD"}},"required":["encodedOrder","orderInfo","orderId"]},"BridgeQuote":{"type":"object","properties":{"quoteId":{"type":"string"},"chainId":{"$ref":"#/components/schemas/ChainId"},"destinationChainId":{"$ref":"#/components/schemas/ChainId"},"swapper":{"$ref":"#/components/schemas/Address"},"input":{"$ref":"#/components/schemas/ClassicInput"},"output":{"$ref":"#/components/schemas/ClassicOutput"},"tradeType":{"$ref":"#/components/schemas/TradeType"},"quoteTimestamp":{"type":"number"},"gasPrice":{"type":"string"},"maxFeePerGas":{"type":"string"},"maxPriorityFeePerGas":{"type":"string"},"gasFee":{"type":"string"},"gasUseEstimate":{"type":"string"},"gasFeeUSD":{"type":"string"},"portionBips":{"type":"number"},"portionAmount":{"type":"string"},"portionRecipient":{"$ref":"#/components/schemas/Address"},"estimatedFillTimeMs":{"type":"number"}}},"SafetyLevel":{"type":"string","enum":["BLOCKED","MEDIUM_WARNING","STRONG_WARNING","VERIFIED"]},"TradeType":{"type":"string","enum":["EXACT_INPUT","EXACT_OUTPUT"]},"Routing":{"type":"string","enum":["DUTCH_LIMIT","CLASSIC","DUTCH_V2","BRIDGE","LIMIT_ORDER","PRIORITY"]},"Quote":{"oneOf":[{"$ref":"#/components/schemas/DutchQuote"},{"$ref":"#/components/schemas/ClassicQuote"},{"$ref":"#/components/schemas/WrapUnwrapQuote"},{"$ref":"#/components/schemas/DutchQuoteV2"},{"$ref":"#/components/schemas/BridgeQuote"},{"$ref":"#/components/schemas/PriorityQuote"}]},"CheckApprovalLPRequest":{"type":"object","properties":{"protocol":{"$ref":"#/components/schemas/ProtocolItems"},"token0":{"$ref":"#/components/schemas/Address"},"token1":{"$ref":"#/components/schemas/Address"},"positionToken":{"$ref":"#/components/schemas/Address"},"chainId":{"$ref":"#/components/schemas/ChainId"},"walletAddress":{"$ref":"#/components/schemas/Address"},"amount0":{"type":"string"},"amount1":{"type":"string"},"positionAmount":{"type":"string"},"simulateTransaction":{"type":"boolean"}}},"CheckApprovalLPResponse":{"type":"object","properties":{"requestId":{"$ref":"#/components/schemas/RequestId"},"token0Approval":{"$ref":"#/components/schemas/TransactionRequest"},"token1Approval":{"$ref":"#/components/schemas/TransactionRequest"},"positionTokenApproval":{"$ref":"#/components/schemas/TransactionRequest"},"permitData":{"$ref":"#/components/schemas/NullablePermit"},"gasFeeToken0Approval":{"type":"string"},"gasFeeToken1Approval":{"type":"string"},"gasFeePositionTokenApproval":{"type":"string"}}},"ApprovalRequest":{"type":"object","properties":{"walletAddress":{"$ref":"#/components/schemas/Address"},"token":{"$ref":"#/components/schemas/Address"},"amount":{"$ref":"#/components/schemas/TokenAmount"},"chainId":{"$ref":"#/components/schemas/ChainId"},"urgency":{"$ref":"#/components/schemas/Urgency"},"includeGasInfo":{"type":"boolean","default":false},"tokenOut":{"$ref":"#/components/schemas/Address"},"tokenOutChainId":{"$ref":"#/components/schemas/ChainId"}},"required":["walletAddress","token","amount"]},"ApprovalResponse":{"type":"object","properties":{"requestId":{"$ref":"#/components/schemas/RequestId"},"approval":{"$ref":"#/components/schemas/TransactionRequest"},"cancel":{"$ref":"#/components/schemas/TransactionRequest"},"gasFee":{"type":"string"},"cancelGasFee":{"type":"string"}},"required":["requestId","approval","cancel"]},"ClassicQuote":{"type":"object","properties":{"input":{"$ref":"#/components/schemas/ClassicInput"},"output":{"$ref":"#/components/schemas/ClassicOutput"},"swapper":{"$ref":"#/components/schemas/Address"},"chainId":{"$ref":"#/components/schemas/ChainId"},"slippage":{"type":"number"},"tradeType":{"$ref":"#/components/schemas/TradeType"},"gasFee":{"type":"string","description":"The gas fee in terms of wei. It does NOT include the additional gas for token approvals."},"gasFeeUSD":{"type":"string","description":"The gas fee in terms of USD. It does NOT include the additional gas for token approvals."},"gasFeeQuote":{"type":"string","description":"The gas fee in terms of the quoted currency. It does NOT include the additional gas for token approvals."},"route":{"type":"array","items":{"type":"array","items":{"oneOf":[{"$ref":"#/components/schemas/V3PoolInRoute"},{"$ref":"#/components/schemas/V2PoolInRoute"},{"$ref":"#/components/schemas/V4PoolInRoute"}]}}},"portionBips":{"type":"number","description":"The portion of the swap that will be taken as a fee. The fee will be taken from the output token."},"portionAmount":{"type":"string","description":"The amount of the swap that will be taken as a fee. The fee will be taken from the output token."},"portionRecipient":{"$ref":"#/components/schemas/Address"},"routeString":{"type":"string","description":"The route in string format."},"quoteId":{"type":"string","description":"The quote id. Used for analytics purposes."},"gasUseEstimate":{"type":"string","description":"The estimated gas use. It does NOT include the additional gas for token approvals."},"blockNumber":{"type":"string","description":"The current block number."},"gasPrice":{"type":"string","description":"The gas price in terms of wei for pre EIP1559 transactions."},"maxFeePerGas":{"type":"string","description":"The maximum fee per gas in terms of wei for EIP1559 transactions."},"maxPriorityFeePerGas":{"type":"string","description":"The maximum priority fee per gas in terms of wei for EIP1559 transactions."},"txFailureReasons":{"type":"array","items":{"$ref":"#/components/schemas/TransactionFailureReason"}},"priceImpact":{"type":"number","description":"The impact the trade has on the market price of the pool, between 0-100 percent"}}},"WrapUnwrapQuote":{"type":"object","properties":{"swapper":{"$ref":"#/components/schemas/Address"},"input":{"$ref":"#/components/schemas/ClassicInput"},"output":{"$ref":"#/components/schemas/ClassicOutput"},"chainId":{"$ref":"#/components/schemas/ChainId"},"tradeType":{"$ref":"#/components/schemas/TradeType"},"gasFee":{"type":"string","description":"The gas fee in terms of wei."},"gasFeeUSD":{"type":"string","description":"The gas fee in terms of USD."},"gasFeeQuote":{"type":"string","description":"The gas fee in terms of the quoted currency."},"gasUseEstimate":{"type":"string","description":"The estimated gas use."},"gasPrice":{"type":"string","description":"The gas price in terms of wei for pre EIP1559 transactions."},"maxFeePerGas":{"type":"string","description":"The maximum fee per gas in terms of wei for EIP1559 transactions."},"maxPriorityFeePerGas":{"type":"string","description":"The maximum priority fee per gas in terms of wei for EIP1559 transactions."}}},"TokenInRoute":{"type":"object","properties":{"address":{"$ref":"#/components/schemas/Address"},"chainId":{"$ref":"#/components/schemas/ChainId"},"symbol":{"type":"string"},"decimals":{"type":"string"},"buyFeeBps":{"type":"string"},"sellFeeBps":{"type":"string"}}},"V2Reserve":{"type":"object","properties":{"token":{"$ref":"#/components/schemas/TokenInRoute"},"quotient":{"type":"string"}}},"V2PoolInRoute":{"type":"object","properties":{"type":{"type":"string","default":"v2-pool"},"address":{"$ref":"#/components/schemas/Address"},"tokenIn":{"$ref":"#/components/schemas/TokenInRoute"},"tokenOut":{"$ref":"#/components/schemas/TokenInRoute"},"reserve0":{"$ref":"#/components/schemas/V2Reserve"},"reserve1":{"$ref":"#/components/schemas/V2Reserve"},"amountIn":{"type":"string"},"amountOut":{"type":"string"}}},"V3PoolInRoute":{"type":"object","properties":{"type":{"type":"string","default":"v3-pool"},"address":{"$ref":"#/components/schemas/Address"},"tokenIn":{"$ref":"#/components/schemas/TokenInRoute"},"tokenOut":{"$ref":"#/components/schemas/TokenInRoute"},"sqrtRatioX96":{"type":"string"},"liquidity":{"type":"string"},"tickCurrent":{"type":"string"},"fee":{"type":"string"},"amountIn":{"type":"string"},"amountOut":{"type":"string"}}},"V4PoolInRoute":{"type":"object","properties":{"type":{"type":"string","default":"v4-pool"},"address":{"$ref":"#/components/schemas/Address"},"tokenIn":{"$ref":"#/components/schemas/TokenInRoute"},"tokenOut":{"$ref":"#/components/schemas/TokenInRoute"},"sqrtRatioX96":{"type":"string"},"liquidity":{"type":"string"},"tickCurrent":{"type":"string"},"fee":{"type":"string"},"tickSpacing":{"type":"string"},"hooks":{"type":"string"},"amountIn":{"type":"string"},"amountOut":{"type":"string"}},"required":["type","address","tokenIn","tokenOut","sqrtRatioX96","liquidity","tickCurrent","fee","tickSpacing","hooks"]},"TransactionHash":{"type":"string","pattern":"^(0x)?[0-9a-fA-F]{64}$"},"ClassicInput":{"type":"object","properties":{"token":{"$ref":"#/components/schemas/Address"},"amount":{"type":"string"}}},"ClassicOutput":{"type":"object","properties":{"token":{"$ref":"#/components/schemas/Address"},"amount":{"type":"string"},"recipient":{"$ref":"#/components/schemas/Address"}}},"RequestId":{"type":"string"},"SpreadOptimization":{"type":"string","enum":["EXECUTION","PRICE"],"description":"For **Dutch Limit** orders only. When set to `EXECUTION`, quotes optimize for looser spreads at higher fill rates. When set to `PRICE`, quotes optimize for tighter spreads at lower fill rates","default":"EXECUTION"},"AutoSlippage":{"type":"string","enum":["DEFAULT"],"description":"For **Classic** swaps only. The auto slippage strategy to employ. If auto slippage is not defined then we don't compute it. If the auto slippage strategy is `DEFAULT`, then the swap will use the default slippage tolerance computation. You cannot define auto slippage and slippage tolerance at the same time. \n\n **NOTE**: slippage is in terms of trade type. If the trade type is `EXACT_INPUT`, then the slippage is in terms of the output token. If the trade type is `EXACT_OUTPUT`, then the slippage is in terms of the input token.","default":"undefined"},"RoutingPreference":{"type":"string","description":"The routing preference determines which protocol to use for the swap. If the routing preference is `UNISWAPX`, then the swap will be routed through the UniswapX Dutch Auction Protocol. If the routing preference is `CLASSIC`, then the swap will be routed through the Classic Protocol. If the routing preference is `BEST_PRICE`, then the swap will be routed through the protocol that provides the best price. When `UNIXWAPX_V2` is passed, the swap will be routed through the UniswapX V2 Dutch Auction Protocol. When `V3_ONLY` is passed, the swap will be routed ONLY through the Uniswap V3 Protocol. When `V2_ONLY` is passed, the swap will be routed ONLY through the Uniswap V2 Protocol.","enum":["CLASSIC","UNISWAPX","BEST_PRICE","BEST_PRICE_V2","UNISWAPX_V2","V3_ONLY","V2_ONLY"],"default":"BEST_PRICE"},"ProtocolItems":{"type":"string","enum":["V2","V3","V4","UNISWAPX","UNISWAPX_V2","PRIORITY"]},"TransactionRequest":{"type":"object","properties":{"to":{"$ref":"#/components/schemas/Address"},"from":{"$ref":"#/components/schemas/Address"},"data":{"type":"string","description":"The calldata for the transaction."},"value":{"type":"string","description":"The value of the transaction in terms of wei in hex format."},"gasLimit":{"type":"string"},"chainId":{"type":"integer"},"maxFeePerGas":{"type":"string"},"maxPriorityFeePerGas":{"type":"string"},"gasPrice":{"type":"string"}},"required":["to","from","data","value","chainId"]},"TransactionFailureReason":{"type":"string","enum":["SIMULATION_ERROR","UNSUPPORTED_SIMULATION"]},"SwapSafetyMode":{"type":"string","enum":["SAFE"],"description":"The safety mode determines the safety level of the swap. If the safety mode is `SAFE`, then the swap will include a SWEEP for the native token."},"IndicativeQuoteRequest":{"type":"object","properties":{"type":{"$ref":"#/components/schemas/TradeType"},"amount":{"type":"string"},"tokenInChainId":{"$ref":"#/components/schemas/ChainId"},"tokenOutChainId":{"$ref":"#/components/schemas/ChainId"},"tokenIn":{"type":"string"},"tokenOut":{"type":"string"}},"required":["type","amount","tokenInChainId","tokenOutChainId","tokenIn","tokenOut"]},"IndicativeQuoteResponse":{"type":"object","properties":{"requestId":{"$ref":"#/components/schemas/RequestId"},"input":{"$ref":"#/components/schemas/IndicativeQuoteToken"},"output":{"$ref":"#/components/schemas/IndicativeQuoteToken"},"type":{"$ref":"#/components/schemas/TradeType"}},"required":["requestId","input","output","type"]},"CreateLPPositionRequest":{"type":"object","properties":{"protocol":{"$ref":"#/components/schemas/ProtocolItems"},"position":{"$ref":"#/components/schemas/Position"},"walletAddress":{"$ref":"#/components/schemas/Address"},"chainId":{"$ref":"#/components/schemas/ChainId"},"initialPrice":{"type":"string"},"poolLiquidity":{"type":"string"},"currentTick":{"type":"number"},"sqrtRatioX96":{"type":"string"},"amount0":{"type":"string"},"amount1":{"type":"string"},"slippageTolerance":{"type":"string"},"deadline":{"type":"number"},"signature":{"type":"string","description":"The signed permit."},"batchPermitData":{"allOf":[{"$ref":"#/components/schemas/Permit"}]},"simulateTransaction":{"type":"boolean"}}},"CreateLPPositionResponse":{"type":"object","properties":{"requestId":{"$ref":"#/components/schemas/RequestId"},"create":{"$ref":"#/components/schemas/TransactionRequest"},"gasFee":{"type":"string"}}},"IncreaseLPPositionRequest":{"type":"object","properties":{"protocol":{"$ref":"#/components/schemas/ProtocolItems"},"tokenId":{"type":"number"},"position":{"$ref":"#/components/schemas/Position"},"poolLiquidity":{"type":"string"},"currentTick":{"type":"number"},"sqrtRatioX96":{"type":"string"},"walletAddress":{"$ref":"#/components/schemas/Address"},"chainId":{"$ref":"#/components/schemas/ChainId"},"amount0":{"type":"string"},"amount1":{"type":"string"},"slippageTolerance":{"type":"string"},"deadline":{"type":"number"},"signature":{"type":"string","description":"The signed permit."},"batchPermitData":{"allOf":[{"$ref":"#/components/schemas/Permit"}]},"simulateTransaction":{"type":"boolean"}}},"IncreaseLPPositionResponse":{"type":"object","properties":{"requestId":{"$ref":"#/components/schemas/RequestId"},"increase":{"$ref":"#/components/schemas/TransactionRequest"},"gasFee":{"type":"string"}}},"DecreaseLPPositionRequest":{"type":"object","properties":{"protocol":{"$ref":"#/components/schemas/ProtocolItems"},"tokenId":{"type":"number"},"position":{"$ref":"#/components/schemas/Position"},"walletAddress":{"$ref":"#/components/schemas/Address"},"chainId":{"$ref":"#/components/schemas/ChainId"},"liquidityPercentageToDecrease":{"type":"number"},"liquidity0":{"type":"string"},"liquidity1":{"type":"string"},"slippageTolerance":{"type":"string"},"poolLiquidity":{"type":"string"},"currentTick":{"type":"number"},"sqrtRatioX96":{"type":"string"},"positionLiquidity":{"type":"string"},"expectedTokenOwed0RawAmount":{"type":"string"},"expectedTokenOwed1RawAmount":{"type":"string"},"collectAsWETH":{"type":"boolean"},"deadline":{"type":"number"},"simulateTransaction":{"type":"boolean"}}},"DecreaseLPPositionResponse":{"type":"object","properties":{"requestId":{"$ref":"#/components/schemas/RequestId"},"decrease":{"$ref":"#/components/schemas/TransactionRequest"},"gasFee":{"type":"string"}}},"ClaimLPFeesRequest":{"type":"object","properties":{"protocol":{"$ref":"#/components/schemas/ProtocolItems"},"tokenId":{"type":"number"},"position":{"$ref":"#/components/schemas/Position"},"walletAddress":{"$ref":"#/components/schemas/Address"},"chainId":{"$ref":"#/components/schemas/ChainId"},"expectedTokenOwed0RawAmount":{"type":"string"},"expectedTokenOwed1RawAmount":{"type":"string"},"collectAsWETH":{"type":"boolean"},"simulateTransaction":{"type":"boolean"}}},"ClaimLPFeesResponse":{"type":"object","properties":{"requestId":{"$ref":"#/components/schemas/RequestId"},"claim":{"$ref":"#/components/schemas/TransactionRequest"},"gasFee":{"type":"string"}}},"MigrateLPPositionRequest":{"type":"object","properties":{"tokenId":{"type":"number"},"walletAddress":{"$ref":"#/components/schemas/Address"},"chainId":{"$ref":"#/components/schemas/ChainId"},"inputProtocol":{"$ref":"#/components/schemas/ProtocolItems"},"inputPosition":{"$ref":"#/components/schemas/Position"},"inputPoolLiquidity":{"type":"string"},"inputCurrentTick":{"type":"number"},"inputSqrtRatioX96":{"type":"string"},"inputPositionLiquidity":{"type":"string"},"signature":{"type":"string"},"amount0":{"type":"string"},"amount1":{"type":"string"},"outputProtocol":{"$ref":"#/components/schemas/ProtocolItems"},"outputPosition":{"$ref":"#/components/schemas/Position"},"initialPrice":{"type":"string"},"outputPoolLiquidity":{"type":"string"},"outputCurrentTick":{"type":"number"},"outputSqrtRatioX96":{"type":"string"},"expectedTokenOwed0RawAmount":{"type":"string"},"expectedTokenOwed1RawAmount":{"type":"string"},"slippageTolerance":{"type":"number"},"deadline":{"type":"number"},"signatureDeadline":{"type":"number"},"simulateTransaction":{"type":"boolean","default":false}},"required":["tokenId","chainId","walletAddress","inputProtocol","inputPosition","inputPoolLiquidity","inputCurrentTick","inputSqrtRatioX96","inputPositionLiquidity","amount0","amount1","outputProtocol","outputPosition","expectedTokenOwed0RawAmount","expectedTokenOwed1RawAmount"]},"MigrateLPPositionResponse":{"type":"object","properties":{"requestId":{"$ref":"#/components/schemas/RequestId"},"migrate":{"$ref":"#/components/schemas/TransactionRequest"},"gasFee":{"type":"string"}}},"IndicativeQuoteToken":{"type":"object","properties":{"amount":{"type":"string"},"chainId":{"$ref":"#/components/schemas/ChainId"},"token":{"$ref":"#/components/schemas/Address"}}}},"parameters":{"universalRouterVersionHeader":{"name":"x-universal-router-version","in":"header","description":"The version of the Universal Router to use for the swap journey. *MUST* be consistent throughout the API calls.","required":false,"schema":{"$ref":"#/components/schemas/UniversalRouterVersion"}},"addressParam":{"name":"address","in":"path","schema":{"$ref":"#/components/schemas/Address"},"required":true},"tokenIdParam":{"name":"tokenId","in":"path","schema":{"type":"string"},"required":true},"cursorParam":{"name":"cursor","in":"query","schema":{"type":"string"},"required":false},"limitParam":{"name":"limit","in":"query","schema":{"type":"number"},"required":false},"chainIdParam":{"name":"chainId","in":"query","schema":{"$ref":"#/components/schemas/ChainId"},"required":false},"bridgeTokenInChainIdParam":{"name":"tokenInChainId","in":"query","schema":{"$ref":"#/components/schemas/ChainId"},"required":false},"bridgeTokenOutChainIdParam":{"name":"tokenOutChainId","in":"query","schema":{"$ref":"#/components/schemas/ChainId"},"required":false},"tokenInParam":{"name":"tokenIn","in":"query","schema":{"$ref":"#/components/schemas/Address"},"required":false},"tokenOutParam":{"name":"tokenOut","in":"query","schema":{"$ref":"#/components/schemas/Address"},"required":false},"addressPathParam":{"name":"address","in":"query","schema":{"$ref":"#/components/schemas/Address"},"required":false},"orderStatusParam":{"name":"orderStatus","in":"query","description":"Filter by order status.","required":false,"schema":{"$ref":"#/components/schemas/OrderStatus"}},"orderTypeParam":{"name":"orderType","in":"query","description":"The default orderType is Dutch_V1_V2 and will grab both Dutch and Dutch_V2 orders.","required":false,"schema":{"$ref":"#/components/schemas/OrderTypeQuery"}},"orderIdParam":{"name":"orderId","in":"query","required":false,"schema":{"$ref":"#/components/schemas/OrderId"}},"orderIdsParam":{"name":"orderIds","in":"query","required":false,"description":"ids split by commas","schema":{"$ref":"#/components/schemas/OrderIds"}},"swapperParam":{"name":"swapper","in":"query","description":"Filter by swapper address.","required":false,"schema":{"$ref":"#/components/schemas/Address"}},"fillerParam":{"name":"filler","in":"query","description":"Filter by filler address.","required":false,"schema":{"$ref":"#/components/schemas/Address"}},"sortKeyParam":{"name":"sortKey","in":"query","description":"Order the query results by the sort key.","required":false,"schema":{"$ref":"#/components/schemas/SortKey"}},"sortParam":{"name":"sort","in":"query","description":"Sort query. For example: `sort=gt(UNIX_TIMESTAMP)`, `sort=between(1675872827, 1675872930)`, or `lt(1675872930)`.","required":false,"schema":{"type":"string"}},"descParam":{"description":"Sort query results by sortKey in descending order.","name":"desc","in":"query","required":false,"schema":{"type":"string"}},"transactionHashesParam":{"description":"The transaction hashes.","name":"txHashes","in":"query","required":true,"style":"form","explode":false,"schema":{"type":"array","items":{"$ref":"#/components/schemas/TransactionHash"}}}},"securitySchemes":{"apiKey":{"type":"apiKey","in":"header","name":"x-api-key"}}},"security":[{"apiKey":[]}]} \ No newline at end of file +{"openapi":"3.0.0","servers":[{"description":"Uniswap trading APIs Beta","url":"https://beta.trade-api.gateway.uniswap.org/v1"},{"description":"Uniswap trading APIs","url":"https://trade-api.gateway.uniswap.org/v1"}],"info":{"version":"1.0.0","title":"Token Trading","description":"Uniswap trading APIs for fungible tokens."},"paths":{"/check_approval":{"post":{"tags":["Approval"],"summary":"Check if token approval is required","description":"Checks if the swapper has the required approval. If the swapper does not have the required approval, then the response will include the transaction to approve the token. If the swapper has the required approval, then the response will be empty. If the parameter `includeGasInfo` is set to `true`, then the response will include the gas fee for the approval transaction.","operationId":"check_approval","security":[{"apiKey":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApprovalRequest"}}}},"responses":{"200":{"$ref":"#/components/responses/ApprovalSuccess200"},"400":{"$ref":"#/components/responses/BadRequest400"},"401":{"$ref":"#/components/responses/ApprovalUnauthorized401"},"404":{"$ref":"#/components/responses/ApprovalNotFound404"},"419":{"$ref":"#/components/responses/RateLimitedErr429"},"500":{"$ref":"#/components/responses/InternalErr500"},"504":{"$ref":"#/components/responses/Timeout504"}}}},"/quote":{"post":{"tags":["Quote"],"summary":"Get a quote","description":"Get a quote according to the provided configuration. Optionally adds a fee to the quote according to the API key being used. The fee is **ALWAYS** taken from the output token. If there is a fee and the trade is `EXACT_INPUT`, then the output amount will **NOT** include the fee subtraction. For `EXACT_INPUT` swaps, use `portionBips` to calculate the fee from the quoted amount. If there is a fee and the trade is `EXACT_OUTPUT`, then the input amount will **NOT** include the fee addition to account for the fee. For `EXACT_OUTPUT` swaps, use `portionAmount` to get the fee. \n \n We also support Wrapping and Unwrapping of native tokens on their respective chains. Wrapping and Unwrapping only works for when `routingPreference` is `CLASSIC`, `BEST_PRICE`, or `BEST_PRICE_V2`. We do not support `UNISWAPX` or `UNISWAPX_V2` for these actions.","operationId":"aggregator_quote","security":[{"apiKey":[]}],"parameters":[{"$ref":"#/components/parameters/universalRouterVersionHeader"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuoteRequest"}}}},"responses":{"200":{"$ref":"#/components/responses/QuoteSuccess200"},"400":{"$ref":"#/components/responses/BadRequest400"},"401":{"$ref":"#/components/responses/Unauthorized401"},"404":{"$ref":"#/components/responses/QuoteNotFound404"},"419":{"$ref":"#/components/responses/RateLimitedErr429"},"500":{"$ref":"#/components/responses/InternalErr500"},"504":{"$ref":"#/components/responses/Timeout504"}}}},"/order":{"post":{"tags":["Order"],"summary":"Create a gasless order","description":"Submits a new gasless encoded order. The order will be validated and if valid, will be submitted to the filler network. The network will try to fill the order at the quoted `startAmount`, and if not, the amount will start decaying until the `endAmount` is reached. While the order is within `decayEndTime`, the `orderStatus` is `open`. If the order does not get filled after the `decayEndTime` has passed, that is reflected in the `expired` `orderStatus`. then The order will be filled at the best price possible. Once the order is filled, `orderStatus` becomes `filled`.","operationId":"post_order","security":[{"apiKey":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderRequest"}}}},"responses":{"201":{"$ref":"#/components/responses/OrderSuccess201"},"400":{"$ref":"#/components/responses/BadRequest400"},"401":{"$ref":"#/components/responses/Unauthorized401"},"419":{"$ref":"#/components/responses/RateLimitedErr429"},"500":{"$ref":"#/components/responses/InternalErr500"},"504":{"$ref":"#/components/responses/Timeout504"}}}},"/orders":{"get":{"tags":["Order"],"summary":"Get gasless orders","description":"Retrieve gasless orders filtered by query param(s). Some fields on the order can be used as query param.","operationId":"get_order","security":[{"apiKey":[]}],"parameters":[{"$ref":"#/components/parameters/orderTypeParam"},{"$ref":"#/components/parameters/orderIdParam"},{"$ref":"#/components/parameters/orderIdsParam"},{"$ref":"#/components/parameters/limitParam"},{"$ref":"#/components/parameters/orderStatusParam"},{"$ref":"#/components/parameters/swapperParam"},{"$ref":"#/components/parameters/sortKeyParam"},{"$ref":"#/components/parameters/sortParam"},{"$ref":"#/components/parameters/fillerParam"},{"$ref":"#/components/parameters/cursorParam"}],"responses":{"200":{"$ref":"#/components/responses/OrdersSuccess200"},"400":{"$ref":"#/components/responses/OrdersBadRequest400"},"404":{"$ref":"#/components/responses/OrdersNotFound404"},"419":{"$ref":"#/components/responses/RateLimitedErr429"},"500":{"$ref":"#/components/responses/InternalErr500"},"504":{"$ref":"#/components/responses/Timeout504"}}}},"/swap":{"post":{"tags":["Swap"],"summary":"Create swap calldata","description":"Create the calldata for a swap transaction (including wrap/unwrap) against the Uniswap Protocols. If the `quote` parameter includes the fee parameters, then the calldata will include the fee disbursement. The gas estimates will be **more precise** when the the response calldata would be valid if submitted on-chain.","operationId":"create_swap_transaction","security":[{"apiKey":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSwapRequest"}}}},"parameters":[{"$ref":"#/components/parameters/universalRouterVersionHeader"}],"responses":{"200":{"$ref":"#/components/responses/CreateSwapSuccess200"},"400":{"$ref":"#/components/responses/BadRequest400"},"401":{"$ref":"#/components/responses/SwapUnauthorized401"},"404":{"$ref":"#/components/responses/SwapNotFound404"},"419":{"$ref":"#/components/responses/RateLimitedErr429"},"500":{"$ref":"#/components/responses/InternalErr500"},"504":{"$ref":"#/components/responses/Timeout504"}}}},"/swaps":{"get":{"tags":["Swap"],"summary":"Get swaps status","description":"Get the status of a swap or bridge transactions.","operationId":"get_swaps","security":[{"apiKey":[]}],"parameters":[{"$ref":"#/components/parameters/transactionHashesParam"},{"$ref":"#/components/parameters/chainIdParam"}],"responses":{"200":{"$ref":"#/components/responses/GetSwapsSuccess200"},"400":{"$ref":"#/components/responses/BadRequest400"},"404":{"$ref":"#/components/responses/SwapNotFound404"},"419":{"$ref":"#/components/responses/RateLimitedErr429"},"500":{"$ref":"#/components/responses/InternalErr500"},"504":{"$ref":"#/components/responses/Timeout504"}}}},"/indicative_quote":{"post":{"tags":["IndicativeQuote"],"summary":"Get an indicative quote","description":"Get an indicative quote according to the provided configuration. The quote will not include a fee.","operationId":"indicative_quote","security":[{"apiKey":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IndicativeQuoteRequest"}}}},"responses":{"200":{"$ref":"#/components/responses/IndicativeQuoteSuccess200"},"400":{"$ref":"#/components/responses/BadRequest400"},"404":{"$ref":"#/components/responses/QuoteNotFound404"},"419":{"$ref":"#/components/responses/RateLimitedErr429"},"500":{"$ref":"#/components/responses/InternalErr500"},"504":{"$ref":"#/components/responses/Timeout504"}}}},"/send":{"post":{"tags":["Send"],"summary":"Create send calldata","description":"Create the calldata for a send transaction.","operationId":"create_send","security":[{"apiKey":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSendRequest"}}}},"responses":{"200":{"$ref":"#/components/responses/CreateSendSuccess200"},"400":{"$ref":"#/components/responses/BadRequest400"},"401":{"$ref":"#/components/responses/Unauthorized401"},"404":{"$ref":"#/components/responses/SendNotFound404"},"429":{"$ref":"#/components/responses/RateLimitedErr429"},"500":{"$ref":"#/components/responses/InternalErr500"},"504":{"$ref":"#/components/responses/Timeout504"}}}},"/swappable_tokens":{"get":{"tags":["SwappableTokens"],"summary":"Get swappable tokens","description":"Get the swappable tokens for the given configuration. Either tokenIn (with tokenInChainId or (tokenInChainId and tokenOutChainId)) or tokenOut (with tokenOutChainId or (tokenOutChainId and tokenInChainId)) must be provided but not both.","operationId":"get_swappable_tokens","security":[{"apiKey":[]}],"parameters":[{"$ref":"#/components/parameters/tokenInParam"},{"$ref":"#/components/parameters/tokenOutParam"},{"$ref":"#/components/parameters/bridgeTokenInChainIdParam"},{"$ref":"#/components/parameters/bridgeTokenOutChainIdParam"}],"responses":{"200":{"$ref":"#/components/responses/GetSwappableTokensSuccess200"},"400":{"$ref":"#/components/responses/BadRequest400"},"401":{"$ref":"#/components/responses/Unauthorized401"},"404":{"$ref":"#/components/responses/QuoteNotFound404"},"429":{"$ref":"#/components/responses/RateLimitedErr429"},"500":{"$ref":"#/components/responses/InternalErr500"},"504":{"$ref":"#/components/responses/Timeout504"}}}},"/limit_order_quote":{"post":{"tags":["LimitOrderQuote"],"summary":"Get a limit order quote","description":"Get a quote for a limit order according to the provided configuration.","operationId":"get_limit_order_quote","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LimitOrderQuoteRequest"}}}},"responses":{"200":{"$ref":"#/components/responses/LimitOrderQuoteSuccess200"},"400":{"$ref":"#/components/responses/BadRequest400"},"401":{"$ref":"#/components/responses/Unauthorized401"},"404":{"$ref":"#/components/responses/QuoteNotFound404"},"419":{"$ref":"#/components/responses/RateLimitedErr429"},"500":{"$ref":"#/components/responses/InternalErr500"},"504":{"$ref":"#/components/responses/Timeout504"}}}},"/lp/approve":{"post":{"tags":["Liquidity"],"summary":"Check if tokens and permits need to be approved to add liquidity","description":"Checks if the wallet address has the required approvals. If the wallet address does not have the required approval, then the response will include the transactions to approve the tokens. If the wallet address has the required approval, then the response will be empty for the corresponding tokens. If the parameter `simulateTransaction` is set to `true`, then the response will include the gas fees for the approval transactions.","operationId":"check_approval_lp","security":[{"apiKey":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckApprovalLPRequest"}}}},"responses":{"200":{"$ref":"#/components/responses/CheckApprovalLPSuccess200"},"400":{"$ref":"#/components/responses/BadRequest400"},"401":{"$ref":"#/components/responses/ApprovalUnauthorized401"},"404":{"$ref":"#/components/responses/ApprovalNotFound404"},"419":{"$ref":"#/components/responses/RateLimitedErr429"},"500":{"$ref":"#/components/responses/InternalErr500"},"504":{"$ref":"#/components/responses/Timeout504"}}}},"/lp/create":{"post":{"tags":["Liquidity"],"summary":"Create pool and position calldata","description":"Create pool and position calldata. If the pool is not yet created, then the response will include the transaction to create the new pool with the initial price. If the pool is already created, then the response will not have the transaction to create the pool. The response will also have the transaction to create the position for the corresponding pool. If the parameter `simulateTransaction` is set to `true`, then the response will include the gas fees for the creation transactions.","operationId":"create_lp_position","security":[{"apiKey":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateLPPositionRequest"}}}},"responses":{"200":{"$ref":"#/components/responses/CreateLPPositionSuccess200"},"400":{"$ref":"#/components/responses/BadRequest400"},"401":{"$ref":"#/components/responses/ApprovalUnauthorized401"},"404":{"$ref":"#/components/responses/LPNotFound404"},"419":{"$ref":"#/components/responses/RateLimitedErr429"},"500":{"$ref":"#/components/responses/InternalErr500"},"504":{"$ref":"#/components/responses/Timeout504"}}}},"/lp/increase":{"post":{"tags":["Liquidity"],"summary":"Increase LP position calldata","description":"The response will also have the transaction to increase the position for the corresponding pool. If the parameter `simulateTransaction` is set to `true`, then the response will include the gas fees for the increase transaction.","operationId":"increase_lp_position","security":[{"apiKey":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IncreaseLPPositionRequest"}}}},"responses":{"200":{"$ref":"#/components/responses/IncreaseLPPositionSuccess200"},"400":{"$ref":"#/components/responses/BadRequest400"},"401":{"$ref":"#/components/responses/ApprovalUnauthorized401"},"404":{"$ref":"#/components/responses/LPNotFound404"},"419":{"$ref":"#/components/responses/RateLimitedErr429"},"500":{"$ref":"#/components/responses/InternalErr500"},"504":{"$ref":"#/components/responses/Timeout504"}}}},"/lp/decrease":{"post":{"tags":["Liquidity"],"summary":"Decrease LP position calldata","description":"The response will also have the transaction to decrease the position for the corresponding pool. If the parameter `simulateTransaction` is set to `true`, then the response will include the gas fees for the decrease transaction.","operationId":"decrease_lp_position","security":[{"apiKey":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DecreaseLPPositionRequest"}}}},"responses":{"200":{"$ref":"#/components/responses/DecreaseLPPositionSuccess200"},"400":{"$ref":"#/components/responses/BadRequest400"},"401":{"$ref":"#/components/responses/ApprovalUnauthorized401"},"404":{"$ref":"#/components/responses/LPNotFound404"},"419":{"$ref":"#/components/responses/RateLimitedErr429"},"500":{"$ref":"#/components/responses/InternalErr500"},"504":{"$ref":"#/components/responses/Timeout504"}}}},"/lp/claim":{"post":{"tags":["Liquidity"],"summary":"Claim LP fees calldata","description":"The response will also have the transaction to claim the fees for an LP position for the corresponding pool. If the parameter `simulateTransaction` is set to `true`, then the response will include the gas fees for the claim transaction.","operationId":"claim_lp_fees","security":[{"apiKey":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClaimLPFeesRequest"}}}},"responses":{"200":{"$ref":"#/components/responses/ClaimLPFeesSuccess200"},"400":{"$ref":"#/components/responses/BadRequest400"},"401":{"$ref":"#/components/responses/ApprovalUnauthorized401"},"404":{"$ref":"#/components/responses/LPNotFound404"},"419":{"$ref":"#/components/responses/RateLimitedErr429"},"500":{"$ref":"#/components/responses/InternalErr500"},"504":{"$ref":"#/components/responses/Timeout504"}}}},"/lp/migrate":{"post":{"tags":["Liquidity"],"summary":"Migrate LP position calldata","description":"The response will also have the transaction to migrate the position for the corresponding pool. If the parameter `simulateTransaction` is set to `true`, then the response will include the gas fees for the migrate transaction.","operationId":"migrate_lp_position","security":[{"apiKey":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MigrateLPPositionRequest"}}}},"responses":{"200":{"$ref":"#/components/responses/MigrateLPPositionSuccess200"},"400":{"$ref":"#/components/responses/BadRequest400"},"401":{"$ref":"#/components/responses/ApprovalUnauthorized401"},"404":{"$ref":"#/components/responses/LPNotFound404"},"419":{"$ref":"#/components/responses/RateLimitedErr429"},"500":{"$ref":"#/components/responses/InternalErr500"},"504":{"$ref":"#/components/responses/Timeout504"}}}}},"components":{"responses":{"OrdersSuccess200":{"description":"The request orders matching the query parameters.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetOrdersResponse"}}}},"OrderSuccess201":{"description":"Encoded order submitted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderResponse"}}}},"QuoteSuccess200":{"description":"Quote request successful.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuoteResponse"}}}},"LimitOrderQuoteSuccess200":{"description":"Limit Order Quote request successful.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LimitOrderQuoteResponse"}}}},"CheckApprovalLPSuccess200":{"description":"Approve LP successful.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckApprovalLPResponse"}}}},"ApprovalSuccess200":{"description":"Check approval successful.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApprovalResponse"}}}},"CreateSendSuccess200":{"description":"Create send successful.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSendResponse"}}}},"CreateSwapSuccess200":{"description":"Create swap successful.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSwapResponse"}}}},"GetSwapsSuccess200":{"description":"Get swap successful.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetSwapsResponse"}}}},"GetSwappableTokensSuccess200":{"description":"Get swappable tokens successful.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetSwappableTokensResponse"}}}},"CreateLPPositionSuccess200":{"description":"Create LP Position successful.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateLPPositionResponse"}}}},"IncreaseLPPositionSuccess200":{"description":"Create LP Position successful.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IncreaseLPPositionResponse"}}}},"DecreaseLPPositionSuccess200":{"description":"Decrease LP Position successful.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DecreaseLPPositionResponse"}}}},"ClaimLPFeesSuccess200":{"description":"Claim LP Fees successful.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClaimLPFeesResponse"}}}},"MigrateLPPositionSuccess200":{"description":"Migrate LP Position successful.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MigrateLPPositionResponse"}}}},"BadRequest400":{"description":"RequestValidationError, Bad Input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err400"}}}},"ApprovalUnauthorized401":{"description":"UnauthorizedError eg. Account is blocked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err401"}}}},"ApprovalNotFound404":{"description":"ResourceNotFound eg. Token allowance not found or Gas info not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err404"}}}},"Unauthorized401":{"description":"UnauthorizedError eg. Account is blocked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err401"}}}},"QuoteNotFound404":{"description":"ResourceNotFound eg. No quotes available or Gas fee/price not available","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err404"}}}},"SendNotFound404":{"description":"ResourceNotFound eg. Gas fee not available","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err404"}}}},"SwapBadRequest400":{"description":"RequestValidationError, Bad Input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err400"}}}},"SwapUnauthorized401":{"description":"UnauthorizedError eg. Account is blocked or Fee is not enabled.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err401"}}}},"SwapNotFound404":{"description":"ResourceNotFound eg. No quotes available or Gas fee/price not available","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err404"}}}},"OrdersNotFound404":{"description":"Orders not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err404"}}}},"LPNotFound404":{"description":"ResourceNotFound eg. Cant Find LP Position.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err404"}}}},"OrdersBadRequest400":{"description":"RequestValidationError eg. Token allowance not valid or Insufficient Funds.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err400"}}}},"RateLimitedErr429":{"description":"Ratelimited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err429"}}}},"InternalErr500":{"description":"Unexpected error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err500"}}}},"Timeout504":{"description":"Request duration limit reached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err504"}}}},"IndicativeQuoteSuccess200":{"description":"Indicative quote request successful.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IndicativeQuoteResponse"}}}}},"schemas":{"NullablePermit":{"allOf":[{"$ref":"#/components/schemas/Permit"},{"type":"object","nullable":true}]},"TokenAmount":{"type":"string"},"SwapStatus":{"type":"string","enum":["PENDING","SUCCESS","NOT_FOUND","FAILED","EXPIRED"]},"GetSwapsResponse":{"type":"object","properties":{"requestId":{"$ref":"#/components/schemas/RequestId"},"swaps":{"type":"array","items":{"type":"object","properties":{"swapType":{"$ref":"#/components/schemas/Routing"},"status":{"$ref":"#/components/schemas/SwapStatus"},"txHash":{"type":"string"},"swapId":{"type":"number"}}}}},"required":["requestId","status"]},"GetSwappableTokensResponse":{"type":"object","properties":{"requestId":{"$ref":"#/components/schemas/RequestId"},"tokens":{"type":"array","items":{"type":"object","properties":{"address":{"$ref":"#/components/schemas/Address"},"chainId":{"$ref":"#/components/schemas/ChainId"},"name":{"type":"string"},"symbol":{"type":"string"},"project":{"$ref":"#/components/schemas/TokenProject"},"isSpam":{"type":"boolean"},"decimals":{"type":"number"}},"required":["address","chainId","name","symbol","project","decimals"]}}},"required":["requestId","tokens"]},"CreateSwapRequest":{"type":"object","description":"The parameters **signature** and **permitData** should only be included if *permitData* was returned from **/quote**.","properties":{"quote":{"oneOf":[{"$ref":"#/components/schemas/ClassicQuote"},{"$ref":"#/components/schemas/WrapUnwrapQuote"},{"$ref":"#/components/schemas/BridgeQuote"}]},"signature":{"type":"string","description":"The signed permit."},"includeGasInfo":{"type":"boolean","default":false,"deprecated":true,"description":"Use `refreshGasPrice` instead."},"refreshGasPrice":{"type":"boolean","default":false,"description":"If true, the gas price will be re-fetched from the network."},"simulateTransaction":{"type":"boolean","default":false,"description":"If true, the transaction will be simulated. If the simulation results on an onchain error, endpoint will return an error."},"permitData":{"allOf":[{"$ref":"#/components/schemas/Permit"}]},"safetyMode":{"$ref":"#/components/schemas/SwapSafetyMode"},"deadline":{"type":"integer","description":"The deadline for the swap in unix timestamp format. If the deadline is not defined OR in the past then the default deadline is 30 minutes."},"urgency":{"$ref":"#/components/schemas/Urgency"}},"required":["quote"]},"CreateSendRequest":{"type":"object","properties":{"sender":{"$ref":"#/components/schemas/Address"},"recipient":{"$ref":"#/components/schemas/Address"},"token":{"$ref":"#/components/schemas/Address"},"amount":{"$ref":"#/components/schemas/TokenAmount"},"chainId":{"$ref":"#/components/schemas/ChainId"},"urgency":{"$ref":"#/components/schemas/Urgency"}},"required":["sender","recipient","token","amount"]},"UniversalRouterVersion":{"type":"string","enum":["1.2","2.0"],"default":"1.2"},"Address":{"type":"string","pattern":"^(0x)?[0-9a-fA-F]{40}$"},"Position":{"type":"object","properties":{"pool":{"$ref":"#/components/schemas/Pool"},"tickLower":{"type":"number"},"tickUpper":{"type":"number"}},"required":["pool"]},"Pool":{"type":"object","properties":{"token0":{"$ref":"#/components/schemas/Address"},"token1":{"$ref":"#/components/schemas/Address"},"fee":{"type":"number"},"tickSpacing":{"type":"number"},"hooks":{"$ref":"#/components/schemas/Address"}},"required":["token0","token1"]},"ClassicGasUseEstimateUSD":{"description":"The gas fee you would pay if you opted for a CLASSIC swap over a Uniswap X order in terms of USD.","type":"string"},"CreateSwapResponse":{"type":"object","properties":{"requestId":{"$ref":"#/components/schemas/RequestId"},"swap":{"$ref":"#/components/schemas/TransactionRequest"},"gasFee":{"type":"string"}},"required":["requestId","swap"]},"CreateSendResponse":{"type":"object","properties":{"requestId":{"$ref":"#/components/schemas/RequestId"},"send":{"$ref":"#/components/schemas/TransactionRequest"},"gasFee":{"type":"string"},"gasFeeUSD":{"type":"number"}},"required":["requestId","send"]},"QuoteResponse":{"type":"object","properties":{"requestId":{"$ref":"#/components/schemas/RequestId"},"quote":{"$ref":"#/components/schemas/Quote"},"routing":{"$ref":"#/components/schemas/Routing"},"permitData":{"$ref":"#/components/schemas/NullablePermit"}},"required":["routing","quote","permitData","requestId"]},"LimitOrderQuoteResponse":{"type":"object","properties":{"requestId":{"$ref":"#/components/schemas/RequestId"},"quote":{"$ref":"#/components/schemas/DutchQuote"},"routing":{"type":"string","enum":["LIMIT_ORDER"]},"permitData":{"$ref":"#/components/schemas/NullablePermit"}},"required":["routing","quote","permitData","requestId"]},"QuoteRequest":{"type":"object","properties":{"type":{"$ref":"#/components/schemas/TradeType"},"amount":{"type":"string"},"tokenInChainId":{"$ref":"#/components/schemas/ChainId"},"tokenOutChainId":{"$ref":"#/components/schemas/ChainId"},"tokenIn":{"type":"string"},"tokenOut":{"type":"string"},"swapper":{"$ref":"#/components/schemas/Address"},"slippageTolerance":{"description":"For **Classic** swaps, the slippage tolerance is the maximum amount the price can change between the time the transaction is submitted and the time it is executed. The slippage tolerance is represented as a percentage of the total value of the swap. \n\n Slippage tolerance works differently in **DutchLimit** swaps, it does not set a limit on the Spread in an order. See [here](https://uniswap-docs.readme.io/reference/faqs#why-do-the-uniswapx-quotes-have-more-slippage-than-the-tolerance-i-set) for more information. \n\n **NOTE**: slippage is in terms of trade type. If the trade type is `EXACT_INPUT`, then the slippage is in terms of the output token. If the trade type is `EXACT_OUTPUT`, then the slippage is in terms of the input token.","type":"number"},"autoSlippage":{"$ref":"#/components/schemas/AutoSlippage"},"routingPreference":{"$ref":"#/components/schemas/RoutingPreference"},"protocols":{"$ref":"#/components/schemas/Protocols"},"spreadOptimization":{"$ref":"#/components/schemas/SpreadOptimization"},"urgency":{"$ref":"#/components/schemas/Urgency"}},"required":["type","amount","tokenInChainId","tokenOutChainId","tokenIn","tokenOut","swapper"]},"LimitOrderQuoteRequest":{"type":"object","properties":{"swapper":{"$ref":"#/components/schemas/Address"},"limitPrice":{"type":"string"},"amount":{"type":"string"},"orderDeadline":{"type":"number"},"type":{"$ref":"#/components/schemas/TradeType"},"tokenIn":{"type":"string"},"tokenOut":{"type":"string"},"tokenInChainId":{"$ref":"#/components/schemas/ChainId"},"tokenOutChainId":{"$ref":"#/components/schemas/ChainId"}},"required":["swapper","type","amount","tokenIn","tokenOut","tokenInChainId","tokenOutChainId"]},"GetOrdersResponse":{"type":"object","properties":{"requestId":{"$ref":"#/components/schemas/RequestId"},"orders":{"type":"array","items":{"$ref":"#/components/schemas/UniswapXOrder"}},"cursor":{"type":"string"}},"required":["orders","requestId"]},"OrderResponse":{"type":"object","properties":{"requestId":{"$ref":"#/components/schemas/RequestId"},"orderId":{"type":"string"},"orderStatus":{"$ref":"#/components/schemas/OrderStatus"}},"required":["requestId","orderId","orderStatus"]},"OrderRequest":{"type":"object","properties":{"signature":{"type":"string","description":"The signed permit."},"quote":{"oneOf":[{"$ref":"#/components/schemas/DutchQuote"},{"$ref":"#/components/schemas/DutchQuoteV2"},{"$ref":"#/components/schemas/PriorityQuote"}]},"routing":{"$ref":"#/components/schemas/Routing"}},"required":["signature","quote"]},"Urgency":{"type":"string","enum":["normal","fast","urgent"],"description":"The urgency determines the urgency of the transaction. The default value is `urgent`.","default":"urgent"},"Protocols":{"type":"array","items":{"$ref":"#/components/schemas/ProtocolItems"},"description":"The protocols to use for the swap/order. If the `protocols` field is defined, then you can only set the `routingPreference` to `BEST_PRICE`"},"Err400":{"type":"object","properties":{"errorCode":{"default":"RequestValidationError","type":"string"},"detail":{"type":"string"}}},"Err401":{"type":"object","properties":{"errorCode":{"default":"UnauthorizedError","type":"string"},"detail":{"type":"string"}}},"Err404":{"type":"object","properties":{"errorCode":{"enum":["ResourceNotFound","QuoteAmountTooLowError"],"type":"string"},"detail":{"type":"string"}}},"Err429":{"type":"object","properties":{"errorCode":{"default":"Ratelimited","type":"string"},"detail":{"type":"string"}}},"Err500":{"type":"object","properties":{"errorCode":{"default":"InternalServerError","type":"string"},"detail":{"type":"string"}}},"Err504":{"type":"object","properties":{"errorCode":{"default":"Timeout","type":"string"},"detail":{"type":"string"}}},"ChainId":{"type":"number","enum":[1,10,56,137,8453,42161,81457,43114,42220,7777777,324,11155111,1301,480]},"OrderInput":{"type":"object","properties":{"token":{"type":"string"},"startAmount":{"type":"string"},"endAmount":{"type":"string"}},"required":["token"]},"OrderOutput":{"type":"object","properties":{"token":{"type":"string"},"startAmount":{"type":"string"},"endAmount":{"type":"string"},"isFeeOutput":{"type":"boolean"},"recipient":{"type":"string"}},"required":["token"]},"CosignerData":{"type":"object","properties":{"decayStartTime":{"type":"number"},"decayEndTime":{"type":"number"},"exclusiveFiller":{"type":"string"},"inputOverride":{"type":"string"},"outputOverrides":{"type":"array","items":{"type":"string"}}}},"SettledAmount":{"type":"object","properties":{"tokenOut":{"$ref":"#/components/schemas/Address"},"amountOut":{"type":"string"},"tokenIn":{"$ref":"#/components/schemas/Address"},"amountIn":{"type":"string"}}},"OrderType":{"type":"string","enum":["DutchLimit","Dutch","Dutch_V2"]},"OrderTypeQuery":{"type":"string","enum":["Dutch","Dutch_V2","Dutch_V1_V2","Limit","Priority"]},"UniswapXOrder":{"type":"object","properties":{"type":{"$ref":"#/components/schemas/OrderType"},"encodedOrder":{"type":"string"},"signature":{"type":"string"},"nonce":{"type":"string"},"orderStatus":{"$ref":"#/components/schemas/OrderStatus"},"orderId":{"type":"string"},"chainId":{"$ref":"#/components/schemas/ChainId"},"quoteId":{"type":"string"},"swapper":{"type":"string"},"txHash":{"type":"string"},"input":{"$ref":"#/components/schemas/OrderInput"},"outputs":{"type":"array","items":{"$ref":"#/components/schemas/OrderOutput"}},"settledAmounts":{"type":"array","items":{"$ref":"#/components/schemas/SettledAmount"}},"cosignature":{"type":"string"},"cosignerData":{"$ref":"#/components/schemas/CosignerData"}},"required":["encodedOrder","signature","nonce","orderId","orderStatus","chainId","type"]},"SortKey":{"type":"string","enum":["createdAt"]},"OrderId":{"type":"string"},"OrderIds":{"type":"string"},"OrderStatus":{"type":"string","enum":["open","expired","error","cancelled","filled","unverified","insufficient-funds"]},"Permit":{"type":"object","properties":{"domain":{"type":"object"},"values":{"type":"object"},"types":{"type":"object"}}},"TokenProject":{"type":"object","properties":{"logo":{"$ref":"#/components/schemas/TokenProjectLogo","nullable":true},"safetyLevel":{"$ref":"#/components/schemas/SafetyLevel"},"isSpam":{"type":"boolean"}},"required":["logo","safetyLevel","isSpam"]},"TokenProjectLogo":{"type":"object","properties":{"url":{"type":"string"}},"required":["url"]},"DutchInput":{"type":"object","properties":{"startAmount":{"type":"string"},"endAmount":{"type":"string"},"token":{"type":"string"}},"required":["startAmount","endAmount","type"]},"DutchOutput":{"type":"object","properties":{"startAmount":{"type":"string"},"endAmount":{"type":"string"},"token":{"type":"string"},"recipient":{"type":"string"}},"required":["startAmount","endAmount","token","recipient"]},"DutchOrderInfo":{"type":"object","properties":{"chainId":{"$ref":"#/components/schemas/ChainId"},"nonce":{"type":"string"},"reactor":{"type":"string"},"swapper":{"type":"string"},"deadline":{"type":"number"},"additionalValidationContract":{"type":"string"},"additionalValidationData":{"type":"string"},"decayStartTime":{"type":"number"},"decayEndTime":{"type":"number"},"exclusiveFiller":{"type":"string"},"exclusivityOverrideBps":{"type":"string"},"input":{"$ref":"#/components/schemas/DutchInput"},"outputs":{"type":"array","items":{"$ref":"#/components/schemas/DutchOutput"}}},"required":["chainId","nonce","reactor","swapper","deadline","validationContract","validationData","startTime","endTime","exclusiveFiller","exclusivityOverrideBps","input","outputs"]},"DutchOrderInfoV2":{"type":"object","properties":{"chainId":{"$ref":"#/components/schemas/ChainId"},"nonce":{"type":"string"},"reactor":{"type":"string"},"swapper":{"type":"string"},"deadline":{"type":"number"},"additionalValidationContract":{"type":"string"},"additionalValidationData":{"type":"string"},"input":{"$ref":"#/components/schemas/DutchInput"},"outputs":{"type":"array","items":{"$ref":"#/components/schemas/DutchOutput"}},"cosigner":{"$ref":"#/components/schemas/Address"}},"required":["chainId","nonce","reactor","swapper","deadline","validationContract","validationData","startTime","endTime","exclusiveFiller","exclusivityOverrideBps","input","outputs"]},"DutchQuote":{"type":"object","properties":{"encodedOrder":{"type":"string"},"orderId":{"type":"string"},"orderInfo":{"$ref":"#/components/schemas/DutchOrderInfo"},"portionBips":{"type":"number"},"portionAmount":{"type":"string"},"portionRecipient":{"$ref":"#/components/schemas/Address"},"quoteId":{"type":"string"},"slippageTolerance":{"type":"number"},"classicGasUseEstimateUSD":{"$ref":"#/components/schemas/ClassicGasUseEstimateUSD"}},"required":["encodedOrder","orderInfo","orderId"]},"DutchQuoteV2":{"type":"object","properties":{"encodedOrder":{"type":"string"},"orderId":{"type":"string"},"orderInfo":{"$ref":"#/components/schemas/DutchOrderInfoV2"},"portionBips":{"type":"number"},"portionAmount":{"type":"string"},"portionRecipient":{"$ref":"#/components/schemas/Address"},"quoteId":{"type":"string"},"slippageTolerance":{"type":"number"},"deadlineBufferSecs":{"type":"number"},"classicGasUseEstimateUSD":{"$ref":"#/components/schemas/ClassicGasUseEstimateUSD"}},"required":["encodedOrder","orderInfo","orderId"]},"PriorityInput":{"type":"object","properties":{"amount":{"type":"string"},"token":{"type":"string"},"mpsPerPriorityFeeWei":{"type":"string"}},"required":["amount","token","mpsPerPriorityFeeWei"]},"PriorityOutput":{"type":"object","properties":{"amount":{"type":"string"},"token":{"type":"string"},"recipient":{"type":"string"},"mpsPerPriorityFeeWei":{"type":"string"}},"required":["amount","token","recipient","mpsPerPriorityFeeWei"]},"PriorityOrderInfo":{"type":"object","properties":{"chainId":{"$ref":"#/components/schemas/ChainId"},"nonce":{"type":"string"},"reactor":{"type":"string"},"swapper":{"type":"string"},"deadline":{"type":"number"},"additionalValidationContract":{"type":"string"},"additionalValidationData":{"type":"string"},"auctionStartBlock":{"type":"string"},"baselinePriorityFeeWei":{"type":"string"},"input":{"$ref":"#/components/schemas/PriorityInput"},"outputs":{"type":"array","items":{"$ref":"#/components/schemas/PriorityOutput"}},"cosigner":{"$ref":"#/components/schemas/Address"}},"required":["chainId","nonce","reactor","swapper","deadline","validationContract","validationData","auctionStartBlock","baselinePriorityFeeWei","input","outputs","cosigner"]},"PriorityQuote":{"type":"object","properties":{"encodedOrder":{"type":"string"},"orderId":{"type":"string"},"orderInfo":{"$ref":"#/components/schemas/PriorityOrderInfo"},"portionBips":{"type":"number"},"portionAmount":{"type":"string"},"portionRecipient":{"$ref":"#/components/schemas/Address"},"quoteId":{"type":"string"},"slippageTolerance":{"type":"number"},"deadlineBufferSecs":{"type":"number"},"classicGasUseEstimateUSD":{"$ref":"#/components/schemas/ClassicGasUseEstimateUSD"},"expectedAmountIn":{"type":"string"},"expectedAmountOut":{"type":"string"}},"required":["encodedOrder","orderInfo","orderId"]},"BridgeQuote":{"type":"object","properties":{"quoteId":{"type":"string"},"chainId":{"$ref":"#/components/schemas/ChainId"},"destinationChainId":{"$ref":"#/components/schemas/ChainId"},"swapper":{"$ref":"#/components/schemas/Address"},"input":{"$ref":"#/components/schemas/ClassicInput"},"output":{"$ref":"#/components/schemas/ClassicOutput"},"tradeType":{"$ref":"#/components/schemas/TradeType"},"quoteTimestamp":{"type":"number"},"gasPrice":{"type":"string"},"maxFeePerGas":{"type":"string"},"maxPriorityFeePerGas":{"type":"string"},"gasFee":{"type":"string"},"gasUseEstimate":{"type":"string"},"gasFeeUSD":{"type":"string"},"portionBips":{"type":"number"},"portionAmount":{"type":"string"},"portionRecipient":{"$ref":"#/components/schemas/Address"},"estimatedFillTimeMs":{"type":"number"}}},"SafetyLevel":{"type":"string","enum":["BLOCKED","MEDIUM_WARNING","STRONG_WARNING","VERIFIED"]},"TradeType":{"type":"string","enum":["EXACT_INPUT","EXACT_OUTPUT"]},"Routing":{"type":"string","enum":["DUTCH_LIMIT","CLASSIC","DUTCH_V2","BRIDGE","LIMIT_ORDER","PRIORITY"]},"Quote":{"oneOf":[{"$ref":"#/components/schemas/DutchQuote"},{"$ref":"#/components/schemas/ClassicQuote"},{"$ref":"#/components/schemas/WrapUnwrapQuote"},{"$ref":"#/components/schemas/DutchQuoteV2"},{"$ref":"#/components/schemas/BridgeQuote"},{"$ref":"#/components/schemas/PriorityQuote"}]},"CheckApprovalLPRequest":{"type":"object","properties":{"protocol":{"$ref":"#/components/schemas/ProtocolItems"},"token0":{"$ref":"#/components/schemas/Address"},"token1":{"$ref":"#/components/schemas/Address"},"positionToken":{"$ref":"#/components/schemas/Address"},"chainId":{"$ref":"#/components/schemas/ChainId"},"walletAddress":{"$ref":"#/components/schemas/Address"},"amount0":{"type":"string"},"amount1":{"type":"string"},"positionAmount":{"type":"string"},"simulateTransaction":{"type":"boolean"}}},"CheckApprovalLPResponse":{"type":"object","properties":{"requestId":{"$ref":"#/components/schemas/RequestId"},"token0Approval":{"$ref":"#/components/schemas/TransactionRequest"},"token1Approval":{"$ref":"#/components/schemas/TransactionRequest"},"positionTokenApproval":{"$ref":"#/components/schemas/TransactionRequest"},"permitData":{"$ref":"#/components/schemas/NullablePermit"},"gasFeeToken0Approval":{"type":"string"},"gasFeeToken1Approval":{"type":"string"},"gasFeePositionTokenApproval":{"type":"string"}}},"ApprovalRequest":{"type":"object","properties":{"walletAddress":{"$ref":"#/components/schemas/Address"},"token":{"$ref":"#/components/schemas/Address"},"amount":{"$ref":"#/components/schemas/TokenAmount"},"chainId":{"$ref":"#/components/schemas/ChainId"},"urgency":{"$ref":"#/components/schemas/Urgency"},"includeGasInfo":{"type":"boolean","default":false},"tokenOut":{"$ref":"#/components/schemas/Address"},"tokenOutChainId":{"$ref":"#/components/schemas/ChainId"}},"required":["walletAddress","token","amount"]},"ApprovalResponse":{"type":"object","properties":{"requestId":{"$ref":"#/components/schemas/RequestId"},"approval":{"$ref":"#/components/schemas/TransactionRequest"},"cancel":{"$ref":"#/components/schemas/TransactionRequest"},"gasFee":{"type":"string"},"cancelGasFee":{"type":"string"}},"required":["requestId","approval","cancel"]},"ClassicQuote":{"type":"object","properties":{"input":{"$ref":"#/components/schemas/ClassicInput"},"output":{"$ref":"#/components/schemas/ClassicOutput"},"swapper":{"$ref":"#/components/schemas/Address"},"chainId":{"$ref":"#/components/schemas/ChainId"},"slippage":{"type":"number"},"tradeType":{"$ref":"#/components/schemas/TradeType"},"gasFee":{"type":"string","description":"The gas fee in terms of wei. It does NOT include the additional gas for token approvals."},"gasFeeUSD":{"type":"string","description":"The gas fee in terms of USD. It does NOT include the additional gas for token approvals."},"gasFeeQuote":{"type":"string","description":"The gas fee in terms of the quoted currency. It does NOT include the additional gas for token approvals."},"route":{"type":"array","items":{"type":"array","items":{"oneOf":[{"$ref":"#/components/schemas/V3PoolInRoute"},{"$ref":"#/components/schemas/V2PoolInRoute"},{"$ref":"#/components/schemas/V4PoolInRoute"}]}}},"portionBips":{"type":"number","description":"The portion of the swap that will be taken as a fee. The fee will be taken from the output token."},"portionAmount":{"type":"string","description":"The amount of the swap that will be taken as a fee. The fee will be taken from the output token."},"portionRecipient":{"$ref":"#/components/schemas/Address"},"routeString":{"type":"string","description":"The route in string format."},"quoteId":{"type":"string","description":"The quote id. Used for analytics purposes."},"gasUseEstimate":{"type":"string","description":"The estimated gas use. It does NOT include the additional gas for token approvals."},"blockNumber":{"type":"string","description":"The current block number."},"gasPrice":{"type":"string","description":"The gas price in terms of wei for pre EIP1559 transactions."},"maxFeePerGas":{"type":"string","description":"The maximum fee per gas in terms of wei for EIP1559 transactions."},"maxPriorityFeePerGas":{"type":"string","description":"The maximum priority fee per gas in terms of wei for EIP1559 transactions."},"txFailureReasons":{"type":"array","items":{"$ref":"#/components/schemas/TransactionFailureReason"}},"priceImpact":{"type":"number","description":"The impact the trade has on the market price of the pool, between 0-100 percent"}}},"WrapUnwrapQuote":{"type":"object","properties":{"swapper":{"$ref":"#/components/schemas/Address"},"input":{"$ref":"#/components/schemas/ClassicInput"},"output":{"$ref":"#/components/schemas/ClassicOutput"},"chainId":{"$ref":"#/components/schemas/ChainId"},"tradeType":{"$ref":"#/components/schemas/TradeType"},"gasFee":{"type":"string","description":"The gas fee in terms of wei."},"gasFeeUSD":{"type":"string","description":"The gas fee in terms of USD."},"gasFeeQuote":{"type":"string","description":"The gas fee in terms of the quoted currency."},"gasUseEstimate":{"type":"string","description":"The estimated gas use."},"gasPrice":{"type":"string","description":"The gas price in terms of wei for pre EIP1559 transactions."},"maxFeePerGas":{"type":"string","description":"The maximum fee per gas in terms of wei for EIP1559 transactions."},"maxPriorityFeePerGas":{"type":"string","description":"The maximum priority fee per gas in terms of wei for EIP1559 transactions."}}},"TokenInRoute":{"type":"object","properties":{"address":{"$ref":"#/components/schemas/Address"},"chainId":{"$ref":"#/components/schemas/ChainId"},"symbol":{"type":"string"},"decimals":{"type":"string"},"buyFeeBps":{"type":"string"},"sellFeeBps":{"type":"string"}}},"V2Reserve":{"type":"object","properties":{"token":{"$ref":"#/components/schemas/TokenInRoute"},"quotient":{"type":"string"}}},"V2PoolInRoute":{"type":"object","properties":{"type":{"type":"string","default":"v2-pool"},"address":{"$ref":"#/components/schemas/Address"},"tokenIn":{"$ref":"#/components/schemas/TokenInRoute"},"tokenOut":{"$ref":"#/components/schemas/TokenInRoute"},"reserve0":{"$ref":"#/components/schemas/V2Reserve"},"reserve1":{"$ref":"#/components/schemas/V2Reserve"},"amountIn":{"type":"string"},"amountOut":{"type":"string"}}},"V3PoolInRoute":{"type":"object","properties":{"type":{"type":"string","default":"v3-pool"},"address":{"$ref":"#/components/schemas/Address"},"tokenIn":{"$ref":"#/components/schemas/TokenInRoute"},"tokenOut":{"$ref":"#/components/schemas/TokenInRoute"},"sqrtRatioX96":{"type":"string"},"liquidity":{"type":"string"},"tickCurrent":{"type":"string"},"fee":{"type":"string"},"amountIn":{"type":"string"},"amountOut":{"type":"string"}}},"V4PoolInRoute":{"type":"object","properties":{"type":{"type":"string","default":"v4-pool"},"address":{"$ref":"#/components/schemas/Address"},"tokenIn":{"$ref":"#/components/schemas/TokenInRoute"},"tokenOut":{"$ref":"#/components/schemas/TokenInRoute"},"sqrtRatioX96":{"type":"string"},"liquidity":{"type":"string"},"tickCurrent":{"type":"string"},"fee":{"type":"string"},"tickSpacing":{"type":"string"},"hooks":{"type":"string"},"amountIn":{"type":"string"},"amountOut":{"type":"string"}},"required":["type","address","tokenIn","tokenOut","sqrtRatioX96","liquidity","tickCurrent","fee","tickSpacing","hooks"]},"TransactionHash":{"type":"string","pattern":"^(0x)?[0-9a-fA-F]{64}$"},"ClassicInput":{"type":"object","properties":{"token":{"$ref":"#/components/schemas/Address"},"amount":{"type":"string"}}},"ClassicOutput":{"type":"object","properties":{"token":{"$ref":"#/components/schemas/Address"},"amount":{"type":"string"},"recipient":{"$ref":"#/components/schemas/Address"}}},"RequestId":{"type":"string"},"SpreadOptimization":{"type":"string","enum":["EXECUTION","PRICE"],"description":"For **Dutch Limit** orders only. When set to `EXECUTION`, quotes optimize for looser spreads at higher fill rates. When set to `PRICE`, quotes optimize for tighter spreads at lower fill rates","default":"EXECUTION"},"AutoSlippage":{"type":"string","enum":["DEFAULT"],"description":"For **Classic** swaps only. The auto slippage strategy to employ. If auto slippage is not defined then we don't compute it. If the auto slippage strategy is `DEFAULT`, then the swap will use the default slippage tolerance computation. You cannot define auto slippage and slippage tolerance at the same time. \n\n **NOTE**: slippage is in terms of trade type. If the trade type is `EXACT_INPUT`, then the slippage is in terms of the output token. If the trade type is `EXACT_OUTPUT`, then the slippage is in terms of the input token.","default":"undefined"},"RoutingPreference":{"type":"string","description":"The routing preference determines which protocol to use for the swap. If the routing preference is `UNISWAPX`, then the swap will be routed through the UniswapX Dutch Auction Protocol. If the routing preference is `CLASSIC`, then the swap will be routed through the Classic Protocol. If the routing preference is `BEST_PRICE`, then the swap will be routed through the protocol that provides the best price. When `UNIXWAPX_V2` is passed, the swap will be routed through the UniswapX V2 Dutch Auction Protocol. When `V3_ONLY` is passed, the swap will be routed ONLY through the Uniswap V3 Protocol. When `V2_ONLY` is passed, the swap will be routed ONLY through the Uniswap V2 Protocol.","enum":["CLASSIC","UNISWAPX","BEST_PRICE","BEST_PRICE_V2","UNISWAPX_V2","V3_ONLY","V2_ONLY"],"default":"BEST_PRICE"},"ProtocolItems":{"type":"string","enum":["V2","V3","V4","UNISWAPX","UNISWAPX_V2","PRIORITY"]},"TransactionRequest":{"type":"object","properties":{"to":{"$ref":"#/components/schemas/Address"},"from":{"$ref":"#/components/schemas/Address"},"data":{"type":"string","description":"The calldata for the transaction."},"value":{"type":"string","description":"The value of the transaction in terms of wei in hex format."},"gasLimit":{"type":"string"},"chainId":{"type":"integer"},"maxFeePerGas":{"type":"string"},"maxPriorityFeePerGas":{"type":"string"},"gasPrice":{"type":"string"}},"required":["to","from","data","value","chainId"]},"TransactionFailureReason":{"type":"string","enum":["SIMULATION_ERROR","UNSUPPORTED_SIMULATION"]},"SwapSafetyMode":{"type":"string","enum":["SAFE"],"description":"The safety mode determines the safety level of the swap. If the safety mode is `SAFE`, then the swap will include a SWEEP for the native token."},"IndicativeQuoteRequest":{"type":"object","properties":{"type":{"$ref":"#/components/schemas/TradeType"},"amount":{"type":"string"},"tokenInChainId":{"$ref":"#/components/schemas/ChainId"},"tokenOutChainId":{"$ref":"#/components/schemas/ChainId"},"tokenIn":{"type":"string"},"tokenOut":{"type":"string"}},"required":["type","amount","tokenInChainId","tokenOutChainId","tokenIn","tokenOut"]},"IndicativeQuoteResponse":{"type":"object","properties":{"requestId":{"$ref":"#/components/schemas/RequestId"},"input":{"$ref":"#/components/schemas/IndicativeQuoteToken"},"output":{"$ref":"#/components/schemas/IndicativeQuoteToken"},"type":{"$ref":"#/components/schemas/TradeType"}},"required":["requestId","input","output","type"]},"CreateLPPositionRequest":{"type":"object","properties":{"protocol":{"$ref":"#/components/schemas/ProtocolItems"},"position":{"$ref":"#/components/schemas/Position"},"walletAddress":{"$ref":"#/components/schemas/Address"},"chainId":{"$ref":"#/components/schemas/ChainId"},"initialPrice":{"type":"string"},"poolLiquidity":{"type":"string"},"currentTick":{"type":"number"},"sqrtRatioX96":{"type":"string"},"amount0":{"type":"string"},"amount1":{"type":"string"},"slippageTolerance":{"type":"string"},"deadline":{"type":"number"},"signature":{"type":"string","description":"The signed permit."},"batchPermitData":{"allOf":[{"$ref":"#/components/schemas/Permit"}]},"simulateTransaction":{"type":"boolean"}}},"CreateLPPositionResponse":{"type":"object","properties":{"requestId":{"$ref":"#/components/schemas/RequestId"},"create":{"$ref":"#/components/schemas/TransactionRequest"},"gasFee":{"type":"string"}}},"IncreaseLPPositionRequest":{"type":"object","properties":{"protocol":{"$ref":"#/components/schemas/ProtocolItems"},"tokenId":{"type":"number"},"position":{"$ref":"#/components/schemas/Position"},"poolLiquidity":{"type":"string"},"currentTick":{"type":"number"},"sqrtRatioX96":{"type":"string"},"walletAddress":{"$ref":"#/components/schemas/Address"},"chainId":{"$ref":"#/components/schemas/ChainId"},"amount0":{"type":"string"},"amount1":{"type":"string"},"slippageTolerance":{"type":"string"},"deadline":{"type":"number"},"signature":{"type":"string","description":"The signed permit."},"batchPermitData":{"allOf":[{"$ref":"#/components/schemas/Permit"}]},"simulateTransaction":{"type":"boolean"}}},"IncreaseLPPositionResponse":{"type":"object","properties":{"requestId":{"$ref":"#/components/schemas/RequestId"},"increase":{"$ref":"#/components/schemas/TransactionRequest"},"gasFee":{"type":"string"}}},"DecreaseLPPositionRequest":{"type":"object","properties":{"protocol":{"$ref":"#/components/schemas/ProtocolItems"},"tokenId":{"type":"number"},"position":{"$ref":"#/components/schemas/Position"},"walletAddress":{"$ref":"#/components/schemas/Address"},"chainId":{"$ref":"#/components/schemas/ChainId"},"liquidityPercentageToDecrease":{"type":"number"},"liquidity0":{"type":"string"},"liquidity1":{"type":"string"},"slippageTolerance":{"type":"string"},"poolLiquidity":{"type":"string"},"currentTick":{"type":"number"},"sqrtRatioX96":{"type":"string"},"positionLiquidity":{"type":"string"},"expectedTokenOwed0RawAmount":{"type":"string"},"expectedTokenOwed1RawAmount":{"type":"string"},"collectAsWETH":{"type":"boolean"},"deadline":{"type":"number"},"simulateTransaction":{"type":"boolean"}}},"DecreaseLPPositionResponse":{"type":"object","properties":{"requestId":{"$ref":"#/components/schemas/RequestId"},"decrease":{"$ref":"#/components/schemas/TransactionRequest"},"gasFee":{"type":"string"}}},"ClaimLPFeesRequest":{"type":"object","properties":{"protocol":{"$ref":"#/components/schemas/ProtocolItems"},"tokenId":{"type":"number"},"position":{"$ref":"#/components/schemas/Position"},"walletAddress":{"$ref":"#/components/schemas/Address"},"chainId":{"$ref":"#/components/schemas/ChainId"},"expectedTokenOwed0RawAmount":{"type":"string"},"expectedTokenOwed1RawAmount":{"type":"string"},"collectAsWETH":{"type":"boolean"},"simulateTransaction":{"type":"boolean"}}},"ClaimLPFeesResponse":{"type":"object","properties":{"requestId":{"$ref":"#/components/schemas/RequestId"},"claim":{"$ref":"#/components/schemas/TransactionRequest"},"gasFee":{"type":"string"}}},"MigrateLPPositionRequest":{"type":"object","properties":{"tokenId":{"type":"number"},"walletAddress":{"$ref":"#/components/schemas/Address"},"chainId":{"$ref":"#/components/schemas/ChainId"},"inputProtocol":{"$ref":"#/components/schemas/ProtocolItems"},"inputPosition":{"$ref":"#/components/schemas/Position"},"inputPoolLiquidity":{"type":"string"},"inputCurrentTick":{"type":"number"},"inputSqrtRatioX96":{"type":"string"},"inputPositionLiquidity":{"type":"string"},"signature":{"type":"string"},"amount0":{"type":"string"},"amount1":{"type":"string"},"outputProtocol":{"$ref":"#/components/schemas/ProtocolItems"},"outputPosition":{"$ref":"#/components/schemas/Position"},"initialPrice":{"type":"string"},"outputPoolLiquidity":{"type":"string"},"outputCurrentTick":{"type":"number"},"outputSqrtRatioX96":{"type":"string"},"expectedTokenOwed0RawAmount":{"type":"string"},"expectedTokenOwed1RawAmount":{"type":"string"},"slippageTolerance":{"type":"number"},"deadline":{"type":"number"},"signatureDeadline":{"type":"number"},"simulateTransaction":{"type":"boolean","default":false}},"required":["tokenId","chainId","walletAddress","inputProtocol","inputPosition","inputPoolLiquidity","inputCurrentTick","inputSqrtRatioX96","inputPositionLiquidity","amount0","amount1","outputProtocol","outputPosition","expectedTokenOwed0RawAmount","expectedTokenOwed1RawAmount"]},"MigrateLPPositionResponse":{"type":"object","properties":{"requestId":{"$ref":"#/components/schemas/RequestId"},"migrate":{"$ref":"#/components/schemas/TransactionRequest"},"gasFee":{"type":"string"}}},"IndicativeQuoteToken":{"type":"object","properties":{"amount":{"type":"string"},"chainId":{"$ref":"#/components/schemas/ChainId"},"token":{"$ref":"#/components/schemas/Address"}}}},"parameters":{"universalRouterVersionHeader":{"name":"x-universal-router-version","in":"header","description":"The version of the Universal Router to use for the swap journey. *MUST* be consistent throughout the API calls.","required":false,"schema":{"$ref":"#/components/schemas/UniversalRouterVersion"}},"addressParam":{"name":"address","in":"path","schema":{"$ref":"#/components/schemas/Address"},"required":true},"tokenIdParam":{"name":"tokenId","in":"path","schema":{"type":"string"},"required":true},"cursorParam":{"name":"cursor","in":"query","schema":{"type":"string"},"required":false},"limitParam":{"name":"limit","in":"query","schema":{"type":"number"},"required":false},"chainIdParam":{"name":"chainId","in":"query","schema":{"$ref":"#/components/schemas/ChainId"},"required":false},"bridgeTokenInChainIdParam":{"name":"tokenInChainId","in":"query","schema":{"$ref":"#/components/schemas/ChainId"},"required":false},"bridgeTokenOutChainIdParam":{"name":"tokenOutChainId","in":"query","schema":{"$ref":"#/components/schemas/ChainId"},"required":false},"tokenInParam":{"name":"tokenIn","in":"query","schema":{"$ref":"#/components/schemas/Address"},"required":false},"tokenOutParam":{"name":"tokenOut","in":"query","schema":{"$ref":"#/components/schemas/Address"},"required":false},"addressPathParam":{"name":"address","in":"query","schema":{"$ref":"#/components/schemas/Address"},"required":false},"orderStatusParam":{"name":"orderStatus","in":"query","description":"Filter by order status.","required":false,"schema":{"$ref":"#/components/schemas/OrderStatus"}},"orderTypeParam":{"name":"orderType","in":"query","description":"The default orderType is Dutch_V1_V2 and will grab both Dutch and Dutch_V2 orders.","required":false,"schema":{"$ref":"#/components/schemas/OrderTypeQuery"}},"orderIdParam":{"name":"orderId","in":"query","required":false,"schema":{"$ref":"#/components/schemas/OrderId"}},"orderIdsParam":{"name":"orderIds","in":"query","required":false,"description":"ids split by commas","schema":{"$ref":"#/components/schemas/OrderIds"}},"swapperParam":{"name":"swapper","in":"query","description":"Filter by swapper address.","required":false,"schema":{"$ref":"#/components/schemas/Address"}},"fillerParam":{"name":"filler","in":"query","description":"Filter by filler address.","required":false,"schema":{"$ref":"#/components/schemas/Address"}},"sortKeyParam":{"name":"sortKey","in":"query","description":"Order the query results by the sort key.","required":false,"schema":{"$ref":"#/components/schemas/SortKey"}},"sortParam":{"name":"sort","in":"query","description":"Sort query. For example: `sort=gt(UNIX_TIMESTAMP)`, `sort=between(1675872827, 1675872930)`, or `lt(1675872930)`.","required":false,"schema":{"type":"string"}},"descParam":{"description":"Sort query results by sortKey in descending order.","name":"desc","in":"query","required":false,"schema":{"type":"string"}},"transactionHashesParam":{"description":"The transaction hashes.","name":"txHashes","in":"query","required":true,"style":"form","explode":false,"schema":{"type":"array","items":{"$ref":"#/components/schemas/TransactionHash"}}}},"securitySchemes":{"apiKey":{"type":"apiKey","in":"header","name":"x-api-key"}}},"security":[{"apiKey":[]}]} \ No newline at end of file diff --git a/packages/uniswap/src/entities/assets.ts b/packages/uniswap/src/entities/assets.ts index 4331aee6848..1829b00d633 100644 --- a/packages/uniswap/src/entities/assets.ts +++ b/packages/uniswap/src/entities/assets.ts @@ -1,4 +1,4 @@ -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' export type TradeableAsset = CurrencyAsset | NFTAsset diff --git a/packages/uniswap/src/features/address/ExplorerView.tsx b/packages/uniswap/src/features/address/ExplorerView.tsx deleted file mode 100644 index 8d2475bb5a6..00000000000 --- a/packages/uniswap/src/features/address/ExplorerView.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { SharedEventName } from '@uniswap/analytics-events' -import { Currency } from '@uniswap/sdk-core' -import { useState } from 'react' -import { useDispatch } from 'react-redux' -import { Anchor, Flex, Text, TouchableArea } from 'ui/src' -import { CheckCircleFilled } from 'ui/src/components/icons/CheckCircleFilled' -import { CopyAlt } from 'ui/src/components/icons/CopyAlt' -import { ExternalLink } from 'ui/src/components/icons/ExternalLink' -import { MicroConfirmation } from 'uniswap/src/components/MicroConfirmation' -import { pushNotification } from 'uniswap/src/features/notifications/slice' -import { AppNotificationType, CopyNotificationType } from 'uniswap/src/features/notifications/types' -import { ElementName } from 'uniswap/src/features/telemetry/constants' -import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' -import { WarningModalInfoContainer } from 'uniswap/src/features/tokens/TokenWarningModal' -import { useTranslation } from 'uniswap/src/i18n' -import { setClipboard } from 'uniswap/src/utils/clipboard' -import { ExplorerDataType, getExplorerLink } from 'uniswap/src/utils/linking' -import { isInterface } from 'utilities/src/platform' - -export function ExplorerView({ currency, modalName }: { currency: Currency; modalName: string }): JSX.Element | null { - const { t } = useTranslation() - const dispatch = useDispatch() - - const [showTooltip, setShowTooltip] = useState(false) - if (currency) { - const explorerLink = getExplorerLink( - currency.chainId, - currency.isToken ? currency.address : '', - currency.isToken ? ExplorerDataType.TOKEN : ExplorerDataType.NATIVE, - ) - const onPressCopyAddress = async (): Promise => { - await setClipboard(explorerLink) - - if (isInterface) { - setShowTooltip(true) - setTimeout(() => { - setShowTooltip(false) - }, 1000) - } else { - dispatch( - pushNotification({ type: AppNotificationType.Copied, copyType: CopyNotificationType.BlockExplorerUrl }), - ) - } - - sendAnalyticsEvent(SharedEventName.ELEMENT_CLICKED, { - element: ElementName.Copy, - modal: modalName, - }) - } - - return ( - - - - - - {explorerLink} - - - - - - - - } - showTooltip={showTooltip} - trigger={ - - - - } - /> - - ) - } else { - return null - } -} diff --git a/packages/uniswap/src/features/address/TokenAddressView.tsx b/packages/uniswap/src/features/address/TokenAddressView.tsx new file mode 100644 index 00000000000..84ba1737049 --- /dev/null +++ b/packages/uniswap/src/features/address/TokenAddressView.tsx @@ -0,0 +1,73 @@ +import { SharedEventName } from '@uniswap/analytics-events' +import { Currency } from '@uniswap/sdk-core' +import { useState } from 'react' +import { useDispatch } from 'react-redux' +import { Flex, Text, TouchableArea } from 'ui/src' +import { CheckCircleFilled } from 'ui/src/components/icons/CheckCircleFilled' +import { CopyAlt } from 'ui/src/components/icons/CopyAlt' +import { MicroConfirmation } from 'uniswap/src/components/MicroConfirmation' +import { pushNotification } from 'uniswap/src/features/notifications/slice' +import { AppNotificationType, CopyNotificationType } from 'uniswap/src/features/notifications/types' +import { ElementName } from 'uniswap/src/features/telemetry/constants' +import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' +import { WarningModalInfoContainer } from 'uniswap/src/features/tokens/WarningInfoModalContainer' +import { useTranslation } from 'uniswap/src/i18n' +import { setClipboard } from 'uniswap/src/utils/clipboard' +import { isInterface } from 'utilities/src/platform' + +export function TokenAddressView({ + currency, + modalName, +}: { + currency: Currency + modalName: string +}): JSX.Element | null { + const { t } = useTranslation() + const dispatch = useDispatch() + + const [showTooltip, setShowTooltip] = useState(false) + + if (!currency || !currency.isToken) { + return null + } + + const onPressCopyAddress = async (): Promise => { + await setClipboard(currency.address) + + if (isInterface) { + setShowTooltip(true) + setTimeout(() => { + setShowTooltip(false) + }, 1000) + } else { + dispatch(pushNotification({ type: AppNotificationType.Copied, copyType: CopyNotificationType.ContractAddress })) + } + + sendAnalyticsEvent(SharedEventName.ELEMENT_CLICKED, { + element: ElementName.Copy, + modal: modalName, + }) + } + + return ( + + + + + {currency.address} + + + } + showTooltip={showTooltip} + trigger={ + + + + } + /> + + + ) +} diff --git a/packages/uniswap/src/features/bridging/constants.ts b/packages/uniswap/src/features/bridging/constants.ts index 2c31fbd5fa2..a4000ac9ecf 100644 --- a/packages/uniswap/src/features/bridging/constants.ts +++ b/packages/uniswap/src/features/bridging/constants.ts @@ -1,5 +1,5 @@ -import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { extractBaseUrl } from 'utilities/src/format/urls' /* @@ -66,7 +66,7 @@ export const BRIDGING_DAPP_URLS = [ export function getCanonicalBridgingDappUrls(chainIds: UniverseChainId[]): string[] { const canonicalUrls = chainIds .map((chainId) => { - const chainInfo = UNIVERSE_CHAIN_INFO[chainId] + const chainInfo = getChainInfo(chainId) return chainInfo?.bridge ? extractBaseUrl(chainInfo.bridge) : undefined }) .filter((url): url is string => url !== undefined) diff --git a/packages/uniswap/src/features/bridging/hooks/chains.ts b/packages/uniswap/src/features/bridging/hooks/chains.ts index e894266386c..a21081e2aa2 100644 --- a/packages/uniswap/src/features/bridging/hooks/chains.ts +++ b/packages/uniswap/src/features/bridging/hooks/chains.ts @@ -1,12 +1,12 @@ import { useMemo } from 'react' import { useTradingApiSwappableTokensQuery } from 'uniswap/src/data/apiClients/tradingApi/useTradingApiSwappableTokensQuery' import { ChainId } from 'uniswap/src/data/tradingApi/__generated__' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { toSupportedChainId } from 'uniswap/src/features/chains/utils' import { NATIVE_ADDRESS_FOR_TRADING_API, toTradingApiSupportedChainId, } from 'uniswap/src/features/transactions/swap/utils/tradingApi' -import { UniverseChainId } from 'uniswap/src/types/chains' const FALLBACK_NUM_CHAINS = 8 diff --git a/packages/uniswap/src/features/bridging/hooks/tokens.ts b/packages/uniswap/src/features/bridging/hooks/tokens.ts index ac6e6feb584..7d5057de5ce 100644 --- a/packages/uniswap/src/features/bridging/hooks/tokens.ts +++ b/packages/uniswap/src/features/bridging/hooks/tokens.ts @@ -1,6 +1,6 @@ import { useCallback, useMemo } from 'react' import { filter } from 'uniswap/src/components/TokenSelector/filter' -import { usePortfolioBalancesForAddressById } from 'uniswap/src/components/TokenSelector/hooks' +import { usePortfolioBalancesForAddressById } from 'uniswap/src/components/TokenSelector/hooks/usePortfolioBalancesForAddressById' import { TokenOption } from 'uniswap/src/components/TokenSelector/types' import { createEmptyTokenOptionFromBridgingToken } from 'uniswap/src/components/TokenSelector/utils' import { useTradingApiSwappableTokensQuery } from 'uniswap/src/data/apiClients/tradingApi/useTradingApiSwappableTokensQuery' @@ -10,17 +10,15 @@ import { useTokenProjectsQuery } from 'uniswap/src/data/graphql/uniswap-data-api import { GetSwappableTokensResponse } from 'uniswap/src/data/tradingApi/__generated__' import { GqlResult } from 'uniswap/src/data/types' import { TradeableAsset } from 'uniswap/src/entities/assets' +import { ALL_CHAIN_IDS, UniverseChainId } from 'uniswap/src/features/chains/types' import { toSupportedChainId } from 'uniswap/src/features/chains/utils' import { CurrencyInfo, PortfolioBalance } from 'uniswap/src/features/dataApi/types' import { currencyIdToContractInput } from 'uniswap/src/features/dataApi/utils' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { NATIVE_ADDRESS_FOR_TRADING_API, getTokenAddressFromChainForTradingApi, toTradingApiSupportedChainId, } from 'uniswap/src/features/transactions/swap/utils/tradingApi' -import { COMBINED_CHAIN_IDS, UniverseChainId } from 'uniswap/src/types/chains' import { buildCurrencyId, buildNativeCurrencyId } from 'uniswap/src/utils/currencyId' import { logger } from 'utilities/src/logger/logger' @@ -39,8 +37,6 @@ export function useBridgingTokenWithHighestBalance({ currencyInfo: CurrencyInfo } | undefined { - const isBridgingEnabled = useFeatureFlag(FeatureFlags.Bridging) - const currencyId = buildCurrencyId(currencyChainId, currencyAddress) const tokenIn = currencyAddress ? getTokenAddressFromChainForTradingApi(currencyAddress, currencyChainId) : undefined const tokenInChainId = toTradingApiSupportedChainId(currencyChainId) @@ -59,7 +55,7 @@ export function useBridgingTokenWithHighestBalance({ const { data: bridgingTokens } = useTradingApiSwappableTokensQuery({ params: - otherChainBalances && otherChainBalances?.length > 0 && tokenIn && tokenInChainId && isBridgingEnabled + otherChainBalances && otherChainBalances?.length > 0 && tokenIn && tokenInChainId ? { tokenIn, tokenInChainId, @@ -119,8 +115,6 @@ export function useBridgingTokensOptions({ walletAddress: Address | undefined chainFilter: UniverseChainId | null }): GqlResult & { shouldNest?: boolean } { - const isBridgingEnabled = useFeatureFlag(FeatureFlags.Bridging) - const tokenIn = input?.address ? getTokenAddressFromChainForTradingApi(input.address, input.chainId) : undefined const tokenInChainId = toTradingApiSupportedChainId(input?.chainId) @@ -131,7 +125,7 @@ export function useBridgingTokensOptions({ refetch: refetchBridgingTokens, } = useTradingApiSwappableTokensQuery({ params: - tokenIn && tokenInChainId && isBridgingEnabled + tokenIn && tokenInChainId ? { tokenIn, tokenInChainId, @@ -145,7 +139,7 @@ export function useBridgingTokensOptions({ error: portfolioBalancesByIdError, refetch: portfolioBalancesByIdRefetch, loading: loadingPorfolioBalancesById, - } = usePortfolioBalancesForAddressById(isBridgingEnabled ? walletAddress : undefined) + } = usePortfolioBalancesForAddressById(walletAddress) const tokenOptions = useBridgingTokensToTokenOptions(bridgingTokens?.tokens, portfolioBalancesById) // Filter out tokens that are not on the current chain, unless the input token is the same as the current chain @@ -159,20 +153,9 @@ export function useBridgingTokensOptions({ const error = (!portfolioBalancesById && portfolioBalancesByIdError) || (!tokenOptions && errorBridgingTokens) const refetch = useCallback(async () => { - if (isBridgingEnabled) { - portfolioBalancesByIdRefetch?.() - await refetchBridgingTokens?.() - } - }, [portfolioBalancesByIdRefetch, refetchBridgingTokens, isBridgingEnabled]) - - if (!isBridgingEnabled) { - return { - data: undefined, - loading: false, - error: undefined, - refetch: undefined, - } - } + portfolioBalancesByIdRefetch?.() + await refetchBridgingTokens?.() + }, [portfolioBalancesByIdRefetch, refetchBridgingTokens]) return { data: filteredTokenOptions, @@ -193,7 +176,7 @@ function useBridgingTokensToTokenOptions( } // We sort the tokens by chain in the same order chains in the network selector - const chainOrder = COMBINED_CHAIN_IDS + const chainOrder = ALL_CHAIN_IDS const sortedBridgingTokens = [...bridgingTokens].sort((a, b) => { if (!a || !b) { return 0 diff --git a/packages/uniswap/src/constants/chains.ts b/packages/uniswap/src/features/chains/chainInfo.ts similarity index 87% rename from packages/uniswap/src/constants/chains.ts rename to packages/uniswap/src/features/chains/chainInfo.ts index cb3b5d66e51..d79f8256942 100644 --- a/packages/uniswap/src/constants/chains.ts +++ b/packages/uniswap/src/features/chains/chainInfo.ts @@ -39,15 +39,15 @@ import { USDT, } from 'uniswap/src/constants/tokens' import { Chain as BackendChainId } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { ElementName } from 'uniswap/src/features/telemetry/constants' import { - InterfaceGqlChain, + GqlChainId, NetworkLayer, RPCType, RetryOptions, UniverseChainId, UniverseChainInfo, -} from 'uniswap/src/types/chains' +} from 'uniswap/src/features/chains/types' +import { ElementName } from 'uniswap/src/features/telemetry/constants' import { isInterface } from 'utilities/src/platform' import { ONE_MINUTE_MS } from 'utilities/src/time/time' import { @@ -71,6 +71,10 @@ export const DEFAULT_RETRY_OPTIONS: RetryOptions = { n: 10, minWait: 250, maxWai export const DEFAULT_MS_BEFORE_WARNING = ONE_MINUTE_MS * 10 +export function getChainInfo(chainId: UniverseChainId): UniverseChainInfo { + return UNIVERSE_CHAIN_INFO[chainId] +} + export const UNIVERSE_CHAIN_INFO: Record = { [UniverseChainId.Mainnet]: { ...mainnet, @@ -78,7 +82,7 @@ export const UNIVERSE_CHAIN_INFO: Record = { sdkId: UniswapSDKChainId.MAINNET, assetRepoNetworkName: 'ethereum', backendChain: { - chain: BackendChainId.Ethereum as InterfaceGqlChain, + chain: BackendChainId.Ethereum as GqlChainId, backendSupported: true, isSecondaryChain: false, nativeTokenBackendAddress: undefined, @@ -117,13 +121,13 @@ export const UNIVERSE_CHAIN_INFO: Record = { [RPCType.Public]: { http: [config.quicknodeMainnetRpcUrl], }, - default: { + [RPCType.Default]: { http: ['https://cloudflare-eth.com'], }, - fallback: { + [RPCType.Fallback]: { http: ['https://rpc.ankr.com/eth', 'https://eth-mainnet.public.blastapi.io'], }, - appOnly: { + [RPCType.Interface]: { http: [`https://mainnet.infura.io/v3/${config.infuraKey}`, config.quicknodeMainnetRpcUrl], }, }, @@ -131,7 +135,7 @@ export const UNIVERSE_CHAIN_INFO: Record = { statusPage: undefined, spotPriceStablecoinAmount: CurrencyAmount.fromRawAmount(USDC, 100_000e6), stablecoins: [USDC, DAI, USDT], - supportsClientSideRouting: true, + supportsInterfaceClientSideRouting: true, supportsGasEstimates: true, wrappedNativeCurrency: { name: 'Wrapped Ether', @@ -146,7 +150,7 @@ export const UNIVERSE_CHAIN_INFO: Record = { sdkId: UniswapSDKChainId.SEPOLIA, assetRepoNetworkName: undefined, backendChain: { - chain: BackendChainId.EthereumSepolia as InterfaceGqlChain, + chain: BackendChainId.EthereumSepolia as GqlChainId, backendSupported: true, isSecondaryChain: false, nativeTokenBackendAddress: undefined, @@ -182,10 +186,10 @@ export const UNIVERSE_CHAIN_INFO: Record = { [RPCType.Public]: { http: [config.quicknodeSepoliaRpcUrl], }, - default: { + [RPCType.Default]: { http: ['https://rpc.sepolia.org/'], }, - fallback: { + [RPCType.Fallback]: { http: [ 'https://rpc.sepolia.org/', 'https://rpc2.sepolia.org/', @@ -195,12 +199,12 @@ export const UNIVERSE_CHAIN_INFO: Record = { 'https://rpc.bordel.wtf/sepolia', ], }, - appOnly: { http: [`https://sepolia.infura.io/v3/${config.infuraKey}`] }, + [RPCType.Interface]: { http: [`https://sepolia.infura.io/v3/${config.infuraKey}`] }, }, spotPriceStablecoinAmount: CurrencyAmount.fromRawAmount(USDC_SEPOLIA, 100e6), stablecoins: [USDC_SEPOLIA], statusPage: undefined, - supportsClientSideRouting: true, + supportsInterfaceClientSideRouting: true, supportsGasEstimates: false, urlParam: 'ethereum_sepolia', wrappedNativeCurrency: { @@ -216,7 +220,7 @@ export const UNIVERSE_CHAIN_INFO: Record = { sdkId: UniswapSDKChainId.ARBITRUM_ONE, assetRepoNetworkName: 'arbitrum', backendChain: { - chain: BackendChainId.Arbitrum as InterfaceGqlChain, + chain: BackendChainId.Arbitrum as GqlChainId, backendSupported: true, isSecondaryChain: false, nativeTokenBackendAddress: undefined, @@ -251,14 +255,14 @@ export const UNIVERSE_CHAIN_INFO: Record = { spotPriceStablecoinAmount: CurrencyAmount.fromRawAmount(USDC_ARBITRUM, 10_000e6), stablecoins: [USDC_ARBITRUM, DAI_ARBITRUM_ONE], statusPage: undefined, - supportsClientSideRouting: true, + supportsInterfaceClientSideRouting: true, supportsGasEstimates: true, urlParam: 'arbitrum', rpcUrls: { [RPCType.Public]: { http: [config.quicknodeArbitrumRpcUrl] }, - default: { http: ['https://arb1.arbitrum.io/rpc'] }, - fallback: { http: ['https://arbitrum.public-rpc.com'] }, - appOnly: { + [RPCType.Default]: { http: ['https://arb1.arbitrum.io/rpc'] }, + [RPCType.Fallback]: { http: ['https://arbitrum.public-rpc.com'] }, + [RPCType.Interface]: { http: [`https://arbitrum-mainnet.infura.io/v3/${config.infuraKey}`, config.quicknodeArbitrumRpcUrl], }, [RPCType.PublicAlt]: { http: ['https://arb1.arbitrum.io/rpc'] }, @@ -276,7 +280,7 @@ export const UNIVERSE_CHAIN_INFO: Record = { sdkId: UniswapSDKChainId.OPTIMISM, assetRepoNetworkName: 'optimism', backendChain: { - chain: BackendChainId.Optimism as InterfaceGqlChain, + chain: BackendChainId.Optimism as GqlChainId, backendSupported: true, isSecondaryChain: false, nativeTokenBackendAddress: undefined, @@ -311,14 +315,14 @@ export const UNIVERSE_CHAIN_INFO: Record = { rpcUrls: { [RPCType.Public]: { http: [config.quicknodeOpRpcUrl] }, [RPCType.PublicAlt]: { http: ['https://mainnet.optimism.io'] }, - default: { http: ['https://mainnet.optimism.io/'] }, - fallback: { http: ['https://rpc.ankr.com/optimism'] }, - appOnly: { http: [`https://optimism-mainnet.infura.io/v3/${config.infuraKey}`] }, + [RPCType.Default]: { http: ['https://mainnet.optimism.io/'] }, + [RPCType.Fallback]: { http: ['https://rpc.ankr.com/optimism'] }, + [RPCType.Interface]: { http: [`https://optimism-mainnet.infura.io/v3/${config.infuraKey}`] }, }, spotPriceStablecoinAmount: CurrencyAmount.fromRawAmount(DAI_OPTIMISM, 10_000e18), stablecoins: [USDC_OPTIMISM, DAI_OPTIMISM], statusPage: 'https://optimism.io/status', - supportsClientSideRouting: true, + supportsInterfaceClientSideRouting: true, supportsGasEstimates: true, urlParam: 'optimism', wrappedNativeCurrency: { @@ -333,7 +337,7 @@ export const UNIVERSE_CHAIN_INFO: Record = { id: UniverseChainId.Base, sdkId: UniswapSDKChainId.BASE, backendChain: { - chain: BackendChainId.Base as InterfaceGqlChain, + chain: BackendChainId.Base as GqlChainId, backendSupported: true, isSecondaryChain: false, nativeTokenBackendAddress: undefined, @@ -365,14 +369,14 @@ export const UNIVERSE_CHAIN_INFO: Record = { networkLayer: NetworkLayer.L2, pendingTransactionsRetryOptions: DEFAULT_RETRY_OPTIONS, statusPage: 'https://status.base.org/', - supportsClientSideRouting: true, + supportsInterfaceClientSideRouting: true, supportsGasEstimates: true, urlParam: 'base', rpcUrls: { [RPCType.Public]: { http: [config.quicknodeBaseRpcUrl] }, - default: { http: ['https://mainnet.base.org/'] }, - fallback: { http: ['https://1rpc.io/base', 'https://base.meowrpc.com'] }, - appOnly: { http: [`https://base-mainnet.infura.io/v3/${config.infuraKey}`] }, + [RPCType.Default]: { http: ['https://mainnet.base.org/'] }, + [RPCType.Fallback]: { http: ['https://1rpc.io/base', 'https://base.meowrpc.com'] }, + [RPCType.Interface]: { http: [`https://base-mainnet.infura.io/v3/${config.infuraKey}`] }, }, spotPriceStablecoinAmount: CurrencyAmount.fromRawAmount(USDC_BASE, 10_000e6), assetRepoNetworkName: 'base', @@ -390,7 +394,7 @@ export const UNIVERSE_CHAIN_INFO: Record = { sdkId: UniswapSDKChainId.BNB, assetRepoNetworkName: 'smartchain', backendChain: { - chain: BackendChainId.Bnb as InterfaceGqlChain, + chain: BackendChainId.Bnb as GqlChainId, backendSupported: true, isSecondaryChain: false, nativeTokenBackendAddress: undefined, @@ -425,13 +429,13 @@ export const UNIVERSE_CHAIN_INFO: Record = { pendingTransactionsRetryOptions: undefined, rpcUrls: { [RPCType.Public]: { http: [config.quicknodeBnbRpcUrl] }, - default: { http: ['https://bsc-dataseed1.bnbchain.org'] }, - appOnly: { http: [config.quicknodeBnbRpcUrl] }, + [RPCType.Default]: { http: ['https://bsc-dataseed1.bnbchain.org'] }, + [RPCType.Interface]: { http: [config.quicknodeBnbRpcUrl] }, }, spotPriceStablecoinAmount: CurrencyAmount.fromRawAmount(USDC_BNB, 100e18), stablecoins: [USDC_BNB], statusPage: undefined, - supportsClientSideRouting: true, + supportsInterfaceClientSideRouting: true, supportsGasEstimates: true, urlParam: 'bnb', wrappedNativeCurrency: { @@ -448,7 +452,7 @@ export const UNIVERSE_CHAIN_INFO: Record = { assetRepoNetworkName: 'polygon', blockPerMainnetEpochForChainId: 5, backendChain: { - chain: BackendChainId.Polygon as InterfaceGqlChain, + chain: BackendChainId.Polygon as GqlChainId, backendSupported: true, nativeTokenBackendAddress: '0x0000000000000000000000000000000000001010', isSecondaryChain: false, @@ -482,13 +486,13 @@ export const UNIVERSE_CHAIN_INFO: Record = { rpcUrls: { [RPCType.Public]: { http: [config.quicknodePolygonRpcUrl] }, [RPCType.PublicAlt]: { http: ['https://polygon-rpc.com/'] }, - default: { http: ['https://polygon-rpc.com/'] }, - appOnly: { http: [`https://polygon-mainnet.infura.io/v3/${config.infuraKey}`] }, + [RPCType.Default]: { http: ['https://polygon-rpc.com/'] }, + [RPCType.Interface]: { http: [`https://polygon-mainnet.infura.io/v3/${config.infuraKey}`] }, }, spotPriceStablecoinAmount: CurrencyAmount.fromRawAmount(USDC_POLYGON, 10_000e6), stablecoins: [USDC_POLYGON, DAI_POLYGON], statusPage: undefined, - supportsClientSideRouting: true, + supportsInterfaceClientSideRouting: true, supportsGasEstimates: true, urlParam: 'polygon', wrappedNativeCurrency: { @@ -504,7 +508,7 @@ export const UNIVERSE_CHAIN_INFO: Record = { sdkId: UniswapSDKChainId.BLAST, assetRepoNetworkName: 'blast', backendChain: { - chain: BackendChainId.Blast as InterfaceGqlChain, + chain: BackendChainId.Blast as GqlChainId, backendSupported: true, isSecondaryChain: false, nativeTokenBackendAddress: undefined, @@ -531,7 +535,7 @@ export const UNIVERSE_CHAIN_INFO: Record = { spotPriceStablecoinAmount: CurrencyAmount.fromRawAmount(USDB_BLAST, 10_000e18), stablecoins: [USDB_BLAST], statusPage: undefined, - supportsClientSideRouting: false, + supportsInterfaceClientSideRouting: false, supportsGasEstimates: true, urlParam: 'blast', nativeCurrency: { @@ -543,8 +547,8 @@ export const UNIVERSE_CHAIN_INFO: Record = { }, rpcUrls: { [RPCType.Public]: { http: [config.quicknodeBlastRpcUrl] }, - default: { http: ['https://rpc.blast.io/'] }, - appOnly: { http: [`https://blast-mainnet.infura.io/v3/${config.infuraKey}`] }, + [RPCType.Default]: { http: ['https://rpc.blast.io/'] }, + [RPCType.Interface]: { http: [`https://blast-mainnet.infura.io/v3/${config.infuraKey}`] }, }, wrappedNativeCurrency: { name: 'Wrapped Ether', @@ -559,7 +563,7 @@ export const UNIVERSE_CHAIN_INFO: Record = { sdkId: UniswapSDKChainId.AVALANCHE, assetRepoNetworkName: 'avalanchec', backendChain: { - chain: BackendChainId.Avalanche as InterfaceGqlChain, + chain: BackendChainId.Avalanche as GqlChainId, backendSupported: true, isSecondaryChain: false, nativeTokenBackendAddress: undefined, @@ -593,13 +597,13 @@ export const UNIVERSE_CHAIN_INFO: Record = { pendingTransactionsRetryOptions: undefined, rpcUrls: { [RPCType.Public]: { http: [config.quicknodeAvaxRpcUrl] }, - default: { http: ['https://api.avax.network/ext/bc/C/rpc'] }, - appOnly: { http: [`https://avalanche-mainnet.infura.io/v3/${config.infuraKey}`] }, + [RPCType.Default]: { http: ['https://api.avax.network/ext/bc/C/rpc'] }, + [RPCType.Interface]: { http: [`https://avalanche-mainnet.infura.io/v3/${config.infuraKey}`] }, }, spotPriceStablecoinAmount: CurrencyAmount.fromRawAmount(USDC_AVALANCHE, 10_000e6), stablecoins: [USDC_AVALANCHE], statusPage: undefined, - supportsClientSideRouting: true, + supportsInterfaceClientSideRouting: true, supportsGasEstimates: true, urlParam: 'avalanche', wrappedNativeCurrency: { @@ -615,7 +619,7 @@ export const UNIVERSE_CHAIN_INFO: Record = { sdkId: UniswapSDKChainId.CELO, assetRepoNetworkName: 'celo', backendChain: { - chain: BackendChainId.Celo as InterfaceGqlChain, + chain: BackendChainId.Celo as GqlChainId, backendSupported: true, nativeTokenBackendAddress: '0x471EcE3750Da237f93B8E339c536989b8978a438', isSecondaryChain: false, @@ -650,13 +654,13 @@ export const UNIVERSE_CHAIN_INFO: Record = { spotPriceStablecoinAmount: CurrencyAmount.fromRawAmount(CUSD_CELO, 10_000e18), stablecoins: [USDC_CELO], statusPage: undefined, - supportsClientSideRouting: true, + supportsInterfaceClientSideRouting: true, supportsGasEstimates: true, urlParam: 'celo', rpcUrls: { [RPCType.Public]: { http: [config.quicknodeCeloRpcUrl] }, - default: { http: [`https://forno.celo.org`] }, - appOnly: { http: [`https://celo-mainnet.infura.io/v3/${config.infuraKey}`] }, + [RPCType.Default]: { http: [`https://forno.celo.org`] }, + [RPCType.Interface]: { http: [`https://celo-mainnet.infura.io/v3/${config.infuraKey}`] }, }, wrappedNativeCurrency: { name: 'Celo', @@ -672,7 +676,7 @@ export const UNIVERSE_CHAIN_INFO: Record = { sdkId: UniswapSDKChainId.WORLDCHAIN, assetRepoNetworkName: 'worldcoin', backendChain: { - chain: BackendChainId.Worldchain as InterfaceGqlChain, + chain: BackendChainId.Worldchain as GqlChainId, backendSupported: true, isSecondaryChain: false, nativeTokenBackendAddress: undefined, @@ -706,8 +710,8 @@ export const UNIVERSE_CHAIN_INFO: Record = { [RPCType.Public]: { http: [config.quicknodeWorldChainRpcUrl], }, - default: { http: ['https://worldchain-mainnet.g.alchemy.com/public'] }, - appOnly: { + [RPCType.Default]: { http: ['https://worldchain-mainnet.g.alchemy.com/public'] }, + [RPCType.Interface]: { http: [config.quicknodeWorldChainRpcUrl], }, }, @@ -715,7 +719,7 @@ export const UNIVERSE_CHAIN_INFO: Record = { statusPage: undefined, spotPriceStablecoinAmount: CurrencyAmount.fromRawAmount(USDC_WORLD_CHAIN, 10_000e6), stablecoins: [USDC_WORLD_CHAIN], - supportsClientSideRouting: false, + supportsInterfaceClientSideRouting: false, supportsGasEstimates: false, wrappedNativeCurrency: { name: 'Wrapped Ether', @@ -730,7 +734,7 @@ export const UNIVERSE_CHAIN_INFO: Record = { sdkId: UniswapSDKChainId.ZORA, assetRepoNetworkName: 'zora', backendChain: { - chain: BackendChainId.Zora as InterfaceGqlChain, + chain: BackendChainId.Zora as GqlChainId, backendSupported: true, isSecondaryChain: false, nativeTokenBackendAddress: undefined, @@ -762,13 +766,13 @@ export const UNIVERSE_CHAIN_INFO: Record = { pendingTransactionsRetryOptions: undefined, rpcUrls: { [RPCType.Public]: { http: [config.quicknodeZoraRpcUrl] }, - default: { http: ['https://rpc.zora.energy/'] }, - appOnly: { http: [config.quicknodeZoraRpcUrl] }, + [RPCType.Default]: { http: ['https://rpc.zora.energy/'] }, + [RPCType.Interface]: { http: [config.quicknodeZoraRpcUrl] }, }, spotPriceStablecoinAmount: CurrencyAmount.fromRawAmount(USDC_ZORA, 10_000e6), stablecoins: [USDC_ZORA], statusPage: undefined, - supportsClientSideRouting: false, + supportsInterfaceClientSideRouting: false, supportsGasEstimates: true, urlParam: 'zora', wrappedNativeCurrency: { @@ -784,7 +788,7 @@ export const UNIVERSE_CHAIN_INFO: Record = { sdkId: UniswapSDKChainId.ZKSYNC, assetRepoNetworkName: 'zksync', backendChain: { - chain: BackendChainId.Zksync as InterfaceGqlChain, + chain: BackendChainId.Zksync as GqlChainId, backendSupported: true, isSecondaryChain: false, nativeTokenBackendAddress: undefined, @@ -817,14 +821,14 @@ export const UNIVERSE_CHAIN_INFO: Record = { pendingTransactionsRetryOptions: undefined, rpcUrls: { [RPCType.Public]: { http: [config.quicknodeZkSyncRpcUrl] }, - default: { http: ['https://mainnet.era.zksync.io/'] }, - appOnly: { http: [config.quicknodeZkSyncRpcUrl] }, + [RPCType.Default]: { http: ['https://mainnet.era.zksync.io/'] }, + [RPCType.Interface]: { http: [config.quicknodeZkSyncRpcUrl] }, }, urlParam: 'zksync', statusPage: undefined, spotPriceStablecoinAmount: CurrencyAmount.fromRawAmount(USDC_ZKSYNC, 10_000e6), stablecoins: [USDC_ZKSYNC], - supportsClientSideRouting: false, + supportsInterfaceClientSideRouting: false, supportsGasEstimates: false, wrappedNativeCurrency: { name: 'Wrapped Ether', @@ -841,7 +845,7 @@ export const UNIVERSE_CHAIN_INFO: Record = { sdkId: UniswapSDKChainId.ASTROCHAIN_SEPOLIA, assetRepoNetworkName: undefined, backendChain: { - chain: BackendChainId.AstrochainSepolia as InterfaceGqlChain, + chain: BackendChainId.AstrochainSepolia as GqlChainId, backendSupported: true, isSecondaryChain: false, nativeTokenBackendAddress: undefined, @@ -875,17 +879,17 @@ export const UNIVERSE_CHAIN_INFO: Record = { [RPCType.Public]: { http: [config.quicknodeAstrochainSepoliaRpcUrl], }, - default: { + [RPCType.Default]: { http: [config.quicknodeAstrochainSepoliaRpcUrl], }, - appOnly: { + [RPCType.Interface]: { http: [config.quicknodeAstrochainSepoliaRpcUrl], }, }, spotPriceStablecoinAmount: CurrencyAmount.fromRawAmount(USDC_ASTROCHAIN_SEPOLIA, 10_000e6), stablecoins: [USDC_ASTROCHAIN_SEPOLIA], statusPage: undefined, - supportsClientSideRouting: true, + supportsInterfaceClientSideRouting: true, supportsGasEstimates: false, urlParam: 'astrochain_sepolia', wrappedNativeCurrency: { @@ -907,6 +911,4 @@ export const GQL_TESTNET_CHAINS = Object.values(UNIVERSE_CHAIN_INFO) .map((chain) => chain.backendChain.chain) .filter((backendChain) => !!backendChain) -/** Used for making graphql queries to all chains supported by the graphql backend. Must be mutable for some apollo typechecking. */ -export const GQL_MAINNET_CHAINS_MUTABLE = GQL_MAINNET_CHAINS.map((c) => c) -export const GQL_TESTNET_CHAINS_MUTABLE = GQL_TESTNET_CHAINS.map((c) => c) +export const ALL_GQL_CHAINS: GqlChainId[] = [...GQL_MAINNET_CHAINS, ...GQL_TESTNET_CHAINS] diff --git a/packages/uniswap/src/features/chains/hooks.ts b/packages/uniswap/src/features/chains/hooks.ts new file mode 100644 index 00000000000..c5b3614a1c9 --- /dev/null +++ b/packages/uniswap/src/features/chains/hooks.ts @@ -0,0 +1,121 @@ +import { useCallback, useMemo } from 'react' +import { useSelector } from 'react-redux' +import { CONNECTION_PROVIDER_IDS } from 'uniswap/src/constants/web3' +import { useConnector } from 'uniswap/src/contexts/UniswapContext' +import { ALL_CHAIN_IDS, GqlChainId, UniverseChainId } from 'uniswap/src/features/chains/types' +import { filterChainIdsByFeatureFlag, getEnabledChains } from 'uniswap/src/features/chains/utils' +import { selectIsTestnetModeEnabled } from 'uniswap/src/features/settings/selectors' +import { WalletConnectConnector } from 'uniswap/src/features/web3/walletConnect' +import { isTestEnv } from 'utilities/src/environment/env' +import { logger } from 'utilities/src/logger/logger' +import { isInterface } from 'utilities/src/platform' +import { Connector } from 'wagmi' + +// Note: only use this hook for useConnectedWalletSupportedChains +// for wallet we expect useConnector to throw because there is no connector +function useConnectorWithCatch(): Connector | undefined { + try { + return useConnector() + } catch (_e) { + if (isInterface && !isTestEnv()) { + logger.error(_e, { + tags: { file: 'src/features/settings/hooks', function: 'useConnectorWithCatch' }, + }) + } + return undefined + } +} + +// Returns the chain ids supported by the connector +function useConnectorSupportedChains(connector?: Connector): UniverseChainId[] { + // We need to memoize the connected wallet chain ids to avoid infinite loops + // caused by modifying the gqlChains returned by useEnabledChains + return useMemo(() => { + switch (connector?.type) { + case CONNECTION_PROVIDER_IDS.UNISWAP_WALLET_CONNECT_CONNECTOR_ID: + case CONNECTION_PROVIDER_IDS.WALLET_CONNECT_CONNECTOR_ID: + // Wagmi currently offers no way to discriminate a Connector as a WalletConnect connector providing access to getNamespaceChainsIds. + return (connector as WalletConnectConnector).getNamespaceChainsIds?.().length + ? (connector as WalletConnectConnector).getNamespaceChainsIds?.() + : ALL_CHAIN_IDS + default: + return ALL_CHAIN_IDS + } + }, [connector]) +} + +// Returns the chain ids supported by the user's connected wallet +function useConnectedWalletSupportedChains(): UniverseChainId[] { + const connector = useConnectorWithCatch() + return useConnectorSupportedChains(connector) +} + +export function useEnabledChains(): { + chains: UniverseChainId[] + gqlChains: GqlChainId[] + defaultChainId: UniverseChainId + isTestnetModeEnabled: boolean +} { + const featureFlaggedChainIds = useFeatureFlaggedChainIds() + const connectedWalletChainIds = useConnectedWalletSupportedChains() + const isTestnetModeEnabled = useSelector(selectIsTestnetModeEnabled) + + return useMemo( + () => getEnabledChains({ isTestnetModeEnabled, connectedWalletChainIds, featureFlaggedChainIds }), + [isTestnetModeEnabled, connectedWalletChainIds, featureFlaggedChainIds], + ) +} + +function useEnabledChainsWithConnector(connector?: Connector): { + chains: UniverseChainId[] + gqlChains: GqlChainId[] + defaultChainId: UniverseChainId + isTestnetModeEnabled: boolean +} { + const featureFlaggedChainIds = useFeatureFlaggedChainIds() + const connectedWalletChainIds = useConnectorSupportedChains(connector) + const isTestnetModeEnabled = useSelector(selectIsTestnetModeEnabled) + + return useMemo( + () => getEnabledChains({ isTestnetModeEnabled, connectedWalletChainIds, featureFlaggedChainIds }), + [isTestnetModeEnabled, connectedWalletChainIds, featureFlaggedChainIds], + ) +} + +// Used to feature flag chains. If a chain is not included in the object, it is considered enabled by default. +export function useFeatureFlaggedChainIds(): UniverseChainId[] { + // You can use the useFeatureFlag hook here to enable/disable chains based on feature flags. + // Example: [ChainId.BLAST]: useFeatureFlag(FeatureFlags.BLAST) + // IMPORTANT: Don't forget to also update getEnabledChainIdsSaga + + return useMemo(() => filterChainIdsByFeatureFlag({}), []) +} + +export function useSupportedChainId(chainId?: number | UniverseChainId): UniverseChainId | undefined { + const { chains } = useEnabledChains() + return chains.includes(chainId as UniverseChainId) ? (chainId as UniverseChainId) : undefined +} + +export function useIsSupportedChainId(chainId?: number | UniverseChainId): chainId is UniverseChainId { + const supportedChainId = useSupportedChainId(chainId) + return supportedChainId !== undefined +} + +export function useIsSupportedChainIdCallback(): (chainId?: number | UniverseChainId) => chainId is UniverseChainId { + const { chains } = useEnabledChains() + + return useCallback( + (chainId?: number | UniverseChainId): chainId is UniverseChainId => { + return chains.includes(chainId as UniverseChainId) + }, + [chains], + ) +} + +export function useSupportedChainIdWithConnector( + chainId?: number | UniverseChainId, + connector?: Connector, +): UniverseChainId | undefined { + const { chains } = useEnabledChainsWithConnector(connector) + return chains.includes(chainId as UniverseChainId) ? (chainId as UniverseChainId) : undefined +} diff --git a/packages/uniswap/src/types/chains.ts b/packages/uniswap/src/features/chains/types.ts similarity index 80% rename from packages/uniswap/src/types/chains.ts rename to packages/uniswap/src/features/chains/types.ts index 41a1e0d6030..2aca6ce8175 100644 --- a/packages/uniswap/src/types/chains.ts +++ b/packages/uniswap/src/features/chains/types.ts @@ -6,6 +6,10 @@ import { Chain as BackendChainId } from 'uniswap/src/data/graphql/uniswap-data-a import { ElementNameType } from 'uniswap/src/features/telemetry/constants' import { Chain as WagmiChain } from 'wagmi/chains' +export function isUniverseChainId(chainId?: number | UniverseChainId | null): chainId is UniverseChainId { + return !!chainId && ALL_CHAIN_IDS.includes(chainId as UniverseChainId) +} + export enum UniverseChainId { Mainnet = UniswapSDKChainId.MAINNET, Sepolia = UniswapSDKChainId.SEPOLIA, @@ -39,14 +43,20 @@ export const SUPPORTED_CHAIN_IDS: UniverseChainId[] = [ UniverseChainId.Zksync, ] -export const SUPPORTED_TESTNET_CHAIN_IDS: UniverseChainId[] = [UniverseChainId.Sepolia, UniverseChainId.AstrochainSepolia] +export const SUPPORTED_TESTNET_CHAIN_IDS: UniverseChainId[] = [ + UniverseChainId.Sepolia, + UniverseChainId.AstrochainSepolia, +] -export const COMBINED_CHAIN_IDS: UniverseChainId[] = [...SUPPORTED_CHAIN_IDS, ...SUPPORTED_TESTNET_CHAIN_IDS] +export const ALL_CHAIN_IDS: UniverseChainId[] = [...SUPPORTED_CHAIN_IDS, ...SUPPORTED_TESTNET_CHAIN_IDS] export enum RPCType { Public = 'public', Private = 'private', PublicAlt = 'public_alternative', + Interface = 'interface', + Fallback = 'fallback', + Default = 'default', } export enum NetworkLayer { @@ -60,10 +70,10 @@ export interface RetryOptions { maxWait: number } -export type InterfaceGqlChain = Exclude +export type GqlChainId = Exclude export interface BackendChain { - chain: InterfaceGqlChain + chain: GqlChainId /** * Set to false if the chain is not available on Explore. */ @@ -78,6 +88,7 @@ export interface BackendChain { nativeTokenBackendAddress: string | undefined } +type ChainRPCUrls = { http: string[] } export interface UniverseChainInfo extends WagmiChain { readonly id: UniverseChainId readonly sdkId: UniswapSDKChainId @@ -94,6 +105,14 @@ export interface UniverseChainInfo extends WagmiChain { url: string apiURL?: string } + readonly rpcUrls: { + [RPCType.Default]: ChainRPCUrls + [RPCType.Private]?: ChainRPCUrls + [RPCType.Public]?: ChainRPCUrls + [RPCType.PublicAlt]?: ChainRPCUrls + [RPCType.Interface]: ChainRPCUrls + [RPCType.Fallback]?: ChainRPCUrls + } readonly helpCenterUrl: string | undefined readonly infoLink: string readonly infuraPrefix: string | undefined @@ -113,7 +132,7 @@ export interface UniverseChainInfo extends WagmiChain { readonly spotPriceStablecoinAmount: CurrencyAmount readonly stablecoins: Token[] readonly statusPage?: string - readonly supportsClientSideRouting: boolean + readonly supportsInterfaceClientSideRouting: boolean readonly supportsGasEstimates: boolean readonly urlParam: string readonly wrappedNativeCurrency: { diff --git a/packages/uniswap/src/features/chains/utils.test.ts b/packages/uniswap/src/features/chains/utils.test.ts index da9e1bb669e..a93c0d14ad9 100644 --- a/packages/uniswap/src/features/chains/utils.test.ts +++ b/packages/uniswap/src/features/chains/utils.test.ts @@ -1,6 +1,7 @@ import { BigNumber } from '@ethersproject/bignumber' import { PollingInterval } from 'uniswap/src/constants/misc' import { Chain } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { SUPPORTED_CHAIN_IDS, SUPPORTED_TESTNET_CHAIN_IDS, UniverseChainId } from 'uniswap/src/features/chains/types' import { chainIdToHexadecimalString, fromGraphQLChain, @@ -11,7 +12,6 @@ import { toSupportedChainId, toUniswapWebAppLink, } from 'uniswap/src/features/chains/utils' -import { SUPPORTED_CHAIN_IDS, SUPPORTED_TESTNET_CHAIN_IDS, UniverseChainId } from 'uniswap/src/types/chains' describe(toSupportedChainId, () => { it('handles undefined input', () => { diff --git a/packages/uniswap/src/features/chains/utils.ts b/packages/uniswap/src/features/chains/utils.ts index 05bb3d8dea1..97261285364 100644 --- a/packages/uniswap/src/features/chains/utils.ts +++ b/packages/uniswap/src/features/chains/utils.ts @@ -1,69 +1,61 @@ import { BigNumber, BigNumberish } from '@ethersproject/bignumber' -import { ChainId } from '@uniswap/sdk-core' -import { useMemo } from 'react' -import { - GQL_MAINNET_CHAINS_MUTABLE, - GQL_TESTNET_CHAINS_MUTABLE, - UNIVERSE_CHAIN_INFO, -} from 'uniswap/src/constants/chains' import { PollingInterval } from 'uniswap/src/constants/misc' import { Chain } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { - COMBINED_CHAIN_IDS, - InterfaceGqlChain, + GQL_MAINNET_CHAINS, + GQL_TESTNET_CHAINS, + // eslint-disable-next-line no-restricted-imports + UNIVERSE_CHAIN_INFO, + getChainInfo, +} from 'uniswap/src/features/chains/chainInfo' +import { + ALL_CHAIN_IDS, + GqlChainId, NetworkLayer, SUPPORTED_CHAIN_IDS, SUPPORTED_TESTNET_CHAIN_IDS, UniverseChainId, -} from 'uniswap/src/types/chains' - -export function toGraphQLChain(chainId: ChainId | number): Chain | undefined { - switch (chainId) { - case ChainId.MAINNET: - return Chain.Ethereum - case ChainId.SEPOLIA: - return Chain.EthereumSepolia - case ChainId.ARBITRUM_ONE: - return Chain.Arbitrum - case ChainId.OPTIMISM: - return Chain.Optimism - case ChainId.POLYGON: - return Chain.Polygon - case ChainId.BASE: - return Chain.Base - case ChainId.BNB: - return Chain.Bnb - case ChainId.AVALANCHE: - return Chain.Avalanche - case ChainId.CELO: - return Chain.Celo - case ChainId.BLAST: - return Chain.Blast - case ChainId.WORLDCHAIN: - return Chain.Worldchain - case ChainId.ZORA: - return Chain.Zora - case ChainId.ZKSYNC: - return Chain.Zksync - case ChainId.ASTROCHAIN_SEPOLIA: - return Chain.AstrochainSepolia - } - return undefined -} +} from 'uniswap/src/features/chains/types' // Some code from the web app uses chainId types as numbers // This validates them as coerces into SupportedChainId export function toSupportedChainId(chainId?: BigNumberish): UniverseChainId | null { - const ids = COMBINED_CHAIN_IDS - - if (!chainId || !ids.map((c) => c.toString()).includes(chainId.toString())) { + if (!chainId || !ALL_CHAIN_IDS.map((c) => c.toString()).includes(chainId.toString())) { return null } return parseInt(chainId.toString(), 10) as UniverseChainId } +export function chainSupportsGasEstimates(chainId: UniverseChainId): boolean { + return getChainInfo(chainId).supportsGasEstimates +} + +export function getChainLabel(chainId: UniverseChainId): string { + return getChainInfo(chainId).label +} + +export function isTestnetChain(chainId: UniverseChainId): boolean { + return Boolean(getChainInfo(chainId).testnet) +} + +export function getChainIdByInfuraPrefix(prefix: string): UniverseChainId | undefined { + return Object.values(UNIVERSE_CHAIN_INFO).find((i) => i.infuraPrefix === prefix)?.id +} + +export function isBackendSupportedChainId(chainId: UniverseChainId): boolean { + const info = getChainInfo(chainId) + return info.backendChain.backendSupported && !info.backendChain.isSecondaryChain +} + +export function isBackendSupportedChain(chain: Chain): chain is GqlChainId { + const chainId = fromGraphQLChain(chain) + if (!chainId) { + return false + } + + return chainId && isBackendSupportedChainId(chainId) +} + export function chainIdToHexadecimalString(chainId: UniverseChainId): string { return BigNumber.from(chainId).toHexString() } @@ -73,13 +65,17 @@ export function hexadecimalStringToInt(hex: string): number { } export function isL2ChainId(chainId?: UniverseChainId): boolean { - return chainId !== undefined && UNIVERSE_CHAIN_INFO[chainId].networkLayer === NetworkLayer.L2 + return chainId !== undefined && getChainInfo(chainId).networkLayer === NetworkLayer.L2 } export function isMainnetChainId(chainId?: UniverseChainId): boolean { return chainId === UniverseChainId.Mainnet || chainId === UniverseChainId.Sepolia } +export function toGraphQLChain(chainId: UniverseChainId): GqlChainId { + return getChainInfo(chainId).backendChain.chain +} + export function fromGraphQLChain(chain: Chain | string | undefined): UniverseChainId | null { switch (chain) { case Chain.Ethereum: @@ -189,33 +185,14 @@ export function toUniswapWebAppLink(chainId: UniverseChainId): string | null { } } -type ActiveChainIdFeatureFlags = UniverseChainId.WorldChain - export function filterChainIdsByFeatureFlag(featureFlaggedChainIds: { - [UniverseChainId.WorldChain]: boolean + [key in UniverseChainId]?: boolean }): UniverseChainId[] { - return COMBINED_CHAIN_IDS.filter((chainId) => { - return featureFlaggedChainIds[chainId as ActiveChainIdFeatureFlags] ?? true + return ALL_CHAIN_IDS.filter((chainId) => { + return featureFlaggedChainIds[chainId] ?? true }) } -// Used to feature flag chains. If a chain is not included in the object, it is considered enabled by default. -export function useFeatureFlaggedChainIds(): UniverseChainId[] { - // You can use the useFeatureFlag hook here to enable/disable chains based on feature flags. - // Example: [ChainId.BLAST]: useFeatureFlag(FeatureFlags.BLAST) - // IMPORTANT: Don't forget to also update getEnabledChainIdsSaga - - const worldChainEnabled = useFeatureFlag(FeatureFlags.WorldChain) - - return useMemo( - () => - filterChainIdsByFeatureFlag({ - [UniverseChainId.WorldChain]: worldChainEnabled, - }), - [worldChainEnabled], - ) -} - export function getEnabledChains({ isTestnetModeEnabled, featureFlaggedChainIds, @@ -226,7 +203,7 @@ export function getEnabledChains({ connectedWalletChainIds?: UniverseChainId[] }): { chains: UniverseChainId[] - gqlChains: InterfaceGqlChain[] + gqlChains: GqlChainId[] defaultChainId: UniverseChainId isTestnetModeEnabled: boolean } { @@ -239,7 +216,7 @@ export function getEnabledChains({ return { chains: supportedTestnetChainIds, - gqlChains: GQL_TESTNET_CHAINS_MUTABLE, + gqlChains: GQL_TESTNET_CHAINS, defaultChainId: UniverseChainId.Sepolia as UniverseChainId, isTestnetModeEnabled, } @@ -251,7 +228,7 @@ export function getEnabledChains({ (connectedWalletChainIds ? connectedWalletChainIds.includes(chainId) : true), ) - const supportedGqlChains = GQL_MAINNET_CHAINS_MUTABLE.filter((chain) => { + const supportedGqlChains = GQL_MAINNET_CHAINS.filter((chain) => { const chainId = fromGraphQLChain(chain) return chainId && supportedChainIds.includes(chainId) }) diff --git a/packages/uniswap/src/features/dataApi/balances.ts b/packages/uniswap/src/features/dataApi/balances.ts index 7702b70924e..add0aae0c35 100644 --- a/packages/uniswap/src/features/dataApi/balances.ts +++ b/packages/uniswap/src/features/dataApi/balances.ts @@ -12,20 +12,18 @@ import { usePortfolioBalancesQuery, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { GqlResult, SpamCode } from 'uniswap/src/data/types' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks' import { fromGraphQLChain } from 'uniswap/src/features/chains/utils' import { PortfolioBalance } from 'uniswap/src/features/dataApi/types' import { buildCurrency, buildCurrencyInfo, currencyIdToContractInput, + getCurrencySafetyInfo, sortByName, usePersistedError, } from 'uniswap/src/features/dataApi/utils' -import { - useEnabledChains, - useHideSmallBalancesSetting, - useHideSpamTokensSetting, -} from 'uniswap/src/features/settings/hooks' +import { useHideSmallBalancesSetting, useHideSpamTokensSetting } from 'uniswap/src/features/settings/hooks' import { useCurrencyIdToVisibility } from 'uniswap/src/features/transactions/selectors' import { CurrencyId } from 'uniswap/src/types/currency' import { currencyId } from 'uniswap/src/utils/currencyId' @@ -57,7 +55,6 @@ export type PortfolioCacheUpdater = (hidden: boolean, portfolioBalance?: Portfol * we don't need to duplicate the polling interval when token selector is open * @param onCompleted * @param fetchPolicy - * @returns */ export function usePortfolioBalances({ address, @@ -103,6 +100,10 @@ export function usePortfolioBalances({ const byId: Record = {} balancesForAddress.forEach((balance) => { + if (!balance) { + return + } + const { __typename: tokenBalanceType, id: tokenBalanceId, @@ -111,24 +112,26 @@ export function usePortfolioBalances({ tokenProjectMarket, quantity, isHidden, - } = balance || {} - const { name, address: tokenAddress, chain, decimals, symbol, project } = token || {} - const { logoUrl, isSpam, safetyLevel, spamCode } = project || {} - const chainId = fromGraphQLChain(chain) + } = balance // require all of these fields to be defined - if (!balance || !quantity || !token) { + if (!quantity || !token) { return } + const { name, address: tokenAddress, chain, decimals, symbol, project, feeData, protectionInfo } = token + const { logoUrl, isSpam, safetyLevel, spamCode } = project || {} + const chainId = fromGraphQLChain(chain) + const currency = buildCurrency({ chainId, address: tokenAddress, decimals, symbol, name, + buyFeeBps: feeData?.buyFeeBps, + sellFeeBps: feeData?.sellFeeBps, }) - if (!currency) { return } @@ -137,14 +140,16 @@ export function usePortfolioBalances({ const currencyInfo = buildCurrencyInfo({ currency, - currencyId: currencyId(currency), + currencyId: id, logoUrl, isSpam, safetyLevel, + safetyInfo: getCurrencySafetyInfo(safetyLevel, protectionInfo), spamCode, }) const portfolioBalance = buildPortfolioBalance({ + id: tokenBalanceId, cacheId: `${tokenBalanceType}:${tokenBalanceId}`, quantity, balanceUSD: denominatedValue?.value, diff --git a/packages/uniswap/src/features/dataApi/searchTokens.test.ts b/packages/uniswap/src/features/dataApi/searchTokens.test.ts index e992b5b3922..7ccb426097b 100644 --- a/packages/uniswap/src/features/dataApi/searchTokens.test.ts +++ b/packages/uniswap/src/features/dataApi/searchTokens.test.ts @@ -1,4 +1,5 @@ import { waitFor } from '@testing-library/react-native' +import { TokenQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { useSearchTokens } from 'uniswap/src/features/dataApi/searchTokens' import { useTokenProjects } from 'uniswap/src/features/dataApi/tokenProjects' import { gqlTokenToCurrencyInfo } from 'uniswap/src/features/dataApi/utils' @@ -25,15 +26,26 @@ describe(useTokenProjects, () => { const { resolvers, resolved } = queryResolvers({ searchTokens: () => createArray(5, token), }) - const { result } = renderHook(() => useSearchTokens('', null, false), { + const { result } = renderHook(() => useSearchTokens('hi', null, false), { resolvers, }) await waitFor(async () => { - const expectedData = (await resolved.searchTokens).map(gqlTokenToCurrencyInfo).map(removeSafetyInfo) + const expectedData = (await resolved.searchTokens) + .map(removeIsSpam) + .map(gqlTokenToCurrencyInfo) + .map(removeSafetyInfo) const actualData = result.current.data?.map(removeSafetyInfo) expect(actualData).toEqual(expectedData) }) }) }) + +// TODO(WALL-5157): remove once `queryResolvers` is fixed. +function removeIsSpam( + searchToken: NonNullable>, +): NonNullable> { + delete searchToken.project?.isSpam + return searchToken +} diff --git a/packages/uniswap/src/features/dataApi/searchTokens.ts b/packages/uniswap/src/features/dataApi/searchTokens.ts index 1a2bd32f20c..9a1101e16ee 100644 --- a/packages/uniswap/src/features/dataApi/searchTokens.ts +++ b/packages/uniswap/src/features/dataApi/searchTokens.ts @@ -1,11 +1,11 @@ import { useCallback, useMemo } from 'react' import { useSearchTokensQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { GqlResult } from 'uniswap/src/data/types' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { toGraphQLChain } from 'uniswap/src/features/chains/utils' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' import { gqlTokenToCurrencyInfo, usePersistedError } from 'uniswap/src/features/dataApi/utils' -import { useEnabledChains } from 'uniswap/src/features/settings/hooks' -import { UniverseChainId } from 'uniswap/src/types/chains' export function useSearchTokens( searchQuery: string | null, @@ -19,7 +19,7 @@ export function useSearchTokens( searchQuery: searchQuery ?? '', chains: gqlChainFilter ? [gqlChainFilter] : gqlChains, }, - skip, + skip: skip || !searchQuery, }) const persistedError = usePersistedError(loading, error) diff --git a/packages/uniswap/src/features/dataApi/topTokens.test.ts b/packages/uniswap/src/features/dataApi/topTokens.test.ts index 21130400fc8..a10875490a4 100644 --- a/packages/uniswap/src/features/dataApi/topTokens.test.ts +++ b/packages/uniswap/src/features/dataApi/topTokens.test.ts @@ -1,9 +1,9 @@ +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { usePopularTokens } from 'uniswap/src/features/dataApi/topTokens' import { gqlTokenToCurrencyInfo } from 'uniswap/src/features/dataApi/utils' import { token } from 'uniswap/src/test/fixtures' import { act, renderHook, waitFor } from 'uniswap/src/test/test-utils' import { createArray, queryResolvers } from 'uniswap/src/test/utils' -import { UniverseChainId } from 'uniswap/src/types/chains' describe(usePopularTokens, () => { it('returns loading true when data is being fetched', async () => { diff --git a/packages/uniswap/src/features/dataApi/topTokens.ts b/packages/uniswap/src/features/dataApi/topTokens.ts index cff7b2c928e..fe2f0b4fb7f 100644 --- a/packages/uniswap/src/features/dataApi/topTokens.ts +++ b/packages/uniswap/src/features/dataApi/topTokens.ts @@ -1,19 +1,18 @@ import { useMemo } from 'react' -import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' import { TokenSortableField, useTopTokensQuery, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { GqlResult } from 'uniswap/src/data/types' -import { toGraphQLChain } from 'uniswap/src/features/chains/utils' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { isTestnetChain, toGraphQLChain } from 'uniswap/src/features/chains/utils' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' import { gqlTokenToCurrencyInfo, usePersistedError } from 'uniswap/src/features/dataApi/utils' -import { UniverseChainId } from 'uniswap/src/types/chains' import { useMemoCompare } from 'utilities/src/react/hooks' export function usePopularTokens(chainFilter: UniverseChainId): GqlResult { const gqlChainFilter = toGraphQLChain(chainFilter) - const isTestnet = UNIVERSE_CHAIN_INFO[chainFilter].testnet + const isTestnet = isTestnetChain(chainFilter) const { data, loading, error, refetch } = useTopTokensQuery({ variables: { diff --git a/packages/uniswap/src/features/dataApi/types.ts b/packages/uniswap/src/features/dataApi/types.ts index 78159ba9e66..b2ff404dbc1 100644 --- a/packages/uniswap/src/features/dataApi/types.ts +++ b/packages/uniswap/src/features/dataApi/types.ts @@ -34,6 +34,7 @@ export type CurrencyInfo = { // Portfolio balance as exposed to the app export type PortfolioBalance = { + id: string cacheId: string quantity: number // float representation of balance balanceUSD: Maybe diff --git a/packages/uniswap/src/features/dataApi/utils.test.ts b/packages/uniswap/src/features/dataApi/utils.test.ts index 96ffdde8d09..74d38269ffc 100644 --- a/packages/uniswap/src/features/dataApi/utils.test.ts +++ b/packages/uniswap/src/features/dataApi/utils.test.ts @@ -5,6 +5,7 @@ import { Token as GQLToken, TokenProject, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { fromGraphQLChain } from 'uniswap/src/features/chains/utils' import { CurrencyInfo, PortfolioBalance } from 'uniswap/src/features/dataApi/types' import { @@ -24,7 +25,6 @@ import { usdcTokenProject, } from 'uniswap/src/test/fixtures' import { renderHook } from 'uniswap/src/test/test-utils' -import { UniverseChainId } from 'uniswap/src/types/chains' describe(currencyIdToContractInput, () => { it('converts currencyId to ContractInput', () => { diff --git a/packages/uniswap/src/features/dataApi/utils.ts b/packages/uniswap/src/features/dataApi/utils.ts index 9351ee78a92..6d0862c3ccc 100644 --- a/packages/uniswap/src/features/dataApi/utils.ts +++ b/packages/uniswap/src/features/dataApi/utils.ts @@ -11,10 +11,10 @@ import { TokenProjectsQuery, TokenQuery, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { fromGraphQLChain, toGraphQLChain } from 'uniswap/src/features/chains/utils' import { AttackType, CurrencyInfo, PortfolioBalance, SafetyInfo, TokenList } from 'uniswap/src/features/dataApi/types' import { NativeCurrency } from 'uniswap/src/features/tokens/NativeCurrency' -import { UniverseChainId } from 'uniswap/src/types/chains' import { CurrencyId } from 'uniswap/src/types/currency' import { currencyId, @@ -22,6 +22,7 @@ import { currencyIdToGraphQLAddress, isNativeCurrencyAddress, } from 'uniswap/src/utils/currencyId' +import { sortKeysRecursively } from 'utilities/src/primitives/objects' type BuildCurrencyParams = { chainId?: Nullable @@ -51,7 +52,7 @@ export function tokenProjectToCurrencyInfos( ?.flatMap((project) => project?.tokens.map((token) => { const { logoUrl, safetyLevel } = project ?? {} - const { name, chain, address, decimals, symbol } = token ?? {} + const { name, chain, address, decimals, symbol, feeData, protectionInfo } = token ?? {} const chainId = fromGraphQLChain(chain) if (chainFilter && chainFilter !== chainId) { @@ -64,6 +65,8 @@ export function tokenProjectToCurrencyInfos( decimals, symbol, name, + buyFeeBps: feeData?.buyFeeBps, + sellFeeBps: feeData?.sellFeeBps, }) if (!currency) { @@ -75,10 +78,7 @@ export function tokenProjectToCurrencyInfos( currencyId: currencyId(currency), logoUrl, safetyLevel, - safetyInfo: { - tokenList: getTokenListFromSafetyLevel(project?.safetyLevel), - protectionResult: ProtectionResult.Unknown, - }, + safetyInfo: getCurrencySafetyInfo(safetyLevel, protectionInfo), }) return currencyInfo @@ -113,11 +113,13 @@ export function buildCurrency(args: BuildCurrencyParams): Token | NativeCurrency return undefined } - const cacheKey = JSON.stringify(args, Object.keys(args).sort()) + const cacheKey = JSON.stringify(sortKeysRecursively(args)) - if (CURRENCY_CACHE.has(cacheKey)) { + const cachedCurrency = CURRENCY_CACHE.get(cacheKey) + + if (cachedCurrency) { // This allows us to better memoize components that use a `Currency` as a dependency. - return CURRENCY_CACHE.get(cacheKey) + return cachedCurrency } const buyFee = buyFeeBps && BigNumber.from(buyFeeBps).gt(0) ? BigNumber.from(buyFeeBps) : undefined @@ -134,13 +136,7 @@ export function buildCurrency(args: BuildCurrencyParams): Token | NativeCurrency const CURRENCY_INFO_CACHE = new Map() export function buildCurrencyInfo(args: CurrencyInfo): CurrencyInfo { - const sortedArgs = Object.fromEntries( - Object.keys(args) - .sort() - .map((key) => [key, args[key as keyof CurrencyInfo]]), - ) as CurrencyInfo - - const cacheKey = JSON.stringify(sortedArgs) + const cacheKey = JSON.stringify(sortKeysRecursively(args)) const cachedCurrencyInfo = CURRENCY_INFO_CACHE.get(cacheKey) @@ -174,6 +170,8 @@ function getHighestPriorityAttackType(attackTypes?: (ProtectionAttackType | unde return AttackType.Impersonator } else if (attackTypeSet.has(ProtectionAttackType.AirdropPattern)) { return AttackType.Airdrop + } else if (attackTypeSet.has(ProtectionAttackType.HighFees)) { + return AttackType.HighFees } else { return AttackType.Other } diff --git a/packages/uniswap/src/features/ens/api.ts b/packages/uniswap/src/features/ens/api.ts index 5b5e3490952..5ae517f9623 100644 --- a/packages/uniswap/src/features/ens/api.ts +++ b/packages/uniswap/src/features/ens/api.ts @@ -1,8 +1,8 @@ /* eslint-disable @typescript-eslint/explicit-function-return-type */ import { skipToken, useQuery } from '@tanstack/react-query' import { providers } from 'ethers/lib/ethers' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { createEthersProvider } from 'uniswap/src/features/providers/createEthersProvider' -import { UniverseChainId } from 'uniswap/src/types/chains' import { areAddressesEqual } from 'uniswap/src/utils/addresses' import { ONE_MINUTE_MS } from 'utilities/src/time/time' diff --git a/packages/uniswap/src/features/ens/constants.ts b/packages/uniswap/src/features/ens/constants.ts new file mode 100644 index 00000000000..963bbc2b825 --- /dev/null +++ b/packages/uniswap/src/features/ens/constants.ts @@ -0,0 +1 @@ +export const ENS_SUFFIX = '.eth' diff --git a/packages/uniswap/src/features/ens/useENS.ts b/packages/uniswap/src/features/ens/useENS.ts index c5b3fde72eb..5cf31b33d7f 100644 --- a/packages/uniswap/src/features/ens/useENS.ts +++ b/packages/uniswap/src/features/ens/useENS.ts @@ -1,7 +1,8 @@ // Copied from https://github.com/Uniswap/interface/blob/main/src/hooks/useENS.ts +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { useAddressFromEns, useENSName } from 'uniswap/src/features/ens/api' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { ENS_SUFFIX } from 'uniswap/src/features/ens/constants' import { getValidAddress } from 'uniswap/src/utils/addresses' import { useDebounce } from 'utilities/src/time/timing' @@ -50,5 +51,5 @@ export function getCompletedENSName(name: string | null): string | null { } // Append the .eth if does not already exist - return name.endsWith('.eth') ? name : name.concat('.eth') + return name.endsWith(ENS_SUFFIX) ? name : name.concat(ENS_SUFFIX) } diff --git a/packages/uniswap/src/features/favorites/slice.ts b/packages/uniswap/src/features/favorites/slice.ts index 1e000d1cabf..103dc257934 100644 --- a/packages/uniswap/src/features/favorites/slice.ts +++ b/packages/uniswap/src/features/favorites/slice.ts @@ -1,7 +1,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { Ether } from '@uniswap/sdk-core' import { WBTC } from 'uniswap/src/constants/tokens' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { CurrencyId } from 'uniswap/src/types/currency' import { currencyId as idFromCurrency } from 'uniswap/src/utils/currencyId' import { logger } from 'utilities/src/logger/logger' @@ -90,6 +90,7 @@ export const slice = createSlice({ { payload: { nftKey, isSpam } }: PayloadAction<{ nftKey: string; isSpam?: boolean }>, ) => { const isVisible = state.nftsVisibility[nftKey]?.isVisible ?? !isSpam + state.nftsVisibility[nftKey] = { isVisible: !isVisible } }, }, diff --git a/packages/uniswap/src/features/fiatCurrency/hooks.ts b/packages/uniswap/src/features/fiatCurrency/hooks.ts index 3856794a122..d990f86820a 100644 --- a/packages/uniswap/src/features/fiatCurrency/hooks.ts +++ b/packages/uniswap/src/features/fiatCurrency/hooks.ts @@ -1,8 +1,9 @@ -import { useCallback } from 'react' +import { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' import { AppTFunction } from 'ui/src/i18n/types' -import { FiatCurrency } from 'uniswap/src/features/fiatCurrency/constants' +import { useUrlContext } from 'uniswap/src/contexts/UrlContext' +import { FiatCurrency, ORDERED_CURRENCIES } from 'uniswap/src/features/fiatCurrency/constants' import { FiatCurrencyInfo } from 'uniswap/src/features/fiatOnRamp/types' import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext' import { useCurrentLocale } from 'uniswap/src/features/language/hooks' @@ -109,12 +110,28 @@ export function useFiatCurrencyInfo(currency: FiatCurrency): FiatCurrencyInfo { } } +function useUrlLocalCurrency(): FiatCurrency | undefined { + const { useParsedQueryString } = useUrlContext() + const parsed = useParsedQueryString() + const parsedLocalCurrency = parsed.cur + + if (typeof parsedLocalCurrency !== 'string') { + return undefined + } + + const lowerCaseSupportedLocalCurrency = parsedLocalCurrency.toLowerCase() + return ORDERED_CURRENCIES.find((localCurrency) => localCurrency.toLowerCase() === lowerCaseSupportedLocalCurrency) +} + /** * Hook used to return the current selected fiat currency in the app * @returns currently selected fiat currency */ export function useAppFiatCurrency(): FiatCurrency { - return useSelector((state: UniswapState) => state.userSettings.currentCurrency) + const storeFiatCurrency = useSelector((state: UniswapState) => state.userSettings.currentCurrency) + const urlFiatCurrency = useUrlLocalCurrency() + + return useMemo(() => urlFiatCurrency ?? storeFiatCurrency, [storeFiatCurrency, urlFiatCurrency]) } /** diff --git a/packages/uniswap/src/features/fiatOnRamp/api.ts b/packages/uniswap/src/features/fiatOnRamp/api.ts index 0edd6ad2e16..927ec6d4a86 100644 --- a/packages/uniswap/src/features/fiatOnRamp/api.ts +++ b/packages/uniswap/src/features/fiatOnRamp/api.ts @@ -7,6 +7,7 @@ import { FORQuoteRequest, FORQuoteResponse, FORServiceProvidersResponse, + FORSupportedCountriesRequest, FORSupportedCountriesResponse, FORSupportedFiatCurrenciesRequest, FORSupportedFiatCurrenciesResponse, @@ -17,9 +18,126 @@ import { FORTransferWidgetUrlRequest, FORWidgetUrlRequest, FORWidgetUrlResponse, + OffRampTransferDetailsRequest, + OffRampTransferDetailsResponse, + OffRampWidgetUrlRequest, } from 'uniswap/src/features/fiatOnRamp/types' import { transformPaymentMethods } from 'uniswap/src/features/fiatOnRamp/utils' +import { FeatureFlags } from 'uniswap/src/features/gating/flags' +import { getFeatureFlag } from 'uniswap/src/features/gating/hooks' +import { isMobileApp } from 'utilities/src/platform' +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export function getFiatOnRampAggregatorApi() { + if (!isMobileApp) { + return fiatOnRampAggregatorApi + } + + let isForMigrationEnabled = false + try { + isForMigrationEnabled = getFeatureFlag(FeatureFlags.ForMonorepoMigration) + } catch {} + + return isForMigrationEnabled ? fiatOnRampAggregatorApiV2 : fiatOnRampAggregatorApi +} + +export const fiatOnRampAggregatorApiV2 = createApi({ + reducerPath: 'fiatOnRampAggregatorApi-uniswap', + baseQuery: fetchBaseQuery({ + baseUrl: uniswapUrls.forApiUrl, + headers: FOR_API_HEADERS, + }), + endpoints: (builder) => ({ + fiatOnRampAggregatorCountryList: builder.query({ + query: (request) => ({ url: '/SupportedCountries', body: request, method: 'POST' }), + }), + fiatOnRampAggregatorGetCountry: builder.query({ + query: () => ({ url: '/GetCountry', body: {}, method: 'POST' }), + }), + fiatOnRampAggregatorCryptoQuote: builder.query({ + query: (request) => ({ + url: '/Quote', + body: request, + method: 'POST', + }), + keepUnusedDataFor: 0, + transformResponse: (response: FORQuoteResponse) => ({ + ...response, + quotes: response.quotes?.map((quote) => ({ + ...quote, + serviceProviderDetails: { + ...quote.serviceProviderDetails, + paymentMethods: transformPaymentMethods(quote.serviceProviderDetails.paymentMethods), + }, + })), + }), + }), + fiatOnRampAggregatorTransferServiceProviders: builder.query({ + query: () => ({ url: '/TransferServiceProviders', body: {}, method: 'POST' }), + keepUnusedDataFor: 60 * 60, // 1 hour + }), + fiatOnRampAggregatorSupportedTokens: builder.query({ + query: (request) => ({ + url: '/SupportedTokens', + body: request, + method: 'POST', + }), + }), + fiatOnRampAggregatorSupportedFiatCurrencies: builder.query< + FORSupportedFiatCurrenciesResponse, + FORSupportedFiatCurrenciesRequest + >({ + query: (request) => ({ + url: '/SupportedFiatCurrencies', + body: request, + method: 'POST', + }), + }), + fiatOnRampAggregatorWidget: builder.query({ + query: (request) => ({ + url: '/WidgetUrl', + body: request, + method: 'POST', + }), + }), + fiatOnRampAggregatorTransferWidget: builder.query({ + query: (request) => ({ + url: '/TransferWidgetUrl', + body: request, + method: 'POST', + }), + }), + /** + * Fetches a fiat onramp transaction by its ID, with no signature authentication. + */ + fiatOnRampAggregatorTransaction: builder.query< + FORTransactionResponse, + // TODO: make sessionId required in FORTransactionRequest after backend is updated + Omit & { sessionId: string } + >({ + query: (request) => ({ url: '/Transaction', body: request, method: 'POST' }), + }), + fiatOnRampAggregatorOffRampWidget: builder.query({ + query: (request) => ({ + url: '/OffRampWidgetUrl', + body: request, + method: 'POST', + }), + }), + fiatOnRampAggregatorOffRampTransferDetails: builder.query< + OffRampTransferDetailsResponse, + OffRampTransferDetailsRequest + >({ + query: (request) => ({ + url: '/OffRampTransferDetails', + body: request, + method: 'POST', + }), + }), + }), +}) + +// TODO: WALL-5189 - remove this once we finish migrating away from original FOR endpoint service export const fiatOnRampAggregatorApi = createApi({ reducerPath: 'fiatOnRampAggregatorApi-uniswap', baseQuery: fetchBaseQuery({ @@ -27,7 +145,7 @@ export const fiatOnRampAggregatorApi = createApi({ headers: FOR_API_HEADERS, }), endpoints: (builder) => ({ - fiatOnRampAggregatorCountryList: builder.query({ + fiatOnRampAggregatorCountryList: builder.query({ query: () => `/supported-countries`, }), fiatOnRampAggregatorGetCountry: builder.query({ @@ -56,13 +174,19 @@ export const fiatOnRampAggregatorApi = createApi({ keepUnusedDataFor: 60 * 60, // 1 hour }), fiatOnRampAggregatorSupportedTokens: builder.query({ - query: (request) => `/supported-tokens?${new URLSearchParams(request).toString()}`, + query: (request) => ({ + url: `/supported-tokens`, + params: request, + }), }), fiatOnRampAggregatorSupportedFiatCurrencies: builder.query< FORSupportedFiatCurrenciesResponse, FORSupportedFiatCurrenciesRequest >({ - query: (request) => `/supported-fiat-currencies?${new URLSearchParams(request).toString()}`, + query: (request) => ({ + url: '/supported-fiat-currencies', + params: request, + }), }), fiatOnRampAggregatorWidget: builder.query({ query: (request) => ({ @@ -88,6 +212,14 @@ export const fiatOnRampAggregatorApi = createApi({ >({ query: (request) => `/transaction?${objectToQueryString(request)}`, }), + // stubbing out these endpoints so that v2 works + fiatOnRampAggregatorOffRampWidget: builder.query({ + query: () => '', + }), + fiatOnRampAggregatorOffRampTransferDetails: builder.query< + OffRampTransferDetailsResponse, + OffRampTransferDetailsRequest + >({ query: () => '' }), }), }) @@ -100,4 +232,4 @@ export const { useFiatOnRampAggregatorSupportedFiatCurrenciesQuery, useFiatOnRampAggregatorWidgetQuery, useFiatOnRampAggregatorTransferWidgetQuery, -} = fiatOnRampAggregatorApi +} = getFiatOnRampAggregatorApi() diff --git a/packages/uniswap/src/features/fiatOnRamp/hooks.ts b/packages/uniswap/src/features/fiatOnRamp/hooks.ts index bb5132c468a..68469a7303e 100644 --- a/packages/uniswap/src/features/fiatOnRamp/hooks.ts +++ b/packages/uniswap/src/features/fiatOnRamp/hooks.ts @@ -5,24 +5,21 @@ import { useCallback, useMemo, useRef } from 'react' import { useTranslation } from 'react-i18next' import { getCountry } from 'react-native-localize' import { useDispatch } from 'react-redux' -import { useCurrencies } from 'uniswap/src/components/TokenSelector/hooks' +import { useCurrencies } from 'uniswap/src/components/TokenSelector/hooks/useCurrencies' import { useAccountMeta } from 'uniswap/src/contexts/UniswapContext' import { Routing } from 'uniswap/src/data/tradingApi/__generated__/index' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { toSupportedChainId } from 'uniswap/src/features/chains/utils' import { FiatCurrency } from 'uniswap/src/features/fiatCurrency/constants' import { useAppFiatCurrencyInfo, useFiatCurrencyInfo } from 'uniswap/src/features/fiatCurrency/hooks' -import { - useFiatOnRampAggregatorCryptoQuoteQuery, - useFiatOnRampAggregatorGetCountryQuery, - useFiatOnRampAggregatorSupportedFiatCurrenciesQuery, - useFiatOnRampAggregatorSupportedTokensQuery, -} from 'uniswap/src/features/fiatOnRamp/api' +import { getFiatOnRampAggregatorApi } from 'uniswap/src/features/fiatOnRamp/api' import { FORQuote, FORSupportedFiatCurrency, FORSupportedToken, FiatCurrencyInfo, FiatOnRampCurrency, + RampDirection, } from 'uniswap/src/features/fiatOnRamp/types' import { createOnRampTransactionId, @@ -39,7 +36,6 @@ import { TransactionStatus, TransactionType, } from 'uniswap/src/features/transactions/types/transactionDetails' -import { UniverseChainId } from 'uniswap/src/types/chains' import { getFormattedCurrencyAmount } from 'uniswap/src/utils/currency' import { buildCurrencyId, buildNativeCurrencyId } from 'uniswap/src/utils/currencyId' import { NumberType } from 'utilities/src/format/types' @@ -100,6 +96,7 @@ export function useFiatOnRampTransactionCreator( export function useMeldFiatCurrencySupportInfo( countryCode: string, skip: boolean = false, + rampDirection?: RampDirection, ): { appFiatCurrencySupportedInMeld: boolean meldSupportedFiatCurrency: FiatCurrencyInfo @@ -110,8 +107,9 @@ export function useMeldFiatCurrencySupportInfo( const fallbackCurrencyInfo = useFiatCurrencyInfo(FiatCurrency.UnitedStatesDollar) const appFiatCurrencyCode = appFiatCurrencyInfo.code.toLowerCase() + const { useFiatOnRampAggregatorSupportedFiatCurrenciesQuery } = getFiatOnRampAggregatorApi() const { data: supportedFiatCurrencies } = useFiatOnRampAggregatorSupportedFiatCurrenciesQuery( - { countryCode }, + { countryCode, rampDirection }, { skip }, ) @@ -142,22 +140,28 @@ export function useFiatOnRampSupportedTokens({ sourceCurrencyCode, countryCode, skip = false, + rampDirection, }: { sourceCurrencyCode: string countryCode: string skip?: boolean + rampDirection?: RampDirection }): { error: boolean list: FiatOnRampCurrency[] | undefined loading: boolean refetch: () => void } { + const { useFiatOnRampAggregatorSupportedTokensQuery } = getFiatOnRampAggregatorApi() const { data: supportedTokensResponse, isLoading: supportedTokensLoading, error: supportedTokensError, refetch: refetchSupportedTokens, - } = useFiatOnRampAggregatorSupportedTokensQuery({ fiatCurrency: sourceCurrencyCode, countryCode }, { skip }) + } = useFiatOnRampAggregatorSupportedTokensQuery( + { fiatCurrency: sourceCurrencyCode, countryCode, rampDirection }, + { skip }, + ) const supportedTokensById: Record = useMemo( () => @@ -212,12 +216,14 @@ export function useFiatOnRampQuotes({ quoteCurrencyCode, countryCode, countryState, + rampDirection, }: { baseCurrencyAmount?: number baseCurrencyCode: string | undefined quoteCurrencyCode: string | undefined countryCode: string | undefined countryState: string | undefined + rampDirection: RampDirection }): { loading: boolean error?: FetchBaseQueryError | SerializedError @@ -226,6 +232,7 @@ export function useFiatOnRampQuotes({ const debouncedBaseCurrencyAmount = useDebounce(baseCurrencyAmount, SHORT_DELAY) const walletAddress = useAccountMeta()?.address + const { useFiatOnRampAggregatorCryptoQuoteQuery } = getFiatOnRampAggregatorApi() const { currentData: quotesResponse, isFetching: quotesFetching, @@ -234,11 +241,12 @@ export function useFiatOnRampQuotes({ baseCurrencyAmount && countryCode && quoteCurrencyCode && baseCurrencyCode ? { sourceAmount: baseCurrencyAmount, - sourceCurrencyCode: baseCurrencyCode, - destinationCurrencyCode: quoteCurrencyCode, + sourceCurrencyCode: rampDirection === RampDirection.OFFRAMP ? quoteCurrencyCode : baseCurrencyCode, + destinationCurrencyCode: rampDirection === RampDirection.OFFRAMP ? baseCurrencyCode : quoteCurrencyCode, countryCode, walletAddress: walletAddress ?? undefined, state: countryState, + rampDirection, } : skipToken, { @@ -300,6 +308,7 @@ export function useIsSupportedFiatOnRampCurrency( skip: boolean = false, ): FiatOnRampCurrency | undefined { const fallbackCountryCode = getCountry() + const { useFiatOnRampAggregatorGetCountryQuery } = getFiatOnRampAggregatorApi() const { currentData: ipCountryData } = useFiatOnRampAggregatorGetCountryQuery(undefined, { skip }) const { meldSupportedFiatCurrency } = useMeldFiatCurrencySupportInfo( ipCountryData?.countryCode ?? fallbackCountryCode, diff --git a/packages/uniswap/src/features/fiatOnRamp/types.ts b/packages/uniswap/src/features/fiatOnRamp/types.ts index 9379f79d008..222ba0e5d92 100644 --- a/packages/uniswap/src/features/fiatOnRamp/types.ts +++ b/packages/uniswap/src/features/fiatOnRamp/types.ts @@ -26,6 +26,10 @@ export type FORGetCountryResponse = FORCountry // /supported-countries +export type FORSupportedCountriesRequest = { + rampDirection?: RampDirection +} + export type FORSupportedCountriesResponse = { supportedCountries: FORCountry[] } @@ -39,6 +43,7 @@ export type FORQuoteRequest = { sourceCurrencyCode: string walletAddress?: string state?: string + rampDirection?: RampDirection } export type FORQuote = { @@ -83,6 +88,7 @@ export type FORServiceProvidersResponse = { export type FORSupportedTokensRequest = { fiatCurrency: string countryCode: string + rampDirection?: RampDirection } export type FORSupportedToken = { @@ -102,6 +108,7 @@ export type FORSupportedTokensResponse = { export type FORSupportedFiatCurrenciesRequest = { countryCode: string + rampDirection?: RampDirection } export type FORSupportedFiatCurrency = { @@ -114,7 +121,7 @@ export type FORSupportedFiatCurrenciesResponse = { fiatCurrencies: FORSupportedFiatCurrency[] } -// /widget-url +// /widget-url and /offramp-widget-url export type FORWidgetUrlRequest = { sourceAmount: number @@ -132,6 +139,21 @@ export type FORWidgetUrlResponse = { widgetUrl: string } +export type OffRampWidgetUrlRequest = { + sourceAmount: number + baseCurrencyCode: string + refundWalletAddress: string + countryCode: string + quoteCurrencyCode: string + serviceProvider: string + externalSessionId: string + lockAmount?: string + requestSource?: string + externalTransactionId?: string + externalCustomerId?: string + redirectUrl?: string +} + // /transfer-widget-url export type FORTransferWidgetUrlRequest = { @@ -141,6 +163,43 @@ export type FORTransferWidgetUrlRequest = { redirectUrl: string } +// /offramp-transfer-details + +export type OffRampTransferDetailsRequest = { + requestSource?: string + transferProviderDetails: + | { + value: MoonpayOffRampTransferDetailsRequest + case: 'moonpayDetails' + } + | { + value: MeldOffRampTransferDetailsRequest + case: 'meldDetails' + } + | { + case: undefined + value?: undefined + } +} + +type MoonpayOffRampTransferDetailsRequest = { + baseCurrencyCode: string + baseCurrencyAmount: number + depositWalletAddress: string + depositWalletAddressTag?: string +} + +type MeldOffRampTransferDetailsRequest = { + sessionId: string +} + +export type OffRampTransferDetailsResponse = { + baseCurrencyCode: string + baseCurrencyAmount: number + depositWalletAddress: string + depositWalletAddressTag?: string +} + // /transactions export type FORCryptoDetails = { @@ -199,3 +258,8 @@ export enum RampToggle { BUY = 'BUY', SELL = 'SELL', } + +export enum RampDirection { + ONRAMP = 0, + OFFRAMP = 1, +} diff --git a/packages/uniswap/src/features/fiatOnRamp/useCexTransferProviders.ts b/packages/uniswap/src/features/fiatOnRamp/useCexTransferProviders.ts index ee1ddd91aa1..98dacf8d071 100644 --- a/packages/uniswap/src/features/fiatOnRamp/useCexTransferProviders.ts +++ b/packages/uniswap/src/features/fiatOnRamp/useCexTransferProviders.ts @@ -1,8 +1,9 @@ import { useMemo } from 'react' -import { useFiatOnRampAggregatorTransferServiceProvidersQuery } from 'uniswap/src/features/fiatOnRamp/api' +import { getFiatOnRampAggregatorApi } from 'uniswap/src/features/fiatOnRamp/api' import { FORServiceProvider } from 'uniswap/src/features/fiatOnRamp/types' export function useCexTransferProviders(params?: { isDisabled?: boolean }): FORServiceProvider[] { + const { useFiatOnRampAggregatorTransferServiceProvidersQuery } = getFiatOnRampAggregatorApi() const { data } = useFiatOnRampAggregatorTransferServiceProvidersQuery(undefined, { skip: params?.isDisabled, }) diff --git a/packages/uniswap/src/features/gas/hooks.ts b/packages/uniswap/src/features/gas/hooks.ts index 02ee00a532f..1731430a06d 100644 --- a/packages/uniswap/src/features/gas/hooks.ts +++ b/packages/uniswap/src/features/gas/hooks.ts @@ -9,6 +9,8 @@ import { useAccountMeta, useProvider } from 'uniswap/src/contexts/UniswapContext import { useGasFeeQuery } from 'uniswap/src/data/apiClients/uniswapApi/useGasFeeQuery' import { GasEstimate, GasStrategy } from 'uniswap/src/data/tradingApi/types' import { AccountMeta } from 'uniswap/src/features/accounts/types' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { FormattedUniswapXGasFeeInfo, GasFeeResult, @@ -21,7 +23,6 @@ import { DynamicConfigs, GasStrategies, GasStrategyType } from 'uniswap/src/feat import { Statsig, useConfig } from 'uniswap/src/features/gating/sdk/statsig' import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext' import { useOnChainNativeCurrencyBalance } from 'uniswap/src/features/portfolio/api' -import { useEnabledChains } from 'uniswap/src/features/settings/hooks' import { NativeCurrency } from 'uniswap/src/features/tokens/NativeCurrency' import { ValueType, getCurrencyAmount } from 'uniswap/src/features/tokens/getCurrencyAmount' import { DerivedSendInfo } from 'uniswap/src/features/transactions/send/types' @@ -29,7 +30,6 @@ import { usePollingIntervalByChain } from 'uniswap/src/features/transactions/swa import { useUSDCValueWithStatus } from 'uniswap/src/features/transactions/swap/hooks/useUSDCPrice' import { DerivedSwapInfo } from 'uniswap/src/features/transactions/swap/types/derivedSwapInfo' import { UniswapXGasBreakdown } from 'uniswap/src/features/transactions/swap/types/swapTxAndGasInfo' -import { UniverseChainId } from 'uniswap/src/types/chains' import { CurrencyField } from 'uniswap/src/types/currency' import { NumberType } from 'utilities/src/format/types' import { logger } from 'utilities/src/logger/logger' diff --git a/packages/uniswap/src/features/gas/useMaxAmountSpend.ts b/packages/uniswap/src/features/gas/useMaxAmountSpend.ts index db0ad19dae3..ced24cadf10 100644 --- a/packages/uniswap/src/features/gas/useMaxAmountSpend.ts +++ b/packages/uniswap/src/features/gas/useMaxAmountSpend.ts @@ -1,10 +1,10 @@ import { Currency, CurrencyAmount } from '@uniswap/sdk-core' import JSBI from 'jsbi' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { DynamicConfigs, SwapConfigKey } from 'uniswap/src/features/gating/configs' import { useDynamicConfigValue } from 'uniswap/src/features/gating/hooks' import { ValueType, getCurrencyAmount } from 'uniswap/src/features/tokens/getCurrencyAmount' import { TransactionType } from 'uniswap/src/features/transactions/types/transactionDetails' -import { UniverseChainId } from 'uniswap/src/types/chains' import { logger } from 'utilities/src/logger/logger' const NATIVE_CURRENCY_DECIMAL = 18 diff --git a/packages/uniswap/src/features/gating/configs.ts b/packages/uniswap/src/features/gating/configs.ts index 1b50285b0b5..a104038f615 100644 --- a/packages/uniswap/src/features/gating/configs.ts +++ b/packages/uniswap/src/features/gating/configs.ts @@ -6,6 +6,7 @@ import { GasStrategy } from 'uniswap/src/data/tradingApi/types' */ export enum DynamicConfigs { // Wallet + HomeScreenExploreTokens = 'home_screen_explore_tokens', MobileForceUpgrade = 'force_upgrade', OnDeviceRecovery = 'on_device_recovery', UwuLink = 'uwulink_config', @@ -24,6 +25,11 @@ export enum ForceUpgradeConfigKey { Status = 'status', } +export enum HomeScreenExploreTokensConfigKey { + EthChainId = 'ethChainId', + Tokens = 'tokens', +} + export enum OnDeviceRecoveryConfigKey { AppLoadingTimeoutMs = 'appLoadingTimeoutMs', MaxMnemonicsToLoad = 'maxMnemonicsToLoad', @@ -84,6 +90,7 @@ export type DynamicConfigKeys = { [DynamicConfigs.Swap]: SwapConfigKey // Wallet + [DynamicConfigs.HomeScreenExploreTokens]: HomeScreenExploreTokensConfigKey [DynamicConfigs.MobileForceUpgrade]: ForceUpgradeConfigKey [DynamicConfigs.OnDeviceRecovery]: OnDeviceRecoveryConfigKey [DynamicConfigs.UwuLink]: UwuLinkConfigKey diff --git a/packages/uniswap/src/features/gating/experiments.ts b/packages/uniswap/src/features/gating/experiments.ts index 065a82f7f6d..0a24661b57e 100644 --- a/packages/uniswap/src/features/gating/experiments.ts +++ b/packages/uniswap/src/features/gating/experiments.ts @@ -5,8 +5,6 @@ */ export enum Experiments { ArbitrumXV2OpenOrders = 'arbitrum_uniswapx_openorders_v2', - OnboardingRedesignHomeScreen = 'onboarding-redesign-home-screen', - OnboardingRedesignRecoveryBackup = 'onboarding-redesign-recovery-backup', AccountCTAs = 'signin_login_connect_ctas', } @@ -22,17 +20,6 @@ export enum ArbitrumXV2OpenOrderProperties { SlippageTolerance = 'slippageTolerance', } -export enum OnboardingRedesignHomeScreenProperties { - Enabled = 'enabled', - ExploreEthChainId = 'exploreEthChainId', - ExploreTokens = 'exploreTokens', -} - -export enum OnboardingRedesignRecoveryBackupProperties { - Enabled = 'enabled', - BackupReminderDelaySecs = 'backupReminderDelaySecs', -} - export enum AccountCTAsExperimentGroup { Control = 'Control', // Get the app / Connect SignInSignUp = 'SignIn-SignUp', @@ -41,6 +28,4 @@ export enum AccountCTAsExperimentGroup { export type ExperimentProperties = { [Experiments.ArbitrumXV2OpenOrders]: ArbitrumXV2OpenOrderProperties - [Experiments.OnboardingRedesignHomeScreen]: OnboardingRedesignHomeScreenProperties - [Experiments.OnboardingRedesignRecoveryBackup]: OnboardingRedesignRecoveryBackupProperties } diff --git a/packages/uniswap/src/features/gating/flags.ts b/packages/uniswap/src/features/gating/flags.ts index 6007d3c23d7..2e1a5a0f1bb 100644 --- a/packages/uniswap/src/features/gating/flags.ts +++ b/packages/uniswap/src/features/gating/flags.ts @@ -5,7 +5,6 @@ import { isInterface } from 'utilities/src/platform' */ export enum FeatureFlags { // Shared - Bridging, ForAggregator, DisableFiatOnRampKorea, IndicativeSwapQuotes, @@ -23,20 +22,20 @@ export enum FeatureFlags { OpenAIAssistant, UnitagsDeviceAttestation, UniswapX, - TestnetMode, // Mobile - Datadog, ExtensionPromotionGA, FeedTab, OnboardingKeyring, Scantastic, UwULink, FiatOffRamp, + ForMonorepoMigration, // Extension ExtensionAutoConnect, ExtensionClaimUnitag, + Datadog, // Web AATestWeb, @@ -47,18 +46,14 @@ export enum FeatureFlags { GqlTokenLists, LimitsFees, L2NFTs, - MultichainExplore, MultipleRoutingOptions, QuickRouteMainnet, Realtime, - RestExplore, TraceJsonRpc, UniswapXSyntheticQuote, UniswapXv2, - V2Everywhere, V4Everywhere, Zora, - WorldChain, // TODO(WEB-3625): Remove these once we have a generalized system for outage banners. OutageBannerArbitrum, OutageBannerOptimism, @@ -68,7 +63,6 @@ export enum FeatureFlags { // These names must match the gate name on statsig export const WEB_FEATURE_FLAG_NAMES = new Map([ // Shared - [FeatureFlags.Bridging, 'bridging'], [FeatureFlags.ForAggregator, 'for_aggregator_web'], [FeatureFlags.IndicativeSwapQuotes, 'indicative-quotes'], [FeatureFlags.TokenProtection, 'token_protection'], @@ -77,9 +71,7 @@ export const WEB_FEATURE_FLAG_NAMES = new Map([ [FeatureFlags.Datadog, 'datadog'], [FeatureFlags.UniswapXPriorityOrders, 'uniswapx_priority_orders'], [FeatureFlags.SharedSwapArbitrumUniswapXExperiment, 'shared_swap_arbitrum_uniswapx_experiment'], - [FeatureFlags.TestnetMode, 'testnet-mode'], [FeatureFlags.V4Swap, 'v4_swap'], - [FeatureFlags.WorldChain, 'world_chain'], // Web Specific [FeatureFlags.UniversalSwap, 'universal_swap'], @@ -88,16 +80,13 @@ export const WEB_FEATURE_FLAG_NAMES = new Map([ [FeatureFlags.GqlTokenLists, 'gql_token_lists'], [FeatureFlags.LimitsFees, 'limits_fees'], [FeatureFlags.L2NFTs, 'l2_nfts'], - [FeatureFlags.MultichainExplore, 'multichain_explore'], [FeatureFlags.MultipleRoutingOptions, 'multiple_routing_options'], [FeatureFlags.QuickRouteMainnet, 'enable_quick_route_mainnet'], [FeatureFlags.Realtime, 'realtime'], - [FeatureFlags.RestExplore, 'rest_explore'], [FeatureFlags.TraceJsonRpc, 'traceJsonRpc'], [FeatureFlags.AstroChainLaunchModal, 'astro_chain_launch_modal'], [FeatureFlags.UniswapXSyntheticQuote, 'uniswapx_synthetic_quote'], [FeatureFlags.UniswapXv2, 'uniswapx_v2'], - [FeatureFlags.V2Everywhere, 'v2_everywhere'], [FeatureFlags.V4Everywhere, 'v4_everywhere'], [FeatureFlags.Zora, 'zora'], [FeatureFlags.AATestWeb, 'aatest_web'], @@ -110,7 +99,6 @@ export const WEB_FEATURE_FLAG_NAMES = new Map([ // These names must match the gate name on statsig export const WALLET_FEATURE_FLAG_NAMES = new Map([ // Shared - [FeatureFlags.Bridging, 'bridging'], [FeatureFlags.ForAggregator, 'for-aggregator'], [FeatureFlags.DisableFiatOnRampKorea, 'disable-fiat-onramp-korea'], [FeatureFlags.IndicativeSwapQuotes, 'indicative-quotes'], @@ -118,9 +106,7 @@ export const WALLET_FEATURE_FLAG_NAMES = new Map([ [FeatureFlags.SelfReportSpamNFTs, 'self-report-spam-nfts'], [FeatureFlags.UniswapXPriorityOrders, 'uniswapx_priority_orders'], [FeatureFlags.SharedSwapArbitrumUniswapXExperiment, 'shared_swap_arbitrum_uniswapx_experiment'], - [FeatureFlags.TestnetMode, 'testnet-mode'], [FeatureFlags.V4Swap, 'v4_swap'], - [FeatureFlags.WorldChain, 'world_chain'], // Wallet Specific [FeatureFlags.Datadog, 'datadog'], @@ -136,6 +122,8 @@ export const WALLET_FEATURE_FLAG_NAMES = new Map([ [FeatureFlags.UwULink, 'uwu-link'], [FeatureFlags.UniswapX, 'uniswapx'], [FeatureFlags.FiatOffRamp, 'fiat-offramp'], + [FeatureFlags.ForMonorepoMigration, 'for-monorepo-migration'], + // Extension Specific [FeatureFlags.ExtensionAutoConnect, 'extension-auto-connect'], [FeatureFlags.ExtensionClaimUnitag, 'extension-claim-unitag'], diff --git a/packages/uniswap/src/features/language/constants.ts b/packages/uniswap/src/features/language/constants.ts index 9296ec3ae48..2409679537e 100644 --- a/packages/uniswap/src/features/language/constants.ts +++ b/packages/uniswap/src/features/language/constants.ts @@ -1,3 +1,5 @@ +import { isInterface } from 'utilities/src/platform' + /** * List of supported languages in app, represented by ISO 639 language code. * If you add a new locale here, be sure to add polyfills for it in intl.js, @@ -35,7 +37,6 @@ export enum Language { SpanishUnitedStates = 'es-US', Swahili = 'sw', Swedish = 'sv', - Thai = 'th', Turkish = 'tr', Ukrainian = 'uk', Urdu = 'ur', @@ -85,13 +86,14 @@ export const WEB_SUPPORTED_LANGUAGES: Language[] = [ Language.SpanishUnitedStates, Language.Swahili, Language.Swedish, - Language.Thai, Language.Turkish, Language.Ukrainian, Language.Urdu, Language.Vietnamese, ] +export const PLATFORM_SUPPORTED_LANGUAGES = isInterface ? WEB_SUPPORTED_LANGUAGES : WALLET_SUPPORTED_LANGUAGES + /** * External mapping to be used with system locale strings trying to resolve to specific language * Included different Spanish variations availabled on Android/iOS as of 11/17/23 @@ -158,7 +160,6 @@ export enum Locale { SpanishUnitedStates = 'es-US', SwahiliTanzania = 'sw-TZ', SwedishSweden = 'sv-SE', - ThaiThailand = 'th-TH', TurkishTurkey = 'tr-TR', UkrainianUkraine = 'uk-UA', UrduPakistan = 'ur-PK', @@ -203,7 +204,6 @@ export const mapLanguageToLocale: Record = { [Language.Serbian]: Locale.Serbian, [Language.Swahili]: Locale.SwahiliTanzania, [Language.Swedish]: Locale.SwedishSweden, - [Language.Thai]: Locale.ThaiThailand, [Language.Turkish]: Locale.TurkishTurkey, [Language.Ukrainian]: Locale.UkrainianUkraine, [Language.Urdu]: Locale.UrduPakistan, @@ -247,7 +247,6 @@ export const mapLocaleToLanguage: Record = { [Locale.SpanishUnitedStates]: Language.SpanishUnitedStates, [Locale.SwahiliTanzania]: Language.Swahili, [Locale.SwedishSweden]: Language.Swedish, - [Locale.ThaiThailand]: Language.Thai, [Locale.TurkishTurkey]: Language.Turkish, [Locale.UkrainianUkraine]: Language.Ukrainian, [Locale.UrduPakistan]: Language.Urdu, diff --git a/packages/uniswap/src/features/language/hooks.tsx b/packages/uniswap/src/features/language/hooks.tsx index e1aec09b4f6..8cb3421dd92 100644 --- a/packages/uniswap/src/features/language/hooks.tsx +++ b/packages/uniswap/src/features/language/hooks.tsx @@ -1,16 +1,17 @@ +import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' import { AppTFunction } from 'ui/src/i18n/types' -import { Language, Locale, mapLanguageToLocale } from 'uniswap/src/features/language/constants' +import { useUrlContext } from 'uniswap/src/contexts/UrlContext' +import { + DEFAULT_LOCALE, + Language, + Locale, + PLATFORM_SUPPORTED_LANGUAGES, + mapLanguageToLocale, +} from 'uniswap/src/features/language/constants' import { selectCurrentLanguage } from 'uniswap/src/features/settings/selectors' - -/** - * Hook used to get the currently selected language for the app - * @returns currently selected language enum - */ -export function useCurrentLanguage(): Language { - return useSelector(selectCurrentLanguage) -} +import { isInterface } from 'utilities/src/platform' export type LanguageInfo = { displayName: string @@ -226,12 +227,6 @@ export function getLanguageInfo(t: AppTFunction, language: Language): LanguageIn loggingName: 'Swedish', locale: getLocale(Language.Swedish), }, - [Language.Thai]: { - displayName: t('language.thai'), - originName: t('language.thai', { lng: getLocale(Language.Thai) }), - loggingName: 'Thai', - locale: getLocale(Language.Thai), - }, [Language.Turkish]: { displayName: t('language.turkish'), originName: t('language.turkish', { lng: getLocale(Language.Turkish) }), @@ -261,25 +256,80 @@ export function getLanguageInfo(t: AppTFunction, language: Language): LanguageIn return languageToLanguageInfo[language] } +// Language + +/** + * Hook used to get the currently selected language for the app + * @returns currently selected language enum + */ +export function useCurrentLanguage(): Language { + return useSelector(selectCurrentLanguage) +} + export function useLanguageInfo(language: Language): LanguageInfo { const { t } = useTranslation() return getLanguageInfo(t, language) } /** - * Hook used to get the locale for the currently selected language in the app - * @returns locale for the currently selected language + * Hook used to get all relevant info for the currently selected language in the app + * @returns all relevant language info */ -export function useCurrentLocale(): Locale { +export function useCurrentLanguageInfo(): LanguageInfo { const currentLanguage = useCurrentLanguage() - return getLocale(currentLanguage) + return useLanguageInfo(currentLanguage) } +// Locale + /** - * Hook used to get all relevant info for the currently selected language in the app - * @returns all relevant language info + * Returns the supported locale read from the user agent (navigator) */ -export function useCurrentLanguageInfo(): LanguageInfo { +export function navigatorLocale(): Locale | undefined { + if (!navigator.language) { + return undefined + } + + const [language, region] = navigator.language.split('-') + + if (region) { + return parseLocale(`${language}-${region.toUpperCase()}`) ?? parseLocale(language) + } + + return parseLocale(language) +} + +/** + * Given a locale string (e.g. from user agent), return the best match for corresponding Locale enum object + * @param maybeSupportedLocale the fuzzy locale identifier + */ +export function parseLocale(maybeSupportedLocale: unknown): Locale | undefined { + if (typeof maybeSupportedLocale !== 'string') { + return undefined + } + const lowerMaybeSupportedLocale = maybeSupportedLocale.toLowerCase() + return PLATFORM_SUPPORTED_LANGUAGES.map((lang) => getLocale(lang)).find( + (locale) => + locale.toLowerCase() === lowerMaybeSupportedLocale || locale.split('-')[0] === lowerMaybeSupportedLocale, + ) +} + +/** + * Hook used to get the locale for the currently selected language in the app + * @returns locale for the currently selected language + */ +export function useCurrentLocale(): Locale { + const { useParsedQueryString } = useUrlContext() + const parsedQueryString = useParsedQueryString() + const urlLocale = parseLocale(parsedQueryString.lng) const currentLanguage = useCurrentLanguage() - return useLanguageInfo(currentLanguage) + const currentLocale = getLocale(currentLanguage) + + return useMemo(() => { + if (isInterface) { + return urlLocale ?? currentLocale ?? navigatorLocale() ?? DEFAULT_LOCALE + } else { + return currentLocale + } + }, [urlLocale, currentLocale]) } diff --git a/packages/uniswap/src/features/language/localizedDayjs.ts b/packages/uniswap/src/features/language/localizedDayjs.ts index 729d4293f8a..ace270f5bff 100644 --- a/packages/uniswap/src/features/language/localizedDayjs.ts +++ b/packages/uniswap/src/features/language/localizedDayjs.ts @@ -77,7 +77,6 @@ const mapLocaleToSupportedDayjsLocale: Record = { [Locale.SpanishUnitedStates]: 'es-us', [Locale.SwahiliTanzania]: 'sw', [Locale.SwedishSweden]: 'sv', - [Locale.ThaiThailand]: 'th', [Locale.TurkishTurkey]: 'tr', [Locale.UkrainianUkraine]: 'uk', [Locale.UrduPakistan]: 'ur', diff --git a/packages/uniswap/src/features/language/saga.ts b/packages/uniswap/src/features/language/saga.ts index 5e55c277cef..24c171c3653 100644 --- a/packages/uniswap/src/features/language/saga.ts +++ b/packages/uniswap/src/features/language/saga.ts @@ -6,7 +6,7 @@ import { call, put, select, takeLatest } from 'typed-redux-saga' import { Language, Locale, - WALLET_SUPPORTED_LANGUAGES, + PLATFORM_SUPPORTED_LANGUAGES, mapDeviceLanguageToLanguage, mapLocaleToLanguage, } from 'uniswap/src/features/language/constants' @@ -61,7 +61,7 @@ function getDeviceLanguage(): Language { // Prefer languageTag as it's more specific, falls back to languageCode const mappedLanguage = mappedLanguageFromTag || mappedLanguageFromCode - if (mappedLanguage && WALLET_SUPPORTED_LANGUAGES.includes(mappedLanguage)) { + if (mappedLanguage && PLATFORM_SUPPORTED_LANGUAGES.includes(mappedLanguage)) { return mappedLanguage } } diff --git a/packages/uniswap/src/features/notifications/types.ts b/packages/uniswap/src/features/notifications/types.ts index e7854661590..e3ecf5dba6a 100644 --- a/packages/uniswap/src/features/notifications/types.ts +++ b/packages/uniswap/src/features/notifications/types.ts @@ -1,9 +1,9 @@ import { TradeType } from '@uniswap/sdk-core' import { AssetType } from 'uniswap/src/entities/assets' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' import { FinalizedTransactionStatus, TransactionType } from 'uniswap/src/features/transactions/types/transactionDetails' import { WrapType } from 'uniswap/src/features/transactions/types/wrap' -import { UniverseChainId } from 'uniswap/src/types/chains' import { WalletConnectEvent } from 'uniswap/src/types/walletConnect' export enum AppNotificationType { diff --git a/packages/uniswap/src/features/portfolio/api.ts b/packages/uniswap/src/features/portfolio/api.ts index 889aa76cc19..f1d57a6de6a 100644 --- a/packages/uniswap/src/features/portfolio/api.ts +++ b/packages/uniswap/src/features/portfolio/api.ts @@ -3,11 +3,11 @@ import { Currency, CurrencyAmount, NativeCurrency as NativeCurrencyClass } from import { Contract } from 'ethers/lib/ethers' import { useMemo } from 'react' import ERC20_ABI from 'uniswap/src/abis/erc20.json' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { getPollingIntervalByBlocktime } from 'uniswap/src/features/chains/utils' import { createEthersProvider } from 'uniswap/src/features/providers/createEthersProvider' import { NativeCurrency } from 'uniswap/src/features/tokens/NativeCurrency' import { ValueType, getCurrencyAmount } from 'uniswap/src/features/tokens/getCurrencyAmount' -import { UniverseChainId } from 'uniswap/src/types/chains' import { currencyAddress as getCurrencyAddress } from 'uniswap/src/utils/currencyId' const ONCHAIN_BALANCES_CACHE_KEY = 'OnchainBalances' diff --git a/packages/uniswap/src/features/providers/createEthersProvider.ts b/packages/uniswap/src/features/providers/createEthersProvider.ts index 3f8a20e974b..b410dc3ea44 100644 --- a/packages/uniswap/src/features/providers/createEthersProvider.ts +++ b/packages/uniswap/src/features/providers/createEthersProvider.ts @@ -1,9 +1,9 @@ import { Signer, providers as ethersProviders } from 'ethers/lib/ethers' -import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' +import { RPCType, UniverseChainId } from 'uniswap/src/features/chains/types' import { FeatureFlags, getFeatureFlagName } from 'uniswap/src/features/gating/flags' import { Statsig } from 'uniswap/src/features/gating/sdk/statsig' import { FlashbotsRpcProvider } from 'uniswap/src/features/providers/FlashbotsRpcProvider' -import { RPCType, UniverseChainId } from 'uniswap/src/types/chains' import { logger } from 'utilities/src/logger/logger' // Should use ProviderManager for provider access unless being accessed outside of ProviderManagerContext (e.g., Apollo initialization) @@ -14,7 +14,7 @@ export function createEthersProvider( ): ethersProviders.JsonRpcProvider | null { try { if (rpcType === RPCType.Private) { - const privateRPCUrl = UNIVERSE_CHAIN_INFO[chainId].rpcUrls?.[RPCType.Private]?.http[0] + const privateRPCUrl = getChainInfo(chainId).rpcUrls?.[RPCType.Private]?.http[0] if (!privateRPCUrl) { throw new Error(`No private RPC available for chain ${chainId}`) } @@ -29,13 +29,13 @@ export function createEthersProvider( } try { - const publicRPCUrl = UNIVERSE_CHAIN_INFO[chainId].rpcUrls?.[RPCType.Public]?.http[0] + const publicRPCUrl = getChainInfo(chainId).rpcUrls?.[RPCType.Public]?.http[0] if (publicRPCUrl) { return new ethersProviders.JsonRpcProvider(publicRPCUrl) } throw new Error(`No public RPC available for chain ${chainId}`) } catch (error) { - const altPublicRPCUrl = UNIVERSE_CHAIN_INFO[chainId].rpcUrls?.[RPCType.PublicAlt]?.http[0] + const altPublicRPCUrl = getChainInfo(chainId).rpcUrls?.[RPCType.PublicAlt]?.http[0] return new ethersProviders.JsonRpcProvider(altPublicRPCUrl) } } catch (error) { diff --git a/packages/uniswap/src/features/search/SearchResult.test.ts b/packages/uniswap/src/features/search/SearchResult.test.ts new file mode 100644 index 00000000000..aedef777490 --- /dev/null +++ b/packages/uniswap/src/features/search/SearchResult.test.ts @@ -0,0 +1,18 @@ +import { ENS_SUFFIX } from 'uniswap/src/features/ens/constants' +import { SearchResultType, extractDomain } from 'uniswap/src/features/search/SearchResult' +import { UNITAG_SUFFIX } from 'uniswap/src/features/unitags/constants' + +describe('extractDomain', () => { + it.each` + walletName | type | expected + ${'test'} | ${SearchResultType.Unitag} | ${UNITAG_SUFFIX} + ${'test'} | ${SearchResultType.ENSAddress} | ${ENS_SUFFIX} + ${'test.'} | ${SearchResultType.Unitag} | ${UNITAG_SUFFIX} + ${'test.eth'} | ${SearchResultType.ENSAddress} | ${'.eth'} + ${'test.uni.eth'} | ${SearchResultType.Unitag} | ${'.uni.eth'} + ${'test.something.eth'} | ${SearchResultType.ENSAddress} | ${'.something.eth'} + ${'test.cb.id'} | ${SearchResultType.ENSAddress} | ${'.cb.id'} + `('walletName=$walletName type=$type should return expected=$expected', ({ walletName, type, expected }) => { + expect(extractDomain(walletName, type)).toEqual(expected) + }) +}) diff --git a/packages/uniswap/src/features/search/SearchResult.ts b/packages/uniswap/src/features/search/SearchResult.ts index 031cdcab35c..916a06030f3 100644 --- a/packages/uniswap/src/features/search/SearchResult.ts +++ b/packages/uniswap/src/features/search/SearchResult.ts @@ -1,6 +1,8 @@ -import { SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { FeeData, SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { SafetyInfo } from 'uniswap/src/features/dataApi/types' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { ENS_SUFFIX } from 'uniswap/src/features/ens/constants' +import { UNITAG_SUBDOMAIN, UNITAG_SUFFIX } from 'uniswap/src/features/unitags/constants' export type SearchResult = TokenSearchResult | WalletSearchResult | EtherscanSearchResult | NFTCollectionSearchResult @@ -30,6 +32,7 @@ export interface TokenSearchResult extends SearchResultBase { logoUrl: string | null safetyLevel: SafetyLevel | null safetyInfo?: SafetyInfo | null + feeData?: FeeData | null } export function isTokenSearchResult(x: SearchResult): x is TokenSearchResult { @@ -75,11 +78,12 @@ export interface UnitagSearchResult extends SearchResultBase { unitag: string } -export function extractDomain(walletName: string, type: SearchResultType): string { +export function extractDomain(walletName: string, type: SearchResultType.Unitag | SearchResultType.ENSAddress): string { const index = walletName.indexOf('.') if (index === -1 || index === walletName.length - 1) { - return type === SearchResultType.Unitag ? '.uni.eth' : '.eth' + return type === SearchResultType.Unitag ? UNITAG_SUFFIX : ENS_SUFFIX } - return walletName.substring(index + 1) + const domain = walletName.substring(index) + return domain === UNITAG_SUBDOMAIN ? UNITAG_SUFFIX : domain } diff --git a/packages/uniswap/src/features/search/utils.ts b/packages/uniswap/src/features/search/utils.ts index 9dae31d6a51..275654f28a9 100644 --- a/packages/uniswap/src/features/search/utils.ts +++ b/packages/uniswap/src/features/search/utils.ts @@ -1,5 +1,5 @@ import { getNativeAddress } from 'uniswap/src/constants/addresses' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { buildCurrencyId, currencyIdToGraphQLAddress } from 'uniswap/src/utils/currencyId' export const BACKEND_NATIVE_CHAIN_ADDRESS_STRING = 'NATIVE' diff --git a/packages/uniswap/src/features/settings/hooks.test.ts b/packages/uniswap/src/features/settings/hooks.test.ts index fc0033eabd7..b795e557ff8 100644 --- a/packages/uniswap/src/features/settings/hooks.test.ts +++ b/packages/uniswap/src/features/settings/hooks.test.ts @@ -1,4 +1,3 @@ -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { TESTNET_MODE_BANNER_HEIGHT, useHideSpamTokensSetting, @@ -8,8 +7,9 @@ import { selectIsTestnetModeEnabled, selectWalletHideSpamTokensSetting } from 'u import { renderHook } from 'uniswap/src/test/test-utils' +const UserAgentMock = jest.requireMock('utilities/src/platform') jest.mock('utilities/src/platform', () => ({ - isMobileApp: jest.fn(), + ...jest.requireActual('utilities/src/platform'), })) jest.mock('uniswap/src/features/gating/hooks', () => ({ @@ -22,9 +22,8 @@ jest.mock('uniswap/src/features/settings/selectors', () => ({ selectWalletHideSpamTokensSetting: jest.fn(), })) -const mockedSelectIsTestnetModeEnabled = selectIsTestnetModeEnabled as jest.Mock const mockedSelectWalletHideSpamTokensSetting = selectWalletHideSpamTokensSetting as jest.Mock -const mockedUseFeatureFlag = useFeatureFlag as jest.Mock +const mockedSelectIsTestnetModeEnabled = selectIsTestnetModeEnabled as jest.Mock describe('useHideSpamTokensSetting', () => { it('should return true when hideSpamTokens is true', () => { @@ -44,10 +43,13 @@ describe('useHideSpamTokensSetting', () => { }) describe('useTestnetModeBannerHeight', () => { + beforeEach(() => { + UserAgentMock.isMobileApp = false + }) + it('should return TESTNET_MODE_BANNER_HEIGHT when isTestnetModeEnabled is true and isMobileApp is true', () => { + UserAgentMock.isMobileApp = true mockedSelectIsTestnetModeEnabled.mockReturnValue(true) - mockedUseFeatureFlag.mockReturnValue(true) - const { result } = renderHook(() => useTestnetModeBannerHeight()) expect(result.current).toBe(TESTNET_MODE_BANNER_HEIGHT) @@ -55,17 +57,6 @@ describe('useHideSpamTokensSetting', () => { it('should return 0 when isTestnetModeEnabled is true and isMobileApp is false', () => { mockedSelectIsTestnetModeEnabled.mockReturnValue(true) - mockedUseFeatureFlag.mockReturnValue(false) - - const { result } = renderHook(() => useTestnetModeBannerHeight()) - - expect(result.current).toBe(0) - }) - - it('should return 0 when isTestnetModeEnabled is false', () => { - mockedSelectIsTestnetModeEnabled.mockReturnValue(false) - mockedUseFeatureFlag.mockReturnValue(false) - const { result } = renderHook(() => useTestnetModeBannerHeight()) expect(result.current).toBe(0) diff --git a/packages/uniswap/src/features/settings/hooks.ts b/packages/uniswap/src/features/settings/hooks.ts index 130bea9f519..3de33cd02d1 100644 --- a/packages/uniswap/src/features/settings/hooks.ts +++ b/packages/uniswap/src/features/settings/hooks.ts @@ -1,23 +1,11 @@ -import { useMemo } from 'react' import { useSelector } from 'react-redux' -import { CONNECTION_PROVIDER_IDS } from 'uniswap/src/constants/web3' -import { useConnector } from 'uniswap/src/contexts/UniswapContext' -import { getEnabledChains, useFeatureFlaggedChainIds } from 'uniswap/src/features/chains/utils' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks' import { selectIsTestnetModeEnabled, selectWalletHideSmallBalancesSetting, selectWalletHideSpamTokensSetting, } from 'uniswap/src/features/settings/selectors' -import { WalletConnectConnector } from 'uniswap/src/features/web3/walletConnect' -import { COMBINED_CHAIN_IDS, InterfaceGqlChain, UniverseChainId } from 'uniswap/src/types/chains' -import { isTestEnv } from 'utilities/src/environment/env' -import { logger } from 'utilities/src/logger/logger' -import { isInterface, isMobileApp } from 'utilities/src/platform' -import { Connector } from 'wagmi' - -export const TESTNET_MODE_BANNER_HEIGHT = 44 +import { isMobileApp } from 'utilities/src/platform' export function useHideSmallBalancesSetting(): boolean { const { isTestnetModeEnabled } = useEnabledChains() @@ -29,60 +17,10 @@ export function useHideSpamTokensSetting(): boolean { return useSelector(selectWalletHideSpamTokensSetting) } -// Note: only use this hook for useConnectedWalletSupportedChains -// for wallet we expect useConnector to throw because there is no connector -function useConnectorWithCatch(): Connector | undefined { - try { - return useConnector() - } catch (_e) { - if (isInterface && !isTestEnv()) { - logger.error(_e, { - tags: { file: 'src/features/settings/hooks', function: 'useConnectorWithCatch' }, - }) - } - return undefined - } -} - -// Returns the chain ids supported by the user's connected wallet -function useConnectedWalletSupportedChains(): UniverseChainId[] { - const connector = useConnectorWithCatch() - // We need to memoize the connected wallet chain ids to avoid infinite loops - // caused by modifying the gqlChains returned by useEnabledChains - return useMemo(() => { - switch (connector?.type) { - case CONNECTION_PROVIDER_IDS.UNISWAP_WALLET_CONNECT_CONNECTOR_ID: - case CONNECTION_PROVIDER_IDS.WALLET_CONNECT_CONNECTOR_ID: - // Wagmi currently offers no way to discriminate a Connector as a WalletConnect connector providing access to getNamespaceChainsIds. - return (connector as WalletConnectConnector).getNamespaceChainsIds?.().length - ? (connector as WalletConnectConnector).getNamespaceChainsIds?.() - : COMBINED_CHAIN_IDS - default: - return COMBINED_CHAIN_IDS - } - }, [connector]) -} +export const TESTNET_MODE_BANNER_HEIGHT = 44 function useIsTestnetModeEnabled(): boolean { - const isTestnetModeFromState = useSelector(selectIsTestnetModeEnabled) - const isTestnetModeFromFlag = useFeatureFlag(FeatureFlags.TestnetMode) - return isTestnetModeFromState && isTestnetModeFromFlag -} - -export function useEnabledChains(): { - chains: UniverseChainId[] - gqlChains: InterfaceGqlChain[] - defaultChainId: UniverseChainId - isTestnetModeEnabled: boolean -} { - const featureFlaggedChainIds = useFeatureFlaggedChainIds() - const connectedWalletChainIds = useConnectedWalletSupportedChains() - const isTestnetModeEnabled = useIsTestnetModeEnabled() - - return useMemo( - () => getEnabledChains({ isTestnetModeEnabled, connectedWalletChainIds, featureFlaggedChainIds }), - [isTestnetModeEnabled, connectedWalletChainIds, featureFlaggedChainIds], - ) + return useSelector(selectIsTestnetModeEnabled) } /** diff --git a/packages/uniswap/src/features/settings/saga.ts b/packages/uniswap/src/features/settings/saga.ts index 687900ee572..65600ec4577 100644 --- a/packages/uniswap/src/features/settings/saga.ts +++ b/packages/uniswap/src/features/settings/saga.ts @@ -1,22 +1,14 @@ import { call, select } from 'typed-redux-saga' import { filterChainIdsByFeatureFlag, getEnabledChains } from 'uniswap/src/features/chains/utils' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { getFeatureFlag } from 'uniswap/src/features/gating/hooks' import { selectIsTestnetModeEnabled } from 'uniswap/src/features/settings/selectors' -import { UniverseChainId } from 'uniswap/src/types/chains' export function* getEnabledChainIdsSaga() { - const testnetModeFeatureFlag = getFeatureFlag(FeatureFlags.Datadog) - const testnetModeEnabled = yield* select(selectIsTestnetModeEnabled) + const isTestnetModeEnabled = yield* select(selectIsTestnetModeEnabled) - const worldChainEnabled = getFeatureFlag(FeatureFlags.WorldChain) - - const featureFlaggedChainIds = filterChainIdsByFeatureFlag({ - [UniverseChainId.WorldChain]: worldChainEnabled, - }) + const featureFlaggedChainIds = filterChainIdsByFeatureFlag({}) return yield* call(getEnabledChains, { - isTestnetModeEnabled: testnetModeEnabled && testnetModeFeatureFlag, + isTestnetModeEnabled, featureFlaggedChainIds, }) } diff --git a/packages/uniswap/src/features/telemetry/constants/trace.ts b/packages/uniswap/src/features/telemetry/constants/trace.ts index 2b7f263636b..78c61a8ee26 100644 --- a/packages/uniswap/src/features/telemetry/constants/trace.ts +++ b/packages/uniswap/src/features/telemetry/constants/trace.ts @@ -19,6 +19,7 @@ export const ModalName = { BridgingWarning: 'bridging-warning-modal', BuyNativeToken: 'buy-native-token-modal', ChooseProfilePhoto: 'choose-profile-photo-modal', + ClaimFee: 'claim-fee-modal', CloudBackupInfo: 'cloud-backup-info-modal', CreatePosition: 'create-position-modal', DappRequest: 'dapp-request', @@ -42,6 +43,7 @@ export const ModalName = { KoreaCexTransferInfoModal: 'korea-cex-transfer-info-modal', HiddenTokenInfoModal: 'hidden-token-info-modal', HiddenNFTInfoModal: 'hidden-nft-info-modal', + Hook: 'hook', Legal: 'legal', LanguageSelector: 'language-selector-modal', MigrateLiquidity: 'migrate-liquidity', diff --git a/packages/uniswap/src/features/telemetry/types.ts b/packages/uniswap/src/features/telemetry/types.ts index 622ec40ad6f..645289f2391 100644 --- a/packages/uniswap/src/features/telemetry/types.ts +++ b/packages/uniswap/src/features/telemetry/types.ts @@ -23,7 +23,9 @@ import { WalletConnectionResult, } from '@uniswap/analytics-events' import { Protocol } from '@uniswap/router-sdk' +import { TokenOptionSection } from 'uniswap/src/components/TokenSelector/types' import { NftStandard } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { FiatCurrency } from 'uniswap/src/features/fiatCurrency/constants' import { ExtensionEventName, @@ -40,7 +42,6 @@ import { import { WrapType } from 'uniswap/src/features/transactions/types/wrap' import { UnitagClaimContext } from 'uniswap/src/features/unitags/types' import { RenderPassReport } from 'uniswap/src/types/RenderPassReport' -import { UniverseChainId } from 'uniswap/src/types/chains' import { CurrencyField } from 'uniswap/src/types/currency' import { LimitsExpiry } from 'uniswap/src/types/limits' import { ImportType } from 'uniswap/src/types/onboarding' @@ -732,6 +733,7 @@ export type UniverseEventProperties = { AssetDetailsBaseProperties & SearchResultContextProperties & { field: CurrencyField + tokenSection: TokenOptionSection }) | InterfaceTokenSelectedProperties [UnitagEventName.UnitagBannerActionTaken]: { diff --git a/packages/uniswap/src/features/tokens/NativeCurrency.ts b/packages/uniswap/src/features/tokens/NativeCurrency.ts index 151cbc4b497..795d336fe96 100644 --- a/packages/uniswap/src/features/tokens/NativeCurrency.ts +++ b/packages/uniswap/src/features/tokens/NativeCurrency.ts @@ -1,9 +1,9 @@ // adapted from https://github.com/Uniswap/interface/src/constants/tokens.ts import { Currency, NativeCurrency as NativeCurrencyClass, Token } from '@uniswap/sdk-core' import { getNativeAddress } from 'uniswap/src/constants/addresses' -import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { toSupportedChainId } from 'uniswap/src/features/chains/utils' -import { UniverseChainId } from 'uniswap/src/types/chains' import { wrappedNativeCurrency } from 'uniswap/src/utils/currency' export class NativeCurrency implements NativeCurrencyClass { @@ -13,15 +13,12 @@ export class NativeCurrency implements NativeCurrencyClass { throw new Error(`Unsupported chain ID: ${chainId}`) } - const chainInfo = UNIVERSE_CHAIN_INFO[supportedChainId] - if (!chainInfo) { - throw new Error('Native currrency info not found') - } + const { nativeCurrency } = getChainInfo(supportedChainId) this.chainId = supportedChainId - this.decimals = chainInfo.nativeCurrency.decimals - this.name = chainInfo.nativeCurrency.name - this.symbol = chainInfo.nativeCurrency.symbol + this.decimals = nativeCurrency.decimals + this.name = nativeCurrency.name + this.symbol = nativeCurrency.symbol this.isNative = true this.isToken = false this.address = getNativeAddress(this.chainId) diff --git a/packages/uniswap/src/features/tokens/TokenWarningCard.tsx b/packages/uniswap/src/features/tokens/TokenWarningCard.tsx index 40b10af224f..2c11ef85554 100644 --- a/packages/uniswap/src/features/tokens/TokenWarningCard.tsx +++ b/packages/uniswap/src/features/tokens/TokenWarningCard.tsx @@ -1,26 +1,99 @@ +import { TouchableArea } from 'ui/src' import { InlineWarningCard } from 'uniswap/src/components/InlineWarningCard/InlineWarningCard' +import { WarningSeverity } from 'uniswap/src/components/modals/WarningModal/types' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' -import { getTokenWarningSeverity, useTokenWarningCardText } from 'uniswap/src/features/tokens/safetyUtils' +import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext' +import { + TokenProtectionWarning, + getCardHeaderText, + getCardSubtitleText, + getFeeOnTransfer, + getSeverityFromTokenProtectionWarning, + getTokenWarningSeverity, + useTokenWarningCardText, +} from 'uniswap/src/features/tokens/safetyUtils' +import { useTranslation } from 'uniswap/src/i18n' type TokenWarningCardProps = { currencyInfo: Maybe - onPressCtaButton?: () => void + tokenProtectionWarningOverride?: TokenProtectionWarning + feePercentOverride?: number + onPress?: () => void + headingTestId?: string + descriptionTestId?: string + hideCtaIcon?: boolean + checked?: boolean + setChecked?: (checked: boolean) => void } -export function TokenWarningCard({ currencyInfo, onPressCtaButton }: TokenWarningCardProps): JSX.Element | null { - const severity = getTokenWarningSeverity(currencyInfo) - const { heading, description } = useTokenWarningCardText(currencyInfo) +function useTokenWarningOverrides( + currencyInfo: Maybe, + tokenProtectionWarningOverride?: TokenProtectionWarning, + feePercentOverride?: number, +): { severity: WarningSeverity; heading: string | null; description: string | null } { + const { t } = useTranslation() + const { formatPercent } = useLocalizationContext() + const { heading: headingDefault, description: descriptionDefault } = useTokenWarningCardText(currencyInfo) + + const severity = tokenProtectionWarningOverride + ? getSeverityFromTokenProtectionWarning(tokenProtectionWarningOverride) + : getTokenWarningSeverity(currencyInfo) + + const headingOverride = getCardHeaderText({ + t, + tokenProtectionWarning: tokenProtectionWarningOverride ?? TokenProtectionWarning.None, + }) + + const descriptionOverride = getCardSubtitleText({ + t, + tokenProtectionWarning: tokenProtectionWarningOverride ?? TokenProtectionWarning.None, + tokenSymbol: currencyInfo?.currency.symbol, + feePercent: feePercentOverride ?? getFeeOnTransfer(currencyInfo?.currency), + formatPercent, + }) + + const heading = tokenProtectionWarningOverride ? headingOverride : headingDefault + const description = tokenProtectionWarningOverride ? descriptionOverride : descriptionDefault + + return { severity, heading, description } +} + +export function TokenWarningCard({ + currencyInfo, + tokenProtectionWarningOverride, + feePercentOverride, + headingTestId, + descriptionTestId, + hideCtaIcon, + checked, + setChecked, + onPress, +}: TokenWarningCardProps): JSX.Element | null { + const { t } = useTranslation() + const { severity, heading, description } = useTokenWarningOverrides( + currencyInfo, + tokenProtectionWarningOverride, + feePercentOverride, + ) if (!currencyInfo || !severity || !description) { return null } return ( - + + + ) } diff --git a/packages/uniswap/src/features/tokens/TokenWarningModal.tsx b/packages/uniswap/src/features/tokens/TokenWarningModal.tsx index e24936aff2e..537cf9b0513 100644 --- a/packages/uniswap/src/features/tokens/TokenWarningModal.tsx +++ b/packages/uniswap/src/features/tokens/TokenWarningModal.tsx @@ -1,8 +1,10 @@ import { BigNumber } from '@ethersproject/bignumber' +import { Percent } from '@uniswap/sdk-core' +import { TFunction } from 'i18next' import { useState } from 'react' import { Trans } from 'react-i18next' import { capitalize } from 'tsafe' -import { AnimateTransition, Flex, LabeledCheckbox, Text, TouchableArea, styled, useSporeColors } from 'ui/src' +import { AnimateTransition, Flex, LabeledCheckbox, Text, TouchableArea, useSporeColors } from 'ui/src' import { X } from 'ui/src/components/icons/X' import { BlockaidLogo } from 'ui/src/components/logos/BlockaidLogo' import { Modal } from 'uniswap/src/components/modals/Modal' @@ -12,24 +14,28 @@ import { WarningSeverity } from 'uniswap/src/components/modals/WarningModal/type import { LearnMoreLink } from 'uniswap/src/components/text/LearnMoreLink' import WarningIcon from 'uniswap/src/components/warnings/WarningIcon' import { uniswapUrls } from 'uniswap/src/constants/urls' -import { ExplorerView } from 'uniswap/src/features/address/ExplorerView' +import { TokenAddressView } from 'uniswap/src/features/address/TokenAddressView' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext' import { ModalName } from 'uniswap/src/features/telemetry/constants' import DeprecatedTokenWarningModal from 'uniswap/src/features/tokens/DeprecatedTokenWarningModal' +import { WarningModalInfoContainer } from 'uniswap/src/features/tokens/WarningInfoModalContainer' import { + getFeeOnTransfer, + getFeeWarning, getIsFeeRelatedWarning, + getModalHeaderText, + getModalSubtitleText, + getSeverityFromTokenProtectionWarning, getShouldHaveCombinedPluralTreatment, + getTokenProtectionWarning, getTokenWarningSeverity, - useModalHeaderText, - useModalSubtitleText, } from 'uniswap/src/features/tokens/safetyUtils' +import { useDismissedTokenWarnings } from 'uniswap/src/features/tokens/slice/hooks' import { useTranslation } from 'uniswap/src/i18n' -import { currencyId } from 'uniswap/src/utils/currencyId' -import { NumberType } from 'utilities/src/format/types' -import { isMobileApp } from 'utilities/src/platform' +import { currencyId, currencyIdToAddress } from 'uniswap/src/utils/currencyId' interface TokenWarningProps { currencyInfo0: CurrencyInfo // required, primary currency @@ -37,11 +43,14 @@ interface TokenWarningProps { isInfoOnlyWarning?: boolean // if this is an informational-only warning. Hides the Reject button shouldBeCombinedPlural?: boolean // some 2-token warnings will be combined into one plural modal (see `getShouldHaveCombinedPluralTreatment`) hasSecondWarning?: boolean // true if this is a 2-token warning with two separate warning screens + feeOnTransferOverride?: { fee: Percent; feeType: 'buy' | 'sell' } // used on SwapReviewScreen to force TokenWarningModal to display FOT content and overrides fee with TradingApi's input/output tax } interface TokenWarningModalContentProps extends TokenWarningProps { onRejectButton: () => void onAcknowledgeButton: () => void + onDismissTokenWarning0: () => void + onDismissTokenWarning1?: () => void } interface TokenWarningModalProps extends TokenWarningProps { isVisible: boolean @@ -60,21 +69,65 @@ function TokenWarningModalContent({ onAcknowledgeButton, shouldBeCombinedPlural, hasSecondWarning, + feeOnTransferOverride, + onDismissTokenWarning0, + onDismissTokenWarning1, }: TokenWarningModalContentProps): JSX.Element | null { const { t } = useTranslation() - const [dontShowAgain, setDontShowAgain] = useState(false) // TODO(WALL-4596): implement dismissedTokenWarnings redux + const { formatPercent } = useLocalizationContext() - const severity = getTokenWarningSeverity(currencyInfo0) - const isFeeRelatedWarning = getIsFeeRelatedWarning(currencyInfo0) + const tokenProtectionWarning = feeOnTransferOverride + ? getFeeWarning(feeOnTransferOverride.fee) + : getTokenProtectionWarning(currencyInfo0) + const severity = getSeverityFromTokenProtectionWarning(tokenProtectionWarning) + const feePercent = feeOnTransferOverride + ? parseFloat(feeOnTransferOverride.fee.toFixed()) + : getFeeOnTransfer(currencyInfo0.currency) + const isFeeRelatedWarning = getIsFeeRelatedWarning(tokenProtectionWarning) + const tokenSymbol = currencyInfo0.currency.symbol + const titleText = getModalHeaderText({ + t, + tokenSymbol0: tokenSymbol, + tokenSymbol1: currencyInfo1?.currency.symbol, + tokenProtectionWarning, + shouldHavePluralTreatment: shouldBeCombinedPlural, + }) + const subtitleText = getModalSubtitleText({ + t, + tokenProtectionWarning, + tokenSymbol, + tokenList: currencyInfo0.safetyInfo?.tokenList, + feePercent, + shouldHavePluralTreatment: shouldBeCombinedPlural, + formatPercent, + }) + const { text: titleTextColor } = getAlertColor(severity) + + // Logic for "don't show again" dismissal of warnings + const [dontShowAgain, setDontShowAgain] = useState(false) + const showCheckbox = !isInfoOnlyWarning && severity === WarningSeverity.Low + const showBlockaidLogo = !isFeeRelatedWarning && severity !== WarningSeverity.Low - const titleText = useModalHeaderText(currencyInfo0, shouldBeCombinedPlural ? currencyInfo1 : undefined) - const subtitleText = useModalSubtitleText(currencyInfo0, shouldBeCombinedPlural ? currencyInfo1 : undefined) + const onAcknowledge = (): void => { + if (showCheckbox) { + if (dontShowAgain) { + onDismissTokenWarning0() + onDismissTokenWarning1?.() + } + } + onAcknowledgeButton() + } if (severity === WarningSeverity.None) { return null } - const { text: titleTextColor } = getAlertColor(severity) + const { rejectText, acknowledgeText } = getWarningModalButtonTexts( + t, + !!isInfoOnlyWarning, + severity, + !!hasSecondWarning, + ) return ( @@ -89,23 +142,8 @@ function TokenWarningModalContent({ } - rejectText={ - // if this is an informational-only warning or a 2-token warning, we should always show the Reject / back button - // or, if a token is blocked, it should not have a Reject button, only an Acknowledge button - isInfoOnlyWarning || hasSecondWarning || severity !== WarningSeverity.Blocked - ? t('common.button.back') - : undefined - } - acknowledgeText={ - // if this is an informational-only warning, we don't show the Acknowledge button at all - isInfoOnlyWarning - ? undefined - : // if a token is blocked & is not part of a 2-token warning, the Acknowledge button should say "Close" - severity === WarningSeverity.Blocked && !hasSecondWarning - ? t('common.button.close') - : // otherwise, Acknowledge button should say "Continue" - t('common.button.continue') - } + rejectText={rejectText} + acknowledgeText={acknowledgeText} icon={} backgroundIconColor={false} severity={severity} @@ -116,7 +154,7 @@ function TokenWarningModalContent({ } onReject={onRejectButton} onClose={onRejectButton} - onAcknowledge={onAcknowledgeButton} + onAcknowledge={onAcknowledge} > {isFeeRelatedWarning && currencyInfo0.currency.isToken ? ( ) : ( <> - + {shouldBeCombinedPlural && currencyInfo1 && ( - + )} )} - {!isFeeRelatedWarning && ( + {showBlockaidLogo && ( )} - {!isInfoOnlyWarning && severity === WarningSeverity.Low && ( + {showCheckbox && ( // only show "Don't show this warning again" checkbox if this is an actionable modal & the token is low-severity void + currencyInfo1: CurrencyInfo | undefined + onDismissTokenWarning1: () => void | undefined +} | null { + const address0 = currencyIdToAddress(t0.currencyId) + const address1 = t1 && currencyIdToAddress(t1.currencyId) + const { tokenWarningDismissed: tokenWarningDismissed0, onDismissTokenWarning: onDismissTokenWarning0 } = + useDismissedTokenWarnings(t0?.currency.isNative ? undefined : { chainId: t0.currency.chainId, address: address0 }) + const { tokenWarningDismissed: tokenWarningDismissed1, onDismissTokenWarning: onDismissTokenWarning1 } = + useDismissedTokenWarnings( + !t1 || !address1 || t1?.currency.isNative ? undefined : { chainId: t1.currency.chainId, address: address1 }, + ) + let currencyInfo0: CurrencyInfo | undefined = t0 + let currencyInfo1: CurrencyInfo | undefined = t1 + if (!isInfoOnlyWarning) { + if (tokenWarningDismissed0 && tokenWarningDismissed1) { + // If both tokens are dismissed + return null + } else if (tokenWarningDismissed0) { + // If only the first token is dismissed, we use currencyInfo1 as primary token to show warning + if (!t1) { + return null + } + currencyInfo0 = t1 ?? undefined + } else if (tokenWarningDismissed1) { + // If only the second token is dismissed, we use currencyInfo0 as primary token to show warning + currencyInfo0 = t0 + currencyInfo1 = undefined + } + } + return { currencyInfo0, onDismissTokenWarning0, currencyInfo1, onDismissTokenWarning1 } +} + /** * Warning speedbump for selecting certain tokens. */ export default function TokenWarningModal({ isVisible, - currencyInfo0, - currencyInfo1, + currencyInfo0: t0, + currencyInfo1: t1, isInfoOnlyWarning, + feeOnTransferOverride, onReject, onToken0BlockAcknowledged, onToken1BlockAcknowledged, @@ -179,14 +258,21 @@ export default function TokenWarningModal({ }: TokenWarningModalProps): JSX.Element | null { const tokenProtectionEnabled = useFeatureFlag(FeatureFlags.TokenProtection) const colors = useSporeColors() + const [warningIndex, setWarningIndex] = useState<0 | 1>(0) + + // Check for dismissed warnings + const warningModalCurrencies = useWarningModalCurrenciesDismissed(t0, t1, isInfoOnlyWarning) + if (!warningModalCurrencies) { + return null + } + const { currencyInfo0, currencyInfo1, onDismissTokenWarning0, onDismissTokenWarning1 } = warningModalCurrencies // If BOTH tokens are blocked or BOTH are low-severity, they'll be combined into one plural modal const combinedPlural = getShouldHaveCombinedPluralTreatment(currencyInfo0, currencyInfo1) const isBlocked0 = getTokenWarningSeverity(currencyInfo0) === WarningSeverity.Blocked const isBlocked1 = getTokenWarningSeverity(currencyInfo1) === WarningSeverity.Blocked - const [warningIndex, setWarningIndex] = useState<0 | 1>(0) - const hasSecondWarning = Boolean(!combinedPlural && currencyInfo1) + const hasSecondWarning = Boolean(!combinedPlural && getTokenWarningSeverity(currencyInfo1) !== WarningSeverity.None) return tokenProtectionEnabled ? ( { if (hasSecondWarning) { @@ -225,15 +312,22 @@ export default function TokenWarningModal({ } else if (isBlocked0) { // If both tokens are blocked, they'll be combined into one plural modal. See `getShouldHaveCombinedPluralTreatment`. combinedPlural && isBlocked1 && onToken1BlockAcknowledged?.() + onToken0BlockAcknowledged?.() + closeModalOnly() + } else if (isInfoOnlyWarning) { + closeModalOnly() } else { onAcknowledge() } }} + onDismissTokenWarning0={onDismissTokenWarning0} + onDismissTokenWarning1={onDismissTokenWarning1} /> {hasSecondWarning && currencyInfo1 && ( { setWarningIndex(0) }} @@ -263,31 +357,23 @@ export default function TokenWarningModal({ ) } -export const WarningModalInfoContainer = styled(Flex, { - width: '100%', - backgroundColor: '$surface2', - borderRadius: '$rounded12', - borderWidth: 1, - borderColor: '$surface3', - px: '$spacing16', - py: isMobileApp ? '$spacing8' : '$spacing12', - alignItems: 'center', - flexWrap: 'nowrap', -}) - -function FeeRow({ feeType, feeBps }: { feeType: 'buy' | 'sell'; feeBps?: BigNumber }): JSX.Element { +// feePercent is the percentage as an integer. I.e. feePercent = 5 means 5% +export function FeeRow({ feeType, feePercent = 0 }: { feeType: 'buy' | 'sell'; feePercent?: number }): JSX.Element { const { t } = useTranslation() - const textColor = getAlertColor(WarningSeverity.Medium) - const { formatNumberOrString } = useLocalizationContext() - const fee: string = feeBps - ? formatNumberOrString({ value: feeBps.toNumber() / 10_000, type: NumberType.Percentage }) - : '0%' + // Convert percentage to basis points (multiply by 100) to get integer values + const basisPoints = Math.round(feePercent * 100) + const tokenProtectionWarning = getFeeWarning(new Percent(basisPoints, 10000)) + const severity = getSeverityFromTokenProtectionWarning(tokenProtectionWarning) + const { headerText: textColor } = getAlertColor(severity) + const { formatPercent } = useLocalizationContext() return ( - + {feeType === 'buy' ? capitalize(t('token.fee.buy.label')) : capitalize(t('token.fee.sell.label'))} - {fee} + + {formatPercent(feePercent)} + ) } @@ -299,10 +385,53 @@ export function FeeDisplayTable({ buyFeeBps?: BigNumber sellFeeBps?: BigNumber }): JSX.Element { + const buyFeePercent = buyFeeBps ? buyFeeBps.toNumber() / 100 : undefined + const sellFeePercent = sellFeeBps ? sellFeeBps.toNumber() / 100 : undefined return ( - - - + + + ) } + +/* +Logic explanation + +Reject button text +- if this is an informational-only warning or a 2-token warning, we should always show the Reject / back button +- or, if a token is blocked, it should not have a Reject button, only an Acknowledge button + +Acknowledge button text +- if this is an informational-only warning, we don't show the Acknowledge button at all +- if a token is blocked & is not part of a 2-token warning, the Acknowledge button should say "Close" +- otherwise, Acknowledge button should say "Continue" +*/ +export function getWarningModalButtonTexts( + t: TFunction, + isInfoOnlyWarning: boolean, + severity: WarningSeverity, + hasSecondWarning: boolean, +): { + rejectText: string | undefined + acknowledgeText: string | undefined +} { + if (isInfoOnlyWarning) { + return { + rejectText: t('common.button.close'), + acknowledgeText: undefined, + } + } + + if (severity === WarningSeverity.Blocked && !hasSecondWarning) { + return { + rejectText: undefined, + acknowledgeText: t('common.button.close'), + } + } + + return { + rejectText: t('common.button.back'), + acknowledgeText: t('common.button.continue'), + } +} diff --git a/packages/uniswap/src/features/tokens/WarningInfoModalContainer.tsx b/packages/uniswap/src/features/tokens/WarningInfoModalContainer.tsx new file mode 100644 index 00000000000..18324a63a0b --- /dev/null +++ b/packages/uniswap/src/features/tokens/WarningInfoModalContainer.tsx @@ -0,0 +1,14 @@ +import { Flex, styled } from 'ui/src' +import { isMobileApp } from 'utilities/src/platform' + +export const WarningModalInfoContainer = styled(Flex, { + width: '100%', + backgroundColor: '$surface2', + borderRadius: '$rounded12', + borderWidth: 1, + borderColor: '$surface3', + px: '$spacing16', + py: isMobileApp ? '$spacing8' : '$spacing12', + alignItems: 'center', + flexWrap: 'nowrap', +}) diff --git a/packages/uniswap/src/features/tokens/getCurrencyAmount.test.ts b/packages/uniswap/src/features/tokens/getCurrencyAmount.test.ts index 80daf5e6cef..748a2446059 100644 --- a/packages/uniswap/src/features/tokens/getCurrencyAmount.test.ts +++ b/packages/uniswap/src/features/tokens/getCurrencyAmount.test.ts @@ -3,6 +3,7 @@ import { DAI } from 'uniswap/src/constants/tokens' import { getCurrencyAmount, ValueType } from 'uniswap/src/features/tokens/getCurrencyAmount' import { noOpFunction } from 'utilities/src/test/utils' +const ZERO_DAI = CurrencyAmount.fromRawAmount(DAI, '0') const ONE_DAI = CurrencyAmount.fromRawAmount(DAI, '1000000000000000000') const HALF_DAI = CurrencyAmount.fromRawAmount(DAI, '500000000000000000') const FRACTION_OF_DAI = CurrencyAmount.fromRawAmount(DAI, '1000000000000000') @@ -28,8 +29,12 @@ describe(getCurrencyAmount, () => { expect(getCurrencyAmount({ value: '1000000000', valueType: ValueType.Exact, currency: undefined })).toBeUndefined() }) - it('return null when float value is 0', () => { - expect(getCurrencyAmount({ value: '0', valueType: ValueType.Exact, currency: DAI })).toBeNull() + it('return 0 when float value is 0', () => { + expect(getCurrencyAmount({ value: '0', valueType: ValueType.Exact, currency: DAI })).toEqual(ZERO_DAI) + }) + + it('return undefined when float value is undefined', () => { + expect(getCurrencyAmount({ value: undefined, valueType: ValueType.Exact, currency: DAI })).toBeUndefined() }) it('parse standard float amount', () => { @@ -89,7 +94,7 @@ describe(getCurrencyAmount, () => { valueType: ValueType.Exact, currency: DAI, }), - ).toBeNull() + ).toEqual(ZERO_DAI) }) it('handle invalid values', () => { diff --git a/packages/uniswap/src/features/tokens/getCurrencyAmount.ts b/packages/uniswap/src/features/tokens/getCurrencyAmount.ts index f99e821ed98..6749243df30 100644 --- a/packages/uniswap/src/features/tokens/getCurrencyAmount.ts +++ b/packages/uniswap/src/features/tokens/getCurrencyAmount.ts @@ -40,9 +40,6 @@ export function getCurrencyAmount({ if (valueType === ValueType.Exact) { parsedValue = parseUnits(parsedValue, currency.decimals).toString() - if (parsedValue === '0') { - return null - } } return CurrencyAmount.fromRawAmount(currency, parsedValue) diff --git a/packages/uniswap/src/features/tokens/hooks.ts b/packages/uniswap/src/features/tokens/hooks.ts index 6c027843411..762a6f96e67 100644 --- a/packages/uniswap/src/features/tokens/hooks.ts +++ b/packages/uniswap/src/features/tokens/hooks.ts @@ -5,7 +5,7 @@ import { SearchPopularTokensQuery, useSearchPopularTokensQuery, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { useEnabledChains } from 'uniswap/src/features/settings/hooks' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks' import { areAddressesEqual } from 'uniswap/src/utils/addresses' export type TopToken = NonNullable[0]> diff --git a/packages/uniswap/src/features/tokens/safetyUtils.test.ts b/packages/uniswap/src/features/tokens/safetyUtils.test.ts index 6ac8a5ea0f3..d5979161f99 100644 --- a/packages/uniswap/src/features/tokens/safetyUtils.test.ts +++ b/packages/uniswap/src/features/tokens/safetyUtils.test.ts @@ -40,9 +40,12 @@ describe('safetyUtils', () => { } as CurrencyInfo describe('getTokenWarningSeverity', () => { - it('should return undefined when currencyInfo is not provided', () => { - expect(getTokenWarningSeverity(undefined)).toBeUndefined() - expect(getTokenWarningSeverity({ ...mockCurrencyInfo, safetyInfo: undefined })).toBeUndefined() + it('should return None when currencyInfo is fully undefined', () => { + expect(getTokenWarningSeverity(undefined)).toBe(WarningSeverity.None) + }) + + it('should return Low when currencyInfo is defined but safetyInfo is undefined', () => { + expect(getTokenWarningSeverity({ ...mockCurrencyInfo, safetyInfo: undefined })).toBe(WarningSeverity.Low) }) it('should return Low for non-default token', () => { diff --git a/packages/uniswap/src/features/tokens/safetyUtils.ts b/packages/uniswap/src/features/tokens/safetyUtils.ts index 016e4d2a598..8118304295e 100644 --- a/packages/uniswap/src/features/tokens/safetyUtils.ts +++ b/packages/uniswap/src/features/tokens/safetyUtils.ts @@ -1,33 +1,32 @@ /* eslint-disable consistent-return */ -import { Currency, NativeCurrency } from '@uniswap/sdk-core' +import { Currency, NativeCurrency, Percent } from '@uniswap/sdk-core' import { TFunction } from 'i18next' import { useTranslation } from 'react-i18next' import { WarningSeverity } from 'uniswap/src/components/modals/WarningModal/types' import { ProtectionResult } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { AttackType, CurrencyInfo, TokenList } from 'uniswap/src/features/dataApi/types' import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext' -import { FormatNumberOrStringInput } from 'uniswap/src/features/language/formatter' -import { NumberType } from 'utilities/src/format/types' import { isInterface } from 'utilities/src/platform' export enum TokenProtectionWarning { - MaliciousHoneypot = 'malicious-honeypot', // 100% fot - MaliciousImpersonator = 'malicious-impersonator', - SpamAirdrop = 'spam-airdrop', - MaliciousGeneral = 'malicious-general', - FotVeryHigh = 'fot-very-high', // [80, 100)% fot - FotHigh = 'fot-high', // [5, 80)% fot - FotLow = 'fot-low', // (0, 5)% fot - Blocked = 'blocked', - NonDefault = 'non-default', - None = 'none', + // THESE NUMERIC VALUES MATTER -- they are used for severity comparison + Blocked = 10, + MaliciousHoneypot = 9, // 100% fot + FotVeryHigh = 8, // [80, 100)% fot + MaliciousImpersonator = 7, + FotHigh = 6, // [5, 80)% fot + MaliciousGeneral = 5, + SpamAirdrop = 4, + FotLow = 3, // (0, 5)% fot + NonDefault = 2, + None = 1, } export const TOKEN_PROTECTION_FOT_HONEYPOT_BREAKPOINT = 100 export const TOKEN_PROTECTION_FOT_HIGH_FEE_BREAKPOINT = 80 export const TOKEN_PROTECTION_FOT_FEE_BREAKPOINT = 5 -function getFeeOnTransfer(currency?: Currency): number { +export function getFeeOnTransfer(currency?: Currency): number { if (!currency || currency.isNative) { return 0 } @@ -37,9 +36,9 @@ function getFeeOnTransfer(currency?: Currency): number { } // eslint-disable-next-line complexity -function getTokenProtectionWarning(currencyInfo?: Maybe): TokenProtectionWarning | undefined { +export function getTokenProtectionWarning(currencyInfo?: Maybe): TokenProtectionWarning { if (!currencyInfo?.currency || !currencyInfo?.safetyInfo) { - return undefined + return TokenProtectionWarning.NonDefault } const { currency, safetyInfo } = currencyInfo @@ -61,13 +60,13 @@ function getTokenProtectionWarning(currencyInfo?: Maybe): TokenPro attackType === AttackType.HighFees) ) { return TokenProtectionWarning.FotVeryHigh - } else if (feeOnTransfer >= TOKEN_PROTECTION_FOT_FEE_BREAKPOINT) { - return TokenProtectionWarning.FotHigh } else if ( (protectionResult === ProtectionResult.Malicious || protectionResult === ProtectionResult.Spam) && attackType === AttackType.Impersonator ) { return TokenProtectionWarning.MaliciousImpersonator + } else if (feeOnTransfer >= TOKEN_PROTECTION_FOT_FEE_BREAKPOINT) { + return TokenProtectionWarning.FotHigh } else if ( (protectionResult === ProtectionResult.Malicious || protectionResult === ProtectionResult.Spam) && attackType === AttackType.Other @@ -84,22 +83,41 @@ function getTokenProtectionWarning(currencyInfo?: Maybe): TokenPro return TokenProtectionWarning.None } -export function getIsFeeRelatedWarning(currencyInfo?: CurrencyInfo): boolean { - const warning = getTokenProtectionWarning(currencyInfo) +export function getIsFeeRelatedWarning(tokenProtectionWarning?: TokenProtectionWarning): boolean { return ( - warning === TokenProtectionWarning.MaliciousHoneypot || - warning === TokenProtectionWarning.FotVeryHigh || - warning === TokenProtectionWarning.FotHigh || - warning === TokenProtectionWarning.FotLow + tokenProtectionWarning === TokenProtectionWarning.MaliciousHoneypot || + tokenProtectionWarning === TokenProtectionWarning.FotVeryHigh || + tokenProtectionWarning === TokenProtectionWarning.FotHigh || + tokenProtectionWarning === TokenProtectionWarning.FotLow ) } -export function getTokenWarningSeverity(currencyInfo: Maybe): WarningSeverity | undefined { - const tokenProtectionWarning = getTokenProtectionWarning(currencyInfo) - if (!currencyInfo || tokenProtectionWarning === undefined) { - return undefined +export function getFeeWarning(fee: Percent): TokenProtectionWarning { + // WarningSeverity for styling. Same logic as getTokenWarningSeverity but without non-fee-related cases. + // If fee >= 5% then HIGH, else 0% < fee < 5% then MEDIUM, else NONE + const feeInt = parseFloat(fee.toFixed()) + let tokenProtectionWarning = TokenProtectionWarning.None + if (feeInt >= TOKEN_PROTECTION_FOT_HONEYPOT_BREAKPOINT) { + tokenProtectionWarning = TokenProtectionWarning.MaliciousHoneypot + } else if (feeInt >= TOKEN_PROTECTION_FOT_HIGH_FEE_BREAKPOINT) { + tokenProtectionWarning = TokenProtectionWarning.FotVeryHigh + } else if (feeInt >= TOKEN_PROTECTION_FOT_FEE_BREAKPOINT) { + tokenProtectionWarning = TokenProtectionWarning.FotHigh + } else if (feeInt > 0) { + tokenProtectionWarning = TokenProtectionWarning.FotLow } + return tokenProtectionWarning +} + +export function getTokenWarningSeverity(currencyInfo: Maybe): WarningSeverity { + if (!currencyInfo) { + return WarningSeverity.None + } + const tokenProtectionWarning = getTokenProtectionWarning(currencyInfo) + return getSeverityFromTokenProtectionWarning(tokenProtectionWarning) +} +export function getSeverityFromTokenProtectionWarning(tokenProtectionWarning: TokenProtectionWarning): WarningSeverity { switch (tokenProtectionWarning) { case TokenProtectionWarning.Blocked: return WarningSeverity.Blocked @@ -160,11 +178,14 @@ export function getModalHeaderText({ shouldHavePluralTreatment, }: { t: TFunction - tokenProtectionWarning: TokenProtectionWarning + tokenProtectionWarning?: TokenProtectionWarning tokenSymbol0?: string tokenSymbol1?: string shouldHavePluralTreatment?: boolean }): string | null { + if (!tokenProtectionWarning) { + return null + } switch (tokenProtectionWarning) { case TokenProtectionWarning.Blocked: return shouldHavePluralTreatment @@ -198,21 +219,41 @@ export function useModalSubtitleText(currencyInfo0: CurrencyInfo, currencyInfo1? throw new Error('Should only combine into one plural-languaged modal if BOTH are low or BOTH are blocked') } const { t } = useTranslation() - const { formatNumberOrString } = useLocalizationContext() - + const { formatPercent } = useLocalizationContext() const tokenProtectionWarning = getTokenProtectionWarning(currencyInfo0) - const tokenList = currencyInfo0.safetyInfo?.tokenList + return getModalSubtitleText({ + t, + tokenProtectionWarning, + tokenSymbol: currencyInfo0.currency.symbol, + tokenList: currencyInfo0.safetyInfo?.tokenList, + feePercent: getFeeOnTransfer(currencyInfo0.currency), + shouldHavePluralTreatment, + formatPercent, + }) +} +export function getModalSubtitleText({ + t, + tokenProtectionWarning, + tokenSymbol, + tokenList, + feePercent, + shouldHavePluralTreatment, + formatPercent, +}: { + t: TFunction + tokenProtectionWarning: TokenProtectionWarning | undefined + tokenSymbol?: string + tokenList?: TokenList + feePercent: number + shouldHavePluralTreatment?: boolean + formatPercent: (value: Maybe) => string +}): string | null { if (!tokenProtectionWarning) { return null } - const formattedFeePercent = formatNumberOrString({ - value: getFeeOnTransfer(currencyInfo0.currency) / 100, - type: NumberType.Percentage, - }) - - const tokenSymbol = currencyInfo0.currency?.symbol + const formattedFeePercent = formatPercent(feePercent) const warningCopy = getModalSubtitleTokenWarningText({ t, tokenProtectionWarning, @@ -289,25 +330,37 @@ export function getModalSubtitleTokenWarningText({ } export function useTokenWarningCardText(currencyInfo: Maybe): { - heading?: string + heading: string | null description: string | null } { const { t } = useTranslation() - const { formatNumberOrString } = useLocalizationContext() + const { formatPercent } = useLocalizationContext() + if (!currencyInfo) { + return { + heading: null, + description: null, + } + } const tokenProtectionWarning = getTokenProtectionWarning(currencyInfo) return { heading: getCardHeaderText({ t, tokenProtectionWarning }), - description: getCardSubtitleText({ t, currencyInfo, tokenProtectionWarning, formatNumberOrString }), + description: getCardSubtitleText({ + t, + tokenProtectionWarning, + tokenSymbol: currencyInfo.currency.symbol, + feePercent: getFeeOnTransfer(currencyInfo.currency), + formatPercent, + }), } } -function getCardHeaderText({ +export function getCardHeaderText({ t, tokenProtectionWarning, }: { t: TFunction - tokenProtectionWarning?: TokenProtectionWarning -}): string | undefined { + tokenProtectionWarning: TokenProtectionWarning +}): string | null { switch (tokenProtectionWarning) { case TokenProtectionWarning.Blocked: return t('token.safetyLevel.blocked.header') @@ -323,28 +376,26 @@ function getCardHeaderText({ return t('token.safety.warning.highFeeDetected.title') case TokenProtectionWarning.FotLow: return t('token.safety.warning.feeDetected.title') + case TokenProtectionWarning.NonDefault: case TokenProtectionWarning.None: - default: - return undefined + return null } } -function getCardSubtitleText({ - currencyInfo, - tokenProtectionWarning, +export function getCardSubtitleText({ t, - formatNumberOrString, + tokenProtectionWarning, + tokenSymbol, + feePercent, + formatPercent, }: { t: TFunction - currencyInfo: Maybe - tokenProtectionWarning?: TokenProtectionWarning - formatNumberOrString: (input: FormatNumberOrStringInput) => string + tokenProtectionWarning: TokenProtectionWarning + tokenSymbol?: string + feePercent: number + formatPercent: (value: Maybe) => string }): string | null { - const feePercent: string = formatNumberOrString({ - value: getFeeOnTransfer(currencyInfo?.currency) / 100, - type: NumberType.Percentage, - }) - const tokenSymbol = currencyInfo?.currency?.symbol + const formattedFeePercent: string = formatPercent(feePercent) switch (tokenProtectionWarning) { case TokenProtectionWarning.Blocked: return isInterface @@ -359,13 +410,11 @@ function getCardSubtitleText({ return t('token.safety.warning.spam.message', { tokenSymbol }) case TokenProtectionWarning.FotVeryHigh: case TokenProtectionWarning.FotHigh: - return t('token.safety.warning.tokenChargesFee.percent.message', { tokenSymbol, feePercent }) case TokenProtectionWarning.FotLow: - return t('token.safety.warning.tokenChargesFee.message') + return t('token.safety.warning.tokenChargesFee.percent.message', { tokenSymbol, feePercent: formattedFeePercent }) case TokenProtectionWarning.NonDefault: - return t('token.safety.warning.medium.heading.default_one') + return t('token.safety.warning.medium.heading.named', { tokenSymbol }) case TokenProtectionWarning.None: return null } - return null } diff --git a/packages/uniswap/src/features/tokens/slice/types.ts b/packages/uniswap/src/features/tokens/slice/types.ts index 41429596d6e..ae32caba332 100644 --- a/packages/uniswap/src/features/tokens/slice/types.ts +++ b/packages/uniswap/src/features/tokens/slice/types.ts @@ -1,4 +1,4 @@ -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' export type SerializedTokenMap = { [chainId: number]: { diff --git a/packages/uniswap/src/features/tokens/useCurrencyInfo.ts b/packages/uniswap/src/features/tokens/useCurrencyInfo.ts index 5df39b8c4b6..ac62dfa8dde 100644 --- a/packages/uniswap/src/features/tokens/useCurrencyInfo.ts +++ b/packages/uniswap/src/features/tokens/useCurrencyInfo.ts @@ -1,8 +1,8 @@ import { useMemo } from 'react' import { useTokenQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' import { currencyIdToContractInput, gqlTokenToCurrencyInfo } from 'uniswap/src/features/dataApi/utils' -import { UniverseChainId } from 'uniswap/src/types/chains' import { buildNativeCurrencyId, buildWrappedNativeCurrencyId } from 'uniswap/src/utils/currencyId' export function useCurrencyInfo( diff --git a/packages/uniswap/src/features/transactions/DecimalPadInput/DecimalPadInput.tsx b/packages/uniswap/src/features/transactions/DecimalPadInput/DecimalPadInput.tsx index c3283e86eae..3520bb10c5c 100644 --- a/packages/uniswap/src/features/transactions/DecimalPadInput/DecimalPadInput.tsx +++ b/packages/uniswap/src/features/transactions/DecimalPadInput/DecimalPadInput.tsx @@ -9,7 +9,7 @@ import { useRef, useState, } from 'react' -import { Flex } from 'ui/src' +import { Flex, useIsShortMobileDevice } from 'ui/src' import { TextInputProps } from 'uniswap/src/components/input/TextInput' import { DecimalPad } from 'uniswap/src/features/transactions/DecimalPadInput/DecimalPad' // eslint-disable-next-line no-restricted-imports -- type import is safe @@ -39,6 +39,14 @@ export type DecimalPadInputRef = { setMaxHeight(height: number): void } +export enum DecimalPadCalculatedSpaceId { + Swap, + Send, + FiatOnRamp, +} + +const precalculatedSpace: Partial> = {} + /* This component is used to calculate the space that the `DecimalPad` can use. We position the `DecimalPad` with `position: absolute` at the bottom of the screen instead of @@ -46,19 +54,35 @@ putting it inside this container in order to avoid any overflows while the `Deci is automatically resizing to find the right size for the screen. */ export function DecimalPadCalculateSpace({ - isShortMobileDevice, + id, decimalPadRef, }: { - isShortMobileDevice: boolean + id: DecimalPadCalculatedSpaceId decimalPadRef: RefObject }): JSX.Element { + const isShortMobileDevice = useIsShortMobileDevice() + const onBottomScreenLayout = useCallback( (event: LayoutChangeEvent): void => { - decimalPadRef.current?.setMaxHeight(event.nativeEvent.layout.height) + const height = event.nativeEvent.layout.height + decimalPadRef.current?.setMaxHeight(height) + precalculatedSpace[id] = height }, - [decimalPadRef], + [decimalPadRef, id], ) + useEffect(() => { + const precalculatedHeight = precalculatedSpace[id] + + if (precalculatedHeight) { + // If we have already rendered this screen, we already know how much space this phone has, + // so we optimistically set the height instead of waiting for the layout event. + // This improves the perceived loading time of the `DecimalPad`, + // given that it fades in only after the height is known. + decimalPadRef.current?.setMaxHeight(precalculatedHeight) + } + }, [decimalPadRef, id]) + return } diff --git a/packages/uniswap/src/features/transactions/InsufficientNativeTokenWarning/BridgeTokenButton.tsx b/packages/uniswap/src/features/transactions/InsufficientNativeTokenWarning/BridgeTokenButton.tsx index 7e2dfff6c66..6e221bffde6 100644 --- a/packages/uniswap/src/features/transactions/InsufficientNativeTokenWarning/BridgeTokenButton.tsx +++ b/packages/uniswap/src/features/transactions/InsufficientNativeTokenWarning/BridgeTokenButton.tsx @@ -3,12 +3,12 @@ import { useTranslation } from 'react-i18next' import { Button, isWeb } from 'ui/src' import { opacify, validColor } from 'ui/src/theme' import { AssetType } from 'uniswap/src/entities/assets' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' -import { useEnabledChains } from 'uniswap/src/features/settings/hooks' import Trace from 'uniswap/src/features/telemetry/Trace' import { ElementName } from 'uniswap/src/features/telemetry/constants' import { getDefaultState, useSwapFormContext } from 'uniswap/src/features/transactions/swap/contexts/SwapFormContext' -import { UniverseChainId } from 'uniswap/src/types/chains' import { useNetworkColors } from 'uniswap/src/utils/colors' import { currencyIdToAddress } from 'uniswap/src/utils/currencyId' diff --git a/packages/uniswap/src/features/transactions/InsufficientNativeTokenWarning/BuyNativeTokenButton.tsx b/packages/uniswap/src/features/transactions/InsufficientNativeTokenWarning/BuyNativeTokenButton.tsx index ea82d57a0cb..032420fd915 100644 --- a/packages/uniswap/src/features/transactions/InsufficientNativeTokenWarning/BuyNativeTokenButton.tsx +++ b/packages/uniswap/src/features/transactions/InsufficientNativeTokenWarning/BuyNativeTokenButton.tsx @@ -2,9 +2,9 @@ import { useTranslation } from 'react-i18next' import { Button, isWeb } from 'ui/src' import { opacify, validColor } from 'ui/src/theme' import { useUniswapContext } from 'uniswap/src/contexts/UniswapContext' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' import { useIsSupportedFiatOnRampCurrency } from 'uniswap/src/features/fiatOnRamp/hooks' -import { useEnabledChains } from 'uniswap/src/features/settings/hooks' import Trace from 'uniswap/src/features/telemetry/Trace' import { ElementName } from 'uniswap/src/features/telemetry/constants' import { useNetworkColors } from 'uniswap/src/utils/colors' diff --git a/packages/uniswap/src/features/transactions/InsufficientNativeTokenWarning/InsufficientNativeTokenBaseComponent.tsx b/packages/uniswap/src/features/transactions/InsufficientNativeTokenWarning/InsufficientNativeTokenBaseComponent.tsx index 77e68e2c5ea..3767abea9d6 100644 --- a/packages/uniswap/src/features/transactions/InsufficientNativeTokenWarning/InsufficientNativeTokenBaseComponent.tsx +++ b/packages/uniswap/src/features/transactions/InsufficientNativeTokenWarning/InsufficientNativeTokenBaseComponent.tsx @@ -2,9 +2,9 @@ import { Trans } from 'react-i18next' import { Flex, Text, isWeb } from 'ui/src' import { AlertTriangleFilled } from 'ui/src/components/icons/AlertTriangleFilled' import { InfoCircle } from 'ui/src/components/icons/InfoCircle' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { INSUFFICIENT_NATIVE_TOKEN_TEXT_VARIANT } from 'uniswap/src/features/transactions/InsufficientNativeTokenWarning/constants' import { useInsufficientNativeTokenWarning } from 'uniswap/src/features/transactions/InsufficientNativeTokenWarning/useInsufficientNativeTokenWarning' -import { UniverseChainId } from 'uniswap/src/types/chains' export function InsufficientNativeTokenBaseComponent({ parsedInsufficentNativeTokenWarning, diff --git a/packages/uniswap/src/features/transactions/InsufficientNativeTokenWarning/InsufficientNativeTokenWarningContent.native.tsx b/packages/uniswap/src/features/transactions/InsufficientNativeTokenWarning/InsufficientNativeTokenWarningContent.native.tsx index 59707dc4a41..e043fc2f1ce 100644 --- a/packages/uniswap/src/features/transactions/InsufficientNativeTokenWarning/InsufficientNativeTokenWarningContent.native.tsx +++ b/packages/uniswap/src/features/transactions/InsufficientNativeTokenWarning/InsufficientNativeTokenWarningContent.native.tsx @@ -7,13 +7,13 @@ import { WarningModal } from 'uniswap/src/components/modals/WarningModal/Warning import { LearnMoreLink } from 'uniswap/src/components/text/LearnMoreLink' import { uniswapUrls } from 'uniswap/src/constants/urls' import { useBridgingTokenWithHighestBalance } from 'uniswap/src/features/bridging/hooks/tokens' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' import { ModalName } from 'uniswap/src/features/telemetry/constants' import { BridgeTokenButton } from 'uniswap/src/features/transactions/InsufficientNativeTokenWarning/BridgeTokenButton' import { BuyNativeTokenButton } from 'uniswap/src/features/transactions/InsufficientNativeTokenWarning/BuyNativeTokenButton' import { InsufficientNativeTokenBaseComponent } from 'uniswap/src/features/transactions/InsufficientNativeTokenWarning/InsufficientNativeTokenBaseComponent' import { useInsufficientNativeTokenWarning } from 'uniswap/src/features/transactions/InsufficientNativeTokenWarning/useInsufficientNativeTokenWarning' -import { UniverseChainId } from 'uniswap/src/types/chains' import { currencyIdToAddress } from 'uniswap/src/utils/currencyId' export function InsufficientNativeTokenWarningContent({ diff --git a/packages/uniswap/src/features/transactions/InsufficientNativeTokenWarning/useInsufficientNativeTokenWarning.tsx b/packages/uniswap/src/features/transactions/InsufficientNativeTokenWarning/useInsufficientNativeTokenWarning.tsx index 383e048f58b..aa3fc017d6e 100644 --- a/packages/uniswap/src/features/transactions/InsufficientNativeTokenWarning/useInsufficientNativeTokenWarning.tsx +++ b/packages/uniswap/src/features/transactions/InsufficientNativeTokenWarning/useInsufficientNativeTokenWarning.tsx @@ -3,11 +3,10 @@ import { ComponentProps, useMemo } from 'react' import { Trans } from 'react-i18next' import { Text } from 'ui/src' import { Warning, WarningLabel } from 'uniswap/src/components/modals/WarningModal/types' -import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' -import { toSupportedChainId } from 'uniswap/src/features/chains/utils' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { getChainLabel, toSupportedChainId } from 'uniswap/src/features/chains/utils' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext' -import { useEnabledChains } from 'uniswap/src/features/settings/hooks' import { NativeCurrency } from 'uniswap/src/features/tokens/NativeCurrency' import { ValueType, getCurrencyAmount } from 'uniswap/src/features/tokens/getCurrencyAmount' import { useNativeCurrencyInfo } from 'uniswap/src/features/tokens/useCurrencyInfo' @@ -66,7 +65,7 @@ export function useInsufficientNativeTokenWarning({ throw new Error(`Unsupported chain ID: ${nativeCurrency?.chainId}`) } - const networkName = UNIVERSE_CHAIN_INFO[supportedChainId].label + const networkName = getChainLabel(supportedChainId) const modalOrTooltipMainMessage = ( = 5% then HIGH, else 0% < fee < 5% then MEDIUM, else NONE - const feeInt = parseFloat(fee.toFixed()) - if (feeInt >= TOKEN_PROTECTION_FOT_HONEYPOT_BREAKPOINT) { - return { - severity: WarningSeverity.High, - tokenProtectionWarning: TokenProtectionWarning.MaliciousHoneypot, - } - } else if (feeInt >= TOKEN_PROTECTION_FOT_HIGH_FEE_BREAKPOINT) { - return { - severity: WarningSeverity.High, - tokenProtectionWarning: TokenProtectionWarning.FotVeryHigh, - } - } else if (feeInt >= TOKEN_PROTECTION_FOT_FEE_BREAKPOINT) { - return { - severity: WarningSeverity.High, - tokenProtectionWarning: TokenProtectionWarning.FotHigh, - } - } else if (feeInt >= 0) { - return { - severity: WarningSeverity.Medium, - tokenProtectionWarning: TokenProtectionWarning.FotLow, - } - } else { - return { - severity: WarningSeverity.None, - tokenProtectionWarning: TokenProtectionWarning.None, - } - } -} - export function FeeOnTransferFeeGroup({ inputTokenInfo, outputTokenInfo, @@ -65,21 +17,24 @@ export function FeeOnTransferFeeGroup({ return null } + // The input token is the one you're selling, therefore it would have a sell fee + // The output token is the one you're buying, therefore it would have a buy fee return ( - - {inputTokenInfo.fee.greaterThan(0) && } - {outputTokenInfo.fee.greaterThan(0) && } + + {inputTokenInfo.fee.greaterThan(0) && } + {outputTokenInfo.fee.greaterThan(0) && } ) } -function FeeOnTransferFeeRow({ feeInfo }: { feeInfo: TokenFeeInfo }): JSX.Element { +function FeeOnTransferFeeRow({ feeType, feeInfo }: { feeType: FoTFeeType; feeInfo: TokenFeeInfo }): JSX.Element { const { t } = useTranslation() const { severity } = getFeeSeverity(feeInfo.fee) + const usdAmountLoading = feeInfo.formattedUsdAmount === '-' return ( - + {t('swap.details.feeOnTransfer', { tokenSymbol: feeInfo.tokenSymbol })} @@ -87,7 +42,7 @@ function FeeOnTransferFeeRow({ feeInfo }: { feeInfo: TokenFeeInfo }): JSX.Elemen - {feeInfo.formattedUsdAmount} + {usdAmountLoading ? `${feeInfo.formattedAmount} ${feeInfo.tokenSymbol}` : feeInfo.formattedUsdAmount} diff --git a/packages/uniswap/src/features/transactions/TransactionDetails/FeeOnTransferWarningCard.tsx b/packages/uniswap/src/features/transactions/TransactionDetails/FeeOnTransferWarningCard.tsx deleted file mode 100644 index 77fd23b2edc..00000000000 --- a/packages/uniswap/src/features/transactions/TransactionDetails/FeeOnTransferWarningCard.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { useTranslation } from 'react-i18next' -import { InlineWarningCard } from 'uniswap/src/components/InlineWarningCard/InlineWarningCard' -import { WarningSeverity } from 'uniswap/src/components/modals/WarningModal/types' -import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext' -import { - FeeOnTransferFeeGroupProps, - getFeeSeverity, -} from 'uniswap/src/features/transactions/TransactionDetails/FeeOnTransferFee' - -type FeeOnTransferWarningCardProps = { - checked: boolean - setChecked: (checked: boolean) => void -} & FeeOnTransferFeeGroupProps - -export function FeeOnTransferWarningCard({ - inputTokenInfo, - outputTokenInfo, - checked, - setChecked, -}: FeeOnTransferWarningCardProps): JSX.Element | null { - const { t } = useTranslation() - const { formatPercent } = useLocalizationContext() - - // Don't show warning card if neither token is FOT - if (!inputTokenInfo.fee.greaterThan(0) && !outputTokenInfo.fee.greaterThan(0)) { - return null - } - - const highestFeeTokenInfo = inputTokenInfo.fee.greaterThan(outputTokenInfo.fee) ? inputTokenInfo : outputTokenInfo - const { severity: feeSeverity } = getFeeSeverity(highestFeeTokenInfo.fee) - - // Only show the warning card if the fee is HIGH severity - if (feeSeverity !== WarningSeverity.High) { - return null - } - - return ( - - ) -} diff --git a/packages/uniswap/src/features/transactions/TransactionDetails/SwapReviewTokenWarningCard.tsx b/packages/uniswap/src/features/transactions/TransactionDetails/SwapReviewTokenWarningCard.tsx new file mode 100644 index 00000000000..7b1f20a207f --- /dev/null +++ b/packages/uniswap/src/features/transactions/TransactionDetails/SwapReviewTokenWarningCard.tsx @@ -0,0 +1,46 @@ +import { TokenWarningCard } from 'uniswap/src/features/tokens/TokenWarningCard' +import { + FeeOnTransferFeeGroupProps, + TokenWarningProps, +} from 'uniswap/src/features/transactions/TransactionDetails/types' +import { getShouldDisplayTokenWarningCard } from 'uniswap/src/features/transactions/TransactionDetails/utils' + +type FeeOnTransferWarningCardProps = { + checked: boolean + setChecked: (checked: boolean) => void + feeOnTransferProps?: FeeOnTransferFeeGroupProps + tokenWarningProps: TokenWarningProps +} + +export function SwapReviewTokenWarningCard({ + feeOnTransferProps, + tokenWarningProps, + checked, + setChecked, +}: FeeOnTransferWarningCardProps): JSX.Element | null { + const { + showFeeSeverityWarning, + shouldDisplayTokenWarningCard, + tokenProtectionWarningToDisplay, + feePercent, + currencyInfoToDisplay, + } = getShouldDisplayTokenWarningCard({ + tokenWarningProps, + feeOnTransferProps, + }) + + if (!shouldDisplayTokenWarningCard) { + return null + } + + return ( + + ) +} diff --git a/packages/uniswap/src/features/transactions/TransactionDetails/TransactionDetails.tsx b/packages/uniswap/src/features/transactions/TransactionDetails/TransactionDetails.tsx index 07de89cd7e8..b6c8e8474b3 100644 --- a/packages/uniswap/src/features/transactions/TransactionDetails/TransactionDetails.tsx +++ b/packages/uniswap/src/features/transactions/TransactionDetails/TransactionDetails.tsx @@ -9,18 +9,21 @@ import { AnglesMinimize } from 'ui/src/components/icons/AnglesMinimize' import { NetworkFee } from 'uniswap/src/components/gas/NetworkFee' import { getAlertColor } from 'uniswap/src/components/modals/WarningModal/getAlertColor' import { Warning } from 'uniswap/src/components/modals/WarningModal/types' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { GasFeeResult } from 'uniswap/src/features/gas/types' +import { FeatureFlags } from 'uniswap/src/features/gating/flags' +import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' +import { FeeOnTransferFeeGroup } from 'uniswap/src/features/transactions/TransactionDetails/FeeOnTransferFee' +import { SwapFee } from 'uniswap/src/features/transactions/TransactionDetails/SwapFee' +import { SwapReviewTokenWarningCard } from 'uniswap/src/features/transactions/TransactionDetails/SwapReviewTokenWarningCard' import { - FeeOnTransferFeeGroup, FeeOnTransferFeeGroupProps, -} from 'uniswap/src/features/transactions/TransactionDetails/FeeOnTransferFee' -import { FeeOnTransferWarningCard } from 'uniswap/src/features/transactions/TransactionDetails/FeeOnTransferWarningCard' -import { SwapFee } from 'uniswap/src/features/transactions/TransactionDetails/SwapFee' + TokenWarningProps, +} from 'uniswap/src/features/transactions/TransactionDetails/types' import { EstimatedTime } from 'uniswap/src/features/transactions/swap/review/EstimatedTime' import { UniswapXGasBreakdown } from 'uniswap/src/features/transactions/swap/types/swapTxAndGasInfo' import { SwapFee as SwapFeeType } from 'uniswap/src/features/transactions/swap/types/trade' -import { UniverseChainId } from 'uniswap/src/types/chains' import { openUri } from 'uniswap/src/utils/linking' interface TransactionDetailsProps { @@ -36,8 +39,9 @@ interface TransactionDetailsProps { showSeparatorToggle?: boolean warning?: Warning feeOnTransferProps?: FeeOnTransferFeeGroupProps - feeOnTransferWarningChecked?: boolean - setFeeOnTransferWarningChecked?: (checked: boolean) => void + tokenWarningProps?: TokenWarningProps + tokenWarningChecked?: boolean + setTokenWarningChecked?: (checked: boolean) => void outputCurrency?: Currency onShowWarning?: () => void indicative?: boolean @@ -65,8 +69,9 @@ export function TransactionDetails({ showWarning, warning, feeOnTransferProps, - feeOnTransferWarningChecked, - setFeeOnTransferWarningChecked, + tokenWarningProps, + tokenWarningChecked, + setTokenWarningChecked, onShowWarning, indicative = false, isSwap, @@ -78,9 +83,7 @@ export function TransactionDetails({ RateInfo, }: PropsWithChildren): JSX.Element { const { t } = useTranslation() - const showFeeOnTransferWarningCard = - !!feeOnTransferProps && !feeOnTransferWarningChecked && !!setFeeOnTransferWarningChecked - + const tokenProtectionEnabled = useFeatureFlag(FeatureFlags.TokenProtection) const [showChildren, setShowChildren] = useState(showExpandedChildren) const onPressToggleShowChildren = (): void => { @@ -127,11 +130,12 @@ export function TransactionDetails({ ) : null} - {showFeeOnTransferWarningCard && ( - )} diff --git a/packages/uniswap/src/features/transactions/TransactionDetails/types.ts b/packages/uniswap/src/features/transactions/TransactionDetails/types.ts new file mode 100644 index 00000000000..20b39c59769 --- /dev/null +++ b/packages/uniswap/src/features/transactions/TransactionDetails/types.ts @@ -0,0 +1,25 @@ +import { Percent } from '@uniswap/sdk-core' +import { WarningSeverity } from 'uniswap/src/components/modals/WarningModal/types' +import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' +import { TokenProtectionWarning } from 'uniswap/src/features/tokens/safetyUtils' + +export type FoTFeeType = 'buy' | 'sell' + +export type FeeOnTransferFeeGroupProps = { + inputTokenInfo: TokenFeeInfo + outputTokenInfo: TokenFeeInfo +} + +export type TokenFeeInfo = { + currencyInfo: Maybe + tokenSymbol: string + fee: Percent + formattedUsdAmount: string + formattedAmount: string +} + +export type TokenWarningProps = { + currencyInfo: Maybe + tokenProtectionWarning: TokenProtectionWarning + severity: WarningSeverity +} diff --git a/packages/uniswap/src/features/transactions/TransactionDetails/utils.ts b/packages/uniswap/src/features/transactions/TransactionDetails/utils.ts new file mode 100644 index 00000000000..6e086501ebc --- /dev/null +++ b/packages/uniswap/src/features/transactions/TransactionDetails/utils.ts @@ -0,0 +1,105 @@ +import { Percent } from '@uniswap/sdk-core' +import { WarningSeverity } from 'uniswap/src/components/modals/WarningModal/types' +import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' +import { + TokenProtectionWarning, + getFeeWarning, + getIsFeeRelatedWarning, + getSeverityFromTokenProtectionWarning, + getTokenProtectionWarning, +} from 'uniswap/src/features/tokens/safetyUtils' +import { + FeeOnTransferFeeGroupProps, + TokenFeeInfo, + TokenWarningProps, +} from 'uniswap/src/features/transactions/TransactionDetails/types' +import { DerivedSwapInfo } from 'uniswap/src/features/transactions/swap/types/derivedSwapInfo' + +export function getFeeSeverity(fee: Percent): { + severity: WarningSeverity + tokenProtectionWarning: TokenProtectionWarning +} { + // WarningSeverity for styling. Same logic as getTokenWarningSeverity but without non-fee-related cases. + // If fee >= 5% then HIGH, else 0% < fee < 5% then MEDIUM, else NONE + const tokenProtectionWarning = getFeeWarning(fee) + const severity = getSeverityFromTokenProtectionWarning(tokenProtectionWarning) + return { severity, tokenProtectionWarning } +} + +export function getHighestFeeSeverity(feeOnTransferProps: FeeOnTransferFeeGroupProps | undefined): { + highestFeeTokenInfo?: TokenFeeInfo + tokenProtectionWarning: TokenProtectionWarning + severity: WarningSeverity +} { + if (!feeOnTransferProps) { + return { severity: WarningSeverity.None, tokenProtectionWarning: TokenProtectionWarning.None } + } + + const { inputTokenInfo, outputTokenInfo } = feeOnTransferProps + if (!inputTokenInfo.fee.greaterThan(0) && !outputTokenInfo.fee.greaterThan(0)) { + return { severity: WarningSeverity.None, tokenProtectionWarning: TokenProtectionWarning.None } + } + + const highestFeeTokenInfo = inputTokenInfo.fee.greaterThan(outputTokenInfo.fee) ? inputTokenInfo : outputTokenInfo + return { highestFeeTokenInfo, ...getFeeSeverity(highestFeeTokenInfo.fee) } +} + +export function getShouldDisplayTokenWarningCard({ + feeOnTransferProps, + tokenWarningProps, +}: { + tokenWarningProps: TokenWarningProps + feeOnTransferProps?: FeeOnTransferFeeGroupProps +}): { + shouldDisplayTokenWarningCard: boolean + severityToDisplay: WarningSeverity + tokenProtectionWarningToDisplay: TokenProtectionWarning + feePercent: number | undefined + currencyInfoToDisplay: Maybe + showFeeSeverityWarning: boolean +} { + const { tokenProtectionWarning, severity, currencyInfo } = tokenWarningProps + + const { + severity: feeSeverity, + tokenProtectionWarning: feeWarning, + highestFeeTokenInfo, + } = getHighestFeeSeverity(feeOnTransferProps) + const feePercent = highestFeeTokenInfo ? parseFloat(highestFeeTokenInfo.fee.toFixed()) : undefined + + const feeWarningMoreSevere = feeWarning > tokenProtectionWarning + const tokenWarningIsFeeRelated = getIsFeeRelatedWarning(tokenProtectionWarning) + const tokenFeeWarningNotRelevant = tokenWarningIsFeeRelated && (feeSeverity === severity || !highestFeeTokenInfo) + + // We want to show the feeWarning over the tokenWarning IF + // 1) the fewWarning is a higher priority than the tokenWarning + // 2) if the tokenWarning is fee-related and feeWarning and tokenWarning of are equal severity, since the feeWarning's fee % is fresher (simulated trade from TradingApi) + // 3) if the tokenWarning is fee-related but there is no feeWarning (for example if the token has a buy tax but we're selling the token) + const showFeeSeverityWarning = feeWarningMoreSevere || tokenFeeWarningNotRelevant + const severityToDisplay = showFeeSeverityWarning ? feeSeverity : severity + const tokenProtectionWarningToDisplay = showFeeSeverityWarning ? feeWarning : tokenProtectionWarning + const currencyInfoToDisplay = showFeeSeverityWarning ? highestFeeTokenInfo?.currencyInfo : currencyInfo + return { + shouldDisplayTokenWarningCard: severityToDisplay === WarningSeverity.High, + severityToDisplay, + tokenProtectionWarningToDisplay, + feePercent, + currencyInfoToDisplay, + showFeeSeverityWarning, + } +} + +export function getRelevantTokenWarningSeverity( + acceptedDerivedSwapInfo?: DerivedSwapInfo, +): TokenWarningProps { + // We only care about a non-fee-related warning on the output token, since the user already owns the input token, so only sell-tax warning are relevant + const outputCurrency = acceptedDerivedSwapInfo?.currencies.output + const outputWarning = getTokenProtectionWarning(outputCurrency) + const outputSeverity = getSeverityFromTokenProtectionWarning(outputWarning) + + return { + currencyInfo: outputCurrency, + tokenProtectionWarning: outputWarning, + severity: outputSeverity, + } +} diff --git a/packages/uniswap/src/features/transactions/TransactionModal/TransactionModal.native.tsx b/packages/uniswap/src/features/transactions/TransactionModal/TransactionModal.native.tsx index f2e51c3b1d2..e8a2fc043dd 100644 --- a/packages/uniswap/src/features/transactions/TransactionModal/TransactionModal.native.tsx +++ b/packages/uniswap/src/features/transactions/TransactionModal/TransactionModal.native.tsx @@ -78,6 +78,7 @@ export function TransactionModal({ hideKeyboardOnDismiss overrideInnerContainer renderBehindTopInset + isBehindFixedBanners animatedPosition={animatedPosition} backgroundColor={colors.surface1.val} fullScreen={fullscreen} diff --git a/packages/uniswap/src/features/transactions/TransactionModal/TransactionModalContext.tsx b/packages/uniswap/src/features/transactions/TransactionModal/TransactionModalContext.tsx index 769631f993d..15ec65f7a98 100644 --- a/packages/uniswap/src/features/transactions/TransactionModal/TransactionModalContext.tsx +++ b/packages/uniswap/src/features/transactions/TransactionModal/TransactionModalContext.tsx @@ -4,7 +4,7 @@ import { createContext, PropsWithChildren, useContext, useMemo } from 'react' import type { StyleProp } from 'react-native/Libraries/StyleSheet/StyleSheet' import type { ViewStyle } from 'react-native/Libraries/StyleSheet/StyleSheetTypes' import { AuthTrigger } from 'uniswap/src/features/auth/types' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { CurrencyField } from 'uniswap/src/types/currency' export enum TransactionScreen { @@ -26,12 +26,16 @@ export type SwapRedirectFn = ({ chainId: UniverseChainId }) => void +export type BiometricsIconProps = { + color?: string +} + export type TransactionModalContextState = { bottomSheetViewStyles: StyleProp openWalletRestoreModal?: () => void walletNeedsRestore?: boolean onClose: () => void - BiometricsIcon?: JSX.Element | null + renderBiometricsIcon?: (({ color }: BiometricsIconProps) => JSX.Element) | null authTrigger?: AuthTrigger screen: TransactionScreen setScreen: (newScreen: TransactionScreen) => void @@ -42,7 +46,7 @@ export const TransactionModalContext = createContext): JSX.Element { const state = useMemo( (): TransactionModalContextState => ({ - BiometricsIcon, + renderBiometricsIcon, authTrigger, bottomSheetViewStyles, onClose, @@ -65,7 +69,7 @@ export function TransactionModalContextProvider({ walletNeedsRestore, }), [ - BiometricsIcon, + renderBiometricsIcon, authTrigger, bottomSheetViewStyles, onClose, diff --git a/packages/uniswap/src/features/transactions/TransactionModal/TransactionModalProps.tsx b/packages/uniswap/src/features/transactions/TransactionModal/TransactionModalProps.tsx index cbbeb4f9172..644051ecbc3 100644 --- a/packages/uniswap/src/features/transactions/TransactionModal/TransactionModalProps.tsx +++ b/packages/uniswap/src/features/transactions/TransactionModal/TransactionModalProps.tsx @@ -10,8 +10,8 @@ export type TransactionModalProps = PropsWithChildren<{ onClose: () => void openWalletRestoreModal?: TransactionModalContextState['openWalletRestoreModal'] swapRedirectCallback?: TransactionModalContextState['swapRedirectCallback'] + renderBiometricsIcon?: TransactionModalContextState['renderBiometricsIcon'] walletNeedsRestore?: TransactionModalContextState['walletNeedsRestore'] - BiometricsIcon?: TransactionModalContextState['BiometricsIcon'] authTrigger?: TransactionModalContextState['authTrigger'] }> diff --git a/packages/uniswap/src/features/transactions/errors.tsx b/packages/uniswap/src/features/transactions/errors.tsx index 8c46d5295e4..26d17dda79e 100644 --- a/packages/uniswap/src/features/transactions/errors.tsx +++ b/packages/uniswap/src/features/transactions/errors.tsx @@ -9,6 +9,7 @@ import { } from 'uniswap/src/features/transactions/swap/types/steps' import { Sentry } from 'utilities/src/logger/Sentry' import { OverridesSentryFingerprint } from 'utilities/src/logger/types' +import { isMobileApp } from 'utilities/src/platform' /** Superclass used to differentiate categorized/known transaction errors from generic/unknown errors. */ export abstract class TransactionError extends Error { @@ -78,14 +79,16 @@ export class TransactionStepFailedError extends TransactionError implements Over fingerprint.push(String(this.originalError.data.detail)) } } catch (e) { - Sentry.addBreadCrumb({ - level: 'info', - category: 'transaction', - message: `problem determining fingerprint for ${this.step.type}`, - data: { - errorMessage: e instanceof Error ? e.message : undefined, - }, - }) + if (!isMobileApp) { + Sentry.addBreadCrumb({ + level: 'info', + category: 'transaction', + message: `problem determining fingerprint for ${this.step.type}`, + data: { + errorMessage: e instanceof Error ? e.message : undefined, + }, + }) + } } return fingerprint diff --git a/packages/uniswap/src/features/transactions/hooks/useUSDTokenUpdater.ts b/packages/uniswap/src/features/transactions/hooks/useUSDTokenUpdater.ts index 34712831895..cbdbb06a258 100644 --- a/packages/uniswap/src/features/transactions/hooks/useUSDTokenUpdater.ts +++ b/packages/uniswap/src/features/transactions/hooks/useUSDTokenUpdater.ts @@ -3,7 +3,6 @@ import { useEffect, useRef } from 'react' import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext' import { ValueType, getCurrencyAmount } from 'uniswap/src/features/tokens/getCurrencyAmount' import { STABLECOIN_AMOUNT_OUT, useUSDCPrice } from 'uniswap/src/features/transactions/swap/hooks/useUSDCPrice' -import { NumberType } from 'utilities/src/format/types' const NUM_DECIMALS_USD = 2 const NUM_DECIMALS_DISPLAY = 2 @@ -39,7 +38,7 @@ export function useUSDTokenUpdater({ return undefined } - const exactAmountUSD = (parseFloat(exactAmountFiat) / conversionRate).toFixed(NUM_DECIMALS_USD) + const exactAmountUSD = (parseFloat(exactAmountFiat || '0') / conversionRate).toFixed(NUM_DECIMALS_USD) if (shouldUseUSDRef.current) { const stablecoinAmount = getCurrencyAmount({ @@ -50,13 +49,7 @@ export function useUSDTokenUpdater({ const currencyAmount = stablecoinAmount ? price?.invert().quote(stablecoinAmount) : undefined - return onTokenAmountUpdated( - formatCurrencyAmount({ - value: currencyAmount, - type: NumberType.SwapTradeAmount, - placeholder: '', - }), - ) + return onTokenAmountUpdated(currencyAmount?.toExact() ?? '') } const exactCurrencyAmount = getCurrencyAmount({ diff --git a/packages/uniswap/src/features/transactions/selectors.ts b/packages/uniswap/src/features/transactions/selectors.ts index dd4a253c670..5185105d875 100644 --- a/packages/uniswap/src/features/transactions/selectors.ts +++ b/packages/uniswap/src/features/transactions/selectors.ts @@ -3,6 +3,7 @@ import { useMemo } from 'react' import { useSelector } from 'react-redux' import { SearchableRecipient } from 'uniswap/src/features/address/types' import { uniqueAddressesOnly } from 'uniswap/src/features/address/utils' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { selectTokensVisibility } from 'uniswap/src/features/favorites/selectors' import { CurrencyIdToVisibility } from 'uniswap/src/features/favorites/slice' import { TransactionsState } from 'uniswap/src/features/transactions/slice' @@ -15,7 +16,6 @@ import { UniswapXOrderDetails, } from 'uniswap/src/features/transactions/types/transactionDetails' import { UniswapState } from 'uniswap/src/state/uniswapReducer' -import { UniverseChainId } from 'uniswap/src/types/chains' import { buildCurrencyId } from 'uniswap/src/utils/currencyId' import { unique } from 'utilities/src/primitives/array' import { flattenObjectOfObjects } from 'utilities/src/primitives/objects' diff --git a/packages/uniswap/src/features/transactions/send/types.ts b/packages/uniswap/src/features/transactions/send/types.ts index 494b3e93664..c214ccf8265 100644 --- a/packages/uniswap/src/features/transactions/send/types.ts +++ b/packages/uniswap/src/features/transactions/send/types.ts @@ -1,8 +1,8 @@ import { AssetType } from 'uniswap/src/entities/assets' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' import { GQLNftAsset } from 'uniswap/src/features/nfts/types' import { BaseDerivedInfo } from 'uniswap/src/features/transactions/types/baseDerivedInfo' -import { UniverseChainId } from 'uniswap/src/types/chains' import { CurrencyField } from 'uniswap/src/types/currency' export type DerivedSendInfo = BaseDerivedInfo & { diff --git a/packages/uniswap/src/features/transactions/slice.test.ts b/packages/uniswap/src/features/transactions/slice.test.ts index 5493681efe9..a053fc0c5da 100644 --- a/packages/uniswap/src/features/transactions/slice.test.ts +++ b/packages/uniswap/src/features/transactions/slice.test.ts @@ -1,5 +1,6 @@ import { createStore, Store } from '@reduxjs/toolkit' import { Routing } from 'uniswap/src/data/tradingApi/__generated__/index' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { addTransaction, cancelTransaction, @@ -19,7 +20,6 @@ import { TransactionTypeInfo, } from 'uniswap/src/features/transactions/types/transactionDetails' import { finalizedTransactionAction } from 'uniswap/src/test/fixtures' -import { UniverseChainId } from 'uniswap/src/types/chains' const finalizedTxAction = finalizedTransactionAction() diff --git a/packages/uniswap/src/features/transactions/slice.ts b/packages/uniswap/src/features/transactions/slice.ts index 3bf3e1031fa..57387e5b42c 100644 --- a/packages/uniswap/src/features/transactions/slice.ts +++ b/packages/uniswap/src/features/transactions/slice.ts @@ -45,12 +45,15 @@ const slice = createSlice({ state[from]![chainId]![id] = transaction }, finalizeTransaction: (state, { payload: transaction }: PayloadAction) => { - const { chainId, id, status, receipt, from, hash } = transaction + const { chainId, id, status, receipt, from, hash, networkFee } = transaction assert(state?.[from]?.[chainId]?.[id], `finalizeTransaction: Attempted to finalize a missing tx with id ${id}`) state[from]![chainId]![id]!.status = status if (receipt) { state[from]![chainId]![id]!.receipt = receipt } + if (networkFee) { + state[from]![chainId]![id]!.networkFee = networkFee + } if (isUniswapX(transaction) && status === TransactionStatus.Success) { assert(hash, `finalizeTransaction: Attempted to finalize an order without providing the fill tx hash`) state[from]![chainId]![id]!.hash = hash diff --git a/packages/uniswap/src/features/transactions/swap/contexts/SwapFormContext.tsx b/packages/uniswap/src/features/transactions/swap/contexts/SwapFormContext.tsx index c9585c26abf..7bee2220f20 100644 --- a/packages/uniswap/src/features/transactions/swap/contexts/SwapFormContext.tsx +++ b/packages/uniswap/src/features/transactions/swap/contexts/SwapFormContext.tsx @@ -1,9 +1,10 @@ import { createContext, ReactNode, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react' import { getNativeAddress } from 'uniswap/src/constants/addresses' import { AssetType, TradeableAsset } from 'uniswap/src/entities/assets' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' -import { useEnabledChains } from 'uniswap/src/features/settings/hooks' import { useSwapAnalytics } from 'uniswap/src/features/transactions/swap/analytics' import { useDerivedSwapInfo } from 'uniswap/src/features/transactions/swap/hooks/useDerivedSwapInfo' import { DEFAULT_CUSTOM_DEADLINE } from 'uniswap/src/features/transactions/swap/settings/useDeadlineSettings' @@ -11,7 +12,6 @@ import { DEFAULT_PROTOCOL_OPTIONS, FrontendSupportedProtocol, } from 'uniswap/src/features/transactions/swap/utils/protocols' -import { UniverseChainId } from 'uniswap/src/types/chains' import { CurrencyField } from 'uniswap/src/types/currency' import { currencyId } from 'uniswap/src/utils/currencyId' import { logContextUpdate } from 'utilities/src/logger/contextEnhancer' diff --git a/packages/uniswap/src/features/transactions/swap/form/SwapFormScreen.tsx b/packages/uniswap/src/features/transactions/swap/form/SwapFormScreen.tsx index 9e9e367660f..a6b8651f0cd 100644 --- a/packages/uniswap/src/features/transactions/swap/form/SwapFormScreen.tsx +++ b/packages/uniswap/src/features/transactions/swap/form/SwapFormScreen.tsx @@ -26,6 +26,7 @@ import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' import { ElementName, SectionName } from 'uniswap/src/features/telemetry/constants' import Trace from 'uniswap/src/features/telemetry/Trace' import { + DecimalPadCalculatedSpaceId, DecimalPadCalculateSpace, DecimalPadInput, DecimalPadInputRef, @@ -530,7 +531,7 @@ function SwapFormContent({ wrapCallback }: { wrapCallback?: WrapCallback }): JSX ) // We *always* want to show the footer on native mobile because it's used to calculate the available space for the `DecimalPad`. - const showFooter = !hideFooter && (isMobileApp || (exactAmountToken && input && output)) + const showFooter = Boolean(!hideFooter && (isMobileApp || (exactAmountToken && input && output))) return ( @@ -675,7 +676,8 @@ function SwapFormContent({ wrapCallback }: { wrapCallback?: WrapCallback }): JSX {!isWeb && ( <> - + + , ): FeeOnTransferFeeGroupProps | undefined { const { t } = useTranslation() - const { convertFiatAmountFormatted } = useLocalizationContext() + const { convertFiatAmountFormatted, formatCurrencyAmount } = useLocalizationContext() const { inputCurrencyAmount, outputCurrencyAmount } = getTradeAmounts(acceptedDerivedSwapInfo) const usdAmountIn = useUSDCValue(inputCurrencyAmount) @@ -24,6 +24,9 @@ export function useFeeOnTransferAmounts( return undefined } + const { currencies } = acceptedDerivedSwapInfo + const { input: inputCurrencyInfo, output: outputCurrencyInfo } = currencies + const acceptedTrade = acceptedDerivedSwapInfo.trade.trade ?? acceptedDerivedSwapInfo.trade.indicativeTrade const tradeHasFeeToken = acceptedTrade?.inputTax?.greaterThan(0) || acceptedTrade?.outputTax?.greaterThan(0) @@ -37,17 +40,35 @@ export function useFeeOnTransferAmounts( const formattedUsdTaxAmountIn = convertFiatAmountFormatted(usdTaxAmountIn, NumberType.FiatTokenQuantity) const formattedUsdTaxAmountOut = convertFiatAmountFormatted(usdTaxAmountOut, NumberType.FiatTokenQuantity) + const taxAmountIn = inputCurrencyAmount?.multiply(acceptedTrade.inputTax) + const taxAmountOut = outputCurrencyAmount?.multiply(acceptedTrade.outputTax) + const formattedAmountIn = formatCurrencyAmount({ value: taxAmountIn, type: NumberType.TokenTx }) + const formattedAmountOut = formatCurrencyAmount({ value: taxAmountOut, type: NumberType.TokenTx }) + return { inputTokenInfo: { + currencyInfo: inputCurrencyInfo, fee: acceptedTrade.inputTax, tokenSymbol: acceptedTrade.inputAmount.currency.symbol ?? t('token.symbol.input.fallback'), formattedUsdAmount: formattedUsdTaxAmountIn, + formattedAmount: formattedAmountIn, }, outputTokenInfo: { + currencyInfo: outputCurrencyInfo, fee: acceptedTrade.outputTax, tokenSymbol: acceptedTrade.outputAmount.currency.symbol ?? t('token.symbol.output.fallback'), formattedUsdAmount: formattedUsdTaxAmountOut, + formattedAmount: formattedAmountOut, }, } - }, [acceptedDerivedSwapInfo, usdAmountIn, usdAmountOut, convertFiatAmountFormatted, t]) + }, [ + acceptedDerivedSwapInfo, + usdAmountIn, + usdAmountOut, + convertFiatAmountFormatted, + formatCurrencyAmount, + inputCurrencyAmount, + outputCurrencyAmount, + t, + ]) } diff --git a/packages/uniswap/src/features/transactions/swap/hooks/usePollingIntervalByChain.ts b/packages/uniswap/src/features/transactions/swap/hooks/usePollingIntervalByChain.ts index 7d7080cf0ba..35d134c20e8 100644 --- a/packages/uniswap/src/features/transactions/swap/hooks/usePollingIntervalByChain.ts +++ b/packages/uniswap/src/features/transactions/swap/hooks/usePollingIntervalByChain.ts @@ -1,22 +1,23 @@ +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { isMainnetChainId } from 'uniswap/src/features/chains/utils' import { DynamicConfigs, SwapConfigKey } from 'uniswap/src/features/gating/configs' import { useDynamicConfigValue } from 'uniswap/src/features/gating/hooks' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { ONE_SECOND_MS } from 'utilities/src/time/time' -const FALLBACK_L1_BLOCK_TIME_MS = 12000 -const FALLBACK_L2_BLOCK_TIME_MS = 3000 +export const AVERAGE_L1_BLOCK_TIME_MS = 12 * ONE_SECOND_MS +const AVERAGE_L2_BLOCK_TIME_MS = 3 * ONE_SECOND_MS export function usePollingIntervalByChain(chainId?: UniverseChainId): number { const averageL1BlockTimeMs = useDynamicConfigValue( DynamicConfigs.Swap, SwapConfigKey.AverageL1BlockTimeMs, - FALLBACK_L1_BLOCK_TIME_MS, + AVERAGE_L1_BLOCK_TIME_MS, ) const averageL2BlockTimeMs = useDynamicConfigValue( DynamicConfigs.Swap, SwapConfigKey.AverageL2BlockTimeMs, - FALLBACK_L2_BLOCK_TIME_MS, + AVERAGE_L2_BLOCK_TIME_MS, ) return isMainnetChainId(chainId) ? averageL1BlockTimeMs : averageL2BlockTimeMs diff --git a/packages/uniswap/src/features/transactions/swap/hooks/useSwapNetworkNotification.test.ts b/packages/uniswap/src/features/transactions/swap/hooks/useSwapNetworkNotification.test.ts index c7d76b905ce..e24f322cf94 100644 --- a/packages/uniswap/src/features/transactions/swap/hooks/useSwapNetworkNotification.test.ts +++ b/packages/uniswap/src/features/transactions/swap/hooks/useSwapNetworkNotification.test.ts @@ -1,7 +1,7 @@ import { act, renderHook } from '@testing-library/react-native' import { useUniswapContext } from 'uniswap/src/contexts/UniswapContext' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { useSwapNetworkNotification } from 'uniswap/src/features/transactions/swap/hooks/useSwapNetworkNotification' -import { UniverseChainId } from 'uniswap/src/types/chains' jest.mock('uniswap/src/contexts/UniswapContext', () => ({ useUniswapContext: jest.fn(), diff --git a/packages/uniswap/src/features/transactions/swap/hooks/useSwapNetworkNotification.ts b/packages/uniswap/src/features/transactions/swap/hooks/useSwapNetworkNotification.ts index e8febe47f27..80c902ea6a0 100644 --- a/packages/uniswap/src/features/transactions/swap/hooks/useSwapNetworkNotification.ts +++ b/packages/uniswap/src/features/transactions/swap/hooks/useSwapNetworkNotification.ts @@ -1,6 +1,6 @@ import { useEffect, useRef } from 'react' import { useUniswapContext } from 'uniswap/src/contexts/UniswapContext' -import { UniverseChainId } from 'uniswap/src/types/chains' +import { UniverseChainId } from 'uniswap/src/features/chains/types' interface SwapChains { inputChainId?: UniverseChainId diff --git a/packages/uniswap/src/features/transactions/swap/hooks/useSwapPrefilledState.ts b/packages/uniswap/src/features/transactions/swap/hooks/useSwapPrefilledState.ts index e692511fe51..2bee84814b8 100644 --- a/packages/uniswap/src/features/transactions/swap/hooks/useSwapPrefilledState.ts +++ b/packages/uniswap/src/features/transactions/swap/hooks/useSwapPrefilledState.ts @@ -1,10 +1,10 @@ import { useMemo } from 'react' import { getNativeAddress } from 'uniswap/src/constants/addresses' import { AssetType, CurrencyAsset } from 'uniswap/src/entities/assets' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { SwapFormState } from 'uniswap/src/features/transactions/swap/contexts/SwapFormContext' import { DEFAULT_PROTOCOL_OPTIONS } from 'uniswap/src/features/transactions/swap/utils/protocols' import { TransactionState } from 'uniswap/src/features/transactions/types/transactionState' -import { UniverseChainId } from 'uniswap/src/types/chains' import { CurrencyField } from 'uniswap/src/types/currency' import { areAddressesEqual } from 'uniswap/src/utils/addresses' diff --git a/packages/uniswap/src/features/transactions/swap/hooks/useSwapTxAndGasInfo.test.ts b/packages/uniswap/src/features/transactions/swap/hooks/useSwapTxAndGasInfo.test.ts index 9d37bddb8f9..25ba8d86eda 100644 --- a/packages/uniswap/src/features/transactions/swap/hooks/useSwapTxAndGasInfo.test.ts +++ b/packages/uniswap/src/features/transactions/swap/hooks/useSwapTxAndGasInfo.test.ts @@ -3,6 +3,7 @@ import { UNI, WBTC } from 'uniswap/src/constants/tokens' import { Routing } from 'uniswap/src/data/tradingApi/__generated__/index' import { FeeType } from 'uniswap/src/data/tradingApi/types' import { AccountType, SignerMnemonicAccountMeta } from 'uniswap/src/features/accounts/types' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { DEFAULT_GAS_STRATEGY } from 'uniswap/src/features/gas/hooks' import { useSwapTxAndGasInfo } from 'uniswap/src/features/transactions/swap/hooks/useSwapTxAndGasInfo' import { useTokenApprovalInfo } from 'uniswap/src/features/transactions/swap/hooks/useTokenApprovalInfo' @@ -14,7 +15,6 @@ import { DerivedSwapInfo } from 'uniswap/src/features/transactions/swap/types/de import { ClassicSwapTxAndGasInfo } from 'uniswap/src/features/transactions/swap/types/swapTxAndGasInfo' import { ApprovalAction } from 'uniswap/src/features/transactions/swap/types/trade' import { createMockDerivedSwapInfo } from 'uniswap/src/test/fixtures/transactions/swap' -import { UniverseChainId } from 'uniswap/src/types/chains' jest.mock('uniswap/src/features/transactions/swap/hooks/useTokenApprovalInfo') jest.mock('uniswap/src/features/transactions/swap/hooks/useTransactionRequestInfo') diff --git a/packages/uniswap/src/features/transactions/swap/hooks/useSwapTxAndGasInfo.ts b/packages/uniswap/src/features/transactions/swap/hooks/useSwapTxAndGasInfo.ts index 4e56fe5ae1a..247aeea9609 100644 --- a/packages/uniswap/src/features/transactions/swap/hooks/useSwapTxAndGasInfo.ts +++ b/packages/uniswap/src/features/transactions/swap/hooks/useSwapTxAndGasInfo.ts @@ -137,11 +137,13 @@ function getTotalGasFee( tokenApprovalInfo: TokenApprovalInfoWithGas, account?: AccountMeta, ): GasFeeResult { - const isConnected = account?.address - const isLoading = (isConnected && !tokenApprovalInfo) || swapGasResult.isLoading - const hasApprovalError = - isConnected && !tokenApprovalInfo?.isLoading && tokenApprovalInfo.action === ApprovalAction.Unknown - let error = swapGasResult.error ?? hasApprovalError ? new Error('Approval action unknown') : null + const isConnected = !!account?.address + const blockingUnknownApprovalStatus = isConnected && tokenApprovalInfo.action === ApprovalAction.Unknown + const isLoading = swapGasResult.isLoading || (blockingUnknownApprovalStatus && tokenApprovalInfo.isLoading) + + const approvalError = + blockingUnknownApprovalStatus && !tokenApprovalInfo.isLoading ? new Error('Approval action unknown') : null + let error = swapGasResult.error ?? approvalError // If swap requires revocation we expect simulation error so set error to null if (tokenApprovalInfo?.action === ApprovalAction.RevokeAndPermit2Approve) { @@ -166,7 +168,7 @@ function getTotalGasFee( // Do not populate gas fee: // - If errors exist on swap or approval requests. // - If we don't have both the approval and transaction gas fees. - if (approvalGasFeeMissing || swapGasFeeMissing || hasApprovalError || error) { + if (approvalGasFeeMissing || swapGasFeeMissing || blockingUnknownApprovalStatus || error) { return { value: undefined, error, isLoading } } diff --git a/packages/uniswap/src/features/transactions/swap/hooks/useSwapWarningUtils.ts b/packages/uniswap/src/features/transactions/swap/hooks/useSwapWarningUtils.ts new file mode 100644 index 00000000000..ca1f01d59d7 --- /dev/null +++ b/packages/uniswap/src/features/transactions/swap/hooks/useSwapWarningUtils.ts @@ -0,0 +1,64 @@ +import { Currency, TradeType } from '@uniswap/sdk-core' +import { TFunction } from 'i18next' +import { Warning, WarningAction, WarningLabel, WarningSeverity } from 'uniswap/src/components/modals/WarningModal/types' +import { FetchError, isRateLimitFetchError } from 'uniswap/src/data/apiClients/FetchError' +import { Err404 } from 'uniswap/src/data/tradingApi/__generated__' +import { NoRoutesError, SWAP_QUOTE_ERROR } from 'uniswap/src/features/transactions/swap/hooks/useTrade' +import { Trade, TradeWithStatus } from 'uniswap/src/features/transactions/swap/types/trade' + +export const getSwapWarningDetails = ( + trade: TradeWithStatus>, + t: TFunction, +): Warning[] => { + const warnings: Warning[] = [] + const { error } = trade + + if (error) { + if ( + error instanceof NoRoutesError || + (error instanceof FetchError && error?.data?.errorCode === SWAP_QUOTE_ERROR) + ) { + warnings.push({ + type: WarningLabel.LowLiquidity, + severity: WarningSeverity.Medium, + action: WarningAction.DisableReview, + title: t('swap.warning.lowLiquidity.title'), + message: t('swap.warning.lowLiquidity.message'), + }) + } else if (isRateLimitFetchError(error)) { + warnings.push({ + type: WarningLabel.RateLimit, + severity: WarningSeverity.Medium, + action: WarningAction.DisableReview, + title: t('swap.warning.rateLimit.title'), + message: t('swap.warning.rateLimit.message'), + }) + } else if (error instanceof FetchError && error?.data?.errorCode === Err404.errorCode.QUOTE_AMOUNT_TOO_LOW_ERROR) { + warnings.push({ + type: WarningLabel.EnterLargerAmount, + severity: WarningSeverity.Low, + action: WarningAction.DisableReview, + title: t('swap.warning.enterLargerAmount.title'), + message: '', + }) + } else if (error instanceof FetchError && error?.data?.errorCode === Err404.errorCode.RESOURCE_NOT_FOUND) { + warnings.push({ + type: WarningLabel.EnterLargerAmount, + severity: WarningSeverity.Low, + action: WarningAction.DisableReview, + title: t('swap.warning.noRoutesFound.title'), + message: t('swap.warning.noRoutesFound.message'), + }) + } else { + warnings.push({ + type: WarningLabel.SwapRouterError, + severity: WarningSeverity.Low, + action: WarningAction.DisableReview, + title: t('swap.warning.router.title'), + message: t('swap.warning.router.message'), + }) + } + } + + return warnings +} diff --git a/packages/uniswap/src/features/transactions/swap/hooks/useSwapWarnings.test.ts b/packages/uniswap/src/features/transactions/swap/hooks/useSwapWarnings.test.ts index 7b1dd33ed60..9808c017146 100644 --- a/packages/uniswap/src/features/transactions/swap/hooks/useSwapWarnings.test.ts +++ b/packages/uniswap/src/features/transactions/swap/hooks/useSwapWarnings.test.ts @@ -1,6 +1,7 @@ import { CurrencyAmount } from '@uniswap/sdk-core' import { WarningLabel } from 'uniswap/src/components/modals/WarningModal/types' import { DAI, USDC } from 'uniswap/src/constants/tokens' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { Locale } from 'uniswap/src/features/language/constants' import { NativeCurrency } from 'uniswap/src/features/tokens/NativeCurrency' import { getSwapWarnings } from 'uniswap/src/features/transactions/swap/hooks/useSwapWarnings' @@ -10,7 +11,6 @@ import i18n from 'uniswap/src/i18n/i18n' import { daiCurrencyInfo, ethCurrencyInfo } from 'uniswap/src/test/fixtures' import { createGasFeeEstimates } from 'uniswap/src/test/fixtures/tradingApi' import { mockLocalizedFormatter } from 'uniswap/src/test/mocks' -import { UniverseChainId } from 'uniswap/src/types/chains' import { CurrencyField } from 'uniswap/src/types/currency' const ETH = NativeCurrency.onChain(UniverseChainId.Mainnet) diff --git a/packages/uniswap/src/features/transactions/swap/hooks/useSwapWarnings.tsx b/packages/uniswap/src/features/transactions/swap/hooks/useSwapWarnings.tsx index 459ea393e38..553dbf7fbef 100644 --- a/packages/uniswap/src/features/transactions/swap/hooks/useSwapWarnings.tsx +++ b/packages/uniswap/src/features/transactions/swap/hooks/useSwapWarnings.tsx @@ -8,8 +8,6 @@ import { isWeb } from 'ui/src' import { Warning, WarningAction, WarningLabel, WarningSeverity } from 'uniswap/src/components/modals/WarningModal/types' import { uniswapUrls } from 'uniswap/src/constants/urls' import { useAccountMeta } from 'uniswap/src/contexts/UniswapContext' -import { FetchError, isRateLimitFetchError } from 'uniswap/src/data/apiClients/FetchError' -import { Err404 } from 'uniswap/src/data/tradingApi/__generated__' import { selectHasDismissedBridgingWarning } from 'uniswap/src/features/behaviorHistory/selectors' import { useTransactionGasWarning } from 'uniswap/src/features/gas/hooks' import { LocalizationContextState, useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext' @@ -19,7 +17,7 @@ import { } from 'uniswap/src/features/transactions/hooks/useParsedTransactionWarnings' import { useSwapFormContext } from 'uniswap/src/features/transactions/swap/contexts/SwapFormContext' import { useSwapTxContext } from 'uniswap/src/features/transactions/swap/contexts/SwapTxContext' -import { NoRoutesError, SWAP_QUOTE_ERROR } from 'uniswap/src/features/transactions/swap/hooks/useTrade' +import { getSwapWarningDetails } from 'uniswap/src/features/transactions/swap/hooks/useSwapWarningUtils' import { DerivedSwapInfo } from 'uniswap/src/features/transactions/swap/types/derivedSwapInfo' import { isBridge } from 'uniswap/src/features/transactions/swap/utils/routing' import { ParsedWarnings } from 'uniswap/src/features/transactions/types/transactionDetails' @@ -67,48 +65,8 @@ export function getSwapWarnings( }) } - const { error } = trade - - // low liquidity and other swap errors - if (error) { - if ( - error instanceof NoRoutesError || - (error instanceof FetchError && error?.data?.errorCode === SWAP_QUOTE_ERROR) - ) { - warnings.push({ - type: WarningLabel.LowLiquidity, - severity: WarningSeverity.Medium, - action: WarningAction.DisableReview, - title: t('swap.warning.lowLiquidity.title'), - message: t('swap.warning.lowLiquidity.message'), - }) - } else if (isRateLimitFetchError(error)) { - warnings.push({ - type: WarningLabel.RateLimit, - severity: WarningSeverity.Medium, - action: WarningAction.DisableReview, - title: t('swap.warning.rateLimit.title'), - message: t('swap.warning.rateLimit.message'), - }) - } else if (error instanceof FetchError && error?.data?.errorCode === Err404.errorCode.QUOTE_AMOUNT_TOO_LOW_ERROR) { - warnings.push({ - type: WarningLabel.EnterLargerAmount, - severity: WarningSeverity.Low, - action: WarningAction.DisableReview, - title: t('swap.warning.enterLargerAmount.title'), - message: '', - }) - } else { - // catch all other router errors in a generic swap router error message - warnings.push({ - type: WarningLabel.SwapRouterError, - severity: WarningSeverity.Medium, - action: WarningAction.DisableReview, - title: t('swap.warning.router.title'), - message: t('swap.warning.router.message'), - }) - } - } + const swapWarnings = getSwapWarningDetails(trade, t) + warnings.push(...swapWarnings) // swap form is missing input, output fields if (formIncomplete(derivedSwapInfo)) { diff --git a/packages/uniswap/src/features/transactions/swap/hooks/useTokenApprovalInfo.test.ts b/packages/uniswap/src/features/transactions/swap/hooks/useTokenApprovalInfo.test.ts index d8c9635c59b..32c8ba6f997 100644 --- a/packages/uniswap/src/features/transactions/swap/hooks/useTokenApprovalInfo.test.ts +++ b/packages/uniswap/src/features/transactions/swap/hooks/useTokenApprovalInfo.test.ts @@ -5,6 +5,7 @@ import { useCheckApprovalQuery } from 'uniswap/src/data/apiClients/tradingApi/us import { Routing } from 'uniswap/src/data/tradingApi/__generated__/index' import { FeeType } from 'uniswap/src/data/tradingApi/types' import { AccountMeta, AccountType } from 'uniswap/src/features/accounts/types' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { DEFAULT_GAS_STRATEGY } from 'uniswap/src/features/gas/hooks' import { TokenApprovalInfoParams, @@ -13,7 +14,6 @@ import { import { ApprovalAction } from 'uniswap/src/features/transactions/swap/types/trade' import { GasFeeEstimates } from 'uniswap/src/features/transactions/types/transactionDetails' import { WrapType } from 'uniswap/src/features/transactions/types/wrap' -import { UniverseChainId } from 'uniswap/src/types/chains' import { logger } from 'utilities/src/logger/logger' jest.mock('uniswap/src/data/apiClients/tradingApi/useCheckApprovalQuery') diff --git a/packages/uniswap/src/features/transactions/swap/hooks/useTokenApprovalInfo.ts b/packages/uniswap/src/features/transactions/swap/hooks/useTokenApprovalInfo.ts index 9d5aa15c41d..cb8e0393f37 100644 --- a/packages/uniswap/src/features/transactions/swap/hooks/useTokenApprovalInfo.ts +++ b/packages/uniswap/src/features/transactions/swap/hooks/useTokenApprovalInfo.ts @@ -3,16 +3,17 @@ import { useMemo } from 'react' import { useCheckApprovalQuery } from 'uniswap/src/data/apiClients/tradingApi/useCheckApprovalQuery' import { ApprovalRequest, Routing } from 'uniswap/src/data/tradingApi/__generated__/index' import { AccountMeta } from 'uniswap/src/features/accounts/types' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { useActiveGasStrategy, useShadowGasStrategies } from 'uniswap/src/features/gas/hooks' import { areEqualGasStrategies } from 'uniswap/src/features/gas/types' import { ApprovalAction, TokenApprovalInfo } from 'uniswap/src/features/transactions/swap/types/trade' +import { isUniswapX } from 'uniswap/src/features/transactions/swap/utils/routing' import { getTokenAddressForApi, toTradingApiSupportedChainId, } from 'uniswap/src/features/transactions/swap/utils/tradingApi' import { GasFeeEstimates } from 'uniswap/src/features/transactions/types/transactionDetails' import { WrapType } from 'uniswap/src/features/transactions/types/wrap' -import { UniverseChainId } from 'uniswap/src/types/chains' import { logger } from 'utilities/src/logger/logger' import { ONE_MINUTE_MS, ONE_SECOND_MS } from 'utilities/src/time/time' @@ -39,8 +40,9 @@ export function useTokenApprovalInfo(params: TokenApprovalInfoParams): TokenAppr const isWrap = wrapType !== WrapType.NotApplicable const address = account?.address + const inputWillBeWrapped = routing && isUniswapX({ routing }) // Off-chain orders must have wrapped currencies approved, rather than natives. - const currencyIn = routing === Routing.DUTCH_V2 ? currencyInAmount?.currency.wrapped : currencyInAmount?.currency + const currencyIn = inputWillBeWrapped ? currencyInAmount?.currency.wrapped : currencyInAmount?.currency const amount = currencyInAmount?.quotient.toString() const tokenInAddress = getTokenAddressForApi(currencyIn) diff --git a/packages/uniswap/src/features/transactions/swap/hooks/useTransactionRequestInfo.test.ts b/packages/uniswap/src/features/transactions/swap/hooks/useTransactionRequestInfo.test.ts index bd06759c011..f1c9d6c9895 100644 --- a/packages/uniswap/src/features/transactions/swap/hooks/useTransactionRequestInfo.test.ts +++ b/packages/uniswap/src/features/transactions/swap/hooks/useTransactionRequestInfo.test.ts @@ -2,6 +2,7 @@ import { renderHook } from '@testing-library/react-hooks' import { providers } from 'ethers/lib/ethers' import { useTradingApiSwapQuery } from 'uniswap/src/data/apiClients/tradingApi/useTradingApiSwapQuery' import { AccountMeta, AccountType } from 'uniswap/src/features/accounts/types' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { useTransactionGasFee } from 'uniswap/src/features/gas/hooks' import { GasFeeResult } from 'uniswap/src/features/gas/types' import { usePermit2SignatureWithData } from 'uniswap/src/features/transactions/swap/hooks/usePermit2Signature' @@ -10,7 +11,6 @@ import { useWrapTransactionRequest } from 'uniswap/src/features/transactions/swa import { WrapType } from 'uniswap/src/features/transactions/types/wrap' import { ETH, WETH } from 'uniswap/src/test/fixtures' import { createMockDerivedSwapInfo, createMockTokenApprovalInfo } from 'uniswap/src/test/fixtures/transactions/swap' -import { UniverseChainId } from 'uniswap/src/types/chains' jest.mock('uniswap/src/data/apiClients/tradingApi/useTradingApiSwapQuery') jest.mock('uniswap/src/features/transactions/swap/hooks/usePermit2Signature') diff --git a/packages/uniswap/src/features/transactions/swap/hooks/useUSDCPrice.ts b/packages/uniswap/src/features/transactions/swap/hooks/useUSDCPrice.ts index 335874137bf..0273f931016 100644 --- a/packages/uniswap/src/features/transactions/swap/hooks/useUSDCPrice.ts +++ b/packages/uniswap/src/features/transactions/swap/hooks/useUSDCPrice.ts @@ -17,9 +17,9 @@ import { USDC_ZORA, USDT_BNB, } from 'uniswap/src/constants/tokens' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { useTrade } from 'uniswap/src/features/transactions/swap/hooks/useTrade' import { isClassic } from 'uniswap/src/features/transactions/swap/utils/routing' -import { UniverseChainId } from 'uniswap/src/types/chains' import { areCurrencyIdsEqual, currencyId } from 'uniswap/src/utils/currencyId' // Stablecoin amounts used when calculating spot price for a given currency. diff --git a/packages/uniswap/src/features/transactions/swap/hooks/useWrapTransactionRequest.ts b/packages/uniswap/src/features/transactions/swap/hooks/useWrapTransactionRequest.ts index 9adba37f6d6..ae853873ca1 100644 --- a/packages/uniswap/src/features/transactions/swap/hooks/useWrapTransactionRequest.ts +++ b/packages/uniswap/src/features/transactions/swap/hooks/useWrapTransactionRequest.ts @@ -6,10 +6,10 @@ import WETH_ABI from 'uniswap/src/abis/weth.json' import { getWrappedNativeAddress } from 'uniswap/src/constants/addresses' import { useProvider } from 'uniswap/src/contexts/UniswapContext' import { AccountMeta } from 'uniswap/src/features/accounts/types' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { DerivedSwapInfo } from 'uniswap/src/features/transactions/swap/types/derivedSwapInfo' import { isUniswapX } from 'uniswap/src/features/transactions/swap/utils/routing' import { WrapType } from 'uniswap/src/features/transactions/types/wrap' -import { UniverseChainId } from 'uniswap/src/types/chains' import { useAsyncData } from 'utilities/src/react/hooks' export async function getWethContract(chainId: UniverseChainId, provider: providers.Provider): Promise { diff --git a/packages/uniswap/src/features/transactions/swap/modals/BridgingModal.tsx b/packages/uniswap/src/features/transactions/swap/modals/BridgingModal.tsx index 2b3bd697ab1..f551432fde5 100644 --- a/packages/uniswap/src/features/transactions/swap/modals/BridgingModal.tsx +++ b/packages/uniswap/src/features/transactions/swap/modals/BridgingModal.tsx @@ -7,9 +7,8 @@ import { iconSizes } from 'ui/src/theme' import { NetworkLogo } from 'uniswap/src/components/CurrencyLogo/NetworkLogo' import { WarningModal } from 'uniswap/src/components/modals/WarningModal/WarningModal' import { WarningSeverity } from 'uniswap/src/components/modals/WarningModal/types' -import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' import { setHasDismissedBridgingWarning } from 'uniswap/src/features/behaviorHistory/slice' -import { toSupportedChainId } from 'uniswap/src/features/chains/utils' +import { getChainLabel, toSupportedChainId } from 'uniswap/src/features/chains/utils' import { ModalName } from 'uniswap/src/features/telemetry/constants' import { DerivedSwapInfo } from 'uniswap/src/features/transactions/swap/types/derivedSwapInfo' @@ -56,8 +55,8 @@ export function BridgingModal({ ): JSX.Element { +export function FeeOnTransferWarning({ + children, + feeInfo, + feeType, +}: PropsWithChildren<{ feeInfo: TokenFeeInfo; feeType: FoTFeeType }>): JSX.Element { const { t } = useTranslation() - const { severity } = getFeeSeverity(feeInfo.fee) - const caption = t('swap.warning.feeOnTransfer.message') - const title = t('swap.warning.feeOnTransfer.title') + const { formatPercent } = useLocalizationContext() + const [showModal, setShowModal] = useState(false) + + const { fee, tokenSymbol } = feeInfo + const feePercent = parseFloat(fee.toFixed()) + const formattedFeePercent = formatPercent(feePercent) + + const { tokenProtectionWarning } = getFeeSeverity(feeInfo.fee) + // These should never be null bc tokenProtectionWarning is never None + const title = getModalHeaderText({ t, tokenProtectionWarning, tokenSymbol0: tokenSymbol }) ?? '' + const subtitle = + getModalSubtitleTokenWarningText({ t, tokenProtectionWarning, tokenSymbol, formattedFeePercent }) ?? '' + + if (isInterface) { + return ( + + + + } + trigger={} + triggerPlacement="end" + > + {children} + + ) + } + + const onPress = (): void => { + setShowModal(true) + } + + const onClose = (): void => { + setShowModal(false) + } return ( - + + + {children} + + + + {feeInfo.currencyInfo && ( + - } - modalProps={{ - caption, - rejectText: t('common.button.close'), - icon: , - modalName: ModalName.FOTInfo, - title, - rejectButtonTheme: 'tertiary', - backgroundIconColor: false, - }} - tooltipProps={{ - text: caption, - title, - placement: 'top', - }} - > - {children} - + )} + ) } diff --git a/packages/uniswap/src/features/transactions/swap/modals/RoutingInfo.tsx b/packages/uniswap/src/features/transactions/swap/modals/RoutingInfo.tsx index a1f49893260..2ff514663fc 100644 --- a/packages/uniswap/src/features/transactions/swap/modals/RoutingInfo.tsx +++ b/packages/uniswap/src/features/transactions/swap/modals/RoutingInfo.tsx @@ -5,13 +5,13 @@ import { Flex, Text, TouchableArea, UniswapXText, isWeb } from 'ui/src' import RoutingDiagram from 'uniswap/src/components/RoutingDiagram/RoutingDiagram' import { WarningInfo } from 'uniswap/src/components/modals/WarningModal/WarningInfo' import { uniswapUrls } from 'uniswap/src/constants/urls' +import { UniverseChainId } from 'uniswap/src/features/chains/types' import { useUSDValueOfGasFee } from 'uniswap/src/features/gas/hooks' import { GasFeeResult } from 'uniswap/src/features/gas/types' import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext' import { ModalName } from 'uniswap/src/features/telemetry/constants' import { useSwapTxContext } from 'uniswap/src/features/transactions/swap/contexts/SwapTxContext' import { isClassic, isUniswapX } from 'uniswap/src/features/transactions/swap/utils/routing' -import { UniverseChainId } from 'uniswap/src/types/chains' import getRoutingDiagramEntries from 'uniswap/src/utils/getRoutingDiagramEntries' import { openUri } from 'uniswap/src/utils/linking' diff --git a/packages/uniswap/src/features/transactions/swap/review/SubmitSwapButton.tsx b/packages/uniswap/src/features/transactions/swap/review/SubmitSwapButton.tsx index ef7963b679b..614e98644b2 100644 --- a/packages/uniswap/src/features/transactions/swap/review/SubmitSwapButton.tsx +++ b/packages/uniswap/src/features/transactions/swap/review/SubmitSwapButton.tsx @@ -31,7 +31,7 @@ export function SubmitSwapButton({ warning, }: SubmitSwapButtonProps): JSX.Element { const { t } = useTranslation() - const { BiometricsIcon } = useTransactionModalContext() + const { renderBiometricsIcon } = useTransactionModalContext() const { isSubmitting, derivedSwapInfo } = useSwapFormContext() const { @@ -87,10 +87,10 @@ export function SubmitSwapButton({ return (