ci(release): publish latest release

parent 0b9f7711
...@@ -5,6 +5,7 @@ ignores: [ ...@@ -5,6 +5,7 @@ ignores: [
'@uniswap/eslint-config', '@uniswap/eslint-config',
'i18next', 'i18next',
'moti', 'moti',
'wrangler',
# Dependencies that depcheck thinks are missing but are actually present or never used # Dependencies that depcheck thinks are missing but are actually present or never used
'@yarnpkg/core', '@yarnpkg/core',
'@yarnpkg/cli', '@yarnpkg/cli',
......
...@@ -60,3 +60,9 @@ apps/mobile/.maestro/scripts/testIds.js ...@@ -60,3 +60,9 @@ apps/mobile/.maestro/scripts/testIds.js
# RNEF # RNEF
.rnef/ .rnef/
# claude
claude.md
claude.local.md
CLAUDE.md
CLAUDE.local.md
...@@ -11,11 +11,11 @@ __mocks__ ...@@ -11,11 +11,11 @@ __mocks__
*.html *.html
*.inc *.inc
*.json *.json
*.jsonc
*.md *.md
*.yml *.yml
build build
craco.config.cjs craco.config.cjs
cypress
dist dist
jest-setup.js jest-setup.js
jest.config.js jest.config.js
......
IPFS hash of the deployment: IPFS hash of the deployment:
- CIDv0: `QmbnHL5TQqNFjVrQtUdXNUci2KGYPBypTjbTEm8PQJjBSX` - CIDv0: `QmVzsLbRpsi2d5BKNShX4XHtieNHiwLTMAnf78ehMM1YZ8`
- CIDv1: `bafybeighxdnvvampcjgznnlroji2h4t4fes5acglqdugvuezoob3hvkmga` - CIDv1: `bafybeidrzqulidzlilytjjw4hihdvu2iyod7xvuy2f35koh6vebadxnf4m`
The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org). The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org).
...@@ -10,14 +10,14 @@ You can also access the Uniswap Interface from an IPFS gateway. ...@@ -10,14 +10,14 @@ You can also access the Uniswap Interface from an IPFS gateway.
Your Uniswap settings are never remembered across different URLs. Your Uniswap settings are never remembered across different URLs.
IPFS gateways: IPFS gateways:
- https://bafybeighxdnvvampcjgznnlroji2h4t4fes5acglqdugvuezoob3hvkmga.ipfs.dweb.link/ - https://bafybeidrzqulidzlilytjjw4hihdvu2iyod7xvuy2f35koh6vebadxnf4m.ipfs.dweb.link/
- [ipfs://QmbnHL5TQqNFjVrQtUdXNUci2KGYPBypTjbTEm8PQJjBSX/](ipfs://QmbnHL5TQqNFjVrQtUdXNUci2KGYPBypTjbTEm8PQJjBSX/) - [ipfs://QmVzsLbRpsi2d5BKNShX4XHtieNHiwLTMAnf78ehMM1YZ8/](ipfs://QmVzsLbRpsi2d5BKNShX4XHtieNHiwLTMAnf78ehMM1YZ8/)
### 5.99.3 (2025-06-18) ### 5.100.1 (2025-06-25)
### Bug Fixes ### Bug Fixes
* **web:** dynamic fee hotfix prod (#21010) 6fb9aea * **web:** use SERVICE_ACCOUNT_PAT instead of GITHUB_TOKEN (#21251) b92a468
web/5.99.3 web/5.100.1
\ No newline at end of file \ No newline at end of file
...@@ -2,6 +2,10 @@ ...@@ -2,6 +2,10 @@
## Developer Quickstart ## Developer Quickstart
### Environment variables
Before running the extension, you need to get the environment variables from 1password in order to get full functionality. Run the command `yarn extension env:local:download` to copy them to your root folder.
### Running the extension locally ### Running the extension locally
To run the extension, run the following from the top level of the monorepo: To run the extension, run the following from the top level of the monorepo:
...@@ -11,11 +15,7 @@ yarn ...@@ -11,11 +15,7 @@ yarn
yarn extension start yarn extension start
``` ```
### Environment variables Then, load the extension into Chrome:
You need to get the environment variables from 1password in order to get full functionality. Run the command `yarn extension env:local:download` to copy them to your root folder.
### Loading the extension into Chrome
1. Go to **chrome://extensions** 1. Go to **chrome://extensions**
2. At the top right, turn on **Developer mode** 2. At the top right, turn on **Developer mode**
......
...@@ -38,7 +38,7 @@ ...@@ -38,7 +38,7 @@
"react-native-web": "0.19.13", "react-native-web": "0.19.13",
"react-qr-code": "2.0.12", "react-qr-code": "2.0.12",
"react-redux": "8.0.5", "react-redux": "8.0.5",
"react-router-dom": "6.10.0", "react-router-dom": "6.30.1",
"redux": "4.2.1", "redux": "4.2.1",
"redux-logger": "3.0.6", "redux-logger": "3.0.6",
"redux-persist": "6.0.0", "redux-persist": "6.0.0",
......
import { useQuery } from '@tanstack/react-query'
import { forwardRef } from 'react' import { forwardRef } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { TextInput } from 'react-native' import { TextInput } from 'react-native'
import { Input, InputProps } from 'src/app/components/Input' import { Input, InputProps } from 'src/app/components/Input'
import { biometricUnlockCredentialQuery } from 'src/app/features/biometricUnlock/biometricUnlockCredentialQuery' import { useShouldShowBiometricUnlock } from 'src/app/features/biometricUnlock/useShouldShowBiometricUnlock'
import { Flex, FlexProps, IconProps, Text, TouchableArea } from 'ui/src' import { Flex, FlexProps, IconProps, Text, TouchableArea } from 'ui/src'
import { Eye, EyeOff, Fingerprint } from 'ui/src/components/icons' import { Eye, EyeOff, Fingerprint } from 'ui/src/components/icons'
import { PasswordStrength, getPasswordStrengthTextAndColor } from 'wallet/src/utils/password' import { PasswordStrength, getPasswordStrengthTextAndColor } from 'wallet/src/utils/password'
...@@ -57,8 +56,7 @@ export const PasswordInputWithBiometrics = forwardRef< ...@@ -57,8 +56,7 @@ export const PasswordInputWithBiometrics = forwardRef<
TextInput, TextInput,
PasswordInputProps & { onPressBiometricUnlock: () => void } PasswordInputProps & { onPressBiometricUnlock: () => void }
>(function PasswordInputWithBiometrics({ onPressBiometricUnlock, ...passwordInputProps }, ref): JSX.Element { >(function PasswordInputWithBiometrics({ onPressBiometricUnlock, ...passwordInputProps }, ref): JSX.Element {
const { data: biometricUnlockCredential } = useQuery(biometricUnlockCredentialQuery()) const shouldShowBiometricUnlock = useShouldShowBiometricUnlock()
const hasBiometricUnlockCredential = !!biometricUnlockCredential
return ( return (
<Flex row alignItems="center"> <Flex row alignItems="center">
...@@ -66,14 +64,14 @@ export const PasswordInputWithBiometrics = forwardRef< ...@@ -66,14 +64,14 @@ export const PasswordInputWithBiometrics = forwardRef<
<PasswordInput <PasswordInput
ref={ref} ref={ref}
{...passwordInputProps} {...passwordInputProps}
{...(hasBiometricUnlockCredential && { {...(shouldShowBiometricUnlock && {
borderTopRightRadius: 0, borderTopRightRadius: 0,
borderBottomRightRadius: 0, borderBottomRightRadius: 0,
})} })}
/> />
</Flex> </Flex>
{hasBiometricUnlockCredential && ( {shouldShowBiometricUnlock && (
<TouchableArea <TouchableArea
height="100%" height="100%"
justifyContent="center" justifyContent="center"
......
...@@ -35,6 +35,7 @@ import { OTPInput } from 'src/app/features/onboarding/scan/OTPInput' ...@@ -35,6 +35,7 @@ import { OTPInput } from 'src/app/features/onboarding/scan/OTPInput'
import { ScanToOnboard } from 'src/app/features/onboarding/scan/ScanToOnboard' import { ScanToOnboard } from 'src/app/features/onboarding/scan/ScanToOnboard'
import { ScantasticContextProvider } from 'src/app/features/onboarding/scan/ScantasticContextProvider' import { ScantasticContextProvider } from 'src/app/features/onboarding/scan/ScantasticContextProvider'
import { OnboardingRoutes, TopLevelRoutes } from 'src/app/navigation/constants' import { OnboardingRoutes, TopLevelRoutes } from 'src/app/navigation/constants'
import { ROUTER_FUTURE_FLAGS, ROUTER_PROVIDER_FUTURE_FLAGS } from 'src/app/navigation/routerConfig'
import { setRouter, setRouterState } from 'src/app/navigation/state' import { setRouter, setRouterState } from 'src/app/navigation/state'
import { initExtensionAnalytics } from 'src/app/utils/analytics' import { initExtensionAnalytics } from 'src/app/utils/analytics'
import { checksIfSupportsSidePanel } from 'src/app/utils/chrome' import { checksIfSupportsSidePanel } from 'src/app/utils/chrome'
...@@ -139,14 +140,19 @@ const allRoutes = [ ...@@ -139,14 +140,19 @@ const allRoutes = [
}, },
] ]
const router = createHashRouter([ const router = createHashRouter(
[
{ {
path: `/${TopLevelRoutes.Onboarding}`, path: `/${TopLevelRoutes.Onboarding}`,
element: <OnboardingWrapper />, element: <OnboardingWrapper />,
errorElement: <ErrorElement />, errorElement: <ErrorElement />,
children: !supportsSidePanel ? [unsupportedRoute] : allRoutes, children: !supportsSidePanel ? [unsupportedRoute] : allRoutes,
}, },
]) ],
{
future: ROUTER_FUTURE_FLAGS,
},
)
function ScantasticFlow({ isResetting = false }: { isResetting?: boolean }): JSX.Element { function ScantasticFlow({ isResetting = false }: { isResetting?: boolean }): JSX.Element {
return ( return (
...@@ -193,7 +199,7 @@ export default function OnboardingApp(): JSX.Element { ...@@ -193,7 +199,7 @@ export default function OnboardingApp(): JSX.Element {
<PersistGate persistor={getReduxPersistor()}> <PersistGate persistor={getReduxPersistor()}>
<BaseAppContainer appName={DatadogAppNameTag.Onboarding}> <BaseAppContainer appName={DatadogAppNameTag.Onboarding}>
<PrimaryAppInstanceDebuggerLazy /> <PrimaryAppInstanceDebuggerLazy />
<RouterProvider router={router} /> <RouterProvider router={router} future={ROUTER_PROVIDER_FUTURE_FLAGS} />
</BaseAppContainer> </BaseAppContainer>
</PersistGate> </PersistGate>
) )
......
...@@ -7,6 +7,7 @@ import { RouterProvider, createHashRouter } from 'react-router-dom' ...@@ -7,6 +7,7 @@ import { RouterProvider, createHashRouter } from 'react-router-dom'
import { ErrorElement } from 'src/app/components/ErrorElement' import { ErrorElement } from 'src/app/components/ErrorElement'
import { BaseAppContainer } from 'src/app/core/BaseAppContainer' import { BaseAppContainer } from 'src/app/core/BaseAppContainer'
import { DatadogAppNameTag } from 'src/app/datadog' import { DatadogAppNameTag } from 'src/app/datadog'
import { ROUTER_FUTURE_FLAGS, ROUTER_PROVIDER_FUTURE_FLAGS } from 'src/app/navigation/routerConfig'
import { initExtensionAnalytics } from 'src/app/utils/analytics' import { initExtensionAnalytics } from 'src/app/utils/analytics'
import { Button, Flex, Image, Text } from 'ui/src' import { Button, Flex, Image, Text } from 'ui/src'
import { UNISWAP_LOGO } from 'ui/src/assets' import { UNISWAP_LOGO } from 'ui/src/assets'
...@@ -17,13 +18,18 @@ import { ElementName } from 'uniswap/src/features/telemetry/constants' ...@@ -17,13 +18,18 @@ import { ElementName } from 'uniswap/src/features/telemetry/constants'
import { ExtensionScreens } from 'uniswap/src/types/screens/extension' import { ExtensionScreens } from 'uniswap/src/types/screens/extension'
import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testnetMode/hooks/useTestnetModeForLoggingAndAnalytics' import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testnetMode/hooks/useTestnetModeForLoggingAndAnalytics'
const router = createHashRouter([ const router = createHashRouter(
[
{ {
path: '', path: '',
element: <PopupContent />, element: <PopupContent />,
errorElement: <ErrorElement />, errorElement: <ErrorElement />,
}, },
]) ],
{
future: ROUTER_FUTURE_FLAGS,
},
)
function PopupContent(): JSX.Element { function PopupContent(): JSX.Element {
const { t } = useTranslation() const { t } = useTranslation()
...@@ -102,7 +108,7 @@ export default function PopupApp(): JSX.Element { ...@@ -102,7 +108,7 @@ export default function PopupApp(): JSX.Element {
return ( return (
<BaseAppContainer appName={DatadogAppNameTag.Popup}> <BaseAppContainer appName={DatadogAppNameTag.Popup}>
<RouterProvider router={router} /> <RouterProvider router={router} future={ROUTER_PROVIDER_FUTURE_FLAGS} />
</BaseAppContainer> </BaseAppContainer>
) )
} }
...@@ -29,6 +29,7 @@ import { SwapFlowScreen } from 'src/app/features/swap/SwapFlowScreen' ...@@ -29,6 +29,7 @@ import { SwapFlowScreen } from 'src/app/features/swap/SwapFlowScreen'
import { useIsWalletUnlocked } from 'src/app/hooks/useIsWalletUnlocked' import { useIsWalletUnlocked } from 'src/app/hooks/useIsWalletUnlocked'
import { AppRoutes, RemoveRecoveryPhraseRoutes, SettingsRoutes } from 'src/app/navigation/constants' import { AppRoutes, RemoveRecoveryPhraseRoutes, SettingsRoutes } from 'src/app/navigation/constants'
import { MainContent, WebNavigation } from 'src/app/navigation/navigation' import { MainContent, WebNavigation } from 'src/app/navigation/navigation'
import { ROUTER_FUTURE_FLAGS, ROUTER_PROVIDER_FUTURE_FLAGS } from 'src/app/navigation/routerConfig'
import { setRouter, setRouterState } from 'src/app/navigation/state' import { setRouter, setRouterState } from 'src/app/navigation/state'
import { initExtensionAnalytics } from 'src/app/utils/analytics' import { initExtensionAnalytics } from 'src/app/utils/analytics'
import { import {
...@@ -48,7 +49,8 @@ import { useInterval } from 'utilities/src/time/timing' ...@@ -48,7 +49,8 @@ import { useInterval } from 'utilities/src/time/timing'
import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testnetMode/hooks/useTestnetModeForLoggingAndAnalytics' import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testnetMode/hooks/useTestnetModeForLoggingAndAnalytics'
import { getReduxPersistor } from 'wallet/src/state/persistor' import { getReduxPersistor } from 'wallet/src/state/persistor'
const router = createHashRouter([ const router = createHashRouter(
[
{ {
path: '', path: '',
element: <SidebarWrapper />, element: <SidebarWrapper />,
...@@ -129,7 +131,11 @@ const router = createHashRouter([ ...@@ -129,7 +131,11 @@ const router = createHashRouter([
}, },
], ],
}, },
]) ],
{
future: ROUTER_FUTURE_FLAGS,
},
)
const PORT_PING_INTERVAL = 5 * ONE_SECOND_MS const PORT_PING_INTERVAL = 5 * ONE_SECOND_MS
function useDappRequestPortListener(): void { function useDappRequestPortListener(): void {
...@@ -246,7 +252,7 @@ export default function SidebarApp(): JSX.Element { ...@@ -246,7 +252,7 @@ export default function SidebarApp(): JSX.Element {
<BaseAppContainer appName={DatadogAppNameTag.Sidebar}> <BaseAppContainer appName={DatadogAppNameTag.Sidebar}>
<DappContextProvider> <DappContextProvider>
<PrimaryAppInstanceDebuggerLazy /> <PrimaryAppInstanceDebuggerLazy />
<RouterProvider router={router} /> <RouterProvider router={router} future={ROUTER_PROVIDER_FUTURE_FLAGS} />
</DappContextProvider> </DappContextProvider>
</BaseAppContainer> </BaseAppContainer>
</PersistGate> </PersistGate>
......
...@@ -19,6 +19,7 @@ import { UnitagConfirmationScreen } from 'src/app/features/unitags/UnitagConfirm ...@@ -19,6 +19,7 @@ import { UnitagConfirmationScreen } from 'src/app/features/unitags/UnitagConfirm
import { UnitagCreateUsernameScreen } from 'src/app/features/unitags/UnitagCreateUsernameScreen' import { UnitagCreateUsernameScreen } from 'src/app/features/unitags/UnitagCreateUsernameScreen'
import { UnitagIntroScreen } from 'src/app/features/unitags/UnitagIntroScreen' import { UnitagIntroScreen } from 'src/app/features/unitags/UnitagIntroScreen'
import { UnitagClaimRoutes } from 'src/app/navigation/constants' import { UnitagClaimRoutes } from 'src/app/navigation/constants'
import { ROUTER_FUTURE_FLAGS, ROUTER_PROVIDER_FUTURE_FLAGS } from 'src/app/navigation/routerConfig'
import { setRouter, setRouterState } from 'src/app/navigation/state' import { setRouter, setRouterState } from 'src/app/navigation/state'
import { initExtensionAnalytics } from 'src/app/utils/analytics' import { initExtensionAnalytics } from 'src/app/utils/analytics'
import { Flex } from 'ui/src' import { Flex } from 'ui/src'
...@@ -27,7 +28,8 @@ import { usePrevious } from 'utilities/src/react/hooks' ...@@ -27,7 +28,8 @@ import { usePrevious } from 'utilities/src/react/hooks'
import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testnetMode/hooks/useTestnetModeForLoggingAndAnalytics' import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testnetMode/hooks/useTestnetModeForLoggingAndAnalytics'
import { useAccountAddressFromUrlWithThrow } from 'wallet/src/features/wallet/hooks' import { useAccountAddressFromUrlWithThrow } from 'wallet/src/features/wallet/hooks'
const router = createHashRouter([ const router = createHashRouter(
[
{ {
path: '', path: '',
element: <UnitagAppInner />, element: <UnitagAppInner />,
...@@ -44,14 +46,18 @@ const router = createHashRouter([ ...@@ -44,14 +46,18 @@ const router = createHashRouter([
}, },
], ],
}, },
]) ],
{
future: ROUTER_FUTURE_FLAGS,
},
)
/** /**
* Note: we are using a pattern here to avoid circular dependencies, because * Note: we are using a pattern here to avoid circular dependencies, because
* this is the root of the app and it imports all sub-pages, we need to push the * this is the root of the app and it imports all sub-pages, we need to push the
* router/router state to a different file so it can be imported by those pages * router/router state to a different file so it can be imported by those pages
*/ */
router.subscribe((state) => { router.subscribe((state: any) => {
setRouterState(state) setRouterState(state)
}) })
...@@ -75,7 +81,7 @@ function UnitagAppInner(): JSX.Element { ...@@ -75,7 +81,7 @@ function UnitagAppInner(): JSX.Element {
// needed to reload on address param change for hash router // needed to reload on address param change for hash router
router router
.navigate(0) .navigate(0)
.catch((e) => logger.error(e, { tags: { file: 'UnitagClaimApp.tsx', function: 'UnitagClaimAppInner' } })) .catch((e: any) => logger.error(e, { tags: { file: 'UnitagClaimApp.tsx', function: 'UnitagClaimAppInner' } }))
} }
}, [address, prevAddress]) }, [address, prevAddress])
...@@ -138,7 +144,7 @@ export default function UnitagClaimApp(): JSX.Element { ...@@ -138,7 +144,7 @@ export default function UnitagClaimApp(): JSX.Element {
return ( return (
<BaseAppContainer appName={DatadogAppNameTag.UnitagClaim}> <BaseAppContainer appName={DatadogAppNameTag.UnitagClaim}>
<RouterProvider router={router} /> <RouterProvider router={router} future={ROUTER_PROVIDER_FUTURE_FLAGS} />
</BaseAppContainer> </BaseAppContainer>
) )
} }
...@@ -115,7 +115,7 @@ export function AccountItem({ address, onAccountSelect, balanceUSD }: AccountIte ...@@ -115,7 +115,7 @@ export function AccountItem({ address, onAccountSelect, balanceUSD }: AccountIte
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
navigateTo(`${AppRoutes.Settings}/${SettingsRoutes.ManageConnections}`) navigateTo(`/${AppRoutes.Settings}/${SettingsRoutes.ManageConnections}`)
}, },
Icon: Globe, Icon: Globe,
}, },
......
...@@ -178,7 +178,7 @@ export function AccountSwitcherScreen(): JSX.Element { ...@@ -178,7 +178,7 @@ export function AccountSwitcherScreen(): JSX.Element {
{ {
label: t('account.wallet.menu.manageConnections'), label: t('account.wallet.menu.manageConnections'),
onPress: () => navigateTo(`${AppRoutes.Settings}/${SettingsRoutes.ManageConnections}`), onPress: () => navigateTo(`/${AppRoutes.Settings}/${SettingsRoutes.ManageConnections}`),
Icon: Globe, Icon: Globe,
}, },
{ {
......
import { useQuery } from '@tanstack/react-query'
import { biometricUnlockCredentialQuery } from 'src/app/features/biometricUnlock/biometricUnlockCredentialQuery'
import { DynamicConfigs, ExtensionBiometricUnlockConfigKey } from 'uniswap/src/features/gating/configs'
import { useDynamicConfigValue } from 'uniswap/src/features/gating/hooks'
export function useShouldShowBiometricUnlock(): boolean {
const isEnabled = useDynamicConfigValue({
config: DynamicConfigs.ExtensionBiometricUnlock,
key: ExtensionBiometricUnlockConfigKey.EnableUnlocking,
defaultValue: true,
})
const hasBiometricUnlockCredential = useHasBiometricUnlockCredential()
return isEnabled && hasBiometricUnlockCredential
}
export function useHasBiometricUnlockCredential(): boolean {
const { data: biometricUnlockCredential } = useQuery(biometricUnlockCredentialQuery())
return !!biometricUnlockCredential
}
import { useQuery } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
import { builtInBiometricCapabilitiesQuery } from 'src/app/utils/device/builtInBiometricCapabilitiesQuery'
import { DynamicConfigs, ExtensionBiometricUnlockConfigKey } from 'uniswap/src/features/gating/configs'
import { useDynamicConfigValue } from 'uniswap/src/features/gating/hooks'
export function useShouldShowBiometricUnlockEnrollment({ flow }: { flow: 'onboarding' | 'settings' }): boolean {
const { t } = useTranslation()
const isEnabled = useDynamicConfigValue({
config: DynamicConfigs.ExtensionBiometricUnlock,
key:
flow === 'onboarding'
? ExtensionBiometricUnlockConfigKey.EnableOnboardingEnrollment
: ExtensionBiometricUnlockConfigKey.EnableSettingsEnrollment,
defaultValue: false,
})
const { data: biometricCapabilities } = useQuery(builtInBiometricCapabilitiesQuery({ t }))
const shouldShowBiometricUnlockEnrollment = isEnabled && Boolean(biometricCapabilities?.hasBuiltInBiometricSensor)
return shouldShowBiometricUnlockEnrollment
}
...@@ -17,7 +17,7 @@ export function HomeIntroCardStack(): JSX.Element | null { ...@@ -17,7 +17,7 @@ export function HomeIntroCardStack(): JSX.Element | null {
}, [activeAccount.address]) }, [activeAccount.address])
const navigateToBackupFlow = useCallback((): void => { const navigateToBackupFlow = useCallback((): void => {
navigateTo(`${AppRoutes.Settings}/${SettingsRoutes.BackupRecoveryPhrase}`) navigateTo(`/${AppRoutes.Settings}/${SettingsRoutes.BackupRecoveryPhrase}`)
}, [navigateTo]) }, [navigateTo])
const { cards } = useSharedIntroCards({ const { cards } = useSharedIntroCards({
......
import { useQuery } from '@tanstack/react-query'
import { useState } from 'react' import { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { PADDING_STRENGTH_INDICATOR, PasswordInput } from 'src/app/components/PasswordInput' import { PADDING_STRENGTH_INDICATOR, PasswordInput } from 'src/app/components/PasswordInput'
import { useShouldShowBiometricUnlockEnrollment } from 'src/app/features/biometricUnlock/useShouldShowBiometricUnlockEnrollment'
import { BiometricUnlockSetUp } from 'src/app/features/onboarding/BiometricUnlockSetUp' import { BiometricUnlockSetUp } from 'src/app/features/onboarding/BiometricUnlockSetUp'
import { OnboardingScreen } from 'src/app/features/onboarding/OnboardingScreen' import { OnboardingScreen } from 'src/app/features/onboarding/OnboardingScreen'
import { useOnboardingSteps } from 'src/app/features/onboarding/OnboardingSteps' import { useOnboardingSteps } from 'src/app/features/onboarding/OnboardingSteps'
import { TopLevelRoutes } from 'src/app/navigation/constants' import { TopLevelRoutes } from 'src/app/navigation/constants'
import { navigate } from 'src/app/navigation/state' import { navigate } from 'src/app/navigation/state'
import { builtInBiometricCapabilitiesQuery } from 'src/app/utils/device/builtInBiometricCapabilitiesQuery'
import { Flex, Square, Text } from 'ui/src' import { Flex, Square, Text } from 'ui/src'
import { Lock } from 'ui/src/components/icons' import { Lock } from 'ui/src/components/icons'
import { iconSizes } from 'ui/src/theme' import { iconSizes } from 'ui/src/theme'
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 Trace from 'uniswap/src/features/telemetry/Trace'
import { ExtensionOnboardingFlow, ExtensionOnboardingScreens } from 'uniswap/src/types/screens/extension' import { ExtensionOnboardingFlow, ExtensionOnboardingScreens } from 'uniswap/src/types/screens/extension'
import { useEvent } from 'utilities/src/react/hooks' import { useEvent } from 'utilities/src/react/hooks'
...@@ -28,17 +25,14 @@ export function Password({ ...@@ -28,17 +25,14 @@ export function Password({
onComplete: (password: string) => Promise<void> onComplete: (password: string) => Promise<void>
onBack?: () => void onBack?: () => void
}): JSX.Element { }): JSX.Element {
const { t } = useTranslation()
const { resetOnboardingContextData } = useOnboardingContext() const { resetOnboardingContextData } = useOnboardingContext()
const [password, setPassword] = useState<null | string>(null) const [password, setPassword] = useState<null | string>(null)
const isBiometricUnlockEnabled = useFeatureFlag(FeatureFlags.ExtensionBiometricUnlock) const shouldShowBiometricUnlockEnrollment = useShouldShowBiometricUnlockEnrollment({ flow: 'onboarding' })
const { data: biometricCapabilities } = useQuery(builtInBiometricCapabilitiesQuery({ t }))
const shouldShowBiometricUnlockSetUp = isBiometricUnlockEnabled && biometricCapabilities?.hasBuiltInBiometricSensor
const onPasswordNext = useEvent(async (password: string) => { const onPasswordNext = useEvent(async (password: string) => {
if (shouldShowBiometricUnlockSetUp) { if (shouldShowBiometricUnlockEnrollment) {
setPassword(password) setPassword(password)
} else { } else {
await onComplete(password) await onComplete(password)
......
...@@ -21,7 +21,6 @@ import WalletPreviewCard from 'wallet/src/components/WalletPreviewCard/WalletPre ...@@ -21,7 +21,6 @@ import WalletPreviewCard from 'wallet/src/components/WalletPreviewCard/WalletPre
import { useOnboardingContext } from 'wallet/src/features/onboarding/OnboardingContext' import { useOnboardingContext } from 'wallet/src/features/onboarding/OnboardingContext'
import { useImportableAccounts } from 'wallet/src/features/onboarding/hooks/useImportableAccounts' import { useImportableAccounts } from 'wallet/src/features/onboarding/hooks/useImportableAccounts'
import { useSelectAccounts } from 'wallet/src/features/onboarding/hooks/useSelectAccounts' import { useSelectAccounts } from 'wallet/src/features/onboarding/hooks/useSelectAccounts'
import { useAnyAccountEligibleForDelegation } from 'wallet/src/features/smartWallet/hooks/useAnyAccountEligibleForDelegation'
import { BackupType } from 'wallet/src/features/wallet/accounts/types' import { BackupType } from 'wallet/src/features/wallet/accounts/types'
export function SelectWallets({ flow }: { flow: ExtensionOnboardingFlow }): JSX.Element { export function SelectWallets({ flow }: { flow: ExtensionOnboardingFlow }): JSX.Element {
...@@ -37,15 +36,11 @@ export function SelectWallets({ flow }: { flow: ExtensionOnboardingFlow }): JSX. ...@@ -37,15 +36,11 @@ export function SelectWallets({ flow }: { flow: ExtensionOnboardingFlow }): JSX.
const { importableAccounts, isLoading, showError, refetch } = useImportableAccounts(generatedAddresses) const { importableAccounts, isLoading, showError, refetch } = useImportableAccounts(generatedAddresses)
const { eligible: isAnyAccountEligibleForDelegation, loading: isDelegationChecksLoading } =
useAnyAccountEligibleForDelegation(importableAccounts)
const { selectedAddresses, toggleAddressSelection } = useSelectAccounts(importableAccounts) const { selectedAddresses, toggleAddressSelection } = useSelectAccounts(importableAccounts)
const smartWalletEnabled = useFeatureFlag(FeatureFlags.SmartWallet) const smartWalletEnabled = useFeatureFlag(FeatureFlags.SmartWallet)
const enableSubmit = const enableSubmit = showError || (selectedAddresses.length > 0 && !isLoading)
(showError || (selectedAddresses.length > 0 && !isLoading)) && !(isDelegationChecksLoading && smartWalletEnabled)
const onSubmit = useEvent(async () => { const onSubmit = useEvent(async () => {
if (!enableSubmit) { if (!enableSubmit) {
...@@ -67,8 +62,8 @@ export function SelectWallets({ flow }: { flow: ExtensionOnboardingFlow }): JSX. ...@@ -67,8 +62,8 @@ export function SelectWallets({ flow }: { flow: ExtensionOnboardingFlow }): JSX.
useSubmitOnEnter(showError ? refetch : onSubmit) useSubmitOnEnter(showError ? refetch : onSubmit)
const belowFrameContent = useMemo( const belowFrameContent = useMemo(
() => (smartWalletEnabled && isAnyAccountEligibleForDelegation ? <SmartWalletTooltip /> : undefined), () => (smartWalletEnabled ? <SmartWalletTooltip /> : undefined),
[smartWalletEnabled, isAnyAccountEligibleForDelegation], [smartWalletEnabled],
) )
return ( return (
...@@ -101,7 +96,7 @@ export function SelectWallets({ flow }: { flow: ExtensionOnboardingFlow }): JSX. ...@@ -101,7 +96,7 @@ export function SelectWallets({ flow }: { flow: ExtensionOnboardingFlow }): JSX.
<Text color="$statusCritical" textAlign="center" variant="buttonLabel2"> <Text color="$statusCritical" textAlign="center" variant="buttonLabel2">
{t('onboarding.selectWallets.error')} {t('onboarding.selectWallets.error')}
</Text> </Text>
) : isDelegationChecksLoading ? ( ) : isLoading ? (
<Flex> <Flex>
<SelectWalletsSkeleton repeat={3} /> <SelectWalletsSkeleton repeat={3} />
</Flex> </Flex>
......
...@@ -68,7 +68,7 @@ export function ConnectPopupContent({ ...@@ -68,7 +68,7 @@ export function ConnectPopupContent({
} }
const openManageConnections = (): void => { const openManageConnections = (): void => {
navigateTo(`${AppRoutes.Settings}/${SettingsRoutes.ManageConnections}`) navigateTo(`/${AppRoutes.Settings}/${SettingsRoutes.ManageConnections}`)
} }
const fallbackIcon = <DappIconPlaceholder iconSize={iconSizes.icon40} name={dappUrl} /> const fallbackIcon = <DappIconPlaceholder iconSize={iconSizes.icon40} name={dappUrl} />
......
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { biometricUnlockCredentialQuery } from 'src/app/features/biometricUnlock/biometricUnlockCredentialQuery'
import { useBiometricUnlockDisableMutation } from 'src/app/features/biometricUnlock/useBiometricUnlockDisableMutation' import { useBiometricUnlockDisableMutation } from 'src/app/features/biometricUnlock/useBiometricUnlockDisableMutation'
import { useBiometricUnlockSetupMutation } from 'src/app/features/biometricUnlock/useBiometricUnlockSetupMutation' import { useBiometricUnlockSetupMutation } from 'src/app/features/biometricUnlock/useBiometricUnlockSetupMutation'
import { useHasBiometricUnlockCredential } from 'src/app/features/biometricUnlock/useShouldShowBiometricUnlock'
import { useShouldShowBiometricUnlockEnrollment } from 'src/app/features/biometricUnlock/useShouldShowBiometricUnlockEnrollment'
import { SettingsToggleRow } from 'src/app/features/settings/components/SettingsToggleRow' import { SettingsToggleRow } from 'src/app/features/settings/components/SettingsToggleRow'
import { EnterPasswordModal } from 'src/app/features/settings/password/EnterPasswordModal' import { EnterPasswordModal } from 'src/app/features/settings/password/EnterPasswordModal'
import { builtInBiometricCapabilitiesQuery } from 'src/app/utils/device/builtInBiometricCapabilitiesQuery' import { builtInBiometricCapabilitiesQuery } from 'src/app/utils/device/builtInBiometricCapabilitiesQuery'
import { Fingerprint } from 'ui/src/components/icons'
import { useEvent } from 'utilities/src/react/hooks' import { useEvent } from 'utilities/src/react/hooks'
import { useBooleanState } from 'utilities/src/react/useBooleanState' import { useBooleanState } from 'utilities/src/react/useBooleanState'
...@@ -13,44 +15,49 @@ export function BiometricUnlockSettingsToggleRow(): JSX.Element | null { ...@@ -13,44 +15,49 @@ export function BiometricUnlockSettingsToggleRow(): JSX.Element | null {
const { t } = useTranslation() const { t } = useTranslation()
const { value: isPasswordModalOpen, setTrue: showPasswordModal, setFalse: hidePasswordModal } = useBooleanState(false) const { value: isPasswordModalOpen, setTrue: showPasswordModal, setFalse: hidePasswordModal } = useBooleanState(false)
const { data: biometricUnlockCredential } = useQuery(biometricUnlockCredentialQuery()) const hasBiometricUnlockCredential = useHasBiometricUnlockCredential()
const showBiometricUnlockEnrollment = useShouldShowBiometricUnlockEnrollment({ flow: 'settings' })
// We want to show the toggle when the user has a credential even if enrollment is not available,
// so that they can remove their passkey if they want to.
const showBiometricUnlockToggle = hasBiometricUnlockCredential || showBiometricUnlockEnrollment
const { data: biometricCapabilities } = useQuery(builtInBiometricCapabilitiesQuery({ t })) const { data: biometricCapabilities } = useQuery(builtInBiometricCapabilitiesQuery({ t }))
const { mutate: setupBiometricUnlock } = useBiometricUnlockSetupMutation() const { mutate: setupBiometricUnlock } = useBiometricUnlockSetupMutation()
const { mutate: disableBiometricUnlock } = useBiometricUnlockDisableMutation() const { mutate: disableBiometricUnlock } = useBiometricUnlockDisableMutation()
const hasBiometricUnlockCredential = !!biometricUnlockCredential const onPasswordModalNext = useEvent((password?: string): void => {
hidePasswordModal()
if (!password) {
return
}
const handleToggleChange = useEvent(() => {
if (hasBiometricUnlockCredential) { if (hasBiometricUnlockCredential) {
disableBiometricUnlock() disableBiometricUnlock()
} else { } else {
showPasswordModal() setupBiometricUnlock(password)
} }
}) })
if (!biometricCapabilities?.hasBuiltInBiometricSensor) { if (!showBiometricUnlockToggle) {
return null return null
} }
return ( return (
<> <>
<SettingsToggleRow <SettingsToggleRow
Icon={biometricCapabilities.icon} Icon={biometricCapabilities?.icon ?? Fingerprint}
title={biometricCapabilities.name} title={biometricCapabilities?.name ?? t('common.biometrics.generic')}
checked={hasBiometricUnlockCredential} checked={hasBiometricUnlockCredential}
onCheckedChange={handleToggleChange} onCheckedChange={showPasswordModal}
/> />
{isPasswordModalOpen && ( {isPasswordModalOpen && (
<EnterPasswordModal <EnterPasswordModal
isOpen={true} isOpen={true}
onNext={(password): void => { onNext={onPasswordModalNext}
hidePasswordModal()
if (password) {
setupBiometricUnlock(password)
}
}}
onClose={hidePasswordModal} onClose={hidePasswordModal}
shouldReturnPassword shouldReturnPassword
/> />
......
import { useQuery } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { ScreenHeader } from 'src/app/components/layout/ScreenHeader' import { ScreenHeader } from 'src/app/components/layout/ScreenHeader'
import { BiometricUnlockSettingsToggleRow } from 'src/app/features/settings/BiometricUnlock/BiometricUnlockSettingsToggleRow' import { BiometricUnlockSettingsToggleRow } from 'src/app/features/settings/BiometricUnlock/BiometricUnlockSettingsToggleRow'
import { SettingsItem } from 'src/app/features/settings/components/SettingsItem' import { SettingsItem } from 'src/app/features/settings/components/SettingsItem'
import { AppRoutes, SettingsRoutes } from 'src/app/navigation/constants' import { AppRoutes, SettingsRoutes } from 'src/app/navigation/constants'
import { useExtensionNavigation } from 'src/app/navigation/utils' import { useExtensionNavigation } from 'src/app/navigation/utils'
import { builtInBiometricCapabilitiesQuery } from 'src/app/utils/device/builtInBiometricCapabilitiesQuery'
import { Flex, ScrollView } from 'ui/src' import { Flex, ScrollView } from 'ui/src'
import { Key } from 'ui/src/components/icons/Key' import { Key } from 'ui/src/components/icons/Key'
...@@ -11,9 +13,11 @@ export function DeviceAccessScreen(): JSX.Element { ...@@ -11,9 +13,11 @@ export function DeviceAccessScreen(): JSX.Element {
const { t } = useTranslation() const { t } = useTranslation()
const { navigateTo } = useExtensionNavigation() const { navigateTo } = useExtensionNavigation()
const title = useDeviceAccessScreenTitle()
return ( return (
<Flex fill backgroundColor="$surface1" gap="$spacing8"> <Flex fill backgroundColor="$surface1" gap="$spacing8">
<ScreenHeader title={t('settings.setting.deviceAccess.title')} /> <ScreenHeader title={title} />
<ScrollView showsVerticalScrollIndicator={false}> <ScrollView showsVerticalScrollIndicator={false}>
<BiometricUnlockSettingsToggleRow /> <BiometricUnlockSettingsToggleRow />
...@@ -21,9 +25,18 @@ export function DeviceAccessScreen(): JSX.Element { ...@@ -21,9 +25,18 @@ export function DeviceAccessScreen(): JSX.Element {
<SettingsItem <SettingsItem
Icon={Key} Icon={Key}
title={t('settings.setting.password.title')} title={t('settings.setting.password.title')}
onPress={(): void => navigateTo(`${AppRoutes.Settings}/${SettingsRoutes.ChangePassword}`)} onPress={(): void => navigateTo(`/${AppRoutes.Settings}/${SettingsRoutes.ChangePassword}`)}
/> />
</ScrollView> </ScrollView>
</Flex> </Flex>
) )
} }
export function useDeviceAccessScreenTitle(): string {
const { t } = useTranslation()
const { data: biometricCapabilities } = useQuery(builtInBiometricCapabilitiesQuery({ t }))
return biometricCapabilities?.os === 'mac'
? t('settings.setting.deviceAccess.title.touchId')
: t('settings.setting.deviceAccess.title.biometrics')
}
...@@ -32,7 +32,7 @@ export function RemoveRecoveryPhraseWallets(): JSX.Element { ...@@ -32,7 +32,7 @@ export function RemoveRecoveryPhraseWallets(): JSX.Element {
title={t('setting.recoveryPhrase.remove.initial.title')} title={t('setting.recoveryPhrase.remove.initial.title')}
onNextPressed={(): void => { onNextPressed={(): void => {
navigateTo( navigateTo(
`${AppRoutes.Settings}/${SettingsRoutes.RemoveRecoveryPhrase}/${RemoveRecoveryPhraseRoutes.Verify}`, `/${AppRoutes.Settings}/${SettingsRoutes.RemoveRecoveryPhrase}/${RemoveRecoveryPhraseRoutes.Verify}`,
) )
}} }}
> >
......
...@@ -2,6 +2,9 @@ import { useCallback, useEffect, useState } from 'react' ...@@ -2,6 +2,9 @@ import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import { ScreenHeader } from 'src/app/components/layout/ScreenHeader' import { ScreenHeader } from 'src/app/components/layout/ScreenHeader'
import { useShouldShowBiometricUnlock } from 'src/app/features/biometricUnlock/useShouldShowBiometricUnlock'
import { useShouldShowBiometricUnlockEnrollment } from 'src/app/features/biometricUnlock/useShouldShowBiometricUnlockEnrollment'
import { useDeviceAccessScreenTitle } from 'src/app/features/settings/DeviceAccessScreen'
import { SettingsItemWithDropdown } from 'src/app/features/settings/SettingsItemWithDropdown' import { SettingsItemWithDropdown } from 'src/app/features/settings/SettingsItemWithDropdown'
import ThemeToggle from 'src/app/features/settings/ThemeToggle' import ThemeToggle from 'src/app/features/settings/ThemeToggle'
import { SettingsItem } from 'src/app/features/settings/components/SettingsItem' import { SettingsItem } from 'src/app/features/settings/components/SettingsItem'
...@@ -69,7 +72,12 @@ export function SettingsScreen(): JSX.Element { ...@@ -69,7 +72,12 @@ export function SettingsScreen(): JSX.Element {
const appFiatCurrencyInfo = useAppFiatCurrencyInfo() const appFiatCurrencyInfo = useAppFiatCurrencyInfo()
const hasViewedConnectionMigration = useSelector(selectHasViewedConnectionMigration) const hasViewedConnectionMigration = useSelector(selectHasViewedConnectionMigration)
const isBiometricUnlockEnabled = useFeatureFlag(FeatureFlags.ExtensionBiometricUnlock) const hasBiometricUnlockCredential = useShouldShowBiometricUnlock()
const showBiometricUnlockEnrollment = useShouldShowBiometricUnlockEnrollment({ flow: 'settings' })
const showNewDeviceAccessPage = hasBiometricUnlockCredential || showBiometricUnlockEnrollment
const deviceAccessScreenTitle = useDeviceAccessScreenTitle()
const isSmartWalletEnabled = useFeatureFlag(FeatureFlags.SmartWalletSettings) const isSmartWalletEnabled = useFeatureFlag(FeatureFlags.SmartWalletSettings)
const signerAccount = useSignerAccounts()[0] const signerAccount = useSignerAccounts()[0]
...@@ -118,7 +126,7 @@ export function SettingsScreen(): JSX.Element { ...@@ -118,7 +126,7 @@ export function SettingsScreen(): JSX.Element {
const handleAdvancedModalClose = useCallback(() => setIsAdvancedModalOpen(false), []) const handleAdvancedModalClose = useCallback(() => setIsAdvancedModalOpen(false), [])
const handleSmartWalletPress = useCallback(() => { const handleSmartWalletPress = useCallback(() => {
navigateTo(`${AppRoutes.Settings}/${SettingsRoutes.SmartWallet}`) navigateTo(`/${AppRoutes.Settings}/${SettingsRoutes.SmartWallet}`)
setIsAdvancedModalOpen(false) setIsAdvancedModalOpen(false)
}, [navigateTo]) }, [navigateTo])
...@@ -184,7 +192,7 @@ export function SettingsScreen(): JSX.Element { ...@@ -184,7 +192,7 @@ export function SettingsScreen(): JSX.Element {
<SettingsItem <SettingsItem
Icon={Settings} Icon={Settings}
title="Developer Settings" title="Developer Settings"
onPress={(): void => navigateTo(`${AppRoutes.Settings}/${SettingsRoutes.DevMenu}`)} onPress={(): void => navigateTo(`/${AppRoutes.Settings}/${SettingsRoutes.DevMenu}`)}
/> />
)} )}
</> </>
...@@ -273,23 +281,23 @@ export function SettingsScreen(): JSX.Element { ...@@ -273,23 +281,23 @@ export function SettingsScreen(): JSX.Element {
)} )}
<Flex pt="$padding16"> <Flex pt="$padding16">
<SettingsSection title={t('settings.section.privacyAndSecurity')}> <SettingsSection title={t('settings.section.privacyAndSecurity')}>
{isBiometricUnlockEnabled ? ( {showNewDeviceAccessPage ? (
<SettingsItem <SettingsItem
Icon={Lock} Icon={Lock}
title={t('settings.setting.deviceAccess.title')} title={deviceAccessScreenTitle}
onPress={(): void => navigateTo(`${AppRoutes.Settings}/${SettingsRoutes.DeviceAccess}`)} onPress={(): void => navigateTo(`/${AppRoutes.Settings}/${SettingsRoutes.DeviceAccess}`)}
/> />
) : ( ) : (
<SettingsItem <SettingsItem
Icon={Key} Icon={Key}
title={t('settings.setting.password.title')} title={t('settings.setting.password.title')}
onPress={(): void => navigateTo(`${AppRoutes.Settings}/${SettingsRoutes.ChangePassword}`)} onPress={(): void => navigateTo(`/${AppRoutes.Settings}/${SettingsRoutes.ChangePassword}`)}
/> />
)} )}
<SettingsItem <SettingsItem
Icon={FileListLock} Icon={FileListLock}
title={t('settings.setting.recoveryPhrase.title')} title={t('settings.setting.recoveryPhrase.title')}
onPress={(): void => navigateTo(`${AppRoutes.Settings}/${SettingsRoutes.ViewRecoveryPhrase}`)} onPress={(): void => navigateTo(`/${AppRoutes.Settings}/${SettingsRoutes.ViewRecoveryPhrase}`)}
/> />
<> <>
{hasPasskeyBackup && ( {hasPasskeyBackup && (
......
import { useState } from 'react' import { useEffect, useState } from 'react'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
import { useExtensionNavigation } from 'src/app/navigation/utils' import { useExtensionNavigation } from 'src/app/navigation/utils'
import { Flex } from 'ui/src' import { Flex } from 'ui/src'
...@@ -8,8 +8,10 @@ import { selectFilteredChainIds } from 'uniswap/src/features/transactions/swap/c ...@@ -8,8 +8,10 @@ import { selectFilteredChainIds } from 'uniswap/src/features/transactions/swap/c
import { useSwapPrefilledState } from 'uniswap/src/features/transactions/swap/form/hooks/useSwapPrefilledState' import { useSwapPrefilledState } from 'uniswap/src/features/transactions/swap/form/hooks/useSwapPrefilledState'
import { prepareSwapFormState } from 'uniswap/src/features/transactions/types/transactionState' import { prepareSwapFormState } from 'uniswap/src/features/transactions/types/transactionState'
import { CurrencyField } from 'uniswap/src/types/currency' import { CurrencyField } from 'uniswap/src/types/currency'
import { logger } from 'utilities/src/logger/logger'
import { WalletSwapFlow } from 'wallet/src/features/transactions/swap/WalletSwapFlow' import { WalletSwapFlow } from 'wallet/src/features/transactions/swap/WalletSwapFlow'
import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks' import { invalidateAndRefetchWalletDelegationQueries } from 'wallet/src/features/transactions/watcher/transactionFinalizationSaga'
import { useActiveAccountWithThrow, useSignerAccounts } from 'wallet/src/features/wallet/hooks'
export function SwapFlowScreen(): JSX.Element { export function SwapFlowScreen(): JSX.Element {
const { navigateBack, locationState } = useExtensionNavigation() const { navigateBack, locationState } = useExtensionNavigation()
...@@ -27,6 +29,17 @@ export function SwapFlowScreen(): JSX.Element { ...@@ -27,6 +29,17 @@ export function SwapFlowScreen(): JSX.Element {
filteredChainIdsOverride: ignorePersistedFilteredChainIds ? undefined : persistedFilteredChainIds, filteredChainIdsOverride: ignorePersistedFilteredChainIds ? undefined : persistedFilteredChainIds,
}) })
const signerMnemonicAccounts = useSignerAccounts()
const chains = useEnabledChains()
const accountAddresses = signerMnemonicAccounts.map((account) => account.address)
// Update flow start timestamp every time modal is opened for logging
useEffect(() => {
invalidateAndRefetchWalletDelegationQueries({ accountAddresses, chainIds: chains.chains }).catch((error) =>
logger.debug('SwapFlowScreen', 'useEffect', 'Failed to invalidate and refetch wallet delegation queries', error),
)
}, [accountAddresses, chains.chains])
/** Initialize the initial state once. On navigation the locationState changes causing an unwanted re-render. */ /** Initialize the initial state once. On navigation the locationState changes causing an unwanted re-render. */
const [initialTransactionState] = useState(() => locationState?.initialTransactionState ?? initialState) const [initialTransactionState] = useState(() => locationState?.initialTransactionState ?? initialState)
......
...@@ -33,7 +33,7 @@ export function StorageWarningModal({ isOnboarding }: StorageWarningModalProps): ...@@ -33,7 +33,7 @@ export function StorageWarningModal({ isOnboarding }: StorageWarningModalProps):
? undefined ? undefined
: (): void => { : (): void => {
onStorageWarningClose() onStorageWarningClose()
navigateTo(`${AppRoutes.Settings}/${SettingsRoutes.ViewRecoveryPhrase}`) navigateTo(`/${AppRoutes.Settings}/${SettingsRoutes.ViewRecoveryPhrase}`)
} }
} }
/> />
......
...@@ -24,6 +24,7 @@ import { ONE_SECOND_MS } from 'utilities/src/time/time' ...@@ -24,6 +24,7 @@ import { ONE_SECOND_MS } from 'utilities/src/time/time'
import { TransactionHistoryUpdater } from 'wallet/src/features/transactions/TransactionHistoryUpdater' import { TransactionHistoryUpdater } from 'wallet/src/features/transactions/TransactionHistoryUpdater'
import { WalletUniswapProvider } from 'wallet/src/features/transactions/contexts/WalletUniswapContext' import { WalletUniswapProvider } from 'wallet/src/features/transactions/contexts/WalletUniswapContext'
import { QueuedOrderModal } from 'wallet/src/features/transactions/swap/modals/QueuedOrderModal' import { QueuedOrderModal } from 'wallet/src/features/transactions/swap/modals/QueuedOrderModal'
import { NativeWalletProvider } from 'wallet/src/features/wallet/providers/NativeWalletProvider'
export function MainContent(): JSX.Element { export function MainContent(): JSX.Element {
const isOnboarded = useSelector(isOnboardedSelector) const isOnboarded = useSelector(isOnboardedSelector)
...@@ -131,12 +132,14 @@ export function WebNavigation(): JSX.Element { ...@@ -131,12 +132,14 @@ export function WebNavigation(): JSX.Element {
return ( return (
<SideBarNavigationProvider> <SideBarNavigationProvider>
<NativeWalletProvider>
<WalletUniswapProvider> <WalletUniswapProvider>
<NotificationToastWrapper /> <NotificationToastWrapper />
{shouldRestoreScroll && <ScrollRestoration />} {shouldRestoreScroll && <ScrollRestoration />}
{childrenMemo} {childrenMemo}
{isLoggedIn && <ForceUpgradeModal />} {isLoggedIn && <ForceUpgradeModal />}
</WalletUniswapProvider> </WalletUniswapProvider>
</NativeWalletProvider>
</SideBarNavigationProvider> </SideBarNavigationProvider>
) )
} }
......
// Shared future flags configuration for createHashRouter
export const ROUTER_FUTURE_FLAGS = {
v7_relativeSplatPath: true,
v7_fetcherPersist: true,
v7_normalizeFormMethod: true,
v7_partialHydration: true,
}
// Shared future flags configuration for RouterProvider
export const ROUTER_PROVIDER_FUTURE_FLAGS = {
v7_startTransition: true,
}
...@@ -11,6 +11,7 @@ type BuiltInBiometricCapabilities = { ...@@ -11,6 +11,7 @@ type BuiltInBiometricCapabilities = {
name: string name: string
icon: GeneratedIcon icon: GeneratedIcon
hasBuiltInBiometricSensor: boolean hasBuiltInBiometricSensor: boolean
os: chrome.runtime.PlatformOs
} }
export function builtInBiometricCapabilitiesQuery({ t }: { t: TFunction }) { export function builtInBiometricCapabilitiesQuery({ t }: { t: TFunction }) {
...@@ -27,6 +28,7 @@ async function getBuiltInBiometricCapabilities({ t }: { t: TFunction }): Promise ...@@ -27,6 +28,7 @@ async function getBuiltInBiometricCapabilities({ t }: { t: TFunction }): Promise
const { os } = await getChromeRuntimeWithThrow().getPlatformInfo() const { os } = await getChromeRuntimeWithThrow().getPlatformInfo()
return { return {
os,
hasBuiltInBiometricSensor: await isUserVerifyingPlatformAuthenticatorAvailable(), hasBuiltInBiometricSensor: await isUserVerifyingPlatformAuthenticatorAvailable(),
...getPlatformAuthenticatorNameAndIcon({ os, t }), ...getPlatformAuthenticatorNameAndIcon({ os, t }),
} }
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
"manifest_version": 3, "manifest_version": 3,
"name": "Uniswap Extension", "name": "Uniswap Extension",
"description": "The Uniswap Extension is a self-custody crypto wallet that's built for swapping.", "description": "The Uniswap Extension is a self-custody crypto wallet that's built for swapping.",
"version": "1.23.0", "version": "1.24.0",
"minimum_chrome_version": "116", "minimum_chrome_version": "116",
"icons": { "icons": {
"16": "assets/icon16.png", "16": "assets/icon16.png",
......
...@@ -71,9 +71,9 @@ if (isCI && datadogPropertiesAvailable) { ...@@ -71,9 +71,9 @@ if (isCI && datadogPropertiesAvailable) {
apply from: "../../../../node_modules/@datadog/mobile-react-native/datadog-sourcemaps.gradle" apply from: "../../../../node_modules/@datadog/mobile-react-native/datadog-sourcemaps.gradle"
} }
def devVersionName = "1.53" def devVersionName = "1.54"
def betaVersionName = "1.53" def betaVersionName = "1.54"
def prodVersionName = "1.53" def prodVersionName = "1.54"
android { android {
ndkVersion rootProject.ext.ndkVersion ndkVersion rootProject.ext.ndkVersion
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
"private": true, "private": true,
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",
"scripts": { "scripts": {
"android": "rnef run:android --variant=devDebug --app-id-suffix=dev", "android": "rnef run:android --variant=devDebug --app-id-suffix=dev && yarn start",
"android:release": "rnef run:android --variant=devRelease --app-id-suffix=dev", "android:release": "rnef run:android --variant=devRelease --app-id-suffix=dev",
"android:beta": "rnef run:android --variant=betaDebug --app-id-suffix=beta", "android:beta": "rnef run:android --variant=betaDebug --app-id-suffix=beta",
"android:beta:release": "rnef run:android --variant=betaRelease --app-id-suffix=beta", "android:beta:release": "rnef run:android --variant=betaRelease --app-id-suffix=beta",
...@@ -30,7 +30,7 @@ ...@@ -30,7 +30,7 @@
"firestore:deploy:rules": "firebase deploy --only firestore:rules", "firestore:deploy:rules": "firebase deploy --only firestore:rules",
"link:assets": "react-native-asset", "link:assets": "react-native-asset",
"check:circular": "../../scripts/check-circular-imports.sh ./src/app/App.tsx 0", "check:circular": "../../scripts/check-circular-imports.sh ./src/app/App.tsx 0",
"ios": "rnef run:ios --scheme Uniswap --configuration Debug", "ios": "rnef run:ios --scheme Uniswap --configuration Debug && yarn start",
"ios:smol": "rnef run:ios --device=\"iPhone SE (3rd generation)\"", "ios:smol": "rnef run:ios --device=\"iPhone SE (3rd generation)\"",
"ios:dev:release": "rnef run:ios --configuration Dev", "ios:dev:release": "rnef run:ios --configuration Dev",
"ios:beta": "rnef run:ios --configuration Beta", "ios:beta": "rnef run:ios --configuration Beta",
...@@ -39,7 +39,7 @@ ...@@ -39,7 +39,7 @@
"format": "../../scripts/prettier.sh", "format": "../../scripts/prettier.sh",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --max-warnings=0", "lint": "eslint . --ext .js,.jsx,.ts,.tsx --max-warnings=0",
"lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix", "lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
"start": "NODE_ENV=development rnef start", "start": "NODE_ENV=development rnef start --client-logs",
"start:production": "NODE_ENV=production rnef start --reset-cache", "start:production": "NODE_ENV=production rnef start --reset-cache",
"test": "node --max-old-space-size=8912 ../../node_modules/.bin/jest", "test": "node --max-old-space-size=8912 ../../node_modules/.bin/jest",
"snapshots": "jest -u", "snapshots": "jest -u",
......
#!/bin/bash #!/bin/bash
MAX_SIZE=23.33 MAX_SIZE=23.44
MAX_BUFFER=0.5 MAX_BUFFER=0.5
# Check OS type and use appropriate stat command # Check OS type and use appropriate stat command
......
...@@ -87,7 +87,8 @@ import { WalletUniswapProvider } from 'wallet/src/features/transactions/contexts ...@@ -87,7 +87,8 @@ import { WalletUniswapProvider } from 'wallet/src/features/transactions/contexts
import { Account } from 'wallet/src/features/wallet/accounts/types' import { Account } from 'wallet/src/features/wallet/accounts/types'
import { WalletContextProvider } from 'wallet/src/features/wallet/context' import { WalletContextProvider } from 'wallet/src/features/wallet/context'
import { useAccounts } from 'wallet/src/features/wallet/hooks' import { useAccounts } from 'wallet/src/features/wallet/hooks'
import { SharedWalletProvider } from 'wallet/src/providers/SharedWalletProvider' import { NativeWalletProvider } from 'wallet/src/features/wallet/providers/NativeWalletProvider'
import { SharedWalletProvider as SharedWalletReduxProvider } from 'wallet/src/providers/SharedWalletProvider'
enableFreeze(true) enableFreeze(true)
...@@ -152,14 +153,14 @@ function App(): JSX.Element | null { ...@@ -152,14 +153,14 @@ function App(): JSX.Element | null {
<StrictMode> <StrictMode>
<I18nextProvider i18n={i18n}> <I18nextProvider i18n={i18n}>
<SafeAreaProvider> <SafeAreaProvider>
<SharedWalletProvider reduxStore={store}> <SharedWalletReduxProvider reduxStore={store}>
<AnalyticsNavigationContextProvider <AnalyticsNavigationContextProvider
shouldLogScreen={shouldLogScreen} shouldLogScreen={shouldLogScreen}
useIsPartOfNavigationTree={useIsPartOfNavigationTree} useIsPartOfNavigationTree={useIsPartOfNavigationTree}
> >
<AppOuter /> <AppOuter />
</AnalyticsNavigationContextProvider> </AnalyticsNavigationContextProvider>
</SharedWalletProvider> </SharedWalletReduxProvider>
</SafeAreaProvider> </SafeAreaProvider>
</I18nextProvider> </I18nextProvider>
</StrictMode> </StrictMode>
...@@ -254,6 +255,7 @@ function AppOuter(): JSX.Element | null { ...@@ -254,6 +255,7 @@ function AppOuter(): JSX.Element | null {
<DataUpdaters /> <DataUpdaters />
<NavigationContainer> <NavigationContainer>
<MobileWalletNavigationProvider> <MobileWalletNavigationProvider>
<NativeWalletProvider>
<WalletUniswapProvider> <WalletUniswapProvider>
<BottomSheetModalProvider> <BottomSheetModalProvider>
<AppModals /> <AppModals />
...@@ -262,6 +264,7 @@ function AppOuter(): JSX.Element | null { ...@@ -262,6 +264,7 @@ function AppOuter(): JSX.Element | null {
</PerformanceProfiler> </PerformanceProfiler>
</BottomSheetModalProvider> </BottomSheetModalProvider>
</WalletUniswapProvider> </WalletUniswapProvider>
</NativeWalletProvider>
<NotificationToastWrapper /> <NotificationToastWrapper />
</MobileWalletNavigationProvider> </MobileWalletNavigationProvider>
</NavigationContainer> </NavigationContainer>
......
...@@ -7,7 +7,7 @@ import { ...@@ -7,7 +7,7 @@ import {
import { NativeStackNavigationProp, NativeStackScreenProps } from '@react-navigation/native-stack' import { NativeStackNavigationProp, NativeStackScreenProps } from '@react-navigation/native-stack'
import { TokenWarningModalState } from 'src/app/modals/TokenWarningModalState' import { TokenWarningModalState } from 'src/app/modals/TokenWarningModalState'
import { RemoveWalletModalState } from 'src/components/RemoveWallet/RemoveWalletModalState' import { RemoveWalletModalState } from 'src/components/RemoveWallet/RemoveWalletModalState'
import { RestoreWalletModalState } from 'src/components/RestoreWalletModal/RestoreWalletModalState' import { RestoreWalletModalState, WalletRestoreType } from 'src/components/RestoreWalletModal/RestoreWalletModalState'
import { ConnectionsDappsListModalState } from 'src/components/Settings/ConnectionsDappModal/ConnectionsDappsListModalState' import { ConnectionsDappsListModalState } from 'src/components/Settings/ConnectionsDappModal/ConnectionsDappsListModalState'
import { EditWalletSettingsModalState } from 'src/components/Settings/EditWalletModal/EditWalletSettingsModalState' import { EditWalletSettingsModalState } from 'src/components/Settings/EditWalletModal/EditWalletSettingsModalState'
import { ManageWalletsModalState } from 'src/components/Settings/ManageWalletsModalState' import { ManageWalletsModalState } from 'src/components/Settings/ManageWalletsModalState'
...@@ -113,6 +113,7 @@ export type SettingsStackParamList = { ...@@ -113,6 +113,7 @@ export type SettingsStackParamList = {
export type OnboardingStackBaseParams = { export type OnboardingStackBaseParams = {
importType: ImportType importType: ImportType
entryPoint: OnboardingEntryPoint entryPoint: OnboardingEntryPoint
restoreType?: WalletRestoreType
} }
export type OnboardingStackParamList = { export type OnboardingStackParamList = {
......
...@@ -11,7 +11,8 @@ import { ArrowDownCircleFilledWithBorder, WalletFilled } from 'ui/src/components ...@@ -11,7 +11,8 @@ import { ArrowDownCircleFilledWithBorder, WalletFilled } from 'ui/src/components
import { spacing } from 'ui/src/theme' import { spacing } from 'ui/src/theme'
import { GenericHeader } from 'uniswap/src/components/misc/GenericHeader' import { GenericHeader } from 'uniswap/src/components/misc/GenericHeader'
import { Modal } from 'uniswap/src/components/modals/Modal' import { Modal } from 'uniswap/src/components/modals/Modal'
import { ModalName } from 'uniswap/src/features/telemetry/constants' import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants'
import Trace from 'uniswap/src/features/telemetry/Trace'
import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { TestID } from 'uniswap/src/test/fixtures/testIDs'
import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding' import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding'
import { MobileScreens, OnboardingScreens } from 'uniswap/src/types/screens/mobile' import { MobileScreens, OnboardingScreens } from 'uniswap/src/types/screens/mobile'
...@@ -35,19 +36,21 @@ export function RestoreWalletModal({ route }: AppStackScreenProp<typeof ModalNam ...@@ -35,19 +36,21 @@ export function RestoreWalletModal({ route }: AppStackScreenProp<typeof ModalNam
const { onClose } = useReactNavigationModal() const { onClose } = useReactNavigationModal()
const restoreType = route.params.restoreType const restoreType = route.params.restoreType
const { title, description, isDismissible } = useMemo(() => { const { title, description, isDismissible, modalName } = useMemo(() => {
switch (restoreType) { switch (restoreType) {
case WalletRestoreType.SeedPhrase: case WalletRestoreType.SeedPhrase:
return { return {
title: t('account.wallet.restore.seed_phrase.title'), title: t('account.wallet.restore.seed_phrase.title'),
description: t('account.wallet.restore.seed_phrase.description'), description: t('account.wallet.restore.seed_phrase.description'),
isDismissible: true, isDismissible: true,
modalName: ModalName.RestoreWalletSeedPhrase,
} }
case WalletRestoreType.NewDevice: case WalletRestoreType.NewDevice:
return { return {
title: t('account.wallet.restore.new_device.title'), title: t('account.wallet.restore.new_device.title'),
description: t('account.wallet.restore.new_device.description'), description: t('account.wallet.restore.new_device.description'),
isDismissible: false, isDismissible: false,
modalName: ModalName.RestoreWallet,
} }
default: default:
return {} return {}
...@@ -65,6 +68,7 @@ export function RestoreWalletModal({ route }: AppStackScreenProp<typeof ModalNam ...@@ -65,6 +68,7 @@ export function RestoreWalletModal({ route }: AppStackScreenProp<typeof ModalNam
params: { params: {
entryPoint: OnboardingEntryPoint.Sidebar, entryPoint: OnboardingEntryPoint.Sidebar,
importType: ImportType.RestoreMnemonic, importType: ImportType.RestoreMnemonic,
restoreType,
}, },
}) })
break break
...@@ -75,6 +79,7 @@ export function RestoreWalletModal({ route }: AppStackScreenProp<typeof ModalNam ...@@ -75,6 +79,7 @@ export function RestoreWalletModal({ route }: AppStackScreenProp<typeof ModalNam
params: { params: {
entryPoint: OnboardingEntryPoint.Sidebar, entryPoint: OnboardingEntryPoint.Sidebar,
importType: ImportType.RestoreMnemonic, importType: ImportType.RestoreMnemonic,
restoreType,
}, },
}) })
break break
...@@ -87,7 +92,7 @@ export function RestoreWalletModal({ route }: AppStackScreenProp<typeof ModalNam ...@@ -87,7 +92,7 @@ export function RestoreWalletModal({ route }: AppStackScreenProp<typeof ModalNam
hideHandlebar hideHandlebar
backgroundColor={colors.surface1.val} backgroundColor={colors.surface1.val}
isDismissible={isDismissible} isDismissible={isDismissible}
name={ModalName.RestoreWallet} name={modalName ?? ModalName.RestoreWallet}
onClose={onClose} onClose={onClose}
> >
<Flex centered gap="$spacing24" px="$spacing24" py="$spacing12" backgroundColor="$surface1"> <Flex centered gap="$spacing24" px="$spacing24" py="$spacing12" backgroundColor="$surface1">
...@@ -127,15 +132,19 @@ export function RestoreWalletModal({ route }: AppStackScreenProp<typeof ModalNam ...@@ -127,15 +132,19 @@ export function RestoreWalletModal({ route }: AppStackScreenProp<typeof ModalNam
<GenericHeader title={title} titleVariant="subheading1" subtitle={description} subtitleVariant="body3" /> <GenericHeader title={title} titleVariant="subheading1" subtitle={description} subtitleVariant="body3" />
<Flex gap="$spacing8" width="100%"> <Flex gap="$spacing8" width="100%">
<Flex row> <Flex row>
<Trace logPress element={ElementName.Continue}>
<Button testID={TestID.Continue} variant="branded" emphasis="primary" size="medium" onPress={onRestore}> <Button testID={TestID.Continue} variant="branded" emphasis="primary" size="medium" onPress={onRestore}>
{t('common.button.continue')} {t('common.button.continue')}
</Button> </Button>
</Trace>
</Flex> </Flex>
{isDismissible && ( {isDismissible && (
<Flex row> <Flex row>
<Trace logPress element={ElementName.Cancel}>
<Button testID={TestID.Cancel} variant="default" emphasis="secondary" size="medium" onPress={onClose}> <Button testID={TestID.Cancel} variant="default" emphasis="secondary" size="medium" onPress={onClose}>
{t('common.button.notNow')} {t('common.button.notNow')}
</Button> </Button>
</Trace>
</Flex> </Flex>
)} )}
</Flex> </Flex>
......
...@@ -14,10 +14,12 @@ export const ExploreScreenSearchResultsList = memo(function _ExploreScreenSearch ...@@ -14,10 +14,12 @@ export const ExploreScreenSearchResultsList = memo(function _ExploreScreenSearch
searchQuery, searchQuery,
parsedSearchQuery, parsedSearchQuery,
chainFilter, chainFilter,
parsedChainFilter,
}: { }: {
searchQuery: string searchQuery: string
parsedSearchQuery: string | null parsedSearchQuery: string | null
chainFilter: UniverseChainId | null chainFilter: UniverseChainId | null
parsedChainFilter: UniverseChainId | null
}): JSX.Element { }): JSX.Element {
const debouncedSearchQuery = useDebounce(searchQuery) const debouncedSearchQuery = useDebounce(searchQuery)
const debouncedParsedSearchQuery = useDebounce(parsedSearchQuery) const debouncedParsedSearchQuery = useDebounce(parsedSearchQuery)
...@@ -58,6 +60,7 @@ export const ExploreScreenSearchResultsList = memo(function _ExploreScreenSearch ...@@ -58,6 +60,7 @@ export const ExploreScreenSearchResultsList = memo(function _ExploreScreenSearch
{searchQuery && searchQuery.length > 0 ? ( {searchQuery && searchQuery.length > 0 ? (
<SearchModalResultsList <SearchModalResultsList
chainFilter={chainFilter} chainFilter={chainFilter}
parsedChainFilter={parsedChainFilter}
debouncedParsedSearchFilter={debouncedParsedSearchQuery} debouncedParsedSearchFilter={debouncedParsedSearchQuery}
debouncedSearchFilter={debouncedSearchQuery} debouncedSearchFilter={debouncedSearchQuery}
searchFilter={searchQuery} searchFilter={searchQuery}
......
...@@ -46,6 +46,10 @@ export function CloudBackupProcessingAnimation({ ...@@ -46,6 +46,10 @@ export function CloudBackupProcessingAnimation({
const account = activeAccount || onboardingContextAccount const account = activeAccount || onboardingContextAccount
// Compute backup status at component level to avoid stale closure - ensures fresh evaluation on each render
// when onboardingContextAccount updates with new backup state
const accountHasCloudBackup = hasBackup(BackupType.Cloud, account)
if (!account) { if (!account) {
throw Error('No account available for backup') throw Error('No account available for backup')
} }
...@@ -56,14 +60,14 @@ export function CloudBackupProcessingAnimation({ ...@@ -56,14 +60,14 @@ export function CloudBackupProcessingAnimation({
// Handle finished backing up to Cloud // Handle finished backing up to Cloud
useEffect(() => { useEffect(() => {
if (hasBackup(BackupType.Cloud, account)) { if (accountHasCloudBackup) {
doneProcessing() doneProcessing()
// Show success state for 1s before navigating // Show success state for 1s before navigating
const timer = setTimeout(onBackupComplete, ONE_SECOND_MS) const timer = setTimeout(onBackupComplete, ONE_SECOND_MS)
return () => clearTimeout(timer) return () => clearTimeout(timer)
} }
return undefined return undefined
}, [account, onBackupComplete]) }, [accountHasCloudBackup, onBackupComplete])
// Handle backup to Cloud when screen appears // Handle backup to Cloud when screen appears
const backup = useCallback(async () => { const backup = useCallback(async () => {
......
...@@ -92,7 +92,8 @@ export function ExploreScreen(): JSX.Element { ...@@ -92,7 +92,8 @@ export function ExploreScreen(): JSX.Element {
<ExploreScreenSearchResultsList <ExploreScreenSearchResultsList
searchQuery={searchFilter ?? ''} searchQuery={searchFilter ?? ''}
parsedSearchQuery={parsedSearchFilter} parsedSearchQuery={parsedSearchFilter}
chainFilter={chainFilter ?? parsedChainFilter} chainFilter={chainFilter}
parsedChainFilter={parsedChainFilter}
/> />
) : ( ) : (
isSheetReady && canRenderList && <ExploreSections listRef={listRef} /> isSheetReady && canRenderList && <ExploreSections listRef={listRef} />
......
...@@ -40,10 +40,8 @@ const ANDROID_E2E_WORKAROUND = config.isE2ETest && isAndroid ...@@ -40,10 +40,8 @@ const ANDROID_E2E_WORKAROUND = config.isE2ETest && isAndroid
export function RestoreCloudBackupLoadingScreen({ navigation, route: { params } }: Props): JSX.Element { export function RestoreCloudBackupLoadingScreen({ navigation, route: { params } }: Props): JSX.Element {
const { t } = useTranslation() const { t } = useTranslation()
const dispatch = useDispatch() const dispatch = useDispatch()
const entryPoint = params.entryPoint
const importType = params.importType
const isRestoringMnemonic = importType === ImportType.RestoreMnemonic const isRestoringMnemonic = params.importType === ImportType.RestoreMnemonic
// inits with null before fetchCloudStorageBackups starts fetching // inits with null before fetchCloudStorageBackups starts fetching
const [isLoading, setIsLoading] = useState<boolean | null>(null) const [isLoading, setIsLoading] = useState<boolean | null>(null)
const [isError, setIsError] = useState(false) const [isError, setIsError] = useState(false)
...@@ -132,17 +130,15 @@ export function RestoreCloudBackupLoadingScreen({ navigation, route: { params } ...@@ -132,17 +130,15 @@ export function RestoreCloudBackupLoadingScreen({ navigation, route: { params }
} }
if (backups.length === 1 && backups[0]) { if (backups.length === 1 && backups[0]) {
navigation.replace(OnboardingScreens.RestoreCloudBackupPassword, { navigation.replace(OnboardingScreens.RestoreCloudBackupPassword, {
importType, ...params,
entryPoint,
mnemonicId: backups[0].mnemonicId, mnemonicId: backups[0].mnemonicId,
}) })
} else { } else {
navigation.replace(OnboardingScreens.RestoreCloudBackup, { navigation.replace(OnboardingScreens.RestoreCloudBackup, {
importType, ...params,
entryPoint,
}) })
} }
}, [backups, entryPoint, importType, isLoading, navigation]) }, [backups, isLoading, navigation, params])
if (isError) { if (isError) {
return ( return (
...@@ -162,9 +158,8 @@ export function RestoreCloudBackupLoadingScreen({ navigation, route: { params } ...@@ -162,9 +158,8 @@ export function RestoreCloudBackupLoadingScreen({ navigation, route: { params }
if (isLoading === false && backups.length === 0) { if (isLoading === false && backups.length === 0) {
if (isRestoringMnemonic) { if (isRestoringMnemonic) {
navigation.replace(OnboardingScreens.SeedPhraseInput, { navigation.replace(OnboardingScreens.SeedPhraseInput, {
...params,
showAsCloudBackupFallback: true, showAsCloudBackupFallback: true,
importType,
entryPoint,
}) })
} else { } else {
return ( return (
......
...@@ -39,7 +39,7 @@ export function RestoreMethodScreen({ navigation, route: { params } }: Props): J ...@@ -39,7 +39,7 @@ export function RestoreMethodScreen({ navigation, route: { params } }: Props): J
const handleOnPress = async (nav: OnboardingScreens, importType: ImportType): Promise<void> => { const handleOnPress = async (nav: OnboardingScreens, importType: ImportType): Promise<void> => {
navigation.navigate({ navigation.navigate({
name: nav, name: nav,
params: { importType, entryPoint }, params: { ...params, importType, entryPoint },
merge: true, merge: true,
}) })
} }
......
...@@ -18,7 +18,8 @@ import { Button, Flex, MobileDeviceHeight, Text, TouchableArea, useIsShortMobile ...@@ -18,7 +18,8 @@ import { Button, Flex, MobileDeviceHeight, Text, TouchableArea, useIsShortMobile
import { PapersText, QuestionInCircleFilled } from 'ui/src/components/icons' import { PapersText, QuestionInCircleFilled } from 'ui/src/components/icons'
import { uniswapUrls } from 'uniswap/src/constants/urls' import { uniswapUrls } from 'uniswap/src/constants/urls'
import Trace from 'uniswap/src/features/telemetry/Trace' import Trace from 'uniswap/src/features/telemetry/Trace'
import { ElementName } from 'uniswap/src/features/telemetry/constants' import { ElementName, MobileEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { TestID } from 'uniswap/src/test/fixtures/testIDs'
import { ImportType } from 'uniswap/src/types/onboarding' import { ImportType } from 'uniswap/src/types/onboarding'
import { OnboardingScreens } from 'uniswap/src/types/screens/mobile' import { OnboardingScreens } from 'uniswap/src/types/screens/mobile'
...@@ -77,6 +78,7 @@ export function SeedPhraseInputScreen({ navigation, route: { params } }: SeedPhr ...@@ -77,6 +78,7 @@ export function SeedPhraseInputScreen({ navigation, route: { params } }: SeedPhr
) )
const handleSubmitError: NativeSeedPhraseInputProps['onSubmitError'] = useCallback(() => { const handleSubmitError: NativeSeedPhraseInputProps['onSubmitError'] = useCallback(() => {
sendAnalyticsEvent(MobileEventName.SeedPhraseInputSubmitError)
setIsSubmitEnabled(true) setIsSubmitEnabled(true)
}, [setIsSubmitEnabled]) }, [setIsSubmitEnabled])
......
import { NativeStackScreenProps } from '@react-navigation/native-stack' import { NativeStackScreenProps } from '@react-navigation/native-stack'
import React, { ComponentProps, useCallback } from 'react' import React, { ComponentProps, useCallback } from 'react'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import { ScrollView, StyleSheet } from 'react-native' import { ScrollView } from 'react-native'
import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated' import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'
import { navigate } from 'src/app/navigation/rootNavigation' import { navigate } from 'src/app/navigation/rootNavigation'
import { OnboardingStackParamList } from 'src/app/navigation/types' import { OnboardingStackParamList } from 'src/app/navigation/types'
import { OnboardingScreen } from 'src/features/onboarding/OnboardingScreen' import { OnboardingScreen } from 'src/features/onboarding/OnboardingScreen'
import { import { Button, Flex, Loader, Text, TouchableArea, useLayoutAnimationOnChange } from 'ui/src'
Button,
Flex,
LinearGradient,
Loader,
Text,
TouchableArea,
useLayoutAnimationOnChange,
useSporeColors,
} from 'ui/src'
import { WalletFilled } from 'ui/src/components/icons' import { WalletFilled } from 'ui/src/components/icons'
import { opacify, spacing } from 'ui/src/theme' import { spacing } from 'ui/src/theme'
import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard' import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard'
import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
...@@ -30,7 +21,6 @@ import WalletPreviewCard from 'wallet/src/components/WalletPreviewCard/WalletPre ...@@ -30,7 +21,6 @@ import WalletPreviewCard from 'wallet/src/components/WalletPreviewCard/WalletPre
import { useOnboardingContext } from 'wallet/src/features/onboarding/OnboardingContext' import { useOnboardingContext } from 'wallet/src/features/onboarding/OnboardingContext'
import { useImportableAccounts } from 'wallet/src/features/onboarding/hooks/useImportableAccounts' import { useImportableAccounts } from 'wallet/src/features/onboarding/hooks/useImportableAccounts'
import { useSelectAccounts } from 'wallet/src/features/onboarding/hooks/useSelectAccounts' import { useSelectAccounts } from 'wallet/src/features/onboarding/hooks/useSelectAccounts'
import { useAnyAccountEligibleForDelegation } from 'wallet/src/features/smartWallet/hooks/useAnyAccountEligibleForDelegation'
const ANIMATION_DURATION = 300 const ANIMATION_DURATION = 300
...@@ -40,7 +30,6 @@ export function SelectWalletScreen({ navigation, route: { params } }: Props): JS ...@@ -40,7 +30,6 @@ export function SelectWalletScreen({ navigation, route: { params } }: Props): JS
const { t } = useTranslation() const { t } = useTranslation()
const { selectImportedAccounts, getImportedAccountsAddresses } = useOnboardingContext() const { selectImportedAccounts, getImportedAccountsAddresses } = useOnboardingContext()
const importedAddresses = getImportedAccountsAddresses() const importedAddresses = getImportedAccountsAddresses()
const colors = useSporeColors()
const { const {
importableAccounts, importableAccounts,
...@@ -78,13 +67,9 @@ export function SelectWalletScreen({ navigation, route: { params } }: Props): JS ...@@ -78,13 +67,9 @@ export function SelectWalletScreen({ navigation, route: { params } }: Props): JS
const highlightComponent = <CustomHighlightText /> const highlightComponent = <CustomHighlightText />
const { eligible: isAnyAccountEligibleForDelegation, loading: isDelegationChecksLoading } = const isContinueButtonDisabled = isLoading || !!showError || selectedAddresses.length === 0
useAnyAccountEligibleForDelegation(importableAccounts)
const isContinueButtonDisabled = const showSmartWalletDisclaimer = smartWalletEnabled && !isContinueButtonDisabled
isLoading || !!showError || selectedAddresses.length === 0 || (isDelegationChecksLoading && smartWalletEnabled)
const showSmartWalletDisclaimer = smartWalletEnabled && !isContinueButtonDisabled && isAnyAccountEligibleForDelegation
const opacityStyle = useAnimatedStyle( const opacityStyle = useAnimatedStyle(
() => ({ () => ({
...@@ -108,18 +93,11 @@ export function SelectWalletScreen({ navigation, route: { params } }: Props): JS ...@@ -108,18 +93,11 @@ export function SelectWalletScreen({ navigation, route: { params } }: Props): JS
title={t('account.wallet.select.error')} title={t('account.wallet.select.error')}
onRetry={refetchAccounts} onRetry={refetchAccounts}
/> />
) : isLoading || isDelegationChecksLoading ? ( ) : isLoading ? (
<Flex grow justifyContent="space-between" px="$spacing16"> <Flex grow justifyContent="space-between" px="$spacing16">
<Loader.Wallets repeat={5} /> <Loader.Wallets repeat={5} />
</Flex> </Flex>
) : ( ) : (
<Flex flexGrow={1} flexShrink={1}>
<LinearGradient
colors={[colors.surface1.val, opacify(0, colors.surface1.val)]}
end={{ x: 0, y: 1 }}
start={{ x: 0, y: 0 }}
style={ListSheet.topGradient}
/>
<ScrollView testID={TestID.SelectWalletScreenLoaded}> <ScrollView testID={TestID.SelectWalletScreenLoaded}>
<Flex height="$spacing12" /> <Flex height="$spacing12" />
<Flex gap="$gap12"> <Flex gap="$gap12">
...@@ -147,20 +125,12 @@ export function SelectWalletScreen({ navigation, route: { params } }: Props): JS ...@@ -147,20 +125,12 @@ export function SelectWalletScreen({ navigation, route: { params } }: Props): JS
})} })}
</Flex> </Flex>
</ScrollView> </ScrollView>
<LinearGradient
colors={[opacify(0, colors.surface1.val), colors.surface1.val]}
end={{ x: 0, y: 1 }}
start={{ x: 0, y: 0 }}
style={ListSheet.bottomGradient}
/>
</Flex>
)} )}
<Animated.View <Animated.View
style={[ style={[
opacityStyle, opacityStyle,
{ {
marginBottom: spacing.spacing16, marginBottom: spacing.spacing16,
marginTop: spacing.spacing16,
marginHorizontal: spacing.spacing24, marginHorizontal: spacing.spacing24,
}, },
]} ]}
...@@ -185,7 +155,7 @@ export function SelectWalletScreen({ navigation, route: { params } }: Props): JS ...@@ -185,7 +155,7 @@ export function SelectWalletScreen({ navigation, route: { params } }: Props): JS
<Flex opacity={showError ? 0 : 1} px="$spacing16"> <Flex opacity={showError ? 0 : 1} px="$spacing16">
<Flex row> <Flex row>
<Button <Button
isDisabled={isLoading || !!showError || selectedAddresses.length === 0} isDisabled={isContinueButtonDisabled}
variant="branded" variant="branded"
size="large" size="large"
testID={TestID.Continue} testID={TestID.Continue}
...@@ -200,24 +170,6 @@ export function SelectWalletScreen({ navigation, route: { params } }: Props): JS ...@@ -200,24 +170,6 @@ export function SelectWalletScreen({ navigation, route: { params } }: Props): JS
) )
} }
const ListSheet = StyleSheet.create({
bottomGradient: {
bottom: 0,
height: spacing.spacing16,
left: 0,
position: 'absolute',
width: '100%',
},
topGradient: {
height: spacing.spacing16,
left: 0,
position: 'absolute',
top: 0,
width: '100%',
zIndex: 1,
},
})
function CustomHighlightText(props: ComponentProps<typeof Text>): JSX.Element { function CustomHighlightText(props: ComponentProps<typeof Text>): JSX.Element {
return <Text variant="buttonLabel4" color="$neutral1" {...props} /> return <Text variant="buttonLabel4" color="$neutral1" {...props} />
} }
...@@ -38,6 +38,7 @@ export function onRestoreComplete({ ...@@ -38,6 +38,7 @@ export function onRestoreComplete({
sendAnalyticsEvent(MobileEventName.RestoreSuccess, { sendAnalyticsEvent(MobileEventName.RestoreSuccess, {
is_restoring_mnemonic: isRestoringMnemonic, is_restoring_mnemonic: isRestoringMnemonic,
import_type: params.importType, import_type: params.importType,
restore_type: params.restoreType,
screen, screen,
}) })
} }
...@@ -91,9 +91,12 @@ export function ViewPrivateKeysScreen({ navigation, route }: Props): JSX.Element ...@@ -91,9 +91,12 @@ export function ViewPrivateKeysScreen({ navigation, route }: Props): JSX.Element
<BulletRow Icon={Laptop} description={t('privateKeys.export.modal.speedbump.bullet3')} /> <BulletRow Icon={Laptop} description={t('privateKeys.export.modal.speedbump.bullet3')} />
</Flex> </Flex>
<Flex row py="$spacing24" gap="$gap8"> <Flex row py="$spacing24" gap="$gap8">
<Trace logPress element={ElementName.Cancel}>
<Button variant="default" emphasis="secondary" size="medium" onPress={navigation.goBack}> <Button variant="default" emphasis="secondary" size="medium" onPress={navigation.goBack}>
{t('common.button.close')} {t('common.button.close')}
</Button> </Button>
</Trace>
<Trace logPress element={ElementName.Continue}>
<Button <Button
variant="branded" variant="branded"
emphasis="primary" emphasis="primary"
...@@ -103,6 +106,7 @@ export function ViewPrivateKeysScreen({ navigation, route }: Props): JSX.Element ...@@ -103,6 +106,7 @@ export function ViewPrivateKeysScreen({ navigation, route }: Props): JSX.Element
> >
{t('common.button.continue')} {t('common.button.continue')}
</Button> </Button>
</Trace>
</Flex> </Flex>
</Flex> </Flex>
) )
......
...@@ -48,8 +48,6 @@ ignores: [ ...@@ -48,8 +48,6 @@ ignores: [
'detect-package-manager', 'detect-package-manager',
'eslint-plugin-storybook', 'eslint-plugin-storybook',
'prop-types', 'prop-types',
## Testing
'@types/testing-library__cypress',
## i18n ## i18n
'dotenv-cli', 'dotenv-cli',
'@crowdin/cli', '@crowdin/cli',
...@@ -70,6 +68,7 @@ ignores: [ ...@@ -70,6 +68,7 @@ ignores: [
'constants', 'constants',
'dev', 'dev',
'featureFlags', 'featureFlags',
'features',
'hooks', 'hooks',
'lib', 'lib',
'locales', 'locales',
......
...@@ -12,9 +12,6 @@ module.exports = { ...@@ -12,9 +12,6 @@ module.exports = {
}, },
}, },
rules: { rules: {
// TODO: had to add this rule to avoid errors on monorepo migration that didnt happen in interface
'cypress/unsafe-to-chain-command': 'off',
// let prettier do things: // let prettier do things:
semi: 0, semi: 0,
quotes: 0, quotes: 0,
...@@ -27,7 +24,6 @@ module.exports = { ...@@ -27,7 +24,6 @@ module.exports = {
{ {
files: [ files: [
'src/index.tsx', 'src/index.tsx',
'cypress/utils/index.ts',
'src/tracing/index.ts', 'src/tracing/index.ts',
'src/state/index.ts', 'src/state/index.ts',
'src/state/explore/index.tsx', 'src/state/explore/index.tsx',
......
...@@ -30,6 +30,8 @@ bundlemeta.json ...@@ -30,6 +30,8 @@ bundlemeta.json
# misc # misc
.DS_Store .DS_Store
tsconfig.tsbuildinfo
!.env !.env
.env.local .env.local
...@@ -52,10 +54,6 @@ notes.txt ...@@ -52,10 +54,6 @@ notes.txt
package-lock.json package-lock.json
cypress/downloads
cypress/videos
cypress/screenshots
.vercel .vercel
.wrangler .wrangler
......
import { defineConfig } from 'cypress'
import { setupHardhatEvents } from 'cypress-hardhat'
const TIMEOUT = 24000 // 2x average block time
export default defineConfig({
projectId: 'fabfoi',
defaultCommandTimeout: TIMEOUT,
requestTimeout: TIMEOUT,
chromeWebSecurity: false,
experimentalMemoryManagement: true, // better memory management, see https://github.com/cypress-io/cypress/pull/25462
retries: { runMode: process.env.CYPRESS_RETRIES ? +process.env.CYPRESS_RETRIES : 1 },
video: false, // GH provides 2 CPUs, and cypress video eats one up, see https://github.com/cypress-io/cypress/issues/20468#issuecomment-1307608025
e2e: {
async setupNodeEvents(on, config) {
await setupHardhatEvents(on, config)
return config
},
baseUrl: 'http://localhost:3000',
specPattern: 'cypress/{e2e,staging}/**/*.test.ts',
},
})
This diff is collapsed.
// see https://github.com/Uniswap/interface/pull/4115
describe('Link', () => {
it('should update route', () => {
cy.viewport(2000, 1600)
cy.visit('/swap')
cy.contains('Pool').click()
cy.get('[data-cy="join-pool-button"]').should('exist')
})
})
import { BigNumber } from '@ethersproject/bignumber'
import { MaxUint160, MaxUint256 } from '@uniswap/permit2-sdk'
import { CurrencyAmount, Token } from '@uniswap/sdk-core'
import { DAI, USDT } from 'uniswap/src/constants/tokens'
import { HARDHAT_TIMEOUT, setupHardhat } from '../utils'
/** Initiates a swap. */
function initiateSwap(swapButtonText?: string) {
// The swap button is re-rendered once enabled, so we must wait until the original button is not disabled to re-select the appropriate button.
cy.get('#swap-button').should('not.be.disabled')
// Completes the swap.
cy.get('#swap-button').click()
cy.contains(swapButtonText ?? 'Confirm swap').click()
}
describe('Permit2', () => {
function setupInputs(inputToken: Token, outputToken: Token) {
// Sets up a swap between inputToken and outputToken.
cy.visit(`/swap/?inputCurrency=${inputToken.address}&outputCurrency=${outputToken.address}`)
cy.get('#swap-currency-input .token-amount-input').type('0.01')
}
/** Asserts permit2 has a max approval for spend of the input token on-chain. */
function expectTokenAllowanceForPermit2ToBeMax(inputToken: Token) {
// check token approval
cy.hardhat()
.then(({ approval, wallet }) => approval.getTokenAllowanceForPermit2({ owner: wallet, token: inputToken }))
.then((allowance) => {
Cypress.log({ name: `Token allowance: ${allowance.toString()}` })
cy.wrap(allowance).should('deep.equal', MaxUint256)
})
}
/** Asserts the universal router has a max permit2 approval for spend of the input token on-chain. */
function expectPermit2AllowanceForUniversalRouterToBeMax(inputToken: Token) {
cy.hardhat()
.then(({ approval, wallet }) => approval.getPermit2Allowance({ owner: wallet, token: inputToken }))
.then((allowance) => {
Cypress.log({ name: `Permit2 allowance: ${allowance.amount.toString()}` })
cy.wrap(allowance.amount).should('deep.equal', MaxUint160)
// Asserts that the on-chain expiration is in 30 days, within a tolerance of 40 seconds.
const THIRTY_DAYS_SECONDS = 2_592_000
const expected = Math.floor(Date.now() / 1000 + THIRTY_DAYS_SECONDS)
cy.wrap(allowance.expiration).should('be.closeTo', expected, 40)
})
}
setupHardhat(async (hardhat) => {
await hardhat.fund(hardhat.wallet, CurrencyAmount.fromRawAmount(DAI, 1e18))
await hardhat.mine()
})
describe('approval process (with intermediate screens)', () => {
// Turn off automine so that intermediate screens are available to assert on.
before(() => cy.hardhat({ automine: false }))
after(() => cy.hardhat({ automine: true }))
it('swaps after completing full permit2 approval process', () => {
setupInputs(DAI, USDT)
initiateSwap('Approve and swap')
// verify that the modal retains its state when the window loses focus
cy.window().trigger('blur')
// Verify token approval
cy.contains('Approval pending...')
cy.wait('@eth_sendRawTransaction')
cy.hardhat().then((hardhat) => hardhat.mine())
expectTokenAllowanceForPermit2ToBeMax(DAI)
// Verify permit2 approval
cy.wait('@eth_signTypedData_v4')
cy.wait('@eth_sendRawTransaction')
cy.contains('Swap pending...')
cy.hardhat().then((hardhat) => hardhat.mine())
cy.contains('Swap success!')
expectPermit2AllowanceForUniversalRouterToBeMax(DAI)
})
it('swaps with existing permit approval and missing token approval', () => {
setupInputs(DAI, USDT)
cy.hardhat().then({ timeout: HARDHAT_TIMEOUT }, async (hardhat) => {
await hardhat.approval.setPermit2Allowance({ owner: hardhat.wallet, token: DAI })
await hardhat.mine()
})
initiateSwap('Approve and swap')
// Verify token approval
cy.contains('Approval pending...')
cy.wait('@eth_sendRawTransaction')
cy.hardhat().then((hardhat) => hardhat.mine())
expectTokenAllowanceForPermit2ToBeMax(DAI)
// Verify transaction
cy.wait('@eth_sendRawTransaction')
cy.hardhat().then((hardhat) => hardhat.mine())
cy.contains('Swap success!')
})
/**
* On mainnet, you have to revoke USDT approval before increasing it.
* From the token contract:
* To change the approve amount you first have to reduce the addresses`
* allowance to zero by calling `approve(_spender, 0)` if it is not
* already 0 to mitigate the race condition described here:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
*/
it('swaps USDT with existing but insufficient approval permit', () => {
cy.hardhat().then({ timeout: HARDHAT_TIMEOUT }, async (hardhat) => {
await hardhat.fund(hardhat.wallet, CurrencyAmount.fromRawAmount(USDT, 2e6))
await hardhat.approval.setTokenAllowanceForPermit2({ owner: hardhat.wallet, token: USDT }, 1e6)
await hardhat.mine()
await hardhat.approval.setPermit2Allowance({ owner: hardhat.wallet, token: USDT })
await hardhat.mine()
})
setupInputs(USDT, DAI)
cy.get('#swap-currency-input .token-amount-input').clear().type('2')
initiateSwap('Approve and swap')
// Verify allowance revocation
cy.contains('Resetting USDT limit...')
cy.wait('@eth_sendRawTransaction')
cy.hardhat().then((hardhat) => hardhat.mine())
cy.hardhat()
.then(({ approval, wallet }) => approval.getTokenAllowanceForPermit2({ owner: wallet, token: USDT }))
.should('deep.equal', BigNumber.from(0))
// Verify token approval
cy.contains('Approval pending...')
cy.wait('@eth_sendRawTransaction')
cy.hardhat().then((hardhat) => hardhat.mine())
expectTokenAllowanceForPermit2ToBeMax(USDT)
// Verify transaction
cy.wait('@eth_sendRawTransaction')
cy.hardhat().then((hardhat) => hardhat.mine())
cy.contains('Swap success!')
})
})
it('swaps when user has already approved token and permit2', () => {
cy.hardhat().then({ timeout: HARDHAT_TIMEOUT }, async (hardhat) => {
await hardhat.approval.setTokenAllowanceForPermit2({ owner: hardhat.wallet, token: DAI })
await hardhat.approval.setPermit2Allowance({ owner: hardhat.wallet, token: DAI })
})
setupInputs(DAI, USDT)
initiateSwap()
// Verify transaction
cy.contains('Swap success!')
})
it('swaps after handling user rejection of both approval and signature', () => {
setupInputs(DAI, USDT)
const USER_REJECTION = { code: 4001 }
cy.hardhat().then((hardhat) => {
// Reject token approval
const tokenApprovalStub = cy.stub(hardhat.wallet, 'sendTransaction').log(true)
tokenApprovalStub.rejects(USER_REJECTION) // rejects token approval
initiateSwap('Approve and swap')
// Verify token approval rejection
cy.wrap(tokenApprovalStub).should('be.calledOnce')
cy.contains('Approve and swap')
// Allow token approval
cy.then(() => tokenApprovalStub.restore())
// Reject permit2 approval
const permitApprovalStub = cy.stub(hardhat.provider, 'send').log(false)
permitApprovalStub.withArgs('eth_signTypedData_v4').rejects(USER_REJECTION) // rejects permit approval
permitApprovalStub.callThrough() // allows non-eth_signTypedData_v4 send calls to return non-stubbed values
cy.contains('Approve and swap').click()
// Verify token approval
cy.wait('@eth_sendRawTransaction')
cy.hardhat().then((hardhat) => hardhat.mine())
expectTokenAllowanceForPermit2ToBeMax(DAI)
// Verify permit2 approval rejection
cy.wrap(permitApprovalStub).should('be.calledWith', 'eth_signTypedData_v4')
cy.contains('Sign and swap')
// Allow permit2 approval
cy.then(() => permitApprovalStub.restore())
cy.contains('Sign and swap').click()
// Verify permit2 approval
cy.contains('Swap success!')
expectPermit2AllowanceForUniversalRouterToBeMax(DAI)
})
})
it('prompts token approval when existing approval amount is too low', () => {
setupInputs(DAI, USDT)
cy.hardhat().then({ timeout: HARDHAT_TIMEOUT }, async (hardhat) => {
await hardhat.approval.setPermit2Allowance({ owner: hardhat.wallet, token: DAI })
await hardhat.approval.setTokenAllowanceForPermit2({ owner: hardhat.wallet, token: DAI }, 1)
})
initiateSwap('Approve and swap')
// Verify token approval
expectPermit2AllowanceForUniversalRouterToBeMax(DAI)
})
it('prompts signature when existing permit approval is expired', () => {
const expiredAllowance = { expiration: Math.floor((Date.now() - 1) / 1000) }
cy.hardhat()
.then({ timeout: HARDHAT_TIMEOUT }, async (hardhat) => {
await hardhat.approval.setTokenAllowanceForPermit2({ owner: hardhat.wallet, token: DAI })
await hardhat.approval.setPermit2Allowance({ owner: hardhat.wallet, token: DAI }, expiredAllowance)
})
.then(() => {
setupInputs(DAI, USDT)
initiateSwap('Sign and swap')
})
// Verify permit2 approval
cy.wait('@eth_signTypedData_v4')
cy.contains('Swap success!')
expectPermit2AllowanceForUniversalRouterToBeMax(DAI)
})
it('prompts signature when existing permit approval amount is too low', () => {
const smallAllowance = { amount: 1 }
cy.hardhat()
.then({ timeout: HARDHAT_TIMEOUT }, async (hardhat) => {
await hardhat.approval.setTokenAllowanceForPermit2({ owner: hardhat.wallet, token: DAI })
await hardhat.approval.setPermit2Allowance({ owner: hardhat.wallet, token: DAI }, smallAllowance)
})
.then(() => {
setupInputs(DAI, USDT)
initiateSwap('Sign and swap')
})
// Verify permit2 approval
cy.wait('@eth_signTypedData_v4')
cy.contains('Swap success!')
expectPermit2AllowanceForUniversalRouterToBeMax(DAI)
})
})
describe('Pool', () => {
beforeEach(() => {
cy.visit('/pool').then(() => {
cy.wait('@eth_blockNumber')
})
})
it('add liquidity links to /add/ETH', () => {
cy.get('body').then(() => {
cy.get('#join-pool-button')
.click()
.then(() => {
cy.url().should('contain', '/add/ETH')
})
})
})
})
describe('Redirect', () => {
it('should redirect to /vote/create-proposal when visiting /create-proposal', () => {
cy.visit('/create-proposal')
cy.url().should('match', /\/vote.uniswapfoundation.org/)
})
it('should redirect to /not-found when visiting nonexist url', () => {
cy.visit('/none-exist-url')
cy.url().should('match', /\/not-found/)
})
})
describe('RedirectExplore', () => {
it('should redirect from /tokens/ to /explore', () => {
cy.visit('/tokens')
cy.url().should('match', /\/explore/)
cy.visit('/tokens/ethereum')
cy.url().should('match', /\/explore\/tokens\/ethereum/)
cy.visit('/tokens/optimism/NATIVE')
cy.url().should('match', /\/explore\/tokens\/optimism\/NATIVE/)
})
})
describe('Legacy Pool Redirects', () => {
it('should redirect /pool to /positions', () => {
cy.visit('/pool')
cy.url().should('match', /\/positions/)
})
it('should redirect /pool/:tokenId with chain param to /positions/v3/:chainName/:tokenId', () => {
cy.visit('/pool/123?chain=mainnet')
cy.url().should('match', /\/positions\/v3\/ethereum\/123/)
})
it('should redirect add v2 liquidity to positions create page', () => {
cy.visit('/add/v2/0x318400242bFdE3B20F49237a9490b8eBB6bdB761/ETH')
cy.url().should('match', /\/positions\/create\/v2\?currencyA=0x318400242bFdE3B20F49237a9490b8eBB6bdB761&currencyB=ETH/)
})
it('should redirect add v3 liquidity to positions create page', () => {
cy.visit('/add/0x318400242bFdE3B20F49237a9490b8eBB6bdB761/ETH')
cy.url().should('match', /\/positions\/create\/v3\?currencyA=0x318400242bFdE3B20F49237a9490b8eBB6bdB761&currencyB=ETH/)
})
it('should redirect remove v2 liquidity to positions page', () => {
cy.visit('/remove/v2/0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2')
cy.url().should('match', /\/positions\/v2\/ethereum\/0xBb2b8038a1640196FbE3e38816F3e67Cba72D940/)
})
it('should redirect remove v3 liquidity to positions page', () => {
cy.visit('/remove/825708')
cy.url().should('match', /\/positions\/v3\/ethereum\/825708/)
})
})
import { NATIVE_CHAIN_ID } from 'constants/tokens'
import { UNI } from 'uniswap/src/constants/tokens'
import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { getTestSelector } from '../utils'
const UNI_ADDRESS = UNI[UniverseChainId.Mainnet].address.toLowerCase()
describe('Universal search bar', () => {
function openSearch() {
// can't just type "/" because on mobile it doesn't respond to that
return cy.get('[data-cy="nav-search-container"] [data-cy="nav-search-icon"]').first().click()
}
beforeEach(() => {
cy.visit('/')
})
function getSearchBar() {
return cy.get('[data-cy="nav-search-container"] input').click()
}
it('should yield clickable result that is then added to recent searches', () => {
// Search for UNI token by name.
openSearch()
getSearchBar().clear().type('uni')
cy.get(getTestSelector(`searchbar-token-row-ETHEREUM-${UNI_ADDRESS}`))
.should('contain.text', 'Uniswap')
.and('contain.text', 'UNI')
.and('contain.text', '$')
.and('contain.text', '%')
.click()
cy.location('pathname').should('equal', '/explore/tokens/ethereum/0x1f9840a85d5af5bf1d1762f925bdaddc4201f984')
openSearch()
cy.get(getTestSelector('searchbar-dropdown'))
.contains(getTestSelector('searchbar-dropdown'), 'Recent searches')
.find(getTestSelector(`searchbar-token-row-ETHEREUM-${UNI_ADDRESS}`))
.should('exist')
})
it('should go to the selected result when recent results are shown', () => {
// Seed recent results with UNI.
openSearch()
getSearchBar().type('uni')
cy.get(getTestSelector(`searchbar-token-row-ETHEREUM-${UNI_ADDRESS}`))
getSearchBar().clear().type('{esc}')
// Search a different token by name.
openSearch()
getSearchBar().type('eth')
cy.get(getTestSelector(`searchbar-token-row-ETHEREUM-${NATIVE_CHAIN_ID}`))
// Validate that we go to the searched/selected result.
cy.get(getTestSelector(`searchbar-token-row-ETHEREUM-${NATIVE_CHAIN_ID}`)).click()
cy.url().should('contain', `/explore/tokens/ethereum/${NATIVE_CHAIN_ID}`)
})
})
import { getTestSelector, resetHardhatChain } from '../../utils'
// TODO(WEB-4923): Re-enable network switching tests when theres a non-UI way to switch networks
// function switchChain(chain: string) {
// cy.get(getTestSelector('chain-selector')).click()
// cy.contains(chain).click()
// }
describe('network switching', () => {
beforeEach(() => {
cy.visit('/swap')
cy.get(getTestSelector('web3-status-connected'))
})
afterEach(resetHardhatChain)
// function rejectsNetworkSwitchWith(rejection: unknown) {
// cy.hardhat().then((hardhat) => {
// // Reject network switch
// const sendStub = cy.stub(hardhat.provider, 'send').log(false).as('switch')
// sendStub.withArgs('wallet_switchEthereumChain').rejects(rejection)
// sendStub.callThrough() // allows other calls to return non-stubbed values
// })
// switchChain('Polygon')
// // Verify rejected network switch
// cy.get('@switch').should('have.been.calledWith', 'wallet_switchEthereumChain')
// cy.get(getTestSelector('web3-status-connected'))
// }
// it('should not display message on user rejection', () => {
// const USER_REJECTION = { code: 4001 }
// rejectsNetworkSwitchWith(USER_REJECTION)
// cy.get(getTestSelector('popups')).should('not.contain', 'Failed to switch networks')
// })
// it('should display message on unknown error', () => {
// rejectsNetworkSwitchWith(new Error('Unknown error'))
// cy.get(getTestSelector('popups')).contains('Failed to switch networks')
// })
// it('should add missing chain', () => {
// cy.hardhat().then((hardhat) => {
// // https://docs.metamask.io/guide/rpc-api.html#unrestricted-methods
// const CHAIN_NOT_ADDED = { code: 4902 } // missing message in useSelectChain
// // Reject network switch with CHAIN_NOT_ADDED
// const sendStub = cy.stub(hardhat.provider, 'send').log(false).as('switch')
// let added = false
// sendStub
// .withArgs('wallet_switchEthereumChain')
// .callsFake(() => (added ? Promise.resolve(null) : Promise.reject(CHAIN_NOT_ADDED)))
// sendStub.withArgs('wallet_addEthereumChain').callsFake(() => {
// added = true
// return Promise.resolve(null)
// })
// sendStub.callThrough() // allows other calls to return non-stubbed values
// })
// switchChain('Polygon')
// // Verify the network was added
// cy.get('@switch').should('have.been.calledWith', 'wallet_switchEthereumChain')
// cy.get('@switch').should('have.been.calledWith', 'wallet_addEthereumChain')
// })
// it('should not disconnect while switching', () => {
// // Defer the connection so we can see the pending state
// cy.hardhat().then((hardhat) => {
// const sendStub = cy.stub(hardhat.provider, 'send').log(false).as('switch')
// sendStub.withArgs('wallet_switchEthereumChain').returns(new Promise(() => {}))
// sendStub.callThrough() // allows other calls to return non-stubbed values
// })
// switchChain('Polygon')
// // Verify there is no disconnection
// cy.get('@switch').should('have.been.calledWith', 'wallet_switchEthereumChain')
// cy.contains('Connecting to Polygon')
// cy.get(getTestSelector('web3-status-connected')).should('be.disabled')
// })
// it('switches networks', () => {
// // Select an output currency
// cy.get('#swap-currency-output .open-currency-select-button').click()
// cy.get(getTestSelector('token-option-1-USDT')).click()
// // Populate input/output fields
// cy.get('#swap-currency-input .token-amount-input').clear().type('1')
// cy.get('#swap-currency-output .token-amount-input').should('not.equal', '')
// // Switch network
// switchChain('Polygon')
// // Verify network switch
// cy.wait('@wallet_switchEthereumChain')
// cy.get(getTestSelector('web3-status-connected'))
// cy.url().should('contain', 'chain=polygon')
// // Verify that the input/output fields were reset
// cy.get('#swap-currency-input .token-amount-input').should('have.value', '')
// cy.get(`#swap-currency-input .token-symbol-container`).should('contain.text', 'MATIC')
// cy.get(`#swap-currency-output .token-amount-input`).should('not.have.value')
// cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'Select token')
// })
// describe('from URL param', () => {
// it('should switch network from URL param', () => {
// cy.visit('/swap?chain=optimism')
// cy.wait('@wallet_switchEthereumChain')
// cy.get(getTestSelector('web3-status-connected'))
// })
// it('should switch network with inputCurrency from URL param', () => {
// cy.visit('/swap?chain=optimism&outputCurrency=0x0b2c639c533813f4aa9d7837caf62653d097ff85')
// cy.wait('@wallet_switchEthereumChain')
// cy.get(getTestSelector('web3-status-connected'))
// cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'USDC')
// })
// it('should not switch network with no chain in param', () => {
// cy.hardhat().then((hardhat) => {
// cy.visit('/swap?outputCurrency=0x2260fac5e5542a773aa44fbcfedf7c193bc2c599')
// const sendSpy = cy.spy(hardhat.provider, 'send')
// cy.wrap(sendSpy).should('not.be.calledWith', 'wallet_switchEthereumChain')
// cy.wrap(hardhat.provider.network.chainId).should('eq', 1)
// cy.get(getTestSelector('web3-status-connected'))
// cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'WBTC')
// })
// })
// it('should be able to switch network after loading from URL param', () => {
// cy.visit('/swap?chain=optimism&outputCurrency=0x0b2c639c533813f4aa9d7837caf62653d097ff85')
// cy.wait('@wallet_switchEthereumChain')
// cy.get(getTestSelector('web3-status-connected'))
// cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'USDC')
// // switching to another chain clears query param
// switchChain('Ethereum')
// cy.wait('@wallet_switchEthereumChain')
// cy.url().should('not.contain', 'chain=optimism')
// cy.url().should('not.contain', 'outputCurrency=0xff970a61a04b1ca14834a43f5de4533ebddb5cc8')
// })
// })
describe('multichain', () => {
it('does not switchEthereumChain when multichain is enabled', () => {
cy.hardhat().then((hardhat) => {
cy.visit('/swap')
cy.get('#swap-currency-input .open-currency-select-button').click()
cy.get(getTestSelector('chain-selector')).last().click()
cy.contains('Arbitrum').click()
const sendSpy = cy.spy(hardhat.provider, 'send')
cy.wrap(sendSpy).should('not.be.calledWith', 'wallet_switchEthereumChain')
cy.wrap(hardhat.provider.network.chainId).should('eq', 1)
})
})
})
})
import { getTestSelector } from '../utils'
describe('Wallet Dropdown', () => {
function itChangesTheme() {
it('should change theme', () => {
cy.get(getTestSelector('theme-light')).click()
cy.get('html').should('have.class', 't_light')
cy.get(getTestSelector('theme-dark')).click()
cy.get('html').should('have.class', 't_dark')
cy.get(getTestSelector('theme-auto')).click()
cy.get('html').should('not.have.class', 't_light')
})
}
function itChangesLocale() {
it('should change locale', () => {
cy.contains('Uniswap available in: English').should('not.exist')
cy.get(getTestSelector('language-settings-button')).click()
cy.get(getTestSelector('wallet-language-item')).contains('Afrikaans').click({ force: true })
cy.location('search').should('include', 'lng=af-ZA')
cy.get(getTestSelector('wallet-language-item')).contains('English').click({ force: true })
cy.location('search').should('include', 'lng=en-US')
})
}
describe('connected', () => {
beforeEach(() => {
cy.visit('/')
cy.get(getTestSelector('web3-status-connected')).click()
cy.get(getTestSelector('wallet-settings')).click()
})
itChangesTheme()
itChangesLocale()
it('should not show buy crypto button in uk', () => {
cy.document().then((doc) => {
const meta = document.createElement('meta')
meta.setAttribute('property', 'x:blocked-paths')
meta.setAttribute('content', '/,/nfts,/buy')
doc.head.appendChild(meta)
})
cy.get(getTestSelector('wallet-buy-crypto')).should('not.exist')
})
})
describe('do not render buy button when /buy is blocked', () => {
beforeEach(() => {
cy.document().then((doc) => {
const meta = document.createElement('meta')
meta.setAttribute('property', 'x:blocked-paths')
meta.setAttribute('content', '/buy')
doc.head.appendChild(meta)
})
cy.visit('/')
cy.get(getTestSelector('web3-status-connected')).click()
cy.get(getTestSelector('wallet-settings')).click()
})
it('should not render buy button', () => {
cy.get(getTestSelector('wallet-buy-crypto')).should('not.exist')
})
})
describe('should change locale with feature flag', () => {
beforeEach(() => {
cy.visit('/')
cy.get(getTestSelector('web3-status-connected')).click()
cy.get(getTestSelector('wallet-settings')).click()
})
itChangesLocale()
})
describe('testnet toggle', () => {
beforeEach(() => {
cy.visit('/swap')
it('should toggle testnet visibility', () => {
cy.get('#swap-currency-input .open-currency-select-button').click()
cy.get(getTestSelector('chain-selector')).last().click()
cy.get(getTestSelector('network-button-11155111')).should('not.exist')
cy.get(getTestSelector('web3-status-connected')).click({ force: true })
cy.get(getTestSelector('wallet-settings')).click()
cy.get(getTestSelector('testnets-toggle')).click()
cy.contains('Close').click()
cy.get(getTestSelector('close-account-drawer')).click()
cy.get(getTestSelector('choose-input-token')).click()
cy.get(getTestSelector('token-option-11155111-ETH')).should('exist')
})
})
describe('disconnected', () => {
beforeEach(() => {
cy.visit('/')
cy.get(getTestSelector('web3-status-connected')).click()
// click twice, first time to show confirmation, second to confirm
cy.get(getTestSelector('wallet-disconnect')).click()
cy.get(getTestSelector('wallet-disconnect')).should('contain', 'Disconnect')
cy.get(getTestSelector('wallet-disconnect')).click()
})
it('wallet settings should not be accessible', () => {
cy.get(getTestSelector('wallet-settings')).should('not.exist')
})
})
// TODO(WEB-3905): Fix tamagui error causing these tests to fail.
describe.skip('with color theme', () => {
function visitSwapWithColorTheme({ dark }: { dark: boolean }) {
cy.visit('/swap', {
onBeforeLoad(win) {
cy.stub(win, 'matchMedia')
.withArgs('(prefers-color-scheme: dark)')
.returns({
matches: dark,
addEventListener() {
/* noop */
},
removeEventListener() {
/* noop */
},
})
},
})
}
it('should properly use dark system theme when auto theme setting is selected', () => {
visitSwapWithColorTheme({ dark: true })
cy.get(getTestSelector('web3-status-connected')).click()
cy.get(getTestSelector('wallet-settings')).click()
cy.get(getTestSelector('theme-auto')).click()
cy.get(getTestSelector('wallet-header')).should('have.css', 'color', 'rgb(155, 155, 155)')
})
it('should properly use light system theme when auto theme setting is selected', () => {
visitSwapWithColorTheme({ dark: false })
cy.get(getTestSelector('web3-status-connected')).click()
cy.get(getTestSelector('wallet-settings')).click()
cy.get(getTestSelector('theme-auto')).click()
cy.get(getTestSelector('wallet-header')).should('have.css', 'color', 'rgb(125, 125, 125)')
})
})
describe('mobile', () => {
beforeEach(() => {
cy.viewport('iphone-6').visit('/')
})
it('should dismiss the wallet bottom sheet when clicking buy crypto', () => {
cy.get(getTestSelector('web3-status-connected')).click()
cy.get(getTestSelector('wallet-buy-crypto')).click()
cy.get(getTestSelector('wallet-settings')).should('not.be.visible')
})
it('should use a bottom sheet and dismiss when on a mobile screen size', () => {
cy.get(getTestSelector('web3-status-connected')).click()
cy.root().click(15, 20)
cy.get(getTestSelector('wallet-settings')).should('not.be.visible')
})
})
describe('local currency', () => {
it('loads local currency from the query param', () => {
cy.visit('/')
cy.get(getTestSelector('web3-status-connected')).click()
cy.get(getTestSelector('wallet-settings')).click()
cy.contains('USD')
cy.visit('/?cur=AUD')
cy.get(getTestSelector('web3-status-connected')).click()
cy.get(getTestSelector('wallet-settings')).click()
cy.contains('AUD')
})
it('loads local currency from menu', () => {
cy.visit('/')
cy.get(getTestSelector('web3-status-connected')).click()
cy.get(getTestSelector('wallet-settings')).click()
cy.contains('USD')
cy.get(getTestSelector('local-currency-settings-button')).click()
cy.get(getTestSelector('wallet-local-currency-item')).contains('AUD').click({ force: true })
cy.location('search').should('include', 'cur=AUD')
cy.contains('AUD')
cy.get(getTestSelector('wallet-local-currency-item')).contains('USD').click({ force: true })
cy.location('search').should('include', 'cur=USD')
cy.contains('USD')
})
})
})
})
{
"data": {
"v3PoolsForTokenPair": [
{
"feeTier": 3000.0,
"token0Supply": 3084.78636993,
"token1Supply": 76658.86823272712,
"__typename": "V3Pool"
},
{
"feeTier": 500.0,
"token0Supply": 917.54087138,
"token1Supply": 22788.333363067817,
"__typename": "V3Pool"
},
{
"feeTier": 10000.0,
"token0Supply": 14.67505995,
"token1Supply": 276.9691974507059,
"__typename": "V3Pool"
},
{
"feeTier": 100.0,
"token0Supply": 0.04782301,
"token1Supply": 1.677630863841559,
"__typename": "V3Pool"
}
]
}
}
{
"countryCode": "US",
"displayName": "United States",
"state": "US-NY"
}
{
"error": null,
"message": null,
"quotes": [
{
"countryCode": "US",
"destinationAmount": 0.03131402821107613,
"destinationCurrencyCode": "ETH",
"isMostRecentlyUsedProvider": false,
"serviceProvider": "STRIPE",
"serviceProviderDetails": {
"logos": {
"darkLogo": "https://images-serviceprovider.meld.io/STRIPE/short_logo_light.png",
"lightLogo": "https://images-serviceprovider.meld.io/STRIPE/short_logo_light.png"
},
"name": "Stripe",
"paymentMethods": [
"Debit Card",
"ACH"
],
"serviceProvider": "STRIPE",
"supportUrl": "https://support.stripe.com/",
"url": "http://www.stripe.com"
},
"sourceAmount": 100,
"sourceCurrencyCode": "USD",
"totalFee": 2.19
},
{
"countryCode": "US",
"destinationAmount": 0.03034965,
"destinationCurrencyCode": "ETH",
"isMostRecentlyUsedProvider": true,
"serviceProvider": "COINBASEPAY",
"serviceProviderDetails": {
"logos": {
"darkLogo": "https://images-serviceprovider.meld.io/COINBASEPAY/short_logo_light.png",
"lightLogo": "https://images-serviceprovider.meld.io/COINBASEPAY/short_logo_light.png"
},
"name": "Coinbase",
"paymentMethods": [
"Debit Card",
"ACH"
],
"serviceProvider": "COINBASEPAY",
"supportUrl": "https://help.coinbase.com/",
"url": "https://www.coinbase.com/"
},
"sourceAmount": 100,
"sourceCurrencyCode": "USD",
"totalFee": 2.83
},
{
"countryCode": "US",
"destinationAmount": 0.0303,
"destinationCurrencyCode": "ETH",
"isMostRecentlyUsedProvider": false,
"serviceProvider": "MOONPAY",
"serviceProviderDetails": {
"logos": {
"darkLogo": "https://images-serviceprovider.meld.io/MOONPAY/short_logo_light.png",
"lightLogo": "https://images-serviceprovider.meld.io/MOONPAY/short_logo_light.png"
},
"name": "MoonPay",
"serviceProvider": "MOONPAY",
"supportUrl": "https://www.moonpay.com/contact-us",
"url": "https://buy.moonpay.com",
"paymentMethods": [
"Debit Card",
"Apple Pay",
"Google Pay",
"PayPal"
]
},
"sourceAmount": 100,
"sourceCurrencyCode": "USD",
"totalFee": 5.040000000000006
}
]
}
{
"fiatCurrencies": [
{
"displayName": "Argentine Peso",
"fiatCurrencyCode": "ARS",
"symbol": "https://images-currency.meld.io/fiat/ARS/symbol.png"
},
{
"displayName": "Australian Dollar",
"fiatCurrencyCode": "AUD",
"symbol": "https://images-currency.meld.io/fiat/AUD/symbol.png"
},
{
"displayName": "Azerbaijan Manat",
"fiatCurrencyCode": "AZN",
"symbol": "https://images-currency.meld.io/fiat/AZN/symbol.png"
},
{
"displayName": "Balboa",
"fiatCurrencyCode": "PAB",
"symbol": "https://images-currency.meld.io/fiat/PAB/symbol.png"
},
{
"displayName": "Boliviano",
"fiatCurrencyCode": "BOB",
"symbol": "https://images-currency.meld.io/fiat/BOB/symbol.png"
},
{
"displayName": "Brazilian Real",
"fiatCurrencyCode": "BRL",
"symbol": "https://images-currency.meld.io/fiat/BRL/symbol.png"
},
{
"displayName": "Bulgarian Lev",
"fiatCurrencyCode": "BGN",
"symbol": "https://images-currency.meld.io/fiat/BGN/symbol.png"
},
{
"displayName": "CFA Franc BCEAO",
"fiatCurrencyCode": "XOF",
"symbol": "https://images-currency.meld.io/fiat/XOF/symbol.png"
},
{
"displayName": "CFA Franc BEAC",
"fiatCurrencyCode": "XAF",
"symbol": "https://images-currency.meld.io/fiat/XAF/symbol.png"
},
{
"displayName": "Canadian Dollar",
"fiatCurrencyCode": "CAD",
"symbol": "https://images-currency.meld.io/fiat/CAD/symbol.png"
},
{
"displayName": "Chilean Peso",
"fiatCurrencyCode": "CLP",
"symbol": "https://images-currency.meld.io/fiat/CLP/symbol.png"
},
{
"displayName": "Colombian Peso",
"fiatCurrencyCode": "COP",
"symbol": "https://images-currency.meld.io/fiat/COP/symbol.png"
},
{
"displayName": "Costa Rican Colon",
"fiatCurrencyCode": "CRC",
"symbol": "https://images-currency.meld.io/fiat/CRC/symbol.png"
},
{
"displayName": "Croatian kuna",
"fiatCurrencyCode": "HRK",
"symbol": "https://images-currency.meld.io/fiat/HRK/symbol.png"
},
{
"displayName": "Czech Koruna",
"fiatCurrencyCode": "CZK",
"symbol": "https://images-currency.meld.io/fiat/CZK/symbol.png"
},
{
"displayName": "Danish Krone",
"fiatCurrencyCode": "DKK",
"symbol": "https://images-currency.meld.io/fiat/DKK/symbol.png"
},
{
"displayName": "Dominican Peso",
"fiatCurrencyCode": "DOP",
"symbol": "https://images-currency.meld.io/fiat/DOP/symbol.png"
},
{
"displayName": "Euro",
"fiatCurrencyCode": "EUR",
"symbol": "https://images-currency.meld.io/fiat/EUR/symbol.png"
},
{
"displayName": "Forint",
"fiatCurrencyCode": "HUF",
"symbol": "https://images-currency.meld.io/fiat/HUF/symbol.png"
},
{
"displayName": "Ghana Cedi",
"fiatCurrencyCode": "GHS",
"symbol": "https://images-currency.meld.io/fiat/GHS/symbol.png"
},
{
"displayName": "Hong Kong Dollar",
"fiatCurrencyCode": "HKD",
"symbol": "https://images-currency.meld.io/fiat/HKD/symbol.png"
},
{
"displayName": "Hryvnia",
"fiatCurrencyCode": "UAH",
"symbol": "https://images-currency.meld.io/fiat/UAH/symbol.png"
},
{
"displayName": "Iceland Krona",
"fiatCurrencyCode": "ISK",
"symbol": "https://images-currency.meld.io/fiat/ISK/symbol.png"
},
{
"displayName": "Indian Rupee",
"fiatCurrencyCode": "INR",
"symbol": "https://images-currency.meld.io/fiat/INR/symbol.png"
},
{
"displayName": "Jamaican Dollar",
"fiatCurrencyCode": "JMD",
"symbol": "https://images-currency.meld.io/fiat/JMD/symbol.png"
},
{
"displayName": "Jordanian Dinar",
"fiatCurrencyCode": "JOD",
"symbol": "https://images-currency.meld.io/fiat/JOD/symbol.png"
},
{
"displayName": "Kenyan Shilling",
"fiatCurrencyCode": "KES",
"symbol": "https://images-currency.meld.io/fiat/KES/symbol.png"
},
{
"displayName": "Kuwaiti Dinar",
"fiatCurrencyCode": "KWD",
"symbol": "https://images-currency.meld.io/fiat/KWD/symbol.png"
},
{
"displayName": "Malagasy Ariary",
"fiatCurrencyCode": "MGA",
"symbol": "https://images-currency.meld.io/fiat/MGA/symbol.png"
},
{
"displayName": "Mexican Peso",
"fiatCurrencyCode": "MXN",
"symbol": "https://images-currency.meld.io/fiat/MXN/symbol.png"
},
{
"displayName": "Naira",
"fiatCurrencyCode": "NGN",
"symbol": "https://images-currency.meld.io/fiat/NGN/symbol.png"
},
{
"displayName": "Nepalese Rupee",
"fiatCurrencyCode": "NPR",
"symbol": "https://images-currency.meld.io/fiat/NPR/symbol.png"
},
{
"displayName": "New Taiwan Dollar",
"fiatCurrencyCode": "TWD",
"symbol": "https://images-currency.meld.io/fiat/TWD/symbol.png"
},
{
"displayName": "New Zealand Dollar",
"fiatCurrencyCode": "NZD",
"symbol": "https://images-currency.meld.io/fiat/NZD/symbol.png"
},
{
"displayName": "Norwegian Krone",
"fiatCurrencyCode": "NOK",
"symbol": "https://images-currency.meld.io/fiat/NOK/symbol.png"
},
{
"displayName": "Pakistan Rupee",
"fiatCurrencyCode": "PKR",
"symbol": "https://images-currency.meld.io/fiat/PKR/symbol.png"
},
{
"displayName": "Peso Uruguayo",
"fiatCurrencyCode": "UYU",
"symbol": "https://images-currency.meld.io/fiat/UYU/symbol.png"
},
{
"displayName": "Philippine Peso",
"fiatCurrencyCode": "PHP",
"symbol": "https://images-currency.meld.io/fiat/PHP/symbol.png"
},
{
"displayName": "Pound Sterling",
"fiatCurrencyCode": "GBP",
"symbol": "https://images-currency.meld.io/fiat/GBP/symbol.png"
},
{
"displayName": "Quetzal",
"fiatCurrencyCode": "GTQ",
"symbol": "https://images-currency.meld.io/fiat/GTQ/symbol.png"
},
{
"displayName": "Rand",
"fiatCurrencyCode": "ZAR",
"symbol": "https://images-currency.meld.io/fiat/ZAR/symbol.png"
},
{
"displayName": "Riel",
"fiatCurrencyCode": "KHR",
"symbol": "https://images-currency.meld.io/fiat/KHR/symbol.png"
},
{
"displayName": "Romanian Leu",
"fiatCurrencyCode": "RON",
"symbol": "https://images-currency.meld.io/fiat/RON/symbol.png"
},
{
"displayName": "Rupiah",
"fiatCurrencyCode": "IDR",
"symbol": "https://images-currency.meld.io/fiat/IDR/symbol.png"
},
{
"displayName": "Serbian Dinar",
"fiatCurrencyCode": "RSD",
"symbol": "https://images-currency.meld.io/fiat/RSD/symbol.png"
},
{
"displayName": "Singapore Dollar",
"fiatCurrencyCode": "SGD",
"symbol": "https://images-currency.meld.io/fiat/SGD/symbol.png"
},
{
"displayName": "Sol",
"fiatCurrencyCode": "PEN",
"symbol": "https://images-currency.meld.io/fiat/PEN/symbol.png"
},
{
"displayName": "Sri Lanka Rupee",
"fiatCurrencyCode": "LKR",
"symbol": "https://images-currency.meld.io/fiat/LKR/symbol.png"
},
{
"displayName": "Swedish Krona",
"fiatCurrencyCode": "SEK",
"symbol": "https://images-currency.meld.io/fiat/SEK/symbol.png"
},
{
"displayName": "Swiss Franc",
"fiatCurrencyCode": "CHF",
"symbol": "https://images-currency.meld.io/fiat/CHF/symbol.png"
},
{
"displayName": "Tenge",
"fiatCurrencyCode": "KZT",
"symbol": "https://images-currency.meld.io/fiat/KZT/symbol.png"
},
{
"displayName": "Tugrik",
"fiatCurrencyCode": "MNT",
"symbol": "https://images-currency.meld.io/fiat/MNT/symbol.png"
},
{
"displayName": "Turkish Lira",
"fiatCurrencyCode": "TRY",
"symbol": "https://images-currency.meld.io/fiat/TRY/symbol.png"
},
{
"displayName": "UAE Dirham",
"fiatCurrencyCode": "AED",
"symbol": "https://images-currency.meld.io/fiat/AED/symbol.png"
},
{
"displayName": "US Dollar",
"fiatCurrencyCode": "USD",
"symbol": "https://images-currency.meld.io/fiat/USD/symbol.png"
},
{
"displayName": "Uganda Shilling",
"fiatCurrencyCode": "UGX",
"symbol": "https://images-currency.meld.io/fiat/UGX/symbol.png"
},
{
"displayName": "Uzbekistan Sum",
"fiatCurrencyCode": "UZS",
"symbol": "https://images-currency.meld.io/fiat/UZS/symbol.png"
},
{
"displayName": "Yemeni Rial",
"fiatCurrencyCode": "YER",
"symbol": "https://images-currency.meld.io/fiat/YER/symbol.png"
},
{
"displayName": "Zambian Kwacha",
"fiatCurrencyCode": "ZMW",
"symbol": "https://images-currency.meld.io/fiat/ZMW/symbol.png"
},
{
"displayName": "Zloty",
"fiatCurrencyCode": "PLN",
"symbol": "https://images-currency.meld.io/fiat/PLN/symbol.png"
},
{
"fiatCurrencyCode": "EGP",
"displayName": "Egyptian Pound",
"symbol": "https://images-currency.meld.io/fiat/EGP/symbol.png"
},
{
"fiatCurrencyCode": "ILS",
"displayName": "Israeli New Shekel",
"symbol": "https://images-currency.meld.io/fiat/ILS/symbol.png"
},
{
"fiatCurrencyCode": "OMR",
"displayName": "Omani Rial",
"symbol": "https://images-currency.meld.io/fiat/OMR/symbol.png"
},
{
"fiatCurrencyCode": "VND",
"displayName": "Vietnamese Dong",
"symbol": "https://images-currency.meld.io/fiat/VND/symbol.png"
}
]
}
{
"supportedTokens": [
{
"cryptoCurrencyCode": "ETH",
"displayName": "Ethereum",
"address": null,
"cryptoCurrencyChain": "Ethereum",
"chainId": "1",
"symbol": "https://images-currency.meld.io/crypto/ETH/symbol.png"
},
{
"cryptoCurrencyCode": "USDC",
"displayName": "USD Coin",
"address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"cryptoCurrencyChain": "Ethereum",
"chainId": "1",
"symbol": "https://images-currency.meld.io/crypto/USDC/symbol.png"
},
{
"cryptoCurrencyCode": "USDT",
"displayName": "Tether",
"address": "0xdac17f958d2ee523a2206206994597c13d831ec7",
"cryptoCurrencyChain": "Ethereum",
"chainId": "1",
"symbol": "https://images-currency.meld.io/crypto/USDT/symbol.png"
},
{
"cryptoCurrencyCode": "DAI",
"displayName": "Dai",
"address": "0x6b175474e89094c44da98b954eedeac495271d0f",
"cryptoCurrencyChain": "Ethereum",
"chainId": "1",
"symbol": "https://images-currency.meld.io/crypto/DAI/symbol.png"
},
{
"cryptoCurrencyCode": "WBTC",
"displayName": "Wrapped Bitcoin",
"address": "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599",
"cryptoCurrencyChain": "Ethereum",
"chainId": "1",
"symbol": "https://images-currency.meld.io/crypto/WBTC/symbol.png"
},
{
"cryptoCurrencyCode": "WETH",
"displayName": "Wrapped Ether (ERC-20)",
"address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
"cryptoCurrencyChain": "ethereum",
"chainId": "1",
"symbol": ""
},
{
"cryptoCurrencyCode": "ETH_BASE",
"displayName": "Ethereum",
"address": null,
"cryptoCurrencyChain": "Base",
"chainId": "8453",
"symbol": "https://images-currency.meld.io/crypto/ETH_BASE/symbol.png"
},
{
"cryptoCurrencyCode": "USDC_BASE",
"displayName": "USD Coin",
"address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"cryptoCurrencyChain": "Base",
"chainId": "8453",
"symbol": "https://images-currency.meld.io/crypto/USDC_BASE/symbol.png"
},
{
"cryptoCurrencyCode": "ETH_OPTIMISM",
"displayName": "Ethereum",
"address": null,
"cryptoCurrencyChain": "Optimism",
"chainId": "10",
"symbol": "https://images-currency.meld.io/crypto/ETH_OPTIMISM/symbol.png"
},
{
"cryptoCurrencyCode": "USDC_OPTIMISM",
"displayName": "USD Coin",
"address": "0x7f5c764cbc14f9669b88837ca1490cca17c31607",
"cryptoCurrencyChain": "Optimism",
"chainId": "10",
"symbol": "https://images-currency.meld.io/crypto/USDC_OPTIMISM/symbol.png"
},
{
"cryptoCurrencyCode": "ETH_ARBITRUM",
"displayName": "Ethereum",
"address": null,
"cryptoCurrencyChain": "Arbitrum",
"chainId": "42161",
"symbol": "https://images-currency.meld.io/crypto/ETH_ARBITRUM/symbol.png"
},
{
"cryptoCurrencyCode": "USDC_ARBITRUM",
"displayName": "USD Coin",
"address": "0xaf88d065e77c8cc2239327c5edb3a432268e5831",
"cryptoCurrencyChain": "Arbitrum",
"chainId": "42161",
"symbol": "https://images-currency.meld.io/crypto/USDC_ARBITRUM/symbol.png"
},
{
"cryptoCurrencyCode": "MATIC",
"displayName": "Matic",
"address": null,
"cryptoCurrencyChain": "Polygon",
"chainId": "137",
"symbol": "https://images-currency.meld.io/crypto/MATIC/symbol.png"
}
]
}
{
"errorCode": "QUOTE_ERROR",
"detail": "No quotes available",
"id": "63363cc1-d474-4584-b386-7c356814b79f"
}
\ No newline at end of file
{
"alpha2": "US",
"alpha3": "USA",
"country": "United States of America",
"ipAddress": "142.154.213.230",
"isAllowed": true,
"isBuyAllowed": false,
"isNftAllowed": true,
"isSellAllowed": false,
"isBalanceLedgerWithdrawAllowed": true,
"isLowLimitEnabled": false,
"state": "NY"
}
{
"alpha2": "US",
"alpha3": "USA",
"country": "United States of America",
"ipAddress": "142.154.213.230",
"isAllowed": true,
"isBuyAllowed": true,
"isNftAllowed": true,
"isSellAllowed": true,
"isBalanceLedgerWithdrawAllowed": true,
"isLowLimitEnabled": true,
"state": "NY"
}
This diff is collapsed.
This diff is collapsed.
{"data":{"searchTokens":[{"id":"VG9rZW46WktTWU5DX251bGw=","chain":"ZKSYNC","address":null,"decimals":18,"symbol":"ETH","name":"Ethereum","project":{"id":"VG9rZW5Qcm9qZWN0OkVUSEVSRVVNX251bGxfRXRoZXJldW0=","logoUrl":"https://token-icons.s3.amazonaws.com/eth.png","safetyLevel":"VERIFIED","__typename":"TokenProject"},"protectionInfo":null,"__typename":"Token"},{"id":"VG9rZW46V09STERDSEFJTl9udWxs","chain":"WORLDCHAIN","address":null,"decimals":18,"symbol":"ETH","name":"Ethereum","project":{"id":"VG9rZW5Qcm9qZWN0OkVUSEVSRVVNX251bGxfRXRoZXJldW0=","logoUrl":"https://token-icons.s3.amazonaws.com/eth.png","safetyLevel":"VERIFIED","__typename":"TokenProject"},"protectionInfo":null,"__typename":"Token"},{"id":"VG9rZW46QkxBU1RfbnVsbA==","chain":"BLAST","address":null,"decimals":18,"symbol":"ETH","name":"Ethereum","project":{"id":"VG9rZW5Qcm9qZWN0OkVUSEVSRVVNX251bGxfRXRoZXJldW0=","logoUrl":"https://token-icons.s3.amazonaws.com/eth.png","safetyLevel":"VERIFIED","__typename":"TokenProject"},"protectionInfo":null,"__typename":"Token"},{"id":"VG9rZW46Wk9SQV9udWxs","chain":"ZORA","address":null,"decimals":18,"symbol":"ETH","name":"Ethereum","project":{"id":"VG9rZW5Qcm9qZWN0OkVUSEVSRVVNX251bGxfRXRoZXJldW0=","logoUrl":"https://token-icons.s3.amazonaws.com/eth.png","safetyLevel":"VERIFIED","__typename":"TokenProject"},"protectionInfo":null,"__typename":"Token"},{"id":"VG9rZW46RVRIRVJFVU1fbnVsbA==","chain":"ETHEREUM","address":null,"decimals":18,"symbol":"ETH","name":"Ethereum","project":{"id":"VG9rZW5Qcm9qZWN0OkVUSEVSRVVNX251bGxfRXRoZXJldW0=","logoUrl":"https://token-icons.s3.amazonaws.com/eth.png","safetyLevel":"VERIFIED","__typename":"TokenProject"},"protectionInfo":null,"__typename":"Token"},{"id":"VG9rZW46QVJCSVRSVU1fbnVsbA==","chain":"ARBITRUM","address":null,"decimals":18,"symbol":"ETH","name":"Ethereum","project":{"id":"VG9rZW5Qcm9qZWN0OkVUSEVSRVVNX251bGxfRXRoZXJldW0=","logoUrl":"https://token-icons.s3.amazonaws.com/eth.png","safetyLevel":"VERIFIED","__typename":"TokenProject"},"protectionInfo":null,"__typename":"Token"},{"id":"VG9rZW46T1BUSU1JU01fbnVsbA==","chain":"OPTIMISM","address":null,"decimals":18,"symbol":"ETH","name":"Ethereum","project":{"id":"VG9rZW5Qcm9qZWN0OkVUSEVSRVVNX251bGxfRXRoZXJldW0=","logoUrl":"https://token-icons.s3.amazonaws.com/eth.png","safetyLevel":"VERIFIED","__typename":"TokenProject"},"protectionInfo":null,"__typename":"Token"},{"id":"VG9rZW46T1BUSU1JU01fbnVsbA==","chain":"BASE","address":null,"decimals":18,"symbol":"ETH","name":"Ethereum","project":{"id":"VG9rZW5Qcm9qZWN0OkVUSEVSRVVNX251bGxfRXRoZXJldW0=","logoUrl":"https://token-icons.s3.amazonaws.com/eth.png","safetyLevel":"VERIFIED","__typename":"TokenProject"},"protectionInfo":null,"__typename":"Token"},{"id":"VG9rZW46RVRIRVJFVU1fMHhDMDJhYUEzOWIyMjNGRThEMEEwZTVDNEYyN2VBRDkwODNDNzU2Q2My","chain":"ETHEREUM","address":"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2","decimals":18,"symbol":"WETH","name":"Wrapped Ether","project":{"id":"VG9rZW5Qcm9qZWN0OkVUSEVSRVVNXzB4YzAyYWFhMzliMjIzZmU4ZDBhMGU1YzRmMjdlYWQ5MDgzYzc1NmNjMl9XRVRI","logoUrl":"https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png","safetyLevel":"VERIFIED","__typename":"TokenProject"},"protectionInfo":{"result":"BENIGN","attackTypes":[],"__typename":"ProtectionInfo"},"__typename":"Token"},{"id":"VG9rZW46QVJCSVRSVU1fMHg4MmFmNDk0NDdkOGEwN2UzYmQ5NWJkMGQ1NmYzNTI0MTUyM2ZiYWIx","chain":"ARBITRUM","address":"0x82af49447d8a07e3bd95bd0d56f35241523fbab1","decimals":18,"symbol":"WETH","name":"Wrapped Ether","project":{"id":"VG9rZW5Qcm9qZWN0OkVUSEVSRVVNXzB4YzAyYWFhMzliMjIzZmU4ZDBhMGU1YzRmMjdlYWQ5MDgzYzc1NmNjMl9BcmJpdHJ1bSBCcmlkZ2VkIFdFVEggKEFyYml0cnVtIE9uZSk=","logoUrl":"https://coin-images.coingecko.com/coins/images/39713/large/WETH.PNG?1723731496","safetyLevel":"VERIFIED","__typename":"TokenProject"},"protectionInfo":{"result":"BENIGN","attackTypes":[],"__typename":"ProtectionInfo"},"__typename":"Token"}]}}
\ No newline at end of file
{"data":{"searchTokens":[{"id":"VG9rZW46RVRIRVJFVU1fMHhhMGI4Njk5MWM2MjE4YjM2YzFkMTlkNGEyZTllYjBjZTM2MDZlYjQ4","chain":"ETHEREUM","address":"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48","decimals":6,"symbol":"USDC","project":{"id":"VG9rZW5Qcm9qZWN0OkVUSEVSRVVNXzB4YTBiODY5OTFjNjIxOGIzNmMxZDE5ZDRhMmU5ZWIwY2UzNjA2ZWI0OF9VU0RD","name":"USDC","logoUrl":"https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694","safetyLevel":"VERIFIED","__typename":"TokenProject"},"__typename":"Token"}]}}
\ No newline at end of file
{
"data": {
"token": {
"id": "VG9rZW46RVRIRVJFVU1fbnVsbA==",
"address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"chain": "ETHEREUM",
"symbol": "USDC",
"name": "USD Coin",
"decimals": 6,
"standard": "ERC20",
"project": {
"id": "VG9rZW5Qcm9qZWN0OkVUSEVSRVVNX251bGxfRXRoZXJldW0=",
"name": "Circle: USDC",
"logo": {
"id": "SW1hZ2U6aHR0cHM6Ly90b2tlbi1pY29ucy5zMy5hbWF6b25hd3MuY29tL2V0aC5wbmc=",
"url": "https://etherscan.io/token/images/centre-usdc_28.png",
"__typename": "Image"
},
"safetyLevel": "VERIFIED",
"logoUrl": "https://etherscan.io/token/images/centre-usdc_28.png",
"isSpam": false,
"__typename": "TokenProject"
},
"__typename": "Token"
}
}
}
{"data":{"tokenProjects":[{"id":"VG9rZW5Qcm9qZWN0OkVUSEVSRVVNX251bGxfRXRoZXJldW0=","name":"Ethereum","logoUrl":"https://token-icons.s3.amazonaws.com/eth.png","safetyLevel":"VERIFIED","tokens":[{"chain":"ETHEREUM","address":null,"decimals":18,"symbol":"ETH","__typename":"Token"},{"chain":"ARBITRUM","address":null,"decimals":18,"symbol":"ETH","__typename":"Token"},{"chain":"OPTIMISM","address":null,"decimals":18,"symbol":"ETH","__typename":"Token"},{"chain":"BASE","address":null,"decimals":18,"symbol":"ETH","__typename":"Token"},{"chain":"BLAST","address":null,"decimals":18,"symbol":"ETH","__typename":"Token"},{"chain":"ZORA","address":null,"decimals":18,"symbol":"ETH","__typename":"Token"},{"chain":"ZKSYNC","address":null,"decimals":18,"symbol":"ETH","__typename":"Token"}],"__typename":"TokenProject"}]}}
\ No newline at end of file
This diff is collapsed.
{"data":{"token":{"id":"VG9rZW46RVRIRVJFVU1fMHgxZjk4NDBhODVkNWFmNWJmMWQxNzYyZjkyNWJkYWRkYzQyMDFmOTg0","address":"0x1f9840a85d5af5bf1d1762f925bdaddc4201f984","chain":"ETHEREUM","decimals":18,"name":"Uniswap","standard":"ERC20","symbol":"UNI","project":{"id":"VG9rZW5Qcm9qZWN0OkVUSEVSRVVNXzB4MWY5ODQwYTg1ZDVhZjViZjFkMTc2MmY5MjViZGFkZGM0MjAxZjk4NF9Vbmlzd2Fw","isSpam":false,"logoUrl":"https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/logo.png","name":"Uniswap","safetyLevel":"VERIFIED","__typename":"TokenProject"},"feeData":{"buyFeeBps":null,"sellFeeBps":null,"__typename":"FeeData"},"__typename":"Token"}}}
\ No newline at end of file
import { getTestSelector } from '../utils'
describe('translations', () => {
// TODO re-enable web test
it.skip('loads locale from the query param', () => {
cy.visit('/?lng=fr-FR')
cy.contains('Échanger')
cy.contains('Uniswap disponible en : English')
})
// TODO re-enable web test
it.skip('loads locale from menu', () => {
cy.visit('/')
cy.get(getTestSelector('web3-status-connected')).click()
cy.get(getTestSelector('wallet-settings')).click()
cy.get(getTestSelector('wallet-language-item')).contains('français').click({ force: true })
cy.location('search').should('match', /\?lng=fr-FR$/)
cy.contains('Échanger')
cy.contains('Uniswap disponible en : English')
})
})
import 'cypress-hardhat/lib/browser'
import { Eip1193 } from 'cypress-hardhat/lib/browser/eip1193'
import { FeatureFlagClient, FeatureFlags, getFeatureFlagName } from 'uniswap/src/features/gating/flags'
import { ALLOW_ANALYTICS_ATOM_KEY } from 'utilities/src/telemetry/analytics/constants'
import { UserState, initialState } from '../../src/state/user/reducer'
import { setInitialUserState } from '../utils/user-state'
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface ApplicationWindow {
ethereum: Eip1193
}
interface Chainable<Subject> {
/**
* Wait for a specific event to be sent to amplitude. If the event is found, the subject will be the event.
*
* @param {string} eventName - The type of the event to search for e.g. SwapEventName.SwapTransactionCompleted
* @param {number} timeout - The maximum amount of time (in ms) to wait for the event.
* @returns {Chainable<Subject>}
*/
waitForAmplitudeEvent(eventName: string, requiredProperties?: string[]): Chainable<Subject>
/**
* Intercepts a specific graphql operation and responds with the given fixture.
* @param {string} operationName - The name of the graphql operation to intercept.
* @param {string} fixturePath - The path to the fixture to respond with.
*/
interceptGraphqlOperation(operationName: string, fixturePath: string): Chainable<Subject>
/**
* Intercepts a quote request and responds with the given fixture.
* @param {string} fixturePath - The path to the fixture to respond with.
*/
interceptQuoteRequest(fixturePath: string): Chainable<Subject>
}
interface Cypress {
eagerlyConnect?: boolean
}
interface VisitOptions {
featureFlags?: Array<{ flag: FeatureFlags; value: boolean }>
/**
* Initial user state.
* @default {@type import('../utils/user-state').CONNECTED_WALLET_USER_STATE}
*/
userState?: Partial<UserState>
/**
* If false, prevents the app from eagerly connecting to the injected provider.
* @default true
*/
eagerlyConnect?: false
}
}
}
export function registerCommands() {
// sets up the injected provider to be a mock ethereum provider with the given mnemonic/index
// eslint-disable-next-line no-undef
Cypress.Commands.overwrite(
'visit',
// eslint-disable-next-line max-params
(original, url: string | Partial<Cypress.VisitOptions>, options?: Partial<Cypress.VisitOptions>) => {
if (typeof url !== 'string') {
throw new Error('Invalid arguments. The first argument to cy.visit must be the path.')
}
// Parse overrides
const flagsOn: FeatureFlags[] = []
const flagsOff: FeatureFlags[] = []
options?.featureFlags?.forEach((f) => {
if (f.value) {
flagsOn.push(f.flag)
} else {
flagsOff.push(f.flag)
}
})
// Format into URL parameters
const overrideParams = new URLSearchParams()
if (flagsOn.length > 0) {
overrideParams.append(
'featureFlagOverride',
flagsOn.map((flag) => getFeatureFlagName(flag, FeatureFlagClient.Web)).join(','),
)
}
if (flagsOff.length > 0) {
overrideParams.append(
'featureFlagOverrideOff',
flagsOn.map((flag) => getFeatureFlagName(flag, FeatureFlagClient.Web)).join(','),
)
}
return cy.provider().then((provider) =>
original({
...options,
url:
[...overrideParams.entries()].length === 0
? url
: url.includes('?')
? `${url}&${overrideParams.toString()}`
: `${url}?${overrideParams.toString()}`,
onBeforeLoad(win) {
options?.onBeforeLoad?.(win)
setInitialUserState(win, {
...initialState,
...(options?.userState ?? {}),
})
win.ethereum = provider
win.Cypress.eagerlyConnect = options?.eagerlyConnect ?? true
win.localStorage.setItem(ALLOW_ANALYTICS_ATOM_KEY, 'true')
win.localStorage.setItem('showUniswapExtensionLaunchAtom', 'false')
},
}),
)
},
)
Cypress.Commands.add('waitForAmplitudeEvent', (eventName, requiredProperties) => {
function findAndDiscardEventsUpToTarget() {
const events = Cypress.env('amplitudeEventCache')
const targetEventIndex = events.findIndex((event) => {
if (event.event_type !== eventName) {
return false
}
if (requiredProperties) {
return requiredProperties.every((prop) => event.event_properties[prop])
}
return true
})
if (targetEventIndex !== -1) {
const event = events[targetEventIndex]
Cypress.env('amplitudeEventCache', events.slice(targetEventIndex + 1))
return cy.wrap(event)
} else {
// If not found, retry after waiting for more events to be sent.
return cy.wait('@amplitude').then(findAndDiscardEventsUpToTarget)
}
}
return findAndDiscardEventsUpToTarget()
})
Cypress.env('graphqlInterceptions', new Map())
Cypress.Commands.add('interceptGraphqlOperation', (operationName, fixturePath) => {
const graphqlInterceptions = Cypress.env('graphqlInterceptions')
cy.intercept(/(?:interface|beta).gateway.uniswap.org\/v1\/graphql/, (req) => {
req.headers['origin'] = 'https://app.uniswap.org'
const currentOperationName = req.body.operationName
if (graphqlInterceptions.has(currentOperationName)) {
const fixturePath = graphqlInterceptions.get(currentOperationName)
req.reply({ fixture: fixturePath })
} else {
req.continue()
}
}).as(operationName)
graphqlInterceptions.set(operationName, fixturePath)
})
Cypress.Commands.add('interceptQuoteRequest', (fixturePath) => {
return cy.intercept(/(?:interface|beta).gateway.uniswap.org\/v2\/quote/, (req) => {
req.headers['origin'] = 'https://app.uniswap.org'
req.reply({ fixture: fixturePath })
})
})
}
// ***********************************************************
// This file is processed and loaded automatically before your test files.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
import { registerCommands } from './commands'
import { registerSetupTests } from './setupTests'
// In order to use cypress commands with sideEffects set in package.json
// we need to import the commands and setupTests files here.
// See: https://github.com/cypress-io/cypress-documentation/pull/5454/files
registerCommands()
registerSetupTests()
// Squelch logs from fetches, as they clutter the logs so much as to make them unusable.
// See https://docs.cypress.io/api/commands/intercept#Disabling-logs-for-a-request.
// TODO(https://github.com/cypress-io/cypress/issues/26069): Squelch only wildcard logs once Cypress allows it.
const log = Cypress.log
Cypress.log = function (options, ...args) {
if (options.displayName === 'script' || options.name === 'request') {
return undefined
}
return log(options, ...args)
} as typeof log
Cypress.on('uncaught:exception', () => {
// returning false here prevents Cypress from failing the test
return false
})
import { CyHttpMessages } from 'cypress/types/net-stubbing'
import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { revertHardhat, setupHardhat } from '../utils'
export function registerSetupTests() {
beforeEach(() => {
// Many API calls enforce that requests come from our app, so we must mock Origin and Referer.
cy.intercept('*', (req) => {
req.headers['referer'] = 'https://app.uniswap.org'
req.headers['origin'] = 'https://app.uniswap.org'
})
// Network RPCs are disabled for cypress tests - calls should be routed through the connected wallet instead.
cy.intercept(/infura.io/, { statusCode: 404 })
cy.intercept(/quiknode.pro/, { statusCode: 404 })
// Log requests to hardhat.
cy.intercept(/:8545/, logJsonRpc)
Cypress.env('amplitudeEventCache', [])
// Mock analytics responses to avoid analytics in tests.
cy.intercept('https://metrics.interface.gateway.uniswap.org/v1/amplitude-proxy', (req) => {
const requestBody = JSON.stringify(req.body)
const byteSize = new Blob([requestBody]).size
req.alias = 'amplitude'
req.reply(
JSON.stringify({
code: 200,
server_upload_time: Date.now(),
payload_size_bytes: byteSize,
events_ingested: req.body.events.length,
}),
{
'origin-country': 'US',
},
)
Cypress.env('amplitudeEventCache').push(...req.body.events)
})
// Mock statsig to allow us to mock flags.
cy.intercept(/statsig/, { statusCode: 409 })
// Mock graphql ops required by TokenSelector
cy.interceptGraphqlOperation('PortfolioBalances', 'portfolio_balances.json').as('PortfolioBalances')
cy.interceptGraphqlOperation('QuickTokenBalancesWeb', 'quick_token_balances.json').as('QuickTokenBalancesWeb')
cy.interceptGraphqlOperation('TopTokens', 'top_tokens.json').as('TopTokens')
cy.interceptGraphqlOperation('TokenProjects', 'token_projects.json').as('TokenProjects')
})
// Reset hardhat between suites to ensure isolation.
// This resets the fork, as well as options like automine.
before(() => cy.hardhat().then((hardhat) => hardhat.reset(UniverseChainId.Mainnet)))
// Reverts hardhat between tests to ensure isolation.
// This reverts the fork, but not options like automine.
setupHardhat()
beforeEach(revertHardhat)
}
function logJsonRpc(req: CyHttpMessages.IncomingHttpRequest) {
req.alias = req.body.method
const log = Cypress.log({
autoEnd: false,
name: req.body.method,
message: req.body.params?.map((param: any) =>
typeof param === 'object' ? '{...}' : param?.toString().substring(0, 10),
),
})
req.on('after:response', (res) => {
if (res.statusCode === 200) {
log.end()
} else {
log.error(new Error(`${res.statusCode}: ${res.statusMessage}`))
}
})
}
{
"extends": "../tsconfig.json",
"compilerOptions": {
"composite": false,
"incremental": true,
"isolatedModules": false,
"noImplicitAny": false,
"sourceMap": false,
"target": "ES6",
"tsBuildInfoFile": "../node_modules/.cache/tsbuildinfo/cypress", // avoid clobbering the build tsbuildinfo
"types": ["cypress", "node"],
},
"include": ["**/*.ts"],
"references": [
{
"path": "../../../packages/ui"
},
{
"path": "../../../packages/utilities"
},
{
"path": "../../../packages/uniswap"
}
]
}
import { HardhatProvider } from 'cypress-hardhat/lib/browser/provider'
import { Utils } from 'cypress-hardhat/lib/browser/utils'
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Mocha {
interface Context {
snapshots?: string[]
}
}
}
// Extended timeout for hardhat to mine a single block.
// Use as part of cy.hardhat().then({ timeout: HARDHAT_TIMEOUT }, (hardhat) => ...),
// but limit the hardhat usage *per thennable* to a single mined block.
export const HARDHAT_TIMEOUT = 48_000
export const getTestSelector = (selectorId: string) => `[data-testid=${selectorId}]`
/**
* Sets up hardhat, and reverts it after tests to ensure isolation.
* This reverts the fork, but not options like automine.
*/
export function setupHardhat(fn?: (hardhat: Utils) => Promise<void>) {
let snapshot: string
before(function () {
// This stack - on the Mocha Context - tracks all snapshots derived from setupHardhat.
this.snapshots ||= []
return cy.hardhat().then({ timeout: HARDHAT_TIMEOUT }, async (hardhat) => {
await fn?.(hardhat)
snapshot = await hardhat.send('evm_snapshot', [])
this.snapshots?.push(snapshot)
})
})
after(function () {
this.snapshots?.pop()
})
}
//
// Reverts hardhat to the top snapshot on the stack.
// Must only be called once per test. Should only be called in setupTests.ts.
export function revertHardhat(this: Mocha.Context) {
return cy.hardhat().then({ timeout: HARDHAT_TIMEOUT }, async (hardhat) => {
const snapshot = this.snapshots?.pop()
// Only revert the latest snapshot, as reverting past that will invalidate other snapshots.
if (snapshot) {
await hardhat.send('evm_revert', [snapshot])
// Providers will not "rewind" to an older block number nor notice chain changes, so they must be reset.
hardhat.providers.forEach((provider) => (provider as HardhatProvider).reset())
this.snapshots?.push(await hardhat.send('evm_snapshot', []))
}
})
}
/** Revert back to MAINNET. Used after each test which changes chains. */
export function resetHardhatChain() {
cy.hardhat().then((hardhat) => {
// Intentionally not awaited for, to avoid stalling in case the method is stubbed.
hardhat.send('wallet_switchEthereumChain', [{ chainId: '0x1' }])
})
}
import { UserState } from '../../src/state/user/reducer'
/**
* This sets the initial value of the "user" slice in localStorage.
* Other persisted slices are not set, so they will be filled with their respective initial values
* when the app runs.
*/
export function setInitialUserState(win: Cypress.AUTWindow, state: UserState) {
// We want to test from a clean state, so we clear the local storage (which clears redux).
win.localStorage.clear()
// Set initial user state.
win.localStorage.setItem(
'redux/persist:interface',
JSON.stringify({
user: state,
})
)
}
...@@ -94,7 +94,7 @@ function PoolImage({ ...@@ -94,7 +94,7 @@ function PoolImage({
) )
} }
export const onRequest: PagesFunction = async ({ params, request }) => { export const onRequest: PagesFunction = async ({ params, request, env }) => {
try { try {
const origin = new URL(request.url).origin const origin = new URL(request.url).origin
const { index } = params const { index } = params
...@@ -112,7 +112,7 @@ export const onRequest: PagesFunction = async ({ params, request }) => { ...@@ -112,7 +112,7 @@ export const onRequest: PagesFunction = async ({ params, request }) => {
return new Response('Pool not found.', { status: 404 }) return new Response('Pool not found.', { status: 404 })
} }
const [fontData] = await Promise.all([getFont(origin)]) const [fontData] = await Promise.all([getFont(origin, env)])
const networkLogo = getNetworkLogoUrl(networkName.toUpperCase(), origin) const networkLogo = getNetworkLogoUrl(networkName.toUpperCase(), origin)
return new ImageResponse( return new ImageResponse(
...@@ -156,6 +156,7 @@ export const onRequest: PagesFunction = async ({ params, request }) => { ...@@ -156,6 +156,7 @@ export const onRequest: PagesFunction = async ({ params, request }) => {
<img <img
src={networkLogo} src={networkLogo}
width="48px" width="48px"
height="48px"
style={{ style={{
position: 'absolute', position: 'absolute',
right: '2px', right: '2px',
......
...@@ -8,7 +8,7 @@ import { getRGBColor } from '../../../utils/getRGBColor' ...@@ -8,7 +8,7 @@ import { getRGBColor } from '../../../utils/getRGBColor'
import { getRequest } from '../../../utils/getRequest' import { getRequest } from '../../../utils/getRequest'
import getToken from '../../../utils/getToken' import getToken from '../../../utils/getToken'
export const onRequest: PagesFunction = async ({ params, request }) => { export const onRequest: PagesFunction = async ({ params, request, env }) => {
try { try {
const origin = new URL(request.url).origin const origin = new URL(request.url).origin
const { index } = params const { index } = params
...@@ -27,7 +27,7 @@ export const onRequest: PagesFunction = async ({ params, request }) => { ...@@ -27,7 +27,7 @@ export const onRequest: PagesFunction = async ({ params, request }) => {
return new Response('Token not found.', { status: 404 }) return new Response('Token not found.', { status: 404 })
} }
const [fontData, palette] = await Promise.all([getFont(origin), getRGBColor(data.ogImage, true)]) const [fontData, palette] = await Promise.all([getFont(origin, env), getRGBColor(data.ogImage, true)])
const networkLogo = getNetworkLogoUrl(networkName.toUpperCase(), origin) const networkLogo = getNetworkLogoUrl(networkName.toUpperCase(), origin)
...@@ -66,11 +66,12 @@ export const onRequest: PagesFunction = async ({ params, request }) => { ...@@ -66,11 +66,12 @@ export const onRequest: PagesFunction = async ({ params, request }) => {
}} }}
> >
{ogImage ? ( {ogImage ? (
<img src={ogImage} width="144px" style={{ borderRadius: '100%' }}> <img src={ogImage} width="144px" height="144px" style={{ borderRadius: '100%' }}>
{networkLogo != '' && ( {networkLogo != '' && (
<img <img
src={networkLogo} src={networkLogo}
width="48px" width="48px"
height="48px"
style={{ style={{
position: 'absolute', position: 'absolute',
right: '2px', right: '2px',
...@@ -105,6 +106,7 @@ export const onRequest: PagesFunction = async ({ params, request }) => { ...@@ -105,6 +106,7 @@ export const onRequest: PagesFunction = async ({ params, request }) => {
<img <img
src={networkLogo} src={networkLogo}
width="48px" width="48px"
height="48px"
style={{ style={{
position: 'absolute', position: 'absolute',
right: '2px', right: '2px',
......
...@@ -121,6 +121,7 @@ window.$RefreshSig$ = () => (type) => type;</script> ...@@ -121,6 +121,7 @@ window.$RefreshSig$ = () => (type) => type;</script>
<div id="root"></div> <div id="root"></div>
<div id="background-radial-gradient"></div> <div id="background-radial-gradient"></div>
<script type="module" src="/zone-events.js"></script>
<!-- Vite entry point --> <!-- Vite entry point -->
<script type="module" src="/src/index.tsx"></script> <script type="module" src="/src/index.tsx"></script>
</body> </body>
...@@ -249,6 +250,7 @@ window.$RefreshSig$ = () => (type) => type;</script> ...@@ -249,6 +250,7 @@ window.$RefreshSig$ = () => (type) => type;</script>
<div id="root"></div> <div id="root"></div>
<div id="background-radial-gradient"></div> <div id="background-radial-gradient"></div>
<script type="module" src="/zone-events.js"></script>
<!-- Vite entry point --> <!-- Vite entry point -->
<script type="module" src="/src/index.tsx"></script> <script type="module" src="/src/index.tsx"></script>
</body> </body>
...@@ -377,6 +379,7 @@ window.$RefreshSig$ = () => (type) => type;</script> ...@@ -377,6 +379,7 @@ window.$RefreshSig$ = () => (type) => type;</script>
<div id="root"></div> <div id="root"></div>
<div id="background-radial-gradient"></div> <div id="background-radial-gradient"></div>
<script type="module" src="/zone-events.js"></script>
<!-- Vite entry point --> <!-- Vite entry point -->
<script type="module" src="/src/index.tsx"></script> <script type="module" src="/src/index.tsx"></script>
</body> </body>
......
...@@ -121,6 +121,7 @@ window.$RefreshSig$ = () => (type) => type;</script> ...@@ -121,6 +121,7 @@ window.$RefreshSig$ = () => (type) => type;</script>
<div id="root"></div> <div id="root"></div>
<div id="background-radial-gradient"></div> <div id="background-radial-gradient"></div>
<script type="module" src="/zone-events.js"></script>
<!-- Vite entry point --> <!-- Vite entry point -->
<script type="module" src="/src/index.tsx"></script> <script type="module" src="/src/index.tsx"></script>
</body> </body>
...@@ -249,6 +250,7 @@ window.$RefreshSig$ = () => (type) => type;</script> ...@@ -249,6 +250,7 @@ window.$RefreshSig$ = () => (type) => type;</script>
<div id="root"></div> <div id="root"></div>
<div id="background-radial-gradient"></div> <div id="background-radial-gradient"></div>
<script type="module" src="/zone-events.js"></script>
<!-- Vite entry point --> <!-- Vite entry point -->
<script type="module" src="/src/index.tsx"></script> <script type="module" src="/src/index.tsx"></script>
</body> </body>
...@@ -377,6 +379,7 @@ window.$RefreshSig$ = () => (type) => type;</script> ...@@ -377,6 +379,7 @@ window.$RefreshSig$ = () => (type) => type;</script>
<div id="root"></div> <div id="root"></div>
<div id="background-radial-gradient"></div> <div id="background-radial-gradient"></div>
<script type="module" src="/zone-events.js"></script>
<!-- Vite entry point --> <!-- Vite entry point -->
<script type="module" src="/src/index.tsx"></script> <script type="module" src="/src/index.tsx"></script>
</body> </body>
...@@ -505,6 +508,7 @@ window.$RefreshSig$ = () => (type) => type;</script> ...@@ -505,6 +508,7 @@ window.$RefreshSig$ = () => (type) => type;</script>
<div id="root"></div> <div id="root"></div>
<div id="background-radial-gradient"></div> <div id="background-radial-gradient"></div>
<script type="module" src="/zone-events.js"></script>
<!-- Vite entry point --> <!-- Vite entry point -->
<script type="module" src="/src/index.tsx"></script> <script type="module" src="/src/index.tsx"></script>
</body> </body>
......
export default async function getFont(origin: string) { export default async function getFont(
origin: string,
env: {
ASSETS: {
fetch: typeof fetch
}
},
) {
try {
// Cloudflare Workers needs a full URL to fetch from,
// but will only use the pathname to fetch from the ASSETS binding.
const req = new Request('https://dummy.example/fonts/Inter-normal.var.ttf', {
headers: { 'Accept-Encoding': 'identity' }, // prevent TTF → WOFF2 rewrite
})
const font = await env.ASSETS.fetch(req)
if (!font.ok) {
throw new Error('Failed to fetch font from ASSETS binding')
}
return font.arrayBuffer()
} catch (e) {
// Fallback to fetching from the origin if the ASSETS binding is not available.
const url = origin + '/fonts/Inter-normal.var.ttf' const url = origin + '/fonts/Inter-normal.var.ttf'
const font = await fetch(url) const font = await fetch(url)
return font.arrayBuffer() return font.arrayBuffer()
}
} }
import { ChainId } from '@uniswap/sdk-core'
/* eslint-env node */
require('dotenv').config()
const forkingConfig = {
httpHeaders: {
Origin: 'localhost:3000', // infura allowlists requests by origin
},
}
const forks = {
[ChainId.MAINNET]: {
url: `https://mainnet.infura.io/v3/${process.env.REACT_APP_INFURA_KEY}`,
...forkingConfig,
},
[ChainId.POLYGON]: {
url: `https://polygon-mainnet.infura.io/v3/${process.env.REACT_APP_INFURA_KEY}`,
...forkingConfig,
},
[ChainId.OPTIMISM]: {
url: `https://optimism-mainnet.infura.io/v3/${process.env.REACT_APP_INFURA_KEY}`,
...forkingConfig,
},
}
module.exports = {
forks,
networks: {
hardhat: {
loggingEnabled: !process.env.CI,
chainId: ChainId.MAINNET,
forking: forks[ChainId.MAINNET],
accounts: {
count: 2,
},
mining: {
auto: true, // automine to make tests easier to write.
interval: 0, // do not interval mine so that tests remain deterministic
},
},
},
}
...@@ -111,6 +111,7 @@ ...@@ -111,6 +111,7 @@
<div id="root"></div> <div id="root"></div>
<div id="background-radial-gradient"></div> <div id="background-radial-gradient"></div>
<script type="module" src="/zone-events.js"></script>
<!-- Vite entry point --> <!-- Vite entry point -->
<script type="module" src="/src/index.tsx"></script> <script type="module" src="/src/index.tsx"></script>
</body> </body>
......
...@@ -5,8 +5,8 @@ ...@@ -5,8 +5,8 @@
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",
"scripts": { "scripts": {
"ajv": "node scripts/compile-ajv-validators.js", "ajv": "node scripts/compile-ajv-validators.js",
"anvil:mainnet": "dotenv -- bash -c 'RUST_LOG=debug anvil --fork-url https://${REACT_APP_QUICKNODE_ENDPOINT_NAME}.quiknode.pro/${REACT_APP_QUICKNODE_ENDPOINT_TOKEN} --hardfork prague'", "anvil:mainnet": "dotenv -- bash -c 'RUST_LOG=debug anvil --print-traces --fork-url https://${REACT_APP_QUICKNODE_ENDPOINT_NAME}.quiknode.pro/${REACT_APP_QUICKNODE_ENDPOINT_TOKEN} --hardfork prague'",
"anvil:base": "dotenv -- bash -c 'RUST_LOG=debug anvil --fork-url https://${REACT_APP_QUICKNODE_ENDPOINT_NAME}.base-mainnet.quiknode.pro/${REACT_APP_QUICKNODE_ENDPOINT_TOKEN} --hardfork prague --port 8546'", "anvil:base": "dotenv -- bash -c 'RUST_LOG=debug anvil --print-traces --fork-url https://${REACT_APP_QUICKNODE_ENDPOINT_NAME}.base-mainnet.quiknode.pro/${REACT_APP_QUICKNODE_ENDPOINT_TOKEN} --hardfork prague --port 8546'",
"check:deps:usage": "depcheck", "check:deps:usage": "depcheck",
"check:circular": "concurrently \"../../scripts/check-circular-imports.sh ./src/pages/App.tsx 3\" \"../../scripts/check-circular-imports.sh ./src/setupTests.ts 0\"", "check:circular": "concurrently \"../../scripts/check-circular-imports.sh ./src/pages/App.tsx 3\" \"../../scripts/check-circular-imports.sh ./src/setupTests.ts 0\"",
"sitemap:generate": "node scripts/generate-sitemap.js", "sitemap:generate": "node scripts/generate-sitemap.js",
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
"vite:build:production": "ROLLDOWN_OPTIONS_VALIDATION=loose vite build", "vite:build:production": "ROLLDOWN_OPTIONS_VALIDATION=loose vite build",
"vite:build:staging": "ROLLDOWN_OPTIONS_VALIDATION=loose vite build --mode staging", "vite:build:staging": "ROLLDOWN_OPTIONS_VALIDATION=loose vite build --mode staging",
"vite:preview": "ROLLDOWN_OPTIONS_VALIDATION=loose vite preview", "vite:preview": "ROLLDOWN_OPTIONS_VALIDATION=loose vite preview",
"functions:build": "wrangler --config ./wrangler-vite-worker.jsonc pages functions build --outdir=./build/worker/",
"start:cloud": "NODE_OPTIONS=--dns-result-order=ipv4first REACT_APP_SKIP_CSP=1 npx wrangler@4.14.4 pages dev --compatibility-flags=nodejs_compat --compatibility-date=2023-08-01 --proxy=3001 --port=3000 -- yarn vite:dev --port 3001", "start:cloud": "NODE_OPTIONS=--dns-result-order=ipv4first REACT_APP_SKIP_CSP=1 npx wrangler@4.14.4 pages dev --compatibility-flags=nodejs_compat --compatibility-date=2023-08-01 --proxy=3001 --port=3000 -- yarn vite:dev --port 3001",
"build:production": "NODE_OPTIONS=--max-old-space-size=8192 craco build", "build:production": "NODE_OPTIONS=--max-old-space-size=8192 craco build",
"build:production:analyze": "UNISWAP_ANALYZE_BUNDLE_SIZE=static craco build", "build:production:analyze": "UNISWAP_ANALYZE_BUNDLE_SIZE=static craco build",
...@@ -25,9 +26,8 @@ ...@@ -25,9 +26,8 @@
"lint": "yarn eslint --ignore-path .gitignore --cache --cache-location node_modules/.cache/eslint/ --max-warnings=0 .", "lint": "yarn eslint --ignore-path .gitignore --cache --cache-location node_modules/.cache/eslint/ --max-warnings=0 .",
"lint:fix": "yarn eslint --ignore-path .gitignore --cache --cache-location node_modules/.cache/eslint/ . --fix", "lint:fix": "yarn eslint --ignore-path .gitignore --cache --cache-location node_modules/.cache/eslint/ . --fix",
"playwright:test": "playwright test", "playwright:test": "playwright test",
"typecheck": "tsc && yarn typecheck:cloud && yarn typecheck:cypress", "typecheck": "tsc && yarn typecheck:cloud",
"typecheck:cloud": "tsc -p functions/tsconfig.json", "typecheck:cloud": "tsc -p functions/tsconfig.json",
"typecheck:cypress": "tsc -p cypress/tsconfig.json",
"find:unused": "bash scripts/delete-unused-assets.sh", "find:unused": "bash scripts/delete-unused-assets.sh",
"test": "NODE_OPTIONS='--no-deprecation' vitest run", "test": "NODE_OPTIONS='--no-deprecation' vitest run",
"test:set1": "NODE_OPTIONS='--no-deprecation' vitest run src/components", "test:set1": "NODE_OPTIONS='--no-deprecation' vitest run src/components",
...@@ -38,8 +38,6 @@ ...@@ -38,8 +38,6 @@
"test:bundle": "node -r esbuild-register ./src/test-utils/bundle-size-test.ts", "test:bundle": "node -r esbuild-register ./src/test-utils/bundle-size-test.ts",
"snapshots": "NODE_OPTIONS='--no-deprecation' vitest run -u", "snapshots": "NODE_OPTIONS='--no-deprecation' vitest run -u",
"test:cloud": "NODE_OPTIONS='--no-deprecation' vitest run functions --config functions/vitest.config.ts", "test:cloud": "NODE_OPTIONS='--no-deprecation' vitest run functions --config functions/vitest.config.ts",
"cypress:open": "cypress open --browser chrome --e2e",
"cypress:run": "cypress run --browser chrome --e2e",
"deduplicate": "yarn-deduplicate --strategy=highest", "deduplicate": "yarn-deduplicate --strategy=highest",
"storybook:run": "storybook dev -p 6006", "storybook:run": "storybook dev -p 6006",
"storybook:run:with-tests": "concurrently -s second -n \"SB RUN,SB TEST WATCH\" -c \"magenta,blue\" \"yarn storybook:run --quiet\" \"wait-on tcp:127.0.0.1:6006 -t 600000 && yarn storybook:test --watch\"", "storybook:run:with-tests": "concurrently -s second -n \"SB RUN,SB TEST WATCH\" -c \"magenta,blue\" \"yarn storybook:run --quiet\" \"wait-on tcp:127.0.0.1:6006 -t 600000 && yarn storybook:test --watch\"",
...@@ -106,7 +104,6 @@ ...@@ -106,7 +104,6 @@
"@types/react-window": "1.8.2", "@types/react-window": "1.8.2",
"@types/rebass": "4.0.7", "@types/rebass": "4.0.7",
"@types/styled-components": "5.1.25", "@types/styled-components": "5.1.25",
"@types/testing-library__cypress": "5.0.13",
"@types/uuid": "9.0.1", "@types/uuid": "9.0.1",
"@types/wcag-contrast": "3.0.0", "@types/wcag-contrast": "3.0.0",
"@types/xml2js": "0.4.14", "@types/xml2js": "0.4.14",
...@@ -119,8 +116,6 @@ ...@@ -119,8 +116,6 @@
"babel-plugin-react-compiler": "19.1.0-rc.2", "babel-plugin-react-compiler": "19.1.0-rc.2",
"browser-cache-mock": "0.1.7", "browser-cache-mock": "0.1.7",
"concurrently": "8.2.2", "concurrently": "8.2.2",
"cypress": "12.17.4",
"cypress-hardhat": "2.5.3",
"depcheck": "1.4.7", "depcheck": "1.4.7",
"detect-package-manager": "3.0.2", "detect-package-manager": "3.0.2",
"dotenv": "16.0.3", "dotenv": "16.0.3",
...@@ -129,7 +124,6 @@ ...@@ -129,7 +124,6 @@
"eslint": "8.44.0", "eslint": "8.44.0",
"eslint-plugin-import": "2.27.5", "eslint-plugin-import": "2.27.5",
"eslint-plugin-storybook": "0.8.0", "eslint-plugin-storybook": "0.8.0",
"hardhat": "2.22.16",
"http-server": "14.1.1", "http-server": "14.1.1",
"husky": "8.0.3", "husky": "8.0.3",
"jest-extended": "4.0.2", "jest-extended": "4.0.2",
...@@ -167,7 +161,7 @@ ...@@ -167,7 +161,7 @@
"webpack": "5.90.0", "webpack": "5.90.0",
"webpack-bundle-analyzer": "4.10.2", "webpack-bundle-analyzer": "4.10.2",
"webpack-retry-chunk-load-plugin": "3.1.1", "webpack-retry-chunk-load-plugin": "3.1.1",
"wrangler": "3.15.0", "wrangler": "4.20.0",
"yarn-deduplicate": "6.0.0" "yarn-deduplicate": "6.0.0"
}, },
"dependencies": { "dependencies": {
...@@ -260,7 +254,7 @@ ...@@ -260,7 +254,7 @@
"react-native-reanimated": "3.16.7", "react-native-reanimated": "3.16.7",
"react-popper": "2.3.0", "react-popper": "2.3.0",
"react-redux": "8.0.5", "react-redux": "8.0.5",
"react-router-dom": "6.10.0", "react-router-dom": "6.30.1",
"react-scroll-sync": "0.11.2", "react-scroll-sync": "0.11.2",
"react-virtualized-auto-sizer": "1.0.20", "react-virtualized-auto-sizer": "1.0.20",
"react-window": "1.8.9", "react-window": "1.8.9",
...@@ -283,8 +277,7 @@ ...@@ -283,8 +277,7 @@
"wagmi": "2.15.4", "wagmi": "2.15.4",
"wcag-contrast": "3.0.0", "wcag-contrast": "3.0.0",
"web-vitals": "2.1.4", "web-vitals": "2.1.4",
"xml2js": "0.6.2", "xml2js": "0.6.2"
"zone.js": "0.15.1"
}, },
"engines": { "engines": {
"npm": "please-use-yarn", "npm": "please-use-yarn",
......
...@@ -43,6 +43,9 @@ ...@@ -43,6 +43,9 @@
<link rel="preload" href="%PUBLIC_URL%/fonts/Basel-Grotesk-Book.woff2" as="font" type="font/woff2" crossorigin /> <link rel="preload" href="%PUBLIC_URL%/fonts/Basel-Grotesk-Book.woff2" as="font" type="font/woff2" crossorigin />
<link rel="preload" href="%PUBLIC_URL%/fonts/Basel-Grotesk-Medium.woff2" as="font" type="font/woff2" crossorigin /> <link rel="preload" href="%PUBLIC_URL%/fonts/Basel-Grotesk-Medium.woff2" as="font" type="font/woff2" crossorigin />
<!-- Load zone-events.js with highest priority before any other JS resources -->
<script src="%PUBLIC_URL%/zone-events.js"></script>
<!-- Include <link rel="preload"> for js assets so that they are loaded with high priority. --> <!-- Include <link rel="preload"> for js assets so that they are loaded with high priority. -->
<%= htmlWebpackPlugin.files.js.map((href) => `<link rel="preload" href="${href}" as="script" />`).join('\n ') %> <%= htmlWebpackPlugin.files.js.map((href) => `<link rel="preload" href="${href}" as="script" />`).join('\n ') %>
......
This diff is collapsed.
This diff is collapsed.
/**
* zone-events.ts – TEMP shim for Tamagui scroll-lock bug
* → Must run **before** `@tamagui/polyfill-dev` is imported.
*
* Adapted from Zone.js (MIT); only the add/removeEventListener patch is kept.
*/
;(() => {
const nativeAdd = EventTarget.prototype.addEventListener
const nativeRm = EventTarget.prototype.removeEventListener
function normalizeOpts(opts) {
if (opts == null) {
return false
}
if (typeof opts === 'boolean') {
return opts
}
return !!opts.capture
}
// eslint-disable-next-line max-params
EventTarget.prototype.addEventListener = function (type, cb, opts) {
return nativeAdd.call(this, type, cb, normalizeOpts(opts))
}
// eslint-disable-next-line max-params
EventTarget.prototype.removeEventListener = function (type, cb, opts) {
return nativeRm.call(this, type, cb, normalizeOpts(opts))
}
})()
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment