ci(release): publish latest release

parent 6f8caaf7
* @uniswap/web-admins
IPFS hash of the deployment:
- CIDv0: `QmeqY4kue58BB4LG4rseGWe5sav5XjAt2CNKhwpmpCmaKF`
- CIDv1: `bafybeihvefjgw6woi34lg7b5oxbc77nqluqp66eeg2kfqq7tn32rcjcdgy`
# We are back an exciting new update!
The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org).
Introducing Unichain: Faster swaps. Lower fees. Unichain is optimized to be the home for cross‑chain liquidity.
You can also access the Uniswap Interface from an IPFS gateway.
**BEWARE**: The Uniswap interface uses [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) to remember your settings, such as which tokens you have imported.
**You should always use an IPFS gateway that enforces origin separation**, or our hosted deployment of the latest release at [app.uniswap.org](https://app.uniswap.org).
Your Uniswap settings are never remembered across different URLs.
IPFS gateways:
- https://bafybeihvefjgw6woi34lg7b5oxbc77nqluqp66eeg2kfqq7tn32rcjcdgy.ipfs.dweb.link/
- [ipfs://QmeqY4kue58BB4LG4rseGWe5sav5XjAt2CNKhwpmpCmaKF/](ipfs://QmeqY4kue58BB4LG4rseGWe5sav5XjAt2CNKhwpmpCmaKF/)
## 5.71.0 (2025-02-11)
### Features
* **web:** remove unichain beta toggle - prod (#16168) 6fcd098
- Swap instantly.
- Save up to 95% on fees compared to Ethereum.
- Lower costs for creating pools and managing positions.
Other changes:
- Improved warnings for send transactions to smart contract addresses
- Various bug fixes and performance improvements
\ No newline at end of file
web/5.71.0
\ No newline at end of file
mobile/1.45.2
\ No newline at end of file
......@@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "Uniswap Extension",
"description": "The Uniswap Extension is a self-custody crypto wallet that's built for swapping.",
"version": "1.16.0",
"version": "1.15.0",
"minimum_chrome_version": "116",
"icons": {
"16": "assets/icon16.png",
......
......@@ -89,9 +89,9 @@ if (isCI && datadogPropertiesAvailable && !isE2E) {
apply from: "../../../../node_modules/@datadog/mobile-react-native/datadog-sourcemaps.gradle"
}
def devVersionName = "1.46"
def betaVersionName = "1.46"
def prodVersionName = "1.46"
def devVersionName = "1.45.2"
def betaVersionName = "1.45.2"
def prodVersionName = "1.45.2"
android {
ndkVersion rootProject.ext.ndkVersion
......
......@@ -76,8 +76,8 @@ class NotificationExtension : OSRemoteNotificationReceivedHandler {
"https://gating.android.wallet.gateway.uniswap.org/v1/statsig-proxy"
private const val STATSIG_ENVIRONMENT_KEY_TIER = "tier"
private const val FEATURE_GATE_UNFUNDED_WALLET = "notification_unfunded_wallet"
private const val FEATURE_GATE_PRICE_ALERT = "notification_price_alerts"
private const val FEATURE_GATE_UNFUNDED_WALLET = "notification_unfunded_wallet_android"
private const val FEATURE_GATE_PRICE_ALERT = "notification_price_alerts_android"
private const val FIELD_NOTIFICATION_TYPE = "notification_type"
private const val TYPE_UNFUNDED_WALLET_REMINDER = "unfunded_wallet_reminder"
......
......@@ -87,6 +87,6 @@ struct Constants {
static let typeUnfundedWallet = "unfunded_wallet_reminder"
static let typePriceAlert = "price_alert"
static let gateUnfundedWallet = "notification_unfunded_wallet"
static let gatePriceAlert = "notification_price_alerts"
static let gateUnfundedWallet = "notification_unfunded_wallet_ios"
static let gatePriceAlert = "notification_price_alerts_ios"
}
......@@ -2234,7 +2234,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 1.46;
MARKETING_VERSION = 1.45.2;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
......@@ -2287,7 +2287,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 1.46;
MARKETING_VERSION = 1.45.2;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCore;
......@@ -2340,7 +2340,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 1.46;
MARKETING_VERSION = 1.45.2;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCore;
......@@ -2393,7 +2393,7 @@
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 1.46;
MARKETING_VERSION = 1.45.2;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCore;
......@@ -2431,7 +2431,7 @@
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.46;
MARKETING_VERSION = 1.45.2;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
......@@ -2467,7 +2467,7 @@
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.46;
MARKETING_VERSION = 1.45.2;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCoreTests;
......@@ -2502,7 +2502,7 @@
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.46;
MARKETING_VERSION = 1.45.2;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCoreTests;
......@@ -2537,7 +2537,7 @@
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 1.46;
MARKETING_VERSION = 1.45.2;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCoreTests;
......@@ -2584,7 +2584,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.46;
MARKETING_VERSION = 1.45.2;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
......@@ -2630,7 +2630,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.46;
MARKETING_VERSION = 1.45.2;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.widgets;
......@@ -2676,7 +2676,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.46;
MARKETING_VERSION = 1.45.2;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.widgets;
......@@ -2722,7 +2722,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.46;
MARKETING_VERSION = 1.45.2;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.widgets;
......@@ -2764,7 +2764,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.46;
MARKETING_VERSION = 1.45.2;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
......@@ -2807,7 +2807,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.46;
MARKETING_VERSION = 1.45.2;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.WidgetIntentExtension;
......@@ -2850,7 +2850,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.46;
MARKETING_VERSION = 1.45.2;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.WidgetIntentExtension;
......@@ -2893,7 +2893,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.46;
MARKETING_VERSION = 1.45.2;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.WidgetIntentExtension;
......@@ -2929,7 +2929,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.46;
MARKETING_VERSION = 1.45.2;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
......@@ -2967,7 +2967,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.46;
MARKETING_VERSION = 1.45.2;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
......@@ -3168,7 +3168,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.46;
MARKETING_VERSION = 1.45.2;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
......@@ -3213,7 +3213,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.46;
MARKETING_VERSION = 1.45.2;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.OneSignalNotificationServiceExtension;
......@@ -3324,7 +3324,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.46;
MARKETING_VERSION = 1.45.2;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
......@@ -3396,7 +3396,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.46;
MARKETING_VERSION = 1.45.2;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.OneSignalNotificationServiceExtension;
......@@ -3507,7 +3507,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.46;
MARKETING_VERSION = 1.45.2;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
......@@ -3579,7 +3579,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.46;
MARKETING_VERSION = 1.45.2;
MTL_FAST_MATH = YES;
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.OneSignalNotificationServiceExtension;
......
......@@ -256,8 +256,8 @@ function AppOuter(): JSX.Element | null {
// Used in case we aren't able to resolve notification filtering issues on iOS
if (isIOS) {
const notificationsPriceAlertsEnabled = getFeatureFlag(FeatureFlags.NotificationPriceAlerts)
const notificationsUnfundedWalletEnabled = getFeatureFlag(FeatureFlags.NotificationUnfundedWallets)
const notificationsPriceAlertsEnabled = getFeatureFlag(FeatureFlags.NotificationPriceAlertsIOS)
const notificationsUnfundedWalletEnabled = getFeatureFlag(FeatureFlags.NotificationUnfundedWalletsIOS)
OneSignal.sendTags({
[OneSignalUserTagField.GatingPriceAlertsEnabled]: notificationsPriceAlertsEnabled ? 'true' : 'false',
......
import { PermissionStatus, scanFromURLAsync } from 'expo-barcode-scanner'
import { BarCodeScanningResult, CameraType } from 'expo-camera'
import { BarCodeScanner, PermissionStatus } from 'expo-barcode-scanner'
import { BarCodeScanningResult, CameraType } from 'expo-camera/build/Camera.types'
import { CameraProps, CameraView, useCameraPermissions } from 'expo-camera/next'
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
......@@ -59,7 +59,7 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E
const [infoLayout, setInfoLayout] = useState<LayoutRectangle | null>()
const [bottomLayout, setBottomLayout] = useState<LayoutRectangle | null>()
const handleBarcodeScanned = useCallback(
const handleBarCodeScanned = useCallback(
(result: BarCodeScanningResult): void => {
if (shouldFreezeCamera) {
return
......@@ -90,7 +90,7 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E
return
}
const result = (await scanFromURLAsync(uri, [BarcodeType.QR]))[0]
const result = (await BarCodeScanner.scanFromURLAsync(uri, [BarCodeScanner.Constants.BarCodeType.qr]))[0]
if (!result) {
Alert.alert(t('qrScanner.error.none'))
......@@ -98,8 +98,8 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E
return
}
handleBarcodeScanned(result)
}, [handleBarcodeScanned, isReadingImageFile, t])
handleBarCodeScanned(result)
}, [handleBarCodeScanned, isReadingImageFile, t])
useEffect(() => {
const handlePermissionStatus = async (): Promise<void> => {
......@@ -146,7 +146,7 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E
}}
facing={CameraType.back}
style={StyleSheet.absoluteFillObject}
onBarcodeScanned={handleBarcodeScanned}
onBarcodeScanned={handleBarCodeScanned}
/>
)}
</Flex>
......
......@@ -30,6 +30,10 @@ export function SearchEmptySection({ selectedChain }: { selectedChain: UniverseC
const [showPopularInfo, setShowPopularInfo] = useState(false)
// Popular NFT collections data is only available on Mainnet
// TODO(WALL-5876): Update this once we have a way to fetch NFT collections for all chains
const showPopularNftCollections = !selectedChain || selectedChain === UniverseChainId.Mainnet
const onPressClearSearchHistory = (): void => {
dispatch(clearSearchHistory())
}
......@@ -79,10 +83,12 @@ export function SearchEmptySection({ selectedChain }: { selectedChain: UniverseC
/>
<SearchPopularTokens selectedChain={selectedChain} />
</Flex>
{showPopularNftCollections && (
<Flex gap="$spacing4">
<SectionHeaderText icon={TrendUpIcon} title={t('explore.search.section.popularNFT')} />
<SearchPopularNFTCollections />
</Flex>
)}
</AnimatedFlex>
<WarningModal
backgroundIconColor={colors.surface3.get()}
......
......@@ -31,6 +31,7 @@ import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard'
import { useExploreSearchQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains'
import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { toGraphQLChain } from 'uniswap/src/features/chains/utils'
import { SearchContext } from 'uniswap/src/features/search/SearchContext'
import {
NFTCollectionSearchResult,
......@@ -61,7 +62,11 @@ export function SearchResultsSection({
error,
refetch,
} = useExploreSearchQuery({
variables: { searchQuery, nftCollectionsFilter: { nameQuery: searchQuery } },
variables: {
searchQuery,
nftCollectionsFilter: { nameQuery: searchQuery },
chains: selectedChain ? [toGraphQLChain(selectedChain)] : undefined,
},
})
const onRetry = useCallback(async () => {
......
......@@ -3,7 +3,7 @@ import OneSignal, { NotificationReceivedEvent, OpenedEvent } from 'react-native-
import { NotificationType } from 'src/features/notifications/constants'
import { config } from 'uniswap/src/config'
import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { getFeatureFlagWithExposureLoggingDisabled } from 'uniswap/src/features/gating/hooks'
import { getFeatureFlag } from 'uniswap/src/features/gating/hooks'
import { GQL_QUERIES_TO_REFETCH_ON_TXN_UPDATE } from 'uniswap/src/features/portfolio/portfolioUpdates/constants'
import { getUniqueId } from 'utilities/src/device/getUniqueId'
import { logger } from 'utilities/src/logger/logger'
......@@ -20,14 +20,14 @@ export const initOneSignal = (): void => {
const notificationType = additionalData?.notification_type
let enabled = false
// Some special notif filtering logic is needed for iOS, avoiding exposure
// Some special notif filtering logic is needed for iOS
if (isIOS) {
switch (notificationType) {
case NotificationType.UnfundedWalletReminder:
enabled = getFeatureFlagWithExposureLoggingDisabled(FeatureFlags.NotificationUnfundedWallets)
enabled = getFeatureFlag(FeatureFlags.NotificationPriceAlertsIOS)
break
case NotificationType.PriceAlert:
enabled = getFeatureFlagWithExposureLoggingDisabled(FeatureFlags.NotificationPriceAlerts)
enabled = getFeatureFlag(FeatureFlags.NotificationPriceAlertsIOS)
break
default:
enabled = false
......
......@@ -82,6 +82,7 @@ function preloadServiceProviderLogos(serviceProviders: FORServiceProvider[], isD
}
const PREDEFINED_AMOUNTS_SUPPORTED_CURRENCIES = ['usd', 'eur', 'gbp', 'aud', 'cad', 'sgd']
const US_STATES_WITH_RESTRICTIONS = 'US-NY'
export function FiatOnRampScreen({ navigation }: Props): JSX.Element {
const isOffRampEnabled = useFeatureFlag(FeatureFlags.FiatOffRamp)
......@@ -398,7 +399,10 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element {
meldSupportedFiatCurrency.code.toLowerCase(),
)
const notAvailableInThisRegion = supportedFiatCurrencies?.length === 0
const notAvailableInThisRegion =
supportedFiatCurrencies?.length === 0 ||
(!supportedTokensLoading && supportedTokensList?.length === 0) ||
(US_STATES_WITH_RESTRICTIONS.includes(countryState || '') && quotes?.length === 0)
const { errorText } = useParseFiatOnRampError({
error: !notAvailableInThisRegion && quotesError,
......
......@@ -656,7 +656,7 @@ export function HomeScreen(props?: AppStackScreenProp<MobileScreens.Home>): JSX.
{contentHeader}
</Animated.View>
{isTabsDataLoaded && (
{isTabsDataLoaded && isLayoutReady && (
<TraceTabView
lazy
initialLayout={{
......
import { useEffect } from 'react'
import { useEffect, useRef } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { useNftsTabQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains'
......@@ -25,6 +25,14 @@ export function useHomeScreenState(): {
)
const { gqlChains } = useEnabledChains()
const neverCached = hasUsedWalletFromCache === undefined
// There's a race condition during onboarding where the editAccountSaga is
// editing the account in redux in an unconventional way that causes the
// hasUsedWalletFromCache to reset after being set by this hook. This ref
// is a work around to only trigger the loading state once.
const dataLoadedRef = useRef(false)
const { data: balancesById, loading: areBalancesLoading } = usePortfolioBalances({
address,
skip: hasUsedWalletFromCache,
......@@ -48,24 +56,27 @@ export function useHomeScreenState(): {
})
const hasNft = !!nftData?.nftBalances?.edges.length
const hasTokenBalance = !!Object.entries(balancesById || {}).length
const hasTokenBalance = balancesById ? Object.keys(balancesById).length > 0 : false
const hasUsedWalletFromRemote = hasTokenBalance || hasNft || hasActivity
const dataIsLoading = areBalancesLoading || areNFTsLoading || isActivityLoading
// Note: This is to prevent loading the empty wallet state for an active
// wallet loading tabs for the first time.
const isTabsDataLoaded = !(dataIsLoading && hasUsedWalletFromCache)
const isTabsDataLoaded = neverCached ? !dataIsLoading : true
const hasUsedWallet = hasUsedWalletFromCache || hasUsedWalletFromRemote
const shouldUpdateCache = neverCached && isTabsDataLoaded
const addressIsNowUsed = hasUsedWalletFromCache === false && hasUsedWalletFromRemote
useEffect(() => {
if (hasUsedWallet && !hasUsedWalletFromCache) {
dispatch(setHasBalanceOrActivity({ address, hasBalanceOrActivity: true }))
if (shouldUpdateCache || addressIsNowUsed) {
dispatch(setHasBalanceOrActivity({ address, hasBalanceOrActivity: hasUsedWallet }))
}
}, [hasUsedWallet, dispatch, address, shouldUpdateCache, addressIsNowUsed])
if (!dataLoadedRef.current && isTabsDataLoaded) {
dataLoadedRef.current = true
}
}, [hasUsedWallet, dispatch, address, hasUsedWalletFromCache])
return {
showEmptyWalletState: !hasUsedWallet,
isTabsDataLoaded,
isTabsDataLoaded: dataLoadedRef.current || isTabsDataLoaded,
}
}
import React, { memo } from 'react'
import React, { memo, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { BackHeader } from 'src/components/layout/BackHeader'
import { Screen } from 'src/components/layout/Screen'
......@@ -14,6 +14,8 @@ import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
import { MobileEventName } from 'uniswap/src/features/telemetry/constants'
import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send'
import { NotificationToggleLoggingType } from 'uniswap/src/features/telemetry/types'
import { isIOS } from 'utilities/src/platform'
import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay'
import { useAccountsList } from 'wallet/src/features/wallet/hooks'
......@@ -22,13 +24,26 @@ export function SettingsNotificationsScreen(): JSX.Element {
const accounts = useAccountsList()
const priceAlertsToggleEnabled = useFeatureFlag(FeatureFlags.NotificationPriceAlerts)
const priceAlertsToggleEnabled = useFeatureFlag(
isIOS ? FeatureFlags.NotificationPriceAlertsIOS : FeatureFlags.NotificationPriceAlertsAndroid,
)
const onGeneralUpdatesToggle = useCallback(
(enabled: boolean) => onPermissionChanged(enabled, NotifSettingType.GeneralUpdates),
[],
)
const onPriceAlertsToggle = useCallback(
(enabled: boolean) => onPermissionChanged(enabled, NotifSettingType.PriceAlerts),
[],
)
const { isEnabled: updatesNotifEnabled, toggle: toggleUpdatesNotif } = useSettingNotificationToggle({
type: NotifSettingType.GeneralUpdates,
onToggle: onGeneralUpdatesToggle,
})
const { isEnabled: priceAlertsNotifEnabled, toggle: togglePriceAlertsNotif } = useSettingNotificationToggle({
type: NotifSettingType.PriceAlerts,
onToggle: onPriceAlertsToggle,
})
return (
......@@ -117,14 +132,14 @@ function NotificationSettingRow({
)
}
function onPermissionChanged(enabled: boolean): void {
sendAnalyticsEvent(MobileEventName.NotificationsToggled, { enabled })
function onPermissionChanged(enabled: boolean, type: NotificationToggleLoggingType): void {
sendAnalyticsEvent(MobileEventName.NotificationsToggled, { enabled, type })
}
function _AddressNotificationsSwitch({ address }: { address: string }): JSX.Element {
const { isEnabled, isPending, toggle } = useAddressNotificationToggle({
address,
onToggle: onPermissionChanged,
onToggle: (enabled) => onPermissionChanged(enabled, 'wallet_activity'),
})
return <Switch checked={isEnabled} disabled={isPending} variant="branded" onCheckedChange={toggle} />
......
......@@ -126,4 +126,16 @@
<changefreq>weekly</changefreq>
<priority>0.5</priority>
</url>
<url>
<loc>https://app.uniswap.org/positions/create</loc>
<lastmod>2024-09-17T19:57:27.976Z</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://app.uniswap.org/positions</loc>
<lastmod>2024-09-17T19:57:27.976Z</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
</url>
</urlset>
......@@ -100,7 +100,6 @@
"https://x6ahx1oagk.execute-api.us-east-2.amazonaws.com",
"https://mainnet.era.zksync.io/",
"https://8mr3mthjba.execute-api.us-east-2.amazonaws.com",
"https://mainnet.unichain.org/",
"wss://*.uniswap.org",
"wss://relay.walletconnect.com",
"wss://relay.walletconnect.org",
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
......@@ -92,7 +92,7 @@ export function LiquidityRangeInput({
)
// Set via a callback from the LiquidityPositionRangeChart, which is important when the price axis is auto-scaled.
// This is also used to set the bounds of the ActiveLiquidityChart, so it's necessary to keep separate from the zooming state.
// This is also used to set the bounds of the ActiveLiquiditChart, so it's necessary to keep separate from the zooming state.
const [boundaryPrices, setBoundaryPrices] = useState<[number, number]>()
const [zoomFactor, setZoomFactor] = useState(1)
......@@ -278,7 +278,6 @@ export function LiquidityRangeInput({
width={showChartErrorView ? sizes.chartContainerWidth : sizes.loadedPriceChartWidth}
height={sizes.chartHeight + sizes.bottomAxisHeight}
overflow="hidden"
zIndex={1}
>
{(priceData.loading || showChartErrorView) && (!priceData.entries || priceData.entries.length === 0) && (
<Shine height={sizes.chartHeight} disabled={showChartErrorView} zIndex={0}>
......@@ -303,7 +302,6 @@ export function LiquidityRangeInput({
right={0}
top={0}
pointerEvents="none"
zIndex={2}
>
{(liquidityDataLoading || priceData.loading) && (
<Shine
......@@ -347,9 +345,13 @@ export function LiquidityRangeInput({
// While scrolling we receive updates to the range because the yScale changes,
// but we can filter them out because they have an undefined "mode".
// The initial range suggestion also comes with an undefined "mode", so we allow that here.
const rejectAutoRangeSuggestion =
minPrice !== undefined && maxPrice !== undefined && minPrice >= 0 && maxPrice >= 0
if (!mode && rejectAutoRangeSuggestion) {
const hasValidRange =
minPrice !== undefined &&
maxPrice !== undefined &&
minPrice < maxPrice &&
minPrice >= 0 &&
maxPrice >= 0
if (!mode && hasValidRange) {
return
}
setMinPrice(domain[0])
......
......@@ -65,7 +65,7 @@ export function DepositInputForm({
const handleOnSetMax = (field: PositionField) => {
return (amount: string) => {
setFocusedInputField(field)
onSetMax(field, amount)
onSetMax(field, amount) // TODO(WEB-4978): update this to account for gas
}
}
......
......@@ -93,18 +93,14 @@ export function useAllFeeTierPoolData({
feeTierData: mergeFeeTiers(
feeTierData,
Object.values(
getDefaultFeeTiersForChainWithDynamicFeeTier({
chainId,
dynamicFeeTierEnabled: withDynamicFeeTier,
protocolVersion,
}),
getDefaultFeeTiersForChainWithDynamicFeeTier({ chainId, dynamicFeeTierEnabled: withDynamicFeeTier }),
),
formatPercent,
t('fee.dynamic'),
),
hasExistingFeeTiers: Object.values(feeTierData).length > 0,
}
}, [poolData, sortedCurrencies, chainId, withDynamicFeeTier, formatPercent, protocolVersion, t])
}, [poolData, sortedCurrencies, chainId, withDynamicFeeTier, formatPercent, t])
}
/**
......
......@@ -491,17 +491,10 @@ export function mergeFeeTiers(
}
function getDefaultFeeTiersForChain(
chainId: UniverseChainId | undefined,
protocolVersion: ProtocolVersion,
chainId?: UniverseChainId,
): Record<FeeAmount, { feeAmount: FeeAmount; tickSpacing: number }> {
const feeData = Object.values(defaultFeeTiers)
.filter((feeTier) => {
// Only filter by chain support if we're on V3
if (protocolVersion === ProtocolVersion.V3) {
return !feeTier.supportedChainIds || (chainId && feeTier.supportedChainIds.includes(chainId))
}
return !feeTier.supportedChainIds
})
.filter((feeTier) => !feeTier.supportedChainIds || (chainId && feeTier.supportedChainIds.includes(chainId)))
.map((feeTier) => feeTier.feeData)
return feeData.reduce(
......@@ -516,32 +509,27 @@ function getDefaultFeeTiersForChain(
export function getDefaultFeeTiersForChainWithDynamicFeeTier({
chainId,
dynamicFeeTierEnabled,
protocolVersion,
}: {
chainId?: UniverseChainId
dynamicFeeTierEnabled: boolean
protocolVersion: ProtocolVersion
}) {
const feeTiers = getDefaultFeeTiersForChain(chainId, protocolVersion)
if (!dynamicFeeTierEnabled) {
return feeTiers
return getDefaultFeeTiersForChain(chainId)
}
return { ...feeTiers, [DYNAMIC_FEE_DATA.feeAmount]: DYNAMIC_FEE_DATA }
return { ...getDefaultFeeTiersForChain(chainId), [DYNAMIC_FEE_DATA.feeAmount]: DYNAMIC_FEE_DATA }
}
export function getDefaultFeeTiersWithData({
chainId,
feeTierData,
protocolVersion,
t,
}: {
chainId?: UniverseChainId
feeTierData: Record<number, FeeTierData>
protocolVersion: ProtocolVersion
t: AppTFunction
}) {
const defaultFeeTiersForChain = getDefaultFeeTiersForChain(chainId, protocolVersion)
const defaultFeeTiersForChain = getDefaultFeeTiersForChain(chainId)
const feeTiers = [
{
......@@ -595,9 +583,7 @@ export function getDefaultFeeTiersWithData({
},
] as const
return feeTiers.filter(
(feeTier) => feeTier.value !== undefined && Object.keys(feeTierData).includes(feeTier.tier.toString()),
)
return feeTiers.filter((feeTier) => Object.keys(feeTierData).includes(feeTier.tier.toString()))
}
export function isDynamicFeeTier(feeData: FeeData): feeData is DynamicFeeData {
......
......@@ -54,9 +54,9 @@ const PoolTransactionColumnWidth: { [key in PoolTransactionColumn]: number } = {
function comparePoolTokens(tokenA: PoolTableTransaction['pool']['token0'], tokenB?: Token) {
if (tokenB?.address === NATIVE_CHAIN_ID) {
const chainId = supportedChainIdFromGQLChain(tokenB.chain)
return chainId && tokenA.id?.toLowerCase() === WRAPPED_NATIVE_CURRENCY[chainId]?.address.toLowerCase()
return chainId && tokenA.id.toLowerCase() === WRAPPED_NATIVE_CURRENCY[chainId]?.address.toLowerCase()
}
return tokenA.id?.toLowerCase() === tokenB?.address?.toLowerCase()
return tokenA.id.toLowerCase() === tokenB?.address?.toLowerCase()
}
export function PoolDetailsTransactionsTable({
......
......@@ -7,7 +7,7 @@ import { useNavigate } from 'react-router-dom'
import { useAppDispatch } from 'state/hooks'
import { useMultichainContext } from 'state/multichain/useMultichainContext'
import { serializeSwapStateToURLParameters } from 'state/swap/hooks'
import { ClickableTamaguiStyle, ExternalLink } from 'theme/components'
import { ClickableTamaguiStyle, ExternalLink, HideSmall } from 'theme/components'
import { useIsDarkMode } from 'theme/components/ThemeToggle'
import { ElementAfterText, Flex, Text, TouchableArea, TouchableAreaEvent, useSporeColors } from 'ui/src'
import { UNICHAIN_BANNER_COLD, UNICHAIN_BANNER_WARM } from 'ui/src/assets'
......@@ -269,16 +269,15 @@ function CardInner({
textProps={{ color: textColor ?? '$neutral1', variant: 'subheading2' }}
element={isNew ? <NewTag /> : undefined}
/>
<Flex $md={{ display: 'none' }}>
<HideSmall>
<Text variant="body4" color={textColor ?? '$neutral2'}>
{subtitle}
</Text>
</Flex>
</HideSmall>
</Flex>
{onDismiss ? (
<TouchableArea
alignSelf="flex-start"
$md={{ alignSelf: 'center' }}
hitSlop={ICON_SIZE}
onPress={(e: TouchableAreaEvent) => {
e.stopPropagation()
......@@ -288,9 +287,7 @@ function CardInner({
<X color="$neutral3" size={ICON_SIZE} />
</TouchableArea>
) : (
<TouchableArea alignSelf="flex-start" $md={{ alignSelf: 'center' }}>
<ArrowUpRight width={ICON_SIZE_PX} height={ICON_SIZE_PX} color={textColor} />
</TouchableArea>
)}
</Flex>
</Flex>
......
......@@ -126,7 +126,6 @@ export const TokenDetailsHeader = () => {
$sm={{ mb: '$spacing8', alignItems: 'flex-start' }}
animation="quick"
data-testid="token-info-container"
zIndex="$default"
>
<Flex row alignItems="center" $sm={{ alignItems: 'flex-start', flexDirection: 'column' }} gap="$gap12">
<PortfolioLogo currencies={[currency]} chainId={currency.chainId} size={32} />
......
......@@ -86,7 +86,7 @@ export default function TableNetworkFilter({ showMultichainOption = true }: { sh
toggleOpen={toggleMenu}
menuLabel={
<NetworkLabel>
{(!currentChainId || !isSupportedChainCallback(currentChainId)) && showMultichainOption ? (
{!currentChainId && showMultichainOption ? (
<NetworkLogo chainId={null} />
) : (
<ChainLogo
......
......@@ -4,7 +4,6 @@ import {
Token,
useSearchTokensWebQuery,
} from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains'
import { isBackendSupportedChain } from 'uniswap/src/features/chains/utils'
// Filters out results that are undefined, or where the token's chain is not supported in explore.
......@@ -13,12 +12,7 @@ function isExploreSupportedToken(token: GqlSearchToken | undefined): token is To
}
export function useSearchTokens(searchQuery: string = '') {
const { gqlChains: chains } = useEnabledChains()
const { data, loading, error } = useSearchTokensWebQuery({
variables: { searchQuery, chains },
skip: searchQuery === '',
})
const { data, loading, error } = useSearchTokensWebQuery({ variables: { searchQuery }, skip: searchQuery === '' })
return useMemo(() => {
const sortedTokens = data?.searchTokens?.filter(isExploreSupportedToken) ?? []
......
......@@ -126,10 +126,10 @@ export function usePoolData(
protocolVersion: pool.protocolVersion,
token0: pool.token0 as Token,
tvlToken0: pool.token0Supply,
token0Price: pool.token0?.project?.markets?.[0]?.price?.value ?? pool.token0?.market?.price?.value,
token0Price: pool.token0?.project?.markets?.[0]?.price?.value,
token1: pool.token1 as Token,
tvlToken1: pool.token1Supply,
token1Price: pool.token1?.project?.markets?.[0]?.price?.value ?? pool.token1?.market?.price?.value,
token1Price: pool.token1?.project?.markets?.[0]?.price?.value,
feeTier,
volumeUSD24H: pool.volume24h?.value,
volumeUSD24HChange: calc24HVolChange(pool.historicalVolume?.concat()),
......
......@@ -30,11 +30,11 @@ export interface PoolTableTransaction {
transaction: string
pool: {
token0: {
id: string | null
id: string
symbol: string
}
token1: {
id: string | null
id: string
symbol: string
}
}
......@@ -189,11 +189,11 @@ export function usePoolTransactions(
transaction: tx.hash,
pool: {
token0: {
id: tx.token0.address ?? null,
id: tx.token0.address ?? '',
symbol: tx.token0.symbol ?? '',
},
token1: {
id: tx.token1.address ?? null,
id: tx.token1.address ?? '',
symbol: tx.token1.symbol ?? '',
},
},
......
......@@ -2,11 +2,10 @@ import useIsWindowVisible from 'hooks/useIsWindowVisible'
import { useCallback, useMemo, useRef } from 'react'
import {
Chain,
PoolTransaction,
PoolTransactionType,
PoolTxFragment,
useV2TransactionsQuery,
useV3TransactionsQuery,
useV4TransactionsQuery,
} from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
export enum TransactionType {
......@@ -29,15 +28,6 @@ export function useAllTransactions(
) {
const isWindowVisible = useIsWindowVisible()
const {
data: dataV4,
loading: loadingV4,
error: errorV4,
fetchMore: fetchMoreV4,
} = useV4TransactionsQuery({
variables: { chain, first: ALL_TX_DEFAULT_QUERY_SIZE },
skip: !isWindowVisible,
})
const {
data: dataV3,
loading: loadingV3,
......@@ -57,39 +47,17 @@ export function useAllTransactions(
skip: !isWindowVisible,
})
const loadingMoreV4 = useRef(false)
const loadingMoreV3 = useRef(false)
const loadingMoreV2 = useRef(false)
const querySizeRef = useRef(ALL_TX_DEFAULT_QUERY_SIZE)
const loadMore = useCallback(
({ onComplete }: { onComplete?: () => void }) => {
if (loadingMoreV4.current || loadingMoreV3.current || loadingMoreV2.current) {
if (loadingMoreV3.current || loadingMoreV2.current) {
return
}
loadingMoreV4.current = true
loadingMoreV3.current = true
loadingMoreV2.current = true
querySizeRef.current += ALL_TX_DEFAULT_QUERY_SIZE
fetchMoreV4({
variables: {
cursor: dataV4?.v4Transactions?.[dataV4.v4Transactions.length - 1]?.timestamp,
},
updateQuery: (prev, { fetchMoreResult }) => {
if (!fetchMoreResult) {
return prev
}
if (!loadingMoreV3.current && !loadingMoreV2.current) {
onComplete?.()
}
const mergedData = {
v4Transactions: [...(prev.v4Transactions ?? []), ...(fetchMoreResult.v4Transactions ?? [])],
}
loadingMoreV4.current = false
return mergedData
},
})
fetchMoreV3({
variables: {
cursor: dataV3?.v3Transactions?.[dataV3.v3Transactions.length - 1]?.timestamp,
......@@ -98,7 +66,7 @@ export function useAllTransactions(
if (!fetchMoreResult) {
return prev
}
if (!loadingMoreV2.current && !loadingMoreV4.current) {
if (!loadingMoreV2.current) {
onComplete?.()
}
const mergedData = {
......@@ -117,9 +85,7 @@ export function useAllTransactions(
if (!fetchMoreResult) {
return prev
}
if (!loadingMoreV3.current && !loadingMoreV4.current) {
onComplete?.()
}
!loadingMoreV3.current && onComplete?.()
const mergedData = {
v2Transactions: [...(prev.v2Transactions ?? []), ...(fetchMoreResult.v2Transactions ?? [])],
}
......@@ -128,30 +94,29 @@ export function useAllTransactions(
},
})
},
[dataV2?.v2Transactions, dataV3?.v3Transactions, dataV4?.v4Transactions, fetchMoreV2, fetchMoreV3, fetchMoreV4],
)
const filterTransaction = useCallback(
(tx: PoolTxFragment | undefined): tx is PoolTxFragment => {
return !!tx?.type && filter.includes(BETypeToTransactionType[tx.type])
},
[filter],
[dataV2?.v2Transactions, dataV3?.v3Transactions, fetchMoreV2, fetchMoreV3],
)
const transactions: PoolTxFragment[] = useMemo(() => {
return [...(dataV4?.v4Transactions ?? []), ...(dataV3?.v3Transactions ?? []), ...(dataV2?.v2Transactions ?? [])]
.filter(filterTransaction)
const transactions: PoolTransaction[] = useMemo(() => {
const v3Transactions =
dataV3?.v3Transactions?.filter(
(tx): tx is PoolTransaction => tx.type && filter.includes(BETypeToTransactionType[tx.type]),
) ?? []
const v2Transactions =
dataV2?.v2Transactions?.filter(
(tx): tx is PoolTransaction => tx !== undefined && tx.type && filter.includes(BETypeToTransactionType[tx.type]),
) ?? []
return [...v3Transactions, ...v2Transactions]
.sort((a, b) => (b?.timestamp || 0) - (a?.timestamp || 0))
.slice(0, querySizeRef.current)
}, [dataV2?.v2Transactions, dataV3?.v3Transactions, dataV4?.v4Transactions, filterTransaction])
}, [dataV2?.v2Transactions, dataV3?.v3Transactions, filter])
return {
transactions,
// useIsWindowVisible briefly initializes as false, which skips the GQL transaction query, so the "no data found" state initially flashes
loading: loadingV2 || loadingV3 || loadingV4 || !isWindowVisible,
loading: loadingV2 || loadingV3 || !isWindowVisible,
errorV2,
errorV3,
errorV4,
loadMore,
}
}
......@@ -3,10 +3,8 @@ import {
Chain,
PoolTransaction,
PoolTransactionType,
PoolTxFragment,
useV2TokenTransactionsQuery,
useV3TokenTransactionsQuery,
useV4TokenTransactionsQuery,
} from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains'
import { UniverseChainId } from 'uniswap/src/features/chains/types'
......@@ -25,18 +23,6 @@ export function useTokenTransactions(
filter: TokenTransactionType[] = [TokenTransactionType.BUY, TokenTransactionType.SELL],
) {
const { defaultChainId } = useEnabledChains()
const {
data: dataV4,
loading: loadingV4,
fetchMore: fetchMoreV4,
error: errorV4,
} = useV4TokenTransactionsQuery({
variables: {
address: address.toLowerCase(),
chain: toGraphQLChain(chainId ?? defaultChainId),
first: TokenTransactionDefaultQuerySize,
},
})
const {
data: dataV3,
loading: loadingV3,
......@@ -61,42 +47,17 @@ export function useTokenTransactions(
chain: toGraphQLChain(chainId),
},
})
const loadingMoreV4 = useRef(false)
const loadingMoreV3 = useRef(false)
const loadingMoreV2 = useRef(false)
const querySizeRef = useRef(TokenTransactionDefaultQuerySize)
const loadMore = useCallback(
({ onComplete }: { onComplete?: () => void }) => {
if (loadingMoreV4.current || loadingMoreV3.current || loadingMoreV2.current) {
if (loadingMoreV3.current || loadingMoreV2.current) {
return
}
loadingMoreV4.current = true
loadingMoreV3.current = true
loadingMoreV2.current = true
querySizeRef.current += TokenTransactionDefaultQuerySize
fetchMoreV4({
variables: {
cursor: dataV4?.token?.v4Transactions?.[dataV4.token?.v4Transactions.length - 1]?.timestamp,
},
updateQuery: (prev, { fetchMoreResult }) => {
if (!fetchMoreResult) {
return prev
}
if (!loadingMoreV3.current && !loadingMoreV2.current) {
onComplete?.()
}
const mergedData = {
token: {
...prev.token,
id: prev?.token?.id ?? '',
chain: prev?.token?.chain ?? Chain.Ethereum,
v4Transactions: [...(prev.token?.v4Transactions ?? []), ...(fetchMoreResult.token?.v4Transactions ?? [])],
},
}
loadingMoreV4.current = false
return mergedData
},
})
fetchMoreV3({
variables: {
cursor: dataV3?.token?.v3Transactions?.[dataV3.token?.v3Transactions.length - 1]?.timestamp,
......@@ -105,7 +66,7 @@ export function useTokenTransactions(
if (!fetchMoreResult) {
return prev
}
if (!loadingMoreV2.current && !loadingMoreV4.current) {
if (!loadingMoreV2.current) {
onComplete?.()
}
const mergedData = {
......@@ -128,7 +89,7 @@ export function useTokenTransactions(
if (!fetchMoreResult) {
return prev
}
if (!loadingMoreV3.current && !loadingMoreV4.current) {
if (!loadingMoreV3.current) {
onComplete?.()
}
const mergedData = {
......@@ -144,18 +105,13 @@ export function useTokenTransactions(
},
})
},
[
dataV2?.token?.v2Transactions,
dataV3?.token?.v3Transactions,
dataV4?.token?.v4Transactions,
fetchMoreV2,
fetchMoreV3,
fetchMoreV4,
],
[dataV2?.token?.v2Transactions, dataV3?.token?.v3Transactions, fetchMoreV2, fetchMoreV3],
)
const filterTransaction = useCallback(
(tx: PoolTxFragment | undefined) => {
const transactions = useMemo(
() =>
[
...(dataV3?.token?.v3Transactions?.filter((tx) => {
if (!tx) {
return false
}
......@@ -165,34 +121,33 @@ export function useTokenTransactions(
tx.type === PoolTransactionType.Swap &&
filter.includes(isSell ? TokenTransactionType.SELL : TokenTransactionType.BUY)
)
},
[address, filter],
}) ?? []),
...(dataV2?.token?.v2Transactions?.filter((tx) => {
if (!tx) {
return false
}
const tokenBeingSold = parseFloat(tx.token0Quantity) > 0 ? tx.token0 : tx.token1
const isSell = tokenBeingSold.address?.toLowerCase() === address.toLowerCase()
return (
tx.type === PoolTransactionType.Swap &&
filter.includes(isSell ? TokenTransactionType.SELL : TokenTransactionType.BUY)
)
const transactions = useMemo(
() =>
[
...(dataV4?.token?.v4Transactions ?? []),
...(dataV3?.token?.v3Transactions ?? []),
...(dataV2?.token?.v2Transactions ?? []),
}) ?? []),
]
.filter(filterTransaction)
.sort((a, b): number =>
a?.timestamp && b?.timestamp ? b.timestamp - a.timestamp : a?.timestamp === null ? -1 : 1,
)
.slice(0, querySizeRef.current),
[dataV2?.token?.v2Transactions, dataV3?.token?.v3Transactions, dataV4?.token?.v4Transactions, filterTransaction],
[address, dataV2?.token?.v2Transactions, dataV3?.token?.v3Transactions, filter],
)
return useMemo(
() => ({
return useMemo(() => {
return {
transactions: transactions as PoolTransaction[],
loading: loadingV4 || loadingV3 || loadingV2,
loading: loadingV3 || loadingV2,
loadMore,
errorV2,
errorV3,
errorV4,
}),
[transactions, loadingV4, loadingV3, loadingV2, loadMore, errorV2, errorV3, errorV4],
)
}
}, [transactions, loadingV3, loadingV2, loadMore, errorV2, errorV3])
}
......@@ -13,7 +13,7 @@ export function clientToProvider(client?: Client<Transport, UniverseChainInfo>,
}
const { chain, transport } = client
const ensAddress = chain?.contracts?.ensRegistry?.address
const ensAddress = chain.contracts?.ensRegistry?.address
const network = chain
? {
chainId: chain.id,
......
......@@ -86,7 +86,7 @@ function MigrateV3Inner({ positionInfo }: { positionInfo: PositionInfo }) {
const { value: lpRedesignEnabled, isLoading: isLPRedesignGateLoading } = useFeatureFlagWithLoading(
FeatureFlags.LPRedesign,
)
const isMigrateToV4Enabled = useFeatureFlag(FeatureFlags.MigrateV3ToV4)
const isMigrateEnabled = useFeatureFlag(FeatureFlags.MigrateV3ToV4)
const [transactionSteps, setTransactionSteps] = useState<TransactionStep[]>([])
const [currentTransactionStep, setCurrentTransactionStep] = useState<
......@@ -113,7 +113,7 @@ function MigrateV3Inner({ positionInfo }: { positionInfo: PositionInfo }) {
return <Navigate to="/pools" replace />
}
if (!isMigrateToV4Enabled || !isSameAddress(account?.address, owner)) {
if (!isMigrateEnabled || !isSameAddress(account?.address, owner)) {
navigate('/positions')
}
......
......@@ -29,7 +29,6 @@ import { DeprecatedButton, Flex, Main, Switch, Text, styled } from 'ui/src'
import { InfoCircleFilled } from 'ui/src/components/icons/InfoCircleFilled'
import { useGetPositionQuery } from 'uniswap/src/data/rest/getPosition'
import { getChainInfo } from 'uniswap/src/features/chains/chainInfo'
import { useSupportedChainId } from 'uniswap/src/features/chains/hooks/useSupportedChainId'
import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag, useFeatureFlagWithLoading } from 'uniswap/src/features/gating/hooks'
......@@ -120,7 +119,6 @@ function PositionPage() {
const chainId = useChainIdFromUrlParam()
const chainInfo = chainId ? getChainInfo(chainId) : undefined
const account = useAccount()
const supportedAccountChainId = useSupportedChainId(account.chainId)
const { pathname } = useLocation()
const {
data,
......@@ -134,7 +132,7 @@ function PositionPage() {
? ProtocolVersion.V4
: ProtocolVersion.UNSPECIFIED,
tokenId: tokenIdFromUrl,
chainId: chainId ?? supportedAccountChainId,
chainId: chainId ?? account.chainId,
})
const position = data?.position
const positionInfo = useMemo(() => parseRestPosition(position), [position])
......@@ -146,7 +144,7 @@ function PositionPage() {
const { value: lpRedesignEnabled, isLoading } = useFeatureFlagWithLoading(FeatureFlags.LPRedesign)
const isV4DataEnabled = useFeatureFlag(FeatureFlags.V4Data)
const isMigrateToV4Enabled = useFeatureFlag(FeatureFlags.MigrateV3ToV4)
const isMigrateEnabled = useFeatureFlag(FeatureFlags.MigrateV3ToV4)
const { formatCurrencyAmount } = useFormatter()
const navigate = useNavigate()
......@@ -257,7 +255,7 @@ function PositionPage() {
{positionInfo.version === ProtocolVersion.V3 &&
status !== PositionStatus.CLOSED &&
isV4DataEnabled &&
isMigrateToV4Enabled && (
isMigrateEnabled && (
<MouseoverTooltip
text={t('pool.migrateLiquidityDisabledTooltip')}
disabled={!showV4UnsupportedTooltip}
......
......@@ -3,6 +3,7 @@ import { PositionStatus, ProtocolVersion } from '@uniswap/client-pools/dist/pool
import { DropdownSelector } from 'components/DropdownSelector'
import { lpStatusConfig } from 'components/Liquidity/constants'
import { getProtocolStatusLabel, getProtocolVersionLabel } from 'components/Liquidity/utils'
import { useAccount } from 'hooks/useAccount'
import { useMemo, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom'
......@@ -49,6 +50,7 @@ export function PositionsHeader({
onStatusChange,
}: PositionsHeaderProps) {
const { t } = useTranslation()
const { isConnected } = useAccount()
const { chains } = useEnabledChains()
const navigate = useNavigate()
const isV4DataEnabled = useFeatureFlag(FeatureFlags.V4Data)
......@@ -137,6 +139,7 @@ export function PositionsHeader({
return (
<Flex gap={16}>
<Text variant="heading3">{t('pool.positions.title')}</Text>
{isConnected && (
<Flex gap="$gap8" row $sm={{ flexDirection: 'column' }}>
<Flex gap="$spacing1" row>
<Flex
......@@ -238,6 +241,7 @@ export function PositionsHeader({
</Flex>
)}
</Flex>
)}
</Flex>
)
}
......@@ -23,7 +23,6 @@ import { usePendingLPTransactionsChangeListener } from 'state/transactions/hooks
import { usePairAdder } from 'state/user/hooks'
import { Circle, DeprecatedButton, Flex, Main, Shine, Text, styled } from 'ui/src'
import { useGetPositionQuery } from 'uniswap/src/data/rest/getPosition'
import { useSupportedChainId } from 'uniswap/src/features/chains/hooks/useSupportedChainId'
import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlagWithLoading } from 'uniswap/src/features/gating/hooks'
import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext'
......@@ -80,7 +79,6 @@ function V2PositionPage() {
const { pairAddress } = useParams<{ pairAddress: string }>()
const chainId = useChainIdFromUrlParam()
const account = useAccount()
const supportedAccountChainId = useSupportedChainId(account.chainId)
const {
data,
......@@ -90,7 +88,7 @@ function V2PositionPage() {
owner: account?.address ?? ZERO_ADDRESS,
protocolVersion: ProtocolVersion.V2,
pairAddress,
chainId: chainId ?? supportedAccountChainId,
chainId: chainId ?? account.chainId,
})
const position = data?.position
const positionInfo = useMemo(() => parseRestPosition(position), [position])
......
......@@ -262,11 +262,7 @@ export function CreateTxContextProvider({ children }: { children: React.ReactNod
}
const actualGasFee = createCalldata?.gasFee
const needsApprovals = !!(approvalCalldata?.token0Approval || approvalCalldata?.token1Approval)
const { value: calculatedGasFee } = useTransactionGasFee(
createCalldata?.create,
!!actualGasFee || needsApprovals /* skip */,
)
const { value: calculatedGasFee } = useTransactionGasFee(createCalldata?.create, !!actualGasFee)
const increaseGasFeeUsd = useUSDCurrencyAmountOfGasFee(
createCalldata?.create?.chainId,
actualGasFee || calculatedGasFee,
......
......@@ -248,11 +248,7 @@ export function CreatePositionModal({ isOpen, onClose }: { isOpen: boolean; onCl
</Text>
<BaseQuoteFiatAmount
variant="body1"
price={
derivedPriceRangeInfo.invertPrice
? derivedPriceRangeInfo.price?.invert()
: derivedPriceRangeInfo.price
}
price={derivedPriceRangeInfo?.price}
base={baseCurrency}
quote={quoteCurrency}
/>
......
......@@ -7,7 +7,6 @@ import { BaseQuoteFiatAmount } from 'pages/Pool/Positions/create/BaseQuoteFiatAm
import { useCreatePositionContext, usePriceRangeContext } from 'pages/Pool/Positions/create/CreatePositionContext'
import { PoolOutOfSyncError } from 'pages/Pool/Positions/create/PoolOutOfSyncError'
import { Container } from 'pages/Pool/Positions/create/shared'
import { CreatePositionInfo, PriceRangeState } from 'pages/Pool/Positions/create/types'
import { getInvertedTuple } from 'pages/Pool/Positions/create/utils'
import { useCallback, useMemo, useState } from 'react'
import { Minus, Plus } from 'react-feather'
......@@ -439,27 +438,6 @@ export const SelectPriceRangeStep = ({
[priceRangeState.fullRange, setPriceRangeState],
)
const { rangeInputMinPrice, rangeInputMaxPrice } = useMemo(() => {
if (priceRangeState.fullRange) {
return {
rangeInputMinPrice: undefined,
rangeInputMaxPrice: undefined,
}
}
if (invertPrice) {
return {
rangeInputMinPrice: prices?.[1] ? parseFloat(prices?.[1].invert().toSignificant(8)) : undefined,
rangeInputMaxPrice: prices?.[0] ? parseFloat(prices?.[0].invert().toSignificant(8)) : undefined,
}
}
return {
rangeInputMinPrice: prices?.[0] ? parseFloat(prices?.[0].toSignificant(8)) : undefined,
rangeInputMaxPrice: prices?.[1] ? parseFloat(prices?.[1].toSignificant(8)) : undefined,
}
}, [priceRangeState.fullRange, prices, invertPrice])
const invalidState =
onDisableContinue ||
invalidPrice ||
......@@ -495,7 +473,6 @@ export const SelectPriceRangeStep = ({
}
const showIncrementButtons = !!derivedPositionInfo.pool && !priceRangeState.fullRange
return (
<Container {...rest}>
{creatingPoolOrPair && <InitialPriceInput />}
......@@ -557,7 +534,6 @@ export const SelectPriceRangeStep = ({
)}
{isPriceRangeInputV2Enabled && baseCurrency && quoteCurrency && derivedPositionInfo.poolId && (
<LiquidityRangeInput
key={buildRangeInputKey({ derivedPositionInfo, priceRangeState })}
currency0={quoteCurrency}
currency1={baseCurrency}
feeTier={fee.feeAmount}
......@@ -566,8 +542,16 @@ export const SelectPriceRangeStep = ({
protocolVersion={derivedPositionInfo.protocolVersion}
poolId={derivedPositionInfo.poolId}
disableBrushInteraction={priceRangeState.fullRange}
minPrice={rangeInputMinPrice}
maxPrice={rangeInputMaxPrice}
minPrice={
priceRangeState.fullRange
? undefined
: parseFloat((invertPrice ? prices?.[0]?.invert() : prices?.[0])?.toSignificant(8) ?? '0')
}
maxPrice={
priceRangeState.fullRange
? undefined
: parseFloat((invertPrice ? prices?.[1]?.invert() : prices?.[1])?.toSignificant(8) ?? '0')
}
setMinPrice={(minPrice?: number) => {
handleChartRangeInput(RangeSelectionInput.MIN, minPrice?.toString())
}}
......@@ -627,13 +611,3 @@ export const SelectPriceRangeStep = ({
</Container>
)
}
function buildRangeInputKey({
derivedPositionInfo,
priceRangeState,
}: {
derivedPositionInfo: CreatePositionInfo
priceRangeState: PriceRangeState
}) {
return `${derivedPositionInfo.poolId}-${priceRangeState.fullRange}-${priceRangeState.priceInverted}-${derivedPositionInfo.protocolVersion}`
}
......@@ -247,7 +247,7 @@ export function SelectTokensStep({
hook: hook ?? ZERO_ADDRESS,
})
const feeTiers = getDefaultFeeTiersWithData({ chainId: token0?.chainId, feeTierData, protocolVersion, t })
const feeTiers = getDefaultFeeTiersWithData({ chainId: token0?.chainId, feeTierData, t })
const [defaultFeeTierSelected, setDefaultFeeTierSelected] = useState(false)
const mostUsedFeeTier = useMemo(() => {
if (hasExistingFeeTiers && feeTierData && Object.keys(feeTierData).length > 0) {
......
......@@ -54,7 +54,6 @@ import { useUrlContext } from 'uniswap/src/contexts/UrlContext'
import { useGetPoolsByTokens } from 'uniswap/src/data/rest/getPools'
import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains'
import { useSupportedChainId } from 'uniswap/src/features/chains/hooks/useSupportedChainId'
import { useMaxAmountSpend } from 'uniswap/src/features/gas/useMaxAmountSpend'
import { useOnChainCurrencyBalance } from 'uniswap/src/features/portfolio/api'
import { useUSDCValue } from 'uniswap/src/features/transactions/swap/hooks/useUSDCPrice'
import { getValidAddress } from 'uniswap/src/utils/addresses'
......@@ -356,8 +355,6 @@ export function useDepositInfo(state: UseDepositInfoProps): DepositInfo {
const { balance: token0Balance } = useOnChainCurrencyBalance(token0, address)
const { balance: token1Balance } = useOnChainCurrencyBalance(token1, address)
const token0MaxAmount = useMaxAmountSpend({ currencyAmount: token0Balance })
const token1MaxAmount = useMaxAmountSpend({ currencyAmount: token1Balance })
const [independentToken, dependentToken] = exactField === PositionField.TOKEN0 ? [token0, token1] : [token1, token0]
const independentAmount = tryParseCurrencyAmount(exactAmounts[exactField], independentToken)
......@@ -432,8 +429,8 @@ export function useDepositInfo(state: UseDepositInfoProps): DepositInfo {
return t('common.noAmount.error')
}
const insufficientToken0Balance = currency0Amount && token0MaxAmount?.lessThan(currency0Amount)
const insufficientToken1Balance = currency1Amount && token1MaxAmount?.lessThan(currency1Amount)
const insufficientToken0Balance = currency0Amount && token0Balance?.lessThan(currency0Amount)
const insufficientToken1Balance = currency1Amount && token1Balance?.lessThan(currency1Amount)
if (insufficientToken0Balance && insufficientToken1Balance) {
return <Trans i18nKey="common.insufficientBalance.error" />
......@@ -468,9 +465,9 @@ export function useDepositInfo(state: UseDepositInfoProps): DepositInfo {
deposit0Disabled,
deposit1Disabled,
currency0Amount,
token0MaxAmount,
token0Balance,
currency1Amount,
token1MaxAmount,
token1Balance,
t,
token0?.symbol,
token1?.symbol,
......
......@@ -108,7 +108,6 @@ type BasePriceRangeInfo = {
deposit0Disabled: boolean
deposit1Disabled: boolean
price?: Price<Currency, Currency>
invertPrice: boolean
}
export type OptionalCurrencyPrice = Price<Currency, Currency> | undefined
......@@ -118,6 +117,7 @@ type BasePoolPriceRangeInfo = {
ticksAtLimit: [boolean, boolean]
tickSpaceLimits: [OptionalNumber, OptionalNumber]
isSorted: boolean
invertPrice: boolean
invalidPrice: boolean
invalidRange: boolean
outOfRange: boolean
......
......@@ -395,11 +395,22 @@ function createMockV4Pool({
return pool
}
function createMockPair(price?: Price<Currency, Currency>) {
if (price) {
function createMockPair({
baseCurrency,
quoteCurrency,
price,
}: {
baseCurrency?: Currency
quoteCurrency?: Currency
price?: Price<Currency, Currency>
}) {
const baseToken = baseCurrency?.wrapped
const quoteToken = quoteCurrency?.wrapped
if (baseToken && quoteToken && price) {
return new Pair(
CurrencyAmount.fromRawAmount(price.quoteCurrency.wrapped, price.numerator),
CurrencyAmount.fromRawAmount(price.baseCurrency.wrapped, price.denominator),
CurrencyAmount.fromRawAmount(baseToken, price.numerator),
CurrencyAmount.fromRawAmount(quoteToken, price.denominator),
)
} else {
return undefined
......@@ -519,27 +530,25 @@ export function getV2PriceRangeInfo({
const { currencies } = derivedPositionInfo
const [baseCurrency] = getInvertedTuple(currencies, state.priceInverted)
const baseToken = getCurrencyWithWrap(baseCurrency, ProtocolVersion.V2)
const sortedTokens = getSortedCurrenciesTuple(
const price = getInitialPrice({
baseCurrency: getCurrencyWithWrap(baseCurrency, ProtocolVersion.V2),
sortedCurrencies: getSortedCurrenciesTuple(
getCurrencyWithWrap(currencies[0], ProtocolVersion.V2),
getCurrencyWithWrap(currencies[1], ProtocolVersion.V2),
)
const price = getInitialPrice({
baseCurrency: baseToken,
sortedCurrencies: sortedTokens,
),
initialPrice: state.initialPrice,
})
const invertPrice = Boolean(baseToken && sortedTokens[0] && !baseToken.equals(sortedTokens[0]))
return {
protocolVersion: ProtocolVersion.V2,
price,
mockPair: createMockPair(price),
mockPair: createMockPair({
baseCurrency: currencies[0],
quoteCurrency: currencies[1],
price,
}),
deposit0Disabled: false,
deposit1Disabled: false,
invertPrice,
} satisfies V2PriceRangeInfo
}
......
......@@ -21,7 +21,6 @@ import { ThemeProvider } from 'theme'
import { nativeOnChain } from 'uniswap/src/constants/tokens'
import { useTokenWebQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { getChainInfo } from 'uniswap/src/features/chains/chainInfo'
import { useIsSupportedChainId } from 'uniswap/src/features/chains/hooks/useSupportedChainId'
import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { isAddress } from 'utilities/src/addresses'
import { useChainIdFromUrlParam } from 'utils/chainParams'
......@@ -160,7 +159,6 @@ export default function TokenDetailsPage() {
const pageChainId = account.chainId ?? UniverseChainId.Mainnet
const contextValue = useCreateTDPContext()
const { tokenColor, address, currency, currencyChain, currencyChainId, tokenQuery } = contextValue
const isSupportedChain = useIsSupportedChainId(currencyChainId)
const tokenQueryData = tokenQuery.data?.token
const metatagProperties = useMemo(() => {
......@@ -187,7 +185,7 @@ export default function TokenDetailsPage() {
))}
</Helmet>
{(() => {
if (currency && isSupportedChain) {
if (currency) {
return (
<TDPProvider contextValue={contextValue}>
<TokenDetails />
......
......@@ -4,7 +4,6 @@ import { createContext, useMemo } from 'react'
import { ALL_NETWORKS_ARG } from 'uniswap/src/data/rest/base'
import { useExploreStatsQuery } from 'uniswap/src/data/rest/exploreStats'
import { useProtocolStatsQuery } from 'uniswap/src/data/rest/protocolStats'
import { useIsSupportedChainId } from 'uniswap/src/features/chains/hooks/useSupportedChainId'
import { UniverseChainId } from 'uniswap/src/features/chains/types'
interface QueryResult<T> {
......@@ -49,21 +48,19 @@ export function ExploreContextProvider({
chainId?: UniverseChainId
children: React.ReactNode
}) {
const isSupportedChain = useIsSupportedChainId(chainId)
const {
data: exploreStatsData,
isLoading: exploreStatsLoading,
error: exploreStatsError,
} = useExploreStatsQuery({
chainId: isSupportedChain ? chainId?.toString() : ALL_NETWORKS_ARG,
chainId: chainId ? chainId.toString() : ALL_NETWORKS_ARG,
})
const {
data: protocolStatsData,
isLoading: protocolStatsLoading,
error: protocolStatsError,
} = useProtocolStatsQuery({
chainId: isSupportedChain ? chainId?.toString() : ALL_NETWORKS_ARG,
chainId: chainId ? chainId.toString() : ALL_NETWORKS_ARG,
})
const exploreContext = useMemo(() => {
......
......@@ -18,7 +18,7 @@ import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
function useFilteredPools(pools?: PoolStat[]) {
const filterString = useAtomValue(exploreSearchStringAtom)
const isV4DataEnabled = useFeatureFlag(FeatureFlags.V4Data)
const isV4Enabled = useFeatureFlag(FeatureFlags.V4Data)
const lowercaseFilterString = useMemo(() => filterString.toLowerCase(), [filterString])
......@@ -39,10 +39,10 @@ function useFilteredPools(pools?: PoolStat[]) {
token0HashIncludesFilterString ||
token1HashIncludesFilterString ||
poolNameIncludesFilterString) &&
(pool.protocolVersion?.toLowerCase() !== 'v4' || isV4DataEnabled)
(pool.protocolVersion?.toLowerCase() !== 'v4' || isV4Enabled)
)
}),
[isV4DataEnabled, lowercaseFilterString, pools],
[isV4Enabled, lowercaseFilterString, pools],
)
}
......
import { TransactionResponse } from '@ethersproject/providers'
import { LiquidityEventName } from '@uniswap/analytics-events'
import { getLiquidityEventName } from 'components/Liquidity/analytics'
import { PopupType, addPopup } from 'state/application/reducer'
......@@ -111,13 +112,13 @@ function* handlePositionTransactionStep(params: HandlePositionStepParams) {
const info = getLiquidityTransactionInfo(action)
const txRequest = yield* call(getLiquidityTxRequest, step, signature)
const onModification = ({ hash, data }: { hash: string; data: string }) => {
const onModification = (response: TransactionResponse) => {
if (analytics) {
sendAnalyticsEvent(LiquidityEventName.TRANSACTION_MODIFIED_IN_WALLET, {
...analytics,
transaction_hash: hash,
transaction_hash: response.hash,
expected: txRequest.data?.toString(),
actual: data,
actual: response.data,
})
}
}
......
/* eslint-disable rulesdir/no-undefined-or */
import { TransactionResponse } from '@ethersproject/providers'
import { SwapEventName } from '@uniswap/analytics-events'
import { ZERO_PERCENT } from 'constants/misc'
import { useTotalBalancesUsdForAnalytics } from 'graphql/data/apollo/useTotalBalancesUsdForAnalytics'
......@@ -18,7 +19,6 @@ import {
handleSignatureStep,
} from 'state/sagas/transactions/utils'
import { handleWrapStep } from 'state/sagas/transactions/wrapSaga'
import { VitalTxFields } from 'state/transactions/types'
import invariant from 'tiny-invariant'
import { call, put } from 'typed-redux-saga'
import { FetchError } from 'uniswap/src/data/apiClients/FetchError'
......@@ -55,11 +55,11 @@ import {
ValidatedUniswapXSwapTxAndGasInfo,
} from 'uniswap/src/features/transactions/swap/types/swapTxAndGasInfo'
import { BridgeTrade, ClassicTrade } from 'uniswap/src/features/transactions/swap/types/trade'
import { slippageToleranceToPercent } from 'uniswap/src/features/transactions/swap/utils/format'
import { generateTransactionSteps } from 'uniswap/src/features/transactions/swap/utils/generateTransactionSteps'
import { isClassic } from 'uniswap/src/features/transactions/swap/utils/routing'
import { getClassicQuoteFromResponse } from 'uniswap/src/features/transactions/swap/utils/tradingApi'
import { createSaga } from 'uniswap/src/utils/saga'
import { percentFromFloat } from 'utilities/src/format/percent'
import { logger } from 'utilities/src/logger/logger'
import { useTrace } from 'utilities/src/telemetry/trace/TraceContext'
import { didUserReject } from 'utils/swapErrorToUserReadableMessage'
......@@ -76,12 +76,12 @@ function* handleSwapTransactionStep(params: HandleSwapStepParams) {
const info = getSwapTransactionInfo(trade)
const txRequest = yield* call(getSwapTxRequest, step, signature)
const onModification = ({ hash, data }: VitalTxFields) => {
const onModification = (response: TransactionResponse) => {
sendAnalyticsEvent(SwapEventName.SWAP_MODIFIED_IN_WALLET, {
...analytics,
txHash: hash,
txHash: response.hash,
expected: txRequest.data?.toString() ?? '',
actual: data,
actual: response.data,
})
}
......@@ -96,11 +96,12 @@ function* handleSwapTransactionStep(params: HandleSwapStepParams) {
onModification,
})
const allowedSlippage = trade.slippageTolerance ? percentFromFloat(trade.slippageTolerance) : ZERO_PERCENT
sendAnalyticsEvent(
SwapEventName.SWAP_SIGNED,
formatSwapSignedAnalyticsEventProperties({
trade,
allowedSlippage: trade.slippageTolerance ? slippageToleranceToPercent(trade.slippageTolerance) : ZERO_PERCENT,
allowedSlippage,
fiatValues: {
amountIn: analytics.token_in_amount_usd,
amountOut: analytics.token_out_amount_usd,
......
......@@ -20,7 +20,7 @@ import { HandledTransactionInterrupt } from 'uniswap/src/features/transactions/e
import { getBaseTradeAnalyticsProperties } from 'uniswap/src/features/transactions/swap/analytics'
import { UniswapXSignatureStep } from 'uniswap/src/features/transactions/swap/types/steps'
import { UniswapXTrade } from 'uniswap/src/features/transactions/swap/types/trade'
import { slippageToleranceToPercent } from 'uniswap/src/features/transactions/swap/utils/format'
import { percentFromFloat } from 'utilities/src/format/percent'
interface HandleUniswapXSignatureStepParams extends HandleSignatureStepParams<UniswapXSignatureStep> {
trade: UniswapXTrade
......@@ -35,7 +35,7 @@ export function* handleUniswapXSignatureStep(params: HandleUniswapXSignatureStep
const analyticsParams: Parameters<typeof formatSwapSignedAnalyticsEventProperties>[0] = {
trade,
allowedSlippage: slippageToleranceToPercent(trade.slippageTolerance),
allowedSlippage: percentFromFloat(trade.slippageTolerance),
fiatValues: {
amountIn: analytics.token_in_amount_usd,
amountOut: analytics.token_out_amount_usd,
......@@ -61,7 +61,7 @@ export function* handleUniswapXSignatureStep(params: HandleUniswapXSignatureStep
SwapEventName.SWAP_SIGNED,
formatSwapSignedAnalyticsEventProperties({
trade,
allowedSlippage: slippageToleranceToPercent(trade.slippageTolerance),
allowedSlippage: percentFromFloat(trade.slippageTolerance),
fiatValues: {
amountIn: analytics.token_in_amount_usd,
amountOut: analytics.token_out_amount_usd,
......
import { TransactionResponse } from '@ethersproject/abstract-provider'
import { JsonRpcSigner, Web3Provider } from '@ethersproject/providers'
import { JsonRpcSigner, TransactionResponse, Web3Provider } from '@ethersproject/providers'
import { TradeType } from '@uniswap/sdk-core'
import { wagmiConfig } from 'components/Web3Provider/wagmiConfig'
import { clientToProvider } from 'hooks/useEthersProvider'
......@@ -14,15 +13,13 @@ import {
TransactionDetails,
TransactionInfo,
TransactionType,
VitalTxFields,
} from 'state/transactions/types'
import { isPendingTx } from 'state/transactions/utils'
import { InterfaceState } from 'state/webReducer'
import { SagaGenerator, call, cancel, delay, fork, put, race, select, take } from 'typed-redux-saga'
import { SagaGenerator, call, cancel, fork, put, race, select, take } from 'typed-redux-saga'
import { TransactionStatus } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { Routing } from 'uniswap/src/data/tradingApi/__generated__'
import { AccountMeta } from 'uniswap/src/features/accounts/types'
import { isL2ChainId } from 'uniswap/src/features/chains/utils'
import {
ApprovalEditedInWalletError,
HandledTransactionInterrupt,
......@@ -49,8 +46,7 @@ import { Sentry } from 'utilities/src/logger/Sentry'
import noop from 'utilities/src/react/noop'
import { currencyId } from 'utils/currencyId'
import { signTypedData } from 'utils/signing'
import { Transaction } from 'viem'
import { getConnectorClient, getTransaction } from 'wagmi/actions'
import { getConnectorClient } from 'wagmi/actions'
export interface HandleSignatureStepParams<T extends SignatureTransactionStep = SignatureTransactionStep> {
account: AccountMeta
......@@ -96,11 +92,12 @@ export interface HandleOnChainStepParams<T extends OnChainTransactionStep = OnCh
/** Controls whether the function should wait to return until after the transaction has confirmed. Defaults to `true`. */
shouldWaitForConfirmation?: boolean
/** Called when data returned from a submitted transaction differs from data originally sent to the wallet. */
onModification?: (response: VitalTxFields) => void | Generator<unknown, void, unknown>
onModification?: (response: TransactionResponse) => void | Generator<unknown, void, unknown>
}
export function* handleOnChainStep<T extends OnChainTransactionStep>(params: HandleOnChainStepParams<T>) {
const { account, step, setCurrentStep, info, allowDuplicativeTx, ignoreInterrupt, onModification } = params
const { chainId } = step.txRequest
const signer = yield* call(getSigner, account.address)
addTransactionBreadcrumb({ step, data: { ...info } })
......@@ -124,7 +121,8 @@ export function* handleOnChainStep<T extends OnChainTransactionStep>(params: Han
setCurrentStep({ step, accepted: false })
// Prompt wallet to submit transaction
const { hash, nonce, data } = yield* call(submitTransaction, params)
const response = yield* call([signer, 'sendTransaction'], step.txRequest)
const { hash, nonce, data } = response
// Trigger waiting UI after user accepts
setCurrentStep({ step, accepted: true })
......@@ -133,7 +131,7 @@ export function* handleOnChainStep<T extends OnChainTransactionStep>(params: Han
yield* put(addTransaction({ from: account.address, info, hash, nonce, chainId }))
if (step.txRequest.data !== data && onModification) {
yield* call(onModification, { hash, data, nonce })
yield* call(onModification, response)
}
// If the transaction flow was interrupted while awaiting input, throw an error after input is received
......@@ -169,58 +167,6 @@ function* handleOnChainConfirmation(params: HandleOnChainStepParams, hash: strin
return hash
}
/** Submits a transaction and handles potential wallet errors */
function* submitTransaction(params: HandleOnChainStepParams): SagaGenerator<VitalTxFields> {
const { account, step } = params
const signer = yield* call(getSigner, account.address)
try {
const response = yield* call([signer, 'sendTransaction'], step.txRequest)
return transformTransactionResponse(response)
} catch (error) {
if (error && typeof error === 'object' && 'transactionHash' in error) {
return yield* recoverTransactionFromHash(error.transactionHash as `0x${string}`, step)
}
throw error
}
}
/** Polls for transaction details when only hash is known */
function* recoverTransactionFromHash(hash: `0x${string}`, step: OnChainTransactionStep): SagaGenerator<VitalTxFields> {
const transaction = yield* pollForTransaction(hash, step.txRequest.chainId)
if (!transaction) {
throw new TransactionStepFailedError({ message: `Transaction not found`, step })
}
return transformTransactionResponse(transaction)
}
/** Polls until transaction is found or timeout is reached */
function* pollForTransaction(hash: `0x${string}`, chainId: number) {
const POLL_INTERVAL = 2_000
const MAX_POLLING_TIME = isL2ChainId(chainId) ? 12_000 : 24_000
let elapsed = 0
while (elapsed < MAX_POLLING_TIME) {
try {
return yield* call(getTransaction, wagmiConfig, { chainId, hash })
} catch {
yield* delay(POLL_INTERVAL)
elapsed += POLL_INTERVAL
}
}
return null
}
/** Transforms a TransactionResponse or a Transaction into { hash: string; data: string; nonce: number } */
function transformTransactionResponse(response: TransactionResponse | Transaction): VitalTxFields {
if ('data' in response) {
return { hash: response.hash, data: response.data, nonce: response.nonce }
}
return { hash: response.hash, data: response.input, nonce: response.nonce }
}
interface HandleApprovalStepParams
extends Omit<HandleOnChainStepParams<TokenApprovalTransactionStep | TokenRevocationTransactionStep>, 'info'> {}
export function* handleApprovalTransactionStep(params: HandleApprovalStepParams) {
......@@ -229,14 +175,14 @@ export function* handleApprovalTransactionStep(params: HandleApprovalStepParams)
return yield* call(handleOnChainStep, {
...params,
info,
*onModification({ hash, data }: VitalTxFields) {
const { isInsufficient, approvedAmount } = checkApprovalAmount(data, step)
*onModification(response: TransactionResponse) {
const { isInsufficient, approvedAmount } = checkApprovalAmount(response, step)
// Update state to reflect hte actual approval amount submitted on-chain
yield* put(
updateTransactionInfo({
chainId: step.txRequest.chainId,
hash,
hash: response.hash,
info: { ...info, amount: approvedAmount },
}),
)
......@@ -259,9 +205,12 @@ function getApprovalTransactionInfo(
}
}
function checkApprovalAmount(data: string, step: TokenApprovalTransactionStep | TokenRevocationTransactionStep) {
function checkApprovalAmount(
response: TransactionResponse,
step: TokenApprovalTransactionStep | TokenRevocationTransactionStep,
) {
const requiredAmount = BigInt(`0x${parseInt(step.amount, 10).toString(16)}`)
const submitted = parseERC20ApproveCalldata(data)
const submitted = parseERC20ApproveCalldata(response.data)
const approvedAmount = submitted.amount.toString(10)
// Special case: for revoke tx's, the approval is insufficient if anything other than an empty approval was submitted on chain.
......
......@@ -454,15 +454,13 @@ export function useInitialCurrencyState(): {
defaultChainId,
])
const outputChainIsSupported = useSupportedChainId(parsedCurrencyState.outputChainId)
const initialOutputCurrencyAddress = useMemo(
() =>
// clear output if identical unless there's a supported outputChainId which means we're bridging
initialInputCurrencyAddress === parsedCurrencyState.outputCurrencyId && !outputChainIsSupported
// clear output if identical unless there's an outputChainId which means we're bridging
initialInputCurrencyAddress === parsedCurrencyState.outputCurrencyId && !parsedCurrencyState.outputChainId
? undefined
: parsedCurrencyState.outputCurrencyId,
[initialInputCurrencyAddress, parsedCurrencyState.outputCurrencyId, outputChainIsSupported],
[initialInputCurrencyAddress, parsedCurrencyState.outputCurrencyId, parsedCurrencyState.outputChainId],
)
const initialInputCurrency = useCurrency(initialInputCurrencyAddress, initialChainId)
......
import { TransactionResponse } from '@ethersproject/abstract-provider'
import { TradeType } from '@uniswap/sdk-core'
import { VoteOption } from 'state/governance/types'
import {
......@@ -282,5 +281,3 @@ export interface ConfirmedTransactionDetails extends BaseTransactionDetails {
}
export type TransactionDetails = PendingTransactionDetails | ConfirmedTransactionDetails
export type VitalTxFields = Pick<TransactionResponse, 'hash' | 'nonce' | 'data'>
......@@ -194,7 +194,7 @@ export function useTrackedTokenPairs(): [Token, Token][] {
() =>
chainId && popularTokens?.topTokens
? popularTokens.topTokens.flatMap((gqlToken) => {
if (!gqlToken || !gqlToken.address) {
if (!gqlToken) {
return []
}
const token = gqlToCurrency(gqlToken)
......
......@@ -2,7 +2,6 @@ import { ParsedQs } from 'qs'
import { useParams } from 'react-router-dom'
// eslint-disable-next-line no-restricted-imports
import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/features/chains/chainInfo'
import { useSupportedChainId } from 'uniswap/src/features/chains/hooks/useSupportedChainId'
import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { CurrencyField } from 'uniswap/src/types/currency'
......@@ -30,9 +29,7 @@ export function useChainIdFromUrlParam(): UniverseChainId | undefined {
const chainName = useParams<{ chainName?: string }>().chainName
// In the case where /explore/:chainName is used, the chainName is passed as a tab param
const tab = useParams<{ tab?: string }>().tab
const chainId = getChainIdFromChainUrlParam(chainName ?? tab)
const supportedChainId = useSupportedChainId(chainId)
return supportedChainId
return getChainIdFromChainUrlParam(chainName ?? tab)
}
export function getParsedChainId(
......
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="arrow">
<path id="Vector" fill-rule="evenodd" clip-rule="evenodd" d="M5.24408 14.7559C5.56951 15.0814 6.09715 15.0814 6.42259 14.7559L13.3333 7.84518V14.1667C13.3333 14.6269 13.7064 15 14.1667 15C14.6269 15 15 14.6269 15 14.1667V5.83333C15 5.3731 14.6269 5 14.1667 5H5.83333C5.3731 5 5 5.3731 5 5.83333C5 6.29357 5.3731 6.66667 5.83333 6.66667H12.1548L5.24408 13.5774C4.91864 13.9028 4.91864 14.4305 5.24408 14.7559Z" fill="#7D7D7D"/>
</g>
</svg>
......@@ -13,6 +13,7 @@ type InlineCardProps = {
iconBackgroundColor?: ColorTokens
heading?: string | JSX.Element
CtaButtonIcon?: GeneratedIcon | ((props: IconProps) => JSX.Element)
CtaButtonIconColor?: ColorTokens
onPressCtaButton?: () => void
}
......@@ -25,6 +26,7 @@ export function InlineCard({
heading,
description,
CtaButtonIcon,
CtaButtonIconColor = '$neutral3',
onPressCtaButton,
}: InlineCardProps): JSX.Element {
const icon = <Icon color={iconColor ?? color} size="$icon.20" />
......@@ -64,7 +66,7 @@ export function InlineCard({
</Flex>
{CtaButtonIcon && (
<TouchableArea onPress={onPressCtaButton}>
<CtaButtonIcon color="$neutral3" size="$icon.20" />
<CtaButtonIcon color={CtaButtonIconColor} size="$icon.20" />
</TouchableArea>
)}
</Flex>
......
import { G, Path, Svg } from 'react-native-svg'
// eslint-disable-next-line no-relative-import-paths/no-relative-import-paths
import { createIcon } from '../factories/createIcon'
export const [ArrowUpRight, AnimatedArrowUpRight] = createIcon({
name: 'ArrowUpRight',
getIcon: (props) => (
<Svg viewBox="0 0 20 20" fill="none" {...props}>
<G id="arrow">
<Path
id="Vector"
d="M5.24408 14.7559C5.56951 15.0814 6.09715 15.0814 6.42259 14.7559L13.3333 7.84518V14.1667C13.3333 14.6269 13.7064 15 14.1667 15C14.6269 15 15 14.6269 15 14.1667V5.83333C15 5.3731 14.6269 5 14.1667 5H5.83333C5.3731 5 5 5.3731 5 5.83333C5 6.29357 5.3731 6.66667 5.83333 6.66667H12.1548L5.24408 13.5774C4.91864 13.9028 4.91864 14.4305 5.24408 14.7559Z"
fill={'currentColor' ?? '#7D7D7D'}
fillRule="evenodd"
clipRule="evenodd"
/>
</G>
</Svg>
),
defaultFill: '#7D7D7D',
})
......@@ -23,6 +23,7 @@ export * from './ArrowTurnDownRight'
export * from './ArrowUpCircle'
export * from './ArrowUpDown'
export * from './ArrowUpInCircle'
export * from './ArrowUpRight'
export * from './ArrowsLeftRight'
export * from './BadgeDollar'
export * from './Bank'
......
import { ComponentProps, ReactNode } from 'react'
import { Popover } from 'tamagui'
import { WebBottomSheet } from 'ui/src/components/modal/AdaptiveWebModal'
type AdaptiveWebPopoverContentProps = Omit<ComponentProps<typeof Popover.Content>, 'children'> & {
children: ReactNode
isOpen: boolean
webBottomSheetProps?: Omit<ComponentProps<typeof WebBottomSheet>, 'children' | 'isOpen'>
}
/**
* AdaptiveWebPopoverContent is a responsive popover component that adapts to different screen sizes.
* On larger screens, it renders as a popover.
* On smaller screens (mobile devices), it adapts into a bottom sheet.
*/
export function AdaptiveWebPopoverContent({
children,
isOpen,
webBottomSheetProps,
...popoverContentProps
}: AdaptiveWebPopoverContentProps): JSX.Element {
return (
<>
<Popover.Content {...popoverContentProps}>{children}</Popover.Content>
<Popover.Adapt when="sm">
<WebBottomSheet isOpen={isOpen} {...(webBottomSheetProps || {})}>
<Popover.Adapt.Contents />
</WebBottomSheet>
</Popover.Adapt>
</>
)
}
......@@ -70,7 +70,6 @@ export type { GeneratedIcon, IconProps } from './components/factories/createIcon
export * from './components/input/utils'
export { Flex, Inset, Separator, flexStyles, type FlexProps } from './components/layout'
export { ModalCloseIcon, WebBottomSheet } from './components/modal/AdaptiveWebModal'
export { AdaptiveWebPopoverContent } from './components/popover/AdaptiveWebPopoverContent'
export * from './components/radio/Radio'
export { ClickableWithinGesture } from './components/swipeablecards/ClickableWithinGesture'
export { SwipeableCardStack } from './components/swipeablecards/SwipeableCardStack'
......
import React from 'react'
// eslint-disable-next-line no-restricted-imports
import type { ImageSourcePropType } from 'react-native'
import { Flex, FlexProps, Image, useSporeColors } from 'ui/src'
import { ALL_NETWORKS_LOGO, ALL_NETWORKS_LOGO_UNICHAIN } from 'ui/src/assets'
import { iconSizes, zIndices } from 'ui/src/theme'
......@@ -8,7 +6,6 @@ import { getChainInfo } from 'uniswap/src/features/chains/chainInfo'
import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
import { isMobileWeb } from 'utilities/src/platform'
export const SQUIRCLE_BORDER_RADIUS_RATIO = 0.3
......@@ -52,7 +49,7 @@ function _NetworkLogo({
return (
<Flex testID="all-networks-logo">
<NetworkImage logo={logo} imageSize={size} />
<Image resizeMode="contain" source={logo} style={imageStyle} />
</Flex>
)
}
......@@ -62,19 +59,9 @@ function _NetworkLogo({
return logo ? (
<Flex testID="network-logo" overflow="hidden" style={imageStyle} zIndex={zIndices.mask}>
<NetworkImage logo={logo} imageSize={imageSize} />
<Image resizeMode="contain" source={logo} width={imageSize} height={imageSize} />
</Flex>
) : null
}
function NetworkImage({ logo, imageSize }: { logo: ImageSourcePropType; imageSize: number }): JSX.Element {
// As of iOS 18.3 network logos are no longer displaying because react-native-web-lite
// adds z-index: -1 to the image. This is a workaround to display the logos on mobile web.
return isMobileWeb && typeof logo === 'string' ? (
<img src={logo} style={{ width: imageSize, height: imageSize }} />
) : (
<Image resizeMode="contain" source={logo} width={imageSize} height={imageSize} />
)
}
export const NetworkLogo = React.memo(_NetworkLogo)
import { SharedEventName } from '@uniswap/analytics-events'
import { useState } from 'react'
import { Flex, GeneratedIcon, InlineCard, LabeledCheckbox, Text } from 'ui/src'
import { Flex, InlineCard, LabeledCheckbox, Text } from 'ui/src'
import { InfoCircleFilled } from 'ui/src/components/icons/InfoCircleFilled'
import { WarningSeverity } from 'uniswap/src/components/modals/WarningModal/types'
import { getWarningIcon, getWarningIconColors } from 'uniswap/src/components/warnings/utils'
......@@ -21,7 +21,6 @@ type InlineWarningCardProps = {
headingTestId?: string
descriptionTestId?: string
analyticsProperties?: Record<string, unknown>
Icon?: GeneratedIcon
}
export function InlineWarningCard({
......@@ -37,7 +36,6 @@ export function InlineWarningCard({
headingTestId,
descriptionTestId,
analyticsProperties,
Icon,
}: InlineWarningCardProps): JSX.Element | null {
const [checkedFallback, setCheckedFallback] = useState(false)
const { color, textColor, backgroundColor } = getWarningIconColors(severity)
......@@ -83,7 +81,7 @@ export function InlineWarningCard({
return (
<InlineCard
CtaButtonIcon={shouldShowCtaIcon ? InfoCircleFilled : undefined}
Icon={Icon ?? WarningIcon}
Icon={WarningIcon}
color={textColor}
description={
<Flex gap="$spacing8">
......
......@@ -12,12 +12,7 @@ import {
TokenSectionBaseList,
TokenSectionBaseListRef,
} from 'uniswap/src/components/TokenSelector/lists/TokenSectionBaseList/TokenSectionBaseList'
import {
OnSelectCurrency,
TokenOption,
TokenOptionSection,
TokenSection,
} from 'uniswap/src/components/TokenSelector/types'
import { OnSelectCurrency, TokenOption, TokenSection } from 'uniswap/src/components/TokenSelector/types'
import { useBottomSheetFocusHook } from 'uniswap/src/components/modals/hooks'
import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains'
import { UniverseChainId } from 'uniswap/src/features/chains/types'
......@@ -181,7 +176,7 @@ function _TokenSelectorList({
isKeyboardOpen={isKeyboardOpen}
section={section}
showTokenAddress={showTokenAddress}
showWarnings={section.sectionKey === TokenOptionSection.BridgingTokens ? false : showTokenWarnings}
showWarnings={showTokenWarnings}
tokenOption={item}
onSelectCurrency={onSelectCurrency}
/>
......
import React, { useCallback, useEffect, useState } from 'react'
import React, { useCallback, useState } from 'react'
import { useDispatch } from 'react-redux'
import { Flex, Text, TouchableArea, useSporeColors } from 'ui/src'
import Check from 'ui/src/assets/icons/check.svg'
......@@ -10,7 +10,7 @@ import { WarningSeverity } from 'uniswap/src/components/modals/WarningModal/type
import WarningIcon from 'uniswap/src/components/warnings/WarningIcon'
import { getWarningIconColors } from 'uniswap/src/components/warnings/utils'
import { SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { setHasSeenBridgingAnimation, setHasSeenBridgingTooltip } from 'uniswap/src/features/behaviorHistory/slice'
import { setHasSeenBridgingTooltip } from 'uniswap/src/features/behaviorHistory/slice'
import { UniverseChainId } from 'uniswap/src/features/chains/types'
import TokenWarningModal from 'uniswap/src/features/tokens/TokenWarningModal'
import { getTokenWarningSeverity } from 'uniswap/src/features/tokens/safetyUtils'
......@@ -19,7 +19,6 @@ import { getSymbolDisplayText } from 'uniswap/src/utils/currency'
import { shortenAddress } from 'utilities/src/addresses'
import { dismissNativeKeyboard } from 'utilities/src/device/keyboard'
import { isInterface } from 'utilities/src/platform'
import { ONE_SECOND_MS } from 'utilities/src/time/time'
interface OptionProps {
option: TokenOption
......@@ -61,9 +60,9 @@ function _TokenOptionItem({
const { colorSecondary: warningIconColor } = getWarningIconColors(severity)
const shouldShowWarningModalOnPress = isBlocked || (severity !== WarningSeverity.None && !tokenWarningDismissed)
const { shouldShowUnichainBridgingAnimation } = useUnichainTooltipVisibility()
const { shouldShowUnichainBridgingTooltip } = useUnichainTooltipVisibility()
const isUnichainEth = currency.isNative && currency.chainId === UniverseChainId.Unichain
const showUnichainPromoAnimation = shouldShowUnichainBridgingAnimation && isUnichainEth
const showUnichainPromoAnimation = shouldShowUnichainBridgingTooltip && isUnichainEth
const handleShowWarningModal = useCallback((): void => {
dismissNativeKeyboard()
......@@ -93,17 +92,6 @@ function _TokenOptionItem({
onPress()
}, [onPress])
useEffect(() => {
if (showUnichainPromoAnimation) {
// delay to prevent ux jank
const delay = setTimeout(() => {
dispatch(setHasSeenBridgingAnimation(true))
}, ONE_SECOND_MS * 2)
return () => clearTimeout(delay)
}
return undefined
}, [dispatch, showUnichainPromoAnimation])
return (
<>
<TouchableArea
......
......@@ -37,12 +37,7 @@ export const SectionFooter = memo(function _SectionFooter({
}
return (
<AnimateInOrder
index={2}
delayMs={isMobileWeb ? 800 : 1500}
animation="125msDelayedLong"
enterStyle={{ opacity: 0 }}
>
<AnimateInOrder index={0} delayMs={800} animation="125msDelayedLong" enterStyle={{ opacity: 0 }}>
<Flex
row
alignItems="center"
......
......@@ -104,10 +104,11 @@ export function NetworkOption({
<AnimateInOrder
index={showUnichainAnimation ? 1 : 0}
delayMs={showUnichainAnimation ? (isInterface ? 250 : 750) : 0}
animation="125ms"
enterStyle={showUnichainAnimation ? { height: 0 } : null}
// fixed height is necessary to allow for smooth animation
animation={showUnichainAnimation ? '125ms' : null}
enterStyle={showUnichainAnimation ? { height: 0, width: 200 } : null}
// defined height & width are necessary to allow for smooth animation
height={44}
width="auto"
>
<Flex
row
......
......@@ -41,6 +41,7 @@ export const uniswapUrls = {
limitsInfo: `${helpUrl}/sections/24372644881293`,
limitsNetworkSupport: `${helpUrl}/articles/24470251716237-What-networks-do-limits-support`,
fiatOnRampHelp: `${helpUrl}/articles/11306574799117`,
fiatOffRampHelp: `${helpUrl}/articles/34006552258957`,
transferCryptoHelp: `${helpUrl}/articles/27103878635661-How-to-transfer-crypto-from-a-Robinhood-or-Coinbase-account-to-the-Uniswap-Wallet`,
moonpayRegionalAvailability: `${helpUrl}/articles/11306664890381-Why-isn-t-MoonPay-available-in-my-region-`,
networkFeeInfo: `${helpUrl}/articles/8370337377805-What-is-a-network-fee-`,
......
import { uniswapUrls } from 'uniswap/src/constants/urls'
import { FetchError } from 'uniswap/src/data/apiClients/FetchError'
import { REQUEST_SOURCE, getVersionHeader } from 'uniswap/src/data/constants'
import { logger } from 'utilities/src/logger/logger'
import { isMobileApp } from 'utilities/src/platform'
export const BASE_UNISWAP_HEADERS = {
......@@ -31,14 +32,22 @@ export function createApiClient({
return {
get fetch() {
return (path: string, options: Parameters<typeof fetch>[1]) => {
return fetch(`${baseUrl}${path}`, {
return async (path: string, options: Parameters<typeof fetch>[1]) => {
try {
return await fetch(`${baseUrl}${path}`, {
...options,
headers: {
...headers,
...options?.headers,
},
})
} catch (error) {
logger.debug('apiClients', 'fetch', 'Failed to fetch', error, {
path,
...options,
})
throw error
}
}
},
......@@ -63,6 +72,10 @@ export function createApiClient({
if (!response.ok) {
let data: object | undefined
logger.debug('apiClients', 'get', 'Failed to fetch', response, {
path,
...options,
})
try {
data = await response.json()
} catch (e) {
......
......@@ -38,6 +38,8 @@ import {
UniversalRouterVersion,
} from 'uniswap/src/data/tradingApi/__generated__'
import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { getFeatureFlag } from 'uniswap/src/features/gating/hooks'
import { isTestEnv } from 'utilities/src/environment/env'
// TradingAPI team is looking into updating type generation to produce the following types for it's current QuoteResponse type:
......@@ -127,15 +129,18 @@ export async function fetchOrders({ orderIds }: { orderIds: string[] }): Promise
}
export async function fetchSwappableTokens(params: SwappableTokensParams): Promise<GetSwappableTokensResponse> {
const chainBlocklist = params.unichainEnabled ? [] : [UniverseChainId.Unichain.toString()]
const unichainEnabled = getFeatureFlag(FeatureFlags.Unichain)
const chainBlocklist = unichainEnabled ? [] : [UniverseChainId.Unichain.toString()]
return await TradingApiClient.get<GetSwappableTokensResponse>(uniswapUrls.tradingApiPaths.swappableTokens, {
params: {
tokenIn: params.tokenIn,
tokenInChainId: params.tokenInChainId,
...(params.tokenOut && { tokenOut: params.tokenOut }),
...(params.tokenOutChainId && { tokenOutChainId: params.tokenOutChainId }),
},
headers:
params.unichainEnabled || isTestEnv()
unichainEnabled || isTestEnv()
? {}
: {
'x-chain-blocklist': chainBlocklist.join(','),
......
......@@ -13,17 +13,19 @@ import { TRADING_API_CACHE_KEY, fetchSwappableTokens } from 'uniswap/src/data/ap
import { UseQueryApiHelperHookArgs } from 'uniswap/src/data/apiClients/types'
import { ChainId, GetSwappableTokensResponse } from 'uniswap/src/data/tradingApi/__generated__'
import { TradeableAsset } from 'uniswap/src/entities/assets'
import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { getFeatureFlag } from 'uniswap/src/features/gating/hooks'
import {
getTokenAddressFromChainForTradingApi,
toTradingApiSupportedChainId,
} from 'uniswap/src/features/transactions/swap/utils/tradingApi'
import { logger } from 'utilities/src/logger/logger'
import { MAX_REACT_QUERY_CACHE_TIME_MS } from 'utilities/src/time/time'
export type SwappableTokensParams = {
tokenIn: Address
tokenInChainId: ChainId
unichainEnabled?: boolean
tokenOut?: Address
tokenOutChainId?: ChainId
}
export function useTradingApiSwappableTokensQuery({
......@@ -38,9 +40,6 @@ export function useTradingApiSwappableTokensQuery({
return useQuery<GetSwappableTokensResponse>({
queryKey,
queryFn: params ? swappableTokensQueryFn(params) : skipToken,
// In order for `getSwappableTokensQueryData` to be more likely to have cached data,
// we set the `gcTime` to the longest possible time.
gcTime: MAX_REACT_QUERY_CACHE_TIME_MS,
...rest,
})
}
......@@ -77,9 +76,6 @@ export function usePrefetchSwappableTokens(input: Maybe<TradeableAsset>): void {
tokenIn,
tokenInChainId,
}),
// In order for `getSwappableTokensQueryData` to be more likely to have cached data,
// we set the `gcTime` to the longest possible time.
gcTime: MAX_REACT_QUERY_CACHE_TIME_MS,
})
}
......@@ -92,7 +88,14 @@ export function usePrefetchSwappableTokens(input: Maybe<TradeableAsset>): void {
}
const swappableTokensQueryKey = (params?: SwappableTokensParams): QueryKey => {
return [TRADING_API_CACHE_KEY, uniswapUrls.tradingApiPaths.swappableTokens, params]
const unichainEnabled = getFeatureFlag(FeatureFlags.Unichain)
return [
TRADING_API_CACHE_KEY,
uniswapUrls.tradingApiPaths.swappableTokens,
params?.tokenIn,
params?.tokenInChainId,
unichainEnabled,
]
}
const swappableTokensQueryFn = (
......
......@@ -3,7 +3,7 @@ import { getNativeAddress } from 'uniswap/src/constants/addresses'
import { GetSwappableTokensResponse } from 'uniswap/src/data/tradingApi/__generated__'
import { toSupportedChainId } from 'uniswap/src/features/chains/utils'
import { CurrencyInfo } from 'uniswap/src/features/dataApi/types'
import { buildCurrency } from 'uniswap/src/features/dataApi/utils'
import { buildCurrency, getCurrencySafetyInfo } from 'uniswap/src/features/dataApi/utils'
import { NATIVE_ADDRESS_FOR_TRADING_API } from 'uniswap/src/features/transactions/swap/utils/tradingApi'
import { currencyId } from 'uniswap/src/utils/currencyId'
......@@ -29,12 +29,15 @@ export function tradingApiSwappableTokenToCurrencyInfo(
return undefined
}
const safetyLevel = toGqlSafetyLevel(token.project.safetyLevel)
const currencyInfo: CurrencyInfo = {
currency,
currencyId: currencyId(currency),
logoUrl: token.project?.logo?.url,
isSpam: token.project?.isSpam,
safetyLevel: toGqlSafetyLevel(token.project?.safetyLevel),
logoUrl: token.project.logo?.url,
isSpam: token.project.isSpam,
safetyLevel,
safetyInfo: getCurrencySafetyInfo(safetyLevel ?? undefined),
}
return currencyInfo
......
......@@ -830,8 +830,9 @@ query SearchTokens($searchQuery: String!, $chains: [Chain!]!) {
query ExploreSearch(
$searchQuery: String!
$nftCollectionsFilter: NftCollectionsFilterInput!
$chains: [Chain!]
) {
searchTokens(searchQuery: $searchQuery) {
searchTokens(searchQuery: $searchQuery, chains: $chains) {
chain
address
decimals
......@@ -1014,6 +1015,61 @@ fragment TopTokenParts on Token {
}
}
query ExploreTokensTab($topTokensOrderBy: TokenSortableField!, $chain: Chain!, $pageSize: Int!) {
topTokens(
chain: $chain
page: 1
pageSize: $pageSize
orderBy: $topTokensOrderBy
) {
...TopTokenParts
}
# `topTokens` returns WETH rather than ETH
# here we retrieve ETH information to swap out in the UI
eth: token(address: null, chain: $chain) {
...TopTokenParts
}
}
fragment AITopTokenParts on Token {
symbol
chain
address
market {
totalValueLocked {
value
}
volume(duration: DAY) {
value
}
}
name
project {
markets(currencies: [USD]) {
price {
value
}
pricePercentChange24h {
value
}
marketCap {
value
}
}
}
}
query AITopTokens($topTokensOrderBy: TokenSortableField!, $chain: Chain!, $pageSize: Int!) {
topTokens(
chain: $chain
page: 1
pageSize: $pageSize
orderBy: $topTokensOrderBy
) {
...AITopTokenParts
}
}
fragment HomeScreenTokenParts on Token {
...TokenBasicInfoParts
project {
......
query SearchPopularTokensWeb($chain: Chain!, $orderBy: TokenSortableField) {
topTokens(chain: $chain, orderBy: $orderBy, page: 1, pageSize: 100) {
id
address
chain
symbol
name
decimals
project {
id
logoUrl
safetyLevel
isSpam
}
}
}
fragment TokenPrice on Token {
query V3Pool($chain: Chain!, $address: String!) {
v3Pool(chain: $chain, address: $address) {
id
protocolVersion
address
feeTier
token0 {
...SimpleTokenDetails
project {
id
markets(currencies: [USD]) {
......@@ -14,29 +20,24 @@ fragment TokenPrice on Token {
url
}
}
market(currency: USD) {
}
token0Supply
token1 {
...SimpleTokenDetails
project {
id
markets(currencies: [USD]) {
id
price {
id
value
}
}
}
query V3Pool($chain: Chain!, $address: String!) {
v3Pool(chain: $chain, address: $address) {
logo {
id
protocolVersion
address
feeTier
token0 {
...SimpleTokenDetails
...TokenPrice
url
}
}
token0Supply
token1 {
...SimpleTokenDetails
...TokenPrice
}
token1Supply
txCount
......@@ -68,12 +69,38 @@ query V4Pool($chain: Chain!, $poolId: String!) {
}
token0 {
...SimpleTokenDetails
...TokenPrice
project {
id
markets(currencies: [USD]) {
id
price {
id
value
}
}
logo {
id
url
}
}
}
token0Supply
token1 {
...SimpleTokenDetails
...TokenPrice
project {
id
markets(currencies: [USD]) {
id
price {
id
value
}
}
logo {
id
url
}
}
}
token1Supply
txCount
......@@ -163,12 +190,38 @@ query V2Pair($chain: Chain!, $address: String!) {
address
token0 {
...SimpleTokenDetails
...TokenPrice
project {
id
markets(currencies: [USD]) {
id
price {
id
value
}
}
logo {
id
url
}
}
}
token0Supply
token1 {
...SimpleTokenDetails
...TokenPrice
project {
id
markets(currencies: [USD]) {
id
price {
id
value
}
}
logo {
id
url
}
}
}
token1Supply
txCount
......
query V4TokenTransactions($chain: Chain!, $address: String!, $first: Int!, $cursor: Int) {
token(chain: $chain, address: $address) {
...TransactionToken
v4Transactions(first: $first, timestampCursor: $cursor) {
...PoolTx
}
}
}
query V3TokenTransactions($chain: Chain!, $address: String!, $first: Int!, $cursor: Int) {
token(chain: $chain, address: $address) {
...TransactionToken
v3Transactions(first: $first, timestampCursor: $cursor) {
...PoolTx
timestamp
hash
account
token0 {
...TransactionToken
}
token0Quantity
token1 {
...TransactionToken
}
token1Quantity
usdValue {
value
}
type
}
}
}
query V2TokenTransactions($chain: Chain!, $address: String!, $first: Int!, $cursor: Int) {
token(chain: $chain, address: $address) {
...TransactionToken
v2Transactions(first: $first, timestampCursor: $cursor) {
...PoolTx
timestamp
hash
account
token0 {
...TransactionToken
}
token0Quantity
token1 {
...TransactionToken
}
token1Quantity
usdValue {
value
}
type
}
}
}
......@@ -20,7 +20,8 @@ fragment TransactionToken on Token {
}
}
fragment PoolTx on PoolTransaction {
query V3Transactions($chain: Chain!, $first: Int!, $cursor: Int) {
v3Transactions(chain: $chain, first: $first, timestampCursor: $cursor) {
id
chain
protocolVersion
......@@ -40,17 +41,30 @@ fragment PoolTx on PoolTransaction {
value
}
type
}
query V4Transactions($chain: Chain!, $first: Int!, $cursor: Int) {
v4Transactions(chain: $chain, first: $first, timestampCursor: $cursor) { ...PoolTx }
}
query V3Transactions($chain: Chain!, $first: Int!, $cursor: Int) {
v3Transactions(chain: $chain, first: $first, timestampCursor: $cursor) { ...PoolTx }
}
}
query V2Transactions($chain: Chain!, $first: Int!, $cursor: Int) {
v2Transactions(chain: $chain, first: $first, timestampCursor: $cursor) { ...PoolTx }
v2Transactions(chain: $chain, first: $first, timestampCursor: $cursor) {
id
chain
protocolVersion
timestamp
hash
account
token0 {
...TransactionToken
}
token0Quantity
token1 {
...TransactionToken
}
token1Quantity
usdValue {
id
value
}
type
}
}
......@@ -48,7 +48,7 @@ export const PERSONAL3_CONVERSION_URL = 'https://www.persona3.tech/events/attrib
const REDDIT_PIXEL_ID = 't2_tic7kuip'
export const REDDIT_CONVERSION_URL = `https://ads-api.reddit.com/api/v2.0/conversions/events/${REDDIT_PIXEL_ID}`
const GOOGLE_CUSTOMER_ID = '3416874723'
const GOOGLE_CUSTOMER_ID = '9871826344'
export const GOOGLE_CONVERSION_URL = `https://googleads.googleapis.com/v18/customers/${GOOGLE_CUSTOMER_ID}:uploadClickConversions`
export const GOOGLE_CONVERSION_DATETIME_FORMAT = 'yyyy-MM-dd HH:mm:ssXXX'
......@@ -56,24 +56,24 @@ export const GOOGLE_CONVERSION_EVENTS = {
Web: {
WalletConnected: {
platformIdType: PlatformIdType.Google,
eventId: `customers/${GOOGLE_CUSTOMER_ID}/conversionActions/7029146589`,
eventId: 'customers/987-182-6344/conversionActions/6886211413',
eventName: 'Wallet Connected - Web - CAPI',
},
WalletFunded: {
platformIdType: PlatformIdType.Google,
eventId: `customers/${GOOGLE_CUSTOMER_ID}/conversionActions/7029146586`,
eventId: 'customers/987-182-6344/conversionActions/6886211407',
eventName: 'Wallet Funded - Web - CAPI',
},
},
Extension: {
Download: {
platformIdType: PlatformIdType.Google,
eventId: `customers/${GOOGLE_CUSTOMER_ID}/conversionActions/7029146592`,
eventId: 'customers/987-182-6344/conversionActions/6886211416',
eventName: 'Download - Extension - CAPI',
},
WalletFunded: {
platformIdType: PlatformIdType.Google,
eventId: `customers/${GOOGLE_CUSTOMER_ID}/conversionActions/7029146595`,
eventId: 'customers/987-182-6344/conversionActions/6886211410',
eventName: 'Wallet Funded - Extension - CAPI',
},
},
......
......@@ -24,8 +24,5 @@ export const selectHasSeenUnichainPromotionNetworkSelectorTooltip = (state: Unis
export const selectHasSeenUnichainPromotionBridgingTooltip = (state: UniswapState): boolean =>
state.uniswapBehaviorHistory.unichainPromotion?.bridgingTooltipSeen === true
export const selectHasSeenUnichainPromotionBridgingAnimation = (state: UniswapState): boolean =>
state.uniswapBehaviorHistory.unichainPromotion?.bridgingAnimationSeen === true
export const selectIsFirstUnichainBridgeSelection = (state: UniswapState): boolean =>
state.uniswapBehaviorHistory.unichainPromotion?.isFirstUnichainBridgeSelection === true
......@@ -14,7 +14,6 @@ export interface UniswapBehaviorHistoryState {
networkSelectorAnimationSeen?: boolean
networkSelectorTooltipSeen?: boolean
bridgingTooltipSeen?: boolean
bridgingAnimationSeen?: boolean
isFirstUnichainBridgeSelection?: boolean
}
}
......@@ -29,7 +28,6 @@ export const initialUniswapBehaviorHistoryState: UniswapBehaviorHistoryState = {
networkSelectorAnimationSeen: false,
networkSelectorTooltipSeen: false,
bridgingTooltipSeen: false,
bridgingAnimationSeen: false,
isFirstUnichainBridgeSelection: true,
},
}
......@@ -71,10 +69,6 @@ const slice = createSlice({
state.unichainPromotion ??= {}
state.unichainPromotion.isFirstUnichainBridgeSelection = action.payload
},
setHasSeenBridgingAnimation: (state, action: PayloadAction<boolean>) => {
state.unichainPromotion ??= {}
state.unichainPromotion.bridgingAnimationSeen = action.payload
},
// Should only be used for testing
resetUniswapBehaviorHistory: (_state, _action: PayloadAction) => {
return initialUniswapBehaviorHistoryState
......@@ -92,7 +86,6 @@ export const {
setHasSeenNetworkSelectorTooltip,
setHasSeenBridgingTooltip,
setIsFirstUnichainBridgeSelection,
setHasSeenBridgingAnimation,
resetUniswapBehaviorHistory,
} = slice.actions
......
......@@ -3,8 +3,6 @@ import { useTradingApiSwappableTokensQuery } from 'uniswap/src/data/apiClients/t
import { ChainId } from 'uniswap/src/data/tradingApi/__generated__'
import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { toSupportedChainId } from 'uniswap/src/features/chains/utils'
import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
import {
NATIVE_ADDRESS_FOR_TRADING_API,
toTradingApiSupportedChainId,
......@@ -13,12 +11,10 @@ import {
const FALLBACK_NUM_CHAINS = 8
export function useNumBridgingChains(): number {
const unichainEnabled = useFeatureFlag(FeatureFlags.Unichain)
const { data: bridgingTokens } = useTradingApiSwappableTokensQuery({
params: {
tokenIn: NATIVE_ADDRESS_FOR_TRADING_API,
tokenInChainId: ChainId._1,
unichainEnabled,
},
})
......@@ -29,12 +25,10 @@ export function useNumBridgingChains(): number {
}
export function useIsBridgingChain(chainId: UniverseChainId): boolean {
const unichainEnabled = useFeatureFlag(FeatureFlags.Unichain)
const { data: bridgingTokens } = useTradingApiSwappableTokensQuery({
params: {
tokenIn: NATIVE_ADDRESS_FOR_TRADING_API,
tokenInChainId: ChainId._1,
unichainEnabled,
},
})
......@@ -45,12 +39,10 @@ export function useIsBridgingChain(chainId: UniverseChainId): boolean {
}
export function useBridgingSupportedChainIds(): UniverseChainId[] {
const unichainEnabled = useFeatureFlag(FeatureFlags.Unichain)
const { data: bridgingTokens } = useTradingApiSwappableTokensQuery({
params: {
tokenIn: NATIVE_ADDRESS_FOR_TRADING_API,
tokenInChainId: ChainId._1,
unichainEnabled,
},
})
......
......@@ -14,8 +14,6 @@ import { ALL_CHAIN_IDS, UniverseChainId } from 'uniswap/src/features/chains/type
import { toSupportedChainId } from 'uniswap/src/features/chains/utils'
import { CurrencyInfo, PortfolioBalance } from 'uniswap/src/features/dataApi/types'
import { currencyIdToContractInput } from 'uniswap/src/features/dataApi/utils'
import { FeatureFlags } from 'uniswap/src/features/gating/flags'
import { useFeatureFlag } from 'uniswap/src/features/gating/hooks'
import {
NATIVE_ADDRESS_FOR_TRADING_API,
getTokenAddressFromChainForTradingApi,
......@@ -56,14 +54,12 @@ export function useBridgingTokenWithHighestBalance({
fetchPolicy: 'cache-first',
})
const unichainEnabled = useFeatureFlag(FeatureFlags.Unichain)
const { data: bridgingTokens } = useTradingApiSwappableTokensQuery({
params:
otherChainBalances && otherChainBalances?.length > 0 && tokenIn && tokenInChainId
? {
tokenIn,
tokenInChainId,
unichainEnabled,
}
: undefined,
})
......@@ -122,7 +118,7 @@ export function useBridgingTokensOptions({
}): GqlResult<TokenOption[] | undefined> & { shouldNest?: boolean } {
const tokenIn = input?.address ? getTokenAddressFromChainForTradingApi(input.address, input.chainId) : undefined
const tokenInChainId = toTradingApiSupportedChainId(input?.chainId)
const unichainEnabled = useFeatureFlag(FeatureFlags.Unichain)
const {
data: bridgingTokens,
isLoading: loadingBridgingTokens,
......@@ -134,7 +130,6 @@ export function useBridgingTokensOptions({
? {
tokenIn,
tokenInChainId,
unichainEnabled,
}
: undefined,
})
......
......@@ -827,7 +827,7 @@ export const UNIVERSE_CHAIN_INFO: Record<UniverseChainId, UniverseChainInfo> = {
pendingTransactionsRetryOptions: undefined,
rpcUrls: {
[RPCType.Public]: { http: [getQuicknodeEndpointUrl(UniverseChainId.Unichain)] },
[RPCType.Default]: { http: ['https://mainnet.unichain.org'] },
[RPCType.Default]: { http: [getQuicknodeEndpointUrl(UniverseChainId.Unichain)] },
[RPCType.Interface]: { http: [getQuicknodeEndpointUrl(UniverseChainId.Unichain)] },
},
spotPriceStablecoinAmount: CurrencyAmount.fromRawAmount(USDC_UNICHAIN, 10_000e6),
......
......@@ -30,9 +30,11 @@ export enum FeatureFlags {
ExtensionClaimUnitag,
ExtensionPromotionGA,
FiatOffRamp,
NotificationPriceAlerts,
NotificationPriceAlertsAndroid,
NotificationPriceAlertsIOS,
NotificationOnboardingCard,
NotificationUnfundedWallets,
NotificationUnfundedWalletsAndroid,
NotificationUnfundedWalletsIOS,
OnboardingKeyring,
OpenAIAssistant,
PrivateRpc,
......@@ -125,8 +127,10 @@ export const WALLET_FEATURE_FLAG_NAMES = new Map<FeatureFlags, string>([
[FeatureFlags.FiatOffRamp, 'fiat-offramp'],
[FeatureFlags.ForAggregator, 'for-aggregator'],
[FeatureFlags.NotificationOnboardingCard, 'notification_onboarding_card'],
[FeatureFlags.NotificationPriceAlerts, 'notification_price_alerts'],
[FeatureFlags.NotificationUnfundedWallets, 'notification_unfunded_wallet'],
[FeatureFlags.NotificationPriceAlertsAndroid, 'notification_price_alerts_android'],
[FeatureFlags.NotificationPriceAlertsIOS, 'notification_price_alerts_ios'],
[FeatureFlags.NotificationUnfundedWalletsAndroid, 'notification_unfunded_wallet_android'],
[FeatureFlags.NotificationUnfundedWalletsIOS, 'notification_unfunded_wallet_ios'],
[FeatureFlags.OnboardingKeyring, 'onboarding-keyring'],
[FeatureFlags.OpenAIAssistant, 'openai-assistant'],
[FeatureFlags.PrivateRpc, 'mev-blocker'],
......
......@@ -7,7 +7,7 @@ import {
TokenDocument,
TokenQuery,
} from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks'
import { IndicativeQuoteResponse, TradeType } from 'uniswap/src/data/tradingApi/__generated__'
import { IndicativeQuoteRequest, IndicativeQuoteResponse, TradeType } from 'uniswap/src/data/tradingApi/__generated__'
import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { fromGraphQLChain } from 'uniswap/src/features/chains/utils'
import { currencyIdToContractInput, gqlTokenToCurrencyInfo } from 'uniswap/src/features/dataApi/utils'
......@@ -183,31 +183,14 @@ async function getDenominatedValue({
return undefined
}
const indicativeQuoteParams = {
const indicativeQuote = await fetchIndicativeQuote({
type: TradeType.EXACT_INPUT,
amount: onchainQuantityCurrencyAmount.quotient.toString(),
tokenInChainId: chainId,
tokenOutChainId: chainId,
tokenIn: tokenAddress,
tokenOut: stablecoinCurrency.address,
}
let indicativeQuote: IndicativeQuoteResponse | undefined
try {
indicativeQuote = await fetchTradingApiIndicativeQuoteIgnoring404({ params: indicativeQuoteParams })
} catch (error) {
// We log any other errors, but we don't want to throw and instead just continue with an "N/A" value.
logger.error(error, {
tags: {
file: 'fetchOnChainBalances.ts',
function: 'getDenominatedValue',
},
extra: {
indicativeQuoteParams,
},
})
}
const amountOut = indicativeQuote?.output.amount
......@@ -255,3 +238,21 @@ function getInferredCachedDenominatedValue({
return undefined
}
async function fetchIndicativeQuote(params: IndicativeQuoteRequest): Promise<IndicativeQuoteResponse | undefined> {
try {
return await fetchTradingApiIndicativeQuoteIgnoring404({ params })
} catch (error) {
// We log any other errors, but we don't want to throw and instead just continue with an "N/A" value.
logger.error(error, {
tags: {
file: 'fetchOnChainBalances.ts',
function: 'getIndicativeQuote',
},
extra: {
params,
},
})
return undefined
}
}
......@@ -358,6 +358,11 @@ export type LiquidityAnalyticsProperties = ITraceContext & {
currencyInfo1Decimals: number
}
export type NotificationToggleLoggingType =
| 'settings_general_updates_enabled'
| 'settings_price_alerts_enabled'
| 'wallet_activity'
// Please sort new values by EventName type!
export type UniverseEventProperties = {
[ExtensionEventName.OnboardingLoad]: undefined
......@@ -590,6 +595,7 @@ export type UniverseEventProperties = {
[MobileEventName.FiatOnRampQuickActionButtonPressed]: ITraceContext
[MobileEventName.NotificationsToggled]: ITraceContext & {
enabled: boolean
type: NotificationToggleLoggingType
}
[MobileEventName.OnboardingCompleted]: OnboardingCompletedProps & ITraceContext
[MobileEventName.PerformanceReport]: RenderPassReport
......
......@@ -159,7 +159,7 @@ export function TransactionDetails({
{showWarning && warning && onShowWarning && (
<TransactionWarning warning={warning} onShowWarning={onShowWarning} />
)}
{!isInterface && isSwap && (
{isSwap && (
<TransactionSettingsModal
settings={[SlippageUpdate]}
initialSelectedSetting={SlippageUpdate}
......
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { DEFAULT_CUSTOM_DEADLINE } from 'uniswap/src/constants/transactions'
import {
DEFAULT_PROTOCOL_OPTIONS,
FrontendSupportedProtocol,
......@@ -22,6 +23,7 @@ export interface TransactionSettingsState {
}
export const initialTransactionSettingsState: TransactionSettingsState = {
customDeadline: DEFAULT_CUSTOM_DEADLINE,
selectedProtocols: DEFAULT_PROTOCOL_OPTIONS,
isOnlyV2Allowed: false,
slippageWarningModalSeen: false,
......
......@@ -13,13 +13,13 @@ import { TransactionSettingsContextState } from 'uniswap/src/features/transactio
import { DerivedSwapInfo } from 'uniswap/src/features/transactions/swap/types/derivedSwapInfo'
import { Trade } from 'uniswap/src/features/transactions/swap/types/trade'
import { SwapEventType, timestampTracker } from 'uniswap/src/features/transactions/swap/utils/SwapEventTimestampTracker'
import { slippageToleranceToPercent } from 'uniswap/src/features/transactions/swap/utils/format'
import { getSwapFeeUsd } from 'uniswap/src/features/transactions/swap/utils/getSwapFeeUsd'
import { isClassic } from 'uniswap/src/features/transactions/swap/utils/routing'
import { getClassicQuoteFromResponse } from 'uniswap/src/features/transactions/swap/utils/tradingApi'
import { TransactionOriginType } from 'uniswap/src/features/transactions/types/transactionDetails'
import { CurrencyField } from 'uniswap/src/types/currency'
import { getCurrencyAddressForAnalytics } from 'uniswap/src/utils/currencyId'
import { percentFromFloat } from 'utilities/src/format/percent'
import { NumberType } from 'utilities/src/format/types'
import { logger } from 'utilities/src/logger/logger'
import { ITraceContext, useTrace } from 'utilities/src/telemetry/trace/TraceContext'
......@@ -91,7 +91,7 @@ export function getBaseTradeAnalyticsProperties({
const finalOutputAmount = feeCurrencyAmount ? trade.outputAmount.subtract(feeCurrencyAmount) : trade.outputAmount
const slippagePercent = slippageToleranceToPercent(trade.slippageTolerance ?? 0)
const slippagePercent = percentFromFloat(trade.slippageTolerance ?? 0)
return {
...trace,
......
......@@ -77,9 +77,6 @@ export function SwapFormSettings({
const topAlignment = adjustTopAlignment ? (isInterface ? -38 : 6) : 0
const rightAlignment = adjustRightAlignment ? (isMobileApp ? 24 : 4) : 0
const popoverOffset = isInterface
? { crossAxis: adjustRightAlignment ? 0 : 8, mainAxis: adjustTopAlignment ? 0 : 8 }
: undefined
const showCustomSlippage = customSlippageTolerance && !isBridgeTrade
......@@ -113,7 +110,6 @@ export function SwapFormSettings({
{!isViewOnlyWallet && (
<Popover
offset={popoverOffset}
placement="bottom-end"
open={showTransactionSettingsModal}
onOpenChange={(open: boolean) => {
......
......@@ -14,7 +14,7 @@ import { Warning, WarningLabel } from 'uniswap/src/components/modals/WarningModa
import { getCanonicalBridgingDappUrls } from 'uniswap/src/features/bridging/constants'
import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains'
import { UniverseChainId } from 'uniswap/src/features/chains/types'
import { getChainLabel } from 'uniswap/src/features/chains/utils'
import { getChainLabel, toSupportedChainId } from 'uniswap/src/features/chains/utils'
import {
useFormattedUniswapXGasFeeInfo,
useGasFeeFormattedDisplayAmounts,
......@@ -27,7 +27,7 @@ import { useSwapTxContext } from 'uniswap/src/features/transactions/swap/context
import { NetworkFeeWarning } from 'uniswap/src/features/transactions/swap/modals/NetworkFeeWarning'
import { SwapRateRatio } from 'uniswap/src/features/transactions/swap/review/SwapRateRatio'
import { IndicativeTrade, Trade } from 'uniswap/src/features/transactions/swap/types/trade'
import { isUniswapX } from 'uniswap/src/features/transactions/swap/utils/routing'
import { isBridge, isUniswapX } from 'uniswap/src/features/transactions/swap/utils/routing'
import { CurrencyField } from 'uniswap/src/types/currency'
import { useNetworkColors } from 'uniswap/src/utils/colors'
import { openUri } from 'uniswap/src/utils/linking'
......@@ -173,10 +173,6 @@ export function TradeInfoRow({
const warningColor = getAlertColor(warning?.severity)
const { isTestnetModeEnabled } = useEnabledChains()
const {
derivedSwapInfo: { currencies },
} = useSwapFormContext()
if (isTestnetModeEnabled) {
return null
}
......@@ -186,11 +182,12 @@ export function TradeInfoRow({
}
// On interface, if the warning is a no quotes found warning, we want to show an external link to a canonical bridge
const inputChainId = currencies.input?.currency.chainId
const outputChainId = currencies.output?.currency.chainId
const showCanonicalBridge =
isInterface && warning?.type === WarningLabel.NoQuotesFound && inputChainId !== outputChainId
isInterface &&
warning?.type === WarningLabel.NoQuotesFound &&
!debouncedTrade?.indicative &&
debouncedTrade &&
isBridge(debouncedTrade)
return (
<Flex centered row>
......@@ -212,7 +209,9 @@ export function TradeInfoRow({
</Flex>
{showCanonicalBridge ? (
<CanonicalBridgeLink chainId={outputChainId ?? UniverseChainId.Mainnet} />
<CanonicalBridgeLink
chainId={toSupportedChainId(debouncedTrade?.quote?.quote?.destinationChainId) ?? UniverseChainId.Mainnet}
/>
) : debouncedTrade ? (
<Accordion.Trigger
p="$none"
......
......@@ -266,10 +266,7 @@ export function useParsedSwapWarnings(): ParsedWarnings {
}
function getSwapWarningFromError(error: Error, t: TFunction, derivedSwapInfo: DerivedSwapInfo): Warning {
// Trade object is null for quote not found case
const isBridgeTrade =
derivedSwapInfo.currencies.input?.currency.chainId !== derivedSwapInfo.currencies.output?.currency.chainId
const isBridgeTrade = derivedSwapInfo.trade.trade !== null && isBridge(derivedSwapInfo.trade.trade)
if (error instanceof FetchError) {
// Special case: rate limit errors are not parsed by errorCode
if (isRateLimitFetchError(error)) {
......@@ -294,8 +291,8 @@ function getSwapWarningFromError(error: Error, t: TFunction, derivedSwapInfo: De
}
}
case Err404.errorCode.RESOURCE_NOT_FOUND: {
if (isBridgeTrade) {
// no bridging quotes found warning
case Err404.errorCode.RESOURCE_NOT_FOUND && isBridgeTrade: {
return {
type: WarningLabel.NoQuotesFound,
severity: WarningSeverity.Low,
......@@ -304,6 +301,8 @@ function getSwapWarningFromError(error: Error, t: TFunction, derivedSwapInfo: De
message: t('swap.warning.noQuotesFound.bridging.message'),
}
}
case Err404.errorCode.RESOURCE_NOT_FOUND: {
return {
type: WarningLabel.NoRoutesError,
severity: WarningSeverity.Low,
......
......@@ -77,7 +77,6 @@ export function SlippageControl({ saveOnBlur }: SlippageControlProps): JSX.Eleme
<Flex style={{ position: 'relative' }}>
<Input
ref={inputRef}
keyboardType="decimal-pad"
backgroundColor={backgroundColor}
color={inputValueTextColor}
editable={true}
......
import type { TFunction } from 'i18next'
import { useMemo, useState } from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
AdaptiveWebPopoverContent,
DeprecatedButton,
Flex,
isWeb,
Text,
TouchableArea,
useMedia,
useSporeColors,
} from 'ui/src'
import { DeprecatedButton, Flex, Popover, Text, TouchableArea, isWeb, useSporeColors } from 'ui/src'
import { RotatableChevron } from 'ui/src/components/icons/RotatableChevron'
import { iconSizes } from 'ui/src/theme'
import { Modal } from 'uniswap/src/components/modals/Modal'
import { WarningMessage } from 'uniswap/src/components/WarningMessage/WarningMessage'
import { Modal } from 'uniswap/src/components/modals/Modal'
import { SLIPPAGE_CRITICAL_TOLERANCE, WARNING_DEADLINE_TOLERANCE } from 'uniswap/src/constants/transactions'
import { ModalName } from 'uniswap/src/features/telemetry/constants'
import {
......@@ -22,10 +12,10 @@ import {
useTransactionSettingsContext,
} from 'uniswap/src/features/transactions/settings/contexts/TransactionSettingsContext'
import { SwapFormContext, useSwapFormContext } from 'uniswap/src/features/transactions/swap/contexts/SwapFormContext'
import { SwapSettingConfig, SwapSettingId } from 'uniswap/src/features/transactions/swap/settings/configs/types'
import { SwapSettingRow } from 'uniswap/src/features/transactions/swap/settings/SwapSettingsRow'
import { SwapSettingConfig, SwapSettingId } from 'uniswap/src/features/transactions/swap/settings/configs/types'
import { useSlippageSettings } from 'uniswap/src/features/transactions/swap/settings/useSlippageSettings'
import { isExtension, isInterface, isMobileApp, isMobileWeb } from 'utilities/src/platform'
import { isExtension, isInterface, isMobileWeb } from 'utilities/src/platform'
const POPOVER_WIDTH = 320
......@@ -44,29 +34,58 @@ const TransactionSettingsModalContent = ({
onClose,
}: Omit<TransactionSettingsModalProps, 'isOpen'>): JSX.Element => {
const { t } = useTranslation()
const media = useMedia()
const { customSlippageTolerance, customDeadline } = useTransactionSettingsContext()
const { autoSlippageTolerance } = useSlippageSettings()
const isCriticalSlippage = customSlippageTolerance && customSlippageTolerance >= SLIPPAGE_CRITICAL_TOLERANCE
const [SelectedSetting, setSelectedSetting] = useState<SwapSettingConfig>()
const rowWarningContent: Record<SwapSettingId, { condition: boolean; render: () => JSX.Element | undefined }> = {
[SwapSettingId.SLIPPAGE]: {
condition: !!customSlippageTolerance && customSlippageTolerance > autoSlippageTolerance,
render: () => (
<WarningMessage
warningMessage={isCriticalSlippage ? t('swap.settings.slippage.warning') : t('swap.settings.slippage.alert')}
tooltipText={isWeb && !isMobileWeb ? t('swap.settings.slippage.warning.hover') : undefined}
color={isCriticalSlippage ? '$statusCritical' : '$statusWarning'}
/>
),
},
[SwapSettingId.DEADLINE]: {
condition: !!customDeadline && customDeadline >= WARNING_DEADLINE_TOLERANCE,
render: () => <WarningMessage warningMessage={t('swap.settings.deadline.warning')} color="$statusWarning" />,
},
}
const [SelectedSetting, setSelectedSetting] = useState<SwapSettingConfig | undefined>(initialSelectedSetting)
const getSettingsRowWarning = (settingId: SwapSettingId): JSX.Element | undefined => {
const warning = rowWarningContent[settingId]
return warning?.condition ? warning.render() : undefined
}
const title = SelectedSetting ? SelectedSetting.renderTitle(t) : defaultTitle ?? t('swap.settings.title')
const screen = SelectedSetting?.Screen ? (
<SelectedSetting.Screen />
) : (
<TopLevelSettings settings={settings} setSelectedSetting={setSelectedSetting} />
<Flex gap="$spacing8" py="$spacing12">
{settings.map((setting, index) => {
const warning = setting.settingId ? getSettingsRowWarning(setting.settingId) : undefined
return (
<SwapSettingRow
key={`swap-setting-${index}`}
setSelectedSetting={setSelectedSetting}
setting={setting}
warning={warning}
/>
)
})}
</Flex>
)
// For selected settings, show title on all platforms unless it is explicitly hidden via hideTitle.
// For top level settings (not selected), show title on mobile + small screen web only.
const isWebSmallScreen = media.sm && isInterface
const shouldShowTitle = SelectedSetting ? !SelectedSetting.hideTitle : isMobileApp || isWebSmallScreen
// Hide close button on desktop web and extension unless there is custom button text
const shouldShowCloseButton = isMobileApp || isWebSmallScreen || Boolean(SelectedSetting?.renderCloseButtonText)
return (
<Flex gap="$spacing16" px={isWeb ? '$spacing4' : '$spacing24'} py={isWeb ? '$spacing4' : '$spacing12'} width="100%">
{shouldShowTitle && (
<Flex row justifyContent="space-between" pt={isWeb ? '$spacing8' : 0}>
{!SelectedSetting?.hideTitle && (
<Flex row justifyContent="space-between">
<TouchableArea onPress={(): void => setSelectedSetting(undefined)}>
<RotatableChevron
color={
......@@ -85,103 +104,23 @@ const TransactionSettingsModalContent = ({
</Flex>
)}
{screen}
{shouldShowCloseButton && (
<Flex centered row pb={isWebSmallScreen ? '$spacing24' : '$spacing8'}>
<Flex centered row>
<DeprecatedButton fill testID="swap-settings-close" theme="secondary" onPress={onClose}>
{SelectedSetting?.renderCloseButtonText
? SelectedSetting.renderCloseButtonText(t)
: t('common.button.save')}
{SelectedSetting?.renderCloseButtonText ? SelectedSetting.renderCloseButtonText(t) : t('common.button.close')}
</DeprecatedButton>
</Flex>
)}
</Flex>
)
}
const TopLevelSettings = ({
settings,
setSelectedSetting,
}: {
settings: SwapSettingConfig[]
setSelectedSetting: React.Dispatch<React.SetStateAction<SwapSettingConfig | undefined>>
}): JSX.Element => {
const { t } = useTranslation()
const { customSlippageTolerance, customDeadline } = useTransactionSettingsContext()
const { autoSlippageTolerance } = useSlippageSettings()
const rowWarningContent = useMemo(
() =>
createRowWarningContent({
t,
customSlippageTolerance,
autoSlippageTolerance,
customDeadline,
}),
[t, customSlippageTolerance, autoSlippageTolerance, customDeadline],
)
const getSettingsRowWarning = (settingId: SwapSettingId): JSX.Element | undefined => {
const warning = rowWarningContent[settingId]
return warning?.condition ? warning.render() : undefined
}
return (
<Flex gap={isWeb ? '$spacing4' : '$spacing8'} py={isWeb ? '$spacing8' : '$spacing12'}>
{settings.map((setting, index) => {
const warning = setting.settingId ? getSettingsRowWarning(setting.settingId) : undefined
return (
<SwapSettingRow
key={`swap-setting-${index}`}
setSelectedSetting={setSelectedSetting}
setting={setting}
warning={warning}
/>
)
})}
</Flex>
)
}
function createRowWarningContent({
t,
autoSlippageTolerance,
customSlippageTolerance,
customDeadline,
}: {
t: TFunction
autoSlippageTolerance: number
customSlippageTolerance?: number
customDeadline?: number
}): Record<SwapSettingId, { condition: boolean; render: () => JSX.Element | undefined }> {
const isCriticalSlippage = Boolean(customSlippageTolerance && customSlippageTolerance >= SLIPPAGE_CRITICAL_TOLERANCE)
return {
[SwapSettingId.SLIPPAGE]: {
condition: !!customSlippageTolerance && customSlippageTolerance > autoSlippageTolerance,
render: () => (
<WarningMessage
warningMessage={isCriticalSlippage ? t('swap.settings.slippage.warning') : t('swap.settings.slippage.alert')}
tooltipText={isWeb && !isMobileWeb ? t('swap.settings.slippage.warning.hover') : undefined}
color={isCriticalSlippage ? '$statusCritical' : '$statusWarning'}
/>
),
},
[SwapSettingId.DEADLINE]: {
condition: !!customDeadline && customDeadline >= WARNING_DEADLINE_TOLERANCE,
render: () => <WarningMessage warningMessage={t('swap.settings.deadline.warning')} color="$statusWarning" />,
},
}
}
function TransactionSettingsModalInterface({
settings,
defaultTitle,
initialSelectedSetting,
onClose,
isOpen,
}: TransactionSettingsModalProps): JSX.Element {
return (
<AdaptiveWebPopoverContent
<Popover.Content
animation={[
'quick',
{
......@@ -195,14 +134,11 @@ function TransactionSettingsModalInterface({
borderWidth="$spacing1"
enterStyle={{ y: -10, opacity: 0 }}
exitStyle={{ y: -10, opacity: 0 }}
px="$spacing12"
py="$spacing4"
p="$spacing12"
shadowColor="$shadowColor"
shadowOpacity={0.06}
shadowRadius={6}
width={POPOVER_WIDTH}
isOpen={isOpen}
webBottomSheetProps={{ px: '$padding16' }}
>
<TransactionSettingsModalContent
defaultTitle={defaultTitle}
......@@ -210,7 +146,7 @@ function TransactionSettingsModalInterface({
settings={settings}
onClose={onClose}
/>
</AdaptiveWebPopoverContent>
</Popover.Content>
)
}
......
......@@ -27,7 +27,6 @@ function isDefaultOptions(selectedProtocols: FrontendSupportedProtocol[]): boole
export const ProtocolPreference: SwapSettingConfig = {
renderTitle: (t) => t('swap.settings.routingPreference.title'),
renderCloseButtonText: (t) => t('common.button.save'),
Control() {
const { t } = useTranslation()
const { selectedProtocols } = useTransactionSettingsContext()
......
import { useSelector } from 'react-redux'
import {
selectHasSeenUnichainPromotionBridgingAnimation,
selectHasSeenUnichainPromotionBridgingTooltip,
selectHasSeenUnichainPromotionNetworkSelectorTooltip,
} from 'uniswap/src/features/behaviorHistory/selectors'
......@@ -11,7 +10,6 @@ import { selectIsTestnetModeEnabled } from 'uniswap/src/features/settings/select
export function useUnichainTooltipVisibility(): {
shouldShowUnichainNetworkSelectorTooltip: boolean
shouldShowUnichainBridgingTooltip: boolean
shouldShowUnichainBridgingAnimation: boolean
} {
const unichainEnabled = useFeatureFlag(FeatureFlags.Unichain)
const unichainPromoEnabled = useFeatureFlag(FeatureFlags.UnichainPromo)
......@@ -19,7 +17,6 @@ export function useUnichainTooltipVisibility(): {
selectHasSeenUnichainPromotionNetworkSelectorTooltip,
)
const hasSeenUnichainPromotionBridgingTooltip = useSelector(selectHasSeenUnichainPromotionBridgingTooltip)
const hasSeenUnichainPromotionBridgingAnimation = useSelector(selectHasSeenUnichainPromotionBridgingAnimation)
const isTestnetModeEnabled = useSelector(selectIsTestnetModeEnabled)
// Don't show promotion if:
......@@ -30,13 +27,11 @@ export function useUnichainTooltipVisibility(): {
return {
shouldShowUnichainNetworkSelectorTooltip: false,
shouldShowUnichainBridgingTooltip: false,
shouldShowUnichainBridgingAnimation: false,
}
}
return {
shouldShowUnichainNetworkSelectorTooltip: !hasSeenUnichainPromotionNetworkSelectorTooltip,
shouldShowUnichainBridgingTooltip: !hasSeenUnichainPromotionBridgingTooltip,
shouldShowUnichainBridgingAnimation: !hasSeenUnichainPromotionBridgingAnimation,
}
}
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