From d21815f6a1a18a4f9ea888f8451395fe9a36952f Mon Sep 17 00:00:00 2001 From: Uniswap Labs Service Account Date: Mon, 15 Jul 2024 17:22:46 +0000 Subject: [PATCH] ci(release): publish latest release --- .depcheckrc | 30 +- .prettierignore | 48 + .prettierrc | 9 + .../detox-npm-20.18.1-b532b310b4.patch | 14 - .../detox-npm-20.23.0-6d61110e63.patch | Bin 0 -> 2202 bytes CODEOWNERS | 1 + RELEASE | 71 +- VERSION | 2 +- apps/mobile/.prettierignore | 3 - apps/mobile/README.md | 9 + .../__mocks__/@react-navigation/native.js | 30 + .../__mocks__/@shopify/react-native-skia.ts | 5 +- .../react-native-context-menu-view.ts | 5 +- .../__mocks__/react-native-fast-image.ts | 5 +- apps/mobile/android/app/build.gradle | 6 +- .../onboarding/import/SeedPhraseInput.kt | 30 +- .../import/SeedPhraseInputViewModel.kt | 30 +- apps/mobile/android/settings.gradle | 2 +- apps/mobile/e2e/Home.e2e.ts | 11 + apps/mobile/e2e/Onboarding.e2e.ts | 1 + apps/mobile/e2e/README.md | 12 +- apps/mobile/e2e/usecases/CreateNewWallet.ts | 49 - .../usecases/home/HomeBasicInteractions.ts | 75 + .../usecases/onboarding/CreateNewWallet.ts | 32 +- .../e2e/usecases/onboarding/ImportWallet.ts | 29 +- .../e2e/usecases/onboarding/WatchWallet.ts | 16 +- .../usecases/swap/SwapBasicInteractions.ts | 64 +- apps/mobile/e2e/utils/fixtures.ts | 3 +- .../ios/Uniswap.xcodeproj/project.pbxproj | 32 +- .../Import/SeedPhraseInputManager.m | 1 + .../Import/SeedPhraseInputView.swift | 15 +- .../Import/SeedPhraseInputViewModel.swift | 29 +- .../ios/Uniswap/RNEthersRs/RNEthersRS.swift | 7 +- apps/mobile/jest-setup.js | 10 +- apps/mobile/package.json | 7 +- apps/mobile/scripts/podinstall.sh | 26 +- apps/mobile/src/app/App.tsx | 40 +- .../app/MobileWalletNavigationProvider.tsx | 52 +- apps/mobile/src/app/hooks.ts | 16 +- apps/mobile/src/app/migrations.test.ts | 118 +- apps/mobile/src/app/migrations.ts | 82 +- .../src/app/modals/AccountSwitcherModal.tsx | 56 +- apps/mobile/src/app/modals/AppModals.tsx | 14 +- .../src/app/modals/ExperimentsModal.tsx | 22 +- apps/mobile/src/app/modals/ExploreModal.tsx | 10 +- .../src/app/modals/ExtensionPromoModal.tsx | 4 +- apps/mobile/src/app/modals/SwapModal.tsx | 11 +- .../src/app/modals/TransferTokenModal.tsx | 8 +- .../src/app/modals/ViewOnlyExplainerModal.tsx | 14 +- .../AccountSwitcherModal.test.tsx.snap | 8 +- apps/mobile/src/app/modals/utils.test.tsx | 4 +- apps/mobile/src/app/navigation/NavBar.tsx | 56 +- .../app/navigation/NavigationContainer.tsx | 19 +- apps/mobile/src/app/navigation/components.tsx | 9 + apps/mobile/src/app/navigation/hooks.ts | 10 +- apps/mobile/src/app/navigation/navigation.tsx | 136 +- .../src/app/navigation/rootNavigation.ts | 13 +- apps/mobile/src/app/navigation/types.ts | 30 +- apps/mobile/src/app/saga.ts | 13 +- apps/mobile/src/app/store.ts | 16 +- apps/mobile/src/components/NFT/NftView.tsx | 17 +- .../PriceExplorer/AnimatedDecimalNumber.tsx | 23 +- .../PriceExplorer/PriceExplorer.tsx | 26 +- .../PriceExplorerAnimatedNumber.tsx | 70 +- .../PriceExplorer/PriceExplorerError.tsx | 3 +- .../src/components/PriceExplorer/Text.tsx | 21 +- .../PriceExplorer/TimeRangeGroup.tsx | 34 +- .../src/components/PriceExplorer/constants.ts | 24 +- .../PriceExplorer/useChartDimensions.test.ts | 8 +- .../components/PriceExplorer/usePrice.test.ts | 25 +- .../src/components/PriceExplorer/usePrice.tsx | 26 +- .../PriceExplorer/usePriceHistory.test.ts | 48 +- .../PriceExplorer/usePriceHistory.ts | 12 +- .../QRCodeScanner/QRCodeScanner.tsx | 83 +- .../RecipientSelect/RecipientScanModal.tsx | 33 +- .../RecipientSelect/RecipientSelect.tsx | 50 +- .../components/RecipientSelect/hooks.test.ts | 100 +- .../RemoveWallet/AssociatedAccountsList.tsx | 6 +- .../RemoveLastMnemonicWalletFooter.tsx | 14 +- .../RemoveWallet/RemoveWalletModal.tsx | 60 +- .../RemoveWallet/useModalContent.tsx | 20 +- .../src/components/RemoveWallet/utils.ts | 2 +- .../ConnectedDapps/ConnectedDappsList.tsx | 41 +- .../DappConnectedNetworksModal.tsx | 21 +- .../ConnectedDapps/DappConnectionItem.tsx | 31 +- .../DappHeaderIcon.tsx | 0 .../ModalWithOverlay/ModalWithOverlay.tsx | 42 +- .../ModalWithOverlay/ScrollDownOverlay.tsx | 8 +- .../RequestModal/ClientDetails.tsx | 10 +- .../RequestModal/HeaderText.tsx | 5 +- .../RequestModal/KidSuperCheckinModal.tsx | 23 +- .../RequestModal/RequestDetails.tsx | 42 +- .../RequestModal/UwULinkErc20SendModal.tsx | 21 +- .../WalletConnectRequestModal.tsx | 78 +- .../WalletConnectRequestModalContent.tsx | 35 +- .../RequestModal/hooks.ts | 7 +- .../ScanSheet/PendingConnectionModal.tsx | 73 +- .../PendingConnectionSwitchAccountModal.tsx | 10 +- .../PendingConnectionSwitchNetworkModal.tsx | 21 +- .../ScanSheet/SwitchAccountOption.tsx | 2 +- .../ScanSheet/WalletConnectModal.tsx | 119 +- .../ScanSheet/util.test.ts | 6 +- .../ScanSheet/util.ts | 54 +- .../WalletConnectModals.tsx | 47 +- .../RestoreWalletModal/RestoreWalletModal.tsx | 23 +- .../Settings/BiometricAuthWarningModal.tsx | 11 +- .../src/components/Settings/SettingsRow.tsx | 27 +- .../TokenBalanceItemContextMenu.tsx | 8 +- .../TokenBalanceList/TokenBalanceList.tsx | 328 +- .../components/TokenDetails/LinkButton.tsx | 28 +- .../components/TokenDetails/SendButton.tsx | 9 +- .../components/TokenDetails/TokenBalances.tsx | 12 +- .../TokenDetailsActionButtons.tsx | 8 +- .../TokenDetailsFavoriteButton.tsx | 5 +- .../TokenDetails/TokenDetailsHeader.tsx | 17 +- .../TokenDetails/TokenDetailsLinks.tsx | 8 +- .../TokenDetails/TokenDetailsStats.tsx | 42 +- .../src/components/TokenDetails/hooks.test.ts | 30 +- .../src/components/TokenDetails/hooks.ts | 25 +- .../TokenSelector/TokenFiatOnRampList.tsx | 29 +- .../Trace/TraceUserProperties.test.tsx | 52 +- .../components/Trace/TraceUserProperties.tsx | 21 +- .../accounts/AccountCardItem.test.tsx | 14 +- .../components/accounts/AccountCardItem.tsx | 22 +- .../accounts/AccountHeader.test.tsx | 11 +- .../src/components/accounts/AccountHeader.tsx | 61 +- .../components/accounts/AccountList.test.tsx | 18 +- .../src/components/accounts/AccountList.tsx | 15 +- .../__snapshots__/AccountHeader.test.tsx.snap | 3 +- .../src/components/banners/BottomBanner.tsx | 10 +- .../banners/ExtensionPromoBanner.tsx | 36 +- .../src/components/banners/OfflineBanner.tsx | 8 +- .../src/components/buttons/BackButton.tsx | 11 +- .../buttons/CopyTextButton.test.tsx | 4 +- .../src/components/buttons/CopyTextButton.tsx | 6 +- .../components/buttons/LinkButton.test.tsx | 8 +- .../src/components/buttons/LinkButton.tsx | 6 +- apps/mobile/src/components/buttons/utils.ts | 7 +- .../src/components/carousel/Indicator.tsx | 23 +- .../src/components/education/SeedPhrase.tsx | 44 +- .../components/explore/ExploreSections.tsx | 54 +- .../explore/FavoriteHeaderRow.test.tsx | 13 +- .../components/explore/FavoriteHeaderRow.tsx | 24 +- .../explore/FavoriteTokenCard.test.tsx | 8 +- .../components/explore/FavoriteTokenCard.tsx | 21 +- .../components/explore/FavoriteTokensGrid.tsx | 17 +- .../explore/FavoriteWalletCard.test.tsx | 12 +- .../components/explore/FavoriteWalletCard.tsx | 10 +- .../explore/FavoriteWalletsGrid.tsx | 13 +- .../src/components/explore/RemoveButton.tsx | 4 +- .../components/explore/SortButton.test.tsx | 8 +- .../src/components/explore/SortButton.tsx | 22 +- .../src/components/explore/TokenItem.test.tsx | 14 +- .../src/components/explore/TokenItem.tsx | 30 +- .../FavoriteHeaderRow.test.tsx.snap | 4 +- .../FavoriteWalletCard.test.tsx.snap | 21 +- .../__snapshots__/TokenItem.test.tsx.snap | 1 + .../src/components/explore/hooks.test.ts | 56 +- apps/mobile/src/components/explore/hooks.ts | 38 +- .../explore/search/SearchEmptySection.tsx | 29 +- .../search/SearchPopularNFTCollections.tsx | 37 +- .../explore/search/SearchPopularTokens.tsx | 18 +- .../explore/search/SearchResultsLoader.tsx | 6 +- .../explore/search/SearchResultsSection.tsx | 34 +- .../explore/search/SearchSectionHeader.tsx | 8 +- .../src/components/explore/search/hooks.ts | 25 +- .../search/items/SearchENSAddressItem.tsx | 19 +- .../search/items/SearchEtherscanItem.tsx | 26 +- .../search/items/SearchNFTCollectionItem.tsx | 38 +- .../explore/search/items/SearchTokenItem.tsx | 41 +- .../explore/search/items/SearchUnitagItem.tsx | 17 +- .../items/SearchWalletByAddressItem.tsx | 12 +- .../search/items/SearchWalletItemBase.tsx | 25 +- .../components/explore/search/utils.test.ts | 18 +- .../src/components/explore/search/utils.ts | 81 +- .../src/components/fiatOnRamp/CtaButton.tsx | 3 +- .../forceUpgrade/ForceUpgradeModal.tsx | 31 +- .../gradients/GradientBackground.tsx | 9 +- .../src/components/home/ActivityTab.tsx | 46 +- apps/mobile/src/components/home/FeedTab.tsx | 25 +- apps/mobile/src/components/home/NftsTab.tsx | 19 +- apps/mobile/src/components/home/TokensTab.tsx | 177 +- apps/mobile/src/components/home/hooks.tsx | 2 +- .../src/components/icons/BiometricsIcon.tsx | 3 +- .../components/icons/BlockExplorerIcon.tsx | 6 +- apps/mobile/src/components/icons/Favorite.tsx | 9 +- .../src/components/input/PasswordInput.tsx | 20 +- .../src/components/input/SelectionCircle.tsx | 3 +- .../components/layout/AnimatedFlatList.tsx | 38 +- .../src/components/layout/BackHeader.tsx | 3 +- apps/mobile/src/components/layout/Delayed.tsx | 5 +- .../components/layout/SafeKeyboardScreen.tsx | 75 + apps/mobile/src/components/layout/Screen.tsx | 8 +- .../src/components/layout/TabHelpers.tsx | 50 +- .../src/components/layout/VirtualizedList.tsx | 46 +- .../layout/screens/HeaderScrollScreen.tsx | 8 +- .../layout/screens/ScrollHeader.tsx | 25 +- .../layout/screens/WithScrollToTop.tsx | 21 +- .../components/loading/TransactionLoader.tsx | 23 +- .../src/components/loading/WaveLoader.tsx | 8 +- apps/mobile/src/components/loading/index.tsx | 6 +- .../mnemonic/HiddenMnemonicWordView.tsx | 16 +- .../mnemonic/MnemonicConfirmation.tsx | 3 +- .../components/mnemonic/MnemonicDisplay.tsx | 3 +- .../components/mnemonic/SeedPhraseDisplay.tsx | 26 +- apps/mobile/src/components/modals/Modal.tsx | 9 +- .../sortableGrid/SortableGirdProvider.tsx | 19 +- .../components/sortableGrid/SortableGrid.tsx | 16 +- .../sortableGrid/SortableGridItem.tsx | 42 +- .../contexts/AutoScrollContextProvider.tsx | 37 +- .../contexts/DragContextProvider.tsx | 96 +- .../contexts/LayoutContextProvider.tsx | 20 +- .../src/components/sortableGrid/hooks.ts | 39 +- .../src/components/sortableGrid/utils.ts | 23 +- .../src/components/text/AnimatedText.tsx | 14 +- .../components/text/DecimalNumber.test.tsx | 8 +- .../src/components/text/DecimalNumber.tsx | 9 +- .../components/text/LongMarkdownText.test.tsx | 10 +- .../src/components/text/LongMarkdownText.tsx | 10 +- apps/mobile/src/components/text/LongText.tsx | 13 +- .../src/components/text/Pill.stories.mdx | 3 +- .../text/TextWithFuseMatches.test.tsx | 2 +- .../src/components/tokens/TokenMetadata.tsx | 5 +- .../src/components/tooltip/TooltipButton.tsx | 6 +- .../components/unitags/ChangeUnitagModal.tsx | 57 +- .../src/components/unitags/ChooseNftModal.tsx | 3 +- .../unitags/ChoosePhotoOptionsModal.tsx | 17 +- .../components/unitags/DeleteUnitagModal.tsx | 21 +- .../src/components/unitags/UnitagBanner.tsx | 37 +- .../unitags/UnitagProfilePicture.tsx | 18 +- .../unitags/UnitagWithProfilePicture.tsx | 9 +- .../components/unitags/UnitagsIntroModal.tsx | 5 +- .../CloudBackupPasswordFormContext.tsx | 120 + .../CloudBackupForm/ContinueButton.tsx | 16 + .../CloudBackupForm/PasswordInput.tsx | 93 + .../CloudBackup/CloudBackupForm/index.ts | 9 + .../CloudBackup/CloudBackupPasswordForm.tsx | 156 - .../CloudBackupProcessingAnimation.tsx | 26 +- .../RNCloudStorageBackupsManager.ts | 10 +- .../CloudBackup/passwordLockoutSlice.ts | 8 +- apps/mobile/src/features/CloudBackup/saga.ts | 7 +- .../src/features/CloudBackup/testingUtils.ts | 4 +- .../src/features/analytics/appsflyer.tsx | 4 +- .../src/features/appLoading/SplashScreen.tsx | 3 +- apps/mobile/src/features/appRating/saga.ts | 72 +- .../src/features/appRating/selectors.test.ts | 6 +- .../src/features/appRating/selectors.ts | 12 +- .../authentication/LockScreenModal.tsx | 3 +- .../authentication/lockScreenContext.tsx | 3 +- .../src/features/biometrics/context.tsx | 9 +- apps/mobile/src/features/biometrics/hooks.tsx | 14 +- .../features/biometrics/useBiometricCheck.ts | 10 +- .../src/features/contracts/useContract.ts | 2 +- .../src/features/dataApi/balances.test.ts | 8 +- apps/mobile/src/features/dataApi/balances.ts | 4 +- .../deepLinking/handleDeepLinkSaga.ts | 51 +- ....ts => handleOnRampReturnLinkSaga.test.ts} | 8 +- ...kSaga.ts => handleOnRampReturnLinkSaga.ts} | 2 +- .../deepLinking/handleSwapLinkSaga.test.ts | 33 +- .../deepLinking/handleSwapLinkSaga.ts | 29 +- apps/mobile/src/features/explore/utils.ts | 16 +- .../externalProfile/ProfileContextMenu.tsx | 29 +- .../externalProfile/ProfileHeader.tsx | 64 +- apps/mobile/src/features/favorites/hooks.ts | 14 +- .../fiatOnRamp/ExchangeTransferModal.tsx | 8 +- ...xchangeTransferServiceProviderSelector.tsx | 20 +- .../fiatOnRamp/FiatOnRampAggregatorModal.tsx | 10 +- .../fiatOnRamp/FiatOnRampAmountSection.tsx | 23 +- .../features/fiatOnRamp/FiatOnRampContext.tsx | 22 +- .../fiatOnRamp/FiatOnRampCountryListModal.tsx | 35 +- .../features/fiatOnRamp/FiatOnRampModal.tsx | 319 -- .../fiatOnRamp/FiatOnRampTokenSelector.tsx | 10 +- .../features/fiatOnRamp/aggregatorHooks.ts | 151 - .../src/features/fiatOnRamp/constants.ts | 12 - apps/mobile/src/features/fiatOnRamp/hooks.ts | 464 +-- .../src/features/firebase/firebaseDataSaga.ts | 41 +- apps/mobile/src/features/firebase/utils.ts | 6 +- .../import/GenericImportForm.test.tsx | 6 +- .../src/features/import/GenericImportForm.tsx | 11 +- .../import/InputWithSuffix.android.tsx | 6 +- .../features/import/InputWithSuffix.ios.tsx | 4 +- .../mobile/src/features/modals/ModalsState.ts | 4 +- apps/mobile/src/features/modals/hooks.ts | 27 - .../src/features/modals/modalSlice.test.ts | 7 +- apps/mobile/src/features/modals/modalSlice.ts | 13 +- apps/mobile/src/features/modals/saga.ts | 7 +- .../src/features/modals/selectModalState.ts | 4 +- .../collection/NFTCollectionContextMenu.tsx | 9 +- .../nfts/collection/NFTCollectionHeader.tsx | 28 +- .../nfts/item/BlurredImageBackground.tsx | 3 +- .../nfts/item/CollectionPreviewCard.test.tsx | 4 +- .../nfts/item/CollectionPreviewCard.tsx | 30 +- .../CollectionPreviewCard.test.tsx.snap | 8 +- apps/mobile/src/features/nfts/item/traits.tsx | 12 +- .../NotificationToastWrapper.tsx | 6 +- .../src/features/notifications/Onesignal.ts | 19 +- .../ScantasticCompleteNotification.tsx | 3 +- .../features/notifications/WCNotification.tsx | 14 +- .../hooks/useNavigateToProfileTab.ts | 2 +- .../useNotificationOSPermissionsEnabled.ts | 8 +- .../features/onboarding/OnboardingScreen.tsx | 44 +- .../src/features/onboarding/OptionCard.tsx | 17 +- .../SafeKeyboardOnboardingScreen.tsx | 136 +- apps/mobile/src/features/onboarding/hooks.ts | 8 +- .../features/scantastic/ScantasticModal.tsx | 55 +- .../features/telemetry/directLogScreens.ts | 5 +- apps/mobile/src/features/telemetry/saga.ts | 12 +- apps/mobile/src/features/telemetry/utils.ts | 4 +- .../FiatPurchaseSummaryItem.stories.tsx | 7 +- .../NFTApproveSummaryItem.stories.tsx | 1 + .../NFTMintSummaryItem.stories.tsx | 1 + .../NFTTradeSummaryItem.stories.tsx | 3 +- .../ReceiveSummaryItem.stories.tsx | 3 +- .../SummaryItems/SendSummaryItem.stories.tsx | 3 +- .../SummaryItems/SwapSummaryItem.stories.tsx | 7 +- .../SummaryItems/WrapSummaryItem.stories.tsx | 15 +- .../TransactionPending/TransactionPending.tsx | 15 +- .../swap/hooks/useOnCloseSendModal.tsx | 4 +- .../transactions/transfer/TransferFlow.tsx | 109 +- .../transactions/transfer/TransferHeader.tsx | 17 +- .../transactions/transfer/TransferStatus.tsx | 33 +- .../transfer/transferRewrite/TransferFlow.tsx | 12 +- .../transferRewrite/TransferScreenContext.tsx | 10 +- apps/mobile/src/features/tweaks/slice.ts | 5 +- .../unitags/ChooseProfilePictureScreen.tsx | 45 +- .../features/unitags/ClaimUnitagScreen.tsx | 122 +- .../features/unitags/ConfirmationElements.tsx | 177 - .../unitags/EditUnitagProfileScreen.tsx | 94 +- .../unitags/UnitagConfirmationScreen.tsx | 31 +- .../src/features/unitags/UnitagName.tsx | 9 +- apps/mobile/src/features/wallet/hooks.ts | 6 +- apps/mobile/src/features/wallet/saga.ts | 2 +- apps/mobile/src/features/walletConnect/api.ts | 4 +- .../mobile/src/features/walletConnect/saga.ts | 39 +- .../src/features/walletConnect/selectors.ts | 8 +- .../walletConnect/signWcRequestSaga.ts | 35 +- .../src/features/walletConnect/utils.test.ts | 23 +- .../src/features/walletConnect/utils.ts | 14 +- .../walletConnect/walletConnectSlice.ts | 41 +- apps/mobile/src/features/widgets/widgets.ts | 9 +- apps/mobile/src/index.ts | 2 +- apps/mobile/src/lib/RNEthersRs.ts | 22 +- apps/mobile/src/screens/AppLoadingScreen.tsx | 62 +- apps/mobile/src/screens/DevScreen.tsx | 20 +- apps/mobile/src/screens/EducationScreen.tsx | 2 +- .../screens/ExchangeTransferConnecting.tsx | 27 +- apps/mobile/src/screens/ExploreScreen.tsx | 10 +- .../src/screens/ExternalProfileScreen.tsx | 27 +- .../src/screens/FiatOnRampConnecting.tsx | 61 +- apps/mobile/src/screens/FiatOnRampScreen.tsx | 130 +- .../screens/FiatOnRampServiceProviders.tsx | 31 +- apps/mobile/src/screens/HomeScreen.tsx | 156 +- .../src/screens/Import/ImportMethodScreen.tsx | 27 +- .../screens/Import/OnDeviceRecoveryScreen.tsx | 50 +- .../OnDeviceRecoveryViewSeedPhraseScreen.tsx | 5 +- .../Import/OnDeviceRecoveryWalletCard.tsx | 21 +- .../RestoreCloudBackupLoadingScreen.tsx | 20 +- .../RestoreCloudBackupPasswordScreen.test.tsx | 7 +- .../RestoreCloudBackupPasswordScreen.tsx | 27 +- .../Import/RestoreCloudBackupScreen.test.tsx | 7 +- .../Import/RestoreCloudBackupScreen.tsx | 13 +- .../src/screens/Import/SeedPhraseInput.tsx | 11 +- .../screens/Import/SeedPhraseInputScreen.tsx | 30 +- .../SeedPhraseInputScreenV2.android.mock.tsx | 167 + .../Import/SeedPhraseInputScreenV2.tsx | 28 +- .../src/screens/Import/SelectWalletScreen.tsx | 25 +- .../src/screens/Import/WatchWalletScreen.tsx | 36 +- ...oreCloudBackupPasswordScreen.test.tsx.snap | 2 +- .../screens/Import/useOnDeviceRecoveryData.ts | 20 +- .../src/screens/NFTCollectionScreen.tsx | 49 +- apps/mobile/src/screens/NFTItemScreen.tsx | 124 +- .../screens/Onboarding/BackupScreen.test.tsx | 2 +- .../src/screens/Onboarding/BackupScreen.tsx | 55 +- .../CloudBackupPasswordConfirmScreen.tsx | 45 +- .../CloudBackupPasswordCreateScreen.tsx | 56 +- .../CloudBackupProcessingScreen.tsx | 5 +- .../src/screens/Onboarding/LandingScreen.tsx | 49 +- .../screens/Onboarding/ManualBackupScreen.tsx | 43 +- .../Onboarding/NotificationsSetupScreen.tsx | 56 +- .../OnboardingElements/HeartElement.tsx | 15 - .../OnboardingElements/SendElement.tsx | 15 - .../Onboarding/SecuritySetupScreen.tsx | 54 +- .../src/screens/Onboarding/TermsOfService.tsx | 2 +- .../Onboarding/WelcomeWalletScreen.tsx | 28 +- .../mobile/src/screens/ReceiveCryptoModal.tsx | 54 +- .../src/screens/SettingsAppearanceScreen.tsx | 27 +- .../screens/SettingsBiometricAuthScreen.tsx | 50 +- ...ttingsCloudBackupPasswordConfirmScreen.tsx | 49 +- ...ettingsCloudBackupPasswordCreateScreen.tsx | 69 +- .../SettingsCloudBackupProcessingScreen.tsx | 5 +- .../src/screens/SettingsCloudBackupStatus.tsx | 38 +- .../src/screens/SettingsFiatCurrencyModal.tsx | 22 +- apps/mobile/src/screens/SettingsScreen.tsx | 131 +- .../screens/SettingsViewSeedPhraseScreen.tsx | 6 +- apps/mobile/src/screens/SettingsWallet.tsx | 53 +- .../mobile/src/screens/SettingsWalletEdit.tsx | 43 +- .../SettingsWalletManageConnection.tsx | 7 +- .../mobile/src/screens/TokenDetailsScreen.tsx | 81 +- apps/mobile/src/screens/WebViewScreen.tsx | 20 +- .../src/stories/Introduction.stories.mdx | 4 +- apps/mobile/src/test/fixtures/explore.ts | 4 +- apps/mobile/src/test/fixtures/redux.ts | 7 +- apps/mobile/src/test/render.tsx | 13 +- apps/mobile/src/utils/hooks.ts | 6 +- apps/mobile/src/utils/reanimated.test.ts | 18 +- apps/mobile/src/utils/reanimated.ts | 21 +- .../mobile/src/utils/useAddBackButton.test.ts | 4 +- apps/mobile/src/utils/useAddBackButton.tsx | 4 +- apps/mobile/src/utils/useAppStateTrigger.ts | 6 +- apps/mobile/src/utils/useNoYoloParser.ts | 18 - apps/mobile/src/utils/useSagaStatus.ts | 11 +- apps/mobile/src/utils/version.ts | 2 +- apps/web/.depcheckrc | 2 +- apps/web/.eslintrc.js | 11 + .../e2e/mini-portfolio/accounts.test.ts | 9 +- .../wallet-connection/switch-network.test.ts | 33 +- .../api/image/nfts/asset/[[index]].tsx | 4 +- .../api/image/nfts/collection/[index].tsx | 4 +- .../functions/api/image/pools/[[index]].tsx | 4 +- .../functions/api/image/tokens/[[index]].tsx | 4 +- .../components/metaTagInjector.test.ts | 8 +- .../functions/components/metaTagInjector.ts | 5 +- apps/web/functions/default.test.ts | 4 +- .../functions/explore/tokens/token.test.ts | 6 +- apps/web/functions/nfts/asset/nft.test.ts | 6 +- .../nfts/collection/collection.test.ts | 8 +- apps/web/functions/utils/getRequest.ts | 2 +- apps/web/functions/utils/transformResponse.ts | 2 +- apps/web/package.json | 13 +- apps/web/public/csp.json | 5 +- .../announcement_modal_desktop.png | Bin 0 -> 274777 bytes .../announcement_modal_mobile.png | Bin 0 -> 829291 bytes .../extension_promo/background_connector.png | Bin 0 -> 17859 bytes apps/web/public/index.html | 4 +- apps/web/public/nfts-sitemap.xml | 293 +- apps/web/public/pools-sitemap.xml | 3037 ++++++++++----- apps/web/public/tokens-sitemap.xml | 3277 +++++++++++++---- .../assets/images/extensionIllustration.jpg | Bin 244079 -> 0 bytes .../assets/images/extensionIllustration.png | Bin 0 -> 145206 bytes .../images/walletAnnouncementBannerQR.png | Bin 1197 -> 0 bytes .../src/assets/images/walletIllustration.jpg | Bin 398165 -> 0 bytes .../src/assets/images/walletIllustration.png | Bin 0 -> 199070 bytes .../AccountDetails/AddressDisplay.tsx | 42 + .../components/AccountDrawer/ActionTile.tsx | 2 +- .../AccountDrawer/AuthenticatedHeader.tsx | 109 +- .../components/AccountDrawer/DefaultMenu.tsx | 6 +- .../AccountDrawer/DownloadButton.tsx | 2 +- .../AccountDrawer/GitVersionRow.tsx | 2 +- .../components/AccountDrawer/IconButton.tsx | 20 +- .../AccountDrawer/LocalCurrencyMenu.tsx | 6 +- .../MiniPortfolio/Activity/ActivityRow.tsx | 22 +- .../Activity/CancelOrdersDialog.test.tsx | 12 +- .../Activity/CancelOrdersDialog.tsx | 20 +- .../MiniPortfolio/Activity/Logos.tsx | 2 +- .../Activity/OffchainActivityModal.test.tsx | 6 +- .../Activity/OffchainActivityModal.tsx | 10 +- .../Activity/OffchainOrderLineItem.test.tsx | 6 +- .../Activity/OffchainOrderLineItem.tsx | 2 +- .../CancelOrdersDialog.test.tsx.snap | 804 ++-- .../OffchainActivityModal.test.tsx.snap | 354 +- .../MiniPortfolio/Activity/getCurrency.ts | 10 +- .../MiniPortfolio/Activity/hooks.ts | 6 +- .../MiniPortfolio/Activity/index.tsx | 4 +- .../MiniPortfolio/Activity/parseLocal.test.ts | 46 +- .../MiniPortfolio/Activity/parseLocal.ts | 32 +- .../Activity/parseRemote.test.tsx | 6 +- .../MiniPortfolio/Activity/parseRemote.tsx | 133 +- .../MiniPortfolio/Activity/types.ts | 1 + .../MiniPortfolio/Activity/utils.test.ts | 6 +- .../MiniPortfolio/Activity/utils.ts | 10 +- .../MiniPortfolio/EmptyWallet.tsx | 87 + .../MiniPortfolio/ExpandoRow.tsx | 2 +- .../MiniPortfolio/ExtensionDeeplinks.tsx | 113 + .../Limits/LimitDetailActivityRow.test.tsx | 6 +- .../Limits/LimitDetailActivityRow.tsx | 6 +- .../MiniPortfolio/Limits/LimitsMenu.tsx | 2 +- .../Limits/OpenLimitOrdersButton.tsx | 52 +- .../LimitDetailActivityRow.test.tsx.snap | 46 +- .../__snapshots__/LimitsMenu.test.tsx.snap | 152 +- .../OpenLimitOrdersButton.test.tsx.snap | 51 +- .../MiniPortfolio/NFTs/NFTItem.tsx | 62 +- .../MiniPortfolio/NFTs/index.tsx | 116 +- .../Pools/UniExtensionPoolsMenu.tsx | 20 + .../MiniPortfolio/Pools/cache.ts | 16 +- .../MiniPortfolio/Pools/getTokensAsync.ts | 12 +- .../MiniPortfolio/Pools/hooks.ts | 10 +- .../MiniPortfolio/Pools/index.tsx | 10 +- .../Pools/useMultiChainPositions.tsx | 44 +- .../MiniPortfolio/PortfolioLogo.test.tsx | 2 +- .../MiniPortfolio/PortfolioLogo.tsx | 2 +- .../MiniPortfolio/PortfolioRow.tsx | 2 +- .../MiniPortfolio/Tokens/index.tsx | 8 +- .../AccountDrawer/MiniPortfolio/index.tsx | 2 +- .../AccountDrawer/MiniPortfolio/shared.tsx | 46 + .../components/AccountDrawer/SettingsMenu.tsx | 2 +- .../AccountDrawer/SettingsToggle.test.tsx | 2 +- .../AccountDrawer/SettingsToggle.tsx | 2 +- .../components/AccountDrawer/SlideOutMenu.tsx | 2 +- .../src/components/AccountDrawer/Status.tsx | 28 +- .../AccountDrawer/UniwalletModal.tsx | 4 +- .../__snapshots__/index.test.tsx.snap | 1657 +++++++++ .../components/AccountDrawer/index.test.tsx | 33 + .../src/components/AccountDrawer/index.tsx | 67 +- .../src/components/AccountDrawer/shared.tsx | 2 +- .../components/AddressInputPanel/index.tsx | 7 +- .../src/components/AddressQRModal/index.tsx | 91 + .../AddressQRModal/useAvatarColorProps.tsx | 82 + apps/web/src/components/Badge/RangeBadge.tsx | 2 +- apps/web/src/components/Badge/index.tsx | 2 +- .../components/Banner/Outage/OutageBanner.tsx | 2 +- .../src/components/Banner/shared/styled.tsx | 2 +- .../components/BreadcrumbNav/index.test.tsx | 2 +- .../src/components/BreadcrumbNav/index.tsx | 2 +- apps/web/src/components/Button/GetHelp.tsx | 2 +- apps/web/src/components/Button/index.tsx | 4 +- apps/web/src/components/Card/index.tsx | 2 +- .../web/src/components/Charts/ChartHeader.tsx | 2 +- apps/web/src/components/Charts/ChartModel.tsx | 6 +- .../Charts/LiquidityChart/index.tsx | 8 +- .../Charts/LiquidityChart/renderer.tsx | 4 +- .../src/components/Charts/LoadingState.tsx | 2 +- .../RoundedCandlestickSeries/renderer.ts | 8 +- .../components/Charts/PriceChart/index.tsx | 2 +- .../Charts/SparklineChart/LineChart.tsx | 2 +- .../Charts/SparklineChart/index.tsx | 6 +- .../Charts/StackedLineChart/index.tsx | 2 +- .../stacked-area-series/renderer.ts | 4 +- .../src/components/Charts/TimeSelector.tsx | 2 +- .../CrosshairHighlightPrimitive.tsx | 8 +- .../components/Charts/VolumeChart/index.tsx | 4 +- .../Charts/VolumeChart/renderer.tsx | 4 +- .../components/Charts/VolumeChart/utils.ts | 8 +- apps/web/src/components/Charts/hooks.ts | 2 +- apps/web/src/components/Column/index.tsx | 5 +- apps/web/src/components/Common/index.tsx | 2 +- .../src/components/ConfirmSwapModal/Error.tsx | 2 +- .../components/ConfirmSwapModal/Head.test.tsx | 6 +- .../src/components/ConfirmSwapModal/Head.tsx | 33 +- .../src/components/ConfirmSwapModal/Modal.tsx | 2 +- .../ConfirmSwapModal/Pending.test.tsx | 8 +- .../components/ConfirmSwapModal/Pending.tsx | 2 +- .../ConfirmSwapModal/ProgressIndicator.tsx | 4 +- .../src/components/ConfirmSwapModal/Step.tsx | 2 +- .../ConfirmSwapModal/TradeSummary.tsx | 2 +- .../__snapshots__/Head.test.tsx.snap | 284 +- .../__snapshots__/Pending.test.tsx.snap | 456 +-- .../components/ConfirmSwapModal/animations.ts | 2 +- .../src/components/ConfirmSwapModal/index.tsx | 4 +- .../ConnectedAccountBlocked/index.tsx | 2 +- .../CurrencyInputPanel/FiatValue.tsx | 2 +- .../LimitPriceButton.test.tsx | 2 +- .../LimitPriceInputPanel/LimitPriceButton.tsx | 2 +- .../LimitPriceInputLabel.tsx | 2 +- .../LimitPriceInputPanel.test.tsx | 7 +- .../LimitPriceInputPanel.tsx | 24 +- .../useCurrentPriceAdjustment.ts | 2 +- .../SwapCurrencyInputPanel.tsx | 8 +- .../components/CurrencyInputPanel/index.tsx | 2 +- .../web/src/components/Dialog/Dialog.test.tsx | 12 +- apps/web/src/components/Dialog/Dialog.tsx | 15 +- .../Dialog/__snapshots__/Dialog.test.tsx.snap | 516 ++- apps/web/src/components/DoubleLogo/index.tsx | 2 +- .../DropdownSelector/FilterButton.tsx | 2 +- .../src/components/DropdownSelector/index.tsx | 2 +- .../src/components/ErrorBoundary/index.tsx | 11 +- apps/web/src/components/Expand/index.test.tsx | 6 +- apps/web/src/components/Expand/index.tsx | 2 +- .../FeatureFlagModal/FeatureFlagModal.tsx | 37 +- .../src/components/FeeSelector/FeeOption.tsx | 2 +- apps/web/src/components/FeeSelector/index.tsx | 8 +- .../src/components/FiatOnrampModal/index.tsx | 14 +- .../FormattedCurrencyAmount/index.tsx | 4 +- .../src/components/HoverInlineText/index.tsx | 2 +- .../components/Icons/AlertTriangleFilled.tsx | 2 +- .../src/components/Icons/BraveBrowserLogo.tsx | 52 - apps/web/src/components/Icons/Collapse.tsx | 14 + apps/web/src/components/Icons/Error.tsx | 2 +- apps/web/src/components/Icons/Expand.tsx | 18 + apps/web/src/components/Icons/Images.tsx | 6 +- .../src/components/Icons/LoadingSpinner.tsx | 2 +- apps/web/src/components/Icons/TimeForward.tsx | 13 - apps/web/src/components/Icons/shared.tsx | 2 +- .../components/Identicon/ENSAvatarIcon.tsx | 2 +- .../src/components/Identicon/StatusIcon.tsx | 2 +- apps/web/src/components/Identicon/index.tsx | 2 +- .../InputStepCounter/InputStepCounter.tsx | 2 +- .../LiquidityChartRangeInput/Area.tsx | 6 +- .../LiquidityChartRangeInput/AxisBottom.tsx | 4 +- .../LiquidityChartRangeInput/Brush.tsx | 6 +- .../LiquidityChartRangeInput/Chart.tsx | 4 +- .../LiquidityChartRangeInput/Line.tsx | 4 +- .../LiquidityChartRangeInput/Zoom.tsx | 4 +- .../LiquidityChartRangeInput/index.tsx | 6 +- apps/web/src/components/Loader/styled.tsx | 2 +- apps/web/src/components/Logo/AssetLogo.tsx | 2 +- apps/web/src/components/Logo/CurrencyLogo.tsx | 2 +- apps/web/src/components/Logo/NavIcon.tsx | 2 +- .../src/components/Logo/QueryTokenLogo.tsx | 2 +- apps/web/src/components/Menu/index.tsx | 9 +- .../src/components/Modal/GetHelpHeader.tsx | 39 + apps/web/src/components/Modal/index.tsx | 12 +- apps/web/src/components/ModalViews/index.tsx | 2 +- apps/web/src/components/NavBar/Bag.tsx | 2 +- .../ChainSelector/ChainSelectorRow.test.tsx | 6 +- .../NavBar/ChainSelector/ChainSelectorRow.tsx | 7 +- .../components/NavBar/ChainSelector/index.tsx | 32 +- .../NavBar/CompanyMenu/DownloadAppCTA.tsx | 2 +- .../NavBar/CompanyMenu/MenuDropdown.tsx | 2 +- .../NavBar/CompanyMenu/MobileMenuDrawer.tsx | 4 +- .../components/NavBar/CompanyMenu/index.tsx | 2 +- .../NavBar/DownloadApp/GetTheAppButton.tsx | 2 +- .../NavBar/DownloadApp/Modal/Content.tsx | 6 +- .../NavBar/DownloadApp/Modal/GetStarted.tsx | 40 +- .../NavBar/DownloadApp/Modal/GetTheApp.tsx | 12 +- .../NavBar/DownloadApp/Modal/index.tsx | 87 +- .../web/src/components/NavBar/LEGACY/Blur.tsx | 2 +- .../web/src/components/NavBar/LEGACY/Menu.tsx | 2 +- .../components/NavBar/LEGACY/MenuContent.tsx | 2 +- .../SearchBar/RecentlySearchedAssets.ts | 6 +- .../NavBar/LEGACY/SearchBar/SearchBar.tsx | 10 +- .../LEGACY/SearchBar/SearchBarDropdown.tsx | 8 +- .../NavBar/LEGACY/SearchBar/SuggestionRow.tsx | 4 +- .../src/components/NavBar/LEGACY/index.tsx | 2 +- .../MobileBottomBar/MobileBottomBar.tsx | 7 +- .../NavBar/MobileBottomBar/TDPActionTabs.tsx | 21 +- apps/web/src/components/NavBar/NavBar.tsx | 5 +- .../NavBar/NavDropdown/NavDropdown.tsx | 4 +- .../components/NavBar/NavDropdown/shared.tsx | 2 +- apps/web/src/components/NavBar/NavIcon.tsx | 2 +- .../NavBar/PreferencesMenu/Header.tsx | 2 +- .../NavBar/PreferencesMenu/Preferences.tsx | 2 +- .../NavBar/PreferencesMenu/index.tsx | 2 +- .../NavBar/PreferencesMenu/shared.tsx | 2 +- .../web/src/components/NavBar/ScreenSizes.tsx | 2 +- .../SearchBar/RecentlySearchedAssets.ts | 6 +- .../NavBar/SearchBar/SearchBarDropdown.tsx | 8 +- .../NavBar/SearchBar/SuggestionRow.tsx | 4 +- .../src/components/NavBar/SearchBar/index.tsx | 2 +- .../components/NavBar/Tabs/QuickKey.test.tsx | 19 + apps/web/src/components/NavBar/Tabs/Tabs.tsx | 4 +- .../components/NavBar/Tabs/TabsContent.tsx | 34 +- .../src/components/NavigationTabs/index.tsx | 2 +- .../components/NetworkAlert/NetworkAlert.tsx | 2 +- .../src/components/NumericalInput/index.tsx | 4 +- .../Polling/ChainConnectivityWarning.tsx | 2 +- apps/web/src/components/Polling/index.tsx | 10 +- .../Pools/PoolDetails/ChartSection/hooks.ts | 6 +- .../Pools/PoolDetails/ChartSection/index.tsx | 10 +- .../PoolDetails/PoolDetailsHeader.test.tsx | 8 +- .../Pools/PoolDetails/PoolDetailsHeader.tsx | 6 +- .../PoolDetails/PoolDetailsLink.test.tsx | 8 +- .../Pools/PoolDetails/PoolDetailsLink.tsx | 6 +- .../PoolDetails/PoolDetailsPositionsTable.tsx | 10 +- .../PoolDetails/PoolDetailsStats.test.tsx | 4 +- .../Pools/PoolDetails/PoolDetailsStats.tsx | 2 +- .../PoolDetailsStatsButtons.test.tsx | 6 +- .../PoolDetails/PoolDetailsStatsButtons.tsx | 8 +- .../Pools/PoolDetails/PoolDetailsTable.tsx | 6 +- .../PoolDetailsTransactionsTable.tsx | 6 +- .../components/Pools/PoolDetails/shared.ts | 2 +- .../components/Pools/PoolTable/PoolTable.tsx | 6 +- apps/web/src/components/Popover/index.tsx | 8 +- apps/web/src/components/Popups/ClaimPopup.tsx | 2 +- .../src/components/Popups/PopupContent.tsx | 2 +- apps/web/src/components/Popups/index.tsx | 2 +- .../web/src/components/PositionCard/Sushi.tsx | 2 +- apps/web/src/components/PositionCard/V2.tsx | 2 +- .../web/src/components/PositionCard/index.tsx | 4 +- .../web/src/components/PositionList/index.tsx | 2 +- .../PositionListItem.test.tsx | 2 +- .../src/components/PositionListItem/index.tsx | 2 +- .../src/components/PositionPreview/index.tsx | 6 +- .../src/components/PrivacyPolicy/index.tsx | 2 +- .../src/components/QuestionHelper/index.tsx | 2 +- .../RangeSelector/PresetsButtons.tsx | 2 +- .../ReceiveCryptoModal/ChooseProvider.tsx | 132 + .../ReceiveCryptoModal/ProviderOption.tsx | 66 + .../components/ReceiveCryptoModal/index.tsx | 53 + .../RouterLabel/UniswapXRouterLabel.tsx | 2 +- apps/web/src/components/RouterLabel/index.tsx | 2 +- .../RoutingDiagram/RoutingDiagram.test.tsx | 2 +- .../RoutingDiagram/RoutingDiagram.tsx | 2 +- apps/web/src/components/Row/index.tsx | 7 +- .../SearchModal/CommonBases.test.tsx | 6 +- .../components/SearchModal/CommonBases.tsx | 4 +- .../SearchModal/CurrencyList/index.test.tsx | 8 +- .../SearchModal/CurrencyList/index.tsx | 18 +- .../components/SearchModal/CurrencySearch.tsx | 15 +- .../SearchModal/CurrencySearchModal.tsx | 2 +- .../web/src/components/SearchModal/styled.tsx | 3 +- .../SearchModal/useCurrencySearchResults.ts | 7 +- .../src/components/Settings/Input/index.tsx | 2 +- .../Settings/MaxSlippageSettings/index.tsx | 2 +- .../Settings/MenuButton/index.test.tsx | 2 +- .../components/Settings/MenuButton/index.tsx | 2 +- .../Settings/MultipleRoutingOptions.test.tsx | 8 +- .../Settings/MultipleRoutingOptions.tsx | 6 +- .../RouterPreferenceSettings/index.tsx | 2 +- apps/web/src/components/Settings/index.tsx | 14 +- apps/web/src/components/Slider/index.tsx | 19 +- apps/web/src/components/Table/Cell.tsx | 2 +- apps/web/src/components/Table/ErrorBox.tsx | 2 +- apps/web/src/components/Table/Filter.tsx | 4 +- apps/web/src/components/Table/index.tsx | 2 +- apps/web/src/components/Table/styled.tsx | 2 +- .../src/components/TextInput/index.test.tsx | 8 +- apps/web/src/components/TextInput/index.tsx | 8 +- .../web/src/components/Toggle/MultiToggle.tsx | 2 +- .../src/components/Toggle/PillMultiToggle.tsx | 8 +- apps/web/src/components/Toggle/index.tsx | 2 +- .../TokenSafety/TokenSafetyIcon.tsx | 2 +- .../TokenSafety/TokenSafetyLabel.tsx | 2 +- .../TokenSafety/TokenSafetyMessage.tsx | 2 +- apps/web/src/components/TokenSafety/index.tsx | 2 +- .../components/Tokens/TokenDetails/About.tsx | 2 +- .../Tokens/TokenDetails/ActivitySection.tsx | 2 +- .../Tokens/TokenDetails/BalanceSummary.tsx | 4 +- .../ChartSection/AdvancedPriceChartToggle.tsx | 2 +- .../ChartSection/ChartTypeSelector.tsx | 4 +- .../Tokens/TokenDetails/ChartSection/hooks.ts | 6 +- .../TokenDetails/ChartSection/index.tsx | 2 +- .../Tokens/TokenDetails/ChartSection/util.ts | 2 +- .../components/Tokens/TokenDetails/Delta.tsx | 2 +- .../TokenDetails/InvalidTokenDetails.tsx | 2 +- .../MobileBalanceSummaryFooter.tsx | 2 +- .../Tokens/TokenDetails/Resource.tsx | 2 +- .../Tokens/TokenDetails/ShareButton.tsx | 4 +- .../Tokens/TokenDetails/Skeleton.test.tsx | 2 +- .../Tokens/TokenDetails/Skeleton.tsx | 4 +- .../Tokens/TokenDetails/StatsSection.tsx | 2 +- .../Tokens/TokenDetails/TokenDescription.tsx | 6 +- .../TokenDetails/TokenDetailsHeader.tsx | 4 +- .../components/Tokens/TokenDetails/index.tsx | 20 +- .../components/Tokens/TokenDetails/shared.ts | 4 +- .../tables/TokenDetailsPoolsTable.test.tsx | 6 +- .../tables/TokenDetailsPoolsTable.tsx | 4 +- .../TokenDetails/tables/TransactionsTable.tsx | 10 +- .../Tokens/TokenTable/NetworkFilter.tsx | 2 +- .../Tokens/TokenTable/SearchBar.tsx | 2 +- .../Tokens/TokenTable/TimeSelector.tsx | 2 +- .../components/Tokens/TokenTable/index.tsx | 4 +- apps/web/src/components/Tokens/loading.tsx | 2 +- apps/web/src/components/Tooltip/index.tsx | 60 +- .../TopLevelBanners/MobileAppPromoBanner.tsx | 2 +- .../components/TopLevelBanners/UkBanner.tsx | 2 +- .../TopLevelModals/ExtensionLaunchModal.tsx | 172 + .../TopLevelModals/UkDisclaimerModal.tsx | 2 +- .../src/components/TopLevelModals/index.tsx | 11 +- .../AnimatedConfirmation.tsx | 2 +- .../ConfirmationModalContent.test.tsx | 4 +- .../TransactionConfirmationModal/index.tsx | 8 +- .../UniTag/UniTagProfilePicture.tsx | 2 +- .../src/components/V2Unsupported/index.tsx | 2 +- .../WalletModal/ConnectionErrorView.tsx | 2 +- .../WalletModal/DownloadWalletOption.tsx | 69 + .../web/src/components/WalletModal/Option.tsx | 2 +- .../WalletModal/PrivacyPolicyNotice.tsx | 4 +- .../WalletModal/UniswapWalletOptions.test.tsx | 53 + .../WalletModal/UniswapWalletOptions.tsx | 42 +- .../UniswapWalletOptions.test.tsx.snap | 646 ++++ apps/web/src/components/WalletModal/index.tsx | 61 +- .../WalletModal/useOrderedConnections.tsx | 48 +- .../src/components/WalletOneLinkQR/index.tsx | 424 +++ .../components/Web3Provider/index.test.tsx | 2 +- .../web/src/components/Web3Provider/index.tsx | 2 +- apps/web/src/components/Web3Status/index.tsx | 18 +- .../addLiquidity/OwnershipWarning.tsx | 2 +- .../components/addLiquidity/PoolWarning.tsx | 2 +- .../components/claim/AddressClaimModal.tsx | 2 +- apps/web/src/components/earn/styled.tsx | 2 +- .../src/components/swap/DetailLineItem.tsx | 2 +- .../components/swap/GasBreakdownTooltip.tsx | 2 +- .../components/swap/GasEstimateTooltip.tsx | 6 +- .../src/components/swap/LimitDisclaimer.tsx | 2 +- .../src/components/swap/PriceImpactModal.tsx | 2 +- .../components/swap/PriceImpactWarning.tsx | 2 +- .../swap/SwapBuyFiatButton.test.tsx | 2 +- .../src/components/swap/SwapDetails.test.tsx | 16 +- apps/web/src/components/swap/SwapDetails.tsx | 2 +- .../swap/SwapDetailsDropdown.test.tsx | 18 +- .../components/swap/SwapDetailsDropdown.tsx | 2 +- .../src/components/swap/SwapHeader.test.tsx | 6 +- apps/web/src/components/swap/SwapHeader.tsx | 12 +- .../src/components/swap/SwapLineItem.test.tsx | 4 +- apps/web/src/components/swap/SwapLineItem.tsx | 2 +- .../components/swap/SwapModalHeaderAmount.tsx | 2 +- .../src/components/swap/SwapPreview.test.tsx | 10 +- apps/web/src/components/swap/SwapPreview.tsx | 2 +- apps/web/src/components/swap/SwapRoute.tsx | 5 +- apps/web/src/components/swap/SwapSkeleton.tsx | 2 +- apps/web/src/components/swap/TradePrice.tsx | 2 +- .../swap/UnsupportedCurrencyFooter.test.tsx | 10 +- .../swap/UnsupportedCurrencyFooter.tsx | 2 +- .../__snapshots__/SwapLineItem.test.tsx.snap | 32 +- .../swap/confirmPriceImpactWithoutFee.ts | 8 +- apps/web/src/components/swap/styled.tsx | 2 +- .../web/src/components/vote/DelegateModal.tsx | 2 +- apps/web/src/components/vote/ExecuteModal.tsx | 2 +- .../components/vote/ProposalEmptyState.tsx | 2 +- apps/web/src/components/vote/QueueModal.tsx | 2 +- apps/web/src/components/vote/VoteModal.tsx | 2 +- apps/web/src/connection/web3reactShim.ts | 2 +- apps/web/src/constants/chains.test.ts | 59 +- apps/web/src/constants/chains.ts | 53 +- apps/web/src/constants/providers.ts | 6 +- apps/web/src/constants/routing.ts | 4 +- apps/web/src/constants/tokens.ts | 124 +- apps/web/src/dev/DevFlagsBox.tsx | 4 +- .../dynamicConfig/quickRouteChains.ts | 14 +- .../src/graphql/data/RecentTokenTransfers.ts | 2 +- apps/web/src/graphql/data/SearchTokens.ts | 6 +- apps/web/src/graphql/data/TopTokens.ts | 10 +- apps/web/src/graphql/data/TrendingTokens.ts | 2 +- .../graphql/data/apollo/AdaptiveRefetch.tsx | 2 +- .../data/apollo/AssetActivityProvider.tsx | 30 +- .../apollo/TokenBalancesProvider.test.tsx | 6 +- .../data/apollo/TokenBalancesProvider.tsx | 2 +- apps/web/src/graphql/data/nft/Asset.ts | 10 +- apps/web/src/graphql/data/nft/Collection.ts | 2 +- .../src/graphql/data/nft/CollectionSearch.ts | 6 +- apps/web/src/graphql/data/nft/Details.ts | 4 +- apps/web/src/graphql/data/nft/NftActivity.ts | 6 +- apps/web/src/graphql/data/nft/NftBalance.ts | 43 +- .../graphql/data/nft/TrendingCollections.ts | 2 +- .../web/src/graphql/data/pools/usePoolData.ts | 4 +- .../graphql/data/pools/usePoolTransactions.ts | 8 +- .../data/pools/usePoolsFromTokenAddress.ts | 4 +- .../web/src/graphql/data/pools/useTopPools.ts | 6 +- apps/web/src/graphql/data/protocolStats.ts | 4 +- apps/web/src/graphql/data/types.test.ts | 2 +- apps/web/src/graphql/data/types.ts | 5 +- .../src/graphql/data/useAllTransactions.ts | 8 +- .../src/graphql/data/useTokenTransactions.ts | 8 +- apps/web/src/graphql/data/util.tsx | 18 +- apps/web/src/hooks/Tokens.ts | 34 +- .../web/src/hooks/screenSize/useScreenSize.ts | 2 +- apps/web/src/hooks/useAccount.ts | 6 +- apps/web/src/hooks/useActiveLocalCurrency.ts | 6 +- apps/web/src/hooks/useActiveLocale.ts | 3 +- apps/web/src/hooks/useApproveCallback.ts | 2 +- apps/web/src/hooks/useArgentWalletContract.ts | 2 +- apps/web/src/hooks/useAutoRouterSupported.tsx | 6 - .../web/src/hooks/useAutoSlippageTolerance.ts | 7 +- apps/web/src/hooks/useColor.ts | 4 +- apps/web/src/hooks/useConfirmModalState.ts | 10 +- apps/web/src/hooks/useContract.ts | 21 +- .../web/src/hooks/useCurrentBlockTimestamp.ts | 2 +- .../src/hooks/useDebouncedChangeHandler.tsx | 4 +- apps/web/src/hooks/useDebouncedTrade.ts | 18 +- apps/web/src/hooks/useENS.ts | 2 +- apps/web/src/hooks/useENSAddress.ts | 4 +- apps/web/src/hooks/useENSAvatar.ts | 20 +- apps/web/src/hooks/useENSName.ts | 4 +- apps/web/src/hooks/useERC20Permit.ts | 2 +- apps/web/src/hooks/useEthersProvider.ts | 4 +- apps/web/src/hooks/useFeeTierDistribution.ts | 12 +- apps/web/src/hooks/useFetchListCallback.ts | 4 +- ...seFilterPossiblyMaliciousPositions.test.ts | 2 +- .../useFilterPossiblyMaliciousPositions.ts | 2 +- .../src/hooks/useGroupedRecentTransfers.ts | 23 +- apps/web/src/hooks/useIsPoolOutOfSync.ts | 12 +- apps/web/src/hooks/useIsTickAtLimit.ts | 4 +- .../src/hooks/useLocalCurrencyLinkProps.ts | 2 +- apps/web/src/hooks/useLocationLinkProps.ts | 2 +- apps/web/src/hooks/useMachineTime.ts | 2 +- apps/web/src/hooks/useOnClickOutside.ts | 2 +- apps/web/src/hooks/usePermit2Allowance.ts | 6 +- apps/web/src/hooks/usePermitAllowance.ts | 10 +- apps/web/src/hooks/usePoolTickData.ts | 8 +- apps/web/src/hooks/usePools.ts | 14 +- apps/web/src/hooks/usePositionTokenURI.ts | 2 +- apps/web/src/hooks/useSelectChain.ts | 8 +- apps/web/src/hooks/useSendCallback.ts | 4 +- apps/web/src/hooks/useStablecoinPrice.ts | 15 +- apps/web/src/hooks/useSwapCallback.tsx | 36 +- apps/web/src/hooks/useSwapTaxes.ts | 17 +- apps/web/src/hooks/useSwitchChain.ts | 14 +- apps/web/src/hooks/useSyncChainQuery.ts | 22 +- apps/web/src/hooks/useTokenAllowance.ts | 10 +- apps/web/src/hooks/useTokenBalances.test.ts | 13 +- apps/web/src/hooks/useTokenBalances.ts | 29 +- apps/web/src/hooks/useTokenWarningColor.ts | 2 +- apps/web/src/hooks/useTotalSupply.ts | 2 +- apps/web/src/hooks/useTransactionDeadline.ts | 4 +- apps/web/src/hooks/useTransactionGasFee.ts | 2 +- apps/web/src/hooks/useUSDPrice.ts | 6 +- apps/web/src/hooks/useUSDTokenUpdater.ts | 21 +- apps/web/src/hooks/useUniswapWalletOptions.ts | 23 +- apps/web/src/hooks/useUniswapXSwapCallback.ts | 14 +- apps/web/src/hooks/useUniversalRouter.ts | 6 +- apps/web/src/hooks/useUnmountingAnimation.ts | 2 +- .../src/hooks/useV2LiquidityTokenPermit.ts | 2 +- apps/web/src/hooks/useV2Pairs.ts | 6 +- apps/web/src/hooks/useV3PositionFees.ts | 4 +- apps/web/src/hooks/useWrapCallback.tsx | 34 +- apps/web/src/i18n.tsx | 2 +- apps/web/src/i18n/Plural.tsx | 3 +- apps/web/src/i18n/locales/source/en-US.json | 57 +- apps/web/src/i18n/useTranslation.tsx | 3 +- apps/web/src/index.tsx | 21 +- .../routing/clientSideSmartOrderRouter.ts | 6 +- .../hooks/routing/useRoutingAPIArguments.ts | 22 +- apps/web/src/lib/hooks/useApproval.ts | 6 +- apps/web/src/lib/hooks/useBlockNumber.tsx | 2 +- apps/web/src/lib/hooks/useCurrencyBalance.ts | 27 +- apps/web/src/lib/hooks/useNativeCurrency.ts | 2 +- .../hooks/useTokenList/fetchTokenList.test.ts | 10 +- .../lib/hooks/useTokenList/fetchTokenList.ts | 4 +- .../lib/hooks/useTokenList/sorting.test.ts | 2 +- .../web/src/lib/hooks/useTokenList/sorting.ts | 4 +- apps/web/src/lib/state/multicall.tsx | 2 +- apps/web/src/lib/styled-components.ts | 18 + apps/web/src/lib/utils/analytics.ts | 8 +- .../lib/utils/contenthashToUri.test.skip.ts | 2 +- apps/web/src/lib/utils/searchBar.ts | 2 +- .../src/lib/utils/tryParseCurrencyAmount.ts | 2 +- apps/web/src/nft/components/Box.ts | 1 + apps/web/src/nft/components/bag/Bag.tsx | 6 +- .../src/nft/components/bag/BagFooter.test.tsx | 2 +- apps/web/src/nft/components/bag/BagFooter.tsx | 8 +- apps/web/src/nft/components/bag/BagHeader.tsx | 2 +- apps/web/src/nft/components/bag/BagRow.tsx | 8 +- .../src/nft/components/bag/ButtonStates.tsx | 4 +- .../src/nft/components/bag/EmptyContent.tsx | 2 +- .../src/nft/components/bag/MobileHoverBag.tsx | 4 +- .../src/nft/components/bag/TimedLoader.tsx | 2 +- .../src/nft/components/card/containers.tsx | 2 +- apps/web/src/nft/components/card/icons.tsx | 2 +- apps/web/src/nft/components/card/index.tsx | 2 +- apps/web/src/nft/components/card/media.tsx | 2 +- apps/web/src/nft/components/card/utils.tsx | 10 +- .../nft/components/collection/Activity.tsx | 8 +- .../collection/ActivityCells.test.tsx | 2 +- .../components/collection/ActivityCells.tsx | 8 +- .../collection/ActivitySwitcher.tsx | 2 +- .../collection/CollectionAsset.test.tsx | 2 +- .../components/collection/CollectionAsset.tsx | 2 +- .../components/collection/CollectionNfts.tsx | 12 +- .../collection/CollectionPageSkeleton.tsx | 2 +- .../components/collection/CollectionStats.tsx | 2 +- .../src/nft/components/collection/Filters.tsx | 2 +- .../collection/MarketplaceSelect.tsx | 4 +- .../nft/components/collection/PriceRange.tsx | 2 +- .../src/nft/components/collection/Sweep.tsx | 25 +- .../nft/components/collection/TraitChip.tsx | 2 +- .../nft/components/collection/TraitSelect.tsx | 10 +- .../components/collection/TraitsHeader.tsx | 2 +- .../collection/TransactionCompleteModal.tsx | 6 +- .../UnavailableCollectionPage.test.tsx | 2 +- .../collection/UnavailableCollectionPage.tsx | 2 +- .../src/nft/components/collection/shared.tsx | 2 +- .../SortDropdown/FilterSortDropdown.tsx | 2 +- .../components/common/SortDropdown/index.tsx | 2 +- .../nft/components/details/AssetActivity.tsx | 2 +- .../nft/components/details/AssetDetails.tsx | 12 +- .../details/AssetDetailsLoading.tsx | 2 +- .../components/details/AssetPriceDetails.tsx | 10 +- .../components/details/DetailsContainer.tsx | 2 +- .../nft/components/details/InfoContainer.tsx | 2 +- .../components/details/TraitsContainer.tsx | 4 +- .../web/src/nft/components/explore/Banner.tsx | 6 +- .../src/nft/components/explore/Carousel.tsx | 8 +- .../nft/components/explore/CarouselCard.tsx | 4 +- .../nft/components/explore/Cells/Cells.tsx | 2 +- .../components/explore/CollectionTable.tsx | 2 +- apps/web/src/nft/components/explore/Table.tsx | 4 +- .../explore/TrendingCollections.tsx | 4 +- apps/web/src/nft/components/icons.tsx | 24 +- .../src/nft/components/layout/Checkbox.tsx | 2 +- .../nft/components/profile/list/Dropdown.tsx | 2 +- .../nft/components/profile/list/ListPage.tsx | 6 +- .../components/profile/list/ListingButton.tsx | 4 +- .../profile/list/MarketplaceRow.tsx | 15 +- .../list/Modal/BelowFloorWarningModal.tsx | 4 +- .../profile/list/Modal/ContentRow.tsx | 2 +- .../profile/list/Modal/ListModal.tsx | 10 +- .../profile/list/Modal/ListModalSection.tsx | 2 +- .../profile/list/Modal/SuccessScreen.tsx | 2 +- .../components/profile/list/NFTListRow.tsx | 2 +- .../profile/list/NFTListingsGrid.tsx | 4 +- .../profile/list/PriceTextInput.tsx | 10 +- .../profile/list/RoyaltyTooltip.tsx | 2 +- .../list/SelectMarketplacesDropdown.tsx | 4 +- .../profile/list/SetDurationModal.tsx | 4 +- .../nft/components/profile/list/shared.tsx | 2 +- .../src/nft/components/profile/list/utils.ts | 36 +- .../profile/view/EmptyWalletContent.test.tsx | 2 +- .../profile/view/EmptyWalletContent.tsx | 2 +- .../components/profile/view/FilterSidebar.tsx | 12 +- .../components/profile/view/ProfilePage.tsx | 12 +- .../view/ProfilePageLoadingSkeleton.tsx | 2 +- .../profile/view/ViewMyNftsAsset.test.tsx | 2 +- .../profile/view/ViewMyNftsAsset.tsx | 4 +- .../src/nft/components/profile/view/icons.tsx | 2 +- apps/web/src/nft/css/cssObjectFromTheme.ts | 4 +- apps/web/src/nft/hooks/useBag.ts | 12 +- apps/web/src/nft/hooks/useBagTotalEthPrice.ts | 6 +- .../web/src/nft/hooks/useCollectionFilters.ts | 4 +- .../useDerivedPayWithAnyTokenSwapInfo.ts | 4 +- apps/web/src/nft/hooks/useFetchAssets.ts | 2 +- apps/web/src/nft/hooks/useFiltersExpanded.ts | 6 +- .../src/nft/hooks/useIsCollectionLoading.ts | 4 +- apps/web/src/nft/hooks/useNFTList.ts | 12 +- .../src/nft/hooks/usePayWithAnyTokenSwap.ts | 2 +- apps/web/src/nft/hooks/usePriceImpact.ts | 6 +- apps/web/src/nft/hooks/usePriceRange.ts | 4 +- apps/web/src/nft/hooks/useProfilePageState.ts | 4 +- apps/web/src/nft/hooks/usePurchaseAssets.ts | 4 +- apps/web/src/nft/hooks/useSellAsset.ts | 16 +- apps/web/src/nft/hooks/useSendTransaction.ts | 18 +- apps/web/src/nft/hooks/useTokenInput.ts | 4 +- apps/web/src/nft/hooks/useTraitsOpen.ts | 4 +- .../src/nft/hooks/useTransactionResponse.ts | 4 +- .../web/src/nft/hooks/useWalletCollections.ts | 4 +- apps/web/src/nft/pages/asset/Asset.tsx | 9 +- .../web/src/nft/pages/collection/index.css.ts | 2 +- apps/web/src/nft/pages/collection/index.tsx | 2 +- apps/web/src/nft/pages/explore/index.tsx | 2 +- apps/web/src/nft/pages/profile/index.tsx | 2 +- apps/web/src/nft/types/sell/index.ts | 2 + apps/web/src/nft/utils/asset.tsx | 2 +- apps/web/src/nft/utils/bag.ts | 6 +- apps/web/src/nft/utils/carousel.ts | 2 +- apps/web/src/nft/utils/listNfts.ts | 12 +- apps/web/src/nft/utils/nftRoute.ts | 2 +- apps/web/src/nft/utils/pooledAssets.ts | 10 +- apps/web/src/nft/utils/transactionResponse.ts | 2 +- .../utils/txRoute/combineItemsWithTxRoute.ts | 12 +- apps/web/src/nft/utils/updatedAssets.ts | 2 +- apps/web/src/nft/utils/x2y2.ts | 2 +- apps/web/src/pages/AddLiquidity/Review.tsx | 2 +- .../src/pages/AddLiquidity/blastAlerts.tsx | 2 +- apps/web/src/pages/AddLiquidity/index.tsx | 36 +- apps/web/src/pages/AddLiquidity/styled.tsx | 2 +- .../AddLiquidityV2/PoolPriceBar.test.tsx | 2 +- .../src/pages/AddLiquidityV2/PoolPriceBar.tsx | 2 +- apps/web/src/pages/AddLiquidityV2/index.tsx | 15 +- apps/web/src/pages/App.tsx | 6 +- apps/web/src/pages/App/AppBody.tsx | 2 +- apps/web/src/pages/App/Body.tsx | 2 +- apps/web/src/pages/App/Header.tsx | 7 +- apps/web/src/pages/App/Layout.tsx | 2 +- .../CreateProposal/ProposalActionDetail.tsx | 4 +- .../CreateProposal/ProposalActionSelector.tsx | 4 +- .../pages/CreateProposal/ProposalEditor.tsx | 2 +- .../ProposalSubmissionModal.tsx | 2 +- apps/web/src/pages/CreateProposal/index.tsx | 22 +- .../Explore/charts/ExploreChartsSection.tsx | 8 +- apps/web/src/pages/Explore/index.tsx | 6 +- .../Explore/tables/RecentTransactions.tsx | 2 +- apps/web/src/pages/Landing/Fold.tsx | 2 +- apps/web/src/pages/Landing/LandingV2.tsx | 2 +- .../src/pages/Landing/components/Generics.tsx | 2 +- .../src/pages/Landing/components/StatCard.tsx | 10 +- .../Landing/components/TokenCloud/Ticker.tsx | 2 +- .../Landing/components/TokenCloud/Token.tsx | 6 +- .../Landing/components/TokenCloud/index.tsx | 2 +- .../pages/Landing/components/animations.tsx | 2 +- .../components/cards/DocumentationCard.tsx | 2 +- .../components/cards/DownloadWalletCard.tsx | 2 +- .../components/cards/LiquidityCard.tsx | 2 +- .../Landing/components/cards/PillButton.tsx | 2 +- .../components/cards/ValuePropCard.tsx | 2 +- .../Landing/components/cards/WebappCard.tsx | 10 +- apps/web/src/pages/Landing/index.tsx | 2 +- .../pages/Landing/sections/DirectToDefi.tsx | 2 +- .../web/src/pages/Landing/sections/Footer.tsx | 2 +- apps/web/src/pages/Landing/sections/Hero.tsx | 30 +- .../pages/Landing/sections/NewsletterEtc.tsx | 2 +- apps/web/src/pages/Landing/sections/Stats.tsx | 2 +- .../src/pages/Landing/sections/useInView.tsx | 2 +- .../web/src/pages/MigrateV2/MigrateV2Pair.tsx | 36 +- apps/web/src/pages/MigrateV2/index.tsx | 10 +- apps/web/src/pages/NotFound/index.tsx | 2 +- apps/web/src/pages/Pool/CTACards.tsx | 2 +- apps/web/src/pages/Pool/PositionPage.test.tsx | 2 +- apps/web/src/pages/Pool/PositionPage.tsx | 14 +- apps/web/src/pages/Pool/index.tsx | 6 +- apps/web/src/pages/Pool/shared.tsx | 2 +- apps/web/src/pages/Pool/styled.tsx | 2 +- apps/web/src/pages/Pool/v2.tsx | 22 +- apps/web/src/pages/PoolDetails/index.test.tsx | 4 +- apps/web/src/pages/PoolDetails/index.tsx | 2 +- apps/web/src/pages/PoolFinder/index.tsx | 8 +- apps/web/src/pages/RemoveLiquidity/V3.tsx | 4 +- apps/web/src/pages/RemoveLiquidity/index.tsx | 32 +- apps/web/src/pages/RemoveLiquidity/styled.tsx | 2 +- apps/web/src/pages/RouteDefinitions.tsx | 2 +- apps/web/src/pages/Swap/Buy/BuyForm.tsx | 68 +- apps/web/src/pages/Swap/Buy/BuyFormButton.tsx | 19 +- .../web/src/pages/Swap/Buy/BuyFormContext.tsx | 70 +- .../pages/Swap/Buy/ChooseProviderModal.tsx | 176 +- .../pages/Swap/Buy/CountryListModal.test.tsx | 6 +- .../pages/Swap/Buy/CountryListRow.test.tsx | 2 +- .../web/src/pages/Swap/Buy/CountryListRow.tsx | 2 +- .../Swap/Buy/FiatOnRampCurrencyModal.tsx | 2 +- .../pages/Swap/Buy/PredefinedAmount.test.tsx | 4 +- .../src/pages/Swap/Buy/PredefinedAmount.tsx | 41 +- .../Swap/Buy/ProviderConnectedView.test.tsx | 22 + .../pages/Swap/Buy/ProviderConnectedView.tsx | 70 + .../Swap/Buy/ProviderConnectionError.test.tsx | 24 + .../Swap/Buy/ProviderConnectionError.tsx | 59 + .../web/src/pages/Swap/Buy/ProviderOption.tsx | 93 + .../PredefinedAmount.test.tsx.snap | 46 +- .../ProviderConnectedView.test.tsx.snap | 227 ++ .../ProviderConnectionError.test.tsx.snap | 228 ++ apps/web/src/pages/Swap/Buy/hooks.ts | 6 +- apps/web/src/pages/Swap/Buy/shared.ts | 11 - apps/web/src/pages/Swap/Buy/shared.tsx | 78 + apps/web/src/pages/Swap/Buy/test/constants.ts | 43 + .../Swap/Limit/LimitExpirySection.test.tsx | 6 +- .../pages/Swap/Limit/LimitExpirySection.tsx | 2 +- apps/web/src/pages/Swap/Limit/LimitForm.tsx | 50 +- .../pages/Swap/Limit/LimitPriceError.test.tsx | 4 +- .../src/pages/Swap/Limit/LimitPriceError.tsx | 2 +- .../Swap/Send/NewAddressSpeedBump.test.tsx | 2 +- .../pages/Swap/Send/NewAddressSpeedBump.tsx | 2 +- .../Swap/Send/SendCurrencyInputForm.test.tsx | 7 +- .../pages/Swap/Send/SendCurrencyInputForm.tsx | 10 +- apps/web/src/pages/Swap/Send/SendForm.tsx | 35 +- .../Swap/Send/SendRecipientForm.test.tsx | 9 +- .../src/pages/Swap/Send/SendRecipientForm.tsx | 19 +- .../pages/Swap/Send/SendReviewModal.test.tsx | 5 +- .../src/pages/Swap/Send/SendReviewModal.tsx | 22 +- .../Swap/Send/SmartContractSpeedBump.tsx | 2 +- .../Swap/Send/SmartContractSpeedbump.test.tsx | 2 +- .../NewAddressSpeedBump.test.tsx.snap | 262 +- .../SendReviewModal.test.tsx.snap | 538 +-- .../SmartContractSpeedbump.test.tsx.snap | 258 +- apps/web/src/pages/Swap/SwapForm.tsx | 94 +- apps/web/src/pages/Swap/TaxTooltipBody.tsx | 2 +- apps/web/src/pages/Swap/common/shared.tsx | 2 +- apps/web/src/pages/Swap/index.tsx | 16 +- apps/web/src/pages/TokenDetails/index.tsx | 8 +- apps/web/src/pages/Vote/Landing.tsx | 6 +- apps/web/src/pages/Vote/VotePage.tsx | 14 +- apps/web/src/pages/Vote/styled.tsx | 2 +- apps/web/src/pages/getExploreTitle.ts | 2 +- apps/web/src/pages/metatags.ts | 4 +- apps/web/src/pages/paths.test.ts | 2 +- apps/web/src/rpc/AppJsonRpcProvider.ts | 2 +- apps/web/src/rpc/ConfiguredJsonRpcProvider.ts | 2 +- apps/web/src/setupRive.ts | 5 + apps/web/src/setupTests.ts | 3 +- apps/web/src/shared-cloud/metatags.ts | 2 +- apps/web/src/state/activity/polling/orders.ts | 6 +- .../src/state/activity/polling/retry.test.ts | 2 +- apps/web/src/state/activity/polling/retry.ts | 2 +- .../activity/polling/transactions.test.ts | 8 +- .../state/activity/polling/transactions.ts | 6 +- apps/web/src/state/activity/subscription.ts | 10 +- apps/web/src/state/activity/updater.tsx | 8 +- apps/web/src/state/application/hooks.ts | 6 +- apps/web/src/state/application/reducer.ts | 2 + apps/web/src/state/burn/hooks.tsx | 6 +- apps/web/src/state/burn/reducer.ts | 2 +- apps/web/src/state/burn/v3/hooks.tsx | 6 +- apps/web/src/state/burn/v3/reducer.ts | 2 +- apps/web/src/state/claim/hooks.ts | 6 +- .../src/state/fiatOnRampTransactions/hooks.ts | 22 + .../fiatOnRampTransactions/reducer.test.ts | 86 + .../state/fiatOnRampTransactions/reducer.ts | 52 + .../src/state/fiatOnRampTransactions/types.ts | 24 + .../fiatOnRampTransactions/updater.test.tsx | 61 + .../state/fiatOnRampTransactions/updater.ts | 99 + apps/web/src/state/governance/hooks.ts | 16 +- apps/web/src/state/index.ts | 45 +- apps/web/src/state/limit/hooks.ts | 14 +- apps/web/src/state/lists/hooks.ts | 2 +- apps/web/src/state/lists/reducer.test.ts | 24 +- apps/web/src/state/lists/reducer.ts | 4 +- apps/web/src/state/lists/updater.ts | 6 +- apps/web/src/state/logs/hooks.ts | 4 +- apps/web/src/state/logs/slice.ts | 6 +- apps/web/src/state/logs/updater.ts | 4 +- apps/web/src/state/migrations.test.ts | 6 +- apps/web/src/state/migrations/1.test.ts | 10 +- apps/web/src/state/migrations/11.test.ts | 4 +- apps/web/src/state/migrations/12.test.ts | 2 +- apps/web/src/state/migrations/2.test.ts | 6 +- apps/web/src/state/migrations/3.test.ts | 14 +- apps/web/src/state/migrations/3.ts | 4 +- apps/web/src/state/migrations/4.test.ts | 2 +- apps/web/src/state/migrations/5.test.ts | 10 +- apps/web/src/state/migrations/6.test.ts | 4 +- apps/web/src/state/migrations/7.test.ts | 10 +- apps/web/src/state/migrations/8.test.ts | 6 +- apps/web/src/state/migrations/9.test.ts | 2 +- apps/web/src/state/migrations/9.ts | 2 +- apps/web/src/state/mint/hooks.tsx | 16 +- apps/web/src/state/mint/reducer.ts | 2 +- apps/web/src/state/mint/v3/actions.ts | 2 +- apps/web/src/state/mint/v3/hooks.tsx | 63 +- apps/web/src/state/mint/v3/reducer.ts | 2 +- apps/web/src/state/mint/v3/utils.test.ts | 12 +- apps/web/src/state/mint/v3/utils.ts | 4 +- apps/web/src/state/reducer.ts | 4 +- apps/web/src/state/reducerTypeTest.ts | 2 + apps/web/src/state/routing/gas.ts | 4 +- apps/web/src/state/routing/slice.ts | 2 +- apps/web/src/state/routing/types.ts | 4 +- apps/web/src/state/routing/usePreviewTrade.ts | 4 +- .../state/routing/useRoutingAPITrade.test.ts | 8 +- .../src/state/routing/useRoutingAPITrade.ts | 8 +- apps/web/src/state/routing/utils.ts | 20 +- apps/web/src/state/send/SendContext.tsx | 39 +- apps/web/src/state/send/hooks.tsx | 18 +- apps/web/src/state/signatures/hooks.ts | 8 +- apps/web/src/state/stake/hooks.tsx | 24 +- apps/web/src/state/swap/SwapContext.test.tsx | 34 +- apps/web/src/state/swap/SwapContext.tsx | 29 +- apps/web/src/state/swap/hooks.test.ts | 56 +- apps/web/src/state/swap/hooks.tsx | 72 +- apps/web/src/state/swap/types.ts | 12 +- .../web/src/state/transactions/hooks.test.tsx | 2 +- apps/web/src/state/transactions/hooks.tsx | 17 +- .../src/state/transactions/reducer.test.ts | 30 +- apps/web/src/state/transactions/reducer.ts | 10 +- apps/web/src/state/user/hooks.tsx | 31 +- apps/web/src/state/user/reducer.test.ts | 16 +- apps/web/src/state/user/utils.ts | 2 +- apps/web/src/state/wallets/hooks.tsx | 2 +- apps/web/src/test-utils/bundle-size-test.ts | 2 +- apps/web/src/test-utils/constants.ts | 12 +- apps/web/src/test-utils/matchers.test.tsx | 2 +- apps/web/src/test-utils/pools/fixtures.ts | 2 +- apps/web/src/test-utils/render.tsx | 2 +- apps/web/src/test-utils/tokens/mocks.ts | 4 +- apps/web/src/theme/colors.ts | 8 + .../web/src/theme/components/FadePresence.tsx | 5 +- apps/web/src/theme/components/ThemeToggle.tsx | 8 +- apps/web/src/theme/components/index.tsx | 14 +- apps/web/src/theme/components/text.tsx | 2 +- apps/web/src/theme/index.tsx | 4 +- apps/web/src/theme/styles.ts | 2 +- .../tracing/SwapEventTimestampTracker.test.ts | 2 +- apps/web/src/tracing/amplitude.ts | 22 + apps/web/src/tracing/errors.test.ts | 12 +- apps/web/src/tracing/index.ts | 64 +- apps/web/src/tracing/sentry.ts | 34 + apps/web/src/tracing/swapFlowLoggers.test.ts | 27 +- apps/web/src/tracing/swapFlowLoggers.ts | 27 +- apps/web/src/tracing/trace.test.ts | 6 +- apps/web/src/tracing/utils.ts | 2 +- .../src/utils/addressesAreEquivalent.test.ts | 9 +- apps/web/src/utils/anonymizeLink.test.ts | 6 +- apps/web/src/utils/approveAmountCalldata.ts | 2 +- apps/web/src/utils/chains.tsx | 9 +- .../src/utils/computeFiatValuePriceImpact.tsx | 2 +- apps/web/src/utils/computeSurroundingTicks.ts | 6 +- .../src/utils/computeUniCirculation.test.ts | 6 +- apps/web/src/utils/computeUniCirculation.ts | 18 +- apps/web/src/utils/env.test.ts | 3 +- apps/web/src/utils/env.ts | 18 +- apps/web/src/utils/fiatCurrency.ts | 2 +- apps/web/src/utils/formatCurrencyAmount.ts | 2 +- apps/web/src/utils/formatNumbers.test.ts | 4 +- apps/web/src/utils/formatNumbers.ts | 38 +- apps/web/src/utils/getExplorerLink.test.ts | 8 +- apps/web/src/utils/getInitialLogoURL.ts | 2 +- ...pportedChainIdsFromWalletConnectSession.ts | 2 +- apps/web/src/utils/loggingFormatters.ts | 10 +- apps/web/src/utils/maxAmountSpend.ts | 2 +- apps/web/src/utils/prices.test.ts | 20 +- apps/web/src/utils/prices.ts | 12 +- apps/web/src/utils/signing.test.ts | 2 +- apps/web/src/utils/signing.ts | 4 +- apps/web/src/utils/splitHiddenTokens.tsx | 2 +- .../utils/swapErrorToUserReadableMessage.tsx | 4 +- apps/web/src/utils/tradeMeaningFullyDiffer.ts | 2 +- apps/web/src/utils/transfer.ts | 2 +- .../transformSwapRouteToGetQuoteResult.ts | 2 +- apps/web/src/utils/urlChecks.ts | 2 +- config/jest-presets/jest/globals.js | 9 +- dangerfile.ts | 159 +- i18next-parser.config.js | 5 +- package.json | 21 +- packages/eslint-config/.depcheckrc | 1 - .../__snapshots__/preset.test.ts.snap | 2253 +----------- packages/eslint-config/base.js | 13 - packages/eslint-config/native.js | 40 +- packages/eslint-config/package.json | 5 +- packages/eslint-config/restrictedImports.js | 1 + packages/ui/.depcheckrc | 1 + packages/ui/.eslintignore | 2 + packages/ui/.prettierignore | 4 - packages/ui/jest.config.js | 11 + packages/ui/package.json | 2 + packages/ui/src/animations/AnimateInOrder.tsx | 7 +- packages/ui/src/animations/Jiggly.tsx | 19 +- .../assets/backgrounds/for-connecting-v2.svg | 1857 ++++++++++ .../src/assets/icons/arrow-right-to-line.svg | 3 + .../ui/src/assets/icons/block-explorer.svg | 3 + packages/ui/src/assets/icons/copy-sheets.svg | 4 +- packages/ui/src/assets/icons/edit.svg | 4 +- packages/ui/src/assets/icons/trash-filled.svg | 2 +- packages/ui/src/assets/index.ts | 1 + packages/ui/src/assets/misc/dot-grid.png | Bin 0 -> 18549 bytes .../AnimatedFlashList/AnimatedFlashList.tsx | 27 +- .../custom-qr-code-generator/index.d.ts | 3 - .../QRCode}/custom-qr-code-generator/index.js | 0 .../custom-qr-code-generator/src/genMatrix.js | 10 + .../custom-qr-code-generator/src/index.jsx | 48 +- .../src/transformMatrixIntoCirclePath.js | 20 +- .../src/components/QRCode/index.tsx} | 107 +- .../ui/src/components/Unicon/index.native.tsx | 13 +- .../ui/src/components/Unicon/index.web.tsx | 16 +- packages/ui/src/components/Unicon/utils.ts | 8 +- .../UniversalImage/UniversalImage.tsx | 8 +- .../internal/FastImageWrapper.native.tsx | 9 +- .../internal/FastImageWrapper.tsx | 5 +- .../internal/PlainImage.native.tsx | 9 +- .../UniversalImage/internal/PlainImage.tsx | 13 +- .../components/UniversalImage/utils.test.tsx | 9 +- .../ui/src/components/UniversalImage/utils.ts | 9 +- packages/ui/src/components/button/Button.tsx | 10 +- .../ui/src/components/factories/animated.tsx | 2 +- .../src/components/factories/createIcon.tsx | 12 +- packages/ui/src/components/icons/AaveIcon.tsx | 11 +- .../ui/src/components/icons/AddButton.tsx | 20 +- .../ui/src/components/icons/AlertCircle.tsx | 16 +- .../ui/src/components/icons/AlertTriangle.tsx | 2 +- .../src/components/icons/AngleRightSmall.tsx | 2 +- .../src/components/icons/AnglesMaximize.tsx | 2 +- .../src/components/icons/AnglesMinimize.tsx | 2 +- packages/ui/src/components/icons/Approve.tsx | 6 +- .../ui/src/components/icons/ApproveFilled.tsx | 2 +- .../ui/src/components/icons/ArrowChange.tsx | 2 +- .../ui/src/components/icons/ArrowDown.tsx | 18 +- .../src/components/icons/ArrowDownCircle.tsx | 4 +- .../icons/ArrowDownCircleFilled.tsx | 2 +- .../components/icons/ArrowDownInCircle.tsx | 8 +- .../src/components/icons/ArrowRightToLine.tsx | 16 + .../components/icons/ArrowRightwardsDown.tsx | 6 +- .../components/icons/ArrowTurnDownRight.tsx | 2 +- .../ui/src/components/icons/ArrowUpDown.tsx | 2 +- .../src/components/icons/ArrowUpInCircle.tsx | 8 +- .../ui/src/components/icons/BackArrow.tsx | 6 +- packages/ui/src/components/icons/Bell.tsx | 2 +- .../ui/src/components/icons/BlockExplorer.tsx | 17 + packages/ui/src/components/icons/Book.tsx | 2 +- packages/ui/src/components/icons/BookOpen.tsx | 2 +- packages/ui/src/components/icons/Buy.tsx | 2 +- packages/ui/src/components/icons/Camera.tsx | 2 +- .../ui/src/components/icons/CameraScan.tsx | 4 +- .../ui/src/components/icons/CameraScanAlt.tsx | 4 +- packages/ui/src/components/icons/Chart.tsx | 2 +- packages/ui/src/components/icons/ChartBar.tsx | 2 +- packages/ui/src/components/icons/ChartPie.tsx | 2 +- packages/ui/src/components/icons/Check.tsx | 4 +- .../ui/src/components/icons/CheckCircle.tsx | 6 +- .../ui/src/components/icons/Checkmark.tsx | 7 +- .../src/components/icons/CheckmarkCircle.tsx | 2 +- packages/ui/src/components/icons/Chevron.tsx | 10 +- .../ui/src/components/icons/ChevronLeft.tsx | 8 +- .../ui/src/components/icons/CircleSpinner.tsx | 9 +- .../ui/src/components/icons/Clipboard.tsx | 2 +- .../src/components/icons/ClipboardPaste.tsx | 2 +- packages/ui/src/components/icons/Clock.tsx | 2 +- packages/ui/src/components/icons/Cloud.tsx | 2 +- packages/ui/src/components/icons/Code.tsx | 2 +- packages/ui/src/components/icons/Coffee.tsx | 30 +- packages/ui/src/components/icons/Coin.tsx | 2 +- .../ui/src/components/icons/CoinConvert.tsx | 2 +- packages/ui/src/components/icons/Coins.tsx | 2 +- .../components/icons/ContractInteraction.tsx | 2 +- packages/ui/src/components/icons/Contrast.tsx | 2 +- packages/ui/src/components/icons/CopyAlt.tsx | 2 +- .../ui/src/components/icons/CopyFilled.tsx | 4 +- .../ui/src/components/icons/CopySheets.tsx | 4 +- packages/ui/src/components/icons/DaiIcon.tsx | 4 +- packages/ui/src/components/icons/Dash.tsx | 2 +- .../components/icons/DiamondExclamation.tsx | 2 +- .../ui/src/components/icons/DocumentList.tsx | 2 +- packages/ui/src/components/icons/Dollar.tsx | 2 +- .../ui/src/components/icons/DoubleChevron.tsx | 18 +- packages/ui/src/components/icons/Edit.tsx | 4 +- packages/ui/src/components/icons/Ellipsis.tsx | 8 +- .../ui/src/components/icons/EmptySpinner.tsx | 2 +- .../src/components/icons/EmptyStateCoin.tsx | 4 +- .../components/icons/EmptyStatePicture.tsx | 16 +- .../src/components/icons/EmptyStateTokens.tsx | 41 +- .../icons/EmptyStateTransaction.tsx | 14 +- .../ui/src/components/icons/ErrorLoading.tsx | 18 +- packages/ui/src/components/icons/EthIcon.tsx | 13 +- .../ui/src/components/icons/EthLightIcon.tsx | 24 +- .../ui/src/components/icons/ExternalLink.tsx | 2 +- packages/ui/src/components/icons/Eye.tsx | 2 +- packages/ui/src/components/icons/EyeOff.tsx | 2 +- packages/ui/src/components/icons/Eyeball.tsx | 11 +- packages/ui/src/components/icons/Faceid.tsx | 2 +- .../ui/src/components/icons/FaceidThin.tsx | 2 +- packages/ui/src/components/icons/Feedback.tsx | 2 +- .../ui/src/components/icons/FileListCheck.tsx | 4 +- .../ui/src/components/icons/FileListLock.tsx | 2 +- .../ui/src/components/icons/Fingerprint.tsx | 2 +- .../ui/src/components/icons/Flashbots.tsx | 2 +- packages/ui/src/components/icons/Gallery.tsx | 2 +- packages/ui/src/components/icons/Gas.tsx | 4 +- packages/ui/src/components/icons/Global.tsx | 2 +- packages/ui/src/components/icons/Globe.tsx | 4 +- .../ui/src/components/icons/GlobeFilled.tsx | 2 +- .../ui/src/components/icons/GoogleDrive.tsx | 2 +- .../ui/src/components/icons/GraduationCap.tsx | 2 +- .../ui/src/components/icons/Hamburger.tsx | 26 +- packages/ui/src/components/icons/Heart.tsx | 2 +- packages/ui/src/components/icons/Help.tsx | 2 +- .../ui/src/components/icons/HelpCenter.tsx | 2 +- packages/ui/src/components/icons/Home.tsx | 4 +- .../ui/src/components/icons/InfoCircle.tsx | 2 +- .../src/components/icons/InfoCircleFilled.tsx | 2 +- .../src/components/icons/InformationIcon.tsx | 2 +- packages/ui/src/components/icons/Key.tsx | 2 +- packages/ui/src/components/icons/KeyIcon.tsx | 6 +- packages/ui/src/components/icons/Language.tsx | 4 +- packages/ui/src/components/icons/Laptop.tsx | 2 +- .../ui/src/components/icons/LeftArrow.tsx | 4 +- .../ui/src/components/icons/Lightning.tsx | 2 +- .../ui/src/components/icons/LikeSquare.tsx | 2 +- .../ui/src/components/icons/LineChartDots.tsx | 2 +- .../components/icons/LinkBrokenHorizontal.tsx | 4 +- .../components/icons/LinkHorizontalAlt.tsx | 2 +- packages/ui/src/components/icons/List.tsx | 50 +- .../components/icons/LoadingSpinnerInner.tsx | 4 +- .../components/icons/LoadingSpinnerOuter.tsx | 4 +- packages/ui/src/components/icons/Lock.tsx | 2 +- packages/ui/src/components/icons/Map.tsx | 20 +- packages/ui/src/components/icons/Masonry.tsx | 26 +- .../src/components/icons/MessageQuestion.tsx | 2 +- .../ui/src/components/icons/MessageStar.tsx | 4 +- packages/ui/src/components/icons/Mobile.tsx | 2 +- .../ui/src/components/icons/MoneyBillSend.tsx | 2 +- packages/ui/src/components/icons/Moon.tsx | 2 +- packages/ui/src/components/icons/More.tsx | 2 +- packages/ui/src/components/icons/NoNfts.tsx | 6 +- packages/ui/src/components/icons/NoPools.tsx | 6 +- packages/ui/src/components/icons/NoTokens.tsx | 6 +- .../src/components/icons/NoTransactions.tsx | 8 +- .../ui/src/components/icons/PaperStack.tsx | 4 +- .../ui/src/components/icons/PapersText.tsx | 2 +- packages/ui/src/components/icons/Paste.tsx | 6 +- packages/ui/src/components/icons/Pen.tsx | 4 +- packages/ui/src/components/icons/PenLine.tsx | 2 +- packages/ui/src/components/icons/Pencil.tsx | 2 +- .../ui/src/components/icons/PencilBox.tsx | 10 +- .../src/components/icons/PencilDetailed.tsx | 2 +- packages/ui/src/components/icons/Person.tsx | 2 +- packages/ui/src/components/icons/Photo.tsx | 2 +- packages/ui/src/components/icons/Pin.tsx | 6 +- .../ui/src/components/icons/PlusCircle.tsx | 2 +- .../ui/src/components/icons/PlusSquare.tsx | 8 +- packages/ui/src/components/icons/Power.tsx | 2 +- packages/ui/src/components/icons/Profile.tsx | 6 +- .../ui/src/components/icons/ProfileFilled.tsx | 2 +- packages/ui/src/components/icons/QrCode.tsx | 20 +- .../src/components/icons/QuestionInCircle.tsx | 12 +- .../icons/QuestionInCircleFilled.tsx | 2 +- packages/ui/src/components/icons/Quotes.tsx | 2 +- .../ui/src/components/icons/ReceiptText.tsx | 4 +- packages/ui/src/components/icons/Receive.tsx | 2 +- .../ui/src/components/icons/ReceiveAlt.tsx | 4 +- .../ui/src/components/icons/ReceiveArrow.tsx | 2 +- .../ui/src/components/icons/ReceiveDots.tsx | 2 +- .../ui/src/components/icons/RightArrow.tsx | 4 +- .../src/components/icons/RotatableChevron.tsx | 8 +- packages/ui/src/components/icons/Scan.tsx | 8 +- packages/ui/src/components/icons/ScanHome.tsx | 4 +- packages/ui/src/components/icons/ScanQr.tsx | 4 +- packages/ui/src/components/icons/ScanQrWc.tsx | 4 +- .../ui/src/components/icons/ScanReceive.tsx | 4 +- packages/ui/src/components/icons/Search.tsx | 6 +- .../ui/src/components/icons/SearchFocused.tsx | 6 +- .../src/components/icons/SeedPhraseIcon.tsx | 69 +- .../ui/src/components/icons/SelectIcon.tsx | 2 +- .../ui/src/components/icons/SendAction.tsx | 2 +- .../components/icons/SendRoundedAirplane.tsx | 2 +- packages/ui/src/components/icons/Settings.tsx | 2 +- packages/ui/src/components/icons/Share.tsx | 4 +- .../ui/src/components/icons/ShieldCheck.tsx | 2 +- .../src/components/icons/ShieldQuestion.tsx | 2 +- .../ui/src/components/icons/SlashCircle.tsx | 8 +- packages/ui/src/components/icons/Sort.tsx | 6 +- packages/ui/src/components/icons/Sparkle.tsx | 2 +- packages/ui/src/components/icons/Stacked.tsx | 4 +- packages/ui/src/components/icons/Star.tsx | 2 +- .../ui/src/components/icons/StarGroup.tsx | 23 +- .../src/components/icons/StickyNoteSquare.tsx | 2 +- .../components/icons/StickyNoteTextSquare.tsx | 2 +- packages/ui/src/components/icons/Sun.tsx | 2 +- .../src/components/icons/SwapActionButton.tsx | 6 +- .../ui/src/components/icons/SwapArrow.tsx | 6 +- packages/ui/src/components/icons/Testnets.tsx | 2 +- packages/ui/src/components/icons/TextEdit.tsx | 2 +- packages/ui/src/components/icons/ThumbsUp.tsx | 2 +- packages/ui/src/components/icons/Ticket.tsx | 2 +- packages/ui/src/components/icons/TimePast.tsx | 2 +- .../ui/src/components/icons/ToggleOnAlt.tsx | 2 +- packages/ui/src/components/icons/Tooltip.tsx | 10 +- packages/ui/src/components/icons/Trash.tsx | 4 +- .../ui/src/components/icons/TrashFilled.tsx | 3 +- .../ui/src/components/icons/TrendDown.tsx | 2 +- packages/ui/src/components/icons/TrendUp.tsx | 2 +- .../ui/src/components/icons/TripleDots.tsx | 14 +- .../ui/src/components/icons/UniswapLogo.tsx | 4 +- packages/ui/src/components/icons/UniswapX.tsx | 9 +- .../ui/src/components/icons/UserSquare.tsx | 2 +- packages/ui/src/components/icons/Verified.tsx | 4 +- packages/ui/src/components/icons/Wallet.tsx | 4 +- .../ui/src/components/icons/WalletFilled.tsx | 2 +- .../ui/src/components/icons/Walletconnect.tsx | 6 +- packages/ui/src/components/icons/Wifi.tsx | 2 +- .../ui/src/components/icons/WifiSlash.tsx | 2 +- packages/ui/src/components/icons/X.tsx | 2 +- packages/ui/src/components/icons/XOctagon.tsx | 8 +- packages/ui/src/components/icons/XTwitter.tsx | 2 +- packages/ui/src/components/icons/exported.ts | 2 + packages/ui/src/components/input/CheckBox.tsx | 7 +- packages/ui/src/components/layout/Flex.tsx | 9 +- packages/ui/src/components/layout/Inset.tsx | 5 +- .../src/components/logos/ArbiscanLogoDark.tsx | 7 +- .../components/logos/ArbiscanLogoLight.tsx | 7 +- packages/ui/src/components/logos/Ethereum.tsx | 2 +- .../ui/src/components/logos/EthereumLogo.tsx | 28 +- .../components/logos/EtherscanLogoDark.tsx | 2 +- .../components/logos/EtherscanLogoLight.tsx | 2 +- packages/ui/src/components/logos/Moonpay.tsx | 4 +- .../components/logos/OpEtherscanLogoDark.tsx | 10 +- .../components/logos/OpEtherscanLogoLight.tsx | 10 +- .../ui/src/components/logos/PolygonPurple.tsx | 2 +- .../components/logos/PolygonscanLogoDark.tsx | 9 +- .../components/logos/PolygonscanLogoLight.tsx | 9 +- packages/ui/src/components/logos/exported.ts | 12 + .../ui/src/components/menu/ContextMenu.tsx | 107 + .../ui/src/components/menu/MenuContent.tsx | 51 + packages/ui/src/components/menu/types.ts | 13 + .../modal/AdaptiveWebModalSheet.tsx | 83 + .../ui/src/components/text/GradientText.tsx | 4 +- .../text/HiddenFromScreenReaders.native.tsx | 10 +- .../text/HiddenFromScreenReaders.tsx | 5 +- packages/ui/src/components/text/Text.tsx | 14 +- .../components/text/UniswapXText.native.tsx | 3 +- .../components/touchable/TouchableArea.tsx | 30 +- .../src/hooks/useDeviceDimensions.native.ts | 19 + packages/ui/src/hooks/useDeviceDimensions.ts | 14 +- packages/ui/src/index.ts | 6 + packages/ui/src/loading/Loader.tsx | 23 +- packages/ui/src/loading/Shine.native.tsx | 9 +- packages/ui/src/loading/Shine.web.tsx | 3 +- packages/ui/src/loading/Skeleton.native.tsx | 19 +- .../ui/src/loading/SpinningLoader.native.tsx | 2 +- packages/ui/src/loading/SpinningLoader.tsx | 11 +- packages/ui/src/loading/TokenLoader.tsx | 28 +- packages/ui/src/loading/TransactionLoader.tsx | 23 +- packages/ui/src/loading/WalletLoader.tsx | 3 +- packages/ui/src/scripts/componentize-icons.ts | 54 +- packages/ui/src/theme/animations.ts | 34 + packages/ui/src/theme/color/colors.ts | 4 + packages/ui/src/theme/color/utils.ts | 4 +- packages/ui/src/theme/fonts.ts | 4 +- packages/ui/src/theme/themes.ts | 30 + packages/ui/src/theme/tokens.ts | 2 +- .../ui/src/utils/colors/getExtractedColors.ts | 2 +- packages/ui/src/utils/colors/index.ts | 36 +- .../utils/haptics/HapticFeedback.native.ts | 2 +- packages/uniswap/codegen.ts | 6 +- packages/uniswap/package.json | 17 +- packages/uniswap/src/assets/chainLogos.tsx | 25 +- .../src/components/BaseCard/BaseCard.test.tsx | 49 +- .../src/components/BaseCard/BaseCard.tsx | 46 +- .../CurrencyLogo/NetworkLogo.test.tsx | 5 +- .../components/CurrencyLogo/NetworkLogo.tsx | 13 +- .../CurrencyLogo/SplitLogo.test.tsx | 25 +- .../src/components/CurrencyLogo/SplitLogo.tsx | 13 +- .../CurrencyLogo/TokenLogo.test.tsx | 39 +- .../src/components/CurrencyLogo/TokenLogo.tsx | 20 +- .../__snapshots__/SplitLogo.test.tsx.snap | 14 +- .../TokenSelector/SuggestedToken.tsx | 9 +- .../TokenSelector/TokenOptionItem.tsx | 61 +- .../TokenSectionBaseList.native.tsx | 8 +- .../TokenSelector/TokenSectionBaseList.tsx | 4 +- .../TokenSectionBaseList.web.tsx | 36 +- .../TokenSelector/TokenSelector.tsx | 163 +- .../TokenSelectorEmptySearchList.tsx | 52 + .../TokenSelector/TokenSelectorList.tsx | 83 +- .../TokenSelectorSearchResultsList.tsx | 102 + .../TokenSelector/TokenSelectorSendList.tsx | 128 + .../TokenSelectorSwapInputList.tsx | 119 + .../TokenSelectorSwapOutputList.tsx | 80 +- .../src/components/TokenSelector/filter.ts | 12 +- .../renderSuggestedTokenItem.tsx | 8 +- .../suggestedTokensKeyExtractor.tsx | 2 +- .../src/components/TokenSelector/types.ts | 97 + .../src/components/TokenSelector/utils.ts | 19 +- .../ViewGestureHandler/index.native.tsx | 8 + .../components/ViewGestureHandler/index.tsx | 8 + .../ViewGestureHandler/index.web.tsx | 7 + .../components/buttons/PasteButton.test.tsx | 6 +- .../src/components/buttons/PasteButton.tsx | 5 +- .../__snapshots__/PasteButton.test.tsx.snap | 8 +- .../dropdowns/ActionSheetDropdown.test.tsx | 2 +- .../dropdowns/ActionSheetDropdown.tsx | 38 +- .../src/components/icons/WarningIcon.tsx | 2 +- .../src/components/input/TextInput.tsx | 2 +- .../src/components/misc/ActionCard.tsx | 61 + .../uniswap/src/components/misc/Scrollbar.tsx | 15 +- .../components/modals/ActionSheetModal.tsx | 13 +- .../components/modals/BottomSheetContext.tsx | 2 +- .../modals/BottomSheetModal.native.tsx | 49 +- .../components/modals/BottomSheetModal.tsx | 2 +- .../modals/BottomSheetModal.web.tsx | 28 +- .../components/modals/HandleBar.native.tsx | 9 +- .../components/modals/ScrollLock.native.tsx | 4 + .../src/components/modals/ScrollLock.tsx | 52 + .../src/components/modals/hooks.native.ts} | 0 .../uniswap/src/components/modals/hooks.ts | 5 + .../src/components/modals/hooks.web.ts | 3 + .../src/components/network/NetworkFilter.tsx | 33 +- .../src/components/network/NetworkLogos.tsx | 8 +- .../src/components/network/NetworkOption.tsx | 3 +- .../components/network/NetworkPill.test.tsx | 4 +- .../src/components/network/NetworkPill.tsx | 12 +- .../__snapshots__/NetworkPill.test.tsx.snap | 0 .../uniswap/src/components/network/hooks.tsx | 6 +- .../uniswap/src/components/pill/Pill.test.tsx | 8 +- packages/uniswap/src/components/pill/Pill.tsx | 3 +- .../components/text/LearnMoreLink.test.tsx | 8 +- .../src/components/text/LearnMoreLink.tsx | 6 +- .../__snapshots__/LearnMoreLink.test.tsx.snap | 0 packages/uniswap/src/config.ts | 41 +- .../src/constants/addresses.ts | 7 +- packages/uniswap/src/constants/chains.ts | 109 +- .../{wallet => uniswap}/src/constants/misc.ts | 2 - packages/uniswap/src/constants/tokens.ts | 125 +- packages/uniswap/src/constants/urls.ts | 9 +- packages/uniswap/src/data/cache.ts | 10 +- packages/uniswap/src/data/constants.ts | 17 +- .../graphql/uniswap-data-api/queries.graphql | 7 + .../graphql/uniswap-data-api/schema.graphql | 47 +- .../uniswap-data-api/web/activity.graphql | 57 +- .../web/nft/NftBalance.graphql | 3 + .../uniswap-data-api/web/portfolios.graphql | 25 + packages/uniswap/src/data/rest.ts | 26 +- .../src/entities/assets.ts | 0 .../src/extension/useIsChromeWindowFocused.ts | 50 + .../uniswap/src/features/chains/utils.test.ts | 126 +- packages/uniswap/src/features/chains/utils.ts | 152 + .../src/features/dataApi/searchTokens.ts | 16 +- .../src/features/dataApi/tokenProjects.ts | 10 +- .../src/features/dataApi/topTokens.ts | 4 +- .../src/features/dataApi/utils.ts | 34 +- .../src/features/fiatOnRamp/FORQuoteItem.tsx | 95 +- .../FiatOnRampConnectingView.native.tsx | 25 +- .../fiatOnRamp/FiatOnRampConnectingView.tsx | 48 + .../fiatOnRamp/FiatOnRampCountryPicker.tsx | 6 +- .../features/fiatOnRamp/SelectTokenButton.tsx | 27 +- .../uniswap/src/features/fiatOnRamp/api.ts | 30 +- .../src/features/fiatOnRamp/constants.ts | 17 + .../uniswap/src/features/fiatOnRamp/types.ts | 9 +- .../fiatOnRamp/useCexTransferProviders.ts | 14 + .../src/features/fiatOnRamp/utils.test.ts | 155 +- .../uniswap/src/features/fiatOnRamp/utils.ts | 96 +- .../uniswap/src/features/gating/configs.ts | 87 +- .../src/features/gating/experiments.ts | 11 +- packages/uniswap/src/features/gating/flags.ts | 20 +- packages/uniswap/src/features/gating/hooks.ts | 40 +- .../customPersistedOverrides.native.ts | 16 +- .../src/features/language/formatter.ts | 8 + .../src/features/search/SearchContext.tsx | 0 .../src/features/search/SearchResult.ts | 9 + .../src/features/search/SearchTextInput.tsx | 39 +- .../uniswap/src/features/telemetry/Trace.tsx | 3 +- .../features/telemetry/constants/extension.ts | 9 +- .../features/telemetry/constants/mobile.ts | 1 - .../src/features/telemetry/constants/trace.ts | 35 +- .../features/telemetry/constants/wallet.ts | 7 +- .../src/features/telemetry/send.native.ts | 13 +- .../uniswap/src/features/telemetry/send.ts | 5 +- .../src/features/telemetry/send.web.ts | 5 +- .../uniswap/src/features/telemetry/types.ts | 48 +- .../uniswap/src/features/telemetry/user.ts | 4 +- .../src/features/tokens/NativeCurrency.ts | 11 +- .../src/features/tokens/TokenWarningModal.tsx | 30 +- .../src/features/tokens/safetyHooks.ts | 14 + .../src/features/tokens/utils.ts | 5 +- .../transactions/transactionState/types.ts | 2 +- .../features/transactions/transfer/types.ts | 4 + packages/uniswap/src/features/unitags/api.ts | 18 +- .../uniswap/src/features/unitags/context.tsx | 4 +- .../src/i18n/locales/source/en-US.json | 79 +- packages/uniswap/src/react-native-dotenv.d.ts | 3 - packages/uniswap/src/test/fixtures/index.ts | 1 + packages/uniswap/src/test/fixtures/testIDs.ts | 68 + .../src/test/fixtures/wallet/currencies.ts | 18 +- .../uniswap/src/test/fixtures/wallet/index.ts | 1 + packages/uniswap/src/test/render.tsx | 7 +- .../{wallet => uniswap}/src/test/shared.ts | 0 packages/uniswap/src/test/test-utils.ts | 2 - .../src/test/utils/factory.ts | 38 +- packages/uniswap/src/test/utils/index.ts | 1 + packages/uniswap/src/types/chains.ts | 3 +- .../uniswap/src/types/screens/extension.ts | 40 +- .../src/utils/addresses.test.ts | 2 +- .../src/utils/addresses.ts | 11 +- .../src/utils/clipboard.native.ts | 5 +- .../src/utils/clipboard.ts | 0 .../src/utils/clipboard.web.ts | 2 +- .../src/utils/colors.test.ts | 2 +- .../{wallet => uniswap}/src/utils/colors.tsx | 17 +- packages/uniswap/src/utils/currency.ts | 15 + packages/uniswap/src/utils/currencyId.ts | 99 + packages/uniswap/src/utils/env/index.ts | 4 +- packages/uniswap/src/utils/link.native.ts | 9 + packages/uniswap/src/utils/link.ts | 9 + packages/uniswap/src/utils/link.web.ts | 8 + packages/uniswap/src/utils/linking.ts | 60 + .../src/utils/useKeyboardLayout.native.ts | 4 +- .../src/utils/usePlatformBasedFetchPolicy.ts | 19 + .../src/utils/usePlatformBasedValue.native.ts | 5 + .../src/utils/usePlatformBasedValue.ts | 33 + packages/utilities/package.json | 7 +- .../utilities/src/addresses/addresses.test.ts | 24 +- .../utilities/src/apollo/SubscriptionLink.ts | 2 +- .../utilities/src/apollo/splitSubscription.ts | 7 +- .../utilities/src/contracts/getContract.ts | 2 +- .../src/device/locales.native.test.ts | 4 +- .../utilities/src/device/locales.native.ts | 6 +- .../utilities/src/device/locales.web.test.ts | 8 +- packages/utilities/src/device/locales.web.ts | 6 +- .../utilities/src/environment/constants.ts | 3 + .../src/environment}/index.native.ts | 14 + packages/utilities/src/environment/index.ts | 55 +- .../src/format/convertScientificNotation.ts | 7 +- .../utilities/src/format/localeBased.test.ts | 302 +- packages/utilities/src/format/localeBased.ts | 13 +- .../src/format/truncateToMaxDecimals.test.ts | 23 +- packages/utilities/src/format/urls.test.ts | 4 +- packages/utilities/src/logger/Datadog.ts | 75 + .../utilities/src/logger/Sentry.native.ts | 7 +- packages/utilities/src/logger/Sentry.ts | 9 +- packages/utilities/src/logger/Sentry.web.ts | 7 +- packages/utilities/src/logger/console.ts | 3 +- packages/utilities/src/logger/logger.ts | 50 +- packages/utilities/src/logger/mocks.ts | 7 + packages/utilities/src/logger/types.ts | 7 + packages/utilities/src/platform/index.ts | 10 +- packages/utilities/src/primitives/array.ts | 11 +- .../utilities/src/primitives/objects.test.ts | 10 +- .../utilities/src/primitives/string.test.ts | 7 +- packages/utilities/src/primitives/string.ts | 4 +- packages/utilities/src/react/hook.test.tsx | 25 +- packages/utilities/src/react/hooks.ts | 40 +- .../telemetry/analytics/analytics.native.ts | 14 +- .../src/telemetry/analytics/analytics.ts | 4 +- .../src/telemetry/analytics/analytics.web.ts | 20 +- .../src/telemetry/analytics/logging.ts | 2 +- .../trace/AnalyticsNavigationContext.tsx | 6 +- .../utilities/src/telemetry/trace/Trace.tsx | 23 +- .../utilities/src/telemetry/trace/utils.ts | 17 +- packages/utilities/src/time/date.ts | 6 +- packages/utilities/src/time/timing.test.ts | 2 +- packages/utilities/src/time/timing.ts | 20 +- packages/wallet/jest-setup.js | 4 +- packages/wallet/package.json | 10 +- .../CurrencyLogo/CurrencyLogo.test.tsx | 12 +- .../CurrencyLogo/LogoWithTxStatus.test.tsx | 53 +- .../CurrencyLogo/LogoWithTxStatus.tsx | 45 +- .../LogoWithTxStatus.test.tsx.snap | 2 + .../components/CurrencyLogo/index.test.tsx | 6 +- .../DevelopmentOnly/UniconSampleSheet.tsx | 6 +- .../ErrorBoundary/ErrorBoundary.tsx | 12 +- .../components/QRCodeScanner/WalletQRCode.tsx | 33 +- .../custom-qr-code-generator/src/genMatrix.js | 14 - .../QRCodeScanner/useQRColorProps.ts | 53 + .../RecipientSearch/RecipientList.tsx | 27 +- .../ViewOnlyRecipientModal.tsx | 8 +- .../components/RecipientSearch/filter.test.ts | 16 +- .../src/components/RecipientSearch/filter.ts | 13 +- .../src/components/RecipientSearch/hooks.ts | 46 +- .../components/RecipientSearch/utils.test.ts | 6 +- .../src/components/RecipientSearch/utils.ts | 2 +- .../TokenSelector/SelectTokenButton.tsx | 31 +- .../TokenSelectorEmptySearchList.tsx | 143 - .../TokenSelectorSearchResultsList.tsx | 176 - .../TokenSelector/TokenSelectorSendList.tsx | 117 - .../TokenSelectorSwapInputList.tsx | 104 - .../components/TokenSelector/filter.test.ts | 31 +- .../TokenSelector/flowToModalName.tsx | 2 +- .../components/TokenSelector/hooks.test.ts | 74 +- .../TokenSelector/{hooks.ts => hooks.tsx} | 291 +- .../src/components/TokenSelector/types.ts | 27 - .../WalletConnect/DappIconPlaceholder.tsx | 16 +- .../WalletPreviewCard.test.tsx | 4 +- .../WalletPreviewCard/WalletPreviewCard.tsx | 14 +- .../WalletPreviewCard.test.tsx.snap | 13 +- .../accounts/AccountDetails.test.tsx | 4 +- .../components/accounts/AccountDetails.tsx | 11 +- .../src/components/accounts/AccountIcon.tsx | 16 +- .../components/accounts/AddressDisplay.tsx | 57 +- .../accounts/AnimatedUnitagDisplayName.tsx | 40 +- .../components/buttons/PlusMinusButton.tsx | 3 +- .../wallet/src/components/buttons/Switch.tsx | 6 +- .../buttons/TransferArrowButton.tsx | 3 +- .../src/components/gating/GatingOverrides.tsx | 12 +- .../wallet/src/components/icons/Arrow.tsx | 8 +- .../src/components/icons/PlusCircle.tsx | 3 +- .../src/components/input/AmountInput.test.tsx | 14 +- .../src/components/input/AmountInput.tsx | 28 +- .../src/components/input/MaxAmountButton.tsx | 29 +- .../components/input/RecipientInputPanel.tsx | 12 +- .../landing}/LandingBackground.mock.tsx | 0 .../components/landing}/LandingBackground.tsx | 253 +- .../landing/elements}/BuyElement.tsx | 13 +- .../landing/elements}/ENSElement.tsx | 10 +- .../landing/elements}/EmojiElement.tsx | 3 +- .../landing/elements}/FroggyElement.tsx | 7 +- .../landing/elements/HeartElement.tsx | 19 + .../landing/elements}/OpenseaElement.tsx | 9 +- .../landing/elements}/PolygonElement.tsx | 10 +- .../landing/elements}/ReceiveUSDCElement.tsx | 15 +- .../landing/elements/SendElement.tsx | 19 + .../landing/elements}/SwapElement.tsx | 37 +- .../landing/elements}/TextElement.tsx | 0 .../landing/elements}/UniconElement.tsx | 10 +- .../components/landing/elements}/index.tsx | 0 .../landing/landingBackgroundGradients.tsx | 42 + .../legacy/CurrencyInputPanelLegacy.tsx | 62 +- .../components/legacy/DecimalPadLegacy.tsx | 23 +- .../modals/WarningModal/WarningInfo.tsx | 11 +- .../modals/WarningModal/WarningModal.tsx | 29 +- .../modals/WarningModal/WarningTooltip.tsx | 4 +- .../WarningModal/WarningTooltip.web.tsx | 5 +- .../components/network/NetworkFee.test.tsx | 8 +- .../src/components/network/NetworkFee.tsx | 38 +- .../src/components/nfts/NFTHiddenRow.tsx | 33 +- .../src/components/nfts/NFTTransfer.tsx | 14 +- .../wallet/src/components/nfts/NftsList.tsx | 25 +- .../settings/AnalyticsToggleLineSwitch.tsx | 5 +- .../language/SettingsLanguageModal.native.tsx | 15 +- .../language/SettingsLanguageModal.web.tsx | 5 +- .../components/text/RelativeChange.test.tsx | 6 +- .../src/components/text/RelativeChange.tsx | 16 +- .../RelativeChange.test.tsx.snap | 3 + packages/wallet/src/constants/tokens.ts | 76 +- .../src/contexts/WalletNavigationContext.tsx | 49 +- .../data/apollo/usePersistedApolloClient.tsx | 16 +- packages/wallet/src/data/links.ts | 27 +- packages/wallet/src/data/tradingApi/api.json | 2 +- packages/wallet/src/data/utils.ts | 18 +- .../wallet/src/features/activity/hooks.ts | 33 +- .../src/features/activity/useActivityData.tsx | 33 +- .../wallet/src/features/activity/utils.ts | 12 +- packages/wallet/src/features/address/utils.ts | 5 +- packages/wallet/src/features/auth/saga.ts | 17 +- packages/wallet/src/features/auth/types.ts | 5 +- .../src/features/behaviorHistory/selectors.ts | 13 +- .../wallet/src/features/chains/utils.test.ts | 131 - packages/wallet/src/features/chains/utils.ts | 154 - .../src/features/contracts/ContractManager.ts | 16 +- .../wallet/src/features/contracts/hooks.ts | 7 +- .../src/features/dataApi/balances.test.ts | 133 +- .../wallet/src/features/dataApi/balances.ts | 72 +- .../src/features/dataApi/searchTokens.test.ts | 7 +- .../features/dataApi/tokenProject.test.tsx | 5 +- .../src/features/dataApi/topTokens.test.ts | 5 +- .../wallet/src/features/dataApi/utils.test.ts | 26 +- packages/wallet/src/features/ens/api.ts | 28 +- .../src/features/ens/parseENSAddress.ts | 4 +- packages/wallet/src/features/ens/useENS.ts | 6 +- .../src/features/favorites/selectors.ts | 21 +- .../wallet/src/features/favorites/slice.ts | 49 +- .../src/features/fiatCurrency/conversion.ts | 27 +- .../wallet/src/features/fiatCurrency/hooks.ts | 7 +- .../wallet/src/features/fiatOnRamp/api.ts | 353 +- .../wallet/src/features/fiatOnRamp/hooks.ts | 64 - .../wallet/src/features/fiatOnRamp/types.ts | 152 +- .../src/features/fiatOnRamp/utils.test.ts | 153 - .../wallet/src/features/fiatOnRamp/utils.ts | 81 - .../wallet/src/features/gas/adjustGasFee.ts | 46 +- packages/wallet/src/features/gas/api.ts | 8 +- .../gas/formatExternalTxnWithGasEstimates.tsx | 43 + packages/wallet/src/features/gas/hooks.ts | 63 +- .../src/features/gating/userPropertyHooks.ts | 9 +- .../src/features/images/ImageUri.native.tsx | 10 +- .../src/features/images/ImageUri.web.tsx | 19 +- .../src/features/images/NFTPreviewImage.tsx | 12 +- .../wallet/src/features/images/NFTViewer.tsx | 44 +- .../src/features/images/RemoteImage.tsx | 3 +- .../wallet/src/features/images/RemoteSvg.tsx | 4 +- .../src/features/images/WebSvgUri.web.tsx | 9 +- packages/wallet/src/features/images/hooks.ts | 7 +- packages/wallet/src/features/images/utils.tsx | 9 +- .../features/language/LocalizationContext.tsx | 5 +- .../wallet/src/features/language/formatter.ts | 29 +- packages/wallet/src/features/language/saga.ts | 18 +- .../wallet/src/features/language/slice.ts | 6 +- packages/wallet/src/features/nfts/hooks.ts | 23 +- packages/wallet/src/features/nfts/types.ts | 5 +- .../src/features/nfts/useNftContextMenu.tsx | 65 +- .../notifications/buildReceiveNotification.ts | 22 +- .../builtReceiveNotification.test.ts | 14 +- .../components/ApproveNotification.tsx | 11 +- .../ChangeAssetVisibilityNotification.tsx | 12 +- .../NetworkChangedNotification.test.tsx | 6 +- .../NotSupportedNetworkNotification.test.tsx | 4 +- .../components/NotificationToast.tsx | 86 +- .../PasswordChangedNotification.tsx | 4 +- .../components/PendingNotificationBadge.tsx | 17 +- .../SharedNotificationToastRouter.tsx | 8 +- .../components/SwapNotification.tsx | 4 +- .../components/SwapPendingNotification.tsx | 6 +- .../TransferCurrencyNotification.tsx | 18 +- .../components/TransferNFTNotification.tsx | 14 +- .../components/UnknownNotification.tsx | 8 +- .../components/WrapNotification.tsx | 11 +- ...SupportedNetworkNotification.test.tsx.snap | 1 + .../notificationWatcherSaga.test.ts | 43 +- .../notifications/notificationWatcherSaga.ts | 32 +- .../src/features/notifications/selectors.ts | 17 +- .../src/features/notifications/slice.ts | 31 +- .../features/notifications/testingUtils.ts | 7 +- .../src/features/notifications/types.ts | 14 +- .../src/features/notifications/utils.test.ts | 18 +- .../src/features/notifications/utils.ts | 104 +- .../features/onboarding/OnboardingContext.tsx | 137 +- .../onboarding/createImportedAccounts.ts | 10 +- .../onboarding/createOnboardingAccount.ts | 12 +- .../onboarding/createViewOnlyAccount.ts | 2 +- .../onboarding/hooks/useSelectAccounts.tsx | 9 +- .../src/features/portfolio/AnimatedNumber.tsx | 61 +- .../features/portfolio/HiddenTokensRow.tsx | 27 +- .../features/portfolio/PortfolioBalance.tsx | 15 +- .../portfolio/PortfolioEmptyState.tsx | 110 +- .../features/portfolio/TokenBalanceItem.tsx | 11 +- .../portfolio/TokenBalanceListContext.tsx | 23 +- packages/wallet/src/features/portfolio/api.ts | 25 +- .../portfolio/useTokenContextMenu.tsx | 39 +- .../src/features/providers/ProviderManager.ts | 6 +- .../providers/createEthersProvider.e2e.js | 5 +- .../providers/createEthersProvider.ts | 2 +- .../wallet/src/features/scantastic/types.ts | 2 +- .../wallet/src/features/search/SearchBar.tsx | 17 +- .../src/features/search/SearchResult.ts | 22 +- .../src/features/search/searchHistorySlice.ts | 8 +- .../wallet}/src/features/telemetry/hooks.ts | 26 +- .../src/features/telemetry/selectors.ts | 3 +- .../wallet/src/features/telemetry/slice.ts | 10 +- .../wallet/src/features/timing/selectors.ts | 3 +- packages/wallet/src/features/timing/slice.ts | 5 +- .../tokens/dismissedWarningTokensSelector.ts | 2 +- packages/wallet/src/features/tokens/hooks.ts | 7 +- .../wallet/src/features/tokens/safetyHooks.ts | 23 +- .../src/features/tokens/useCurrencyInfo.ts | 7 +- .../InsufficientNativeTokenBaseComponent.tsx | 15 +- .../InsufficientNativeTokenWarning.native.tsx | 8 +- .../InsufficientNativeTokenWarning.tsx | 4 +- .../InsufficientNativeTokenWarning.web.tsx | 2 +- .../useInsufficientNativeTokenWarning.tsx | 23 +- .../ApproveTransactionDetails.tsx | 70 + .../SummaryCards/DetailsModal/HeaderLogo.tsx | 126 +- .../NftTransactionDetails.test.tsx | 56 + .../DetailsModal/NftTransactionDetails.tsx | 109 + .../DetailsModal/OnRampTransactionDetails.tsx | 47 + .../SwapTransactionDetails.test.tsx | 71 + .../DetailsModal/SwapTransactionDetails.tsx | 140 +- .../TransactionDetailsInfoRows.tsx | 233 ++ .../TransactionDetailsModal.test.tsx | 121 + .../DetailsModal/TransactionDetailsModal.tsx | 293 +- .../TransferTransactionDetails.test.tsx | 94 + .../TransferTransactionDetails.tsx | 101 + .../DetailsModal/WrapTransactionDetails.tsx | 31 + .../NftTransactionDetails.test.tsx.snap | 128 + .../SwapTransactionDetails.test.tsx.snap | 191 + .../TransactionDetailsModal.test.tsx.snap | 636 ++++ .../TransferTransactionDetails.test.tsx.snap | 86 + .../SummaryCards/DetailsModal/hooks.ts | 28 + .../SummaryCards/DetailsModal/types.ts | 54 +- .../DetailsModal/useTransactionActions.tsx | 277 ++ .../useTransactionActionsCancelModals.tsx | 152 - .../SummaryCards/DetailsModal/utils.ts | 20 +- .../SummaryItems/ApproveSummaryItem.tsx | 29 +- .../SummaryItems/CancelConfirmationView.tsx | 78 +- .../SummaryItems/FiatPurchaseSummaryItem.tsx | 25 +- .../SummaryItems/NFTApproveSummaryItem.tsx | 8 +- .../SummaryItems/NFTMintSummaryItem.tsx | 8 +- .../SummaryItems/NFTSummaryItem.tsx | 9 +- .../SummaryItems/NFTTradeSummaryItem.tsx | 2 + .../OnRampTransferSummaryItem.tsx | 12 +- .../SummaryItems/ReceiveSummaryItem.tsx | 2 + .../SummaryItems/SendSummaryItem.tsx | 8 +- .../SummaryItems/SwapSummaryItem.tsx | 24 +- .../SummaryItems/TransactionActionsModal.tsx | 68 +- .../SummaryItems/TransactionSummaryLayout.tsx | 23 +- .../SummaryItems/TransactionSummaryTitle.tsx | 5 +- .../SummaryItems/TransferTokenSummaryItem.tsx | 21 +- .../SummaryItems/UnknownSummaryItem.tsx | 23 +- .../SummaryItems/WCSummaryItem.tsx | 7 +- .../SummaryItems/WrapSummaryItem.tsx | 30 +- .../transactions/SummaryCards/types.ts | 6 +- .../transactions/SummaryCards/utils.ts | 44 +- .../TransactionDetails/TransactionDetails.tsx | 33 +- .../TransactionHistoryUpdater.test.tsx | 18 +- .../TransactionHistoryUpdater.tsx | 64 +- .../TransactionRequest/AddressFooter.tsx | 11 +- .../TransactionRequest/ContentRow.tsx | 6 +- .../TransactionRequest/NetworkFeeFooter.tsx | 14 +- .../TransactionRequest}/SpendingDetails.tsx | 18 +- .../TransactionReview/TransactionReview.tsx | 30 +- .../transactions/cancelTransactionSaga.ts | 97 +- .../transactions/contexts/SwapFormContext.tsx | 13 +- .../contexts/SwapScreenContext.tsx | 2 +- .../transactions/contexts/SwapTxContext.tsx | 76 +- .../contexts/TransactionModalContext.tsx | 21 +- .../transactions/getAmountsFromTrade.ts | 5 +- .../history/conversion/conversion.test.ts | 16 +- .../extractFiatOnRampTransactionDetails.ts | 10 +- .../extractMoonpayTransactionDetails.ts | 142 - .../conversion/extractTransactionDetails.ts | 22 +- .../conversion/extractUniswapXOrderDetails.ts | 20 +- .../conversion/parseApproveTransaction.ts | 11 +- .../conversion/parseMintTransaction.ts | 32 +- .../conversion/parseOnRampTransaction.ts | 4 +- .../conversion/parseReceiveTransaction.ts | 19 +- .../conversion/parseSendTransaction.ts | 22 +- .../conversion/parseTradeTransaction.ts | 52 +- .../features/transactions/history/utils.ts | 42 +- .../wallet/src/features/transactions/hooks.ts | 150 +- .../useAllTransactionsBetweenAddresses.ts | 5 +- .../hooks/useParsedTransactionWarnings.tsx | 35 +- .../hooks/useSwapWarnings.test.ts | 38 +- .../transactions/hooks/useSwapWarnings.tsx | 20 +- .../useSyncFiatAndTokenAmountUpdater.tsx | 26 +- .../hooks/useTokenAndFiatDisplayAmounts.tsx | 8 +- .../hooks/useTokenFormActionHandlers.ts | 31 +- .../hooks/useTokenSelectorActionHandlers.ts | 20 +- .../hooks/useTransactionGasWarning.tsx | 8 +- .../features/transactions/orderWatcherSaga.ts | 154 + .../transactions/refetchGQLQueriesSaga.ts | 45 +- .../replaceTransactionSaga.test.ts | 22 +- .../transactions/replaceTransactionSaga.ts | 15 +- .../src/features/transactions/selectors.ts | 87 +- .../transactions/sendTransactionSaga.test.ts | 25 +- .../transactions/sendTransactionSaga.ts | 17 +- .../src/features/transactions/slice.test.ts | 36 +- .../wallet/src/features/transactions/slice.ts | 53 +- .../transactions/swap/CurrencyInputPanel.tsx | 107 +- .../features/transactions/swap/DecimalPad.tsx | 9 +- .../transactions/swap/DecimalPadInput.tsx | 28 +- .../swap/GasAndWarningRows.native.tsx | 41 +- .../swap/GasAndWarningRows.web.tsx | 42 +- .../swap/HoldToSwapProgressCircle.tsx | 10 +- .../transactions/swap/SwapArrowButton.tsx | 14 +- .../transactions/swap/SwapDetails.tsx | 52 +- .../features/transactions/swap/SwapFlow.tsx | 10 +- .../transactions/swap/SwapFormButton.tsx | 78 +- .../transactions/swap/SwapFormHeader.tsx | 35 +- .../transactions/swap/SwapFormScreen.tsx | 130 +- .../transactions/swap/SwapRateRatio.tsx | 9 +- .../transactions/swap/SwapReviewScreen.tsx | 152 +- .../transactions/swap/SwapTokenSelector.tsx | 56 +- .../swap/TransactionAmountsReview.tsx | 85 +- .../swap/TransactionModal.native.tsx | 34 +- .../transactions/swap/TransactionModal.tsx | 8 +- .../swap/TransactionModal.web.tsx | 7 +- .../features/transactions/swap/analytics.ts | 43 +- .../swap/createSwapFormFromTxDetails.ts | 17 +- .../features/transactions/swap/customRpc.ts | 4 +- .../swap/hooks/useExactOutputWillFail.test.ts | 16 +- .../swap/hooks/useExactOutputWillFail.ts | 6 +- .../hooks/useGasFeeHighRelativeToValue.ts | 2 +- .../swap/hooks/useSwapPrefilledState.ts | 18 +- .../swap/modals/FeeOnTransferWarning.tsx | 5 +- .../swap/modals/NetworkFeeWarning.tsx | 12 +- .../swap/modals/PriceImpactWarning.tsx | 5 +- .../swap/modals/QueuedOrderModal.tsx | 157 + .../swap/modals/SlippageInfoModal.tsx | 24 +- .../swap/modals/SwapFeeWarning.tsx | 14 +- .../swap/modals/SwapProtectionModal.tsx | 5 +- .../transactions/swap/modals/UniswapXInfo.tsx | 50 + .../swap/modals/UniswapXInfoModal.tsx | 9 +- .../settings/ProtocolPreferenceScreen.tsx | 66 +- .../settings/SlippageSettingsRow.native.tsx | 15 +- .../settings/SlippageSettingsRow.web.tsx | 18 +- .../settings/SlippageSettingsScreen.tsx | 11 +- .../modals/settings/SwapSettingsMessage.tsx | 2 +- .../modals/settings/SwapSettingsModal.tsx | 34 +- .../modals/settings/useSlippageSettings.ts | 18 +- .../transactions/swap/submitOrderSaga.ts | 112 + .../transactions/swap/swapSaga.test.ts | 41 +- .../features/transactions/swap/swapSaga.ts | 124 +- .../swap/trade/{tradingApi => api}/client.ts | 14 +- .../trade/api/hooks/useSwapTxAndGasInfo.ts | 126 + .../hooks/useTokenApprovalInfo.ts | 21 +- .../hooks/useTrade.ts} | 78 +- .../hooks/useTransactionRequestInfo.ts | 53 +- .../swap/trade/{tradingApi => api}/utils.ts | 101 +- .../swap/trade/hooks/useAcceptedTrade.ts | 11 +- .../swap/trade/hooks/useDerivedSwapInfo.ts | 43 +- .../swap/trade/hooks/useSetTradeSlippage.ts | 39 +- .../hooks/useShowSwapNetworkNotification.ts | 6 +- .../swap/trade/hooks/useSwapCallback.ts | 125 +- .../swap/trade/hooks/useUSDCPrice.ts | 12 +- .../swap/trade/hooks/useUSDTokenUpdater.ts | 15 +- .../swap/trade/hooks/useWrapCallback.ts | 6 +- .../trade/hooks/useWrapTransactionRequest.ts | 49 +- .../hooks/useSwapTxAndGasInfoTradingApi.ts | 63 - .../features/transactions/swap/trade/types.ts | 32 +- .../features/transactions/swap/trade/utils.ts | 6 +- .../src/features/transactions/swap/types.ts | 8 +- .../transactions/swap/usePermit2Signature.ts | 38 +- .../features/transactions/swap/utils.test.ts | 17 +- .../src/features/transactions/swap/utils.ts | 63 +- .../transactions/swap/wrapSaga.test.ts | 9 +- .../features/transactions/swap/wrapSaga.ts | 15 +- .../transactionState/transactionState.test.ts | 44 +- .../transactionState/transactionState.ts | 25 +- .../transactionWatcherSaga.test.ts | 35 +- .../transactions/transactionWatcherSaga.ts | 323 +- .../transfer/TokenSelectorPanel.tsx | 51 +- .../transfer/TransferAmountInput.tsx | 35 +- .../transfer/TransferFormWarnings.tsx | 25 +- .../transactions/transfer/TransferReview.tsx | 17 +- .../transfer/TransferTokenForm.tsx | 101 +- .../transfer/getSendPrefilledState.ts | 9 +- .../transfer/hooks/useDerivedTransferInfo.ts | 32 +- .../hooks/useIsSmartContractAddress.ts | 2 +- .../transfer/hooks/useOnSelectRecipient.ts | 6 +- .../hooks/useOnToggleShowRecipientSelector.ts | 6 +- .../hooks/useShowSendNetworkNotification.ts | 2 +- .../transfer/hooks/useTransferCallback.ts | 16 +- .../hooks/useTransferTransactionRequest.ts | 64 +- .../hooks/useTransferWarnings.test.ts | 15 +- .../transfer/hooks/useTransferWarnings.ts | 22 +- .../transfer/transferTokenSaga.test.ts | 11 +- .../transfer/transferTokenSaga.ts | 31 +- .../features/transactions/transfer/types.ts | 9 +- .../wallet/src/features/transactions/types.ts | 105 +- .../src/features/transactions/utils.test.ts | 3 +- .../wallet/src/features/transactions/utils.ts | 36 +- .../features/trm/BlockedAddressWarning.tsx | 17 +- packages/wallet/src/features/trm/hooks.ts | 2 +- packages/wallet/src/features/unitags/api.ts | 42 +- .../wallet/src/features/unitags/avatars.ts | 11 +- packages/wallet/src/features/unitags/hooks.ts | 68 +- packages/wallet/src/features/unitags/utils.ts | 6 +- .../features/wallet/Keyring/Keyring.native.ts | 4 + .../features/wallet/Keyring/Keyring.test.ts | 21 +- .../src/features/wallet/Keyring/Keyring.ts | 18 +- .../wallet/Keyring/__mocks__/Keyring.ts | 9 +- .../src/features/wallet/Keyring/crypto.ts | 23 +- .../wallet/accounts/editAccountSaga.ts | 32 +- .../wallet/src/features/wallet/context.tsx | 15 +- .../wallet/create/createAccountsSaga.test.ts | 21 +- .../wallet/create/createAccountsSaga.ts | 2 +- .../src/features/wallet/getAccountId.ts | 2 +- packages/wallet/src/features/wallet/hooks.ts | 17 +- .../wallet/src/features/wallet/selectors.ts | 48 +- .../wallet/signing/NativeSigner.native.ts | 20 +- .../features/wallet/signing/NativeSigner.ts | 15 +- .../features/wallet/signing/signing.native.ts | 20 +- .../src/features/wallet/signing/signing.ts | 17 +- .../wallet/src/features/wallet/slice.test.ts | 6 +- packages/wallet/src/features/wallet/slice.ts | 16 +- .../wallet/src/provider/tamagui-provider.tsx | 8 +- packages/wallet/src/state/README.md | 2 +- packages/wallet/src/state/createMigrate.ts | 27 +- packages/wallet/src/state/index.ts | 4 +- packages/wallet/src/state/reducer.ts | 5 +- packages/wallet/src/state/saga.ts | 19 +- packages/wallet/src/state/sharedMigrations.ts | 2 +- .../wallet/src/state/sharedMigrationsTests.ts | 18 +- packages/wallet/src/state/testUtils.ts | 10 +- .../src/test/fixtures/gql/activities/index.ts | 11 +- .../src/test/fixtures/gql/activities/nfts.ts | 5 +- .../src/test/fixtures/gql/activities/swap.ts | 5 +- .../test/fixtures/gql/activities/tokens.ts | 11 +- .../wallet/src/test/fixtures/gql/amounts.ts | 5 +- .../fixtures/{ => gql/assets}/constants.ts | 0 .../src/test/fixtures/gql/assets/index.ts | 1 + .../src/test/fixtures/gql/assets/nfts.ts | 5 +- .../src/test/fixtures/gql/assets/tokens.ts | 37 +- .../wallet/src/test/fixtures/gql/history.ts | 7 +- packages/wallet/src/test/fixtures/gql/misc.ts | 9 +- .../wallet/src/test/fixtures/gql/portfolio.ts | 5 +- .../src/test/fixtures/gql/transactions.ts | 10 +- packages/wallet/src/test/fixtures/index.ts | 2 +- .../wallet/src/test/fixtures/lib/ethers.ts | 10 +- .../wallet/src/test/fixtures/lib/netinfo.ts | 2 +- packages/wallet/src/test/fixtures/lib/sdk.ts | 28 +- .../src/test/fixtures/wallet/accounts.ts | 11 +- .../src/test/fixtures/wallet/balances.ts | 26 +- .../wallet/src/test/fixtures/wallet/index.ts | 1 - .../src/test/fixtures/wallet/notifications.ts | 61 +- .../src/test/fixtures/wallet/recipients.ts | 9 +- .../wallet/src/test/fixtures/wallet/redux.ts | 9 +- .../fixtures/wallet/transactions/fixtures.ts | 52 +- .../fixtures/wallet/transactions/helpers.ts | 8 +- .../src/test/fixtures/wallet/walletConnect.ts | 4 +- packages/wallet/src/test/mocks/gql/mocks.ts | 2 +- .../wallet/src/test/mocks/gql/provider.tsx | 5 +- .../wallet/src/test/mocks/gql/resolvers.ts | 5 +- packages/wallet/src/test/mocks/providers.ts | 11 +- packages/wallet/src/test/mocks/sdk.ts | 4 +- packages/wallet/src/test/mocks/utils.ts | 6 +- packages/wallet/src/test/render.tsx | 27 +- packages/wallet/src/test/test-utils.ts | 2 +- packages/wallet/src/test/utils/array.ts | 5 +- packages/wallet/src/test/utils/index.ts | 1 - packages/wallet/src/test/utils/random.ts | 4 +- packages/wallet/src/test/utils/resolvers.ts | 55 +- .../wallet/src/test/utils/wallet/balances.ts | 15 +- packages/wallet/src/utils/animations.ts | 11 +- packages/wallet/src/utils/balance.test.ts | 25 +- packages/wallet/src/utils/balance.ts | 10 +- packages/wallet/src/utils/currency.test.ts | 6 +- packages/wallet/src/utils/currency.ts | 6 +- packages/wallet/src/utils/currencyId.test.ts | 59 +- packages/wallet/src/utils/currencyId.ts | 21 +- .../src/utils/getCurrencyAmount.test.ts | 44 +- .../wallet/src/utils/getCurrencyAmount.ts | 8 +- packages/wallet/src/utils/linking.test.ts | 16 +- packages/wallet/src/utils/linking.ts | 111 +- packages/wallet/src/utils/mnemonics.test.ts | 24 +- packages/wallet/src/utils/mnemonics.ts | 2 +- packages/wallet/src/utils/password.test.ts | 12 +- packages/wallet/src/utils/password.ts | 9 +- packages/wallet/src/utils/persistedStorage.ts | 2 +- packages/wallet/src/utils/saga.ts | 16 +- packages/wallet/src/utils/transaction.ts | 4 +- .../src/utils/useDynamicFontSizing.test.ts | 8 +- .../wallet/src/utils/useDynamicFontSizing.ts | 6 +- packages/wallet/src/utils/useNoYoloParser.ts | 27 + scripts/prettier.sh | 20 + scripts/turbo-changed.sh | 4 +- turbo.json | 7 + yarn.lock | 769 ++-- 2235 files changed, 35029 insertions(+), 27762 deletions(-) create mode 100644 .prettierignore create mode 100644 .prettierrc delete mode 100644 .yarn/patches/detox-npm-20.18.1-b532b310b4.patch create mode 100644 .yarn/patches/detox-npm-20.23.0-6d61110e63.patch create mode 100644 CODEOWNERS delete mode 100644 apps/mobile/.prettierignore create mode 100644 apps/mobile/__mocks__/@react-navigation/native.js create mode 100644 apps/mobile/e2e/Home.e2e.ts delete mode 100644 apps/mobile/e2e/usecases/CreateNewWallet.ts create mode 100644 apps/mobile/e2e/usecases/home/HomeBasicInteractions.ts create mode 100644 apps/mobile/src/app/navigation/components.tsx rename apps/mobile/src/components/{WalletConnect => Requests}/ConnectedDapps/ConnectedDappsList.tsx (77%) rename apps/mobile/src/components/{WalletConnect => Requests}/ConnectedDapps/DappConnectedNetworksModal.tsx (89%) rename apps/mobile/src/components/{WalletConnect => Requests}/ConnectedDapps/DappConnectionItem.tsx (87%) rename apps/mobile/src/components/{WalletConnect => Requests}/DappHeaderIcon.tsx (100%) rename apps/mobile/src/components/{WalletConnect => Requests}/ModalWithOverlay/ModalWithOverlay.tsx (85%) rename apps/mobile/src/components/{WalletConnect => Requests}/ModalWithOverlay/ScrollDownOverlay.tsx (93%) rename apps/mobile/src/components/{WalletConnect => Requests}/RequestModal/ClientDetails.tsx (79%) rename apps/mobile/src/components/{WalletConnect => Requests}/RequestModal/HeaderText.tsx (95%) rename apps/mobile/src/components/{WalletConnect => Requests}/RequestModal/KidSuperCheckinModal.tsx (78%) rename apps/mobile/src/components/{WalletConnect => Requests}/RequestModal/RequestDetails.tsx (82%) rename apps/mobile/src/components/{WalletConnect => Requests}/RequestModal/UwULinkErc20SendModal.tsx (90%) rename apps/mobile/src/components/{WalletConnect => Requests}/RequestModal/WalletConnectRequestModal.tsx (78%) rename apps/mobile/src/components/{WalletConnect => Requests}/RequestModal/WalletConnectRequestModalContent.tsx (84%) rename apps/mobile/src/components/{WalletConnect => Requests}/RequestModal/hooks.ts (89%) rename apps/mobile/src/components/{WalletConnect => Requests}/ScanSheet/PendingConnectionModal.tsx (86%) rename apps/mobile/src/components/{WalletConnect => Requests}/ScanSheet/PendingConnectionSwitchAccountModal.tsx (82%) rename apps/mobile/src/components/{WalletConnect => Requests}/ScanSheet/PendingConnectionSwitchNetworkModal.tsx (77%) rename apps/mobile/src/components/{WalletConnect => Requests}/ScanSheet/SwitchAccountOption.tsx (95%) rename apps/mobile/src/components/{WalletConnect => Requests}/ScanSheet/WalletConnectModal.tsx (84%) rename apps/mobile/src/components/{WalletConnect => Requests}/ScanSheet/util.test.ts (97%) rename apps/mobile/src/components/{WalletConnect => Requests}/ScanSheet/util.ts (86%) rename apps/mobile/src/components/{WalletConnect => Requests}/WalletConnectModals.tsx (74%) create mode 100644 apps/mobile/src/components/layout/SafeKeyboardScreen.tsx create mode 100644 apps/mobile/src/features/CloudBackup/CloudBackupForm/CloudBackupPasswordFormContext.tsx create mode 100644 apps/mobile/src/features/CloudBackup/CloudBackupForm/ContinueButton.tsx create mode 100644 apps/mobile/src/features/CloudBackup/CloudBackupForm/PasswordInput.tsx create mode 100644 apps/mobile/src/features/CloudBackup/CloudBackupForm/index.ts delete mode 100644 apps/mobile/src/features/CloudBackup/CloudBackupPasswordForm.tsx rename apps/mobile/src/features/deepLinking/{handleMoonpayReturnLinkSaga.test.ts => handleOnRampReturnLinkSaga.test.ts} (78%) rename apps/mobile/src/features/deepLinking/{handleMoonpayReturnLinkSaga.ts => handleOnRampReturnLinkSaga.ts} (92%) delete mode 100644 apps/mobile/src/features/fiatOnRamp/FiatOnRampModal.tsx delete mode 100644 apps/mobile/src/features/fiatOnRamp/aggregatorHooks.ts delete mode 100644 apps/mobile/src/features/fiatOnRamp/constants.ts delete mode 100644 apps/mobile/src/features/modals/hooks.ts delete mode 100644 apps/mobile/src/features/unitags/ConfirmationElements.tsx create mode 100644 apps/mobile/src/screens/Import/SeedPhraseInputScreenV2.android.mock.tsx delete mode 100644 apps/mobile/src/screens/Onboarding/OnboardingElements/HeartElement.tsx delete mode 100644 apps/mobile/src/screens/Onboarding/OnboardingElements/SendElement.tsx delete mode 100644 apps/mobile/src/utils/useNoYoloParser.ts create mode 100644 apps/web/public/images/extension_promo/announcement_modal_desktop.png create mode 100644 apps/web/public/images/extension_promo/announcement_modal_mobile.png create mode 100644 apps/web/public/images/extension_promo/background_connector.png delete mode 100644 apps/web/src/assets/images/extensionIllustration.jpg create mode 100644 apps/web/src/assets/images/extensionIllustration.png delete mode 100644 apps/web/src/assets/images/walletAnnouncementBannerQR.png delete mode 100644 apps/web/src/assets/images/walletIllustration.jpg create mode 100644 apps/web/src/assets/images/walletIllustration.png create mode 100644 apps/web/src/components/AccountDetails/AddressDisplay.tsx create mode 100644 apps/web/src/components/AccountDrawer/MiniPortfolio/EmptyWallet.tsx create mode 100644 apps/web/src/components/AccountDrawer/MiniPortfolio/ExtensionDeeplinks.tsx create mode 100644 apps/web/src/components/AccountDrawer/MiniPortfolio/Pools/UniExtensionPoolsMenu.tsx create mode 100644 apps/web/src/components/AccountDrawer/MiniPortfolio/shared.tsx create mode 100644 apps/web/src/components/AccountDrawer/__snapshots__/index.test.tsx.snap create mode 100644 apps/web/src/components/AccountDrawer/index.test.tsx create mode 100644 apps/web/src/components/AddressQRModal/index.tsx create mode 100644 apps/web/src/components/AddressQRModal/useAvatarColorProps.tsx delete mode 100644 apps/web/src/components/Icons/BraveBrowserLogo.tsx create mode 100644 apps/web/src/components/Icons/Collapse.tsx create mode 100644 apps/web/src/components/Icons/Expand.tsx delete mode 100644 apps/web/src/components/Icons/TimeForward.tsx create mode 100644 apps/web/src/components/Modal/GetHelpHeader.tsx create mode 100644 apps/web/src/components/NavBar/Tabs/QuickKey.test.tsx create mode 100644 apps/web/src/components/ReceiveCryptoModal/ChooseProvider.tsx create mode 100644 apps/web/src/components/ReceiveCryptoModal/ProviderOption.tsx create mode 100644 apps/web/src/components/ReceiveCryptoModal/index.tsx create mode 100644 apps/web/src/components/TopLevelModals/ExtensionLaunchModal.tsx create mode 100644 apps/web/src/components/WalletModal/DownloadWalletOption.tsx create mode 100644 apps/web/src/components/WalletModal/UniswapWalletOptions.test.tsx create mode 100644 apps/web/src/components/WalletModal/__snapshots__/UniswapWalletOptions.test.tsx.snap create mode 100644 apps/web/src/components/WalletOneLinkQR/index.tsx delete mode 100644 apps/web/src/hooks/useAutoRouterSupported.tsx create mode 100644 apps/web/src/lib/styled-components.ts create mode 100644 apps/web/src/pages/Swap/Buy/ProviderConnectedView.test.tsx create mode 100644 apps/web/src/pages/Swap/Buy/ProviderConnectedView.tsx create mode 100644 apps/web/src/pages/Swap/Buy/ProviderConnectionError.test.tsx create mode 100644 apps/web/src/pages/Swap/Buy/ProviderConnectionError.tsx create mode 100644 apps/web/src/pages/Swap/Buy/ProviderOption.tsx create mode 100644 apps/web/src/pages/Swap/Buy/__snapshots__/ProviderConnectedView.test.tsx.snap create mode 100644 apps/web/src/pages/Swap/Buy/__snapshots__/ProviderConnectionError.test.tsx.snap delete mode 100644 apps/web/src/pages/Swap/Buy/shared.ts create mode 100644 apps/web/src/pages/Swap/Buy/shared.tsx create mode 100644 apps/web/src/pages/Swap/Buy/test/constants.ts create mode 100644 apps/web/src/setupRive.ts create mode 100644 apps/web/src/state/fiatOnRampTransactions/hooks.ts create mode 100644 apps/web/src/state/fiatOnRampTransactions/reducer.test.ts create mode 100644 apps/web/src/state/fiatOnRampTransactions/reducer.ts create mode 100644 apps/web/src/state/fiatOnRampTransactions/types.ts create mode 100644 apps/web/src/state/fiatOnRampTransactions/updater.test.tsx create mode 100644 apps/web/src/state/fiatOnRampTransactions/updater.ts create mode 100644 apps/web/src/tracing/amplitude.ts create mode 100644 apps/web/src/tracing/sentry.ts delete mode 100644 packages/eslint-config/.depcheckrc create mode 100644 packages/ui/.eslintignore delete mode 100644 packages/ui/.prettierignore create mode 100644 packages/ui/src/assets/backgrounds/for-connecting-v2.svg create mode 100644 packages/ui/src/assets/icons/arrow-right-to-line.svg create mode 100644 packages/ui/src/assets/icons/block-explorer.svg create mode 100644 packages/ui/src/assets/misc/dot-grid.png rename packages/{wallet/src/components/QRCodeScanner => ui/src/components/QRCode}/custom-qr-code-generator/index.d.ts (88%) rename packages/{wallet/src/components/QRCodeScanner => ui/src/components/QRCode}/custom-qr-code-generator/index.js (100%) create mode 100644 packages/ui/src/components/QRCode/custom-qr-code-generator/src/genMatrix.js rename packages/{wallet/src/components/QRCodeScanner => ui/src/components/QRCode}/custom-qr-code-generator/src/index.jsx (71%) rename packages/{wallet/src/components/QRCodeScanner => ui/src/components/QRCode}/custom-qr-code-generator/src/transformMatrixIntoCirclePath.js (61%) rename packages/{wallet/src/components/QRCodeScanner/QRCode.tsx => ui/src/components/QRCode/index.tsx} (53%) create mode 100644 packages/ui/src/components/icons/ArrowRightToLine.tsx create mode 100644 packages/ui/src/components/icons/BlockExplorer.tsx create mode 100644 packages/ui/src/components/logos/exported.ts create mode 100644 packages/ui/src/components/menu/ContextMenu.tsx create mode 100644 packages/ui/src/components/menu/MenuContent.tsx create mode 100644 packages/ui/src/components/menu/types.ts create mode 100644 packages/ui/src/components/modal/AdaptiveWebModalSheet.tsx create mode 100644 packages/ui/src/hooks/useDeviceDimensions.native.ts rename packages/{wallet => uniswap}/src/components/CurrencyLogo/SplitLogo.test.tsx (90%) rename packages/{wallet => uniswap}/src/components/CurrencyLogo/SplitLogo.tsx (92%) rename packages/{wallet => uniswap}/src/components/CurrencyLogo/__snapshots__/SplitLogo.test.tsx.snap (92%) rename packages/{wallet => uniswap}/src/components/TokenSelector/SuggestedToken.tsx (91%) rename packages/{wallet => uniswap}/src/components/TokenSelector/TokenOptionItem.tsx (68%) rename packages/{wallet => uniswap}/src/components/TokenSelector/TokenSectionBaseList.native.tsx (86%) rename packages/{wallet => uniswap}/src/components/TokenSelector/TokenSectionBaseList.tsx (95%) rename packages/{wallet => uniswap}/src/components/TokenSelector/TokenSectionBaseList.web.tsx (86%) rename packages/{wallet => uniswap}/src/components/TokenSelector/TokenSelector.tsx (53%) create mode 100644 packages/uniswap/src/components/TokenSelector/TokenSelectorEmptySearchList.tsx rename packages/{wallet => uniswap}/src/components/TokenSelector/TokenSelectorList.tsx (65%) create mode 100644 packages/uniswap/src/components/TokenSelector/TokenSelectorSearchResultsList.tsx create mode 100644 packages/uniswap/src/components/TokenSelector/TokenSelectorSendList.tsx create mode 100644 packages/uniswap/src/components/TokenSelector/TokenSelectorSwapInputList.tsx rename packages/{wallet => uniswap}/src/components/TokenSelector/TokenSelectorSwapOutputList.tsx (53%) rename packages/{wallet => uniswap}/src/components/TokenSelector/filter.ts (93%) rename packages/{wallet => uniswap}/src/components/TokenSelector/renderSuggestedTokenItem.tsx (75%) rename packages/{wallet => uniswap}/src/components/TokenSelector/suggestedTokensKeyExtractor.tsx (69%) create mode 100644 packages/uniswap/src/components/TokenSelector/types.ts rename packages/{wallet => uniswap}/src/components/TokenSelector/utils.ts (81%) create mode 100644 packages/uniswap/src/components/ViewGestureHandler/index.native.tsx create mode 100644 packages/uniswap/src/components/ViewGestureHandler/index.tsx create mode 100644 packages/uniswap/src/components/ViewGestureHandler/index.web.tsx rename packages/{wallet => uniswap}/src/components/buttons/PasteButton.test.tsx (65%) rename packages/{wallet => uniswap}/src/components/buttons/PasteButton.tsx (94%) rename packages/{wallet => uniswap}/src/components/buttons/__snapshots__/PasteButton.test.tsx.snap (97%) rename packages/{wallet => uniswap}/src/components/icons/WarningIcon.tsx (91%) create mode 100644 packages/uniswap/src/components/misc/ActionCard.tsx create mode 100644 packages/uniswap/src/components/modals/ScrollLock.native.tsx create mode 100644 packages/uniswap/src/components/modals/ScrollLock.tsx rename packages/{wallet/src/components/modals/hooks.ts => uniswap/src/components/modals/hooks.native.ts} (100%) create mode 100644 packages/uniswap/src/components/modals/hooks.ts create mode 100644 packages/uniswap/src/components/modals/hooks.web.ts rename packages/{wallet => uniswap}/src/components/network/NetworkLogos.tsx (82%) rename packages/{wallet => uniswap}/src/components/network/NetworkPill.test.tsx (80%) rename packages/{wallet => uniswap}/src/components/network/NetworkPill.tsx (84%) rename packages/{wallet => uniswap}/src/components/network/__snapshots__/NetworkPill.test.tsx.snap (100%) rename packages/{wallet => uniswap}/src/components/text/LearnMoreLink.test.tsx (79%) rename packages/{wallet => uniswap}/src/components/text/LearnMoreLink.tsx (76%) rename packages/{wallet => uniswap}/src/components/text/__snapshots__/LearnMoreLink.test.tsx.snap (100%) rename packages/{wallet => uniswap}/src/constants/addresses.ts (81%) rename packages/{wallet => uniswap}/src/constants/misc.ts (82%) rename packages/{wallet => uniswap}/src/entities/assets.ts (100%) create mode 100644 packages/uniswap/src/extension/useIsChromeWindowFocused.ts rename packages/{wallet => uniswap}/src/features/dataApi/searchTokens.ts (75%) rename packages/{wallet => uniswap}/src/features/dataApi/tokenProjects.ts (80%) rename packages/{wallet => uniswap}/src/features/dataApi/topTokens.ts (89%) rename packages/{wallet => uniswap}/src/features/dataApi/utils.ts (85%) rename apps/mobile/src/features/fiatOnRamp/FiatOnRampConnecting.tsx => packages/uniswap/src/features/fiatOnRamp/FiatOnRampConnectingView.native.tsx (85%) create mode 100644 packages/uniswap/src/features/fiatOnRamp/FiatOnRampConnectingView.tsx create mode 100644 packages/uniswap/src/features/fiatOnRamp/useCexTransferProviders.ts create mode 100644 packages/uniswap/src/features/language/formatter.ts rename packages/{wallet => uniswap}/src/features/search/SearchContext.tsx (100%) create mode 100644 packages/uniswap/src/features/search/SearchResult.ts rename packages/{wallet => uniswap}/src/features/search/SearchTextInput.tsx (91%) rename packages/{wallet => uniswap}/src/features/tokens/NativeCurrency.ts (80%) rename packages/{wallet => uniswap}/src/features/tokens/TokenWarningModal.tsx (80%) create mode 100644 packages/uniswap/src/features/tokens/safetyHooks.ts rename packages/{wallet => uniswap}/src/features/tokens/utils.ts (78%) rename packages/{wallet => uniswap}/src/features/transactions/transactionState/types.ts (91%) create mode 100644 packages/uniswap/src/features/transactions/transfer/types.ts create mode 100644 packages/uniswap/src/test/fixtures/testIDs.ts rename packages/{wallet => uniswap}/src/test/fixtures/wallet/currencies.ts (91%) create mode 100644 packages/uniswap/src/test/fixtures/wallet/index.ts rename packages/{wallet => uniswap}/src/test/shared.ts (100%) rename packages/{wallet => uniswap}/src/test/utils/factory.ts (86%) create mode 100644 packages/uniswap/src/test/utils/index.ts rename packages/{wallet => uniswap}/src/utils/addresses.test.ts (95%) rename packages/{wallet => uniswap}/src/utils/addresses.ts (93%) rename packages/{wallet => uniswap}/src/utils/clipboard.native.ts (91%) rename packages/{wallet => uniswap}/src/utils/clipboard.ts (100%) rename packages/{wallet => uniswap}/src/utils/clipboard.web.ts (94%) rename packages/{wallet => uniswap}/src/utils/colors.test.ts (98%) rename packages/{wallet => uniswap}/src/utils/colors.tsx (94%) create mode 100644 packages/uniswap/src/utils/currencyId.ts create mode 100644 packages/uniswap/src/utils/link.native.ts create mode 100644 packages/uniswap/src/utils/link.ts create mode 100644 packages/uniswap/src/utils/link.web.ts create mode 100644 packages/uniswap/src/utils/linking.ts create mode 100644 packages/uniswap/src/utils/usePlatformBasedFetchPolicy.ts create mode 100644 packages/uniswap/src/utils/usePlatformBasedValue.native.ts create mode 100644 packages/uniswap/src/utils/usePlatformBasedValue.ts create mode 100644 packages/utilities/src/environment/constants.ts rename packages/{uniswap/src/utils/env => utilities/src/environment}/index.native.ts (52%) create mode 100644 packages/utilities/src/logger/Datadog.ts create mode 100644 packages/utilities/src/logger/mocks.ts create mode 100644 packages/utilities/src/logger/types.ts delete mode 100644 packages/wallet/src/components/QRCodeScanner/custom-qr-code-generator/src/genMatrix.js create mode 100644 packages/wallet/src/components/QRCodeScanner/useQRColorProps.ts delete mode 100644 packages/wallet/src/components/TokenSelector/TokenSelectorEmptySearchList.tsx delete mode 100644 packages/wallet/src/components/TokenSelector/TokenSelectorSearchResultsList.tsx delete mode 100644 packages/wallet/src/components/TokenSelector/TokenSelectorSendList.tsx delete mode 100644 packages/wallet/src/components/TokenSelector/TokenSelectorSwapInputList.tsx rename packages/wallet/src/components/TokenSelector/{hooks.ts => hooks.tsx} (51%) delete mode 100644 packages/wallet/src/components/TokenSelector/types.ts rename {apps/mobile/src/components/gradients => packages/wallet/src/components/landing}/LandingBackground.mock.tsx (100%) rename {apps/mobile/src/components/gradients => packages/wallet/src/components/landing}/LandingBackground.tsx (60%) rename {apps/mobile/src/screens/Onboarding/OnboardingElements => packages/wallet/src/components/landing/elements}/BuyElement.tsx (51%) rename {apps/mobile/src/screens/Onboarding/OnboardingElements => packages/wallet/src/components/landing/elements}/ENSElement.tsx (64%) rename {apps/mobile/src/screens/Onboarding/OnboardingElements => packages/wallet/src/components/landing/elements}/EmojiElement.tsx (90%) rename {apps/mobile/src/screens/Onboarding/OnboardingElements => packages/wallet/src/components/landing/elements}/FroggyElement.tsx (66%) create mode 100644 packages/wallet/src/components/landing/elements/HeartElement.tsx rename {apps/mobile/src/screens/Onboarding/OnboardingElements => packages/wallet/src/components/landing/elements}/OpenseaElement.tsx (50%) rename {apps/mobile/src/screens/Onboarding/OnboardingElements => packages/wallet/src/components/landing/elements}/PolygonElement.tsx (55%) rename {apps/mobile/src/screens/Onboarding/OnboardingElements => packages/wallet/src/components/landing/elements}/ReceiveUSDCElement.tsx (62%) create mode 100644 packages/wallet/src/components/landing/elements/SendElement.tsx rename {apps/mobile/src/screens/Onboarding/OnboardingElements => packages/wallet/src/components/landing/elements}/SwapElement.tsx (50%) rename {apps/mobile/src/screens/Onboarding/OnboardingElements => packages/wallet/src/components/landing/elements}/TextElement.tsx (100%) rename {apps/mobile/src/screens/Onboarding/OnboardingElements => packages/wallet/src/components/landing/elements}/UniconElement.tsx (57%) rename {apps/mobile/src/screens/Onboarding/OnboardingElements => packages/wallet/src/components/landing/elements}/index.tsx (100%) create mode 100644 packages/wallet/src/components/landing/landingBackgroundGradients.tsx delete mode 100644 packages/wallet/src/features/chains/utils.test.ts delete mode 100644 packages/wallet/src/features/chains/utils.ts delete mode 100644 packages/wallet/src/features/fiatOnRamp/hooks.ts delete mode 100644 packages/wallet/src/features/fiatOnRamp/utils.test.ts delete mode 100644 packages/wallet/src/features/fiatOnRamp/utils.ts create mode 100644 packages/wallet/src/features/gas/formatExternalTxnWithGasEstimates.tsx rename {apps/mobile => packages/wallet}/src/features/telemetry/hooks.ts (83%) create mode 100644 packages/wallet/src/features/transactions/SummaryCards/DetailsModal/ApproveTransactionDetails.tsx create mode 100644 packages/wallet/src/features/transactions/SummaryCards/DetailsModal/NftTransactionDetails.test.tsx create mode 100644 packages/wallet/src/features/transactions/SummaryCards/DetailsModal/NftTransactionDetails.tsx create mode 100644 packages/wallet/src/features/transactions/SummaryCards/DetailsModal/OnRampTransactionDetails.tsx create mode 100644 packages/wallet/src/features/transactions/SummaryCards/DetailsModal/SwapTransactionDetails.test.tsx create mode 100644 packages/wallet/src/features/transactions/SummaryCards/DetailsModal/TransactionDetailsInfoRows.tsx create mode 100644 packages/wallet/src/features/transactions/SummaryCards/DetailsModal/TransactionDetailsModal.test.tsx create mode 100644 packages/wallet/src/features/transactions/SummaryCards/DetailsModal/TransferTransactionDetails.test.tsx create mode 100644 packages/wallet/src/features/transactions/SummaryCards/DetailsModal/TransferTransactionDetails.tsx create mode 100644 packages/wallet/src/features/transactions/SummaryCards/DetailsModal/WrapTransactionDetails.tsx create mode 100644 packages/wallet/src/features/transactions/SummaryCards/DetailsModal/__snapshots__/NftTransactionDetails.test.tsx.snap create mode 100644 packages/wallet/src/features/transactions/SummaryCards/DetailsModal/__snapshots__/SwapTransactionDetails.test.tsx.snap create mode 100644 packages/wallet/src/features/transactions/SummaryCards/DetailsModal/__snapshots__/TransactionDetailsModal.test.tsx.snap create mode 100644 packages/wallet/src/features/transactions/SummaryCards/DetailsModal/__snapshots__/TransferTransactionDetails.test.tsx.snap create mode 100644 packages/wallet/src/features/transactions/SummaryCards/DetailsModal/hooks.ts create mode 100644 packages/wallet/src/features/transactions/SummaryCards/DetailsModal/useTransactionActions.tsx delete mode 100644 packages/wallet/src/features/transactions/SummaryCards/DetailsModal/useTransactionActionsCancelModals.tsx rename {apps/mobile/src/components/WalletConnect/RequestModal => packages/wallet/src/features/transactions/TransactionRequest}/SpendingDetails.tsx (82%) delete mode 100644 packages/wallet/src/features/transactions/history/conversion/extractMoonpayTransactionDetails.ts create mode 100644 packages/wallet/src/features/transactions/orderWatcherSaga.ts create mode 100644 packages/wallet/src/features/transactions/swap/modals/QueuedOrderModal.tsx create mode 100644 packages/wallet/src/features/transactions/swap/modals/UniswapXInfo.tsx create mode 100644 packages/wallet/src/features/transactions/swap/submitOrderSaga.ts rename packages/wallet/src/features/transactions/swap/trade/{tradingApi => api}/client.ts (79%) create mode 100644 packages/wallet/src/features/transactions/swap/trade/api/hooks/useSwapTxAndGasInfo.ts rename packages/wallet/src/features/transactions/swap/trade/{tradingApi => api}/hooks/useTokenApprovalInfo.ts (80%) rename packages/wallet/src/features/transactions/swap/trade/{tradingApi/hooks/useTradingApiTrade.ts => api/hooks/useTrade.ts} (75%) rename packages/wallet/src/features/transactions/swap/trade/{tradingApi => api}/hooks/useTransactionRequestInfo.ts (78%) rename packages/wallet/src/features/transactions/swap/trade/{tradingApi => api}/utils.ts (81%) delete mode 100644 packages/wallet/src/features/transactions/swap/trade/tradingApi/hooks/useSwapTxAndGasInfoTradingApi.ts rename packages/wallet/src/test/fixtures/{ => gql/assets}/constants.ts (100%) create mode 100644 packages/wallet/src/utils/useNoYoloParser.ts create mode 100755 scripts/prettier.sh diff --git a/.depcheckrc b/.depcheckrc index e9383a7985d..e71798a1ce0 100644 --- a/.depcheckrc +++ b/.depcheckrc @@ -1,18 +1,20 @@ ignores: [ # Dependencies that depcheck thinks are unused but are actually used - "@graphql-codegen/*", - "@commitlint/*", - "i18next", + '@graphql-codegen/*', + '@commitlint/*', + 'i18next', # Dependencies that depcheck thinks are missing but are actually present or never used - "@yarnpkg/core", - "@yarnpkg/cli", - "clipanion", - "@yarnpkg/fslib", - "bufferutil", - "utf-8-validate", - "@yarnpkg/parsers", - "@yarnpkg/plugin-git", - "semver", - "typanion", - "turbo-ignore", + '@yarnpkg/core', + '@yarnpkg/cli', + 'clipanion', + '@yarnpkg/fslib', + 'bufferutil', + 'utf-8-validate', + '@yarnpkg/parsers', + '@yarnpkg/plugin-git', + 'semver', + 'typanion', + 'turbo-ignore', + 'prettier', + 'prettier-plugin-organize-imports', ] diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000000..fe5f5f88cd3 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,48 @@ +__generated__ +__mocks__ +.detoxrc.js +.eslintrc +.eslintrc.js +.prettierrc +.tamagui +.turbo +.turbo +.yarn +@types +*.graphql +*.html +*.inc +*.json +*.md +*.yml +babel.config.js +build +craco.config.cjs +cypress +dist +jest-setup.js +jest.config.js +jest.config.js +metro.config.js +node_modules +tsconfig.json +types + +# app/package specific + +# mobile + +apps/mobile/ios +apps/mobile/android + +# extension + +apps/extension/dev + +# packages + +packages/uniswap/codegen.ts + +packages/eslint-config/react.js +packages/eslint-config/restrictedImports.js +packages/eslint-config/native.js diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000000..bd9eac07dbf --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "bracketSameLine": false, + "singleQuote": true, + "printWidth": 120, + "semi": false, + "plugins": [ + "prettier-plugin-organize-imports" + ] +} diff --git a/.yarn/patches/detox-npm-20.18.1-b532b310b4.patch b/.yarn/patches/detox-npm-20.18.1-b532b310b4.patch deleted file mode 100644 index 540b89a9292..00000000000 --- a/.yarn/patches/detox-npm-20.18.1-b532b310b4.patch +++ /dev/null @@ -1,14 +0,0 @@ -diff --git a/android/rninfo.gradle b/android/rninfo.gradle -index c09d2af1d219a4134dc0301e9270aef568730d2b..f1b887cf5dcf56c2f66fff3e6f1b674d48704dac 100644 ---- a/android/rninfo.gradle -+++ b/android/rninfo.gradle -@@ -3,7 +3,8 @@ import groovy.json.JsonSlurper - def getRNVersion = { workingDir -> - println("RNInfo: workingDir=$workingDir") - def jsonSlurper = new JsonSlurper() -- def packageFile = "$workingDir/../node_modules/react-native/package.json" -+ // Fixes patch to node_modules in monorepo project -+ def packageFile = "$workingDir/../../../node_modules/react-native/package.json" - println("RNInfo: reading $packageFile") - Map packageJSON = jsonSlurper.parse(new File(packageFile)) - String rnVersion = packageJSON.get('version') diff --git a/.yarn/patches/detox-npm-20.23.0-6d61110e63.patch b/.yarn/patches/detox-npm-20.23.0-6d61110e63.patch new file mode 100644 index 0000000000000000000000000000000000000000..509f663b9d1533d6c70a556131848e6b68cc571f GIT binary patch literal 2202 zcmd6oQES{d5XTFB^Jln+Ar!h{*R~|fmVI1G;6k}8G%X3-)4?H2Bgc&+8QZ(%x;K7Xj(<70@)g20@@@o_YN`p@T2AOH6A?TKH;Cz}!d z4yO)$XkBX&7^Cr2>NiH(|-FWX2d;{>4|Bmt(IbL{@;mq~peR;aIy1C(TQ1ZGK zhEU>(Ww}ya z)Z%zYg6`Qo4oEC2jjXC`rl~G?#_O_Xl|&(OsY}I>mnzRHdjIFPQe#ZhJ$bi;aEsOd zA&9}Y*0@bk-jTic|E88IQfVd))r?k>(~N6HGD5Lpf=D!ZQHqRcRws#}by*5!a_wiK zm?;WlOooMjQ3$SiDG1k6UDxVRQnxK)|Lpw&yZ;P2%{fbJ9Tz;aCZ8u z$MfTk&C#>1{lVedJh%;qZO-vtLr2;S{?6vHFI-fk{W&v;NpzOspm{;d-PQjDe0h;u3 m7;zS$25hq(hliUhSUZEkZf|yD75pXs(r3?C>q~!r82t (listeners[name] = l)), + getListener: (name) => listeners[name], + triggerListener: (name, ...params) => listeners[name](...params), + resetListeners: () => { + listeners = {} + }, +} + +const useNavigation = () => navigation +let params = {} +const useRoute = () => ({ + params, +}) + +module.exports = { + ...RNN, + useNavigation, + useRoute, + setParams: (p) => (params = { ...params, ...p }), +} diff --git a/apps/mobile/__mocks__/@shopify/react-native-skia.ts b/apps/mobile/__mocks__/@shopify/react-native-skia.ts index 1a3bab1e49d..766d3d19967 100644 --- a/apps/mobile/__mocks__/@shopify/react-native-skia.ts +++ b/apps/mobile/__mocks__/@shopify/react-native-skia.ts @@ -3,10 +3,7 @@ import { View, ViewProps } from 'react-native' // Source: https://github.com/Shopify/react-native-skia/issues/548#issuecomment-1157609472 -const PlainView = ({ - children, - ...props -}: PropsWithChildren): React.CElement => { +const PlainView = ({ children, ...props }: PropsWithChildren): React.CElement => { return React.createElement(View, props, children) } const noop = (): null => null diff --git a/apps/mobile/__mocks__/react-native-context-menu-view.ts b/apps/mobile/__mocks__/react-native-context-menu-view.ts index e1054143fb3..7987c3a4c3f 100644 --- a/apps/mobile/__mocks__/react-native-context-menu-view.ts +++ b/apps/mobile/__mocks__/react-native-context-menu-view.ts @@ -1,10 +1,7 @@ import React, { PropsWithChildren } from 'react' import { View, ViewProps } from 'react-native' -const PlainView = ({ - children, - ...props -}: PropsWithChildren): React.CElement => { +const PlainView = ({ children, ...props }: PropsWithChildren): React.CElement => { return React.createElement(View, props, children) } diff --git a/apps/mobile/__mocks__/react-native-fast-image.ts b/apps/mobile/__mocks__/react-native-fast-image.ts index 92f6067322c..d89ee717f21 100644 --- a/apps/mobile/__mocks__/react-native-fast-image.ts +++ b/apps/mobile/__mocks__/react-native-fast-image.ts @@ -1,10 +1,7 @@ import React, { PropsWithChildren } from 'react' import { Image, ImageProps } from 'react-native' -const PlainImage = ({ - children, - ...props -}: PropsWithChildren): React.CElement => { +const PlainImage = ({ children, ...props }: PropsWithChildren): React.CElement => { return React.createElement(Image, props, children) } diff --git a/apps/mobile/android/app/build.gradle b/apps/mobile/android/app/build.gradle index 92dbe1b82dd..f162a677cea 100644 --- a/apps/mobile/android/app/build.gradle +++ b/apps/mobile/android/app/build.gradle @@ -131,17 +131,17 @@ android { dev { isDefault(true) applicationIdSuffix ".dev" - versionName "1.30" + versionName "1.31" dimension "variant" } beta { applicationIdSuffix ".beta" - versionName "1.30" + versionName "1.31" dimension "variant" } prod { dimension "variant" - versionName "1.30" + versionName "1.31" } } diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/import/SeedPhraseInput.kt b/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/import/SeedPhraseInput.kt index 7f7bda84d6d..0770108f68b 100644 --- a/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/import/SeedPhraseInput.kt +++ b/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/import/SeedPhraseInput.kt @@ -25,6 +25,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester @@ -141,26 +142,29 @@ fun SeedPhraseInput( private fun SeedPhraseError(viewModel: SeedPhraseInputViewModel) { val status = viewModel.status val rnStrings = viewModel.rnStrings + var text = "" if (status is Error) { - val text = when (val error = status.error) { + text = when (val error = status.error) { is InvalidWord -> "${rnStrings.errorInvalidWord} ${error.word}" is NotEnoughWords, TooManyWords -> rnStrings.errorPhraseLength is WrongRecoveryPhrase -> rnStrings.errorWrongPhrase is InvalidPhrase -> rnStrings.errorInvalidPhrase } - Row( - horizontalArrangement = Arrangement.spacedBy(UniswapTheme.spacing.spacing4), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - painter = painterResource(id = R.drawable.uniswap_icon_alert_triangle), - tint = UniswapTheme.colors.statusCritical, - contentDescription = null, - modifier = Modifier.size(16.dp) - ) - Text(text, style = UniswapTheme.typography.body3, color = UniswapTheme.colors.statusCritical) - } + } + + Row( + horizontalArrangement = Arrangement.spacedBy(UniswapTheme.spacing.spacing4), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.alpha(if (text.isEmpty()) 0f else 1f) + ) { + Icon( + painter = painterResource(id = R.drawable.uniswap_icon_alert_triangle), + tint = UniswapTheme.colors.statusCritical, + contentDescription = null, + modifier = Modifier.size(16.dp) + ) + Text(text, style = UniswapTheme.typography.body3, color = UniswapTheme.colors.statusCritical) } } diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/import/SeedPhraseInputViewModel.kt b/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/import/SeedPhraseInputViewModel.kt index 32768d11bf8..f9d839e1dda 100644 --- a/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/import/SeedPhraseInputViewModel.kt +++ b/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/import/SeedPhraseInputViewModel.kt @@ -6,8 +6,13 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.uniswap.EthersRs import com.uniswap.RnEthersRs +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch class SeedPhraseInputViewModel( private val ethersRs: RnEthersRs, @@ -55,13 +60,26 @@ class SeedPhraseInputViewModel( private set var status by mutableStateOf(Status.None) private set + private var validateLastWordTimeout: Long = 1000 + private var validateLastWordJob: Job? = null fun handleInputChange(value: TextFieldValue) { input = value val normalized = normalizeInput(value) val skipLastWord = normalized.lastOrNull() != ' ' - val mnemonic = normalized.trim() + validateInput(normalized, skipLastWord) + + validateLastWordJob?.cancel() + + validateLastWordJob = viewModelScope.launch(Dispatchers.Default) { + delay(validateLastWordTimeout) + validateInput(normalized, false) + } + } + + private fun validateInput(normalizedInput: String, skipLastWord: Boolean) { + val mnemonic = normalizedInput.trim() val words = mnemonic.split(" ") if (words.isEmpty()) { @@ -71,14 +89,14 @@ class SeedPhraseInputViewModel( val isValidLength = words.size in MIN_LENGTH..MAX_LENGTH val firstInvalidWord = EthersRs.findInvalidWord(mnemonic) - if (firstInvalidWord == words.last() && skipLastWord) { - status = Status.None + status = if (firstInvalidWord == words.last() && skipLastWord) { + Status.None } else if (firstInvalidWord.isEmpty() && isValidLength) { - status = Status.Valid + Status.Valid } else if (firstInvalidWord.isNotEmpty()) { - status = Status.Error(MnemonicError.InvalidWord(firstInvalidWord)) + Status.Error(MnemonicError.InvalidWord(firstInvalidWord)) } else { - status = Status.None + Status.None } val canSubmit = status !is Status.Error && mnemonic != "" && firstInvalidWord.isEmpty() diff --git a/apps/mobile/android/settings.gradle b/apps/mobile/android/settings.gradle index 556232de3e9..abce13dff8c 100644 --- a/apps/mobile/android/settings.gradle +++ b/apps/mobile/android/settings.gradle @@ -8,6 +8,6 @@ apply from: new File(["node", "--print", "require.resolve('../../../node_modules useExpoModules() include ':@sentry_react-native' -project(':@sentry_react-native').projectDir = new File('../../../node_modules/@sentry/react-native/android') + include ':detox' project(':detox').projectDir = new File('../../../node_modules/detox/android/detox') diff --git a/apps/mobile/e2e/Home.e2e.ts b/apps/mobile/e2e/Home.e2e.ts new file mode 100644 index 00000000000..b3ecd4a6112 --- /dev/null +++ b/apps/mobile/e2e/Home.e2e.ts @@ -0,0 +1,11 @@ +import { HomeBasicInteractions } from 'e2e/usecases/home/HomeBasicInteractions' +import { WatchWallet } from 'e2e/usecases/onboarding/WatchWallet' + +describe('Home', () => { + beforeEach(async () => { + await device.launchApp() + await WatchWallet() + }) + + it('tests basic home screen interactions', HomeBasicInteractions) +}) diff --git a/apps/mobile/e2e/Onboarding.e2e.ts b/apps/mobile/e2e/Onboarding.e2e.ts index d82f6d0870a..8a23dd2871b 100644 --- a/apps/mobile/e2e/Onboarding.e2e.ts +++ b/apps/mobile/e2e/Onboarding.e2e.ts @@ -8,6 +8,7 @@ describe('Onboarding', () => { }) afterEach(async () => { + await device.clearKeychain() await device.uninstallApp() await device.installApp() }) diff --git a/apps/mobile/e2e/README.md b/apps/mobile/e2e/README.md index 89b2fc88a4e..881e91cc6d2 100644 --- a/apps/mobile/e2e/README.md +++ b/apps/mobile/e2e/README.md @@ -29,6 +29,12 @@ Run ios e2e tests in debug mode: yarn mobile e2e:ios:test:debug ``` +Useful perameters: + +`--testNamePattern test-name` to run a single test, replace `test-name` with test file name without extension e.g.: `Swap` or `Onboarding`. + +`--reuse` to start the test from a current app state. Useful for testing nested screen behaviour without going through onboarding and navigation steps. + #### Release mode To run tests in release mode: @@ -45,6 +51,10 @@ E2E tests should remain as close as possible to production, but sometimes mockin Only mocking entire files is supported at the moment, so you may need to reorganize functions. To mock a file, create a new one with the same name and extension `mock.ts` (e.g. `AnimatedHeader.ts` -> `AnimatedHeader.mock.ts`) in the same directory. The metro bundler will override any file that has a `mock.ts` equivalent in Detox runs. -Native views, libraries relying on the native code and libraries utilizing long-running asynchronouse background processes like sentry are not supported by detox currently. Imports mocking is unfortunatelly not supported by detox yet. If such problems occur, the entire component using problematic library needs to be mocked or a component exposing only targeted library needs to be created and then it can be mocked, precisely replacing only targeted library. +Android native views based on jetpack compose and libraries utilizing long-running asynchronouse background processes like sentry are not supported by detox currently. Imports mocking is unfortunatelly not supported by detox yet. If such problems occur, the entire component using problematic library needs to be mocked or a component exposing only targeted library needs to be created and then it can be mocked, precisely replacing only targeted library. + +To mock a component for specific platform follow this pattern: +iOS: `AnimatedHeader.ts` -> `AnimatedHeader.ios.mock.ts` +Android: `AnimatedHeader.ts` -> `AnimatedHeader.android.mock.ts` Read more here https://wix.github.io/Detox/docs/guide/mocking/ diff --git a/apps/mobile/e2e/usecases/CreateNewWallet.ts b/apps/mobile/e2e/usecases/CreateNewWallet.ts deleted file mode 100644 index a35d60bf3da..00000000000 --- a/apps/mobile/e2e/usecases/CreateNewWallet.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { by, element, expect } from 'detox' -import { TestWallet } from 'e2e/utils/fixtures' -import { ElementName } from 'uniswap/src/features/telemetry/constants' - -export function CreateNewWallet(): void { - it('creates a new wallet', async () => { - // Selects "Create a new wallet" option on the landing screen - await element(by.id(ElementName.CreateAccount)).tap() - - // Skips unitag flow - await element(by.id(ElementName.Skip)).tap() - - // Taps "Let's keep it safe" on QRAnimation screen - await element(by.id(ElementName.Next)).tap() - - // Check is both manual and cloud backup options are available on BackupScreen - await expect(element(by.id(ElementName.AddCloudBackup))).toBeVisible() - await expect(element(by.id(ElementName.AddManualBackup))).toBeVisible() - - // Picks "Manual backup" option - await element(by.id(ElementName.AddManualBackup)).tap() - - // Checks if ManualBackupScreen warning displays and taps "I'm ready" button - await expect(element(by.id(ElementName.Confirm))).toBeVisible() - await element(by.id(ElementName.Confirm)).tap() - - // Taps continue on ManualBackupScreen - await element(by.id(ElementName.Next)).tap() - - // Taps continue on manual backup confirmation screen. It is replaced by mock because detox - // can't interact with native screens - await element(by.id(ElementName.Continue)).tap() - - // Skips notification setup by tapping "Maybe later" button - await element(by.id(ElementName.Skip)).tap() - - // Skips biometrics setup by tapping "Maybe later" button - await element(by.id(ElementName.Skip)).tap() - - // Confirms by tapping "Skip" on warning modal - await element(by.id(ElementName.Confirm)).tap() - - // Confirms if user successfuly finished create new wallet flow by checking if provided wallet name is - // displayed and other - await expect(element(by.text(TestWallet.name))).toBeVisible() - await expect(element(by.id(ElementName.Swap))).toBeVisible() - await expect(element(by.id(ElementName.SearchTokensAndWallets))).toBeVisible() - }) -} diff --git a/apps/mobile/e2e/usecases/home/HomeBasicInteractions.ts b/apps/mobile/e2e/usecases/home/HomeBasicInteractions.ts new file mode 100644 index 00000000000..339b10d1cd1 --- /dev/null +++ b/apps/mobile/e2e/usecases/home/HomeBasicInteractions.ts @@ -0,0 +1,75 @@ +import { by, element, expect } from 'detox' +import { TestWatchedWallet } from 'e2e/utils/fixtures' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' + +export async function HomeBasicInteractions(): Promise { + await expect(element(by.text(TestWatchedWallet.displayName))).toBeVisible() + await expect(element(by.id(TestID.Swap))).toBeVisible() + await expect(element(by.id(TestID.SearchTokensAndWallets))).toBeVisible() + + // opens AccountSwitcherModal by clicking on account avatar + await expect(element(by.id(TestID.AccountHeaderAvatar))).toBeVisible() + + // checks if portfolio balance is visible + await expect(element(by.id(TestID.PortfolioBalance))).toBeVisible() + + // copies wallet address from AccountSwitcherModal + await element(by.id(TestID.AccountHeaderCopyAddress)).tap() + + // checks if notification toast is visible with title "Address copied" + await expect(element(by.id(TestID.NotificationToastTitle))).toBeVisible() + await expect(element(by.id(TestID.NotificationToastTitle))).toHaveText('Address copied') + + // checks if list was rendered properly by checking if the first item is visible + await expect(element(by.id('token-list-item-0'))).toBeVisible() + + // scrolls to the bottom of the token list + await element(by.id('token-list-item-0')).swipe('up') + + // checks if only tabs headers are visible then scrolled to bottom + await expect(element(by.id(TestID.AccountHeaderAvatar))).not.toBeVisible() + await expect(element(by.id(TestID.PortfolioBalance))).not.toBeVisible() + // for some reason react-native-tab-view renders headers twice, thats why first matching item was picked + await expect(element(by.id('home-tab-Tokens')).atIndex(0)).toBeVisible() + await expect(element(by.id('home-tab-NFTs')).atIndex(0)).toBeVisible() + await expect(element(by.id('home-tab-Activity')).atIndex(0)).toBeVisible() + + // checks if the first item of hidden list is not visible + await expect(element(by.id('token-list-item-0'))).not.toBeVisible() + + // hidden item does not exist + await expect(element(by.id('token-list-item-25'))).not.toExist() + + // taps on "show" button to show hidden elements + await element(by.id(TestID.ShowHiddenTokens)).tap() + + // checks if first hidden element is visible + await expect(element(by.id('token-list-item-25'))).toExist() + + // taps on "hide" button to show hidden elements + await element(by.id(TestID.ShowHiddenTokens)).tap() + + // checks if first item of the hidden item is not visible again + await expect(element(by.id('token-list-item-25'))).not.toExist() + + // switches to NFTs tab + await element(by.id('home-tab-NFTs')).atIndex(0).tap() + + // checks is if tokens are visible + await expect(element(by.id('nfts-list-item-0'))).toBeVisible() + + // switches to Activity tab + await element(by.id('home-tab-Activity')).atIndex(0).tap() + + // checks is if tokens are visible + await expect(element(by.id('activity-list-item-0'))).toBeVisible() + + // switches back to tokens tab + await element(by.id('home-tab-Tokens')).atIndex(0).tap() + + // scrolls to the bottom of the token list + await element(by.id('token-list-item-16')).swipe('down') + + // checks if list of tokens was rendered properly by checking first token visibility + await expect(element(by.id('token-list-item-0'))).toBeVisible() +} diff --git a/apps/mobile/e2e/usecases/onboarding/CreateNewWallet.ts b/apps/mobile/e2e/usecases/onboarding/CreateNewWallet.ts index 623fe569392..3adfa60421b 100644 --- a/apps/mobile/e2e/usecases/onboarding/CreateNewWallet.ts +++ b/apps/mobile/e2e/usecases/onboarding/CreateNewWallet.ts @@ -1,47 +1,47 @@ import { by, element, expect } from 'detox' import { TestWallet } from 'e2e/utils/fixtures' -import { ElementName } from 'uniswap/src/features/telemetry/constants' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' export async function CreateNewWallet(): Promise { // Selects "Create a new wallet" option on the landing screen - await element(by.id(ElementName.CreateAccount)).tap() + await element(by.id(TestID.CreateAccount)).tap() // Skips unitag flow - await element(by.id(ElementName.Skip)).tap() + await element(by.id(TestID.Skip)).tap() // Taps "Let's keep it safe" on QRAnimation screen - await element(by.id(ElementName.Next)).tap() + await element(by.id(TestID.Next)).tap() // Check is both manual and cloud backup options are available on BackupScreen - await expect(element(by.id(ElementName.AddCloudBackup))).toBeVisible() - await expect(element(by.id(ElementName.AddManualBackup))).toBeVisible() + await expect(element(by.id(TestID.AddCloudBackup))).toBeVisible() + await expect(element(by.id(TestID.AddManualBackup))).toBeVisible() // Picks "Manual backup" option - await element(by.id(ElementName.AddManualBackup)).tap() + await element(by.id(TestID.AddManualBackup)).tap() // Checks if ManualBackupScreen warning displays and taps "I'm ready" button - await expect(element(by.id(ElementName.Confirm))).toBeVisible() - await element(by.id(ElementName.Confirm)).tap() + await expect(element(by.id(TestID.Confirm))).toBeVisible() + await element(by.id(TestID.Confirm)).tap() // Taps continue on ManualBackupScreen - await element(by.id(ElementName.Next)).tap() + await element(by.id(TestID.Next)).tap() // Taps continue on manual backup confirmation screen. It is replaced by mock because detox // can't interact with native screens - await element(by.id(ElementName.Continue)).tap() + await element(by.id(TestID.Continue)).tap() // Skips notification setup by tapping "Maybe later" button - await element(by.id(ElementName.Skip)).tap() + await element(by.id(TestID.Skip)).tap() // Skips biometrics setup by tapping "Maybe later" button - await element(by.id(ElementName.Skip)).tap() + await element(by.id(TestID.Skip)).tap() // Confirms by tapping "Skip" on warning modal - await element(by.id(ElementName.Confirm)).tap() + await element(by.id(TestID.Confirm)).tap() // Confirms if user successfuly finished create new wallet flow by checking if provided wallet name is // displayed and other await expect(element(by.text(TestWallet.name))).toBeVisible() - await expect(element(by.id(ElementName.Swap))).toBeVisible() - await expect(element(by.id(ElementName.SearchTokensAndWallets))).toBeVisible() + await expect(element(by.id(TestID.Swap))).toBeVisible() + await expect(element(by.id(TestID.SearchTokensAndWallets))).toBeVisible() } diff --git a/apps/mobile/e2e/usecases/onboarding/ImportWallet.ts b/apps/mobile/e2e/usecases/onboarding/ImportWallet.ts index b23cea7e47b..539b64cb548 100644 --- a/apps/mobile/e2e/usecases/onboarding/ImportWallet.ts +++ b/apps/mobile/e2e/usecases/onboarding/ImportWallet.ts @@ -1,43 +1,42 @@ import { by, element, expect } from 'detox' import { TestWallet } from 'e2e/utils/fixtures' -import { ElementName } from 'uniswap/src/features/telemetry/constants' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' export async function ImportWallet(): Promise { // Selects "Add an existing wallet" option on the landing screen - await element(by.id(ElementName.ImportAccount)).tap() + await element(by.id(TestID.ImportAccount)).tap() // Picks Import a wallet by recovery phase option - await element(by.id(ElementName.OnboardingImportSeedPhrase)).tap() + await element(by.id(TestID.OnboardingImportSeedPhrase)).tap() // Checks if recovery phase input is in focus and types recovery phrase in - await expect(element(by.id(ElementName.ImportAccountInput))).toBeFocused() - await element(by.id(ElementName.ImportAccountInput)).typeText(TestWallet.recoveryPhrase) + await element(by.id(TestID.ImportAccountInput)).typeText(TestWallet.recoveryPhrase) // Taps continue navigating to SelectWalletScreen - await element(by.id(ElementName.Continue)).tap() + await element(by.id(TestID.Continue)).tap() // Taps continue on SelectWalletScreen - await waitFor(element(by.id(`${ElementName.WalletCard}-1`))) + await waitFor(element(by.id(`${TestID.WalletCard}-1`))) .toBeVisible() .withTimeout(10000) - await element(by.id(ElementName.Next)).tap() + await element(by.id(TestID.Next)).tap() // Skips cloud backup step on BackupScreen by clicking "Maybe later" - await expect(element(by.id(ElementName.AddCloudBackup))).toBeVisible() - await element(by.id(ElementName.Next)).tap() + await expect(element(by.id(TestID.AddCloudBackup))).toBeVisible() + await element(by.id(TestID.Next)).tap() // Skips notification setup by tapping "Maybe later" button - await element(by.id(ElementName.Skip)).tap() + await element(by.id(TestID.Skip)).tap() // Skips biometrics setup by tapping "Maybe later" button - await element(by.id(ElementName.Skip)).tap() + await element(by.id(TestID.Skip)).tap() // Confirms by tapping "Skip" on warning modal - await element(by.id(ElementName.Confirm)).tap() + await element(by.id(TestID.Confirm)).tap() // Confirms if user successfuly finished create new wallet flow by checking if provided wallet name is // displayed and other await expect(element(by.text(TestWallet.name))).toBeVisible() - await expect(element(by.id(ElementName.Swap))).toBeVisible() - await expect(element(by.id(ElementName.SearchTokensAndWallets))).toBeVisible() + await expect(element(by.id(TestID.Swap))).toBeVisible() + await expect(element(by.id(TestID.SearchTokensAndWallets))).toBeVisible() } diff --git a/apps/mobile/e2e/usecases/onboarding/WatchWallet.ts b/apps/mobile/e2e/usecases/onboarding/WatchWallet.ts index 0eee4c4def7..4b389146a83 100644 --- a/apps/mobile/e2e/usecases/onboarding/WatchWallet.ts +++ b/apps/mobile/e2e/usecases/onboarding/WatchWallet.ts @@ -1,23 +1,23 @@ import { by, element, expect } from 'detox' import { TestWatchedWallet } from 'e2e/utils/fixtures' -import { ElementName } from 'uniswap/src/features/telemetry/constants' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' export async function WatchWallet(): Promise { // Selects "Add an existing wallet" option on the landing screen - await element(by.id(ElementName.ImportAccount)).tap() + await element(by.id(TestID.ImportAccount)).tap() // Picks Watch a wallet option on ImportMethodScreen - await element(by.id(ElementName.WatchWallet)).tap() + await element(by.id(TestID.WatchWallet)).tap() // Checks if wallet name is in focus and types recovery phrase in - await expect(element(by.id(ElementName.ImportAccountInput))).toBeFocused() - await element(by.id(ElementName.ImportAccountInput)).typeText(TestWatchedWallet.ens) + await expect(element(by.id(TestID.ImportAccountInput))).toBeFocused() + await element(by.id(TestID.ImportAccountInput)).typeText(TestWatchedWallet.ens) // Confirms the entered wallet name by tapping "continue" - await element(by.id(ElementName.Next)).tap() + await element(by.id(TestID.Next)).tap() // Checks if Home screen is displayed with a proper user name await expect(element(by.text(TestWatchedWallet.displayName))).toBeVisible() - await expect(element(by.id(ElementName.Swap))).toBeVisible() - await expect(element(by.id(ElementName.SearchTokensAndWallets))).toBeVisible() + await expect(element(by.id(TestID.Swap))).toBeVisible() + await expect(element(by.id(TestID.SearchTokensAndWallets))).toBeVisible() } diff --git a/apps/mobile/e2e/usecases/swap/SwapBasicInteractions.ts b/apps/mobile/e2e/usecases/swap/SwapBasicInteractions.ts index adf6233cd46..7d7414f0c65 100644 --- a/apps/mobile/e2e/usecases/swap/SwapBasicInteractions.ts +++ b/apps/mobile/e2e/usecases/swap/SwapBasicInteractions.ts @@ -1,86 +1,88 @@ import { by, element, expect } from 'detox' import { TestWatchedWallet } from 'e2e/utils/fixtures' -import { ElementName } from 'uniswap/src/features/telemetry/constants' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' export async function SwapBasicInteractions(): Promise { // Navigate to swap screen - await element(by.id(ElementName.Swap)).tap() + await element(by.id(TestID.Swap)).tap() // Checks if currency input is selected - await expect(element(by.id(ElementName.AmountInputIn))).toBeFocused() + await expect(element(by.id(TestID.AmountInputIn))).toBeFocused() // Checks if "Max" button is available - await expect(element(by.id(ElementName.SetMaxInput))).toBeVisible() + await expect(element(by.id(TestID.SetMaxInput))).toBeVisible() // Opens token selector modal on Swap screen - await element(by.id(ElementName.ChooseOutputToken)).tap() + await element(by.id(TestID.ChooseOutputToken)).tap() // Picks usdc output token await element(by.text('USDC')).atIndex(0).tap() - // Taps 1234567890 number into swap input - await element(by.id('decimal-pad-1')).tap() - await element(by.id('decimal-pad-2')).tap() - await element(by.id('decimal-pad-3')).tap() - await element(by.id('decimal-pad-4')).tap() - await element(by.id('decimal-pad-5')).tap() - await element(by.id('decimal-pad-6')).tap() - await element(by.id('decimal-pad-7')).tap() - await element(by.id('decimal-pad-8')).tap() + // Taps .98765432101 into the swap input await element(by.id('decimal-pad-.')).tap() - await element(by.id('decimal-pad-0')).tap() await element(by.id('decimal-pad-9')).tap() + await element(by.id('decimal-pad-8')).tap() + await element(by.id('decimal-pad-7')).tap() + await element(by.id('decimal-pad-6')).tap() + await element(by.id('decimal-pad-5')).tap() + await element(by.id('decimal-pad-4')).tap() + await element(by.id('decimal-pad-3')).tap() + await element(by.id('decimal-pad-2')).tap() + await element(by.id('decimal-pad-1')).tap() + await element(by.id('decimal-pad-0')).tap() await element(by.id('decimal-pad-1')).tap() + + // Taps a backspace button leaving .9876543210 value in the input field await element(by.id('decimal-pad-backspace')).tap() - // Checks if expected input expected value: "12345678.09" - await expect(element(by.id(ElementName.AmountInputIn))).toHaveValue('12345678.09') + // Checks if expected input expected value: ".9876543210" + await expect(element(by.id(TestID.AmountInputIn))).toHaveText('.9876543210') // Checks if expected error is displayed await expect(element(by.text('You don’t have enough ETH'))).toBeVisible() // Checks if expected output expected value: "0" - await expect(element(by.id(ElementName.AmountInputOut))).not.toHaveValue('0') + await expect(element(by.id(TestID.AmountInputOut))).not.toHaveText('0') // Swaps input and output currencies - await element(by.id(ElementName.SwitchCurrenciesButton)).tap() + await element(by.id(TestID.SwitchCurrenciesButton)).tap() // Checks if expected input expected value: "0" - await expect(element(by.id(ElementName.AmountInputIn))).toHaveValue('0') + await expect(element(by.id(TestID.AmountInputIn))).not.toHaveText('0') // Checks if expected error is displayed - await expect(element(by.text('Not enough liquidity'))).toBeVisible() + await expect(element(by.text('You don’t have enough USDC'))).toBeVisible() - // Checks if expected output expected value: "12345678.09" - await expect(element(by.id(ElementName.AmountInputOut))).toHaveValue('12345678.09') + // Checks if expected output expected value: ".9876543210" + await expect(element(by.id(TestID.AmountInputOut))).toHaveText('.9876543210') // Swaps input and output currencies - await element(by.id(ElementName.SwitchCurrenciesButton)).tap() + await element(by.id(TestID.SwitchCurrenciesButton)).tap() // Selects currency output - await element(by.id(ElementName.AmountInputOut)).tap() + await element(by.id(TestID.AmountInputOut)).tap() // Clears the output field - await element(by.id(ElementName.AmountInputOut)).clearText() + await element(by.id(TestID.AmountInputOut)).clearText() await element(by.id('decimal-pad-1')).tap() await element(by.id('decimal-pad-2')).tap() await element(by.id('decimal-pad-3')).tap() // Checks if output has expected value: "123" - await expect(element(by.id(ElementName.AmountInputOut))).toHaveValue('123') + await expect(element(by.id(TestID.AmountInputOut))).toHaveText('123') // Checks if expected input value to be cleared - await expect(element(by.id(ElementName.AmountInputIn))).not.toHaveValue('0') + await expect(element(by.id(TestID.AmountInputIn))).not.toHaveText('0') // Checks dollar value to be visible await expect(element(by.text('$123.00'))).toBeVisible() // Swipes swap modal by dragging down SwapFormHeader - await element(by.id(ElementName.SwapFormHeader)).swipe('down', 'fast', 0.75) + await element(by.id(TestID.SwapFormHeader)).swipe('down', 'fast', 0.75) // Checks if Home screen is visible and not covered await expect(element(by.text(TestWatchedWallet.displayName))).toBeVisible() - await expect(element(by.id(ElementName.Swap))).toBeVisible() - await expect(element(by.id(ElementName.SearchTokensAndWallets))).toBeVisible() + await expect(element(by.id(TestID.Swap))).toBeVisible() + await expect(element(by.id(TestID.SearchTokensAndWallets))).toBeVisible() } diff --git a/apps/mobile/e2e/utils/fixtures.ts b/apps/mobile/e2e/utils/fixtures.ts index 1f8f2a49eac..c13cc6161ff 100644 --- a/apps/mobile/e2e/utils/fixtures.ts +++ b/apps/mobile/e2e/utils/fixtures.ts @@ -1,7 +1,6 @@ export const TestWallet = { name: 'Wallet 1', - recoveryPhrase: - 'oak reduce strong borrow control funny library disagree radio clarify degree pistol', + recoveryPhrase: 'oak reduce strong borrow control funny library disagree radio clarify degree pistol', } export const TestWatchedWallet = { diff --git a/apps/mobile/ios/Uniswap.xcodeproj/project.pbxproj b/apps/mobile/ios/Uniswap.xcodeproj/project.pbxproj index 881767ca5a4..6aea6512149 100644 --- a/apps/mobile/ios/Uniswap.xcodeproj/project.pbxproj +++ b/apps/mobile/ios/Uniswap.xcodeproj/project.pbxproj @@ -2534,7 +2534,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.30; + MARKETING_VERSION = 1.31; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; @@ -2580,7 +2580,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.30; + MARKETING_VERSION = 1.31; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.widgets; @@ -2626,7 +2626,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.30; + MARKETING_VERSION = 1.31; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.widgets; @@ -2672,7 +2672,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.30; + MARKETING_VERSION = 1.31; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.widgets; @@ -2714,7 +2714,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.30; + MARKETING_VERSION = 1.31; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; @@ -2757,7 +2757,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.30; + MARKETING_VERSION = 1.31; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.WidgetIntentExtension; @@ -2800,7 +2800,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.30; + MARKETING_VERSION = 1.31; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.WidgetIntentExtension; @@ -2843,7 +2843,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.30; + MARKETING_VERSION = 1.31; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.WidgetIntentExtension; @@ -2879,7 +2879,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.30; + MARKETING_VERSION = 1.31; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -2917,7 +2917,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.30; + MARKETING_VERSION = 1.31; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -3103,7 +3103,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.30; + MARKETING_VERSION = 1.31; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; @@ -3147,7 +3147,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.30; + MARKETING_VERSION = 1.31; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.OneSignalNotificationServiceExtension; @@ -3251,7 +3251,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.30; + MARKETING_VERSION = 1.31; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -3322,7 +3322,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.30; + MARKETING_VERSION = 1.31; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.OneSignalNotificationServiceExtension; @@ -3426,7 +3426,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.30; + MARKETING_VERSION = 1.31; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -3497,7 +3497,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.30; + MARKETING_VERSION = 1.31; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.OneSignalNotificationServiceExtension; diff --git a/apps/mobile/ios/Uniswap/Onboarding/Import/SeedPhraseInputManager.m b/apps/mobile/ios/Uniswap/Onboarding/Import/SeedPhraseInputManager.m index 2342c17486a..71430dc2682 100644 --- a/apps/mobile/ios/Uniswap/Onboarding/Import/SeedPhraseInputManager.m +++ b/apps/mobile/ios/Uniswap/Onboarding/Import/SeedPhraseInputManager.m @@ -16,6 +16,7 @@ @interface RCT_EXTERN_MODULE(SeedPhraseInputManager, RCTViewManager) RCT_EXPORT_VIEW_PROPERTY(onPasteStart, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onPasteEnd, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onHeightMeasured, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(testID, NSString?) RCT_EXTERN_METHOD(handleSubmit: (nonnull NSNumber *)node) @end diff --git a/apps/mobile/ios/Uniswap/Onboarding/Import/SeedPhraseInputView.swift b/apps/mobile/ios/Uniswap/Onboarding/Import/SeedPhraseInputView.swift index f2dda700695..55781b4ef48 100644 --- a/apps/mobile/ios/Uniswap/Onboarding/Import/SeedPhraseInputView.swift +++ b/apps/mobile/ios/Uniswap/Onboarding/Import/SeedPhraseInputView.swift @@ -78,6 +78,12 @@ class SeedPhraseInputView: UIView { set { vc.rootView.viewModel.onHeightMeasured = newValue } get { return vc.rootView.viewModel.onHeightMeasured } } + + @objc + var testID: String? { + get { vc.rootView.viewModel.testID } + set { vc.rootView.viewModel.testID = newValue } + } @objc var handleSubmit: () -> Void { @@ -112,12 +118,14 @@ struct SeedPhraseInput: View { VStack(spacing: 12) { VStack { VStack { - ZStack(alignment: .leading) { + ZStack(alignment: .topLeading) { TextEditor(text: $viewModel.input) .focused($focused) .autocorrectionDisabled() .textInputAutocapitalization(.never) .modifier(TextEditModifier()) + .frame(minHeight: 96) // 120 - 2 * 12 for padding + .accessibility(identifier: viewModel.testID ?? "import-account-input") if (viewModel.input.isEmpty) { Text(viewModel.strings.inputPlaceholder) @@ -131,7 +139,6 @@ struct SeedPhraseInput: View { .fixedSize(horizontal: false, vertical: true) .background(Colors.surface1) .padding(12) // Adds to default TextEditor padding 8 - .frame(minHeight: 120, alignment: .top) .cornerRadius(16) .overlay( RoundedRectangle(cornerRadius: 16) @@ -141,7 +148,7 @@ struct SeedPhraseInput: View { .onTapGesture { focused = true } - .onAppear() { + .onAppear { DispatchQueue.main.async { focused = true } @@ -189,7 +196,7 @@ struct SeedPhraseInput: View { } ) } - .frame(maxWidth:.infinity, maxHeight: .infinity, alignment: .top) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) .font(font) } diff --git a/apps/mobile/ios/Uniswap/Onboarding/Import/SeedPhraseInputViewModel.swift b/apps/mobile/ios/Uniswap/Onboarding/Import/SeedPhraseInputViewModel.swift index 62432057f05..b6d9f2fc896 100644 --- a/apps/mobile/ios/Uniswap/Onboarding/Import/SeedPhraseInputViewModel.swift +++ b/apps/mobile/ios/Uniswap/Onboarding/Import/SeedPhraseInputViewModel.swift @@ -36,6 +36,9 @@ class SeedPhraseInputViewModel: ObservableObject { // Following block of variables will come from RN @Published var targetMnemonicId: String? = nil + + @Published var testID: String? = nil + @Published var rawRNStrings: Dictionary = Dictionary() { didSet { strings = ReactNativeStrings( @@ -62,11 +65,15 @@ class SeedPhraseInputViewModel: ObservableObject { @Published var onPasteEnd: RCTDirectEventBlock = { _ in } @Published var onHeightMeasured: RCTDirectEventBlock = { _ in } + private var lastWordValidationTimer: Timer? + private let lastWordValidationTimeout: TimeInterval = 1.0 + @Published var input = "" { didSet { - validateInput() + handleInputChange() } } + @Published var skipLastWord = true @Published var status: Status = .none @Published var error: MnemonicError? = nil @@ -135,10 +142,26 @@ class SeedPhraseInputViewModel: ObservableObject { return value.trimmingCharacters(in: .whitespacesAndNewlines) } - private func validateInput() { + private func handleInputChange() { let normalized = normalizeInput(value: input) let skipLastWord = normalized.last != " " - let mnemonic = trimInput(value: normalized) + validateInput(normalizedInput: normalized, skipLastWord: skipLastWord) + + lastWordValidationTimer?.invalidate() + + if (skipLastWord) { + lastWordValidationTimer = Timer.scheduledTimer( + withTimeInterval: lastWordValidationTimeout, + repeats: false) { _ in + DispatchQueue.global(qos: .background).async { + self.validateInput(normalizedInput: normalized, skipLastWord: false) + } + } + } + } + + private func validateInput(normalizedInput: String, skipLastWord: Bool) { + let mnemonic = trimInput(value: normalizedInput) let words = mnemonic.components(separatedBy: " ") diff --git a/apps/mobile/ios/Uniswap/RNEthersRs/RNEthersRS.swift b/apps/mobile/ios/Uniswap/RNEthersRs/RNEthersRS.swift index 2454ffb7a4a..433e2835494 100644 --- a/apps/mobile/ios/Uniswap/RNEthersRs/RNEthersRS.swift +++ b/apps/mobile/ios/Uniswap/RNEthersRs/RNEthersRS.swift @@ -81,8 +81,8 @@ class RNEthersRS: NSObject { resolve(res) return } - let err = NSError.init() - reject("error", "error", err) + + reject("Unable to import new mnemonic", "Failed store new mnemonic in ethers library", nil) return } @@ -192,8 +192,7 @@ class RNEthersRS: NSObject { let mnemonic = retrieveMnemonic(mnemonicId: mnemonicId) if (mnemonic == nil) { - let err = NSError.init() - reject("Mnemonic not found", "Could not find mnemonic for given mnemonicId", err) + reject("Mnemonic not found", "Could not find mnemonic for given mnemonicId", nil) return } diff --git a/apps/mobile/jest-setup.js b/apps/mobile/jest-setup.js index b41fb54b1cf..eead5ec6431 100644 --- a/apps/mobile/jest-setup.js +++ b/apps/mobile/jest-setup.js @@ -3,6 +3,7 @@ import 'core-js' // necessary so setImmediate works in tests import 'uniswap/src/i18n/i18n' // Uses real translations for tests +import 'utilities/src/logger/mocks' import mockRNCNetInfo from '@react-native-community/netinfo/jest/netinfo-mock.js' import { localizeMock as mockRNLocalize } from 'react-native-localize/mock' @@ -69,10 +70,6 @@ jest.mock('react-native', () => { return RN }) -jest.mock('expo-localization', () => ({ - getLocales: jest.fn(() => [{ languageCode: 'en', countryCode: 'US' }]), -})) - jest.mock('react-native-safe-area-context', () => ({ useSafeAreaInsets: jest.fn().mockImplementation(() => ({})), useSafeAreaFrame: jest.fn().mockImplementation(() => ({})), @@ -125,8 +122,3 @@ jest.mock('wallet/src/features/appearance/hooks', () => { useSelectedColorScheme: () => 'light', } }) - -jest.mock('wallet/src/features/fiatOnRamp/api', () => ({ - ...jest.requireActual('wallet/src/features/fiatOnRamp/api'), - useFiatOnRampIpAddressQuery: jest.fn().mockReturnValue({}), -})) diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 2e89fb6594f..26f39ae3b5a 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -33,7 +33,7 @@ "link:assets": "react-native-asset", "graphql:generate:swift": "cd ios && ./Pods/Apollo/apollo-ios-cli generate", "hardhat": "hardhat node", - "check:circular": "../../scripts/check-circular-imports.sh ./src/app/App.tsx 6", + "check:circular": "../../scripts/check-circular-imports.sh ./src/app/App.tsx 5", "ios": "yarn ios:prebuild && SKIP_BUNDLING=1 react-native run-ios", "ios:prebuild": "yarn graphql:generate:swift && cd ios/WidgetsCore/MobileSchema && rm -rf !(README.md) && cd ../../.. && yarn graphql:generate:swift && yarn env:local:copy:swift", "ios:smol": "SKIP_BUNDLING=1 react-native run-ios --simulator=\"iPhone SE (3rd generation)\"", @@ -41,6 +41,7 @@ "ios:beta": "react-native run-ios --configuration Beta", "ios:bundle": "react-native bundle --entry-file='index.js' --bundle-output='./ios/main.jsbundle' --dev=false --platform='ios' --assets-dest='./ios'", "ios:release": "react-native run-ios --configuration Release", + "format": "../../scripts/prettier.sh", "lint": "eslint . --ext .js,.jsx,.ts,.tsx --max-warnings=0", "lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix", "start": "NODE_ENV=development react-native start", @@ -82,7 +83,7 @@ "@shopify/react-native-performance-navigation": "3.0.0", "@shopify/react-native-skia": "1.2.0", "@uniswap/analytics": "1.7.0", - "@uniswap/analytics-events": "2.32.0", + "@uniswap/analytics-events": "2.34.0", "@uniswap/ethers-rs-mobile": "0.0.5", "@uniswap/sdk-core": "5.3.0", "@uniswap/v3-sdk": "3.13.0", @@ -170,7 +171,7 @@ "babel-plugin-module-resolver": "5.0.0", "babel-plugin-react-native-web": "0.17.5", "core-js": "2.6.12", - "detox": "20.18.1", + "detox": "20.23.0", "eslint": "8.44.0", "expo-modules-core": "1.11.13", "hardhat": "2.14.0", diff --git a/apps/mobile/scripts/podinstall.sh b/apps/mobile/scripts/podinstall.sh index e6b5f42feaa..fc7d144225a 100755 --- a/apps/mobile/scripts/podinstall.sh +++ b/apps/mobile/scripts/podinstall.sh @@ -1,2 +1,26 @@ #!/bin/bash -cd ios/ && bundle install && bundle exec pod install && cd .. + +set -e + +REQUIRED_XCODE_VERSION="15.2" + +check_xcode_version() { + local current_version=$(xcodebuild -version | grep "Xcode" | cut -d' ' -f2) + if [ "$current_version" != "$REQUIRED_XCODE_VERSION" ]; then + echo "Error: Xcode version mismatch" + echo "Required: $REQUIRED_XCODE_VERSION" + echo "Current: $current_version" + exit 1 + fi + echo "Xcode version check passed: $current_version" +} + +# Check Xcode version +check_xcode_version + +# Install pods +cd ios/ +bundle install +bundle exec pod install +cd .. + diff --git a/apps/mobile/src/app/App.tsx b/apps/mobile/src/app/App.tsx index 3ab664dd515..95b75963cda 100644 --- a/apps/mobile/src/app/App.tsx +++ b/apps/mobile/src/app/App.tsx @@ -12,9 +12,10 @@ import { GestureHandlerRootView } from 'react-native-gesture-handler' import { MMKV } from 'react-native-mmkv' import { SafeAreaProvider } from 'react-native-safe-area-context' import { enableFreeze } from 'react-native-screens' +import { useDispatch } from 'react-redux' import { PersistGate } from 'redux-persist/integration/react' import { MobileWalletNavigationProvider } from 'src/app/MobileWalletNavigationProvider' -import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { useAppSelector } from 'src/app/hooks' import { AppModals } from 'src/app/modals/AppModals' import { NavigationContainer } from 'src/app/navigation/NavigationContainer' import { useIsPartOfNavigationTree } from 'src/app/navigation/hooks' @@ -36,11 +37,7 @@ import { setI18NUserDefaults, } from 'src/features/widgets/widgets' import { useAppStateTrigger } from 'src/utils/useAppStateTrigger' -import { - getSentryEnvironment, - getSentryTracesSamplingRate, - getStatsigEnvironmentTier, -} from 'src/utils/version' +import { getSentryEnvironment, getSentryTracesSamplingRate, getStatsigEnvironmentTier } from 'src/utils/version' import { flexStyles, useIsDarkMode } from 'ui/src' import { config } from 'uniswap/src/config' import { uniswapUrls } from 'uniswap/src/constants/urls' @@ -48,19 +45,14 @@ import { DUMMY_STATSIG_SDK_KEY, StatsigCustomAppValue } from 'uniswap/src/featur import { Experiments } from 'uniswap/src/features/gating/experiments' import { WALLET_FEATURE_FLAG_NAMES } from 'uniswap/src/features/gating/flags' import { loadStatsigOverrides } from 'uniswap/src/features/gating/overrides/customPersistedOverrides' -import { - Statsig, - StatsigOptions, - StatsigProvider, - StatsigUser, -} from 'uniswap/src/features/gating/sdk/statsig' +import { Statsig, StatsigOptions, StatsigProvider, StatsigUser } from 'uniswap/src/features/gating/sdk/statsig' import Trace from 'uniswap/src/features/telemetry/Trace' import { MobileEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { UnitagUpdaterContextProvider } from 'uniswap/src/features/unitags/context' import i18n from 'uniswap/src/i18n/i18n' import { CurrencyId } from 'uniswap/src/types/currency' -import { isDetoxBuild } from 'utilities/src/environment' +import { isDetoxBuild } from 'utilities/src/environment/constants' import { registerConsoleOverrides } from 'utilities/src/logger/console' import { logger } from 'utilities/src/logger/logger' import { useAsyncData } from 'utilities/src/react/hooks' @@ -173,7 +165,8 @@ function App(): JSX.Element | null { + useIsPartOfNavigationTree={useIsPartOfNavigationTree} + > @@ -195,7 +188,7 @@ function SentryTags({ children }: PropsWithChildren): JSX.Element { for (const experiment of Object.values(Experiments)) { Sentry.setTag( `experiment.${experiment}`, - Statsig.getExperimentWithExposureLoggingDisabled(experiment).getGroupName() + Statsig.getExperimentWithExposureLoggingDisabled(experiment).getGroupName(), ) } }, []) @@ -237,7 +230,8 @@ function AppOuter(): JSX.Element | null { { routingInstrumentation.registerNavigationContainer(navigationRef) - }}> + }} + > @@ -262,7 +256,7 @@ function AppOuter(): JSX.Element | null { } function AppInner(): JSX.Element { - const dispatch = useAppDispatch() + const dispatch = useDispatch() const isDarkMode = useIsDarkMode() const themeSetting = useCurrentAppearanceSetting() const allowAnalytics = useAppSelector(selectAllowAnalytics) @@ -276,11 +270,7 @@ function AppInner(): JSX.Element { if (typeof res === 'string' && res === 'Success') { logger.debug('AppsFlyer', 'status', 'stopped') } else { - logger.warn( - 'AppsFlyer', - 'stop', - `Got an error when trying to stop the AppsFlyer SDK: ${res}` - ) + logger.warn('AppsFlyer', 'stop', `Got an error when trying to stop the AppsFlyer SDK: ${res}`) } }) } @@ -301,11 +291,7 @@ function AppInner(): JSX.Element { <> - + ) } diff --git a/apps/mobile/src/app/MobileWalletNavigationProvider.tsx b/apps/mobile/src/app/MobileWalletNavigationProvider.tsx index 1a19a459b35..567bc98e84b 100644 --- a/apps/mobile/src/app/MobileWalletNavigationProvider.tsx +++ b/apps/mobile/src/app/MobileWalletNavigationProvider.tsx @@ -1,6 +1,6 @@ import { PropsWithChildren, useCallback } from 'react' import { Share } from 'react-native' -import { useAppDispatch } from 'src/app/hooks' +import { useDispatch } from 'react-redux' import { exploreNavigationRef } from 'src/app/navigation/navigation' import { useAppStackNavigation } from 'src/app/navigation/types' import { closeModal, openModal } from 'src/features/modals/modalSlice' @@ -14,6 +14,7 @@ import { ShareableEntity } from 'uniswap/src/types/sharing' import { logger } from 'utilities/src/logger/logger' import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants' import { + NavigateToNftCollectionArgs, NavigateToNftItemArgs, NavigateToSendFlowArgs, NavigateToSwapFlowArgs, @@ -23,7 +24,6 @@ import { getNavigateToSendFlowArgsInitialState, getNavigateToSwapFlowArgsInitialState, } from 'wallet/src/contexts/WalletNavigationContext' -import { useFiatOnRampIpAddressQuery } from 'wallet/src/features/fiatOnRamp/api' import { getNftUrl, getTokenUrl } from 'wallet/src/utils/linking' export function MobileWalletNavigationProvider({ children }: PropsWithChildren): JSX.Element { @@ -32,6 +32,7 @@ export function MobileWalletNavigationProvider({ children }: PropsWithChildren): const navigateToAccountActivityList = useNavigateToHomepageTab(HomeScreenTabIndex.Activity) const navigateToAccountTokenList = useNavigateToHomepageTab(HomeScreenTabIndex.Tokens) const navigateToBuyOrReceiveWithEmptyWallet = useNavigateToBuyOrReceiveWithEmptyWallet() + const navigateToNftCollection = useNavigateToNftCollection() const navigateToNftDetails = useNavigateToNftDetails() const navigateToReceive = useNavigateToReceive() const navigateToSend = useNavigateToSend() @@ -45,11 +46,13 @@ export function MobileWalletNavigationProvider({ children }: PropsWithChildren): navigateToAccountActivityList={navigateToAccountActivityList} navigateToAccountTokenList={navigateToAccountTokenList} navigateToBuyOrReceiveWithEmptyWallet={navigateToBuyOrReceiveWithEmptyWallet} + navigateToNftCollection={navigateToNftCollection} navigateToNftDetails={navigateToNftDetails} navigateToReceive={navigateToReceive} navigateToSend={navigateToSend} navigateToSwapFlow={navigateToSwapFlow} - navigateToTokenDetails={navigateToTokenDetails}> + navigateToTokenDetails={navigateToTokenDetails} + > {children} ) @@ -110,29 +113,27 @@ function useNavigateToHomepageTab(tab: HomeScreenTabIndex): () => void { } function useNavigateToReceive(): () => void { - const dispatch = useAppDispatch() + const dispatch = useDispatch() return useCallback((): void => { - dispatch( - openModal({ name: ModalName.WalletConnectScan, initialState: ScannerModalState.WalletQr }) - ) + dispatch(openModal({ name: ModalName.WalletConnectScan, initialState: ScannerModalState.WalletQr })) }, [dispatch]) } function useNavigateToSend(): (args: NavigateToSendFlowArgs) => void { - const dispatch = useAppDispatch() + const dispatch = useDispatch() return useCallback( (args: NavigateToSendFlowArgs) => { const initialSendState = getNavigateToSendFlowArgsInitialState(args) dispatch(openModal({ name: ModalName.Send, initialState: initialSendState })) }, - [dispatch] + [dispatch], ) } function useNavigateToSwapFlow(): (args: NavigateToSwapFlowArgs) => void { - const dispatch = useAppDispatch() + const dispatch = useDispatch() return useCallback( (args: NavigateToSwapFlowArgs): void => { @@ -140,7 +141,7 @@ function useNavigateToSwapFlow(): (args: NavigateToSwapFlowArgs) => void { dispatch(closeModal({ name: ModalName.Swap })) dispatch(openModal({ name: ModalName.Swap, initialState })) }, - [dispatch] + [dispatch], ) } @@ -155,7 +156,7 @@ function useNavigateToTokenDetails(): (currencyId: string) => void { appNavigation.navigate(MobileScreens.TokenDetails, { currencyId }) } }, - [appNavigation] + [appNavigation], ) } @@ -172,15 +173,26 @@ function useNavigateToNftDetails(): (args: NavigateToNftItemArgs) => void { fallbackData, }) }, - [navigation] + [navigation], ) } -function useNavigateToBuyOrReceiveWithEmptyWallet(): () => void { - const dispatch = useAppDispatch() +function useNavigateToNftCollection(): (args: NavigateToNftCollectionArgs) => void { + const navigation = useAppStackNavigation() - const { data } = useFiatOnRampIpAddressQuery() - const moonpayFiatOnRampEligible = Boolean(data?.isBuyAllowed) + return useCallback( + ({ collectionAddress }: NavigateToNftCollectionArgs): void => { + navigation.navigate(MobileScreens.NFTCollection, { + collectionAddress, + }) + }, + [navigation], + ) +} + +function useNavigateToBuyOrReceiveWithEmptyWallet(): () => void { + const dispatch = useDispatch() + // This flag is enabled only for supported countries. const forAggregatorEnabled = useFeatureFlag(FeatureFlags.ForAggregator) return useCallback((): void => { @@ -188,15 +200,13 @@ function useNavigateToBuyOrReceiveWithEmptyWallet(): () => void { if (forAggregatorEnabled) { dispatch(openModal({ name: ModalName.FiatOnRampAggregator })) - } else if (moonpayFiatOnRampEligible) { - dispatch(openModal({ name: ModalName.FiatOnRamp })) } else { dispatch( openModal({ name: ModalName.WalletConnectScan, initialState: ScannerModalState.WalletQr, - }) + }), ) } - }, [dispatch, forAggregatorEnabled, moonpayFiatOnRampEligible]) + }, [dispatch, forAggregatorEnabled]) } diff --git a/apps/mobile/src/app/hooks.ts b/apps/mobile/src/app/hooks.ts index 87f3bcc5dbc..639606da04f 100644 --- a/apps/mobile/src/app/hooks.ts +++ b/apps/mobile/src/app/hooks.ts @@ -1,16 +1,13 @@ import { useFocusEffect } from '@react-navigation/core' -import { ThunkDispatch } from '@reduxjs/toolkit' import { useCallback, useRef, useState } from 'react' import { LayoutChangeEvent } from 'react-native' -import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux' +import { TypedUseSelectorHook, useSelector } from 'react-redux' import type { MobileState } from 'src/app/reducer' -import type { AppDispatch } from 'src/app/store' import { SagaGenerator, select } from 'typed-redux-saga' import { spacing } from 'ui/src/theme' // Use throughout the app instead of plain `useDispatch` and `useSelector` -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const useAppDispatch = (): ThunkDispatch => useDispatch() + export const useAppSelector: TypedUseSelectorHook = useSelector // Use in sagas for better typing when selecting from redux state @@ -46,9 +43,7 @@ export function useShouldShowNativeKeyboard(): { const isLayoutPending = containerHeight === undefined || decimalPadY === undefined // If decimal pad renders below the input panel, we need to show the native keyboard - const showNativeKeyboard = isLayoutPending - ? false - : containerHeight + MIN_INPUT_DECIMAL_PAD_GAP > decimalPadY + const showNativeKeyboard = isLayoutPending ? false : containerHeight + MIN_INPUT_DECIMAL_PAD_GAP > decimalPadY return { onInputPanelLayout, @@ -56,8 +51,7 @@ export function useShouldShowNativeKeyboard(): { isLayoutPending, showNativeKeyboard, // can be used to imitate flexGrow=1 for the input panel - maxContentHeight: - isLayoutPending || showNativeKeyboard ? undefined : decimalPadY - MIN_INPUT_DECIMAL_PAD_GAP, + maxContentHeight: isLayoutPending || showNativeKeyboard ? undefined : decimalPadY - MIN_INPUT_DECIMAL_PAD_GAP, } } @@ -78,7 +72,7 @@ export function useNativeComponentKey(autoUpdate = true): { return } setKey(getNativeComponentKey()) - }, [autoUpdate]) + }, [autoUpdate]), ) const triggerUpdate = useCallback(() => { diff --git a/apps/mobile/src/app/migrations.test.ts b/apps/mobile/src/app/migrations.test.ts index 82073bc0c06..fc3078f59a8 100644 --- a/apps/mobile/src/app/migrations.test.ts +++ b/apps/mobile/src/app/migrations.test.ts @@ -81,10 +81,7 @@ import { initialWalletConnectState } from 'src/features/walletConnect/walletConn import { ModalName } from 'uniswap/src/features/telemetry/constants' import { UniverseChainId, WalletChainId } from 'uniswap/src/types/chains' import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants' -import { - ExtensionOnboardingState, - initialBehaviorHistoryState, -} from 'wallet/src/features/behaviorHistory/slice' +import { ExtensionOnboardingState, initialBehaviorHistoryState } from 'wallet/src/features/behaviorHistory/slice' import { initialFavoritesState } from 'wallet/src/features/favorites/slice' import { initialFiatCurrencyState } from 'wallet/src/features/fiatCurrency/slice' import { initialLanguageState } from 'wallet/src/features/language/slice' @@ -94,20 +91,12 @@ import { initialTelemetryState } from 'wallet/src/features/telemetry/slice' import { initialTokensState } from 'wallet/src/features/tokens/tokensSlice' import { initialTransactionsState } from 'wallet/src/features/transactions/slice' import { TransactionStatus, TransactionType } from 'wallet/src/features/transactions/types' -import { - Account, - AccountType, - SignerMnemonicAccount, -} from 'wallet/src/features/wallet/accounts/types' +import { Account, AccountType, SignerMnemonicAccount } from 'wallet/src/features/wallet/accounts/types' import { initialWalletState, SwapProtectionSetting } from 'wallet/src/features/wallet/slice' import { createMigrate } from 'wallet/src/state/createMigrate' import { testActivatePendingAccounts } from 'wallet/src/state/sharedMigrationsTests' import { getAllKeysOfNestedObject } from 'wallet/src/state/testUtils' -import { - fiatPurchaseTransactionInfo, - signerMnemonicAccount, - transactionDetails, -} from 'wallet/src/test/fixtures' +import { fiatPurchaseTransactionInfo, signerMnemonicAccount, transactionDetails } from 'wallet/src/test/fixtures' expect.extend({ toIncludeSameMembers }) @@ -289,21 +278,17 @@ describe('Redux state migrations', () => { expect(newSchema.transactions[UniverseChainId.Mainnet]).toBeUndefined() expect(newSchema.transactions.lastTxHistoryUpdate).toBeUndefined() - expect( - newSchema.transactions['0xShadowySuperCoder'][UniverseChainId.Mainnet]['0'].status - ).toEqual(TransactionStatus.Pending) + expect(newSchema.transactions['0xShadowySuperCoder'][UniverseChainId.Mainnet]['0'].status).toEqual( + TransactionStatus.Pending, + ) expect(newSchema.transactions['0xKingHodler'][UniverseChainId.Mainnet]).toBeUndefined() expect(newSchema.transactions['0xKingHodler'][UniverseChainId.Goerli]['0']).toBeUndefined() - expect(newSchema.transactions['0xKingHodler'][UniverseChainId.Goerli]['1'].from).toEqual( - '0xKingHodler' - ) + expect(newSchema.transactions['0xKingHodler'][UniverseChainId.Goerli]['1'].from).toEqual('0xKingHodler') expect(newSchema.notifications.lastTxNotificationUpdate).toBeDefined() - expect( - newSchema.notifications.lastTxNotificationUpdate['0xShadowySuperCoder'][ - UniverseChainId.Mainnet - ] - ).toEqual(12345678912345) + expect(newSchema.notifications.lastTxNotificationUpdate['0xShadowySuperCoder'][UniverseChainId.Mainnet]).toEqual( + 12345678912345, + ) }) it('migrates from v0 to v1', () => { @@ -408,12 +393,7 @@ describe('Redux state migrations', () => { }) it('migrates from v6 to v7', () => { - const TEST_ADDRESSES: [string, string, string, string] = [ - '0xTest', - '0xTest2', - '0xTest3', - '0xTest4', - ] + const TEST_ADDRESSES: [string, string, string, string] = ['0xTest', '0xTest2', '0xTest3', '0xTest4'] const TEST_IMPORT_TIME_MS = 12345678912345 const v6SchemaStub = { @@ -471,12 +451,7 @@ describe('Redux state migrations', () => { }) it('migrates from v8 to v9', () => { - const TEST_ADDRESSES: [string, string, string, string] = [ - '0xTest', - '0xTest2', - '0xTest3', - '0xTest4', - ] + const TEST_ADDRESSES: [string, string, string, string] = ['0xTest', '0xTest2', '0xTest3', '0xTest4'] const TEST_IMPORT_TIME_MS = 12345678912345 const v8SchemaStub = { @@ -512,15 +487,18 @@ describe('Redux state migrations', () => { const TEST_ADDRESSES = ['0xTest', OLD_DEMO_ACCOUNT_ADDRESS, '0xTest2', '0xTest3'] const TEST_IMPORT_TIME_MS = 12345678912345 - const accounts = TEST_ADDRESSES.reduce((acc, address) => { - acc[address] = { - address, - timeImportedMs: TEST_IMPORT_TIME_MS, - type: 'native', - } as unknown as Account + const accounts = TEST_ADDRESSES.reduce( + (acc, address) => { + acc[address] = { + address, + timeImportedMs: TEST_IMPORT_TIME_MS, + type: 'native', + } as unknown as Account - return acc - }, {} as { [address: string]: Account }) + return acc + }, + {} as { [address: string]: Account }, + ) const v9SchemaStub = { ...v9Schema, @@ -998,34 +976,18 @@ describe('Redux state migrations', () => { const v30 = migrations[30](v29Stub) // expect fiat onramp txdetails to change - expect(v30.transactions[account.address][UniverseChainId.Mainnet]['0'].typeInfo).toEqual( - expectedTypeInfo - ) + expect(v30.transactions[account.address][UniverseChainId.Mainnet]['0'].typeInfo).toEqual(expectedTypeInfo) expect(v30.transactions[account.address][UniverseChainId.Goerli]['0']).toBeUndefined() expect(v30.transactions[account.address][UniverseChainId.ArbitrumOne]).toBeUndefined() // does not create an object for chain - expect( - v30.transactions['0xshadowySuperCoder'][UniverseChainId.ArbitrumOne]['0'].typeInfo - ).toEqual(expectedTypeInfo) - expect(v30.transactions['0xshadowySuperCoder'][UniverseChainId.Optimism]['0'].typeInfo).toEqual( - expectedTypeInfo - ) - expect(v30.transactions['0xshadowySuperCoder'][UniverseChainId.Optimism]['1'].typeInfo).toEqual( - expectedTypeInfo - ) + expect(v30.transactions['0xshadowySuperCoder'][UniverseChainId.ArbitrumOne]['0'].typeInfo).toEqual(expectedTypeInfo) + expect(v30.transactions['0xshadowySuperCoder'][UniverseChainId.Optimism]['0'].typeInfo).toEqual(expectedTypeInfo) + expect(v30.transactions['0xshadowySuperCoder'][UniverseChainId.Optimism]['1'].typeInfo).toEqual(expectedTypeInfo) expect(v30.transactions['0xdeleteMe']).toBe(undefined) // expect non-for txDetails to not change - expect(v30.transactions[account.address][UniverseChainId.Mainnet]['1']).toEqual( - txDetailsConfirmed - ) - expect(v30.transactions[account.address][UniverseChainId.Goerli]['1']).toEqual( - txDetailsConfirmed - ) - expect(v30.transactions['0xshadowySuperCoder'][UniverseChainId.ArbitrumOne]['1']).toEqual( - txDetailsConfirmed - ) - expect(v30.transactions['0xshadowySuperCoder'][UniverseChainId.Optimism]['2']).toEqual( - txDetailsConfirmed - ) + expect(v30.transactions[account.address][UniverseChainId.Mainnet]['1']).toEqual(txDetailsConfirmed) + expect(v30.transactions[account.address][UniverseChainId.Goerli]['1']).toEqual(txDetailsConfirmed) + expect(v30.transactions['0xshadowySuperCoder'][UniverseChainId.ArbitrumOne]['1']).toEqual(txDetailsConfirmed) + expect(v30.transactions['0xshadowySuperCoder'][UniverseChainId.Optimism]['2']).toEqual(txDetailsConfirmed) }) it('migrates from v31 to 32', () => { @@ -1098,24 +1060,16 @@ describe('Redux state migrations', () => { const v36Stub = { ...v36Schema, transactions } - expect( - v36Stub.transactions[account.address]?.[UniverseChainId.Mainnet][id1].typeInfo.id - ).toBeUndefined() - expect( - v36Stub.transactions[account.address]?.[UniverseChainId.Mainnet][id2].typeInfo.id - ).toBeUndefined() + expect(v36Stub.transactions[account.address]?.[UniverseChainId.Mainnet][id1].typeInfo.id).toBeUndefined() + expect(v36Stub.transactions[account.address]?.[UniverseChainId.Mainnet][id2].typeInfo.id).toBeUndefined() const v37 = migrations[37](v36Stub) expect(v37.transactions[account.address]?.[UniverseChainId.Mainnet][id1].typeInfo.id).toEqual( - fiatOnRampTxDetailsFailed.typeInfo.id - ) - expect( - v36Stub.transactions[account.address]?.[UniverseChainId.Mainnet][id2].typeInfo.id - ).toBeUndefined() - expect(v36Stub.transactions[account.address]?.[UniverseChainId.Mainnet][id3]).toEqual( - txDetailsConfirmed + fiatOnRampTxDetailsFailed.typeInfo.id, ) + expect(v36Stub.transactions[account.address]?.[UniverseChainId.Mainnet][id2].typeInfo.id).toBeUndefined() + expect(v36Stub.transactions[account.address]?.[UniverseChainId.Mainnet][id3]).toEqual(txDetailsConfirmed) }) it('migrates from v37 to 38', () => { diff --git a/apps/mobile/src/app/migrations.ts b/apps/mobile/src/app/migrations.ts index d9a62294bf0..12e3b5dd791 100644 --- a/apps/mobile/src/app/migrations.ts +++ b/apps/mobile/src/app/migrations.ts @@ -4,19 +4,15 @@ /* eslint-disable max-lines */ import dayjs from 'dayjs' +import { toSupportedChainId } from 'uniswap/src/features/chains/utils' import { ModalName } from 'uniswap/src/features/telemetry/constants' import { UniverseChainId, WalletChainId } from 'uniswap/src/types/chains' import { ExtensionOnboardingState } from 'wallet/src/features/behaviorHistory/slice' -import { toSupportedChainId } from 'wallet/src/features/chains/utils' import { initialFiatCurrencyState } from 'wallet/src/features/fiatCurrency/slice' import { initialLanguageState } from 'wallet/src/features/language/slice' import { getNFTAssetKey } from 'wallet/src/features/nfts/utils' import { TransactionStateMap } from 'wallet/src/features/transactions/slice' -import { - ChainIdToTxIdToDetails, - TransactionStatus, - TransactionType, -} from 'wallet/src/features/transactions/types' +import { ChainIdToTxIdToDetails, TransactionStatus, TransactionType } from 'wallet/src/features/transactions/types' import { Account, AccountType } from 'wallet/src/features/wallet/accounts/types' import { SwapProtectionSetting } from 'wallet/src/features/wallet/slice' import { @@ -273,7 +269,7 @@ export const migrations = { tempState.byChainId[chainId] = chainInfo return tempState }, - { byChainId: {} } + { byChainId: {} }, ) const blockState: any | undefined = newState?.blocks @@ -292,7 +288,7 @@ export const migrations = { tempState.byChainId[chainId] = blockInfo return tempState }, - { byChainId: {} } + { byChainId: {} }, ) const transactionState: TransactionStateMap | undefined = newState?.transactions @@ -303,28 +299,25 @@ export const migrations = { return tempState } - const newAddressTxState = Object.keys(txs).reduce( - (tempAddressState, chainIdString) => { - const chainId = toSupportedChainId(chainIdString) - if (!chainId) { - return tempAddressState - } - - const txInfo = txs[chainId] - if (!txInfo) { - return tempAddressState - } + const newAddressTxState = Object.keys(txs).reduce((tempAddressState, chainIdString) => { + const chainId = toSupportedChainId(chainIdString) + if (!chainId) { + return tempAddressState + } - tempAddressState[chainId] = txInfo + const txInfo = txs[chainId] + if (!txInfo) { return tempAddressState - }, - {} - ) + } + + tempAddressState[chainId] = txInfo + return tempAddressState + }, {}) tempState[address] = newAddressTxState return tempState }, - {} + {}, ) return { @@ -559,8 +552,7 @@ export const migrations = { newTransactionState[address] ??= {} newTransactionState[address][chainId] ??= {} newTransactionState[address][chainId][txId] = - txDetails.typeInfo.type === TransactionType.FiatPurchase && - txDetails.status === TransactionStatus.Failed + txDetails.typeInfo.type === TransactionType.FiatPurchase && txDetails.status === TransactionStatus.Failed ? { ...txDetails, typeInfo: { @@ -615,10 +607,7 @@ export const migrations = { const accountAddresses = Object.keys(state.favorites?.hiddenNfts ?? {}) - type AccountToNftData = Record< - Address, - Record - > + type AccountToNftData = Record> const nftsData: AccountToNftData = {} for (const accountAddress of accountAddresses) { @@ -830,35 +819,34 @@ export const migrations = { 61: function flattenTokenVisibility(state: any) { const newState = { ...state } - type AccountToNftData = Record< - Address, - Record - > + type AccountToNftData = Record> type NFTKeyToVisibility = Record type AccountToTokenVisibility = Record> type CurrencyIdToVisibility = Record const tokenVisibilityByAccount: AccountToTokenVisibility = state.favorites.tokensVisibility - const flattenedTokenVisibility: CurrencyIdToVisibility = Object.values( - tokenVisibilityByAccount - ).reduce((acc, currencyIdToVisibility) => ({ ...acc, ...currencyIdToVisibility }), {}) + const flattenedTokenVisibility: CurrencyIdToVisibility = Object.values(tokenVisibilityByAccount).reduce( + (acc, currencyIdToVisibility) => ({ ...acc, ...currencyIdToVisibility }), + {}, + ) const nftDataByAccount: AccountToNftData = state.favorites.nftsData const flattenedNFTData = Object.values(nftDataByAccount).reduce( (acc, nftIdToVisibility) => ({ ...acc, ...nftIdToVisibility }), - {} + {}, ) - const flattenedTransformedNFTData: NFTKeyToVisibility = Object.keys( - flattenedNFTData - ).reduce((acc, nftKey) => { - const { isHidden, isSpamIgnored } = flattenedNFTData[nftKey] ?? {} - return { - ...acc, - [nftKey]: { isVisible: isHidden === false || isSpamIgnored === true }, - } - }, {}) + const flattenedTransformedNFTData: NFTKeyToVisibility = Object.keys(flattenedNFTData).reduce( + (acc, nftKey) => { + const { isHidden, isSpamIgnored } = flattenedNFTData[nftKey] ?? {} + return { + ...acc, + [nftKey]: { isVisible: isHidden === false || isSpamIgnored === true }, + } + }, + {}, + ) newState.favorites = { ...state.favorites, diff --git a/apps/mobile/src/app/modals/AccountSwitcherModal.tsx b/apps/mobile/src/app/modals/AccountSwitcherModal.tsx index bc4f405bae5..deedeec4b0c 100644 --- a/apps/mobile/src/app/modals/AccountSwitcherModal.tsx +++ b/apps/mobile/src/app/modals/AccountSwitcherModal.tsx @@ -1,8 +1,9 @@ import React, { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { Alert } from 'react-native' +import { useDispatch } from 'react-redux' import { Action } from 'redux' -import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { useAppSelector } from 'src/app/hooks' import { navigate } from 'src/app/navigation/rootNavigation' import { AccountList } from 'src/components/accounts/AccountList' import { isCloudStorageAvailable } from 'src/features/CloudBackup/RNCloudStorageBackupsManager' @@ -13,8 +14,9 @@ import { useDeviceDimensions } from 'ui/src/hooks/useDeviceDimensions' import { spacing } from 'ui/src/theme' import { ActionSheetModal, MenuItemProp } from 'uniswap/src/components/modals/ActionSheetModal' import { BottomSheetModal } from 'uniswap/src/components/modals/BottomSheetModal' -import { ElementName, MobileEventName, ModalName } from 'uniswap/src/features/telemetry/constants' +import { ElementName, ModalName, WalletEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding' import { MobileScreens, OnboardingScreens } from 'uniswap/src/types/screens/mobile' import { isAndroid } from 'utilities/src/platform' @@ -24,22 +26,20 @@ import { createOnboardingAccount } from 'wallet/src/features/onboarding/createOn import { AccountType, BackupType } from 'wallet/src/features/wallet/accounts/types' import { createAccountsActions } from 'wallet/src/features/wallet/create/createAccountsSaga' import { useActiveAccountAddress, useNativeAccountExists } from 'wallet/src/features/wallet/hooks' -import { - selectAllAccountsSorted, - selectSortedSignerMnemonicAccounts, -} from 'wallet/src/features/wallet/selectors' +import { selectAllAccountsSorted, selectSortedSignerMnemonicAccounts } from 'wallet/src/features/wallet/selectors' import { setAccountAsActive } from 'wallet/src/features/wallet/slice' import { openSettings } from 'wallet/src/utils/linking' export function AccountSwitcherModal(): JSX.Element { - const dispatch = useAppDispatch() + const dispatch = useDispatch() const colors = useSporeColors() return ( dispatch(closeModal({ name: ModalName.AccountSwitcher }))}> + onClose={(): Action => dispatch(closeModal({ name: ModalName.AccountSwitcher }))} + > { @@ -60,7 +60,7 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme const dimensions = useDeviceDimensions() const { t } = useTranslation() const activeAccountAddress = useActiveAccountAddress() - const dispatch = useAppDispatch() + const dispatch = useDispatch() const hasImportedSeedPhrase = useNativeAccountExists() const modalState = useAppSelector(selectModalState(ModalName.AccountSwitcher)) const sortedMnemonicAccounts = useAppSelector(selectSortedSignerMnemonicAccounts) @@ -77,7 +77,7 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme dispatch(setAccountAsActive(address)) }) }, - [dispatch, onClose] + [dispatch, onClose], ) const onPressAddWallet = (): void => { @@ -109,15 +109,15 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme dispatch( createAccountsActions.trigger({ accounts: [newAccount], - }) + }), ) - // Log analytics event - sendAnalyticsEvent(MobileEventName.WalletAdded, { + sendAnalyticsEvent(WalletEventName.WalletAdded, { wallet_type: ImportType.CreateAdditional, accounts_imported_count: 1, wallets_imported: [newAccount.address], cloud_backup_used: newAccount.backups?.includes(BackupType.Cloud) ?? false, + modal: ModalName.AccountSwitcher, }) } @@ -182,7 +182,7 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme style: 'default', }, { text: t('account.cloud.error.unavailable.button.cancel'), style: 'cancel' }, - ] + ], ) return } @@ -200,11 +200,7 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme key: ElementName.CreateAccount, onPress: onPressCreateNewWallet, render: () => ( - + {t('account.wallet.button.create')} ), @@ -236,9 +232,7 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme render: () => ( - {isAndroid - ? t('account.cloud.button.restore.android') - : t('account.cloud.button.restore.ios')} + {isAndroid ? t('account.cloud.button.restore.android') : t('account.cloud.button.restore.ios')} ), @@ -250,15 +244,13 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme const accountsWithoutActive = accounts.filter((a) => a.address !== activeAccountAddress) - const isViewOnly = - accounts.find((a) => a.address === activeAccountAddress)?.type === AccountType.Readonly + const isViewOnly = accounts.find((a) => a.address === activeAccountAddress)?.type === AccountType.Readonly if (!activeAccountAddress) { return null } - const fullScreenContentHeight = - dimensions.fullHeight - insets.top - insets.bottom - spacing.spacing36 // approximate bottom sheet handle height + padding bottom + const fullScreenContentHeight = dimensions.fullHeight - insets.top - insets.bottom - spacing.spacing36 // approximate bottom sheet handle height + padding bottom return ( @@ -273,20 +265,12 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme variant="subheading1" /> - - + diff --git a/apps/mobile/src/app/modals/AppModals.tsx b/apps/mobile/src/app/modals/AppModals.tsx index ef5c37a6de0..005668f9205 100644 --- a/apps/mobile/src/app/modals/AppModals.tsx +++ b/apps/mobile/src/app/modals/AppModals.tsx @@ -1,4 +1,5 @@ import React, { useCallback } from 'react' +import { useDispatch } from 'react-redux' import { AccountSwitcherModal } from 'src/app/modals/AccountSwitcherModal' import { ExperimentsModal } from 'src/app/modals/ExperimentsModal' import { ExploreModal } from 'src/app/modals/ExploreModal' @@ -7,24 +8,23 @@ import { TransferTokenModal } from 'src/app/modals/TransferTokenModal' import { ViewOnlyExplainerModal } from 'src/app/modals/ViewOnlyExplainerModal' import { LazyModalRenderer } from 'src/app/modals/utils' import { RemoveWalletModal } from 'src/components/RemoveWallet/RemoveWalletModal' +import { WalletConnectModals } from 'src/components/Requests/WalletConnectModals' import { RestoreWalletModal } from 'src/components/RestoreWalletModal/RestoreWalletModal' -import { WalletConnectModals } from 'src/components/WalletConnect/WalletConnectModals' import { ForceUpgradeModal } from 'src/components/forceUpgrade/ForceUpgradeModal' import { UnitagsIntroModal } from 'src/components/unitags/UnitagsIntroModal' import { LockScreenModal } from 'src/features/authentication/LockScreenModal' import { ExchangeTransferModal } from 'src/features/fiatOnRamp/ExchangeTransferModal' import { FiatOnRampAggregatorModal } from 'src/features/fiatOnRamp/FiatOnRampAggregatorModal' -import { FiatOnRampModal } from 'src/features/fiatOnRamp/FiatOnRampModal' import { closeModal } from 'src/features/modals/modalSlice' import { ScantasticModal } from 'src/features/scantastic/ScantasticModal' import { ReceiveCryptoModal } from 'src/screens/ReceiveCryptoModal' import { SettingsFiatCurrencyModal } from 'src/screens/SettingsFiatCurrencyModal' import { ModalName } from 'uniswap/src/features/telemetry/constants' import { SettingsLanguageModal } from 'wallet/src/components/settings/language/SettingsLanguageModal' -import { useAppDispatch } from 'wallet/src/state' +import { QueuedOrderModal } from 'wallet/src/features/transactions/swap/modals/QueuedOrderModal' export function AppModals(): JSX.Element { - const dispatch = useAppDispatch() + const dispatch = useDispatch() const onCloseLanguageModal = useCallback(() => { dispatch(closeModal({ name: ModalName.LanguageSelector })) @@ -40,10 +40,6 @@ export function AppModals(): JSX.Element { - - - - @@ -78,6 +74,8 @@ export function AppModals(): JSX.Element { + + diff --git a/apps/mobile/src/app/modals/ExperimentsModal.tsx b/apps/mobile/src/app/modals/ExperimentsModal.tsx index aef0e5674aa..55d42893e1a 100644 --- a/apps/mobile/src/app/modals/ExperimentsModal.tsx +++ b/apps/mobile/src/app/modals/ExperimentsModal.tsx @@ -1,8 +1,9 @@ import { useApolloClient } from '@apollo/client' import React, { useState } from 'react' import { ScrollView } from 'react-native-gesture-handler' +import { useDispatch } from 'react-redux' import { Action } from 'redux' -import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { useAppSelector } from 'src/app/hooks' import { closeModal } from 'src/features/modals/modalSlice' import { selectCustomEndpoint } from 'src/features/tweaks/selectors' import { setCustomEndpoint } from 'src/features/tweaks/slice' @@ -15,7 +16,7 @@ import { AccordionHeader, GatingOverrides } from 'wallet/src/components/gating/G export function ExperimentsModal(): JSX.Element { const insets = useDeviceInsets() - const dispatch = useAppDispatch() + const dispatch = useDispatch() const customEndpoint = useAppSelector(selectCustomEndpoint) const apollo = useApolloClient() @@ -34,7 +35,7 @@ export function ExperimentsModal(): JSX.Element { dispatch( setCustomEndpoint({ customEndpoint: { url, key }, - }) + }), ) } else { clearEndpoint() @@ -46,13 +47,15 @@ export function ExperimentsModal(): JSX.Element { fullScreen renderBehindBottomInset name={ModalName.Experiments} - onClose={(): Action => dispatch(closeModal({ name: ModalName.Experiments }))}> + onClose={(): Action => dispatch(closeModal({ name: ModalName.Experiments }))} + > + }} + > Server @@ -60,8 +63,8 @@ export function ExperimentsModal(): JSX.Element { - You will need to restart the application to pick up any changes in this section. - Beware of client side caching! + You will need to restart the application to pick up any changes in this section. Beware of client side + caching! @@ -90,10 +93,7 @@ export function ExperimentsModal(): JSX.Element { - diff --git a/apps/mobile/src/app/modals/ExploreModal.tsx b/apps/mobile/src/app/modals/ExploreModal.tsx index 7c89bb7068d..fcd7fda8845 100644 --- a/apps/mobile/src/app/modals/ExploreModal.tsx +++ b/apps/mobile/src/app/modals/ExploreModal.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { useAppDispatch } from 'src/app/hooks' +import { useDispatch } from 'react-redux' import { ExploreStackNavigator } from 'src/app/navigation/navigation' import { closeModal } from 'src/features/modals/modalSlice' import { useSporeColors } from 'ui/src' @@ -8,7 +8,7 @@ import { ModalName } from 'uniswap/src/features/telemetry/constants' export function ExploreModal(): JSX.Element { const colors = useSporeColors() - const appDispatch = useAppDispatch() + const appDispatch = useDispatch() const onClose = (): void => { appDispatch(closeModal({ name: ModalName.Explore })) @@ -22,12 +22,10 @@ export function ExploreModal(): JSX.Element { renderBehindBottomInset renderBehindTopInset backgroundColor={colors.transparent.val} - // Don't dismiss on back press, as this modal is used for the ExploreStack navigation. - // (the modal should be dismissed only when the user navigates to the initial Explore screen) - dismissOnBackPress={false} hideHandlebar={true} name={ModalName.Explore} - onClose={onClose}> + onClose={onClose} + > ) diff --git a/apps/mobile/src/app/modals/ExtensionPromoModal.tsx b/apps/mobile/src/app/modals/ExtensionPromoModal.tsx index accbd7105e9..90892bb3e17 100644 --- a/apps/mobile/src/app/modals/ExtensionPromoModal.tsx +++ b/apps/mobile/src/app/modals/ExtensionPromoModal.tsx @@ -19,9 +19,7 @@ export function ExtensionPromoModal({ onClose }: { onClose: () => void }): JSX.E const isDarkMode = useIsDarkMode() const isExtensionGAPromotionEnabled = useFeatureFlag(FeatureFlags.ExtensionPromotionGA) - const bannerImageGA = isDarkMode - ? EXTENSION_PROMO_BANNER_DARK_GA - : EXTENSION_PROMO_BANNER_LIGHT_GA + const bannerImageGA = isDarkMode ? EXTENSION_PROMO_BANNER_DARK_GA : EXTENSION_PROMO_BANNER_LIGHT_GA const bannerImageBeta = isDarkMode ? EXTENSION_PROMO_BANNER_DARK : EXTENSION_PROMO_BANNER_LIGHT diff --git a/apps/mobile/src/app/modals/SwapModal.tsx b/apps/mobile/src/app/modals/SwapModal.tsx index b2cf30f0470..a54f83c9486 100644 --- a/apps/mobile/src/app/modals/SwapModal.tsx +++ b/apps/mobile/src/app/modals/SwapModal.tsx @@ -1,11 +1,8 @@ import React, { useCallback, useEffect } from 'react' -import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { useDispatch } from 'react-redux' +import { useAppSelector } from 'src/app/hooks' import { BiometricsIcon } from 'src/components/icons/BiometricsIcon' -import { - useBiometricAppSettings, - useBiometricPrompt, - useOsBiometricAuthEnabled, -} from 'src/features/biometrics/hooks' +import { useBiometricAppSettings, useBiometricPrompt, useOsBiometricAuthEnabled } from 'src/features/biometrics/hooks' import { closeModal } from 'src/features/modals/modalSlice' import { selectModalState } from 'src/features/modals/selectModalState' import { useWalletRestore } from 'src/features/wallet/hooks' @@ -15,7 +12,7 @@ import { SwapFlow } from 'wallet/src/features/transactions/swap/SwapFlow' import { useSwapPrefilledState } from 'wallet/src/features/transactions/swap/hooks/useSwapPrefilledState' export function SwapModal(): JSX.Element { - const appDispatch = useAppDispatch() + const appDispatch = useDispatch() const { initialState } = useAppSelector(selectModalState(ModalName.Swap)) const onClose = useCallback((): void => { diff --git a/apps/mobile/src/app/modals/TransferTokenModal.tsx b/apps/mobile/src/app/modals/TransferTokenModal.tsx index 7f134d51a24..95bc22c9265 100644 --- a/apps/mobile/src/app/modals/TransferTokenModal.tsx +++ b/apps/mobile/src/app/modals/TransferTokenModal.tsx @@ -1,5 +1,6 @@ import React, { useCallback } from 'react' -import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { useDispatch } from 'react-redux' +import { useAppSelector } from 'src/app/hooks' import { closeModal } from 'src/features/modals/modalSlice' import { selectModalState } from 'src/features/modals/selectModalState' import { TransferFlow } from 'src/features/transactions/transfer/TransferFlow' @@ -12,7 +13,7 @@ import { ModalName } from 'uniswap/src/features/telemetry/constants' export function TransferTokenModal(): JSX.Element { const colors = useSporeColors() - const appDispatch = useAppDispatch() + const appDispatch = useDispatch() const modalState = useAppSelector(selectModalState(ModalName.Send)) const onClose = useCallback((): void => { @@ -32,7 +33,8 @@ export function TransferTokenModal(): JSX.Element { renderBehindTopInset backgroundColor={colors.surface1.get()} name={ModalName.Send} - onClose={onClose}> + onClose={onClose} + > ) diff --git a/apps/mobile/src/app/modals/ViewOnlyExplainerModal.tsx b/apps/mobile/src/app/modals/ViewOnlyExplainerModal.tsx index bb20e62a357..5266bad6bd3 100644 --- a/apps/mobile/src/app/modals/ViewOnlyExplainerModal.tsx +++ b/apps/mobile/src/app/modals/ViewOnlyExplainerModal.tsx @@ -1,4 +1,5 @@ import { useTranslation } from 'react-i18next' +import { useDispatch } from 'react-redux' import { navigate } from 'src/app/navigation/rootNavigation' import { closeModal, openModal } from 'src/features/modals/modalSlice' import { Button, Flex, Text, useIsDarkMode } from 'ui/src' @@ -9,14 +10,13 @@ import { ModalName } from 'uniswap/src/features/telemetry/constants' import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding' import { MobileScreens, OnboardingScreens } from 'uniswap/src/types/screens/mobile' import { useActiveAccountAddress, useNativeAccountExists } from 'wallet/src/features/wallet/hooks' -import { useAppDispatch } from 'wallet/src/state' const WALLET_IMAGE_ASPECT_RATIO = 327 / 215 export function ViewOnlyExplainerModal(): JSX.Element { const { t } = useTranslation() const activeAccountAddress = useActiveAccountAddress() - const dispatch = useAppDispatch() + const dispatch = useDispatch() const hasImportedSeedPhrase = useNativeAccountExists() const isDarkMode = useIsDarkMode() @@ -53,12 +53,7 @@ export function ViewOnlyExplainerModal(): JSX.Element { - diff --git a/apps/mobile/src/app/modals/__snapshots__/AccountSwitcherModal.test.tsx.snap b/apps/mobile/src/app/modals/__snapshots__/AccountSwitcherModal.test.tsx.snap index 7d57c058fa5..5d3f18dd6c1 100644 --- a/apps/mobile/src/app/modals/__snapshots__/AccountSwitcherModal.test.tsx.snap +++ b/apps/mobile/src/app/modals/__snapshots__/AccountSwitcherModal.test.tsx.snap @@ -258,8 +258,8 @@ exports[`AccountSwitcher renders correctly 1`] = ` ] } tintColor="#7D7D7D" - vbHeight={16} - vbWidth={15} + vbHeight={24} + vbWidth={24} > { Rendered , - { preloadedState: preloadedMobileState() } + { preloadedState: preloadedMobileState() }, ) expect(tree.toJSON()).toBeNull() @@ -28,7 +28,7 @@ describe(LazyModalRenderer, () => { [ModalName.Experiments]: { isOpen: true }, }), }), - } + }, ) expect(tree.toJSON()).toMatchInlineSnapshot(` diff --git a/apps/mobile/src/app/navigation/NavBar.tsx b/apps/mobile/src/app/navigation/NavBar.tsx index d24fe34dfb2..ca600672974 100644 --- a/apps/mobile/src/app/navigation/NavBar.tsx +++ b/apps/mobile/src/app/navigation/NavBar.tsx @@ -11,7 +11,7 @@ import { useAnimatedStyle, useSharedValue, } from 'react-native-reanimated' -import { useAppDispatch } from 'src/app/hooks' +import { useDispatch } from 'react-redux' import { pulseAnimation } from 'src/components/buttons/utils' import { openModal } from 'src/features/modals/modalSlice' import { @@ -30,12 +30,13 @@ import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { borderRadii, fonts } from 'ui/src/theme' import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { MobileScreens } from 'uniswap/src/types/screens/mobile' +import { opacify } from 'uniswap/src/utils/colors' import { isAndroid, isIOS } from 'utilities/src/platform' import { useHighestBalanceNativeCurrencyId } from 'wallet/src/features/dataApi/balances' import { prepareSwapFormState } from 'wallet/src/features/transactions/swap/utils' import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' -import { opacify } from 'wallet/src/utils/colors' export const NAV_BAR_HEIGHT_XS = 52 export const NAV_BAR_HEIGHT_SM = 72 @@ -57,11 +58,7 @@ export function NavBar(): JSX.Element { return ( <> - + + style={{ paddingBottom: insets.bottom }} + > + pointerEvents="auto" + > @@ -107,7 +106,7 @@ type SwapTabBarButtonProps = { const SwapFAB = memo(function _SwapFAB({ activeScale = 0.96 }: SwapTabBarButtonProps) { const { t } = useTranslation() - const dispatch = useAppDispatch() + const dispatch = useDispatch() const isDarkMode = useIsDarkMode() @@ -119,7 +118,7 @@ const SwapFAB = memo(function _SwapFAB({ activeScale = 0.96 }: SwapTabBarButtonP openModal({ name: ModalName.Swap, initialState: prepareSwapFormState({ inputCurrencyId }), - }) + }), ) await HapticFeedback.impact() @@ -151,7 +150,8 @@ const SwapFAB = memo(function _SwapFAB({ activeScale = 0.96 }: SwapTabBarButtonP shadowOffset={SWAP_BUTTON_SHADOW_OFFSET} shadowOpacity={isDarkMode ? 0.6 : 0.4} shadowRadius={borderRadii.rounded20} - style={[animatedStyle]}> + style={[animatedStyle]} + > - + top={0} + > + - + {t('common.button.swap')} @@ -191,7 +182,7 @@ type ExploreTabBarButtonProps = { } function ExploreTabBarButton({ activeScale = 0.98 }: ExploreTabBarButtonProps): JSX.Element { - const dispatch = useAppDispatch() + const dispatch = useDispatch() const colors = useSporeColors() const isDarkMode = useIsDarkMode() const { t } = useTranslation() @@ -230,10 +221,9 @@ function ExploreTabBarButton({ activeScale = 0.98 }: ExploreTabBarButtonProps): hapticFeedback activeOpacity={1} style={[styles.searchBar, { borderRadius: borderRadii.roundedFull }]} - onPress={onPress}> - + onPress={onPress} + > + + shadowRadius={borderRadii.rounded20} + > + variant="body1" + > {t('common.input.search')} diff --git a/apps/mobile/src/app/navigation/NavigationContainer.tsx b/apps/mobile/src/app/navigation/NavigationContainer.tsx index 64d512834dc..f2bbb93034b 100644 --- a/apps/mobile/src/app/navigation/NavigationContainer.tsx +++ b/apps/mobile/src/app/navigation/NavigationContainer.tsx @@ -7,7 +7,7 @@ import { import { SharedEventName } from '@uniswap/analytics-events' import React, { FC, PropsWithChildren, useCallback, useState } from 'react' import { Linking } from 'react-native' -import { useAppDispatch } from 'src/app/hooks' +import { useDispatch } from 'react-redux' import { RootParamList } from 'src/app/navigation/types' import { openDeepLink } from 'src/features/deepLinking/handleDeepLinkSaga' import { DIRECT_LOG_ONLY_SCREENS } from 'src/features/telemetry/directLogScreens' @@ -27,10 +27,7 @@ interface Props { export const navigationRef = createNavigationContainerRef() /** Wrapped `NavigationContainer` with telemetry tracing. */ -export const NavigationContainer: FC> = ({ - children, - onReady, -}: PropsWithChildren) => { +export const NavigationContainer: FC> = ({ children, onReady }: PropsWithChildren) => { const colors = useSporeColors() const [routeName, setRouteName] = useState() const [routeParams, setRouteParams] = useState | undefined>() @@ -58,8 +55,7 @@ export const NavigationContainer: FC> = ({ }} onStateChange={(): void => { const previousRouteName = routeName - const currentRouteName: MobileAppScreen = navigationRef.getCurrentRoute() - ?.name as MobileAppScreen + const currentRouteName: MobileAppScreen = navigationRef.getCurrentRoute()?.name as MobileAppScreen if ( currentRouteName && @@ -68,7 +64,7 @@ export const NavigationContainer: FC> = ({ ) { const currentRouteParams = getEventParams( currentRouteName, - navigationRef.getCurrentRoute()?.params as RootParamList[MobileAppScreen] + navigationRef.getCurrentRoute()?.params as RootParamList[MobileAppScreen], ) setLogImpression(true) setRouteName(currentRouteName) @@ -76,7 +72,8 @@ export const NavigationContainer: FC> = ({ } else { setLogImpression(false) } - }}> + }} + > {children} @@ -85,7 +82,7 @@ export const NavigationContainer: FC> = ({ } export const useManageDeepLinks = (): void => { - const dispatch = useAppDispatch() + const dispatch = useDispatch() const manageDeepLinks = useCallback(async () => { const url = await Linking.getInitialURL() if (url) { @@ -95,7 +92,7 @@ export const useManageDeepLinks = (): void => { // as then there is a change we dispatch `openDeepLink` action twice if app was lauched by a deep link await sleep(2000) // 2000 was chosen imperically const urlListener = Linking.addEventListener('url', (event: { url: string }) => - dispatch(openDeepLink({ url: event.url, coldStart: false })) + dispatch(openDeepLink({ url: event.url, coldStart: false })), ) return urlListener.remove diff --git a/apps/mobile/src/app/navigation/components.tsx b/apps/mobile/src/app/navigation/components.tsx new file mode 100644 index 00000000000..ea31bfb4a44 --- /dev/null +++ b/apps/mobile/src/app/navigation/components.tsx @@ -0,0 +1,9 @@ +import { BackButton } from 'src/components/buttons/BackButton' +import { RotatableChevron } from 'ui/src/components/icons' +import { iconSizes } from 'ui/src/theme' + +export const renderHeaderBackButton = (): JSX.Element => + +export const renderHeaderBackImage = (): JSX.Element => ( + +) diff --git a/apps/mobile/src/app/navigation/hooks.ts b/apps/mobile/src/app/navigation/hooks.ts index e3c850811ca..7a7b425ff18 100644 --- a/apps/mobile/src/app/navigation/hooks.ts +++ b/apps/mobile/src/app/navigation/hooks.ts @@ -25,12 +25,12 @@ export function useEagerActivityNavigation(): { }, }) }, - [load] + [load], ) const navigate = useCallback( () => navigation.navigate(MobileScreens.Home, { tab: HomeScreenTabIndex.Activity }), - [navigation] + [navigation], ) return { preload, navigate } @@ -52,14 +52,14 @@ export function useEagerExternalProfileNavigation(): { async (address: string) => { await load({ variables: { address } }) }, - [load] + [load], ) const navigate = useCallback( (address: string) => { navigation.navigate(MobileScreens.ExternalProfile, { address }) }, - [navigation] + [navigation], ) return { preload, navigate } @@ -79,7 +79,7 @@ export function useEagerExternalProfileRootNavigation(): { }, }) }, - [load] + [load], ) const navigate = useCallback(async (address: string, callback?: () => void) => { diff --git a/apps/mobile/src/app/navigation/navigation.tsx b/apps/mobile/src/app/navigation/navigation.tsx index c4c9b93a1cd..cbb449b805b 100644 --- a/apps/mobile/src/app/navigation/navigation.tsx +++ b/apps/mobile/src/app/navigation/navigation.tsx @@ -3,6 +3,7 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack' import { createStackNavigator, TransitionPresets } from '@react-navigation/stack' import React from 'react' import { useAppSelector } from 'src/app/hooks' +import { renderHeaderBackButton, renderHeaderBackImage } from 'src/app/navigation/components' import { AppStackParamList, AppStackScreenProp, @@ -12,7 +13,6 @@ import { SettingsStackParamList, UnitagStackParamList, } from 'src/app/navigation/types' -import { BackButton } from 'src/components/buttons/BackButton' import { HorizontalEdgeGestureTarget } from 'src/components/layout/screens/EdgeGestureTarget' import { useBiometricCheck } from 'src/features/biometrics/useBiometricCheck' import { FiatOnRampProvider } from 'src/features/fiatOnRamp/FiatOnRampContext' @@ -65,17 +65,11 @@ import { SettingsWalletManageConnection } from 'src/screens/SettingsWalletManage import { TokenDetailsScreen } from 'src/screens/TokenDetailsScreen' import { WebViewScreen } from 'src/screens/WebViewScreen' import { useDeviceInsets, useSporeColors } from 'ui/src' -import { RotatableChevron } from 'ui/src/components/icons' import { spacing } from 'ui/src/theme' import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { OnboardingEntryPoint } from 'uniswap/src/types/onboarding' -import { - FiatOnRampScreens, - MobileScreens, - OnboardingScreens, - UnitagScreens, -} from 'uniswap/src/types/screens/mobile' +import { FiatOnRampScreens, MobileScreens, OnboardingScreens, UnitagScreens } from 'uniswap/src/types/screens/mobile' import { OnboardingContextProvider } from 'wallet/src/features/onboarding/OnboardingContext' import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks' import { selectFinishedOnboarding } from 'wallet/src/features/wallet/selectors' @@ -94,27 +88,19 @@ function SettingsStackGroup(): JSX.Element { ...navOptions.noHeader, fullScreenGestureEnabled: true, animation: 'slide_from_right', - }}> + }} + > - + - - + + - - - + + + ) } @@ -169,7 +146,8 @@ export function ExploreStackNavigator(): JSX.Element { border: 'transparent', notification: 'transparent', }, - }}> + }} + > + }} + > - + {(props): JSX.Element => } @@ -208,33 +186,24 @@ export function FiatOnRampStackNavigator(): JSX.Element { fullScreenGestureEnabled: true, gestureEnabled: true, animation: 'slide_from_right', - }}> - + }} + > + - + ) } -const renderHeaderBackButton = (): JSX.Element => - export function OnboardingStackNavigator(): JSX.Element { const colors = useSporeColors() const seedPhraseRefactorEnabled = useFeatureFlag(FeatureFlags.SeedPhraseRefactorNative) - const SeedPhraseInputComponent = seedPhraseRefactorEnabled - ? SeedPhraseInputScreenV2 - : SeedPhraseInputScreen + const SeedPhraseInputComponent = seedPhraseRefactorEnabled ? SeedPhraseInputScreenV2 : SeedPhraseInputScreen const isOnboardingKeyringEnabled = useFeatureFlag(FeatureFlags.OnboardingKeyring) @@ -244,12 +213,14 @@ export function OnboardingStackNavigator(): JSX.Element { + }} + > {isOnboardingKeyringEnabled && ( - - - - + + + + - + - + - - - + + + ) } -const renderHeaderBackImage = (): JSX.Element => ( - -) - export function UnitagStackNavigator(): JSX.Element { const colors = useSporeColors() const insets = useDeviceInsets() @@ -363,7 +303,8 @@ export function UnitagStackNavigator(): JSX.Element { headerLeftContainerStyle: { paddingLeft: spacing.spacing16 }, headerRightContainerStyle: { paddingRight: spacing.spacing16 }, ...TransitionPresets.SlideFromRightIOS, - }}> + }} + > - {finishedOnboarding && ( - - )} + }} + > + {finishedOnboarding && } = - undefined extends RootParamList[RouteName] - ? [RouteName] | [RouteName, RootParamList[RouteName]] - : [RouteName, RootParamList[RouteName]] +export type RootNavigationArgs = undefined extends RootParamList[RouteName] + ? [RouteName] | [RouteName, RootParamList[RouteName]] + : [RouteName, RootParamList[RouteName]] function isNavigationRefReady(): boolean { if (!navigationRef.isReady()) { @@ -18,9 +17,7 @@ function isNavigationRefReady(): boolean { return true } -export function navigate( - ...args: RootNavigationArgs -): void { +export function navigate(...args: RootNavigationArgs): void { const [routeName, params] = args if (!isNavigationRefReady()) { return @@ -42,7 +39,7 @@ export function goBack(): void { } export function dispatchNavigationAction( - action: NavigationAction | ((state: NavigationState) => NavigationAction) + action: NavigationAction | ((state: NavigationState) => NavigationAction), ): void { if (!isNavigationRefReady()) { return diff --git a/apps/mobile/src/app/navigation/types.ts b/apps/mobile/src/app/navigation/types.ts index 609a5b9a5b0..b56fef04414 100644 --- a/apps/mobile/src/app/navigation/types.ts +++ b/apps/mobile/src/app/navigation/types.ts @@ -8,12 +8,7 @@ import { NativeStackNavigationProp, NativeStackScreenProps } from '@react-naviga import { EducationContentType } from 'src/components/education' import { HomeScreenTabIndex } from 'src/screens/HomeScreenTabIndex' import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding' -import { - FiatOnRampScreens, - MobileScreens, - OnboardingScreens, - UnitagScreens, -} from 'uniswap/src/types/screens/mobile' +import { FiatOnRampScreens, MobileScreens, OnboardingScreens, UnitagScreens } from 'uniswap/src/types/screens/mobile' import { NFTItem } from 'wallet/src/features/nfts/types' type NFTItemScreenParams = { @@ -71,10 +66,7 @@ export type OnboardingStackBaseParams = { entryPoint: OnboardingEntryPoint } -export type UnitagEntryPoint = - | OnboardingScreens.Landing - | MobileScreens.Home - | MobileScreens.Settings +export type UnitagEntryPoint = OnboardingScreens.Landing | MobileScreens.Home | MobileScreens.Settings export type SharedUnitagScreenParams = { [UnitagScreens.ClaimUnitag]: { @@ -168,16 +160,20 @@ export type SettingsStackNavigationProp = CompositeNavigationProp< AppStackNavigationProp > -export type SettingsStackScreenProp = - CompositeScreenProps, AppStackScreenProps> +export type SettingsStackScreenProp = CompositeScreenProps< + NativeStackScreenProps, + AppStackScreenProps +> export type OnboardingStackNavigationProp = CompositeNavigationProp< NativeStackNavigationProp, AppStackNavigationProp > -export type UnitagStackScreenProp = - NativeStackScreenProps +export type UnitagStackScreenProp = NativeStackScreenProps< + UnitagStackParamList, + Screen +> export type RootParamList = AppStackParamList & ExploreStackParamList & @@ -186,10 +182,8 @@ export type RootParamList = AppStackParamList & UnitagStackParamList & FiatOnRampStackParamList -export const useAppStackNavigation = (): AppStackNavigationProp => - useNavigation() -export const useExploreStackNavigation = (): ExploreStackNavigationProp => - useNavigation() +export const useAppStackNavigation = (): AppStackNavigationProp => useNavigation() +export const useExploreStackNavigation = (): ExploreStackNavigationProp => useNavigation() export const useSettingsStackNavigation = (): SettingsStackNavigationProp => useNavigation() export const useOnboardingStackNavigation = (): OnboardingStackNavigationProp => diff --git a/apps/mobile/src/app/saga.ts b/apps/mobile/src/app/saga.ts index 0e5f13fa834..4aa307137ce 100644 --- a/apps/mobile/src/app/saga.ts +++ b/apps/mobile/src/app/saga.ts @@ -11,12 +11,7 @@ import { signWcRequestSaga } from 'src/features/walletConnect/signWcRequestSaga' import { call, delay, select, spawn } from 'typed-redux-saga' import { apolloClientRef } from 'wallet/src/data/apollo/usePersistedApolloClient' import { appLanguageWatcherSaga } from 'wallet/src/features/language/saga' -import { - swapActions, - swapReducer, - swapSaga, - swapSagaName, -} from 'wallet/src/features/transactions/swap/swapSaga' +import { swapActions, swapReducer, swapSaga, swapSagaName } from 'wallet/src/features/transactions/swap/swapSaga' import { tokenWrapActions, tokenWrapReducer, @@ -87,11 +82,7 @@ export const monitoredSagaReducers = getMonitoredSagaReducers(monitoredSagas) export function* mobileSaga() { // wait until redux-persist has finished rehydration while (true) { - if ( - yield* select( - (state: { _persist?: PersistState }): boolean | undefined => state._persist?.rehydrated - ) - ) { + if (yield* select((state: { _persist?: PersistState }): boolean | undefined => state._persist?.rehydrated)) { break } yield* delay(REHYDRATION_STATUS_POLLING_INTERVAL) diff --git a/apps/mobile/src/app/store.ts b/apps/mobile/src/app/store.ts index bdc2ff2a166..9d0cc056850 100644 --- a/apps/mobile/src/app/store.ts +++ b/apps/mobile/src/app/store.ts @@ -6,10 +6,9 @@ import { Storage, persistReducer, persistStore } from 'redux-persist' import { MOBILE_STATE_VERSION, migrations } from 'src/app/migrations' import { MobileState, ReducerNames, mobileReducer } from 'src/app/reducer' import { mobileSaga } from 'src/app/saga' -import { fiatOnRampAggregatorApi as sharedFiatOnRampAggregatorApi } from 'uniswap/src/features/fiatOnRamp/api' -import { isNonJestDev } from 'utilities/src/environment' +import { fiatOnRampAggregatorApi } from 'uniswap/src/features/fiatOnRamp/api' +import { isNonJestDev } from 'utilities/src/environment/constants' import { logger } from 'utilities/src/logger/logger' -import { fiatOnRampAggregatorApi, fiatOnRampApi } from 'wallet/src/features/fiatOnRamp/api' import { createStore } from 'wallet/src/state' import { createMigrate } from 'wallet/src/state/createMigrate' import { RootReducerNames, sharedPersistedStateWhitelist } from 'wallet/src/state/reducer' @@ -82,18 +81,14 @@ const sentryReduxEnhancer = Sentry.createReduxEnhancer({ }, }) -const middlewares: Middleware[] = [ - fiatOnRampApi.middleware, - fiatOnRampAggregatorApi.middleware, - sharedFiatOnRampAggregatorApi.middleware, -] +const middlewares: Middleware[] = [fiatOnRampAggregatorApi.middleware] if (isNonJestDev) { const createDebugger = require('redux-flipper').default middlewares.push(createDebugger()) } export const setupStore = ( - preloadedState?: PreloadedState + preloadedState?: PreloadedState, // eslint-disable-next-line @typescript-eslint/explicit-function-return-type ) => { return createStore({ @@ -107,6 +102,3 @@ export const setupStore = ( export const store = setupStore() export const persistor = persistStore(store) - -export type AppDispatch = typeof store.dispatch -export type AppStore = typeof store diff --git a/apps/mobile/src/components/NFT/NftView.tsx b/apps/mobile/src/components/NFT/NftView.tsx index 8cade9cab67..2884773ec21 100644 --- a/apps/mobile/src/components/NFT/NftView.tsx +++ b/apps/mobile/src/components/NFT/NftView.tsx @@ -3,10 +3,7 @@ import { Flex, ImpactFeedbackStyle, TouchableArea } from 'ui/src' import { borderRadii } from 'ui/src/theme' import noop from 'utilities/src/react/noop' import { NFTViewer } from 'wallet/src/features/images/NFTViewer' -import { - ESTIMATED_NFT_LIST_ITEM_SIZE, - MAX_NFT_IMAGE_SIZE, -} from 'wallet/src/features/nfts/constants' +import { ESTIMATED_NFT_LIST_ITEM_SIZE, MAX_NFT_IMAGE_SIZE } from 'wallet/src/features/nfts/constants' import { NFTItem } from 'wallet/src/features/nfts/types' import { useNFTContextMenu } from 'wallet/src/features/nfts/useNftContextMenu' @@ -14,9 +11,11 @@ export function NftView({ owner, item, onPress, + index, }: { owner: Address item: NFTItem + index?: number onPress: () => void }): JSX.Element { const { menuActions, onContextMenuPress } = useNFTContextMenu({ @@ -32,21 +31,25 @@ export function NftView({ actions={menuActions} disabled={menuActions.length === 0} style={{ borderRadius: borderRadii.rounded16 }} - onPress={onContextMenuPress}> + onPress={onContextMenuPress} + > + onPress={onPress} + > + width="100%" + > number.formatted.value.split(separator)[0] || '', - [number, separator] - ) + const wholePart = useDerivedValue(() => number.formatted.value.split(separator)[0] || '', [number, separator]) const decimalPart = useDerivedValue( () => separator + (number.formatted.value.split(separator)[1] || ''), - [number, separator] + [number, separator], ) const wholeStyle = useMemo(() => { @@ -90,19 +87,9 @@ export const AnimatedDecimalNumber = memo(function AnimatedDecimalNumber( return ( - + {decimalPart.value !== separator && ( - + )} ) diff --git a/apps/mobile/src/components/PriceExplorer/PriceExplorer.tsx b/apps/mobile/src/components/PriceExplorer/PriceExplorer.tsx index de9443811a2..868c2ee927e 100644 --- a/apps/mobile/src/components/PriceExplorer/PriceExplorer.tsx +++ b/apps/mobile/src/components/PriceExplorer/PriceExplorer.tsx @@ -9,11 +9,7 @@ import { TimeRangeGroup } from 'src/components/PriceExplorer/TimeRangeGroup' import { CURSOR_INNER_SIZE, CURSOR_SIZE } from 'src/components/PriceExplorer/constants' import { useChartDimensions } from 'src/components/PriceExplorer/useChartDimensions' import { useLineChartPrice } from 'src/components/PriceExplorer/usePrice' -import { - PriceNumberOfDigits, - TokenSpotData, - useTokenPriceHistory, -} from 'src/components/PriceExplorer/usePriceHistory' +import { PriceNumberOfDigits, TokenSpotData, useTokenPriceHistory } from 'src/components/PriceExplorer/usePriceHistory' import { Loader } from 'src/components/loading' import { Flex, HapticFeedback } from 'ui/src' import { spacing } from 'ui/src/theme' @@ -36,11 +32,7 @@ function PriceTextSection({ loading, numberOfDigits, spotPrice }: PriceTextProps return ( - + @@ -68,9 +60,8 @@ export const PriceExplorer = memo(function PriceExplorer({ useTokenPriceHistory(currencyId) const { convertFiatAmount } = useLocalizationContext() - const conversionRate = convertFiatAmount().amount - const shouldShowAnimatedDot = - selectedDuration === HistoryDuration.Day || selectedDuration === HistoryDuration.Hour + const conversionRate = convertFiatAmount(1).amount + const shouldShowAnimatedDot = selectedDuration === HistoryDuration.Day || selectedDuration === HistoryDuration.Hour const additionalPadding = shouldShowAnimatedDot ? 40 : 0 const { lastPricePoint, convertedPriceHistory } = useMemo(() => { @@ -93,10 +84,7 @@ export const PriceExplorer = memo(function PriceExplorer({ ) }, [data, convertedSpotValue]) - if ( - !loading && - (!convertedPriceHistory || (!convertedSpot && selectedDuration === HistoryDuration.Day)) - ) { + if (!loading && (!convertedPriceHistory || (!convertedSpot && selectedDuration === HistoryDuration.Day))) { // Propagate retry up while refetching, if available const refetchAndRetry = (): void => { if (refetch) { @@ -127,9 +115,7 @@ export const PriceExplorer = memo(function PriceExplorer({ } return ( - + { if (index >= commaIndex && commaIndex > DEEMPHASIZED_DECIMALS_THRESHOLD) { return deemphasizedColor @@ -42,17 +42,9 @@ const getEmphasizedNumberColor = ( return emphasizedColor } -const shouldUseSeparator = ( - index: number, - commaIndex: number, - decimalPlaceIndex: number -): boolean => { +const shouldUseSeparator = (index: number, commaIndex: number, decimalPlaceIndex: number): boolean => { 'worklet' - return ( - (index - commaIndex) % 4 === 0 && - index - commaIndex < 0 && - index > commaIndex - decimalPlaceIndex - ) + return (index - commaIndex) % 4 === 0 && index - commaIndex < 0 && index > commaIndex - decimalPlaceIndex } const NumbersMain = ({ @@ -97,7 +89,8 @@ const NumbersMain = ({ }, animatedTextStyle, ]} - onLayout={onLayout}> + onLayout={onLayout} + > {NUMBER_ARRAY} ) @@ -126,12 +119,7 @@ const RollNumber = ({ currency: FiatCurrencyInfo }): JSX.Element => { const colors = useSporeColors() - const numberColor = getEmphasizedNumberColor( - index, - commaIndex, - colors.neutral1.val, - colors.neutral3.val - ) + const numberColor = getEmphasizedNumberColor(index, commaIndex, colors.neutral1.val, colors.neutral3.val) const animatedDigit = useDerivedValue(() => { const char = chars.value[index - (commaIndex - decimalPlace.value)] @@ -161,8 +149,7 @@ const RollNumber = ({ }, [animatedDigit, shouldAnimate]) const animatedWrapperStyle = useAnimatedStyle(() => { - const digitWidth = - animatedDigit.value !== undefined ? NUMBER_WIDTH_ARRAY[animatedDigit.value] ?? 0 : 0 + const digitWidth = animatedDigit.value !== undefined ? NUMBER_WIDTH_ARRAY[animatedDigit.value] ?? 0 : 0 const rowWidth = digitWidth + ADDITIONAL_WIDTH_FOR_ANIMATIONS - 7 return { @@ -184,8 +171,7 @@ const RollNumber = ({ } } - const digitWidth = - chars.value[index - (commaIndex - decimalPlace.value)] === currency.groupingSeparator ? 8 : 0 + const digitWidth = chars.value[index - (commaIndex - decimalPlace.value)] === currency.groupingSeparator ? 8 : 0 const rowWidth = Math.max(digitWidth, 0) @@ -207,7 +193,8 @@ const RollNumber = ({ animatedFontStyle, AnimatedFontStyles.fontStyle, { height: DIGIT_HEIGHT, backgroundColor: colors.surface1.val }, - ]}> + ]} + > {currency.decimalSeparator} ) @@ -222,7 +209,8 @@ const RollNumber = ({ animatedFontStyle, AnimatedFontStyles.fontStyle, { height: DIGIT_HEIGHT, backgroundColor: colors.surface1.val }, - ]}> + ]} + > {currency.groupingSeparator} @@ -236,12 +224,9 @@ const RollNumber = ({ { marginRight: -ADDITIONAL_WIDTH_FOR_ANIMATIONS, }, - ]}> - + ]} + > + ) } @@ -272,7 +257,8 @@ const Numbers = ({ (index) => ( + style={[{ height: DIGIT_HEIGHT }, AnimatedCharStyles.wrapperStyle]} + > - ) + ), ) } @@ -334,7 +320,7 @@ const PriceExplorerAnimatedNumber = ({ return Number(accumulator) + Number(NUMBER_WIDTH_ARRAY[Number(currentValue)]) } return accumulator - }) + }), ) }, (priceWidth: number) => { @@ -348,7 +334,7 @@ const PriceExplorerAnimatedNumber = ({ scale.value = withTiming(1) offset.value = withTiming(0) } - } + }, ) const hidePlaceholder = (): void => { @@ -358,7 +344,8 @@ const PriceExplorerAnimatedNumber = ({ const currencySymbol = ( + style={[AnimatedFontStyles.fontStyle, { height: DIGIT_HEIGHT, color: colors.neutral1.val }]} + > {currency.fullSymbol} ) @@ -366,22 +353,15 @@ const PriceExplorerAnimatedNumber = ({ const lessThanSymbol = ( + style={[AnimatedFontStyles.fontStyle, { height: DIGIT_HEIGHT, color: colors.neutral1.val }, lessThanStyle]} + > {'<'} ) const scaleWraper = useAnimatedStyle(() => { return { - transform: [ - { translateX: -SCREEN_WIDTH / 2 }, - { scale: scale.value }, - { translateX: SCREEN_WIDTH / 2 }, - ], + transform: [{ translateX: -SCREEN_WIDTH / 2 }, { scale: scale.value }, { translateX: SCREEN_WIDTH / 2 }], } }) diff --git a/apps/mobile/src/components/PriceExplorer/PriceExplorerError.tsx b/apps/mobile/src/components/PriceExplorer/PriceExplorerError.tsx index a18b71e8fab..779f4657129 100644 --- a/apps/mobile/src/components/PriceExplorer/PriceExplorerError.tsx +++ b/apps/mobile/src/components/PriceExplorer/PriceExplorerError.tsx @@ -27,7 +27,8 @@ export function PriceExplorerError({ borderRadius="$rounded16" height={chartHeight} justifyContent="center" - overflow="hidden"> + overflow="hidden" + > + 0 ? -1 : 1 }, ]} /> - + ) } diff --git a/apps/mobile/src/components/PriceExplorer/TimeRangeGroup.tsx b/apps/mobile/src/components/PriceExplorer/TimeRangeGroup.tsx index a97bb5a6ee0..38796adb87a 100644 --- a/apps/mobile/src/components/PriceExplorer/TimeRangeGroup.tsx +++ b/apps/mobile/src/components/PriceExplorer/TimeRangeGroup.tsx @@ -1,11 +1,6 @@ import React, { useState } from 'react' import { I18nManager, StyleSheet, View } from 'react-native' -import { - SharedValue, - interpolateColor, - useAnimatedStyle, - useSharedValue, -} from 'react-native-reanimated' +import { SharedValue, interpolateColor, useAnimatedStyle, useSharedValue } from 'react-native-reanimated' import { TIME_RANGES } from 'src/components/PriceExplorer/constants' import { useChartDimensions } from 'src/components/PriceExplorer/useChartDimensions' import { Flex, TouchableArea, useSporeColors } from 'ui/src' @@ -31,11 +26,7 @@ export function TimeRangeLabel({ index, label, selectedIndex, transition }: Prop return { color: colors.neutral2.val } } - const color = interpolateColor( - transition.value, - [0, 1], - [colors.neutral2.val, colors.neutral1.val] - ) + const color = interpolateColor(transition.value, [0, 1], [colors.neutral2.val, colors.neutral1.val]) return { color } }) @@ -47,11 +38,7 @@ export function TimeRangeLabel({ index, label, selectedIndex, transition }: Prop ) } -export function TimeRangeGroup({ - setDuration, -}: { - setDuration: (newDuration: HistoryDuration) => void -}): JSX.Element { +export function TimeRangeGroup({ setDuration }: { setDuration: (newDuration: HistoryDuration) => void }): JSX.Element { const { chartWidth, buttonWidth, labelWidth } = useChartDimensions() const transition = useSharedValue(1) const previousIndex = useSharedValue(1) @@ -72,7 +59,7 @@ export function TimeRangeGroup({ }, ], }), - [adjustedLabelWidth, buttonWidth, currentIndex, isRTL] + [adjustedLabelWidth, buttonWidth, currentIndex, isRTL], ) return ( @@ -98,7 +85,8 @@ export function TimeRangeGroup({ transition.value = 0 currentIndex.value = index transition.value = 1 - }}> + }} + > adjustedLabelWidth) { setAdjustedLabelWidth(width) } - }}> - + }} + > + diff --git a/apps/mobile/src/components/PriceExplorer/constants.ts b/apps/mobile/src/components/PriceExplorer/constants.ts index 2b128b60a56..e725b225076 100644 --- a/apps/mobile/src/components/PriceExplorer/constants.ts +++ b/apps/mobile/src/components/PriceExplorer/constants.ts @@ -9,27 +9,11 @@ export const CURSOR_SIZE = CURSOR_INNER_SIZE + 6 export const LINE_WIDTH = 1 export const TIME_RANGES = [ - [ - HistoryDuration.Hour, - i18n.t('token.priceExplorer.timeRangeLabel.hour'), - ElementName.TimeFrame1H, - ], + [HistoryDuration.Hour, i18n.t('token.priceExplorer.timeRangeLabel.hour'), ElementName.TimeFrame1H], [HistoryDuration.Day, i18n.t('token.priceExplorer.timeRangeLabel.day'), ElementName.TimeFrame1D], - [ - HistoryDuration.Week, - i18n.t('token.priceExplorer.timeRangeLabel.week'), - ElementName.TimeFrame1W, - ], - [ - HistoryDuration.Month, - i18n.t('token.priceExplorer.timeRangeLabel.month'), - ElementName.TimeFrame1M, - ], - [ - HistoryDuration.Year, - i18n.t('token.priceExplorer.timeRangeLabel.year'), - ElementName.TimeFrame1Y, - ], + [HistoryDuration.Week, i18n.t('token.priceExplorer.timeRangeLabel.week'), ElementName.TimeFrame1W], + [HistoryDuration.Month, i18n.t('token.priceExplorer.timeRangeLabel.month'), ElementName.TimeFrame1M], + [HistoryDuration.Year, i18n.t('token.priceExplorer.timeRangeLabel.year'), ElementName.TimeFrame1Y], // TODO (MOB-3585): fix performance issue with All time range and re-enable // [HistoryDuration.Max, i18n.t('token.priceExplorer.timeRangeLabel.all'), ElementName.TimeFrameAll], ] as const diff --git a/apps/mobile/src/components/PriceExplorer/useChartDimensions.test.ts b/apps/mobile/src/components/PriceExplorer/useChartDimensions.test.ts index 463fca48452..91cb356dedd 100644 --- a/apps/mobile/src/components/PriceExplorer/useChartDimensions.test.ts +++ b/apps/mobile/src/components/PriceExplorer/useChartDimensions.test.ts @@ -12,9 +12,7 @@ const sharedDimensions = { describe(useChartDimensions, () => { it('returns small chart height for small screens', () => { - jest - .spyOn(Dimensions, 'get') - .mockReturnValue({ ...sharedDimensions, height: heightBreakpoints.short - 1 }) + jest.spyOn(Dimensions, 'get').mockReturnValue({ ...sharedDimensions, height: heightBreakpoints.short - 1 }) const { result } = renderHook(() => useChartDimensions()) expect(result.current).toEqual({ @@ -26,9 +24,7 @@ describe(useChartDimensions, () => { }) it('returns large chart height for large screens', () => { - jest - .spyOn(Dimensions, 'get') - .mockReturnValue({ ...sharedDimensions, height: heightBreakpoints.short }) + jest.spyOn(Dimensions, 'get').mockReturnValue({ ...sharedDimensions, height: heightBreakpoints.short }) const { result } = renderHook(() => useChartDimensions()) expect(result.current).toEqual({ diff --git a/apps/mobile/src/components/PriceExplorer/usePrice.test.ts b/apps/mobile/src/components/PriceExplorer/usePrice.test.ts index 83ed0a3307f..563ea30d6fc 100644 --- a/apps/mobile/src/components/PriceExplorer/usePrice.test.ts +++ b/apps/mobile/src/components/PriceExplorer/usePrice.test.ts @@ -6,10 +6,7 @@ import { useLineChartPrice as useRNWagmiChartLineChartPrice, } from 'react-native-wagmi-charts' import { act } from 'react-test-renderer' -import { - useLineChartPrice, - useLineChartRelativeChange, -} from 'src/components/PriceExplorer/usePrice' +import { useLineChartPrice, useLineChartRelativeChange } from 'src/components/PriceExplorer/usePrice' import { renderHookWithProviders } from 'src/test/render' jest.mock('react-native-wagmi-charts') @@ -20,9 +17,7 @@ const cursorFormattedValue = makeMutable('-') const currentIndex = makeMutable(0) const isActive = makeMutable(false) -const mockData = ( - args: { data?: TLineChartData; currentIndex?: number; isActive?: boolean } = {} -): void => { +const mockData = (args: { data?: TLineChartData; currentIndex?: number; isActive?: boolean } = {}): void => { currentIndex.value = args.currentIndex ?? 0 isActive.value = args.isActive ?? false // react-native-wagmi-charts is mocked so we can mock the return @@ -52,9 +47,7 @@ describe(useLineChartPrice, () => { beforeEach(() => { const originalModule = jest.requireActual('react-native-wagmi-charts') ;(useLineChart as ReturnType).mockImplementation(originalModule.useLineChart) - ;(useRNWagmiChartLineChartPrice as ReturnType).mockImplementation( - originalModule.useLineChartPrice - ) + ;(useRNWagmiChartLineChartPrice as ReturnType).mockImplementation(originalModule.useLineChartPrice) }) afterAll(() => { @@ -172,7 +165,7 @@ describe(useLineChartPrice, () => { expect.objectContaining({ value: expect.objectContaining({ value: 1 }), formatted: expect.objectContaining({ value: '$1.00' }), - }) + }), ) mockCursorPrice('2') // updates shared values @@ -182,7 +175,7 @@ describe(useLineChartPrice, () => { expect.objectContaining({ value: expect.objectContaining({ value: 2 }), formatted: expect.objectContaining({ value: '$2.00' }), - }) + }), ) }) }) @@ -199,7 +192,7 @@ describe(useLineChartPrice, () => { expect.objectContaining({ value: expect.objectContaining({ value: 1 }), shouldAnimate: expect.objectContaining({ value: true }), - }) + }), ) }) @@ -212,7 +205,7 @@ describe(useLineChartPrice, () => { expect.objectContaining({ value: expect.objectContaining({ value: 2 }), shouldAnimate: expect.objectContaining({ value: false }), - }) + }), ) }) }) @@ -267,7 +260,7 @@ describe(useLineChartRelativeChange, () => { expect.objectContaining({ value: expect.objectContaining({ value: 400 }), formatted: expect.objectContaining({ value: '400.00%' }), - }) + }), ) currentIndex.value = 2 @@ -279,7 +272,7 @@ describe(useLineChartRelativeChange, () => { expect.objectContaining({ value: expect.objectContaining({ value: 900 }), formatted: expect.objectContaining({ value: '900.00%' }), - }) + }), ) }) }) diff --git a/apps/mobile/src/components/PriceExplorer/usePrice.tsx b/apps/mobile/src/components/PriceExplorer/usePrice.tsx index a9bc690bede..438b15b9625 100644 --- a/apps/mobile/src/components/PriceExplorer/usePrice.tsx +++ b/apps/mobile/src/components/PriceExplorer/usePrice.tsx @@ -1,14 +1,6 @@ import { useMemo } from 'react' -import { - SharedValue, - useAnimatedReaction, - useDerivedValue, - useSharedValue, -} from 'react-native-reanimated' -import { - useLineChart, - useLineChartPrice as useRNWagmiChartLineChartPrice, -} from 'react-native-wagmi-charts' +import { SharedValue, useAnimatedReaction, useDerivedValue, useSharedValue } from 'react-native-reanimated' +import { useLineChart, useLineChartPrice as useRNWagmiChartLineChartPrice } from 'react-native-wagmi-charts' import { numberToLocaleStringWorklet, numberToPercentWorklet } from 'src/utils/reanimated' import { useAppFiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks' import { useCurrentLocale } from 'wallet/src/features/language/hooks' @@ -26,9 +18,7 @@ export type ValueAndFormattedWithAnimation = ValueAndFormatted & { * Wrapper around react-native-wagmi-chart#useLineChartPrice * @returns latest price when not scrubbing and active price when scrubbing */ -export function useLineChartPrice( - currentSpot?: SharedValue -): ValueAndFormattedWithAnimation { +export function useLineChartPrice(currentSpot?: SharedValue): ValueAndFormattedWithAnimation { const { value: activeCursorPrice } = useRNWagmiChartLineChartPrice({ // do not round precision: 18, @@ -44,7 +34,7 @@ export function useLineChartPrice( if (previousValue && currentValue && shouldAnimate.value) { shouldAnimate.value = false } - } + }, ) const currencyInfo = useAppFiatCurrencyInfo() const locale = useCurrentLocale() @@ -68,7 +58,7 @@ export function useLineChartPrice( style: 'currency', currency: code, }, - symbol + symbol, ) }) @@ -78,7 +68,7 @@ export function useLineChartPrice( formatted: priceFormatted, shouldAnimate, }), - [price, priceFormatted, shouldAnimate] + [price, priceFormatted, shouldAnimate], ) } @@ -99,9 +89,7 @@ export function useLineChartRelativeChange(): ValueAndFormatted { // scrubbing: close price is active price // not scrubbing: close price is period end price - const closePrice = isActive.value - ? data[currentIndex.value]?.value - : data[data.length - 1]?.value + const closePrice = isActive.value ? data[currentIndex.value]?.value : data[data.length - 1]?.value if (openPrice === undefined || closePrice === undefined || openPrice === 0) { return 0 diff --git a/apps/mobile/src/components/PriceExplorer/usePriceHistory.test.ts b/apps/mobile/src/components/PriceExplorer/usePriceHistory.test.ts index 173d629de2e..3d9347d4c95 100644 --- a/apps/mobile/src/components/PriceExplorer/usePriceHistory.test.ts +++ b/apps/mobile/src/components/PriceExplorer/usePriceHistory.test.ts @@ -161,12 +161,7 @@ describe(useTokenPriceHistory, () => { const { resolvers } = queryResolvers({ tokenProjects: () => [ usdcTokenProject({ - priceHistory: [ - undefined, - timestampedAmount({ value: 1 }), - undefined, - timestampedAmount({ value: 2 }), - ], + priceHistory: [undefined, timestampedAmount({ value: 1 }), undefined, timestampedAmount({ value: 2 })], }), ], }) @@ -222,10 +217,7 @@ describe(useTokenPriceHistory, () => { describe('when duration is set to default value (day)', () => { it('returns correct price history', async () => { - const { result } = renderHookWithProviders( - () => useTokenPriceHistory(SAMPLE_CURRENCY_ID_1), - { resolvers } - ) + const { result } = renderHookWithProviders(() => useTokenPriceHistory(SAMPLE_CURRENCY_ID_1), { resolvers }) await waitFor(() => { expect(result.current).toEqual( @@ -235,16 +227,13 @@ describe(useTokenPriceHistory, () => { spot: expect.anything(), }, selectedDuration: HistoryDuration.Day, - }) + }), ) }) }) it('returns correct spot price', async () => { - const { result } = renderHookWithProviders( - () => useTokenPriceHistory(SAMPLE_CURRENCY_ID_1), - { resolvers } - ) + const { result } = renderHookWithProviders(() => useTokenPriceHistory(SAMPLE_CURRENCY_ID_1), { resolvers }) await waitFor(() => { expect(result.current.data?.spot).toEqual({ @@ -261,7 +250,7 @@ describe(useTokenPriceHistory, () => { it('returns correct price history', async () => { const { result } = renderHookWithProviders( () => useTokenPriceHistory(SAMPLE_CURRENCY_ID_1, jest.fn(), HistoryDuration.Year), - { resolvers } + { resolvers }, ) await waitFor(() => { @@ -272,7 +261,7 @@ describe(useTokenPriceHistory, () => { spot: expect.anything(), }, selectedDuration: HistoryDuration.Year, - }) + }), ) }) }) @@ -280,7 +269,7 @@ describe(useTokenPriceHistory, () => { it('returns correct spot price', async () => { const { result } = renderHookWithProviders( () => useTokenPriceHistory(SAMPLE_CURRENCY_ID_1, jest.fn(), HistoryDuration.Year), - { resolvers } + { resolvers }, ) await waitFor(() => { expect(result.current.data?.spot).toEqual({ @@ -296,10 +285,9 @@ describe(useTokenPriceHistory, () => { describe('when duration is changed', () => { it('re-fetches data', async () => { const onCompleted = jest.fn() - const { result } = renderHookWithProviders( - () => useTokenPriceHistory(SAMPLE_CURRENCY_ID_1, onCompleted), - { resolvers } - ) + const { result } = renderHookWithProviders(() => useTokenPriceHistory(SAMPLE_CURRENCY_ID_1, onCompleted), { + resolvers, + }) await waitFor(() => { expect(result.current).toEqual( @@ -307,7 +295,7 @@ describe(useTokenPriceHistory, () => { loading: false, error: false, selectedDuration: HistoryDuration.Day, - }) + }), ) }) @@ -324,7 +312,7 @@ describe(useTokenPriceHistory, () => { loading: false, error: false, selectedDuration: HistoryDuration.Week, - }) + }), ) }) @@ -332,10 +320,7 @@ describe(useTokenPriceHistory, () => { }) it('returns new price history and spot price', async () => { - const { result } = renderHookWithProviders( - () => useTokenPriceHistory(SAMPLE_CURRENCY_ID_1), - { resolvers } - ) + const { result } = renderHookWithProviders(() => useTokenPriceHistory(SAMPLE_CURRENCY_ID_1), { resolvers }) await waitFor(() => { expect(result.current.data).toEqual({ @@ -376,10 +361,9 @@ describe(useTokenPriceHistory, () => { throw new Error('error') }, }) - const { result } = renderHookWithProviders( - () => useTokenPriceHistory(SAMPLE_CURRENCY_ID_1), - { resolvers: errorResolvers } - ) + const { result } = renderHookWithProviders(() => useTokenPriceHistory(SAMPLE_CURRENCY_ID_1), { + resolvers: errorResolvers, + }) await waitFor(() => { expect(result.current.loading).toBe(false) diff --git a/apps/mobile/src/components/PriceExplorer/usePriceHistory.ts b/apps/mobile/src/components/PriceExplorer/usePriceHistory.ts index e8a7ddcc5c5..601a89000de 100644 --- a/apps/mobile/src/components/PriceExplorer/usePriceHistory.ts +++ b/apps/mobile/src/components/PriceExplorer/usePriceHistory.ts @@ -2,15 +2,15 @@ import { maxBy } from 'lodash' import { Dispatch, SetStateAction, useCallback, useMemo, useRef, useState } from 'react' import { SharedValue, useDerivedValue } from 'react-native-reanimated' import { TLineChartData } from 'react-native-wagmi-charts' +import { PollingInterval } from 'uniswap/src/constants/misc' import { HistoryDuration, TimestampedAmount, useTokenPriceHistoryQuery, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { GqlResult } from 'uniswap/src/data/types' -import { PollingInterval } from 'wallet/src/constants/misc' +import { currencyIdToContractInput } from 'uniswap/src/features/dataApi/utils' import { isError, isNonPollingRequestInFlight } from 'wallet/src/data/utils' -import { currencyIdToContractInput } from 'wallet/src/features/dataApi/utils' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' export type TokenSpotData = { @@ -29,7 +29,7 @@ export type PriceNumberOfDigits = { export function useTokenPriceHistory( currencyId: string, onCompleted?: () => void, - initialDuration: HistoryDuration = HistoryDuration.Day + initialDuration: HistoryDuration = HistoryDuration.Day, ): Omit< GqlResult<{ priceHistory?: TLineChartData @@ -86,7 +86,7 @@ export function useTokenPriceHistory( relativeChange: spotRelativeChange, } : undefined, - [price, spotValue, spotRelativeChange] + [price, spotValue, spotRelativeChange], ) const formattedPriceHistory = useMemo(() => { @@ -102,7 +102,7 @@ export function useTokenPriceHistory( priceHistory: formattedPriceHistory, spot, }), - [formattedPriceHistory, spot] + [formattedPriceHistory, spot], ) const numberOfDigits = useMemo(() => { @@ -139,6 +139,6 @@ export function useTokenPriceHistory( numberOfDigits, onCompleted, }), - [data, duration, networkStatus, priceData, retry, onCompleted, numberOfDigits] + [data, duration, networkStatus, priceData, retry, onCompleted, numberOfDigits], ) } diff --git a/apps/mobile/src/components/QRCodeScanner/QRCodeScanner.tsx b/apps/mobile/src/components/QRCodeScanner/QRCodeScanner.tsx index 6ab4c88f278..b132c5c7939 100644 --- a/apps/mobile/src/components/QRCodeScanner/QRCodeScanner.tsx +++ b/apps/mobile/src/components/QRCodeScanner/QRCodeScanner.tsx @@ -13,9 +13,9 @@ import { Global, Photo } from 'ui/src/components/icons' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { useDeviceDimensions } from 'ui/src/hooks/useDeviceDimensions' import { iconSizes, spacing } from 'ui/src/theme' +import PasteButton from 'uniswap/src/components/buttons/PasteButton' import { Sentry } from 'utilities/src/logger/Sentry' import { DevelopmentOnly } from 'wallet/src/components/DevelopmentOnly/DevelopmentOnly' -import PasteButton from 'wallet/src/components/buttons/PasteButton' import { openSettings } from 'wallet/src/utils/linking' type QRCodeScannerProps = { @@ -62,7 +62,7 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E onScanCode(data) setIsReadingImageFile(false) }, - [onScanCode, shouldFreezeCamera] + [onScanCode, shouldFreezeCamera], ) const onPickImageFilePress = useCallback(async (): Promise => { @@ -84,9 +84,7 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E return } - const result = ( - await BarCodeScanner.scanFromURLAsync(uri, [BarCodeScanner.Constants.BarCodeType.qr]) - )[0] + const result = (await BarCodeScanner.scanFromURLAsync(uri, [BarCodeScanner.Constants.BarCodeType.qr]))[0] if (!result) { Alert.alert(t('qrScanner.error.none')) @@ -127,12 +125,7 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E const scannerSize = Math.min(overlayWidth, cameraWidth) * SCAN_ICON_WIDTH_RATIO return ( - + {permissionStatus === PermissionStatus.GRANTED && !isReadingImageFile && ( @@ -147,17 +140,14 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E )} - + setOverlayLayout(event.nativeEvent.layout)}> + onLayout={(event: LayoutChangeEvent): void => setOverlayLayout(event.nativeEvent.layout)} + > setInfoLayout(event.nativeEvent.layout)}> + onLayout={(event: LayoutChangeEvent): void => setInfoLayout(event.nativeEvent.layout)} + > {t('qrScanner.title')} {!shouldFreezeCamera ? ( // camera isn't frozen (after seeing barcode) — show the camera scan icon (the four white corners) - + ) : ( // camera has been frozen (has seen a barcode) — show the loading spinner and "Connecting..." or "Loading..." - + + top={scannerSize / 2 - LOADER_SIZE / 2} + > - {isWalletConnectModal - ? t('qrScanner.status.connecting') - : t('qrScanner.status.loading')} + {isWalletConnectModal ? t('qrScanner.status.connecting') : t('qrScanner.status.loading')} @@ -213,18 +193,15 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E {/* when in development mode AND there's no camera (using iOS Simulator), add a paste button */} {!shouldFreezeCamera ? ( - + + p="$spacing12" + > This paste button will only show up in development mode @@ -246,15 +223,15 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E }, ], }} - onLayout={(event: LayoutChangeEvent): void => - setBottomLayout(event.nativeEvent.layout) - }> + onLayout={(event: LayoutChangeEvent): void => setBottomLayout(event.nativeEvent.layout)} + > + onPress={onPickImageFilePress} + > {isReadingImageFile ? ( ) : ( @@ -267,7 +244,8 @@ export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.E fontFamily="$body" icon={} theme="secondary" - onPress={props.onPressConnections}> + onPress={props.onPressConnections} + > {t('qrScanner.button.connections', { count: props.numConnections })} )} @@ -329,24 +307,17 @@ const GradientOverlay = memo(function GradientOverlay({ justifyContent="center" position="absolute" style={StyleSheet.absoluteFill} - onLayout={onLayout}> + onLayout={onLayout} + > - + - + {!shouldFreezeCamera ? ( diff --git a/apps/mobile/src/components/RecipientSelect/RecipientScanModal.tsx b/apps/mobile/src/components/RecipientSelect/RecipientScanModal.tsx index af72743da39..ad42e86c55f 100644 --- a/apps/mobile/src/components/RecipientSelect/RecipientScanModal.tsx +++ b/apps/mobile/src/components/RecipientSelect/RecipientScanModal.tsx @@ -4,13 +4,14 @@ import { Alert } from 'react-native' import 'react-native-reanimated' import { useAppSelector } from 'src/app/hooks' import { QRCodeScanner } from 'src/components/QRCodeScanner/QRCodeScanner' -import { getSupportedURI, URIType } from 'src/components/WalletConnect/ScanSheet/util' +import { getSupportedURI, URIType } from 'src/components/Requests/ScanSheet/util' import { Flex, HapticFeedback, Text, TouchableArea, useIsDarkMode, useSporeColors } from 'ui/src' import Scan from 'ui/src/assets/icons/receive.svg' import ScanQRIcon from 'ui/src/assets/icons/scan.svg' import { iconSizes } from 'ui/src/theme' import { BottomSheetModal } from 'uniswap/src/components/modals/BottomSheetModal' -import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants' +import { ModalName } from 'uniswap/src/features/telemetry/constants' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants' import { WalletQRCode } from 'wallet/src/components/QRCodeScanner/WalletQRCode' import { selectActiveAccountAddress } from 'wallet/src/features/wallet/selectors' @@ -24,9 +25,7 @@ export function RecipientScanModal({ onSelectRecipient, onClose }: Props): JSX.E const { t } = useTranslation() const colors = useSporeColors() const activeAddress = useAppSelector(selectActiveAccountAddress) - const [currentScreenState, setCurrentScreenState] = useState( - ScannerModalState.ScanQr - ) + const [currentScreenState, setCurrentScreenState] = useState(ScannerModalState.ScanQr) const [shouldFreezeCamera, setShouldFreezeCamera] = useState(false) const onScanCode = async (uri: string): Promise => { @@ -68,13 +67,12 @@ export function RecipientScanModal({ onSelectRecipient, onClose }: Props): JSX.E fullScreen backgroundColor={colors.surface1.get()} name={ModalName.WalletConnectScan} - onClose={onClose}> + onClose={onClose} + > {currentScreenState === ScannerModalState.ScanQr && ( )} - {currentScreenState === ScannerModalState.WalletQr && activeAddress && ( - - )} + {currentScreenState === ScannerModalState.WalletQr && activeAddress && } + testID={TestID.QRCodeModalToggle} + onPress={onPressBottomToggle} + > {currentScreenState === ScannerModalState.ScanQr ? ( - + ) : ( - + )} {currentScreenState === ScannerModalState.ScanQr diff --git a/apps/mobile/src/components/RecipientSelect/RecipientSelect.tsx b/apps/mobile/src/components/RecipientSelect/RecipientSelect.tsx index 4573aea1fd5..eb1c26b97cc 100644 --- a/apps/mobile/src/components/RecipientSelect/RecipientSelect.tsx +++ b/apps/mobile/src/components/RecipientSelect/RecipientSelect.tsx @@ -1,6 +1,6 @@ -import React, { memo, useCallback, useState } from 'react' +import React, { memo, useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' -import { Keyboard } from 'react-native' +import { Keyboard, TextInput } from 'react-native' import { FadeIn, FadeOut } from 'react-native-reanimated' import { RecipientScanModal } from 'src/components/RecipientSelect/RecipientScanModal' import { Flex, Text, TouchableArea, useSporeColors } from 'ui/src' @@ -8,43 +8,50 @@ import ScanQRIcon from 'ui/src/assets/icons/scan.svg' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { iconSizes } from 'ui/src/theme' import { useBottomSheetContext } from 'uniswap/src/components/modals/BottomSheetContext' -import { ElementName } from 'uniswap/src/features/telemetry/constants' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { RecipientList } from 'wallet/src/components/RecipientSearch/RecipientList' import { useFilteredRecipientSections } from 'wallet/src/components/RecipientSearch/hooks' import { SearchBar } from 'wallet/src/features/search/SearchBar' interface RecipientSelectProps { onSelectRecipient: (newRecipientAddress: string) => void - onToggleShowRecipientSelector: () => void + onHideRecipientSelector: () => void recipient?: string + focusInput?: boolean } function QRScannerIconButton({ onPress }: { onPress: () => void }): JSX.Element { const colors = useSporeColors() return ( - - + + ) } export function _RecipientSelect({ onSelectRecipient, - onToggleShowRecipientSelector, + onHideRecipientSelector, recipient, + focusInput, }: RecipientSelectProps): JSX.Element { const { t } = useTranslation() const { isSheetReady } = useBottomSheetContext() + const inputRef = useRef(null) const [pattern, setPattern] = useState('') const [showQRScanner, setShowQRScanner] = useState(false) const sections = useFilteredRecipientSections(pattern) + useEffect(() => { + if (focusInput) { + inputRef.current?.focus() + } else { + inputRef.current?.blur() + } + }, [focusInput]) + const onPressQRScanner = useCallback(() => { Keyboard.dismiss() setShowQRScanner(true) @@ -56,24 +63,19 @@ export function _RecipientSelect({ return ( <> - + {t('qrScanner.recipient.label.send')} } placeholder={t('qrScanner.recipient.input.placeholder')} value={pattern ?? ''} - onBack={recipient ? onToggleShowRecipientSelector : undefined} + onBack={recipient ? onHideRecipientSelector : undefined} onChangeText={setPattern} + onDismiss={() => Keyboard.dismiss()} /> {!sections.length ? ( @@ -84,14 +86,10 @@ export function _RecipientSelect({ ) : ( // Show either suggested recipients or filtered sections based on query - isSheetReady && ( - - ) + isSheetReady && )} - {showQRScanner && ( - - )} + {showQRScanner && } ) } diff --git a/apps/mobile/src/components/RecipientSelect/hooks.test.ts b/apps/mobile/src/components/RecipientSelect/hooks.test.ts index 6221a1b8e61..0157de8eeb3 100644 --- a/apps/mobile/src/components/RecipientSelect/hooks.test.ts +++ b/apps/mobile/src/components/RecipientSelect/hooks.test.ts @@ -140,10 +140,8 @@ describe(useRecipients, () => { expect(result.current).toEqual( expect.objectContaining({ - sections: expect.not.arrayContaining([ - expect.objectContaining({ title: 'Search results' }), - ]), - }) + sections: expect.not.arrayContaining([expect.objectContaining({ title: 'Search results' })]), + }), ) }) @@ -170,7 +168,7 @@ describe(useRecipients, () => { data: expect.objectContaining({ address: SAMPLE_SEED_ADDRESS_1 }), key: SAMPLE_SEED_ADDRESS_1, }, - ]) + ]), ) }) }) @@ -185,7 +183,7 @@ describe(useRecipients, () => { expect(result.current).toEqual( expect.objectContaining({ sections: expect.not.arrayContaining([expect.objectContaining({ title: 'Recent' })]), - }) + }), ) }) @@ -214,7 +212,7 @@ describe(useRecipients, () => { ], }, ]), - }) + }), ) }) @@ -223,18 +221,9 @@ describe(useRecipients, () => { preloadedState: getPreloadedState({ transactions: { [activeAccount.address]: { - [UniverseChainId.Base as WalletChainId]: [ - sendTxDetailsPending, - sendTxDetailsConfirmed, - ], - [UniverseChainId.Mainnet as WalletChainId]: [ - sendTxDetailsConfirmed, - sendTxDetailsFailed, - ], - [UniverseChainId.Bnb as WalletChainId]: [ - sendTxDetailsPending, - sendTxDetailsConfirmed, - ], + [UniverseChainId.Base as WalletChainId]: [sendTxDetailsPending, sendTxDetailsConfirmed], + [UniverseChainId.Mainnet as WalletChainId]: [sendTxDetailsConfirmed, sendTxDetailsFailed], + [UniverseChainId.Bnb as WalletChainId]: [sendTxDetailsPending, sendTxDetailsConfirmed], }, }, }), @@ -265,11 +254,7 @@ describe(useRecipients, () => { preloadedState: getPreloadedState({ transactions: { [activeAccount.address]: { - [sendTxDetailsPending.chainId]: [ - sendTxDetailsPending, - sendTxDetailsFailed, - sendTxDetailsConfirmed, - ], + [sendTxDetailsPending.chainId]: [sendTxDetailsPending, sendTxDetailsFailed, sendTxDetailsConfirmed], }, }, }), @@ -279,7 +264,7 @@ describe(useRecipients, () => { expect(result.current).toEqual( expect.objectContaining({ sections: expect.arrayContaining([recentRecipientsSectionResult]), - }) + }), ) }) @@ -288,18 +273,9 @@ describe(useRecipients, () => { preloadedState: getPreloadedState({ transactions: { [activeAccount.address]: { - [UniverseChainId.Base as WalletChainId]: [ - sendTxDetailsPending, - sendTxDetailsConfirmed, - ], - [UniverseChainId.Mainnet as WalletChainId]: [ - sendTxDetailsConfirmed, - sendTxDetailsFailed, - ], - [UniverseChainId.Bnb as WalletChainId]: [ - sendTxDetailsPending, - sendTxDetailsConfirmed, - ], + [UniverseChainId.Base as WalletChainId]: [sendTxDetailsPending, sendTxDetailsConfirmed], + [UniverseChainId.Mainnet as WalletChainId]: [sendTxDetailsConfirmed, sendTxDetailsFailed], + [UniverseChainId.Bnb as WalletChainId]: [sendTxDetailsPending, sendTxDetailsConfirmed], }, }, }), @@ -319,10 +295,8 @@ describe(useRecipients, () => { expect(result.current).toEqual( expect.objectContaining({ - sections: expect.not.arrayContaining([ - expect.objectContaining({ title: 'Your wallets' }), - ]), - }) + sections: expect.not.arrayContaining([expect.objectContaining({ title: 'Your wallets' })]), + }), ) }) @@ -335,7 +309,7 @@ describe(useRecipients, () => { expect(result.current).toEqual( expect.objectContaining({ sections: expect.arrayContaining([inactiveWalletsSectionResult]), - }) + }), ) }) @@ -348,7 +322,7 @@ describe(useRecipients, () => { expect(result.current).toEqual( expect.objectContaining({ searchableRecipientOptions: [{ data: inactiveAccount, key: inactiveAccount.address }], - }) + }), ) }) }) @@ -362,10 +336,8 @@ describe(useRecipients, () => { expect(result.current).toEqual( expect.objectContaining({ - sections: expect.not.arrayContaining([ - expect.objectContaining({ title: 'Favorite wallets' }), - ]), - }) + sections: expect.not.arrayContaining([expect.objectContaining({ title: 'Favorite wallets' })]), + }), ) }) @@ -380,7 +352,7 @@ describe(useRecipients, () => { expect(result.current).toEqual( expect.objectContaining({ sections: expect.arrayContaining([favoriteWalletsSectionResult]), - }) + }), ) }) }) @@ -393,18 +365,9 @@ describe(useRecipients, () => { hasInactiveAccounts: true, transactions: { [activeAccount.address]: { - [UniverseChainId.Base as WalletChainId]: [ - sendTxDetailsPending, - sendTxDetailsConfirmed, - ], - [UniverseChainId.Mainnet as WalletChainId]: [ - sendTxDetailsConfirmed, - sendTxDetailsFailed, - ], - [UniverseChainId.Bnb as WalletChainId]: [ - sendTxDetailsPending, - sendTxDetailsConfirmed, - ], + [UniverseChainId.Base as WalletChainId]: [sendTxDetailsPending, sendTxDetailsConfirmed], + [UniverseChainId.Mainnet as WalletChainId]: [sendTxDetailsConfirmed, sendTxDetailsFailed], + [UniverseChainId.Bnb as WalletChainId]: [sendTxDetailsPending, sendTxDetailsConfirmed], }, }, }), @@ -420,7 +383,7 @@ describe(useRecipients, () => { inactiveWalletsSectionResult, favoriteWalletsSectionResult, ]), - }) + }), ) }) }) @@ -432,18 +395,9 @@ describe(useRecipients, () => { hasInactiveAccounts: true, transactions: { [activeAccount.address]: { - [UniverseChainId.Base as WalletChainId]: [ - sendTxDetailsPending, - sendTxDetailsConfirmed, - ], - [UniverseChainId.Mainnet as WalletChainId]: [ - sendTxDetailsConfirmed, - sendTxDetailsFailed, - ], - [UniverseChainId.Bnb as WalletChainId]: [ - sendTxDetailsPending, - sendTxDetailsConfirmed, - ], + [UniverseChainId.Base as WalletChainId]: [sendTxDetailsPending, sendTxDetailsConfirmed], + [UniverseChainId.Mainnet as WalletChainId]: [sendTxDetailsConfirmed, sendTxDetailsFailed], + [UniverseChainId.Bnb as WalletChainId]: [sendTxDetailsPending, sendTxDetailsConfirmed], }, }, }), diff --git a/apps/mobile/src/components/RemoveWallet/AssociatedAccountsList.tsx b/apps/mobile/src/components/RemoveWallet/AssociatedAccountsList.tsx index d2550c2f796..5b594520f0e 100644 --- a/apps/mobile/src/components/RemoveWallet/AssociatedAccountsList.tsx +++ b/apps/mobile/src/components/RemoveWallet/AssociatedAccountsList.tsx @@ -44,7 +44,8 @@ function _AssociatedAccountsList({ accounts }: { accounts: Account[] }): JSX.Ele borderWidth={1} maxHeight={accountsScrollViewHeight} px="$spacing12" - width="100%"> + width="100%" + > {sortedAddressesByBalance.map(({ address, balance }, index) => ( + pb={index !== totalCount - 1 ? '$spacing16' : undefined} + > diff --git a/apps/mobile/src/components/RemoveWallet/RemoveLastMnemonicWalletFooter.tsx b/apps/mobile/src/components/RemoveWallet/RemoveLastMnemonicWalletFooter.tsx index 6d6178fa1e1..2252a4cfc85 100644 --- a/apps/mobile/src/components/RemoveWallet/RemoveLastMnemonicWalletFooter.tsx +++ b/apps/mobile/src/components/RemoveWallet/RemoveLastMnemonicWalletFooter.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react' import { useTranslation } from 'react-i18next' import { Button, CheckBox, Flex, SpinningLoader, Text } from 'ui/src' -import { ElementName } from 'uniswap/src/features/telemetry/constants' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' export function RemoveLastMnemonicWalletFooter({ onPress, @@ -17,12 +17,7 @@ export function RemoveLastMnemonicWalletFooter({ return ( <> - + : undefined} - testID={ElementName.Confirm} + testID={TestID.Confirm} theme="detrimental" - onPress={onPress}> + onPress={onPress} + > {!inProgress ? t('account.wallet.button.remove') : undefined} diff --git a/apps/mobile/src/components/RemoveWallet/RemoveWalletModal.tsx b/apps/mobile/src/components/RemoveWallet/RemoveWalletModal.tsx index 35a50e5dc3a..896dc9e9af5 100644 --- a/apps/mobile/src/components/RemoveWallet/RemoveWalletModal.tsx +++ b/apps/mobile/src/components/RemoveWallet/RemoveWalletModal.tsx @@ -1,7 +1,8 @@ -import React, { useCallback, useState } from 'react' +import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { useAnimatedStyle, withTiming } from 'react-native-reanimated' -import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { useDispatch } from 'react-redux' +import { useAppSelector } from 'src/app/hooks' import { navigate } from 'src/app/navigation/rootNavigation' import { AssociatedAccountsList } from 'src/components/RemoveWallet/AssociatedAccountsList' import { RemoveLastMnemonicWalletFooter } from 'src/components/RemoveWallet/RemoveLastMnemonicWalletFooter' @@ -11,19 +12,17 @@ import { Delay } from 'src/components/layout/Delayed' import { useBiometricAppSettings, useBiometricPrompt } from 'src/features/biometrics/hooks' import { closeModal } from 'src/features/modals/modalSlice' import { selectModalState } from 'src/features/modals/selectModalState' -import { Button, ColorTokens, Flex, SpinningLoader, Text, ThemeKeys, useSporeColors } from 'ui/src' +import { Button, Flex, SpinningLoader, Text, ThemeKeys, useSporeColors } from 'ui/src' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { iconSizes, opacify } from 'ui/src/theme' import { BottomSheetModal } from 'uniswap/src/components/modals/BottomSheetModal' -import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants' +import { ElementName, ModalName, WalletEventName } from 'uniswap/src/features/telemetry/constants' +import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding' import { MobileScreens, OnboardingScreens } from 'uniswap/src/types/screens/mobile' import { logger } from 'utilities/src/logger/logger' import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring' -import { - EditAccountAction, - editAccountActions, -} from 'wallet/src/features/wallet/accounts/editAccountSaga' +import { EditAccountAction, editAccountActions } from 'wallet/src/features/wallet/accounts/editAccountSaga' import { useAccounts } from 'wallet/src/features/wallet/hooks' import { selectSignerMnemonicAccounts } from 'wallet/src/features/wallet/selectors' import { setFinishedOnboarding } from 'wallet/src/features/wallet/slice' @@ -31,7 +30,7 @@ import { setFinishedOnboarding } from 'wallet/src/features/wallet/slice' export function RemoveWalletModal(): JSX.Element | null { const { t } = useTranslation() const colors = useSporeColors() - const dispatch = useAppDispatch() + const dispatch = useDispatch() const addressToAccount = useAccounts() const associatedAccounts = useAppSelector(selectSignerMnemonicAccounts) @@ -48,12 +47,11 @@ export function RemoveWalletModal(): JSX.Element | null { const isRemovingLastMnemonic = isRemovingMnemonic && associatedAccounts.length === 1 const isRemovingRecoveryPhrase = isReplacing || isRemovingLastMnemonic - const hasAccountsLeft = - Object.keys(addressToAccount).length > (isReplacing ? associatedAccounts.length : 1) + const hasAccountsLeft = Object.keys(addressToAccount).length > (isReplacing ? associatedAccounts.length : 1) const [inProgress, setInProgress] = useState(false) const [currentStep, setCurrentStep] = useState( - isRemovingRecoveryPhrase ? RemoveWalletStep.Warning : RemoveWalletStep.Final + isRemovingRecoveryPhrase ? RemoveWalletStep.Warning : RemoveWalletStep.Final, ) const onClose = useCallback((): void => { @@ -87,7 +85,7 @@ export function RemoveWalletModal(): JSX.Element | null { editAccountActions.trigger({ type: EditAccountAction.Remove, accounts: accountsToRemove, - }) + }), ) }) .catch((error) => { @@ -101,10 +99,14 @@ export function RemoveWalletModal(): JSX.Element | null { editAccountActions.trigger({ type: EditAccountAction.Remove, accounts: accountsToRemove, - }) + }), ) } + sendAnalyticsEvent(WalletEventName.WalletRemoved, { + wallets_removed: accountsToRemove.map((a) => a.address), + }) + onClose() setInProgress(false) }, [account, associatedAccounts, dispatch, isReplacing, hasAccountsLeft, onClose]) @@ -115,7 +117,7 @@ export function RemoveWalletModal(): JSX.Element | null { }, () => { setInProgress(false) - } + }, ) const { @@ -163,17 +165,16 @@ export function RemoveWalletModal(): JSX.Element | null { return null } - const { title, description, Icon, iconColorLabel, actionButtonTheme, actionButtonLabel } = - modalContent + const { title, description, Icon, iconColorLabel, actionButtonTheme, actionButtonLabel } = modalContent - // TODO(MOB-1420): clean up types const labelColor: ThemeKeys = iconColorLabel return ( + onClose={onClose} + > - + }} + > + @@ -215,18 +213,12 @@ export function RemoveWalletModal(): JSX.Element | null { )} - ) : undefined - } + icon={inProgress ? : undefined} testID={isRemovingRecoveryPhrase ? ElementName.Continue : ElementName.Remove} theme={actionButtonTheme} width="100%" - onPress={onPress}> + onPress={onPress} + > {inProgress ? undefined : actionButtonLabel} diff --git a/apps/mobile/src/components/RemoveWallet/useModalContent.tsx b/apps/mobile/src/components/RemoveWallet/useModalContent.tsx index 6f4f2f71533..3a0ec12bb2b 100644 --- a/apps/mobile/src/components/RemoveWallet/useModalContent.tsx +++ b/apps/mobile/src/components/RemoveWallet/useModalContent.tsx @@ -91,9 +91,7 @@ export const useModalContent = ({ description: ( - ), + highlight: , }} i18nKey="account.recoveryPhrase.remove.final.description" values={{ cloudProviderName: getCloudProviderName() }} @@ -107,10 +105,8 @@ export const useModalContent = ({ // removing mnemonic account if (account?.type === AccountType.SignerMnemonic && currentStep === RemoveWalletStep.Final) { const associatedAccountNames = concatStrings( - associatedAccounts - .filter((aa): aa is Account => aa.address !== account?.address) - .map((aa) => aa.name ?? ''), - t('common.endAdornment') + associatedAccounts.filter((aa): aa is Account => aa.address !== account?.address).map((aa) => aa.name ?? ''), + t('common.endAdornment'), ) return { @@ -162,13 +158,5 @@ export const useModalContent = ({ actionButtonTheme: 'secondary', } } - }, [ - account, - associatedAccounts, - currentStep, - displayName, - isRemovingRecoveryPhrase, - isReplacing, - t, - ]) + }, [account, associatedAccounts, currentStep, displayName, isRemovingRecoveryPhrase, isReplacing, t]) } diff --git a/apps/mobile/src/components/RemoveWallet/utils.ts b/apps/mobile/src/components/RemoveWallet/utils.ts index 811a6330160..df0cb6213aa 100644 --- a/apps/mobile/src/components/RemoveWallet/utils.ts +++ b/apps/mobile/src/components/RemoveWallet/utils.ts @@ -33,6 +33,6 @@ export function navigateToOnboardingImportMethod(): void { }, }, ], - }) + }), ) } diff --git a/apps/mobile/src/components/WalletConnect/ConnectedDapps/ConnectedDappsList.tsx b/apps/mobile/src/components/Requests/ConnectedDapps/ConnectedDappsList.tsx similarity index 77% rename from apps/mobile/src/components/WalletConnect/ConnectedDapps/ConnectedDappsList.tsx rename to apps/mobile/src/components/Requests/ConnectedDapps/ConnectedDappsList.tsx index 5875d371855..c87a625b624 100644 --- a/apps/mobile/src/components/WalletConnect/ConnectedDapps/ConnectedDappsList.tsx +++ b/apps/mobile/src/components/Requests/ConnectedDapps/ConnectedDappsList.tsx @@ -2,15 +2,12 @@ import React, { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { FlatList, StyleSheet } from 'react-native' import { FadeIn, FadeOut } from 'react-native-reanimated' -import { useAppDispatch } from 'src/app/hooks' -import { DappConnectedNetworkModal } from 'src/components/WalletConnect/ConnectedDapps/DappConnectedNetworksModal' -import { DappConnectionItem } from 'src/components/WalletConnect/ConnectedDapps/DappConnectionItem' +import { useDispatch } from 'react-redux' +import { DappConnectedNetworkModal } from 'src/components/Requests/ConnectedDapps/DappConnectedNetworksModal' +import { DappConnectionItem } from 'src/components/Requests/ConnectedDapps/DappConnectionItem' import { BackButton } from 'src/components/buttons/BackButton' import { openModal } from 'src/features/modals/modalSlice' -import { - WalletConnectSession, - removePendingSession, -} from 'src/features/walletConnect/walletConnectSlice' +import { WalletConnectSession, removePendingSession } from 'src/features/walletConnect/walletConnectSlice' import { Flex, Text, TouchableArea } from 'ui/src' import { Edit, Scan } from 'ui/src/components/icons' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' @@ -25,7 +22,7 @@ type ConnectedDappsProps = { } export function ConnectedDappsList({ backButton, sessions }: ConnectedDappsProps): JSX.Element { - const dispatch = useAppDispatch() + const dispatch = useDispatch() const { t } = useTranslation() const { fullHeight } = useDeviceDimensions() const [isEditing, setIsEditing] = useState(false) @@ -34,20 +31,13 @@ export function ConnectedDappsList({ backButton, sessions }: ConnectedDappsProps const onPressScan = useCallback(() => { // in case we received a pending session from a previous scan after closing modal dispatch(removePendingSession()) - dispatch( - openModal({ name: ModalName.WalletConnectScan, initialState: ScannerModalState.ScanQr }) - ) + dispatch(openModal({ name: ModalName.WalletConnectScan, initialState: ScannerModalState.ScanQr })) }, [dispatch]) return ( <> - + {backButton ?? } @@ -61,12 +51,9 @@ export function ConnectedDappsList({ backButton, sessions }: ConnectedDappsProps { setIsEditing(!isEditing) - }}> - {isEditing ? ( - - ) : ( - - )} + }} + > + {isEditing ? : } ) : ( @@ -102,7 +89,8 @@ export function ConnectedDappsList({ backButton, sessions }: ConnectedDappsProps px="$spacing24" style={{ paddingTop: fullHeight / 5, - }}> + }} + > {t('walletConnect.dapps.manage.empty.title')} @@ -113,10 +101,7 @@ export function ConnectedDappsList({ backButton, sessions }: ConnectedDappsProps )} {selectedSession && ( - setSelectedSession(undefined)} - /> + setSelectedSession(undefined)} /> )} ) diff --git a/apps/mobile/src/components/WalletConnect/ConnectedDapps/DappConnectedNetworksModal.tsx b/apps/mobile/src/components/Requests/ConnectedDapps/DappConnectedNetworksModal.tsx similarity index 89% rename from apps/mobile/src/components/WalletConnect/ConnectedDapps/DappConnectedNetworksModal.tsx rename to apps/mobile/src/components/Requests/ConnectedDapps/DappConnectedNetworksModal.tsx index bd472393b1d..bce5adb51ec 100644 --- a/apps/mobile/src/components/WalletConnect/ConnectedDapps/DappConnectedNetworksModal.tsx +++ b/apps/mobile/src/components/Requests/ConnectedDapps/DappConnectedNetworksModal.tsx @@ -2,8 +2,8 @@ import { getSdkError } from '@walletconnect/utils' import React from 'react' import { Trans, useTranslation } from 'react-i18next' import 'react-native-reanimated' -import { useAppDispatch } from 'src/app/hooks' -import { DappHeaderIcon } from 'src/components/WalletConnect/DappHeaderIcon' +import { useDispatch } from 'react-redux' +import { DappHeaderIcon } from 'src/components/Requests/DappHeaderIcon' import { wcWeb3Wallet } from 'src/features/walletConnect/saga' import { WalletConnectSession, removeSession } from 'src/features/walletConnect/walletConnectSlice' import { Button, Flex, Text } from 'ui/src' @@ -23,13 +23,10 @@ interface DappConnectedNetworkModalProps { onClose: () => void } -export function DappConnectedNetworkModal({ - session, - onClose, -}: DappConnectedNetworkModalProps): JSX.Element { +export function DappConnectedNetworkModal({ session, onClose }: DappConnectedNetworkModalProps): JSX.Element { const { t } = useTranslation() const address = useActiveAccountAddressWithThrow() - const dispatch = useAppDispatch() + const dispatch = useDispatch() const { dapp, id } = session const onDisconnect = async (): Promise => { @@ -52,7 +49,7 @@ export function DappConnectedNetworkModal({ event: WalletConnectEvent.Disconnected, imageUrl: dapp.icon, hideDelay: 3 * ONE_SECOND_MS, - }) + }), ) onClose() } catch (error) { @@ -77,13 +74,7 @@ export function DappConnectedNetworkModal({ - + {session.chains.map((chainId) => ( diff --git a/apps/mobile/src/components/WalletConnect/ConnectedDapps/DappConnectionItem.tsx b/apps/mobile/src/components/Requests/ConnectedDapps/DappConnectionItem.tsx similarity index 87% rename from apps/mobile/src/components/WalletConnect/ConnectedDapps/DappConnectionItem.tsx rename to apps/mobile/src/components/Requests/ConnectedDapps/DappConnectionItem.tsx index 46ed0509828..70fac046a8a 100644 --- a/apps/mobile/src/components/WalletConnect/ConnectedDapps/DappConnectionItem.tsx +++ b/apps/mobile/src/components/Requests/ConnectedDapps/DappConnectionItem.tsx @@ -5,18 +5,18 @@ import { NativeSyntheticEvent, StyleSheet } from 'react-native' import ContextMenu, { ContextMenuOnPressNativeEvent } from 'react-native-context-menu-view' import 'react-native-reanimated' import { FadeIn, FadeOut } from 'react-native-reanimated' -import { useAppDispatch } from 'src/app/hooks' -import { DappHeaderIcon } from 'src/components/WalletConnect/DappHeaderIcon' +import { useDispatch } from 'react-redux' +import { DappHeaderIcon } from 'src/components/Requests/DappHeaderIcon' import { wcWeb3Wallet } from 'src/features/walletConnect/saga' import { WalletConnectSession, removeSession } from 'src/features/walletConnect/walletConnectSlice' import { disableOnPress } from 'src/utils/disableOnPress' import { AnimatedTouchableArea, Flex, ImpactFeedbackStyle, Text, TouchableArea } from 'ui/src' import { iconSizes, spacing } from 'ui/src/theme' -import { ElementName } from 'uniswap/src/features/telemetry/constants' +import { NetworkLogos } from 'uniswap/src/components/network/NetworkLogos' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { WalletConnectEvent } from 'uniswap/src/types/walletConnect' import { logger } from 'utilities/src/logger/logger' import { ONE_SECOND_MS } from 'utilities/src/time/time' -import { NetworkLogos } from 'wallet/src/components/network/NetworkLogos' import { pushNotification } from 'wallet/src/features/notifications/slice' import { AppNotificationType } from 'wallet/src/features/notifications/types' import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' @@ -32,7 +32,7 @@ export function DappConnectionItem({ }): JSX.Element { const { t } = useTranslation() const { dapp } = session - const dispatch = useAppDispatch() + const dispatch = useDispatch() const address = useActiveAccountAddressWithThrow() const onDisconnect = async (): Promise => { @@ -55,16 +55,14 @@ export function DappConnectionItem({ event: WalletConnectEvent.Disconnected, imageUrl: dapp.icon, hideDelay: 3 * ONE_SECOND_MS, - }) + }), ) } catch (error) { logger.error(error, { tags: { file: 'DappConnectionItem', function: 'onDisconnect' } }) } } - const menuActions = [ - { title: t('common.button.disconnect'), systemIcon: 'trash', destructive: true }, - ] + const menuActions = [{ title: t('common.button.disconnect'), systemIcon: 'trash', destructive: true }] const onPress = async (e: NativeSyntheticEvent): Promise => { if (e.nativeEvent.index === 0) { @@ -83,13 +81,15 @@ export function DappConnectionItem({ mb="$spacing12" pb="$spacing12" pt="$spacing24" - px="$spacing12"> + px="$spacing12" + > + zIndex="$tooltip" + > {isEditing ? ( + onPress={onDisconnect} + > ) : ( @@ -123,15 +124,17 @@ export function DappConnectionItem({ onPressChangeNetwork(session)}> + onPress={(): void => onPressChangeNetwork(session)} + > diff --git a/apps/mobile/src/components/WalletConnect/DappHeaderIcon.tsx b/apps/mobile/src/components/Requests/DappHeaderIcon.tsx similarity index 100% rename from apps/mobile/src/components/WalletConnect/DappHeaderIcon.tsx rename to apps/mobile/src/components/Requests/DappHeaderIcon.tsx diff --git a/apps/mobile/src/components/WalletConnect/ModalWithOverlay/ModalWithOverlay.tsx b/apps/mobile/src/components/Requests/ModalWithOverlay/ModalWithOverlay.tsx similarity index 85% rename from apps/mobile/src/components/WalletConnect/ModalWithOverlay/ModalWithOverlay.tsx rename to apps/mobile/src/components/Requests/ModalWithOverlay/ModalWithOverlay.tsx index 8ab3cb7d02b..b0b5550e94e 100644 --- a/apps/mobile/src/components/WalletConnect/ModalWithOverlay/ModalWithOverlay.tsx +++ b/apps/mobile/src/components/Requests/ModalWithOverlay/ModalWithOverlay.tsx @@ -1,8 +1,4 @@ -import { - BottomSheetFooter, - BottomSheetScrollView, - useBottomSheetInternal, -} from '@gorhom/bottom-sheet' +import { BottomSheetFooter, BottomSheetScrollView, useBottomSheetInternal } from '@gorhom/bottom-sheet' import { PropsWithChildren, useCallback, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { @@ -16,12 +12,12 @@ import { ViewStyle, } from 'react-native' import { AnimatedStyle, useDerivedValue } from 'react-native-reanimated' -import { ScrollDownOverlay } from 'src/components/WalletConnect/ModalWithOverlay/ScrollDownOverlay' +import { ScrollDownOverlay } from 'src/components/Requests/ModalWithOverlay/ScrollDownOverlay' import { Button, Flex, useDeviceInsets } from 'ui/src' import { spacing } from 'ui/src/theme' import { BottomSheetModal } from 'uniswap/src/components/modals/BottomSheetModal' import { BottomSheetModalProps } from 'uniswap/src/components/modals/BottomSheetModalProps' -import { ElementName } from 'uniswap/src/features/telemetry/constants' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' const MEASURE_LAYOUT_TIMEOUT = 100 @@ -36,11 +32,7 @@ type ModalWithOverlayProps = PropsWithChildren< } > -const isCloseToBottom = ({ - layoutMeasurement, - contentOffset, - contentSize, -}: NativeScrollEvent): boolean => { +const isCloseToBottom = ({ layoutMeasurement, contentOffset, contentSize }: NativeScrollEvent): boolean => { return layoutMeasurement.height + contentOffset.y >= contentSize.height - spacing.spacing24 } @@ -72,7 +64,7 @@ export function ModalWithOverlay({ setConfirmationEnabled(true) } }, - [showOverlay] + [showOverlay], ) const handleScrollDown = useCallback(() => { @@ -111,7 +103,7 @@ export function ModalWithOverlay({ measureContent(parentHeight) }, MEASURE_LAYOUT_TIMEOUT) }, - [measureContent] + [measureContent], ) return ( @@ -126,7 +118,8 @@ export function ModalWithOverlay({ } showsVerticalScrollIndicator={false} onLayout={handleScrollViewLayout} - onScroll={handleScroll}> + onScroll={handleScroll} + > {children} @@ -173,16 +166,13 @@ function ModalFooter({ () => Math.max(0, animatedContainerHeight.value - animatedPosition.value) - animatedFooterHeight.value - - animatedHandleHeight.value + animatedHandleHeight.value, ) return ( {showScrollDownOverlay && ( - + )} - diff --git a/apps/mobile/src/components/WalletConnect/ModalWithOverlay/ScrollDownOverlay.tsx b/apps/mobile/src/components/Requests/ModalWithOverlay/ScrollDownOverlay.tsx similarity index 93% rename from apps/mobile/src/components/WalletConnect/ModalWithOverlay/ScrollDownOverlay.tsx rename to apps/mobile/src/components/Requests/ModalWithOverlay/ScrollDownOverlay.tsx index 00b154b6d20..11d0bb05e52 100644 --- a/apps/mobile/src/components/WalletConnect/ModalWithOverlay/ScrollDownOverlay.tsx +++ b/apps/mobile/src/components/Requests/ModalWithOverlay/ScrollDownOverlay.tsx @@ -13,10 +13,7 @@ type ScrollDownOverlayProps = { onScrollDownPress: () => void } -export function ScrollDownOverlay({ - onScrollDownPress, - scrollDownButonText, -}: ScrollDownOverlayProps): JSX.Element { +export function ScrollDownOverlay({ onScrollDownPress, scrollDownButonText }: ScrollDownOverlayProps): JSX.Element { const { t } = useTranslation() const { fullHeight, fullWidth } = useDeviceDimensions() const colors = useSporeColors() @@ -32,7 +29,8 @@ export function ScrollDownOverlay({ pb="$spacing24" pointerEvents="box-none" position="absolute" - width="100%"> + width="100%" + > diff --git a/apps/mobile/src/components/WalletConnect/RequestModal/ClientDetails.tsx b/apps/mobile/src/components/Requests/RequestModal/ClientDetails.tsx similarity index 79% rename from apps/mobile/src/components/WalletConnect/RequestModal/ClientDetails.tsx rename to apps/mobile/src/components/Requests/RequestModal/ClientDetails.tsx index abb03e3d2e1..156a8c25602 100644 --- a/apps/mobile/src/components/WalletConnect/RequestModal/ClientDetails.tsx +++ b/apps/mobile/src/components/Requests/RequestModal/ClientDetails.tsx @@ -1,7 +1,7 @@ import React from 'react' import { LinkButton } from 'src/components/buttons/LinkButton' -import { DappHeaderIcon } from 'src/components/WalletConnect/DappHeaderIcon' -import { HeaderText } from 'src/components/WalletConnect/RequestModal/HeaderText' +import { DappHeaderIcon } from 'src/components/Requests/DappHeaderIcon' +import { HeaderText } from 'src/components/Requests/RequestModal/HeaderText' import { WalletConnectRequest } from 'src/features/walletConnect/walletConnectSlice' import { Flex, useSporeColors } from 'ui/src' import { iconSizes } from 'ui/src/theme' @@ -28,11 +28,7 @@ export function ClientDetails({ return ( - + { + const getReadableMethodName = (ethMethod: EthMethod | UwULinkMethod, dappNameOrUrl: string): JSX.Element => { switch (ethMethod) { case EthMethod.PersonalSign: case EthMethod.EthSign: diff --git a/apps/mobile/src/components/WalletConnect/RequestModal/KidSuperCheckinModal.tsx b/apps/mobile/src/components/Requests/RequestModal/KidSuperCheckinModal.tsx similarity index 78% rename from apps/mobile/src/components/WalletConnect/RequestModal/KidSuperCheckinModal.tsx rename to apps/mobile/src/components/Requests/RequestModal/KidSuperCheckinModal.tsx index cd3cf9b8994..5db87b36a44 100644 --- a/apps/mobile/src/components/WalletConnect/RequestModal/KidSuperCheckinModal.tsx +++ b/apps/mobile/src/components/Requests/RequestModal/KidSuperCheckinModal.tsx @@ -1,9 +1,9 @@ import { useBottomSheetInternal } from '@gorhom/bottom-sheet' import { useTranslation } from 'react-i18next' import Animated, { useAnimatedStyle } from 'react-native-reanimated' -import { ModalWithOverlay } from 'src/components/WalletConnect/ModalWithOverlay/ModalWithOverlay' -import { RequestDetailsContent } from 'src/components/WalletConnect/RequestModal/RequestDetails' -import { useUwuLinkContractAllowlist } from 'src/components/WalletConnect/ScanSheet/util' +import { ModalWithOverlay } from 'src/components/Requests/ModalWithOverlay/ModalWithOverlay' +import { RequestDetailsContent } from 'src/components/Requests/RequestModal/RequestDetails' +import { useUwuLinkContractAllowlist } from 'src/components/Requests/ScanSheet/util' import { SignRequest } from 'src/features/walletConnect/walletConnectSlice' import { Flex, useIsDarkMode } from 'ui/src' import { spacing } from 'ui/src/theme' @@ -17,12 +17,7 @@ type Props = { request: SignRequest } -export function KidSuperCheckinModal({ - onClose, - onConfirm, - onReject, - request, -}: Props): JSX.Element { +export function KidSuperCheckinModal({ onClose, onConfirm, onReject, request }: Props): JSX.Element { const { t } = useTranslation() return ( @@ -36,7 +31,8 @@ export function KidSuperCheckinModal({ scrollDownButtonText={t('walletConnect.request.button.scrollDown')} onClose={onClose} onConfirm={onConfirm} - onReject={onReject}> + onReject={onReject} + > ) @@ -45,9 +41,7 @@ export function KidSuperCheckinModal({ function useUniswapCafeLogo(): string | undefined { const isDarkMode = useIsDarkMode() const uwuLinkContractAllowlist = useUwuLinkContractAllowlist() - const logos = uwuLinkContractAllowlist.tokenRecipients.find( - (recipient) => recipient.name === 'Uniswap Cafe' - )?.logo + const logos = uwuLinkContractAllowlist.tokenRecipients.find((recipient) => recipient.name === 'Uniswap Cafe')?.logo if (!logos) { return @@ -75,7 +69,8 @@ function KidSuperCheckinModalContent({ request }: { request: SignRequest }): JSX borderWidth={1} gap="$spacing12" px="$spacing24" - py="$spacing24"> + py="$spacing24" + > diff --git a/apps/mobile/src/components/WalletConnect/RequestModal/RequestDetails.tsx b/apps/mobile/src/components/Requests/RequestModal/RequestDetails.tsx similarity index 82% rename from apps/mobile/src/components/WalletConnect/RequestModal/RequestDetails.tsx rename to apps/mobile/src/components/Requests/RequestModal/RequestDetails.tsx index 90e34cf6989..e1834f6d89d 100644 --- a/apps/mobile/src/components/WalletConnect/RequestModal/RequestDetails.tsx +++ b/apps/mobile/src/components/Requests/RequestModal/RequestDetails.tsx @@ -3,24 +3,20 @@ import { Transaction, TransactionDescription } from 'no-yolo-signatures' import React, { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { ScrollView } from 'react-native-gesture-handler' -import { SpendingDetails } from 'src/components/WalletConnect/RequestModal/SpendingDetails' import { LinkButton } from 'src/components/buttons/LinkButton' -import { - SignRequest, - WalletConnectRequest, - isTransactionRequest, -} from 'src/features/walletConnect/walletConnectSlice' -import { useNoYoloParser } from 'src/utils/useNoYoloParser' +import { SignRequest, WalletConnectRequest, isTransactionRequest } from 'src/features/walletConnect/walletConnectSlice' import { Flex, Text, useSporeColors } from 'ui/src' import { TextVariantTokens, iconSizes } from 'ui/src/theme' +import { toSupportedChainId } from 'uniswap/src/features/chains/utils' import { UniverseChainId, WalletChainId } from 'uniswap/src/types/chains' import { EthMethod, EthTransaction } from 'uniswap/src/types/walletConnect' +import { getValidAddress, shortenAddress } from 'uniswap/src/utils/addresses' import { logger } from 'utilities/src/logger/logger' -import { toSupportedChainId } from 'wallet/src/features/chains/utils' import { useENS } from 'wallet/src/features/ens/useENS' import { ContentRow } from 'wallet/src/features/transactions/TransactionRequest/ContentRow' -import { getValidAddress, shortenAddress } from 'wallet/src/utils/addresses' +import { SpendingDetails } from 'wallet/src/features/transactions/TransactionRequest/SpendingDetails' import { ExplorerDataType, getExplorerLink } from 'wallet/src/utils/linking' +import { useNoYoloParser } from 'wallet/src/utils/useNoYoloParser' const getStrMessage = (request: WalletConnectRequest): string => { if (request.type === EthMethod.PersonalSign || request.type === EthMethod.EthSign) { @@ -59,7 +55,7 @@ const MAX_TYPED_DATA_PARSE_DEPTH = 3 // eslint-disable-next-line @typescript-eslint/no-explicit-any const getParsedObjectDisplay = (chainId: number, obj: any, depth = 0): JSX.Element => { if (depth === MAX_TYPED_DATA_PARSE_DEPTH + 1) { - return ... + return ... } return ( @@ -70,7 +66,7 @@ const getParsedObjectDisplay = (chainId: number, obj: any, depth = 0): JSX.Eleme if (typeof childValue === 'object') { return ( - + {objKey} {getParsedObjectDisplay(chainId, childValue, depth + 1)} @@ -80,20 +76,17 @@ const getParsedObjectDisplay = (chainId: number, obj: any, depth = 0): JSX.Eleme if (typeof childValue === 'string') { return ( - - + + {objKey} {getValidAddress(childValue, true) ? ( - + + + ) : ( - + {childValue} )} @@ -154,11 +147,9 @@ function TransactionDetails({ return ( - {value && !BigNumber.from(value).eq(0) ? ( - - ) : null} + {value && !BigNumber.from(value).eq(0) ? : null} {to ? ( - + ) : null} @@ -168,7 +159,8 @@ function TransactionDetails({ borderRadius="$rounded12" borderWidth={1} px="$spacing8" - py="$spacing2"> + py="$spacing2" + > {parsedData ? parsedData.name : t('common.text.unknown')} diff --git a/apps/mobile/src/components/WalletConnect/RequestModal/UwULinkErc20SendModal.tsx b/apps/mobile/src/components/Requests/RequestModal/UwULinkErc20SendModal.tsx similarity index 90% rename from apps/mobile/src/components/WalletConnect/RequestModal/UwULinkErc20SendModal.tsx rename to apps/mobile/src/components/Requests/RequestModal/UwULinkErc20SendModal.tsx index 2bc3d0a5240..1eefd9fdda1 100644 --- a/apps/mobile/src/components/WalletConnect/RequestModal/UwULinkErc20SendModal.tsx +++ b/apps/mobile/src/components/Requests/RequestModal/UwULinkErc20SendModal.tsx @@ -2,7 +2,7 @@ import { useBottomSheetInternal } from '@gorhom/bottom-sheet' import { formatUnits } from 'ethers/lib/utils' import { useTranslation } from 'react-i18next' import Animated, { useAnimatedStyle } from 'react-native-reanimated' -import { ModalWithOverlay } from 'src/components/WalletConnect/ModalWithOverlay/ModalWithOverlay' +import { ModalWithOverlay } from 'src/components/Requests/ModalWithOverlay/ModalWithOverlay' import { UwuLinkErc20Request } from 'src/features/walletConnect/walletConnectSlice' import { Flex, SpinningLoader, Text, useIsDarkMode } from 'ui/src' import { iconSizes, spacing } from 'ui/src/theme' @@ -10,16 +10,16 @@ import { TokenLogo } from 'uniswap/src/components/CurrencyLogo/TokenLogo' import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' import { ModalName } from 'uniswap/src/features/telemetry/constants' +import { NativeCurrency } from 'uniswap/src/features/tokens/NativeCurrency' +import { buildCurrencyId } from 'uniswap/src/utils/currencyId' import { NumberType } from 'utilities/src/format/types' import { NetworkFee } from 'wallet/src/components/network/NetworkFee' import { GasFeeResult } from 'wallet/src/features/gas/types' import { RemoteImage } from 'wallet/src/features/images/RemoteImage' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' import { useOnChainCurrencyBalance } from 'wallet/src/features/portfolio/api' -import { NativeCurrency } from 'wallet/src/features/tokens/NativeCurrency' import { useCurrencyInfo } from 'wallet/src/features/tokens/useCurrencyInfo' import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' -import { buildCurrencyId } from 'wallet/src/utils/currencyId' type Props = { onClose: () => void @@ -61,7 +61,8 @@ export function UwULinkErc20SendModal({ scrollDownButtonText={t('walletConnect.request.button.scrollDown')} onClose={onClose} onConfirm={onConfirm} - onReject={onReject}> + onReject={onReject} + > - + {formatUnits(request.amount, decimals)} {symbol} diff --git a/apps/mobile/src/components/WalletConnect/RequestModal/WalletConnectRequestModal.tsx b/apps/mobile/src/components/Requests/RequestModal/WalletConnectRequestModal.tsx similarity index 78% rename from apps/mobile/src/components/WalletConnect/RequestModal/WalletConnectRequestModal.tsx rename to apps/mobile/src/components/Requests/RequestModal/WalletConnectRequestModal.tsx index 98ca3bcdacf..db9d610cf01 100644 --- a/apps/mobile/src/components/WalletConnect/RequestModal/WalletConnectRequestModal.tsx +++ b/apps/mobile/src/components/Requests/RequestModal/WalletConnectRequestModal.tsx @@ -3,37 +3,31 @@ import { getSdkError } from '@walletconnect/utils' import { providers } from 'ethers' import React, { useMemo, useRef } from 'react' import { useTranslation } from 'react-i18next' -import { useAppDispatch, useAppSelector } from 'src/app/hooks' -import { ModalWithOverlay } from 'src/components/WalletConnect/ModalWithOverlay/ModalWithOverlay' -import { KidSuperCheckinModal } from 'src/components/WalletConnect/RequestModal/KidSuperCheckinModal' -import { UwULinkErc20SendModal } from 'src/components/WalletConnect/RequestModal/UwULinkErc20SendModal' +import { useDispatch } from 'react-redux' +import { useAppSelector } from 'src/app/hooks' +import { ModalWithOverlay } from 'src/components/Requests/ModalWithOverlay/ModalWithOverlay' +import { KidSuperCheckinModal } from 'src/components/Requests/RequestModal/KidSuperCheckinModal' +import { UwULinkErc20SendModal } from 'src/components/Requests/RequestModal/UwULinkErc20SendModal' import { WalletConnectRequestModalContent, methodCostsGas, -} from 'src/components/WalletConnect/RequestModal/WalletConnectRequestModalContent' -import { useHasSufficientFunds } from 'src/components/WalletConnect/RequestModal/hooks' +} from 'src/components/Requests/RequestModal/WalletConnectRequestModalContent' +import { useHasSufficientFunds } from 'src/components/Requests/RequestModal/hooks' import { useBiometricAppSettings, useBiometricPrompt } from 'src/features/biometrics/hooks' import { returnToPreviousApp } from 'src/features/walletConnect/WalletConnect' import { wcWeb3Wallet } from 'src/features/walletConnect/saga' import { selectDidOpenFromDeepLink } from 'src/features/walletConnect/selectors' import { signWcRequestActions } from 'src/features/walletConnect/signWcRequestSaga' -import { - WalletConnectRequest, - isTransactionRequest, -} from 'src/features/walletConnect/walletConnectSlice' +import { WalletConnectRequest, isTransactionRequest } from 'src/features/walletConnect/walletConnectSlice' import { MobileEventName, ModalName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' -import { - EthMethod, - UwULinkMethod, - WCEventType, - WCRequestOutcome, -} from 'uniswap/src/types/walletConnect' +import { EthMethod, UwULinkMethod, WCEventType, WCRequestOutcome } from 'uniswap/src/types/walletConnect' +import { areAddressesEqual } from 'uniswap/src/utils/addresses' +import { formatExternalTxnWithGasEstimates } from 'wallet/src/features/gas/formatExternalTxnWithGasEstimates' import { useTransactionGasFee } from 'wallet/src/features/gas/hooks' import { GasSpeed } from 'wallet/src/features/gas/types' import { useIsBlocked, useIsBlockedActiveAddress } from 'wallet/src/features/trm/hooks' import { useSignerAccounts } from 'wallet/src/features/wallet/hooks' -import { areAddressesEqual } from 'wallet/src/utils/addresses' interface Props { onClose: () => void @@ -64,9 +58,7 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem }, [chainId, request]) const signerAccounts = useSignerAccounts() - const signerAccount = signerAccounts.find((account) => - areAddressesEqual(account.address, request.account) - ) + const signerAccount = signerAccounts.find((account) => areAddressesEqual(account.address, request.account)) const gasFee = useTransactionGasFee(tx, GasSpeed.Urgent) const hasSufficientFunds = useHasSufficientFunds({ @@ -76,10 +68,8 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem value: isTransactionRequest(request) ? request.transaction.value : undefined, }) - const { isBlocked: isSenderBlocked, isBlockedLoading: isSenderBlockedLoading } = - useIsBlockedActiveAddress() - const { isBlocked: isRecipientBlocked, isBlockedLoading: isRecipientBlockedLoading } = - useIsBlocked(tx?.to) + const { isBlocked: isSenderBlocked, isBlockedLoading: isSenderBlockedLoading } = useIsBlockedActiveAddress() + const { isBlocked: isRecipientBlocked, isBlockedLoading: isRecipientBlockedLoading } = useIsBlocked(tx?.to) const isBlocked = isSenderBlocked ?? isRecipientBlocked const isBlockedLoading = isSenderBlockedLoading || isRecipientBlockedLoading @@ -109,7 +99,7 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem } const confirmEnabled = checkConfirmEnabled() - const dispatch = useAppDispatch() + const dispatch = useDispatch() /** * TODO: [MOB-239] implement this behavior in a less janky way. Ideally if we can distinguish between `onClose` being called programmatically and `onClose` as a results of a user dismissing the modal then we can determine what this value should be without this class variable. * Indicates that the modal can reject the request when the modal happens. This will be false when the modal closes as a result of the user explicitly confirming or rejecting a request and true otherwise. @@ -131,9 +121,7 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem rejectOnCloseRef.current = false sendAnalyticsEvent(MobileEventName.WalletConnectSheetCompleted, { - request_type: isTransactionRequest(request) - ? WCEventType.TransactionRequest - : WCEventType.SignRequest, + request_type: isTransactionRequest(request) ? WCEventType.TransactionRequest : WCEventType.SignRequest, eth_method: request.type, dapp_url: request.dapp.url, dapp_name: request.dapp.name, @@ -152,21 +140,27 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem if (!confirmEnabled || !signerAccount) { return } + if (request.type === EthMethod.EthSendTransaction || request.type === UwULinkMethod.Erc20Send) { - if (!gasFee.params) { + if (!tx) { return - } // appeasing typescript + } + const txnWithFormattedGasEstimates = formatExternalTxnWithGasEstimates({ + transaction: tx, + gasFeeResult: gasFee, + }) + dispatch( signWcRequestActions.trigger({ sessionId: request.sessionId, requestInternalId: request.internalId, method: EthMethod.EthSendTransaction, - transaction: { ...tx, ...gasFee.params }, + transaction: txnWithFormattedGasEstimates, account: signerAccount, dapp: request.dapp, chainId, request, - }) + }), ) } else { dispatch( @@ -179,16 +173,14 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem account: signerAccount, dapp: request.dapp, chainId, - }) + }), ) } rejectOnCloseRef.current = false sendAnalyticsEvent(MobileEventName.WalletConnectSheetCompleted, { - request_type: isTransactionRequest(request) - ? WCEventType.TransactionRequest - : WCEventType.SignRequest, + request_type: isTransactionRequest(request) ? WCEventType.TransactionRequest : WCEventType.SignRequest, eth_method: request.type, dapp_url: request.dapp.url, dapp_name: request.dapp.name, @@ -243,28 +235,22 @@ export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Elem // KidSuper Uniswap Cafe check-in screen if (request.type === EthMethod.PersonalSign && request.dapp.name === 'Uniswap Cafe') { return ( - + ) } return ( + onReject={onReject} + > - request.type !== EthMethod.PersonalSign +const isPotentiallyUnsafe = (request: WalletConnectRequest): boolean => request.type !== EthMethod.PersonalSign export const methodCostsGas = (request: WalletConnectRequest): request is TransactionRequest => request.type === EthMethod.EthSendTransaction @@ -93,11 +92,7 @@ export function WalletConnectRequestModalContent({ <> - + {!permitInfo && ( @@ -106,11 +101,7 @@ export function WalletConnectRequestModalContent({ - + @@ -128,11 +119,7 @@ export function WalletConnectRequestModalContent({ + } textColor="$DEP_accentWarning" title={t('walletConnect.request.error.network')} @@ -184,11 +171,7 @@ function WarningSection({ if (!isTransactionRequest(request)) { return ( - + {t('walletConnect.request.warning.general.message')} diff --git a/apps/mobile/src/components/WalletConnect/RequestModal/hooks.ts b/apps/mobile/src/components/Requests/RequestModal/hooks.ts similarity index 89% rename from apps/mobile/src/components/WalletConnect/RequestModal/hooks.ts rename to apps/mobile/src/components/Requests/RequestModal/hooks.ts index d5b3806c7a5..2f8066028ac 100644 --- a/apps/mobile/src/components/WalletConnect/RequestModal/hooks.ts +++ b/apps/mobile/src/components/Requests/RequestModal/hooks.ts @@ -1,8 +1,8 @@ import { useMemo } from 'react' +import { NativeCurrency } from 'uniswap/src/features/tokens/NativeCurrency' import { UniverseChainId, WalletChainId } from 'uniswap/src/types/chains' import { GasFeeResult } from 'wallet/src/features/gas/types' import { useOnChainNativeCurrencyBalance } from 'wallet/src/features/portfolio/api' -import { NativeCurrency } from 'wallet/src/features/tokens/NativeCurrency' import { hasSufficientFundsIncludingGas } from 'wallet/src/features/transactions/utils' import { ValueType, getCurrencyAmount } from 'wallet/src/utils/getCurrencyAmount' @@ -18,10 +18,7 @@ export function useHasSufficientFunds({ value?: string }): boolean { const nativeCurrency = NativeCurrency.onChain(chainId || UniverseChainId.Mainnet) - const { balance: nativeBalance } = useOnChainNativeCurrencyBalance( - chainId ?? UniverseChainId.Mainnet, - account - ) + const { balance: nativeBalance } = useOnChainNativeCurrencyBalance(chainId ?? UniverseChainId.Mainnet, account) const hasSufficientFunds = useMemo(() => { const transactionAmount = diff --git a/apps/mobile/src/components/WalletConnect/ScanSheet/PendingConnectionModal.tsx b/apps/mobile/src/components/Requests/ScanSheet/PendingConnectionModal.tsx similarity index 86% rename from apps/mobile/src/components/WalletConnect/ScanSheet/PendingConnectionModal.tsx rename to apps/mobile/src/components/Requests/ScanSheet/PendingConnectionModal.tsx index 1415bb62612..551fa807775 100644 --- a/apps/mobile/src/components/WalletConnect/ScanSheet/PendingConnectionModal.tsx +++ b/apps/mobile/src/components/Requests/ScanSheet/PendingConnectionModal.tsx @@ -3,11 +3,12 @@ import { getSdkError } from '@walletconnect/utils' import React, { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import Animated, { useAnimatedStyle } from 'react-native-reanimated' -import { useAppDispatch, useAppSelector } from 'src/app/hooks' -import { DappHeaderIcon } from 'src/components/WalletConnect/DappHeaderIcon' -import { ModalWithOverlay } from 'src/components/WalletConnect/ModalWithOverlay/ModalWithOverlay' -import { PendingConnectionSwitchAccountModal } from 'src/components/WalletConnect/ScanSheet/PendingConnectionSwitchAccountModal' -import { truncateQueryParams } from 'src/components/WalletConnect/ScanSheet/util' +import { useDispatch } from 'react-redux' +import { useAppSelector } from 'src/app/hooks' +import { DappHeaderIcon } from 'src/components/Requests/DappHeaderIcon' +import { ModalWithOverlay } from 'src/components/Requests/ModalWithOverlay/ModalWithOverlay' +import { PendingConnectionSwitchAccountModal } from 'src/components/Requests/ScanSheet/PendingConnectionSwitchAccountModal' +import { truncateQueryParams } from 'src/components/Requests/ScanSheet/util' import { LinkButton } from 'src/components/buttons/LinkButton' import { returnToPreviousApp } from 'src/features/walletConnect/WalletConnect' import { wcWeb3Wallet } from 'src/features/walletConnect/saga' @@ -21,13 +22,14 @@ import { import { Flex, Text, TouchableArea, useSporeColors } from 'ui/src' import { Check, RotatableChevron, X } from 'ui/src/components/icons' import { iconSizes } from 'ui/src/theme' -import { ElementName, MobileEventName, ModalName } from 'uniswap/src/features/telemetry/constants' +import { NetworkLogos } from 'uniswap/src/components/network/NetworkLogos' +import { MobileEventName, ModalName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { WalletChainId } from 'uniswap/src/types/chains' import { WCEventType, WCRequestOutcome, WalletConnectEvent } from 'uniswap/src/types/walletConnect' import { formatDappURL } from 'utilities/src/format/urls' import { ONE_SECOND_MS } from 'utilities/src/time/time' -import { NetworkLogos } from 'wallet/src/components/network/NetworkLogos' import { pushNotification } from 'wallet/src/features/notifications/slice' import { AppNotificationType } from 'wallet/src/features/notifications/types' import { AddressFooter } from 'wallet/src/features/transactions/TransactionRequest/AddressFooter' @@ -61,13 +63,10 @@ const SitePermissions = (): JSX.Element => { borderRadius="$rounded16" borderWidth={1} minHeight={44} - p="$spacing12"> + p="$spacing12" + > - + {t('walletConnect.permissions.title')} @@ -79,7 +78,8 @@ const SitePermissions = (): JSX.Element => { allowFontScaling={false} color="$neutral1" flexGrow={1} - variant={infoTextSize}> + variant={infoTextSize} + > {t('walletConnect.permissions.option.viewWalletAddress')} @@ -90,7 +90,8 @@ const SitePermissions = (): JSX.Element => { allowFontScaling={false} color="$neutral1" flexGrow={1} - variant={infoTextSize}> + variant={infoTextSize} + > {t('walletConnect.permissions.option.viewTokenBalances')} @@ -101,7 +102,8 @@ const SitePermissions = (): JSX.Element => { allowFontScaling={false} color="$neutral1" flexGrow={1} - variant={infoTextSize}> + variant={infoTextSize} + > {t('walletConnect.permissions.option.transferAssets')} @@ -114,18 +116,8 @@ const NetworksRow = ({ chains }: { chains: WalletChainId[] }): JSX.Element => { const { t } = useTranslation() return ( - - + + {t('walletConnect.permissions.networks')} @@ -147,16 +139,10 @@ const SwitchAccountRow = ({ activeAddress, setModalState }: SwitchAccountProps): }, [setModalState]) return ( - + - {accountIsSwitchable && ( - - )} + {accountIsSwitchable && } ) @@ -165,14 +151,12 @@ const SwitchAccountRow = ({ activeAddress, setModalState }: SwitchAccountProps): export const PendingConnectionModal = ({ pendingSession, onClose }: Props): JSX.Element => { const { t } = useTranslation() - const dispatch = useAppDispatch() + const dispatch = useDispatch() const activeAddress = useActiveAccountAddressWithThrow() const activeAccount = useActiveAccountWithThrow() const didOpenFromDeepLink = useAppSelector(selectDidOpenFromDeepLink) - const [modalState, setModalState] = useState( - PendingConnectionModalState.Hidden - ) + const [modalState, setModalState] = useState(PendingConnectionModalState.Hidden) const onPressSettleConnection = useCallback( async (approved: boolean) => { @@ -208,7 +192,7 @@ export const PendingConnectionModal = ({ pendingSession, onClose }: Props): JSX. namespaces, }, account: activeAddress, - }) + }), ) dispatch( @@ -219,7 +203,7 @@ export const PendingConnectionModal = ({ pendingSession, onClose }: Props): JSX. dappName: session.peer.metadata.name, imageUrl: session.peer.metadata.icons[0] ?? null, hideDelay: 3 * ONE_SECOND_MS, - }) + }), ) } else { await wcWeb3Wallet.rejectSession({ @@ -234,7 +218,7 @@ export const PendingConnectionModal = ({ pendingSession, onClose }: Props): JSX. returnToPreviousApp() } }, - [activeAddress, dispatch, onClose, pendingSession, didOpenFromDeepLink] + [activeAddress, dispatch, onClose, pendingSession, didOpenFromDeepLink], ) const dappName = pendingSession.dapp.name || pendingSession.dapp.url || '' @@ -247,7 +231,8 @@ export const PendingConnectionModal = ({ pendingSession, onClose }: Props): JSX. scrollDownButtonText={t('walletConnect.pending.button.scrollDown')} onClose={onClose} onConfirm={(): Promise => onPressSettleConnection(true)} - onReject={(): Promise => onPressSettleConnection(false)}> + onReject={(): Promise => onPressSettleConnection(false)} + > void } -export const PendingConnectionSwitchAccountModal = ({ - activeAccount, - onPressAccount, - onClose, -}: Props): JSX.Element => { +export const PendingConnectionSwitchAccountModal = ({ activeAccount, onPressAccount, onClose }: Props): JSX.Element => { const { t } = useTranslation() const signerAccounts = useSignerAccounts() @@ -30,7 +26,7 @@ export const PendingConnectionSwitchAccountModal = ({ render: () => , } }), - [signerAccounts, activeAccount, onPressAccount] + [signerAccounts, activeAccount, onPressAccount], ) return ( diff --git a/apps/mobile/src/components/WalletConnect/ScanSheet/PendingConnectionSwitchNetworkModal.tsx b/apps/mobile/src/components/Requests/ScanSheet/PendingConnectionSwitchNetworkModal.tsx similarity index 77% rename from apps/mobile/src/components/WalletConnect/ScanSheet/PendingConnectionSwitchNetworkModal.tsx rename to apps/mobile/src/components/Requests/ScanSheet/PendingConnectionSwitchNetworkModal.tsx index 1fb75c2db74..34fed7c78f2 100644 --- a/apps/mobile/src/components/WalletConnect/ScanSheet/PendingConnectionSwitchNetworkModal.tsx +++ b/apps/mobile/src/components/Requests/ScanSheet/PendingConnectionSwitchNetworkModal.tsx @@ -15,11 +15,7 @@ type Props = { onClose: () => void } -export const PendingConnectionSwitchNetworkModal = ({ - selectedChainId, - onPressChain, - onClose, -}: Props): JSX.Element => { +export const PendingConnectionSwitchNetworkModal = ({ selectedChainId, onPressChain, onClose }: Props): JSX.Element => { const colors = useSporeColors() const { t } = useTranslation() @@ -33,23 +29,14 @@ export const PendingConnectionSwitchNetworkModal = ({ render: () => ( <> - + {info.label} {chainId === selectedChainId && ( - + )} @@ -57,7 +44,7 @@ export const PendingConnectionSwitchNetworkModal = ({ ), } }), - [selectedChainId, onPressChain, colors.accent1] + [selectedChainId, onPressChain, colors.accent1], ) return ( diff --git a/apps/mobile/src/components/WalletConnect/ScanSheet/SwitchAccountOption.tsx b/apps/mobile/src/components/Requests/ScanSheet/SwitchAccountOption.tsx similarity index 95% rename from apps/mobile/src/components/WalletConnect/ScanSheet/SwitchAccountOption.tsx rename to apps/mobile/src/components/Requests/ScanSheet/SwitchAccountOption.tsx index 5ed9f079064..ab322ee9d4a 100644 --- a/apps/mobile/src/components/WalletConnect/ScanSheet/SwitchAccountOption.tsx +++ b/apps/mobile/src/components/Requests/ScanSheet/SwitchAccountOption.tsx @@ -1,10 +1,10 @@ import React from 'react' import { Flex, Separator, Text, Unicon, useSporeColors } from 'ui/src' import Check from 'ui/src/assets/icons/check.svg' +import { shortenAddress } from 'uniswap/src/utils/addresses' import { DisplayNameText } from 'wallet/src/components/accounts/DisplayNameText' import { Account } from 'wallet/src/features/wallet/accounts/types' import { useDisplayName } from 'wallet/src/features/wallet/hooks' -import { shortenAddress } from 'wallet/src/utils/addresses' type Props = { account: Account diff --git a/apps/mobile/src/components/WalletConnect/ScanSheet/WalletConnectModal.tsx b/apps/mobile/src/components/Requests/ScanSheet/WalletConnectModal.tsx similarity index 84% rename from apps/mobile/src/components/WalletConnect/ScanSheet/WalletConnectModal.tsx rename to apps/mobile/src/components/Requests/ScanSheet/WalletConnectModal.tsx index 44c7554933c..39a40355dd2 100644 --- a/apps/mobile/src/components/WalletConnect/ScanSheet/WalletConnectModal.tsx +++ b/apps/mobile/src/components/Requests/ScanSheet/WalletConnectModal.tsx @@ -2,10 +2,10 @@ import React, { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { Alert } from 'react-native' import 'react-native-reanimated' -import { useAppDispatch } from 'src/app/hooks' +import { useDispatch } from 'react-redux' import { useEagerExternalProfileRootNavigation } from 'src/app/navigation/hooks' import { QRCodeScanner } from 'src/components/QRCodeScanner/QRCodeScanner' -import { ConnectedDappsList } from 'src/components/WalletConnect/ConnectedDapps/ConnectedDappsList' +import { ConnectedDappsList } from 'src/components/Requests/ConnectedDapps/ConnectedDappsList' import { URIType, UWULINK_PREFIX, @@ -14,7 +14,7 @@ import { isAllowedUwuLinkRequest, toTokenTransferRequest, useUwuLinkContractAllowlist, -} from 'src/components/WalletConnect/ScanSheet/util' +} from 'src/components/Requests/ScanSheet/util' import { BackButtonView } from 'src/components/layout/BackButtonView' import { openDeepLink } from 'src/features/deepLinking/handleDeepLinkSaga' import { useWalletConnect } from 'src/features/walletConnect/useWalletConnect' @@ -29,6 +29,7 @@ import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import Trace from 'uniswap/src/features/telemetry/Trace' import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { EthMethod, UwULinkMethod, UwULinkRequest } from 'uniswap/src/types/walletConnect' import { logger } from 'utilities/src/logger/logger' import { WalletQRCode } from 'wallet/src/components/QRCodeScanner/WalletQRCode' @@ -50,11 +51,10 @@ export function WalletConnectModal({ const isDarkMode = useIsDarkMode() const activeAccount = useActiveAccount() const { sessions, hasPendingSessionError } = useWalletConnect(activeAccount?.address) - const [currentScreenState, setCurrentScreenState] = - useState(initialScreenState) + const [currentScreenState, setCurrentScreenState] = useState(initialScreenState) const [shouldFreezeCamera, setShouldFreezeCamera] = useState(false) const { preload, navigate } = useEagerExternalProfileRootNavigation() - const dispatch = useAppDispatch() + const dispatch = useDispatch() const isUwULinkEnabled = useFeatureFlag(FeatureFlags.UwULink) const isScantasticEnabled = useFeatureFlag(FeatureFlags.Scantastic) @@ -85,18 +85,14 @@ export function WalletConnectModal({ }) if (!supportedURI) { setShouldFreezeCamera(true) - Alert.alert( - t('walletConnect.error.unsupported.title'), - t('walletConnect.error.unsupported.message'), - [ - { - text: t('common.button.tryAgain'), - onPress: (): void => { - setShouldFreezeCamera(false) - }, + Alert.alert(t('walletConnect.error.unsupported.title'), t('walletConnect.error.unsupported.message'), [ + { + text: t('common.button.tryAgain'), + onPress: (): void => { + setShouldFreezeCamera(false) }, - ] - ) + }, + ]) return } @@ -109,18 +105,14 @@ export function WalletConnectModal({ if (supportedURI.type === URIType.WalletConnectURL) { setShouldFreezeCamera(true) - Alert.alert( - t('walletConnect.error.unsupportedV1.title'), - t('walletConnect.error.unsupportedV1.message'), - [ - { - text: t('common.button.ok'), - onPress: (): void => { - setShouldFreezeCamera(false) - }, + Alert.alert(t('walletConnect.error.unsupportedV1.title'), t('walletConnect.error.unsupportedV1.message'), [ + { + text: t('common.button.ok'), + onPress: (): void => { + setShouldFreezeCamera(false) }, - ] - ) + }, + ]) return } @@ -132,18 +124,14 @@ export function WalletConnectModal({ logger.error(error, { tags: { file: 'WalletConnectModal', function: 'onScanCode' }, }) - Alert.alert( - t('walletConnect.error.general.title'), - t('walletConnect.error.general.message'), - [ - { - text: t('common.button.ok'), - onPress: (): void => { - setShouldFreezeCamera(false) - }, + Alert.alert(t('walletConnect.error.general.title'), t('walletConnect.error.general.message'), [ + { + text: t('common.button.ok'), + onPress: (): void => { + setShouldFreezeCamera(false) }, - ] - ) + }, + ]) } } @@ -160,18 +148,14 @@ export function WalletConnectModal({ const parsedUwulinkRequest: UwULinkRequest = JSON.parse(supportedURI.value) const isAllowed = isAllowedUwuLinkRequest(parsedUwulinkRequest, uwuLinkContractAllowlist) if (!isAllowed) { - Alert.alert( - t('walletConnect.error.uwu.title'), - t('walletConnect.error.uwu.unsupported'), - [ - { - text: t('common.button.ok'), - onPress: (): void => { - setShouldFreezeCamera(false) - }, + Alert.alert(t('walletConnect.error.uwu.title'), t('walletConnect.error.uwu.unsupported'), [ + { + text: t('common.button.ok'), + onPress: (): void => { + setShouldFreezeCamera(false) }, - ] - ) + }, + ]) return } @@ -202,19 +186,16 @@ export function WalletConnectModal({ // `message` if it exists. so this is mostly to appease Typescript rawMessage: parsedUwulinkRequest.message, }, - }) + }), ) } else if (parsedUwulinkRequest.method === UwULinkMethod.Erc20Send) { const preparedTransaction = await toTokenTransferRequest( parsedUwulinkRequest, activeAccount, providerManager, - contractManager - ) - const tokenRecipient = findAllowedTokenRecipient( - parsedUwulinkRequest, - uwuLinkContractAllowlist + contractManager, ) + const tokenRecipient = findAllowedTokenRecipient(parsedUwulinkRequest, uwuLinkContractAllowlist) dispatch( addRequest({ @@ -235,7 +216,7 @@ export function WalletConnectModal({ ...preparedTransaction, }, }, - }) + }), ) } else { dispatch( @@ -249,7 +230,7 @@ export function WalletConnectModal({ ...parsedUwulinkRequest.value, }, }, - }) + }), ) } onClose() @@ -292,7 +273,7 @@ export function WalletConnectModal({ uwuLinkContractAllowlist, providerManager, contractManager, - ] + ], ) const onPressBottomToggle = (): void => { @@ -320,7 +301,8 @@ export function WalletConnectModal({ fullScreen backgroundColor={colors.surface1.get()} name={ModalName.WalletConnectScan} - onClose={onClose}> + onClose={onClose} + > <> {currentScreenState === ScannerModalState.ConnectedDapps && ( + testID={TestID.QRCodeModalToggle} + onPress={onPressBottomToggle} + > {currentScreenState === ScannerModalState.ScanQr ? ( - + ) : ( - + )} {currentScreenState === ScannerModalState.ScanQr diff --git a/apps/mobile/src/components/WalletConnect/ScanSheet/util.test.ts b/apps/mobile/src/components/Requests/ScanSheet/util.test.ts similarity index 97% rename from apps/mobile/src/components/WalletConnect/ScanSheet/util.test.ts rename to apps/mobile/src/components/Requests/ScanSheet/util.test.ts index d6f94a93e30..fe61ac39333 100644 --- a/apps/mobile/src/components/WalletConnect/ScanSheet/util.test.ts +++ b/apps/mobile/src/components/Requests/ScanSheet/util.test.ts @@ -1,9 +1,5 @@ import * as wcUtils from '@walletconnect/utils' -import { - CUSTOM_UNI_QR_CODE_PREFIX, - URIType, - getSupportedURI, -} from 'src/components/WalletConnect/ScanSheet/util' +import { CUSTOM_UNI_QR_CODE_PREFIX, URIType, getSupportedURI } from 'src/components/Requests/ScanSheet/util' import { wcAsParamInUniwapScheme, wcInUniwapScheme, diff --git a/apps/mobile/src/components/WalletConnect/ScanSheet/util.ts b/apps/mobile/src/components/Requests/ScanSheet/util.ts similarity index 86% rename from apps/mobile/src/components/WalletConnect/ScanSheet/util.ts rename to apps/mobile/src/components/Requests/ScanSheet/util.ts index 1e1b1650438..4567cd8461a 100644 --- a/apps/mobile/src/components/WalletConnect/ScanSheet/util.ts +++ b/apps/mobile/src/components/Requests/ScanSheet/util.ts @@ -6,8 +6,9 @@ import { UNISWAP_URL_SCHEME_WALLETCONNECT_AS_PARAM, UNISWAP_WALLETCONNECT_URL, } from 'src/features/deepLinking/constants' -import { DynamicConfigs } from 'uniswap/src/features/gating/configs' -import { useDynamicConfig } from 'uniswap/src/features/gating/hooks' +import { AssetType } from 'uniswap/src/entities/assets' +import { DynamicConfigs, UwuLinkConfigKey } from 'uniswap/src/features/gating/configs' +import { useDynamicConfigValue } from 'uniswap/src/features/gating/hooks' import { RPCType } from 'uniswap/src/types/chains' import { EthMethod, @@ -16,15 +17,14 @@ import { UwULinkMethod, UwULinkRequest, } from 'uniswap/src/types/walletConnect' +import { areAddressesEqual, getValidAddress } from 'uniswap/src/utils/addresses' import { logger } from 'utilities/src/logger/logger' -import { AssetType } from 'wallet/src/entities/assets' import { ContractManager } from 'wallet/src/features/contracts/ContractManager' import { ProviderManager } from 'wallet/src/features/providers' import { ScantasticParams, ScantasticParamsSchema } from 'wallet/src/features/scantastic/types' import { getTokenTransferRequest } from 'wallet/src/features/transactions/transfer/hooks/useTransferTransactionRequest' import { TransferCurrencyParams } from 'wallet/src/features/transactions/transfer/types' import { Account } from 'wallet/src/features/wallet/accounts/types' -import { areAddressesEqual, getValidAddress } from 'wallet/src/utils/addresses' export enum URIType { WalletConnectURL = 'walletconnect', @@ -76,7 +76,7 @@ export const truncateQueryParams = (url: string): string => { export async function getSupportedURI( uri: string, - enabledFeatureFlags?: EnabledFeatureFlags + enabledFeatureFlags?: EnabledFeatureFlags, ): Promise { if (!uri) { return undefined @@ -128,10 +128,7 @@ export async function getSupportedURI( } } -async function getWcUriWithCustomPrefix( - uri: string, - prefix: string -): Promise<{ uri: string; type: URIType } | null> { +async function getWcUriWithCustomPrefix(uri: string, prefix: string): Promise<{ uri: string; type: URIType } | null> { if (uri.indexOf(prefix) !== 0) { return null } @@ -154,13 +151,31 @@ function isUwULink(uri: string): boolean { // Gets the UWULink contract allow list from statsig dynamic config. // We can safely cast as long as the statsig config format matches our `UwuLinkAllowlist` type. export function useUwuLinkContractAllowlist(): UwULinkAllowlist { - const uwuLinkConfig = useDynamicConfig(DynamicConfigs.UwuLink) - return uwuLinkConfig.getValue('allowlist') as UwULinkAllowlist + return useDynamicConfigValue( + DynamicConfigs.UwuLink, + UwuLinkConfigKey.Allowlist, + { + contracts: [], + tokenRecipients: [], + }, + (x: unknown) => { + const hasFields = + x !== null && typeof x === 'object' && Object.hasOwn(x, 'contracts') && Object.hasOwn(x, 'tokenRecipients') + + if (!hasFields) { + return false + } + + const castedObj = x as { contracts: unknown; tokenRecipients: unknown } + + return Array.isArray(castedObj.contracts) && Array.isArray(castedObj.tokenRecipients) + }, + ) } export function findAllowedTokenRecipient( request: UwULinkRequest, - allowlist: UwULinkAllowlist + allowlist: UwULinkAllowlist, ): UwULinkAllowlistItem | undefined { if (request.method !== UwULinkMethod.Erc20Send) { return @@ -168,7 +183,7 @@ export function findAllowedTokenRecipient( const { chainId, recipient } = request return allowlist.tokenRecipients.find( - (item) => item.chainId === chainId && areAddressesEqual(item.address, recipient) + (item) => item.chainId === chainId && areAddressesEqual(item.address, recipient), ) } /** @@ -183,10 +198,7 @@ export function findAllowedTokenRecipient( * @param request parsed UwULinkRequest * @returns boolean for whether the UwULinkRequest is allowed */ -export function isAllowedUwuLinkRequest( - request: UwULinkRequest, - allowlist: UwULinkAllowlist -): boolean { +export function isAllowedUwuLinkRequest(request: UwULinkRequest, allowlist: UwULinkAllowlist): boolean { // token sends if (request.method === UwULinkMethod.Erc20Send) { return Boolean(findAllowedTokenRecipient(request, allowlist)) @@ -198,10 +210,8 @@ export function isAllowedUwuLinkRequest( // generic transactions const { to, value } = request.value - const belowMaximumValue = - !value || parseFloat(value) <= parseEther(UWULINK_MAX_TXN_VALUE).toNumber() - const isAllowedContractAddress = - to && allowlist.contracts.some((item) => areAddressesEqual(item.address, to)) + const belowMaximumValue = !value || parseFloat(value) <= parseEther(UWULINK_MAX_TXN_VALUE).toNumber() + const isAllowedContractAddress = to && allowlist.contracts.some((item) => areAddressesEqual(item.address, to)) if (!belowMaximumValue || !isAllowedContractAddress) { return false @@ -289,7 +299,7 @@ export async function toTokenTransferRequest( request: UwULinkErc20SendRequest, account: Account, providerManager: ProviderManager, - contractManager: ContractManager + contractManager: ContractManager, ): Promise { const provider = providerManager.getProvider(request.chainId, RPCType.Public) const params: TransferCurrencyParams = { diff --git a/apps/mobile/src/components/WalletConnect/WalletConnectModals.tsx b/apps/mobile/src/components/Requests/WalletConnectModals.tsx similarity index 74% rename from apps/mobile/src/components/WalletConnect/WalletConnectModals.tsx rename to apps/mobile/src/components/Requests/WalletConnectModals.tsx index 9f47f3da266..8a0bf942ecb 100644 --- a/apps/mobile/src/components/WalletConnect/WalletConnectModals.tsx +++ b/apps/mobile/src/components/Requests/WalletConnectModals.tsx @@ -1,9 +1,9 @@ import React, { useEffect } from 'react' import { useTranslation } from 'react-i18next' -import { useAppDispatch } from 'src/app/hooks' -import { WalletConnectRequestModal } from 'src/components/WalletConnect/RequestModal/WalletConnectRequestModal' -import { PendingConnectionModal } from 'src/components/WalletConnect/ScanSheet/PendingConnectionModal' -import { WalletConnectModal } from 'src/components/WalletConnect/ScanSheet/WalletConnectModal' +import { useDispatch } from 'react-redux' +import { WalletConnectRequestModal } from 'src/components/Requests/RequestModal/WalletConnectRequestModal' +import { PendingConnectionModal } from 'src/components/Requests/ScanSheet/PendingConnectionModal' +import { WalletConnectModal } from 'src/components/Requests/ScanSheet/WalletConnectModal' import { closeModal } from 'src/features/modals/modalSlice' import { useWalletConnect } from 'src/features/walletConnect/useWalletConnect' import { @@ -17,19 +17,15 @@ import { Flex, useSporeColors } from 'ui/src' import EyeIcon from 'ui/src/assets/icons/eye.svg' import { iconSizes } from 'ui/src/theme' import { ModalName } from 'uniswap/src/features/telemetry/constants' +import { areAddressesEqual } from 'uniswap/src/utils/addresses' import { AccountDetails } from 'wallet/src/components/accounts/AccountDetails' import { WarningModal } from 'wallet/src/components/modals/WarningModal/WarningModal' import { WarningSeverity } from 'wallet/src/features/transactions/WarningModal/types' -import { - useActiveAccount, - useActiveAccountAddressWithThrow, - useSignerAccounts, -} from 'wallet/src/features/wallet/hooks' -import { areAddressesEqual } from 'wallet/src/utils/addresses' +import { useActiveAccount, useActiveAccountAddressWithThrow, useSignerAccounts } from 'wallet/src/features/wallet/hooks' export function WalletConnectModals(): JSX.Element { const activeAccount = useActiveAccount() - const dispatch = useAppDispatch() + const dispatch = useDispatch() const { pendingRequests, modalState, pendingSession } = useWalletConnect(activeAccount?.address) @@ -69,10 +65,7 @@ export function WalletConnectModals(): JSX.Element { )} {pendingSession ? ( - + ) : null} {currRequest ? : null} @@ -87,18 +80,16 @@ function RequestModal({ currRequest }: RequestModalProps): JSX.Element { const signerAccounts = useSignerAccounts() const activeAccountAddress = useActiveAccountAddressWithThrow() const { t } = useTranslation() - const dispatch = useAppDispatch() + const dispatch = useDispatch() const colors = useSporeColors() // TODO: Move returnToPreviousApp() call to onClose but ensure it is not called twice const onClose = (): void => { - dispatch( - removeRequest({ requestInternalId: currRequest.internalId, account: activeAccountAddress }) - ) + dispatch(removeRequest({ requestInternalId: currRequest.internalId, account: activeAccountAddress })) } const isRequestFromSignerAccount = signerAccounts.some((account) => - areAddressesEqual(account.address, currRequest.account) + areAddressesEqual(account.address, currRequest.account), ) if (!isRequestFromSignerAccount) { @@ -107,23 +98,15 @@ function RequestModal({ currRequest }: RequestModalProps): JSX.Element { caption={t('walletConnect.request.warning.message')} closeText={t('common.button.dismiss')} icon={ - + } modalName={ModalName.WCViewOnlyWarning} severity={WarningSeverity.None} title={t('walletConnect.request.warning.title')} onCancel={onClose} - onClose={onClose}> - + onClose={onClose} + > + diff --git a/apps/mobile/src/components/RestoreWalletModal/RestoreWalletModal.tsx b/apps/mobile/src/components/RestoreWalletModal/RestoreWalletModal.tsx index 30c0f27788c..6445b7550db 100644 --- a/apps/mobile/src/components/RestoreWalletModal/RestoreWalletModal.tsx +++ b/apps/mobile/src/components/RestoreWalletModal/RestoreWalletModal.tsx @@ -1,20 +1,21 @@ import React from 'react' import { useTranslation } from 'react-i18next' -import { useAppDispatch } from 'src/app/hooks' +import { useDispatch } from 'react-redux' import { navigate } from 'src/app/navigation/rootNavigation' import { closeAllModals, closeModal } from 'src/features/modals/modalSlice' import { Button, Flex, Text, useSporeColors } from 'ui/src' import LockIcon from 'ui/src/assets/icons/lock.svg' import { iconSizes, opacify } from 'ui/src/theme' import { BottomSheetModal } from 'uniswap/src/components/modals/BottomSheetModal' -import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants' +import { ModalName } from 'uniswap/src/features/telemetry/constants' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding' import { MobileScreens, OnboardingScreens } from 'uniswap/src/types/screens/mobile' export function RestoreWalletModal(): JSX.Element | null { const { t } = useTranslation() const colors = useSporeColors() - const dispatch = useAppDispatch() + const dispatch = useDispatch() const onDismiss = (): void => { dispatch(closeModal({ name: ModalName.RestoreWallet })) @@ -32,10 +33,7 @@ export function RestoreWalletModal(): JSX.Element | null { } return ( - + - + }} + > + {t('account.wallet.button.restore')} @@ -60,7 +55,7 @@ export function RestoreWalletModal(): JSX.Element | null { - diff --git a/apps/mobile/src/components/Settings/BiometricAuthWarningModal.tsx b/apps/mobile/src/components/Settings/BiometricAuthWarningModal.tsx index 9c5867b08cc..c26a32da8ce 100644 --- a/apps/mobile/src/components/Settings/BiometricAuthWarningModal.tsx +++ b/apps/mobile/src/components/Settings/BiometricAuthWarningModal.tsx @@ -3,10 +3,7 @@ import { useTranslation } from 'react-i18next' import { useBiometricName } from 'src/features/biometrics/hooks' import { ModalName } from 'uniswap/src/features/telemetry/constants' import { isAndroid } from 'utilities/src/platform' -import { - WarningModal, - WarningModalProps, -} from 'wallet/src/components/modals/WarningModal/WarningModal' +import { WarningModal, WarningModalProps } from 'wallet/src/components/modals/WarningModal/WarningModal' import { WarningSeverity } from 'wallet/src/features/transactions/WarningModal/types' type Props = { @@ -15,11 +12,7 @@ type Props = { onClose: WarningModalProps['onClose'] } -export function BiometricAuthWarningModal({ - isTouchIdDevice, - onConfirm, - onClose, -}: Props): JSX.Element { +export function BiometricAuthWarningModal({ isTouchIdDevice, onConfirm, onClose }: Props): JSX.Element { const { t } = useTranslation() const biometricsMethod = useBiometricName(isTouchIdDevice) return ( diff --git a/apps/mobile/src/components/Settings/SettingsRow.tsx b/apps/mobile/src/components/Settings/SettingsRow.tsx index 593f62d77b6..01d7dbf911b 100644 --- a/apps/mobile/src/components/Settings/SettingsRow.tsx +++ b/apps/mobile/src/components/Settings/SettingsRow.tsx @@ -1,6 +1,7 @@ import { NavigatorScreenParams } from '@react-navigation/core' import React from 'react' import { ValueOf } from 'react-native-gesture-handler/lib/typescript/typeUtils' +import { useDispatch } from 'react-redux' import { OnboardingStackNavigationProp, OnboardingStackParamList, @@ -13,10 +14,9 @@ import { RotatableChevron } from 'ui/src/components/icons' import { iconSizes } from 'ui/src/theme' import { ModalName } from 'uniswap/src/features/telemetry/constants' import { MobileScreens } from 'uniswap/src/types/screens/mobile' +import { openUri } from 'uniswap/src/utils/linking' import { Switch } from 'wallet/src/components/buttons/Switch' import { Arrow } from 'wallet/src/components/icons/Arrow' -import { useAppDispatch } from 'wallet/src/state' -import { openUri } from 'wallet/src/utils/linking' export interface SettingsSection { subTitle: string @@ -67,7 +67,7 @@ export function SettingsRow({ navigation, }: SettingsRowProps): JSX.Element { const colors = useSporeColors() - const dispatch = useAppDispatch() + const dispatch = useDispatch() const handleRow = async (): Promise => { if (onToggle) { @@ -88,12 +88,7 @@ export function SettingsRow({ return ( - + {icon} @@ -114,22 +109,12 @@ export function SettingsRow({ {currentSetting ? ( - + {currentSetting} ) : null} - + ) : externalLink ? ( diff --git a/apps/mobile/src/components/TokenBalanceList/TokenBalanceItemContextMenu.tsx b/apps/mobile/src/components/TokenBalanceList/TokenBalanceItemContextMenu.tsx index 22a0691c7df..09b3cc3d30c 100644 --- a/apps/mobile/src/components/TokenBalanceList/TokenBalanceItemContextMenu.tsx +++ b/apps/mobile/src/components/TokenBalanceList/TokenBalanceItemContextMenu.tsx @@ -1,6 +1,7 @@ import React, { memo, useMemo } from 'react' import ContextMenu from 'react-native-context-menu-view' import { borderRadii } from 'ui/src/theme' +import { SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { PortfolioBalance } from 'uniswap/src/features/dataApi/types' import { useTokenContextMenu } from 'wallet/src/features/portfolio/useTokenContextMenu' @@ -13,17 +14,14 @@ export const TokenBalanceItemContextMenu = memo(function _TokenBalanceItem({ }) { const { menuActions, onContextMenuPress } = useTokenContextMenu({ currencyId: portfolioBalance.currencyInfo.currencyId, + isBlocked: portfolioBalance.currencyInfo.safetyLevel === SafetyLevel.Blocked, portfolioBalance, }) const style = useMemo(() => ({ borderRadius: borderRadii.rounded16 }), []) return ( - + {children} ) diff --git a/apps/mobile/src/components/TokenBalanceList/TokenBalanceList.tsx b/apps/mobile/src/components/TokenBalanceList/TokenBalanceList.tsx index 83c252c98e4..09eb1ea3e1f 100644 --- a/apps/mobile/src/components/TokenBalanceList/TokenBalanceList.tsx +++ b/apps/mobile/src/components/TokenBalanceList/TokenBalanceList.tsx @@ -8,11 +8,7 @@ import Animated, { FadeInDown, FadeOut } from 'react-native-reanimated' import { useAppStackNavigation } from 'src/app/navigation/types' import { TokenBalanceItemContextMenu } from 'src/components/TokenBalanceList/TokenBalanceItemContextMenu' import { useAdaptiveFooter } from 'src/components/home/hooks' -import { - TAB_BAR_HEIGHT, - TAB_VIEW_SCROLL_THROTTLE, - TabProps, -} from 'src/components/layout/TabHelpers' +import { TAB_BAR_HEIGHT, TAB_VIEW_SCROLL_THROTTLE, TabProps } from 'src/components/layout/TabHelpers' import { Flex, Loader, useDeviceInsets, useSporeColors } from 'ui/src' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { zIndices } from 'ui/src/theme' @@ -39,15 +35,9 @@ type TokenBalanceListProps = TabProps & { const ESTIMATED_TOKEN_ITEM_HEIGHT = 64 export const TokenBalanceList = forwardRef, TokenBalanceListProps>( - function _TokenBalanceList( - { owner, onPressToken, isExternalProfile = false, ...rest }, - ref - ): JSX.Element { + function _TokenBalanceList({ owner, onPressToken, isExternalProfile = false, ...rest }, ref): JSX.Element { return ( - + , TokenB /> ) - } + }, ) -export const TokenBalanceListInner = forwardRef< - FlatList, - TokenBalanceListProps ->(function _TokenBalanceListInner( - { - empty, - containerProps, - scrollHandler, - isExternalProfile = false, - renderedInModal = false, - refreshing, - headerHeight = 0, - onRefresh, - }, - ref -) { - const { t } = useTranslation() - const colors = useSporeColors() - const insets = useDeviceInsets() +export const TokenBalanceListInner = forwardRef, TokenBalanceListProps>( + function _TokenBalanceListInner( + { + empty, + containerProps, + scrollHandler, + isExternalProfile = false, + renderedInModal = false, + refreshing, + headerHeight = 0, + onRefresh, + testID, + }, + ref, + ) { + const { t } = useTranslation() + const colors = useSporeColors() + const insets = useDeviceInsets() - const { rows, balancesById, networkStatus, refetch } = useTokenBalanceListContext() - const hasError = isError(networkStatus, !!balancesById) + const { rows, balancesById, networkStatus, refetch } = useTokenBalanceListContext() + const hasError = isError(networkStatus, !!balancesById) - const { onContentSizeChange, adaptiveFooter } = useAdaptiveFooter( - containerProps?.contentContainerStyle - ) + const { onContentSizeChange, adaptiveFooter } = useAdaptiveFooter(containerProps?.contentContainerStyle) - // The following logic is meant to speed up the screen transition from the token details screen back to the home screen. - // When we call `navigation.goBack()`, a re-render is triggered *before* the animation begins. - // In order for that first re-render to be fast, we use `cachedData` so that it renders a memoized `FlatList` of tokens, - // (this `FlatList` is the most expensive component on this screen). - // After the transition ends, we set focus to `true` to trigger a re-render using the latest `data`. + // The following logic is meant to speed up the screen transition from the token details screen back to the home screen. + // When we call `navigation.goBack()`, a re-render is triggered *before* the animation begins. + // In order for that first re-render to be fast, we use `cachedData` so that it renders a memoized `FlatList` of tokens, + // (this `FlatList` is the most expensive component on this screen). + // After the transition ends, we set focus to `true` to trigger a re-render using the latest `data`. - const [isFocused, setIsFocused] = useState(true) - const [cachedRows, setCachedRows] = useState(null) + const [isFocused, setIsFocused] = useState(true) + const [cachedRows, setCachedRows] = useState(null) - const rowsRef = useRef(rows) - rowsRef.current = rows + const rowsRef = useRef(rows) + rowsRef.current = rows - useFocusEffect( - useCallback(() => { - return (): void => { - // We save the cached data to avoid a re-render when the user navigates back to it. - // This speeds up the animation while preserving the scroll position. - setCachedRows(rowsRef.current) - setIsFocused(false) - } - }, []) - ) + useFocusEffect( + useCallback(() => { + return (): void => { + // We save the cached data to avoid a re-render when the user navigates back to it. + // This speeds up the animation while preserving the scroll position. + setCachedRows(rowsRef.current) + setIsFocused(false) + } + }, []), + ) - const navigation = useAppStackNavigation() + const navigation = useAppStackNavigation() - useEffect(() => { - // We use this instead of relying on react-navigation's `useIsFocused` because we want to speed up the screen transition - // when the user goes from the token details screen back to the home screen, so we want this state to change *after* the animation is done instead of *before*. - const unsubscribeTransitionEnd = navigation.addListener('transitionEnd', (e) => { - if (!e.data.closing) { - setIsFocused(true) - } - }) + useEffect(() => { + // We use this instead of relying on react-navigation's `useIsFocused` because we want to speed up the screen transition + // when the user goes from the token details screen back to the home screen, so we want this state to change *after* the animation is done instead of *before*. + const unsubscribeTransitionEnd = navigation.addListener('transitionEnd', (e) => { + if (!e.data.closing) { + setIsFocused(true) + } + }) - return (): void => unsubscribeTransitionEnd() - }, [navigation]) + return (): void => unsubscribeTransitionEnd() + }, [navigation]) - const refreshControl = useMemo(() => { - return ( - + const refreshControl = useMemo(() => { + return ( + + ) + }, [insets.top, headerHeight, refreshing, colors.neutral3, onRefresh]) + + // In order to avoid unnecessary re-renders of the entire FlatList, the `renderItem` function should never change. + // That's why we use a context provider so that each row can read from there instead of passing down new props every time the data changes. + const renderItem = useCallback( + ({ item, index }: { item: TokenBalanceListRow; index: number }): JSX.Element => ( + + ), + [], ) - }, [insets.top, headerHeight, refreshing, colors.neutral3, onRefresh]) - // In order to avoid unnecessary re-renders of the entire FlatList, the `renderItem` function should never change. - // That's why we use a context provider so that each row can read from there instead of passing down new props every time the data changes. - const renderItem = useCallback( - ({ item }: { item: TokenBalanceListRow }): JSX.Element => , - [] - ) + const keyExtractor = useCallback((item: TokenBalanceListRow): string => item, []) - const keyExtractor = useCallback((item: TokenBalanceListRow): string => item, []) + const ListEmptyComponent = useMemo(() => { + if (hasError) { + return ( + + refetch?.()} + /> + + ) + } - const ListEmptyComponent = useMemo(() => { - if (hasError) { - return ( - - refetch?.()} - /> - - ) - } + if (isNonPollingRequestInFlight(networkStatus)) { + return ( + + + + ) + } - if (isNonPollingRequestInFlight(networkStatus)) { return ( - - + + {empty} ) - } - - return ( - - {empty} - - ) - }, [hasError, empty, t, networkStatus, refetch]) + }, [hasError, empty, t, networkStatus, refetch]) - const ListHeaderComponent = useMemo(() => { - return hasError ? ( - - - - ) : null - }, [hasError, refetch, t]) + const ListHeaderComponent = useMemo(() => { + return hasError ? ( + + + + ) : null + }, [hasError, refetch, t]) - // add negative z index to prevent footer from covering hidden tokens row when minimized - const ListFooterComponentStyle = useMemo(() => ({ zIndex: zIndices.negative }), []) + // add negative z index to prevent footer from covering hidden tokens row when minimized + const ListFooterComponentStyle = useMemo(() => ({ zIndex: zIndices.negative }), []) - const List = renderedInModal - ? BottomSheetFlatList - : Animated.FlatList + const List = renderedInModal ? BottomSheetFlatList : Animated.FlatList - const getItemLayout = useCallback( - ( - _: Maybe>, - index: number - ): { length: number; offset: number; index: number } => ({ - length: ESTIMATED_TOKEN_ITEM_HEIGHT, - offset: ESTIMATED_TOKEN_ITEM_HEIGHT * index, - index, - }), - [] - ) + const getItemLayout = useCallback( + (_: Maybe>, index: number): { length: number; offset: number; index: number } => ({ + length: ESTIMATED_TOKEN_ITEM_HEIGHT, + offset: ESTIMATED_TOKEN_ITEM_HEIGHT * index, + index, + }), + [], + ) - const data = balancesById ? (isFocused ? rows : cachedRows) : undefined + const data = balancesById ? (isFocused ? rows : cachedRows) : undefined - // Note: `PerformanceView` must wrap the entire return statement to properly track interactive states. - return ( - - - - ) -}) + // Note: `PerformanceView` must wrap the entire return statement to properly track interactive states. + return ( + + + + ) + }, +) const TokenBalanceItemRow = memo(function TokenBalanceItemRow({ item, + index, }: { item: TokenBalanceListRow + index?: number }) { const { balancesById, @@ -285,6 +272,7 @@ const TokenBalanceItemRow = memo(function TokenBalanceItemRow({ => { @@ -42,8 +45,12 @@ export function LinkButton({ pushNotification({ type: AppNotificationType.Copied, copyType: CopyNotificationType.Address, - }) + }), ) + sendAnalyticsEvent(SharedEventName.ELEMENT_CLICKED, { + element: ElementName.CopyAddress, + screen: MobileScreens.TokenDetails, + }) } const onPress = async (): Promise => { @@ -63,18 +70,15 @@ export function LinkButton({ px="$spacing12" py="$spacing8" testID={element} - onPress={onPress}> + onPress={onPress} + > {Icon && } {label} {buttonType === LinkButtonType.Copy && ( - + )} diff --git a/apps/mobile/src/components/TokenDetails/SendButton.tsx b/apps/mobile/src/components/TokenDetails/SendButton.tsx index f6698be17b7..f20b1dc42ed 100644 --- a/apps/mobile/src/components/TokenDetails/SendButton.tsx +++ b/apps/mobile/src/components/TokenDetails/SendButton.tsx @@ -2,7 +2,7 @@ import React from 'react' import { Flex, TouchableArea } from 'ui/src' import SendIcon from 'ui/src/assets/icons/send-action.svg' import { iconSizes } from 'ui/src/theme' -import { ElementName } from 'uniswap/src/features/telemetry/constants' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' type Props = { onPress: () => void @@ -12,12 +12,7 @@ type Props = { export function SendButton({ onPress, color, size = iconSizes.icon24 }: Props): JSX.Element { return ( - + diff --git a/apps/mobile/src/components/TokenDetails/TokenBalances.tsx b/apps/mobile/src/components/TokenDetails/TokenBalances.tsx index a3b567c3465..91bbe252934 100644 --- a/apps/mobile/src/components/TokenDetails/TokenBalances.tsx +++ b/apps/mobile/src/components/TokenDetails/TokenBalances.tsx @@ -5,13 +5,13 @@ import { useTokenDetailsNavigation } from 'src/components/TokenDetails/hooks' import { Flex, Separator, Text, TouchableArea, useSporeColors } from 'ui/src' import { iconSizes } from 'ui/src/theme' import { TokenLogo } from 'uniswap/src/components/CurrencyLogo/TokenLogo' +import { InlineNetworkPill } from 'uniswap/src/components/network/NetworkPill' import { PortfolioBalance } from 'uniswap/src/features/dataApi/types' import Trace from 'uniswap/src/features/telemetry/Trace' import { MobileEventName } from 'uniswap/src/features/telemetry/constants' import { CurrencyId } from 'uniswap/src/types/currency' import { getSymbolDisplayText } from 'uniswap/src/utils/currency' import { NumberType } from 'utilities/src/format/types' -import { InlineNetworkPill } from 'wallet/src/components/network/NetworkPill' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' import { AccountType } from 'wallet/src/features/wallet/accounts/types' import { useActiveAccount, useDisplayName } from 'wallet/src/features/wallet/hooks' @@ -45,7 +45,7 @@ export function TokenBalances({ preload(currencyId) navigateWithPop(currencyId) }, - [navigateWithPop, preload] + [navigateWithPop, preload], ) if (!hasCurrentChainBalances && !hasOtherChainBalances) { @@ -106,14 +106,10 @@ export function CurrentChainBalance({ - {isReadonly - ? t('token.balances.viewOnly', { ownerAddress: displayName }) - : t('token.balances.main')} + {isReadonly ? t('token.balances.viewOnly', { ownerAddress: displayName }) : t('token.balances.main')} - - {convertFiatAmountFormatted(balance.balanceUSD, NumberType.FiatTokenDetails)} - + {convertFiatAmountFormatted(balance.balanceUSD, NumberType.FiatTokenDetails)} {formatNumberOrString({ value: balance.quantity, type: NumberType.TokenNonTx })}{' '} {getSymbolDisplayText(balance.currencyInfo.currency.symbol)} diff --git a/apps/mobile/src/components/TokenDetails/TokenDetailsActionButtons.tsx b/apps/mobile/src/components/TokenDetails/TokenDetailsActionButtons.tsx index 9e483974c44..862fc21a98d 100644 --- a/apps/mobile/src/components/TokenDetails/TokenDetailsActionButtons.tsx +++ b/apps/mobile/src/components/TokenDetails/TokenDetailsActionButtons.tsx @@ -4,7 +4,7 @@ import { Button, Flex, useSporeColors } from 'ui/src' import { opacify, validColor } from 'ui/src/theme' import Trace from 'uniswap/src/features/telemetry/Trace' import { ElementName, ElementNameType, SectionName } from 'uniswap/src/features/telemetry/constants' -import { getContrastPassingTextColor } from 'wallet/src/utils/colors' +import { getContrastPassingTextColor } from 'uniswap/src/utils/colors' function CTAButton({ title, @@ -30,7 +30,8 @@ function CTAButton({ // eslint-disable-next-line react/jsx-sort-props onPress={onPress} size="large" - backgroundColor={validColor(tokenColor) ?? '$accent1'}> + backgroundColor={validColor(tokenColor) ?? '$accent1'} + > {title} @@ -59,7 +60,8 @@ export function TokenDetailsActionButtons({ gap="$spacing8" pb="$spacing16" pt="$spacing12" - px="$spacing16"> + px="$spacing16" + > + ) diff --git a/apps/mobile/src/components/TokenDetails/TokenDetailsHeader.tsx b/apps/mobile/src/components/TokenDetails/TokenDetailsHeader.tsx index 85279662419..db8c958dd67 100644 --- a/apps/mobile/src/components/TokenDetails/TokenDetailsHeader.tsx +++ b/apps/mobile/src/components/TokenDetails/TokenDetailsHeader.tsx @@ -1,12 +1,12 @@ import React from 'react' import { Flex, flexStyles, Text, TouchableArea } from 'ui/src' import { TokenLogo } from 'uniswap/src/components/CurrencyLogo/TokenLogo' +import WarningIcon from 'uniswap/src/components/icons/WarningIcon' import { SafetyLevel, TokenDetailsScreenQuery, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import WarningIcon from 'wallet/src/components/icons/WarningIcon' -import { fromGraphQLChain } from 'wallet/src/features/chains/utils' +import { fromGraphQLChain } from 'uniswap/src/features/chains/utils' export interface TokenDetailsHeaderProps { data?: TokenDetailsScreenQuery @@ -31,23 +31,14 @@ export function TokenDetailsHeader({ url={tokenProject?.logoUrl ?? undefined} /> - + {tokenProject?.name ?? '—'} {/* Suppress warning icon on low warning level */} {(tokenProject?.safetyLevel === SafetyLevel.StrongWarning || tokenProject?.safetyLevel === SafetyLevel.Blocked) && ( - + )} diff --git a/apps/mobile/src/components/TokenDetails/TokenDetailsLinks.tsx b/apps/mobile/src/components/TokenDetails/TokenDetailsLinks.tsx index d54f40ec787..9664fb54908 100644 --- a/apps/mobile/src/components/TokenDetails/TokenDetailsLinks.tsx +++ b/apps/mobile/src/components/TokenDetails/TokenDetailsLinks.tsx @@ -10,11 +10,7 @@ import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' import { TokenDetailsScreenQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { ElementName } from 'uniswap/src/features/telemetry/constants' import { UniverseChainId } from 'uniswap/src/types/chains' -import { - currencyIdToAddress, - currencyIdToChain, - isDefaultNativeAddress, -} from 'wallet/src/utils/currencyId' +import { currencyIdToAddress, currencyIdToChain, isDefaultNativeAddress } from 'wallet/src/utils/currencyId' import { ExplorerDataType, getExplorerLink, getTwitterLink } from 'wallet/src/utils/linking' export function TokenDetailsLinks({ @@ -69,7 +65,7 @@ export function TokenDetailsLinks({ )} diff --git a/apps/mobile/src/components/TokenDetails/TokenDetailsStats.tsx b/apps/mobile/src/components/TokenDetails/TokenDetailsStats.tsx index fbd43c3cfa7..51e4f1ba391 100644 --- a/apps/mobile/src/components/TokenDetails/TokenDetailsStats.tsx +++ b/apps/mobile/src/components/TokenDetails/TokenDetailsStats.tsx @@ -3,13 +3,7 @@ import { useTranslation } from 'react-i18next' import Animated, { FadeIn, FadeOut } from 'react-native-reanimated' import { LongText } from 'src/components/text/LongText' import { Flex, Text, TouchableArea, useSporeColors } from 'ui/src' -import { - ChartBar, - ChartPie, - Language as LanguageIcon, - TrendDown, - TrendUp, -} from 'ui/src/components/icons' +import { ChartBar, ChartPie, Language as LanguageIcon, TrendDown, TrendUp } from 'ui/src/components/icons' import { iconSizes } from 'ui/src/theme' import { TokenDetailsScreenQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { NumberType } from 'utilities/src/format/types' @@ -65,35 +59,40 @@ export function TokenDetailsMarketData({ }> + statsIcon={} + > {convertFiatAmountFormatted(marketCap, NumberType.FiatTokenStats)} }> + statsIcon={} + > {convertFiatAmountFormatted(fullyDilutedValuation, NumberType.FiatTokenStats)} }> + statsIcon={} + > {convertFiatAmountFormatted(volume, NumberType.FiatTokenStats)} }> + statsIcon={} + > {convertFiatAmountFormatted(priceHight52W, NumberType.FiatTokenDetails)} }> + statsIcon={} + > {convertFiatAmountFormatted(priceLow52W, NumberType.FiatTokenDetails)} @@ -130,13 +129,10 @@ export function TokenDetailsStats({ const name = offChainData?.name ?? onChainData?.name const marketCap = offChainData?.markets?.[0]?.marketCap?.value const volume = onChainData?.market?.volume?.value - const priceHight52W = - offChainData?.markets?.[0]?.priceHigh52W?.value ?? onChainData?.market?.priceHigh52W?.value - const priceLow52W = - offChainData?.markets?.[0]?.priceLow52W?.value ?? onChainData?.market?.priceLow52W?.value + const priceHight52W = offChainData?.markets?.[0]?.priceHigh52W?.value ?? onChainData?.market?.priceHigh52W?.value + const priceLow52W = offChainData?.markets?.[0]?.priceLow52W?.value ?? onChainData?.market?.priceLow52W?.value const fullyDilutedValuation = offChainData?.markets?.[0]?.fullyDilutedValuation?.value - const currentDescription = - showTranslation && translatedDescription ? translatedDescription : description + const currentDescription = showTranslation && translatedDescription ? translatedDescription : description return ( @@ -157,14 +153,8 @@ export function TokenDetailsStats({ /> {currentLanguage !== Language.English && !!translatedDescription && ( - setShowTranslation(!showTranslation)}> - + setShowTranslation(!showTranslation)}> + {showTranslation ? ( diff --git a/apps/mobile/src/components/TokenDetails/hooks.test.ts b/apps/mobile/src/components/TokenDetails/hooks.test.ts index 0360234c757..e585980d644 100644 --- a/apps/mobile/src/components/TokenDetails/hooks.test.ts +++ b/apps/mobile/src/components/TokenDetails/hooks.test.ts @@ -1,8 +1,8 @@ import { useCrossChainBalances, useTokenDetailsNavigation } from 'src/components/TokenDetails/hooks' import { preloadedMobileState } from 'src/test/fixtures' import { act, renderHook, waitFor } from 'src/test/test-utils' +import { currencyIdToContractInput } from 'uniswap/src/features/dataApi/utils' import { MobileScreens } from 'uniswap/src/types/screens/mobile' -import { currencyIdToContractInput } from 'wallet/src/features/dataApi/utils' import { SAMPLE_CURRENCY_ID_1, portfolio, @@ -41,7 +41,7 @@ describe(useCrossChainBalances, () => { expect(result.current).toEqual( expect.objectContaining({ currentChainBalance: null, - }) + }), ) }) @@ -51,19 +51,16 @@ describe(useCrossChainBalances, () => { const { resolvers } = queryResolvers({ portfolios: () => [Portfolio], }) - const { result } = renderHook( - () => useCrossChainBalances(currentChainBalance.currencyInfo.currencyId, null), - { - preloadedState: preloadedMobileState(), - resolvers, - } - ) + const { result } = renderHook(() => useCrossChainBalances(currentChainBalance.currencyInfo.currencyId, null), { + preloadedState: preloadedMobileState(), + resolvers, + }) await waitFor(() => { expect(result.current).toEqual( expect.objectContaining({ currentChainBalance, - }) + }), ) }) }) @@ -80,15 +77,12 @@ describe(useCrossChainBalances, () => { expect(result.current).toEqual( expect.objectContaining({ otherChainBalances: null, - }) + }), ) }) it('does not include current chain balance in other chain balances', async () => { - const tokenBalances = [ - tokenBalance({ token: usdcBaseToken() }), - tokenBalance({ token: usdcArbitrumToken() }), - ] + const tokenBalances = [tokenBalance({ token: usdcBaseToken() }), tokenBalance({ token: usdcArbitrumToken() })] const bridgeInfo = tokenBalances.map((balance) => ({ chain: balance.token.chain, @@ -107,13 +101,11 @@ describe(useCrossChainBalances, () => { { preloadedState: preloadedMobileState(), resolvers, - } + }, ) await waitFor(() => { - expect(result.current).toEqual( - expect.objectContaining({ currentChainBalance, otherChainBalances }) - ) + expect(result.current).toEqual(expect.objectContaining({ currentChainBalance, otherChainBalances })) }) }) }) diff --git a/apps/mobile/src/components/TokenDetails/hooks.ts b/apps/mobile/src/components/TokenDetails/hooks.ts index 64a465eab18..1e5775b0a1e 100644 --- a/apps/mobile/src/components/TokenDetails/hooks.ts +++ b/apps/mobile/src/components/TokenDetails/hooks.ts @@ -5,21 +5,17 @@ import { Chain, useTokenDetailsScreenLazyQuery, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { fromGraphQLChain } from 'uniswap/src/features/chains/utils' import { PortfolioBalance } from 'uniswap/src/features/dataApi/types' +import { currencyIdToContractInput } from 'uniswap/src/features/dataApi/utils' import { CurrencyId } from 'uniswap/src/types/currency' import { MobileScreens } from 'uniswap/src/types/screens/mobile' -import { fromGraphQLChain } from 'wallet/src/features/chains/utils' -import { currencyIdToContractInput } from 'wallet/src/features/dataApi/utils' -import { - buildCurrencyId, - buildNativeCurrencyId, - currencyIdToChain, -} from 'wallet/src/utils/currencyId' +import { buildCurrencyId, buildNativeCurrencyId, currencyIdToChain } from 'wallet/src/utils/currencyId' /** Helper hook to retrieve balances across chains for a given currency, for the active account. */ export function useCrossChainBalances( currencyId: string, - bridgeInfo: Maybe<{ chain: Chain; address?: Maybe }[]> + bridgeInfo: Maybe<{ chain: Chain; address?: Maybe }[]>, ): { currentChainBalance: PortfolioBalance | null otherChainBalances: PortfolioBalance[] | null @@ -42,7 +38,7 @@ export function useCrossChainBalances( }) .filter((b): b is string => !!b), - [bridgeInfo, currentChainId] + [bridgeInfo, currentChainId], ) const otherChainBalances = useBalances(bridgedCurrencyIds) @@ -67,7 +63,7 @@ export function useTokenDetailsNavigation(): { variables: currencyIdToContractInput(currencyId), }) }, - [load] + [load], ) // the desired behavior is to push the new token details screen onto the stack instead of replacing it @@ -82,18 +78,15 @@ export function useTokenDetailsNavigation(): { } navigation.push(MobileScreens.TokenDetails, { currencyId }) }, - [navigation] + [navigation], ) const navigate = useCallback( (currencyId: CurrencyId): void => { navigation.navigate(MobileScreens.TokenDetails, { currencyId }) }, - [navigation] + [navigation], ) - return useMemo( - () => ({ preload, navigate, navigateWithPop }), - [navigate, navigateWithPop, preload] - ) + return useMemo(() => ({ preload, navigate, navigateWithPop }), [navigate, navigateWithPop, preload]) } diff --git a/apps/mobile/src/components/TokenSelector/TokenFiatOnRampList.tsx b/apps/mobile/src/components/TokenSelector/TokenFiatOnRampList.tsx index 634d57b364c..092fe8f62e0 100644 --- a/apps/mobile/src/components/TokenSelector/TokenFiatOnRampList.tsx +++ b/apps/mobile/src/components/TokenSelector/TokenFiatOnRampList.tsx @@ -1,14 +1,17 @@ import { BottomSheetFlatList } from '@gorhom/bottom-sheet' import React, { memo, useCallback, useMemo, useRef } from 'react' import { useTranslation } from 'react-i18next' -import { ListRenderItemInfo } from 'react-native' +import { Keyboard, ListRenderItemInfo } from 'react-native' import { Flex, Inset, Loader } from 'ui/src' import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard' +import { TokenOptionItem } from 'uniswap/src/components/TokenSelector/TokenOptionItem' +import { useBottomSheetFocusHook } from 'uniswap/src/components/modals/hooks' import { FiatOnRampCurrency } from 'uniswap/src/features/fiatOnRamp/types' import { UniverseChainId } from 'uniswap/src/types/chains' import { CurrencyId } from 'uniswap/src/types/currency' -import { TokenOptionItem } from 'wallet/src/components/TokenSelector/TokenOptionItem' -import { useBottomSheetFocusHook } from 'wallet/src/components/modals/hooks' +import { NumberType } from 'utilities/src/format/types' +import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' +import { useTokenWarningDismissed } from 'wallet/src/features/tokens/safetyHooks' interface Props { onSelectCurrency: (currency: FiatOnRampCurrency) => void @@ -31,9 +34,11 @@ function TokenOptionItemWrapper({ // we need to convert to TokenOption without quantity and balanceUSD // to use in Token Selector () => (currencyInfo ? { currencyInfo, quantity: 0, balanceUSD: 0 } : null), - [currencyInfo] + [currencyInfo], ) const onPress = useCallback(() => onSelectCurrency?.(currency), [currency, onSelectCurrency]) + const { tokenWarningDismissed, dismissWarningCallback } = useTokenWarningDismissed(currencyInfo?.currencyId) + const { convertFiatAmountFormatted, formatNumberOrString } = useLocalizationContext() if (!option) { return null @@ -41,21 +46,21 @@ function TokenOptionItemWrapper({ return ( Keyboard.dismiss()} onPress={onPress} /> ) } -function _TokenFiatOnRampList({ - onSelectCurrency, - error, - onRetry, - list, - loading, -}: Props): JSX.Element { +function _TokenFiatOnRampList({ onSelectCurrency, error, onRetry, list, loading }: Props): JSX.Element { const { t } = useTranslation() const flatListRef = useRef(null) @@ -64,7 +69,7 @@ function _TokenFiatOnRampList({ ({ item: currency }: ListRenderItemInfo) => { return }, - [onSelectCurrency] + [onSelectCurrency], ) if (error) { diff --git a/apps/mobile/src/components/Trace/TraceUserProperties.test.tsx b/apps/mobile/src/components/Trace/TraceUserProperties.test.tsx index 4b25bad611a..ba99f79913a 100644 --- a/apps/mobile/src/components/Trace/TraceUserProperties.test.tsx +++ b/apps/mobile/src/components/Trace/TraceUserProperties.test.tsx @@ -101,50 +101,26 @@ describe('TraceUserProperties', () => { // Check setUserProperty calls with correct values expect(mocked).toHaveBeenCalledWith(MobileUserPropertyName.AppVersion, '1.0.0.345', undefined) expect(mocked).toHaveBeenCalledWith(MobileUserPropertyName.DarkMode, true, undefined) - expect(mocked).toHaveBeenCalledWith( - MobileUserPropertyName.ActiveWalletAddress, - 'address', - undefined - ) - expect(mocked).toHaveBeenCalledWith( - MobileUserPropertyName.ActiveWalletType, - AccountType.SignerMnemonic, - undefined - ) + expect(mocked).toHaveBeenCalledWith(MobileUserPropertyName.ActiveWalletAddress, 'address', undefined) + expect(mocked).toHaveBeenCalledWith(MobileUserPropertyName.ActiveWalletType, AccountType.SignerMnemonic, undefined) expect(mocked).toHaveBeenCalledWith(MobileUserPropertyName.IsCloudBackedUp, true, undefined) expect(mocked).toHaveBeenCalledWith(MobileUserPropertyName.IsPushEnabled, true, undefined) - expect(mocked).toHaveBeenCalledWith( - MobileUserPropertyName.IsHideSmallBalancesEnabled, - false, - undefined - ) - expect(mocked).toHaveBeenCalledWith( - MobileUserPropertyName.IsHideSpamTokensEnabled, - true, - undefined - ) + expect(mocked).toHaveBeenCalledWith(MobileUserPropertyName.IsHideSmallBalancesEnabled, false, undefined) + expect(mocked).toHaveBeenCalledWith(MobileUserPropertyName.IsHideSpamTokensEnabled, true, undefined) expect(mocked).toHaveBeenCalledWith(MobileUserPropertyName.WalletViewOnlyCount, 2, undefined) expect(mocked).toHaveBeenCalledWith(MobileUserPropertyName.WalletSignerCount, 3, undefined) expect(mocked).toHaveBeenCalledWith( MobileUserPropertyName.WalletSignerAccounts, [address1, address2, address3], - undefined + undefined, ) expect(mocked).toHaveBeenCalledWith( MobileUserPropertyName.WalletSwapProtectionSetting, SwapProtectionSetting.On, - undefined - ) - expect(mocked).toHaveBeenCalledWith( - MobileUserPropertyName.AppOpenAuthMethod, - AuthMethod.FaceId, - undefined - ) - expect(mocked).toHaveBeenCalledWith( - MobileUserPropertyName.TransactionAuthMethod, - AuthMethod.FaceId, - undefined + undefined, ) + expect(mocked).toHaveBeenCalledWith(MobileUserPropertyName.AppOpenAuthMethod, AuthMethod.FaceId, undefined) + expect(mocked).toHaveBeenCalledWith(MobileUserPropertyName.TransactionAuthMethod, AuthMethod.FaceId, undefined) expect(mocked).toHaveBeenCalledWith(MobileUserPropertyName.Language, 'English', undefined) expect(mocked).toHaveBeenCalledWith(MobileUserPropertyName.Currency, 'USD', undefined) @@ -185,16 +161,8 @@ describe('TraceUserProperties', () => { expect(mocked).toHaveBeenCalledWith(MobileUserPropertyName.DarkMode, true, undefined) expect(mocked).toHaveBeenCalledWith(MobileUserPropertyName.WalletViewOnlyCount, 0, undefined) expect(mocked).toHaveBeenCalledWith(MobileUserPropertyName.WalletSignerCount, 0, undefined) - expect(mocked).toHaveBeenCalledWith( - MobileUserPropertyName.AppOpenAuthMethod, - AuthMethod.None, - undefined - ) - expect(mocked).toHaveBeenCalledWith( - MobileUserPropertyName.TransactionAuthMethod, - AuthMethod.None, - undefined - ) + expect(mocked).toHaveBeenCalledWith(MobileUserPropertyName.AppOpenAuthMethod, AuthMethod.None, undefined) + expect(mocked).toHaveBeenCalledWith(MobileUserPropertyName.TransactionAuthMethod, AuthMethod.None, undefined) expect(mocked).toHaveBeenCalledTimes(11) }) diff --git a/apps/mobile/src/components/Trace/TraceUserProperties.tsx b/apps/mobile/src/components/Trace/TraceUserProperties.tsx index 17418a0731f..e1e3eb91999 100644 --- a/apps/mobile/src/components/Trace/TraceUserProperties.tsx +++ b/apps/mobile/src/components/Trace/TraceUserProperties.tsx @@ -1,10 +1,7 @@ import { useEffect } from 'react' import { NativeModules } from 'react-native' import { useAppSelector } from 'src/app/hooks' -import { - useBiometricAppSettings, - useDeviceSupportsBiometricAuth, -} from 'src/features/biometrics/hooks' +import { useBiometricAppSettings, useDeviceSupportsBiometricAuth } from 'src/features/biometrics/hooks' import { getAuthMethod } from 'src/features/telemetry/utils' import { getFullAppVersion } from 'src/utils/version' import { useIsDarkMode } from 'ui/src' @@ -75,7 +72,7 @@ export function TraceUserProperties(): null { setUserProperty(MobileUserPropertyName.WalletSignerCount, signerAccounts.length) setUserProperty( MobileUserPropertyName.WalletSignerAccounts, - signerAccounts.map((account) => account.address) + signerAccounts.map((account) => account.address), ) }, [allowAnalytics, signerAccounts]) @@ -89,14 +86,8 @@ export function TraceUserProperties(): null { } setUserProperty(MobileUserPropertyName.ActiveWalletAddress, activeAccount.address) setUserProperty(MobileUserPropertyName.ActiveWalletType, activeAccount.type) - setUserProperty( - MobileUserPropertyName.IsCloudBackedUp, - Boolean(activeAccount.backups?.includes(BackupType.Cloud)) - ) - setUserProperty( - MobileUserPropertyName.IsPushEnabled, - Boolean(activeAccount.pushNotificationsEnabled) - ) + setUserProperty(MobileUserPropertyName.IsCloudBackedUp, Boolean(activeAccount.backups?.includes(BackupType.Cloud))) + setUserProperty(MobileUserPropertyName.IsPushEnabled, Boolean(activeAccount.pushNotificationsEnabled)) setUserProperty(MobileUserPropertyName.IsHideSmallBalancesEnabled, hideSmallBalances) setUserProperty(MobileUserPropertyName.IsHideSpamTokensEnabled, hideSpamTokens) @@ -105,11 +96,11 @@ export function TraceUserProperties(): null { useEffect(() => { setUserProperty( MobileUserPropertyName.AppOpenAuthMethod, - getAuthMethod(biometricsAppSettingsState.requiredForAppAccess, touchId, faceId) + getAuthMethod(biometricsAppSettingsState.requiredForAppAccess, touchId, faceId), ) setUserProperty( MobileUserPropertyName.TransactionAuthMethod, - getAuthMethod(biometricsAppSettingsState.requiredForTransactions, touchId, faceId) + getAuthMethod(biometricsAppSettingsState.requiredForTransactions, touchId, faceId), ) }, [allowAnalytics, biometricsAppSettingsState, touchId, faceId]) diff --git a/apps/mobile/src/components/accounts/AccountCardItem.test.tsx b/apps/mobile/src/components/accounts/AccountCardItem.test.tsx index cd6431e1417..b31777f1576 100644 --- a/apps/mobile/src/components/accounts/AccountCardItem.test.tsx +++ b/apps/mobile/src/components/accounts/AccountCardItem.test.tsx @@ -48,24 +48,14 @@ describe(AccountCardItem, () => { describe('portfolio value', () => { it('displays loading shimmmer when portfolio value is loading', () => { const { rerender } = render( - + , ) // Select shimmer placeholder because the actual shimmer is rendered after onLayout // is fired and this logic is not a part of this test expect(screen.queryByTestId('shimmer-placeholder')).toBeTruthy() - rerender( - - ) + rerender() expect(screen.queryByTestId('shimmer-placeholder')).toBeFalsy() }) diff --git a/apps/mobile/src/components/accounts/AccountCardItem.tsx b/apps/mobile/src/components/accounts/AccountCardItem.tsx index de9aa9122a6..ec1dcf25355 100644 --- a/apps/mobile/src/components/accounts/AccountCardItem.tsx +++ b/apps/mobile/src/components/accounts/AccountCardItem.tsx @@ -1,22 +1,24 @@ +import { SharedEventName } from '@uniswap/analytics-events' import React, { useMemo } from 'react' import { useTranslation } from 'react-i18next' import ContextMenu from 'react-native-context-menu-view' -import { useAppDispatch } from 'src/app/hooks' +import { useDispatch } from 'react-redux' import { navigate } from 'src/app/navigation/rootNavigation' import { NotificationBadge } from 'src/components/notifications/Badge' import { closeModal, openModal } from 'src/features/modals/modalSlice' import { disableOnPress } from 'src/utils/disableOnPress' import { Flex, HapticFeedback, Text, TouchableArea } from 'ui/src' import { iconSizes } from 'ui/src/theme' -import { ModalName } from 'uniswap/src/features/telemetry/constants' +import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants' +import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { MobileScreens } from 'uniswap/src/types/screens/mobile' +import { setClipboard } from 'uniswap/src/utils/clipboard' import { NumberType } from 'utilities/src/format/types' import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' import { useAccountList } from 'wallet/src/features/accounts/hooks' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' import { pushNotification } from 'wallet/src/features/notifications/slice' import { AppNotificationType, CopyNotificationType } from 'wallet/src/features/notifications/types' -import { setClipboard } from 'wallet/src/utils/clipboard' type AccountCardItemProps = { address: Address @@ -71,7 +73,7 @@ export function AccountCardItem({ }: AccountCardItemProps): JSX.Element { const { t } = useTranslation() - const dispatch = useAppDispatch() + const dispatch = useDispatch() const onPressCopyAddress = async (): Promise => { await HapticFeedback.impact() @@ -80,8 +82,12 @@ export function AccountCardItem({ pushNotification({ type: AppNotificationType.Copied, copyType: CopyNotificationType.Address, - }) + }), ) + sendAnalyticsEvent(SharedEventName.ELEMENT_CLICKED, { + element: ElementName.CopyAddress, + modal: ModalName.AccountSwitcher, + }) } const onPressWalletSettings = (): void => { @@ -122,14 +128,16 @@ export function AccountCardItem({ if (e.nativeEvent.index === 2) { onPressRemoveWallet() } - }}> + }} + > onPress(address)}> + onPress={(): void => onPress(address)} + > { it('renders shortened address within section address without name section', () => { render(, { preloadedState: stateWithoutName }) - const addressSection = screen.getByTestId('account-header/address-only') + const addressSection = screen.getByTestId(TestID.AccountHeaderCopyAddress) const addressText = within(addressSection).queryByText(shortenedAddress) expect(addressText).toBeTruthy() @@ -45,7 +46,7 @@ describe(AccountHeader, () => { jest.spyOn(ExpoClipboard, 'setStringAsync').mockImplementation(setStringAsync) render(, { preloadedState: stateWithoutName }) - const addressSection = screen.getByTestId('account-header/address-only') + const addressSection = screen.getByTestId(TestID.AccountHeaderCopyAddress) fireEvent.press(addressSection, ON_PRESS_EVENT_PAYLOAD) await waitFor(() => { @@ -70,9 +71,7 @@ describe(AccountHeader, () => { it('opens account switcher modal when account name is pressed', () => { const { store } = render(, { preloadedState }) - const displayNameText = within(screen.getByTestId('account-header/display-name')).getByText( - ACCOUNT.name - ) + const displayNameText = within(screen.getByTestId('account-header/display-name')).getByText(ACCOUNT.name) expect(isModalOpen(store.getState())).toBe(false) diff --git a/apps/mobile/src/components/accounts/AccountHeader.tsx b/apps/mobile/src/components/accounts/AccountHeader.tsx index 298d5366347..a169672f6bf 100644 --- a/apps/mobile/src/components/accounts/AccountHeader.tsx +++ b/apps/mobile/src/components/accounts/AccountHeader.tsx @@ -1,31 +1,32 @@ +import { SharedEventName } from '@uniswap/analytics-events' import React, { useCallback, useEffect } from 'react' -import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { useDispatch } from 'react-redux' +import { useAppSelector } from 'src/app/hooks' import { navigate } from 'src/app/navigation/rootNavigation' import { openModal } from 'src/features/modals/modalSlice' import { Flex, HapticFeedback, ImpactFeedbackStyle, Text, TouchableArea } from 'ui/src' import { CopyAlt, Settings } from 'ui/src/components/icons' import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants' +import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { MobileUserPropertyName, setUserProperty } from 'uniswap/src/features/telemetry/user' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { MobileScreens } from 'uniswap/src/types/screens/mobile' -import { isDevEnv } from 'uniswap/src/utils/env' +import { sanitizeAddressText, shortenAddress } from 'uniswap/src/utils/addresses' +import { setClipboard } from 'uniswap/src/utils/clipboard' +import { isDevEnv } from 'utilities/src/environment' import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon' import { AnimatedUnitagDisplayName } from 'wallet/src/components/accounts/AnimatedUnitagDisplayName' import { pushNotification } from 'wallet/src/features/notifications/slice' import { AppNotificationType, CopyNotificationType } from 'wallet/src/features/notifications/types' import { AccountType } from 'wallet/src/features/wallet/accounts/types' import { useAvatar, useDisplayName } from 'wallet/src/features/wallet/hooks' -import { - selectActiveAccount, - selectActiveAccountAddress, -} from 'wallet/src/features/wallet/selectors' +import { selectActiveAccount, selectActiveAccountAddress } from 'wallet/src/features/wallet/selectors' import { DisplayNameType } from 'wallet/src/features/wallet/types' -import { sanitizeAddressText, shortenAddress } from 'wallet/src/utils/addresses' -import { setClipboard } from 'wallet/src/utils/clipboard' export function AccountHeader(): JSX.Element { const activeAddress = useAppSelector(selectActiveAccountAddress) const account = useAppSelector(selectActiveAccount) - const dispatch = useAppDispatch() + const dispatch = useDispatch() const { avatar } = useAvatar(activeAddress) const displayName = useDisplayName(activeAddress) @@ -60,8 +61,12 @@ export function AccountHeader(): JSX.Element { pushNotification({ type: AppNotificationType.Copied, copyType: CopyNotificationType.Address, - }) + }), ) + sendAnalyticsEvent(SharedEventName.ELEMENT_CLICKED, { + element: ElementName.CopyAddress, + screen: MobileScreens.Home, + }) } } @@ -69,13 +74,7 @@ export function AccountHeader(): JSX.Element { const iconSize = 52 return ( - + {activeAddress && ( @@ -85,14 +84,15 @@ export function AccountHeader(): JSX.Element { flexDirection="row" hapticStyle={ImpactFeedbackStyle.Medium} hitSlop={20} - testID={ElementName.Manage} + testID={TestID.AccountHeaderAvatar} onLongPress={async (): Promise => { if (isDevEnv()) { await HapticFeedback.selection() dispatch(openModal({ name: ModalName.Experiments })) } }} - onPress={onPressAccountHeader}> + onPress={onPressAccountHeader} + > + onPress={onPressSettings} + > @@ -115,12 +116,9 @@ export function AccountHeader(): JSX.Element { alignItems="center" gap="$spacing8" justifyContent="space-between" - testID="account-header/display-name"> - + testID="account-header/display-name" + > + @@ -128,14 +126,11 @@ export function AccountHeader(): JSX.Element { + testID={TestID.AccountHeaderCopyAddress} + onPress={onPressCopyAddress} + > - + {sanitizeAddressText(shortenAddress(activeAddress))} diff --git a/apps/mobile/src/components/accounts/AccountList.test.tsx b/apps/mobile/src/components/accounts/AccountList.test.tsx index dba6ade3c96..944bceb0b71 100644 --- a/apps/mobile/src/components/accounts/AccountList.test.tsx +++ b/apps/mobile/src/components/accounts/AccountList.test.tsx @@ -1,17 +1,11 @@ import { AccountList } from 'src/components/accounts/AccountList' import { cleanup, fireEvent, render, screen } from 'src/test/test-utils' import { ON_PRESS_EVENT_PAYLOAD } from 'uniswap/src/test/fixtures' +import { sanitizeAddressText, shortenAddress } from 'uniswap/src/utils/addresses' import { NumberType } from 'utilities/src/format/types' -import { - ACCOUNT, - amounts, - portfolio, - readOnlyAccount, - signerMnemonicAccount, -} from 'wallet/src/test/fixtures' +import { ACCOUNT, amounts, portfolio, readOnlyAccount, signerMnemonicAccount } from 'wallet/src/test/fixtures' import { mockLocalizedFormatter } from 'wallet/src/test/mocks' import { createArray, queryResolvers } from 'wallet/src/test/utils' -import { sanitizeAddressText, shortenAddress } from 'wallet/src/utils/addresses' const tokensTotalDenominatedValue = amounts.md() const { resolvers } = queryResolvers({ @@ -28,8 +22,8 @@ describe(AccountList, () => { value: tokensTotalDenominatedValue.value, type: NumberType.PortfolioBalance, currencyCode: 'usd', - }) - ) + }), + ), ).toBeDefined() expect(tree.toJSON()).toMatchSnapshot() }) @@ -46,8 +40,8 @@ describe(AccountList, () => { value: tokensTotalDenominatedValue.value, type: NumberType.PortfolioBalance, currencyCode: 'usd', - }) - ) + }), + ), ).toBeDefined() fireEvent.press(screen.getByTestId(`account-item/${ACCOUNT.address}`), ON_PRESS_EVENT_PAYLOAD) diff --git a/apps/mobile/src/components/accounts/AccountList.tsx b/apps/mobile/src/components/accounts/AccountList.tsx index 508a59aa792..acad9fce570 100644 --- a/apps/mobile/src/components/accounts/AccountList.tsx +++ b/apps/mobile/src/components/accounts/AccountList.tsx @@ -6,8 +6,8 @@ import { AccountCardItem } from 'src/components/accounts/AccountCardItem' import { VirtualizedList } from 'src/components/layout/VirtualizedList' import { Flex, Text, useSporeColors } from 'ui/src' import { opacify, spacing } from 'ui/src/theme' +import { PollingInterval } from 'uniswap/src/constants/misc' import { useAsyncData } from 'utilities/src/react/hooks' -import { PollingInterval } from 'wallet/src/constants/misc' import { isNonPollingRequestInFlight } from 'wallet/src/data/utils' import { useAccountList } from 'wallet/src/features/accounts/hooks' import { Account, AccountType } from 'wallet/src/features/wallet/accounts/types' @@ -82,17 +82,13 @@ export function AccountList({ accounts, onPress, isVisible }: AccountListProps): }, [accounts, data, isPortfolioValueLoading]) const signerAccounts = useMemo(() => { - return accountsWithPortfolioValue.filter( - (account) => account.account.type === AccountType.SignerMnemonic - ) + return accountsWithPortfolioValue.filter((account) => account.account.type === AccountType.SignerMnemonic) }, [accountsWithPortfolioValue]) const hasSignerAccounts = signerAccounts.length > 0 const viewOnlyAccounts = useMemo(() => { - return accountsWithPortfolioValue.filter( - (account) => account.account.type === AccountType.Readonly - ) + return accountsWithPortfolioValue.filter((account) => account.account.type === AccountType.Readonly) }, [accountsWithPortfolioValue]) const hasViewOnlyAccounts = viewOnlyAccounts.length > 0 @@ -108,7 +104,7 @@ export function AccountList({ accounts, onPress, isVisible }: AccountListProps): onPress={onPress} /> ), - [onPress] + [onPress], ) return ( @@ -123,7 +119,8 @@ export function AccountList({ accounts, onPress, isVisible }: AccountListProps): = MIN_ACCOUNTS_TO_ENABLE_SCROLL} - showsVerticalScrollIndicator={false}> + showsVerticalScrollIndicator={false} + > {hasSignerAccounts && ( <> diff --git a/apps/mobile/src/components/accounts/__snapshots__/AccountHeader.test.tsx.snap b/apps/mobile/src/components/accounts/__snapshots__/AccountHeader.test.tsx.snap index 3c1b670cc09..8c8e8ede1cc 100644 --- a/apps/mobile/src/components/accounts/__snapshots__/AccountHeader.test.tsx.snap +++ b/apps/mobile/src/components/accounts/__snapshots__/AccountHeader.test.tsx.snap @@ -60,7 +60,7 @@ exports[`AccountHeader renders correctly 1`] = ` ], } } - testID="manage" + testID="account-header-avatar" > ({ transform: [ { @@ -51,7 +46,8 @@ export function BottomBanner({ position="absolute" right={0} style={animatedStyle} - zIndex="$modal"> + zIndex="$modal" + > {icon} {text} diff --git a/apps/mobile/src/components/banners/ExtensionPromoBanner.tsx b/apps/mobile/src/components/banners/ExtensionPromoBanner.tsx index 335f69493d3..131378d5137 100644 --- a/apps/mobile/src/components/banners/ExtensionPromoBanner.tsx +++ b/apps/mobile/src/components/banners/ExtensionPromoBanner.tsx @@ -1,15 +1,7 @@ import { useTranslation } from 'react-i18next' import { Keyboard, StyleProp, ViewStyle } from 'react-native' -import { useAppDispatch } from 'src/app/hooks' -import { - Flex, - Image, - Text, - TouchableArea, - useIsDarkMode, - useIsShortMobileDevice, - useSporeColors, -} from 'ui/src' +import { useDispatch } from 'react-redux' +import { Flex, Image, Text, TouchableArea, useIsDarkMode, useIsShortMobileDevice, useSporeColors } from 'ui/src' import { EXTENSION_PROMO_BANNER_DARK, EXTENSION_PROMO_BANNER_LIGHT } from 'ui/src/assets' import { useDeviceDimensions } from 'ui/src/hooks/useDeviceDimensions' import { borderRadii, iconSizes, spacing } from 'ui/src/theme' @@ -17,10 +9,7 @@ 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 { - ExtensionOnboardingState, - setExtensionOnboardingState, -} from 'wallet/src/features/behaviorHistory/slice' +import { ExtensionOnboardingState, setExtensionOnboardingState } from 'wallet/src/features/behaviorHistory/slice' const IMAGE_ASPECT_RATIO = 0.69 const IMAGE_SCREEN_WIDTH_PROPORTION = 0.3 @@ -30,7 +19,7 @@ export function ExtensionPromoBanner({ }: { onShowExtensionPromoModal: () => void }): JSX.Element { - const dispatch = useAppDispatch() + const dispatch = useDispatch() const { t } = useTranslation() const { fullWidth } = useDeviceDimensions() const colors = useSporeColors() @@ -79,7 +68,8 @@ export function ExtensionPromoBanner({ pl="$spacing16" shadowColor="$neutral3" shadowOpacity={0.4} - shadowRadius="$spacing4"> + shadowRadius="$spacing4" + > @@ -87,9 +77,7 @@ export function ExtensionPromoBanner({ {!isShortDevice && ( - {isGAEnabled - ? t('home.banner.extension.message.default') - : t('home.banner.extension.message.beta')} + {isGAEnabled ? t('home.banner.extension.message.default') : t('home.banner.extension.message.beta')} )} @@ -100,11 +88,10 @@ export function ExtensionPromoBanner({ ...baseButtonStyle, backgroundColor: colors.neutral1.get(), }} - onPress={onPressJoin}> + onPress={onPressJoin} + > - {isGAEnabled - ? t('home.banner.extension.confirm.default') - : t('home.banner.extension.confirm.beta')} + {isGAEnabled ? t('home.banner.extension.confirm.default') : t('home.banner.extension.confirm.beta')} + onPress={onPressMaybeLater} + > {t('common.button.later')} diff --git a/apps/mobile/src/components/banners/OfflineBanner.tsx b/apps/mobile/src/components/banners/OfflineBanner.tsx index 0589572f627..1def59e1975 100644 --- a/apps/mobile/src/components/banners/OfflineBanner.tsx +++ b/apps/mobile/src/components/banners/OfflineBanner.tsx @@ -33,13 +33,7 @@ export function OfflineBanner(): JSX.Element | null { return showBanner ? ( - } + icon={} text={t('home.banner.offline')} translateY={BANNER_HEIGHT - EXTRA_MARGIN} /> diff --git a/apps/mobile/src/components/buttons/BackButton.tsx b/apps/mobile/src/components/buttons/BackButton.tsx index 04c448ee978..adef1898145 100644 --- a/apps/mobile/src/components/buttons/BackButton.tsx +++ b/apps/mobile/src/components/buttons/BackButton.tsx @@ -10,13 +10,7 @@ type Props = { onPressBack?: () => void } & TouchableAreaProps -export function BackButton({ - onPressBack, - size, - color, - showButtonLabel, - ...rest -}: Props): JSX.Element { +export function BackButton({ onPressBack, size, color, showButtonLabel, ...rest }: Props): JSX.Element { const navigation = useNavigation() const goBack = onPressBack @@ -31,7 +25,8 @@ export function BackButton({ hitSlop={24} testID="buttons/back-button" onPress={goBack} - {...rest}> + {...rest} + > ) diff --git a/apps/mobile/src/components/buttons/CopyTextButton.test.tsx b/apps/mobile/src/components/buttons/CopyTextButton.test.tsx index e7bfd831c60..423572ec10a 100644 --- a/apps/mobile/src/components/buttons/CopyTextButton.test.tsx +++ b/apps/mobile/src/components/buttons/CopyTextButton.test.tsx @@ -1,8 +1,8 @@ import { CopyTextButton } from 'src/components/buttons/CopyTextButton' import { act, fireEvent, render } from 'src/test/test-utils' -import { setClipboard } from 'wallet/src/utils/clipboard' +import { setClipboard } from 'uniswap/src/utils/clipboard' -jest.mock('wallet/src/utils/clipboard') +jest.mock('uniswap/src/utils/clipboard') describe(CopyTextButton, () => { beforeEach(() => { diff --git a/apps/mobile/src/components/buttons/CopyTextButton.tsx b/apps/mobile/src/components/buttons/CopyTextButton.tsx index ae610352a75..12c22aa58a6 100644 --- a/apps/mobile/src/components/buttons/CopyTextButton.tsx +++ b/apps/mobile/src/components/buttons/CopyTextButton.tsx @@ -4,8 +4,8 @@ import { Button, useSporeColors } from 'ui/src' import CheckCircle from 'ui/src/assets/icons/check-circle.svg' import CopySheets from 'ui/src/assets/icons/copy-sheets.svg' import { iconSizes } from 'ui/src/theme' +import { setClipboard } from 'uniswap/src/utils/clipboard' import { useTimeout } from 'utilities/src/time/timing' -import { setClipboard } from 'wallet/src/utils/clipboard' interface Props { copyText?: string @@ -21,9 +21,7 @@ export function CopyTextButton({ copyText }: Props): JSX.Element { const [isCopied, setIsCopied] = useState(false) const copyIcon = - const copiedIcon = ( - - ) + const copiedIcon = const onPress = async (): Promise => { if (copyText) { diff --git a/apps/mobile/src/components/buttons/LinkButton.test.tsx b/apps/mobile/src/components/buttons/LinkButton.test.tsx index bf069787cc9..457bf9e46c3 100644 --- a/apps/mobile/src/components/buttons/LinkButton.test.tsx +++ b/apps/mobile/src/components/buttons/LinkButton.test.tsx @@ -2,7 +2,7 @@ import { LinkButton } from 'src/components/buttons/LinkButton' import { fireEvent, render } from 'src/test/test-utils' import { ON_PRESS_EVENT_PAYLOAD } from 'uniswap/src/test/fixtures' -jest.mock('wallet/src/utils/linking') +jest.mock('uniswap/src/utils/linking') describe(LinkButton, () => { it('renders without error', () => { @@ -32,16 +32,16 @@ describe(LinkButton, () => { label="link text" openExternalBrowser={openExternalBrowser} url="https://example.com" - /> + />, ) const button = getByText('link text') fireEvent.press(button, ON_PRESS_EVENT_PAYLOAD) - expect(require('wallet/src/utils/linking').openUri).toHaveBeenCalledWith( + expect(require('uniswap/src/utils/linking').openUri).toHaveBeenCalledWith( 'https://example.com', openExternalBrowser, - isSafeUri + isSafeUri, ) }) }) diff --git a/apps/mobile/src/components/buttons/LinkButton.tsx b/apps/mobile/src/components/buttons/LinkButton.tsx index 858d9f4ccdb..cc6cedd8932 100644 --- a/apps/mobile/src/components/buttons/LinkButton.tsx +++ b/apps/mobile/src/components/buttons/LinkButton.tsx @@ -2,7 +2,7 @@ import React, { useMemo } from 'react' import { Flex, FlexProps, Text, TouchableArea, TouchableAreaProps, useSporeColors } from 'ui/src' import ExternalLinkIcon from 'ui/src/assets/icons/external-link.svg' import { TextVariantTokens, iconSizes } from 'ui/src/theme' -import { openUri } from 'wallet/src/utils/linking' +import { openUri } from 'uniswap/src/utils/linking' interface LinkButtonProps extends Omit { label: string @@ -38,9 +38,7 @@ export function LinkButton({ }, [color]) return ( - => openUri(url, openExternalBrowser, isSafeUri)} - {...rest}> + => openUri(url, openExternalBrowser, isSafeUri)} {...rest}> {label} diff --git a/apps/mobile/src/components/buttons/utils.ts b/apps/mobile/src/components/buttons/utils.ts index 2f64ab7fbc7..6792a73ef48 100644 --- a/apps/mobile/src/components/buttons/utils.ts +++ b/apps/mobile/src/components/buttons/utils.ts @@ -2,11 +2,8 @@ import { withSequence, withSpring, WithSpringConfig } from 'react-native-reanima export function pulseAnimation( activeScale: number, - spingAnimationConfig: WithSpringConfig = { damping: 1, stiffness: 200 } + spingAnimationConfig: WithSpringConfig = { damping: 1, stiffness: 200 }, ): number { 'worklet' - return withSequence( - withSpring(activeScale, spingAnimationConfig), - withSpring(1, spingAnimationConfig) - ) + return withSequence(withSpring(activeScale, spingAnimationConfig), withSpring(1, spingAnimationConfig)) } diff --git a/apps/mobile/src/components/carousel/Indicator.tsx b/apps/mobile/src/components/carousel/Indicator.tsx index 0c6630551db..9dd6022a963 100644 --- a/apps/mobile/src/components/carousel/Indicator.tsx +++ b/apps/mobile/src/components/carousel/Indicator.tsx @@ -4,23 +4,12 @@ import { Flex } from 'ui/src' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { useDeviceDimensions } from 'ui/src/hooks/useDeviceDimensions' -export function Indicator({ - stepCount, - currentStep, -}: { - stepCount: number - currentStep: number -}): JSX.Element { +export function Indicator({ stepCount, currentStep }: { stepCount: number; currentStep: number }): JSX.Element { const { fullWidth } = useDeviceDimensions() const indicatorWidth = (200 / 375) * fullWidth return ( - + {[...Array(stepCount)].map((_, i) => ( -}): JSX.Element { +function AnimatedIndicatorPill({ index, scroll }: { index: number; scroll: SharedValue }): JSX.Element { const { fullWidth } = useDeviceDimensions() const style = useAnimatedStyle(() => { const inputRange = [(index - 1) * fullWidth, index * fullWidth, (index + 1) * fullWidth] diff --git a/apps/mobile/src/components/education/SeedPhrase.tsx b/apps/mobile/src/components/education/SeedPhrase.tsx index 9845aa3bd9d..36e4b516deb 100644 --- a/apps/mobile/src/components/education/SeedPhrase.tsx +++ b/apps/mobile/src/components/education/SeedPhrase.tsx @@ -10,13 +10,7 @@ import { useDeviceDimensions } from 'ui/src/hooks/useDeviceDimensions' import { OnboardingScreens } from 'uniswap/src/types/screens/mobile' import { getCloudProviderName } from 'uniswap/src/utils/cloud-backup/getCloudProviderName' -function Page({ - text, - params, -}: { - text: ReactNode - params: OnboardingStackBaseParams -}): JSX.Element { +function Page({ text, params }: { text: ReactNode; params: OnboardingStackBaseParams }): JSX.Element { const { t } = useTranslation() const { fullWidth } = useDeviceDimensions() const { goToPrev, goToNext } = useContext(CarouselContext) @@ -35,7 +29,7 @@ function Page({ runOnJS(goToNext)() } }), - [goToPrev, goToNext, fullWidth] + [goToPrev, goToNext, fullWidth], ) const dismissGesture = useMemo( @@ -43,19 +37,14 @@ function Page({ Gesture.Tap().onEnd(() => { runOnJS(onDismiss)() }), - [onDismiss] + [onDismiss], ) return ( - + {t('onboarding.tooltip.recoveryPhrase.trigger')} @@ -78,31 +67,16 @@ export const SeedPhraseEducationContent = (params: OnboardingStackBaseParams): J const highlightComponent = const pageContentList = [ - , - , - , + , + , + , , - , - , + , + , ] return pageContentList.map((content) => ( diff --git a/apps/mobile/src/components/explore/ExploreSections.tsx b/apps/mobile/src/components/explore/ExploreSections.tsx index d071dde20d2..3e59299f77c 100644 --- a/apps/mobile/src/components/explore/ExploreSections.tsx +++ b/apps/mobile/src/components/explore/ExploreSections.tsx @@ -18,23 +18,20 @@ import { import { usePollOnFocusOnly } from 'src/utils/hooks' import { Flex, Loader, Text, useDeviceInsets } from 'ui/src' import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard' +import { getWrappedNativeAddress } from 'uniswap/src/constants/addresses' +import { PollingInterval } from 'uniswap/src/constants/misc' import { Chain, ExploreTokensTabQuery, useExploreTokensTabQuery, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { fromGraphQLChain } from 'uniswap/src/features/chains/utils' +import { usePersistedError } from 'uniswap/src/features/dataApi/utils' import { UniverseChainId } from 'uniswap/src/types/chains' -import { getWrappedNativeAddress } from 'wallet/src/constants/addresses' -import { PollingInterval } from 'wallet/src/constants/misc' -import { fromGraphQLChain } from 'wallet/src/features/chains/utils' -import { usePersistedError } from 'wallet/src/features/dataApi/utils' -import { - selectHasFavoriteTokens, - selectHasWatchedWallets, -} from 'wallet/src/features/favorites/selectors' +import { areAddressesEqual } from 'uniswap/src/utils/addresses' +import { buildCurrencyId, buildNativeCurrencyId } from 'uniswap/src/utils/currencyId' +import { selectHasFavoriteTokens, selectHasWatchedWallets } from 'wallet/src/features/favorites/selectors' import { selectTokensOrderBy } from 'wallet/src/features/wallet/selectors' -import { areAddressesEqual } from 'wallet/src/utils/addresses' -import { buildCurrencyId, buildNativeCurrencyId } from 'wallet/src/utils/currencyId' type ExploreSectionsProps = { listRef: React.MutableRefObject @@ -86,8 +83,7 @@ export function ExploreSections({ listRef }: ExploreSectionsProps): JSX.Element return } - const isWeth = - areAddressesEqual(token.address, wethAddress) && token?.chain === Chain.Ethereum + const isWeth = areAddressesEqual(token.address, wethAddress) && token?.chain === Chain.Ethereum // manually replace weth with eth given backend only returns eth data as a proxy for eth if (isWeth && eth) { @@ -109,20 +105,13 @@ export function ExploreSections({ listRef }: ExploreSectionsProps): JSX.Element const renderItem: ListRenderItem = useCallback( ({ item, index }: ListRenderItemInfo) => { - return ( - - ) + return }, - [tokenMetadataDisplayType] + [tokenMetadataDisplayType], ) // Don't want to show full screen loading state when changing tokens sort, which triggers NetworkStatus.setVariable request - const isLoading = - networkStatus === NetworkStatus.loading || networkStatus === NetworkStatus.refetch + const isLoading = networkStatus === NetworkStatus.loading || networkStatus === NetworkStatus.refetch const hasAllData = !!data?.topTokens const error = usePersistedError(requestLoading, requestError) @@ -161,7 +150,8 @@ export function ExploreSections({ listRef }: ExploreSectionsProps): JSX.Element }, }): void => { visibleListHeight.value = height - }}> + }} + > + pl="$spacing4" + > {t('explore.tokens.top.title')} @@ -200,6 +191,7 @@ export function ExploreSections({ listRef }: ExploreSectionsProps): JSX.Element contentContainerStyle={{ paddingBottom: insets.bottom }} data={showLoading ? undefined : topTokenItems} keyExtractor={tokenKey} + removeClippedSubviews={false} renderItem={renderItem} scrollEventThrottle={16} showsHorizontalScrollIndicator={false} @@ -211,13 +203,11 @@ export function ExploreSections({ listRef }: ExploreSectionsProps): JSX.Element } const tokenKey = (token: TokenItemData): string => { - return token.address - ? buildCurrencyId(token.chainId, token.address) - : buildNativeCurrencyId(token.chainId) + return token.address ? buildCurrencyId(token.chainId, token.address) : buildNativeCurrencyId(token.chainId) } function gqlTokenToTokenItemData( - token: Maybe[0]>> + token: Maybe[0]>>, ): TokenItemData | null { if (!token || !token.project) { return null @@ -256,13 +246,7 @@ function FavoritesSection(props: FavoritesSectionProps): JSX.Element | null { } return ( - + {hasFavoritedTokens && } {hasFavoritedWallets && } diff --git a/apps/mobile/src/components/explore/FavoriteHeaderRow.test.tsx b/apps/mobile/src/components/explore/FavoriteHeaderRow.test.tsx index 8d5b6801ac0..bf4246b0d91 100644 --- a/apps/mobile/src/components/explore/FavoriteHeaderRow.test.tsx +++ b/apps/mobile/src/components/explore/FavoriteHeaderRow.test.tsx @@ -1,6 +1,7 @@ import { FavoriteHeaderRow } from 'src/components/explore/FavoriteHeaderRow' import { fireEvent, render } from 'src/test/test-utils' import { ON_PRESS_EVENT_PAYLOAD } from 'uniswap/src/test/fixtures' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' const defaultProps = { title: 'Title', @@ -27,8 +28,8 @@ describe(FavoriteHeaderRow, () => { it('renders favorite button', () => { const { queryByTestId } = render() - const favoriteButton = queryByTestId('favorite-header-row/favorite-button') - const doneButton = queryByTestId('favorite-header-row/done-button') + const favoriteButton = queryByTestId(TestID.Edit) + const doneButton = queryByTestId(TestID.Done) expect(favoriteButton).toBeTruthy() expect(doneButton).toBeFalsy() @@ -37,7 +38,7 @@ describe(FavoriteHeaderRow, () => { it('calls onPress when favorite icon pressed', () => { const { getByTestId } = render() - const favoriteButton = getByTestId('favorite-header-row/favorite-button') + const favoriteButton = getByTestId(TestID.Edit) fireEvent.press(favoriteButton, ON_PRESS_EVENT_PAYLOAD) expect(defaultProps.onPress).toHaveBeenCalledTimes(1) @@ -61,8 +62,8 @@ describe(FavoriteHeaderRow, () => { it('renders done button', () => { const { queryByTestId } = render() - const favoriteButton = queryByTestId('favorite-header-row/favorite-button') - const doneButton = queryByTestId('favorite-header-row/done-button') + const favoriteButton = queryByTestId(TestID.Edit) + const doneButton = queryByTestId(TestID.Done) expect(favoriteButton).toBeFalsy() expect(doneButton).toBeTruthy() @@ -71,7 +72,7 @@ describe(FavoriteHeaderRow, () => { it('calls onPress when done button pressed', () => { const { getByTestId } = render() - const doneButton = getByTestId('favorite-header-row/done-button') + const doneButton = getByTestId(TestID.Done) fireEvent.press(doneButton, ON_PRESS_EVENT_PAYLOAD) expect(defaultProps.onPress).toHaveBeenCalledTimes(1) diff --git a/apps/mobile/src/components/explore/FavoriteHeaderRow.tsx b/apps/mobile/src/components/explore/FavoriteHeaderRow.tsx index 681f0f48e96..4825ff0a9a1 100644 --- a/apps/mobile/src/components/explore/FavoriteHeaderRow.tsx +++ b/apps/mobile/src/components/explore/FavoriteHeaderRow.tsx @@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next' import { Flex, Text, TouchableArea } from 'ui/src' import { TripleDots } from 'ui/src/components/icons' import { iconSizes } from 'ui/src/theme' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' export function FavoriteHeaderRow({ title, @@ -17,32 +18,17 @@ export function FavoriteHeaderRow({ }): JSX.Element { const { t } = useTranslation() return ( - + {isEditing ? editingTitle : title} {!isEditing ? ( - - + + ) : ( - + {t('common.button.done')} diff --git a/apps/mobile/src/components/explore/FavoriteTokenCard.test.tsx b/apps/mobile/src/components/explore/FavoriteTokenCard.test.tsx index 51b99702271..2fa25c8f4d5 100644 --- a/apps/mobile/src/components/explore/FavoriteTokenCard.test.tsx +++ b/apps/mobile/src/components/explore/FavoriteTokenCard.test.tsx @@ -6,13 +6,7 @@ import { ON_PRESS_EVENT_PAYLOAD } from 'uniswap/src/test/fixtures' import { getSymbolDisplayText } from 'uniswap/src/utils/currency' import { FiatCurrency } from 'wallet/src/features/fiatCurrency/constants' import { Language } from 'wallet/src/features/language/constants' -import { - SAMPLE_CURRENCY_ID_1, - amount, - ethToken, - tokenProject, - tokenProjectMarket, -} from 'wallet/src/test/fixtures' +import { SAMPLE_CURRENCY_ID_1, amount, ethToken, tokenProject, tokenProjectMarket } from 'wallet/src/test/fixtures' import { queryResolvers } from 'wallet/src/test/utils' const mockedNavigation = { diff --git a/apps/mobile/src/components/explore/FavoriteTokenCard.tsx b/apps/mobile/src/components/explore/FavoriteTokenCard.tsx index 5025a1b61c8..73f3cdfa608 100644 --- a/apps/mobile/src/components/explore/FavoriteTokenCard.tsx +++ b/apps/mobile/src/components/explore/FavoriteTokenCard.tsx @@ -2,7 +2,7 @@ import React, { memo, useCallback } from 'react' import { ViewProps } from 'react-native' import ContextMenu from 'react-native-context-menu-view' import { FadeIn, SharedValue } from 'react-native-reanimated' -import { useAppDispatch } from 'src/app/hooks' +import { useDispatch } from 'react-redux' import { useTokenDetailsNavigation } from 'src/components/TokenDetails/hooks' import RemoveButton from 'src/components/explore/RemoveButton' import { useAnimatedCardDragStyle, useExploreTokenContextMenu } from 'src/components/explore/hooks' @@ -14,16 +14,16 @@ import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { borderRadii, imageSizes } from 'ui/src/theme' import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard' import { TokenLogo } from 'uniswap/src/components/CurrencyLogo/TokenLogo' +import { PollingInterval } from 'uniswap/src/constants/misc' import { useFavoriteTokenCardQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { fromGraphQLChain } from 'uniswap/src/features/chains/utils' +import { currencyIdToContractInput } from 'uniswap/src/features/dataApi/utils' import { SectionName } from 'uniswap/src/features/telemetry/constants' import { UniverseChainId } from 'uniswap/src/types/chains' import { getSymbolDisplayText } from 'uniswap/src/utils/currency' import { NumberType } from 'utilities/src/format/types' import { RelativeChange } from 'wallet/src/components/text/RelativeChange' -import { PollingInterval } from 'wallet/src/constants/misc' import { isNonPollingRequestInFlight } from 'wallet/src/data/utils' -import { fromGraphQLChain } from 'wallet/src/features/chains/utils' -import { currencyIdToContractInput } from 'wallet/src/features/dataApi/utils' import { removeFavoriteToken } from 'wallet/src/features/favorites/slice' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' @@ -45,7 +45,7 @@ function FavoriteTokenCard({ setIsEditing, ...rest }: FavoriteTokenCardProps): JSX.Element { - const dispatch = useAppDispatch() + const dispatch = useDispatch() const tokenDetailsNavigation = useTokenDetailsNavigation() const { convertFiatAmountFormatted } = useLocalizationContext() @@ -63,10 +63,7 @@ function FavoriteTokenCard({ // Mirror behavior in top tokens list, use first chain the token is on for the symbol const chainId = fromGraphQLChain(token?.chain) ?? UniverseChainId.Mainnet - const price = convertFiatAmountFormatted( - token?.project?.markets?.[0]?.price?.value, - NumberType.FiatTokenPrice - ) + const price = convertFiatAmountFormatted(token?.project?.markets?.[0]?.price?.value, NumberType.FiatTokenPrice) const pricePercentChange = token?.project?.markets?.[0]?.pricePercentChange24h?.value const onRemove = useCallback(() => { @@ -107,7 +104,8 @@ function FavoriteTokenCard({ disabled={isEditing} style={{ borderRadius: borderRadii.rounded16 }} onPress={onContextMenuPress} - {...rest}> + {...rest} + > + onPress={onPress} + > diff --git a/apps/mobile/src/components/explore/FavoriteTokensGrid.tsx b/apps/mobile/src/components/explore/FavoriteTokensGrid.tsx index 8afe8d3e960..c018560a890 100644 --- a/apps/mobile/src/components/explore/FavoriteTokensGrid.tsx +++ b/apps/mobile/src/components/explore/FavoriteTokensGrid.tsx @@ -1,11 +1,10 @@ import React, { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { FadeIn, useAnimatedStyle, useSharedValue } from 'react-native-reanimated' +import { useDispatch } from 'react-redux' import { useAppSelector } from 'src/app/hooks' import { FavoriteHeaderRow } from 'src/components/explore/FavoriteHeaderRow' -import FavoriteTokenCard, { - FAVORITE_TOKEN_CARD_LOADER_HEIGHT, -} from 'src/components/explore/FavoriteTokenCard' +import FavoriteTokenCard, { FAVORITE_TOKEN_CARD_LOADER_HEIGHT } from 'src/components/explore/FavoriteTokenCard' import { Loader } from 'src/components/loading' import { AutoScrollProps, @@ -17,7 +16,6 @@ import { Flex } from 'ui/src' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { selectFavoriteTokens } from 'wallet/src/features/favorites/selectors' import { setFavoriteTokens } from 'wallet/src/features/favorites/slice' -import { useAppDispatch } from 'wallet/src/state' const NUM_COLUMNS = 2 const ITEM_FLEX = { flex: 1 / NUM_COLUMNS } @@ -27,12 +25,9 @@ type FavoriteTokensGridProps = AutoScrollProps & { } /** Renders the favorite tokens section on the Explore tab */ -export function FavoriteTokensGrid({ - showLoading, - ...rest -}: FavoriteTokensGridProps): JSX.Element | null { +export function FavoriteTokensGrid({ showLoading, ...rest }: FavoriteTokensGridProps): JSX.Element | null { const { t } = useTranslation() - const dispatch = useAppDispatch() + const dispatch = useDispatch() const [isEditing, setIsEditing] = useState(false) const isTokenDragged = useSharedValue(false) @@ -49,7 +44,7 @@ export function FavoriteTokensGrid({ ({ data }: SortableGridChangeEvent) => { dispatch(setFavoriteTokens({ currencyIds: data })) }, - [dispatch] + [dispatch], ) const renderItem = useCallback>( @@ -63,7 +58,7 @@ export function FavoriteTokensGrid({ setIsEditing={setIsEditing} /> ), - [isEditing] + [isEditing], ) const animatedStyle = useAnimatedStyle(() => ({ diff --git a/apps/mobile/src/components/explore/FavoriteWalletCard.test.tsx b/apps/mobile/src/components/explore/FavoriteWalletCard.test.tsx index 916243e359d..4ad93744c9c 100644 --- a/apps/mobile/src/components/explore/FavoriteWalletCard.test.tsx +++ b/apps/mobile/src/components/explore/FavoriteWalletCard.test.tsx @@ -1,20 +1,14 @@ import { makeMutable } from 'react-native-reanimated' import configureMockStore from 'redux-mock-store' -import FavoriteWalletCard, { - FavoriteWalletCardProps, -} from 'src/components/explore/FavoriteWalletCard' +import FavoriteWalletCard, { FavoriteWalletCardProps } from 'src/components/explore/FavoriteWalletCard' import { preloadedMobileState } from 'src/test/fixtures' import { fireEvent, render, waitFor } from 'src/test/test-utils' import * as unitagHooks from 'uniswap/src/features/unitags/hooks' import { ON_PRESS_EVENT_PAYLOAD } from 'uniswap/src/test/fixtures' import { MobileScreens } from 'uniswap/src/types/screens/mobile' +import { sanitizeAddressText, shortenAddress } from 'uniswap/src/utils/addresses' import * as ensHooks from 'wallet/src/features/ens/api' -import { - SAMPLE_SEED_ADDRESS_1, - preloadedWalletState, - signerMnemonicAccount, -} from 'wallet/src/test/fixtures' -import { sanitizeAddressText, shortenAddress } from 'wallet/src/utils/addresses' +import { SAMPLE_SEED_ADDRESS_1, preloadedWalletState, signerMnemonicAccount } from 'wallet/src/test/fixtures' const mockedNavigation = { navigate: jest.fn(), diff --git a/apps/mobile/src/components/explore/FavoriteWalletCard.tsx b/apps/mobile/src/components/explore/FavoriteWalletCard.tsx index a69196f770e..0a248995836 100644 --- a/apps/mobile/src/components/explore/FavoriteWalletCard.tsx +++ b/apps/mobile/src/components/explore/FavoriteWalletCard.tsx @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next' import { ViewProps } from 'react-native' import ContextMenu from 'react-native-context-menu-view' import { SharedValue } from 'react-native-reanimated' -import { useAppDispatch } from 'src/app/hooks' +import { useDispatch } from 'react-redux' import { useEagerExternalProfileNavigation } from 'src/app/navigation/hooks' import RemoveButton from 'src/components/explore/RemoveButton' import { useAnimatedCardDragStyle } from 'src/components/explore/hooks' @@ -35,7 +35,7 @@ function FavoriteWalletCard({ ...rest }: FavoriteWalletCardProps): JSX.Element { const { t } = useTranslation() - const dispatch = useAppDispatch() + const dispatch = useDispatch() const { preload, navigate } = useEagerExternalProfileNavigation() const displayName = useDisplayName(address) @@ -76,7 +76,8 @@ function FavoriteWalletCard({ setIsEditing(true) } }} - {...rest}> + {...rest} + > => { await preload(address) - }}> + }} + > diff --git a/apps/mobile/src/components/explore/FavoriteWalletsGrid.tsx b/apps/mobile/src/components/explore/FavoriteWalletsGrid.tsx index 7bf7cb38e8c..a72d91eb5ae 100644 --- a/apps/mobile/src/components/explore/FavoriteWalletsGrid.tsx +++ b/apps/mobile/src/components/explore/FavoriteWalletsGrid.tsx @@ -1,6 +1,7 @@ import { default as React, useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { FadeIn, useAnimatedStyle, useSharedValue } from 'react-native-reanimated' +import { useDispatch } from 'react-redux' import { useAppSelector } from 'src/app/hooks' import { FavoriteHeaderRow } from 'src/components/explore/FavoriteHeaderRow' import FavoriteWalletCard from 'src/components/explore/FavoriteWalletCard' @@ -15,7 +16,6 @@ import { Flex } from 'ui/src' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { selectWatchedAddressSet } from 'wallet/src/features/favorites/selectors' import { setFavoriteWallets } from 'wallet/src/features/favorites/slice' -import { useAppDispatch } from 'wallet/src/state' const NUM_COLUMNS = 2 const ITEM_FLEX = { flex: 1 / NUM_COLUMNS } @@ -25,12 +25,9 @@ type FavoriteWalletsGridProps = AutoScrollProps & { } /** Renders the favorite wallets section on the Explore tab */ -export function FavoriteWalletsGrid({ - showLoading, - ...rest -}: FavoriteWalletsGridProps): JSX.Element { +export function FavoriteWalletsGrid({ showLoading, ...rest }: FavoriteWalletsGridProps): JSX.Element { const { t } = useTranslation() - const dispatch = useAppDispatch() + const dispatch = useDispatch() const [isEditing, setIsEditing] = useState(false) const isTokenDragged = useSharedValue(false) @@ -48,7 +45,7 @@ export function FavoriteWalletsGrid({ ({ data }: SortableGridChangeEvent) => { dispatch(setFavoriteWallets({ addresses: data })) }, - [dispatch] + [dispatch], ) const renderItem = useCallback>( @@ -62,7 +59,7 @@ export function FavoriteWalletsGrid({ setIsEditing={setIsEditing} /> ), - [isEditing] + [isEditing], ) const animatedStyle = useAnimatedStyle(() => ({ diff --git a/apps/mobile/src/components/explore/RemoveButton.tsx b/apps/mobile/src/components/explore/RemoveButton.tsx index a42ada56f75..af9f6248873 100644 --- a/apps/mobile/src/components/explore/RemoveButton.tsx +++ b/apps/mobile/src/components/explore/RemoveButton.tsx @@ -17,13 +17,15 @@ export default function RemoveButton({ visible = true, ...rest }: RemoveButtonPr alignItems="center" backgroundColor="$neutral3" borderRadius="$roundedFull" + disabled={!visible} height={imageSizes.image24} justifyContent="center" style={animatedVisibilityStyle} testID="explore/remove-button" width={imageSizes.image24} zIndex="$tooltip" - {...rest}> + {...rest} + > ) diff --git a/apps/mobile/src/components/explore/SortButton.test.tsx b/apps/mobile/src/components/explore/SortButton.test.tsx index f7a0902565c..ef305932469 100644 --- a/apps/mobile/src/components/explore/SortButton.test.tsx +++ b/apps/mobile/src/components/explore/SortButton.test.tsx @@ -65,18 +65,16 @@ describe('SortButton', () => { }, { title: 'Price increase (24H)', - systemIcon: - orderBy === ClientTokensOrderBy.PriceChangePercentage24hDesc ? 'checkmark' : '', + systemIcon: orderBy === ClientTokensOrderBy.PriceChangePercentage24hDesc ? 'checkmark' : '', orderBy: ClientTokensOrderBy.PriceChangePercentage24hDesc, }, { title: 'Price decrease (24H)', - systemIcon: - orderBy === ClientTokensOrderBy.PriceChangePercentage24hAsc ? 'checkmark' : '', + systemIcon: orderBy === ClientTokensOrderBy.PriceChangePercentage24hAsc ? 'checkmark' : '', orderBy: ClientTokensOrderBy.PriceChangePercentage24hAsc, }, ], - }) + }), ) }) }) diff --git a/apps/mobile/src/components/explore/SortButton.tsx b/apps/mobile/src/components/explore/SortButton.tsx index 9e4b47f7e1d..1c6bfae4674 100644 --- a/apps/mobile/src/components/explore/SortButton.tsx +++ b/apps/mobile/src/components/explore/SortButton.tsx @@ -1,11 +1,8 @@ import React, { memo, useMemo } from 'react' import { useTranslation } from 'react-i18next' import ContextMenu from 'react-native-context-menu-view' -import { useAppDispatch } from 'src/app/hooks' -import { - getTokensOrderByMenuLabel, - getTokensOrderBySelectedLabel, -} from 'src/features/explore/utils' +import { useDispatch } from 'react-redux' +import { getTokensOrderByMenuLabel, getTokensOrderBySelectedLabel } from 'src/features/explore/utils' import { disableOnPress } from 'src/utils/disableOnPress' import { Flex, Text, TouchableArea, useIsDarkMode } from 'ui/src' import { RotatableChevron } from 'ui/src/components/icons' @@ -22,7 +19,7 @@ interface FilterGroupProps { function _SortButton({ orderBy }: FilterGroupProps): JSX.Element { const isDarkMode = useIsDarkMode() - const dispatch = useAppDispatch() + const dispatch = useDispatch() const { t } = useTranslation() const menuActions = useMemo(() => { @@ -73,7 +70,8 @@ function _SortButton({ orderBy }: FilterGroupProps): JSX.Element { sendAnalyticsEvent(MobileEventName.ExploreFilterSelected, { filter_type: selectedMenuAction.orderBy, }) - }}> + }} + > + onLongPress={disableOnPress} + > {orderBy === TokenSortableField.Volume || orderBy === TokenSortableField.TotalValueLocked} {getTokensOrderBySelectedLabel(orderBy, t)} - + diff --git a/apps/mobile/src/components/explore/TokenItem.test.tsx b/apps/mobile/src/components/explore/TokenItem.test.tsx index 7a73c486e8c..a0ef2fe0919 100644 --- a/apps/mobile/src/components/explore/TokenItem.test.tsx +++ b/apps/mobile/src/components/explore/TokenItem.test.tsx @@ -4,8 +4,8 @@ import * as exploreHooks from 'src/components/explore/hooks' import { TOKEN_ITEM_DATA, tokenItemData } from 'src/test/fixtures' import { fireEvent, render, within } from 'src/test/test-utils' import { ON_PRESS_EVENT_PAYLOAD } from 'uniswap/src/test/fixtures' +import { buildCurrencyId } from 'uniswap/src/utils/currencyId' import { TokenMetadataDisplayType } from 'wallet/src/features/wallet/types' -import { buildCurrencyId } from 'wallet/src/utils/currencyId' describe('TokenItem', () => { const mockedTokenDetailsNavigation = { @@ -15,9 +15,7 @@ describe('TokenItem', () => { } beforeAll(() => { - jest - .spyOn(tokenDetailsHooks, 'useTokenDetailsNavigation') - .mockReturnValue(mockedTokenDetailsNavigation) + jest.spyOn(tokenDetailsHooks, 'useTokenDetailsNavigation').mockReturnValue(mockedTokenDetailsNavigation) jest.spyOn(exploreHooks, 'useExploreTokenContextMenu').mockReturnValue({ menuActions: [], onContextMenuPress: jest.fn(), @@ -50,9 +48,7 @@ describe('TokenItem', () => { fireEvent.press(getByTestId(`token-item-${data.name}`), ON_PRESS_EVENT_PAYLOAD) - expect(mockedTokenDetailsNavigation.navigate).toHaveBeenCalledWith( - buildCurrencyId(data.chainId, data.address) - ) + expect(mockedTokenDetailsNavigation.navigate).toHaveBeenCalledWith(buildCurrencyId(data.chainId, data.address)) }) describe('token price', () => { @@ -111,9 +107,7 @@ describe('TokenItem', () => { ] it.each(cases)('renders $test metadata subtitle', ({ type, expected }) => { - const { getByTestId } = render( - - ) + const { getByTestId } = render() const metadataSubtitle = getByTestId('token-item/metadata-subtitle') diff --git a/apps/mobile/src/components/explore/TokenItem.tsx b/apps/mobile/src/components/explore/TokenItem.tsx index 6668973eb10..1916a03d55b 100644 --- a/apps/mobile/src/components/explore/TokenItem.tsx +++ b/apps/mobile/src/components/explore/TokenItem.tsx @@ -11,16 +11,16 @@ import { TokenLogo } from 'uniswap/src/components/CurrencyLogo/TokenLogo' import { MobileEventName, SectionName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { WalletChainId } from 'uniswap/src/types/chains' -import { NumberType } from 'utilities/src/format/types' -import { RelativeChange } from 'wallet/src/components/text/RelativeChange' -import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' -import { TokenMetadataDisplayType } from 'wallet/src/features/wallet/types' import { buildCurrencyId, buildNativeCurrencyId, currencyIdToAddress, currencyIdToChain, -} from 'wallet/src/utils/currencyId' +} from 'uniswap/src/utils/currencyId' +import { NumberType } from 'utilities/src/format/types' +import { RelativeChange } from 'wallet/src/components/text/RelativeChange' +import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' +import { TokenMetadataDisplayType } from 'wallet/src/features/wallet/types' export type TokenItemData = { name: string @@ -41,11 +41,7 @@ interface TokenItemProps { metadataDisplayType?: TokenMetadataDisplayType } -export const TokenItem = memo(function _TokenItem({ - tokenItemData, - index, - metadataDisplayType, -}: TokenItemProps) { +export const TokenItem = memo(function _TokenItem({ tokenItemData, index, metadataDisplayType }: TokenItemProps) { const { t } = useTranslation() const tokenDetailsNavigation = useTokenDetailsNavigation() const { convertFiatAmountFormatted } = useLocalizationContext() @@ -65,10 +61,7 @@ export const TokenItem = memo(function _TokenItem({ const _currencyId = address ? buildCurrencyId(chainId, address) : buildNativeCurrencyId(chainId) const marketCapFormatted = convertFiatAmountFormatted(marketCap, NumberType.FiatTokenDetails) const volume24hFormatted = convertFiatAmountFormatted(volume24h, NumberType.FiatTokenDetails) - const totalValueLockedFormatted = convertFiatAmountFormatted( - totalValueLocked, - NumberType.FiatTokenDetails - ) + const totalValueLockedFormatted = convertFiatAmountFormatted(totalValueLocked, NumberType.FiatTokenDetails) const getMetadataSubtitle = (): string | undefined => { switch (metadataDisplayType) { @@ -109,7 +102,8 @@ export const TokenItem = memo(function _TokenItem({ hapticStyle={ImpactFeedbackStyle.Light} testID={`token-item-${name}`} onLongPress={disableOnPress} - onPress={onPress}> + onPress={onPress} + > {index !== undefined && ( @@ -125,11 +119,7 @@ export const TokenItem = memo(function _TokenItem({ {name} - + {getMetadataSubtitle()} diff --git a/apps/mobile/src/components/explore/__snapshots__/FavoriteHeaderRow.test.tsx.snap b/apps/mobile/src/components/explore/__snapshots__/FavoriteHeaderRow.test.tsx.snap index d832a972764..5d6f98d78fc 100644 --- a/apps/mobile/src/components/explore/__snapshots__/FavoriteHeaderRow.test.tsx.snap +++ b/apps/mobile/src/components/explore/__snapshots__/FavoriteHeaderRow.test.tsx.snap @@ -67,7 +67,7 @@ exports[`FavoriteHeaderRow when editing renders without error 1`] = ` } } suppressHighlighting={true} - testID="favorite-header-row/done-button" + testID="done" > Done @@ -128,7 +128,7 @@ exports[`FavoriteHeaderRow when not editing renders without error 1`] = ` ], } } - testID="favorite-header-row/favorite-button" + testID="edit" > - diff --git a/apps/mobile/src/components/explore/hooks.test.ts b/apps/mobile/src/components/explore/hooks.test.ts index 20c366e7d3f..96e42dc0d5f 100644 --- a/apps/mobile/src/components/explore/hooks.test.ts +++ b/apps/mobile/src/components/explore/hooks.test.ts @@ -5,9 +5,9 @@ import { useExploreTokenContextMenu } from 'src/components/explore/hooks' import { renderHookWithProviders } from 'src/test/render' import { Resolvers } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { SectionName } from 'uniswap/src/features/telemetry/constants' +import { CurrencyField } from 'uniswap/src/features/transactions/transactionState/types' import { FavoritesState } from 'wallet/src/features/favorites/slice' -import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' -import { SAMPLE_SEED_ADDRESS_1 } from 'wallet/src/test/fixtures/constants' +import { SAMPLE_SEED_ADDRESS_1 } from 'wallet/src/test/fixtures' import { cleanup } from 'wallet/src/test/test-utils' const tokenId = SAMPLE_SEED_ADDRESS_1 @@ -30,10 +30,7 @@ describe(useExploreTokenContextMenu, () => { describe('editing favorite tokens', () => { it('renders proper context menu items when onEditFavorites is not provided', async () => { - const { result } = renderHookWithProviders( - () => useExploreTokenContextMenu(tokenMenuParams), - { resolvers } - ) + const { result } = renderHookWithProviders(() => useExploreTokenContextMenu(tokenMenuParams), { resolvers }) expect(result.current.menuActions).toEqual([ expect.objectContaining({ @@ -60,7 +57,7 @@ describe(useExploreTokenContextMenu, () => { const onEditFavorites = jest.fn() const { result } = renderHookWithProviders( () => useExploreTokenContextMenu({ ...tokenMenuParams, onEditFavorites }), - { resolvers } + { resolvers }, ) expect(result.current.menuActions).toEqual([ @@ -88,11 +85,11 @@ describe(useExploreTokenContextMenu, () => { const onEditFavorites = jest.fn() const { result } = renderHookWithProviders( () => useExploreTokenContextMenu({ ...tokenMenuParams, onEditFavorites }), - { resolvers } + { resolvers }, ) const editFavoritesActionIndex = result.current.menuActions.findIndex( - (action: ContextMenuAction) => action.title === 'Edit favorites' + (action: ContextMenuAction) => action.title === 'Edit favorites', ) result.current.onContextMenuPress({ nativeEvent: { index: editFavoritesActionIndex }, @@ -105,15 +102,12 @@ describe(useExploreTokenContextMenu, () => { describe('adding / removing favorite tokens', () => { it('renders proper context menu items when token is favorited', async () => { - const { result } = renderHookWithProviders( - () => useExploreTokenContextMenu(tokenMenuParams), - { - preloadedState: { - favorites: { tokens: [tokenMenuParams.currencyId.toLowerCase()] } as FavoritesState, - }, - resolvers, - } - ) + const { result } = renderHookWithProviders(() => useExploreTokenContextMenu(tokenMenuParams), { + preloadedState: { + favorites: { tokens: [tokenMenuParams.currencyId.toLowerCase()] } as FavoritesState, + }, + resolvers, + }) expect(result.current.menuActions).toEqual([ expect.objectContaining({ @@ -138,13 +132,13 @@ describe(useExploreTokenContextMenu, () => { it("dispatches add to favorites redux action when 'Favorite token' is pressed", async () => { const store = mockStore({ favorites: { tokens: [] }, appearance: { theme: 'system' } }) - const { result } = renderHookWithProviders( - () => useExploreTokenContextMenu(tokenMenuParams), - { resolvers, store } - ) + const { result } = renderHookWithProviders(() => useExploreTokenContextMenu(tokenMenuParams), { + resolvers, + store, + }) const favoriteTokenActionIndex = result.current.menuActions.findIndex( - (action: ContextMenuAction) => action.title === 'Favorite token' + (action: ContextMenuAction) => action.title === 'Favorite token', ) result.current.onContextMenuPress({ nativeEvent: { index: favoriteTokenActionIndex }, @@ -165,13 +159,13 @@ describe(useExploreTokenContextMenu, () => { favorites: { tokens: [tokenMenuParams.currencyId.toLowerCase()] }, appearance: { theme: 'system' }, }) - const { result } = renderHookWithProviders( - () => useExploreTokenContextMenu(tokenMenuParams), - { resolvers, store } - ) + const { result } = renderHookWithProviders(() => useExploreTokenContextMenu(tokenMenuParams), { + resolvers, + store, + }) const removeFavoriteTokenActionIndex = result.current.menuActions.findIndex( - (action: ContextMenuAction) => action.title === 'Remove favorite' + (action: ContextMenuAction) => action.title === 'Remove favorite', ) result.current.onContextMenuPress({ nativeEvent: { index: removeFavoriteTokenActionIndex }, @@ -198,9 +192,7 @@ describe(useExploreTokenContextMenu, () => { resolvers, }) - const swapActionIndex = result.current.menuActions.findIndex( - (action: ContextMenuAction) => action.title === 'Swap' - ) + const swapActionIndex = result.current.menuActions.findIndex((action: ContextMenuAction) => action.title === 'Swap') result.current.onContextMenuPress({ nativeEvent: { index: swapActionIndex }, } as NativeSyntheticEvent) @@ -235,7 +227,7 @@ describe(useExploreTokenContextMenu, () => { jest.spyOn(Share, 'share') const shareActionIndex = result.current.menuActions.findIndex( - (action: ContextMenuAction) => action.title === 'Share' + (action: ContextMenuAction) => action.title === 'Share', ) result.current.onContextMenuPress({ nativeEvent: { index: shareActionIndex }, diff --git a/apps/mobile/src/components/explore/hooks.ts b/apps/mobile/src/components/explore/hooks.ts index b1c2950d7ec..e422a93953f 100644 --- a/apps/mobile/src/components/explore/hooks.ts +++ b/apps/mobile/src/components/explore/hooks.ts @@ -4,21 +4,18 @@ import { useTranslation } from 'react-i18next' import { NativeSyntheticEvent } from 'react-native' import { ContextMenuAction, ContextMenuOnPressNativeEvent } from 'react-native-context-menu-view' import { SharedValue, StyleProps, interpolate, useAnimatedStyle } from 'react-native-reanimated' +import { useDispatch } from 'react-redux' import { useSelectHasTokenFavorited, useToggleFavoriteCallback } from 'src/features/favorites/hooks' import { openModal } from 'src/features/modals/modalSlice' +import { AssetType } from 'uniswap/src/entities/assets' import { ElementName, ModalName, SectionNameType } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' +import { CurrencyField, TransactionState } from 'uniswap/src/features/transactions/transactionState/types' import { WalletChainId } from 'uniswap/src/types/chains' import { CurrencyId } from 'uniswap/src/types/currency' +import { currencyIdToAddress } from 'uniswap/src/utils/currencyId' import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants' import { useWalletNavigation } from 'wallet/src/contexts/WalletNavigationContext' -import { AssetType } from 'wallet/src/entities/assets' -import { - CurrencyField, - TransactionState, -} from 'wallet/src/features/transactions/transactionState/types' -import { useAppDispatch } from 'wallet/src/state' -import { currencyIdToAddress } from 'wallet/src/utils/currencyId' interface TokenMenuParams { currencyId: CurrencyId @@ -40,7 +37,7 @@ export function useExploreTokenContextMenu({ } { const { t } = useTranslation() const isFavorited = useSelectHasTokenFavorited(currencyId) - const dispatch = useAppDispatch() + const dispatch = useDispatch() const { handleShareToken } = useWalletNavigation() @@ -49,11 +46,8 @@ export function useExploreTokenContextMenu({ const currencyAddress = currencyIdToAddress(currencyId) const onPressReceive = useCallback( - () => - dispatch( - openModal({ name: ModalName.WalletConnectScan, initialState: ScannerModalState.WalletQr }) - ), - [dispatch] + () => dispatch(openModal({ name: ModalName.WalletConnectScan, initialState: ScannerModalState.WalletQr })), + [dispatch], ) const onPressShare = useCallback(async () => { @@ -87,9 +81,7 @@ export function useExploreTokenContextMenu({ const menuActions = useMemo( () => [ { - title: isFavorited - ? t('explore.tokens.favorite.action.remove') - : t('explore.tokens.favorite.action.add'), + title: isFavorited ? t('explore.tokens.favorite.action.remove') : t('explore.tokens.favorite.action.add'), systemIcon: isFavorited ? 'heart.fill' : 'heart', onPress: onPressToggleFavorite, }, @@ -118,22 +110,14 @@ export function useExploreTokenContextMenu({ ] : []), ], - [ - isFavorited, - t, - onPressToggleFavorite, - onEditFavorites, - onPressSwap, - onPressReceive, - onPressShare, - ] + [isFavorited, t, onPressToggleFavorite, onEditFavorites, onPressSwap, onPressReceive, onPressShare], ) const onContextMenuPress = useCallback( async (e: NativeSyntheticEvent): Promise => { await menuActions[e.nativeEvent.index]?.onPress?.() }, - [menuActions] + [menuActions], ) return { menuActions, onContextMenuPress } @@ -141,7 +125,7 @@ export function useExploreTokenContextMenu({ export function useAnimatedCardDragStyle( pressProgress: SharedValue, - dragActivationProgress: SharedValue + dragActivationProgress: SharedValue, ): StyleProps { return useAnimatedStyle(() => ({ opacity: diff --git a/apps/mobile/src/components/explore/search/SearchEmptySection.tsx b/apps/mobile/src/components/explore/search/SearchEmptySection.tsx index c2784b58b42..35874264ebe 100644 --- a/apps/mobile/src/components/explore/search/SearchEmptySection.tsx +++ b/apps/mobile/src/components/explore/search/SearchEmptySection.tsx @@ -2,7 +2,8 @@ import React from 'react' import { useTranslation } from 'react-i18next' import { FlatList } from 'react-native' import { FadeIn, FadeOut } from 'react-native-reanimated' -import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { useDispatch } from 'react-redux' +import { useAppSelector } from 'src/app/hooks' import { SearchPopularNFTCollections } from 'src/components/explore/search/SearchPopularNFTCollections' import { SearchPopularTokens } from 'src/components/explore/search/SearchPopularTokens' import { renderSearchItem } from 'src/components/explore/search/SearchResultsSection' @@ -12,7 +13,8 @@ import ClockIcon from 'ui/src/assets/icons/clock.svg' import { TrendUp } from 'ui/src/components/icons' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { iconSizes } from 'ui/src/theme' -import { SearchResultType, WalletSearchResult } from 'wallet/src/features/search/SearchResult' +import { SearchResultType } from 'uniswap/src/features/search/SearchResult' +import { WalletSearchResult } from 'wallet/src/features/search/SearchResult' import { clearSearchHistory } from 'wallet/src/features/search/searchHistorySlice' import { selectSearchHistory } from 'wallet/src/features/search/selectSearchHistory' @@ -33,7 +35,7 @@ export const SUGGESTED_WALLETS: WalletSearchResult[] = [ export function SearchEmptySection(): JSX.Element { const { t } = useTranslation() - const dispatch = useAppDispatch() + const dispatch = useDispatch() const searchHistory = useAppSelector(selectSearchHistory) const onPressClearSearchHistory = (): void => { @@ -47,16 +49,8 @@ export function SearchEmptySection(): JSX.Element { - } - title={t('explore.search.section.recent')} - /> + + } title={t('explore.search.section.recent')} /> {t('explore.search.action.clear')} @@ -81,10 +75,7 @@ export function SearchEmptySection(): JSX.Element { + } data={SUGGESTED_WALLETS} keyExtractor={walletKey} @@ -100,7 +91,5 @@ const walletKey = (wallet: WalletSearchResult): string => { export const RecentIcon = (): JSX.Element => { const colors = useSporeColors() - return ( - - ) + return } diff --git a/apps/mobile/src/components/explore/search/SearchPopularNFTCollections.tsx b/apps/mobile/src/components/explore/search/SearchPopularNFTCollections.tsx index 2428f98703a..4a78b2c2531 100644 --- a/apps/mobile/src/components/explore/search/SearchPopularNFTCollections.tsx +++ b/apps/mobile/src/components/explore/search/SearchPopularNFTCollections.tsx @@ -1,20 +1,13 @@ import React, { useMemo } from 'react' import { FlatList, ListRenderItemInfo } from 'react-native' import { SearchNFTCollectionItem } from 'src/components/explore/search/items/SearchNFTCollectionItem' -import { - getSearchResultId, - gqlNFTToNFTCollectionSearchResult, -} from 'src/components/explore/search/utils' -import { Inset, Loader } from 'ui/src' +import { getSearchResultId, gqlNFTToNFTCollectionSearchResult } from 'src/components/explore/search/utils' +import { Flex, Loader } from 'ui/src' import { useSearchPopularNftCollectionsQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { - NFTCollectionSearchResult, - SearchResultType, -} from 'wallet/src/features/search/SearchResult' +import { SearchResultType } from 'uniswap/src/features/search/SearchResult' +import { NFTCollectionSearchResult } from 'wallet/src/features/search/SearchResult' -function isNFTCollectionSearchResult( - result: NFTCollectionSearchResult | null -): result is NFTCollectionSearchResult { +function isNFTCollectionSearchResult(result: NFTCollectionSearchResult | null): result is NFTCollectionSearchResult { return (result as NFTCollectionSearchResult).type === SearchResultType.NFTCollection } @@ -27,31 +20,21 @@ export function SearchPopularNFTCollections(): JSX.Element { return } - const searchResults = data.topCollections.edges.map(({ node }) => - gqlNFTToNFTCollectionSearchResult(node) - ) + const searchResults = data.topCollections.edges.map(({ node }) => gqlNFTToNFTCollectionSearchResult(node)) return searchResults.filter(isNFTCollectionSearchResult) }, [data]) if (loading) { return ( - + - + ) } - return ( - - ) + return } -const renderNFTCollectionItem = ({ - item, -}: ListRenderItemInfo): JSX.Element => ( +const renderNFTCollectionItem = ({ item }: ListRenderItemInfo): JSX.Element => ( ) diff --git a/apps/mobile/src/components/explore/search/SearchPopularTokens.tsx b/apps/mobile/src/components/explore/search/SearchPopularTokens.tsx index 9701e286518..58ba3f57a92 100644 --- a/apps/mobile/src/components/explore/search/SearchPopularTokens.tsx +++ b/apps/mobile/src/components/explore/search/SearchPopularTokens.tsx @@ -2,9 +2,10 @@ import React, { useMemo } from 'react' import { FlatList, ListRenderItemInfo } from 'react-native' import { SearchTokenItem } from 'src/components/explore/search/items/SearchTokenItem' import { getSearchResultId } from 'src/components/explore/search/utils' -import { Inset, Loader } from 'ui/src' -import { fromGraphQLChain } from 'wallet/src/features/chains/utils' -import { SearchResultType, TokenSearchResult } from 'wallet/src/features/search/SearchResult' +import { Flex, Loader } from 'ui/src' +import { fromGraphQLChain } from 'uniswap/src/features/chains/utils' +import { SearchResultType } from 'uniswap/src/features/search/SearchResult' +import { TokenSearchResult } from 'wallet/src/features/search/SearchResult' import { TopToken, usePopularTokens } from 'wallet/src/features/tokens/hooks' function gqlTokenToTokenSearchResult(token: Maybe): TokenSearchResult | null { @@ -33,18 +34,15 @@ function gqlTokenToTokenSearchResult(token: Maybe): TokenSearchResult export function SearchPopularTokens(): JSX.Element { const { popularTokens, loading } = usePopularTokens() const tokens = useMemo( - () => - popularTokens - ?.map(gqlTokenToTokenSearchResult) - .filter((t): t is TokenSearchResult => Boolean(t)), - [popularTokens] + () => popularTokens?.map(gqlTokenToTokenSearchResult).filter((t): t is TokenSearchResult => Boolean(t)), + [popularTokens], ) if (loading) { return ( - + - + ) } diff --git a/apps/mobile/src/components/explore/search/SearchResultsLoader.tsx b/apps/mobile/src/components/explore/search/SearchResultsLoader.tsx index b69014155da..544f856694e 100644 --- a/apps/mobile/src/components/explore/search/SearchResultsLoader.tsx +++ b/apps/mobile/src/components/explore/search/SearchResultsLoader.tsx @@ -15,7 +15,7 @@ export const SearchResultsLoader = (): JSX.Element => { icon={} title={t('explore.search.section.tokens')} /> - + @@ -24,7 +24,7 @@ export const SearchResultsLoader = (): JSX.Element => { icon={} title={t('explore.search.section.nft')} /> - + @@ -33,7 +33,7 @@ export const SearchResultsLoader = (): JSX.Element => { icon={} title={t('explore.search.section.wallets')} /> - + diff --git a/apps/mobile/src/components/explore/search/SearchResultsSection.tsx b/apps/mobile/src/components/explore/search/SearchResultsSection.tsx index 1421b99fa41..506a85991af 100644 --- a/apps/mobile/src/components/explore/search/SearchResultsSection.tsx +++ b/apps/mobile/src/components/explore/search/SearchResultsSection.tsx @@ -24,16 +24,13 @@ import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard' import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' import { useExploreSearchQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { SearchContext } from 'uniswap/src/features/search/SearchContext' +import { SearchResultType } from 'uniswap/src/features/search/SearchResult' import i18n from 'uniswap/src/i18n/i18n' import { UniverseChainId } from 'uniswap/src/types/chains' +import { getValidAddress } from 'uniswap/src/utils/addresses' import { logger } from 'utilities/src/logger/logger' -import { SearchContext } from 'wallet/src/features/search/SearchContext' -import { - NFTCollectionSearchResult, - SearchResultType, - TokenSearchResult, -} from 'wallet/src/features/search/SearchResult' -import { getValidAddress } from 'wallet/src/utils/addresses' +import { NFTCollectionSearchResult, TokenSearchResult } from 'wallet/src/features/search/SearchResult' const ICON_SIZE = '$icon.24' const ICON_COLOR = '$neutral2' @@ -108,7 +105,7 @@ export function SearchResultsSection({ searchQuery }: { searchQuery: string }): const validAddress: Address | undefined = useMemo( () => getValidAddress(searchQuery, true, false) ?? undefined, - [searchQuery] + [searchQuery], ) const countTokenResults = tokenResults?.length ?? 0 @@ -116,9 +113,7 @@ export function SearchResultsSection({ searchQuery }: { searchQuery: string }): const countWalletResults = walletSearchResults.length const countTotalResults = countTokenResults + countNftCollectionResults + countWalletResults - const prefixTokenMatch = tokenResults?.find((res: TokenSearchResult) => - isPrefixTokenMatch(res, searchQuery) - ) + const prefixTokenMatch = tokenResults?.find((res: TokenSearchResult) => isPrefixTokenMatch(res, searchQuery)) const hasVerifiedNFTResults = Boolean(nftCollectionResults?.some((res) => res.isVerified)) @@ -126,18 +121,14 @@ export function SearchResultsSection({ searchQuery }: { searchQuery: string }): return searchQuery.includes('.') }, [searchQuery]) - const showWalletSectionFirst = - isUsernameSearch && (exactUnitagMatch || (exactENSMatch && !prefixTokenMatch)) + const showWalletSectionFirst = isUsernameSearch && (exactUnitagMatch || (exactENSMatch && !prefixTokenMatch)) const showNftCollectionsBeforeTokens = hasVerifiedNFTResults && !tokenResults?.length const sortedSearchResults: SearchResultOrHeader[] = useMemo(() => { // Format results arrays with header, and handle empty results - const nftsWithHeader = nftCollectionResults?.length - ? [NFTHeaderItem, ...nftCollectionResults] - : [] + const nftsWithHeader = nftCollectionResults?.length ? [NFTHeaderItem, ...nftCollectionResults] : [] const tokensWithHeader = tokenResults?.length ? [TokenHeaderItem, ...tokenResults] : [] - const walletsWithHeader = - walletSearchResults.length > 0 ? [WalletHeaderItem, ...walletSearchResults] : [] + const walletsWithHeader = walletSearchResults.length > 0 ? [WalletHeaderItem, ...walletSearchResults] : [] let searchResultItems: SearchResultOrHeader[] = [] @@ -215,9 +206,8 @@ export function SearchResultsSection({ searchQuery }: { searchQuery: string }): ? undefined : props.index + 1 - - sortedSearchResults - .slice(0, props.index + 1) - .filter((item) => item.type === SEARCH_RESULT_HEADER_KEY).length + sortedSearchResults.slice(0, props.index + 1).filter((item) => item.type === SEARCH_RESULT_HEADER_KEY) + .length return renderSearchItem({ ...props, searchContext: { @@ -265,7 +255,7 @@ export const renderSearchItem = ({ logger.warn( 'SearchResultsSection', 'renderSearchItem', - `Found invalid list item in search results: ${JSON.stringify(searchResult)}` + `Found invalid list item in search results: ${JSON.stringify(searchResult)}`, ) return null } diff --git a/apps/mobile/src/components/explore/search/SearchSectionHeader.tsx b/apps/mobile/src/components/explore/search/SearchSectionHeader.tsx index b6f4fe2fbb5..6f537ca6e33 100644 --- a/apps/mobile/src/components/explore/search/SearchSectionHeader.tsx +++ b/apps/mobile/src/components/explore/search/SearchSectionHeader.tsx @@ -6,13 +6,9 @@ interface SectionHeaderTextProps { icon?: JSX.Element } -export const SectionHeaderText = ({ - title, - icon, - ...rest -}: SectionHeaderTextProps & TextProps): JSX.Element => { +export const SectionHeaderText = ({ title, icon, ...rest }: SectionHeaderTextProps & TextProps): JSX.Element => { return ( - + {icon && icon} {title} diff --git a/apps/mobile/src/components/explore/search/hooks.ts b/apps/mobile/src/components/explore/search/hooks.ts index 2706ee3e3be..46628095830 100644 --- a/apps/mobile/src/components/explore/search/hooks.ts +++ b/apps/mobile/src/components/explore/search/hooks.ts @@ -1,10 +1,11 @@ import { useMemo } from 'react' +import { SearchResultType } from 'uniswap/src/features/search/SearchResult' import { useUnitagByAddress, useUnitagByName } from 'uniswap/src/features/unitags/hooks' import { UniverseChainId } from 'uniswap/src/types/chains' +import { getValidAddress } from 'uniswap/src/utils/addresses' import { useENS } from 'wallet/src/features/ens/useENS' -import { SearchResultType, WalletSearchResult } from 'wallet/src/features/search/SearchResult' +import { WalletSearchResult } from 'wallet/src/features/search/SearchResult' import { useIsSmartContractAddress } from 'wallet/src/features/transactions/transfer/hooks/useIsSmartContractAddress' -import { getValidAddress } from 'wallet/src/utils/addresses' // eslint-disable-next-line complexity export function useWalletSearchResults(query: string): { @@ -13,10 +14,7 @@ export function useWalletSearchResults(query: string): { exactENSMatch: boolean exactUnitagMatch: boolean } { - const validAddress: Address | undefined = useMemo( - () => getValidAddress(query, true, false) ?? undefined, - [query] - ) + const validAddress: Address | undefined = useMemo(() => getValidAddress(query, true, false) ?? undefined, [query]) const querySkippedIfValidAddress = validAddress ? null : query @@ -38,12 +36,13 @@ export function useWalletSearchResults(query: string): { const { unitag: unitagByName, loading: unitagLoading } = useUnitagByName(query) // Search for matching Unitag by address - const { unitag: unitagByAddress, loading: unitagByAddressLoading } = - useUnitagByAddress(validAddress) + const { unitag: unitagByAddress, loading: unitagByAddressLoading } = useUnitagByAddress(validAddress) // Search for matching EOA wallet address - const { isSmartContractAddress, loading: loadingIsSmartContractAddress } = - useIsSmartContractAddress(validAddress, UniverseChainId.Mainnet) + const { isSmartContractAddress, loading: loadingIsSmartContractAddress } = useIsSmartContractAddress( + validAddress, + UniverseChainId.Mainnet, + ) const hasENSResult = dotEthName && dotEthAddress const hasEOAResult = validAddress && !isSmartContractAddress @@ -108,11 +107,7 @@ export function useWalletSearchResults(query: string): { // Ensure loading is returned const walletsLoading = - dotEthLoading || - ensLoading || - loadingIsSmartContractAddress || - unitagLoading || - unitagByAddressLoading + dotEthLoading || ensLoading || loadingIsSmartContractAddress || unitagLoading || unitagByAddressLoading return { loading: walletsLoading, diff --git a/apps/mobile/src/components/explore/search/items/SearchENSAddressItem.tsx b/apps/mobile/src/components/explore/search/items/SearchENSAddressItem.tsx index 8348dd92172..b96593250ea 100644 --- a/apps/mobile/src/components/explore/search/items/SearchENSAddressItem.tsx +++ b/apps/mobile/src/components/explore/search/items/SearchENSAddressItem.tsx @@ -3,22 +3,19 @@ import { useTranslation } from 'react-i18next' import { SearchWalletItemBase } from 'src/components/explore/search/items/SearchWalletItemBase' import { Flex, Text } from 'ui/src' import { imageSizes } from 'ui/src/theme' +import { SearchContext } from 'uniswap/src/features/search/SearchContext' +import { sanitizeAddressText, shortenAddress } from 'uniswap/src/utils/addresses' import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon' import { useENSAvatar, useENSName } from 'wallet/src/features/ens/api' import { getCompletedENSName } from 'wallet/src/features/ens/useENS' -import { SearchContext } from 'wallet/src/features/search/SearchContext' import { ENSAddressSearchResult } from 'wallet/src/features/search/SearchResult' -import { sanitizeAddressText, shortenAddress } from 'wallet/src/utils/addresses' type SearchENSAddressItemProps = { searchResult: ENSAddressSearchResult searchContext?: SearchContext } -export function SearchENSAddressItem({ - searchResult, - searchContext, -}: SearchENSAddressItemProps): JSX.Element { +export function SearchENSAddressItem({ searchResult, searchContext }: SearchENSAddressItemProps): JSX.Element { const { t } = useTranslation() // Use `savedPrimaryEnsName` for WalletSearchResults that are stored in the search history @@ -36,7 +33,7 @@ export function SearchENSAddressItem({ * is `uniswap.eth`, then we should show "uni.eth | owned by uniswap.eth" */ const { data: fetchedPrimaryENSName, loading: isFetchingPrimaryENSName } = useENSName( - savedPrimaryENSName ? undefined : address + savedPrimaryENSName ? undefined : address, ) const primaryENSName = savedPrimaryENSName ?? fetchedPrimaryENSName @@ -50,14 +47,10 @@ export function SearchENSAddressItem({ return ( - + - + {completedENSName || formattedAddress} {showSecondLine ? ( diff --git a/apps/mobile/src/components/explore/search/items/SearchEtherscanItem.tsx b/apps/mobile/src/components/explore/search/items/SearchEtherscanItem.tsx index ef3674db679..28b155634dc 100644 --- a/apps/mobile/src/components/explore/search/items/SearchEtherscanItem.tsx +++ b/apps/mobile/src/components/explore/search/items/SearchEtherscanItem.tsx @@ -1,15 +1,16 @@ import { default as React } from 'react' -import { useAppDispatch } from 'src/app/hooks' +import { useDispatch } from 'react-redux' import { getBlockExplorerIcon } from 'src/components/icons/BlockExplorerIcon' import { Flex, ImpactFeedbackStyle, Text, TouchableArea, useSporeColors } from 'ui/src' import { iconSizes } from 'ui/src/theme' -import { ElementName } from 'uniswap/src/features/telemetry/constants' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { UniverseChainId } from 'uniswap/src/types/chains' +import { shortenAddress } from 'uniswap/src/utils/addresses' +import { openUri } from 'uniswap/src/utils/linking' import { Arrow } from 'wallet/src/components/icons/Arrow' import { EtherscanSearchResult } from 'wallet/src/features/search/SearchResult' import { addToSearchHistory } from 'wallet/src/features/search/searchHistorySlice' -import { shortenAddress } from 'wallet/src/utils/addresses' -import { ExplorerDataType, getExplorerLink, openUri } from 'wallet/src/utils/linking' +import { ExplorerDataType, getExplorerLink } from 'wallet/src/utils/linking' type SearchEtherscanItemProps = { etherscanResult: EtherscanSearchResult @@ -17,7 +18,7 @@ type SearchEtherscanItemProps = { export function SearchEtherscanItem({ etherscanResult }: SearchEtherscanItemProps): JSX.Element { const colors = useSporeColors() - const dispatch = useAppDispatch() + const dispatch = useDispatch() const { address } = etherscanResult @@ -27,7 +28,7 @@ export function SearchEtherscanItem({ etherscanResult }: SearchEtherscanItemProp dispatch( addToSearchHistory({ searchResult: etherscanResult, - }) + }), ) } @@ -37,15 +38,10 @@ export function SearchEtherscanItem({ etherscanResult }: SearchEtherscanItemProp - + testID={TestID.SearchEtherscanItem} + onPress={onPressViewEtherscan} + > + {shortenAddress(address)} diff --git a/apps/mobile/src/components/explore/search/items/SearchNFTCollectionItem.tsx b/apps/mobile/src/components/explore/search/items/SearchNFTCollectionItem.tsx index 1a177387b64..8361107aefb 100644 --- a/apps/mobile/src/components/explore/search/items/SearchNFTCollectionItem.tsx +++ b/apps/mobile/src/components/explore/search/items/SearchNFTCollectionItem.tsx @@ -1,18 +1,17 @@ import { default as React } from 'react' -import { useAppDispatch } from 'src/app/hooks' +import { useDispatch } from 'react-redux' import { useAppStackNavigation } from 'src/app/navigation/types' import { Flex, ImpactFeedbackStyle, Text, TouchableArea } from 'ui/src' import { Verified } from 'ui/src/components/icons' import { iconSizes } from 'ui/src/theme' -import { ElementName, MobileEventName } from 'uniswap/src/features/telemetry/constants' +import { SearchContext } from 'uniswap/src/features/search/SearchContext' +import { SearchResultType } from 'uniswap/src/features/search/SearchResult' +import { MobileEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { MobileScreens } from 'uniswap/src/types/screens/mobile' import { NFTViewer } from 'wallet/src/features/images/NFTViewer' -import { SearchContext } from 'wallet/src/features/search/SearchContext' -import { - NFTCollectionSearchResult, - SearchResultType, -} from 'wallet/src/features/search/SearchResult' +import { NFTCollectionSearchResult } from 'wallet/src/features/search/SearchResult' import { addToSearchHistory } from 'wallet/src/features/search/searchHistorySlice' type NFTCollectionItemProps = { @@ -20,12 +19,9 @@ type NFTCollectionItemProps = { searchContext?: SearchContext } -export function SearchNFTCollectionItem({ - collection, - searchContext, -}: NFTCollectionItemProps): JSX.Element { +export function SearchNFTCollectionItem({ collection, searchContext }: NFTCollectionItemProps): JSX.Element { const { name, address, chainId, isVerified, imageUrl } = collection - const dispatch = useAppDispatch() + const dispatch = useDispatch() const navigation = useAppStackNavigation() const onPress = (): void => { @@ -56,7 +52,7 @@ export function SearchNFTCollectionItem({ imageUrl, isVerified, }, - }) + }), ) } @@ -64,22 +60,18 @@ export function SearchNFTCollectionItem({ - + testID={TestID.SearchNFTCollectionItem} + onPress={onPress} + > + + width={iconSizes.icon40} + > {imageUrl ? ( ) : ( diff --git a/apps/mobile/src/components/explore/search/items/SearchTokenItem.tsx b/apps/mobile/src/components/explore/search/items/SearchTokenItem.tsx index 5bd969e104f..48147bfd2db 100644 --- a/apps/mobile/src/components/explore/search/items/SearchTokenItem.tsx +++ b/apps/mobile/src/components/explore/search/items/SearchTokenItem.tsx @@ -1,20 +1,22 @@ import { default as React } from 'react' import ContextMenu from 'react-native-context-menu-view' -import { useAppDispatch } from 'src/app/hooks' +import { useDispatch } from 'react-redux' import { useTokenDetailsNavigation } from 'src/components/TokenDetails/hooks' import { useExploreTokenContextMenu } from 'src/components/explore/hooks' import { disableOnPress } from 'src/utils/disableOnPress' import { Flex, ImpactFeedbackStyle, Text, TouchableArea, useIsDarkMode } from 'ui/src' import { TokenLogo } from 'uniswap/src/components/CurrencyLogo/TokenLogo' +import WarningIcon from 'uniswap/src/components/icons/WarningIcon' import { SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { ElementName, MobileEventName, SectionName } from 'uniswap/src/features/telemetry/constants' +import { SearchContext } from 'uniswap/src/features/search/SearchContext' +import { SearchResultType } from 'uniswap/src/features/search/SearchResult' +import { MobileEventName, SectionName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' -import WarningIcon from 'wallet/src/components/icons/WarningIcon' -import { SearchContext } from 'wallet/src/features/search/SearchContext' -import { SearchResultType, TokenSearchResult } from 'wallet/src/features/search/SearchResult' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' +import { shortenAddress } from 'uniswap/src/utils/addresses' +import { buildCurrencyId, buildNativeCurrencyId } from 'uniswap/src/utils/currencyId' +import { TokenSearchResult } from 'wallet/src/features/search/SearchResult' import { addToSearchHistory } from 'wallet/src/features/search/searchHistorySlice' -import { shortenAddress } from 'wallet/src/utils/addresses' -import { buildCurrencyId, buildNativeCurrencyId } from 'wallet/src/utils/currencyId' type SearchTokenItemProps = { token: TokenSearchResult @@ -23,7 +25,7 @@ type SearchTokenItemProps = { export function SearchTokenItem({ token, searchContext }: SearchTokenItemProps): JSX.Element { const isDarkMode = useIsDarkMode() - const dispatch = useAppDispatch() + const dispatch = useDispatch() const tokenDetailsNavigation = useTokenDetailsNavigation() const { chainId, address, name, symbol, logoUrl, safetyLevel } = token @@ -55,7 +57,7 @@ export function SearchTokenItem({ token, searchContext }: SearchTokenItemProps): logoUrl, safetyLevel, }, - }) + }), ) } @@ -70,10 +72,11 @@ export function SearchTokenItem({ token, searchContext }: SearchTokenItemProps): - + onPress={onPress} + > + @@ -82,13 +85,8 @@ export function SearchTokenItem({ token, searchContext }: SearchTokenItemProps): {name} - {(safetyLevel === SafetyLevel.Blocked || - safetyLevel === SafetyLevel.StrongWarning) && ( - + {(safetyLevel === SafetyLevel.Blocked || safetyLevel === SafetyLevel.StrongWarning) && ( + )} @@ -97,10 +95,7 @@ export function SearchTokenItem({ token, searchContext }: SearchTokenItemProps): {address && ( - + {shortenAddress(address)} diff --git a/apps/mobile/src/components/explore/search/items/SearchUnitagItem.tsx b/apps/mobile/src/components/explore/search/items/SearchUnitagItem.tsx index 4549e7761e6..d8331ff4d13 100644 --- a/apps/mobile/src/components/explore/search/items/SearchUnitagItem.tsx +++ b/apps/mobile/src/components/explore/search/items/SearchUnitagItem.tsx @@ -2,23 +2,20 @@ import React from 'react' import { SearchWalletItemBase } from 'src/components/explore/search/items/SearchWalletItemBase' import { Flex, Text } from 'ui/src' import { imageSizes } from 'ui/src/theme' +import { SearchContext } from 'uniswap/src/features/search/SearchContext' +import { sanitizeAddressText, shortenAddress } from 'uniswap/src/utils/addresses' import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon' import { DisplayNameText } from 'wallet/src/components/accounts/DisplayNameText' -import { SearchContext } from 'wallet/src/features/search/SearchContext' import { UnitagSearchResult } from 'wallet/src/features/search/SearchResult' import { useAvatar } from 'wallet/src/features/wallet/hooks' import { DisplayNameType } from 'wallet/src/features/wallet/types' -import { sanitizeAddressText, shortenAddress } from 'wallet/src/utils/addresses' type SearchUnitagItemProps = { searchResult: UnitagSearchResult searchContext?: SearchContext } -export function SearchUnitagItem({ - searchResult, - searchContext, -}: SearchUnitagItemProps): JSX.Element { +export function SearchUnitagItem({ searchResult, searchContext }: SearchUnitagItemProps): JSX.Element { const { address, unitag } = searchResult const { avatar } = useAvatar(address) @@ -26,14 +23,10 @@ export function SearchUnitagItem({ return ( - + - + {sanitizeAddressText(shortenAddress(address))} diff --git a/apps/mobile/src/components/explore/search/items/SearchWalletByAddressItem.tsx b/apps/mobile/src/components/explore/search/items/SearchWalletByAddressItem.tsx index 5659c4b3fdf..dceb54b26c1 100644 --- a/apps/mobile/src/components/explore/search/items/SearchWalletByAddressItem.tsx +++ b/apps/mobile/src/components/explore/search/items/SearchWalletByAddressItem.tsx @@ -2,11 +2,11 @@ import React from 'react' import { SearchWalletItemBase } from 'src/components/explore/search/items/SearchWalletItemBase' import { Flex, Text } from 'ui/src' import { imageSizes } from 'ui/src/theme' +import { SearchContext } from 'uniswap/src/features/search/SearchContext' +import { sanitizeAddressText, shortenAddress } from 'uniswap/src/utils/addresses' import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon' import { useENSAvatar, useENSName } from 'wallet/src/features/ens/api' -import { SearchContext } from 'wallet/src/features/search/SearchContext' import { WalletByAddressSearchResult } from 'wallet/src/features/search/SearchResult' -import { sanitizeAddressText, shortenAddress } from 'wallet/src/utils/addresses' type SearchWalletByAddressItemProps = { searchResult: WalletByAddressSearchResult @@ -24,14 +24,10 @@ export function SearchWalletByAddressItem({ return ( - + - + {ensName || formattedAddress} {ensName ? ( diff --git a/apps/mobile/src/components/explore/search/items/SearchWalletItemBase.tsx b/apps/mobile/src/components/explore/search/items/SearchWalletItemBase.tsx index c6a0a3465b1..ead2cf54655 100644 --- a/apps/mobile/src/components/explore/search/items/SearchWalletItemBase.tsx +++ b/apps/mobile/src/components/explore/search/items/SearchWalletItemBase.tsx @@ -1,20 +1,18 @@ import React, { PropsWithChildren, useMemo } from 'react' import { useTranslation } from 'react-i18next' import ContextMenu from 'react-native-context-menu-view' -import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { useDispatch } from 'react-redux' +import { useAppSelector } from 'src/app/hooks' import { useEagerExternalProfileNavigation } from 'src/app/navigation/hooks' import { useToggleWatchedWalletCallback } from 'src/features/favorites/hooks' import { disableOnPress } from 'src/utils/disableOnPress' import { ImpactFeedbackStyle, TouchableArea } from 'ui/src' +import { SearchContext } from 'uniswap/src/features/search/SearchContext' +import { SearchResultType } from 'uniswap/src/features/search/SearchResult' import { MobileEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { selectWatchedAddressSet } from 'wallet/src/features/favorites/selectors' -import { SearchContext } from 'wallet/src/features/search/SearchContext' -import { - SearchResultType, - WalletSearchResult, - extractDomain, -} from 'wallet/src/features/search/SearchResult' +import { WalletSearchResult, extractDomain } from 'wallet/src/features/search/SearchResult' import { addToSearchHistory } from 'wallet/src/features/search/searchHistorySlice' type SearchWalletItemBaseProps = { @@ -28,7 +26,7 @@ export function SearchWalletItemBase({ searchContext, }: PropsWithChildren): JSX.Element { const { t } = useTranslation() - const dispatch = useAppDispatch() + const dispatch = useDispatch() const { preload, navigate } = useEagerExternalProfileNavigation() const { address, type } = searchResult const isFavorited = useAppSelector(selectWatchedAddressSet).has(address) @@ -40,8 +38,8 @@ export function SearchWalletItemBase({ type === SearchResultType.Unitag ? searchResult.unitag : type === SearchResultType.ENSAddress - ? searchResult.ensName - : undefined + ? searchResult.ensName + : undefined sendAnalyticsEvent(MobileEventName.ExploreSearchResultClicked, { query: searchContext.query, name: walletName, @@ -61,13 +59,13 @@ export function SearchWalletItemBase({ ...searchResult, primaryENSName: searchResult.primaryENSName, }, - }) + }), ) } else { dispatch( addToSearchHistory({ searchResult, - }) + }), ) } } @@ -90,7 +88,8 @@ export function SearchWalletItemBase({ onPress={onPress} onPressIn={async (): Promise => { await preload(address) - }}> + }} + > {children} diff --git a/apps/mobile/src/components/explore/search/utils.test.ts b/apps/mobile/src/components/explore/search/utils.test.ts index 623b26be059..675f5456f15 100644 --- a/apps/mobile/src/components/explore/search/utils.test.ts +++ b/apps/mobile/src/components/explore/search/utils.test.ts @@ -4,12 +4,9 @@ import { formatTokenSearchResults, gqlNFTToNFTCollectionSearchResult, } from 'src/components/explore/search/utils' -import { - Chain, - ExploreSearchQuery, -} from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { fromGraphQLChain } from 'wallet/src/features/chains/utils' -import { SearchResultType } from 'wallet/src/features/search/SearchResult' +import { Chain, ExploreSearchQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { fromGraphQLChain } from 'uniswap/src/features/chains/utils' +import { SearchResultType } from 'uniswap/src/features/search/SearchResult' import { amount, ethToken, @@ -99,9 +96,7 @@ describe(formatTokenSearchResults, () => { it('returns null if required data is missing', () => { expect(gqlNFTToNFTCollectionSearchResult({ ...collection, name: undefined })).toEqual(null) - expect(gqlNFTToNFTCollectionSearchResult({ ...collection, nftContracts: undefined })).toEqual( - null - ) + expect(gqlNFTToNFTCollectionSearchResult({ ...collection, nftContracts: undefined })).toEqual(null) expect(gqlNFTToNFTCollectionSearchResult({ ...collection, nftContracts: [] })).toEqual(null) }) @@ -125,10 +120,7 @@ describe(formatTokenSearchResults, () => { it('filters out nfts that cannot be formatted', () => { const topNFTCollections = createArray(2, nftCollection) const nftSearchResult = { - edges: [ - ...topNFTCollections.map((nft) => ({ node: nft })), - { node: nftCollection({ name: undefined }) }, - ], + edges: [...topNFTCollections.map((nft) => ({ node: nft })), { node: nftCollection({ name: undefined }) }], } const result = formatNFTCollectionSearchResults(nftSearchResult) diff --git a/apps/mobile/src/components/explore/search/utils.ts b/apps/mobile/src/components/explore/search/utils.ts index 1e54e0c221f..eb7760d6a6f 100644 --- a/apps/mobile/src/components/explore/search/utils.ts +++ b/apps/mobile/src/components/explore/search/utils.ts @@ -1,15 +1,9 @@ import { SEARCH_RESULT_HEADER_KEY } from 'src/components/explore/search/constants' import { SearchResultOrHeader } from 'src/components/explore/search/types' -import { - Chain, - ExploreSearchQuery, -} from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { fromGraphQLChain } from 'wallet/src/features/chains/utils' -import { - NFTCollectionSearchResult, - SearchResultType, - TokenSearchResult, -} from 'wallet/src/features/search/SearchResult' +import { Chain, ExploreSearchQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { fromGraphQLChain } from 'uniswap/src/features/chains/utils' +import { SearchResultType } from 'uniswap/src/features/search/SearchResult' +import { NFTCollectionSearchResult, TokenSearchResult } from 'wallet/src/features/search/SearchResult' import { searchResultId } from 'wallet/src/features/search/searchHistorySlice' const MAX_TOKEN_RESULTS_COUNT = 4 @@ -19,7 +13,7 @@ type ExploreSearchResult = NonNullable // Formats the tokens portion of explore search results into sorted array of TokenSearchResult export function formatTokenSearchResults( data: ExploreSearchResult['searchTokens'], - searchQuery: string + searchQuery: string, ): TokenSearchResult[] | undefined { if (!data) { return @@ -28,41 +22,38 @@ export function formatTokenSearchResults( // Prevent showing "duplicate" token search results for tokens that are on multiple chains // and share the same TokenProject id. Only show the token that has the highest 1Y Uniswap trading volume // ex. UNI on Mainnet, Arbitrum, Optimism -> only show UNI on Mainnet b/c it has highest 1Y volume - const tokenResultsMap = data.reduce>( - (tokensMap, token) => { - if (!token) { - return tokensMap - } + const tokenResultsMap = data.reduce>((tokensMap, token) => { + if (!token) { + return tokensMap + } - const { chain, address, symbol, project, market } = token - const chainId = fromGraphQLChain(chain) + const { chain, address, symbol, project, market } = token + const chainId = fromGraphQLChain(chain) - if (!chainId || !project) { - return tokensMap - } + if (!chainId || !project) { + return tokensMap + } - const { name, safetyLevel, logoUrl } = project - - const tokenResult: TokenSearchResult & { volume1D: number } = { - type: SearchResultType.Token, - chainId, - address: address ?? null, - name: name ?? null, - symbol: symbol ?? '', - safetyLevel: safetyLevel ?? null, - logoUrl: logoUrl ?? null, - volume1D: market?.volume?.value ?? 0, - } + const { name, safetyLevel, logoUrl } = project - // For token results that share the same TokenProject id, use the token with highest volume - const currentTokenResult = tokensMap[project.id] - if (!currentTokenResult || tokenResult.volume1D > currentTokenResult.volume1D) { - tokensMap[project.id] = tokenResult - } - return tokensMap - }, - {} - ) + const tokenResult: TokenSearchResult & { volume1D: number } = { + type: SearchResultType.Token, + chainId, + address: address ?? null, + name: name ?? null, + symbol: symbol ?? '', + safetyLevel: safetyLevel ?? null, + logoUrl: logoUrl ?? null, + volume1D: market?.volume?.value ?? 0, + } + + // For token results that share the same TokenProject id, use the token with highest volume + const currentTokenResult = tokensMap[project.id] + if (!currentTokenResult || tokenResult.volume1D > currentTokenResult.volume1D) { + tokensMap[project.id] = tokenResult + } + return tokensMap + }, {}) return Object.values(tokenResultsMap) .slice(0, MAX_TOKEN_RESULTS_COUNT) @@ -88,7 +79,7 @@ function isExactTokenSearchResultMatch(searchResult: TokenSearchResult, query: s } export function formatNFTCollectionSearchResults( - data: ExploreSearchResult['nftCollections'] + data: ExploreSearchResult['nftCollections'], ): NFTCollectionSearchResult[] | undefined { if (!data) { return @@ -107,9 +98,7 @@ type NFTCollectionItemResult = NonNullable< NonNullable>['edges']>[0] >['node'] -export const gqlNFTToNFTCollectionSearchResult = ( - node: NFTCollectionItemResult -): NFTCollectionSearchResult | null => { +export const gqlNFTToNFTCollectionSearchResult = (node: NFTCollectionItemResult): NFTCollectionSearchResult | null => { const contract = node?.nftContracts?.[0] // Only show NFT results that have fully populated results const chainId = fromGraphQLChain(contract?.chain ?? Chain.Ethereum) diff --git a/apps/mobile/src/components/fiatOnRamp/CtaButton.tsx b/apps/mobile/src/components/fiatOnRamp/CtaButton.tsx index 63453f93bc1..af9601bc38b 100644 --- a/apps/mobile/src/components/fiatOnRamp/CtaButton.tsx +++ b/apps/mobile/src/components/fiatOnRamp/CtaButton.tsx @@ -34,7 +34,8 @@ export function FiatOnRampCtaButton({ } size="large" theme={buttonAvailable ? 'primary' : 'tertiary'} - onPress={onPress}> + onPress={onPress} + > {!isLoading && continueText} ) diff --git a/apps/mobile/src/components/forceUpgrade/ForceUpgradeModal.tsx b/apps/mobile/src/components/forceUpgrade/ForceUpgradeModal.tsx index 8a17dfbfb3f..8a1f069873e 100644 --- a/apps/mobile/src/components/forceUpgrade/ForceUpgradeModal.tsx +++ b/apps/mobile/src/components/forceUpgrade/ForceUpgradeModal.tsx @@ -6,44 +6,43 @@ import { APP_STORE_LINK } from 'src/constants/urls' import { UpgradeStatus } from 'src/features/forceUpgrade/types' import { Flex, Text, TouchableArea, useSporeColors } from 'ui/src' import { BottomSheetModal } from 'uniswap/src/components/modals/BottomSheetModal' -import { DynamicConfigs } from 'uniswap/src/features/gating/configs' -import { useDynamicConfig } from 'uniswap/src/features/gating/hooks' +import { DynamicConfigs, ForceUpgradeConfigKey } from 'uniswap/src/features/gating/configs' +import { useDynamicConfigValue } from 'uniswap/src/features/gating/hooks' import { ModalName } from 'uniswap/src/features/telemetry/constants' +import { openUri } from 'uniswap/src/utils/linking' import { WarningModal } from 'wallet/src/components/modals/WarningModal/WarningModal' import { WarningSeverity } from 'wallet/src/features/transactions/WarningModal/types' import { SignerMnemonicAccount } from 'wallet/src/features/wallet/accounts/types' import { useSignerAccounts } from 'wallet/src/features/wallet/hooks' -import { openUri } from 'wallet/src/utils/linking' export function ForceUpgradeModal(): JSX.Element { const { t } = useTranslation() const colors = useSporeColors() - const forceUpgradeConfig = useDynamicConfig(DynamicConfigs.MobileForceUpgrade) + const forceUpgradeStatusString = useDynamicConfigValue( + DynamicConfigs.MobileForceUpgrade, + ForceUpgradeConfigKey.Status, + '' as string, + ) const [isVisible, setIsVisible] = useState(false) const [upgradeStatus, setUpgradeStatus] = useState(UpgradeStatus.NotRequired) // signerAccounts could be empty if no seed phrase imported or in onboarding const signerAccounts = useSignerAccounts() - const mnemonicId = - signerAccounts.length > 0 - ? (signerAccounts?.[0] as SignerMnemonicAccount)?.mnemonicId - : undefined + const mnemonicId = signerAccounts.length > 0 ? (signerAccounts?.[0] as SignerMnemonicAccount)?.mnemonicId : undefined const [showSeedPhrase, setShowSeedPhrase] = useState(false) useEffect(() => { - const statusString = forceUpgradeConfig.getValue('status')?.toString() - let status = UpgradeStatus.NotRequired - if (statusString === 'recommended') { + if (forceUpgradeStatusString === 'recommended') { status = UpgradeStatus.Recommended - } else if (statusString === 'required') { + } else if (forceUpgradeStatusString === 'required') { status = UpgradeStatus.Required } setUpgradeStatus(status) setIsVisible(status !== UpgradeStatus.NotRequired) - }, [forceUpgradeConfig]) + }, [forceUpgradeStatusString]) const onPressConfirm = async (): Promise => { await openUri(APP_STORE_LINK, /*openExternalBrowser=*/ true, /*isSafeUri=*/ true) @@ -72,7 +71,8 @@ export function ForceUpgradeModal(): JSX.Element { severity={WarningSeverity.High} title={t('forceUpgrade.title')} onClose={onClose} - onConfirm={onPressConfirm}> + onConfirm={onPressConfirm} + > {t('forceUpgrade.description')} @@ -88,7 +88,8 @@ export function ForceUpgradeModal(): JSX.Element { fullScreen backgroundColor={colors.surface1.get()} name={ModalName.ForceUpgradeModal} - onClose={onDismiss}> + onClose={onDismiss} + > diff --git a/apps/mobile/src/components/gradients/GradientBackground.tsx b/apps/mobile/src/components/gradients/GradientBackground.tsx index 3f348c9abec..fb91b7a5520 100644 --- a/apps/mobile/src/components/gradients/GradientBackground.tsx +++ b/apps/mobile/src/components/gradients/GradientBackground.tsx @@ -5,14 +5,7 @@ import { zIndices } from 'ui/src/theme' // Fills up entire parent by default export function GradientBackground({ children, ...rest }: FlexProps): JSX.Element { return ( - + {children} ) diff --git a/apps/mobile/src/components/home/ActivityTab.tsx b/apps/mobile/src/components/home/ActivityTab.tsx index 6e5f25c4fa6..937f792a81e 100644 --- a/apps/mobile/src/components/home/ActivityTab.tsx +++ b/apps/mobile/src/components/home/ActivityTab.tsx @@ -1,12 +1,9 @@ import { ForwardedRef, forwardRef, memo, useMemo } from 'react' import { FlatList, RefreshControl } from 'react-native' import Animated from 'react-native-reanimated' -import { useAppDispatch } from 'src/app/hooks' +import { useDispatch } from 'react-redux' import { useAdaptiveFooter } from 'src/components/home/hooks' -import { - AnimatedBottomSheetFlatList, - AnimatedFlatList, -} from 'src/components/layout/AnimatedFlatList' +import { AnimatedBottomSheetFlatList, AnimatedFlatList } from 'src/components/layout/AnimatedFlatList' import { TAB_BAR_HEIGHT, TabProps } from 'src/components/layout/TabHelpers' import { useBiometricAppSettings, useBiometricPrompt } from 'src/features/biometrics/hooks' import { openModal } from 'src/features/modals/modalSlice' @@ -34,47 +31,36 @@ export const ActivityTab = memo( refreshing, onRefresh, }, - ref + ref, ) { - const dispatch = useAppDispatch() + const dispatch = useDispatch() const colors = useSporeColors() const insets = useDeviceInsets() const { trigger: biometricsTrigger } = useBiometricPrompt() const { requiredForTransactions: requiresBiometrics } = useBiometricAppSettings() - const { onContentSizeChange, adaptiveFooter } = useAdaptiveFooter( - containerProps?.contentContainerStyle - ) + const { onContentSizeChange, adaptiveFooter } = useAdaptiveFooter(containerProps?.contentContainerStyle) const onPressReceive = (): void => { // in case we received a pending session from a previous scan after closing modal dispatch(removePendingSession()) - dispatch( - openModal({ name: ModalName.WalletConnectScan, initialState: ScannerModalState.WalletQr }) - ) + dispatch(openModal({ name: ModalName.WalletConnectScan, initialState: ScannerModalState.WalletQr })) } - const { - maybeLoaderComponent, - maybeEmptyComponent, - renderActivityItem, - sectionData, - keyExtractor, - } = useActivityData({ - owner, - authTrigger: requiresBiometrics ? biometricsTrigger : undefined, - isExternalProfile, - emptyComponentStyle: containerProps?.emptyComponentStyle, - onPressEmptyState: onPressReceive, - }) + const { maybeLoaderComponent, maybeEmptyComponent, renderActivityItem, sectionData, keyExtractor } = + useActivityData({ + owner, + authTrigger: requiresBiometrics ? biometricsTrigger : undefined, + isExternalProfile, + emptyComponentStyle: containerProps?.emptyComponentStyle, + onPressEmptyState: onPressReceive, + }) const refreshControl = useMemo(() => { return ( ) - }) + }), ) diff --git a/apps/mobile/src/components/home/FeedTab.tsx b/apps/mobile/src/components/home/FeedTab.tsx index 560f8f43617..a59a7eb67b9 100644 --- a/apps/mobile/src/components/home/FeedTab.tsx +++ b/apps/mobile/src/components/home/FeedTab.tsx @@ -2,7 +2,8 @@ import { ForwardedRef, forwardRef, memo, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { FlatList, RefreshControl } from 'react-native' import Animated from 'react-native-reanimated' -import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { useDispatch } from 'react-redux' +import { useAppSelector } from 'src/app/hooks' import { useAdaptiveFooter } from 'src/components/home/hooks' import { AnimatedFlatList } from 'src/components/layout/AnimatedFlatList' import { TAB_BAR_HEIGHT, TabProps } from 'src/components/layout/TabHelpers' @@ -37,10 +38,10 @@ const SectionTitle = ({ title }: { title: string }): JSX.Element => ( export const FeedTab = memo( forwardRef, TabProps>(function _FeedTab( { containerProps, scrollHandler, headerHeight, refreshing, onRefresh }, - ref + ref, ) { const { t } = useTranslation() - const dispatch = useAppDispatch() + const dispatch = useDispatch() const colors = useSporeColors() const insets = useDeviceInsets() @@ -58,19 +59,19 @@ export const FeedTab = memo( , SectionTitle, undefined, - undefined + undefined, ) }, []) - const { onRetry, hasData, isLoading, isError, sectionData, keyExtractor } = - useFormattedTransactionDataForFeed(watchedWalletsList, hideSpamTokens) + const { onRetry, hasData, isLoading, isError, sectionData, keyExtractor } = useFormattedTransactionDataForFeed( + watchedWalletsList, + hideSpamTokens, + ) const onPressReceive = (): void => { // in case we received a pending session from a previous scan after closing modal dispatch(removePendingSession()) - dispatch( - openModal({ name: ModalName.WalletConnectScan, initialState: ScannerModalState.WalletQr }) - ) + dispatch(openModal({ name: ModalName.WalletConnectScan, initialState: ScannerModalState.WalletQr })) } const errorCard = ( @@ -104,9 +105,7 @@ export const FeedTab = memo( const refreshControl = useMemo(() => { return ( ) - }) + }), ) diff --git a/apps/mobile/src/components/home/NftsTab.tsx b/apps/mobile/src/components/home/NftsTab.tsx index fa8ee6f905a..7659607a2d2 100644 --- a/apps/mobile/src/components/home/NftsTab.tsx +++ b/apps/mobile/src/components/home/NftsTab.tsx @@ -7,6 +7,7 @@ import { useAdaptiveFooter } from 'src/components/home/hooks' import { TAB_BAR_HEIGHT, TabProps } from 'src/components/layout/TabHelpers' import { Flex, useDeviceInsets, useSporeColors } from 'ui/src' import { GQLQueries } from 'uniswap/src/data/graphql/uniswap-data-api/queries' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { MobileScreens } from 'uniswap/src/types/screens/mobile' import { isAndroid } from 'utilities/src/platform' import { NftsList } from 'wallet/src/components/nfts/NftsList' @@ -26,18 +27,18 @@ export const NftsTab = memo( headerHeight = 0, renderedInModal = false, }, - ref + ref, ) { const colors = useSporeColors() const insets = useDeviceInsets() const navigation = useAppStackNavigation() const { onContentSizeChange, footerHeight, adaptiveFooter } = useAdaptiveFooter( - containerProps?.contentContainerStyle + containerProps?.contentContainerStyle, ) const renderNFTItem = useCallback( - (item: NFTItem) => { + (item: NFTItem, index: number) => { const onPressNft = (): void => { navigation.navigate(MobileScreens.NFTItem, { owner, @@ -48,17 +49,15 @@ export const NftsTab = memo( }) } - return + return }, - [owner, navigation] + [owner, navigation], ) const refreshControl = useMemo(() => { return ( + ) - }) + }), ) diff --git a/apps/mobile/src/components/home/TokensTab.tsx b/apps/mobile/src/components/home/TokensTab.tsx index 06781de569d..16a68981bc4 100644 --- a/apps/mobile/src/components/home/TokensTab.tsx +++ b/apps/mobile/src/components/home/TokensTab.tsx @@ -2,7 +2,7 @@ import { useStartProfiler } from '@shopify/react-native-performance' import React, { forwardRef, memo, useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { FlatList } from 'react-native' -import { useAppDispatch } from 'src/app/hooks' +import { useDispatch } from 'react-redux' import { TokenBalanceList } from 'src/components/TokenBalanceList/TokenBalanceList' import { useTokenDetailsNavigation } from 'src/components/TokenDetails/hooks' import { TabProps } from 'src/components/layout/TabHelpers' @@ -11,13 +11,13 @@ import { Flex } from 'ui/src' import { NoTokens } from 'ui/src/components/icons' import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard' import { GQLQueries } from 'uniswap/src/data/graphql/uniswap-data-api/queries' +import { useCexTransferProviders } from 'uniswap/src/features/fiatOnRamp/useCexTransferProviders' import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { ModalName } from 'uniswap/src/features/telemetry/constants' import { CurrencyId } from 'uniswap/src/types/currency' import { MobileScreens } from 'uniswap/src/types/screens/mobile' import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants' -import { useCexTransferProviders } from 'wallet/src/features/fiatOnRamp/api' import { PortfolioEmptyState } from 'wallet/src/features/portfolio/PortfolioEmptyState' import { TokenBalanceListRow } from 'wallet/src/features/portfolio/TokenBalanceListContext' @@ -26,105 +26,104 @@ export const TOKENS_TAB_DATA_DEPENDENCIES = [GQLQueries.PortfolioBalances] // ignore ref type export const TokensTab = memo( - forwardRef, TabProps & { isExternalProfile?: boolean }>( - function _TokensTab( - { - owner, - containerProps, - scrollHandler, - isExternalProfile = false, - renderedInModal = false, - onRefresh, - refreshing, - headerHeight, - }, - ref - ) { - const { t } = useTranslation() - const dispatch = useAppDispatch() - const tokenDetailsNavigation = useTokenDetailsNavigation() - const startProfilerTimer = useStartProfiler() - const forAggregatorEnabled = useFeatureFlag(FeatureFlags.ForAggregator) - const cexTransferEnabled = useFeatureFlag(FeatureFlags.CexTransfers) - const cexTransferProviders = useCexTransferProviders(cexTransferEnabled) + forwardRef, TabProps & { isExternalProfile?: boolean }>(function _TokensTab( + { + owner, + containerProps, + scrollHandler, + isExternalProfile = false, + renderedInModal = false, + onRefresh, + refreshing, + headerHeight, + testID, + }, + ref, + ) { + const { t } = useTranslation() + const dispatch = useDispatch() + const tokenDetailsNavigation = useTokenDetailsNavigation() + const startProfilerTimer = useStartProfiler() + const cexTransferEnabled = useFeatureFlag(FeatureFlags.CexTransfers) + const cexTransferProviders = useCexTransferProviders(cexTransferEnabled) - const onPressToken = useCallback( - (currencyId: CurrencyId): void => { - startProfilerTimer({ source: MobileScreens.Home }) - tokenDetailsNavigation.navigate(currencyId) - }, - [startProfilerTimer, tokenDetailsNavigation] - ) + const onPressToken = useCallback( + (currencyId: CurrencyId): void => { + startProfilerTimer({ source: MobileScreens.Home }) + tokenDetailsNavigation.navigate(currencyId) + }, + [startProfilerTimer, tokenDetailsNavigation], + ) - const onPressAction = useCallback((): void => { - dispatch( - openModal({ name: ModalName.WalletConnectScan, initialState: ScannerModalState.WalletQr }) - ) - }, [dispatch]) + const onPressAction = useCallback((): void => { + dispatch(openModal({ name: ModalName.WalletConnectScan, initialState: ScannerModalState.WalletQr })) + }, [dispatch]) - const onPressBuy = useCallback(() => { - dispatch( - openModal({ - name: forAggregatorEnabled ? ModalName.FiatOnRampAggregator : ModalName.FiatOnRamp, - }) - ) - }, [dispatch, forAggregatorEnabled]) + const onPressBuy = useCallback(() => { + dispatch(openModal({ name: ModalName.FiatOnRampAggregator })) + }, [dispatch]) - const onPressReceive = useCallback(() => { - dispatch( - openModal( - cexTransferProviders.length > 0 - ? { - name: ModalName.ReceiveCryptoModal, - initialState: cexTransferProviders, - } - : { - name: ModalName.WalletConnectScan, - initialState: ScannerModalState.WalletQr, - } - ) - ) - }, [cexTransferProviders, dispatch]) + const onPressReceive = useCallback(() => { + dispatch( + openModal( + cexTransferProviders.length > 0 + ? { + name: ModalName.ReceiveCryptoModal, + initialState: cexTransferProviders, + } + : { + name: ModalName.WalletConnectScan, + initialState: ScannerModalState.WalletQr, + }, + ), + ) + }, [cexTransferProviders, dispatch]) - const onPressImport = useCallback(() => { - dispatch(openModal({ name: ModalName.AccountSwitcher })) - }, [dispatch]) + const onPressImport = useCallback(() => { + dispatch(openModal({ name: ModalName.AccountSwitcher })) + }, [dispatch]) - const renderEmpty = useMemo((): JSX.Element => { - // Show different empty state on external profile pages - return isExternalProfile ? ( + const renderEmpty = useMemo((): JSX.Element => { + // Show different empty state on external profile pages + return isExternalProfile ? ( + } title={t('home.tokens.empty.title')} onPress={onPressAction} /> - ) : ( - - ) - }, [isExternalProfile, onPressAction, onPressBuy, onPressImport, onPressReceive, t]) - - return ( - - + ) : ( + ) - } - ) + }, [ + isExternalProfile, + onPressAction, + onPressBuy, + onPressImport, + onPressReceive, + containerProps?.emptyComponentStyle, + t, + ]) + + return ( + + + + ) + }), ) diff --git a/apps/mobile/src/components/home/hooks.tsx b/apps/mobile/src/components/home/hooks.tsx index dcbf7cacb2a..16764ff2b39 100644 --- a/apps/mobile/src/components/home/hooks.tsx +++ b/apps/mobile/src/components/home/hooks.tsx @@ -46,7 +46,7 @@ export function useAdaptiveFooter(contentContainerStyle?: StyleProp): footerHeight.value = Math.max(0, calculatedFooterHeight) }, - [footerHeight, contentContainerStyle, maxContentHeight] + [footerHeight, contentContainerStyle, maxContentHeight], ) useEffect(() => { diff --git a/apps/mobile/src/components/icons/BiometricsIcon.tsx b/apps/mobile/src/components/icons/BiometricsIcon.tsx index f6c52bb69c7..cf476b11daa 100644 --- a/apps/mobile/src/components/icons/BiometricsIcon.tsx +++ b/apps/mobile/src/components/icons/BiometricsIcon.tsx @@ -2,8 +2,7 @@ import { useDeviceSupportsBiometricAuth } from 'src/features/biometrics/hooks' import { Faceid, Fingerprint } from 'ui/src/components/icons' export function BiometricsIcon(): JSX.Element | null { - const { touchId: isTouchIdSupported, faceId: isFaceIdSupported } = - useDeviceSupportsBiometricAuth() + const { touchId: isTouchIdSupported, faceId: isFaceIdSupported } = useDeviceSupportsBiometricAuth() if (isTouchIdSupported) { return diff --git a/apps/mobile/src/components/icons/BlockExplorerIcon.tsx b/apps/mobile/src/components/icons/BlockExplorerIcon.tsx index f490c15901a..44651ad7cc7 100644 --- a/apps/mobile/src/components/icons/BlockExplorerIcon.tsx +++ b/apps/mobile/src/components/icons/BlockExplorerIcon.tsx @@ -16,11 +16,7 @@ function buildIconComponent(chainId: WalletChainId): React.FC { const isDarkMode = useIsDarkMode() - return isDarkMode ? ( - - ) : ( - - ) + return isDarkMode ? : } Component.displayName = `BlockExplorerIcon_${explorer.name}` iconsCache.set(chainId, Component) diff --git a/apps/mobile/src/components/icons/Favorite.tsx b/apps/mobile/src/components/icons/Favorite.tsx index f4bae471824..1dbf83c0b4d 100644 --- a/apps/mobile/src/components/icons/Favorite.tsx +++ b/apps/mobile/src/components/icons/Favorite.tsx @@ -1,10 +1,5 @@ import React, { useCallback, useEffect, useState } from 'react' -import { - useAnimatedStyle, - useDerivedValue, - withSequence, - withTiming, -} from 'react-native-reanimated' +import { useAnimatedStyle, useDerivedValue, withSequence, withTiming } from 'react-native-reanimated' import { useSporeColors } from 'ui/src' import HeartIcon from 'ui/src/assets/icons/heart.svg' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' @@ -23,7 +18,7 @@ export const Favorite = ({ isFavorited, size }: FavoriteButtonProps): JSX.Elemen const getColor = useCallback( () => (isFavorited ? colors.accent1.val : unfilledColor), - [isFavorited, colors.accent1, unfilledColor] + [isFavorited, colors.accent1, unfilledColor], ) const [color, setColor] = useState(getColor()) diff --git a/apps/mobile/src/components/input/PasswordInput.tsx b/apps/mobile/src/components/input/PasswordInput.tsx index f970e3eac34..14a73d962e4 100644 --- a/apps/mobile/src/components/input/PasswordInput.tsx +++ b/apps/mobile/src/components/input/PasswordInput.tsx @@ -7,10 +7,7 @@ import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { iconSizes } from 'ui/src/theme' import { TextInput, TextInputProps } from 'uniswap/src/components/input/TextInput' -export const PasswordInput = forwardRef(function _PasswordInput( - props, - ref -) { +export const PasswordInput = forwardRef(function _PasswordInput(props, ref) { const colors = useSporeColors() const [showPassword, setShowPassword] = useState(false) @@ -28,7 +25,8 @@ export const PasswordInput = forwardRef(functio borderColor="$surface3" borderRadius="$rounded16" borderWidth={1} - p="$spacing4"> + p="$spacing4" + > (functio {showPassword ? ( - + ) : ( - + )} diff --git a/apps/mobile/src/components/input/SelectionCircle.tsx b/apps/mobile/src/components/input/SelectionCircle.tsx index f4fefc40c53..65fd8fc5c27 100644 --- a/apps/mobile/src/components/input/SelectionCircle.tsx +++ b/apps/mobile/src/components/input/SelectionCircle.tsx @@ -22,7 +22,8 @@ export function SelectionCircle({ borderRadius="$roundedFull" borderWidth={1} height={iconSizes[size]} - width={iconSizes[size]}> + width={iconSizes[size]} + > void @@ -49,30 +47,26 @@ interface ReanimatedFlatlistProps extends FlatListProps { * TODO: [MOB-207] remove this and use Animated.FlatList directly when can use refs with it. Also type the generic T properly for FlatList and dont use `any` */ export const AnimatedFlatList = forwardRef, ReanimatedFlatlistProps>( - function _AnimatedFlatList( - { itemLayoutAnimation, FlatListComponent = ReanimatedFlatList, ...restProps }, - ref - ) { + function _AnimatedFlatList({ itemLayoutAnimation, FlatListComponent = ReanimatedFlatList, ...restProps }, ref) { // eslint-disable-next-line react-hooks/exhaustive-deps const cellRenderer = React.useMemo(() => createCellRenderer(itemLayoutAnimation), []) return - } + }, ) /** * In bottom sheet contexts, this will support pull to dismiss. * See AnimatedFlatList for other props. */ -export const AnimatedBottomSheetFlatList = forwardRef< - Animated.FlatList, - ReanimatedFlatlistProps ->(function _AnimatedBottomSheetFlatList(props, ref) { - return ( - - ) -}) +export const AnimatedBottomSheetFlatList = forwardRef, ReanimatedFlatlistProps>( + function _AnimatedBottomSheetFlatList(props, ref) { + return ( + + ) + }, +) diff --git a/apps/mobile/src/components/layout/BackHeader.tsx b/apps/mobile/src/components/layout/BackHeader.tsx index 1f76adf589d..0b43cc7b18b 100644 --- a/apps/mobile/src/components/layout/BackHeader.tsx +++ b/apps/mobile/src/components/layout/BackHeader.tsx @@ -23,7 +23,8 @@ export function BackHeader({ alignItems="center" justifyContent={alignment === 'left' ? 'flex-start' : 'space-between'} sentry-label="BackHeader" - {...spacingProps}> + {...spacingProps} + > {children} {endAdornment} diff --git a/apps/mobile/src/components/layout/Delayed.tsx b/apps/mobile/src/components/layout/Delayed.tsx index dc5446a070b..04c8b4460c5 100644 --- a/apps/mobile/src/components/layout/Delayed.tsx +++ b/apps/mobile/src/components/layout/Delayed.tsx @@ -13,10 +13,7 @@ type Props = { } /** HOC to delay rendering a component by some time in ms. */ -export const Delayed = ({ - children, - waitBeforeShow = Delay.Short, -}: PropsWithChildren): JSX.Element | null => { +export const Delayed = ({ children, waitBeforeShow = Delay.Short }: PropsWithChildren): JSX.Element | null => { const [isShown, setIsShown] = useReducer(() => true, false) useTimeout(setIsShown, waitBeforeShow) diff --git a/apps/mobile/src/components/layout/SafeKeyboardScreen.tsx b/apps/mobile/src/components/layout/SafeKeyboardScreen.tsx new file mode 100644 index 00000000000..ba56667f491 --- /dev/null +++ b/apps/mobile/src/components/layout/SafeKeyboardScreen.tsx @@ -0,0 +1,75 @@ +import React, { PropsWithChildren, useState } from 'react' +import { KeyboardAvoidingView, ScrollView, StyleSheet } from 'react-native' +import { Screen, ScreenProps } from 'src/components/layout/Screen' +import { Flex, flexStyles } from 'ui/src' +import { spacing } from 'ui/src/theme' +import { useKeyboardLayout } from 'uniswap/src/utils/useKeyboardLayout' +import { isIOS } from 'utilities/src/platform' + +type OnboardingScreenProps = ScreenProps & { + header?: JSX.Element + footer?: JSX.Element + minHeightWhenKeyboardExpanded?: boolean +} + +export function SafeKeyboardScreen({ + children, + header, + footer, + minHeightWhenKeyboardExpanded = false, + ...screenProps +}: PropsWithChildren): JSX.Element { + const [footerHeight, setFooterHeight] = useState(0) + const keyboard = useKeyboardLayout() + + const compact = keyboard.isVisible && keyboard.containerHeight !== 0 + const containerStyle = compact ? styles.compact : styles.expand + + // This makes sure this component behaves just like `behavior="padding"` when + // there's enough space on the screen to show all components. + const minHeight = minHeightWhenKeyboardExpanded && compact ? keyboard.containerHeight - footerHeight : 0 + + return ( + + + {header} + + + {children} + + + { + setFooterHeight(height) + }} + > + {footer} + + + + ) +} + +const styles = StyleSheet.create({ + base: { + flex: 1, + justifyContent: 'flex-end', + }, + compact: { + flexGrow: 0, + }, + container: { + paddingBottom: spacing.spacing12, + }, + expand: { + flexGrow: 1, + }, +}) diff --git a/apps/mobile/src/components/layout/Screen.tsx b/apps/mobile/src/components/layout/Screen.tsx index e027d5cfd29..7dd87c1d712 100644 --- a/apps/mobile/src/components/layout/Screen.tsx +++ b/apps/mobile/src/components/layout/Screen.tsx @@ -5,7 +5,7 @@ import { Flex, FlexProps, useDeviceInsets } from 'ui/src' // Used to determine amount of top padding for short screens export const SHORT_SCREEN_HEADER_HEIGHT_RATIO = 0.88 -type ScreenProps = FlexProps & +export type ScreenProps = FlexProps & // The SafeAreaView from react-native-safe-area-context also supports a `mode` prop which // lets you choose if `edges` are added as margin or padding, but we don’t use that so // our Screen component doesn't need to support it @@ -60,11 +60,7 @@ function SafeAreaWithInsets({ children, edges, noInsets, ...rest }: ScreenProps) ) } -export function Screen({ - backgroundColor = '$surface1', - children, - ...rest -}: ScreenProps): JSX.Element { +export function Screen({ backgroundColor = '$surface1', children, ...rest }: ScreenProps): JSX.Element { return ( {children} diff --git a/apps/mobile/src/components/layout/TabHelpers.tsx b/apps/mobile/src/components/layout/TabHelpers.tsx index 3a6a84da877..e441424ffb4 100644 --- a/apps/mobile/src/components/layout/TabHelpers.tsx +++ b/apps/mobile/src/components/layout/TabHelpers.tsx @@ -85,6 +85,7 @@ export type TabProps = { refreshing?: boolean onRefresh?: () => void headerHeight?: number + testID?: string } export type TabContentProps = Partial> & { @@ -106,15 +107,13 @@ export const TabLabel = ({ isExternalProfile?: boolean }): JSX.Element => { return ( - + {route.title} {/* Streamline UI by hiding the Activity tab spinner when focused and showing it only on the specific pending transactions. */} - {route.title === 'Activity' && !isExternalProfile && !focused ? ( - - ) : null} + {route.title === 'Activity' && !isExternalProfile && !focused ? : null} ) } @@ -125,35 +124,34 @@ export const TabLabel = ({ export const useScrollSync = ( currentTabIndex: SharedValue, scrollPairs: ScrollPair[], - headerConfig: HeaderConfig + headerConfig: HeaderConfig, ): { sync: (event: NativeSyntheticEvent) => void } => { - const sync: - | FlatListProps['onMomentumScrollEnd'] - | FlashListProps['onMomentumScrollEnd'] = useCallback( - (event: { nativeEvent: NativeScrollEvent }) => { - const { y } = event.nativeEvent.contentOffset + const sync: FlatListProps['onMomentumScrollEnd'] | FlashListProps['onMomentumScrollEnd'] = + useCallback( + (event: { nativeEvent: NativeScrollEvent }) => { + const { y } = event.nativeEvent.contentOffset - const { heightCollapsed, heightExpanded } = headerConfig + const { heightCollapsed, heightExpanded } = headerConfig - const headerDiff = heightExpanded - heightCollapsed + const headerDiff = heightExpanded - heightCollapsed - for (const { list, position, index } of scrollPairs) { - const scrollPosition = position.value + for (const { list, position, index } of scrollPairs) { + const scrollPosition = position.value - if (scrollPosition > headerDiff && y > headerDiff) { - continue - } + if (scrollPosition > headerDiff && y > headerDiff) { + continue + } - if (index !== currentTabIndex.value) { - list.current?.scrollToOffset({ - offset: Math.min(y, headerDiff), - animated: false, - }) + if (index !== currentTabIndex.value) { + list.current?.scrollToOffset({ + offset: Math.min(y, headerDiff), + animated: false, + }) + } } - } - }, - [currentTabIndex, scrollPairs, headerConfig] - ) + }, + [currentTabIndex, scrollPairs, headerConfig], + ) return useMemo(() => ({ sync }), [sync]) } diff --git a/apps/mobile/src/components/layout/VirtualizedList.tsx b/apps/mobile/src/components/layout/VirtualizedList.tsx index 326fb485ac2..f5a009dc9ee 100644 --- a/apps/mobile/src/components/layout/VirtualizedList.tsx +++ b/apps/mobile/src/components/layout/VirtualizedList.tsx @@ -1,8 +1,5 @@ import React, { ComponentProps, PropsWithChildren } from 'react' -import { - AnimatedBottomSheetFlatList, - AnimatedFlatList, -} from 'src/components/layout/AnimatedFlatList' +import { AnimatedBottomSheetFlatList, AnimatedFlatList } from 'src/components/layout/AnimatedFlatList' type VirtualizedListProps = PropsWithChildren>> & { renderedInModal?: boolean @@ -10,23 +7,24 @@ type VirtualizedListProps = PropsWithChildren( - function _VirtualizedList({ children, renderedInModal, ...props }: VirtualizedListProps, ref) { - const List = renderedInModal ? AnimatedBottomSheetFlatList : AnimatedFlatList - return ( - {children}} - data={[]} - keyExtractor={(): string => 'key'} - keyboardShouldPersistTaps="always" - renderItem={null} - scrollEventThrottle={16} - showsHorizontalScrollIndicator={false} - showsVerticalScrollIndicator={false} - /> - ) - } -) +export const VirtualizedList = React.forwardRef(function _VirtualizedList( + { children, renderedInModal, ...props }: VirtualizedListProps, + ref, +) { + const List = renderedInModal ? AnimatedBottomSheetFlatList : AnimatedFlatList + return ( + {children}} + data={[]} + keyExtractor={(): string => 'key'} + keyboardShouldPersistTaps="always" + renderItem={null} + scrollEventThrottle={16} + showsHorizontalScrollIndicator={false} + showsVerticalScrollIndicator={false} + /> + ) +}) diff --git a/apps/mobile/src/components/layout/screens/HeaderScrollScreen.tsx b/apps/mobile/src/components/layout/screens/HeaderScrollScreen.tsx index 8ef7d767bc6..7cbafe79b3a 100644 --- a/apps/mobile/src/components/layout/screens/HeaderScrollScreen.tsx +++ b/apps/mobile/src/components/layout/screens/HeaderScrollScreen.tsx @@ -57,10 +57,7 @@ export function HeaderScrollScreen({ }) return ( - + {showHandleBar ? : null} + onScroll={scrollHandler} + > {children} diff --git a/apps/mobile/src/components/layout/screens/ScrollHeader.tsx b/apps/mobile/src/components/layout/screens/ScrollHeader.tsx index 3efccd01322..f8fb8275d45 100644 --- a/apps/mobile/src/components/layout/screens/ScrollHeader.tsx +++ b/apps/mobile/src/components/layout/screens/ScrollHeader.tsx @@ -1,12 +1,7 @@ import { useScrollToTop } from '@react-navigation/native' import React, { ReactElement, useMemo } from 'react' import { StyleProp, ViewStyle } from 'react-native' -import Animated, { - Extrapolate, - SharedValue, - interpolate, - useAnimatedStyle, -} from 'react-native-reanimated' +import Animated, { Extrapolate, SharedValue, interpolate, useAnimatedStyle } from 'react-native-reanimated' import { BackButton } from 'src/components/buttons/BackButton' import { WithScrollToTop } from 'src/components/layout/screens/WithScrollToTop' import { ColorTokens, Flex, useDeviceInsets } from 'ui/src' @@ -50,12 +45,7 @@ export function ScrollHeader({ const visibleOnScrollStyle = useAnimatedStyle(() => { return { - opacity: interpolate( - scrollY.value, - [0, showHeaderScrollYDistance], - [0, 1], - Extrapolate.CLAMP - ), + opacity: interpolate(scrollY.value, [0, showHeaderScrollYDistance], [0, 1], Extrapolate.CLAMP), } }) @@ -71,10 +61,7 @@ export function ScrollHeader({ const headerWrapperStyles = fullScreen ? [visibleOnScrollStyle, { zIndex: zIndices.popover }] : [] return ( - + + style={headerRowStyles} + > {alwaysShowCenterElement ? ( @@ -128,7 +116,8 @@ function HeaderWrapper({ position="absolute" right={0} style={style} - top={0}> + top={0} + > {children} ) diff --git a/apps/mobile/src/components/layout/screens/WithScrollToTop.tsx b/apps/mobile/src/components/layout/screens/WithScrollToTop.tsx index 7b1982bebd8..8b8e038cb56 100644 --- a/apps/mobile/src/components/layout/screens/WithScrollToTop.tsx +++ b/apps/mobile/src/components/layout/screens/WithScrollToTop.tsx @@ -3,15 +3,16 @@ import { Pressable } from 'react-native' // accept any ref // eslint-disable-next-line @typescript-eslint/no-explicit-any -export const WithScrollToTop = forwardRef>( - function _WithScrollToTop({ children }: PropsWithChildren, ref) { - const onPress = (): void => { - if (!ref || typeof ref === 'function') { - return - } - ref.current.scrollToOffset({ animated: true, offset: 0 }) +export const WithScrollToTop = forwardRef>(function _WithScrollToTop( + { children }: PropsWithChildren, + ref, +) { + const onPress = (): void => { + if (!ref || typeof ref === 'function') { + return } - - return {children} + ref.current.scrollToOffset({ animated: true, offset: 0 }) } -) + + return {children} +}) diff --git a/apps/mobile/src/components/loading/TransactionLoader.tsx b/apps/mobile/src/components/loading/TransactionLoader.tsx index 8faac9787e5..1fc37674a20 100644 --- a/apps/mobile/src/components/loading/TransactionLoader.tsx +++ b/apps/mobile/src/components/loading/TransactionLoader.tsx @@ -9,20 +9,8 @@ interface TransactionLoaderProps { export function TransactionLoader({ opacity }: TransactionLoaderProps): JSX.Element { return ( - - + + - + diff --git a/apps/mobile/src/components/loading/WaveLoader.tsx b/apps/mobile/src/components/loading/WaveLoader.tsx index 8da33555d80..93ebddc6de8 100644 --- a/apps/mobile/src/components/loading/WaveLoader.tsx +++ b/apps/mobile/src/components/loading/WaveLoader.tsx @@ -35,13 +35,7 @@ export function WaveLoader(): JSX.Element { })) return ( - + diff --git a/apps/mobile/src/components/loading/index.tsx b/apps/mobile/src/components/loading/index.tsx index 07071ea921f..88f585805a7 100644 --- a/apps/mobile/src/components/loading/index.tsx +++ b/apps/mobile/src/components/loading/index.tsx @@ -11,11 +11,7 @@ function Graph(): JSX.Element { ) } -export const Transaction = memo(function _Transaction({ - repeat = 1, -}: { - repeat?: number -}): JSX.Element { +export const Transaction = memo(function _Transaction({ repeat = 1 }: { repeat?: number }): JSX.Element { return ( diff --git a/apps/mobile/src/components/mnemonic/HiddenMnemonicWordView.tsx b/apps/mobile/src/components/mnemonic/HiddenMnemonicWordView.tsx index d45b83bef5b..142df3b4087 100644 --- a/apps/mobile/src/components/mnemonic/HiddenMnemonicWordView.tsx +++ b/apps/mobile/src/components/mnemonic/HiddenMnemonicWordView.tsx @@ -11,26 +11,22 @@ export function HiddenMnemonicWordView(): JSX.Element { backgroundColor="$surface2" borderRadius="$rounded20" gap="$spacing36" - height="40%" mt="$spacing16" px="$spacing32" - py="$spacing24"> - - - - - - + py="$spacing24" + > + + ) } function HiddenWordViewColumn(): JSX.Element { return ( - <> + {new Array(ROW_COUNT).fill(0).map((_, idx) => ( ))} - + ) } diff --git a/apps/mobile/src/components/mnemonic/MnemonicConfirmation.tsx b/apps/mobile/src/components/mnemonic/MnemonicConfirmation.tsx index c84fd27653e..3699d56824c 100644 --- a/apps/mobile/src/components/mnemonic/MnemonicConfirmation.tsx +++ b/apps/mobile/src/components/mnemonic/MnemonicConfirmation.tsx @@ -12,8 +12,7 @@ interface NativeMnemonicConfirmationProps { onConfirmComplete: () => void } -const NativeMnemonicConfirmation = - requireNativeComponent('MnemonicConfirmation') +const NativeMnemonicConfirmation = requireNativeComponent('MnemonicConfirmation') type MnemonicConfirmationProps = ViewProps & { mnemonicId: Address diff --git a/apps/mobile/src/components/mnemonic/MnemonicDisplay.tsx b/apps/mobile/src/components/mnemonic/MnemonicDisplay.tsx index 275fca82caa..e59084e0871 100644 --- a/apps/mobile/src/components/mnemonic/MnemonicDisplay.tsx +++ b/apps/mobile/src/components/mnemonic/MnemonicDisplay.tsx @@ -53,7 +53,8 @@ export function MnemonicDisplay(props: MnemonicDisplayProps): JSX.Element { // until the height is measured display={height ? 'flex' : 'none'} gap="$spacing8" - p="$spacing16"> + p="$spacing16" + > diff --git a/apps/mobile/src/components/mnemonic/SeedPhraseDisplay.tsx b/apps/mobile/src/components/mnemonic/SeedPhraseDisplay.tsx index 5c028645e3e..1b30e231b53 100644 --- a/apps/mobile/src/components/mnemonic/SeedPhraseDisplay.tsx +++ b/apps/mobile/src/components/mnemonic/SeedPhraseDisplay.tsx @@ -7,7 +7,8 @@ import { MnemonicDisplay } from 'src/components/mnemonic/MnemonicDisplay' import { useBiometricAppSettings, useBiometricPrompt } from 'src/features/biometrics/hooks' import { useWalletRestore } from 'src/features/wallet/hooks' import { Button, Flex } from 'ui/src' -import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants' +import { ModalName } from 'uniswap/src/features/telemetry/constants' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { WarningModal } from 'wallet/src/components/modals/WarningModal/WarningModal' import { WarningSeverity } from 'wallet/src/features/transactions/WarningModal/types' @@ -17,18 +18,12 @@ type Props = { walletNeedsRestore?: boolean } -export function SeedPhraseDisplay({ - mnemonicId, - onDismiss, - walletNeedsRestore, -}: Props): JSX.Element { +export function SeedPhraseDisplay({ mnemonicId, onDismiss, walletNeedsRestore }: Props): JSX.Element { const { t } = useTranslation() const { isModalOpen: isWalletRestoreModalOpen } = useWalletRestore({ openModalImmediately: true }) const [showScreenShotWarningModal, setShowScreenShotWarningModal] = useState(false) const [showSeedPhrase, setShowSeedPhrase] = useState(false) - const [showSeedPhraseViewWarningModal, setShowSeedPhraseViewWarningModal] = useState( - !walletNeedsRestore - ) + const [showSeedPhraseViewWarningModal, setShowSeedPhraseViewWarningModal] = useState(!walletNeedsRestore) const prevIsWalletRestoreModalOpen = usePrevious(isWalletRestoreModalOpen) @@ -70,17 +65,14 @@ export function SeedPhraseDisplay({ ) : ( - + + + )} - diff --git a/apps/mobile/src/components/modals/Modal.tsx b/apps/mobile/src/components/modals/Modal.tsx index 613cdae00b6..3b9e3c4f365 100644 --- a/apps/mobile/src/components/modals/Modal.tsx +++ b/apps/mobile/src/components/modals/Modal.tsx @@ -43,17 +43,20 @@ Props): JSX.Element { animationType={animationType} presentationStyle={presentationStyle} transparent={transparent} /* {...rest} */ - visible={visible}> + visible={visible} + > + onPress={dismissable ? hide : undefined} + > + width={width} + > {title && ( {title} diff --git a/apps/mobile/src/components/sortableGrid/SortableGirdProvider.tsx b/apps/mobile/src/components/sortableGrid/SortableGirdProvider.tsx index f6a9b5afa19..d0a8bd174a8 100644 --- a/apps/mobile/src/components/sortableGrid/SortableGirdProvider.tsx +++ b/apps/mobile/src/components/sortableGrid/SortableGirdProvider.tsx @@ -4,17 +4,10 @@ import type { DragContextProviderProps, LayoutContextProviderProps, } from 'src/components/sortableGrid/contexts' -import { - AutoScrollProvider, - DragContextProvider, - LayoutContextProvider, -} from 'src/components/sortableGrid/contexts' +import { AutoScrollProvider, DragContextProvider, LayoutContextProvider } from 'src/components/sortableGrid/contexts' type SortableGridProviderProps = PropsWithChildren< - Omit< - LayoutContextProviderProps & DragContextProviderProps & AutoScrollProviderProps, - 'itemKeys' - > + Omit & AutoScrollProviderProps, 'itemKeys'> > export function SortableGridProvider({ @@ -55,11 +48,9 @@ export function SortableGridProvider({ keyExtractor={keyExtractor} onChange={onChange} onDragStart={onDragStart} - onDrop={onDrop}> - + onDrop={onDrop} + > + {children} diff --git a/apps/mobile/src/components/sortableGrid/SortableGrid.tsx b/apps/mobile/src/components/sortableGrid/SortableGrid.tsx index b1ae5f74b38..96be8f5e1be 100644 --- a/apps/mobile/src/components/sortableGrid/SortableGrid.tsx +++ b/apps/mobile/src/components/sortableGrid/SortableGrid.tsx @@ -123,17 +123,12 @@ function SortableGridInner({ + onLayout={handleGridMeasurement} + > {data.map((item, index) => { const key = keyExtractor(item, index) return ( - + ) })} @@ -142,10 +137,7 @@ function SortableGridInner({ from the animated style was applied. We can't use onLayout on the grid items wrapper component because it already has the same height as containerHeight value, thus the onLayout callback won't be called again, because the size of the component doesn't change. */} - + ) } diff --git a/apps/mobile/src/components/sortableGrid/SortableGridItem.tsx b/apps/mobile/src/components/sortableGrid/SortableGridItem.tsx index ec669206c62..69b656f96cb 100644 --- a/apps/mobile/src/components/sortableGrid/SortableGridItem.tsx +++ b/apps/mobile/src/components/sortableGrid/SortableGridItem.tsx @@ -17,11 +17,7 @@ import { OFFSET_EPS, TIME_TO_ACTIVATE_PAN, } from 'src/components/sortableGrid/constants' -import { - useAutoScrollContext, - useDragContext, - useLayoutContext, -} from 'src/components/sortableGrid/contexts' +import { useAutoScrollContext, useDragContext, useLayoutContext } from 'src/components/sortableGrid/contexts' import { useItemPosition } from 'src/components/sortableGrid/hooks' import { GridItemExiting } from 'src/components/sortableGrid/layoutAnimations' import { SortableGridRenderItem } from 'src/components/sortableGrid/types' @@ -34,12 +30,7 @@ type SortableGridItemProps = { numColumns: number } -function SortableGridItem({ - item, - itemKey, - renderItem, - numColumns, -}: SortableGridItemProps): JSX.Element { +function SortableGridItem({ item, itemKey, renderItem, numColumns }: SortableGridItemProps): JSX.Element { const { measuredItemsCount, targetContainerHeight, @@ -98,7 +89,7 @@ function SortableGridItem({ itemDimensions.value[key] = { width, height } })(itemKey) }, - [itemKey, itemDimensions, measuredItemsCount] + [itemKey, itemDimensions, measuredItemsCount], ) const handleDragEnd = useCallback(() => { @@ -119,7 +110,7 @@ function SortableGridItem({ isTouched.value = true const progress = withDelay( ACTIVATE_PAN_ANIMATION_DELAY, - withTiming(1, { duration: TIME_TO_ACTIVATE_PAN - ACTIVATE_PAN_ANIMATION_DELAY }) + withTiming(1, { duration: TIME_TO_ACTIVATE_PAN - ACTIVATE_PAN_ANIMATION_DELAY }), ) pressProgress.value = progress activationProgress.value = progress @@ -162,7 +153,7 @@ function SortableGridItem({ pressProgress, scrollY, startScrollOffset, - ] + ], ) // ITEM POSITIONING AND ANIMATION @@ -170,11 +161,7 @@ function SortableGridItem({ // INITIAL RENDER // (relative placements - no absolute positioning yet) // This ensures there is no blank space when grid items are being measured - if ( - !initialRenderCompleted.value || - appliedContainerHeight.value === -1 || - columnWidth.value === -1 - ) { + if (!initialRenderCompleted.value || appliedContainerHeight.value === -1 || columnWidth.value === -1) { return { width: `${100 / numColumns}%`, } @@ -217,12 +204,7 @@ function SortableGridItem({ top: y, left: x, width: columnWidth.value, - zIndex: getItemZIndex( - isActive.value, - pressProgress.value, - { x, y }, - targetItemPosition.value - ), + zIndex: getItemZIndex(isActive.value, pressProgress.value, { x, y }, targetItemPosition.value), } }) @@ -234,7 +216,7 @@ function SortableGridItem({ shadowColor: interpolateColor( pressProgress.value, [0, 1], - ['transparent', `rgba(0, 0, 0, ${activeItemShadowOpacity.value})`] + ['transparent', `rgba(0, 0, 0, ${activeItemShadowOpacity.value})`], ), })) @@ -245,15 +227,11 @@ function SortableGridItem({ pressProgress, dragActivationProgress: activationProgress, }), - [item, renderItem, activationProgress, pressProgress] + [item, renderItem, activationProgress, pressProgress], ) return ( - + {content} diff --git a/apps/mobile/src/components/sortableGrid/contexts/AutoScrollContextProvider.tsx b/apps/mobile/src/components/sortableGrid/contexts/AutoScrollContextProvider.tsx index d1a79b3ec8a..a04293a8def 100644 --- a/apps/mobile/src/components/sortableGrid/contexts/AutoScrollContextProvider.tsx +++ b/apps/mobile/src/components/sortableGrid/contexts/AutoScrollContextProvider.tsx @@ -1,12 +1,6 @@ import { PropsWithChildren, createContext, useContext, useMemo, useRef } from 'react' import { View } from 'react-native' -import { - SharedValue, - runOnJS, - useAnimatedReaction, - useDerivedValue, - useSharedValue, -} from 'react-native-reanimated' +import { SharedValue, runOnJS, useAnimatedReaction, useDerivedValue, useSharedValue } from 'react-native-reanimated' import { AUTO_SCROLL_THRESHOLD } from 'src/components/sortableGrid/constants' import { useDragContext } from 'src/components/sortableGrid/contexts/DragContextProvider' import { useLayoutContext } from 'src/components/sortableGrid/contexts/LayoutContextProvider' @@ -54,7 +48,7 @@ export function AutoScrollProvider({ const scrollTarget = useSharedValue(0) const scrollDirection = useSharedValue(0) // 1 = down, -1 = up const activeItemHeight = useDerivedValue( - () => (activeItemKey.value ? itemDimensions.value[activeItemKey.value]?.height : -1) ?? -1 + () => (activeItemKey.value ? itemDimensions.value[activeItemKey.value]?.height : -1) ?? -1, ) // REFS @@ -64,13 +58,11 @@ export function AutoScrollProvider({ // Values used to scroll the container to the proper offset // (updated from the SortableGridInner component) const containerStartOffset = useSharedValue(0) - const containerEndOffset = useDerivedValue( - () => containerStartOffset.value + targetContainerHeight.value - ) + const containerEndOffset = useDerivedValue(() => containerStartOffset.value + targetContainerHeight.value) const startScrollOffset = useSharedValue(0) const scrollOffsetDiff = useDerivedValue(() => - activeItemKey.value === null ? 0 : scrollYValue.value - startScrollOffset.value + activeItemKey.value === null ? 0 : scrollYValue.value - startScrollOffset.value, ) /** @@ -98,7 +90,7 @@ export function AutoScrollProvider({ () => { // Reset when the active index changes scrollDirection.value = 0 - } + }, ) // AUTO SCROLL HANDLER @@ -110,8 +102,7 @@ export function AutoScrollProvider({ } return { - itemAbsoluteY: - activeItemPosition.value.y + containerStartOffset.value + scrollOffsetDiff.value, + itemAbsoluteY: activeItemPosition.value.y + containerStartOffset.value + scrollOffsetDiff.value, activeHeight: activeItemHeight.value, minOffset: containerStartOffset.value, maxOffset: containerEndOffset.value - visibleHeightValue.value, @@ -146,10 +137,7 @@ export function AutoScrollProvider({ // If the active item is above the current scroll position (with small threshold // to start scrolling earlier) and the scroll position is not at the top of the // grid, scroll up - if ( - itemAbsoluteY < scrollY + AUTO_SCROLL_THRESHOLD && - scrollY > minOffset - AUTO_SCROLL_THRESHOLD - ) { + if (itemAbsoluteY < scrollY + AUTO_SCROLL_THRESHOLD && scrollY > minOffset - AUTO_SCROLL_THRESHOLD) { currentScrollTarget = Math.max(minOffset - AUTO_SCROLL_THRESHOLD, scrollY - activeHeight) currentScrollDirection = -1 } @@ -184,7 +172,7 @@ export function AutoScrollProvider({ scrollDirection.value = currentScrollDirection scrollTarget.value = currentScrollTarget runOnJS(scrollToOffset)(currentScrollTarget) - } + }, ) /** @@ -199,14 +187,7 @@ export function AutoScrollProvider({ startScrollOffset, scrollY: scrollYValue, }), - [ - gridContainerRef, - containerStartOffset, - containerEndOffset, - scrollOffsetDiff, - startScrollOffset, - scrollYValue, - ] + [gridContainerRef, containerStartOffset, containerEndOffset, scrollOffsetDiff, startScrollOffset, scrollYValue], ) return {children} diff --git a/apps/mobile/src/components/sortableGrid/contexts/DragContextProvider.tsx b/apps/mobile/src/components/sortableGrid/contexts/DragContextProvider.tsx index 4b1e7650448..b079e6a0be3 100644 --- a/apps/mobile/src/components/sortableGrid/contexts/DragContextProvider.tsx +++ b/apps/mobile/src/components/sortableGrid/contexts/DragContextProvider.tsx @@ -1,11 +1,5 @@ import { PropsWithChildren, createContext, useContext, useMemo } from 'react' -import { - SharedValue, - runOnJS, - useAnimatedReaction, - useDerivedValue, - useSharedValue, -} from 'react-native-reanimated' +import { SharedValue, runOnJS, useAnimatedReaction, useDerivedValue, useSharedValue } from 'react-native-reanimated' import { useLayoutContext } from 'src/components/sortableGrid/contexts/LayoutContextProvider' import { useStableCallback } from 'src/components/sortableGrid/hooks' import { @@ -92,22 +86,20 @@ export function DragContextProvider({ /** * HANDLERS */ - const handleDragStart = useStableCallback( - async (key: string, keyToIdx: Record) => { - const index = keyToIdx[key] - if (index === undefined) { - return - } - const item = data[index] - if (hapticFeedback) { - await HapticFeedback.impact(ImpactFeedbackStyle.Heavy) - } - if (!onDragStart || !item) { - return - } - onDragStart({ index, item }) + const handleDragStart = useStableCallback(async (key: string, keyToIdx: Record) => { + const index = keyToIdx[key] + if (index === undefined) { + return } - ) + const item = data[index] + if (hapticFeedback) { + await HapticFeedback.impact(ImpactFeedbackStyle.Heavy) + } + if (!onDragStart || !item) { + return + } + onDragStart({ index, item }) + }) const handleDrop = useStableCallback((key: string, keyToIdx: Record) => { const index = keyToIdx[key] @@ -121,39 +113,37 @@ export function DragContextProvider({ onDrop({ index, item }) }) - const handleChange = useStableCallback( - async (swappedKey: string, keyToIdx: Record) => { - if (!onChange) { - return - } - const toIndex = keyToIdx[swappedKey] - if (toIndex === undefined) { - return - } - const fromIndex = itemKeys.indexOf(swappedKey) + const handleChange = useStableCallback(async (swappedKey: string, keyToIdx: Record) => { + if (!onChange) { + return + } + const toIndex = keyToIdx[swappedKey] + if (toIndex === undefined) { + return + } + const fromIndex = itemKeys.indexOf(swappedKey) - const reorderedData: I[] = [] + const reorderedData: I[] = [] - if (hapticFeedback) { - await HapticFeedback.impact(ImpactFeedbackStyle.Medium) - } + if (hapticFeedback) { + await HapticFeedback.impact(ImpactFeedbackStyle.Medium) + } - for (let i = 0; i < data.length; i++) { - const item = data[i] - if (!item) { - return - } - const itemKey = keyExtractor(item, i) - const index = keyToIdx[itemKey] - if (index === undefined) { - return - } - reorderedData[index] = item + for (let i = 0; i < data.length; i++) { + const item = data[i] + if (!item) { + return } - - onChange({ data: reorderedData, fromIndex, toIndex }) + const itemKey = keyExtractor(item, i) + const index = keyToIdx[itemKey] + if (index === undefined) { + return + } + reorderedData[index] = item } - ) + + onChange({ data: reorderedData, fromIndex, toIndex }) + }) /** * REACTIONS @@ -172,7 +162,7 @@ export function DragContextProvider({ prevActiveItemKey.value = key } }, - [handleDragStart, handleChange] + [handleDragStart, handleChange], ) // Handle drop (after animation of the active item is finished @@ -184,7 +174,7 @@ export function DragContextProvider({ runOnJS(handleDrop)(prevActiveItemKey.value, keyToIndex.value) } }, - [handleDrop] + [handleDrop], ) /** @@ -210,7 +200,7 @@ export function DragContextProvider({ activeItemScale, activeItemOpacity, activeItemShadowOpacity, - ] + ], ) return {children} diff --git a/apps/mobile/src/components/sortableGrid/contexts/LayoutContextProvider.tsx b/apps/mobile/src/components/sortableGrid/contexts/LayoutContextProvider.tsx index 849bd2dc689..c60f573d536 100644 --- a/apps/mobile/src/components/sortableGrid/contexts/LayoutContextProvider.tsx +++ b/apps/mobile/src/components/sortableGrid/contexts/LayoutContextProvider.tsx @@ -73,15 +73,11 @@ export function LayoutContextProvider({ const containerWidth = useSharedValue(-1) const containerHeight = useSharedValue(-1) const targetContainerHeight = useSharedValue(-1) - const columnWidth = useDerivedValue(() => - containerWidth.value === -1 ? -1 : containerWidth.value / numColumns - ) + const columnWidth = useDerivedValue(() => (containerWidth.value === -1 ? -1 : containerWidth.value / numColumns)) // KEY-INDEX MAPPINGS const indexToKey = useSharedValue([]) - const keyToIndex = useDerivedValue(() => - Object.fromEntries(indexToKey.value.map((key, index) => [key, index])) - ) + const keyToIndex = useDerivedValue(() => Object.fromEntries(indexToKey.value.map((key, index) => [key, index]))) // POSITIONING const itemPositions = useDerivedValue>(() => { @@ -98,7 +94,7 @@ export function LayoutContextProvider({ x: columnWidth.value * getColumnIndex(parseInt(index, 10), numColumns), y: rowOffsets.value[getRowIndex(parseInt(index, 10), numColumns)] ?? 0, }, - ]) + ]), ) }, [rowsCount, columnWidth, rowOffsets, indexToKey, numColumns]) @@ -125,7 +121,7 @@ export function LayoutContextProvider({ itemDimensions.value = { ...itemDimensions.value } } }, - [itemKeys] + [itemKeys], ) // ROW OFFSETS UPDATER @@ -144,7 +140,7 @@ export function LayoutContextProvider({ const rowIndex = getRowIndex(parseInt(itemIndex, 10), numColumns) offsets[rowIndex + 1] = Math.max( offsets[rowIndex + 1] ?? 0, - (offsets[rowIndex] ?? 0) + (dimensions[key]?.height ?? 0) + (offsets[rowIndex] ?? 0) + (dimensions[key]?.height ?? 0), ) } // Update row offsets only if they have changed @@ -155,7 +151,7 @@ export function LayoutContextProvider({ rowOffsets.value = offsets } }, - [numColumns] + [numColumns], ) useAnimatedReaction( @@ -183,7 +179,7 @@ export function LayoutContextProvider({ containerHeight.value = newHeight } }, - [animateContainerHeight] + [animateContainerHeight], ) /** @@ -217,7 +213,7 @@ export function LayoutContextProvider({ keyToIndex, indexToKey, itemPositions, - ] + ], ) return {children} diff --git a/apps/mobile/src/components/sortableGrid/hooks.ts b/apps/mobile/src/components/sortableGrid/hooks.ts index 879e679246f..d9ad7a31c6e 100644 --- a/apps/mobile/src/components/sortableGrid/hooks.ts +++ b/apps/mobile/src/components/sortableGrid/hooks.ts @@ -1,22 +1,12 @@ import { useCallback, useRef } from 'react' -import { - SharedValue, - runOnJS, - useAnimatedReaction, - useSharedValue, - withTiming, -} from 'react-native-reanimated' -import { - useAutoScrollContext, - useDragContext, - useLayoutContext, -} from 'src/components/sortableGrid/contexts' +import { SharedValue, runOnJS, useAnimatedReaction, useSharedValue, withTiming } from 'react-native-reanimated' +import { useAutoScrollContext, useDragContext, useLayoutContext } from 'src/components/sortableGrid/contexts' import { getColumnIndex, getRowIndex } from 'src/components/sortableGrid/utils' import { HapticFeedback, ImpactFeedbackStyle } from 'ui/src' export function useStableCallback< // eslint-disable-next-line @typescript-eslint/no-explicit-any - C extends (...args: Array) => any + C extends (...args: Array) => any, >(callback?: C): C { const callbackRef = useRef(callback) callbackRef.current = callback @@ -24,7 +14,7 @@ export function useStableCallback< return useCallback( // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return (...args: Array) => callbackRef.current?.(...args), - [] + [], ) as C } @@ -51,7 +41,7 @@ export function useItemPosition(key: string): { x.value = x.value === null ? position.x : withTiming(position.x) y.value = y.value === null ? position.y : withTiming(position.y) }, - [key] + [key], ) useAnimatedReaction( @@ -64,15 +54,14 @@ export function useItemPosition(key: string): { x.value = position.x y.value = position.y + offsetDiff } - } + }, ) return { x, y } } export function useItemOrderUpdater(numColumns: number, hapticFeedback: boolean): void { - const { keyToIndex, indexToKey, rowOffsets, targetContainerHeight, itemDimensions } = - useLayoutContext() + const { keyToIndex, indexToKey, rowOffsets, targetContainerHeight, itemDimensions } = useLayoutContext() const { activeItemKey, activeItemPosition } = useDragContext() const { scrollOffsetDiff } = useAutoScrollContext() @@ -120,11 +109,7 @@ export function useItemOrderUpdater(numColumns: number, hapticFeedback: boolean) let dy = 0 if (yOffsetAbove > 0 && centerY < yOffsetAbove) { dy = -1 - } else if ( - yOffsetBelow !== undefined && - yOffsetBelow < targetContainerHeight.value && - centerY > yOffsetBelow - ) { + } else if (yOffsetBelow !== undefined && yOffsetBelow < targetContainerHeight.value && centerY > yOffsetBelow) { dy = 1 } @@ -132,11 +117,7 @@ export function useItemOrderUpdater(numColumns: number, hapticFeedback: boolean) let dx = 0 if (xOffsetLeft > 0 && centerX < xOffsetLeft) { dx = -1 - } else if ( - columnIndex < numColumns - 1 && - activeIndex < itemsCount && - centerX > xOffsetRight - ) { + } else if (columnIndex < numColumns - 1 && activeIndex < itemsCount && centerX > xOffsetRight) { dx = 1 } @@ -168,6 +149,6 @@ export function useItemOrderUpdater(numColumns: number, hapticFeedback: boolean) runOnJS(vibrate)() } }, - [hapticFeedback] + [hapticFeedback], ) } diff --git a/apps/mobile/src/components/sortableGrid/utils.ts b/apps/mobile/src/components/sortableGrid/utils.ts index e12e24aaa35..e336f1039c1 100644 --- a/apps/mobile/src/components/sortableGrid/utils.ts +++ b/apps/mobile/src/components/sortableGrid/utils.ts @@ -1,20 +1,11 @@ import { Vector } from 'src/components/sortableGrid/types' -export const areArraysDifferent = ( - arr1: T[], - arr2: T[], - areEqual = (a: T, b: T): boolean => a === b -): boolean => { +export const areArraysDifferent = (arr1: T[], arr2: T[], areEqual = (a: T, b: T): boolean => a === b): boolean => { 'worklet' - return ( - arr1.length !== arr2.length || arr1.some((item, index) => !areEqual(item, arr2[index] as T)) - ) + return arr1.length !== arr2.length || arr1.some((item, index) => !areEqual(item, arr2[index] as T)) } -const hasProp = ( - object: O, - prop: P -): object is O & Record => { +const hasProp = (object: O, prop: P): object is O & Record => { return prop in object } @@ -45,11 +36,7 @@ export const getColumnIndex = (index: number, numColumns: number): number => { return index % numColumns } -export const getItemsInColumnCount = ( - index: number, - numColumns: number, - itemsCount: number -): number => { +export const getItemsInColumnCount = (index: number, numColumns: number, itemsCount: number): number => { 'worklet' const columnIndex = getColumnIndex(index, numColumns) return Math.floor(itemsCount / numColumns) + (columnIndex < itemsCount % numColumns ? 1 : 0) @@ -59,7 +46,7 @@ export const getItemZIndex = ( isActive: boolean, pressProgress: number, position: Vector, - targetPosition?: Vector + targetPosition?: Vector, ): number => { 'worklet' if (isActive) { diff --git a/apps/mobile/src/components/text/AnimatedText.tsx b/apps/mobile/src/components/text/AnimatedText.tsx index 4cb62cad7dd..312cf355797 100644 --- a/apps/mobile/src/components/text/AnimatedText.tsx +++ b/apps/mobile/src/components/text/AnimatedText.tsx @@ -1,11 +1,5 @@ import React from 'react' -import { - TextProps as RNTextProps, - StyleSheet, - TextInput, - TextInputProps, - useWindowDimensions, -} from 'react-native' +import { TextProps as RNTextProps, StyleSheet, TextInput, TextInputProps, useWindowDimensions } from 'react-native' import Animated, { useAnimatedProps } from 'react-native-reanimated' import { Flex, TextProps as TamaTextProps, TextFrame, usePropsAndStyle } from 'ui/src' import { TextLoaderWrapper } from 'ui/src/components/text/Text' @@ -58,9 +52,7 @@ export const BaseAnimatedText = ({ {/* Use the text component to properly calculate the width of the loading shimmer. An input component with a width dependent on the length of the content was sometimes rendered with a very small width regardless of the text passed as a value */} - - {loadingPlaceholderText} - + {loadingPlaceholderText} ) @@ -96,7 +88,7 @@ export const AnimatedText = ({ style, ...propsIn }: TextProps): JSX.Element => { }, { forComponent: TextFrame, - } + }, ) const { fontScale } = useWindowDimensions() diff --git a/apps/mobile/src/components/text/DecimalNumber.test.tsx b/apps/mobile/src/components/text/DecimalNumber.test.tsx index 626917cff85..85c77a6fa9e 100644 --- a/apps/mobile/src/components/text/DecimalNumber.test.tsx +++ b/apps/mobile/src/components/text/DecimalNumber.test.tsx @@ -3,17 +3,13 @@ import { DecimalNumber } from 'src/components/text/DecimalNumber' import { render } from 'src/test/test-utils' it('renders a DecimalNumber', () => { - const tree = render( - - ) + const tree = render() expect(tree).toMatchSnapshot() }) it('renders a DecimalNumber without a comma separator', () => { - const tree = render( - - ) + const tree = render() expect(tree).toMatchSnapshot() }) diff --git a/apps/mobile/src/components/text/DecimalNumber.tsx b/apps/mobile/src/components/text/DecimalNumber.tsx index c8d669f63b2..8fc2e69a730 100644 --- a/apps/mobile/src/components/text/DecimalNumber.tsx +++ b/apps/mobile/src/components/text/DecimalNumber.tsx @@ -24,18 +24,13 @@ export function DecimalNumber({ }: DecimalNumberProps): JSX.Element { const [pre, post] = formattedNumber.split(separator) - const decimalPartColor = - number === undefined || number >= decimalThreshold ? '$neutral3' : '$neutral1' + const decimalPartColor = number === undefined || number >= decimalThreshold ? '$neutral3' : '$neutral1' return ( {pre} {post && ( - + {separator} {post} diff --git a/apps/mobile/src/components/text/LongMarkdownText.test.tsx b/apps/mobile/src/components/text/LongMarkdownText.test.tsx index c6adee187cd..91b68e5d156 100644 --- a/apps/mobile/src/components/text/LongMarkdownText.test.tsx +++ b/apps/mobile/src/components/text/LongMarkdownText.test.tsx @@ -67,7 +67,7 @@ describe(LongMarkdownText, () => { // props are at index 0, ref is at index 1 expect(MockedMarkdown.mock.lastCall[0]).toEqual( - getMarkdownPropsWithHeight('auto') // height auto means the text doesn't exceed the limit + getMarkdownPropsWithHeight('auto'), // height auto means the text doesn't exceed the limit ) }) @@ -89,7 +89,7 @@ describe(LongMarkdownText, () => { measureMarkdown(tree, 5) // Assume Some very long text is five lines expect(MockedMarkdown.mock.lastCall[0]).toEqual( - getMarkdownPropsWithHeight(LINE_HEIGHT * 3) // Height is limited to 3 lines + getMarkdownPropsWithHeight(LINE_HEIGHT * 3), // Height is limited to 3 lines ) }) @@ -115,7 +115,7 @@ describe(LongMarkdownText, () => { fireEvent.press(readMoreButton) expect(MockedMarkdown.mock.lastCall[0]).toEqual( - getMarkdownPropsWithHeight('auto') // height auto means the text doesn't exceed the limit + getMarkdownPropsWithHeight('auto'), // height auto means the text doesn't exceed the limit ) }) @@ -142,13 +142,13 @@ describe(LongMarkdownText, () => { fireEvent.press(readMoreButton) // expand expect(MockedMarkdown.mock.lastCall[0]).toEqual( - getMarkdownPropsWithHeight('auto') // height auto means the text doesn't exceed the limit + getMarkdownPropsWithHeight('auto'), // height auto means the text doesn't exceed the limit ) fireEvent.press(readMoreButton) // collapse expect(MockedMarkdown.mock.lastCall[0]).toEqual( - getMarkdownPropsWithHeight(LINE_HEIGHT * 3) // Height is limited to 3 lines + getMarkdownPropsWithHeight(LINE_HEIGHT * 3), // Height is limited to 3 lines ) }) }) diff --git a/apps/mobile/src/components/text/LongMarkdownText.tsx b/apps/mobile/src/components/text/LongMarkdownText.tsx index 491fda5507c..7905472efd0 100644 --- a/apps/mobile/src/components/text/LongMarkdownText.tsx +++ b/apps/mobile/src/components/text/LongMarkdownText.tsx @@ -4,7 +4,7 @@ import { LayoutChangeEvent } from 'react-native' import Markdown, { MarkdownProps } from 'react-native-markdown-display' import { Flex, SpaceTokens, Text, useSporeColors } from 'ui/src' import { fonts } from 'ui/src/theme' -import { openUri } from 'wallet/src/utils/linking' +import { openUri } from 'uniswap/src/utils/linking' type LongMarkdownTextProps = { initialDisplayedLines?: number @@ -48,7 +48,7 @@ export function LongMarkdownText(props: LongMarkdownTextProps): JSX.Element { toggleExpanded() initialContentHeightRef.current = textContentHeight }, - [initialDisplayedLines, textLineHeight] + [initialDisplayedLines, textLineHeight], ) const codeStyle = { backgroundColor: codeBackgroundColor, borderColor: 'transparent' } @@ -86,7 +86,8 @@ export function LongMarkdownText(props: LongMarkdownTextProps): JSX.Element { }, }): void => { setTextLineHeight(height) - }}> + }} + > + onPress={toggleExpanded} + > {expanded ? t('common.longText.button.less') : t('common.longText.button.more')} ) : null} diff --git a/apps/mobile/src/components/text/LongText.tsx b/apps/mobile/src/components/text/LongText.tsx index b9f727d15fc..77917601f2f 100644 --- a/apps/mobile/src/components/text/LongText.tsx +++ b/apps/mobile/src/components/text/LongText.tsx @@ -13,10 +13,7 @@ type LongTextProps = { codeBackgroundColor?: string readMoreOrLessColor?: string variant?: keyof typeof fonts -} & Omit< - ComponentProps, - 'children' | 'numberOfLines' | 'onTextLayout' | 'color' | 'variant' -> +} & Omit, 'children' | 'numberOfLines' | 'onTextLayout' | 'color' | 'variant'> export function LongText(props: LongTextProps): JSX.Element { const colors = useSporeColors() @@ -45,7 +42,7 @@ export function LongText(props: LongTextProps): JSX.Element { setExpanded(false) isInitializedRef.current = true }, - [initialDisplayedLines] + [initialDisplayedLines], ) return ( @@ -55,7 +52,8 @@ export function LongText(props: LongTextProps): JSX.Element { style={{ color }} variant={variant} onTextLayout={onTextLayout} - {...rest}> + {...rest} + > {text} @@ -68,7 +66,8 @@ export function LongText(props: LongTextProps): JSX.Element { style={{ color: readMoreOrLessColor }} testID="read-more-button" variant="buttonLabel3" - onPress={(): void => setExpanded(!expanded)}> + onPress={(): void => setExpanded(!expanded)} + > {expanded ? t('common.longText.button.less') : t('common.longText.button.more')} ) : null} diff --git a/apps/mobile/src/components/text/Pill.stories.mdx b/apps/mobile/src/components/text/Pill.stories.mdx index 0ebac543b18..fedd2e25f5c 100644 --- a/apps/mobile/src/components/text/Pill.stories.mdx +++ b/apps/mobile/src/components/text/Pill.stories.mdx @@ -14,7 +14,8 @@ export const Template = (args) => void 0} /> args={{ label: 'Your tokens', borderColor: 'sporeBlack', - }}> + }} + > {Template.bind({})} diff --git a/apps/mobile/src/components/text/TextWithFuseMatches.test.tsx b/apps/mobile/src/components/text/TextWithFuseMatches.test.tsx index d7c5a8a4e67..11a4d6252c6 100644 --- a/apps/mobile/src/components/text/TextWithFuseMatches.test.tsx +++ b/apps/mobile/src/components/text/TextWithFuseMatches.test.tsx @@ -15,7 +15,7 @@ it('renders text with few matches', () => { { value: 'xt wit', indices: [[4, 8]] }, ]} text="A text without matches" - /> + />, ) expect(tree).toMatchSnapshot() }) diff --git a/apps/mobile/src/components/tokens/TokenMetadata.tsx b/apps/mobile/src/components/tokens/TokenMetadata.tsx index 9a65ae260d3..ec5fcaed49d 100644 --- a/apps/mobile/src/components/tokens/TokenMetadata.tsx +++ b/apps/mobile/src/components/tokens/TokenMetadata.tsx @@ -7,10 +7,7 @@ type TokenMetadataProps = PropsWithChildren<{ }> /** Helper component to format rhs metadata for a given token. */ -export const TokenMetadata = ({ - children, - align = 'flex-end', -}: TokenMetadataProps): JSX.Element => { +export const TokenMetadata = ({ children, align = 'flex-end' }: TokenMetadataProps): JSX.Element => { return ( diff --git a/apps/mobile/src/components/tooltip/TooltipButton.tsx b/apps/mobile/src/components/tooltip/TooltipButton.tsx index a73bdaefd47..e566ddb49fb 100644 --- a/apps/mobile/src/components/tooltip/TooltipButton.tsx +++ b/apps/mobile/src/components/tooltip/TooltipButton.tsx @@ -38,7 +38,8 @@ export function TooltipInfoButton({ Keyboard.dismiss() setShowModal(true) }} - {...rest}> + {...rest} + > setShowModal(false)}> + onClose={(): void => setShowModal(false)} + > {modalContent ?? null} )} diff --git a/apps/mobile/src/components/unitags/ChangeUnitagModal.tsx b/apps/mobile/src/components/unitags/ChangeUnitagModal.tsx index 185489867b4..bacaa3e5156 100644 --- a/apps/mobile/src/components/unitags/ChangeUnitagModal.tsx +++ b/apps/mobile/src/components/unitags/ChangeUnitagModal.tsx @@ -3,15 +3,17 @@ import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { ActivityIndicator, EmitterSubscription, Keyboard } from 'react-native' import { getUniqueId } from 'react-native-device-info' +import { useDispatch } from 'react-redux' import { Button, Flex, Text, useSporeColors } from 'ui/src' import { AlertTriangle } from 'ui/src/components/icons' import { fonts, spacing } from 'ui/src/theme' import { TextInput } from 'uniswap/src/components/input/TextInput' import { BottomSheetModal } from 'uniswap/src/components/modals/BottomSheetModal' -import { ElementName, ModalName, UnitagEventName } from 'uniswap/src/features/telemetry/constants' +import { ModalName, UnitagEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { useUnitagUpdater } from 'uniswap/src/features/unitags/context' import { UnitagErrorCodes } from 'uniswap/src/features/unitags/types' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { logger } from 'utilities/src/logger/logger' import { isIOS } from 'utilities/src/platform' import { useAsyncData } from 'utilities/src/react/hooks' @@ -23,7 +25,6 @@ import { useCanAddressClaimUnitag, useCanClaimUnitagName } from 'wallet/src/feat import { parseUnitagErrorCode } from 'wallet/src/features/unitags/utils' import { useWalletSigners } from 'wallet/src/features/wallet/context' import { useAccount } from 'wallet/src/features/wallet/hooks' -import { useAppDispatch } from 'wallet/src/state' export function ChangeUnitagModal({ unitag, @@ -37,7 +38,7 @@ export function ChangeUnitagModal({ const { t } = useTranslation() const colors = useSporeColors() const navigation = useNavigation() - const dispatch = useAppDispatch() + const dispatch = useDispatch() const { data: deviceId } = useAsyncData(getUniqueId) const account = useAccount(address) const signerManager = useWalletSigners() @@ -49,16 +50,16 @@ export function ChangeUnitagModal({ const [isChangeResponseLoading, setIsChangeResponseLoading] = useState(false) const [unitagToCheck, setUnitagToCheck] = useState(unitag) - const { error: canClaimUnitagNameError, loading: loadingUnitagErrorCheck } = - useCanClaimUnitagName(address, unitagToCheck) + const { error: canClaimUnitagNameError, loading: loadingUnitagErrorCheck } = useCanClaimUnitagName( + address, + unitagToCheck, + ) const { errorCode } = useCanAddressClaimUnitag(address, true) const { triggerRefetchUnitags } = useUnitagUpdater() const isUnitagEdited = unitag !== newUnitag - const isUnitagInvalid = - newUnitag === unitagToCheck && !!canClaimUnitagNameError && !loadingUnitagErrorCheck - const isUnitagValid = - isUnitagEdited && !canClaimUnitagNameError && !loadingUnitagErrorCheck && !!newUnitag + const isUnitagInvalid = newUnitag === unitagToCheck && !!canClaimUnitagNameError && !loadingUnitagErrorCheck + const isUnitagValid = isUnitagEdited && !canClaimUnitagNameError && !loadingUnitagErrorCheck && !!newUnitag const hasReachedAddressLimit = errorCode === UnitagErrorCodes.AddressLimitReached const isSubmitButtonDisabled = isCheckingUnitag || @@ -116,7 +117,7 @@ export function ChangeUnitagModal({ pushNotification({ type: AppNotificationType.Error, errorMessage: parseUnitagErrorCode(t, unitagToCheck, changeResponse.errorCode), - }) + }), ) return } @@ -129,7 +130,7 @@ export function ChangeUnitagModal({ pushNotification({ type: AppNotificationType.Success, title: t('unitags.notification.username.title'), - }) + }), ) navigation.goBack() onClose() @@ -143,7 +144,7 @@ export function ChangeUnitagModal({ pushNotification({ type: AppNotificationType.Error, errorMessage: t('unitags.notification.username.error'), - }) + }), ) onClose() setIsChangeResponseLoading(false) @@ -194,9 +195,7 @@ export function ChangeUnitagModal({ return ( <> - {showConfirmModal && ( - - )} + {showConfirmModal && } 0 ? keyboardHeight - spacing.spacing20 : '$spacing12'} pt="$spacing12" - px="$spacing24"> + px="$spacing24" + > {t('unitags.editUsername.title')} @@ -214,7 +214,8 @@ export function ChangeUnitagModal({ borderColor="$surface3" borderRadius="$rounded16" borderWidth="$spacing1" - px="$spacing24"> + px="$spacing24" + > + width="100%" + > {t('unitags.editUsername.warning.max')} ) : ( - + {t('unitags.editUsername.warning.default')} @@ -272,9 +269,10 @@ export function ChangeUnitagModal({ - diff --git a/apps/mobile/src/components/unitags/ChooseNftModal.tsx b/apps/mobile/src/components/unitags/ChooseNftModal.tsx index ddd6f8cb3db..14928d47672 100644 --- a/apps/mobile/src/components/unitags/ChooseNftModal.tsx +++ b/apps/mobile/src/components/unitags/ChooseNftModal.tsx @@ -31,7 +31,8 @@ export const ChooseNftModal = ({ address, setPhotoUri, onClose }: ChooseNftProps hideHandlebar={false} isDismissible={true} name={ModalName.NftCollection} - onClose={onClose}> + onClose={onClose} + > + onClose={onClose} + > {options.map((option) => ( @@ -114,19 +115,17 @@ const ChoosePhotoOption = ({ type }: { type: PhotoAction }): JSX.Element => { borderRadius="$rounded20" gap="$spacing16" justifyContent="flex-start" - p="$spacing24"> - {type === PhotoAction.BrowseCameraRoll && ( - - )} + p="$spacing24" + > + {type === PhotoAction.BrowseCameraRoll && } {type === PhotoAction.BrowseNftsList && } - {type === PhotoAction.RemovePhoto && ( - - )} + {type === PhotoAction.RemovePhoto && } + variant="buttonLabel2" + > {type === PhotoAction.BrowseCameraRoll && t('unitags.choosePhoto.option.cameraRoll')} {type === PhotoAction.BrowseNftsList && t('unitags.choosePhoto.option.nft')} {type === PhotoAction.RemovePhoto && t('unitags.choosePhoto.option.remove')} diff --git a/apps/mobile/src/components/unitags/DeleteUnitagModal.tsx b/apps/mobile/src/components/unitags/DeleteUnitagModal.tsx index bb04b14ae66..a9782c14530 100644 --- a/apps/mobile/src/components/unitags/DeleteUnitagModal.tsx +++ b/apps/mobile/src/components/unitags/DeleteUnitagModal.tsx @@ -2,20 +2,21 @@ import { useNavigation } from '@react-navigation/native' import { useState } from 'react' import { useTranslation } from 'react-i18next' import { ActivityIndicator } from 'react-native' +import { useDispatch } from 'react-redux' import { Button, Flex, Text, useSporeColors } from 'ui/src' import { AlertTriangle } from 'ui/src/components/icons' import { fonts } from 'ui/src/theme' import { BottomSheetModal } from 'uniswap/src/components/modals/BottomSheetModal' -import { ElementName, ModalName, UnitagEventName } from 'uniswap/src/features/telemetry/constants' +import { ModalName, UnitagEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { useUnitagUpdater } from 'uniswap/src/features/unitags/context' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { logger } from 'utilities/src/logger/logger' import { pushNotification } from 'wallet/src/features/notifications/slice' import { AppNotificationType } from 'wallet/src/features/notifications/types' import { deleteUnitag } from 'wallet/src/features/unitags/api' import { useWalletSigners } from 'wallet/src/features/wallet/context' import { useAccount } from 'wallet/src/features/wallet/hooks' -import { useAppDispatch } from 'wallet/src/state' export function DeleteUnitagModal({ unitag, @@ -29,7 +30,7 @@ export function DeleteUnitagModal({ const { t } = useTranslation() const colors = useSporeColors() const navigation = useNavigation() - const dispatch = useAppDispatch() + const dispatch = useDispatch() const { triggerRefetchUnitags } = useUnitagUpdater() const account = useAccount(address) const signerManager = useWalletSigners() @@ -41,7 +42,7 @@ export function DeleteUnitagModal({ pushNotification({ type: AppNotificationType.Error, errorMessage: t('unitags.notification.delete.error'), - }) + }), ) onClose() } @@ -68,7 +69,7 @@ export function DeleteUnitagModal({ pushNotification({ type: AppNotificationType.Success, title: t('unitags.notification.delete.title'), - }) + }), ) navigation.goBack() onClose() @@ -90,7 +91,8 @@ export function DeleteUnitagModal({ borderRadius="$rounded12" height="$spacing48" mb="$spacing8" - minWidth="$spacing48"> + minWidth="$spacing48" + > @@ -100,12 +102,7 @@ export function DeleteUnitagModal({ {t('unitags.delete.confirm.subtitle')} - + ) +} diff --git a/apps/mobile/src/features/CloudBackup/CloudBackupForm/PasswordInput.tsx b/apps/mobile/src/features/CloudBackup/CloudBackupForm/PasswordInput.tsx new file mode 100644 index 00000000000..1726249dea1 --- /dev/null +++ b/apps/mobile/src/features/CloudBackup/CloudBackupForm/PasswordInput.tsx @@ -0,0 +1,93 @@ +import { useRef } from 'react' +import { useTranslation } from 'react-i18next' +import { TextInput } from 'react-native' +import { PasswordInput as Input } from 'src/components/input/PasswordInput' +import { useCloudBackupPasswordFormContext } from 'src/features/CloudBackup/CloudBackupForm/CloudBackupPasswordFormContext' +import { PasswordError } from 'src/features/onboarding/PasswordError' +import { Flex, Text } from 'ui/src' +import { DiamondExclamation } from 'ui/src/components/icons' +import { iconSizes } from 'ui/src/theme' +import { useDebounce } from 'utilities/src/time/timing' +import { + PASSWORD_VALIDATION_DEBOUNCE_MS, + PasswordErrors, + PasswordStrength, + getPasswordStrengthTextAndColor, +} from 'wallet/src/utils/password' + +export function PasswordInput(): JSX.Element { + const { password, error, passwordStrength, isConfirmation, onPasswordChangeText, onPasswordSubmitEditing } = + useCloudBackupPasswordFormContext() + const debouncedPasswordStrength = useDebounce(passwordStrength, PASSWORD_VALIDATION_DEBOUNCE_MS) + + const { t } = useTranslation() + const passwordInputRef = useRef(null) + + let errorText = '' + if (error === PasswordErrors.PasswordsDoNotMatch) { + errorText = t('settings.setting.backup.password.error.mismatch') + } else if (error) { + // use the upstream zxcvbn error message + errorText = error + } + + return ( + + + { + onPasswordChangeText(newText) + }} + onSubmitEditing={onPasswordSubmitEditing} + /> + {!isConfirmation && } + {error ? : null} + + {!isConfirmation && ( + + + + {t('settings.setting.backup.password.disclaimer')} + + + )} + + ) +} + +function PasswordStrengthText({ strength }: { strength: PasswordStrength }): JSX.Element { + const { t } = useTranslation() + const { color } = getPasswordStrengthTextAndColor(strength) + + const hasPassword = strength !== PasswordStrength.NONE + let strengthText: string = '' + switch (strength) { + case PasswordStrength.STRONG: + strengthText = t('settings.setting.backup.password.strong') + break + case PasswordStrength.MEDIUM: + strengthText = t('settings.setting.backup.password.medium') + break + case PasswordStrength.WEAK: + strengthText = t('settings.setting.backup.password.weak') + break + default: + break + } + + return ( + + + {strengthText} + + + ) +} diff --git a/apps/mobile/src/features/CloudBackup/CloudBackupForm/index.ts b/apps/mobile/src/features/CloudBackup/CloudBackupForm/index.ts new file mode 100644 index 00000000000..fa72f4b3769 --- /dev/null +++ b/apps/mobile/src/features/CloudBackup/CloudBackupForm/index.ts @@ -0,0 +1,9 @@ +import { CloudBackupPasswordFormContextProvider } from 'src/features/CloudBackup/CloudBackupForm/CloudBackupPasswordFormContext' +import { ContinueButton } from 'src/features/CloudBackup/CloudBackupForm/ContinueButton' +import { PasswordInput } from 'src/features/CloudBackup/CloudBackupForm/PasswordInput' + +export const CloudBackupPassword = { + PasswordInput, + ContinueButton, + FormProvider: CloudBackupPasswordFormContextProvider, +} diff --git a/apps/mobile/src/features/CloudBackup/CloudBackupPasswordForm.tsx b/apps/mobile/src/features/CloudBackup/CloudBackupPasswordForm.tsx deleted file mode 100644 index 2ca462ec9c8..00000000000 --- a/apps/mobile/src/features/CloudBackup/CloudBackupPasswordForm.tsx +++ /dev/null @@ -1,156 +0,0 @@ -import React, { useRef, useState } from 'react' -import { useTranslation } from 'react-i18next' -import { Keyboard, TextInput } from 'react-native' -import { PasswordInput } from 'src/components/input/PasswordInput' -import { PasswordError } from 'src/features/onboarding/PasswordError' -import { Button, Flex, Text } from 'ui/src' -import { DiamondExclamation } from 'ui/src/components/icons' -import { iconSizes } from 'ui/src/theme' -import { ElementName } from 'uniswap/src/features/telemetry/constants' -import { useDebounce } from 'utilities/src/time/timing' -import { - PASSWORD_VALIDATION_DEBOUNCE_MS, - PasswordErrors, - PasswordStrength, - getPasswordStrength, - getPasswordStrengthTextAndColor, - isPasswordStrongEnough, -} from 'wallet/src/utils/password' - -export type CloudBackupPasswordProps = { - navigateToNextScreen: ({ password }: { password: string }) => void - isConfirmation?: boolean - passwordToConfirm?: string -} - -export function CloudBackupPasswordForm({ - navigateToNextScreen, - isConfirmation, - passwordToConfirm, -}: CloudBackupPasswordProps): JSX.Element { - const { t } = useTranslation() - - const passwordInputRef = useRef(null) - const [password, setPassword] = useState('') - - const [error, setError] = useState(undefined) - - const [passwordStrength, setPasswordStrength] = useState(PasswordStrength.NONE) - const debouncedPasswordStrength = useDebounce(passwordStrength, PASSWORD_VALIDATION_DEBOUNCE_MS) - const isStrongPassword = isPasswordStrongEnough({ - minStrength: PasswordStrength.MEDIUM, - currentStrength: passwordStrength, - }) - - const isButtonDisabled = - !!error || password.length === 0 || (!isConfirmation && !isStrongPassword) - - const onPasswordChangeText = (newPassword: string): void => { - if (isConfirmation && newPassword === password) { - setError(undefined) - } - // always reset error if not confirmation - if (!isConfirmation) { - setPasswordStrength(getPasswordStrength(newPassword)) - setError(undefined) - } - setPassword(newPassword) - } - - const onPasswordSubmitEditing = (): void => { - if (!isConfirmation && !isStrongPassword) { - return - } - if (isConfirmation && passwordToConfirm !== password) { - setError(PasswordErrors.PasswordsDoNotMatch) - return - } - setError(undefined) - Keyboard.dismiss() - } - - const onPressNext = (): void => { - if (isConfirmation && passwordToConfirm !== password) { - setError(PasswordErrors.PasswordsDoNotMatch) - return - } - - if (!error) { - navigateToNextScreen({ password }) - } - } - - let errorText = '' - if (error === PasswordErrors.PasswordsDoNotMatch) { - errorText = t('settings.setting.backup.password.error.mismatch') - } else if (error) { - // use the upstream zxcvbn error message - errorText = error - } - - return ( - <> - - - { - setError(undefined) - onPasswordChangeText(newText) - }} - onSubmitEditing={onPasswordSubmitEditing} - /> - {!isConfirmation && } - {error ? : null} - - {!isConfirmation && ( - - - - {t('settings.setting.backup.password.disclaimer')} - - - )} - - - - ) -} - -function PasswordStrengthText({ strength }: { strength: PasswordStrength }): JSX.Element { - const { t } = useTranslation() - const { color } = getPasswordStrengthTextAndColor(strength) - - const hasPassword = strength !== PasswordStrength.NONE - let strengthText: string = '' - switch (strength) { - case PasswordStrength.STRONG: - strengthText = t('settings.setting.backup.password.strong') - break - case PasswordStrength.MEDIUM: - strengthText = t('settings.setting.backup.password.medium') - break - case PasswordStrength.WEAK: - strengthText = t('settings.setting.backup.password.weak') - break - default: - break - } - - return ( - - - {strengthText} - - - ) -} diff --git a/apps/mobile/src/features/CloudBackup/CloudBackupProcessingAnimation.tsx b/apps/mobile/src/features/CloudBackup/CloudBackupProcessingAnimation.tsx index f2cee7379c0..efab83194da 100644 --- a/apps/mobile/src/features/CloudBackup/CloudBackupProcessingAnimation.tsx +++ b/apps/mobile/src/features/CloudBackup/CloudBackupProcessingAnimation.tsx @@ -3,6 +3,7 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack' import React, { useCallback, useEffect, useReducer } from 'react' import { useTranslation } from 'react-i18next' import { ActivityIndicator, Alert } from 'react-native' +import { useDispatch } from 'react-redux' import { OnboardingStackParamList, SettingsStackParamList } from 'src/app/navigation/types' import { backupMnemonicToCloudStorage } from 'src/features/CloudBackup/RNCloudStorageBackupsManager' import { Flex, Text } from 'ui/src' @@ -14,13 +15,9 @@ import { logger } from 'utilities/src/logger/logger' import { ONE_SECOND_MS } from 'utilities/src/time/time' import { promiseMinDelay } from 'utilities/src/time/timing' import { useOnboardingContext } from 'wallet/src/features/onboarding/OnboardingContext' -import { - EditAccountAction, - editAccountActions, -} from 'wallet/src/features/wallet/accounts/editAccountSaga' +import { EditAccountAction, editAccountActions } from 'wallet/src/features/wallet/accounts/editAccountSaga' import { AccountType, BackupType } from 'wallet/src/features/wallet/accounts/types' import { useSignerAccountIfExists } from 'wallet/src/features/wallet/hooks' -import { useAppDispatch } from 'wallet/src/state' type Props = { accountAddress: Address @@ -41,7 +38,7 @@ export function CloudBackupProcessingAnimation({ navigation, }: Props): JSX.Element { const { t } = useTranslation() - const dispatch = useAppDispatch() + const dispatch = useDispatch() const { addBackupMethod, getImportedAccounts, getOnboardingAccount } = useOnboardingContext() const onboardingAccount = getOnboardingAccount() const importedAccounts = getImportedAccounts() @@ -81,7 +78,7 @@ export function CloudBackupProcessingAnimation({ type: EditAccountAction.AddBackupMethod, address: accountAddress, backupMethod: BackupType.Cloud, - }) + }), ) } else { addBackupMethod(BackupType.Cloud) @@ -102,19 +99,10 @@ export function CloudBackupProcessingAnimation({ style: 'default', onPress: onErrorPress, }, - ] + ], ) } - }, [ - accountAddress, - activeAccount, - addBackupMethod, - dispatch, - mnemonicId, - onErrorPress, - password, - t, - ]) + }, [accountAddress, activeAccount, addBackupMethod, dispatch, mnemonicId, onErrorPress, password, t]) /** * Delays cloud backup to avoid android oauth consent screen blocking navigation transition @@ -124,7 +112,7 @@ export function CloudBackupProcessingAnimation({ return navigation.addListener('transitionEnd', async () => { await backup() }) - }, [backup, navigation]) + }, [backup, navigation]), ) const iconSize = iconSizes.icon40 diff --git a/apps/mobile/src/features/CloudBackup/RNCloudStorageBackupsManager.ts b/apps/mobile/src/features/CloudBackup/RNCloudStorageBackupsManager.ts index f13f239fdbe..56f3a262eb9 100644 --- a/apps/mobile/src/features/CloudBackup/RNCloudStorageBackupsManager.ts +++ b/apps/mobile/src/features/CloudBackup/RNCloudStorageBackupsManager.ts @@ -32,16 +32,10 @@ export function stopFetchingCloudStorageBackups(): Promise { return RNCloudStorageBackupsManager.stopFetchingCloudStorageBackups() } -export function backupMnemonicToCloudStorage( - mnemonicId: string, - password: string -): Promise { +export function backupMnemonicToCloudStorage(mnemonicId: string, password: string): Promise { return RNCloudStorageBackupsManager.backupMnemonicToCloudStorage(mnemonicId, password) } -export function restoreMnemonicFromCloudStorage( - mnemonicId: string, - password: string -): Promise { +export function restoreMnemonicFromCloudStorage(mnemonicId: string, password: string): Promise { return RNCloudStorageBackupsManager.restoreMnemonicFromCloudStorage(mnemonicId, password) } diff --git a/apps/mobile/src/features/CloudBackup/passwordLockoutSlice.ts b/apps/mobile/src/features/CloudBackup/passwordLockoutSlice.ts index 1732e644154..0d717490156 100644 --- a/apps/mobile/src/features/CloudBackup/passwordLockoutSlice.ts +++ b/apps/mobile/src/features/CloudBackup/passwordLockoutSlice.ts @@ -28,10 +28,6 @@ const slice = createSlice({ }, }) -export const { - incrementPasswordAttempts, - resetPasswordAttempts, - setLockoutEndTime, - resetLockoutEndTime, -} = slice.actions +export const { incrementPasswordAttempts, resetPasswordAttempts, setLockoutEndTime, resetLockoutEndTime } = + slice.actions export const { reducer: passwordLockoutReducer } = slice diff --git a/apps/mobile/src/features/CloudBackup/saga.ts b/apps/mobile/src/features/CloudBackup/saga.ts index 79fca290f0e..584c4ff6cb4 100644 --- a/apps/mobile/src/features/CloudBackup/saga.ts +++ b/apps/mobile/src/features/CloudBackup/saga.ts @@ -2,10 +2,7 @@ import { Action } from '@reduxjs/toolkit' import { NativeEventEmitter, NativeModule, NativeModules } from 'react-native' import { eventChannel } from 'redux-saga' import { foundCloudBackup } from 'src/features/CloudBackup/cloudBackupSlice' -import { - CloudStorageBackupsManagerEventType, - CloudStorageMnemonicBackup, -} from 'src/features/CloudBackup/types' +import { CloudStorageBackupsManagerEventType, CloudStorageMnemonicBackup } from 'src/features/CloudBackup/types' import { call, fork, put, take } from 'typed-redux-saga' import { logger } from 'utilities/src/logger/logger' @@ -43,7 +40,7 @@ export function* cloudBackupsManagerSaga() { export function* watchCloudStorageBackupEvents() { const CloudManagerEvents = new NativeEventEmitter( - NativeModules.RNCloudStorageBackupsManager as unknown as NativeModule + NativeModules.RNCloudStorageBackupsManager as unknown as NativeModule, ) const channel = yield* call(createCloudStorageBackupManagerChannel, CloudManagerEvents) diff --git a/apps/mobile/src/features/CloudBackup/testingUtils.ts b/apps/mobile/src/features/CloudBackup/testingUtils.ts index 003f5bb6bf4..5a532a53746 100644 --- a/apps/mobile/src/features/CloudBackup/testingUtils.ts +++ b/apps/mobile/src/features/CloudBackup/testingUtils.ts @@ -11,9 +11,7 @@ const generateRandomId = (): string => { const generateRandomDate = (): number => { const start = new Date(2023, 4, 12) const end = new Date() - return Math.floor( - new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime())).getTime() / 1000 - ) + return Math.floor(new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime())).getTime() / 1000) } export const useMockCloudBackups = (numberOfBackups?: number): CloudStorageMnemonicBackup[] => { diff --git a/apps/mobile/src/features/analytics/appsflyer.tsx b/apps/mobile/src/features/analytics/appsflyer.tsx index 9aaefd08136..292e277e50b 100644 --- a/apps/mobile/src/features/analytics/appsflyer.tsx +++ b/apps/mobile/src/features/analytics/appsflyer.tsx @@ -1,6 +1,6 @@ import appsFlyer from 'react-native-appsflyer' import { config } from 'uniswap/src/config' -import { isBetaEnv, isDevEnv } from 'uniswap/src/utils/env' +import { isBetaEnv, isDevEnv } from 'utilities/src/environment' import { logger } from 'utilities/src/logger/logger' export function initAppsFlyer(): void { @@ -20,6 +20,6 @@ export function initAppsFlyer(): void { }, (error) => { logger.error(error, { tags: { file: 'appsflyer', function: 'initAppsFlyer' } }) - } + }, ) } diff --git a/apps/mobile/src/features/appLoading/SplashScreen.tsx b/apps/mobile/src/features/appLoading/SplashScreen.tsx index 73f483fa16a..75c5fde75b5 100644 --- a/apps/mobile/src/features/appLoading/SplashScreen.tsx +++ b/apps/mobile/src/features/appLoading/SplashScreen.tsx @@ -22,7 +22,8 @@ export function SplashScreen(): JSX.Element { height: dimensions.fullHeight, width: dimensions.fullWidth, paddingBottom: insets.bottom, - }}> + }} + > {/* Android has a different implementation, which is not set in stone yet, so skipping it for now */} {isAndroid ? ( diff --git a/apps/mobile/src/features/appRating/saga.ts b/apps/mobile/src/features/appRating/saga.ts index 845fbb15066..3bd4a1e8de0 100644 --- a/apps/mobile/src/features/appRating/saga.ts +++ b/apps/mobile/src/features/appRating/saga.ts @@ -7,6 +7,8 @@ import { FeatureFlags, WALLET_FEATURE_FLAG_NAMES } from 'uniswap/src/features/ga import { Statsig } from 'uniswap/src/features/gating/sdk/statsig' import { MobileEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' +import i18n from 'uniswap/src/i18n/i18n' +import { openUri } from 'uniswap/src/utils/linking' import { logger } from 'utilities/src/logger/logger' import { isAndroid } from 'utilities/src/platform' import { ONE_DAY_MS, ONE_SECOND_MS } from 'utilities/src/time/time' @@ -15,7 +17,6 @@ import { TransactionStatus, TransactionType } from 'wallet/src/features/transact import { selectActiveAccountAddress } from 'wallet/src/features/wallet/selectors' import { setAppRating } from 'wallet/src/features/wallet/slice' import { appSelect } from 'wallet/src/state' -import { openUri } from 'wallet/src/utils/linking' // at most once per reminder period (120 days) const MIN_PROMPT_REMINDER_MS = 120 * ONE_DAY_MS @@ -33,10 +34,7 @@ export function* appRatingWatcherSaga() { return } - if ( - action.payload.typeInfo.type === TransactionType.Swap && - action.payload.status === TransactionStatus.Success - ) { + if (action.payload.typeInfo.type === TransactionType.Swap && action.payload.status === TransactionStatus.Success) { yield* delay(SWAP_FINALIZED_PROMPT_DELAY_MS) yield* call(maybeRequestAppRating) } @@ -64,18 +62,14 @@ function* maybeRequestAppRating() { } // avoids prompting again const appRatingPromptedMs = yield* appSelect((state) => state.wallet.appRatingPromptedMs) - const appRatingFeedbackProvidedMs = yield* appSelect( - (state) => state.wallet.appRatingFeedbackProvidedMs - ) + const appRatingFeedbackProvidedMs = yield* appSelect((state) => state.wallet.appRatingFeedbackProvidedMs) const consecutiveSwapsCondition = yield* appSelect(hasConsecutiveRecentSwapsSelector) // prompt if enough time has passed since last prompt or last feedback provided const reminderCondition = - (appRatingPromptedMs !== undefined && - Date.now() - appRatingPromptedMs > MIN_PROMPT_REMINDER_MS) || - (appRatingFeedbackProvidedMs !== undefined && - Date.now() - appRatingFeedbackProvidedMs > MIN_FEEDBACK_REMINDER_MS) + (appRatingPromptedMs !== undefined && Date.now() - appRatingPromptedMs > MIN_PROMPT_REMINDER_MS) || + (appRatingFeedbackProvidedMs !== undefined && Date.now() - appRatingFeedbackProvidedMs > MIN_FEEDBACK_REMINDER_MS) const hasNeverPrompted = appRatingPromptedMs === undefined const shouldPrompt = consecutiveSwapsCondition && (hasNeverPrompted || reminderCondition) @@ -143,47 +137,47 @@ function* maybeRequestAppRating() { */ async function openRatingOptionsAlert() { return new Promise((resolve) => { - Alert.alert( - 'Enjoying Uniswap Wallet?', - "Let us know if you're having a good experience with this app", - [ - { - text: 'Not really', - onPress: () => resolve(false), - style: 'cancel', - }, - { - text: 'Yes', - onPress: () => { - openNativeReviewModal().catch((e) => - logger.error(e, { - tags: { file: 'appRating/saga', function: 'openRatingOptionsAlert' }, - }) - ) - resolve(true) - }, - isPreferred: true, + Alert.alert(i18n.t('mobile.appRating.title'), i18n.t('mobile.appRating.description'), [ + { + text: i18n.t('mobile.appRating.button.decline'), + onPress: () => resolve(false), + style: 'cancel', + }, + { + text: i18n.t('common.button.yes'), + onPress: () => { + openNativeReviewModal().catch((e) => + logger.error(e, { + tags: { file: 'appRating/saga', function: 'openRatingOptionsAlert' }, + }), + ) + resolve(true) }, - ] - ) + isPreferred: true, + }, + ]) }) } /** Opens feedback request modal which will redirect to our feedback form. */ async function openFeedbackRequestAlert() { return new Promise((resolve) => { - Alert.alert("We're sorry to hear that.", 'Let us know how we can improve your experience', [ + Alert.alert(i18n.t('mobile.appRating.feedback.title'), i18n.t('mobile.appRating.feedback.description'), [ { - text: 'Send feedback', + text: i18n.t('mobile.appRating.feedback.button.send'), onPress: () => { openUri(APP_FEEDBACK_LINK).catch((e) => - logger.error(e, { tags: { file: 'appRating/saga', function: 'openFeedbackAlert' } }) + logger.error(e, { tags: { file: 'appRating/saga', function: 'openFeedbackAlert' } }), ) resolve(true) }, isPreferred: true, }, - { text: 'Maybe later', onPress: () => resolve(false), style: 'cancel' }, + { + text: i18n.t('mobile.appRating.feedback.button.cancel'), + onPress: () => resolve(false), + style: 'cancel', + }, ]) }) } @@ -201,7 +195,7 @@ async function openNativeReviewModal() { function shouldSkipRatingPrompt(): boolean { const isPlaystoreRatingPromptEnabled = Statsig.checkGate( - WALLET_FEATURE_FLAG_NAMES.get(FeatureFlags.PlaystoreAppRating) ?? '' + WALLET_FEATURE_FLAG_NAMES.get(FeatureFlags.PlaystoreAppRating) ?? '', ) return isAndroid && !isPlaystoreRatingPromptEnabled } diff --git a/apps/mobile/src/features/appRating/selectors.test.ts b/apps/mobile/src/features/appRating/selectors.test.ts index becc2b51cbe..f99afcd35c7 100644 --- a/apps/mobile/src/features/appRating/selectors.test.ts +++ b/apps/mobile/src/features/appRating/selectors.test.ts @@ -1,11 +1,7 @@ import { hasConsecutiveRecentSwapsSelector } from 'src/features/appRating/selectors' import { UniverseChainId } from 'uniswap/src/types/chains' import { ONE_HOUR_MS, ONE_MINUTE_MS } from 'utilities/src/time/time' -import { - TransactionDetails, - TransactionStatus, - TransactionType, -} from 'wallet/src/features/transactions/types' +import { TransactionDetails, TransactionStatus, TransactionType } from 'wallet/src/features/transactions/types' import { RootState } from 'wallet/src/state' import { signerMnemonicAccount } from 'wallet/src/test/fixtures' import { preloadedWalletState } from 'wallet/src/test/fixtures/wallet/redux' diff --git a/apps/mobile/src/features/appRating/selectors.ts b/apps/mobile/src/features/appRating/selectors.ts index dc3bf542a52..b24ec17a4f8 100644 --- a/apps/mobile/src/features/appRating/selectors.ts +++ b/apps/mobile/src/features/appRating/selectors.ts @@ -3,11 +3,7 @@ import { flattenObjectOfObjects } from 'utilities/src/primitives/objects' import { ONE_MINUTE_MS } from 'utilities/src/time/time' import { selectTransactions } from 'wallet/src/features/transactions/selectors' import { TransactionStateMap } from 'wallet/src/features/transactions/slice' -import { - TransactionDetails, - TransactionStatus, - TransactionType, -} from 'wallet/src/features/transactions/types' +import { TransactionDetails, TransactionStatus, TransactionType } from 'wallet/src/features/transactions/types' import { RootState } from 'wallet/src/state' const NUM_CONSECUTIVE_SWAPS = 2 @@ -33,14 +29,12 @@ export const hasConsecutiveRecentSwapsSelector: Selector = c const recentSwaps = swapTxs.slice(-NUM_CONSECUTIVE_SWAPS) const mostRecentSwapTime = recentSwaps[recentSwaps.length - 1]?.addedTime - const mostRecentSwapLessThanMinAgo = Boolean( - mostRecentSwapTime && Date.now() - mostRecentSwapTime < ONE_MINUTE_MS - ) + const mostRecentSwapLessThanMinAgo = Boolean(mostRecentSwapTime && Date.now() - mostRecentSwapTime < ONE_MINUTE_MS) return ( swapTxs.length >= NUM_CONSECUTIVE_SWAPS && recentSwaps.every((tx) => tx.status === TransactionStatus.Success) && mostRecentSwapLessThanMinAgo ) - } + }, ) diff --git a/apps/mobile/src/features/authentication/LockScreenModal.tsx b/apps/mobile/src/features/authentication/LockScreenModal.tsx index 1668f2d19e5..e607ac27df9 100644 --- a/apps/mobile/src/features/authentication/LockScreenModal.tsx +++ b/apps/mobile/src/features/authentication/LockScreenModal.tsx @@ -26,7 +26,8 @@ export function LockScreenModal(): JSX.Element | null { presentationStyle="fullScreen" showCloseButton={false} transparent={false} - width="100%"> + width="100%" + > => trigger()}> diff --git a/apps/mobile/src/features/authentication/lockScreenContext.tsx b/apps/mobile/src/features/authentication/lockScreenContext.tsx index 78336f6bf28..232ef8f50bb 100644 --- a/apps/mobile/src/features/authentication/lockScreenContext.tsx +++ b/apps/mobile/src/features/authentication/lockScreenContext.tsx @@ -41,7 +41,8 @@ export function LockScreenContextProvider({ children }: PropsWithChildren + }} + > {children} ) diff --git a/apps/mobile/src/features/biometrics/context.tsx b/apps/mobile/src/features/biometrics/context.tsx index 69e6627be34..325357e7013 100644 --- a/apps/mobile/src/features/biometrics/context.tsx +++ b/apps/mobile/src/features/biometrics/context.tsx @@ -20,12 +20,10 @@ const BiometricContext = createContext(biometricContextVa export function BiometricContextProvider({ children }: PropsWithChildren): JSX.Element { // global authenticationStatus - const [status, setStatus] = useState( - BiometricAuthenticationStatus.Invalid - ) + const [status, setStatus] = useState(BiometricAuthenticationStatus.Invalid) const { triggerDebounce, cancelDebounce } = debounceCallback( () => setStatus(BiometricAuthenticationStatus.Invalid), - 10000 + 10000, ) const setAuthenticationStatus = (value: BiometricAuthenticationStatus): void => { setStatus(value) @@ -43,7 +41,8 @@ export function BiometricContextProvider({ children }: PropsWithChildren + }} + > {children} ) diff --git a/apps/mobile/src/features/biometrics/hooks.tsx b/apps/mobile/src/features/biometrics/hooks.tsx index 2bab4553f15..04dbb5b3de1 100644 --- a/apps/mobile/src/features/biometrics/hooks.tsx +++ b/apps/mobile/src/features/biometrics/hooks.tsx @@ -49,7 +49,7 @@ type TriggerArgs = { */ export function useBiometricPrompt( successCallback?: (params?: T) => void, - failureCallback?: () => void + failureCallback?: () => void, ): { trigger: (args?: TriggerArgs) => Promise } { @@ -64,10 +64,7 @@ export function useBiometricPrompt( const _successCallback = args?.successCallback ?? successCallback const _failureCallback = args?.failureCallback ?? failureCallback - if ( - biometricAuthenticationSuccessful(authStatus) || - biometricAuthenticationDisabledByOS(authStatus) - ) { + if (biometricAuthenticationSuccessful(authStatus) || biometricAuthenticationDisabledByOS(authStatus)) { _successCallback?.(args?.params) } else { _failureCallback?.() @@ -85,12 +82,9 @@ export function biometricAuthenticationRejected(status: BiometricAuthenticationS return status === BiometricAuthenticationStatus.Rejected } -export function biometricAuthenticationDisabledByOS( - status: BiometricAuthenticationStatus -): boolean { +export function biometricAuthenticationDisabledByOS(status: BiometricAuthenticationStatus): boolean { return ( - status === BiometricAuthenticationStatus.Unsupported || - status === BiometricAuthenticationStatus.MissingEnrollment + status === BiometricAuthenticationStatus.Unsupported || status === BiometricAuthenticationStatus.MissingEnrollment ) } diff --git a/apps/mobile/src/features/biometrics/useBiometricCheck.ts b/apps/mobile/src/features/biometrics/useBiometricCheck.ts index 1b0f0067095..eb19b36ef7c 100644 --- a/apps/mobile/src/features/biometrics/useBiometricCheck.ts +++ b/apps/mobile/src/features/biometrics/useBiometricCheck.ts @@ -28,10 +28,7 @@ export function useBiometricCheck(): void { useAsyncData(triggerBiometricCheck) useAppStateTrigger('background', 'active', async () => { - if ( - requiredForAppAccess && - authenticationStatus !== BiometricAuthenticationStatus.Authenticated - ) { + if (requiredForAppAccess && authenticationStatus !== BiometricAuthenticationStatus.Authenticated) { await trigger() } }) @@ -69,10 +66,7 @@ export function useBiometricCheck(): void { useAppStateTrigger('active', 'inactive', () => { hideSplashScreen() // In case of a race condition where splash screen is not hidden, we want to hide when FaceID forces an app state change - if ( - requiredForAppAccess && - authenticationStatus !== BiometricAuthenticationStatus.Authenticating - ) { + if (requiredForAppAccess && authenticationStatus !== BiometricAuthenticationStatus.Authenticating) { setIsLockScreenVisible(true) } }) diff --git a/apps/mobile/src/features/contracts/useContract.ts b/apps/mobile/src/features/contracts/useContract.ts index a5c92176b68..36e93fc39c8 100644 --- a/apps/mobile/src/features/contracts/useContract.ts +++ b/apps/mobile/src/features/contracts/useContract.ts @@ -11,7 +11,7 @@ import { useContractManager, useProvider } from 'wallet/src/features/wallet/cont export function useContract( chainId: WalletChainId, addressOrAddressMap: string | { [chainId: number]: string } | undefined, - ABI: ContractInterface + ABI: ContractInterface, ): T | null { const provider = useProvider(chainId) const contractsManager = useContractManager() diff --git a/apps/mobile/src/features/dataApi/balances.test.ts b/apps/mobile/src/features/dataApi/balances.test.ts index 1e9d2ca605c..0bb18abd31b 100644 --- a/apps/mobile/src/features/dataApi/balances.test.ts +++ b/apps/mobile/src/features/dataApi/balances.test.ts @@ -35,10 +35,10 @@ describe(useBalances, () => { const { resolvers } = queryResolvers({ portfolios: () => [Portfolio], }) - const { result } = renderHook( - () => useBalances(balances.map(({ currencyInfo: { currencyId } }) => currencyId)), - { preloadedState, resolvers } - ) + const { result } = renderHook(() => useBalances(balances.map(({ currencyInfo: { currencyId } }) => currencyId)), { + preloadedState, + resolvers, + }) await waitFor(() => { // The response contains only the first currency as the second one is not in the portfolio diff --git a/apps/mobile/src/features/dataApi/balances.ts b/apps/mobile/src/features/dataApi/balances.ts index bfe0f9a3cd9..3314ef78e94 100644 --- a/apps/mobile/src/features/dataApi/balances.ts +++ b/apps/mobile/src/features/dataApi/balances.ts @@ -17,8 +17,6 @@ export function useBalances(currencies: CurrencyId[] | undefined): PortfolioBala return null } - return currencies - .map((id: CurrencyId) => balances[id] ?? null) - .filter((x): x is PortfolioBalance => Boolean(x)) + return currencies.map((id: CurrencyId) => balances[id] ?? null).filter((x): x is PortfolioBalance => Boolean(x)) }, [balances, currencies]) } diff --git a/apps/mobile/src/features/deepLinking/handleDeepLinkSaga.ts b/apps/mobile/src/features/deepLinking/handleDeepLinkSaga.ts index 15eb979810c..bd95777755b 100644 --- a/apps/mobile/src/features/deepLinking/handleDeepLinkSaga.ts +++ b/apps/mobile/src/features/deepLinking/handleDeepLinkSaga.ts @@ -4,16 +4,13 @@ import { Alert } from 'react-native' import { URL } from 'react-native-url-polyfill' import { appSelect } from 'src/app/hooks' import { navigate } from 'src/app/navigation/rootNavigation' -import { - getScantasticQueryParams, - parseScantasticParams, -} from 'src/components/WalletConnect/ScanSheet/util' +import { getScantasticQueryParams, parseScantasticParams } from 'src/components/Requests/ScanSheet/util' import { UNISWAP_URL_SCHEME, UNISWAP_URL_SCHEME_WALLETCONNECT_AS_PARAM, UNISWAP_WALLETCONNECT_URL, } from 'src/features/deepLinking/constants' -import { handleMoonpayReturnLink } from 'src/features/deepLinking/handleMoonpayReturnLinkSaga' +import { handleOnRampReturnLink } from 'src/features/deepLinking/handleOnRampReturnLinkSaga' import { handleSwapLink } from 'src/features/deepLinking/handleSwapLinkSaga' import { handleTransactionLink } from 'src/features/deepLinking/handleTransactionLinkSaga' import { closeAllModals, openModal } from 'src/features/modals/modalSlice' @@ -22,6 +19,7 @@ import { pairWithWalletConnectURI } from 'src/features/walletConnect/utils' import { setDidOpenFromDeepLink } from 'src/features/walletConnect/walletConnectSlice' import { call, put, takeLatest } from 'typed-redux-saga' import { UNISWAP_WEB_HOSTNAME } from 'uniswap/src/constants/urls' +import { fromUniswapWebAppLink } from 'uniswap/src/features/chains/utils' import { FeatureFlags, getFeatureFlagName } from 'uniswap/src/features/gating/flags' import { Statsig } from 'uniswap/src/features/gating/sdk/statsig' import { MobileEventName, ModalName } from 'uniswap/src/features/telemetry/constants' @@ -30,17 +28,13 @@ import i18n from 'uniswap/src/i18n/i18n' import { MobileScreens } from 'uniswap/src/types/screens/mobile' import { ShareableEntity } from 'uniswap/src/types/sharing' import { WidgetType } from 'uniswap/src/types/widgets' +import { buildCurrencyId, buildNativeCurrencyId } from 'uniswap/src/utils/currencyId' +import { openUri } from 'uniswap/src/utils/linking' import { logger } from 'utilities/src/logger/logger' -import { fromUniswapWebAppLink } from 'wallet/src/features/chains/utils' import { ScantasticParams } from 'wallet/src/features/scantastic/types' -import { - selectAccounts, - selectActiveAccount, - selectActiveAccountAddress, -} from 'wallet/src/features/wallet/selectors' +import { selectAccounts, selectActiveAccount, selectActiveAccountAddress } from 'wallet/src/features/wallet/selectors' import { setAccountAsActive } from 'wallet/src/features/wallet/slice' -import { buildCurrencyId, buildNativeCurrencyId } from 'wallet/src/utils/currencyId' -import { UNISWAP_APP_NATIVE_TOKEN, openUri } from 'wallet/src/utils/linking' +import { UNISWAP_APP_NATIVE_TOKEN } from 'wallet/src/utils/linking' export interface DeepLink { url: string @@ -59,7 +53,7 @@ const NFT_ITEM_SHARE_LINK_HASH_REGEX = /^(#\/)?nfts\/asset\/(0x[a-fA-F0-9]{40})\ const NFT_COLLECTION_SHARE_LINK_HASH_REGEX = /^(#\/)?nfts\/collection\/(0x[a-fA-F0-9]{40})$/ const TOKEN_SHARE_LINK_HASH_REGEX = RegExp( // eslint-disable-next-line no-useless-escape - `^(#\/)?tokens\/([\\w\\d]*)\/(0x[a-fA-F0-9]{40}|${UNISWAP_APP_NATIVE_TOKEN})$` + `^(#\/)?tokens\/([\\w\\d]*)\/(0x[a-fA-F0-9]{40}|${UNISWAP_APP_NATIVE_TOKEN})$`, ) const ADDRESS_SHARE_LINK_HASH_REGEX = /^(#\/)?address\/(0x[a-fA-F0-9]{40})$/ @@ -91,7 +85,7 @@ export function* handleUniswapAppDeepLink(path: string, url: string, linkSource: isSpam: false, }, }, - }) + }), ) yield* call(sendAnalyticsEvent, MobileEventName.ShareLinkOpened, { entity: ShareableEntity.NftItem, @@ -115,7 +109,7 @@ export function* handleUniswapAppDeepLink(path: string, url: string, linkSource: collectionAddress: contractAddress, }, }, - }) + }), ) yield* call(sendAnalyticsEvent, MobileEventName.ShareLinkOpened, { entity: ShareableEntity.NftCollection, @@ -144,7 +138,7 @@ export function* handleUniswapAppDeepLink(path: string, url: string, linkSource: currencyId, }, }, - }) + }), ) if (linkSource === LinkSource.Share) { yield* call(sendAnalyticsEvent, MobileEventName.ShareLinkOpened, { @@ -185,7 +179,7 @@ export function* handleUniswapAppDeepLink(path: string, url: string, linkSource: address: accountAddress, }, }, - }) + }), ) } yield* call(sendAnalyticsEvent, MobileEventName.ShareLinkOpened, { @@ -294,7 +288,7 @@ export function* handleDeepLink(action: ReturnType) { switch (screen) { case 'transaction': if (fiatOnRamp) { - yield* call(handleMoonpayReturnLink) + yield* call(handleOnRampReturnLink) } else { yield* call(handleTransactionLink) } @@ -335,7 +329,7 @@ export function* handleWalletConnectDeepLink(wcUri: string) { Alert.alert( i18n.t('walletConnect.error.unsupportedV1.title'), i18n.t('walletConnect.error.unsupportedV1.message'), - [{ text: i18n.t('common.button.ok') }] + [{ text: i18n.t('common.button.ok') }], ) return } @@ -347,10 +341,7 @@ export function* handleWalletConnectDeepLink(wcUri: string) { logger.error(error, { tags: { file: 'handleDeepLinkSaga', function: 'handleWalletConnectDeepLink' }, }) - Alert.alert( - i18n.t('walletConnect.error.general.title'), - i18n.t('walletConnect.error.general.message') - ) + Alert.alert(i18n.t('walletConnect.error.general.title'), i18n.t('walletConnect.error.general.message')) } } @@ -365,7 +356,7 @@ export function* parseAndValidateUserAddress(userAddress: string | null) { const userAccounts = yield* appSelect(selectAccounts) const matchingAccount = Object.values(userAccounts).find( - (account) => account.address.toLowerCase() === userAddress.toLowerCase() + (account) => account.address.toLowerCase() === userAddress.toLowerCase(), ) if (!matchingAccount) { @@ -380,11 +371,9 @@ export function* handleScantasticDeepLink(scantasticQueryParams: string): Genera const scantasticEnabled = Statsig.checkGate(getFeatureFlagName(FeatureFlags.Scantastic)) if (!params || !scantasticEnabled) { - Alert.alert( - i18n.t('walletConnect.error.scantastic.title'), - i18n.t('walletConnect.error.scantastic.message'), - [{ text: i18n.t('common.button.ok') }] - ) + Alert.alert(i18n.t('walletConnect.error.scantastic.title'), i18n.t('walletConnect.error.scantastic.message'), [ + { text: i18n.t('common.button.ok') }, + ]) return } @@ -399,6 +388,6 @@ function* launchScantastic(params: ScantasticParams): Generator { initialState: { params, }, - }) + }), ) } diff --git a/apps/mobile/src/features/deepLinking/handleMoonpayReturnLinkSaga.test.ts b/apps/mobile/src/features/deepLinking/handleOnRampReturnLinkSaga.test.ts similarity index 78% rename from apps/mobile/src/features/deepLinking/handleMoonpayReturnLinkSaga.test.ts rename to apps/mobile/src/features/deepLinking/handleOnRampReturnLinkSaga.test.ts index 0ba9a9bdae3..59d4de9cf4e 100644 --- a/apps/mobile/src/features/deepLinking/handleMoonpayReturnLinkSaga.test.ts +++ b/apps/mobile/src/features/deepLinking/handleOnRampReturnLinkSaga.test.ts @@ -1,15 +1,15 @@ import { call, put } from '@redux-saga/core/effects' import { expectSaga } from 'redux-saga-test-plan' import { navigate } from 'src/app/navigation/rootNavigation' -import { handleMoonpayReturnLink } from 'src/features/deepLinking/handleMoonpayReturnLinkSaga' +import { handleOnRampReturnLink } from 'src/features/deepLinking/handleOnRampReturnLinkSaga' import { HomeScreenTabIndex } from 'src/screens/HomeScreenTabIndex' import { MobileScreens } from 'uniswap/src/types/screens/mobile' import { forceFetchFiatOnRampTransactions } from 'wallet/src/features/transactions/slice' import { dismissInAppBrowser } from 'wallet/src/utils/linking' -describe(handleMoonpayReturnLink, () => { - it('Navigates to the home screen activity tab when coming back from moonpay', () => { - return expectSaga(handleMoonpayReturnLink) +describe(handleOnRampReturnLink, () => { + it('Navigates to the home screen activity tab when coming back from on-ramp widget', () => { + return expectSaga(handleOnRampReturnLink) .provide([ [put(forceFetchFiatOnRampTransactions), undefined], [call(navigate, MobileScreens.Home, { tab: HomeScreenTabIndex.Activity }), undefined], diff --git a/apps/mobile/src/features/deepLinking/handleMoonpayReturnLinkSaga.ts b/apps/mobile/src/features/deepLinking/handleOnRampReturnLinkSaga.ts similarity index 92% rename from apps/mobile/src/features/deepLinking/handleMoonpayReturnLinkSaga.ts rename to apps/mobile/src/features/deepLinking/handleOnRampReturnLinkSaga.ts index adbc437ebc9..19c0a67eeb1 100644 --- a/apps/mobile/src/features/deepLinking/handleMoonpayReturnLinkSaga.ts +++ b/apps/mobile/src/features/deepLinking/handleOnRampReturnLinkSaga.ts @@ -5,7 +5,7 @@ import { MobileScreens } from 'uniswap/src/types/screens/mobile' import { forceFetchFiatOnRampTransactions } from 'wallet/src/features/transactions/slice' import { dismissInAppBrowser } from 'wallet/src/utils/linking' -export function* handleMoonpayReturnLink() { +export function* handleOnRampReturnLink() { yield* put(forceFetchFiatOnRampTransactions()) yield* call(navigate, MobileScreens.Home, { tab: HomeScreenTabIndex.Activity }) yield* call(dismissInAppBrowser) diff --git a/apps/mobile/src/features/deepLinking/handleSwapLinkSaga.test.ts b/apps/mobile/src/features/deepLinking/handleSwapLinkSaga.test.ts index 7a36fa7cbee..47719d2e6e1 100644 --- a/apps/mobile/src/features/deepLinking/handleSwapLinkSaga.test.ts +++ b/apps/mobile/src/features/deepLinking/handleSwapLinkSaga.test.ts @@ -2,14 +2,11 @@ import { URL } from 'react-native-url-polyfill' import { expectSaga } from 'redux-saga-test-plan' import { handleSwapLink } from 'src/features/deepLinking/handleSwapLinkSaga' import { openModal } from 'src/features/modals/modalSlice' +import { DAI, UNI } from 'uniswap/src/constants/tokens' +import { AssetType } from 'uniswap/src/entities/assets' import { ModalName } from 'uniswap/src/features/telemetry/constants' +import { CurrencyField, TransactionState } from 'uniswap/src/features/transactions/transactionState/types' import { UniverseChainId, WalletChainId } from 'uniswap/src/types/chains' -import { DAI, UNI } from 'wallet/src/constants/tokens' -import { AssetType } from 'wallet/src/entities/assets' -import { - CurrencyField, - TransactionState, -} from 'wallet/src/features/transactions/transactionState/types' import { signerMnemonicAccount } from 'wallet/src/test/fixtures' const account = signerMnemonicAccount() @@ -20,7 +17,7 @@ const formSwapUrl = ( inputAddress?: string, outputAddress?: string, currencyField?: string, - amount?: string + amount?: string, ): URL => new URL( `https://uniswap.org/app?screen=swap @@ -28,7 +25,7 @@ const formSwapUrl = ( &inputCurrencyId=${chain}-${inputAddress} &outputCurrencyId=${chain}-${outputAddress} ¤cyField=${currencyField} -&amount=${amount}`.trim() +&amount=${amount}`.trim(), ) const formTransactionState = ( @@ -36,7 +33,7 @@ const formTransactionState = ( inputAddress?: string, outputAddress?: string, currencyField?: string, - amount?: string + amount?: string, ): { input: { address: string | undefined @@ -64,8 +61,8 @@ const formTransactionState = ( exactCurrencyField: !currencyField ? currencyField : currencyField.toLowerCase() === 'output' - ? CurrencyField.OUTPUT - : CurrencyField.INPUT, + ? CurrencyField.OUTPUT + : CurrencyField.INPUT, exactAmountToken: amount, }) @@ -75,7 +72,7 @@ const swapUrl = formSwapUrl( DAI.address, UNI[UniverseChainId.Mainnet].address, 'input', - '100' + '100', ) const invalidOutputCurrencySwapUrl = formSwapUrl( @@ -84,7 +81,7 @@ const invalidOutputCurrencySwapUrl = formSwapUrl( DAI.address, undefined, 'input', - '100' + '100', ) const invalidInputTokenSwapURl = formSwapUrl( @@ -93,7 +90,7 @@ const invalidInputTokenSwapURl = formSwapUrl( '0x00', UNI[UniverseChainId.Mainnet].address, 'input', - '100' + '100', ) const invalidChainSwapUrl = formSwapUrl( @@ -102,7 +99,7 @@ const invalidChainSwapUrl = formSwapUrl( DAI.address, UNI[UniverseChainId.Mainnet].address, 'input', - '100' + '100', ) const invalidAmountSwapUrl = formSwapUrl( @@ -111,7 +108,7 @@ const invalidAmountSwapUrl = formSwapUrl( DAI.address, UNI[UniverseChainId.Mainnet].address, 'input', - 'not a number' + 'not a number', ) const invalidCurrencyFieldSwapUrl = formSwapUrl( @@ -120,7 +117,7 @@ const invalidCurrencyFieldSwapUrl = formSwapUrl( DAI.address, UNI[UniverseChainId.Mainnet].address, 'token1', - '100' + '100', ) const swapFormState = formTransactionState( @@ -128,7 +125,7 @@ const swapFormState = formTransactionState( DAI.address, UNI[UniverseChainId.Mainnet].address, 'input', - '100' + '100', ) as TransactionState describe(handleSwapLink, () => { diff --git a/apps/mobile/src/features/deepLinking/handleSwapLinkSaga.ts b/apps/mobile/src/features/deepLinking/handleSwapLinkSaga.ts index c90d5959fc3..9a0eae1b405 100644 --- a/apps/mobile/src/features/deepLinking/handleSwapLinkSaga.ts +++ b/apps/mobile/src/features/deepLinking/handleSwapLinkSaga.ts @@ -1,27 +1,18 @@ import { BigNumber } from 'ethers' import { openModal } from 'src/features/modals/modalSlice' import { put } from 'typed-redux-saga' +import { AssetType, CurrencyAsset } from 'uniswap/src/entities/assets' import { ModalName } from 'uniswap/src/features/telemetry/constants' +import { CurrencyField, TransactionState } from 'uniswap/src/features/transactions/transactionState/types' import { WALLET_SUPPORTED_CHAIN_IDS } from 'uniswap/src/types/chains' +import { getValidAddress } from 'uniswap/src/utils/addresses' +import { currencyIdToAddress, currencyIdToChain } from 'uniswap/src/utils/currencyId' import { logger } from 'utilities/src/logger/logger' -import { AssetType, CurrencyAsset } from 'wallet/src/entities/assets' -import { - CurrencyField, - TransactionState, -} from 'wallet/src/features/transactions/transactionState/types' -import { getValidAddress } from 'wallet/src/utils/addresses' -import { currencyIdToAddress, currencyIdToChain } from 'wallet/src/utils/currencyId' export function* handleSwapLink(url: URL) { try { - const { - inputChain, - inputAddress, - outputChain, - outputAddress, - exactCurrencyField, - exactAmountToken, - } = parseAndValidateSwapParams(url) + const { inputChain, inputAddress, outputChain, outputAddress, exactCurrencyField, exactAmountToken } = + parseAndValidateSwapParams(url) const inputAsset: CurrencyAsset = { address: inputAddress, @@ -99,15 +90,11 @@ const parseAndValidateSwapParams = (url: URL) => { throw new Error('Invalid swap amount') } - if ( - !currencyField || - (currencyField.toLowerCase() !== 'input' && currencyField.toLowerCase() !== 'output') - ) { + if (!currencyField || (currencyField.toLowerCase() !== 'input' && currencyField.toLowerCase() !== 'output')) { throw new Error('Invalid currencyField. Must be either `input` or `output`') } - const exactCurrencyField = - currencyField.toLowerCase() === 'output' ? CurrencyField.OUTPUT : CurrencyField.INPUT + const exactCurrencyField = currencyField.toLowerCase() === 'output' ? CurrencyField.OUTPUT : CurrencyField.INPUT return { inputChain, diff --git a/apps/mobile/src/features/explore/utils.ts b/apps/mobile/src/features/explore/utils.ts index d829bbfae5a..3ea4d5fa6ad 100644 --- a/apps/mobile/src/features/explore/utils.ts +++ b/apps/mobile/src/features/explore/utils.ts @@ -1,11 +1,7 @@ import { TokenItemData } from 'src/components/explore/TokenItem' import { AppTFunction } from 'ui/src/i18n/types' import { TokenSortableField } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { - ClientTokensOrderBy, - TokenMetadataDisplayType, - TokensOrderBy, -} from 'wallet/src/features/wallet/types' +import { ClientTokensOrderBy, TokenMetadataDisplayType, TokensOrderBy } from 'wallet/src/features/wallet/types' /** * Returns server and client orderBy values to use for topTokens query and client side sorting @@ -28,14 +24,12 @@ export function getTokensOrderByValues(orderBy: TokensOrderBy): { const requiresClientOrderBy = Object.values(ClientTokensOrderBy).includes(orderBy) return { - serverOrderBy: requiresClientOrderBy - ? TokenSortableField.Volume - : (orderBy as TokenSortableField), + serverOrderBy: requiresClientOrderBy ? TokenSortableField.Volume : (orderBy as TokenSortableField), clientOrderBy: requiresClientOrderBy ? (orderBy as ClientTokensOrderBy) : orderBy === TokenSortableField.Volume - ? ClientTokensOrderBy.Volume24hDesc - : undefined, + ? ClientTokensOrderBy.Volume24hDesc + : undefined, } } @@ -43,7 +37,7 @@ export function getTokensOrderByValues(orderBy: TokensOrderBy): { * Returns a compare function to sort tokens client side. */ export function getClientTokensOrderByCompareFn( - orderBy: ClientTokensOrderBy + orderBy: ClientTokensOrderBy, ): (a: TokenItemData, b: TokenItemData) => number { let compareField: keyof TokenItemData let direction = 0 diff --git a/apps/mobile/src/features/externalProfile/ProfileContextMenu.tsx b/apps/mobile/src/features/externalProfile/ProfileContextMenu.tsx index 11dc9d11938..890af5e21bb 100644 --- a/apps/mobile/src/features/externalProfile/ProfileContextMenu.tsx +++ b/apps/mobile/src/features/externalProfile/ProfileContextMenu.tsx @@ -1,24 +1,27 @@ +import { SharedEventName } from '@uniswap/analytics-events' import React, { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { NativeSyntheticEvent, Share } from 'react-native' import ContextMenu, { ContextMenuOnPressNativeEvent } from 'react-native-context-menu-view' -import { useAppDispatch } from 'src/app/hooks' +import { useDispatch } from 'react-redux' import { TripleDot } from 'src/components/icons/TripleDot' import { disableOnPress } from 'src/utils/disableOnPress' import { Flex, HapticFeedback, TouchableArea } from 'ui/src' import { iconSizes } from 'ui/src/theme' import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' import { uniswapUrls } from 'uniswap/src/constants/urls' -import { WalletEventName } from 'uniswap/src/features/telemetry/constants' +import { ElementName, WalletEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { useUnitagByAddress } from 'uniswap/src/features/unitags/hooks' import { UniverseChainId } from 'uniswap/src/types/chains' +import { MobileScreens } from 'uniswap/src/types/screens/mobile' import { ShareableEntity } from 'uniswap/src/types/sharing' +import { setClipboard } from 'uniswap/src/utils/clipboard' +import { openUri } from 'uniswap/src/utils/linking' import { logger } from 'utilities/src/logger/logger' import { pushNotification } from 'wallet/src/features/notifications/slice' import { AppNotificationType, CopyNotificationType } from 'wallet/src/features/notifications/types' -import { setClipboard } from 'wallet/src/utils/clipboard' -import { ExplorerDataType, getExplorerLink, getProfileUrl, openUri } from 'wallet/src/utils/linking' +import { ExplorerDataType, getExplorerLink, getProfileUrl } from 'wallet/src/utils/linking' type MenuAction = { title: string @@ -28,7 +31,7 @@ type MenuAction = { export function ProfileContextMenu({ address }: { address: Address }): JSX.Element { const { t } = useTranslation() - const dispatch = useAppDispatch() + const dispatch = useDispatch() const { unitag } = useUnitagByAddress(address) const onPressCopyAddress = useCallback(async () => { @@ -37,9 +40,11 @@ export function ProfileContextMenu({ address }: { address: Address }): JSX.Eleme } await HapticFeedback.impact() await setClipboard(address) - dispatch( - pushNotification({ type: AppNotificationType.Copied, copyType: CopyNotificationType.Address }) - ) + dispatch(pushNotification({ type: AppNotificationType.Copied, copyType: CopyNotificationType.Address })) + sendAnalyticsEvent(SharedEventName.ELEMENT_CLICKED, { + element: ElementName.CopyAddress, + screen: MobileScreens.ExternalProfile, + }) }, [address, dispatch]) const openExplorerLink = useCallback(async () => { @@ -52,7 +57,7 @@ export function ProfileContextMenu({ address }: { address: Address }): JSX.Eleme params.append('tf_7005922218125', 'report_unitag') // Report Type Dropdown const prefilledRequestUrl = uniswapUrls.helpRequestUrl + '?' + params.toString() openUri(prefilledRequestUrl).catch((e) => - logger.error(e, { tags: { file: 'ProfileContextMenu', function: 'reportProfileLink' } }) + logger.error(e, { tags: { file: 'ProfileContextMenu', function: 'reportProfileLink' } }), ) }, [address]) @@ -110,13 +115,15 @@ export function ProfileContextMenu({ address }: { address: Address }): JSX.Eleme dropdownMenuMode={true} onPress={async (e: NativeSyntheticEvent): Promise => { await menuActions[e.nativeEvent.index]?.action() - }}> + }} + > + onLongPress={disableOnPress} + > diff --git a/apps/mobile/src/features/externalProfile/ProfileHeader.tsx b/apps/mobile/src/features/externalProfile/ProfileHeader.tsx index f256db1f631..4aa3e5d963c 100644 --- a/apps/mobile/src/features/externalProfile/ProfileHeader.tsx +++ b/apps/mobile/src/features/externalProfile/ProfileHeader.tsx @@ -3,7 +3,8 @@ import { useTranslation } from 'react-i18next' import { StatusBar, StyleSheet } from 'react-native' import { FadeIn } from 'react-native-reanimated' import Svg, { ClipPath, Defs, RadialGradient, Rect, Stop } from 'react-native-svg' -import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { useDispatch } from 'react-redux' +import { useAppSelector } from 'src/app/hooks' import { BackButton } from 'src/components/buttons/BackButton' import { Favorite } from 'src/components/icons/Favorite' import { LongText } from 'src/components/text/LongText' @@ -26,14 +27,15 @@ import { ENS_LOGO } from 'ui/src/assets' import { SendAction, XTwitter } from 'ui/src/components/icons' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { iconSizes, imageSizes } from 'ui/src/theme' -import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants' +import { ModalName } from 'uniswap/src/features/telemetry/constants' +import { CurrencyField } from 'uniswap/src/features/transactions/transactionState/types' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' +import { openUri } from 'uniswap/src/utils/linking' import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' import { useENSDescription, useENSName, useENSTwitterUsername } from 'wallet/src/features/ens/api' import { selectWatchedAddressSet } from 'wallet/src/features/favorites/selectors' -import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' import { useAvatar, useDisplayName } from 'wallet/src/features/wallet/hooks' import { DisplayNameType } from 'wallet/src/features/wallet/types' -import { openUri } from 'wallet/src/utils/linking' const HEADER_GRADIENT_HEIGHT = 144 const HEADER_ICON_SIZE = 72 @@ -49,11 +51,9 @@ export const solidHeaderProps = { maxOpacity: HEADER_SOLID_COLOR_OPACITY, } -export const ProfileHeader = memo(function ProfileHeader({ - address, -}: ProfileHeaderProps): JSX.Element { +export const ProfileHeader = memo(function ProfileHeader({ address }: ProfileHeaderProps): JSX.Element { const colors = useSporeColors() - const dispatch = useAppDispatch() + const dispatch = useDispatch() const isDarkMode = useIsDarkMode() const isFavorited = useAppSelector(selectWatchedAddressSet).has(address) @@ -61,8 +61,7 @@ export const ProfileHeader = memo(function ProfileHeader({ // Note that if a user has a Unitag AND ENS, this prioritizes the Unitag's metadata over the ENS metadata const nameToFetchENSMetadata = - (displayName?.type === DisplayNameType.ENS || displayName?.type === DisplayNameType.Unitag) && - displayName?.name + (displayName?.type === DisplayNameType.ENS || displayName?.type === DisplayNameType.Unitag) && displayName?.name ? displayName.name : undefined @@ -109,7 +108,7 @@ export const ProfileHeader = memo(function ProfileHeader({ openModal({ name: ModalName.Send, ...{ initialState: initialSendState }, - }) + }), ) }, [dispatch, initialSendState]) @@ -123,11 +122,7 @@ export const ProfileHeader = memo(function ProfileHeader({ return ( - + {/* fixed gradient at 0.2 opacity overlaid on surface1 */} - + top={0} + > + - {bio ? ( - - ) : null} + {bio ? : null} {(twitter || showENSName) && ( - + {twitter ? ( @@ -235,8 +219,9 @@ export const ProfileHeader = memo(function ProfileHeader({ p="$spacing12" shadowColor="$neutral1" style={styles.buttonShadow} - testID={ElementName.Favorite} - onPress={onPressFavorite}> + testID={TestID.Favorite} + onPress={onPressFavorite} + > + testID={TestID.Send} + onPress={onPressSend} + > - + {t('common.button.send')} diff --git a/apps/mobile/src/features/favorites/hooks.ts b/apps/mobile/src/features/favorites/hooks.ts index f0650cad852..84135cebcb7 100644 --- a/apps/mobile/src/features/favorites/hooks.ts +++ b/apps/mobile/src/features/favorites/hooks.ts @@ -1,12 +1,11 @@ import { useCallback, useMemo } from 'react' -import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { useDispatch } from 'react-redux' +import { useAppSelector } from 'src/app/hooks' import { MobileEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { CurrencyId } from 'uniswap/src/types/currency' -import { - makeSelectHasTokenFavorited, - selectWatchedAddressSet, -} from 'wallet/src/features/favorites/selectors' +import { currencyIdToAddress, currencyIdToChain } from 'uniswap/src/utils/currencyId' +import { makeSelectHasTokenFavorited, selectWatchedAddressSet } from 'wallet/src/features/favorites/selectors' import { addFavoriteToken, addWatchedAddress, @@ -15,10 +14,9 @@ import { } from 'wallet/src/features/favorites/slice' import { useCurrencyInfo } from 'wallet/src/features/tokens/useCurrencyInfo' import { useDisplayName } from 'wallet/src/features/wallet/hooks' -import { currencyIdToAddress, currencyIdToChain } from 'wallet/src/utils/currencyId' export function useToggleFavoriteCallback(id: CurrencyId, isFavoriteToken: boolean): () => void { - const dispatch = useAppDispatch() + const dispatch = useDispatch() const token = useCurrencyInfo(id) return useCallback(() => { @@ -37,7 +35,7 @@ export function useToggleFavoriteCallback(id: CurrencyId, isFavoriteToken: boole } export function useToggleWatchedWalletCallback(address: Address): () => void { - const dispatch = useAppDispatch() + const dispatch = useDispatch() const isFavoriteWallet = useAppSelector(selectWatchedAddressSet).has(address) const displayName = useDisplayName(address) diff --git a/apps/mobile/src/features/fiatOnRamp/ExchangeTransferModal.tsx b/apps/mobile/src/features/fiatOnRamp/ExchangeTransferModal.tsx index 88d05d5f7d1..9b6144524ca 100644 --- a/apps/mobile/src/features/fiatOnRamp/ExchangeTransferModal.tsx +++ b/apps/mobile/src/features/fiatOnRamp/ExchangeTransferModal.tsx @@ -1,4 +1,5 @@ -import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { useDispatch } from 'react-redux' +import { useAppSelector } from 'src/app/hooks' import { closeModal } from 'src/features/modals/modalSlice' import { selectModalState } from 'src/features/modals/selectModalState' import { ExchangeTransferConnecting } from 'src/screens/ExchangeTransferConnecting' @@ -6,7 +7,7 @@ import { BottomSheetModal } from 'uniswap/src/components/modals/BottomSheetModal import { ModalName } from 'uniswap/src/features/telemetry/constants' export function ExchangeTransferModal(): JSX.Element | null { - const dispatch = useAppDispatch() + const dispatch = useDispatch() const onClose = (): void => { dispatch(closeModal({ name: ModalName.ExchangeTransferModal })) } @@ -21,7 +22,8 @@ export function ExchangeTransferModal(): JSX.Element | null { hideKeyboardOnDismiss renderBehindTopInset name={ModalName.ExchangeTransferModal} - onClose={onClose}> + onClose={onClose} + > ) : null diff --git a/apps/mobile/src/features/fiatOnRamp/ExchangeTransferServiceProviderSelector.tsx b/apps/mobile/src/features/fiatOnRamp/ExchangeTransferServiceProviderSelector.tsx index ccc07d8e26c..28e22caf474 100644 --- a/apps/mobile/src/features/fiatOnRamp/ExchangeTransferServiceProviderSelector.tsx +++ b/apps/mobile/src/features/fiatOnRamp/ExchangeTransferServiceProviderSelector.tsx @@ -1,14 +1,14 @@ import React, { useCallback } from 'react' import { FlatList, ListRenderItemInfo } from 'react-native' import { FadeIn, FadeOut } from 'react-native-reanimated' -import { useAppDispatch } from 'src/app/hooks' +import { useDispatch } from 'react-redux' import { openModal } from 'src/features/modals/modalSlice' import { Flex, ImpactFeedbackStyle, Text, TouchableArea, useIsDarkMode } from 'ui/src' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { iconSizes } from 'ui/src/theme' import { FORServiceProvider } from 'uniswap/src/features/fiatOnRamp/types' +import { getServiceProviderLogo } from 'uniswap/src/features/fiatOnRamp/utils' import { ModalName } from 'uniswap/src/features/telemetry/constants' -import { getServiceProviderLogo } from 'wallet/src/features/fiatOnRamp/utils' import { RemoteImage } from 'wallet/src/features/images/RemoteImage' function key(item: FORServiceProvider): string { @@ -42,7 +42,8 @@ function CEXItemWrapper({ gap="$spacing12" maxWidth="100%" mx="$spacing8" - p="$spacing16"> + p="$spacing16" + > void serviceProviders: FORServiceProvider[] }): JSX.Element { - const dispatch = useAppDispatch() + const dispatch = useDispatch() const onSelectServiceProvider = useCallback( (serviceProvider: FORServiceProvider) => { @@ -75,21 +76,18 @@ export function ServiceProviderSelector({ openModal({ name: ModalName.ExchangeTransferModal, initialState: { serviceProvider }, - }) + }), ) onClose() }, - [dispatch, onClose] + [dispatch, onClose], ) const renderItem = useCallback( ({ item: serviceProvider }: ListRenderItemInfo) => ( - + ), - [onSelectServiceProvider] + [onSelectServiceProvider], ) return ( diff --git a/apps/mobile/src/features/fiatOnRamp/FiatOnRampAggregatorModal.tsx b/apps/mobile/src/features/fiatOnRamp/FiatOnRampAggregatorModal.tsx index 0bb53b9c2ec..70e5b50fde5 100644 --- a/apps/mobile/src/features/fiatOnRamp/FiatOnRampAggregatorModal.tsx +++ b/apps/mobile/src/features/fiatOnRamp/FiatOnRampAggregatorModal.tsx @@ -1,5 +1,5 @@ import React, { useCallback } from 'react' -import { useAppDispatch } from 'src/app/hooks' +import { useDispatch } from 'react-redux' import { FiatOnRampStackNavigator } from 'src/app/navigation/navigation' import { closeModal } from 'src/features/modals/modalSlice' import { useSporeColors } from 'ui/src' @@ -9,7 +9,7 @@ import { ModalName } from 'uniswap/src/features/telemetry/constants' export function FiatOnRampAggregatorModal(): JSX.Element { const colors = useSporeColors() - const dispatch = useAppDispatch() + const dispatch = useDispatch() const onClose = useCallback((): void => { dispatch(closeModal({ name: ModalName.FiatOnRampAggregator })) }, [dispatch]) @@ -21,11 +21,9 @@ export function FiatOnRampAggregatorModal(): JSX.Element { hideKeyboardOnDismiss renderBehindTopInset backgroundColor={colors.surface1.get()} - // Don't dismiss on back press, as this modal is used for the FiatOnRampStack navigation. - // (the modal should be dismissed only when the user navigates to the initial FiatOnRamp screen) - dismissOnBackPress={false} name={ModalName.FiatOnRampAggregator} - onClose={onClose}> + onClose={onClose} + > ) diff --git a/apps/mobile/src/features/fiatOnRamp/FiatOnRampAmountSection.tsx b/apps/mobile/src/features/fiatOnRamp/FiatOnRampAmountSection.tsx index 7a239988267..eff43f7f745 100644 --- a/apps/mobile/src/features/fiatOnRamp/FiatOnRampAmountSection.tsx +++ b/apps/mobile/src/features/fiatOnRamp/FiatOnRampAmountSection.tsx @@ -16,6 +16,7 @@ import { fonts, spacing } from 'ui/src/theme' import { Pill } from 'uniswap/src/components/pill/Pill' import { SelectTokenButton } from 'uniswap/src/features/fiatOnRamp/SelectTokenButton' import { FiatCurrencyInfo, FiatOnRampCurrency } from 'uniswap/src/features/fiatOnRamp/types' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { usePrevious } from 'utilities/src/react/hooks' import { DEFAULT_DELAY, useDebounce } from 'utilities/src/time/timing' import { AmountInput } from 'wallet/src/components/input/AmountInput' @@ -130,23 +131,16 @@ export function FiatOnRampAmountSection({ // Design has asked to make it around 100ms and DEFAULT_DELAY is 200ms const debouncedErrorText = useDebounce(errorText, DEFAULT_DELAY / 2) - const formattedAmount = useFormatExactCurrencyAmount( - quoteAmount.toString(), - currency.currencyInfo?.currency - ) + const formattedAmount = useFormatExactCurrencyAmount(quoteAmount.toString(), currency.currencyInfo?.currency) return ( - + + mt={appFiatCurrencySupported ? '$spacing48' : '$spacing24'} + > {debouncedErrorText && errorColor && ( {debouncedErrorText} @@ -160,7 +154,8 @@ export function FiatOnRampAmountSection({ color={!value ? '$neutral3' : '$neutral1'} fontSize={fontSize} height={fontSize} - lineHeight={fontSize}> + lineHeight={fontSize} + > {fiatCurrencyInfo.symbol} )} @@ -254,7 +250,8 @@ function PredefinedAmount({ onPress={async (): Promise => { await HapticFeedback.impact() onPress(amount.toString()) - }}> + }} + > [] | undefined @@ -30,8 +25,6 @@ interface FiatOnRampContextType { setQuoteCurrency: (quoteCurrency: FiatOnRampCurrency) => void amount?: number setAmount: (amount: number | undefined) => void - serviceProviders?: FORServiceProvider[] - setServiceProviders: (serviceProviders: FORServiceProvider[] | undefined) => void } const initialState: FiatOnRampContextType = { @@ -42,7 +35,6 @@ const initialState: FiatOnRampContextType = { setBaseCurrencyInfo: () => undefined, setQuoteCurrency: () => undefined, setAmount: () => undefined, - setServiceProviders: () => undefined, countryCode: '', countryState: undefined, quoteCurrency: { currencyInfo: undefined }, @@ -61,11 +53,10 @@ export function FiatOnRampProvider({ children }: { children: React.ReactNode }): const [countryState, setCountryState] = useState() const [baseCurrencyInfo, setBaseCurrencyInfo] = useState() const [amount, setAmount] = useState() - const [serviceProviders, setServiceProviders] = useState() // We hardcode ETH as the starting currency const ethCurrencyInfo = useCurrencyInfo( - buildCurrencyId(UniverseChainId.Mainnet, getNativeAddress(UniverseChainId.Mainnet)) + buildCurrencyId(UniverseChainId.Mainnet, getNativeAddress(UniverseChainId.Mainnet)), ) const [quoteCurrency, setQuoteCurrency] = useState({ currencyInfo: ethCurrencyInfo, @@ -89,9 +80,8 @@ export function FiatOnRampProvider({ children }: { children: React.ReactNode }): setQuoteCurrency, amount, setAmount, - serviceProviders, - setServiceProviders, - }}> + }} + > {children} ) diff --git a/apps/mobile/src/features/fiatOnRamp/FiatOnRampCountryListModal.tsx b/apps/mobile/src/features/fiatOnRamp/FiatOnRampCountryListModal.tsx index f2403ae776e..4e817a492b7 100644 --- a/apps/mobile/src/features/fiatOnRamp/FiatOnRampCountryListModal.tsx +++ b/apps/mobile/src/features/fiatOnRamp/FiatOnRampCountryListModal.tsx @@ -1,25 +1,25 @@ import { BottomSheetFlatList } from '@gorhom/bottom-sheet' import React, { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -import { ListRenderItemInfo } from 'react-native' +import { Keyboard, ListRenderItemInfo } from 'react-native' import { FadeIn, FadeOut } from 'react-native-reanimated' import { SvgUri } from 'react-native-svg' import { Loader } from 'src/components/loading' -import { FOR_MODAL_SNAP_POINTS } from 'src/features/fiatOnRamp/constants' import { Flex, Text, TouchableArea, useDeviceInsets, useSporeColors } from 'ui/src' import Check from 'ui/src/assets/icons/check.svg' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { useDeviceDimensions } from 'ui/src/hooks/useDeviceDimensions' import { fonts, iconSizes, spacing } from 'ui/src/theme' import { BottomSheetModal } from 'uniswap/src/components/modals/BottomSheetModal' +import { useBottomSheetFocusHook } from 'uniswap/src/components/modals/hooks' import { useFiatOnRampAggregatorCountryListQuery } from 'uniswap/src/features/fiatOnRamp/api' +import { FOR_MODAL_SNAP_POINTS } from 'uniswap/src/features/fiatOnRamp/constants' import { FORCountry } from 'uniswap/src/features/fiatOnRamp/types' import { getCountryFlagSvgUrl } from 'uniswap/src/features/fiatOnRamp/utils' +import { SearchTextInput } from 'uniswap/src/features/search/SearchTextInput' import { ModalName } from 'uniswap/src/features/telemetry/constants' import { bubbleToTop } from 'utilities/src/primitives/array' import { useDebounce } from 'utilities/src/time/timing' -import { useBottomSheetFocusHook } from 'wallet/src/components/modals/hooks' -import { SearchTextInput } from 'wallet/src/features/search/SearchTextInput' const ICON_SIZE = 32 // design prefers a custom value here @@ -32,10 +32,7 @@ function key(item: FORCountry): string { return item.countryCode } -function CountrySelectorContent({ - onSelectCountry, - countryCode, -}: CountrySelectorProps): JSX.Element { +function CountrySelectorContent({ onSelectCountry, countryCode }: CountrySelectorProps): JSX.Element { const { t } = useTranslation() const insets = useDeviceInsets() const colors = useSporeColors() @@ -51,9 +48,7 @@ function CountrySelectorContent({ return [] } return bubbleToTop(data.supportedCountries, (c) => c.countryCode === countryCode).filter( - (item) => - !debouncedSearchText || - item.displayName.toLowerCase().startsWith(debouncedSearchText.toLowerCase()) + (item) => !debouncedSearchText || item.displayName.toLowerCase().startsWith(debouncedSearchText.toLowerCase()), ) }, [countryCode, data, debouncedSearchText]) @@ -64,28 +59,20 @@ function CountrySelectorContent({ return ( onSelectCountry(item)}> - + {item.displayName} {item.countryCode === countryCode && ( - + )} ) }, - [colors.accent1, countryCode, onSelectCountry] + [colors.accent1, countryCode, onSelectCountry], ) return ( @@ -99,6 +86,7 @@ function CountrySelectorContent({ py="$spacing8" value={searchText} onChangeText={setSearchText} + onDismiss={() => Keyboard.dismiss()} /> @@ -162,7 +150,8 @@ export function FiatOnRampCountryListModal({ backgroundColor={colors.surface1.get()} name={ModalName.FiatOnRampCountryList} snapPoints={FOR_MODAL_SNAP_POINTS} - onClose={onClose}> + onClose={onClose} + > ) diff --git a/apps/mobile/src/features/fiatOnRamp/FiatOnRampModal.tsx b/apps/mobile/src/features/fiatOnRamp/FiatOnRampModal.tsx deleted file mode 100644 index 7651eaa62db..00000000000 --- a/apps/mobile/src/features/fiatOnRamp/FiatOnRampModal.tsx +++ /dev/null @@ -1,319 +0,0 @@ -import React, { useCallback, useEffect, useRef, useState } from 'react' -import { useTranslation } from 'react-i18next' -import { StyleSheet, TextInput } from 'react-native' -import { FadeIn, FadeOut, FadeOutDown } from 'react-native-reanimated' -import { useAppDispatch, useShouldShowNativeKeyboard } from 'src/app/hooks' -import { FiatOnRampCtaButton } from 'src/components/fiatOnRamp/CtaButton' -import { FiatOnRampAmountSection } from 'src/features/fiatOnRamp/FiatOnRampAmountSection' -import { FiatOnRampConnectingView } from 'src/features/fiatOnRamp/FiatOnRampConnecting' -import { FiatOnRampTokenSelectorModal } from 'src/features/fiatOnRamp/FiatOnRampTokenSelector' -import { ServiceProviderLogoStyles } from 'src/features/fiatOnRamp/constants' -import { useMoonpayFiatOnRamp, useMoonpaySupportedTokens } from 'src/features/fiatOnRamp/hooks' -import { closeModal } from 'src/features/modals/modalSlice' -import { Flex, Text, useDeviceInsets, useSporeColors } from 'ui/src' -import MoonpayLogo from 'ui/src/assets/logos/svg/moonpay.svg' -import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' -import { TextInputProps } from 'uniswap/src/components/input/TextInput' -import { useBottomSheetContext } from 'uniswap/src/components/modals/BottomSheetContext' -import { BottomSheetModal } from 'uniswap/src/components/modals/BottomSheetModal' -import { HandleBar } from 'uniswap/src/components/modals/HandleBar' -import { FiatOnRampCurrency } from 'uniswap/src/features/fiatOnRamp/types' -import { FiatOnRampEventName, ModalName } from 'uniswap/src/features/telemetry/constants' -import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' -import { UniverseEventProperties } from 'uniswap/src/features/telemetry/types' -import { UniverseChainId } from 'uniswap/src/types/chains' -import { NumberType } from 'utilities/src/format/types' -import { useTimeout } from 'utilities/src/time/timing' -import { DecimalPadLegacy } from 'wallet/src/components/legacy/DecimalPadLegacy' -import { getNativeAddress } from 'wallet/src/constants/addresses' -import { useLocalFiatToUSDConverter } from 'wallet/src/features/fiatCurrency/hooks' -import { useMoonpayFiatCurrencySupportInfo } from 'wallet/src/features/fiatOnRamp/hooks' -import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' -import { useCurrencyInfo } from 'wallet/src/features/tokens/useCurrencyInfo' -import { buildCurrencyId } from 'wallet/src/utils/currencyId' -import { openUri } from 'wallet/src/utils/linking' - -const MOONPAY_UNSUPPORTED_REGION_HELP_URL = - 'https://support.uniswap.org/hc/en-us/articles/11306664890381-Why-isn-t-MoonPay-available-in-my-region-' - -const PREDEFINED_AMOUNTS_SUPPORTED_CURRENCIES = ['USD', 'EUR', 'GBP', 'AUD', 'CAD', 'SGD'] - -const CONNECTING_TIMEOUT = 2000 - -export function FiatOnRampModal(): JSX.Element { - const colors = useSporeColors() - - const dispatch = useAppDispatch() - const onClose = useCallback((): void => { - dispatch(closeModal({ name: ModalName.FiatOnRamp })) - }, [dispatch]) - - return ( - - - - ) -} - -function FiatOnRampContent({ onClose }: { onClose: () => void }): JSX.Element { - const { t } = useTranslation() - const { formatNumberOrString } = useLocalizationContext() - const inputRef = useRef(null) - - const { isSheetReady } = useBottomSheetContext() - - const [showConnectingToMoonpayScreen, setShowConnectingToMoonpayScreen] = useState(false) - - const { showNativeKeyboard, onDecimalPadLayout, isLayoutPending, onInputPanelLayout } = - useShouldShowNativeKeyboard() - - const [selection, setSelection] = useState() - - const resetSelection = (start: number, end?: number): void => { - setSelection({ start, end: end ?? start }) - } - - const [value, setValue] = useState('') - - // We hardcode ETH as the starting currency - const ethCurrencyInfo = useCurrencyInfo( - buildCurrencyId(UniverseChainId.Mainnet, getNativeAddress(UniverseChainId.Mainnet)) - ) - - const [currency, setCurrency] = useState({ - currencyInfo: ethCurrencyInfo, - moonpayCurrencyCode: 'eth', - }) - - const { appFiatCurrencySupportedInMoonpay, moonpaySupportedFiatCurrency } = - useMoonpayFiatCurrencySupportInfo() - - // We only support predefined amounts for certain currencies. - // If the user's app fiat currency is not supported in Moonpay, - // we fallback to USD (which does allow for predefined amounts) - const predefinedAmountsSupported = - PREDEFINED_AMOUNTS_SUPPORTED_CURRENCIES.includes(moonpaySupportedFiatCurrency.code) || - !appFiatCurrencySupportedInMoonpay - - // We might not have ethCurrencyInfo when this component is initially rendered. - // If `ethCurrencyInfo` becomes available later while currency.currencyInfo is still unset, we update the currency state accordingly. - useEffect(() => { - if (ethCurrencyInfo && !currency.currencyInfo) { - setCurrency({ ...currency, currencyInfo: ethCurrencyInfo }) - } - }, [currency, currency.currencyInfo, ethCurrencyInfo]) - - const { - eligible, - quoteAmount, - isLoading, - isError, - externalTransactionId, - dispatchAddTransaction, - fiatOnRampHostUrl, - quoteCurrencyAmountReady, - quoteCurrencyAmountLoading, - errorText, - errorColor, - } = useMoonpayFiatOnRamp({ - baseCurrencyAmount: value, - quoteCurrencyCode: currency.moonpayCurrencyCode, - quoteChainId: currency.currencyInfo?.currency.chainId ?? UniverseChainId.Mainnet, - }) - - useTimeout( - async () => { - if (fiatOnRampHostUrl) { - if (currency?.moonpayCurrencyCode) { - sendAnalyticsEvent(FiatOnRampEventName.FiatOnRampWidgetOpened, { - externalTransactionId, - serviceProvider: 'MOONPAY', - fiatCurrency: moonpaySupportedFiatCurrency.code.toLowerCase(), - cryptoCurrency: currency.moonpayCurrencyCode.toLowerCase(), - }) - } - await openUri(fiatOnRampHostUrl) - dispatchAddTransaction() - onClose() - } - }, - // setTimeout would be called inside this hook, only when delay >= 0 - showConnectingToMoonpayScreen ? CONNECTING_TIMEOUT : -1 - ) - - const buttonEnabled = - !isLoading && (!eligible || (!isError && fiatOnRampHostUrl && quoteCurrencyAmountReady)) - - const fiatToUSDConverter = useLocalFiatToUSDConverter() - - const onChangeValue = - (source: UniverseEventProperties[FiatOnRampEventName.FiatOnRampAmountEntered]['source']) => - (newAmount: string): void => { - sendAnalyticsEvent(FiatOnRampEventName.FiatOnRampAmountEntered, { - source, - amountUSD: fiatToUSDConverter(parseFloat(newAmount)), - }) - setValue(newAmount) - } - - const [showTokenSelector, setShowTokenSelector] = useState(false) - - useEffect(() => { - if (showTokenSelector) { - // hide keyboard when user goes to token selector screen - inputRef.current?.blur() - } else if (showNativeKeyboard && eligible) { - // autofocus - inputRef.current?.focus() - } - }, [showNativeKeyboard, eligible, showTokenSelector]) - - const selectTokenLoading = quoteCurrencyAmountLoading && !errorText && !!value - - const { - list: supportedTokensList, - loading: supportedTokensLoading, - error: supportedTokensError, - refetch: supportedTokensRefetch, - } = useMoonpaySupportedTokens() - - const insets = useDeviceInsets() - - const onSelectCurrency = (newCurrency: FiatOnRampCurrency): void => { - setCurrency(newCurrency) - setShowTokenSelector(false) - if (newCurrency.currencyInfo?.currency.symbol) { - sendAnalyticsEvent(FiatOnRampEventName.FiatOnRampTokenSelected, { - token: newCurrency.currencyInfo.currency.symbol.toLowerCase(), - }) - } - } - - return ( - - {!showConnectingToMoonpayScreen && ( - - {isSheetReady && ( - - - {t('common.button.buy')} - { - setShowTokenSelector(true) - }} - /> - - {!showNativeKeyboard && ( - - )} - => { - if (eligible) { - setShowConnectingToMoonpayScreen(true) - } else { - await openUri(MOONPAY_UNSUPPORTED_REGION_HELP_URL) - } - }} - /> - - - )} - {showTokenSelector && ( - setShowTokenSelector(false)} - onRetry={supportedTokensRefetch} - onSelectCurrency={onSelectCurrency} - /> - )} - - )} - {showConnectingToMoonpayScreen && ( - - - - } - serviceProviderName="MoonPay" - /> - )} - - ) -} - -const styles = StyleSheet.create({ - moonpayLogoWrapper: { - backgroundColor: '#7D00FF', - }, -}) diff --git a/apps/mobile/src/features/fiatOnRamp/FiatOnRampTokenSelector.tsx b/apps/mobile/src/features/fiatOnRamp/FiatOnRampTokenSelector.tsx index 53669b009f6..d287d3c02ac 100644 --- a/apps/mobile/src/features/fiatOnRamp/FiatOnRampTokenSelector.tsx +++ b/apps/mobile/src/features/fiatOnRamp/FiatOnRampTokenSelector.tsx @@ -2,10 +2,10 @@ import React from 'react' import { useTranslation } from 'react-i18next' import { FadeIn, FadeOut } from 'react-native-reanimated' import { TokenFiatOnRampList } from 'src/components/TokenSelector/TokenFiatOnRampList' -import { FOR_MODAL_SNAP_POINTS } from 'src/features/fiatOnRamp/constants' import { Flex, Text, useSporeColors } from 'ui/src' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { BottomSheetModal } from 'uniswap/src/components/modals/BottomSheetModal' +import { FOR_MODAL_SNAP_POINTS } from 'uniswap/src/features/fiatOnRamp/constants' import { FiatOnRampCurrency } from 'uniswap/src/features/fiatOnRamp/types' import Trace from 'uniswap/src/features/telemetry/Trace' import { ElementName, ModalName, SectionName } from 'uniswap/src/features/telemetry/constants' @@ -40,11 +40,9 @@ export function FiatOnRampTokenSelectorModal({ backgroundColor={colors.surface1.get()} name={ModalName.FiatOnRampCountryList} snapPoints={FOR_MODAL_SNAP_POINTS} - onClose={onClose}> - + onClose={onClose} + > + {t('fiatOnRamp.button.chooseToken')} diff --git a/apps/mobile/src/features/fiatOnRamp/aggregatorHooks.ts b/apps/mobile/src/features/fiatOnRamp/aggregatorHooks.ts deleted file mode 100644 index 90e8d4a1639..00000000000 --- a/apps/mobile/src/features/fiatOnRamp/aggregatorHooks.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { SerializedError } from '@reduxjs/toolkit' -import { FetchBaseQueryError, skipToken } from '@reduxjs/toolkit/query/react' -import { useTranslation } from 'react-i18next' -import { Delay } from 'src/components/layout/Delayed' -import { ColorTokens } from 'ui/src' -import { - useFiatOnRampAggregatorCryptoQuoteQuery, - useFiatOnRampAggregatorSupportedFiatCurrenciesQuery, -} from 'uniswap/src/features/fiatOnRamp/api' -import { - FORQuote, - FORSupportedFiatCurrency, - FiatCurrencyInfo, -} from 'uniswap/src/features/fiatOnRamp/types' -import { NumberType } from 'utilities/src/format/types' -import { useDebounce } from 'utilities/src/time/timing' -import { FiatCurrency } from 'wallet/src/features/fiatCurrency/constants' -import { useAppFiatCurrencyInfo, useFiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks' -import { - isFiatOnRampApiError, - isInvalidRequestAmountTooHigh, - isInvalidRequestAmountTooLow, -} from 'wallet/src/features/fiatOnRamp/utils' -import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' -import { useActiveAccountAddress } from 'wallet/src/features/wallet/hooks' - -export function useMeldFiatCurrencySupportInfo(countryCode: string): { - appFiatCurrencySupportedInMeld: boolean - meldSupportedFiatCurrency: FiatCurrencyInfo - supportedFiatCurrencies: FORSupportedFiatCurrency[] | undefined -} { - // Not all the currencies are supported by Meld, so we need to fallback to USD if the currency is not supported - const appFiatCurrencyInfo = useAppFiatCurrencyInfo() - const fallbackCurrencyInfo = useFiatCurrencyInfo(FiatCurrency.UnitedStatesDollar) - const appFiatCurrencyCode = appFiatCurrencyInfo.code.toLowerCase() - - const { data: supportedFiatCurrencies } = useFiatOnRampAggregatorSupportedFiatCurrenciesQuery({ - countryCode, - }) - - const appFiatCurrencySupported = - !supportedFiatCurrencies || - supportedFiatCurrencies.fiatCurrencies.some( - (currency): boolean => appFiatCurrencyCode === currency.fiatCurrencyCode.toLowerCase() - ) - const meldSupportedFiatCurrency = appFiatCurrencySupported - ? appFiatCurrencyInfo - : fallbackCurrencyInfo - - return { - appFiatCurrencySupportedInMeld: appFiatCurrencySupported, - meldSupportedFiatCurrency, - supportedFiatCurrencies: supportedFiatCurrencies?.fiatCurrencies, - } -} - -/** - * Hook to load quotes - */ -export function useFiatOnRampQuotes({ - baseCurrencyAmount, - baseCurrencyCode, - quoteCurrencyCode, - countryCode, - countryState, -}: { - baseCurrencyAmount?: number - baseCurrencyCode: string | undefined - quoteCurrencyCode: string | undefined - countryCode: string | undefined - countryState: string | undefined -}): { - loading: boolean - error?: FetchBaseQueryError | SerializedError - quotes: FORQuote[] | undefined -} { - const debouncedBaseCurrencyAmount = useDebounce(baseCurrencyAmount, Delay.Short) - const walletAddress = useActiveAccountAddress() - - const { - currentData: quotesResponse, - isFetching: quotesFetching, - error: quotesError, - } = useFiatOnRampAggregatorCryptoQuoteQuery( - baseCurrencyAmount && countryCode && quoteCurrencyCode && baseCurrencyCode - ? { - sourceAmount: baseCurrencyAmount, - sourceCurrencyCode: baseCurrencyCode, - destinationCurrencyCode: quoteCurrencyCode, - countryCode, - walletAddress: walletAddress ?? '', - state: countryState, - } - : skipToken, - { - refetchOnMountOrArgChange: true, - } - ) - - const loading = quotesFetching || debouncedBaseCurrencyAmount !== baseCurrencyAmount - - // if user is entering base amount -> ignore previous errors - const error = debouncedBaseCurrencyAmount !== baseCurrencyAmount ? undefined : quotesError - - return { - loading, - error, - quotes: quotesResponse?.quotes ?? undefined, - } -} - -export function useParseFiatOnRampError( - error: unknown, - currencyCode: string -): { - errorText: string | undefined - errorColor: ColorTokens | undefined -} { - const { t } = useTranslation() - const { formatNumberOrString } = useLocalizationContext() - - let errorText, errorColor: ColorTokens | undefined - if (!error) { - return { errorText, errorColor } - } - - errorText = t('fiatOnRamp.error.default') - errorColor = '$DEP_accentWarning' - - if (isFiatOnRampApiError(error)) { - if (isInvalidRequestAmountTooLow(error)) { - const formattedAmount = formatNumberOrString({ - value: error.data.context.minimumAllowed, - type: NumberType.FiatStandard, - currencyCode, - }) - errorText = t('fiatOnRamp.error.min', { amount: formattedAmount }) - errorColor = '$statusCritical' - } else if (isInvalidRequestAmountTooHigh(error)) { - const formattedAmount = formatNumberOrString({ - value: error.data.context.maximumAllowed, - type: NumberType.FiatStandard, - currencyCode, - }) - errorText = t('fiatOnRamp.error.max', { amount: formattedAmount }) - errorColor = '$statusCritical' - } - } - - return { errorText, errorColor } -} diff --git a/apps/mobile/src/features/fiatOnRamp/constants.ts b/apps/mobile/src/features/fiatOnRamp/constants.ts deleted file mode 100644 index 02934f40bf1..00000000000 --- a/apps/mobile/src/features/fiatOnRamp/constants.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { StyleSheet } from 'react-native' - -export const FOR_MODAL_SNAP_POINTS = ['70%', '100%'] -export const SERVICE_PROVIDER_ICON_SIZE = 90 -export const SERVICE_PROVIDER_ICON_BORDER_RADIUS = 20 - -export const ServiceProviderLogoStyles = StyleSheet.create({ - icon: { - height: SERVICE_PROVIDER_ICON_SIZE, - width: SERVICE_PROVIDER_ICON_SIZE, - }, -}) diff --git a/apps/mobile/src/features/fiatOnRamp/hooks.ts b/apps/mobile/src/features/fiatOnRamp/hooks.ts index 9e8c53b9207..f57fbf211c8 100644 --- a/apps/mobile/src/features/fiatOnRamp/hooks.ts +++ b/apps/mobile/src/features/fiatOnRamp/hooks.ts @@ -1,33 +1,39 @@ -import { skipToken } from '@reduxjs/toolkit/query/react' +import { SerializedError } from '@reduxjs/toolkit' +import { FetchBaseQueryError, skipToken } from '@reduxjs/toolkit/query/react' import { Currency } from '@uniswap/sdk-core' import { useCallback, useMemo, useRef } from 'react' import { useTranslation } from 'react-i18next' -import { useAppDispatch } from 'src/app/hooks' +import { useDispatch } from 'react-redux' import { Delay } from 'src/components/layout/Delayed' -import { ColorTokens, useSporeColors } from 'ui/src' -import { uniswapUrls } from 'uniswap/src/constants/urls' +import { ColorTokens } from 'ui/src' +import { toSupportedChainId } from 'uniswap/src/features/chains/utils' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' -import { useFiatOnRampAggregatorSupportedTokensQuery } from 'uniswap/src/features/fiatOnRamp/api' -import { FORSupportedToken, FiatOnRampCurrency } from 'uniswap/src/features/fiatOnRamp/types' +import { + useFiatOnRampAggregatorCryptoQuoteQuery, + useFiatOnRampAggregatorSupportedFiatCurrenciesQuery, + useFiatOnRampAggregatorSupportedTokensQuery, +} from 'uniswap/src/features/fiatOnRamp/api' +import { + FORQuote, + FORSupportedFiatCurrency, + FORSupportedToken, + FiatCurrencyInfo, + FiatOnRampCurrency, +} from 'uniswap/src/features/fiatOnRamp/types' +import { + createOnRampTransactionId, + isFiatOnRampApiError, + isInvalidRequestAmountTooHigh, + isInvalidRequestAmountTooLow, +} from 'uniswap/src/features/fiatOnRamp/utils' import { WalletChainId } from 'uniswap/src/types/chains' -import { logger } from 'utilities/src/logger/logger' +import { buildCurrencyId, buildNativeCurrencyId } from 'uniswap/src/utils/currencyId' +import { NumberType } from 'utilities/src/format/types' import { useDebounce } from 'utilities/src/time/timing' -import { - useAllCommonBaseCurrencies, - useCurrencies, -} from 'wallet/src/components/TokenSelector/hooks' -import { BRIDGED_BASE_ADDRESSES } from 'wallet/src/constants/addresses' +import { useCurrencies } from 'wallet/src/components/TokenSelector/hooks' import { Routing } from 'wallet/src/data/tradingApi/__generated__/index' -import { fromMoonpayNetwork, toSupportedChainId } from 'wallet/src/features/chains/utils' -import { - useFiatOnRampBuyQuoteQuery, - useFiatOnRampIpAddressQuery, - useFiatOnRampLimitsQuery, - useFiatOnRampSupportedTokensQuery, - useFiatOnRampWidgetUrlQuery, -} from 'wallet/src/features/fiatOnRamp/api' -import { useMoonpayFiatCurrencySupportInfo } from 'wallet/src/features/fiatOnRamp/hooks' -import { MoonpayCurrency } from 'wallet/src/features/fiatOnRamp/types' +import { FiatCurrency } from 'wallet/src/features/fiatCurrency/constants' +import { useAppFiatCurrencyInfo, useFiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' import { addTransaction } from 'wallet/src/features/transactions/slice' import { @@ -36,34 +42,18 @@ import { TransactionStatus, TransactionType, } from 'wallet/src/features/transactions/types' -import { createTransactionId } from 'wallet/src/features/transactions/utils' -import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' -import { areAddressesEqual } from 'wallet/src/utils/addresses' +import { useActiveAccountAddress } from 'wallet/src/features/wallet/hooks' import { getFormattedCurrencyAmount } from 'wallet/src/utils/currency' -import { buildCurrencyId, buildNativeCurrencyId } from 'wallet/src/utils/currencyId' import { ValueType } from 'wallet/src/utils/getCurrencyAmount' -const ETH_POLYGON_MOONPAY_CODE = 'eth_polygon' -const WETH_POLYGON_MOONPAY_CODE = 'weth_polygon' -const BNB_MAINNET_MOONPAY_CODE = 'bnb' - -export function useFormatExactCurrencyAmount( - currencyAmount: string, - currency: Maybe -): string | undefined { +export function useFormatExactCurrencyAmount(currencyAmount: string, currency: Maybe): string | undefined { const formatter = useLocalizationContext() if (!currencyAmount || !currency) { return } - const formattedAmount = getFormattedCurrencyAmount( - currency, - currencyAmount, - formatter, - true, - ValueType.Exact - ) + const formattedAmount = getFormattedCurrencyAmount(currency, currencyAmount, formatter, true, ValueType.Exact) // when formattedAmount is not empty it has an empty space in the end return formattedAmount === '' ? '0 ' : formattedAmount @@ -73,14 +63,15 @@ export function useFormatExactCurrencyAmount( export function useFiatOnRampTransactionCreator( ownerAddress: string, chainId: WalletChainId, - initialTypeInfo?: Partial + serviceProvider?: string, + initialTypeInfo?: Partial, ): { externalTransactionId: string dispatchAddTransaction: () => void } { - const dispatch = useAppDispatch() + const dispatch = useDispatch() - const externalTransactionId = useRef(createTransactionId()) + const externalTransactionId = useRef(createOnRampTransactionId(serviceProvider)) const dispatchAddTransaction = useCallback(() => { // adds a dummy transaction detail for now @@ -107,208 +98,37 @@ export function useFiatOnRampTransactionCreator( return { externalTransactionId: externalTransactionId.current, dispatchAddTransaction } } -const MOONPAY_FEES_INCLUDED = true - -/** - * Hook to provide data from Moonpay for Fiat On Ramp Input Amount screen. - */ -export function useMoonpayFiatOnRamp({ - baseCurrencyAmount, - quoteCurrencyCode, - quoteChainId, -}: { - baseCurrencyAmount: string - quoteCurrencyCode: string | undefined - quoteChainId: WalletChainId -}): { - eligible: boolean - quoteAmount: number - quoteCurrencyAmountReady: boolean - quoteCurrencyAmountLoading: boolean - isLoading: boolean - externalTransactionId: string - dispatchAddTransaction: () => void - fiatOnRampHostUrl?: string - isError: boolean - errorText?: string - errorColor?: ColorTokens +export function useMeldFiatCurrencySupportInfo(countryCode: string): { + appFiatCurrencySupportedInMeld: boolean + meldSupportedFiatCurrency: FiatCurrencyInfo + supportedFiatCurrencies: FORSupportedFiatCurrency[] | undefined } { - const colors = useSporeColors() - - const debouncedBaseCurrencyAmount = useDebounce(baseCurrencyAmount, Delay.Short) - - // we can consider adding `ownerAddress` as a prop to this modal in the future - // for now, always assume the user wants to fund the current account - const activeAccountAddress = useActiveAccountAddressWithThrow() - - const { externalTransactionId, dispatchAddTransaction } = useFiatOnRampTransactionCreator( - activeAccountAddress, - quoteChainId - ) - - const { moonpaySupportedFiatCurrency: baseCurrency } = useMoonpayFiatCurrencySupportInfo() - const baseCurrencyCode = baseCurrency.code.toLowerCase() - const baseCurrencySymbol = baseCurrency.symbol - - const { - data: limitsData, - isLoading: limitsLoading, - isError: limitsLoadingQueryError, - } = useFiatOnRampLimitsQuery( - quoteCurrencyCode - ? { - baseCurrencyCode, - quoteCurrencyCode, - areFeesIncluded: MOONPAY_FEES_INCLUDED, - } - : skipToken - ) + // Not all the currencies are supported by Meld, so we need to fallback to USD if the currency is not supported + const appFiatCurrencyInfo = useAppFiatCurrencyInfo() + const fallbackCurrencyInfo = useFiatCurrencyInfo(FiatCurrency.UnitedStatesDollar) + const appFiatCurrencyCode = appFiatCurrencyInfo.code.toLowerCase() - const { maxBuyAmount } = limitsData?.baseCurrency ?? { - maxBuyAmount: Infinity, - } - - // we're adding +1 here because MoonPay API is not precise with limits - // and an actual lower limit is a bit above the number, they provide in limits api - const minBuyAmount = limitsData?.baseCurrency?.minBuyAmount - ? limitsData.baseCurrency.minBuyAmount + 1 - : 0 - - const parsedBaseCurrencyAmount = parseFloat(baseCurrencyAmount) - const amountIsTooSmall = parsedBaseCurrencyAmount < minBuyAmount - const amountIsTooLarge = parsedBaseCurrencyAmount > maxBuyAmount - const isBaseCurrencyAmountValid = - !!parsedBaseCurrencyAmount && !amountIsTooSmall && !amountIsTooLarge - - const { - data: fiatOnRampHostUrl, - isError: isWidgetUrlQueryError, - isLoading: isWidgetUrlLoading, - } = useFiatOnRampWidgetUrlQuery( - // PERF: could consider skipping this call until eligibility in determined (ux tradeoffs) - // as-is, avoids waterfalling requests => better ux - quoteCurrencyCode - ? { - ownerAddress: activeAccountAddress, - colorCode: colors.accent1.val, - externalTransactionId, - amount: baseCurrencyAmount, - currencyCode: quoteCurrencyCode, - baseCurrencyCode, - redirectUrl: `${uniswapUrls.redirectUrlBase}/?screen=transaction&fiatOnRamp=true&userAddress=${activeAccountAddress}`, - } - : skipToken - ) - const { - data: buyQuote, - isFetching: buyQuoteLoading, - isError: buyQuoteLoadingQueryError, - } = useFiatOnRampBuyQuoteQuery( - // When isBaseCurrencyAmountValid is false and the user enters any digit, - // isBaseCurrencyAmountValid becomes true. Since there were no prior calls to the API, - // it takes the debouncedBaseCurrencyAmount and immediately calls an API. - // This only truly matters in the beginning and in cases where the debouncedBaseCurrencyAmount - // is changed while isBaseCurrencyAmountValid is false." - quoteCurrencyCode && - isBaseCurrencyAmountValid && - debouncedBaseCurrencyAmount === baseCurrencyAmount - ? { - baseCurrencyCode, - baseCurrencyAmount: debouncedBaseCurrencyAmount, - quoteCurrencyCode, - areFeesIncluded: MOONPAY_FEES_INCLUDED, - } - : skipToken - ) - - const quoteAmount = buyQuote?.quoteCurrencyAmount ?? 0 - - const { - data: ipAddressData, - isLoading: isEligibleLoading, - isError: isFiatBuyAllowedQueryError, - } = useFiatOnRampIpAddressQuery() - - const eligible = Boolean(ipAddressData?.isBuyAllowed) - - const isLoading = isEligibleLoading || isWidgetUrlLoading - const isError = - isFiatBuyAllowedQueryError || - isWidgetUrlQueryError || - buyQuoteLoadingQueryError || - limitsLoadingQueryError - - const quoteCurrencyAmountLoading = - buyQuoteLoading || limitsLoading || debouncedBaseCurrencyAmount !== baseCurrencyAmount - - const quoteCurrencyAmountReady = isBaseCurrencyAmountValid && !quoteCurrencyAmountLoading - - const { addFiatSymbolToNumber } = useLocalizationContext() - const minBuyAmountWithFiatSymbol = addFiatSymbolToNumber({ - value: minBuyAmount, - currencyCode: baseCurrencyCode, - currencySymbol: baseCurrencySymbol, - }) - const maxBuyAmountWithFiatSymbol = addFiatSymbolToNumber({ - value: maxBuyAmount, - currencyCode: baseCurrencyCode, - currencySymbol: baseCurrencySymbol, + const { data: supportedFiatCurrencies } = useFiatOnRampAggregatorSupportedFiatCurrenciesQuery({ + countryCode, }) - const { errorText, errorColor } = useMoonpayError( - isError, - amountIsTooSmall, - amountIsTooLarge, - minBuyAmountWithFiatSymbol, - maxBuyAmountWithFiatSymbol - ) + const appFiatCurrencySupported = + !supportedFiatCurrencies || + supportedFiatCurrencies.fiatCurrencies.some( + (currency): boolean => appFiatCurrencyCode === currency.fiatCurrencyCode.toLowerCase(), + ) + const meldSupportedFiatCurrency = appFiatCurrencySupported ? appFiatCurrencyInfo : fallbackCurrencyInfo return { - eligible, - quoteAmount, - quoteCurrencyAmountReady, - quoteCurrencyAmountLoading, - isLoading, - externalTransactionId, - dispatchAddTransaction, - fiatOnRampHostUrl, - isError, - errorText, - errorColor, - } -} - -function useMoonpayError( - hasError: boolean, - amountIsTooSmall: boolean, - amountIsTooLarge: boolean, - minBuyAmountWithFiatSymbol: string, - maxBuyAmountWithFiatSymbol: string -): { - errorText: string | undefined - errorColor: ColorTokens | undefined -} { - const { t } = useTranslation() - - let errorText, errorColor: ColorTokens | undefined - - if (hasError) { - errorText = t('fiatOnRamp.error.default') - errorColor = '$DEP_accentWarning' - } else if (amountIsTooSmall) { - errorText = t('fiatOnRamp.error.min', { amount: minBuyAmountWithFiatSymbol }) - errorColor = '$statusCritical' - } else if (amountIsTooLarge) { - errorText = t('fiatOnRamp.error.max', { amount: maxBuyAmountWithFiatSymbol }) - errorColor = '$statusCritical' + appFiatCurrencySupportedInMeld: appFiatCurrencySupported, + meldSupportedFiatCurrency, + supportedFiatCurrencies: supportedFiatCurrencies?.fiatCurrencies, } - - return { errorText, errorColor } } function findTokenOptionForFiatOnRampToken( currencies: CurrencyInfo[] | undefined = [], - fiatOnRampToken: FORSupportedToken + fiatOnRampToken: FORSupportedToken, ): Maybe { return currencies.find((item) => { const symbol = fiatOnRampToken.cryptoCurrencyCode.split('_')?.[0]?.toLowerCase() @@ -321,47 +141,7 @@ function findTokenOptionForFiatOnRampToken( }) } -function findTokenOptionForMoonpayCurrency( - commonBaseCurrencies: CurrencyInfo[] | undefined = [], - moonpayCurrency: MoonpayCurrency -): Maybe { - const currencyInfo = commonBaseCurrencies.find((item) => { - // Moonpay uses WETH on Polygon to represent ETH on Polygon - const moonpayCurrencyCode = - moonpayCurrency.code === ETH_POLYGON_MOONPAY_CODE - ? WETH_POLYGON_MOONPAY_CODE - : moonpayCurrency.code - const [tokenSymbol, network] = moonpayCurrencyCode.split('_') - const chainId = fromMoonpayNetwork(network) - return ( - item && - tokenSymbol && - tokenSymbol.toLowerCase() === item.currency.symbol?.toLowerCase() && - chainId === item.currency.chainId - ) - }) - if ( - !currencyInfo && - !BRIDGED_BASE_ADDRESSES.find((bridgedAddress) => - areAddressesEqual(bridgedAddress, moonpayCurrency.metadata?.contractAddress) - ) && - // We do not support BNB onboarding and Moonpay does not return an address for it so map it manually - moonpayCurrency.code !== BNB_MAINNET_MOONPAY_CODE - ) { - logger.error(`Moonpay currency ${moonpayCurrency.code} cannot be mapped`, { - tags: { file: 'fiatOnRamp/hooks', function: 'useMoonpaySupportedTokens' }, - extra: { - chainId: moonpayCurrency.metadata?.chainId, - address: moonpayCurrency.metadata?.contractAddress, - }, - }) - } - return currencyInfo -} - -function buildCurrencyIdForFORSupportedToken( - supportedToken: FORSupportedToken -): string | undefined { +function buildCurrencyIdForFORSupportedToken(supportedToken: FORSupportedToken): string | undefined { const chainId = toSupportedChainId(supportedToken.chainId) return chainId ? supportedToken.address @@ -394,7 +174,7 @@ export function useFiatOnRampSupportedTokens({ supportedTokensResponse?.supportedTokens .map(buildCurrencyIdForFORSupportedToken) .filter((st): st is string => !!st) ?? [], - [supportedTokensResponse] + [supportedTokensResponse], ) const { @@ -412,7 +192,7 @@ export function useFiatOnRampSupportedTokens({ meldCurrencyCode: fiatOnRampToken.cryptoCurrencyCode, })) .filter((item) => !!item.currencyInfo), - [currencies, supportedTokensResponse?.supportedTokens] + [currencies, supportedTokensResponse?.supportedTokens], ) const loading = supportedTokensLoading || currenciesLoading @@ -429,66 +209,98 @@ export function useFiatOnRampSupportedTokens({ return { list, loading, error, refetch } } -export function useMoonpaySupportedTokens(): { - error: boolean - list: FiatOnRampCurrency[] | undefined +/** + * Hook to load quotes + */ +export function useFiatOnRampQuotes({ + baseCurrencyAmount, + baseCurrencyCode, + quoteCurrencyCode, + countryCode, + countryState, +}: { + baseCurrencyAmount?: number + baseCurrencyCode: string | undefined + quoteCurrencyCode: string | undefined + countryCode: string | undefined + countryState: string | undefined +}): { loading: boolean - refetch: () => void + error?: FetchBaseQueryError | SerializedError + quotes: FORQuote[] | undefined } { - // this should be already cached by the time we need it - const { - data: ipAddressData, - isLoading: ipAddressLoading, - isError: ipAddressError, - refetch: refetchIpAddress, - } = useFiatOnRampIpAddressQuery() + const debouncedBaseCurrencyAmount = useDebounce(baseCurrencyAmount, Delay.Short) + const walletAddress = useActiveAccountAddress() const { - data: supportedTokens, - isLoading: supportedTokensLoading, - isError: supportedTokensError, - refetch: refetchSupportedTokens, - } = useFiatOnRampSupportedTokensQuery( + currentData: quotesResponse, + isFetching: quotesFetching, + error: quotesError, + } = useFiatOnRampAggregatorCryptoQuoteQuery( + baseCurrencyAmount && countryCode && quoteCurrencyCode && baseCurrencyCode + ? { + sourceAmount: baseCurrencyAmount, + sourceCurrencyCode: baseCurrencyCode, + destinationCurrencyCode: quoteCurrencyCode, + countryCode, + walletAddress: walletAddress ?? undefined, + state: countryState, + } + : skipToken, { - isUserInUS: ipAddressData?.alpha3 === 'USA' ?? false, - stateInUS: ipAddressData?.state, + refetchOnMountOrArgChange: true, }, - { skip: !ipAddressData } ) - const { - data: commonBaseCurrencies, - error: commonBaseCurrenciesError, - loading: commonBaseCurrenciesLoading, - refetch: refetchCommonBaseCurrencies, - } = useAllCommonBaseCurrencies() + const loading = quotesFetching || debouncedBaseCurrencyAmount !== baseCurrencyAmount - const list = useMemo(() => { - if (!commonBaseCurrencies || !supportedTokens) { - return undefined - } + // if user is entering base amount -> ignore previous errors + const error = debouncedBaseCurrencyAmount !== baseCurrencyAmount ? undefined : quotesError - return supportedTokens - .map((fiatOnRampToken) => ({ - currencyInfo: findTokenOptionForMoonpayCurrency(commonBaseCurrencies, fiatOnRampToken), - moonpayCurrencyCode: fiatOnRampToken.code, - })) - .filter((item) => !!item.currencyInfo) - }, [commonBaseCurrencies, supportedTokens]) + return { + loading, + error, + quotes: quotesResponse?.quotes ?? undefined, + } +} - const loading = ipAddressLoading || supportedTokensLoading || commonBaseCurrenciesLoading - const error = Boolean(ipAddressError || supportedTokensError || commonBaseCurrenciesError) - const refetch = async (): Promise => { - if (ipAddressError) { - await refetchIpAddress() - } - if (supportedTokensError) { - await refetchSupportedTokens() - } - if (commonBaseCurrenciesError) { - refetchCommonBaseCurrencies?.() +export function useParseFiatOnRampError( + error: unknown, + currencyCode: string, +): { + errorText: string | undefined + errorColor: ColorTokens | undefined +} { + const { t } = useTranslation() + const { formatNumberOrString } = useLocalizationContext() + + let errorText, errorColor: ColorTokens | undefined + if (!error) { + return { errorText, errorColor } + } + + errorText = t('fiatOnRamp.error.default') + errorColor = '$DEP_accentWarning' + + if (isFiatOnRampApiError(error)) { + if (isInvalidRequestAmountTooLow(error)) { + const formattedAmount = formatNumberOrString({ + value: error.data.context.minimumAllowed, + type: NumberType.FiatStandard, + currencyCode, + }) + errorText = t('fiatOnRamp.error.min', { amount: formattedAmount }) + errorColor = '$statusCritical' + } else if (isInvalidRequestAmountTooHigh(error)) { + const formattedAmount = formatNumberOrString({ + value: error.data.context.maximumAllowed, + type: NumberType.FiatStandard, + currencyCode, + }) + errorText = t('fiatOnRamp.error.max', { amount: formattedAmount }) + errorColor = '$statusCritical' } } - return { list, loading, error, refetch } + return { errorText, errorColor } } diff --git a/apps/mobile/src/features/firebase/firebaseDataSaga.ts b/apps/mobile/src/features/firebase/firebaseDataSaga.ts index b7116f1c91f..df4fd5e5d78 100644 --- a/apps/mobile/src/features/firebase/firebaseDataSaga.ts +++ b/apps/mobile/src/features/firebase/firebaseDataSaga.ts @@ -2,11 +2,7 @@ import firebase from '@react-native-firebase/app' import auth from '@react-native-firebase/auth' import firestore from '@react-native-firebase/firestore' import { appSelect } from 'src/app/hooks' -import { - getFirebaseUidOrError, - getFirestoreMetadataRef, - getFirestoreUidRef, -} from 'src/features/firebase/utils' +import { getFirebaseUidOrError, getFirestoreMetadataRef, getFirestoreUidRef } from 'src/features/firebase/utils' import { getOneSignalUserIdOrError } from 'src/features/notifications/Onesignal' import { all, call, put, select, takeEvery, takeLatest } from 'typed-redux-saga' import { logger } from 'utilities/src/logger/logger' @@ -20,10 +16,7 @@ import { editAccountActions, } from 'wallet/src/features/wallet/accounts/editAccountSaga' import { Account, AccountType } from 'wallet/src/features/wallet/accounts/types' -import { - makeSelectAccountNotificationSetting, - selectAccounts, -} from 'wallet/src/features/wallet/selectors' +import { makeSelectAccountNotificationSetting, selectAccounts } from 'wallet/src/features/wallet/selectors' import { addAccounts, editAccount } from 'wallet/src/features/wallet/slice' interface AccountMetadata { @@ -103,7 +96,7 @@ function* updateFirebaseLanguage(addresses: Address[], language: Language) { type: EditAccountAction.UpdateLanguage, address, locale, - }) + }), ) } } @@ -117,8 +110,8 @@ function* editAccountDataInFirebase(actionData: ReturnType - call(removeAccountFromFirebase, account.address, account.pushNotificationsEnabled) - ) + call(removeAccountFromFirebase, account.address, account.pushNotificationsEnabled), + ), ) return } @@ -186,10 +179,7 @@ export function* renameAccountInFirebase(address: Address, newName: string) { } } -export function* toggleFirebaseNotificationSettings({ - address, - enabled, -}: TogglePushNotificationParams) { +export function* toggleFirebaseNotificationSettings({ address, enabled }: TogglePushNotificationParams) { if (!address) { throw new Error('Address is required for toggleFirebaseNotificationSettings') } @@ -214,7 +204,7 @@ export function* toggleFirebaseNotificationSettings({ ...account, pushNotificationsEnabled: enabled, }, - }) + }), ) } catch (error) { logger.error(error, { @@ -267,16 +257,13 @@ async function updateFirebaseMetadata(address: Address, metadata: AccountMetadat const metadataRef = getFirestoreMetadataRef(firebaseApp, address, pushId) // Firestore does not support updating properties with an `undefined` value so must strip them out - const metadataWithDefinedPropsOnly = getKeys(metadata).reduce( - (obj: Record, prop) => { - const value = metadata[prop] - if (value !== undefined) { - obj[prop] = value - } - return obj - }, - {} - ) + const metadataWithDefinedPropsOnly = getKeys(metadata).reduce((obj: Record, prop) => { + const value = metadata[prop] + if (value !== undefined) { + obj[prop] = value + } + return obj + }, {}) await metadataRef.set(metadataWithDefinedPropsOnly, { merge: true }) } catch (error) { diff --git a/apps/mobile/src/features/firebase/utils.ts b/apps/mobile/src/features/firebase/utils.ts index 862644745ee..e09b708269b 100644 --- a/apps/mobile/src/features/firebase/utils.ts +++ b/apps/mobile/src/features/firebase/utils.ts @@ -1,7 +1,7 @@ import type { ReactNativeFirebase } from '@react-native-firebase/app' import '@react-native-firebase/auth' import firestore, { FirebaseFirestoreTypes } from '@react-native-firebase/firestore' -import { isBetaEnv, isDevEnv } from 'uniswap/src/utils/env' +import { isBetaEnv, isDevEnv } from 'utilities/src/environment' const ADDRESS_DATA_COLLECTION = 'address_data' const DEV_ADDRESS_DATA_COLLECTION = 'dev_address_data' @@ -17,7 +17,7 @@ export const getFirebaseUidOrError = (firebaseApp: ReactNativeFirebase.FirebaseA export const getFirestoreUidRef = ( firebaseApp: ReactNativeFirebase.FirebaseApp, - address: Address + address: Address, ): FirebaseFirestoreTypes.DocumentReference => firestore(firebaseApp) .collection(getAddressDataCollectionFromBundleId()) @@ -28,7 +28,7 @@ export const getFirestoreUidRef = ( export const getFirestoreMetadataRef = ( firebaseApp: ReactNativeFirebase.FirebaseApp, address: Address, - pushId: string + pushId: string, ): FirebaseFirestoreTypes.DocumentReference => firestore(firebaseApp) .collection(getAddressDataCollectionFromBundleId()) diff --git a/apps/mobile/src/features/import/GenericImportForm.test.tsx b/apps/mobile/src/features/import/GenericImportForm.test.tsx index e9c0446b32d..0d64d98f766 100644 --- a/apps/mobile/src/features/import/GenericImportForm.test.tsx +++ b/apps/mobile/src/features/import/GenericImportForm.test.tsx @@ -14,7 +14,7 @@ describe(GenericImportForm, () => { value={undefined} onChange={noOpFunction} /> - + , ) expect(await screen.findByText('seed phrase')).toBeDefined() @@ -30,7 +30,7 @@ describe(GenericImportForm, () => { value="hello" onChange={noOpFunction} /> - + , ) expect(await screen.queryByText('seed phrase')).toBeNull() @@ -46,7 +46,7 @@ describe(GenericImportForm, () => { value="wrong value" onChange={noOpFunction} /> - + , ) expect(await screen.findByText('there is an error')).toBeDefined() diff --git a/apps/mobile/src/features/import/GenericImportForm.tsx b/apps/mobile/src/features/import/GenericImportForm.tsx index 16e1cdcbf5f..ecad2210b2e 100644 --- a/apps/mobile/src/features/import/GenericImportForm.tsx +++ b/apps/mobile/src/features/import/GenericImportForm.tsx @@ -3,9 +3,9 @@ import { Keyboard, TextInput as NativeTextInput } from 'react-native' import InputWithSuffix from 'src/features/import/InputWithSuffix' import { Flex, Text, useMedia } from 'ui/src' import { fonts } from 'ui/src/theme' +import PasteButton from 'uniswap/src/components/buttons/PasteButton' import Trace from 'uniswap/src/features/telemetry/Trace' import { SectionName } from 'uniswap/src/features/telemetry/constants' -import PasteButton from 'wallet/src/components/buttons/PasteButton' interface Props { value: string | undefined @@ -101,7 +101,8 @@ export function GenericImportForm({ // when this component is pressed while the keyboard is visible) return focused }} - onTouchEnd={handleFocus}> + onTouchEnd={handleFocus} + > + width="100%" + > {/* TODO: [MOB-225] make Box press re-focus TextInput. Fine for now since TexInput has autoFocus */} + top={0} + > {placeholderLabel} diff --git a/apps/mobile/src/features/import/InputWithSuffix.android.tsx b/apps/mobile/src/features/import/InputWithSuffix.android.tsx index b55fba3ab3a..235c46a5565 100644 --- a/apps/mobile/src/features/import/InputWithSuffix.android.tsx +++ b/apps/mobile/src/features/import/InputWithSuffix.android.tsx @@ -3,7 +3,7 @@ import { LayoutChangeEvent } from 'react-native' import { InputWithSuffixProps } from 'src/features/import/InputWIthSuffixProps' import { Flex } from 'ui/src' import { TextInput } from 'uniswap/src/components/input/TextInput' -import { ElementName } from 'uniswap/src/features/telemetry/constants' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { isIOS } from 'utilities/src/platform' const EPS = 1 @@ -38,7 +38,7 @@ export default function InputWithSuffix({ setShouldWrapLine(contentWidth + EPS >= maxContentWidth) } }, - [multiline] + [multiline], ) const isInputEmpty = !value?.length @@ -96,7 +96,7 @@ export default function InputWithSuffix({ returnKeyType="done" scrollEnabled={false} spellCheck={false} - testID={ElementName.ImportAccountInput} + testID={TestID.ImportAccountInput} textAlign={isInputEmpty ? 'left' : textInputAlignment} textAlignVertical={isInputEmpty ? 'center' : 'bottom'} value={value} diff --git a/apps/mobile/src/features/import/InputWithSuffix.ios.tsx b/apps/mobile/src/features/import/InputWithSuffix.ios.tsx index c27abf6915b..88cec2ba5da 100644 --- a/apps/mobile/src/features/import/InputWithSuffix.ios.tsx +++ b/apps/mobile/src/features/import/InputWithSuffix.ios.tsx @@ -1,7 +1,7 @@ import { InputWithSuffixProps } from 'src/features/import/InputWIthSuffixProps' import { Flex } from 'ui/src' import { TextInput } from 'uniswap/src/components/input/TextInput' -import { ElementName } from 'uniswap/src/features/telemetry/constants' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' export default function InputWithSuffix({ alwaysShowInputSuffix = false, @@ -39,7 +39,7 @@ export default function InputWithSuffix({ returnKeyType="done" scrollEnabled={false} spellCheck={false} - testID={ElementName.ImportAccountInput} + testID={TestID.ImportAccountInput} textAlign={isInputEmpty ? 'left' : textInputAlignment} textAlignVertical="bottom" value={value} diff --git a/apps/mobile/src/features/modals/ModalsState.ts b/apps/mobile/src/features/modals/ModalsState.ts index b9a3daddd66..f058dd00980 100644 --- a/apps/mobile/src/features/modals/ModalsState.ts +++ b/apps/mobile/src/features/modals/ModalsState.ts @@ -4,9 +4,9 @@ import { ScantasticModalState } from 'src/features/scantastic/ScantasticModalSta import { ReceiveCryptoModalState } from 'src/screens/ReceiveCryptoModalState' import { FORServiceProvider } from 'uniswap/src/features/fiatOnRamp/types' import { ModalName } from 'uniswap/src/features/telemetry/constants' +import { TransactionState } from 'uniswap/src/features/transactions/transactionState/types' import { MobileScreens } from 'uniswap/src/types/screens/mobile' import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants' -import { TransactionState } from 'wallet/src/features/transactions/transactionState/types' export interface AppModalState { isOpen: boolean @@ -21,10 +21,10 @@ export interface ModalsState { [ModalName.Experiments]: AppModalState [ModalName.Explore]: AppModalState [ModalName.FiatCurrencySelector]: AppModalState - [ModalName.FiatOnRamp]: AppModalState [ModalName.FiatOnRampAggregator]: AppModalState [ModalName.ReceiveCryptoModal]: AppModalState [ModalName.LanguageSelector]: AppModalState + [ModalName.QueuedOrderModal]: AppModalState [ModalName.RemoveWallet]: AppModalState [ModalName.RestoreWallet]: AppModalState [ModalName.Scantastic]: AppModalState diff --git a/apps/mobile/src/features/modals/hooks.ts b/apps/mobile/src/features/modals/hooks.ts deleted file mode 100644 index 25a876353d0..00000000000 --- a/apps/mobile/src/features/modals/hooks.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useEffect } from 'react' -import { BackHandler } from 'react-native' -import { useAppSelector } from 'src/app/hooks' -import { ModalsState } from 'src/features/modals/ModalsState' -import { closeModal } from 'src/features/modals/modalSlice' -import { selectModalState } from 'src/features/modals/selectModalState' -import { useAppDispatch } from 'wallet/src/state' - -/* This hook is used to close the globally available modals (in Redux store) when the -back button is pressed. */ -export function useReduxModalBackHandler(modalName: keyof ModalsState): void { - const appDispatch = useAppDispatch() - const isBottomSheetOpen = useAppSelector(selectModalState(modalName)).isOpen - - useEffect(() => { - if (!isBottomSheetOpen) { - return - } - - const subscription = BackHandler.addEventListener('hardwareBackPress', () => { - appDispatch(closeModal({ name: modalName })) - return true - }) - - return subscription.remove - }, [isBottomSheetOpen, appDispatch, modalName]) -} diff --git a/apps/mobile/src/features/modals/modalSlice.test.ts b/apps/mobile/src/features/modals/modalSlice.test.ts index ae3f49dd86c..5bd984a145a 100644 --- a/apps/mobile/src/features/modals/modalSlice.test.ts +++ b/apps/mobile/src/features/modals/modalSlice.test.ts @@ -1,10 +1,5 @@ import { createStore, Store } from '@reduxjs/toolkit' -import { - closeModal, - initialModalsState, - modalsReducer, - openModal, -} from 'src/features/modals/modalSlice' +import { closeModal, initialModalsState, modalsReducer, openModal } from 'src/features/modals/modalSlice' import { ModalsState } from 'src/features/modals/ModalsState' import { ModalName } from 'uniswap/src/features/telemetry/constants' import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants' diff --git a/apps/mobile/src/features/modals/modalSlice.ts b/apps/mobile/src/features/modals/modalSlice.ts index cf37dd31fc4..239333724e4 100644 --- a/apps/mobile/src/features/modals/modalSlice.ts +++ b/apps/mobile/src/features/modals/modalSlice.ts @@ -6,10 +6,10 @@ import { ModalsState } from 'src/features/modals/ModalsState' import { ScantasticModalState } from 'src/features/scantastic/ScantasticModalState' import { ReceiveCryptoModalState } from 'src/screens/ReceiveCryptoModalState' import { ModalName } from 'uniswap/src/features/telemetry/constants' +import { TransactionState } from 'uniswap/src/features/transactions/transactionState/types' import { MobileScreens } from 'uniswap/src/types/screens/mobile' import { getKeys } from 'utilities/src/primitives/objects' import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants' -import { TransactionState } from 'wallet/src/features/transactions/transactionState/types' type AccountSwitcherModalParams = { name: typeof ModalName.AccountSwitcher @@ -33,8 +33,6 @@ type FiatCurrencySelectorParams = { initialState?: undefined } -type FiatOnRampModalParams = { name: typeof ModalName.FiatOnRamp; initialState?: undefined } - type FiatOnRampAggregatorModalParams = { name: typeof ModalName.FiatOnRampAggregator initialState?: undefined @@ -87,7 +85,6 @@ export type OpenModalParams = | ExperimentsModalParams | ExploreModalParams | FiatCurrencySelectorParams - | FiatOnRampModalParams | FiatOnRampAggregatorModalParams | ReceiveCryptoModalParams | LanguageSelectorModalParams @@ -107,10 +104,6 @@ export const initialModalsState: ModalsState = { isOpen: false, initialState: undefined, }, - [ModalName.FiatOnRamp]: { - isOpen: false, - initialState: undefined, - }, [ModalName.FiatOnRampAggregator]: { isOpen: false, initialState: undefined, @@ -171,6 +164,10 @@ export const initialModalsState: ModalsState = { isOpen: false, initialState: undefined, }, + [ModalName.QueuedOrderModal]: { + isOpen: false, + initialState: undefined, + }, } const slice = createSlice({ diff --git a/apps/mobile/src/features/modals/saga.ts b/apps/mobile/src/features/modals/saga.ts index c19d2f377f9..389f46beeec 100644 --- a/apps/mobile/src/features/modals/saga.ts +++ b/apps/mobile/src/features/modals/saga.ts @@ -1,11 +1,6 @@ import { PayloadAction } from '@reduxjs/toolkit' import { setTag } from '@sentry/react-native' -import { - closeModal, - CloseModalParams, - openModal, - OpenModalParams, -} from 'src/features/modals/modalSlice' +import { closeModal, CloseModalParams, openModal, OpenModalParams } from 'src/features/modals/modalSlice' import { takeEvery } from 'typed-redux-saga' import { ModalName } from 'uniswap/src/features/telemetry/constants' diff --git a/apps/mobile/src/features/modals/selectModalState.ts b/apps/mobile/src/features/modals/selectModalState.ts index 34c7adbe5a1..ace1ef6562e 100644 --- a/apps/mobile/src/features/modals/selectModalState.ts +++ b/apps/mobile/src/features/modals/selectModalState.ts @@ -1,8 +1,6 @@ import { MobileState } from 'src/app/reducer' import { ModalsState } from 'src/features/modals/ModalsState' -export function selectModalState( - name: T -): (state: MobileState) => ModalsState[T] { +export function selectModalState(name: T): (state: MobileState) => ModalsState[T] { return (state) => state.modals[name] } diff --git a/apps/mobile/src/features/nfts/collection/NFTCollectionContextMenu.tsx b/apps/mobile/src/features/nfts/collection/NFTCollectionContextMenu.tsx index 202ed0715ed..8a83ff92dae 100644 --- a/apps/mobile/src/features/nfts/collection/NFTCollectionContextMenu.tsx +++ b/apps/mobile/src/features/nfts/collection/NFTCollectionContextMenu.tsx @@ -10,8 +10,9 @@ import { iconSizes, spacing } from 'ui/src/theme' import { WalletEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { ShareableEntity } from 'uniswap/src/types/sharing' +import { openUri } from 'uniswap/src/utils/linking' import { logger } from 'utilities/src/logger/logger' -import { getNftCollectionUrl, getTwitterLink, openUri } from 'wallet/src/utils/linking' +import { getNftCollectionUrl, getTwitterLink } from 'wallet/src/utils/linking' type MenuOption = { title: string @@ -102,14 +103,16 @@ export function NFTCollectionContextMenu({ dropdownMenuMode={true} onPress={async (e: NativeSyntheticEvent): Promise => { await menuActions[e.nativeEvent.index]?.action() - }}> + }} + > + onPress={disableOnPress} + > diff --git a/apps/mobile/src/features/nfts/collection/NFTCollectionHeader.tsx b/apps/mobile/src/features/nfts/collection/NFTCollectionHeader.tsx index c06ab664b7d..8ce75667b39 100644 --- a/apps/mobile/src/features/nfts/collection/NFTCollectionHeader.tsx +++ b/apps/mobile/src/features/nfts/collection/NFTCollectionHeader.tsx @@ -77,12 +77,7 @@ export function NFTCollectionHeader({ /> ) : ( // No uri found on collection - + )} {/* Banner buttons */} @@ -91,7 +86,8 @@ export function NFTCollectionHeader({ alignItems="center" justifyContent="space-between" mt={deviceTopPadding + spacing.spacing12} - mx="$spacing24"> + mx="$spacing24" + > @@ -111,13 +107,15 @@ export function NFTCollectionHeader({ borderRadius="$roundedFull" height={PROFILE_IMAGE_WRAPPER_SIZE} justifyContent="center" - width={PROFILE_IMAGE_WRAPPER_SIZE}> + width={PROFILE_IMAGE_WRAPPER_SIZE} + > {data?.image?.url ? ( + width={PROFILE_IMAGE_SIZE} + > ) : ( @@ -132,21 +130,13 @@ export function NFTCollectionHeader({ {/* Collection stats */} - + {data?.name ?? '-'} {data?.isVerified ? ( - + ) : null} diff --git a/apps/mobile/src/features/nfts/item/BlurredImageBackground.tsx b/apps/mobile/src/features/nfts/item/BlurredImageBackground.tsx index db416c8bcb8..4bdbb1bcd58 100644 --- a/apps/mobile/src/features/nfts/item/BlurredImageBackground.tsx +++ b/apps/mobile/src/features/nfts/item/BlurredImageBackground.tsx @@ -24,7 +24,8 @@ export const BlurredImageBackground = ({ entering={FadeIn} justifyContent="center" overflow="hidden" - style={StyleSheet.absoluteFill}> + style={StyleSheet.absoluteFill} + > ) : null} diff --git a/apps/mobile/src/features/nfts/item/CollectionPreviewCard.test.tsx b/apps/mobile/src/features/nfts/item/CollectionPreviewCard.test.tsx index 1187d74ded2..75548548d1d 100644 --- a/apps/mobile/src/features/nfts/item/CollectionPreviewCard.test.tsx +++ b/apps/mobile/src/features/nfts/item/CollectionPreviewCard.test.tsx @@ -4,8 +4,6 @@ import { render } from 'src/test/test-utils' import { NFT_COLLECTION } from 'wallet/src/test/fixtures' it('renders collection preview card', () => { - const tree = render( - null} /> - ) + const tree = render( null} />) expect(tree).toMatchSnapshot() }) diff --git a/apps/mobile/src/features/nfts/item/CollectionPreviewCard.tsx b/apps/mobile/src/features/nfts/item/CollectionPreviewCard.tsx index f8f9bbffff8..13cadd4379c 100644 --- a/apps/mobile/src/features/nfts/item/CollectionPreviewCard.tsx +++ b/apps/mobile/src/features/nfts/item/CollectionPreviewCard.tsx @@ -6,10 +6,7 @@ import { Flex, Text, TouchableArea, useSporeColors } from 'ui/src' import VerifiedIcon from 'ui/src/assets/icons/verified.svg' import { RotatableChevron } from 'ui/src/components/icons' import { iconSizes, imageSizes, spacing } from 'ui/src/theme' -import { - Currency, - NftItemScreenQuery, -} from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { Currency, NftItemScreenQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { NFTViewer } from 'wallet/src/features/images/NFTViewer' import { NFTItem } from 'wallet/src/features/nfts/types' @@ -38,8 +35,7 @@ export function CollectionPreviewCard({ return } - const isViewableCollection = - !shouldDisableLink && Boolean(collection || fallbackData?.contractAddress) + const isViewableCollection = !shouldDisableLink && Boolean(collection || fallbackData?.contractAddress) return ( @@ -51,14 +47,11 @@ export function CollectionPreviewCard({ gap="$spacing8" justifyContent="space-between" px="$spacing12" - py="$spacing12"> + py="$spacing12" + > {collection?.image?.url ? ( - + ) : null} @@ -72,11 +65,7 @@ export function CollectionPreviewCard({ {collection?.isVerified && ( - + )} {collection?.markets?.[0]?.floorPrice && ( @@ -98,12 +87,7 @@ export function CollectionPreviewCard({ {isViewableCollection ? ( - + ) : null} diff --git a/apps/mobile/src/features/nfts/item/__snapshots__/CollectionPreviewCard.test.tsx.snap b/apps/mobile/src/features/nfts/item/__snapshots__/CollectionPreviewCard.test.tsx.snap index 3eea2fb5a3a..bd45da0f481 100644 --- a/apps/mobile/src/features/nfts/item/__snapshots__/CollectionPreviewCard.test.tsx.snap +++ b/apps/mobile/src/features/nfts/item/__snapshots__/CollectionPreviewCard.test.tsx.snap @@ -76,10 +76,14 @@ exports[`renders collection preview card 1`] = ` "alignItems": "center", "aspectRatio": 1, "backgroundColor": "#F9F9F9", + "flex": 1, "flexDirection": "column", "justifyContent": "center", "maxHeight": 60, - "width": "100%", + "paddingBottom": 8, + "paddingLeft": 8, + "paddingRight": 8, + "paddingTop": 8, } } > @@ -89,10 +93,10 @@ exports[`renders collection preview card 1`] = ` style={ { "color": "#7D7D7D", - "flex": 0, "fontFamily": "Basel-Book", "fontSize": 17, "lineHeight": 24, + "textAlign": "center", } } suppressHighlighting={true} diff --git a/apps/mobile/src/features/nfts/item/traits.tsx b/apps/mobile/src/features/nfts/item/traits.tsx index 033aeda00ca..c25b0397f72 100644 --- a/apps/mobile/src/features/nfts/item/traits.tsx +++ b/apps/mobile/src/features/nfts/item/traits.tsx @@ -28,16 +28,8 @@ export function NFTTraitCard({ const colors = useSporeColors() return ( - - + + {trait.name} diff --git a/apps/mobile/src/features/notifications/NotificationToastWrapper.tsx b/apps/mobile/src/features/notifications/NotificationToastWrapper.tsx index c6574ece647..a2c932933b6 100644 --- a/apps/mobile/src/features/notifications/NotificationToastWrapper.tsx +++ b/apps/mobile/src/features/notifications/NotificationToastWrapper.tsx @@ -17,11 +17,7 @@ export function NotificationToastWrapper(): JSX.Element | null { return } -function NotificationToastRouter({ - notification, -}: { - notification: AppNotification -}): JSX.Element | null { +function NotificationToastRouter({ notification }: { notification: AppNotification }): JSX.Element | null { // Insert Mobile-only notifications here. // Shared wallet notifications should go in SharedNotificationToastRouter. switch (notification.type) { diff --git a/apps/mobile/src/features/notifications/Onesignal.ts b/apps/mobile/src/features/notifications/Onesignal.ts index 4b5587ecf2c..f6efa08feac 100644 --- a/apps/mobile/src/features/notifications/Onesignal.ts +++ b/apps/mobile/src/features/notifications/Onesignal.ts @@ -15,18 +15,14 @@ export const initOneSignal = (): void => { }) OneSignal.setNotificationOpenedHandler((event: OpenedEvent) => { - logger.debug( - 'Onesignal', - 'setNotificationOpenedHandler', - `Notification opened: ${event.notification}` - ) + logger.debug('Onesignal', 'setNotificationOpenedHandler', `Notification opened: ${event.notification}`) setTimeout( () => apolloClientRef.current?.refetchQueries({ include: GQL_QUERIES_TO_REFETCH_ON_TXN_UPDATE, }), - ONE_SECOND_MS // Delay by 1s to give a buffer for data sources to synchronize + ONE_SECOND_MS, // Delay by 1s to give a buffer for data sources to synchronize ) // This emits a url event when coldStart = false. Don't call openURI because that will @@ -38,16 +34,9 @@ export const initOneSignal = (): void => { }) } -export const promptPushPermission = ( - successCallback?: () => void, - failureCallback?: () => void -): void => { +export const promptPushPermission = (successCallback?: () => void, failureCallback?: () => void): void => { OneSignal.promptForPushNotificationsWithUserResponse((response) => { - logger.debug( - 'Onesignal', - 'promptForPushNotificationsWithUserResponse', - `Prompt response: ${response}` - ) + logger.debug('Onesignal', 'promptForPushNotificationsWithUserResponse', `Prompt response: ${response}`) if (response) { successCallback?.() } else { diff --git a/apps/mobile/src/features/notifications/ScantasticCompleteNotification.tsx b/apps/mobile/src/features/notifications/ScantasticCompleteNotification.tsx index 54c66c9a203..21e3f80e242 100644 --- a/apps/mobile/src/features/notifications/ScantasticCompleteNotification.tsx +++ b/apps/mobile/src/features/notifications/ScantasticCompleteNotification.tsx @@ -27,7 +27,8 @@ export function ScantasticCompleteNotification({ bottom={0} p="$spacing4" position="absolute" - right={0}> + right={0} + > diff --git a/apps/mobile/src/features/notifications/WCNotification.tsx b/apps/mobile/src/features/notifications/WCNotification.tsx index 0c1dcfd179e..47087843a09 100644 --- a/apps/mobile/src/features/notifications/WCNotification.tsx +++ b/apps/mobile/src/features/notifications/WCNotification.tsx @@ -1,24 +1,20 @@ import React from 'react' -import { useAppDispatch } from 'src/app/hooks' +import { useDispatch } from 'react-redux' import { openModal } from 'src/features/modals/modalSlice' import { iconSizes } from 'ui/src/theme' +import { toSupportedChainId } from 'uniswap/src/features/chains/utils' import { ModalName } from 'uniswap/src/features/telemetry/constants' import { WalletConnectEvent } from 'uniswap/src/types/walletConnect' import { DappLogoWithTxStatus } from 'wallet/src/components/CurrencyLogo/LogoWithTxStatus' import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants' -import { toSupportedChainId } from 'wallet/src/features/chains/utils' import { NotificationToast } from 'wallet/src/features/notifications/components/NotificationToast' import { NOTIFICATION_ICON_SIZE } from 'wallet/src/features/notifications/constants' import { WalletConnectNotification } from 'wallet/src/features/notifications/types' import { formWCNotificationTitle } from 'wallet/src/features/notifications/utils' -export function WCNotification({ - notification, -}: { - notification: WalletConnectNotification -}): JSX.Element { +export function WCNotification({ notification }: { notification: WalletConnectNotification }): JSX.Element { const { imageUrl, chainId, address, event, hideDelay, dappName } = notification - const dispatch = useAppDispatch() + const dispatch = useDispatch() const validChainId = toSupportedChainId(chainId) const title = formWCNotificationTitle(notification) @@ -44,7 +40,7 @@ export function WCNotification({ openModal({ name: ModalName.WalletConnectScan, initialState: ScannerModalState.ConnectedDapps, - }) + }), ) } diff --git a/apps/mobile/src/features/notifications/hooks/useNavigateToProfileTab.ts b/apps/mobile/src/features/notifications/hooks/useNavigateToProfileTab.ts index fcf1b3e66b3..26fad8fd343 100644 --- a/apps/mobile/src/features/notifications/hooks/useNavigateToProfileTab.ts +++ b/apps/mobile/src/features/notifications/hooks/useNavigateToProfileTab.ts @@ -4,7 +4,7 @@ import { closeAllModals } from 'src/features/modals/modalSlice' // Helpers to preload profile data, and dismiss modals and navigate export const useNavigateToProfileTab = ( - address: string | undefined + address: string | undefined, ): { onPressIn: () => Promise onPress: () => void diff --git a/apps/mobile/src/features/notifications/hooks/useNotificationOSPermissionsEnabled.ts b/apps/mobile/src/features/notifications/hooks/useNotificationOSPermissionsEnabled.ts index 874761cd2af..8244c72c03f 100644 --- a/apps/mobile/src/features/notifications/hooks/useNotificationOSPermissionsEnabled.ts +++ b/apps/mobile/src/features/notifications/hooks/useNotificationOSPermissionsEnabled.ts @@ -10,13 +10,13 @@ export enum NotificationPermission { } export function useNotificationOSPermissionsEnabled(): NotificationPermission { - const [notificationPermissionsEnabled, setNotificationPermissionsEnabled] = - useState(NotificationPermission.Loading) + const [notificationPermissionsEnabled, setNotificationPermissionsEnabled] = useState( + NotificationPermission.Loading, + ) const checkNotificationPermissions = async (): Promise => { const { status } = await checkNotifications() - const permission = - status === 'granted' ? NotificationPermission.Enabled : NotificationPermission.Disabled + const permission = status === 'granted' ? NotificationPermission.Enabled : NotificationPermission.Disabled setNotificationPermissionsEnabled(permission) } diff --git a/apps/mobile/src/features/onboarding/OnboardingScreen.tsx b/apps/mobile/src/features/onboarding/OnboardingScreen.tsx index 8f7660de833..0b9b8714bf2 100644 --- a/apps/mobile/src/features/onboarding/OnboardingScreen.tsx +++ b/apps/mobile/src/features/onboarding/OnboardingScreen.tsx @@ -1,7 +1,10 @@ +import { useFocusEffect } from '@react-navigation/core' import { useHeaderHeight } from '@react-navigation/elements' -import React, { PropsWithChildren } from 'react' -import { KeyboardAvoidingView, StyleSheet } from 'react-native' +import React, { PropsWithChildren, useCallback } from 'react' +import { BackHandler, KeyboardAvoidingView, StyleSheet } from 'react-native' import { FadeIn, FadeOut } from 'react-native-reanimated' +import { renderHeaderBackButton } from 'src/app/navigation/components' +import { useOnboardingStackNavigation } from 'src/app/navigation/types' import { SHORT_SCREEN_HEADER_HEIGHT_RATIO, Screen } from 'src/components/layout/Screen' import { Flex, SpaceTokens, Text, useDeviceInsets, useMedia } from 'ui/src' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' @@ -14,6 +17,7 @@ type OnboardingScreenProps = { paddingTop?: SpaceTokens childrenGap?: SpaceTokens keyboardAvoidingViewEnabled?: boolean + disableGoBack?: boolean } export function OnboardingScreen({ @@ -22,29 +26,41 @@ export function OnboardingScreen({ children, paddingTop = '$none', keyboardAvoidingViewEnabled = true, + disableGoBack = false, }: PropsWithChildren): JSX.Element { + const navigation = useOnboardingStackNavigation() const headerHeight = useHeaderHeight() const insets = useDeviceInsets() const media = useMedia() const gapSize = media.short ? '$none' : '$spacing16' + useFocusEffect( + useCallback(() => { + navigation.setOptions({ + headerLeft: disableGoBack ? (): null => null : renderHeaderBackButton, + gestureEnabled: !disableGoBack, + }) + const subscription = BackHandler.addEventListener('hardwareBackPress', () => { + return disableGoBack + }) + + return subscription.remove + }, [navigation, disableGoBack]), + ) + return ( + pt={headerHeight} + > - + style={[WrapperStyle.base, { marginBottom: insets.bottom }]} + > + {/* Text content */} {title && ( @@ -53,7 +69,8 @@ export function OnboardingScreen({ allowFontScaling={false} pt={paddingTop} textAlign="center" - variant="heading3"> + variant="heading3" + > {title} )} @@ -63,7 +80,8 @@ export function OnboardingScreen({ color="$neutral2" maxFontSizeMultiplier={media.short ? 1.1 : fonts.body2.maxFontSizeMultiplier} textAlign="center" - variant="body2"> + variant="body2" + > {subtitle} ) : null} diff --git a/apps/mobile/src/features/onboarding/OptionCard.tsx b/apps/mobile/src/features/onboarding/OptionCard.tsx index feab3fe66ce..15134505bca 100644 --- a/apps/mobile/src/features/onboarding/OptionCard.tsx +++ b/apps/mobile/src/features/onboarding/OptionCard.tsx @@ -3,6 +3,7 @@ import { Flex, Text, TouchableArea, useIsDarkMode } from 'ui/src' import { iconSizes } from 'ui/src/theme' import Trace from 'uniswap/src/features/telemetry/Trace' import { ElementNameType } from 'uniswap/src/features/telemetry/constants' +import { TestIDType } from 'uniswap/src/test/fixtures/testIDs' export function OptionCard({ title, @@ -14,12 +15,14 @@ export function OptionCard({ opacity, badgeText, hapticFeedback, + testID, }: { title: string blurb: string icon: React.ReactNode onPress: () => void elementName: ElementNameType + testID: TestIDType disabled?: boolean opacity?: number badgeText?: string | undefined @@ -38,8 +41,9 @@ export function OptionCard({ hapticFeedback={hapticFeedback} opacity={disabled ? 0.5 : opacity} p="$spacing16" - testID={elementName} - onPress={onPress}> + testID={testID} + onPress={onPress} + > + width={iconSizes.icon24} + > {icon} @@ -58,11 +63,7 @@ export function OptionCard({ {title} {badgeText && ( - + {badgeText} diff --git a/apps/mobile/src/features/onboarding/SafeKeyboardOnboardingScreen.tsx b/apps/mobile/src/features/onboarding/SafeKeyboardOnboardingScreen.tsx index abac95bf489..f2103b1c23d 100644 --- a/apps/mobile/src/features/onboarding/SafeKeyboardOnboardingScreen.tsx +++ b/apps/mobile/src/features/onboarding/SafeKeyboardOnboardingScreen.tsx @@ -1,20 +1,18 @@ import { useHeaderHeight } from '@react-navigation/elements' import { LinearGradient } from 'expo-linear-gradient' -import React, { PropsWithChildren, useState } from 'react' -import { KeyboardAvoidingView, ScrollView, StyleSheet } from 'react-native' +import React, { PropsWithChildren } from 'react' +import { StyleSheet } from 'react-native' import { FadeIn, FadeOut } from 'react-native-reanimated' -import { Screen } from 'src/components/layout/Screen' -import { Flex, SpaceTokens, Text, flexStyles, useMedia, useSporeColors } from 'ui/src' +import { SafeKeyboardScreen } from 'src/components/layout/SafeKeyboardScreen' +import { Flex, SpaceTokens, Text, useMedia, useSporeColors } from 'ui/src' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' -import { opacify, spacing } from 'ui/src/theme' -import { useKeyboardLayout } from 'uniswap/src/utils/useKeyboardLayout' -import { isIOS } from 'utilities/src/platform' +import { opacify } from 'ui/src/theme' type OnboardingScreenProps = { subtitle?: string - title: string + title?: string paddingTop?: SpaceTokens - screenFooter?: JSX.Element + footer?: JSX.Element minHeightWhenKeyboardExpanded?: boolean } @@ -22,34 +20,13 @@ export function SafeKeyboardOnboardingScreen({ title, subtitle, children, - screenFooter, + footer, paddingTop = '$none', minHeightWhenKeyboardExpanded = true, }: PropsWithChildren): JSX.Element { - const [footerHeight, setFooterHeight] = useState(0) const headerHeight = useHeaderHeight() const colors = useSporeColors() const media = useMedia() - const keyboard = useKeyboardLayout() - - const header = ( - - - {title} - - {subtitle ? ( - - {subtitle} - - ) : null} - - ) - - const page = ( - - {children} - - ) const normalGradientPadding = 1.5 const responsiveGradientPadding = media.short ? 1.25 : normalGradientPadding @@ -58,77 +35,58 @@ export function SafeKeyboardOnboardingScreen({ ) - const compact = keyboard.isVisible && keyboard.containerHeight !== 0 - const containerStyle = compact ? styles.compact : styles.expand - - // This makes sure this component behaves just like `behavior="padding"` when - // there's enough space on the screen to show all components. - const minHeight = - minHeightWhenKeyboardExpanded && compact ? keyboard.containerHeight - footerHeight : 0 + const page = ( + <> + {title || subtitle ? ( + + {title && ( + + {title} + + )} + {subtitle && ( + + {subtitle} + + )} + + ) : null} + + {children} + + + ) return ( - - - - - {header} - {page} - - - { - setFooterHeight(height) - }}> - {screenFooter} - - - {topGradient} - + + + {page} + + ) } const styles = StyleSheet.create({ - base: { - flex: 1, - justifyContent: 'flex-end', - }, - compact: { - flexGrow: 0, - }, - container: { - paddingBottom: spacing.spacing12, - }, - expand: { - flexGrow: 1, - }, gradient: { left: 0, position: 'absolute', right: 0, top: 0, + zIndex: 1, }, }) diff --git a/apps/mobile/src/features/onboarding/hooks.ts b/apps/mobile/src/features/onboarding/hooks.ts index cb574fe6f31..40debb481b5 100644 --- a/apps/mobile/src/features/onboarding/hooks.ts +++ b/apps/mobile/src/features/onboarding/hooks.ts @@ -1,5 +1,5 @@ import { SharedEventName } from '@uniswap/analytics-events' -import { useAppDispatch } from 'src/app/hooks' +import { useDispatch } from 'react-redux' import { OnboardingStackBaseParams, useOnboardingStackNavigation } from 'src/app/navigation/types' import { MobileAppsFlyerEvents } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent, sendAppsFlyerEvent } from 'uniswap/src/features/telemetry/send' @@ -20,7 +20,7 @@ export function useCompleteOnboardingCallback({ entryPoint, importType, }: OnboardingStackBaseParams): () => Promise { - const dispatch = useAppDispatch() + const dispatch = useDispatch() const { getAllOnboardingAccounts, finishOnboarding } = useOnboardingContext() const navigation = useOnboardingStackNavigation() @@ -29,12 +29,12 @@ export function useCompleteOnboardingCallback({ return async () => { // Run all shared onboarding completion logic - await finishOnboarding(importType) + await finishOnboarding({ importType }) // Send appsflyer event for mobile attribution if (entryPoint === OnboardingEntryPoint.FreshInstallOrReplace) { sendAppsFlyerEvent(MobileAppsFlyerEvents.OnboardingCompleted, { importType }).catch((error) => - logger.debug('hooks', 'useCompleteOnboardingCallback', error) + logger.debug('hooks', 'useCompleteOnboardingCallback', error), ) } diff --git a/apps/mobile/src/features/scantastic/ScantasticModal.tsx b/apps/mobile/src/features/scantastic/ScantasticModal.tsx index dd1d30404ed..3d4d4a4d1d2 100644 --- a/apps/mobile/src/features/scantastic/ScantasticModal.tsx +++ b/apps/mobile/src/features/scantastic/ScantasticModal.tsx @@ -1,6 +1,7 @@ import React, { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' -import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { useDispatch } from 'react-redux' +import { useAppSelector } from 'src/app/hooks' import { useBiometricAppSettings, useBiometricPrompt } from 'src/features/biometrics/hooks' import { closeAllModals } from 'src/features/modals/modalSlice' import { selectModalState } from 'src/features/modals/selectModalState' @@ -14,10 +15,7 @@ import { ModalName } from 'uniswap/src/features/telemetry/constants' import { logger } from 'utilities/src/logger/logger' import { ONE_MINUTE_MS, ONE_SECOND_MS } from 'utilities/src/time/time' import { useInterval } from 'utilities/src/time/timing' -import { - ExtensionOnboardingState, - setExtensionOnboardingState, -} from 'wallet/src/features/behaviorHistory/slice' +import { ExtensionOnboardingState, setExtensionOnboardingState } from 'wallet/src/features/behaviorHistory/slice' import { pushNotification } from 'wallet/src/features/notifications/slice' import { AppNotificationType } from 'wallet/src/features/notifications/types' import { useSignerAccounts } from 'wallet/src/features/wallet/hooks' @@ -38,11 +36,11 @@ interface OtpStateApiResponse { export function ScantasticModal(): JSX.Element | null { const { t } = useTranslation() const colors = useSporeColors() - const dispatch = useAppDispatch() + const dispatch = useDispatch() // Use the first mnemonic account because zero-balance mnemonic accounts will fail to retrieve the mnemonic from rnEthers const account = useSignerAccounts().sort( - (account1, account2) => account1.derivationIndex - account2.derivationIndex + (account1, account2) => account1.derivationIndex - account2.derivationIndex, )[0] if (!account) { @@ -54,9 +52,7 @@ export function ScantasticModal(): JSX.Element | null { const [OTP, setOTP] = useState('') // Once a user has scanned a QR they have 6 minutes to correctly input the OTP - const [expirationTimestamp, setExpirationTimestamp] = useState( - Date.now() + 6 * ONE_MINUTE_MS - ) + const [expirationTimestamp, setExpirationTimestamp] = useState(Date.now() + 6 * ONE_MINUTE_MS) const pubKey = params?.publicKey const uuid = params?.uuid const device = `${params?.vendor || ''} ${params?.model || ''}`.trim() @@ -81,7 +77,7 @@ export function ScantasticModal(): JSX.Element | null { pushNotification({ type: AppNotificationType.ScantasticComplete, hideDelay: 6 * ONE_SECOND_MS, - }) + }), ) dispatch(setExtensionOnboardingState(ExtensionOnboardingState.Completed)) dispatch(closeAllModals()) @@ -172,8 +168,7 @@ export function ScantasticModal(): JSX.Element | null { requiredForAppAccess: biometricAuthRequiredForAppAccess, requiredForTransactions: biometricAuthRequiredForTransactions, } = useBiometricAppSettings() - const requiresBiometricAuth = - biometricAuthRequiredForAppAccess || biometricAuthRequiredForTransactions + const requiresBiometricAuth = biometricAuthRequiredForAppAccess || biometricAuthRequiredForTransactions const onConfirmSync = async (): Promise => { if (requiresBiometricAuth) { @@ -227,10 +222,7 @@ export function ScantasticModal(): JSX.Element | null { if (showIPWarning) { return ( - + @@ -251,10 +243,7 @@ export function ScantasticModal(): JSX.Element | null { if (expired) { return ( - + @@ -273,10 +262,7 @@ export function ScantasticModal(): JSX.Element | null { if (OTP) { return ( - + @@ -299,10 +285,7 @@ export function ScantasticModal(): JSX.Element | null { if (error) { return ( - + @@ -324,10 +307,7 @@ export function ScantasticModal(): JSX.Element | null { const renderDeviceDetails = Boolean(device || browser) return ( - + @@ -343,7 +323,8 @@ export function ScantasticModal(): JSX.Element | null { borderWidth={1} gap="$spacing12" p="$spacing16" - width="100%"> + width="100%" + > {device && ( @@ -369,7 +350,8 @@ export function ScantasticModal(): JSX.Element | null { borderRadius="$rounded16" gap="$spacing8" p="$spacing16" - width="100%"> + width="100%" + > {t('scantastic.confirmation.warning')} @@ -380,7 +362,8 @@ export function ScantasticModal(): JSX.Element | null { icon={requiresBiometricAuth ? : undefined} mb="$spacing4" theme="primary" - onPress={onConfirmSync}> + onPress={onConfirmSync} + > {t('scantastic.confirmation.button.continue')} diff --git a/apps/mobile/src/features/telemetry/directLogScreens.ts b/apps/mobile/src/features/telemetry/directLogScreens.ts index 0437ea27bb5..292ee4e7782 100644 --- a/apps/mobile/src/features/telemetry/directLogScreens.ts +++ b/apps/mobile/src/features/telemetry/directLogScreens.ts @@ -7,9 +7,6 @@ export const DIRECT_LOG_ONLY_SCREENS: string[] = [ MobileScreens.NFTCollection, ] -export function shouldLogScreen( - directFromPage: boolean | undefined, - screen: string | undefined -): boolean { +export function shouldLogScreen(directFromPage: boolean | undefined, screen: string | undefined): boolean { return directFromPage || screen === undefined || !DIRECT_LOG_ONLY_SCREENS.includes(screen) } diff --git a/apps/mobile/src/features/telemetry/saga.ts b/apps/mobile/src/features/telemetry/saga.ts index 6fad58c6910..ebffb15918d 100644 --- a/apps/mobile/src/features/telemetry/saga.ts +++ b/apps/mobile/src/features/telemetry/saga.ts @@ -1,14 +1,13 @@ // eslint-disable-next-line no-restricted-imports import { OriginApplication } from '@uniswap/analytics' import DeviceInfo, { getUniqueId } from 'react-native-device-info' -import { call, delay, fork, select, takeEvery } from 'typed-redux-saga' +import { call, delay, fork, select } from 'typed-redux-saga' import { uniswapUrls } from 'uniswap/src/constants/urls' import { ApplicationTransport } from 'utilities/src/telemetry/analytics/ApplicationTransport' import { selectAllowAnalytics } from 'wallet/src/features/telemetry/selectors' // eslint-disable-next-line no-restricted-imports import { analytics } from 'utilities/src/telemetry/analytics/analytics' -import { transactionActions } from 'wallet/src/features/transactions/slice' -import { logTransactionEvent } from 'wallet/src/features/transactions/transactionWatcherSaga' +import { watchTransactionEvents } from 'wallet/src/features/transactions/transactionWatcherSaga' export function* telemetrySaga() { yield* delay(1) @@ -23,12 +22,7 @@ export function* telemetrySaga() { }), allowAnalytics, undefined, - async () => getUniqueId() + async () => getUniqueId(), ) yield* fork(watchTransactionEvents) } - -function* watchTransactionEvents() { - // Watch for finalized transactions to send analytics events - yield* takeEvery(transactionActions.finalizeTransaction.type, logTransactionEvent) -} diff --git a/apps/mobile/src/features/telemetry/utils.ts b/apps/mobile/src/features/telemetry/utils.ts index 940194dd1ba..5df615db78a 100644 --- a/apps/mobile/src/features/telemetry/utils.ts +++ b/apps/mobile/src/features/telemetry/utils.ts @@ -11,7 +11,7 @@ export enum AuthMethod { export function getAuthMethod( isSettingEnabled: boolean, isTouchIdSupported: boolean, - isFaceIdSupported: boolean + isFaceIdSupported: boolean, ): AuthMethod { if (isSettingEnabled) { // both cannot be true since no iOS device supports both @@ -28,7 +28,7 @@ export function getAuthMethod( export function getEventParams( screen: MobileAppScreen, - params: RootParamList[MobileAppScreen] + params: RootParamList[MobileAppScreen], ): Record | undefined { switch (screen) { case MobileScreens.SettingsWallet: diff --git a/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/FiatPurchaseSummaryItem.stories.tsx b/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/FiatPurchaseSummaryItem.stories.tsx index 4a947d1be3f..ae95efe8ba8 100644 --- a/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/FiatPurchaseSummaryItem.stories.tsx +++ b/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/FiatPurchaseSummaryItem.stories.tsx @@ -1,11 +1,8 @@ import type { Meta, StoryObj } from '@storybook/react' import React from 'react' -import { - Chain, - TokenDocument, -} from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { getNativeAddress } from 'uniswap/src/constants/addresses' +import { Chain, TokenDocument } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { UniverseChainId } from 'uniswap/src/types/chains' -import { getNativeAddress } from 'wallet/src/constants/addresses' import { Routing } from 'wallet/src/data/tradingApi/__generated__/index' import { FiatPurchaseSummaryItem } from 'wallet/src/features/transactions/SummaryCards/SummaryItems/FiatPurchaseSummaryItem' import TransactionSummaryLayout from 'wallet/src/features/transactions/SummaryCards/SummaryItems/TransactionSummaryLayout' diff --git a/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/NFTApproveSummaryItem.stories.tsx b/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/NFTApproveSummaryItem.stories.tsx index 903e7de0161..1139a81d9e0 100644 --- a/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/NFTApproveSummaryItem.stories.tsx +++ b/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/NFTApproveSummaryItem.stories.tsx @@ -36,6 +36,7 @@ const baseApproveTx: Omit & { 'https://lh3.googleusercontent.com/9LokgAuB0Xqkio273GE0pY0WSJwOExFtFI1SkJT2jK-USvqFc-5if7ZP5PQ1h8s5YPimyJG5cSOdGGR2UaD3gTYMKAhj6yikYaw=s250', name: 'Froggy Friend #1777', tokenId: '1777', + address: '0x7ad05c1b87e93be306a9eadf80ea60d7648f1b6f', }, }, } diff --git a/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/NFTMintSummaryItem.stories.tsx b/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/NFTMintSummaryItem.stories.tsx index 74dc72a45ca..7fa84aa8b97 100644 --- a/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/NFTMintSummaryItem.stories.tsx +++ b/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/NFTMintSummaryItem.stories.tsx @@ -34,6 +34,7 @@ const baseNFTMintTx: Omit & { 'https://lh3.googleusercontent.com/9LokgAuB0Xqkio273GE0pY0WSJwOExFtFI1SkJT2jK-USvqFc-5if7ZP5PQ1h8s5YPimyJG5cSOdGGR2UaD3gTYMKAhj6yikYaw=s250', name: 'Froggy Friend #1777', tokenId: '1777', + address: '0x7ad05c1b87e93be306a9eadf80ea60d7648f1b6f', }, }, } diff --git a/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/NFTTradeSummaryItem.stories.tsx b/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/NFTTradeSummaryItem.stories.tsx index 83c7fbeb53a..2f1a0509038 100644 --- a/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/NFTTradeSummaryItem.stories.tsx +++ b/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/NFTTradeSummaryItem.stories.tsx @@ -1,6 +1,7 @@ import type { Meta, StoryObj } from '@storybook/react' import React from 'react' import { UniverseChainId } from 'uniswap/src/types/chains' +import { buildNativeCurrencyId } from 'uniswap/src/utils/currencyId' import { Routing } from 'wallet/src/data/tradingApi/__generated__/index' import { NFTTradeSummaryItem } from 'wallet/src/features/transactions/SummaryCards/SummaryItems/NFTTradeSummaryItem' import TransactionSummaryLayout from 'wallet/src/features/transactions/SummaryCards/SummaryItems/TransactionSummaryLayout' @@ -11,7 +12,6 @@ import { TransactionStatus, TransactionType, } from 'wallet/src/features/transactions/types' -import { buildNativeCurrencyId } from 'wallet/src/utils/currencyId' const meta: Meta = { title: 'WIP/Activity Items', @@ -38,6 +38,7 @@ const baseNFTBuyTx: Omit & { 'https://lh3.googleusercontent.com/9LokgAuB0Xqkio273GE0pY0WSJwOExFtFI1SkJT2jK-USvqFc-5if7ZP5PQ1h8s5YPimyJG5cSOdGGR2UaD3gTYMKAhj6yikYaw=s250', name: 'Froggy Friend #1777', tokenId: '1777', + address: '0x7ad05c1b87e93be306a9eadf80ea60d7648f1b6f', }, purchaseCurrencyId: buildNativeCurrencyId(UniverseChainId.Mainnet), purchaseCurrencyAmountRaw: '1000000000000000000', diff --git a/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/ReceiveSummaryItem.stories.tsx b/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/ReceiveSummaryItem.stories.tsx index 59c5c99dcd3..c7ea41de3fa 100644 --- a/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/ReceiveSummaryItem.stories.tsx +++ b/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/ReceiveSummaryItem.stories.tsx @@ -1,9 +1,9 @@ import type { Meta, StoryObj } from '@storybook/react' import React from 'react' import { TokenDocument } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { AssetType } from 'uniswap/src/entities/assets' import { UniverseChainId } from 'uniswap/src/types/chains' import { Routing } from 'wallet/src/data/tradingApi/__generated__/index' -import { AssetType } from 'wallet/src/entities/assets' import { ReceiveSummaryItem } from 'wallet/src/features/transactions/SummaryCards/SummaryItems/ReceiveSummaryItem' import TransactionSummaryLayout from 'wallet/src/features/transactions/SummaryCards/SummaryItems/TransactionSummaryLayout' import { @@ -118,6 +118,7 @@ const baseNFTReceiveTx: Omit & { 'https://lh3.googleusercontent.com/9LokgAuB0Xqkio273GE0pY0WSJwOExFtFI1SkJT2jK-USvqFc-5if7ZP5PQ1h8s5YPimyJG5cSOdGGR2UaD3gTYMKAhj6yikYaw=s250', name: 'Froggy Friend #1777', tokenId: '1777', + address: '0x7ad05c1b87e93be306a9eadf80ea60d7648f1b6f', }, }, } diff --git a/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/SendSummaryItem.stories.tsx b/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/SendSummaryItem.stories.tsx index 7d54d8e886b..e459764c256 100644 --- a/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/SendSummaryItem.stories.tsx +++ b/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/SendSummaryItem.stories.tsx @@ -1,8 +1,8 @@ import type { Meta, StoryObj } from '@storybook/react' import React from 'react' import { TokenDocument } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { AssetType } from 'uniswap/src/entities/assets' import { UniverseChainId } from 'uniswap/src/types/chains' -import { AssetType } from 'wallet/src/entities/assets' import { SendSummaryItem } from 'wallet/src/features/transactions/SummaryCards/SummaryItems/SendSummaryItem' import TransactionSummaryLayout from 'wallet/src/features/transactions/SummaryCards/SummaryItems/TransactionSummaryLayout' import { @@ -119,6 +119,7 @@ const baseNFTSendTx: Omit & { 'https://lh3.googleusercontent.com/9LokgAuB0Xqkio273GE0pY0WSJwOExFtFI1SkJT2jK-USvqFc-5if7ZP5PQ1h8s5YPimyJG5cSOdGGR2UaD3gTYMKAhj6yikYaw=s250', name: 'Froggy Friend #1777', tokenId: '1777', + address: '0x7ad05c1b87e93be306a9eadf80ea60d7648f1b6f', }, }, } diff --git a/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/SwapSummaryItem.stories.tsx b/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/SwapSummaryItem.stories.tsx index 6ecc64c8f8c..20a439ee89d 100644 --- a/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/SwapSummaryItem.stories.tsx +++ b/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/SwapSummaryItem.stories.tsx @@ -3,6 +3,7 @@ import { TradeType } from '@uniswap/sdk-core' import React from 'react' import { TokenDocument } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { UniverseChainId } from 'uniswap/src/types/chains' +import { buildCurrencyId, buildNativeCurrencyId } from 'uniswap/src/utils/currencyId' import { Routing } from 'wallet/src/data/tradingApi/__generated__/index' import { SwapSummaryItem } from 'wallet/src/features/transactions/SummaryCards/SummaryItems/SwapSummaryItem' import TransactionSummaryLayout from 'wallet/src/features/transactions/SummaryCards/SummaryItems/TransactionSummaryLayout' @@ -13,7 +14,6 @@ import { TransactionStatus, TransactionType, } from 'wallet/src/features/transactions/types' -import { buildCurrencyId, buildNativeCurrencyId } from 'wallet/src/utils/currencyId' const meta: Meta = { title: 'WIP/Activity Items', @@ -103,10 +103,7 @@ const baseSwapTx: Omit & { expectedInputCurrencyAmountRaw: '50000000000000000', maximumInputCurrencyAmountRaw: '50000000000000000', inputCurrencyId: buildNativeCurrencyId(UniverseChainId.Mainnet), - outputCurrencyId: buildCurrencyId( - UniverseChainId.Mainnet, - '0x6b175474e89094c44da98b954eedeac495271d0f' - ), + outputCurrencyId: buildCurrencyId(UniverseChainId.Mainnet, '0x6b175474e89094c44da98b954eedeac495271d0f'), transactedUSDValue: 105.21800000000002, }, } diff --git a/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/WrapSummaryItem.stories.tsx b/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/WrapSummaryItem.stories.tsx index be2d6ef490f..4747ec5db63 100644 --- a/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/WrapSummaryItem.stories.tsx +++ b/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/WrapSummaryItem.stories.tsx @@ -97,14 +97,13 @@ const baseWrapTx: Omit & { typeInfo: WrapTr }, } -const baseUnwrapTx: Omit & { typeInfo: WrapTransactionInfo } = - { - ...baseWrapTx, - typeInfo: { - ...baseWrapTx.typeInfo, - unwrapped: true, - }, - } +const baseUnwrapTx: Omit & { typeInfo: WrapTransactionInfo } = { + ...baseWrapTx, + typeInfo: { + ...baseWrapTx.typeInfo, + unwrapped: true, + }, +} export const Wrap: StoryObj = { render: () => ( diff --git a/apps/mobile/src/features/transactions/TransactionPending/TransactionPending.tsx b/apps/mobile/src/features/transactions/TransactionPending/TransactionPending.tsx index c7899be822d..4714c057f5e 100644 --- a/apps/mobile/src/features/transactions/TransactionPending/TransactionPending.tsx +++ b/apps/mobile/src/features/transactions/TransactionPending/TransactionPending.tsx @@ -3,13 +3,9 @@ import { useTranslation } from 'react-i18next' import { StatusAnimation } from 'src/features/transactions/TransactionPending/StatusAnimation' import { Button, Flex, Text, TouchableArea } from 'ui/src' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' -import { ElementName } from 'uniswap/src/features/telemetry/constants' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { WalletChainId } from 'uniswap/src/types/chains' -import { - TransactionDetails, - TransactionStatus, - isFinalizedTx, -} from 'wallet/src/features/transactions/types' +import { TransactionDetails, TransactionStatus, isFinalizedTx } from 'wallet/src/features/transactions/types' import { openTransactionLink } from 'wallet/src/utils/linking' type TransactionStatusProps = { @@ -60,14 +56,11 @@ export function TransactionPending({ {transaction && isFinalizedTx(transaction) ? ( - ) : null} - diff --git a/apps/mobile/src/features/transactions/swap/hooks/useOnCloseSendModal.tsx b/apps/mobile/src/features/transactions/swap/hooks/useOnCloseSendModal.tsx index c1d1cf036d4..26792643e47 100644 --- a/apps/mobile/src/features/transactions/swap/hooks/useOnCloseSendModal.tsx +++ b/apps/mobile/src/features/transactions/swap/hooks/useOnCloseSendModal.tsx @@ -1,10 +1,10 @@ import { useCallback } from 'react' -import { useAppDispatch } from 'src/app/hooks' +import { useDispatch } from 'react-redux' import { closeModal } from 'src/features/modals/modalSlice' import { ModalName } from 'uniswap/src/features/telemetry/constants' export function useOnCloseSendModal(): () => void { - const appDispatch = useAppDispatch() + const appDispatch = useDispatch() const onClose = useCallback((): void => { appDispatch(closeModal({ name: ModalName.Send })) diff --git a/apps/mobile/src/features/transactions/transfer/TransferFlow.tsx b/apps/mobile/src/features/transactions/transfer/TransferFlow.tsx index 1d2fd8bda5b..c957d8c40bd 100644 --- a/apps/mobile/src/features/transactions/transfer/TransferFlow.tsx +++ b/apps/mobile/src/features/transactions/transfer/TransferFlow.tsx @@ -1,7 +1,7 @@ import { providers } from 'ethers' import { default as React, useCallback, useEffect, useMemo, useReducer, useState } from 'react' import { useTranslation } from 'react-i18next' -import { StyleSheet, TouchableWithoutFeedback } from 'react-native' +import { Keyboard, LayoutAnimation, StyleSheet, TouchableWithoutFeedback } from 'react-native' import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated' import { useShouldShowNativeKeyboard } from 'src/app/hooks' import { RecipientSelect } from 'src/components/RecipientSelect/RecipientSelect' @@ -14,17 +14,30 @@ import { Flex, useDeviceInsets, useSporeColors } from 'ui/src' import EyeIcon from 'ui/src/assets/icons/eye.svg' import { useDeviceDimensions } from 'ui/src/hooks/useDeviceDimensions' import { iconSizes } from 'ui/src/theme' +import { TokenSelectorModal, TokenSelectorVariation } from 'uniswap/src/components/TokenSelector/TokenSelector' import { useBottomSheetContext } from 'uniswap/src/components/modals/BottomSheetContext' import { HandleBar } from 'uniswap/src/components/modals/HandleBar' import Trace from 'uniswap/src/features/telemetry/Trace' import { ModalName, SectionName } from 'uniswap/src/features/telemetry/constants' +import { CurrencyField, TransactionState } from 'uniswap/src/features/transactions/transactionState/types' +import { TokenSelectorFlow } from 'uniswap/src/features/transactions/transfer/types' +import { currencyAddress } from 'uniswap/src/utils/currencyId' import { - TokenSelectorModal, - TokenSelectorVariation, -} from 'wallet/src/components/TokenSelector/TokenSelector' + useAddToSearchHistory, + useCommonTokensOptions, + useFavoriteTokensOptions, + useFilterCallbacks, + usePopularTokensOptions, + usePortfolioTokenOptions, + useTokenSectionsForEmptySearch, + useTokenSectionsForSearchResults, +} from 'wallet/src/components/TokenSelector/hooks' import { WarningModal } from 'wallet/src/components/modals/WarningModal/WarningModal' +import { useWalletNavigation } from 'wallet/src/contexts/WalletNavigationContext' import { useTransactionGasFee } from 'wallet/src/features/gas/hooks' import { GasFeeResult, GasSpeed } from 'wallet/src/features/gas/types' +import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' +import { useTokenWarningDismissed } from 'wallet/src/features/tokens/safetyHooks' import { WarningAction, WarningSeverity } from 'wallet/src/features/transactions/WarningModal/types' import { useParsedSendWarnings } from 'wallet/src/features/transactions/hooks/useParsedTransactionWarnings' import { useTokenSelectorActionHandlers } from 'wallet/src/features/transactions/hooks/useTokenSelectorActionHandlers' @@ -33,27 +46,20 @@ import { INITIAL_TRANSACTION_STATE, transactionStateReducer, } from 'wallet/src/features/transactions/transactionState/transactionState' -import { - CurrencyField, - TransactionState, -} from 'wallet/src/features/transactions/transactionState/types' import { TransferReview } from 'wallet/src/features/transactions/transfer/TransferReview' import { TransferTokenForm } from 'wallet/src/features/transactions/transfer/TransferTokenForm' import { useDerivedTransferInfo } from 'wallet/src/features/transactions/transfer/hooks/useDerivedTransferInfo' import { useOnSelectRecipient } from 'wallet/src/features/transactions/transfer/hooks/useOnSelectRecipient' -import { useOnToggleShowRecipientSelector } from 'wallet/src/features/transactions/transfer/hooks/useOnToggleShowRecipientSelector' +import { useSetShowRecipientSelector } from 'wallet/src/features/transactions/transfer/hooks/useOnToggleShowRecipientSelector' import { useTransferERC20Callback, useTransferNFTCallback, } from 'wallet/src/features/transactions/transfer/hooks/useTransferCallback' import { useTransferTransactionRequest } from 'wallet/src/features/transactions/transfer/hooks/useTransferTransactionRequest' import { useTransferWarnings } from 'wallet/src/features/transactions/transfer/hooks/useTransferWarnings' -import { - DerivedTransferInfo, - TokenSelectorFlow, -} from 'wallet/src/features/transactions/transfer/types' +import { DerivedTransferInfo } from 'wallet/src/features/transactions/transfer/types' import { TransactionStep, TransferFlowProps } from 'wallet/src/features/transactions/types' -import { currencyAddress } from 'wallet/src/utils/currencyId' +import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' interface TransferFormProps { prefilledState?: TransactionState @@ -66,11 +72,11 @@ export function TransferFlow({ prefilledState, onClose }: TransferFormProps): JS const { t } = useTranslation() const { fullWidth } = useDeviceDimensions() const { isSheetReady } = useBottomSheetContext() + const { formatNumberOrString, convertFiatAmountFormatted } = useLocalizationContext() + const { navigateToBuyOrReceiveWithEmptyWallet } = useWalletNavigation() + const { registerSearch } = useAddToSearchHistory() - const [state, dispatch] = useReducer( - transactionStateReducer, - prefilledState || INITIAL_TRANSACTION_STATE - ) + const [state, dispatch] = useReducer(transactionStateReducer, prefilledState || INITIAL_TRANSACTION_STATE) const derivedTransferInfo = useDerivedTransferInfo(state) const [showViewOnlyModal, setShowViewOnlyModal] = useState(false) const [step, setStep] = useState(TransactionStep.FORM) @@ -78,8 +84,14 @@ export function TransferFlow({ prefilledState, onClose }: TransferFormProps): JS const { isFiatInput, exactAmountToken, exactAmountFiat } = derivedTransferInfo const { showRecipientSelector } = state + const activeAccountAddress = useActiveAccountAddressWithThrow() + const onSelectRecipient = useOnSelectRecipient(dispatch) - const onToggleShowRecipientSelector = useOnToggleShowRecipientSelector(dispatch) + const onSetShowRecipientSelector = useSetShowRecipientSelector(dispatch) + + const onHideRecipientSelector = useCallback(() => { + onSetShowRecipientSelector(false) + }, [onSetShowRecipientSelector]) const txRequest = useTransferTransactionRequest(derivedTransferInfo) const warnings = useTransferWarnings(t, derivedTransferInfo) @@ -87,13 +99,12 @@ export function TransferFlow({ prefilledState, onClose }: TransferFormProps): JS txRequest, GasSpeed.Urgent, // stop polling for gas once transaction is submitted - step === TransactionStep.SUBMITTED || - warnings.some((warning) => warning.action === WarningAction.DisableReview) + step === TransactionStep.SUBMITTED || warnings.some((warning) => warning.action === WarningAction.DisableReview), ) const transferTxWithGasSettings = useMemo( (): providers.TransactionRequest => ({ ...txRequest, ...gasFee.params }), - [gasFee.params, txRequest] + [gasFee.params, txRequest], ) const gasWarning = useTransactionGasWarning({ @@ -107,10 +118,7 @@ export function TransferFlow({ prefilledState, onClose }: TransferFormProps): JS const parsedSendWarnings = useParsedSendWarnings(allWarnings) - const { onSelectCurrency, onHideTokenSelector } = useTokenSelectorActionHandlers( - dispatch, - TokenSelectorFlow.Transfer - ) + const { onSelectCurrency, onHideTokenSelector } = useTokenSelectorActionHandlers(dispatch, TokenSelectorFlow.Transfer) // optimization for not rendering InnerContent initially, // when modal is opened with recipient or token selector presented @@ -169,9 +177,10 @@ export function TransferFlow({ prefilledState, onClose }: TransferFormProps): JS @@ -180,10 +189,7 @@ export function TransferFlow({ prefilledState, onClose }: TransferFormProps): JS height as 100% height doesn't include the handlebar height */} {step !== TransactionStep.SUBMITTED && ( - + )} {renderInnerContentRouter && isSheetReady && ( - } + icon={} modalName={ModalName.SwapWarning} severity={WarningSeverity.Low} title={t('send.warning.viewOnly.title')} @@ -231,10 +231,25 @@ export function TransferFlow({ prefilledState, onClose }: TransferFormProps): JS {!!state.selectingCurrencyField && ( Keyboard.dismiss()} + onPressAnimation={() => LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)} onSelectCurrency={onSelectCurrency} /> )} @@ -253,10 +268,7 @@ type TransferInnerContentProps = { onReviewPrev: () => void onRetrySubmit: () => void setShowViewOnlyModal: (show: boolean) => void -} & Pick< - TransferFlowProps, - 'derivedInfo' | 'onClose' | 'dispatch' | 'gasFee' | 'txRequest' | 'warnings' | 'exactValue' -> +} & Pick function TransferInnerContent({ showingSelectorScreen, @@ -275,8 +287,7 @@ function TransferInnerContent({ }: TransferInnerContentProps): JSX.Element | null { // TODO: move this up in the tree to mobile specific flow const { walletNeedsRestore, openWalletRestoreModal } = useWalletRestore() - const { showNativeKeyboard, onDecimalPadLayout, isLayoutPending, onInputPanelLayout } = - useShouldShowNativeKeyboard() + const { showNativeKeyboard, onDecimalPadLayout, isLayoutPending, onInputPanelLayout } = useShouldShowNativeKeyboard() const { currencyAmounts, recipient, currencyInInfo, nftIn, chainId, txId } = derivedTransferInfo const transferERC20Callback = useTransferERC20Callback( @@ -286,7 +297,7 @@ function TransferInnerContent({ currencyInInfo ? currencyAddress(currencyInInfo.currency) : undefined, currencyAmounts[CurrencyField.INPUT]?.quotient.toString(), txRequest, - onReviewNext + onReviewNext, ) const transferNFTCallback = useTransferNFTCallback( txId, @@ -295,7 +306,7 @@ function TransferInnerContent({ nftIn?.nftContract?.address, nftIn?.tokenId, txRequest, - onReviewNext + onReviewNext, ) const onTransfer = (): void => { @@ -318,11 +329,7 @@ function TransferInnerContent({ case TransactionStep.SUBMITTED: return ( - + ) case TransactionStep.FORM: diff --git a/apps/mobile/src/features/transactions/transfer/TransferHeader.tsx b/apps/mobile/src/features/transactions/transfer/TransferHeader.tsx index 935efb69208..c833eec1dec 100644 --- a/apps/mobile/src/features/transactions/transfer/TransferHeader.tsx +++ b/apps/mobile/src/features/transactions/transfer/TransferHeader.tsx @@ -11,10 +11,7 @@ type HeaderContentProps = Pick & { setShowViewOnlyModal: Dispatch> } -export function TransferHeader({ - flowName, - setShowViewOnlyModal, -}: HeaderContentProps): JSX.Element { +export function TransferHeader({ flowName, setShowViewOnlyModal }: HeaderContentProps): JSX.Element { const colors = useSporeColors() const account = useActiveAccountWithThrow() const { t } = useTranslation() @@ -29,7 +26,8 @@ export function TransferHeader({ mt="$spacing8" pb="$spacing8" pl="$spacing12" - pr="$spacing16"> + pr="$spacing16" + > {flowName} @@ -41,13 +39,10 @@ export function TransferHeader({ justifyContent="center" px="$spacing8" py="$spacing4" - onPress={(): void => setShowViewOnlyModal(true)}> + onPress={(): void => setShowViewOnlyModal(true)} + > - + {t('swap.header.viewOnly')} diff --git a/apps/mobile/src/features/transactions/transfer/TransferStatus.tsx b/apps/mobile/src/features/transactions/transfer/TransferStatus.tsx index 1459053a3ca..6264957444b 100644 --- a/apps/mobile/src/features/transactions/transfer/TransferStatus.tsx +++ b/apps/mobile/src/features/transactions/transfer/TransferStatus.tsx @@ -4,20 +4,13 @@ import { goBack } from 'src/app/navigation/rootNavigation' import { TransactionPending } from 'src/features/transactions/TransactionPending/TransactionPending' import { AppTFunction } from 'ui/src/i18n/types' import { FiatCurrencyInfo } from 'uniswap/src/features/fiatOnRamp/types' +import { CurrencyField } from 'uniswap/src/features/transactions/transactionState/types' import { NumberType } from 'utilities/src/format/types' import { useAppFiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks' -import { - LocalizationContextState, - useLocalizationContext, -} from 'wallet/src/features/language/LocalizationContext' +import { LocalizationContextState, useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' import { useSelectTransaction } from 'wallet/src/features/transactions/hooks' -import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' import { DerivedTransferInfo } from 'wallet/src/features/transactions/transfer/types' -import { - TransactionDetails, - TransactionStatus, - TransactionType, -} from 'wallet/src/features/transactions/types' +import { TransactionDetails, TransactionStatus, TransactionType } from 'wallet/src/features/transactions/types' import { useActiveAccountAddressWithThrow, useDisplayName } from 'wallet/src/features/wallet/hooks' type TransferStatusProps = { @@ -32,13 +25,12 @@ const getTextFromTransferStatus = ( fiatCurrencyInfo: FiatCurrencyInfo, derivedTransferInfo: DerivedTransferInfo, recipient: string | undefined, - transactionDetails?: TransactionDetails + transactionDetails?: TransactionDetails, ): { title: string description: string } => { - const { currencyInInfo, nftIn, currencyAmounts, isFiatInput, exactAmountFiat } = - derivedTransferInfo + const { currencyInInfo, nftIn, currencyAmounts, isFiatInput, exactAmountFiat } = derivedTransferInfo if ( !transactionDetails || transactionDetails.typeInfo.type !== TransactionType.Send || @@ -89,11 +81,7 @@ const getTextFromTransferStatus = ( } } -export function TransferStatus({ - derivedTransferInfo, - onNext, - onTryAgain, -}: TransferStatusProps): JSX.Element | null { +export function TransferStatus({ derivedTransferInfo, onNext, onTryAgain }: TransferStatusProps): JSX.Element | null { const { t } = useTranslation() const formatter = useLocalizationContext() const appFiatCurrencyInfo = useAppFiatCurrencyInfo() @@ -106,14 +94,7 @@ export function TransferStatus({ const displayName = useDisplayName(recipient, { includeUnitagSuffix: true }) const recipientName = displayName?.name ?? recipient const { title, description } = useMemo(() => { - return getTextFromTransferStatus( - t, - formatter, - appFiatCurrencyInfo, - derivedTransferInfo, - recipientName, - transaction - ) + return getTextFromTransferStatus(t, formatter, appFiatCurrencyInfo, derivedTransferInfo, recipientName, transaction) }, [t, formatter, appFiatCurrencyInfo, derivedTransferInfo, recipientName, transaction]) const onClose = useCallback(() => { diff --git a/apps/mobile/src/features/transactions/transfer/transferRewrite/TransferFlow.tsx b/apps/mobile/src/features/transactions/transfer/transferRewrite/TransferFlow.tsx index b55d7a4ae84..2aeff67f62a 100644 --- a/apps/mobile/src/features/transactions/transfer/transferRewrite/TransferFlow.tsx +++ b/apps/mobile/src/features/transactions/transfer/transferRewrite/TransferFlow.tsx @@ -11,13 +11,10 @@ import { import { useWalletRestore } from 'src/features/wallet/hooks' import Trace from 'uniswap/src/features/telemetry/Trace' import { ModalName, SectionName } from 'uniswap/src/features/telemetry/constants' -import { - SwapFormContextProvider, - SwapFormState, -} from 'wallet/src/features/transactions/contexts/SwapFormContext' +import { TradeProtocolPreference } from 'uniswap/src/features/transactions/transactionState/types' +import { SwapFormContextProvider, SwapFormState } from 'wallet/src/features/transactions/contexts/SwapFormContext' import { TransactionModal } from 'wallet/src/features/transactions/swap/TransactionModal' import { getFocusOnCurrencyFieldFromInitialState } from 'wallet/src/features/transactions/swap/hooks/useSwapPrefilledState' -import { TradeProtocolPreference } from 'wallet/src/features/transactions/transactionState/types' /** * @todo: The screens within this flow are not implemented. @@ -37,7 +34,8 @@ export function TransferFlow(): JSX.Element { modalName={ModalName.Send} openWalletRestoreModal={openWalletRestoreModal} walletNeedsRestore={walletNeedsRestore} - onClose={onClose}> + onClose={onClose} + > @@ -99,7 +97,7 @@ function TransferContextsContainer({ children }: { children?: ReactNode }): JSX. tradeProtocolPreference: TradeProtocolPreference.Default, } : undefined, - [initialState] + [initialState], ) return ( diff --git a/apps/mobile/src/features/transactions/transfer/transferRewrite/TransferScreenContext.tsx b/apps/mobile/src/features/transactions/transfer/transferRewrite/TransferScreenContext.tsx index 354be19430f..fbe41b0778f 100644 --- a/apps/mobile/src/features/transactions/transfer/transferRewrite/TransferScreenContext.tsx +++ b/apps/mobile/src/features/transactions/transfer/transferRewrite/TransferScreenContext.tsx @@ -11,9 +11,7 @@ type TransferScreenContextState = { setScreen: (screen: TransferScreen) => void } -export const TransferScreenContext = createContext( - undefined -) +export const TransferScreenContext = createContext(undefined) // TODO: re-use the same context built in extension, and move that to shared folder. export function TransferScreenContextProvider({ children }: { children: ReactNode }): JSX.Element { @@ -31,7 +29,7 @@ export function TransferScreenContextProvider({ children }: { children: ReactNod screenRef, setScreen: wrappedSetScreen, }), - [screen, wrappedSetScreen] + [screen, wrappedSetScreen], ) return {children} @@ -41,9 +39,7 @@ export const useTransferScreenContext = (): TransferScreenContextState => { const transferContext = useContext(TransferScreenContext) if (transferContext === undefined) { - throw new Error( - '`useTransferScreenContext ` must be used inside of `TransferScreenContextProvider`' - ) + throw new Error('`useTransferScreenContext ` must be used inside of `TransferScreenContextProvider`') } return transferContext diff --git a/apps/mobile/src/features/tweaks/slice.ts b/apps/mobile/src/features/tweaks/slice.ts index 0a6bdd47f90..1e6a0c63ff9 100644 --- a/apps/mobile/src/features/tweaks/slice.ts +++ b/apps/mobile/src/features/tweaks/slice.ts @@ -11,10 +11,7 @@ export const slice = createSlice({ name: 'tweaks', initialState: initialTweaksState, reducers: { - setCustomEndpoint: ( - state, - { payload: { customEndpoint } }: PayloadAction<{ customEndpoint?: CustomEndpoint }> - ) => { + setCustomEndpoint: (state, { payload: { customEndpoint } }: PayloadAction<{ customEndpoint?: CustomEndpoint }>) => { state.customEndpoint = customEndpoint }, }, diff --git a/apps/mobile/src/features/unitags/ChooseProfilePictureScreen.tsx b/apps/mobile/src/features/unitags/ChooseProfilePictureScreen.tsx index f7fc81d3b3b..2580a2ae3ab 100644 --- a/apps/mobile/src/features/unitags/ChooseProfilePictureScreen.tsx +++ b/apps/mobile/src/features/unitags/ChooseProfilePictureScreen.tsx @@ -11,8 +11,8 @@ import { UnitagName } from 'src/features/unitags/UnitagName' import { Button, Flex, Text, useIsDarkMode, useSporeColors } from 'ui/src' import { Pen } from 'ui/src/components/icons' import { fonts, iconSizes, imageSizes, spacing } from 'ui/src/theme' -import { ElementName } from 'uniswap/src/features/telemetry/constants' import { UnitagClaimSource } from 'uniswap/src/features/unitags/types' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { UniverseChainId } from 'uniswap/src/types/chains' import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding' import { MobileScreens, OnboardingScreens, UnitagScreens } from 'uniswap/src/types/screens/mobile' @@ -94,7 +94,7 @@ export function ChooseProfilePictureScreen({ { source, hasENSAddress: !!ensName, - } + }, ) setIsClaiming(false) setClaimError(attemptClaimError) @@ -115,7 +115,8 @@ export function ChooseProfilePictureScreen({ return ( + title={t('unitags.onboarding.profile.title')} + > @@ -128,11 +129,9 @@ export function ChooseProfilePictureScreen({ p="$spacing4" position="absolute" right={-spacing.spacing2} - testID={ElementName.Edit}> - + testID={TestID.Edit} + > + @@ -149,9 +148,10 @@ export function ChooseProfilePictureScreen({ - {showInfoModal && ( - setShowInfoModal(false)} /> - )} + {showInfoModal && setShowInfoModal(false)} />} {showClaimPeriodInfoModal && ( - setShowClaimPeriodInfoModal(false)} - /> + setShowClaimPeriodInfoModal(false)} /> )} ) @@ -446,7 +419,8 @@ const InfoModal = ({ px="$spacing12" shadowColor="$neutral3" shadowOpacity={0.4} - shadowRadius="$spacing4"> + shadowRadius="$spacing4" + > {usernamePlaceholder} @@ -463,13 +437,7 @@ const InfoModal = ({ ) } -const ClaimPeriodInfoModal = ({ - onClose, - username, -}: { - onClose: () => void - username: string -}): JSX.Element => { +const ClaimPeriodInfoModal = ({ onClose, username }: { onClose: () => void; username: string }): JSX.Element => { const colors = useSporeColors() const { t } = useTranslation() @@ -478,17 +446,11 @@ const ClaimPeriodInfoModal = ({ backgroundIconColor={colors.surface1.get()} caption={t('unitags.onboarding.claimPeriod.description', { username })} closeText={t('common.button.close')} - icon={ - - } + icon={} modalName={ModalName.ENSClaimPeriod} title={t('unitags.onboarding.claimPeriod.title')} - onClose={onClose}> + onClose={onClose} + > ) diff --git a/apps/mobile/src/features/unitags/ConfirmationElements.tsx b/apps/mobile/src/features/unitags/ConfirmationElements.tsx deleted file mode 100644 index 3bd8fb70ec0..00000000000 --- a/apps/mobile/src/features/unitags/ConfirmationElements.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import { Flex, Image, Text, useSporeColors } from 'ui/src' -import { DAI_LOGO, ENS_LOGO, ETH_LOGO, FROGGY, OPENSEA_LOGO, USDC_LOGO } from 'ui/src/assets' -import HeartIcon from 'ui/src/assets/icons/heart.svg' -import SendIcon from 'ui/src/assets/icons/send-action.svg' -import { colors, iconSizes, imageSizes, opacify } from 'ui/src/theme' -import { Arrow } from 'wallet/src/components/icons/Arrow' - -export const FroggyElement = (): JSX.Element => { - return ( - - - - ) -} - -export const OpenseaElement = (): JSX.Element => { - return ( - - - - ) -} - -export const SwapElement = (): JSX.Element => { - const sporeColors = useSporeColors() - return ( - - - - - ETH - - - - - - - DAI - - - - ) -} - -export const ENSElement = (): JSX.Element => { - return ( - - - - ) -} - -export const ReceiveUSDCElement = (): JSX.Element => { - return ( - - - +100 - - - - ) -} - -export const SendElement = (): JSX.Element => { - return ( - - - - ) -} - -export const HeartElement = (): JSX.Element => { - return ( - - - - ) -} - -export const TextElement = ({ text }: { text: string }): JSX.Element => { - return ( - - - {text} - - - ) -} - -export const EmojiElement = ({ emoji }: { emoji: string }): JSX.Element => { - return ( - - - {emoji} - - - ) -} diff --git a/apps/mobile/src/features/unitags/EditUnitagProfileScreen.tsx b/apps/mobile/src/features/unitags/EditUnitagProfileScreen.tsx index fa479875d5e..7adbf15502a 100644 --- a/apps/mobile/src/features/unitags/EditUnitagProfileScreen.tsx +++ b/apps/mobile/src/features/unitags/EditUnitagProfileScreen.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { Keyboard, KeyboardAvoidingView, StyleSheet } from 'react-native' import ContextMenu from 'react-native-context-menu-view' +import { useDispatch } from 'react-redux' import { navigate } from 'src/app/navigation/rootNavigation' import { UnitagStackScreenProp } from 'src/app/navigation/types' import { BackHeader } from 'src/components/layout/BackHeader' @@ -12,16 +13,7 @@ import { ChoosePhotoOptionsModal } from 'src/components/unitags/ChoosePhotoOptio import { DeleteUnitagModal } from 'src/components/unitags/DeleteUnitagModal' import { UnitagProfilePicture } from 'src/components/unitags/UnitagProfilePicture' import { HeaderRadial, solidHeaderProps } from 'src/features/externalProfile/ProfileHeader' -import { - Button, - Flex, - LinearGradient, - ScrollView, - Text, - getUniconColors, - useIsDarkMode, - useSporeColors, -} from 'ui/src' +import { Button, Flex, LinearGradient, ScrollView, Text, getUniconColors, useIsDarkMode, useSporeColors } from 'ui/src' import { Pen, TripleDots } from 'ui/src/components/icons' import { borderRadii, fonts, iconSizes, imageSizes, spacing } from 'ui/src/theme' import { useExtractedColors } from 'ui/src/utils/colors' @@ -33,6 +25,7 @@ import { useUnitagByAddress } from 'uniswap/src/features/unitags/hooks' import { ProfileMetadata } from 'uniswap/src/features/unitags/types' import { UniverseChainId } from 'uniswap/src/types/chains' import { MobileScreens, UnitagScreens } from 'uniswap/src/types/screens/mobile' +import { shortenAddress } from 'uniswap/src/utils/addresses' import { logger } from 'utilities/src/logger/logger' import { isIOS } from 'utilities/src/platform' import { normalizeTwitterUsername } from 'utilities/src/primitives/string' @@ -46,15 +39,13 @@ import { useAvatarUploadCredsWithRefresh } from 'wallet/src/features/unitags/hoo import { useWalletSigners } from 'wallet/src/features/wallet/context' import { useAccount } from 'wallet/src/features/wallet/hooks' import { DisplayNameType } from 'wallet/src/features/wallet/types' -import { useAppDispatch } from 'wallet/src/state' -import { shortenAddress } from 'wallet/src/utils/addresses' const BIO_TEXT_INPUT_LINES = 6 const isProfileMetadataEdited = ( loading: boolean, updatedMetadata: ProfileMetadata, - initialMetadata?: ProfileMetadata + initialMetadata?: ProfileMetadata, ): boolean => { return ( !loading && @@ -75,14 +66,12 @@ function isFieldEdited(a: string | undefined, b: string | undefined): boolean { } } -export function EditUnitagProfileScreen({ - route, -}: UnitagStackScreenProp): JSX.Element { +export function EditUnitagProfileScreen({ route }: UnitagStackScreenProp): JSX.Element { const { address, unitag, entryPoint } = route.params const { t } = useTranslation() const colors = useSporeColors() const isDarkMode = useIsDarkMode() - const dispatch = useAppDispatch() + const dispatch = useDispatch() const account = useAccount(address) const signerManager = useWalletSigners() @@ -115,11 +104,7 @@ export function EditUnitagProfileScreen({ twitter: twitterInput, } - const profileMetadataEdited = isProfileMetadataEdited( - updateResponseLoading, - updatedMetadata, - unitagMetadata - ) + const profileMetadataEdited = isProfileMetadataEdited(updateResponseLoading, updatedMetadata, unitagMetadata) useEffect(() => { // Only want to set values on first time unitag loads, when we have not yet made the PUT request @@ -195,9 +180,7 @@ export function EditUnitagProfileScreen({ ? { ...updatedMetadata, // Add Date.now() to the end to ensure the resulting URL is not cached by devices - avatar: avatarUploadUrlResponse?.avatarUrl - ? avatarUploadUrlResponse.avatarUrl + `?${Date.now()}` - : undefined, + avatar: avatarUploadUrlResponse?.avatarUrl ? avatarUploadUrlResponse.avatarUrl + `?${Date.now()}` : undefined, } : updatedMetadata @@ -226,7 +209,7 @@ export function EditUnitagProfileScreen({ pushNotification({ type: AppNotificationType.Success, title: t('unitags.notification.profile.title'), - }) + }), ) triggerRefetchUnitags() if (uploadedNewAvatar) { @@ -245,7 +228,7 @@ export function EditUnitagProfileScreen({ pushNotification({ type: AppNotificationType.Error, errorMessage: t('unitags.notification.profile.error'), - }) + }), ) } @@ -263,7 +246,8 @@ export function EditUnitagProfileScreen({ contentContainerStyle={styles.expand} // Disable the keyboard avoiding view when the modals are open, otherwise background elements will shift up when the user is editing their username enabled={!showDeleteUnitagModal && !showChangeUnitagModal} - style={styles.base}> + style={styles.base} + > + }} + > @@ -293,17 +278,17 @@ export function EditUnitagProfileScreen({ p="$spacing16" onPressBack={ // If entering from confirmation screen, back btn navigates to home - entryPoint === UnitagScreens.UnitagConfirmation - ? (): void => navigate(MobileScreens.Home) - : undefined - }> + entryPoint === UnitagScreens.UnitagConfirmation ? (): void => navigate(MobileScreens.Home) : undefined + } + > {t('settings.setting.wallet.action.editProfile')} + showsVerticalScrollIndicator={false} + > @@ -324,24 +309,12 @@ export function EditUnitagProfileScreen({ style={styles.headerGradient} /> {avatarImageUri && avatarColors?.primary ? ( - + ) : null} - + - + - + right={-spacing.spacing2} + > + @@ -440,7 +411,8 @@ export function EditUnitagProfileScreen({ mx="$spacing24" size="medium" theme="primary" - onPress={onPressSaveChanges}> + onPress={onPressSaveChanges} + > {t('common.button.save')} {showAvatarModal && ( @@ -454,18 +426,10 @@ export function EditUnitagProfileScreen({ )} {showDeleteUnitagModal && ( - setShowDeleteUnitagModal(false)} - /> + setShowDeleteUnitagModal(false)} /> )} {showChangeUnitagModal && ( - setShowChangeUnitagModal(false)} - /> + setShowChangeUnitagModal(false)} /> )} ) diff --git a/apps/mobile/src/features/unitags/UnitagConfirmationScreen.tsx b/apps/mobile/src/features/unitags/UnitagConfirmationScreen.tsx index ac52fa574e9..c93ebfd0722 100644 --- a/apps/mobile/src/features/unitags/UnitagConfirmationScreen.tsx +++ b/apps/mobile/src/features/unitags/UnitagConfirmationScreen.tsx @@ -4,6 +4,11 @@ import { navigate } from 'src/app/navigation/rootNavigation' import { UnitagStackScreenProp } from 'src/app/navigation/types' import { Screen } from 'src/components/layout/Screen' import { UnitagWithProfilePicture } from 'src/components/unitags/UnitagWithProfilePicture' +import { AnimatePresence, Button, Flex, Text, useDeviceInsets } from 'ui/src' +import { AnimateInOrder } from 'ui/src/animations' +import { useDeviceDimensions } from 'ui/src/hooks/useDeviceDimensions' +import { spacing } from 'ui/src/theme' +import { MobileScreens, UnitagScreens } from 'uniswap/src/types/screens/mobile' import { ENSElement, EmojiElement, @@ -14,12 +19,7 @@ import { SendElement, SwapElement, TextElement, -} from 'src/screens/Onboarding/OnboardingElements' -import { AnimatePresence, Button, Flex, Text, useDeviceInsets } from 'ui/src' -import { AnimateInOrder } from 'ui/src/animations' -import { useDeviceDimensions } from 'ui/src/hooks/useDeviceDimensions' -import { spacing } from 'ui/src/theme' -import { MobileScreens, UnitagScreens } from 'uniswap/src/types/screens/mobile' +} from 'wallet/src/components/landing/elements' import { UNITAG_SUFFIX } from 'wallet/src/features/unitags/constants' export function UnitagConfirmationScreen({ @@ -62,7 +62,7 @@ export function UnitagConfirmationScreen({ { element: , coordinates: { x: 1, y: 2 } }, { element: , coordinates: { x: 3.5, y: 2.5 } }, ], - [t] + [t], ) return ( @@ -75,7 +75,8 @@ export function UnitagConfirmationScreen({ enterStyle={{ opacity: 0, scale: 0.5 }} exitStyle={{ opacity: 0, scale: 0.5 }} index={1} - position="absolute"> + position="absolute" + > + position="absolute" + > + {...getInsetPropsForCoordinates(boxWidth, coordinates.x, coordinates.y)} + > {element} ))} - + @@ -145,7 +144,7 @@ export function UnitagConfirmationScreen({ const getInsetPropsForCoordinates = ( boxWidth: number, x: number, - y: number + y: number, ): { top?: number; right?: number; bottom?: number; left?: number } => { const unitSize = 10 const unit = boxWidth / unitSize diff --git a/apps/mobile/src/features/unitags/UnitagName.tsx b/apps/mobile/src/features/unitags/UnitagName.tsx index 414101aa1e5..d316fe1e86a 100644 --- a/apps/mobile/src/features/unitags/UnitagName.tsx +++ b/apps/mobile/src/features/unitags/UnitagName.tsx @@ -21,13 +21,15 @@ export function UnitagName({ enterStyle={{ opacity: 0 }} exitStyle={{ opacity: 0 }} gap="$spacing20" - opacity={opacity}> + opacity={opacity} + > + lineHeight={fonts.heading2.lineHeight} + > {name} + top={-spacing.spacing4} + > diff --git a/apps/mobile/src/features/wallet/hooks.ts b/apps/mobile/src/features/wallet/hooks.ts index 1eff07fc5c5..8758f49d45b 100644 --- a/apps/mobile/src/features/wallet/hooks.ts +++ b/apps/mobile/src/features/wallet/hooks.ts @@ -1,4 +1,5 @@ import { useCallback, useEffect, useState } from 'react' +import { useDispatch } from 'react-redux' import { useAppSelector } from 'src/app/hooks' import { openModal } from 'src/features/modals/modalSlice' import { selectModalState } from 'src/features/modals/selectModalState' @@ -8,14 +9,13 @@ import { ModalName } from 'uniswap/src/features/telemetry/constants' import { logger } from 'utilities/src/logger/logger' import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring' import { useNativeAccountExists } from 'wallet/src/features/wallet/hooks' -import { useAppDispatch } from 'wallet/src/state' export function useWalletRestore(params?: { openModalImmediately?: boolean }): { walletNeedsRestore: undefined | boolean openWalletRestoreModal: () => void isModalOpen: boolean } { - const dispatch = useAppDispatch() + const dispatch = useDispatch() const openModalImmediately = params?.openModalImmediately // Means that no private key found for mnemonic wallets const [walletNeedsRestore, setWalletNeedsRestore] = useState(false) @@ -36,7 +36,7 @@ export function useWalletRestore(params?: { openModalImmediately?: boolean }): { setWalletNeedsRestore(hasImportedSeedPhrase && !addresses.length) } openRestoreWalletModalIfNeeded().catch((error) => - logger.error(error, { tags: { file: 'wallet/hooks', function: 'useWalletRestore' } }) + logger.error(error, { tags: { file: 'wallet/hooks', function: 'useWalletRestore' } }), ) }, [dispatch, hasImportedSeedPhrase, isRestoreWalletEnabled]) diff --git a/apps/mobile/src/features/wallet/saga.ts b/apps/mobile/src/features/wallet/saga.ts index 7ada1246464..e070dda55c6 100644 --- a/apps/mobile/src/features/wallet/saga.ts +++ b/apps/mobile/src/features/wallet/saga.ts @@ -19,7 +19,7 @@ function* onRestoreMnemonicComplete() { pushNotification({ type: AppNotificationType.Success, title: i18n.t('notification.restore.success'), - }) + }), ) yield* call(dispatchNavigationAction, StackActions.replace(MobileScreens.Home)) } diff --git a/apps/mobile/src/features/walletConnect/api.ts b/apps/mobile/src/features/walletConnect/api.ts index fcd22b0d068..9a2d43c429f 100644 --- a/apps/mobile/src/features/walletConnect/api.ts +++ b/apps/mobile/src/features/walletConnect/api.ts @@ -1,6 +1,6 @@ import { getOneSignalPushToken } from 'src/features/notifications/Onesignal' import { config } from 'uniswap/src/config' -import { isJestRun } from 'utilities/src/environment' +import { isTestEnv } from 'utilities/src/environment' import { logger } from 'utilities/src/logger/logger' import { isAndroid } from 'utilities/src/platform' @@ -36,7 +36,7 @@ export async function registerWCClientForPushNotifications(clientId: string): Pr await fetch(`${WC_HOSTED_PUSH_SERVER_URL}/clients`, request) } catch (error) { // Shouldn't log if this is a jest run to avoid logging after a test completes - if (!isJestRun) { + if (!isTestEnv()) { logger.error(error, { tags: { file: 'walletConnectApi', function: 'registerWCv2ClientForPushNotifications' }, }) diff --git a/apps/mobile/src/features/walletConnect/saga.ts b/apps/mobile/src/features/walletConnect/saga.ts index b5c3fff7bad..ad2bf357931 100644 --- a/apps/mobile/src/features/walletConnect/saga.ts +++ b/apps/mobile/src/features/walletConnect/saga.ts @@ -74,15 +74,13 @@ function createWalletConnectChannel(): EventChannel { * and the proposal namespaces (chains, methods, events) */ const sessionProposalHandler = async ( - proposalEvent: Omit, 'topic'> + proposalEvent: Omit, 'topic'>, ): Promise => { const { params: proposal } = proposalEvent emit({ type: 'session_proposal', proposal }) } - const sessionRequestHandler = async ( - request: Web3WalletTypes.SessionRequest - ): Promise => { + const sessionRequestHandler = async (request: Web3WalletTypes.SessionRequest): Promise => { emit({ type: 'session_request', request }) } @@ -188,7 +186,7 @@ function* handleSessionProposal(proposal: ProposalTypes.Struct) { source: 'walletconnect', }, }, - }) + }), ) } catch (e) { // Reject pending session if required namespaces includes non-EVM chains or unsupported EVM chains @@ -197,9 +195,7 @@ function* handleSessionProposal(proposal: ProposalTypes.Struct) { reason: getSdkError('UNSUPPORTED_CHAINS'), }) - const chainLabels = WALLET_SUPPORTED_CHAIN_IDS.map( - (chainId) => UNIVERSE_CHAIN_INFO[chainId].label - ).join(', ') + const chainLabels = WALLET_SUPPORTED_CHAIN_IDS.map((chainId) => UNIVERSE_CHAIN_INFO[chainId].label).join(', ') const confirmed = yield* call( showAlert, @@ -207,7 +203,7 @@ function* handleSessionProposal(proposal: ProposalTypes.Struct) { i18n.t('walletConnect.error.connection.message', { chainNames: chainLabels, dappName: dapp.name, - }) + }), ) if (confirmed) { yield* put(setHasPendingSessionError(false)) @@ -220,7 +216,7 @@ function* handleSessionProposal(proposal: ProposalTypes.Struct) { 'WalletConnectSaga', 'sessionProposalHandler', 'Rejected session proposal due to invalid proposal namespaces: ', - e + e, ) } } @@ -248,36 +244,25 @@ function* handleSessionRequest(sessionRequest: PendingRequestTypes.Struct) { addRequest({ account, request, - }) + }), ) break } case EthMethod.EthSendTransaction: { - const { account, request } = parseTransactionRequest( - method, - topic, - id, - chainId, - dapp, - requestParams - ) + const { account, request } = parseTransactionRequest(method, topic, id, chainId, dapp, requestParams) yield* put( addRequest({ account, request, - }) + }), ) break } default: // Reject request for an invalid method - logger.warn( - 'WalletConnectSaga', - 'sessionRequestHandler', - `Session request method is unsupported: ${method}` - ) + logger.warn('WalletConnectSaga', 'sessionRequestHandler', `Session request method is unsupported: ${method}`) yield* call([wcWeb3Wallet, wcWeb3Wallet.respondSessionRequest], { topic, response: { @@ -317,7 +302,7 @@ function* populateActiveSessions() { // Verify account address for session exists in wallet's accounts const matchingAccount = Object.values(accounts).find( - (account) => account.address.toLowerCase() === accountAddress.toLowerCase() + (account) => account.address.toLowerCase() === accountAddress.toLowerCase(), ) if (!matchingAccount) { continue @@ -344,7 +329,7 @@ function* populateActiveSessions() { namespaces: session.namespaces, }, account: accountAddress, - }) + }), ) } } diff --git a/apps/mobile/src/features/walletConnect/selectors.ts b/apps/mobile/src/features/walletConnect/selectors.ts index e06d6763169..79c527d34cd 100644 --- a/apps/mobile/src/features/walletConnect/selectors.ts +++ b/apps/mobile/src/features/walletConnect/selectors.ts @@ -21,11 +21,7 @@ export const selectSessions = return Object.values(wcAccount.sessions) } -export const makeSelectSessions = (): Selector< - MobileState, - WalletConnectSession[] | undefined, - [Maybe
] -> => +export const makeSelectSessions = (): Selector]> => createSelector( (state: MobileState) => state.walletConnect.byAccount, (_: MobileState, address: Maybe
) => address, @@ -40,7 +36,7 @@ export const makeSelectSessions = (): Selector< } return Object.values(wcAccount.sessions) - } + }, ) export const selectPendingRequests = (state: MobileState): WalletConnectRequest[] => { diff --git a/apps/mobile/src/features/walletConnect/signWcRequestSaga.ts b/apps/mobile/src/features/walletConnect/signWcRequestSaga.ts index ffeef9c2330..c008cd87b51 100644 --- a/apps/mobile/src/features/walletConnect/signWcRequestSaga.ts +++ b/apps/mobile/src/features/walletConnect/signWcRequestSaga.ts @@ -1,26 +1,14 @@ import { providers } from 'ethers' import { wcWeb3Wallet } from 'src/features/walletConnect/saga' -import { - TransactionRequest, - UwuLinkErc20Request, -} from 'src/features/walletConnect/walletConnectSlice' +import { TransactionRequest, UwuLinkErc20Request } from 'src/features/walletConnect/walletConnectSlice' import { call, put } from 'typed-redux-saga' +import { AssetType } from 'uniswap/src/entities/assets' import { UniverseChainId, WalletChainId } from 'uniswap/src/types/chains' -import { - DappInfo, - EthMethod, - EthSignMethod, - UwULinkMethod, - WalletConnectEvent, -} from 'uniswap/src/types/walletConnect' +import { DappInfo, EthMethod, EthSignMethod, UwULinkMethod, WalletConnectEvent } from 'uniswap/src/types/walletConnect' import { logger } from 'utilities/src/logger/logger' -import { AssetType } from 'wallet/src/entities/assets' import { pushNotification } from 'wallet/src/features/notifications/slice' import { AppNotificationType } from 'wallet/src/features/notifications/types' -import { - SendTransactionParams, - sendTransaction, -} from 'wallet/src/features/transactions/sendTransactionSaga' +import { SendTransactionParams, sendTransaction } from 'wallet/src/features/transactions/sendTransactionSaga' import { TransactionType } from 'wallet/src/features/transactions/types' import { Account } from 'wallet/src/features/wallet/accounts/types' import { getSignerManager } from 'wallet/src/features/wallet/context' @@ -62,15 +50,12 @@ export function* signWcRequest(params: SignMessageParams | SignTransactionParams pushNotification({ type: AppNotificationType.Success, title: 'Checked in', - }) + }), ) } } else if (method === EthMethod.SignTypedData || method === EthMethod.SignTypedDataV4) { signature = yield* call(signTypedDataMessage, params.message, account, signerManager) - } else if ( - method === EthMethod.EthSendTransaction && - params.request.type === UwULinkMethod.Erc20Send - ) { + } else if (method === EthMethod.EthSendTransaction && params.request.type === UwULinkMethod.Erc20Send) { const txParams: SendTransactionParams = { chainId: params.transaction.chainId || UniverseChainId.Mainnet, account, @@ -107,7 +92,7 @@ export function* signWcRequest(params: SignMessageParams | SignTransactionParams pushNotification({ type: AppNotificationType.TransactionPending, chainId: txParams.chainId, - }) + }), ) } @@ -132,7 +117,7 @@ export function* signWcRequest(params: SignMessageParams | SignTransactionParams }).catch((error) => logger.error(error, { tags: { file: 'walletConnect/saga', function: 'signWcRequest/uwulink' }, - }) + }), ) } } catch (error) { @@ -155,7 +140,7 @@ export function* signWcRequest(params: SignMessageParams | SignTransactionParams imageUrl: params.dapp.icon ?? null, chainId, address: account.address, - }) + }), ) logger.error(error, { tags: { file: 'walletConnect/saga', function: 'signWcRequest' } }) } @@ -163,5 +148,5 @@ export function* signWcRequest(params: SignMessageParams | SignTransactionParams export const { wrappedSaga: signWcRequestSaga, actions: signWcRequestActions } = createSaga( signWcRequest, - 'signWalletConnect' + 'signWalletConnect', ) diff --git a/apps/mobile/src/features/walletConnect/utils.test.ts b/apps/mobile/src/features/walletConnect/utils.test.ts index 6f027b88bae..d3fd396eeaa 100644 --- a/apps/mobile/src/features/walletConnect/utils.test.ts +++ b/apps/mobile/src/features/walletConnect/utils.test.ts @@ -14,15 +14,11 @@ const TEST_ADDRESS = '0xdFb84E543C39ACa3c6a39ea4e3B6c40eE7d2EBdA' describe(getAccountAddressFromEIP155String, () => { it('handles valid eip155 mainnet address', () => { - expect(getAccountAddressFromEIP155String(`${EIP155_MAINNET}:${TEST_ADDRESS}`)).toBe( - TEST_ADDRESS - ) + expect(getAccountAddressFromEIP155String(`${EIP155_MAINNET}:${TEST_ADDRESS}`)).toBe(TEST_ADDRESS) }) it('handles valid eip155 polygon address', () => { - expect(getAccountAddressFromEIP155String(`${EIP155_POLYGON}:${TEST_ADDRESS}`)).toBe( - TEST_ADDRESS - ) + expect(getAccountAddressFromEIP155String(`${EIP155_POLYGON}:${TEST_ADDRESS}`)).toBe(TEST_ADDRESS) }) it('handles invalid eip155 address', () => { @@ -32,15 +28,18 @@ describe(getAccountAddressFromEIP155String, () => { describe(getSupportedWalletConnectChains, () => { it('handles list of valid chains', () => { - expect( - getSupportedWalletConnectChains([EIP155_MAINNET, EIP155_POLYGON, EIP155_OPTIMISM]) - ).toEqual([UniverseChainId.Mainnet, UniverseChainId.Polygon, UniverseChainId.Optimism]) + expect(getSupportedWalletConnectChains([EIP155_MAINNET, EIP155_POLYGON, EIP155_OPTIMISM])).toEqual([ + UniverseChainId.Mainnet, + UniverseChainId.Polygon, + UniverseChainId.Optimism, + ]) }) it('handles list of valid chains including an invalid chain', () => { - expect( - getSupportedWalletConnectChains([EIP155_MAINNET, EIP155_POLYGON, EIP155_LINEA_UNSUPPORTED]) - ).toEqual([UniverseChainId.Mainnet, UniverseChainId.Polygon]) + expect(getSupportedWalletConnectChains([EIP155_MAINNET, EIP155_POLYGON, EIP155_LINEA_UNSUPPORTED])).toEqual([ + UniverseChainId.Mainnet, + UniverseChainId.Polygon, + ]) }) }) diff --git a/apps/mobile/src/features/walletConnect/utils.ts b/apps/mobile/src/features/walletConnect/utils.ts index 23d0f1da835..ceee1373455 100644 --- a/apps/mobile/src/features/walletConnect/utils.ts +++ b/apps/mobile/src/features/walletConnect/utils.ts @@ -3,10 +3,10 @@ import { Web3WalletTypes } from '@walletconnect/web3wallet' import { utils } from 'ethers' import { wcWeb3Wallet } from 'src/features/walletConnect/saga' import { SignRequest, TransactionRequest } from 'src/features/walletConnect/walletConnectSlice' +import { toSupportedChainId } from 'uniswap/src/features/chains/utils' import { WalletChainId } from 'uniswap/src/types/chains' import { EthMethod, EthSignMethod } from 'uniswap/src/types/walletConnect' import { logger } from 'utilities/src/logger/logger' -import { toSupportedChainId } from 'wallet/src/features/chains/utils' /** * Construct WalletConnect 2.0 session namespaces to complete a new pairing. Used when approving a new pairing request. @@ -18,7 +18,7 @@ import { toSupportedChainId } from 'wallet/src/features/chains/utils' */ export const getSessionNamespaces = ( account: Address, - proposalNamespaces: ProposalTypes.RequiredNamespaces + proposalNamespaces: ProposalTypes.RequiredNamespaces, ): SessionTypes.Namespaces => { // Below inspired from https://github.com/WalletConnect/web-examples/blob/main/wallets/react-wallet-v2/src/views/SessionProposalModal.tsx#L63 const namespaces: SessionTypes.Namespaces = {} @@ -46,9 +46,7 @@ export const getSupportedWalletConnectChains = (chains?: string[]): WalletChainI return } - return chains - .map((chain) => getChainIdFromEIP155String(chain)) - .filter((c): c is WalletChainId => Boolean(c)) + return chains.map((chain) => getChainIdFromEIP155String(chain)).filter((c): c is WalletChainId => Boolean(c)) } /** @@ -89,7 +87,7 @@ export const parseSignRequest = ( internalId: number, chainId: WalletChainId, dapp: SignClientTypes.Metadata, - requestParams: Web3WalletTypes.SessionRequest['params']['request']['params'] + requestParams: Web3WalletTypes.SessionRequest['params']['request']['params'], ): { account: Address; request: SignRequest } => { const { address, rawMessage, message } = getAddressAndMessageToSign(method, requestParams) return { @@ -131,7 +129,7 @@ export const parseTransactionRequest = ( internalId: number, chainId: WalletChainId, dapp: SignClientTypes.Metadata, - requestParams: Web3WalletTypes.SessionRequest['params']['request']['params'] + requestParams: Web3WalletTypes.SessionRequest['params']['request']['params'], ): { account: Address; request: TransactionRequest } => { // Omit gasPrice and nonce in tx sent from dapp since it is calculated later const { from, to, data, gasLimit, value } = requestParams[0] @@ -170,7 +168,7 @@ export const parseTransactionRequest = ( */ export function getAddressAndMessageToSign( ethMethod: EthSignMethod, - params: Web3WalletTypes.SessionRequest['params']['request']['params'] + params: Web3WalletTypes.SessionRequest['params']['request']['params'], ): { address: string; rawMessage: string; message: string | null } { switch (ethMethod) { case EthMethod.PersonalSign: diff --git a/apps/mobile/src/features/walletConnect/walletConnectSlice.ts b/apps/mobile/src/features/walletConnect/walletConnectSlice.ts index 87c80c91f41..3c32ad23661 100644 --- a/apps/mobile/src/features/walletConnect/walletConnectSlice.ts +++ b/apps/mobile/src/features/walletConnect/walletConnectSlice.ts @@ -1,13 +1,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { ProposalTypes, SessionTypes } from '@walletconnect/types' import { WalletChainId } from 'uniswap/src/types/chains' -import { - DappInfo, - EthMethod, - EthSignMethod, - EthTransaction, - UwULinkMethod, -} from 'uniswap/src/types/walletConnect' +import { DappInfo, EthMethod, EthSignMethod, EthTransaction, UwULinkMethod } from 'uniswap/src/types/walletConnect' export type WalletConnectPendingSession = { id: string @@ -64,9 +58,7 @@ export interface UwuLinkErc20Request extends BaseRequest { export type WalletConnectRequest = SignRequest | TransactionRequest | UwuLinkErc20Request -export const isTransactionRequest = ( - request: WalletConnectRequest -): request is TransactionRequest => +export const isTransactionRequest = (request: WalletConnectRequest): request is TransactionRequest => request.type === EthMethod.EthSendTransaction || request.type === UwULinkMethod.Erc20Send export interface WalletConnectState { @@ -91,10 +83,7 @@ const slice = createSlice({ name: 'walletConnect', initialState: initialWalletConnectState, reducers: { - addSession: ( - state, - action: PayloadAction<{ account: string; wcSession: WalletConnectSession }> - ) => { + addSession: (state, action: PayloadAction<{ account: string; wcSession: WalletConnectSession }>) => { const { wcSession, account } = action.payload state.byAccount[account] ??= { sessions: {} } // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -102,10 +91,7 @@ const slice = createSlice({ state.pendingSession = null }, - updateSession: ( - state, - action: PayloadAction<{ account: string; wcSession: WalletConnectSession }> - ) => { + updateSession: (state, action: PayloadAction<{ account: string; wcSession: WalletConnectSession }>) => { const { wcSession, account } = action.payload const wcAccount = state.byAccount[account] if (wcAccount) { @@ -135,10 +121,7 @@ const slice = createSlice({ }) }, - addPendingSession: ( - state, - action: PayloadAction<{ wcSession: WalletConnectPendingSession }> - ) => { + addPendingSession: (state, action: PayloadAction<{ wcSession: WalletConnectPendingSession }>) => { const { wcSession } = action.payload state.pendingSession = wcSession }, @@ -147,22 +130,14 @@ const slice = createSlice({ state.pendingSession = null }, - addRequest: ( - state, - action: PayloadAction<{ request: WalletConnectRequest; account: string }> - ) => { + addRequest: (state, action: PayloadAction<{ request: WalletConnectRequest; account: string }>) => { const { request } = action.payload state.pendingRequests.push(request) }, - removeRequest: ( - state, - action: PayloadAction<{ requestInternalId: string; account: string }> - ) => { + removeRequest: (state, action: PayloadAction<{ requestInternalId: string; account: string }>) => { const { requestInternalId } = action.payload - state.pendingRequests = state.pendingRequests.filter( - (req) => req.internalId !== requestInternalId - ) + state.pendingRequests = state.pendingRequests.filter((req) => req.internalId !== requestInternalId) }, setDidOpenFromDeepLink: (state, action: PayloadAction) => { diff --git a/apps/mobile/src/features/widgets/widgets.ts b/apps/mobile/src/features/widgets/widgets.ts index f7425053ce7..c1768c855e6 100644 --- a/apps/mobile/src/features/widgets/widgets.ts +++ b/apps/mobile/src/features/widgets/widgets.ts @@ -1,6 +1,7 @@ import { NativeModules } from 'react-native' import { getItem, reloadAllTimelines, setItem } from 'react-native-widgetkit' import { getBuildVariant } from 'src/utils/version' +import { currencyIdToContractInput } from 'uniswap/src/features/dataApi/utils' import { MobileEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { CurrencyId } from 'uniswap/src/types/currency' @@ -8,7 +9,6 @@ import { WidgetEvent } from 'uniswap/src/types/widgets' import { isAndroid } from 'utilities/src/platform' // eslint-disable-next-line no-restricted-imports import { analytics } from 'utilities/src/telemetry/analytics/analytics' -import { currencyIdToContractInput } from 'wallet/src/features/dataApi/utils' import { Account, AccountType } from 'wallet/src/features/wallet/accounts/types' const APP_GROUP = 'group.com.uniswap.widgets' @@ -57,14 +57,15 @@ export const setFavoritesUserDefaults = (currencyIds: CurrencyId[]): void => { } export const setAccountAddressesUserDefaults = (accounts: Account[]): void => { - const userDefaultAccounts: Array<{ address: string; name: Maybe; isSigner: boolean }> = - accounts.map((account: Account) => { + const userDefaultAccounts: Array<{ address: string; name: Maybe; isSigner: boolean }> = accounts.map( + (account: Account) => { return { address: account.address, name: account.name, isSigner: account.type === AccountType.SignerMnemonic, } - }) + }, + ) const data = { accounts: userDefaultAccounts, } diff --git a/apps/mobile/src/index.ts b/apps/mobile/src/index.ts index 14285a59908..e58b46e762f 100644 --- a/apps/mobile/src/index.ts +++ b/apps/mobile/src/index.ts @@ -12,7 +12,7 @@ declare global { style: Record[] | Record, config?: { shouldMatchAllProps?: boolean - } + }, ): R } } diff --git a/apps/mobile/src/lib/RNEthersRs.ts b/apps/mobile/src/lib/RNEthersRs.ts index 9d9dc5ef31c..cb2e34a848d 100644 --- a/apps/mobile/src/lib/RNEthersRs.ts +++ b/apps/mobile/src/lib/RNEthersRs.ts @@ -32,18 +32,12 @@ export function getAddressesForStoredPrivateKeys(): Promise { } // returns the address for the mnemonic -export function generateAddressForMnemonic( - mnemonic: string, - derivationIndex: number -): Promise { +export function generateAddressForMnemonic(mnemonic: string, derivationIndex: number): Promise { return RNEthersRS.generateAddressForMnemonic(mnemonic, derivationIndex) } // returns the address of the generated key -export function generateAndStorePrivateKey( - mnemonicId: string, - derivationIndex: number -): Promise { +export function generateAndStorePrivateKey(mnemonicId: string, derivationIndex: number): Promise { return RNEthersRS.generateAndStorePrivateKey(mnemonicId, derivationIndex) } @@ -51,11 +45,7 @@ export function removePrivateKey(address: string): Promise { return RNEthersRS.removePrivateKey(address) } -export function signTransactionHashForAddress( - address: string, - hash: string, - chainId: number -): Promise { +export function signTransactionHashForAddress(address: string, hash: string, chainId: number): Promise { return RNEthersRS.signTransactionHashForAddress(address, hash, chainId) } @@ -63,10 +53,6 @@ export function signMessageForAddress(address: string, message: string): Promise return RNEthersRS.signMessageForAddress(address, message) } -export function signHashForAddress( - address: string, - hash: string, - chainId: number -): Promise { +export function signHashForAddress(address: string, hash: string, chainId: number): Promise { return RNEthersRS.signHashForAddress(address, hash, chainId) } diff --git a/apps/mobile/src/screens/AppLoadingScreen.tsx b/apps/mobile/src/screens/AppLoadingScreen.tsx index 8975b4dfb0e..1be91164dfb 100644 --- a/apps/mobile/src/screens/AppLoadingScreen.tsx +++ b/apps/mobile/src/screens/AppLoadingScreen.tsx @@ -2,8 +2,8 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack' import dayjs from 'dayjs' import { isEnrolledAsync } from 'expo-local-authentication' import { t } from 'i18next' -import { isNumber } from 'lodash' import { useCallback, useEffect, useState } from 'react' +import { useDispatch } from 'react-redux' import { OnboardingStackParamList } from 'src/app/navigation/types' import { SplashScreen } from 'src/features/appLoading/SplashScreen' import { useBiometricContext } from 'src/features/biometrics/context' @@ -12,13 +12,10 @@ import { NotificationPermission, useNotificationOSPermissionsEnabled, } from 'src/features/notifications/hooks/useNotificationOSPermissionsEnabled' -import { - RecoveryWalletInfo, - useOnDeviceRecoveryData, -} from 'src/screens/Import/useOnDeviceRecoveryData' +import { RecoveryWalletInfo, useOnDeviceRecoveryData } from 'src/screens/Import/useOnDeviceRecoveryData' import { hideSplashScreen } from 'src/utils/splashScreen' -import { DynamicConfigs } from 'uniswap/src/features/gating/configs' -import { useDynamicConfig } from 'uniswap/src/features/gating/hooks' +import { DynamicConfigs, OnDeviceRecoveryConfigKey } from 'uniswap/src/features/gating/configs' +import { useDynamicConfigValue } from 'uniswap/src/features/gating/hooks' import { MobileEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding' @@ -29,7 +26,7 @@ import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring' import { AccountType, SignerMnemonicAccount } from 'wallet/src/features/wallet/accounts/types' import { selectAnyAddressHasNotificationsEnabled } from 'wallet/src/features/wallet/selectors' import { setFinishedOnboarding } from 'wallet/src/features/wallet/slice' -import { useAppDispatch, useAppSelector } from 'wallet/src/state' +import { useAppSelector } from 'wallet/src/state' export const SPLASH_SCREEN = { uri: 'SplashScreen' } @@ -38,7 +35,7 @@ type Props = NativeStackScreenProps void } { - const dispatch = useAppDispatch() + const dispatch = useDispatch() const { setRecoveredImportedAccounts, finishOnboarding } = useOnboardingContext() const notificationOSPermission = useNotificationOSPermissionsEnabled() @@ -48,21 +45,19 @@ function useFinishAutomatedRecovery(navigation: Props['navigation']): { const importAccounts = useCallback( async (mnemonicId: string, recoveryWalletInfos: RecoveryWalletInfo[]) => { - const accountsToImport = recoveryWalletInfos.map( - (addressInfo, index): SignerMnemonicAccount => { - return { - type: AccountType.SignerMnemonic, - mnemonicId, - name: t('onboarding.wallet.defaultName', { number: index + 1 }), - address: addressInfo.address, - derivationIndex: addressInfo.derivationIndex, - timeImportedMs: dayjs().valueOf(), - } + const accountsToImport = recoveryWalletInfos.map((addressInfo, index): SignerMnemonicAccount => { + return { + type: AccountType.SignerMnemonic, + mnemonicId, + name: t('onboarding.wallet.defaultName', { number: index + 1 }), + address: addressInfo.address, + derivationIndex: addressInfo.derivationIndex, + timeImportedMs: dayjs().valueOf(), } - ) + }) setRecoveredImportedAccounts(accountsToImport) }, - [setRecoveredImportedAccounts] + [setRecoveredImportedAccounts], ) const finishRecovery = useCallback( @@ -101,7 +96,7 @@ function useFinishAutomatedRecovery(navigation: Props['navigation']): { entryPoint: OnboardingEntryPoint.FreshInstallOrReplace, }) } else { - await finishOnboarding(ImportType.OnDeviceRecovery) + await finishOnboarding({ importType: ImportType.OnDeviceRecovery }) dispatch(setFinishedOnboarding({ finishedOnboarding: true })) } }, @@ -114,7 +109,7 @@ function useFinishAutomatedRecovery(navigation: Props['navigation']): { isBiometricAuthEnabled, navigation, notificationOSPermission, - ] + ], ) return { @@ -125,15 +120,18 @@ function useFinishAutomatedRecovery(navigation: Props['navigation']): { const FALLBACK_APP_LOADING_TIMEOUT_MS = 15000 export function AppLoadingScreen({ navigation }: Props): JSX.Element | null { - const dispatch = useAppDispatch() + const dispatch = useDispatch() - const onDeviceRecoveryConfig = useDynamicConfig(DynamicConfigs.OnDeviceRecovery) - const appLoadingTimeoutMs = onDeviceRecoveryConfig.get( - 'appLoadingTimeoutMs', + const appLoadingTimeoutMs = useDynamicConfigValue( + DynamicConfigs.OnDeviceRecovery, + OnDeviceRecoveryConfigKey.AppLoadingTimeoutMs, FALLBACK_APP_LOADING_TIMEOUT_MS, - isNumber ) - const maxMnemonicsToLoad = onDeviceRecoveryConfig.getValue('maxMnemonicsToLoad', 20) as number + const maxMnemonicsToLoad = useDynamicConfigValue( + DynamicConfigs.OnDeviceRecovery, + OnDeviceRecoveryConfigKey.MaxMnemonicsToLoad, + 20, + ) const [finished, setFinished] = useState(false) @@ -164,11 +162,7 @@ export function AppLoadingScreen({ navigation }: Props): JSX.Element | null { if (!finished) { setFinished(true) navigateToLanding() - logger.warn( - 'AppLoadingScreen', - 'useTimeout', - `Loading timeout triggered after ${appLoadingTimeoutMs}ms` - ) + logger.warn('AppLoadingScreen', 'useTimeout', `Loading timeout triggered after ${appLoadingTimeoutMs}ms`) } }, appLoadingTimeoutMs) return () => clearTimeout(timeout) diff --git a/apps/mobile/src/screens/DevScreen.tsx b/apps/mobile/src/screens/DevScreen.tsx index 2e19efebdb3..190e1e8102c 100644 --- a/apps/mobile/src/screens/DevScreen.tsx +++ b/apps/mobile/src/screens/DevScreen.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react' import { I18nManager, ScrollView } from 'react-native' -import { useAppDispatch } from 'src/app/hooks' +import { useDispatch } from 'react-redux' import { navigate } from 'src/app/navigation/rootNavigation' import { BackButton } from 'src/components/buttons/BackButton' import { Screen } from 'src/components/layout/Screen' @@ -22,7 +22,7 @@ import { useAppSelector } from 'wallet/src/state' export function DevScreen(): JSX.Element { const insets = useDeviceInsets() - const dispatch = useAppDispatch() + const dispatch = useDispatch() const activeAccount = useActiveAccount() const [rtlEnabled, setRTLEnabled] = useState(I18nManager.isRTL) const sortedMnemonicAccounts = useAppSelector(selectSortedSignerMnemonicAccounts) @@ -35,7 +35,7 @@ export function DevScreen(): JSX.Element { dispatch( createAccountsActions.trigger({ accounts: [await createOnboardingAccount(sortedMnemonicAccounts)], - }) + }), ) } @@ -46,11 +46,7 @@ export function DevScreen(): JSX.Element { const onPressShowError = (): void => { const address = activeAccount?.address if (!address) { - logger.debug( - 'DevScreen', - 'onPressShowError', - 'Cannot show error if activeAccount is undefined' - ) + logger.debug('DevScreen', 'onPressShowError', 'Cannot show error if activeAccount is undefined') return } @@ -59,7 +55,7 @@ export function DevScreen(): JSX.Element { type: AppNotificationType.Error, address, errorMessage: 'A scary new error has happened. Be afraid!!', - }) + }), ) } @@ -92,11 +88,7 @@ export function DevScreen(): JSX.Element { {Object.values(MobileScreens).map((s) => ( - activateWormhole(s)}> + activateWormhole(s)}> {s} ))} diff --git a/apps/mobile/src/screens/EducationScreen.tsx b/apps/mobile/src/screens/EducationScreen.tsx index 391c93650ac..ff1163a905c 100644 --- a/apps/mobile/src/screens/EducationScreen.tsx +++ b/apps/mobile/src/screens/EducationScreen.tsx @@ -17,7 +17,7 @@ export function EducationScreen({ importType, entryPoint, }), - [entryPoint, importType, type] + [entryPoint, importType, type], ) return ( diff --git a/apps/mobile/src/screens/ExchangeTransferConnecting.tsx b/apps/mobile/src/screens/ExchangeTransferConnecting.tsx index 72e21e439d4..ab3cb778675 100644 --- a/apps/mobile/src/screens/ExchangeTransferConnecting.tsx +++ b/apps/mobile/src/screens/ExchangeTransferConnecting.tsx @@ -1,25 +1,26 @@ import { useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -import { useAppDispatch } from 'src/app/hooks' +import { useDispatch } from 'react-redux' import { Screen } from 'src/components/layout/Screen' -import { FiatOnRampConnectingView } from 'src/features/fiatOnRamp/FiatOnRampConnecting' -import { ServiceProviderLogoStyles } from 'src/features/fiatOnRamp/constants' import { useFiatOnRampTransactionCreator } from 'src/features/fiatOnRamp/hooks' import { Flex, useIsDarkMode } from 'ui/src' import { uniswapUrls } from 'uniswap/src/constants/urls' +import { FiatOnRampConnectingView } from 'uniswap/src/features/fiatOnRamp/FiatOnRampConnectingView' import { useFiatOnRampAggregatorTransferWidgetQuery } from 'uniswap/src/features/fiatOnRamp/api' +import { ServiceProviderLogoStyles } from 'uniswap/src/features/fiatOnRamp/constants' import { FORServiceProvider } from 'uniswap/src/features/fiatOnRamp/types' +import { getServiceProviderLogo } from 'uniswap/src/features/fiatOnRamp/utils' import { InstitutionTransferEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { UniverseChainId } from 'uniswap/src/types/chains' +import { openUri } from 'uniswap/src/utils/linking' import { ONE_SECOND_MS } from 'utilities/src/time/time' import { useTimeout } from 'utilities/src/time/timing' -import { getServiceProviderLogo } from 'wallet/src/features/fiatOnRamp/utils' import { ImageUri } from 'wallet/src/features/images/ImageUri' import { pushNotification } from 'wallet/src/features/notifications/slice' import { AppNotificationType } from 'wallet/src/features/notifications/types' +import { FiatPurchaseTransactionInfo } from 'wallet/src/features/transactions/types' import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' -import { openUri } from 'wallet/src/utils/linking' // Design decision const CONNECTING_TIMEOUT = 2 * ONE_SECOND_MS @@ -32,19 +33,20 @@ export function ExchangeTransferConnecting({ onClose: () => void }): JSX.Element { const { t } = useTranslation() - const dispatch = useAppDispatch() + const dispatch = useDispatch() const activeAccountAddress = useActiveAccountAddressWithThrow() const [timeoutElapsed, setTimeoutElapsed] = useState(false) - const initialTypeInfo = useMemo( - () => ({ serviceProviderLogo: serviceProvider.logos }), - [serviceProvider.logos] + const initialTypeInfo = useMemo>( + () => ({ serviceProviderLogo: serviceProvider.logos, serviceProvider: serviceProvider.serviceProvider }), + [serviceProvider], ) const { externalTransactionId, dispatchAddTransaction } = useFiatOnRampTransactionCreator( activeAccountAddress, UniverseChainId.Mainnet, - initialTypeInfo + serviceProvider.serviceProvider, + initialTypeInfo, ) const onError = useCallback((): void => { @@ -52,7 +54,7 @@ export function ExchangeTransferConnecting({ pushNotification({ type: AppNotificationType.Error, errorMessage: t('common.error.general'), - }) + }), ) onClose() }, [dispatch, onClose, t]) @@ -113,7 +115,8 @@ export function ExchangeTransferConnecting({ alignItems="center" height={ServiceProviderLogoStyles.icon.height} justifyContent="center" - width={ServiceProviderLogoStyles.icon.width}> + width={ServiceProviderLogoStyles.icon.width} + > } diff --git a/apps/mobile/src/screens/ExploreScreen.tsx b/apps/mobile/src/screens/ExploreScreen.tsx index 6481e3fd4f6..e1caeb84407 100644 --- a/apps/mobile/src/screens/ExploreScreen.tsx +++ b/apps/mobile/src/screens/ExploreScreen.tsx @@ -2,7 +2,7 @@ import { useScrollToTop } from '@react-navigation/native' import { SharedEventName } from '@uniswap/analytics-events' import React, { useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' -import { KeyboardAvoidingView, TextInput } from 'react-native' +import { Keyboard, KeyboardAvoidingView, TextInput } from 'react-native' import { FadeIn, FadeOut } from 'react-native-reanimated' import { useAppSelector } from 'src/app/hooks' import { useExploreStackNavigation } from 'src/app/navigation/types' @@ -11,17 +11,16 @@ import { SearchEmptySection } from 'src/components/explore/search/SearchEmptySec import { SearchResultsSection } from 'src/components/explore/search/SearchResultsSection' import { Screen } from 'src/components/layout/Screen' import { VirtualizedList } from 'src/components/layout/VirtualizedList' -import { useReduxModalBackHandler } from 'src/features/modals/hooks' import { selectModalState } from 'src/features/modals/selectModalState' import { ColorTokens, Flex, flexStyles, useIsDarkMode } from 'ui/src' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { useBottomSheetContext } from 'uniswap/src/components/modals/BottomSheetContext' import { HandleBar } from 'uniswap/src/components/modals/HandleBar' +import { SearchTextInput } from 'uniswap/src/features/search/SearchTextInput' import { ModalName, SectionName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { MobileScreens } from 'uniswap/src/types/screens/mobile' import { useDebounce } from 'utilities/src/time/timing' -import { SearchTextInput } from 'wallet/src/features/search/SearchTextInput' export function ExploreScreen(): JSX.Element { const modalInitialState = useAppSelector(selectModalState(ModalName.Explore)).initialState @@ -29,8 +28,6 @@ export function ExploreScreen(): JSX.Element { const { isSheetReady } = useBottomSheetContext() - useReduxModalBackHandler(ModalName.Explore) - // The ExploreStack is not directly accessible from outside // (e.g., navigating from Home to NFTItem within ExploreStack), due to its mount within BottomSheetModal. // To bypass this limitation, we use an initialState to define a specific screen within ExploreStack. @@ -86,12 +83,13 @@ export function ExploreScreen(): JSX.Element { showShadow={!isSearchMode} onCancel={onSearchCancel} onChangeText={onSearchChangeText} + onDismiss={() => Keyboard.dismiss()} onFocus={onSearchFocus} /> {isSearchMode ? ( - + {debouncedSearchQuery.length === 0 ? ( diff --git a/apps/mobile/src/screens/ExternalProfileScreen.tsx b/apps/mobile/src/screens/ExternalProfileScreen.tsx index 9e1cbf201ed..9c34dcfd34d 100644 --- a/apps/mobile/src/screens/ExternalProfileScreen.tsx +++ b/apps/mobile/src/screens/ExternalProfileScreen.tsx @@ -43,7 +43,7 @@ export function ExternalProfileScreen({ { key: SectionName.ProfileNftsTab, title: t('home.nfts.title') }, { key: SectionName.ProfileActivityTab, title: t('home.activity.title') }, ], - [t] + [t], ) const containerStyle = useMemo>( @@ -51,7 +51,7 @@ export function ExternalProfileScreen({ ...TAB_STYLES.tabListInner, paddingBottom: insets.bottom + TAB_STYLES.tabListInner.paddingBottom, }), - [insets.bottom] + [insets.bottom], ) const emptyComponentStyle = useMemo>( @@ -60,7 +60,7 @@ export function ExternalProfileScreen({ paddingHorizontal: spacing.spacing36, paddingBottom: insets.bottom, }), - [insets.bottom] + [insets.bottom], ) const sharedProps = useMemo( @@ -69,7 +69,7 @@ export function ExternalProfileScreen({ loadingContainerStyle: containerStyle, emptyComponentStyle, }), - [containerStyle, emptyComponentStyle] + [containerStyle, emptyComponentStyle], ) const renderTab = useCallback( @@ -93,12 +93,7 @@ export function ExternalProfileScreen({ ) case SectionName.ProfileNftsTab: return ( - + ) case SectionName.ProfileTokensTab: return ( @@ -112,7 +107,7 @@ export function ExternalProfileScreen({ } return null }, - [address, sharedProps, renderedInModal] + [address, sharedProps, renderedInModal], ) const renderTabBar = useCallback( @@ -138,7 +133,7 @@ export function ExternalProfileScreen({ /> ) }, - [colors.surface1, colors.surface3, tabIndex, tabs] + [colors.surface1, colors.surface3, tabIndex, tabs], ) const traceProperties = useMemo( @@ -147,17 +142,13 @@ export function ExternalProfileScreen({ walletName: displayName?.name, displayNameType: displayName?.type ? DisplayNameType[displayName.type] : undefined, }), - [address, displayName?.name, displayName?.type] + [address, displayName?.name, displayName?.type], ) return ( - + ({ serviceProviderLogo: serviceProvider?.logos }), - [serviceProvider?.logos] + const initialTypeInfo = useMemo>( + () => ({ + serviceProviderLogo: serviceProvider?.logos, + serviceProvider: serviceProvider?.serviceProvider, + }), + [serviceProvider], ) const { externalTransactionId, dispatchAddTransaction } = useFiatOnRampTransactionCreator( activeAccountAddress, quoteCurrency.currencyInfo?.currency.chainId ?? UniverseChainId.Mainnet, - initialTypeInfo + serviceProvider?.serviceProvider, + initialTypeInfo, ) const onError = useCallback((): void => { @@ -72,7 +66,7 @@ export function FiatOnRampConnectingScreen({ navigation }: Props): JSX.Element | pushNotification({ type: AppNotificationType.Error, errorMessage: t('common.error.general'), - }) + }), ) navigation.goBack() }, [dispatch, navigation, t]) @@ -93,7 +87,7 @@ export function FiatOnRampConnectingScreen({ navigation }: Props): JSX.Element | externalSessionId: externalTransactionId, redirectUrl: `${uniswapUrls.redirectUrlBase}/?screen=transaction&fiatOnRamp=true&userAddress=${activeAccountAddress}`, } - : skipToken + : skipToken, ) useTimeout(() => { @@ -107,16 +101,11 @@ export function FiatOnRampConnectingScreen({ navigation }: Props): JSX.Element | } async function navigateToWidget(widgetUrl: string): Promise { dispatch(closeModal({ name: ModalName.FiatOnRampAggregator })) - if ( - serviceProvider && - quoteCurrency?.meldCurrencyCode && - baseCurrencyInfo && - quotesSections?.[0]?.data?.[0] - ) { + if (serviceProvider && quoteCurrency?.meldCurrencyCode && baseCurrencyInfo && quotesSections?.[0]?.data?.[0]) { sendAnalyticsEvent(FiatOnRampEventName.FiatOnRampWidgetOpened, { externalTransactionId, serviceProvider: serviceProvider.serviceProvider, - preselectedServiceProvider: quotesSections?.[0]?.data?.[0]?.serviceProvider, + preselectedServiceProvider: quotesSections?.[0]?.data?.[0]?.serviceProviderDetails.serviceProvider, countryCode, countryState, fiatCurrency: baseCurrencyInfo?.code.toLowerCase(), @@ -168,7 +157,8 @@ export function FiatOnRampConnectingScreen({ navigation }: Props): JSX.Element | alignItems="center" height={ServiceProviderLogoStyles.icon.height} justifyContent="center" - width={ServiceProviderLogoStyles.icon.width}> + width={ServiceProviderLogoStyles.icon.width} + > } @@ -180,7 +170,8 @@ export function FiatOnRampConnectingScreen({ navigation }: Props): JSX.Element | position="absolute" px="$spacing24" textAlign="center" - variant="body3"> + variant="body3" + > {t('fiatOnRamp.connection.terms', { serviceProvider: serviceProvider.name })} diff --git a/apps/mobile/src/screens/FiatOnRampScreen.tsx b/apps/mobile/src/screens/FiatOnRampScreen.tsx index 5ed695dbf09..2b32e658140 100644 --- a/apps/mobile/src/screens/FiatOnRampScreen.tsx +++ b/apps/mobile/src/screens/FiatOnRampScreen.tsx @@ -4,7 +4,8 @@ import { useTranslation } from 'react-i18next' import { TextInput, TextInputProps } from 'react-native' import FastImage from 'react-native-fast-image' import { FadeIn, FadeOut, FadeOutDown } from 'react-native-reanimated' -import { useAppDispatch, useShouldShowNativeKeyboard } from 'src/app/hooks' +import { useDispatch } from 'react-redux' +import { useShouldShowNativeKeyboard } from 'src/app/hooks' import { FiatOnRampStackParamList } from 'src/app/navigation/types' import { FiatOnRampCtaButton } from 'src/components/fiatOnRamp/CtaButton' import { Screen } from 'src/components/layout/Screen' @@ -14,26 +15,23 @@ import { FiatOnRampCountryListModal } from 'src/features/fiatOnRamp/FiatOnRampCo import { FiatOnRampTokenSelectorModal } from 'src/features/fiatOnRamp/FiatOnRampTokenSelector' import { useFiatOnRampQuotes, + useFiatOnRampSupportedTokens, useMeldFiatCurrencySupportInfo, useParseFiatOnRampError, -} from 'src/features/fiatOnRamp/aggregatorHooks' -import { useFiatOnRampSupportedTokens } from 'src/features/fiatOnRamp/hooks' +} from 'src/features/fiatOnRamp/hooks' import { Flex, Text, useIsDarkMode } from 'ui/src' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { useBottomSheetContext } from 'uniswap/src/components/modals/BottomSheetContext' import { HandleBar } from 'uniswap/src/components/modals/HandleBar' import { FiatOnRampCountryPicker } from 'uniswap/src/features/fiatOnRamp/FiatOnRampCountryPicker' -import { - useFiatOnRampAggregatorGetCountryQuery, - useFiatOnRampAggregatorServiceProvidersQuery, -} from 'uniswap/src/features/fiatOnRamp/api' +import { useFiatOnRampAggregatorGetCountryQuery } from 'uniswap/src/features/fiatOnRamp/api' import { FORQuote, FORServiceProvider, - FORTransaction, FiatOnRampCurrency, InitialQuoteSelection, } from 'uniswap/src/features/fiatOnRamp/types' +import { getServiceProviderLogo } from 'uniswap/src/features/fiatOnRamp/utils' import { FiatOnRampEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { UniverseEventProperties } from 'uniswap/src/features/telemetry/types' @@ -42,27 +40,23 @@ import { usePrevious } from 'utilities/src/react/hooks' import { DEFAULT_DELAY, useDebounce } from 'utilities/src/time/timing' import { DecimalPadLegacy } from 'wallet/src/components/legacy/DecimalPadLegacy' import { useLocalFiatToUSDConverter } from 'wallet/src/features/fiatCurrency/hooks' -import { useFiatOnRampAggregatorTransactionQuery } from 'wallet/src/features/fiatOnRamp/api' -import { getServiceProviderLogo } from 'wallet/src/features/fiatOnRamp/utils' import { pushNotification } from 'wallet/src/features/notifications/slice' import { AppNotificationType } from 'wallet/src/features/notifications/types' type Props = NativeStackScreenProps -function selectInitialQuote( - quotes: FORQuote[] | undefined, - lastTransaction: FORTransaction | undefined -): { quote: FORQuote | undefined; type: InitialQuoteSelection | undefined } { - const lastUsedServiceProvider = lastTransaction?.serviceProvider - if (lastUsedServiceProvider) { - const quote = quotes?.filter((q) => q.serviceProvider === lastUsedServiceProvider)[0] - if (quote) { - return { - quote, - type: InitialQuoteSelection.MostRecent, - } +function selectInitialQuote(quotes: FORQuote[] | undefined): { + quote: FORQuote | undefined + type: InitialQuoteSelection | undefined +} { + const quoteFromLastUsedProvider = quotes?.find((q) => q.isMostRecentlyUsedProvider) + if (quoteFromLastUsedProvider) { + return { + quote: quoteFromLastUsedProvider, + type: InitialQuoteSelection.MostRecent, } } + const bestQuote = quotes && quotes.length && quotes[0] if (bestQuote) { return { @@ -75,14 +69,9 @@ function selectInitialQuote( return { quote: undefined, type: undefined } } -function preloadServiceProviderLogos( - serviceProviders: FORServiceProvider[], - isDarkMode: boolean -): void { +function preloadServiceProviderLogos(serviceProviders: FORServiceProvider[], isDarkMode: boolean): void { FastImage.preload( - serviceProviders - .map((sp) => ({ uri: getServiceProviderLogo(sp.logos, isDarkMode) })) - .filter((sp) => !!sp.uri) + serviceProviders.map((sp) => ({ uri: getServiceProviderLogo(sp.logos, isDarkMode) })).filter((sp) => !!sp.uri), ) } @@ -90,7 +79,7 @@ const PREDEFINED_AMOUNTS_SUPPORTED_CURRENCIES = ['usd', 'eur', 'gbp', 'aud', 'ca export function FiatOnRampScreen({ navigation }: Props): JSX.Element { const { t } = useTranslation() - const dispatch = useAppDispatch() + const dispatch = useDispatch() const isDarkMode = useIsDarkMode() const [selection, setSelection] = useState() const [value, setValue] = useState('') @@ -111,7 +100,6 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element { amount, setAmount, setBaseCurrencyInfo, - setServiceProviders, quoteCurrency, setQuoteCurrency, } = useFiatOnRampContext() @@ -120,8 +108,7 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element { setSelection({ start, end: end ?? start }) } - const { showNativeKeyboard, onDecimalPadLayout, isLayoutPending, onInputPanelLayout } = - useShouldShowNativeKeyboard() + const { showNativeKeyboard, onDecimalPadLayout, isLayoutPending, onInputPanelLayout } = useShouldShowNativeKeyboard() const { appFiatCurrencySupportedInMeld, meldSupportedFiatCurrency, supportedFiatCurrencies } = useMeldFiatCurrencySupportInfo(countryCode) @@ -150,69 +137,47 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element { } }, [ipCountryData, setCountryCode, setCountryState]) - const { - currentData: serviceProvidersResponse, - isFetching: serviceProvidersLoading, - error: serviceProvidersError, - } = useFiatOnRampAggregatorServiceProvidersQuery({ countryCode }) - // preload service provider logos for given quotes for the next screen useEffect(() => { - if (serviceProvidersResponse?.serviceProviders && quotes) { - const quotesServiceProviderNames = quotes.map((q) => q.serviceProvider) - const serviceProviders = serviceProvidersResponse.serviceProviders.filter( - (sp) => quotesServiceProviderNames.indexOf(sp.serviceProvider) !== -1 + if (quotes) { + preloadServiceProviderLogos( + quotes.map((q) => q.serviceProviderDetails), + isDarkMode, ) - preloadServiceProviderLogos(serviceProviders, isDarkMode) } - }, [serviceProvidersResponse, quotes, isDarkMode]) - - const { currentData: transactionResponse } = useFiatOnRampAggregatorTransactionQuery({}) + }, [quotes, isDarkMode]) const prevQuotes = usePrevious(quotes) useEffect(() => { if (quotes && (!selectedQuote || prevQuotes !== quotes)) { - const { quote, type } = selectInitialQuote(quotes, transactionResponse?.transaction) + const { quote, type } = selectInitialQuote(quotes) if (!quote) { return } if (type === InitialQuoteSelection.MostRecent) { const otherQuotes = quotes.filter((item) => item !== quote) - setQuotesSections([ - { data: [quote], type }, - ...(otherQuotes.length ? [{ data: otherQuotes }] : []), - ]) + setQuotesSections([{ data: [quote], type }, ...(otherQuotes.length ? [{ data: otherQuotes }] : [])]) } else { setQuotesSections([{ data: quotes, type }]) } setSelectedQuote(quote) } - }, [ - prevQuotes, - quotes, - selectedQuote, - setQuotesSections, - setSelectedQuote, - t, - transactionResponse, - ]) + }, [prevQuotes, quotes, selectedQuote, setQuotesSections, setSelectedQuote, t]) useEffect(() => { - if (!quotes && (quotesError || serviceProvidersError || !amount)) { + if (!quotes && (quotesError || !amount)) { setQuotesSections(undefined) setSelectedQuote(undefined) } - }, [amount, quotesError, serviceProvidersError, quotes, setQuotesSections, setSelectedQuote]) + }, [amount, quotesError, quotes, setQuotesSections, setSelectedQuote]) - const onSelectCountry: ComponentProps['onSelectCountry'] = ( - country - ): void => { + const onSelectCountry: ComponentProps['onSelectCountry'] = (country): void => { dispatch( pushNotification({ type: AppNotificationType.ChooseCountry, countryName: country.displayName, countryCode: country.countryCode, - }) + }), ) setSelectingCountry(false) // UI does not allow to set the state @@ -243,22 +208,11 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element { }, [showNativeKeyboard, showTokenSelector]) // we only show loading when there are no errors and quote value is not empty - const buttonDisabled = - serviceProvidersLoading || - !!serviceProvidersError || - selectTokenLoading || - !!quotesError || - !selectedQuote?.destinationAmount + const buttonDisabled = selectTokenLoading || !!quotesError || !selectedQuote?.destinationAmount const onContinue = (): void => { - if ( - quotes && - serviceProvidersResponse?.serviceProviders && - serviceProvidersResponse?.serviceProviders.length > 0 && - quoteCurrency?.currencyInfo?.currency - ) { + if (quotes && quoteCurrency?.currencyInfo?.currency) { setBaseCurrencyInfo(meldSupportedFiatCurrency) - setServiceProviders(serviceProvidersResponse.serviceProviders) navigation.navigate(FiatOnRampScreens.ServiceProviders) } } @@ -285,14 +239,14 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element { // We only support predefined amounts for certain currencies. const predefinedAmountsSupported = PREDEFINED_AMOUNTS_SUPPORTED_CURRENCIES.includes( - meldSupportedFiatCurrency.code.toLowerCase() + meldSupportedFiatCurrency.code.toLowerCase(), ) const notAvailableInThisRegion = supportedFiatCurrencies?.length === 0 const { errorText, errorColor } = useParseFiatOnRampError( - !notAvailableInThisRegion && (quotesError || serviceProvidersError), - meldSupportedFiatCurrency.code + !notAvailableInThisRegion && quotesError, + meldSupportedFiatCurrency.code, ) return ( @@ -300,12 +254,7 @@ export function FiatOnRampScreen({ navigation }: Props): JSX.Element { {isSheetReady && ( - + {t('common.button.buy')} + onLayout={onDecimalPadLayout} + > {!showNativeKeyboard && ( -const key = (item: FORQuote): string => item.serviceProvider +const key = (item: FORQuote): string => item.serviceProviderDetails.serviceProvider function SectionHeader({ Icon, @@ -55,12 +54,11 @@ function Footer(): JSX.Element { export function FiatOnRampServiceProvidersScreen({ navigation }: Props): JSX.Element { const { t } = useTranslation() - const { setSelectedQuote, quotesSections, baseCurrencyInfo, serviceProviders } = - useFiatOnRampContext() + const { setSelectedQuote, quotesSections, baseCurrencyInfo } = useFiatOnRampContext() const renderItem = ({ item }: ListRenderItemInfo): JSX.Element => { const onPress = (): void => { - const serviceProvider = getServiceProviderForQuote(item, serviceProviders) + const serviceProvider = item.serviceProviderDetails if (serviceProvider) { setSelectedQuote(item) navigation.navigate(FiatOnRampScreens.Connecting) @@ -68,12 +66,7 @@ export function FiatOnRampServiceProvidersScreen({ navigation }: Props): JSX.Ele } return ( - {baseCurrencyInfo && ( - - )} + {baseCurrencyInfo && } ) } @@ -85,11 +78,7 @@ export function FiatOnRampServiceProvidersScreen({ navigation }: Props): JSX.Ele }): JSX.Element => ( {type === InitialQuoteSelection.Best ? null : type === InitialQuoteSelection.MostRecent ? ( - + ) : ( @@ -106,13 +95,7 @@ export function FiatOnRampServiceProvidersScreen({ navigation }: Props): JSX.Ele - + {t('fiatOnRamp.checkout.title')} diff --git a/apps/mobile/src/screens/HomeScreen.tsx b/apps/mobile/src/screens/HomeScreen.tsx index 266e2fc37ec..bd488d774ad 100644 --- a/apps/mobile/src/screens/HomeScreen.tsx +++ b/apps/mobile/src/screens/HomeScreen.tsx @@ -18,7 +18,8 @@ import Animated, { } from 'react-native-reanimated' import { SvgProps } from 'react-native-svg' import { SceneRendererProps, TabBar } from 'react-native-tab-view' -import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { useDispatch } from 'react-redux' +import { useAppSelector } from 'src/app/hooks' import { ExtensionPromoModal } from 'src/app/modals/ExtensionPromoModal' import { NavBar, SWAP_BUTTON_HEIGHT } from 'src/app/navigation/NavBar' import { AppStackScreenProp } from 'src/app/navigation/types' @@ -43,20 +44,11 @@ import { import { UnitagBanner } from 'src/components/unitags/UnitagBanner' import { openModal } from 'src/features/modals/modalSlice' import { selectSomeModalOpen } from 'src/features/modals/selectSomeModalOpen' -import { useHeartbeatReporter, useLastBalancesReporter } from 'src/features/telemetry/hooks' import { useWalletRestore } from 'src/features/wallet/hooks' import { removePendingSession } from 'src/features/walletConnect/walletConnectSlice' import { HomeScreenTabIndex } from 'src/screens/HomeScreenTabIndex' import { hideSplashScreen } from 'src/utils/splashScreen' -import { - Flex, - HapticFeedback, - Text, - TouchableArea, - useDeviceInsets, - useMedia, - useSporeColors, -} from 'ui/src' +import { Flex, HapticFeedback, Text, TouchableArea, useDeviceInsets, useMedia, useSporeColors } from 'ui/src' import ReceiveIcon from 'ui/src/assets/icons/arrow-down-circle.svg' import BuyIcon from 'ui/src/assets/icons/buy.svg' import ScanIcon from 'ui/src/assets/icons/scan-home.svg' @@ -64,6 +56,7 @@ import SendIcon from 'ui/src/assets/icons/send-action.svg' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { useDeviceDimensions } from 'ui/src/hooks/useDeviceDimensions' import { iconSizes, spacing } from 'ui/src/theme' +import { useCexTransferProviders } from 'uniswap/src/features/fiatOnRamp/useCexTransferProviders' import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import Trace from 'uniswap/src/features/telemetry/Trace' @@ -75,20 +68,17 @@ import { SectionName, SectionNameType, } from 'uniswap/src/features/telemetry/constants' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { MobileScreens } from 'uniswap/src/types/screens/mobile' -import { ONE_SECOND_MS } from 'utilities/src/time/time' -import { useInterval, useTimeout } from 'utilities/src/time/timing' +import { useTimeout } from 'utilities/src/time/timing' import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants' import { selectHasSkippedUnitagPrompt } from 'wallet/src/features/behaviorHistory/selectors' -import { useCexTransferProviders } from 'wallet/src/features/fiatOnRamp/api' import { useSelectAddressHasNotifications } from 'wallet/src/features/notifications/hooks' import { setNotificationStatus } from 'wallet/src/features/notifications/slice' import { PortfolioBalance } from 'wallet/src/features/portfolio/PortfolioBalance' import { TokenBalanceListRow } from 'wallet/src/features/portfolio/TokenBalanceListContext' -import { - useCanActiveAddressClaimUnitag, - useShowExtensionPromoBanner, -} from 'wallet/src/features/unitags/hooks' +import { useHeartbeatReporter, useLastBalancesReporter } from 'wallet/src/features/telemetry/hooks' +import { useCanActiveAddressClaimUnitag, useShowExtensionPromoBanner } from 'wallet/src/features/unitags/hooks' import { AccountType } from 'wallet/src/features/wallet/accounts/types' import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks' @@ -106,7 +96,7 @@ export function HomeScreen(props?: AppStackScreenProp): JSX. const media = useMedia() const insets = useDeviceInsets() const dimensions = useDeviceDimensions() - const dispatch = useAppDispatch() + const dispatch = useDispatch() const isFocused = useIsFocused() const isModalOpen = useAppSelector(selectSomeModalOpen) const isHomeScreenBlur = !isFocused || isModalOpen @@ -118,12 +108,9 @@ export function HomeScreen(props?: AppStackScreenProp): JSX. useWalletRestore({ openModalImmediately: true }) // Record a heartbeat for anonymous user DAU - const heartbeatReporter = useHeartbeatReporter() - useInterval(heartbeatReporter, ONE_SECOND_MS * 15, true) - + useHeartbeatReporter() // Report balances at most every 24 hours, checking every 15 seconds when app is open - const lastBalancesReporter = useLastBalancesReporter() - useInterval(lastBalancesReporter, ONE_SECOND_MS * 15, true) + useLastBalancesReporter() const [tabIndex, setTabIndex] = useState(props?.route?.params?.tab ?? HomeScreenTabIndex.Tokens) // Necessary to declare these as direct dependencies due to race condition with initializing react-i18next and useMemo @@ -154,7 +141,7 @@ export function HomeScreen(props?: AppStackScreenProp): JSX. } setTabIndex(newTabIndex) }, - [props?.route.params?.tab] + [props?.route.params?.tab], ) const [isLayoutReady, setIsLayoutReady] = useState(false) @@ -165,7 +152,7 @@ export function HomeScreen(props?: AppStackScreenProp): JSX. heightCollapsed: insets.top, heightExpanded: headerHeight, }), - [headerHeight, insets.top] + [headerHeight, insets.top], ) const { heightCollapsed, heightExpanded } = headerConfig const headerHeightDiff = heightExpanded - heightCollapsed @@ -177,20 +164,16 @@ export function HomeScreen(props?: AppStackScreenProp): JSX. const tokensTabScrollValue = useSharedValue(0) const tokensTabScrollHandler = useAnimatedScrollHandler( - (event) => (tokensTabScrollValue.value = event.contentOffset.y) + (event) => (tokensTabScrollValue.value = event.contentOffset.y), ) const nftsTabScrollValue = useSharedValue(0) - const nftsTabScrollHandler = useAnimatedScrollHandler( - (event) => (nftsTabScrollValue.value = event.contentOffset.y) - ) + const nftsTabScrollHandler = useAnimatedScrollHandler((event) => (nftsTabScrollValue.value = event.contentOffset.y)) const activityTabScrollValue = useSharedValue(0) const activityTabScrollHandler = useAnimatedScrollHandler( - (event) => (activityTabScrollValue.value = event.contentOffset.y) + (event) => (activityTabScrollValue.value = event.contentOffset.y), ) const feedTabScrollValue = useSharedValue(0) - const feedTabScrollHandler = useAnimatedScrollHandler( - (event) => (feedTabScrollValue.value = event.contentOffset.y) - ) + const feedTabScrollHandler = useAnimatedScrollHandler((event) => (feedTabScrollValue.value = event.contentOffset.y)) const tokensTabScrollRef = useAnimatedRef>() // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -209,13 +192,7 @@ export function HomeScreen(props?: AppStackScreenProp): JSX. return activityTabScrollValue.value } return feedTabScrollValue.value - }, [ - activityTabScrollValue, - feedTabScrollValue, - nftsTabScrollValue, - tabIndex, - tokensTabScrollValue, - ]) + }, [activityTabScrollValue, feedTabScrollValue, nftsTabScrollValue, tabIndex, tokensTabScrollValue]) // clear the notification indicator if the user is on the activity tab const hasNotifications = useSelectAddressHasNotifications(activeAccount.address) @@ -259,10 +236,7 @@ export function HomeScreen(props?: AppStackScreenProp): JSX. setTabIndex(HomeScreenTabIndex.Tokens) } else if (currentTabIndex.value === HomeScreenTabIndex.NFTs) { nftsTabScrollRef.current?.scrollToOffset({ offset: 0, animated: true }) - } else if ( - currentTabIndex.value === HomeScreenTabIndex.Activity && - isActivityTabAtTop.value - ) { + } else if (currentTabIndex.value === HomeScreenTabIndex.Activity && isActivityTabAtTop.value) { setTabIndex(HomeScreenTabIndex.NFTs) } else if (currentTabIndex.value === HomeScreenTabIndex.Activity) { activityTabScrollRef.current?.scrollToOffset({ offset: 0, animated: true }) @@ -270,7 +244,7 @@ export function HomeScreen(props?: AppStackScreenProp): JSX. tokensTabScrollRef.current?.scrollToOffset({ offset: 0, animated: true }) } }, - }) + }), ) const translateY = useDerivedValue(() => { @@ -298,30 +272,19 @@ export function HomeScreen(props?: AppStackScreenProp): JSX. nftsTabScrollValue, tokensTabScrollRef, tokensTabScrollValue, - ] + ], ) const { sync } = useScrollSync(currentTabIndex, scrollPairs, headerConfig) - const forAggregatorEnabled = useFeatureFlag(FeatureFlags.ForAggregator) const cexTransferEnabled = useFeatureFlag(FeatureFlags.CexTransfers) const cexTransferProviders = useCexTransferProviders(cexTransferEnabled) - const onPressBuy = useCallback( - () => - dispatch( - openModal({ - name: forAggregatorEnabled ? ModalName.FiatOnRampAggregator : ModalName.FiatOnRamp, - }) - ), - [dispatch, forAggregatorEnabled] - ) + const onPressBuy = useCallback(() => dispatch(openModal({ name: ModalName.FiatOnRampAggregator })), [dispatch]) const onPressScan = useCallback(() => { // in case we received a pending session from a previous scan after closing modal dispatch(removePendingSession()) - dispatch( - openModal({ name: ModalName.WalletConnectScan, initialState: ScannerModalState.ScanQr }) - ) + dispatch(openModal({ name: ModalName.WalletConnectScan, initialState: ScannerModalState.ScanQr })) }, [dispatch]) const onPressSend = useCallback(() => dispatch(openModal({ name: ModalName.Send })), [dispatch]) const onPressReceive = useCallback(() => { @@ -329,14 +292,11 @@ export function HomeScreen(props?: AppStackScreenProp): JSX. openModal( cexTransferProviders.length > 0 ? { name: ModalName.ReceiveCryptoModal, initialState: cexTransferProviders } - : { name: ModalName.WalletConnectScan, initialState: ScannerModalState.WalletQr } - ) + : { name: ModalName.WalletConnectScan, initialState: ScannerModalState.WalletQr }, + ), ) }, [dispatch, cexTransferProviders]) - const onPressViewOnlyLabel = useCallback( - () => dispatch(openModal({ name: ModalName.ViewOnlyExplainer })), - [dispatch] - ) + const onPressViewOnlyLabel = useCallback(() => dispatch(openModal({ name: ModalName.ViewOnlyExplainer })), [dispatch]) // Hide actions when active account isn't a signer account. const isSignerAccount = activeAccount.type === AccountType.SignerMnemonic @@ -380,16 +340,7 @@ export function HomeScreen(props?: AppStackScreenProp): JSX. onPress: onPressScan, }, ], - [ - buyLabel, - sendLabel, - scanLabel, - receiveLabel, - onPressBuy, - onPressScan, - onPressSend, - onPressReceive, - ] + [buyLabel, sendLabel, scanLabel, receiveLabel, onPressBuy, onPressScan, onPressSend, onPressReceive], ) const { canClaimUnitag } = useCanActiveAddressClaimUnitag() @@ -412,9 +363,7 @@ export function HomeScreen(props?: AppStackScreenProp): JSX. } else if (showExtensionPromoBanner) { return ( - setShowExtensionPromoModal(true)} - /> + setShowExtensionPromoModal(true)} /> ) } @@ -432,13 +381,7 @@ export function HomeScreen(props?: AppStackScreenProp): JSX. ) : ( - + {viewOnlyLabel} @@ -448,32 +391,25 @@ export function HomeScreen(props?: AppStackScreenProp): JSX. {promoBanner} ) - }, [ - activeAccount.address, - isSignerAccount, - actions, - onPressViewOnlyLabel, - viewOnlyLabel, - promoBanner, - ]) + }, [activeAccount.address, isSignerAccount, actions, onPressViewOnlyLabel, viewOnlyLabel, promoBanner]) const paddingTop = headerHeight + TAB_BAR_HEIGHT + TAB_STYLES.tabListInner.paddingTop - const paddingBottom = - insets.bottom + SWAP_BUTTON_HEIGHT + TAB_STYLES.tabListInner.paddingBottom + spacing.spacing12 + const paddingBottom = insets.bottom + SWAP_BUTTON_HEIGHT + TAB_STYLES.tabListInner.paddingBottom + spacing.spacing12 const contentContainerStyle = useMemo>( () => ({ paddingTop, paddingBottom }), - [paddingTop, paddingBottom] + [paddingTop, paddingBottom], ) const emptyComponentStyle = useMemo>( () => ({ minHeight: dimensions.fullHeight - (paddingTop + paddingBottom), - paddingTop: spacing.none, + paddingTop: media.short ? spacing.spacing12 : spacing.spacing32, + paddingBottom: media.short ? spacing.spacing12 : spacing.spacing32, paddingLeft: media.short ? spacing.none : spacing.spacing12, paddingRight: media.short ? spacing.none : spacing.spacing12, }), - [dimensions.fullHeight, media.short, paddingBottom, paddingTop] + [dimensions.fullHeight, media.short, paddingBottom, paddingTop], ) const sharedProps = useMemo( @@ -484,24 +420,24 @@ export function HomeScreen(props?: AppStackScreenProp): JSX. onScrollEndDrag: sync, scrollEventThrottle: TAB_VIEW_SCROLL_THROTTLE, }), - [contentContainerStyle, emptyComponentStyle, sync] + [contentContainerStyle, emptyComponentStyle, sync], ) const tabBarStyle = useMemo>( () => [{ top: headerHeight }, translatedStyle], - [headerHeight, translatedStyle] + [headerHeight, translatedStyle], ) const headerContainerStyle = useMemo>( () => [TAB_STYLES.headerContainer, { paddingTop: insets.top }, translatedStyle], - [insets.top, translatedStyle] + [insets.top, translatedStyle], ) const statusBarStyle = useAnimatedStyle(() => ({ backgroundColor: interpolateColor( currentScrollValue.value, [0, headerHeightDiff], - [colors.surface1.val, colors.surface1.val] + [colors.surface1.val, colors.surface1.val], ), })) @@ -552,7 +488,7 @@ export function HomeScreen(props?: AppStackScreenProp): JSX. routes, tabBarStyle, tabIndex, - ] + ], ) const [refreshing, setRefreshing] = useState(false) @@ -596,6 +532,7 @@ export function HomeScreen(props?: AppStackScreenProp): JSX. owner={activeAccount?.address} refreshing={refreshing} scrollHandler={tokensTabScrollHandler} + testID={TestID.TokensTab} onRefresh={onRefreshHomeData} /> @@ -612,6 +549,7 @@ export function HomeScreen(props?: AppStackScreenProp): JSX. owner={activeAccount?.address} refreshing={refreshing} scrollHandler={nftsTabScrollHandler} + testID={TestID.NFTsTab} onRefresh={onRefreshHomeData} /> @@ -626,6 +564,7 @@ export function HomeScreen(props?: AppStackScreenProp): JSX. owner={activeAccount?.address} refreshing={refreshing} scrollHandler={activityTabScrollHandler} + testID={TestID.ActivityTab} onRefresh={onRefreshHomeData} /> @@ -662,7 +601,7 @@ export function HomeScreen(props?: AppStackScreenProp): JSX. activityTabScrollHandler, feedTabScrollRef, feedTabScrollHandler, - ] + ], ) // Hides lock screen on next js render cycle, ensuring this component is loaded when the screen is hidden @@ -693,9 +632,7 @@ export function HomeScreen(props?: AppStackScreenProp): JSX. width="100%" zIndex="$sticky" /> - {showExtensionPromoModal && ( - setShowExtensionPromoModal(false)} /> - )} + {showExtensionPromoModal && setShowExtensionPromoModal(false)} />} ) } @@ -760,7 +697,8 @@ function ActionButton({ fill backgroundColor="$DEP_backgroundActionButton" borderRadius="$rounded20" - p="$spacing16"> + p="$spacing16" + > t('onboarding.import.method.restore.title'), @@ -48,6 +51,7 @@ const options: ImportMethodOption[] = [ nav: OnboardingScreens.RestoreCloudBackup, importType: ImportType.Restore, name: ElementName.RestoreFromCloud, + testID: TestID.RestoreFromCloud, }, ] @@ -66,9 +70,7 @@ export function ImportMethodScreen({ navigation, route: { params } }: Props): JS if (!cloudStorageAvailable) { Alert.alert( - isAndroid - ? t('account.cloud.error.unavailable.title.android') - : t('account.cloud.error.unavailable.title.ios'), + isAndroid ? t('account.cloud.error.unavailable.title.android') : t('account.cloud.error.unavailable.title.ios'), isAndroid ? t('account.cloud.error.unavailable.message.android') : t('account.cloud.error.unavailable.message.ios'), @@ -79,7 +81,7 @@ export function ImportMethodScreen({ navigation, route: { params } }: Props): JS style: 'default', }, { text: t('account.cloud.error.unavailable.button.cancel'), style: 'cancel' }, - ] + ], ) return } @@ -116,25 +118,23 @@ export function ImportMethodScreen({ navigation, route: { params } }: Props): JS gap="$spacing12" mt="$spacing4" shadowColor="$surface3" - shadowRadius={!isDarkMode ? '$spacing8' : undefined}> - {importOptions.map(({ title, blurb, icon, nav, importType, name }, i) => ( + shadowRadius={!isDarkMode ? '$spacing8' : undefined} + > + {importOptions.map(({ title, blurb, icon, nav, importType, name, testID }, i) => ( => handleOnPress(nav, importType)} /> ))} - + => - handleOnPress(OnboardingScreens.WatchWallet, ImportType.Watch) - }> + onPress={(): Promise => handleOnPress(OnboardingScreens.WatchWallet, ImportType.Watch)} + > {t('account.wallet.button.watch')} diff --git a/apps/mobile/src/screens/Import/OnDeviceRecoveryScreen.tsx b/apps/mobile/src/screens/Import/OnDeviceRecoveryScreen.tsx index 259be28123f..bb0830413b2 100644 --- a/apps/mobile/src/screens/Import/OnDeviceRecoveryScreen.tsx +++ b/apps/mobile/src/screens/Import/OnDeviceRecoveryScreen.tsx @@ -1,6 +1,5 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack' import dayjs from 'dayjs' -import { isNumber } from 'lodash' import React, { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { ScrollView } from 'react-native-gesture-handler' @@ -16,10 +15,11 @@ import { Flex, Image, Text, TouchableArea, useSporeColors } from 'ui/src' import { UNISWAP_LOGO } from 'ui/src/assets' import { PapersText } from 'ui/src/components/icons' import { iconSizes } from 'ui/src/theme' -import { DynamicConfigs } from 'uniswap/src/features/gating/configs' -import { useDynamicConfig } from 'uniswap/src/features/gating/hooks' +import { DynamicConfigs, OnDeviceRecoveryConfigKey } from 'uniswap/src/features/gating/configs' +import { useDynamicConfigValue } from 'uniswap/src/features/gating/hooks' import Trace from 'uniswap/src/features/telemetry/Trace' -import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants' +import { ModalName } from 'uniswap/src/features/telemetry/constants' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding' import { OnboardingScreens } from 'uniswap/src/types/screens/mobile' import { logger } from 'utilities/src/logger/logger' @@ -44,16 +44,14 @@ export function OnDeviceRecoveryScreen({ const { t } = useTranslation() const colors = useSporeColors() const { setRecoveredImportedAccounts } = useOnboardingContext() - const recoveryLoadingTimeoutMs = useDynamicConfig(DynamicConfigs.OnDeviceRecovery).get( - 'recoveryLoadingTimeoutMs', + const recoveryLoadingTimeoutMs = useDynamicConfigValue( + DynamicConfigs.OnDeviceRecovery, + OnDeviceRecoveryConfigKey.AppLoadingTimeoutMs, FALLBACK_RECOVERY_LOADING_TIMEOUT_MS, - isNumber ) const [selectedMnemonicId, setSelectedMnemonicId] = useState() - const [selectedRecoveryWalletInfos, setSelectedRecoveryWalletInfos] = useState< - RecoveryWalletInfo[] - >([]) + const [selectedRecoveryWalletInfos, setSelectedRecoveryWalletInfos] = useState([]) const [hasAnySignificantWallets, setHasAnySignificantWallets] = useState(false) const [loadedWallets, setLoadedWallets] = useState(0) @@ -73,7 +71,7 @@ export function OnDeviceRecoveryScreen({ if (mnemonicId !== selectedMnemonicId) { return Keyring.removeMnemonic(mnemonicId) } - }) + }), ) } @@ -84,7 +82,7 @@ export function OnDeviceRecoveryScreen({ if (!selectedRecoveryWalletInfos.find((walletInfo) => walletInfo.address === address)) { return Keyring.removePrivateKey(address) } - }) + }), ) } @@ -92,11 +90,7 @@ export function OnDeviceRecoveryScreen({ (significantWalletCount: number) => { setLoadedWallets((prev) => { const loaded = prev + 1 - logger.debug( - 'OnDeviceRecoveryScreen', - 'onLoadComplete', - `${loaded} of ${mnemonicIds.length} loaded` - ) + logger.debug('OnDeviceRecoveryScreen', 'onLoadComplete', `${loaded} of ${mnemonicIds.length} loaded`) return loaded }) @@ -104,7 +98,7 @@ export function OnDeviceRecoveryScreen({ setHasAnySignificantWallets(true) } }, - [mnemonicIds.length] + [mnemonicIds.length], ) const onPressClose = (): void => { @@ -129,7 +123,7 @@ export function OnDeviceRecoveryScreen({ derivationIndex: walletInfo.derivationIndex, timeImportedMs: dayjs().valueOf(), } - }) + }), ) navigation.navigate(OnboardingScreens.Notifications, { importType: ImportType.OnDeviceRecovery, @@ -159,7 +153,7 @@ export function OnDeviceRecoveryScreen({ logger.warn( 'OnDeviceRecoveryScreen', 'useTimeout', - `Loading timeout triggered after ${recoveryLoadingTimeoutMs}ms` + `Loading timeout triggered after ${recoveryLoadingTimeoutMs}ms`, ) } }, recoveryLoadingTimeoutMs) @@ -170,10 +164,7 @@ export function OnDeviceRecoveryScreen({ const showAllWallets = !screenLoading && !hasAnySignificantWallets return ( - + @@ -211,10 +202,7 @@ export function OnDeviceRecoveryScreen({ .fill(0) .map((_, index) => ( - + )) : null} @@ -226,11 +214,7 @@ export function OnDeviceRecoveryScreen({ {t('onboarding.import.onDeviceRecovery.other_options.label')} - + {t('onboarding.import.onDeviceRecovery.other_options')} diff --git a/apps/mobile/src/screens/Import/OnDeviceRecoveryViewSeedPhraseScreen.tsx b/apps/mobile/src/screens/Import/OnDeviceRecoveryViewSeedPhraseScreen.tsx index 96f813acf8b..68809cc125f 100644 --- a/apps/mobile/src/screens/Import/OnDeviceRecoveryViewSeedPhraseScreen.tsx +++ b/apps/mobile/src/screens/Import/OnDeviceRecoveryViewSeedPhraseScreen.tsx @@ -10,10 +10,7 @@ import { Text } from 'ui/src' import Trace from 'uniswap/src/features/telemetry/Trace' import { OnboardingScreens } from 'uniswap/src/types/screens/mobile' -type Props = NativeStackScreenProps< - OnboardingStackParamList, - OnboardingScreens.OnDeviceRecoveryViewSeedPhrase -> +type Props = NativeStackScreenProps export function OnDeviceRecoveryViewSeedPhraseScreen({ navigation, diff --git a/apps/mobile/src/screens/Import/OnDeviceRecoveryWalletCard.tsx b/apps/mobile/src/screens/Import/OnDeviceRecoveryWalletCard.tsx index 56fc6f4154d..53194659c57 100644 --- a/apps/mobile/src/screens/Import/OnDeviceRecoveryWalletCard.tsx +++ b/apps/mobile/src/screens/Import/OnDeviceRecoveryWalletCard.tsx @@ -1,10 +1,7 @@ import React, { useEffect } from 'react' import { useTranslation } from 'react-i18next' import { ViewProps } from 'react-native' -import { - RecoveryWalletInfo, - useOnDeviceRecoveryData, -} from 'src/screens/Import/useOnDeviceRecoveryData' +import { RecoveryWalletInfo, useOnDeviceRecoveryData } from 'src/screens/Import/useOnDeviceRecoveryData' import { Button, Flex, FlexProps, Loader, Text, TouchableArea } from 'ui/src' import { fonts, iconSizes } from 'ui/src/theme' import { NumberType } from 'utilities/src/format/types' @@ -40,9 +37,7 @@ export function OnDeviceRecoveryWalletCard({ const { recoveryWalletInfos, significantRecoveryWalletInfos, totalBalance, loading } = useOnDeviceRecoveryData(mnemonicId) - const targetWalletInfos = showAllWallets - ? recoveryWalletInfos.slice(0, 1) - : significantRecoveryWalletInfos + const targetWalletInfos = showAllWallets ? recoveryWalletInfos.slice(0, 1) : significantRecoveryWalletInfos const firstWalletInfo = targetWalletInfos[0] const remainingWalletCount = targetWalletInfos.length - 1 @@ -66,7 +61,8 @@ export function OnDeviceRecoveryWalletCard({ borderColor="$surface2" borderWidth={1} gap="$spacing16" - p="$spacing12"> + p="$spacing12" + > @@ -96,7 +92,8 @@ export function OnDeviceRecoveryWalletCard({ py="$spacing8" theme="secondary" width="100%" - onPress={() => onPressViewRecoveryPhrase()}> + onPress={() => onPressViewRecoveryPhrase()} + > {t('onboarding.import.onDeviceRecovery.wallet.button')} @@ -116,10 +113,6 @@ export function OnDeviceRecoveryWalletCardLoader({ totalCount: number }): JSX.Element { return ( - + ) } diff --git a/apps/mobile/src/screens/Import/RestoreCloudBackupLoadingScreen.tsx b/apps/mobile/src/screens/Import/RestoreCloudBackupLoadingScreen.tsx index 4c1f67117f4..b83b50030e5 100644 --- a/apps/mobile/src/screens/Import/RestoreCloudBackupLoadingScreen.tsx +++ b/apps/mobile/src/screens/Import/RestoreCloudBackupLoadingScreen.tsx @@ -2,7 +2,7 @@ import { useFocusEffect } from '@react-navigation/core' import { NativeStackScreenProps } from '@react-navigation/native-stack' import React, { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' -import { useAppDispatch } from 'src/app/hooks' +import { useDispatch } from 'react-redux' import { OnboardingStackParamList } from 'src/app/navigation/types' import { startFetchingCloudStorageBackups, @@ -23,21 +23,15 @@ import { logger } from 'utilities/src/logger/logger' import { ONE_SECOND_MS } from 'utilities/src/time/time' import { useSignerAccounts } from 'wallet/src/features/wallet/hooks' -type Props = NativeStackScreenProps< - OnboardingStackParamList, - OnboardingScreens.RestoreCloudBackupLoading -> +type Props = NativeStackScreenProps const MIN_LOADING_UI_MS = ONE_SECOND_MS // 10s timeout time for query for backups, since we don't know when the query completes const MAX_LOADING_TIMEOUT_MS = ONE_SECOND_MS * 10 -export function RestoreCloudBackupLoadingScreen({ - navigation, - route: { params }, -}: Props): JSX.Element { +export function RestoreCloudBackupLoadingScreen({ navigation, route: { params } }: Props): JSX.Element { const { t } = useTranslation() - const dispatch = useAppDispatch() + const dispatch = useDispatch() const entryPoint = params.entryPoint const importType = params.importType @@ -84,14 +78,14 @@ export function RestoreCloudBackupLoadingScreen({ logger.debug( 'RestoreCloudBackupLoadingScreen', 'fetchCloudStorageBackups', - `Timed out fetching cloud backups after ${MAX_LOADING_TIMEOUT_MS}ms` + `Timed out fetching cloud backups after ${MAX_LOADING_TIMEOUT_MS}ms`, ) } // eslint-disable-next-line @typescript-eslint/no-floating-promises stopFetchingCloudStorageBackups() setIsLoading(false) }, - backups.length === 0 ? MAX_LOADING_TIMEOUT_MS : MIN_LOADING_UI_MS + backups.length === 0 ? MAX_LOADING_TIMEOUT_MS : MIN_LOADING_UI_MS, ) return () => { @@ -111,7 +105,7 @@ export function RestoreCloudBackupLoadingScreen({ dispatch(clearCloudBackups()) fetchCloudStorageBackups() }) - }, [dispatch, fetchCloudStorageBackups, navigation]) + }, [dispatch, fetchCloudStorageBackups, navigation]), ) /** diff --git a/apps/mobile/src/screens/Import/RestoreCloudBackupPasswordScreen.test.tsx b/apps/mobile/src/screens/Import/RestoreCloudBackupPasswordScreen.test.tsx index 62add03f64f..ef4da090e33 100644 --- a/apps/mobile/src/screens/Import/RestoreCloudBackupPasswordScreen.test.tsx +++ b/apps/mobile/src/screens/Import/RestoreCloudBackupPasswordScreen.test.tsx @@ -8,10 +8,7 @@ import { OnboardingScreens } from 'uniswap/src/types/screens/mobile' import { TamaguiProvider } from 'wallet/src/provider/tamagui-provider' const setOptionsSpy = jest.fn() -const routeProp = { params: {} } as RouteProp< - OnboardingStackParamList, - OnboardingScreens.RestoreCloudBackupPassword -> +const routeProp = { params: {} } as RouteProp describe(RestoreCloudBackupPasswordScreen, () => { it('renders correctly', () => { @@ -32,7 +29,7 @@ describe(RestoreCloudBackupPasswordScreen, () => { } route={routeProp} /> - + , ).toJSON() expect(tree).toMatchSnapshot() diff --git a/apps/mobile/src/screens/Import/RestoreCloudBackupPasswordScreen.tsx b/apps/mobile/src/screens/Import/RestoreCloudBackupPasswordScreen.tsx index e06b9457a40..c9bc6e4f421 100644 --- a/apps/mobile/src/screens/Import/RestoreCloudBackupPasswordScreen.tsx +++ b/apps/mobile/src/screens/Import/RestoreCloudBackupPasswordScreen.tsx @@ -3,7 +3,8 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack' import React, { useCallback, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { Keyboard, TextInput } from 'react-native' -import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { useDispatch } from 'react-redux' +import { useAppSelector } from 'src/app/hooks' import { OnboardingStackParamList } from 'src/app/navigation/types' import { PasswordInput } from 'src/components/input/PasswordInput' import { restoreMnemonicFromCloudStorage } from 'src/features/CloudBackup/RNCloudStorageBackupsManager' @@ -18,7 +19,7 @@ import { OnboardingScreen } from 'src/features/onboarding/OnboardingScreen' import { PasswordError } from 'src/features/onboarding/PasswordError' import { useAddBackButton } from 'src/utils/useAddBackButton' import { Button, Flex, Text, TouchableArea } from 'ui/src' -import { ElementName } from 'uniswap/src/features/telemetry/constants' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { ImportType } from 'uniswap/src/types/onboarding' import { OnboardingScreens } from 'uniswap/src/types/screens/mobile' import { getCloudProviderName } from 'uniswap/src/utils/cloud-backup/getCloudProviderName' @@ -26,10 +27,7 @@ import { MINUTES_IN_HOUR, ONE_HOUR_MS, ONE_MINUTE_MS } from 'utilities/src/time/ import { useOnboardingContext } from 'wallet/src/features/onboarding/OnboardingContext' import { BackupType } from 'wallet/src/features/wallet/accounts/types' -type Props = NativeStackScreenProps< - OnboardingStackParamList, - OnboardingScreens.RestoreCloudBackupPassword -> +type Props = NativeStackScreenProps /** * If the attempt count does not correspond to a lockout then returns undefined. Otherwise returns the lockout time based on attempts. The lockout time logic is as follows: @@ -69,13 +67,10 @@ function useLockoutTimeMessage(remainingLockoutTime: number): string { return t('account.cloud.lockout.time.minutes', { count: Math.floor(minutes) }) } -export function RestoreCloudBackupPasswordScreen({ - navigation, - route: { params }, -}: Props): JSX.Element { +export function RestoreCloudBackupPasswordScreen({ navigation, route: { params } }: Props): JSX.Element { const { t } = useTranslation() const inputRef = useRef(null) - const dispatch = useAppDispatch() + const dispatch = useDispatch() const { generateImportedAccounts } = useOnboardingContext() const passwordAttemptCount = useAppSelector(selectPasswordAttempts) @@ -103,7 +98,7 @@ export function RestoreCloudBackupPasswordScreen({ return () => clearTimeout(timer) } - }, [isLockedOut, lockoutMessage, remainingLockoutTime, dispatch]) + }, [isLockedOut, lockoutMessage, remainingLockoutTime, dispatch]), ) useAddBackButton(navigation) @@ -148,7 +143,8 @@ export function RestoreCloudBackupPasswordScreen({ return ( + title={t('account.cloud.password.title')} + > )} - diff --git a/apps/mobile/src/screens/Import/RestoreCloudBackupScreen.test.tsx b/apps/mobile/src/screens/Import/RestoreCloudBackupScreen.test.tsx index 78969be3e11..df4780fd161 100644 --- a/apps/mobile/src/screens/Import/RestoreCloudBackupScreen.test.tsx +++ b/apps/mobile/src/screens/Import/RestoreCloudBackupScreen.test.tsx @@ -7,10 +7,7 @@ import { render } from 'src/test/test-utils' import { OnboardingScreens } from 'uniswap/src/types/screens/mobile' const setOptionsSpy = jest.fn() -const routeProp = { params: {} } as RouteProp< - OnboardingStackParamList, - OnboardingScreens.RestoreCloudBackup -> +const routeProp = { params: {} } as RouteProp describe(RestoreCloudBackupScreen, () => { it('renders correctly', () => { @@ -29,7 +26,7 @@ describe(RestoreCloudBackupScreen, () => { > } route={routeProp} - /> + />, ).toJSON() expect(tree).toMatchSnapshot() diff --git a/apps/mobile/src/screens/Import/RestoreCloudBackupScreen.tsx b/apps/mobile/src/screens/Import/RestoreCloudBackupScreen.tsx index 51a88249314..82bcc0b73d8 100644 --- a/apps/mobile/src/screens/Import/RestoreCloudBackupScreen.tsx +++ b/apps/mobile/src/screens/Import/RestoreCloudBackupScreen.tsx @@ -11,12 +11,9 @@ import { Flex, Text, TouchableArea, Unicon, useIsDarkMode } from 'ui/src' import { RotatableChevron } from 'ui/src/components/icons' import { iconSizes } from 'ui/src/theme' import { OnboardingScreens } from 'uniswap/src/types/screens/mobile' +import { sanitizeAddressText, shortenAddress } from 'uniswap/src/utils/addresses' import { getCloudProviderName } from 'uniswap/src/utils/cloud-backup/getCloudProviderName' -import { - FORMAT_DATE_TIME_SHORT, - useLocalizedDayjs, -} from 'wallet/src/features/language/localizedDayjs' -import { sanitizeAddressText, shortenAddress } from 'wallet/src/utils/addresses' +import { FORMAT_DATE_TIME_SHORT, useLocalizedDayjs } from 'wallet/src/features/language/localizedDayjs' type Props = NativeStackScreenProps @@ -42,7 +39,8 @@ export function RestoreCloudBackupScreen({ navigation, route: { params } }: Prop return ( + title={t('account.cloud.backup.title')} + > {sortedBackups.map((backup) => { @@ -57,7 +55,8 @@ export function RestoreCloudBackupScreen({ navigation, route: { params } }: Prop p="$spacing16" shadowColor="$surface3" shadowRadius={!isDarkMode ? '$spacing4' : undefined} - onPress={(): Promise => onPressRestoreBackup(backup)}> + onPress={(): Promise => onPressRestoreBackup(backup)} + > diff --git a/apps/mobile/src/screens/Import/SeedPhraseInput.tsx b/apps/mobile/src/screens/Import/SeedPhraseInput.tsx index bd97ff7babb..7324bf53581 100644 --- a/apps/mobile/src/screens/Import/SeedPhraseInput.tsx +++ b/apps/mobile/src/screens/Import/SeedPhraseInput.tsx @@ -1,12 +1,6 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack' import { forwardRef, RefObject, useEffect, useState } from 'react' -import { - findNodeHandle, - NativeSyntheticEvent, - requireNativeComponent, - StyleSheet, - UIManager, -} from 'react-native' +import { findNodeHandle, NativeSyntheticEvent, requireNativeComponent, StyleSheet, UIManager } from 'react-native' import { useNativeComponentKey } from 'src/app/hooks' import { OnboardingStackParamList } from 'src/app/navigation/types' import { OnboardingScreens } from 'uniswap/src/types/screens/mobile' @@ -32,6 +26,7 @@ export enum StringKey { } interface NativeSeedPhraseInputProps { targetMnemonicId?: string + testID?: string strings: Record onInputValidated: (e: NativeSyntheticEvent) => void onMnemonicStored: (e: NativeSyntheticEvent) => void @@ -89,6 +84,6 @@ export const SeedPhraseInput = forwardRef ) - } + }, ) SeedPhraseInput.displayName = 'NativeSeedPhraseInput' diff --git a/apps/mobile/src/screens/Import/SeedPhraseInputScreen.tsx b/apps/mobile/src/screens/Import/SeedPhraseInputScreen.tsx index b1368140b5a..08860bc6ead 100644 --- a/apps/mobile/src/screens/Import/SeedPhraseInputScreen.tsx +++ b/apps/mobile/src/screens/Import/SeedPhraseInputScreen.tsx @@ -13,11 +13,11 @@ import Trace from 'uniswap/src/features/telemetry/Trace' import { ElementName } from 'uniswap/src/features/telemetry/constants' import { ImportType } from 'uniswap/src/types/onboarding' import { OnboardingScreens } from 'uniswap/src/types/screens/mobile' +import { openUri } from 'uniswap/src/utils/linking' import { useOnboardingContext } from 'wallet/src/features/onboarding/OnboardingContext' import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring' import { BackupType } from 'wallet/src/features/wallet/accounts/types' import { useSignerAccounts } from 'wallet/src/features/wallet/hooks' -import { openUri } from 'wallet/src/utils/linking' import { MnemonicValidationError, translateMnemonicErrorMessage, @@ -80,15 +80,7 @@ export function SeedPhraseInputScreen({ navigation, route: { params } }: Props): if (!isRestoringMnemonic) { navigation.navigate({ name: OnboardingScreens.SelectWallet, params, merge: true }) } - }, [ - value, - mnemonicId, - generateImportedAccountsByMnemonic, - isRestoringMnemonic, - t, - navigation, - params, - ]) + }, [value, mnemonicId, generateImportedAccountsByMnemonic, isRestoringMnemonic, t, navigation, params]) const onBlur = useCallback(() => { const { error, invalidWord } = validateMnemonic(value) @@ -114,8 +106,7 @@ export function SeedPhraseInputScreen({ navigation, route: { params } }: Props): setValue(text) } - const onPressRecoveryHelpButton = (): Promise => - openUri(uniswapUrls.helpArticleUrls.recoveryPhraseHowToImport) + const onPressRecoveryHelpButton = (): Promise => openUri(uniswapUrls.helpArticleUrls.recoveryPhraseHowToImport) const onPressTryAgainButton = (): void => { navigation.replace(OnboardingScreens.RestoreCloudBackupLoading, params) @@ -129,10 +120,9 @@ export function SeedPhraseInputScreen({ navigation, route: { params } }: Props): : t('account.recoveryPhrase.subtitle.import') } title={ - isRestoringMnemonic - ? t('account.recoveryPhrase.title.restoring') - : t('account.recoveryPhrase.title.import') - }> + isRestoringMnemonic ? t('account.recoveryPhrase.title.restoring') : t('account.recoveryPhrase.title.import') + } + > + onPress={isRestoringMnemonic ? onPressTryAgainButton : onPressRecoveryHelpButton} + > {isRestoringMnemonic @@ -166,10 +157,7 @@ export function SeedPhraseInputScreen({ navigation, route: { params } }: Props): - diff --git a/apps/mobile/src/screens/Import/SeedPhraseInputScreenV2.android.mock.tsx b/apps/mobile/src/screens/Import/SeedPhraseInputScreenV2.android.mock.tsx new file mode 100644 index 00000000000..6dea2e459ed --- /dev/null +++ b/apps/mobile/src/screens/Import/SeedPhraseInputScreenV2.android.mock.tsx @@ -0,0 +1,167 @@ +import { NativeStackScreenProps } from '@react-navigation/native-stack' +import React, { useCallback, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { OnboardingStackParamList } from 'src/app/navigation/types' +import { useLockScreenOnBlur } from 'src/features/authentication/lockScreenContext' +import { GenericImportForm } from 'src/features/import/GenericImportForm' +import { SafeKeyboardOnboardingScreen } from 'src/features/onboarding/SafeKeyboardOnboardingScreen' +import { useAddBackButton } from 'src/utils/useAddBackButton' +import { Button, Flex, Text, TouchableArea } from 'ui/src' +import { QuestionInCircleFilled } from 'ui/src/components/icons' +import { uniswapUrls } from 'uniswap/src/constants/urls' +import Trace from 'uniswap/src/features/telemetry/Trace' +import { ElementName } from 'uniswap/src/features/telemetry/constants' +import { ImportType } from 'uniswap/src/types/onboarding' +import { OnboardingScreens } from 'uniswap/src/types/screens/mobile' +import { openUri } from 'uniswap/src/utils/linking' +import { useOnboardingContext } from 'wallet/src/features/onboarding/OnboardingContext' +import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring' +import { BackupType } from 'wallet/src/features/wallet/accounts/types' +import { useSignerAccounts } from 'wallet/src/features/wallet/hooks' +import { + MnemonicValidationError, + translateMnemonicErrorMessage, + userFinishedTypingWord, + validateMnemonic, + validateSetOfWords, +} from 'wallet/src/utils/mnemonics' + +type Props = NativeStackScreenProps + +// Original SeedPhraseInputScreen component including JS input field. Used as a mock for Android Detox e2e testing. +export function SeedPhraseInputScreenV2({ navigation, route: { params } }: Props): JSX.Element { + const { t } = useTranslation() + const { generateImportedAccountsByMnemonic } = useOnboardingContext() + + /** + * If paste permission modal is open, we need to manually disable the splash screen that appears on blur, + * since the modal triggers the same `inactive` app state as does going to app switcher + * + * Technically seed phrase will be blocked if user pastes from keyboard, + * but that is an extreme edge case. + **/ + const [pastePermissionModalOpen, setPastePermissionModalOpen] = useState(false) + useLockScreenOnBlur(pastePermissionModalOpen) + + const [value, setValue] = useState(undefined) + const [errorMessage, setErrorMessage] = useState(undefined) + + const isRestoringMnemonic = params.importType === ImportType.RestoreMnemonic + + useAddBackButton(navigation) + + const signerAccounts = useSignerAccounts() + const mnemonicId = (isRestoringMnemonic && signerAccounts[0]?.mnemonicId) || undefined + + // Add all accounts from mnemonic. + const onSubmit = useCallback(async () => { + // Check phrase validation + const { validMnemonic, error, invalidWord } = validateMnemonic(value) + + if (error) { + setErrorMessage(translateMnemonicErrorMessage(error, invalidWord, t)) + return + } + + if (!validMnemonic) { + return + } + + if (mnemonicId && validMnemonic) { + const generatedMnemonicId = await Keyring.generateAddressForMnemonic(validMnemonic, 0) + if (generatedMnemonicId !== mnemonicId) { + setErrorMessage(t('account.recoveryPhrase.error.wrong')) + return + } + } + + await generateImportedAccountsByMnemonic(validMnemonic, undefined, BackupType.Manual) + + // restore flow is handled in saga after `restoreMnemonicComplete` is dispatched + if (!isRestoringMnemonic) { + navigation.navigate({ name: OnboardingScreens.SelectWallet, params, merge: true }) + } + }, [value, mnemonicId, generateImportedAccountsByMnemonic, isRestoringMnemonic, t, navigation, params]) + + const onBlur = useCallback(() => { + const { error, invalidWord } = validateMnemonic(value) + if (error) { + setErrorMessage(translateMnemonicErrorMessage(error, invalidWord, t)) + } + }, [t, value]) + + const onChange = (text: string | undefined): void => { + const { error, invalidWord } = validateSetOfWords(text) + + // suppress error messages if the user is not done typing a word + const suppressError = + (error === MnemonicValidationError.InvalidWord && !userFinishedTypingWord(text)) || + error === MnemonicValidationError.NotEnoughWords + + if (!error || suppressError) { + setErrorMessage(undefined) + } else { + setErrorMessage(translateMnemonicErrorMessage(error, invalidWord, t)) + } + + setValue(text) + } + + const onPressRecoveryHelpButton = (): Promise => openUri(uniswapUrls.helpArticleUrls.recoveryPhraseHowToImport) + + const onPressTryAgainButton = (): void => { + navigation.replace(OnboardingScreens.RestoreCloudBackupLoading, params) + } + + return ( + + + + setPastePermissionModalOpen(false)} + beforePasteButtonPress={(): void => setPastePermissionModalOpen(true)} + errorMessage={errorMessage} + inputAlignment="flex-start" + placeholderLabel={t('account.recoveryPhrase.input')} + textAlign="left" + value={value} + onBlur={onBlur} + onChange={onChange} + /> + + + + + + + {isRestoringMnemonic + ? t('account.recoveryPhrase.helpText.restoring') + : t('account.recoveryPhrase.helpText.import')} + + + + + + + + + + ) +} diff --git a/apps/mobile/src/screens/Import/SeedPhraseInputScreenV2.tsx b/apps/mobile/src/screens/Import/SeedPhraseInputScreenV2.tsx index 900aaf6c4da..65602aeba8b 100644 --- a/apps/mobile/src/screens/Import/SeedPhraseInputScreenV2.tsx +++ b/apps/mobile/src/screens/Import/SeedPhraseInputScreenV2.tsx @@ -19,12 +19,13 @@ import { QuestionInCircleFilled } from 'ui/src/components/icons' import { uniswapUrls } from 'uniswap/src/constants/urls' import Trace from 'uniswap/src/features/telemetry/Trace' import { ElementName } from 'uniswap/src/features/telemetry/constants' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { ImportType } from 'uniswap/src/types/onboarding' import { OnboardingScreens } from 'uniswap/src/types/screens/mobile' +import { openUri } from 'uniswap/src/utils/linking' import { useOnboardingContext } from 'wallet/src/features/onboarding/OnboardingContext' import { BackupType } from 'wallet/src/features/wallet/accounts/types' import { useSignerAccounts } from 'wallet/src/features/wallet/hooks' -import { openUri } from 'wallet/src/utils/linking' type Props = NativeStackScreenProps @@ -60,11 +61,10 @@ export function SeedPhraseInputScreenV2({ navigation, route: { params } }: Props navigation.navigate({ name: OnboardingScreens.SelectWallet, params, merge: true }) } }, - [generateImportedAccounts, isRestoringMnemonic, navigation, params] + [generateImportedAccounts, isRestoringMnemonic, navigation, params], ) - const onPressRecoveryHelpButton = (): Promise => - openUri(uniswapUrls.helpArticleUrls.recoveryPhraseHowToImport) + const onPressRecoveryHelpButton = (): Promise => openUri(uniswapUrls.helpArticleUrls.recoveryPhraseHowToImport) const onPressTryAgainButton = (): void => { navigation.replace(OnboardingScreens.RestoreCloudBackupLoading, params) @@ -72,31 +72,31 @@ export function SeedPhraseInputScreenV2({ navigation, route: { params } }: Props return ( } + minHeightWhenKeyboardExpanded={false} subtitle={ isRestoringMnemonic ? t('account.recoveryPhrase.subtitle.restoring') : t('account.recoveryPhrase.subtitle.import') } title={ - isRestoringMnemonic - ? t('account.recoveryPhrase.title.restoring') - : t('account.recoveryPhrase.title.import') - }> + isRestoringMnemonic ? t('account.recoveryPhrase.title.restoring') : t('account.recoveryPhrase.title.import') + } + > ): void => setSubmitEnabled(e.nativeEvent.canSubmit) } @@ -125,8 +126,7 @@ export function SeedPhraseInputScreenV2({ navigation, route: { params } }: Props /> - + diff --git a/apps/mobile/src/screens/Import/SelectWalletScreen.tsx b/apps/mobile/src/screens/Import/SelectWalletScreen.tsx index 8be6ece3fe7..cc40d085df5 100644 --- a/apps/mobile/src/screens/Import/SelectWalletScreen.tsx +++ b/apps/mobile/src/screens/Import/SelectWalletScreen.tsx @@ -8,6 +8,7 @@ import { Button, Flex, Loader } from 'ui/src' import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard' import { useSelectWalletScreenQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { ElementName } from 'uniswap/src/features/telemetry/constants' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { ImportType } from 'uniswap/src/types/onboarding' import { OnboardingScreens } from 'uniswap/src/types/screens/mobile' import { ONE_SECOND_MS } from 'utilities/src/time/time' @@ -64,9 +65,7 @@ export function SelectWalletScreen({ navigation, route: { params } }: Props): JS })) .filter(isImportableAccount) - const accountsWithBalance = filteredAccounts?.filter( - (address) => address.balance && address.balance > 0 - ) + const accountsWithBalance = filteredAccounts?.filter((address) => address.balance && address.balance > 0) if (accountsWithBalance?.length) { return accountsWithBalance @@ -95,10 +94,7 @@ export function SelectWalletScreen({ navigation, route: { params } }: Props): JS await selectImportedAccounts(selectedAddresses) navigation.navigate({ - name: - params?.importType === ImportType.Restore - ? OnboardingScreens.Notifications - : OnboardingScreens.Backup, + name: params?.importType === ImportType.Restore ? OnboardingScreens.Notifications : OnboardingScreens.Backup, params, merge: true, }) @@ -118,9 +114,7 @@ export function SelectWalletScreen({ navigation, route: { params } }: Props): JS return ( <> - + {showError ? ( ) @@ -154,11 +148,10 @@ export function SelectWalletScreen({ navigation, route: { params } }: Props): JS )} diff --git a/apps/mobile/src/screens/Import/WatchWalletScreen.tsx b/apps/mobile/src/screens/Import/WatchWalletScreen.tsx index 55c17460b46..8c1ced23c6f 100644 --- a/apps/mobile/src/screens/Import/WatchWalletScreen.tsx +++ b/apps/mobile/src/screens/Import/WatchWalletScreen.tsx @@ -4,7 +4,7 @@ import { TFunction } from 'i18next' import React, { useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { Keyboard } from 'react-native' -import { useAppDispatch } from 'src/app/hooks' +import { useDispatch } from 'react-redux' import { OnboardingStackParamList } from 'src/app/navigation/types' import { GenericImportForm } from 'src/features/import/GenericImportForm' import { SafeKeyboardOnboardingScreen } from 'src/features/onboarding/SafeKeyboardOnboardingScreen' @@ -14,8 +14,10 @@ import { Button, Flex, Text } from 'ui/src' import { GraduationCap } from 'ui/src/components/icons' import { ElementName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { UniverseChainId } from 'uniswap/src/types/chains' import { OnboardingScreens } from 'uniswap/src/types/screens/mobile' +import { areAddressesEqual, getValidAddress } from 'uniswap/src/utils/addresses' import { normalizeTextInput } from 'utilities/src/primitives/string' import { usePortfolioBalances } from 'wallet/src/features/dataApi/balances' import { useENS } from 'wallet/src/features/ens/useENS' @@ -23,7 +25,6 @@ import { createViewOnlyAccount } from 'wallet/src/features/onboarding/createView import { useIsSmartContractAddress } from 'wallet/src/features/transactions/transfer/hooks/useIsSmartContractAddress' import { createAccountsActions } from 'wallet/src/features/wallet/create/createAccountsSaga' import { useAccounts } from 'wallet/src/features/wallet/hooks' -import { areAddressesEqual, getValidAddress } from 'wallet/src/utils/addresses' type Props = NativeStackScreenProps @@ -44,12 +45,7 @@ const validateForm = ({ isSmartContractAddress: boolean isValidSmartContract: boolean }): boolean => { - return ( - (!!isAddress || !!name) && - !walletExists && - !loading && - (!isSmartContractAddress || isValidSmartContract) - ) + return (!!isAddress || !!name) && !walletExists && !loading && (!isSmartContractAddress || isValidSmartContract) } const getErrorText = ({ @@ -75,7 +71,7 @@ const getErrorText = ({ export function WatchWalletScreen({ navigation, route: { params } }: Props): JSX.Element { const { t } = useTranslation() - const dispatch = useAppDispatch() + const dispatch = useDispatch() const accounts = useAccounts() const initialAccounts = useRef(accounts) @@ -88,15 +84,11 @@ export function WatchWalletScreen({ navigation, route: { params } }: Props): JSX // ENS and address parsing. const normalizedValue = normalizeTextInput(value ?? '') const hasSuffixIncluded = normalizedValue.includes('.') - const { address: resolvedAddress, name } = useENS( - UniverseChainId.Mainnet, - normalizedValue, - !hasSuffixIncluded - ) + const { address: resolvedAddress, name } = useENS(UniverseChainId.Mainnet, normalizedValue, !hasSuffixIncluded) const isAddress = getValidAddress(normalizedValue, true, false) const { isSmartContractAddress, loading } = useIsSmartContractAddress( (isAddress || resolvedAddress) ?? undefined, - UniverseChainId.Mainnet + UniverseChainId.Mainnet, ) // Allow smart contracts with non-null balances const { data: balancesById } = usePortfolioBalances({ @@ -109,8 +101,7 @@ export function WatchWalletScreen({ navigation, route: { params } }: Props): JSX const walletExists = Object.keys(initialAccounts).some( (accountAddress) => - areAddressesEqual(accountAddress, resolvedAddress) || - areAddressesEqual(accountAddress, normalizedValue) + areAddressesEqual(accountAddress, resolvedAddress) || areAddressesEqual(accountAddress, normalizedValue), ) // Form validation. @@ -123,9 +114,7 @@ export function WatchWalletScreen({ navigation, route: { params } }: Props): JSX isValidSmartContract, }) - const errorText = !isValid - ? getErrorText({ walletExists, isSmartContractAddress, loading, t }) - : undefined + const errorText = !isValid ? getErrorText({ walletExists, isSmartContractAddress, loading, t }) : undefined const onSubmit = useCallback(async () => { if (isValid && value) { @@ -135,7 +124,7 @@ export function WatchWalletScreen({ navigation, route: { params } }: Props): JSX dispatch( createAccountsActions.trigger({ accounts: [viewOnlyAccount], - }) + }), ) sendAnalyticsEvent(SharedEventName.ELEMENT_CLICKED, { @@ -188,14 +177,15 @@ export function WatchWalletScreen({ navigation, route: { params } }: Props): JSX backgroundColor="$surface2" borderRadius="$rounded16" gap="$spacing16" - p="$spacing16"> + p="$spacing16" + > {t('account.wallet.watch.message')} - diff --git a/apps/mobile/src/screens/Import/__snapshots__/RestoreCloudBackupPasswordScreen.test.tsx.snap b/apps/mobile/src/screens/Import/__snapshots__/RestoreCloudBackupPasswordScreen.test.tsx.snap index 9e5ee1f9e86..c3982f145f3 100644 --- a/apps/mobile/src/screens/Import/__snapshots__/RestoreCloudBackupPasswordScreen.test.tsx.snap +++ b/apps/mobile/src/screens/Import/__snapshots__/RestoreCloudBackupPasswordScreen.test.tsx.snap @@ -304,7 +304,7 @@ exports[`RestoreCloudBackupPasswordScreen renders correctly 1`] = ` "paddingTop": 12, } } - testID="submit" + testID="continue" userSelect="none" > - address && - storedAddresses.find((storedAddress) => areAddressesEqual(storedAddress, address)) + address && storedAddresses.find((storedAddress) => areAddressesEqual(storedAddress, address)) ? { address, derivationIndex: index } - : undefined + : undefined, ) .filter((address): address is AddressWithIndex => !!address) @@ -79,8 +78,7 @@ export function useOnDeviceRecoveryData(mnemonicId: string | undefined): { totalBalance: number | undefined loading: boolean } { - const { addressesWithIndex, loading: addressesLoading } = - useStoredAddressesForMnemonic(mnemonicId) + const { addressesWithIndex, loading: addressesLoading } = useStoredAddressesForMnemonic(mnemonicId) const addresses = addressesWithIndex.map((address) => address.address) const valueModifiers = usePortfolioValueModifiers(addresses) @@ -91,9 +89,7 @@ export function useOnDeviceRecoveryData(mnemonicId: string | undefined): { }, skip: !addresses.length, }) - const balances = balancesData?.portfolios?.map( - (portfolio) => portfolio?.tokensTotalDenominatedValue?.value ?? 0 - ) + const balances = balancesData?.portfolios?.map((portfolio) => portfolio?.tokensTotalDenominatedValue?.value ?? 0) const totalBalance = balances?.reduce((acc, balance) => acc + balance, 0) // Need to fetch ENS names and unitags for each deriviation index @@ -146,9 +142,9 @@ export function useOnDeviceRecoveryData(mnemonicId: string | undefined): { () => recoveryWalletInfos?.filter( (recoveryAddressInfo) => - recoveryAddressInfo.balance || recoveryAddressInfo.ensName || recoveryAddressInfo.unitag + recoveryAddressInfo.balance || recoveryAddressInfo.ensName || recoveryAddressInfo.unitag, ) ?? [], - [recoveryWalletInfos] + [recoveryWalletInfos], ) const loading = addressesLoading || ensLoading || unitagLoading || balancesLoading diff --git a/apps/mobile/src/screens/NFTCollectionScreen.tsx b/apps/mobile/src/screens/NFTCollectionScreen.tsx index 76eaa647120..c653314b8b2 100644 --- a/apps/mobile/src/screens/NFTCollectionScreen.tsx +++ b/apps/mobile/src/screens/NFTCollectionScreen.tsx @@ -10,16 +10,10 @@ import { ScrollHeader } from 'src/components/layout/screens/ScrollHeader' import { Loader } from 'src/components/loading' import { ListPriceBadge } from 'src/features/nfts/collection/ListPriceCard' import { NFTCollectionContextMenu } from 'src/features/nfts/collection/NFTCollectionContextMenu' -import { - NFTCollectionHeader, - NFT_BANNER_HEIGHT, -} from 'src/features/nfts/collection/NFTCollectionHeader' +import { NFTCollectionHeader, NFT_BANNER_HEIGHT } from 'src/features/nfts/collection/NFTCollectionHeader' import { ExploreModalAwareView } from 'src/screens/ModalAwareView' import { Flex, ImpactFeedbackStyle, Text, TouchableArea, useDeviceInsets } from 'ui/src' -import { - AnimatedBottomSheetFlashList, - AnimatedFlashList, -} from 'ui/src/components/AnimatedFlashList/AnimatedFlashList' +import { AnimatedBottomSheetFlashList, AnimatedFlashList } from 'ui/src/components/AnimatedFlashList/AnimatedFlashList' import { useDeviceDimensions } from 'ui/src/hooks/useDeviceDimensions' import { iconSizes, spacing } from 'ui/src/theme' import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard' @@ -44,9 +38,7 @@ const LOADING_BUFFER_AMOUNT = 9 const LOADING_ITEMS_ARRAY: NFTItem[] = Array(LOADING_BUFFER_AMOUNT).fill(LOADING_ITEM) const keyExtractor = (item: NFTItem | string, index: number): string => - typeof item === 'string' - ? `${LOADING_ITEM}-${index}` - : getNFTAssetKey(item.contractAddress ?? '', item.tokenId ?? '') + typeof item === 'string' ? `${LOADING_ITEM}-${index}` : getNFTAssetKey(item.contractAddress ?? '', item.tokenId ?? '') function gqlNFTAssetToNFTItem(data: NftCollectionScreenQuery | undefined): NFTItem[] | undefined { const items = data?.nftAssets?.edges?.flatMap((item) => item.node) @@ -161,7 +153,8 @@ export function NFTCollectionScreen({ backgroundColor="$surface3" borderRadius="$rounded16" overflow="hidden" - style={containerStyle}> + style={containerStyle} + > {typeof item === 'string' ? ( ) : ( @@ -171,7 +164,8 @@ export function NFTCollectionScreen({ alignItems="center" flex={1} hapticStyle={ImpactFeedbackStyle.Light} - onPress={(): void => onPressItem(item)}> + onPress={(): void => onPressItem(item)} + > - collectionData?.name - ? { collectionAddress, collectionName: collectionData?.name } - : undefined, - [collectionAddress, collectionData?.name] + () => (collectionData?.name ? { collectionAddress, collectionName: collectionData?.name } : undefined), + [collectionAddress, collectionData?.name], ) if (isError(networkStatus, !!data)) { @@ -248,29 +237,21 @@ export function NFTCollectionScreen({ directFromPage logImpression={!!traceProperties} properties={traceProperties} - screen={MobileScreens.NFTCollection}> + screen={MobileScreens.NFTCollection} + > {collectionData.name} : undefined - } + centerElement={collectionData?.name ? {collectionData.name} : undefined} listRef={listRef} - rightElement={ - - } + rightElement={} scrollY={scrollY} showHeaderScrollYDistance={NFT_BANNER_HEIGHT} /> - ) + gridDataLoading ? null : } ListHeaderComponent={ asset?.name ?? fallbackData?.name, [asset?.name, fallbackData?.name]) const description = useMemo( () => asset?.description ?? fallbackData?.description, - [asset?.description, fallbackData?.description] + [asset?.description, fallbackData?.description], ) const imageUrl = useMemo( () => asset?.image?.url ?? fallbackData?.imageUrl, - [asset?.image?.url, fallbackData?.imageUrl] + [asset?.image?.url, fallbackData?.imageUrl], ) const imageHeight = asset?.image?.dimensions?.height const imageWidth = asset?.image?.dimensions?.width const imageDimensionsExist = imageHeight && imageWidth - const imageDimensions = imageDimensionsExist - ? { height: imageHeight, width: imageWidth } - : undefined + const imageDimensions = imageDimensionsExist ? { height: imageHeight, width: imageWidth } : undefined const imageAspectRatio = imageDimensions ? imageDimensions.width / imageDimensions.height : 1 const onPressCollection = (): void => { const collectionAddress = asset?.nftContract?.address ?? fallbackData?.contractAddress @@ -135,7 +122,7 @@ function NFTItemScreenContents({ // Disable navigation to profile if user owns NFT or invalid owner const disableProfileNavigation = Boolean( - owner && (areAddressesEqual(owner, activeAccountAddress) || !isAddress(owner)) + owner && (areAddressesEqual(owner, activeAccountAddress) || !isAddress(owner)), ) const onPressOwner = (): void => { @@ -177,10 +164,7 @@ function NFTItemScreenContents({ const { colorLight, colorDark } = useNearestThemeColorFromImageUri(imageUrl) // check if colorLight passes contrast against card bg color, if not use fallback const accentTextColor = useMemo(() => { - if ( - colorLight && - passesContrast(colorLight, colors.surface1.val, MIN_COLOR_CONTRAST_THRESHOLD) - ) { + if (colorLight && passesContrast(colorLight, colors.surface1.val, MIN_COLOR_CONTRAST_THRESHOLD)) { return colorLight } return colors.neutral2.val @@ -193,32 +177,28 @@ function NFTItemScreenContents({ pushNotification({ type: AppNotificationType.Copied, copyType: CopyNotificationType.Image, - }) + }), ) } const rightElement = useMemo( () => , - [asset, isSpam, owner] + [asset, isSpam, owner], ) return ( <> - {isIOS ? ( - - ) : null} + {isIOS ? : null} + screen={MobileScreens.NFTItem} + > <> {isIOS ? ( - + ) : ( )} @@ -232,7 +212,8 @@ function NFTItemScreenContents({ maxHeight={getTokenValue('$icon.40')} maxWidth={getTokenValue('$icon.40')} ml="$spacing16" - overflow="hidden"> + overflow="hidden" + > ) : ( @@ -242,21 +223,18 @@ function NFTItemScreenContents({ ) } renderedInModal={inModal} - rightElement={rightElement}> + rightElement={rightElement} + > {/* Content wrapper */} - + + shadowRadius={16} + > {nftLoading ? ( @@ -272,16 +250,11 @@ function NFTItemScreenContents({ /> ) : ( - + > => - refetch?.() - } + onRetry={(): Promise> => refetch?.()} /> )} @@ -315,11 +288,7 @@ function NFTItemScreenContents({ ) : description ? ( - + ) : null} @@ -373,10 +342,7 @@ function NFTItemScreenContents({ color={accentTextColor} title={t('tokens.nfts.details.owner')} valueComponent={ - + + {menuActions.length > 0 ? ( onlyShare ? ( - + ) : ( - + ) diff --git a/apps/mobile/src/screens/Onboarding/BackupScreen.test.tsx b/apps/mobile/src/screens/Onboarding/BackupScreen.test.tsx index a750543108c..df76bd60137 100644 --- a/apps/mobile/src/screens/Onboarding/BackupScreen.test.tsx +++ b/apps/mobile/src/screens/Onboarding/BackupScreen.test.tsx @@ -47,7 +47,7 @@ describe(BackupScreen, () => { , - { preloadedState: preloadedSharedState({ account: ACCOUNT }) } + { preloadedState: preloadedSharedState({ account: ACCOUNT }) }, ) await act(async () => { diff --git a/apps/mobile/src/screens/Onboarding/BackupScreen.tsx b/apps/mobile/src/screens/Onboarding/BackupScreen.tsx index c78ff5d2c34..aa6ae6cb545 100644 --- a/apps/mobile/src/screens/Onboarding/BackupScreen.tsx +++ b/apps/mobile/src/screens/Onboarding/BackupScreen.tsx @@ -4,11 +4,7 @@ import { StackScreenProps } from '@react-navigation/stack' import React, { useCallback, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { Alert } from 'react-native' -import { - AppStackParamList, - OnboardingStackParamList, - useOnboardingStackNavigation, -} from 'src/app/navigation/types' +import { AppStackParamList, OnboardingStackParamList, useOnboardingStackNavigation } from 'src/app/navigation/types' import { BackButton } from 'src/components/buttons/BackButton' import { EducationContentType } from 'src/components/education' import { isCloudStorageAvailable } from 'src/features/CloudBackup/RNCloudStorageBackupsManager' @@ -20,6 +16,7 @@ import { OSDynamicCloudIcon, QuestionInCircleFilled } from 'ui/src/components/ic import { iconSizes } from 'ui/src/theme' import Trace from 'uniswap/src/features/telemetry/Trace' import { ElementName } from 'uniswap/src/features/telemetry/constants' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { ImportType } from 'uniswap/src/types/onboarding' import { MobileScreens, OnboardingScreens } from 'uniswap/src/types/screens/mobile' import { getCloudProviderName } from 'uniswap/src/utils/cloud-backup/getCloudProviderName' @@ -42,8 +39,7 @@ export function BackupScreen({ navigation, route: { params } }: Props): JSX.Elem const { data: cloudStorageAvailable } = useAsyncData(isCloudStorageAvailable) - const { getImportedAccountsAddresses, getOnboardingAccountAddress, hasBackup } = - useOnboardingContext() + const { getImportedAccountsAddresses, getOnboardingAccountAddress, hasBackup } = useOnboardingContext() const onboardingAccountAddress = getOnboardingAccountAddress() const importedAccountsAddresses = getImportedAccountsAddresses() @@ -61,7 +57,7 @@ export function BackupScreen({ navigation, route: { params } }: Props): JSX.Elem }} /> ), - [navigation] + [navigation], ) useEffect(() => { @@ -93,9 +89,7 @@ export function BackupScreen({ navigation, route: { params } }: Props): JSX.Elem const onPressCloudBackup = (): void => { if (!cloudStorageAvailable) { Alert.alert( - isAndroid - ? t('account.cloud.error.unavailable.title.android') - : t('account.cloud.error.unavailable.title.ios'), + isAndroid ? t('account.cloud.error.unavailable.title.android') : t('account.cloud.error.unavailable.title.ios'), isAndroid ? t('account.cloud.error.unavailable.message.android') : t('account.cloud.error.unavailable.message.ios'), @@ -106,7 +100,7 @@ export function BackupScreen({ navigation, route: { params } }: Props): JSX.Elem style: 'default', }, { text: t('account.cloud.error.unavailable.button.cancel'), style: 'cancel' }, - ] + ], ) return } @@ -123,16 +117,13 @@ export function BackupScreen({ navigation, route: { params } }: Props): JSX.Elem } const showSkipOption = - hasBackup(address) && - (params?.importType === ImportType.SeedPhrase || params?.importType === ImportType.Restore) + hasBackup(address) && (params?.importType === ImportType.SeedPhrase || params?.importType === ImportType.Restore) const hasCloudBackup = hasBackup(address, BackupType.Cloud) const hasManualBackup = hasBackup(address, BackupType.Manual) const isCreatingNew = params?.importType === ImportType.CreateNew - const screenTitle = isCreatingNew - ? t('onboarding.backup.title.new') - : t('onboarding.backup.title.existing') + const screenTitle = isCreatingNew ? t('onboarding.backup.title.new') : t('onboarding.backup.title.existing') const options = [] options.push( } + testID={TestID.AddCloudBackup} title={t('onboarding.backup.option.cloud.title', { cloudProviderName: getCloudProviderName(), })} onPress={onPressCloudBackup} - /> + />, ) if (isCreatingNew) { options.push( @@ -155,9 +147,10 @@ export function BackupScreen({ navigation, route: { params } }: Props): JSX.Elem disabled={hasManualBackup} elementName={ElementName.AddManualBackup} icon={} + testID={TestID.AddManualBackup} title={t('onboarding.backup.option.manual.title')} onPress={onPressManualBackup} - /> + />, ) } @@ -165,24 +158,17 @@ export function BackupScreen({ navigation, route: { params } }: Props): JSX.Elem - + {options} - {!isCreatingNew && ( - - )} + {!isCreatingNew && } - {isCreatingNew && ( - - )} + {isCreatingNew && } {showSkipOption && ( - @@ -193,11 +179,7 @@ export function BackupScreen({ navigation, route: { params } }: Props): JSX.Elem ) } -function RecoveryPhraseTooltip({ - onPressEducationButton, -}: { - onPressEducationButton: () => void -}): JSX.Element { +function RecoveryPhraseTooltip({ onPressEducationButton }: { onPressEducationButton: () => void }): JSX.Element { const { t } = useTranslation() return ( + onPress={onPressEducationButton} + > {t('onboarding.tooltip.recoveryPhrase.trigger')} diff --git a/apps/mobile/src/screens/Onboarding/CloudBackupPasswordConfirmScreen.tsx b/apps/mobile/src/screens/Onboarding/CloudBackupPasswordConfirmScreen.tsx index 974924726e8..214a4bddac0 100644 --- a/apps/mobile/src/screens/Onboarding/CloudBackupPasswordConfirmScreen.tsx +++ b/apps/mobile/src/screens/Onboarding/CloudBackupPasswordConfirmScreen.tsx @@ -1,40 +1,43 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack' -import React from 'react' +import React, { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { OnboardingStackParamList } from 'src/app/navigation/types' -import { CloudBackupPasswordForm } from 'src/features/CloudBackup/CloudBackupPasswordForm' +import { CloudBackupPassword } from 'src/features/CloudBackup/CloudBackupForm' import { SafeKeyboardOnboardingScreen } from 'src/features/onboarding/SafeKeyboardOnboardingScreen' +import { Flex } from 'ui/src' import { OnboardingScreens } from 'uniswap/src/types/screens/mobile' -export type Props = NativeStackScreenProps< - OnboardingStackParamList, - OnboardingScreens.BackupCloudPasswordConfirm -> +export type Props = NativeStackScreenProps -export function CloudBackupPasswordConfirmScreen({ - navigation, - route: { params }, -}: Props): JSX.Element { +export function CloudBackupPasswordConfirmScreen({ navigation, route: { params } }: Props): JSX.Element { const { t } = useTranslation() const { password } = params - const navigateToNextScreen = (): void => { + const navigateToNextScreen = useCallback((): void => { navigation.navigate({ name: OnboardingScreens.BackupCloudProcessing, params, merge: true, }) - } + }, [navigation, params]) return ( - - - + + + + + } + subtitle={t('onboarding.cloud.confirm.description')} + title={t('onboarding.cloud.confirm.title')} + > + + + ) } diff --git a/apps/mobile/src/screens/Onboarding/CloudBackupPasswordCreateScreen.tsx b/apps/mobile/src/screens/Onboarding/CloudBackupPasswordCreateScreen.tsx index 6d7334b1b1a..e71e8bb1d7f 100644 --- a/apps/mobile/src/screens/Onboarding/CloudBackupPasswordCreateScreen.tsx +++ b/apps/mobile/src/screens/Onboarding/CloudBackupPasswordCreateScreen.tsx @@ -1,38 +1,44 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack' -import React from 'react' +import React, { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { OnboardingStackParamList } from 'src/app/navigation/types' -import { CloudBackupPasswordForm } from 'src/features/CloudBackup/CloudBackupPasswordForm' +import { CloudBackupPassword } from 'src/features/CloudBackup/CloudBackupForm' import { SafeKeyboardOnboardingScreen } from 'src/features/onboarding/SafeKeyboardOnboardingScreen' +import { Flex } from 'ui/src' import { OnboardingScreens } from 'uniswap/src/types/screens/mobile' -export type Props = NativeStackScreenProps< - OnboardingStackParamList, - OnboardingScreens.BackupCloudPasswordCreate -> +export type Props = NativeStackScreenProps -export function CloudBackupPasswordCreateScreen({ - navigation, - route: { params }, -}: Props): JSX.Element { +export function CloudBackupPasswordCreateScreen({ navigation, route: { params } }: Props): JSX.Element { const { t } = useTranslation() - const navigateToNextScreen = ({ password }: { password: string }): void => { - navigation.navigate({ - name: OnboardingScreens.BackupCloudPasswordConfirm, - params: { - ...params, - password, - }, - merge: true, - }) - } + const navigateToNextScreen = useCallback( + ({ password }: { password: string }): void => { + navigation.navigate({ + name: OnboardingScreens.BackupCloudPasswordConfirm, + params: { + ...params, + password, + }, + merge: true, + }) + }, + [navigation, params], + ) return ( - - - + + + + + } + subtitle={t('onboarding.cloud.createPassword.description')} + title={t('onboarding.cloud.createPassword.title')} + > + + + ) } diff --git a/apps/mobile/src/screens/Onboarding/CloudBackupProcessingScreen.tsx b/apps/mobile/src/screens/Onboarding/CloudBackupProcessingScreen.tsx index 90952d31124..7ddb5b10060 100644 --- a/apps/mobile/src/screens/Onboarding/CloudBackupProcessingScreen.tsx +++ b/apps/mobile/src/screens/Onboarding/CloudBackupProcessingScreen.tsx @@ -5,10 +5,7 @@ import { Screen } from 'src/components/layout/Screen' import { CloudBackupProcessingAnimation } from 'src/features/CloudBackup/CloudBackupProcessingAnimation' import { OnboardingScreens } from 'uniswap/src/types/screens/mobile' -type Props = NativeStackScreenProps< - OnboardingStackParamList, - OnboardingScreens.BackupCloudProcessing -> +type Props = NativeStackScreenProps /** Screen to perform secure recovery phrase backup to Cloud */ export function CloudBackupProcessingScreen({ diff --git a/apps/mobile/src/screens/Onboarding/LandingScreen.tsx b/apps/mobile/src/screens/Onboarding/LandingScreen.tsx index 63624e478e1..1b1536467cf 100644 --- a/apps/mobile/src/screens/Onboarding/LandingScreen.tsx +++ b/apps/mobile/src/screens/Onboarding/LandingScreen.tsx @@ -2,45 +2,40 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack' import React, { useCallback, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { useAnimatedStyle, useSharedValue, withDelay, withTiming } from 'react-native-reanimated' -import { useAppDispatch } from 'src/app/hooks' +import { useDispatch } from 'react-redux' import { OnboardingStackParamList } from 'src/app/navigation/types' -import { - LANDING_ANIMATION_DURATION, - LandingBackground, -} from 'src/components/gradients/LandingBackground' import { Screen } from 'src/components/layout/Screen' import { openModal } from 'src/features/modals/modalSlice' import { TermsOfService } from 'src/screens/Onboarding/TermsOfService' import { hideSplashScreen } from 'src/utils/splashScreen' -import { Flex, HapticFeedback, Text, TouchableArea, useIsDarkMode } from 'ui/src' +import { Flex, HapticFeedback, Text, TouchableArea } from 'ui/src' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import Trace from 'uniswap/src/features/telemetry/Trace' import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding' import { OnboardingScreens, UnitagScreens } from 'uniswap/src/types/screens/mobile' -import { isDevEnv } from 'uniswap/src/utils/env' +import { isDevEnv } from 'utilities/src/environment' +import { isDetoxBuild } from 'utilities/src/environment/constants' import { ONE_SECOND_MS } from 'utilities/src/time/time' import { useTimeout } from 'utilities/src/time/timing' +import { LANDING_ANIMATION_DURATION, LandingBackground } from 'wallet/src/components/landing/LandingBackground' import { useCanAddressClaimUnitag } from 'wallet/src/features/unitags/hooks' type Props = NativeStackScreenProps export function LandingScreen({ navigation }: Props): JSX.Element { - const dispatch = useAppDispatch() + const dispatch = useDispatch() const { t } = useTranslation() - const isDarkMode = useIsDarkMode() const actionButtonsOpacity = useSharedValue(0) - const actionButtonsStyle = useAnimatedStyle( - () => ({ opacity: actionButtonsOpacity.value }), - [actionButtonsOpacity] - ) + const actionButtonsStyle = useAnimatedStyle(() => ({ opacity: actionButtonsOpacity.value }), [actionButtonsOpacity]) useEffect(() => { - actionButtonsOpacity.value = withDelay( - LANDING_ANIMATION_DURATION, - withTiming(1, { duration: ONE_SECOND_MS }) - ) + // disables looping animation during detox e2e tests which was preventing js thread from idle + if (!isDetoxBuild) { + actionButtonsOpacity.value = withDelay(LANDING_ANIMATION_DURATION, withTiming(1, { duration: ONE_SECOND_MS })) + } }, [actionButtonsOpacity]) const { canClaimUnitag } = useCanAddressClaimUnitag() @@ -70,12 +65,10 @@ export function LandingScreen({ navigation }: Props): JSX.Element { useTimeout(hideSplashScreen, 1) return ( - // TODO(blocked by MOB-1082): delete bg prop - // dark mode onboarding asset needs to be re-exported with #131313 (surface1) as background color - + - + @@ -94,7 +87,9 @@ export function LandingScreen({ navigation }: Props): JSX.Element { shadowColor="$accent1" shadowOpacity={0.4} shadowRadius="$spacing8" - onPress={onPressCreateWallet}> + testID={TestID.CreateAccount} + onPress={onPressCreateWallet} + > {t('onboarding.landing.button.create')} @@ -106,18 +101,16 @@ export function LandingScreen({ navigation }: Props): JSX.Element { hapticFeedback alignItems="center" hitSlop={16} - testID={ElementName.ImportAccount} + testID={TestID.ImportAccount} onLongPress={async (): Promise => { if (isDevEnv()) { await HapticFeedback.selection() dispatch(openModal({ name: ModalName.Experiments })) } }} - onPress={onPressImportWallet}> - + onPress={onPressImportWallet} + > + {t('onboarding.landing.button.add')} diff --git a/apps/mobile/src/screens/Onboarding/ManualBackupScreen.tsx b/apps/mobile/src/screens/Onboarding/ManualBackupScreen.tsx index 3242a4a64e5..80127099f7c 100644 --- a/apps/mobile/src/screens/Onboarding/ManualBackupScreen.tsx +++ b/apps/mobile/src/screens/Onboarding/ManualBackupScreen.tsx @@ -13,8 +13,9 @@ import { Button, Flex, Text, useMedia, useSporeColors } from 'ui/src' import LockIcon from 'ui/src/assets/icons/lock.svg' import { iconSizes } from 'ui/src/theme' import { BottomSheetModal } from 'uniswap/src/components/modals/BottomSheetModal' -import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants' +import { ModalName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { ManualPageViewScreen, OnboardingScreens } from 'uniswap/src/types/screens/mobile' import { WarningModal } from 'wallet/src/components/modals/WarningModal/WarningModal' import { useOnboardingContext } from 'wallet/src/features/onboarding/OnboardingContext' @@ -90,7 +91,8 @@ export function ManualBackupScreen({ navigation, route: { params } }: Props): JS return ( + title={t('onboarding.recoveryPhrase.view.title')} + > {showScreenShotWarningModal && ( - {seedWarningAcknowledged ? ( - - ) : ( - - )} + {seedWarningAcknowledged ? : } - - {!seedWarningAcknowledged && ( - setSeedWarningAcknowledged(true)} /> - )} + {!seedWarningAcknowledged && setSeedWarningAcknowledged(true)} />} ) case View.SeedPhraseConfirm: @@ -127,7 +123,8 @@ export function ManualBackupScreen({ navigation, route: { params } }: Props): JS ? t('onboarding.recoveryPhrase.confirm.subtitle.combined') : t('onboarding.recoveryPhrase.confirm.subtitle.default') } - title={media.short ? undefined : t('onboarding.recoveryPhrase.confirm.title')}> + title={media.short ? undefined : t('onboarding.recoveryPhrase.confirm.title')} + > - @@ -157,14 +151,11 @@ const SeedWarningModal = ({ onPress }: { onPress: () => void }): JSX.Element => backgroundColor={colors.surface1.get()} hideHandlebar={true} isDismissible={false} - name={ModalName.SeedPhraseWarningModal}> + name={ModalName.SeedPhraseWarningModal} + > - + {t('onboarding.recoveryPhrase.warning.final.title')} @@ -172,13 +163,7 @@ const SeedWarningModal = ({ onPress }: { onPress: () => void }): JSX.Element => {t('onboarding.recoveryPhrase.warning.final.message')} - diff --git a/apps/mobile/src/screens/Onboarding/NotificationsSetupScreen.tsx b/apps/mobile/src/screens/Onboarding/NotificationsSetupScreen.tsx index c3e05af8ac7..69eafcdb07a 100644 --- a/apps/mobile/src/screens/Onboarding/NotificationsSetupScreen.tsx +++ b/apps/mobile/src/screens/Onboarding/NotificationsSetupScreen.tsx @@ -1,9 +1,8 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack' -import React, { useCallback, useEffect } from 'react' +import React, { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { Alert, Image, Platform, StyleSheet } from 'react-native' import { OnboardingStackParamList } from 'src/app/navigation/types' -import { BackButton } from 'src/components/buttons/BackButton' import { useBiometricContext } from 'src/features/biometrics/context' import { useBiometricAppSettings } from 'src/features/biometrics/hooks' import { promptPushPermission } from 'src/features/notifications/Onesignal' @@ -14,7 +13,8 @@ import { ONBOARDING_NOTIFICATIONS_DARK, ONBOARDING_NOTIFICATIONS_LIGHT } from 'u import Trace from 'uniswap/src/features/telemetry/Trace' import { ElementName } from 'uniswap/src/features/telemetry/constants' import i18n from 'uniswap/src/i18n/i18n' -import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' +import { OnboardingEntryPoint } from 'uniswap/src/types/onboarding' import { OnboardingScreens } from 'uniswap/src/types/screens/mobile' import { isIOS } from 'utilities/src/platform' import { useOnboardingContext } from 'wallet/src/features/onboarding/OnboardingContext' @@ -32,7 +32,7 @@ export const showNotificationSettingsAlert = (): void => { { text: i18n.t('common.button.cancel'), }, - ] + ], ) } @@ -45,35 +45,6 @@ export function NotificationsSetupScreen({ navigation, route: { params } }: Prop const onCompleteOnboarding = useCompleteOnboardingCallback(params) - const renderBackButton = useCallback( - (nav: OnboardingScreens): JSX.Element => ( - navigation.navigate({ name: nav, params, merge: true })} - /> - ), - [navigation, params] - ) - - /* For some screens, we want to override the back button to go to a different screen. - * This helps avoid re-visiting loading states or confirmation views. - */ - useEffect(() => { - const shouldOverrideBackButton = [ - ImportType.SeedPhrase, - ImportType.Restore, - ImportType.CreateNew, - ].includes(params.importType) - if (shouldOverrideBackButton) { - const nextScreen = - params.importType === ImportType.Restore - ? OnboardingScreens.RestoreCloudBackup - : OnboardingScreens.Backup - navigation.setOptions({ - headerLeft: () => renderBackButton(nextScreen), - }) - } - }, [navigation, params, renderBackButton]) - const navigateToNextScreen = useCallback(async () => { // Skip security setup if already enabled or already imported seed phrase if ( @@ -85,14 +56,7 @@ export function NotificationsSetupScreen({ navigation, route: { params } }: Prop } else { navigation.navigate({ name: OnboardingScreens.Security, params, merge: true }) } - }, [ - deviceSupportsBiometrics, - hasSeedPhrase, - isBiometricAuthEnabled, - navigation, - onCompleteOnboarding, - params, - ]) + }, [deviceSupportsBiometrics, hasSeedPhrase, isBiometricAuthEnabled, navigation, onCompleteOnboarding, params]) const onPressEnableNotifications = useCallback(async () => { promptPushPermission(() => { @@ -104,14 +68,16 @@ export function NotificationsSetupScreen({ navigation, route: { params } }: Prop return ( + title={t('onboarding.notification.title')} + > - + {t('common.button.later')} @@ -133,9 +99,7 @@ const NotificationsBackgroundImage = (): JSX.Element => { diff --git a/apps/mobile/src/screens/Onboarding/OnboardingElements/HeartElement.tsx b/apps/mobile/src/screens/Onboarding/OnboardingElements/HeartElement.tsx deleted file mode 100644 index 1cdb0eb29cd..00000000000 --- a/apps/mobile/src/screens/Onboarding/OnboardingElements/HeartElement.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { Flex } from 'ui/src' -import { Heart } from 'ui/src/components/icons' -import { colors, iconSizes, opacify } from 'ui/src/theme' - -export const HeartElement = (): JSX.Element => { - return ( - - - - ) -} diff --git a/apps/mobile/src/screens/Onboarding/OnboardingElements/SendElement.tsx b/apps/mobile/src/screens/Onboarding/OnboardingElements/SendElement.tsx deleted file mode 100644 index 198fb1ff69e..00000000000 --- a/apps/mobile/src/screens/Onboarding/OnboardingElements/SendElement.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { Flex } from 'ui/src' -import { SendAction } from 'ui/src/components/icons' -import { colors, iconSizes, opacify } from 'ui/src/theme' - -export const SendElement = (): JSX.Element => { - return ( - - - - ) -} diff --git a/apps/mobile/src/screens/Onboarding/SecuritySetupScreen.tsx b/apps/mobile/src/screens/Onboarding/SecuritySetupScreen.tsx index a831faf1eda..1ce5957e73f 100644 --- a/apps/mobile/src/screens/Onboarding/SecuritySetupScreen.tsx +++ b/apps/mobile/src/screens/Onboarding/SecuritySetupScreen.tsx @@ -3,7 +3,7 @@ import { BlurView } from 'expo-blur' import React, { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { ActivityIndicator, Alert, Image, Platform, StyleSheet } from 'react-native' -import { useAppDispatch } from 'src/app/hooks' +import { useDispatch } from 'react-redux' import { OnboardingStackParamList } from 'src/app/navigation/types' import { BiometricAuthWarningModal } from 'src/components/Settings/BiometricAuthWarningModal' import { enroll, tryLocalAuthenticate } from 'src/features/biometrics' @@ -23,10 +23,11 @@ import FingerprintIcon from 'ui/src/assets/icons/fingerprint.svg' import { borderRadii, imageSizes } from 'ui/src/theme' import Trace from 'uniswap/src/features/telemetry/Trace' import { ElementName } from 'uniswap/src/features/telemetry/constants' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { ImportType } from 'uniswap/src/types/onboarding' import { OnboardingScreens } from 'uniswap/src/types/screens/mobile' +import { opacify } from 'uniswap/src/utils/colors' import { isIOS } from 'utilities/src/platform' -import { opacify } from 'wallet/src/utils/colors' import { openSettings } from 'wallet/src/utils/linking' type Props = NativeStackScreenProps @@ -34,7 +35,7 @@ type Props = NativeStackScreenProps {showWarningModal && ( - + )} {isLoadingAccount && ( - + )} @@ -125,7 +113,8 @@ export function SecuritySetupScreen({ route: { params } }: Props): JSX.Element { }) : t('onboarding.security.subtitle.android') } - title={t('onboarding.security.title')}> + title={t('onboarding.security.title')} + > @@ -140,30 +129,19 @@ export function SecuritySetupScreen({ route: { params } }: Props): JSX.Element { borderColor: opacify(15, colors.sporeWhite.val), backgroundColor: opacify(35, colors.surface1.val), }} - top={0}> - + top={0} + > + {isTouchIdDevice ? ( - + ) : ( - + )} - + {t('common.button.later')} diff --git a/apps/mobile/src/screens/Onboarding/TermsOfService.tsx b/apps/mobile/src/screens/Onboarding/TermsOfService.tsx index b30de807e53..447d6f5eaa8 100644 --- a/apps/mobile/src/screens/Onboarding/TermsOfService.tsx +++ b/apps/mobile/src/screens/Onboarding/TermsOfService.tsx @@ -1,7 +1,7 @@ import { Trans } from 'react-i18next' import { Text } from 'ui/src' import { uniswapUrls } from 'uniswap/src/constants/urls' -import { openUri } from 'wallet/src/utils/linking' +import { openUri } from 'uniswap/src/utils/linking' export function TermsOfService(): JSX.Element { return ( diff --git a/apps/mobile/src/screens/Onboarding/WelcomeWalletScreen.tsx b/apps/mobile/src/screens/Onboarding/WelcomeWalletScreen.tsx index eeecf0314c9..0310e181af1 100644 --- a/apps/mobile/src/screens/Onboarding/WelcomeWalletScreen.tsx +++ b/apps/mobile/src/screens/Onboarding/WelcomeWalletScreen.tsx @@ -11,6 +11,7 @@ import LockIcon from 'ui/src/assets/icons/lock.svg' import { fonts, iconSizes, opacify } from 'ui/src/theme' import Trace from 'uniswap/src/features/telemetry/Trace' import { ElementName } from 'uniswap/src/features/telemetry/constants' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { MobileScreens, OnboardingScreens } from 'uniswap/src/types/screens/mobile' import { NumberType } from 'utilities/src/format/types' import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon' @@ -57,9 +58,7 @@ export function WelcomeWalletScreen({ navigation, route: { params } }: Props): J const zeroBalance = convertFiatAmountFormatted(0, NumberType.PortfolioBalance) - const displayName = unitagClaim - ? { type: DisplayNameType.Unitag, name: unitagClaim.username } - : walletName + const displayName = unitagClaim ? { type: DisplayNameType.Unitag, name: unitagClaim.username } : walletName return ( @@ -81,11 +80,7 @@ export function WelcomeWalletScreen({ navigation, route: { params } }: Props): J size={iconSizes.icon64} /> )} - + + variant="heading3" + > {t('onboarding.wallet.title')} + variant="subheading2" + > {t('onboarding.wallet.description.full')} @@ -123,12 +120,9 @@ export function WelcomeWalletScreen({ navigation, route: { params } }: Props): J - + style={{ backgroundColor: opacify(10, colors.sporeWhite.val) }} + > + {t('onboarding.wallet.continue')} @@ -137,7 +131,7 @@ export function WelcomeWalletScreen({ navigation, route: { params } }: Props): J } - testID={ElementName.Next} + testID={TestID.Next} onPress={onPressNext} /> diff --git a/apps/mobile/src/screens/ReceiveCryptoModal.tsx b/apps/mobile/src/screens/ReceiveCryptoModal.tsx index bad42cec4c3..bb3330df2a2 100644 --- a/apps/mobile/src/screens/ReceiveCryptoModal.tsx +++ b/apps/mobile/src/screens/ReceiveCryptoModal.tsx @@ -1,35 +1,29 @@ +import { SharedEventName } from '@uniswap/analytics-events' import { useTranslation } from 'react-i18next' +import { useDispatch } from 'react-redux' import { useAppSelector } from 'src/app/hooks' import { ServiceProviderSelector } from 'src/features/fiatOnRamp/ExchangeTransferServiceProviderSelector' import { closeModal, openModal } from 'src/features/modals/modalSlice' import { selectModalState } from 'src/features/modals/selectModalState' -import { - Flex, - HapticFeedback, - ImpactFeedbackStyle, - Separator, - Text, - TouchableArea, - useSporeColors, -} from 'ui/src' +import { Flex, HapticFeedback, ImpactFeedbackStyle, Separator, Text, TouchableArea, useSporeColors } from 'ui/src' import { CopySheets, QrCode } from 'ui/src/components/icons' import { iconSizes } from 'ui/src/theme' import { BottomSheetModal } from 'uniswap/src/components/modals/BottomSheetModal' -import { ModalName } from 'uniswap/src/features/telemetry/constants' +import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants' +import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' +import { setClipboard } from 'uniswap/src/utils/clipboard' import { ScannerModalState } from 'wallet/src/components/QRCodeScanner/constants' import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' import { pushNotification } from 'wallet/src/features/notifications/slice' import { AppNotificationType, CopyNotificationType } from 'wallet/src/features/notifications/types' import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' -import { useAppDispatch } from 'wallet/src/state' -import { setClipboard } from 'wallet/src/utils/clipboard' const ACCOUNT_IMAGE_SIZE = 52 const ICON_SIZE = 32 const ICON_BORDER_RADIUS = 100 function AccountCardItem({ onClose }: { onClose: () => void }): JSX.Element { - const dispatch = useAppDispatch() + const dispatch = useDispatch() const activeAccountAddress = useActiveAccountAddressWithThrow() const onPressCopyAddress = async (): Promise => { @@ -39,22 +33,21 @@ function AccountCardItem({ onClose }: { onClose: () => void }): JSX.Element { pushNotification({ type: AppNotificationType.Copied, copyType: CopyNotificationType.Address, - }) + }), ) + sendAnalyticsEvent(SharedEventName.ELEMENT_CLICKED, { + element: ElementName.CopyAddress, + modal: ModalName.ReceiveCryptoModal, + }) } const onPressShowWalletQr = (): void => { onClose() - dispatch( - openModal({ name: ModalName.WalletConnectScan, initialState: ScannerModalState.WalletQr }) - ) + dispatch(openModal({ name: ModalName.WalletConnectScan, initialState: ScannerModalState.WalletQr })) } return ( - + void }): JSX.Element { borderRadius="$rounded20" borderWidth="$spacing1" gap="$spacing12" - p="$spacing12"> + p="$spacing12" + > void }): JSX.Element { /> - + + width={ICON_SIZE} + > @@ -93,7 +85,8 @@ function AccountCardItem({ onClose }: { onClose: () => void }): JSX.Element { backgroundColor="$surface3" borderRadius={ICON_BORDER_RADIUS} height={ICON_SIZE} - width={ICON_SIZE}> + width={ICON_SIZE} + > @@ -105,7 +98,7 @@ function AccountCardItem({ onClose }: { onClose: () => void }): JSX.Element { export function ReceiveCryptoModal(): JSX.Element { const colors = useSporeColors() - const dispatch = useAppDispatch() + const dispatch = useDispatch() const { t } = useTranslation() const { initialState } = useAppSelector(selectModalState(ModalName.ReceiveCryptoModal)) @@ -120,7 +113,8 @@ export function ReceiveCryptoModal(): JSX.Element { hideKeyboardOnSwipeDown backgroundColor={colors.surface1.get()} name={ModalName.ReceiveCryptoModal} - onClose={onClose}> + onClose={onClose} + > diff --git a/apps/mobile/src/screens/SettingsAppearanceScreen.tsx b/apps/mobile/src/screens/SettingsAppearanceScreen.tsx index eb96c4982ee..0d8cfebeb58 100644 --- a/apps/mobile/src/screens/SettingsAppearanceScreen.tsx +++ b/apps/mobile/src/screens/SettingsAppearanceScreen.tsx @@ -2,7 +2,7 @@ import { Action } from '@reduxjs/toolkit' import React from 'react' import { useTranslation } from 'react-i18next' import { SvgProps } from 'react-native-svg' -import { useAppDispatch } from 'src/app/hooks' +import { useDispatch } from 'react-redux' import { BackHeader } from 'src/components/layout/BackHeader' import { Screen } from 'src/components/layout/Screen' import { Flex, Text, TouchableArea, useSporeColors } from 'ui/src' @@ -12,10 +12,7 @@ import MoonIcon from 'ui/src/assets/icons/moon.svg' import SunIcon from 'ui/src/assets/icons/sun.svg' import { iconSizes } from 'ui/src/theme' import { useCurrentAppearanceSetting } from 'wallet/src/features/appearance/hooks' -import { - AppearanceSettingType, - setSelectedAppearanceSettings, -} from 'wallet/src/features/appearance/slice' +import { AppearanceSettingType, setSelectedAppearanceSettings } from 'wallet/src/features/appearance/slice' export function SettingsAppearanceScreen(): JSX.Element { const { t } = useTranslation() @@ -61,15 +58,9 @@ interface AppearanceOptionProps { Icon: React.FC } -function AppearanceOption({ - active, - title, - subtitle, - Icon, - option, -}: AppearanceOptionProps): JSX.Element { +function AppearanceOption({ active, title, subtitle, Icon, option }: AppearanceOptionProps): JSX.Element { const colors = useSporeColors() - const dispatch = useAppDispatch() + const dispatch = useDispatch() const showCheckMark = active ? 1 : 0 @@ -79,13 +70,9 @@ function AppearanceOption({ flexDirection="row" justifyContent="space-between" py="$spacing12" - onPress={(): Action => dispatch(setSelectedAppearanceSettings(option))}> - + onPress={(): Action => dispatch(setSelectedAppearanceSettings(option))} + > + {title} diff --git a/apps/mobile/src/screens/SettingsBiometricAuthScreen.tsx b/apps/mobile/src/screens/SettingsBiometricAuthScreen.tsx index 66bdd4fc439..436e62d703a 100644 --- a/apps/mobile/src/screens/SettingsBiometricAuthScreen.tsx +++ b/apps/mobile/src/screens/SettingsBiometricAuthScreen.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { Alert, ListRenderItemInfo } from 'react-native' import { FlatList } from 'react-native-gesture-handler' -import { useAppDispatch } from 'src/app/hooks' +import { useDispatch } from 'react-redux' import { BiometricAuthWarningModal } from 'src/components/Settings/BiometricAuthWarningModal' import { BackHeader } from 'src/components/layout/BackHeader' import { Screen } from 'src/components/layout/Screen' @@ -38,34 +38,30 @@ type BiometricPromptTriggerArgs = { export function SettingsBiometricAuthScreen(): JSX.Element { const { t } = useTranslation() - const dispatch = useAppDispatch() + const dispatch = useDispatch() const [showUnsafeWarningModal, setShowUnsafeWarningModal] = useState(false) - const [unsafeWarningModalType, setUnsafeWarningModalType] = useState( - null - ) + const [unsafeWarningModalType, setUnsafeWarningModalType] = useState(null) const onCloseModal = useCallback(() => setShowUnsafeWarningModal(false), []) const { touchId } = useDeviceSupportsBiometricAuth() const biometricsMethod = useBiometricName(touchId) const { requiredForAppAccess, requiredForTransactions } = useBiometricAppSettings() - const { trigger } = useBiometricPrompt( - (args?: BiometricPromptTriggerArgs) => { - if (!args) { - return - } - const { biometricAppSettingType, newValue } = args - switch (biometricAppSettingType) { - case BiometricSettingType.RequiredForAppAccess: - dispatch(setRequiredForAppAccess(newValue)) - break - case BiometricSettingType.RequiredForTransactions: - dispatch(setRequiredForTransactions(newValue)) - break - } + const { trigger } = useBiometricPrompt((args?: BiometricPromptTriggerArgs) => { + if (!args) { + return } - ) + const { biometricAppSettingType, newValue } = args + switch (biometricAppSettingType) { + case BiometricSettingType.RequiredForAppAccess: + dispatch(setRequiredForAppAccess(newValue)) + break + case BiometricSettingType.RequiredForTransactions: + dispatch(setRequiredForTransactions(newValue)) + break + } + }) const options: BiometricAuthSetting[] = useMemo((): BiometricAuthSetting[] => { const handleOSBiometricAuthTurnedOff = (): void => { @@ -80,7 +76,7 @@ export function SettingsBiometricAuthScreen(): JSX.Element { [ { text: t('common.navigation.systemSettings'), onPress: openSettings }, { text: t('common.button.cancel') }, - ] + ], ) : Alert.alert( isAndroid @@ -89,10 +85,7 @@ export function SettingsBiometricAuthScreen(): JSX.Element { isAndroid ? t('settings.setting.biometrics.unavailable.message.android') : t('settings.setting.biometrics.unavailable.message.ios', { biometricsMethod }), - [ - { text: t('common.button.setup'), onPress: enroll }, - { text: t('common.button.cancel') }, - ] + [{ text: t('common.button.setup'), onPress: enroll }, { text: t('common.button.cancel') }], ) } @@ -182,7 +175,8 @@ export function SettingsBiometricAuthScreen(): JSX.Element { activeOpacity={1} onPress={(): void => { onValueChange(!value) - }}> + }} + > @@ -213,9 +207,7 @@ export function SettingsBiometricAuthScreen(): JSX.Element { )} - - {isAndroid ? t('settings.setting.biometrics.title') : biometricsMethod} - + {isAndroid ? t('settings.setting.biometrics.title') : biometricsMethod} +type Props = NativeStackScreenProps -export function SettingsCloudBackupPasswordConfirmScreen({ - navigation, - route: { params }, -}: Props): JSX.Element { +export function SettingsCloudBackupPasswordConfirmScreen({ navigation, route: { params } }: Props): JSX.Element { const { t } = useTranslation() const { password } = params - const navigateToNextScreen = (): void => { + const navigateToNextScreen = useCallback((): void => { navigation.navigate({ name: MobileScreens.SettingsCloudBackupProcessing, params, merge: true, }) - } + }, [navigation, params]) return ( - - - - + + + + + } + header={} + > + {t('onboarding.cloud.confirm.title')} @@ -41,12 +44,8 @@ export function SettingsCloudBackupPasswordConfirmScreen({ {t('onboarding.cloud.confirm.description')} - - - + + + ) } diff --git a/apps/mobile/src/screens/SettingsCloudBackupPasswordCreateScreen.tsx b/apps/mobile/src/screens/SettingsCloudBackupPasswordCreateScreen.tsx index 15c13979421..3e98b749fbe 100644 --- a/apps/mobile/src/screens/SettingsCloudBackupPasswordCreateScreen.tsx +++ b/apps/mobile/src/screens/SettingsCloudBackupPasswordCreateScreen.tsx @@ -1,22 +1,19 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack' -import React, { useState } from 'react' +import React, { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' -import { ScrollView } from 'react-native' import { SettingsStackParamList } from 'src/app/navigation/types' import { BackHeader } from 'src/components/layout/BackHeader' -import { Screen } from 'src/components/layout/Screen' -import { CloudBackupPasswordForm } from 'src/features/CloudBackup/CloudBackupPasswordForm' +import { SafeKeyboardScreen } from 'src/components/layout/SafeKeyboardScreen' +import { CloudBackupPassword } from 'src/features/CloudBackup/CloudBackupForm' import { Button, Flex, Text, useSporeColors } from 'ui/src' import { OSDynamicCloudIcon } from 'ui/src/components/icons' import { BottomSheetModal } from 'uniswap/src/components/modals/BottomSheetModal' -import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants' +import { ModalName } from 'uniswap/src/features/telemetry/constants' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { MobileScreens } from 'uniswap/src/types/screens/mobile' import { getCloudProviderName } from 'uniswap/src/utils/cloud-backup/getCloudProviderName' -type Props = NativeStackScreenProps< - SettingsStackParamList, - MobileScreens.SettingsCloudBackupPasswordCreate -> +type Props = NativeStackScreenProps // This screen is visited when no iCloud backup exists (checked from settings) export function SettingsCloudBackupPasswordCreateScreen({ @@ -30,23 +27,32 @@ export function SettingsCloudBackupPasswordCreateScreen({ const [showCloudBackupInfoModal, setShowCloudBackupInfoModal] = useState(true) - const navigateToNextScreen = ({ password }: { password: string }): void => { - navigation.navigate({ - name: MobileScreens.SettingsCloudBackupPasswordConfirm, - params: { - password, - address, - }, - merge: true, - }) - } + const navigateToNextScreen = useCallback( + ({ password }: { password: string }): void => { + navigation.navigate({ + name: MobileScreens.SettingsCloudBackupPasswordConfirm, + params: { + password, + address, + }, + merge: true, + }) + }, + [navigation, address], + ) return ( - - - - - + + + + + } + header={} + > + + {t('settings.setting.backup.create.title', { cloudProviderName: getCloudProviderName(), })} @@ -57,11 +63,9 @@ export function SettingsCloudBackupPasswordCreateScreen({ })} - + {showCloudBackupInfoModal && ( - + @@ -82,17 +86,14 @@ export function SettingsCloudBackupPasswordCreateScreen({ - )} - - + + ) } diff --git a/apps/mobile/src/screens/SettingsCloudBackupProcessingScreen.tsx b/apps/mobile/src/screens/SettingsCloudBackupProcessingScreen.tsx index 2a3babd3e8d..f104db4911f 100644 --- a/apps/mobile/src/screens/SettingsCloudBackupProcessingScreen.tsx +++ b/apps/mobile/src/screens/SettingsCloudBackupProcessingScreen.tsx @@ -5,10 +5,7 @@ import { Screen } from 'src/components/layout/Screen' import { CloudBackupProcessingAnimation } from 'src/features/CloudBackup/CloudBackupProcessingAnimation' import { MobileScreens } from 'uniswap/src/types/screens/mobile' -type Props = NativeStackScreenProps< - SettingsStackParamList, - MobileScreens.SettingsCloudBackupProcessing -> +type Props = NativeStackScreenProps export function SettingsCloudBackupProcessingScreen({ navigation, diff --git a/apps/mobile/src/screens/SettingsCloudBackupStatus.tsx b/apps/mobile/src/screens/SettingsCloudBackupStatus.tsx index daedaf0e8f5..b6db1368cfb 100644 --- a/apps/mobile/src/screens/SettingsCloudBackupStatus.tsx +++ b/apps/mobile/src/screens/SettingsCloudBackupStatus.tsx @@ -2,7 +2,7 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack' import React, { useState } from 'react' import { useTranslation } from 'react-i18next' import { Alert } from 'react-native' -import { useAppDispatch } from 'src/app/hooks' +import { useDispatch } from 'react-redux' import { SettingsStackParamList } from 'src/app/navigation/types' import { BackHeader } from 'src/components/layout/BackHeader' import { Screen } from 'src/components/layout/Screen' @@ -12,21 +12,15 @@ import { useBiometricAppSettings, useBiometricPrompt } from 'src/features/biomet import { Button, Flex, Text, useSporeColors } from 'ui/src' import Checkmark from 'ui/src/assets/icons/check.svg' import { iconSizes } from 'ui/src/theme' -import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants' +import { ModalName } from 'uniswap/src/features/telemetry/constants' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { MobileScreens } from 'uniswap/src/types/screens/mobile' import { getCloudProviderName } from 'uniswap/src/utils/cloud-backup/getCloudProviderName' import { logger } from 'utilities/src/logger/logger' import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' import { WarningModal } from 'wallet/src/components/modals/WarningModal/WarningModal' -import { - EditAccountAction, - editAccountActions, -} from 'wallet/src/features/wallet/accounts/editAccountSaga' -import { - AccountType, - BackupType, - SignerMnemonicAccount, -} from 'wallet/src/features/wallet/accounts/types' +import { EditAccountAction, editAccountActions } from 'wallet/src/features/wallet/accounts/editAccountSaga' +import { AccountType, BackupType, SignerMnemonicAccount } from 'wallet/src/features/wallet/accounts/types' import { useAccounts } from 'wallet/src/features/wallet/hooks' type Props = NativeStackScreenProps @@ -39,12 +33,12 @@ export function SettingsCloudBackupStatus({ }: Props): JSX.Element { const { t } = useTranslation() const colors = useSporeColors() - const dispatch = useAppDispatch() + const dispatch = useDispatch() const accounts = useAccounts() const mnemonicId = (accounts[address] as SignerMnemonicAccount)?.mnemonicId const backups = useCloudBackups(mnemonicId) const associatedAccounts = Object.values(accounts).filter( - (a) => a.type === AccountType.SignerMnemonic && a.mnemonicId === mnemonicId + (a) => a.type === AccountType.SignerMnemonic && a.mnemonicId === mnemonicId, ) const [showBackupDeleteWarning, setShowBackupDeleteWarning] = useState(false) @@ -64,7 +58,7 @@ export function SettingsCloudBackupStatus({ type: EditAccountAction.RemoveBackupMethod, address, backupMethod: BackupType.Cloud, - }) + }), ) setShowBackupDeleteWarning(false) navigation.navigate(MobileScreens.Settings) @@ -75,7 +69,7 @@ export function SettingsCloudBackupStatus({ Alert.alert( t('settings.setting.backup.error.title', { cloudProviderName: getCloudProviderName() }), t('settings.setting.backup.error.message.short'), - [{ text: t('common.button.ok'), style: 'default' }] + [{ text: t('common.button.ok'), style: 'default' }], ) } } @@ -115,11 +109,7 @@ export function SettingsCloudBackupStatus({ {/* @TODO: [MOB-249] Add non-backed up state once we have more options on this page */} - + {googleDriveEmail && ( @@ -130,11 +120,12 @@ export function SettingsCloudBackupStatus({ @@ -151,7 +142,8 @@ export function SettingsCloudBackupStatus({ onClose={(): void => { setShowBackupDeleteWarning(false) }} - onConfirm={onConfirmDeleteBackup}> + onConfirm={onConfirmDeleteBackup} + > {associatedAccounts.length > 1 && ( diff --git a/apps/mobile/src/screens/SettingsFiatCurrencyModal.tsx b/apps/mobile/src/screens/SettingsFiatCurrencyModal.tsx index 0b4e75d9593..8128ff32dad 100644 --- a/apps/mobile/src/screens/SettingsFiatCurrencyModal.tsx +++ b/apps/mobile/src/screens/SettingsFiatCurrencyModal.tsx @@ -1,7 +1,7 @@ import React, { useCallback } from 'react' import { useTranslation } from 'react-i18next' +import { useDispatch } from 'react-redux' import { Action } from 'redux' -import { useAppDispatch } from 'src/app/hooks' import { VirtualizedList } from 'src/components/layout/VirtualizedList' import { closeModal } from 'src/features/modals/modalSlice' import { Flex, Text, TouchableArea, useSporeColors } from 'ui/src' @@ -13,14 +13,15 @@ import { useAppFiatCurrency, useFiatCurrencyInfo } from 'wallet/src/features/fia import { setCurrentFiatCurrency } from 'wallet/src/features/fiatCurrency/slice' export function SettingsFiatCurrencyModal(): JSX.Element { - const dispatch = useAppDispatch() + const dispatch = useDispatch() const { t } = useTranslation() return ( dispatch(closeModal({ name: ModalName.FiatCurrencySelector }))}> + onClose={(): Action => dispatch(closeModal({ name: ModalName.FiatCurrencySelector }))} + > {t('settings.setting.currency.title')} @@ -41,11 +42,7 @@ function FiatCurrencySelection({ onClose }: { onClose: () => void }): JSX.Elemen return ( {ORDERED_CURRENCIES.map((currency) => ( - + ))} ) @@ -58,7 +55,7 @@ interface FiatCurrencyOptionProps { } function FiatCurrencyOption({ active, currency, onPress }: FiatCurrencyOptionProps): JSX.Element { - const dispatch = useAppDispatch() + const dispatch = useDispatch() const colors = useSporeColors() const { name, code } = useFiatCurrencyInfo(currency) @@ -68,12 +65,7 @@ function FiatCurrencyOption({ active, currency, onPress }: FiatCurrencyOptionPro }, [dispatch, onPress, currency]) return ( - + {name} diff --git a/apps/mobile/src/screens/SettingsScreen.tsx b/apps/mobile/src/screens/SettingsScreen.tsx index 88ce29c3979..bc256a75f9e 100644 --- a/apps/mobile/src/screens/SettingsScreen.tsx +++ b/apps/mobile/src/screens/SettingsScreen.tsx @@ -1,4 +1,3 @@ -/* eslint-disable max-lines */ import { useNavigation } from '@react-navigation/core' import { default as React, useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -6,7 +5,6 @@ import { Image, ListRenderItemInfo, SectionList, StyleSheet } from 'react-native import { FadeInDown, FadeOutUp } from 'react-native-reanimated' import { SvgProps } from 'react-native-svg' import { useDispatch } from 'react-redux' -import { useAppDispatch } from 'src/app/hooks' import { OnboardingStackNavigationProp, SettingsStackNavigationProp, @@ -24,16 +22,7 @@ import { useBiometricContext } from 'src/features/biometrics/context' import { useBiometricName, useDeviceSupportsBiometricAuth } from 'src/features/biometrics/hooks' import { useWalletRestore } from 'src/features/wallet/hooks' import { getFullAppVersion } from 'src/utils/version' -import { - Button, - Flex, - IconProps, - Text, - TouchableArea, - useDeviceInsets, - useIsDarkMode, - useSporeColors, -} from 'ui/src' +import { Button, Flex, IconProps, Text, TouchableArea, useDeviceInsets, useIsDarkMode, useSporeColors } from 'ui/src' import { AVATARS_DARK, AVATARS_LIGHT } from 'ui/src/assets' import BookOpenIcon from 'ui/src/assets/icons/book-open.svg' import ContrastIcon from 'ui/src/assets/icons/contrast.svg' @@ -69,11 +58,7 @@ import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' import { useCurrentAppearanceSetting } from 'wallet/src/features/appearance/hooks' import { useAppFiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks' import { useCurrentLanguageInfo } from 'wallet/src/features/language/hooks' -import { - AccountType, - BackupType, - SignerMnemonicAccount, -} from 'wallet/src/features/wallet/accounts/types' +import { AccountType, BackupType, SignerMnemonicAccount } from 'wallet/src/features/wallet/accounts/types' import { useAccounts, useHideSmallBalancesSetting, @@ -89,7 +74,7 @@ import { export function SettingsScreen(): JSX.Element { const navigation = useNavigation() - const dispatch = useAppDispatch() + const dispatch = useDispatch() const colors = useSporeColors() const insets = useDeviceInsets() const { deviceSupportsBiometrics } = useBiometricContext() @@ -98,8 +83,7 @@ export function SettingsScreen(): JSX.Element { const currencyConversionEnabled = useFeatureFlag(FeatureFlags.CurrencyConversion) // check if device supports biometric authentication, if not, hide option - const { touchId: isTouchIdSupported, faceId: isFaceIdSupported } = - useDeviceSupportsBiometricAuth() + const { touchId: isTouchIdSupported, faceId: isFaceIdSupported } = useDeviceSupportsBiometricAuth() const biometricsMethod = useBiometricName(isTouchIdSupported) const currentAppearanceSetting = useCurrentAppearanceSetting() @@ -151,8 +135,8 @@ export function SettingsScreen(): JSX.Element { currentAppearanceSetting === 'system' ? t('settings.setting.appearance.option.device.title') : currentAppearanceSetting === 'dark' - ? t('settings.setting.appearance.option.dark.title') - : t('settings.setting.appearance.option.light.title'), + ? t('settings.setting.appearance.option.dark.title') + : t('settings.setting.appearance.option.light.title'), icon: , }, @@ -199,15 +183,10 @@ export function SettingsScreen(): JSX.Element { ...(deviceSupportsBiometrics ? [ { - screen: - MobileScreens.SettingsBiometricAuth as MobileScreens.SettingsBiometricAuth, + screen: MobileScreens.SettingsBiometricAuth as MobileScreens.SettingsBiometricAuth, isHidden: !isTouchIdSupported && !isFaceIdSupported, text: isAndroid ? t('settings.setting.biometrics.title') : biometricsMethod, - icon: isTouchIdSupported ? ( - - ) : ( - - ), + icon: isTouchIdSupported ? : , }, ] : []), @@ -222,8 +201,8 @@ export function SettingsScreen(): JSX.Element { screen: walletNeedsRestore ? MobileScreens.OnboardingStack : hasCloudBackup - ? MobileScreens.SettingsCloudBackupStatus - : MobileScreens.SettingsCloudBackupPasswordCreate, + ? MobileScreens.SettingsCloudBackupStatus + : MobileScreens.SettingsCloudBackupPasswordCreate, screenProps: walletNeedsRestore ? { screen: OnboardingScreens.RestoreCloudBackupLoading, @@ -256,7 +235,7 @@ export function SettingsScreen(): JSX.Element { { screen: MobileScreens.WebView, screenProps: { - uriLink: uniswapUrls.helpArticleUrls.walletHelp, + uriLink: uniswapUrls.helpUrl, headerTitle: t('settings.action.help'), }, text: t('settings.action.help'), @@ -323,9 +302,7 @@ export function SettingsScreen(): JSX.Element { const renderItem = ({ item, - }: ListRenderItemInfo< - SettingsSectionItem | SettingsSectionItemComponent - >): JSX.Element | null => { + }: ListRenderItemInfo): JSX.Element | null => { if (item.isHidden) { return null } @@ -336,9 +313,7 @@ export function SettingsScreen(): JSX.Element { } return ( - {t('settings.title')}}> + {t('settings.title')}}> + }} + > @@ -385,12 +361,7 @@ function OnboardingRow({ iconProps }: { iconProps: SvgProps }): JSX.Element { Onboarding - + ) @@ -434,43 +405,35 @@ function WalletSettings(): JSX.Element { {t('settings.section.wallet.title')} - {allAccounts - .slice(0, showAll ? allAccounts.length : DEFAULT_ACCOUNTS_TO_DISPLAY) - .map((account) => { - const isViewOnlyWallet = account.type === AccountType.Readonly + {allAccounts.slice(0, showAll ? allAccounts.length : DEFAULT_ACCOUNTS_TO_DISPLAY).map((account) => { + const isViewOnlyWallet = account.type === AccountType.Readonly - return ( - handleNavigation(account.address)}> - - - - - - ) - })} + return ( + handleNavigation(account.address)} + > + + + + + + ) + })} {allAccounts.length > DEFAULT_ACCOUNTS_TO_DISPLAY && ( )} @@ -490,18 +453,13 @@ function FooterSettings(): JSX.Element { setShowSignature(false) } : (): void => undefined, - SIGNATURE_VISIBLE_DURATION + SIGNATURE_VISIBLE_DURATION, ) return ( {showSignature ? ( - + {t('settings.footer')} @@ -521,7 +479,8 @@ function FooterSettings(): JSX.Element { variant="body2" onLongPress={(): void => { setShowSignature(true) - }}> + }} + > {t('settings.version', { appVersion: getFullAppVersion() })} diff --git a/apps/mobile/src/screens/SettingsViewSeedPhraseScreen.tsx b/apps/mobile/src/screens/SettingsViewSeedPhraseScreen.tsx index b749ddd1ae8..2e4c69774c0 100644 --- a/apps/mobile/src/screens/SettingsViewSeedPhraseScreen.tsx +++ b/apps/mobile/src/screens/SettingsViewSeedPhraseScreen.tsx @@ -33,11 +33,7 @@ export function SettingsViewSeedPhraseScreen({ {t('settings.setting.recoveryPhrase.title')} - + ) } diff --git a/apps/mobile/src/screens/SettingsWallet.tsx b/apps/mobile/src/screens/SettingsWallet.tsx index ed9488f0395..98e3e96bfcc 100644 --- a/apps/mobile/src/screens/SettingsWallet.tsx +++ b/apps/mobile/src/screens/SettingsWallet.tsx @@ -4,7 +4,7 @@ import React, { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { ListRenderItemInfo, SectionList } from 'react-native' import { SvgProps } from 'react-native-svg' -import { useAppDispatch } from 'src/app/hooks' +import { useDispatch } from 'react-redux' import { navigate } from 'src/app/navigation/rootNavigation' import { OnboardingStackNavigationProp, @@ -31,18 +31,16 @@ import NotificationIcon from 'ui/src/assets/icons/bell.svg' import GlobalIcon from 'ui/src/assets/icons/global.svg' import TextEditIcon from 'ui/src/assets/icons/textEdit.svg' import { iconSizes, spacing } from 'ui/src/theme' -import { ElementName, MobileEventName, ModalName } from 'uniswap/src/features/telemetry/constants' +import { MobileEventName, ModalName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { useUnitagByAddress } from 'uniswap/src/features/unitags/hooks' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { UniverseChainId } from 'uniswap/src/types/chains' import { MobileScreens, UnitagScreens } from 'uniswap/src/types/screens/mobile' import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' import { Switch } from 'wallet/src/components/buttons/Switch' import { useENS } from 'wallet/src/features/ens/useENS' -import { - EditAccountAction, - editAccountActions, -} from 'wallet/src/features/wallet/accounts/editAccountSaga' +import { EditAccountAction, editAccountActions } from 'wallet/src/features/wallet/accounts/editAccountSaga' import { AccountType } from 'wallet/src/features/wallet/accounts/types' import { useAccounts, useSelectAccountNotificationSetting } from 'wallet/src/features/wallet/hooks' @@ -56,7 +54,7 @@ export function SettingsWallet({ params: { address }, }, }: Props): JSX.Element { - const dispatch = useAppDispatch() + const dispatch = useDispatch() const { t } = useTranslation() const colors = useSporeColors() const addressToAccount = useAccounts() @@ -68,9 +66,7 @@ export function SettingsWallet({ const notificationOSPermission = useNotificationOSPermissionsEnabled() const notificationsEnabledOnFirebase = useSelectAccountNotificationSetting(address) - const [notificationSwitchEnabled, setNotificationSwitchEnabled] = useState( - notificationsEnabledOnFirebase - ) + const [notificationSwitchEnabled, setNotificationSwitchEnabled] = useState(notificationsEnabledOnFirebase) const showEditProfile = !readonly @@ -86,11 +82,10 @@ export function SettingsWallet({ useCallback( () => setNotificationSwitchEnabled( - notificationsEnabledOnFirebase && - notificationOSPermission === NotificationPermission.Enabled + notificationsEnabledOnFirebase && notificationOSPermission === NotificationPermission.Enabled, ), - [notificationOSPermission, notificationsEnabledOnFirebase] - ) + [notificationOSPermission, notificationsEnabledOnFirebase], + ), ) const onChangeNotificationSettings = (enabled: boolean): void => { @@ -101,7 +96,7 @@ export function SettingsWallet({ type: EditAccountAction.TogglePushNotification, enabled, address, - }) + }), ) setNotificationSwitchEnabled(enabled) } else { @@ -111,7 +106,7 @@ export function SettingsWallet({ type: EditAccountAction.TogglePushNotification, enabled: true, address, - }) + }), ) setNotificationSwitchEnabled(enabled) }, showNotificationSettingsAlert) @@ -164,9 +159,7 @@ export function SettingsWallet({ const renderItem = ({ item, - }: ListRenderItemInfo< - SettingsSectionItem | SettingsSectionItemComponent - >): JSX.Element | null => { + }: ListRenderItemInfo): JSX.Element | null => { if ('component' in item) { return item.component } @@ -181,7 +174,7 @@ export function SettingsWallet({ openModal({ name: ModalName.RemoveWallet, initialState: { address }, - }) + }), ) } @@ -189,12 +182,7 @@ export function SettingsWallet({ - + @@ -202,9 +190,7 @@ export function SettingsWallet({ : undefined - } + ListHeaderComponent={showEditProfile ? : undefined} keyExtractor={(_item, index): string => 'wallet_settings' + index} renderItem={renderItem} renderSectionFooter={(): JSX.Element => } @@ -220,7 +206,7 @@ export function SettingsWallet({ stickySectionHeadersEnabled={false} /> - @@ -265,12 +251,7 @@ function AddressDisplayHeader({ address }: { address: Address }): JSX.Element { /> {(!ensName || !!unitag) && ( - diff --git a/apps/mobile/src/screens/SettingsWalletManageConnection.tsx b/apps/mobile/src/screens/SettingsWalletManageConnection.tsx index aac99ee62da..d24aa95ffee 100644 --- a/apps/mobile/src/screens/SettingsWalletManageConnection.tsx +++ b/apps/mobile/src/screens/SettingsWalletManageConnection.tsx @@ -1,15 +1,12 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack' import React from 'react' import { SettingsStackParamList } from 'src/app/navigation/types' -import { ConnectedDappsList } from 'src/components/WalletConnect/ConnectedDapps/ConnectedDappsList' +import { ConnectedDappsList } from 'src/components/Requests/ConnectedDapps/ConnectedDappsList' import { Screen } from 'src/components/layout/Screen' import { useWalletConnect } from 'src/features/walletConnect/useWalletConnect' import { MobileScreens } from 'uniswap/src/types/screens/mobile' -type Props = NativeStackScreenProps< - SettingsStackParamList, - MobileScreens.SettingsWalletManageConnection -> +type Props = NativeStackScreenProps export function SettingsWalletManageConnection({ route: { diff --git a/apps/mobile/src/screens/TokenDetailsScreen.tsx b/apps/mobile/src/screens/TokenDetailsScreen.tsx index bb680cd53ec..f2518b5e759 100644 --- a/apps/mobile/src/screens/TokenDetailsScreen.tsx +++ b/apps/mobile/src/screens/TokenDetailsScreen.tsx @@ -19,45 +19,37 @@ import { Loader } from 'src/components/loading' import { selectModalState } from 'src/features/modals/selectModalState' import { disableOnPress } from 'src/utils/disableOnPress' import { useSkeletonLoading } from 'src/utils/useSkeletonLoading' -import { - Flex, - Separator, - Text, - TouchableArea, - useDeviceInsets, - useIsDarkMode, - useSporeColors, -} from 'ui/src' +import { Flex, Separator, Text, TouchableArea, useDeviceInsets, useIsDarkMode, useSporeColors } from 'ui/src' import EllipsisIcon from 'ui/src/assets/icons/ellipsis.svg' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { fonts, iconSizes, spacing } from 'ui/src/theme' import { useExtractedTokenColor } from 'ui/src/utils/colors' import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard' import { TokenLogo } from 'uniswap/src/components/CurrencyLogo/TokenLogo' +import { PollingInterval } from 'uniswap/src/constants/misc' import { SafetyLevel, TokenDetailsScreenQuery, useTokenDetailsScreenQuery, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { fromGraphQLChain } from 'uniswap/src/features/chains/utils' import { PortfolioBalance } from 'uniswap/src/features/dataApi/types' +import { currencyIdToContractInput } from 'uniswap/src/features/dataApi/utils' import Trace from 'uniswap/src/features/telemetry/Trace' import { ModalName } from 'uniswap/src/features/telemetry/constants' +import TokenWarningModal from 'uniswap/src/features/tokens/TokenWarningModal' +import { CurrencyField } from 'uniswap/src/features/transactions/transactionState/types' import { UniverseChainId } from 'uniswap/src/types/chains' import { MobileScreens } from 'uniswap/src/types/screens/mobile' +import { currencyIdToAddress, currencyIdToChain } from 'uniswap/src/utils/currencyId' import { NumberType } from 'utilities/src/format/types' -import { PollingInterval } from 'wallet/src/constants/misc' import { useWalletNavigation } from 'wallet/src/contexts/WalletNavigationContext' import { isError, isNonPollingRequestInFlight } from 'wallet/src/data/utils' -import { fromGraphQLChain } from 'wallet/src/features/chains/utils' -import { currencyIdToContractInput } from 'wallet/src/features/dataApi/utils' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' import { Language } from 'wallet/src/features/language/constants' import { useCurrentLanguage } from 'wallet/src/features/language/hooks' import { useTokenContextMenu } from 'wallet/src/features/portfolio/useTokenContextMenu' -import TokenWarningModal from 'wallet/src/features/tokens/TokenWarningModal' import { useTokenWarningDismissed } from 'wallet/src/features/tokens/safetyHooks' -import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' -import { currencyIdToAddress, currencyIdToChain } from 'wallet/src/utils/currencyId' function HeaderTitleElement({ data, @@ -79,10 +71,7 @@ function HeaderTitleElement({ const chain = onChainData?.chain return ( - + {convertFiatAmountFormatted(price, NumberType.FiatTokenPrice)} @@ -102,9 +91,7 @@ function HeaderTitleElement({ ) } -export function TokenDetailsScreen({ - route, -}: AppStackScreenProp): JSX.Element { +export function TokenDetailsScreen({ route }: AppStackScreenProp): JSX.Element { const { currencyId: _currencyId } = route.params // Potentially delays loading of perf-heavy content to speed up navigation const showSkeleton = useSkeletonLoading() @@ -144,16 +131,12 @@ export function TokenDetailsScreen({ chain: currencyIdToChain(_currencyId), currencyName: data?.token?.project?.name, }), - [_currencyId, data?.token?.project?.name] + [_currencyId, data?.token?.project?.name], ) return ( - + { @@ -216,9 +196,7 @@ function TokenDetails({ const { navigateToSwapFlow, navigateToSend } = useWalletNavigation() // set if attempting buy or sell, use for warning modal - const [activeTransactionType, setActiveTransactionType] = useState( - undefined - ) + const [activeTransactionType, setActiveTransactionType] = useState(undefined) const [showWarningModal, setShowWarningModal] = useState(false) const { tokenWarningDismissed, dismissWarningCallback } = useTokenWarningDismissed(_currencyId) @@ -238,7 +216,7 @@ function TokenDetails({ navigateToSwapFlow({ currencyField, currencyAddress, currencyChainId }) } }, - [currencyAddress, currencyChainId, navigateToSwapFlow, safetyLevel, tokenWarningDismissed] + [currencyAddress, currencyChainId, navigateToSwapFlow, safetyLevel, tokenWarningDismissed], ) const onPressSend = useCallback(() => { @@ -252,13 +230,7 @@ function TokenDetails({ if (activeTransactionType !== undefined) { navigateToSwapFlow({ currencyField: activeTransactionType, currencyAddress, currencyChainId }) } - }, [ - activeTransactionType, - currencyAddress, - currencyChainId, - dismissWarningCallback, - navigateToSwapFlow, - ]) + }, [activeTransactionType, currencyAddress, currencyChainId, dismissWarningCallback, navigateToSwapFlow]) const inModal = useAppSelector(selectModalState(ModalName.Explore)).isOpen @@ -277,11 +249,13 @@ function TokenDetails({ currencyId={_currencyId} currentChainBalance={currentChainBalance} data={data} + isBlocked={safetyLevel === SafetyLevel.Blocked} setEllipsisMenuVisible={setEllipsisMenuVisible} /> ) } - showHandleBar={inModal}> + showHandleBar={inModal} + > {!loading && !tokenColorLoading ? ( - + void }): JSX.Element { @@ -386,6 +359,7 @@ function HeaderRightElement({ const { menuActions, onContextMenuPress } = useTokenContextMenu({ currencyId, + isBlocked, tokenSymbolForNotification: data?.token?.symbol, portfolioBalance: currentChainBalance, }) @@ -408,12 +382,9 @@ function HeaderRightElement({ hitSlop={{ right: 5, left: 20, top: 20, bottom: 20 }} style={{ padding: spacing.spacing8, marginRight: -spacing.spacing8 }} onLongPress={disableOnPress} - onPress={disableOnPress}> - + onPress={disableOnPress} + > + )} diff --git a/apps/mobile/src/screens/WebViewScreen.tsx b/apps/mobile/src/screens/WebViewScreen.tsx index 8eb2a086431..96464583161 100644 --- a/apps/mobile/src/screens/WebViewScreen.tsx +++ b/apps/mobile/src/screens/WebViewScreen.tsx @@ -10,9 +10,7 @@ import { useActiveAccountAddress } from 'wallet/src/features/wallet/hooks' export function WebViewScreen({ route, -}: - | SettingsStackScreenProp - | AppStackScreenProp): JSX.Element { +}: SettingsStackScreenProp | AppStackScreenProp): JSX.Element { const { headerTitle, uriLink } = route.params return ( @@ -23,11 +21,7 @@ export function WebViewScreen({ - {uriLink === uniswapUrls.helpUrl ? ( - - ) : ( - - )} + {uriLink === uniswapUrls.helpUrl ? : } ) } @@ -45,16 +39,10 @@ function ZendeskWebView({ uriLink }: { uriLink: string }): JSX.Element { webviewRef.current?.injectJavaScript(zendeskInjectJs) } }, - [zendeskInjectJs] + [zendeskInjectJs], ) - return ( - - ) + return } const WALLET_ADDRESS_QUERY_SELECTOR = '#request_custom_fields_11041337007757' diff --git a/apps/mobile/src/stories/Introduction.stories.mdx b/apps/mobile/src/stories/Introduction.stories.mdx index 8fc137aa583..430d4ca79a3 100644 --- a/apps/mobile/src/stories/Introduction.stories.mdx +++ b/apps/mobile/src/stories/Introduction.stories.mdx @@ -2,9 +2,7 @@ import { Meta } from '@storybook/addon-docs' - + # `@uniswap/mobile` diff --git a/apps/mobile/src/test/fixtures/explore.ts b/apps/mobile/src/test/fixtures/explore.ts index 1404f583fd6..e07b29ee36e 100644 --- a/apps/mobile/src/test/fixtures/explore.ts +++ b/apps/mobile/src/test/fixtures/explore.ts @@ -1,9 +1,9 @@ import { TokenItemData } from 'src/components/explore/TokenItem' import { Token } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { fromGraphQLChain } from 'uniswap/src/features/chains/utils' +import { createFixture } from 'uniswap/src/test/utils' import { UniverseChainId } from 'uniswap/src/types/chains' -import { fromGraphQLChain } from 'wallet/src/features/chains/utils' import { token } from 'wallet/src/test/fixtures' -import { createFixture } from 'wallet/src/test/utils' type TokenItemDataOptions = { token: Token | null diff --git a/apps/mobile/src/test/fixtures/redux.ts b/apps/mobile/src/test/fixtures/redux.ts index 9584c19c606..6490a00aefe 100644 --- a/apps/mobile/src/test/fixtures/redux.ts +++ b/apps/mobile/src/test/fixtures/redux.ts @@ -2,10 +2,10 @@ import { PreloadedState } from 'redux' import { MobileState } from 'src/app/reducer' import { ModalsState } from 'src/features/modals/ModalsState' import { initialModalsState } from 'src/features/modals/modalSlice' +import { createFixture } from 'uniswap/src/test/utils' import { Account } from 'wallet/src/features/wallet/accounts/types' import { SharedState } from 'wallet/src/state/reducer' import { preloadedSharedState } from 'wallet/src/test/fixtures' -import { createFixture } from 'wallet/src/test/utils' export const preloadedModalsState = createFixture()(() => ({ ...initialModalsState, @@ -15,10 +15,7 @@ type PreloadedMobileStateOptions = { account: Account | undefined } -export const preloadedMobileState = createFixture< - PreloadedState, - PreloadedMobileStateOptions ->({ +export const preloadedMobileState = createFixture, PreloadedMobileStateOptions>({ account: undefined, })(({ account }) => ({ ...(preloadedSharedState({ account }) as PreloadedState), diff --git a/apps/mobile/src/test/render.tsx b/apps/mobile/src/test/render.tsx index 320881827b2..2ea941ef4c8 100644 --- a/apps/mobile/src/test/render.tsx +++ b/apps/mobile/src/test/render.tsx @@ -13,13 +13,14 @@ import React, { PropsWithChildren } from 'react' import { MobileWalletNavigationProvider } from 'src/app/MobileWalletNavigationProvider' import { navigationRef } from 'src/app/navigation/NavigationContainer' import type { MobileState } from 'src/app/reducer' -import type { AppStore } from 'src/app/store' -import { persistedReducer } from 'src/app/store' +import { store as appStore, persistedReducer } from 'src/app/store' import { Resolvers } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { UnitagUpdaterContextProvider } from 'uniswap/src/features/unitags/context' import { SharedProvider } from 'wallet/src/provider' import { AutoMockedApolloProvider } from 'wallet/src/test/mocks/gql/provider' +type AppStore = typeof appStore + // This type extends the default options for render from RTL, as well // as allows the user to specify other things such as initialState, store. type ExtendedRenderOptions = RenderOptions & { @@ -47,7 +48,7 @@ export function renderWithProviders( middleware: (getDefaultMiddleware) => getDefaultMiddleware(), }), ...renderOptions - }: ExtendedRenderOptions = {} + }: ExtendedRenderOptions = {}, ): RenderResult & { store: EnhancedStore } { @@ -85,13 +86,13 @@ type RenderHookWithProvidersResult = Omit( hook: () => R, - hookOptions?: ExtendedRenderHookOptions + hookOptions?: ExtendedRenderHookOptions, ): RenderHookWithProvidersResult // Require hookOptions if hook takes arguments export function renderHookWithProviders( hook: (args: P) => R, - hookOptions: ExtendedRenderHookOptions

+ hookOptions: ExtendedRenderHookOptions

, ): RenderHookWithProvidersResult /** @@ -103,7 +104,7 @@ export function renderHookWithProviders( */ export function renderHookWithProviders( hook: (args: P) => R, - hookOptions?: ExtendedRenderHookOptions

+ hookOptions?: ExtendedRenderHookOptions

, ): RenderHookWithProvidersResult { const { resolvers, diff --git a/apps/mobile/src/utils/hooks.ts b/apps/mobile/src/utils/hooks.ts index 6737080bf03..0024d2982a4 100644 --- a/apps/mobile/src/utils/hooks.ts +++ b/apps/mobile/src/utils/hooks.ts @@ -1,11 +1,11 @@ import { useFocusEffect, useIsFocused } from '@react-navigation/core' import { useCallback, useRef } from 'react' -import { PollingInterval } from 'wallet/src/constants/misc' +import { PollingInterval } from 'uniswap/src/constants/misc' export function usePollOnFocusOnly( startPolling: (interval: PollingInterval) => void, stopPolling: () => void, - pollingInterval: PollingInterval + pollingInterval: PollingInterval, ): void { useFocusEffect( useCallback(() => { @@ -13,7 +13,7 @@ export function usePollOnFocusOnly( return () => { stopPolling() } - }, [startPolling, stopPolling, pollingInterval]) + }, [startPolling, stopPolling, pollingInterval]), ) } diff --git a/apps/mobile/src/utils/reanimated.test.ts b/apps/mobile/src/utils/reanimated.test.ts index 57304b7909d..ac56b746912 100644 --- a/apps/mobile/src/utils/reanimated.test.ts +++ b/apps/mobile/src/utils/reanimated.test.ts @@ -28,7 +28,7 @@ describe('reanimated numberToLocaleStringWorklet', function () { numberToLocaleStringWorklet(num, 'en-US', { style: 'currency', currency: 'USD', - }) + }), ).toBe('<$0.0000000000000001') }) @@ -39,7 +39,7 @@ describe('reanimated numberToLocaleStringWorklet', function () { numberToLocaleStringWorklet(num, 'en-US', { style: 'currency', currency: 'USD', - }) + }), ).toBe('$0.0000000123') }) @@ -121,49 +121,49 @@ describe('reanimated numberToLocaleStringWorklet', function () { numberToLocaleStringWorklet(num, 'en-US', { style, currency, - }) + }), ).toBe('$1,234.56') expect( numberToLocaleStringWorklet(negative_num, 'en-US', { style, currency, - }) + }), ).toBe('-$1,234.56') expect( numberToLocaleStringWorklet(num, 'de-DE', { style, currency, - }) + }), ).toBe('1.234,56 $') expect( numberToLocaleStringWorklet(num, 'hu', { style, currency: 'huf', - }) + }), ).toBe('1\u00A0234,56 Ft') expect( numberToLocaleStringWorklet(num, 'hu-HU', { style, currency: 'huf', - }) + }), ).toBe('1\u00A0234,56 Ft') expect( numberToLocaleStringWorklet(num, 'da-DK', { style, currency: 'DKK', - }) + }), ).toBe('1.234,56 kr') expect( numberToLocaleStringWorklet(num, 'nb-NO', { style, currency: 'NOK', - }) + }), ).toBe('1\u00A0234,56 kr') }) diff --git a/apps/mobile/src/utils/reanimated.ts b/apps/mobile/src/utils/reanimated.ts index 23d9e040240..c970e42fd46 100644 --- a/apps/mobile/src/utils/reanimated.ts +++ b/apps/mobile/src/utils/reanimated.ts @@ -8,10 +8,7 @@ * https://github.com/willsp/polyfill-Number.toLocaleString-with-Locales/blob/master/polyfill.number.toLocaleString.js */ -function replaceSeparators( - sNum: string, - separators: { decimal: string; thousands: string } -): string { +function replaceSeparators(sNum: string, separators: { decimal: string; thousands: string }): string { 'worklet' const sNumParts = sNum.split('.') if (separators && separators.thousands && sNumParts[0]) { @@ -24,10 +21,7 @@ function replaceSeparators( return sNum } -function renderFormat( - template: string, - options: Record & { num?: string; code?: string } -): string { +function renderFormat(template: string, options: Record & { num?: string; code?: string }): string { 'worklet' for (const [option, value] of Object.entries(options)) { let updatedValue = value @@ -60,7 +54,7 @@ const round = (value: number, precision = 0): number => { function mapMatch( map: { [key in Language]: string | ((key: string, options?: OptionsType) => string) }, - locale: Language + locale: Language, ): string | ((key: string, options?: OptionsType) => string) { 'worklet' let match = locale @@ -348,7 +342,7 @@ export function numberToLocaleStringWorklet( value: number, locale: Language = 'en-US', options: OptionsType = {}, - symbol?: string + symbol?: string, ): string { 'worklet' if (locale && locale.length < 2) { @@ -372,10 +366,7 @@ export function numberToLocaleStringWorklet( sNum = value.toFixed(2) } - sNum = (<(key: string, options?: OptionsType) => string>mapMatch(transformForLocale, locale))( - sNum, - options - ) + sNum = (<(key: string, options?: OptionsType) => string>mapMatch(transformForLocale, locale))(sNum, options) if (options && options.currency && options.style === 'currency') { const format = currencyFormats[mapMatch(currencyFormatMap, locale)] @@ -399,7 +390,7 @@ export function numberToPercentWorklet( options: { precision: number absolute: boolean - } = { precision: DEFAULT_PRECISION, absolute: DEFAULT_ABSOLUTE } + } = { precision: DEFAULT_PRECISION, absolute: DEFAULT_ABSOLUTE }, ): string { 'worklet' diff --git a/apps/mobile/src/utils/useAddBackButton.test.ts b/apps/mobile/src/utils/useAddBackButton.test.ts index 4dbb1b5f514..80d505dfb2a 100644 --- a/apps/mobile/src/utils/useAddBackButton.test.ts +++ b/apps/mobile/src/utils/useAddBackButton.test.ts @@ -13,7 +13,7 @@ describe(useAddBackButton, () => { index: 0, }), setOptions: setOptionsSpy, - } as unknown as NativeStackNavigationProp) + } as unknown as NativeStackNavigationProp), ) expect(setOptionsSpy).toHaveBeenCalled() @@ -26,7 +26,7 @@ describe(useAddBackButton, () => { index: 1, }), setOptions: setOptionsSpy, - } as unknown as NativeStackNavigationProp) + } as unknown as NativeStackNavigationProp), ) expect(setOptionsSpy).not.toHaveBeenCalled() diff --git a/apps/mobile/src/utils/useAddBackButton.tsx b/apps/mobile/src/utils/useAddBackButton.tsx index 97993f8b4ae..6fb33be2137 100644 --- a/apps/mobile/src/utils/useAddBackButton.tsx +++ b/apps/mobile/src/utils/useAddBackButton.tsx @@ -9,9 +9,7 @@ import { iconSizes } from 'ui/src/theme' * By default react-navigation will only show the back button if the screen is not the first one in the stack. */ export function useAddBackButton( - navigation: - | NativeStackNavigationProp - | NativeStackNavigationProp + navigation: NativeStackNavigationProp | NativeStackNavigationProp, ): void { useEffect((): void => { const shouldRenderBackButton = navigation.getState().index === 0 diff --git a/apps/mobile/src/utils/useAppStateTrigger.ts b/apps/mobile/src/utils/useAppStateTrigger.ts index 21334fa407a..5c5ed332e94 100644 --- a/apps/mobile/src/utils/useAppStateTrigger.ts +++ b/apps/mobile/src/utils/useAppStateTrigger.ts @@ -2,11 +2,7 @@ import { useEffect, useRef } from 'react' import { AppState, AppStateStatus } from 'react-native' /** Invokes `callback` when app state goes from `from` to `to`. */ -export function useAppStateTrigger( - from: AppStateStatus, - to: AppStateStatus, - callback: () => void -): void { +export function useAppStateTrigger(from: AppStateStatus, to: AppStateStatus, callback: () => void): void { const appState = useRef(AppState.currentState) useEffect(() => { diff --git a/apps/mobile/src/utils/useNoYoloParser.ts b/apps/mobile/src/utils/useNoYoloParser.ts deleted file mode 100644 index 8b5bc83513e..00000000000 --- a/apps/mobile/src/utils/useNoYoloParser.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { getAbiFetchersForChainId, Parser } from 'no-yolo-signatures' -import { useMemo } from 'react' -import { config } from 'uniswap/src/config' -import { UniverseChainId, WalletChainId } from 'uniswap/src/types/chains' - -export function useNoYoloParser(chainId: WalletChainId): Parser { - const parser = useMemo(() => { - // TODO: [MOB-1] use better ABI Fetchers and/or our own Infura nodes for all chains. - const abiFetchers = getAbiFetchersForChainId(chainId, { - rpcUrls: { - [UniverseChainId.Mainnet]: `https://mainnet.infura.io/v3/${config.infuraProjectId}`, - }, - }) - return new Parser({ abiFetchers }) - }, [chainId]) - - return parser -} diff --git a/apps/mobile/src/utils/useSagaStatus.ts b/apps/mobile/src/utils/useSagaStatus.ts index 3a5060c5c90..2b8e6129ea2 100644 --- a/apps/mobile/src/utils/useSagaStatus.ts +++ b/apps/mobile/src/utils/useSagaStatus.ts @@ -1,15 +1,12 @@ import { useEffect } from 'react' -import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { useDispatch } from 'react-redux' +import { useAppSelector } from 'src/app/hooks' import { monitoredSagas } from 'src/app/saga' import { SagaState, SagaStatus } from 'wallet/src/utils/saga' // Convenience hook to get the status + error of an active saga -export function useSagaStatus( - sagaName: string, - onSuccess?: () => void, - resetSagaOnSuccess = true -): SagaState { - const dispatch = useAppDispatch() +export function useSagaStatus(sagaName: string, onSuccess?: () => void, resetSagaOnSuccess = true): SagaState { + const dispatch = useDispatch() const sagaState = useAppSelector((s): SagaState | undefined => s.saga[sagaName]) if (!sagaState) { throw new Error(`No saga state found, is sagaName valid? Name: ${sagaName}`) diff --git a/apps/mobile/src/utils/version.ts b/apps/mobile/src/utils/version.ts index e9196833601..4861bab9978 100644 --- a/apps/mobile/src/utils/version.ts +++ b/apps/mobile/src/utils/version.ts @@ -1,5 +1,5 @@ import DeviceInfo from 'react-native-device-info' -import { isBetaEnv, isDevEnv } from 'uniswap/src/utils/env' +import { isBetaEnv, isDevEnv } from 'utilities/src/environment' import { StatsigEnvironmentTier } from 'wallet/src/version' /** diff --git a/apps/web/.depcheckrc b/apps/web/.depcheckrc index 1dc3f5c08fc..c5218e31f77 100644 --- a/apps/web/.depcheckrc +++ b/apps/web/.depcheckrc @@ -29,7 +29,6 @@ ignores: [ ## Linting and Babel "@babel/preset-env", "eslint-plugin-import", - "prettier", "terser-webpack-plugin", ## Testing "@types/testing-library__cypress", @@ -68,4 +67,5 @@ ignores: [ "utils", "i18n", "tamagui.config", + "setupRive" ] diff --git a/apps/web/.eslintrc.js b/apps/web/.eslintrc.js index 51f20c1859a..713f9fe66e3 100644 --- a/apps/web/.eslintrc.js +++ b/apps/web/.eslintrc.js @@ -14,6 +14,13 @@ module.exports = { rules: { // TODO: had to add this rule to avoid errors on monorepo migration that didnt happen in interface 'cypress/unsafe-to-chain-command': 'off', + + // let prettier do things: + semi: 0, + quotes: 0, + 'comma-dangle': 0, + 'no-trailing-spaces': 0, + 'no-extra-semi': 0, }, overrides: [ @@ -51,6 +58,10 @@ module.exports = { 'error', { paths: [ + { + name: 'styled-components', + message: 'Styled components is deprecated, please use Flex or styled from "ui/src" instead.' + }, { name: 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks', importNames: ['usePortfolioBalancesQuery', 'usePortfolioBalancesWebLazyQuery'], diff --git a/apps/web/cypress/e2e/mini-portfolio/accounts.test.ts b/apps/web/cypress/e2e/mini-portfolio/accounts.test.ts index 32ca3af4c50..b8794abbd67 100644 --- a/apps/web/cypress/e2e/mini-portfolio/accounts.test.ts +++ b/apps/web/cypress/e2e/mini-portfolio/accounts.test.ts @@ -1,5 +1,7 @@ import { getTestSelector } from '../../utils' +const HAYDEN_ADDRESS = '0x50EC05ADe8280758E2077fcBC08D878D4aef79C3' + describe('Mini Portfolio account drawer', () => { beforeEach(() => { const portfolioSpy = cy.spy().as('portfolioSpy') @@ -72,7 +74,7 @@ describe('Mini Portfolio account drawer', () => { it('refetches balances when account changes', () => { cy.hardhat().then((hardhat) => { const accountA = hardhat.wallets[0].address - const accountB = hardhat.wallets[1].address + const accountB = HAYDEN_ADDRESS // Opens the account drawer cy.get(getTestSelector('web3-status-connected')).click() @@ -98,14 +100,13 @@ describe('Mini Portfolio account drawer', () => { }) it('fetches ENS name', () => { - const haydenAccount = '0x50EC05ADe8280758E2077fcBC08D878D4aef79C3' const haydenENS = 'hayden.eth' // Opens the account drawer cy.get(getTestSelector('web3-status-connected')).click() // Simulate wallet changing to Hayden's account - cy.window().then((win) => win.ethereum.emit('accountsChanged', [haydenAccount])) + cy.window().then((win) => win.ethereum.emit('accountsChanged', [HAYDEN_ADDRESS])) // Hayden's ENS name should be shown cy.contains(haydenENS).should('exist') @@ -121,7 +122,7 @@ describe('Mini Portfolio account drawer', () => { cy.get(getTestSelector('web3-status-connected')).click() // Simulate wallet changing to Hayden's account - cy.window().then((win) => win.ethereum.emit('accountsChanged', [haydenAccount])) + cy.window().then((win) => win.ethereum.emit('accountsChanged', [HAYDEN_ADDRESS])) // Hayden's ENS name should be shown cy.contains(haydenENS).should('exist') diff --git a/apps/web/cypress/e2e/wallet-connection/switch-network.test.ts b/apps/web/cypress/e2e/wallet-connection/switch-network.test.ts index a0266bab9f5..469f42cadaf 100644 --- a/apps/web/cypress/e2e/wallet-connection/switch-network.test.ts +++ b/apps/web/cypress/e2e/wallet-connection/switch-network.test.ts @@ -112,23 +112,44 @@ describe('network switching', () => { describe('from URL param', () => { it('should switch network from URL param', () => { - cy.visit('/swap?chain=polygon') + cy.visit('/swap?chain=optimism') + cy.wait('@wallet_switchEthereumChain') + waitsForActiveChain('Optimism') cy.get(getTestSelector('web3-status-connected')) + }) + + it('should switch network with inputCurrency from URL param', () => { + cy.visit('/swap?chain=arbitrum&outputCurrency=0xff970a61a04b1ca14834a43f5de4533ebddb5cc8') cy.wait('@wallet_switchEthereumChain') - waitsForActiveChain('Polygon') + waitsForActiveChain('Arbitrum') + cy.get(getTestSelector('web3-status-connected')) + cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'USDC.e') + }) + + it('should not switch network with no chain in param', () => { + cy.hardhat().then((hardhat) => { + cy.visit('/swap?outputCurrency=0x2260fac5e5542a773aa44fbcfedf7c193bc2c599') + const sendSpy = cy.spy(hardhat.provider, 'send') + cy.wrap(sendSpy).should('not.be.calledWith', 'wallet_switchEthereumChain') + cy.wrap(hardhat.provider.network.chainId).should('eq', 1) + cy.get(getTestSelector('web3-status-connected')) + cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'WBTC') + }) }) it('should be able to switch network after loading from URL param', () => { - cy.visit('/swap?chain=polygon') - cy.get(getTestSelector('web3-status-connected')) + cy.visit('/swap?chain=arbitrum&outputCurrency=0xff970a61a04b1ca14834a43f5de4533ebddb5cc8') cy.wait('@wallet_switchEthereumChain') - waitsForActiveChain('Polygon') + waitsForActiveChain('Arbitrum') + cy.get(getTestSelector('web3-status-connected')) + cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'USDC.e') // switching to another chain clears query param switchChain('Ethereum') cy.wait('@wallet_switchEthereumChain') waitsForActiveChain('Ethereum') - cy.url().should('not.contain', 'chain=polygon') + cy.url().should('not.contain', 'chain=arbitrum') + cy.url().should('not.contain', 'outputCurrency=0xff970a61a04b1ca14834a43f5de4533ebddb5cc8') }) }) diff --git a/apps/web/functions/api/image/nfts/asset/[[index]].tsx b/apps/web/functions/api/image/nfts/asset/[[index]].tsx index bc66b3a09a0..dc1cb078524 100644 --- a/apps/web/functions/api/image/nfts/asset/[[index]].tsx +++ b/apps/web/functions/api/image/nfts/asset/[[index]].tsx @@ -22,7 +22,7 @@ export const onRequest: PagesFunction = async ({ params, request }) => { const data = await getRequest( cacheUrl, () => getAsset(collectionAddress, tokenId, cacheUrl), - (data): data is NonNullable>> => Boolean(data.ogImage) + (data): data is NonNullable>> => Boolean(data.ogImage), ) if (!data) { @@ -67,7 +67,7 @@ export const onRequest: PagesFunction = async ({ params, request }) => { style: 'normal', }, ], - } + }, ) as Response } catch (error: any) { return new Response(error.message || error.toString(), { status: 500 }) diff --git a/apps/web/functions/api/image/nfts/collection/[index].tsx b/apps/web/functions/api/image/nfts/collection/[index].tsx index 662a476ac2e..59b5c4a1634 100644 --- a/apps/web/functions/api/image/nfts/collection/[index].tsx +++ b/apps/web/functions/api/image/nfts/collection/[index].tsx @@ -23,7 +23,7 @@ export const onRequest: PagesFunction = async ({ params, request }) => { cacheUrl, () => getCollection(collectionAddress, cacheUrl), (data): data is NonNullable>> => - Boolean(data.ogImage && data.name && data.nftCollectionData?.isVerified) + Boolean(data.ogImage && data.name && data.nftCollectionData?.isVerified), ) if (!data) { @@ -111,7 +111,7 @@ export const onRequest: PagesFunction = async ({ params, request }) => { style: 'normal', }, ], - } + }, ) as Response } catch (error: any) { return new Response(error.message || error.toString(), { status: 500 }) diff --git a/apps/web/functions/api/image/pools/[[index]].tsx b/apps/web/functions/api/image/pools/[[index]].tsx index ad896050d9f..05ddfd7b717 100644 --- a/apps/web/functions/api/image/pools/[[index]].tsx +++ b/apps/web/functions/api/image/pools/[[index]].tsx @@ -93,7 +93,7 @@ export const onRequest: PagesFunction = async ({ params, request }) => { const data = await getRequest( cacheUrl, () => getPool(networkName, poolAddress, cacheUrl), - (data): data is NonNullable>> => Boolean(data.title) + (data): data is NonNullable>> => Boolean(data.title), ) if (!data) { @@ -215,7 +215,7 @@ export const onRequest: PagesFunction = async ({ params, request }) => { style: 'normal', }, ], - } + }, ) as Response } catch (error: any) { return new Response(error.message || error.toString(), { status: 500 }) diff --git a/apps/web/functions/api/image/tokens/[[index]].tsx b/apps/web/functions/api/image/tokens/[[index]].tsx index 67ce3e90ec4..72e7ff05dfe 100644 --- a/apps/web/functions/api/image/tokens/[[index]].tsx +++ b/apps/web/functions/api/image/tokens/[[index]].tsx @@ -19,7 +19,7 @@ export const onRequest: PagesFunction = async ({ params, request }) => { const data = await getRequest( cacheUrl, () => getToken(networkName, tokenAddress, cacheUrl), - (data): data is NonNullable>> => Boolean(data.tokenData?.symbol && data.name) + (data): data is NonNullable>> => Boolean(data.tokenData?.symbol && data.name), ) if (!data) { @@ -159,7 +159,7 @@ export const onRequest: PagesFunction = async ({ params, request }) => { style: 'normal', }, ], - } + }, ) as Response } catch (error: any) { return new Response(error.message || error.toString(), { status: 500 }) diff --git a/apps/web/functions/components/metaTagInjector.test.ts b/apps/web/functions/components/metaTagInjector.test.ts index 4a3bf697fef..9fd7689e7e0 100644 --- a/apps/web/functions/components/metaTagInjector.test.ts +++ b/apps/web/functions/components/metaTagInjector.test.ts @@ -13,7 +13,7 @@ test('should append meta tag to element', () => { image: 'testImage', description: 'testDescription', }, - new Request('http://localhost') + new Request('http://localhost'), ) injector.appendProperty(element, property, content) expect(element.append).toHaveBeenCalledWith(``, { @@ -31,7 +31,7 @@ test('should append meta tag to element', () => { ``, { html: true, - } + }, ) expect(element.append).toHaveBeenCalledWith(``, { html: true, @@ -56,7 +56,7 @@ test('should append meta tag to element', () => { ``, { html: true, - } + }, ) expect(element.append).toHaveBeenCalledWith(``, { html: true, @@ -84,7 +84,7 @@ test('should pass through header blocked paths', () => { image: 'testImage', description: 'testDescription', }, - request + request, ) injector.element(element) expect(element.append).toHaveBeenCalledWith(``, { diff --git a/apps/web/functions/components/metaTagInjector.ts b/apps/web/functions/components/metaTagInjector.ts index c36480f0457..ae3369df032 100644 --- a/apps/web/functions/components/metaTagInjector.ts +++ b/apps/web/functions/components/metaTagInjector.ts @@ -7,7 +7,10 @@ import { MetaTagInjectorInput } from 'shared-cloud/metatags' export class MetaTagInjector implements HTMLRewriterElementContentHandlers { static SELECTOR = 'head' - constructor(private input: MetaTagInjectorInput, private request: Request) {} + constructor( + private input: MetaTagInjectorInput, + private request: Request, + ) {} append(element: Element, attribute: string, content: string) { // without adding data-rh="true", react-helmet-async doesn't overwrite existing metatags diff --git a/apps/web/functions/default.test.ts b/apps/web/functions/default.test.ts index 293ccf78d85..386f09113dc 100644 --- a/apps/web/functions/default.test.ts +++ b/apps/web/functions/default.test.ts @@ -6,7 +6,7 @@ test.each(defaultUrls)('should inject metadata for valid collections', async (de expect(body).toContain(` { expect(body).toContain(``) expect(body).toContain(``) expect(body).toContain( - `` + ``, ) expect(body).toContain(``) expect(body).toContain( - `` + ``, ) expect(body).toContain(``) expect(body).toContain( - `` + ``, ) }) diff --git a/apps/web/functions/nfts/asset/nft.test.ts b/apps/web/functions/nfts/asset/nft.test.ts index 954c5d7c096..4c7d659df8f 100644 --- a/apps/web/functions/nfts/asset/nft.test.ts +++ b/apps/web/functions/nfts/asset/nft.test.ts @@ -32,15 +32,15 @@ test.each(assets)('should inject metadata for valid assets', async (nft) => { expect(body).toContain(``) expect(body).toContain(``) expect(body).toContain( - `` + ``, ) expect(body).toContain(``) expect(body).toContain( - `` + ``, ) expect(body).toContain(``) expect(body).toContain( - `` + ``, ) }) diff --git a/apps/web/functions/nfts/collection/collection.test.ts b/apps/web/functions/nfts/collection/collection.test.ts index a5987294a8a..d05d2888e97 100644 --- a/apps/web/functions/nfts/collection/collection.test.ts +++ b/apps/web/functions/nfts/collection/collection.test.ts @@ -29,15 +29,15 @@ test.each([...collections])('should inject metadata for collections', async (col expect(body).toContain(``) expect(body).toContain(``) expect(body).toContain( - `` + ``, ) expect(body).toContain(``) expect(body).toContain( - `` + ``, ) expect(body).toContain(``) expect(body).toContain( - `` + ``, ) }) @@ -69,5 +69,5 @@ test.each([...invalidCollections, ...nonexistentCollections])( expect(body).not.toContain('twitter:title') expect(body).not.toContain('twitter:image') expect(body).not.toContain('twitter:image:alt') - } + }, ) diff --git a/apps/web/functions/utils/getRequest.ts b/apps/web/functions/utils/getRequest.ts index 9984fb407f1..474b4ec0c47 100644 --- a/apps/web/functions/utils/getRequest.ts +++ b/apps/web/functions/utils/getRequest.ts @@ -3,7 +3,7 @@ import Cache, { Data } from './cache' export async function getRequest( url: string, getData: () => Promise, - validateData: (data: Data) => data is T + validateData: (data: Data) => data is T, ): Promise { try { const cachedData = await Cache.match(url) diff --git a/apps/web/functions/utils/transformResponse.ts b/apps/web/functions/utils/transformResponse.ts index 18d7ef34a77..4709d969590 100644 --- a/apps/web/functions/utils/transformResponse.ts +++ b/apps/web/functions/utils/transformResponse.ts @@ -6,7 +6,7 @@ import { getRequest } from './getRequest' export async function transformResponse( request: Request, response: Response, - data: (() => Promise) | Data | undefined + data: (() => Promise) | Data | undefined, ) { try { if (typeof data === 'function') { diff --git a/apps/web/package.json b/apps/web/package.json index 479061153c0..859aa1f5a81 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -6,7 +6,7 @@ "scripts": { "ajv": "node scripts/compile-ajv-validators.js", "check:deps:usage": "depcheck", - "check:circular": "concurrently \"../../scripts/check-circular-imports.sh ./src/pages/App.tsx 6\" \"../../scripts/check-circular-imports.sh ./src/setupTests.ts 0\"", + "check:circular": "concurrently \"../../scripts/check-circular-imports.sh ./src/pages/App.tsx 7\" \"../../scripts/check-circular-imports.sh ./src/setupTests.ts 0\"", "sitemap:generate": "node scripts/generate-sitemap.js", "i18n:upload": "./scripts/crowdin.sh upload", "i18n:download": "./scripts/crowdin.sh download", @@ -16,12 +16,13 @@ "build:production": "yarn i18n:download:if-missing && craco build", "analyze": "source-map-explorer 'build/static/js/*.js' --no-border-checks --gzip", "serve": "serve build -s -l 3000", + "format": "../../scripts/prettier.sh", "lint": "yarn eslint --ignore-path .gitignore --cache --cache-location node_modules/.cache/eslint/ .", "lint:fix": "yarn eslint --ignore-path .gitignore --cache --cache-location node_modules/.cache/eslint/ . --fix", "typecheck": "tsc && yarn typecheck:cloud && yarn typecheck:cypress", "typecheck:cloud": "tsc -p functions/tsconfig.json", "typecheck:cypress": "tsc -p cypress/tsconfig.json", - "test": "craco test", + "test": "craco test --watchAll=false", "test:bundle": "node -r esbuild-register ./src/test-utils/bundle-size-test.ts", "snapshots": "craco test -u", "test:cloud": "yarn jest functions --config=functions/jest.config.json", @@ -137,7 +138,6 @@ "mini-css-extract-plugin": "^2.7.6", "path-browserify": "1.0.1", "postinstall-postinstall": "2.1.0", - "prettier": "latest", "process": "0.11.10", "react-scripts": "5.0.1", "resize-observer-polyfill": "1.5.1", @@ -172,7 +172,8 @@ "@reach/dialog": "0.10.5", "@reach/portal": "0.10.5", "@reduxjs/toolkit": "1.9.3", - "@rive-app/react-canvas": "4.6.2", + "@rive-app/canvas": "2.19.0", + "@rive-app/react-canvas": "4.13.0", "@sentry/browser": "7.80.0", "@sentry/core": "7.80.0", "@sentry/react": "7.80.0", @@ -187,7 +188,7 @@ "@types/react-scroll-sync": "0.8.7", "@types/react-window-infinite-loader": "1.0.6", "@uniswap/analytics": "1.7.0", - "@uniswap/analytics-events": "2.32.0", + "@uniswap/analytics-events": "2.34.0", "@uniswap/governance": "1.0.2", "@uniswap/liquidity-staker": "1.0.2", "@uniswap/merkle-distributor": "1.0.1", @@ -283,7 +284,7 @@ "uuid": "9.0.0", "video-extensions": "1.2.0", "viem": "2.x", - "wagmi": "2.5.19", + "wagmi": "2.8.4", "wcag-contrast": "3.0.0", "web-vitals": "2.1.4", "xml2js": "0.6.2", diff --git a/apps/web/public/csp.json b/apps/web/public/csp.json index 4d581910a0a..a6c42c61cd3 100644 --- a/apps/web/public/csp.json +++ b/apps/web/public/csp.json @@ -4,8 +4,8 @@ ], "scriptSrc": [ "'self'", - "'wasm-unsafe-eval'", "data:", + "'wasm-unsafe-eval'", "https://translate.googleapis.com/", "https://www.google-analytics.com", "https://www.googletagmanager.com" @@ -54,7 +54,6 @@ "https://bsc-dataseed1.bnbchain.org", "https://buy.moonpay.com/", "https://cdn.center.app/", - "https://cdn.jsdelivr.net/npm/@rive-app/canvas@2.8.3/rive.wasm", "https://celo-org.github.io", "https://cloudflare-eth.com", "https://cloudflare-ipfs.com", @@ -70,6 +69,7 @@ "https://lh3.googleusercontent.com/", "https://mainnet.base.org/", "https://o1037921.ingest.sentry.io", + "https://browser-intake-datadoghq.com", "https://openseauserdata.com/", "https://performance.radar.cloudflare.com/", "https://polygon-rpc.com/", @@ -89,7 +89,6 @@ "https://statsigapi.net", "https://trustwallet.com", "https://uniswap.org", - "https://unpkg.com/@rive-app/canvas@2.8.3/rive.wasm", "https://us-central1-uniswap-mobile.cloudfunctions.net/", "https://valid.rpki.cloudflare.com", "https://vercel.com", diff --git a/apps/web/public/images/extension_promo/announcement_modal_desktop.png b/apps/web/public/images/extension_promo/announcement_modal_desktop.png new file mode 100644 index 0000000000000000000000000000000000000000..c1778ee166212aa65320fe2a390d5fe31c45bb67 GIT binary patch literal 274777 zcmV(zK<2-RP)_j-N)?py!-?`ik^UH9kSbKU=ZzAmflpZ3c?{dqk1Tx)%Pzh?CD zj5$AlU)Sfa#((ZV{XXBHHu1CF@6Wv%eF)CyyT{{q^xI&WA9&vH4%0L4)Bb_!>1V^~ zfrIGcc}}2dPAl;6e14vNVBIi0zt6&%>lp!O1^0&WpTF<>(@wBx%;)p&N3i*)O-$

U!is#4Y_vOVGFYvf72R(A`3w&vv*9`Cqe}7&f zM*+h%YHotZV(!AHdGXGS=bmvz*Wo2Yl8F^`TfvVOed(Xaz0JOXd%-Y2&$z^7*3?bk ztyo~h_dQJXx!XVD@G3OqH40@e^vwc)p{+>1?`fP>({++4fD#F)^1O!L_fxG}h>dml zBqq;vCP_-w8R*jQ>PPJztY+gIrfU|tb-#@fWOw!(<5Fcw^%>aBpo-nPYNl$JD)dqK zNf9~bJD{30u0ez}CPj5%E)B(0!3XX=c_3$Nm<6E}i?P*%NQ znDd)@i;?7{Oh)?YLD$16DCIMhD6 z-mn%H_;|-0JRIm7yd(TwaAYNo8k>Q^9B4a=pPs)jSU?fvK0J)a0X?3b<~FSy z_4IhG^SK|->z-h`Cba!y0Y>p}I?{M%^MrW)bI(DNO_o5>CWrWVz|akAvFQgsD~Ed4 zy{#PQ=eH*gPv;jTPvuLUV5JJwCdG8X5wK7H6s3vSnxZDBE$3l%|Lqmei;5Oa8@O**3%*p8)sVU{bGDriAj{em)=V|t< z<4U|z{@Cxb4vYAr1%zP{4Mz${O!Wrtn+QE=b$$OhQz_@4yE#Ta#jVK5i0=8Sn%J6N`raW9%_YQa45? zw7eC!3MN@urb!+LZFS=UN}IkkQ;>JE@%9C-~XAGrW_f*h>Bz|IDvxvZ<8 zIlFVc*}%S8h}W3~lUPjP@3$O;#*%uMC^x*1Zfx9Y&YY_zAjfZcR~KZBwP4)5o^6wa zT9PW@+_#DyrJ^O-V;9S_&6jpiKEO>-j27SNST&%aVa{(A%ujQu2V? ziOL1OQcfgg9I`GNB(DP>cj%4<{V<^;2INBH6L!I2kb8sbHps5zLOP+b!=ba}JP+WR z^cH>-H&n+~`h&WSru z$-9sP^O6-g)Z_s}U+hD2KI9634ttJ3+JGG~z>==+sDGbD&&S~Xcs(sF$yv=%iy_r0 zNg^jQ>y}z>7AIwB6x#5~xXC$v8Yr9Ew6R+Ymvm*B!VT>aV?0?VNznFen>7R?oUib4h}fsZ4H(aC2vzpEk3kmyTJ^pCRI? z`#Cq@LUT%d1|0MR7)B~%J{{8S_GPKaKVpb1^LXeS8Td7-U25UkrOU*{|NQ>sBh^$N;1ht0@F&+VZmI2*v#@we9vJh5}op^WcTnRC}v;xidZEU}v#E zSVZhAV@StzphNW`N2rn7prp;dGDVJQ%25$(DgIV}7o8x3T2*$%3T}~JYIpZ>IN6L7 z1#iY|5FgGr9Y*$n`7!(L)>TKtv6q?90@MJmnC1Iz0f>t@PW(VTQ^lJ#hal%aY7F67 z>OUP<>vETs$5uP;vLHg{@EM16nle}ci-&QM-VJ1gBmrgSA zyUb`mG5{NeH#6X36XE&qX>=~7E1YLIrmQdxsGMD`OFrLUSB)qNv+j9-wUqs(qni>7 z3H^CjNGCWRm@`a*wvx26I7ed$PR)l7wX%}3Mhg59MjWjqLemNlw%=Y< zNR2SSg+b+qdv{IkD?!LitJYR_m#`M%^lKCiIag+PGqq+P-0C`%&6gEU?)9q2rPQhT z3<6d`6Zj_KQxFk2*!9ejVZD!_NLFp3y?E~D4Q7uZBfS_eXE=j1J2Zh?W`mWDZchBD z`H2q%yUK8qj)v5X7f6#VnDg7-V+9 z06xgPpLNSjsTowofC}+ak{M@?0+r6v=`3)Hf=j9|SplM_@fZgZog|rlBTJ-nZe49k zlTYjONYy<`jL0sAe&~qqzGhd0`i)MV946xShRQ$dat&o`kztVDz2L~)^sN~t0K(ANSyTHx8@U>FOuy$^FmW~LJ&Qws6uPNe$uUb1q7 z#?v~@ht-&w%dz4a&fUD5lL7frJJG`qoMmvL@n`rhvyY!nmE;lqx7CGccPkS)3%&uk zuY)(4E8$$YiXlfb@oPOu6y9WNHRqiAFvezyW2w9WJZNEeo_YX0V5@LTcxdLH>mu&F zbVWM34jl%)Mt(5sx;F6*xy|Q?wLnTVYfiOw-MP--#`;*vKN#-|CoxV2PU01}X4;j1 z(tHfT*f_@KiXfeGL;HMg=ga<&|KTZ(xjtgu!IR;ZH15poU0fpv^y&6KtPjJqyu5#p z308T4b?JPr_g&+&PE&m`zjY0UY~r? z!eg_xzWEFzew(9f{k1CV+-moSwUvzJ1(USeL;Jt9`Z218UE%q?YThiiQ!snQeuZaF zS+_hsjyDT>hm~hWjmO$+YU)9*0@*0C zmo+UInU|1yQjB^ZFf!A3Dt*F*sduN#EEn9;ctyX=JBXaA(n$;JZ8UB=&R*uD1t6=$ zM*uFsJQ26gpm8fPu<9q_8-*)KMdG!YpHcG@jn$9nAv4xg%e1xtNZe?V4#OmzjR1z= zY}(hJ_B0{rC{=>j_`=KRP0JJ!x89v1)0{^)0W2$Fz}4XQdQN+d->Z+G=F2;8?S5~| z%E)y1#z)K-#%7R}OZ^PsM$JvMF*woUF$OJC|6}wZl{k@Ez7Cf_xa3^KBSKHEnpg#! zGX`N$O+CPDF)#5I-3n!!o69z99wTs0CpX930Di_v9<##B`)zHT)I9i4{iyu_ou>6b zMEuOTZC+PBvlUKdK81fFl)7q&{t1wM7PB2XS&uHOEcN(2rw8|#@1jS`BMF!9jyod{ z&!>drM)sMQbYM-+Q#xv^G-DXP)I3cQuChKiIF-dy(l!~wSm$GmDo?vNoO!cqksKN1>Futk6LBB)h$ zsT_iVpi-mSX)>)&UeQ>a{^SMpr=t<1$t}i6T)SmCmH0dZ1Bb;4#0p0Sd}okw@B>qM z^ccmT)K-+4O+O2b7W(1&{?c;+!x@!O(9q zR91%fHx*!DJt@v4XRr+&AXU07^e6FTiAI*Q1KvSYj$RsI7;Azp^q|^-GJD?JSrG~2 zS(3JGThaJnTgBC0L-lN!l|l3qcX;LzDLF~RE6q!0lhL2l5slUY9(*3vg2TRau5hl; z%A-sKVd6jfMB}oS8`{r;M(4708ep}7DWb|~899hsX2XWHEGA28l70aDI(S#&wZN-} zekG#wP=CTTgBJ3W@ONeurQ*%8(qh&!|1v>IH@B8IqGK!^?SSl%N!?TMB;dFTC6gq+ zk=~%wN}g!*AGH{2koRTHp?^OH|<|H>u}`w!8f+Lxo8>fVJ%>sv+HWnm3frip1}g-kiT)hdn_+qPI`&n zjX%t)j(Wh=ecHXzcj!MoBi-8kSv`LjZf9m02dLE26~KIcj(^N}1`zo3{GvSh~-!#Ee2Nqf!cFh3@CJIzUOL!XqZ~c zEbFy(Pl3{ne;S9DP7f)Yt)NKrmj=`v-Kq?SPOYqzRwAg$+{(Hr>%E7L0GH)9Dphgo zCx@6-0QUC6ucmN&&4r?qmdZ*-Qu1C`VSih99NT&-SEfX)ld(o||F$a8X zsw3wRI9kM8;B{G7;SP0?BpGF@=t$+b;7L_l2`@C8hCZZ9w)oy1R+=fzNo@>piB_sW z8=w>O9~4_UD9^_myz#|j&%a?kOAmUI_x-?~OmbGASyy$u)DJguHW|hkqy;M?Ne4Mo zr^=VwHQ;Wm*HHC)PSaWiALmG?PG*nhLhAP%o2x8~o8?CJrHi>4jhUHg^iMo7zovFM zAB@TBz*agk>vJQl74iq**|-dt(p6a2D_6-^R==O;H89X3&J!Lt`#Ndkx$hs-Dj`0> z{!BQ2dt~R1ztHcLg|r=^7+4lK9rKFkUdd$xlz1Evogph{=!Uj6?;3DlFzTW{!y~R= zY>5pi+4Y-dnI@rTrzX&N`FFyx8r9!G7LjXnXK~Yp$-C4lQ|{h5gObZ|mJYkPPENfX z?>2rsLnl{fA7_XZ^vG1qsQJa|W+~yqIA~G` z&hY-OQ%THfPU-F0M^<)C{%!3XYidkthvZ-4DCQ-a?6tX9J*O*mh`s(awp)rpPMTvwUP74!U>TIdIwHFDn>#>=-}H(U<47a3Yp24+`pXSU+nRDxC0)v(4-d zfkWk(^`Iu>PGvK#?llKjxPRsV!)R9QFNa=macN)lrGuN}LB-_4k(Eizxml@&-{`?i z;Hf4B2*=a>zS>1!55MGbZx}-v_7k7GAB>TXOnj#kP1uvytj5`DKVYVVz7AD-@~83; z2E3DJHZP2hU-3q+8UaZ06V35B`WHR4=X&w@vg6v1Dj2ZhGwGT%cX&qfEMAxw%+mNa z&u(4EF#t)P9@YX%$;?0-mJ-oHZ?XtHP&XQM8F^+9PQcwtj|NP@s9d9DNNrebAo}g! z;38?RySXn?!T=CznahfdTp0X8h3nCjKxXA zAbr-cMylR|+z$;U=VjD$Hwsx?)3%NwN%VqI*E0;4l0&Uvy^&4U4Q7xx#)E3oc&t0s zJdAkp!bv)q3>SwrOF+)e0w0e9x#PYLC>@}DE9SBqJ2kSjVbJJ(V=PFN>X&y}#?^xk zaqekt{Sf^bbWJNAOs9ZH_@IVT=36O|=kHVd%ejdM4BGrTVdFx5v0nVrYVHFcPrIL= zqQ3=9`;-f5jZu7XqjW;J(#l7AJi!x!?c^WPL|)2sPe;xrzJNdN9mueJ)L##}y{^K& zRz6fhBYJ2z%UeZ*;ee~S@EU!kM+cU&@G#SaqvWf3J_kLoQDr9s=?|4>$n$Aui&?c( zF!o$efJMiB|MhCB$3jDmJ4!iL8HuFtI-apou6-@u^3(*XG-h?l?&;{<;dsQjS+6&{ zL+$`4F$j2M2rZ^X2RFwX-FQ-M%3o9&hIu*e5-Iz|2o5;Y0<;-6z)J^AV+w%m&n=hE z(?poH4T6TbYaY*adv_*>HEv7Q%6Mv+eP`|rbForEqR%mWgTc*yEgN7CWPdg|KHh1; zdDAIRoFQ-{zv)?uzWQelY8o6c;DrHI^4I7lRx1^|&6$PsQde~HJZPGZDd<6Gnx>DF zo(GRGh$Oz$Msi}rpT_n>9Tg~_v1V8O6a3Ulh{ymKTR|pGvlcovU&*lYK2(jG>kS#n z``+88`UUS?z+1XBZq0dTu0cVV=E0mq&dEDYgpf0+6JEv7nG6`#bZ9FQ7{3R3(hXt- zI>0Bc1kWDNt=HIFAKUR+^~!R1voAh>@Ccu8eEQ5u&&4}267S*%!D-;>=gSq(#=PwA zx*K7bEkjs?)C5<{VWM^(nDZT;rSNq@`t+L$CRx@Y zEu(&jdul^zfqWGfJdC}D7a^F+Ak=9(lA~~azKBM!EVH)lUEGyU`hXM!uK#*fn5@zX z(cl5$Pk1EDyEP*(twYLY@RVwX1v`TvovWRv#{Bd7&mXgYy-G7`)2UKJ53Cz>%=>i} zpC&G1j(e5l>g#Hn>ou#-7tSNnbwvs&`EXdJ9|G6^^CwD9v?^o8|C^R~4@`Eyvup9` zMVn1_InTnCeQ@&*t)Mi^2)eSKo+;bsxSPt`BOCjzjPQBTj`J)x2|F|Z<>IF ze{_~%EGy@8fVURt@-=VFg@!)WxD{TL_N2psPdbLmNa}r1Y@eAe@fqfk^Q0KA?hp<9 zCyf|regy{pi)_191{S)j@c@43=G-y5`Xx_) zIqbOqTv{r`~k(9WWm@x~Un^d;cve|eX_PF08z0N{ui3}X^vf(dCw@H2 zV{kO#)4$%jc|(P3eJ@umyj~@4W^GNBZ|al=5WKXmT4&7+w8nF5DJtzclZSKPoyOXP z$YUQ~be^r?zOl~rG7xp1TZftntzo zRm3nf90|q({BHdib+a!=DM_=?ZuDu1Y&>qh8PXtup>4*iSHY=;5I@i;JW8xtBHyyr zaEyO*427>Jd!t1LU6V%8N(~Zbs_t}rF~`h6qu-W^gP7*Lyqk#jtYLh!%(AoZFMSfQ z%W2-|I-eB z1;1;oG|`$j>EmT$>Ets{lSmmxvB{-!W)!KOTq+0SXZ0drAiX6M!#b}>&2Y6S*~fc- z8as1in&V|(`4{3!@EQ8GVa>6v0j$cPZm6PYFFDm1Io3fh(*C(lQ~cY-`Rlt84}QS2 z4)IteH>qbJSIN`<9qdxRPltc&IJb5(2k#AxA(gLy#L++DlxwH??J@2O42*cd-xH9x>oCr+;AM4{Bgn{RVDqfj!yo%~(UCSsu zQc+G@`;oS=F3CDqdJ(6hC}3k)uHSi-n(!>#N_rI{FFM?Df1i1bEc8Zu^s~Hppjihi z9>}rY2hNyD)|@Xom+(ThtzDW&bmMxStiYdFr(JqC@$WB|b1Y=s16S%(xYrvz8IbLo zXz)PtnE30Erw0V#Fug-B_#}VAQF;k}6r&DnD^Joy3;c4KwRZqBmAC0?PJqkj#t zze=%rH8H6k1B+_ZFCAaO zXx0uM5{weR<{XWhx7a%~eZVVeVP-AYfo{5)8sCm)IssPss2ty*GSG%*kAWG38uiZ{ zQ-jT63P3EHR%HjxN|mvcLrx;+xu7DOb2SPc(UWLT7=eFBzv+~ZqOYpV==|p8B|i+D zX^J*J6^uX*(={1YNvTIN02^Hn9W+SiHS|darJSc(b900fs=%xWbbRL&A*=px^aANS zpJd_$w`zbWxg_m znk89sa+oHNfmHfH?NcgdNv)%Ay;6Fp&9at#l#}2`xXJT+H-f=E8wZzVJ+!&Cw;M=1 zxb^iI1sKu-SvC^mOLC}Lp6>TynRl1W0@j-Km-c-e!)sK%GNp@_nHu^vi*p^4n?+>- zi!}b0n5$^|I%=j3)<>eb5E#iBzOL?MQKJvQO%17kH>Dgr(wy{x)sk76R%KT6SiG2z z%4PCwY40UVTB{_@pG##O@9>1RaZ@lU6(I{l&2+&_cpc{pNgJv3=;z2*&shQMZs*-$ ze$IgY^Q!T9melii*O5aTivKUjFFg$M%*Nll0s?tFdb6d18_9Tjng$JN);v&s&Ojpc zi@rb0Wij3TSG?(=eOpmKv1~hd^RB^>l*dfMt9UY}bqhSkb|@bR5)W zR{TJu<+wD|P&Kg={Q->WSOKkV>Z7Y|v8%);0|K|^=tyPx(RWi%M|-IRmSd>z1$Z!> z{B2-bSt?*q3uzTG?}MdBZ_;30Uz4*|ig5-96MUhe4eToXn3V}jr`{2#qTfxYRWv+L zWzclCIs<(LlaFo|(^(zg^U1jnYb89QkH!)H0O<@e4Kz$01l~L^$=x{Q zA~W~L{K?xKN4QCM24>c+n>CqX9bhGXf37>6VLIOAVNIz8Y(Y<+l*(62&=J)f)1_4K zuu8`1L=Uu)Q{Kum5|J_X4L+O&@GxsRn{8tpZ4kuleKzst!fj7f@4Py`li@_j=br!9 z|L$#*(0v!##9cb2pdAK?UMZQe#5J@X(%!^03#f@Cn_Tuc+S^>+J z0QZ zTU#ke{5h;e;6fSW)J(&2v){CiU$dI8@Q@n~p<(|1fkq9!`J>Eq%r66?yVf3gUbE=C zpkWz=J@|WgPA_<2;PH&5?DmsAK94TQa2~YKy#9_&k^g#?_7luce}AsN?|`YFT}lMm zl@}x&h^JY?V^+kQQSc?aUB~@h>n=@7D{bFeS5};sL6)c~qEUQx>X87~SMaUK{IJL7 zWBYRMd{*DS$_P3{L~6`fvM$G5=!1f16^o(i{yMq?FyL4?$SsxWn7c$G-)Qg9M59DX z<7Y-KP-_rPp-+xcbF2r%!|dPJIRkE~Eyoy{wVu|pwCb3~oUv}pu8+cn`rRal`uqG~ zt%QNGS~@8MbBs$)g7DTd)7I5CovOscDsw?~%;T(D`?$Z$F8~XJ6ur#BV2&6pQ8e^D zX&CJX)H}@u(BGUf21o<&*dcx?hE}RP&7fH;A!7U8EY!bX=Yp?rAR1*J48QIEL*2S{ z`qQCf&}g6qOCp%ZVUWbYqQPdrv|VM=I6t}^e9nK77vWl$+8FYA14ti-d@+lB^5%_m z0tVn^8HWQ7?K3y1V&QGhj*FHBU-gXe&mdD{otbl*lXVx*YhL%;YWvSB9+#OH8_le!&XYQ{gs$i10MIFx*4UwBIN$4#zBAQuI>w2KVQuY%5~^|c z-_RNZT#%dY%$hn%4R?fn`ujLm^D61ejbs5Y%R@2BHID8$ZXHj z^x}`_w;wo^Iev^YtUIf(*yhU;lq}JBmzK!0qph@RTHoqEX<}AdRV6!I$Nehy3b;fg z(O}EN_{?L^OZXmDAI`JAHNbNkiDb$b_z5_;=v`WWsgSgWRyW3JZlc|LRS>nH4^F;e z`OKER1829@Z_=W{BBdQ;bZy>vTkVjZ5QS(iP%>LFN~zNzf96upsjXN`LsMO(s-qEs zNap2;84lHuaG13$Xc~E_9QCz-2KrlNl+_$_KeAvTNElnjkc(fLq2lKShh{Ow6A1u1xvARYL1xS|h?vFh8S{=VwscV1g2^hiZ3oVX(=+p! z3sEo!$;t=cr>eGZUSOYe5aci^2Q)cH^p`n{!?9_>PN)Zo*&?073~Hm8_8ULfHGBQ5 zQ~*V)@n*omO>xZp3O|!hkJaZ1JV;E%R(pvm6}nrnx#qf`viuit$rYVSezD78z=2kd- zLp#fA<{^!e>fnl&!49~U?Xx0DrG8e`Ah&amW1RPSESeQQq=|Rim==!^E%#9KUB)#lTv_+6F<1s~jM~F|4C$L5wEiAF`?Nz`lqZ_L&>Mr1Gp9pN@G$ z?V-x`Cl{r_Gg*@1^(-@W>BNC|XlBg&0-jCa(IILf8jM4wPNmY?0*F9_I=15;GnwbL zPCR?ToOvB9_)>JP-Y-pT8zy*`axo3!Kf3u%XMKFnBOcG%d(uGkZ~O}J&KH{wYZ+Y5 zS0_+@<+Qx`Bs0`jaOTE5;82y5Y1*x5pTQ2%aHdW%`*v7y-GFKL&5d!GC+WnKRalYNAL^=Jt5@P9>a*wLD{yZItSL*O%hC-C zIxa4158RLOz8rt=_)lkhRzG`8IM&vEf92oMkVe zd2ITH=QnP|d6N7b%B*Dt<)kV#Ar6)Hl=Mwtm(ryyuZ*_1jt(~qk$A+L%}QZtEr>^Z z(Xm%L!VFx!tT{z;ST2P3TEZt6)4`Lr&aIqQVI%8P#a-DCem^B7ZkwDLZbQ~wn5Iw~ zU`tL|;MWC(JHYKlzx&pqRt1y9@%gCt???=_`ACk#N#7F>j{a3mchd57!kcq% zaNfPLeK!Im^~t676IU7Nd7`C)4)BWT!FWBz&k8=_iVJyp9|tTAGyAP787O9ir~U;PO;13Zy+zXOY=$0hz&0KTZd%$j3MhuZ*uZh9Q$nru_FtmPn+p9!1Q zV5;P8ysCepcL(@9H*-H@{6JqGFg8EMir;r1(XZg+K7H|qPRvlhSDg7QeV_zNI`VFL ztet6$`|~f4PTi}XZf&pLes6;p(>&X&lJiJG78~8*j11qr+AoBTr}UYqCynb}1OMEI z)f(1P9bVm37IjsfswD7fo^QSbw`s{*nsiG{IE%_ACdUh_qnu@xUcB#P4bnhi)ZZF# zurO~~YDs%qHo%%|7_+R$3#W5vJk3{DCyzv%g;QCAhuD9@d3>H>R(NZ9ufnQevRvA3 zCqo4HDr34!49xOH8*a#oT3NQ+YL0?UI9xGT@seA?M$M&ihzS#+OgTeq25{8Oxkl-S zw-Wa+SRc{eMtL+Z;O!zzR(RtEyFEYga`4*5D=Yxx;7n@{$ax%_L^Yk(VXb6aze_YTR+spq53fg>!P1XXabhD3 zaeT10n8eSFW84Zhs$Pwos!TDI)f_e|M~}7fc!8t2fu!RQE5=xb43h^Q zSvfYrZtQ#KX{^-D^LeX@)y>PtqeE)0l+4T~s-aaoxUFD9$YI=-pJV2G6&JC(MzD8SQ&81)5*Z z>6rTk^JJihaF+qdvX)1aYzQ|=cbG`ggULR%xeXF!w(u0X;$~nY%!f(%Clhk zs@=Y=m*==;lH>#QXTsCI$8IlVdB(}?G6+Hj9A-Tc^lTrbgOTBkS<6UD+6TB&vwI=U zLb7EoC&HZPS#8=9_i_2#0i?#!{RtQ{dluM|mX?v;T5%SIM~o{@V^&m_MXy!9=J$Pt zKiheM!@TFStRMFVc!^JPHCcdYBX#pdD5G#dJaUaRR}ic=)IIRWK@C-3J1P~BWdp0| zpPDy(9PvfsWtH5L?wrHvaq%sYO#hfw|88AlD^QtKK6NYa zE7GU@L!~@F=8IG|JeFk6+B7L_h;N?gus#agKt8t9nA}~ian4N7dFtMo!Q=c`Kgs$( zKc=~?zKA82-D+sIL$z2}q06c!*S)hgMer+gjaZvv>iB%G1E$rDET3f-QkP5!D}^@d z1S8{cu#3dd4*b%oZzVFI)dJg3vv$&Hc8Rf=L*e*>{~Eo0bA;9J?7T2fe)AF`@}g7{ zot}-a;9~|58b5KpaLc<`%E6LzDpD8eh5PKAa)t(hg-01XGcSaF->M zy%Hn2)IEw*(-B;jujqckfy}T`j>c63E-Pm(rFmJax!n^PMJFQ51i|%Ov<7qZ zesxKEXL<$N%b0~&t45!pVE&T^gHS4px1WHf?xlHCeX<+XW5Ne{nZIFEATbV+D;7NhuZvU_v4?} zm5UxT0+y#DLSK(v7B;{op5b>T+#7zqMJpWmT%|~o!ra2;HF+Z|Fq>h zj0J~__Ueml$GGi+2&ZC?DN8z)Wf|y0&lo_Uld8sh95{}Sxlt^b5#~jQ zPOy9QKOr8%ivx}fHV)h>bCx*5p!&%D*-~9QPZQ6J^y`3uo(MCn1{Eol^IR9`>IeLs z2Oah;rZ^cQV`2XVhfoC^U-TzV@;zepXwp#l6C4?TLVWD#FSDqt^ISUj5I=4lM?x?DkJ$)DiC)vN(xa;oyif!8Bn4J8p z`d2oVFF!g!H#wdg&NiWgPez$5$J|bO3eU=!q#C=e*LlYrm~YI|A+zS+Wv+8|`1Bid zd*Zw1+L-0aUYh?l!l+Sio@4!`3X}9NLrAlHn;OG0 zrcBhYvr=E5pyVL1|*QE-|gR5gJMKk;TfnK}z&K z-rSyTU4n@#v#w#27XtUpbmllgHx*uO8ZTr%y~+ua_M=`h!d)8YiUwieby}C_p+8q& zJCo+2hQ4N5TxFGWYs!qRH(8sWul3+Bs<}wiBw(TIVx|a5`7X zLd(7vdeo0py>-W?k=5{-zbhvJbp@#@*0YWS^*Iq^QY0l(Ck*4-KJX=O%j^qfVaarA%L#{s_|N;hN;F=T*?`(_>1#*d+* zb1PBwpS8~F3*Q@k$a{QFJW747;3xXz@pOVClL2wc#&(#!+GjAOIoyS3wafFK{~`Ri z1EB^*E+etiqQgRZK>qj{Z&yVe`WupwSQaPf?pXlQaXJUYB zpsy}raUlby$nZbkd4<_uzx1@ z8UJEC;7;Sy3JQk{_%pY53WOXiy+!YR7rr0bF0ZU?TDVashKS+kv@fmq@Z8T`=O-%w zc~%7^+3&mP$>SU3MdTXUDCe)0CqbK~h98yjf5v-CJh)*|c->j*)h(4)9flnN(xLHl z9eDOP4Qig`r)%_LLj@q3PiC}XJgoUD!fLc%+&Eu-gO|EQpI(M9u@c}P_OUJy(g zJMl5PdR1T7`1cN+=tI#QgfW_3qvOYa;|K5NCbQf6Oef8%p9MN;zq@eFz{7!KU8hM& zn5<-t3zdnZ)zxMO6~LW?$PWH+>p@oLTs&|Mr`dtSJo(n3Cq|hM$x!l365N{UHp-US z*8xTvlL3j!h^UJi53KsT$B*Oyx1?=+N3O>FTPB%J2I`u8?RNi|(fg;&%*N@v!@_`X zP)7U%n{bzHHAd7Wr@q9qsYix|-v^#Rd4}}4^h?${*?~;&k zKX&MU|G&McGE3WwqB`ZDOTThZH{!t^r%@Ew?-GFk7y(;u$$1auih_bI!vHRh98IaMq#O-tWSP z=0lA|$=K_n7o25C9-YVmjY0?RMt`J(==?wb?i%d5{`IF|5pTR^m3Oc_M7;E~EH@*b z5}U!8mDLf>xO+&Be;!>osxDaC?+%Ucq~)l2K4U%g+F94ViZ?di(5Q+A3p!GIM;pF-E!;^5VJSfgj{QZod z#{*v0(s&t^fDc~Nn&+W4T(Do;<&x|ipRdq_TX~sfnbV3vrj^CZ)YX9HwGg9E+f7cy zO;B-p?a#Bw(>DCbh-+GkJyRc`lYwm2C?hGA!S-JwY4!U$rpP!=V(%DlV%*j*)WHNi zL|ucKD#ng@$?9Hn^fbn@=8O_H`(<1%`=9Wi*}odRn*-gaTHe7((1#~;o~nZ^cf!JZ zsqQcb%~Q+DXLT4*StTgm=fDLZu{Q%HhxyX}dS?59k~S7q?(-)r=d1@BgJKnpg~Nm7 zsUFntvJSg|z!W=Ajr#1?`{y$jIG%^>s3GBK`GHUiHol)3p_b2*2jal&a810I5hZ*8 zUZmne9|M7wO&`^EtG;2qgd+G*Sj~D`(W7N<2Uk4M2(rLr)LTQ^V= z=?NHhe_F*8V%_V=kc*|#AskQ@h{N(!0JGx*L1J% z=_~>+!ba!fY~Nst8?CZz)HvA(@S#4X%914{wtlc};IqBu47|hmQYwL)>IkfOOSlo= zDW720pGn?0agFheV*nPOj;VHS(NXQiGvLh(D1I5+4*2NgCeA{2o@$U69a=pr-b#G= zx4BD`f5L19n|Cmi7Omars+w}}n?tVrNm~$}g}cNrJ{#yN{!00?Qo{qZ7?`__&WnjN zx2BuqRNtrsbm&DBzk%m<9G&{&iM+$$vUryhhc4Z}VgAbaihp!}gDQua|GaP`c(%hW z2VMQEz11^=yerp7RiF=S;Y{0oC=A));eAr-UuB27Hr(r<=KuNs@|}UoG{@>4>$h~O zMmeoI6HjkQTj3Brmoi{!fR8YlsqpPM%*xE^K5H-sIBmsqd61(O#?$Iqaeg|btFp3t zoN0B`*x^XBkZ*x2^&CTFIXBb>nWcvVt&HSE#;}Fc*5Tfp$+Wr5%Ob_}b8c^hh=#w! z=V=vxP*cmzq0~z1lL1s}kkRqAH=25_d2RSs3H3toMQ>(O?l2!!f2 zvzBcY3^xqEoL`zI;b9$)$b>2ScPF!|IZHt3;5g^&)~XpbC+3To0T!;bbwfCosxa`2 zd7qDCOwuLk9DQ9_cx2X?swG7lYWk!R^93w#VU}F;p{k@D&>WdHQY6AMm;nFCY~(7B zm#U)*Trkz=K&NBNKt%mDcAi#=R{x&I=_G3rS7wLXOQ_bG%z%a&;Ch!Ct|HK^7xOHL=?)uu$1P5k{XEhcL$ zepMa5g#hmMu=;Lg8Me%c)T7EGwsu=stWz~`EN#>9W`6gUUZP9xqfk; zU(h@j{i#GG*PLN()P|E(n|bJw7C;0aogN&m+Ws!tk}RN~tvpAfaI@E)y16!=AZ~Mr zer7nVre{{DSlqd7P+jPsMwEtE$lY_jX3_n( zE5``hsU6EazJfL@lfyhqs#Gg;HlZIfpM^XKO_lnS4Aadp(4;xo;1TJy>j-)X8~I9e zBhPNPXjz@2Vc&d5-MMv-f7)j-_Wb@X)ja^8AsE>|!BJM0df(PYLEzM^e)WMI6^ zzREHNfX|6|w~bP=Zy-d>aG~8U35oW*2KvAJx6d?~^>h1{YGnuKA)Q-$2Ip+Y0a#VG z_X4HmoSGd{U_~8eA$jsf8gRFk8k*3AEK*8onq3n2TrXs8gA!ZP&3@h0z))Q;R}n(# zWHP+Fp|xdETA`eE31L4xL858s2%&FS=~hx=AIIshKjn;ymI>RmU(fl`b+4ZFc4#z( z@H>-~c^sdWM$4dNb&H@3{vge*tkIlZK7@PyO|}M)>e6Suk#bo- z8c(kZmy%OqAp_D;zj-_&XDiF9#k#>phUL6ud%#Sm#vu0l_AFy%5M>v-u5Yzj7OLWV zo>{x>JG&||%eTRMu_52BBgaI??X&t!{ma=I)}Ld1DUr(sm*`g_as~;fL#VMfF1-G3 zjWMj1^ULv8C6Jz_lOkK!vfk15e76+a(ykD0YK`9C2ideM#ON)Lh_N~TUkM~||JRaXDGzc*Tr(nNe=kLs3R zqxz6@D@}V7m%#(tlsF;J8@AB0F9fT!P}*p!{`uCS7ogAvKG5T`@JmC4i z&Td)$V!)qSb8?z7#qV;JCS~pUjWyGRBXqNc)2&s( zR=%ORKYfJ5yvG1~RD<6gVXLx@2mbZ>Jm@ECHSvn?j7rmgGHc6s+ZXXl06*VV-VrA^ zy&88DlUgR`8W*`Z!yEg|;%WES3zWJcIfdkJP6&!*_>3Jgk~V3z`2|%%t$aGFR^pn@)GS!i(GHLm zmkt;!iGQ-bK}4;L^WvD^pKt9T<`-3r(CDQ$lkJD4Q5|r)L<+`#%MPc?t!0Q6#86~B zMCm-Los~y)s@2WuR5ul{I(xQ2U=%#3b3@!3=#~zpOaNFWvseS{HDFuaT5R8f5sqbj z0gSmKHJOLwPf^_Cc;RWoVPW1@7uCe^sAqi~I0ZiS-UrQ1{|wJG+zF=D`(;2V-oKEE zouDsiN`BeTav z&D$EAegzN=Ec7fFDu_H&KBeNXq{j+d{tu&ivH+UL4HcK$( zffwm{wbl2SbFMVb`aUAbp*IGaOfl=5)z2%QS$&@ST2KaWOm;`mR%IFL6uSOn|6-uG zS$iAO`+1f3lTFgbKZf0{sLIP(m1dPT5dnA%Svr7j?W~pk7g~FB>_m3ein}vyAdMW( zU9KaUwM|bG64gTf(3s?kw%+5_@4WhQyEy8r3`95z7}FtJDB-qpth8}Yl8`4E zNNbHcV|({22bFZfye@Grq)%DQ%x0o5U8468v#fIDm`Y)GbHU2ptDL+U4)v}F4xIg< zc9$8dFG9|h!%3n#3Pz_u0xyE z%W75^M)A_dffe7puHJ^crAzQg!z-w4uXG>P-#DzMGR2Y|$s#Sr>#Bd-;sfRteDbUY zQQMPOnl&3=%J*~6@H=FNn*lj*TF=Y6-r4;@JMVc~S$1U(W+*r*+fbeZ3kjzJH~h}S zel`o3)$Z?6?ZsL-4WJ<%>eW$UP&Jx5or;~A8rEW8s<1s}#HN~x$m)b+lscgkCloga z^yvG7Drb$a;7sR4uo+kzW6|MC^4qr>Nti>#6kF4ICa!f)EUh+jf|+O8?cp>FSBd}C z=|{(M0d@l`=6yYDrPeoVfv1#hRICT~%J`OnkR~WRoXhV{9=4)E;`66;Fl{EGCLXA;5i1D;&BBRYOc>|0>Fc9qo+Ye zz?aW;6aV1lbPz|GdWYG|+}82z9-I5~7Z7S?T1bcHasi(kj8N_Cs7+43G0V-lk_(ku z(4^)G72aK~_3rYQW(Kd%<|#o*A2`gLJ~#(DryDwC9IlSejMm8-t}OL4j><#=$0Ho& z4Ue9g@qN^slDJO-Nr_O|l3iA6<%qw7c@=GnMyCRt@yv|=|Mn>1*qk1eROV0|!{z6v zCpnAc_@>dNF#@X#4z&Pc=qg7yLzz+5BsG{>t0~oOIhxAe%Aw=rl_lKaT0U4*y-`QX zbV)jDC{vM^yP=7iRDtWvzBP=}6uJH}OOi8`ght@A3=u8BX(62E6K%N;D_D}A!&+KH zn&kytrG@n>Ef77QKK{fCfE*qM8H7`7LN}Q)V3oyEoxHNQdJCrTpXCa`NruQ)+H#+( z{6_YEl_RV&1kqZw(CHz~VYcGAVLj!cIn$ZF53gxQ$zef%q6Rtp$0k1U+@A|U>A<PukZ(8u+5nai9);0YwYiqiq)&6T0^yd2a!V18!ufo#j(bw#Jm@;E^ zU+J_7m>0%({1x3JZqzsq>%>Kx{aI=T+Mmn&*>2=x$qUZ!*4vz5xs-Z5rv!*lp@o#& zdo0m1Ul^lhhV`5EzJyxMiCOCNA_`g%1U066XXcD`zge$1?7IX2b5UQ+sRDQ}&gmg!yTzbA5leL+vv#Vir@P>;FG+b5V{o(YUEvM>myO zV@f^+PyA+zB;FjSIb*n$yvTkr&K$qfJ7vP{tDe`6XB$7FkIdy*<&X548F-mB_3>5W zf-!mGghBq=+`Q&+jjmRQbrq0&Pu$~v&L{e0p2g`>+)&kaz#aTYb*nyEi)?Bmcvd;k zGT(lDHo%;wdsLrjNJEZ9ET-uNy0(Uqra ztk3;?5zDr_K$-}&qVcS`6jPaf9|AAp<2ROhY^Jttr>F4%zu%--p*J@N4oES%^x?M0 z8BndOPT)2SOlL#IeB3n0wvILoADK!K=jyOSy+EZp!gz-87TV6i)q*P>LvaXTU@)L; z-*d?UmGi*bS+!MqE2V878q%#-`die#hGgVv8HUi;N>e@03kQ-WWqWmm*{0Db&=`Wt zLDuiv5xfeA;tvG`vJOJE@S!pf(eRD1d0H{!u6G~s5)Q4#BitnEq7#EE35QO0c`~|x zUR4s5X(95MRmy4ePRxacU;4i1a91Ddh8q`Es-yY*9rf6mrT|SH&x##)0E+-{XUea7 zzG>G{nRUG(i2xOnHowDJSwF zV>F`+)@VolhhESn=UFLSsj^V4`FU8KA-aI?1yBMF?Ob2$#*oIug3X&V1GQpaQzq#( z%jaKI`feCte^+m_SK@&REZq;|r(GQp% zbhCne9Q) z0$2KD&nfL^fie-$W*aZpp^5TcJlIxi;5g3Sg;nuA% zE86&VDQNhDA&d>c_@{wtUz|kA)T&elRU(lM$u8P<1LnEGVgA{DL zO<0FJLTR^xB`tBJ2viTOH~8((LANXBBtDgct8wy_o6+0+;o6~nG>N|} zC)kL6EoAvxbrWco4rNR-GwiEWl=vve2OR}~#GCliSekoNd%`K;&vKPOU^5?Rw?)G{ z-{lx)%^{VF&eL4&n-;>ykVB_AnzfjiCQkj&V?G9ce?Fckgd{ImiT@2ZBKdZdygK2v znt$?{cexc0rhgaDo>_5ipLwp_+C#iyE;M)dK^K+cByM70;Vuz$$ckL1%w>VcCiT@Q zdYG5V$h?~lURvO7;4#sJ+|T~btu8syaD-Kp95C?IjFizg=p~+r-v#@n~s)indQD1cAPfAZZW!AMzhUef~JwsM8gg>}(Izb}G>5bfwUV5i%(iM*mGwF9Y@$_v zlE`F~)ivLCJYzlOh9kxF(}e&=M+4|;8DY?DVBTq=FrVkQpWE7s!f648hsPn#(>E~J zJ(fIz;KdjCDQkaY?t$bgm%p-AsxlsB*iS-c(inf9H{)=Ns55@ap)3jr3hs)oNR ziX)#s4RutU&U04Z2J0oQ`(EWZUR+%mR=bhBB5*m)7fMI+*`Te05S43WmfOCqsNiGm ze9AOv%PRwxB1Osv$CCB_>cdOe5m}(> zY?S)Qg`Cm*mPu%00#b89gM5>LfT6!ps=^NVr3z_|=IEs_j`k;Q1sfexI+2YI3IbP+ z&4rzW7j#jchq;N<89njVf-SSJjIr>yU=l3gQ(u_{T{TCknKajC%{0;pCcVT<+q__r znY>8TP8@3Nq|?8_zi4w2mv+EvmX2P|YgyZa?ST1IYocMQr&aB9u@HeROJ$IsJ%_p^}%X}(;*!TN0J4}+AOb(LjM%z5&_bld7zYH%|Wx=voFHomG->aOc=ieLi&}*iJ z%REU$niuJ!v=jlnv=G-&GxS5ayYUd2M`ak@N;9<6G7hPcu;ytFR+>_lC718LIG>)5B&c~$loZH@L=*gNaJ4jKba1M{3f$zjWjs$cx85JaHcyDn80`D7B?jhQ_6XoSu`H*Fft;+Deus zTY4pU;?h2j@dVAE~}NrU_F#+Tk zd$~Hb*(m3aj-s45QAWq>i375fA}}WhAhqVGzi)=#|v+j6y%abQ(3~QlwS`S=e zN?OhZsr7(k;2yn&#t&}o_04Pj!e;^Rd-cG~AjQi1>(q;UW5ht(Ec)3m6?w({f~MlD ztk;hA+-#^e@1W0Lf4Y^h0;$pBhI9Sg7tfA^_QEyc+G!$5ci>I%$l+)3msV&NZ;pCA z@%-T675IP6y8rxBrd+a}B}IYLET$X4(_xV_=o;m~3pug|&GUj>lHb;GkGwaIc6*4? z+o1%;<<-jA=GY-_+AiTGe%Uq*hqBq}96tXh#Fl+p{I0=9EeT6~Do$fepajPX@Kf)P z!f&dnKr6Qiq;rMozQGbLK%VU>-Wks0P&3)MN#u35oN$)zcN)?RG^C)Z7E#B%J29QN4QAO#wtJtF#o zF8XT)dZ|L#5sgt+-M$@7hz@fMavB}ur&Ht|=Irp7dGJ0--pJ;?n_z>gI`a-c@j>>t z6DMF_;qvq@6Ez16a%wX;3V9l{iw_4}G&h{LbyGlRa|~;T1!zuOAs`*}j2zwOtYRLS zX$P$0orGgqnoWb5Npv%yk?NJox0)2qm{ja}ZuuLZlO9mtyPu;zzoA!V#H>|6rju^P z2Yv)0!>k3sbixzQx3z&qdzv?8p~>S8_{J}Cp>vi~3^8^ol<3#XoS1k&cuTMk!4q}J z3;<+OKa-5##Xkt#2J~y&q;1wWJ8nV(-sgWuK2>$k$zaRu)Sr8Upd8_;$yo{i9SGE! z&kBI(f9NfPKhfqd`Hu1I(^lmj+KaR8bo3WC_SH0`B%RVJE1(kyBo&*R!|h)ur6nE8 zCXec$cTz%3B}m(}lRKcbh2zIjoJUx!jN={Ls%)vA)vMAOj>}mR-t>YLi+QI5 z9et?2S7EZW5DUbfzZr#lX_yPnHdg|e{XHFJdKQE^v-=lb*9KnFy!fV|b@nZo;+{$v zUdZS^_n*l0KYpxT`@|05Emtyb;0`>gG2{gAvYmi$&m*3B#+k4rdOia|^*4&$V`du$KUg00{RvreU=6cJAKN`>I8*rvHzeBVPT#=% z%nEl~48258&U41R;FS#<@=2#7E0G?+HDl;T0`IC=wAEZQkcyjX-|}9>d-U^C46Hs6 z_!)Gu&vXaEiB!Kb85hiyMRR7sVND9e(NSysnJ^`)IE`i-Ru8b^+bI7FdlI>G{vBQdH2& zXG&wv>Pu^_N*6FA&-qjOorS8Gz48Zm2U+K2%bECJ!AhLCg4I%Es@lwD4t^RgB| zrzv3BXGtZnKQtbbQZz1_Zx27f}Qne zn{#V$=)B`K0|%I*RI``0>vX10`cVw!OsbL89@`0qT>8iFRKpX`Ww!Ky;oK;Bn)O0{ z=eON2XwLh&xHmig82c+Aqr;Z{iXTSrPv(FyaBEdlsThxVCY_aKmO5>vPpjsh<57i- z&ZB};z7|o(e%)%`Z6h4oH+3pG9%g*~_3%zx;fJ*HyUVfKZXQ_HTolQ4nsmwGyYo@g zh0Agy;J_aC)P3*I^XX8lPi$D+&#UtFuDNAa`iAWMUAnRe5o6~@LI3D>9XFQ*_ab(_ zKf$a;p96v}WhY0P8xa$sP$=8yuuo!Ympwn-{n!8Q?aZUMSB<44U74pjJLkNkiao=m z*{OR&a$J*~HL0C>y~-hCNXAWTiO6A1!;5p<0fh5NqaYAa*ss@<(^)p_^HWgKLibV6 zu+Magg5&aakE61fv_=cs#;Elp1j|4qa4uMs@q35VsFpUDw%uC#=IMSf^WzokT1gNb zdUnDJQ^V@BUI-%m>b33E)ROWW0mr8GfCk1~S%WSspV{Z2i(cBYpjp)Y2pkV+1kGP? z=`;@&3;x75Uj+!7W1d7Zt<_~eWB>iOo4e8_+ODIUplqi5g`GNkOYg1dJF zYR_g!;Y6_S^-2o-{LJHV{zaQVp)|xVuRxG!!_}8STay6=DlCY6+W&Rc(UAK^HLK>X z&9V=Cf2K7r=KYJe%CeG)06?OlKtwg|;vMpqfIkCLt&xlGgexykalgKMe)wnqqy-ez zI=hAxyTK|i@|{PF5h^dyOqk*x7r0pZ;b@xi%7TF!W@n9NvU! zQZ4!2>?;P{?I8+g#qLt;%u=4&&s53$-3w+%e|nyaew;s@8M153dO92#>#$Iz@jGL? z6LjLyFh_Iy$~Y%aK&@*WI>UK3u34){Vmzu!F`fghYeK!*uoip?UT#ltJNj@eT!zWj zb2K3m&BSBOj}EK&Da9CO#h~iA0BvaB7=M&`Z|2smIj}j-s7gGU4&`WMUbGnLW?wH_ zp&8e_?$xv3$rG!~9yQTcS9Zv$mkcjfivq{WgGtI{>zM|ax>a}1>UP%fFx#QxTV8K=(o&{k23waS#Z{~nN`;L&cnWfnPEUM zqeG^!?{rg zb&RTbcVL2X)WA6Qvr%Vc&wZ_DJ@PQ zh<)U)hxo^G7HT=oMMvBzlTK@)%y(){v$mTjrFKAb%(7H&-FZ;WIcuEhL{KB0&cD(r zf3C_FigwaSW@XtjLq1|t+Ert6B_ZeW;5c|-8OD{?8`YTONvmlpe8u&)cCZ$IgzMlL ztzk>+Yz-d6Jh;!zn`X^oK5Z_Q^B|ZIy;KJDg6E(Ofv1&Mdg{*r4gvFkMqI$nU$1hm zpRxW93t=(u9XJhZR^f4?xhYZU8wCsE(}zv8qVR*=C9=3WN@1UZ6Eu5`PWv4Y181W5 zw9b1A7D!I52VX&%S9G5pmhZh-`AnI%M&x}y2Wc*&A%hTo4T0~xdB=J1Jl&< zXEJf#;nwrwEe2JovZghMcpUxHK^D-;Tbv8G!gX%+8)X8y|6_uNlD&LJ?$vWUs!~&~ zyyRB0BH3`!`Uoob#~b{yz!zp&@tF11V8V2!NBkZo9wul1`R&K+H@${iiKORrCOO2< zz2|g~SG3Wx#1EM3=|{)G?(w{iD|)UUvzE;cBEOPS%Je-~W&yo8QuUVN_H-12Lv>m~ zYDp`sOS8_HXx2_%1&p4{E86UrUPRU~%P}S-C}4WQveVHO_uLCkI5a#ZjG0GiSH3VM zGi=j(I2!OzgXHq_=!kzlrV|es^s*Rb_0!sTaq9|znEW3+`5%$@Wxv$km1aAJi=Rsl+N zrW0caP5Aiw=DQ7>bv|$Sm^~N~K4;7|S56Loq#9r#K^R+qr(=7l0JXnnS7jNGW6WGh z!tDb8{Vk6MwwxJL!-m z9DW34a!OaHRpJM(PvY3DF%9+D#`}-8B*4BR`)pxu_7Qgowl)mmSl8}K%Z{_19OZq+ zV06AW@eRD{+ds~2!r&i!yq#;WdA%T{5;h)2p}OX6->n!x{I2gxm_<@`R#tHq6c?@YIEPXe}jfzeM&=$ zM+LKR@O;-MOg+N_w{=&xo#RXB^`a2XdyZN|JGEqX8{g$TOC~1hETLn`jE=$^(6)|w z2*-Bt2(Q;fCLBDoUwm|E_zZMrc{kz>;Y;*=-=$QTe%kvOR?X*m_VqffogeeJZ#=NP zDuY9_wuYWE7X*^^VSv^6$gO)*ZdDRu6%-{eSRIeVYm};DrX?L){HEu3bEr6U)kK@* zNM{RbWmuEyazIYCLeW|MGwU*?()d?KPBHX=?XX|iFIDg8a~Qyd`cHHZ81MCO*w?!X z5&Wm~Hqh5R@c~#o#$rGr*!`g2Bs)p1dd;7UJu-WASkgh6Jr)=H3f6{mLvG^4wfOK5 z4b4HYQMIR29`lkz`0sdyVktbFbGPVYfIddCv-&M@Xf-AsVFMqXs*Pm_X5V;7-m>E9 z)6q?K@{*J1x9i`~KyAg7x=1sPXI>R|ystS)l}*rS!nA4&jOIbpuRvfOyec(pplQMl zXFmA`PVN9dOYKc7okUsPaOk&iT6g`lTng#MS@_vF#^%9v)WvJ4XCABLGXoFgF!!@q z%-{Fk)&ik6PHq--%guuq8eJf?fZEQsRZ=H?iTL-}`8OCN-?%Nt2u1Kbn{V5;QkKh% zNxD}`*mitYNkKXc>U>%cqs&@3nMPcP26{>B?1Luf>+@+GTbMm++^4NxD5BbF){?q+ zso8&G3~{?O2Qx;Q@fzhQ2M-Am z6M!o_E^DNIl$Fbn+{&7e4uV-J202-@WG&NeMUy{RIau$anSM{lOY;jFU8Azfa>zY| z!jRET_{E#CwxrB2QxtkT5ZBvOCM0_B4U-Du1Rwh&EttlkqiI$=lb%3m&;7EN*?Bty zBUz+b0pvv)sEeij|2X|>v*?>a4|S*-z{8Xx7OQj%0E)7l9n z%CgJT$h!Jpc#*nctdhY`$M68~b9xWJwX|xmx0HTUZa8eEAI%uNq4Z?bON-1KG@v?5 zAJiW}`!ohxyu>|8>D4Tkr62Z&}}0k4^RS{*i;yc~b_c~R^% z#)A4#-L2+54*XFz635fNGVck8cX2&siTG6@D1K(i1>FGRf@&|dQ0j}%-&1xJ$G1`z z-t!ti=dZpFdgp?nl^;g9r1qV7vBC@ICA_HS*~F{*4t4nfu;{Ckhg2Fd7Jd`W6!cy7 zTLEO5Qg{QruD-ulzGkbM(UC zXxeFXz^<*-LZ6{t7?(=KHd?%NG~Nr7=;Pv18YY9WJ*KlatjCWslFbpu_W`&`5>`=t zUFy^9Yq@+LO$X@bjYfSA6wS1QZq3Ob)@d@)_r$!?v2kmed$ra6Z*IO)|HQA_6Hehx zi;Z^hKwj&ZkK1=P61*rn(|OtLCO#X?3&ci$(*c!;y7f~ht@m?&SD)^Ez7zO7aZ8$L z84=$Jm(5p(bZTOV^`?KVkw>R0Ui=eQh{`o^L`FwZE;VF zQQdC3#CZkul~!bU2k0wZ_BgrC!4FIk0jfl$A`4yefMsCyBK#8^&1MU_g=h8J(!i zD9b8N_HWiL;ILNLE2VkUc@J25+?xn3jiI1Mc*=F$^{svKJRG$(Yen-OPMy|)X=Lb} z@^7|rt8*AdqwDi|fj8v1&m(XVJ~R(*X8o-YP#I7CCe5dDRem8WKpKeG<4G3ui)W&N?Db z+qt?93rri z(7sU)72;6mEnXGdSpmtIc>tz#L;%C-EVVe^tz{$A=>?gXb=&da^{ozM1{h$_>7Z&z zSk3woebJBba?K8l&b3Yl1-MKatWHO=hFOVK&UIQ3xR&+vanX;HCg81X>-{;FZg}*~ z40nptvc`*fd5M_jN$1nO&tG%mF?6RxEqT${EWP2?9AT@^zg@)t1pS?S%Q+o$%#V=W zq&fOE(32;8I=hiNUG^2A?5}jHfeYc2aw7R+>4n3Ti+3!{iSm+r7|7$-9K|~ez&m&& z^v-kSTsqY;j}>~&<4C&QmmF)%YrksSF$U%?J(~D~ajVOr*()*7YY4t&N5Bab)*@R=4f%XA|LLWS^b<%64*X zjU17MHQ-(uSaOrnna0I2y4lc--xKN8bzEZq@b`8Qf-Jth5=+50-yhsd;GV zNsSg3CAF@Xrq7d2+?s>UXZN}if{>KZ$0EI~Wuw=j@soykeShJ6o9!@S9Z*j0&RDy( z+Vs>kpk>lqnhP{Y;S0z19dN4LU?FIE=$gUY=&6n@7r84-J8|6HAJp_S74iJtN6cLx z8QTtf0JUi`DsAxA+LeF!>sCFv7hYN4tPDRh-RC-^V9EUrffEInAEbsBwer-sYNLQn ziG+r!UKLqog0`*}MVJXIbf`tvG4DhUJ=glGZDm z@=sfTly7W!k#lW0kx~kpzMH0g!)WCmYluj0t{a8^WKSKaVx?wJQMVwS-hQ8AFsvVJ z1GrA7sb%1fX3B{u{>|g3Mu&-+^a&U=PR8h?#4wXbtJF(6;mbP9ESLndR=$x4a%;|$ z8W=c@h6Q|CpsIM%Kc7zswm_7~wb2WIqxrRqeJoc+Ej- zGbKQ*yt;a7RZ8B2DyawtI@7vY(=+)O&*pP?&7Gw@`Ld5u4qnS;jcy*@06#=|9r)DP z<1oQnotd>*Uurks{%6Hk{7(EYtMM{0^$O&+8je1l&jXJGCPdHYbB9}@4S+ePkOj%t zrJyV3t*XO3x#dx#e~KUEGVDtgiWaOVK85?URV3PNbi#=?EQ)6Ni1=CgrsyvNLTqZ> zf91HIedL{>w)yohLt1a zMI84kr%7!bH2m`_*akEvkNA_st1o3_w-Xk%_3zbr%PfQ^cuE=Srw?uLQ;qKh{67~1 zGJ^%lQVcH#yB#w6baR(8?$DCUsx(P5#p3`qe!j(0Vg9t{yryPe<>mam%PJ*qAUAeH zvwPG)uY%n;@82LeE52t`Cpa87-OFJo<2}#+hlJKjU><<~22!Q`3jHp`EQ<@U0Fiat*ZVqxyK4fYOO4{7}hdj%~6v?SS|h10aZqM z!I5p3SH?;quU=A;8mNO=A^e$0fd#*CgX6*b`7psW)?TmugWmW zDUwqrtJ{hV#YxBf$AC2b)Bdl&tUJg^naC37YC>3`G0O>6Kd*3Z^C&tm9BTREc~0jF zpM`7>UH*Dmd-b?z&h&%IZWJUa!|J0u?V{79Rj?#sW#U(5SKm7Gcz!-Jvz%!?FD-Ru zMbmP7Z_m|^%s>ZR%xGtd4t%nwx2&H$A2j|G0l{8c3eQe*sI0D5g)(~`8Y}DftrQp6 z&__J07o9LT-na?PCE7FHr}iw}AP@0G5j`)9o&k~_+S#ppm0kgr9?fxAjd8j(I1A3i&4}-^ee<}QpI0611~Z#zEK-yL+N14 z(M^?s7>>^GqWT4E@)ghg%UJCTFK*4bB&%qg8P6szyyGTwH&P#F&H3D8BLBo?^>M_@ z2K=-Fm0U|=|T`*O)zD79dvkiTsDIFFW-md=GaXO zRS@3R%d-N`FMbNzq?7;XAyg_3I=OgGwaNpIa|;j`R6U$+)of?6!Q<*Y_rt`gW?4tu z?N@x?fKm2APVp-~%aNw?+N+fxI6nma%x^ouj3iz$E)#=>zF(nml(%C1t=WtP-x@~s45 zbZwz#S&ur)$Z{R%)ZlcI5}Dv;auPtPG7C0e*24X?TybfBXGw+XfVmc14;2om$q9Qp za?lWx%1rG@8(86A)>0`Vt~ICA82xXM5eU$cO9yd4qY2-uJ8+`;2si}8S#BVQNhglv zn+b=tqRr>A2kxKy+rp{-Rxgx!t}IF%BN7i;b!rqxws>gX6sb$_8zAHJt1LaywY>`KSdK2TJ6y zQGJ;8xTrZ91Ei2c2Qo~~!OO|k^Iz;}z=ap7f4k2djE(Q-*sdP5uZ6#1T_rV`U|PNw z{~S(*Svr5|Sc6`4jB`&nmO+RGfkNJ(J%O-oy359KtnG% z$WA)^ztJnzG1TAyEP^3(ED&d@hBwU&#+Rr|EV)ojH6yh>s2+cTnE{#TBSfAqMwYc4 z>Qp#`b#LlWawzrI#!>%(&#coyD=fEiYrwI2@gjMc?#H@ej#qbqJp8;o)g^eh)oaU% zRRSSCbAQNI_sY5G-qW{AD6nxme~Y;(fuM7ma}T$oDLK+&ejXY3{C%8fIl+GdrsikF zXDLVrfy7^`f7_08ILEqw1p%LxS)C9j1nMPe&wVlNTLfEKf$_;eQ;_C_0UvOvuX23F zDFdIMORx6Ub7mF7oyU7Qq#(7vG#9k$DYbrRZdnmhD7D7fuUQL^>7+KT1uNFt!to<9 zQ`!!#|Jy|;t!YO`pyJkEb1j?0 z%-TmZ3ydj-9sLIT?1GWKLwZcCu zK9XZB&qI#0o})&kw&e;3GvA`EYUKAkv{Ljc_?soMf=SM#<}!|P#S6Js2@5Z|ON;G2 zdVe9UcBb$!M9Qj4suG^N%L1Kx!A@QfT+6!itc_B>DZi9@;2sW zzoCy}xVrPVd35yll$-yrph|NToz>SmggIps zHTf`Wn>|0c4}~e~Q*l|XK(ek`Lh*cmoaTSc{+o~{e@Ri? z_J8e@ea_{(Pv5Vis8!m6nwD88X8ZDG?ZdKyKQ%oxb2@Wy1|{K&HkLGoob^$f`V|;^ z-`@t6R)yi0?qxY9oVcv3LDo>hpma}a&r$RC;I`>yeIAG4C)P%`A)nSuS&$BBOZ!I( zLojF=K~l?S=Z*XzFFE! z^i7!!?RYU&8nVh299fE~y%B~Ks0GdzYTd2qDQziuQ-iMxtR4J(L4k3e>QiHySFM|} z7wTIy)l#1rcW*`s#Yde(7?W1xLt<+D!@#NAbc)8u@H9tim3XEag323ie*blbJ5-EKEm8oABD!m}r*Ocu?six#P2)pW{0hYt9U> zVBoWgK?h(=QaPTpqSrk7PId_(x7wTqZ%)IlT~s4tRjEz`4rPZ0Z^A#U9kuSqbBR}u zJ?yLcYpdpAEl?^Ttz{?Ur^?IB`i*o?=i00<0D1QxGG7w7O@|1yS#Y4vjHqy7_MIn5 z8Ux=B9+7HpqN}k-iRkR;XfiChN^=WmnH6e@%ea^3&3nJB#FWFFs!(Gh@TA%!KK65? z+U0_Nj4d-`;Ywss0Gj%3Ef{E*7=-AWA7eDTC-47?o|6rjJBPB@IPnug@OlrTtvjU<8TuB>v^p(ph&^CJiyn+Un zWQa%T5DO18o|k5HXsRVi$Gya1&`?@r2O{Gm3_9;QE!VJoOXe7L#GPOh{u_@B>u(}N zO}AUO`akdD%K>ZWF|K&+ua`ATcO9vn9nhc-SwqYS_(UI_Tp}w<__tEIK&w?X#tUt9 zdPW|ZxCe@C{9W_Q;B{KtHI|-R@S0LKdzlPMCWAil%CxqMAbw@$`VOajf%X$xTx7*E zVY8?T1UJ46aEZ^XDfBWlo)#>fn^kRQh_^saYwgwyZW>*J79zsmf3tCPTC&|J*3_+u zhbPpn*cU!qIeFg2Emd2zV>vlDzNhn~KAm{BmZ42F#`m^RX4ce?7rGm+s2bU)aJYbz z><73~Jf?ew`+2OK*;fq50H)y{YSTy~vyAtp_L`&U)kfpd!O~pK5P-1%qa6dMLZUUE zNxroDPCQOL)98ZxEO!w6;;H87i$ARQVSrbk^*0xbN8#2_JfC>88bJ9yX1^6fXI|j4 zz6g54?Zp>cCSr?HmOyy@z1KH>Qg*ChrGrVBQ;&EY%RYZT!_N=#A+f-;) zHP0cF>OTp(H_LmK?8RBtslRs}a(tE}u3GEo`%}C8AM4|IzoK8w<*^4(-&eF-f%xaQ z|1>`t=5*HG>dl8%*E@X8=eM7I4zv}<9H|XzHKo!;sh!!G*4FdXPTy5el1@ip>nsjy zmsj1XBuu9;D66<%`tYhr?J-{ESji!Fa5+QjbiQtAu+<$B&Bs^gY{AX-8#xtu?=FXd z0fJy*#lIY8YQTkRf6Y&X3-Ie@9a_(@(k2P7;IYyY=guMFYHu7g(8pnk?659jr84t9 zD^0aerw?tGYk*da`Z!3B6PMaUyfr_L=F{B!Tz_808$1{`Q$VtgZh4k70|EsI)a1J#FImv4M{2wn z>^$Uc9Aog`sDIfEnTvsErB2fE5DTm_<{WZRHF~ib zZH_X?soty>j0XB@VjTU6g~`T*RmNB5;9LX{>MY<=8;XXw65QFUZ44}#sD#)uvndt4 zQ4D;ix)jT;>a*eAtyNy_=U0ivIp!RdBakXE&^6mkYbThnFJ?`L=r#(DUwIk4kj~CJ z`t)+FNE6M;pf=RsINC|<_9I^7C?*#ys$bUlns??hfMW&~EwgOjygGr)W)iJ_OTIhs zI}dpB9&x@3kE#JqM-ez`K?(T?W94G9TPss-U<)gyqAvX z1=mcDUjv=fPOIq{6uADm7hLTQ3-xjAIVdar?=HnaX0S2YkoZEe5fW`r5-US zm4>v{PC3YcJL{h*xJ;aEc)~t$h4iwne177Y$DwAqhcecCGoXE(X$ej$+!4pt1ri=# z{whfNBYQ1()dA%>=6?Hfvxgxras&Crk2p#?4UK!zhPe_xzlvecG>(a-Hxm z8tiCm!Z|l&=R&%l#@yV;@)d(bi<#uurNw|Ik(1}Zv3AeM$rg@svmVA}5M_aXH|y($ zvo_pGi}T34ERWE$qxeC1ku)i&%KG>b=RAkz5?!hNG8JOT_FLUk&9j_b2kaU@Ei%ec zc+p|h2^!oyI77O}$Tx-T#ox@-*viiV-RqtwT29{=3rz$&p&vP}$^O=Xa8%uK!E1L9$5xLm?{oix&!9T3=p#BZPeXiX zKQVVY^cq`ZF7TCE=QrY4W1N8o`8v@t>aWb+26=Qo$+@jQU(SF37AlH?WllkSxJnLB z6{qIzeqHyRCl&)9Lao%qQA#bxoxh>Ra<8%*k`8vEFu2>AU-54T6i^aMs=dY$(OVbl zmld-qN)$34r~0pHt-RwwnqCz6+*28<{yimsukCY1LUlUJH&n4ky=(S zd$4b5&)@u8;-&o*rOCNr7&S3j1E-}#15wh(((23%=DEScTGr0+F||FkTPkHQm>pJ7 zmf($)0Mk2WCB@Xp%mPRESu35Echx+#jIFh*%)}nrJ%bKpL1g!oquJ}NA&rFz(;|OQb*c5z~IPY zOv!=ZpjFe~3{I?Y)jT%CX9Sh%Bg?=>$DMHUTwE7D%Xoy$@rzDzt3LRecg=sRoME+I zOgtX2pTU{LWR)r$hFdaLq4yIWKg7%}mxpuyZq3Qctkz|@1K+I2xG8Q2n`zw4su@Hi zzB9us80i>MT`|*2Ri(CsC0R|sc}>y0i>UgtfM)?F!Q9|K;L;qW{y3jpMDbF4S-oX_ zzM1{Cu6kth@)3uV;-!9ass9&TnspT_b)-2l150t|;w;r09oa$t8P@8m>6sE=nV)db z6F4c`bl_?9c}<8jX)=!aS=54)T);<|e`I zs|2!lEwTgdhncKaU7z3ea+Es&-HEkPd&{!cc~~W%yx+GV^wamVze+XGGaTyIRRsMf zY4<7N_&d(c)oXXs*+VTaHzSFX^Xg8uu3R4iXQbx6OM1j%k`y^P&$vH#Eg|dKdvyoJ zt&f{V*{nORW|_^*GkZsIL)Pt1aFI9%+KJjveeq=(P6B!TE~2PkwKeNnV_+Wp@5JxW zsM0)wi}!?ohQMZJg_d{ir~rT&^8qH_^u4PO)4f&NL49wYm?h;6|D!U_I`$wl+R;|8 z*f!we_KFIhN8j(R#fdA?{8@TYVV1JBkJI^@J*%tQ-z>6QHGhXzk(PhoC3teUSuiK( z^ulIbi}~b+#rVfprTx62NoQ9eZWZ{6*M6epLb!g>8Mp4ZF3iom^IonXtxm&DtE*dI zIlq#XB~2M{%44DUN%FQ9*eP4NzvkeKV= z0Gw@M4)aeZ1J3Dw1P%mQFBh>^Ih9#weFhD}5y+im1grzjj(#IO^o%OvJYZ{1Dak1O zFYC^$+1G0cHk)Dbv9X}n^He8vUZ#PMtk3&(73|3iUZQw`FL)Ay<8zjbjAQ&^Eij~- zU{+ClV-VGhP0hNhLZ5?FZBE{n6VeGA8*g{1diAm8#;x);XSN$Elt<8Qn4W5b zzY8uo8R_ChI`RnvVrnAkCG1$`e!Cv+1|au+rnwKPp0@8 z=Wij|N=UXHeSjaIKA!CKzk*PyxWG*NofI+fb6>~S{Xx4GLa3Lg=(oRPEFp2f+aktz zp5J97;iEj+=)V_^vD+L3!@mt`I5)~@$*I=K66dD8eDhx*RWEZ;W{|G{ZdN6vIJyK3kDW&%X=C|ftv8T;lJz5F0BeMoYw)ks#TIz=Uf-~R2%c$Ko2}8 zK2*jyS0zSitA%J(ulAG$mqVR2oIRM45_x{troG(N8};6Qa7u<~uQMt2d#=A)@S-6A z3|MqTUuymdm|_llGX!|}jaq1?jzo7|Ty0uxH=&sgFb=Du=Dg^qxn!IX_bvvGRy^vU zN%JB{DQU|6)=8s&*Ppb!R(z$up*lR;`&C9~Q+k$zkOpGIT+?<;N82G352ThE1yU}x z0tNGe>Vt|=tg;N_dEdsMST8f%qgO3yx?+tSJVZjOC{g7lGK|0DkzBY*r+JjByu(Rp zTRrq3RQ0*-Lo8t=>43o}Tn2LLcHsLR&Wd77&0qbQb=9u`DB(5OT&M@f@2pA_TZ^^X zhZbu%{ZzZ7v%BCLfQ1!TG+Ft!da0u85FdzLJ_nk%`3KJmembYGSHVy9#7erx<977@ z%cYY$H1>o`ku|}X@8&5lla_ES^eNm!Zdo#-;4A|@uW_!}^PPMw(1K~i19r?`-&=4U z@;eT9pp{m{v&s~wZm}?1ky}wFH=SJNHXnF@tf{vTPz8ftU_z&P%O`NgtaC1gzmNL{ zx%oThrMWH4eO2Edr`tp5{NpVDptF2AUcjp;bSKXIqrZPbiAcxaP!P|v+ZK;czwem* zbN_LqpFW=FUm*Bah%xSS?@z!ad+}aXB9aH~drk21bsjtU|NI0PT)W8S(N&Uap^46o zKPg>ieZx?ttP2$N;g%qwau7Z>Nqi~mwzm+A@zm!TVi=^?T6)reZnp-P*m{snedAik@ zxl_VAF3f6-`|c92@nxZ9HTC-H2`wUiQ8rWbxV=&&w3^R2I(dRYlkPvm)WfS2oquu;%zk4XJ-# znU7eQ7x8BOxpR}?+2{4kbDZA}W-!lcjBKi*HTqm?7Yj2V9%+!2>;`h*Q{*gF7H@n*LFLP#GGJRILhL+5|mkRIUHr4 zVav&7`0FK6BcxQYr2{dlk0l9lYng6l=yOTkD_}{dPWT&@rQ7oc{-j~Wq;x>DBqne& zaZl$;XW2%Cz!SJ&wp?>+o9Dc&H#Pb4Ixg)EjggMvRS}>v{wnc#aQX+O8X2t3jvvu{ z1U<>`nkzN`lID;G9#OJkwNX*H(K+SMZ|%ss0T`_0VLPs`h?K`K&Z~Xq^$u;6D-Glc z1FWm)djbBf7;bBO7##Q)uc-Wk@|UYkF~1RWZYj0U&^tRG{pYI1EI!SwfAIA+tOrM< zYkKj92Yxh`<~1>Q;Z{1qyqC>5TFi*yzCu_)w;gNU@4*Z;Z^vwkfv7up3iF?sxl=CE$sm#?NW+hEFYDXG)u5FwhrNR=|11jFFt@8YaUp+Se9Sx4)E!~Y9 z3)va~>Zb9T7p-MdQ;$4jr~bCKIGuT%VePuGaWE&gN?mU~a1~goe*!;yqhFlz!4o$& z%rNKayxQlP=ZHFO%FMRUO}##wNe#!~JTXI?yi3TnX zQk7MO;yGt4Eug|AX*-~ycAX4_sBteDCp4w%zf~h^;~-G%ft$?z}4d&2-P6o&t^txy5kG(Ei-TqY$pNi1{W& z$bz)BSwC-CxOthil%HstsYLee)(Y)pSyd{+bXcC@9*(VW*3RXckQ* zB4@?F1J9%r>y{g8+H$)boB@;_q&%xtR+(i;DlWUF=V?%p8;7{=$HD(hFv*t)7Ss{9it9Dm` zTgLSb$70_w1DYdM?t%j)7CtlO>YM5+rjqn%Rb{_i12kyCn^bkEr#;5i`#!6(+&Zjk z!(5VZoNB=fC!7O$RV5%vqv%6_wko;f}a5v zRhe+6Q!=D3GTUz8hgtZ}H~!(iVAUXd+<@JIJE?4=3cTU6C{V@er50@hB~4Z{_}YSP z)hO~dnOPo1xBc5rB7OQDucGU|?&I0Zs^0l&^Ar6)5B&)sZ!j~RW{bAFqyj?z0Pr}+ zvX4(6D#5q_^oq$TBHJ&$bIbP)Y^T~vm^9bFu0ps>6vcb_#^POB?38(Wo%~>Er2_ zhFMNQu4&G)AiMotOZVuUn-S>mjC6zz#9(eq$vQ}NNP-EYgPO8dj6dcI%|E<>$Sy{su zGR0yg?aXku_GUy`lci3^2d0Myw4wEaH?dzd;$?MjYBtdWRxPby2!e36V(ceV{2zEP z>Vt#7{)CsYO0a0@wYhZ|G`$>i@XZXF+3l{P1Z# zyf@*0LZ&rmmGl@ah@Q>99eB603Jgv)eP;M27iQ^PR4v{}r9Z@8t3)G>zxs9`sJ@GS={Uj}QeO`C zX10-P=z?iVb0WA${VtWC^A1tSEW2AHcCwz={8)-YTkqueQGLs~90+1;bUb9RG7gbC z@IZVauK8YU^@@1~AH=AwNGLy%A`NF)FlB~3)aAlF4D*;YWxCYIF%IXhF=T$5!y99r z)p~hP^ngsOzvk=LrDI_Gb-cI2F2%ej-L-h zynYRdwMLT0Ck&U~Sf_Ob8EPE__zo^g0KM9$D#G5J;R`jbA$&iS&JZPt-Na-b^hn78dkMwYdls~Dc_6- z3(bsv!oJE$*BnviiqE@)PYiC#h@+HBc`b)dRWuPbEs=eBm- zo%ltM$eE`Xwc>ZaplOVvD@#)(<4WhgK%*U8Tj`7~f5A^QA_{V)N6flqn?MG#|Kp!~ z$@(5cTvof)gT+#GM|HVbPs{1md!0HF;*j^%pY?u{^&MbOtYm_9CR8X(1ekduteJ!f zHM9Z_Rt{w>?iII;Xg6byiZu=DpA#2q^iOep=0gTVlsFtE1{q@!52IMViKun)ZqBfy zf3xPK(kaYqb-byMHR>6beLNRcW}|^7Gp%hYGV7|0&SInc>ajAN+-zfYKUHz4oh(xU zuO`nx{bhz3{pxx0oN(JtWAVakg6U;m+CAY8`fj(As)(S8fdwrys40!cn5qu{-w1N;N z6RB$7n0LTWHzx5bH(*+Gv`;7BB6B|B7)$>o&1S*Zd3@N{4p%w2%uGMe>!l|;7y2E~ zKQ`>oDBYjm4I2{u`1m9C~$xHZGi#jGA^u`S?%na(`Y?_re z)?c0f+pd9wu+5qSu1qo8wlLGnVI6>%(kLktzPNtK@wf)7DNs{PNJNh19p1A|a;^bp z18w3yVN<_Ke`KAwcU)#F!>vV=xzvww%*u-Cp2~6NRVxbI?X;l9E;w>$Rx&N+=o*<; zMJo*|L*crmGSS)ur+G_=xvr^s%y=fx!nU$N%fM>`N@H}n(A?g4r>Pq7@J>JYL_Bqa zKcwX?##1JlQS}^$l@Yx~<5e^uZ5w|Eo><5@4)|2VE;=QgkP)5QLLM>5glM|}kAip$ z{K+bEGpNR&nD2~#^jxBg{Ge>JerI6jfa$rU67K3p`GWTE{uT%G{<%4_@sG>WG~~Pe zDhHfSQp+r2dUWKzHkWODSzESFAK`#G)t*#Es7zm-#RtYE3(donr0CiqCI|e)llCsF zQqADhGOL^$^7!4XEt=^5JYXI%@M3n)OGVOoPn?YsY2_wZqdI?aSg5iL>c-JVyuf@y z=4HJ4r;13N&To7(Ct*iGARHu*35Hqls^P%A&5F*WP?BSjkMcfYx8fZGza)P^MagZD z89F!H@*iD;SKwc6hIk_Lc%X>^_c>3Q)ySFn`+#qi>DwysE<7fmP3v{sz?q&Qt~QU~ z)&nN+Vba(kdl)bIP&hUF(oI@LlT0vR&6l3MkMZ$}Z1Lu9A8_rg`E8wNq+c%n=p7c7 zR&2*x%EqfkI+?|cZdmp$y)QukzoyOqU*bV+xR6~Mfam%AIM=GSNMZB3_c-0Ef0jd^ ztDnQl$}1zcPfWNfTzkI@$O{^br({s#)0tccMT25Ae!jZ4ofDi1D$DXLa`@;{X+$n0 zg-&)0ap73W3Chv}$KT?C)Lw#JPJv1`1j~e#(6MQ`{u%%DI`q>iNDVam2CwE=KvUlc zZDoz~1utNf{Ws=&gX5f18F1&R5LcAe%(9{;*k72t)!err-oPE`zVZJ5viGJzmnBzq z=*~R%`>LwDrEZN#LPAIgkT8>turWq{Vi51i#zO!$VF$JehkrQU56_Nx{^1D1VLKe* z9}Y)&b~wTg8?f+zJYvg;Q6LaP0!e5B0h-Xzn$_JkeD|DuXYJf;?|p7nbyru{@KtrL z?pyb}_nwoPC(pEUot1lwsnWI$9G!9<+CX+wkn6mk^09D|0&3D);-qd$B#!IYF_B3& zk7XuV@gy@6L0PAKhe{``9PD#iJ+d`|@BXaS`hok;pD#+<*Ddyola6WQ2N*k{~tI$B?3Bl8_i={RLsHL^eD zV~c&Dbrsn@mz8u)Zn9ycl8(*1jF%}vwl!Y2qzi77Euk)Po|a|=psZcf_e&l_`o(=p zH%&>u+cG)9$>9yx)o9t4>*FxiS7bAW=Oxx}?kx`a{GIPU+ECKv&Bsuw&P>GfuH?fD zA730CQm&P>c(JX#J;=ds_FbLY>Le@mA6-Yj{c?X<}&Vp^F&W^|rH*b@LO3 z93h58y#y=j=gcCTDHMmxZw52@vQ)$;@@+1wRCDKO6ZL|U_8r@j2YYJInA7@c(BtzR z^1d?qK?{ucbv-zqIm({5eWKgR_F8#06I<4`h-8IaE?5CNcT1viY7e$`J)B+X=a0`{ zX`O(BtZHm#=`X0lP?((*OWc!mT?uviQsR~u?C&EHK5=j_k*4xq8-6C;kMy2voZyTJ zRwV4~8=KtNSx}u)2U(Br(oN5m&vyA|SN^7SC6@3O&uPfV^vLLG5i487~zckCyW1zQGwpHu&#ct*M4Y}d=wIgyRjKuZIOTuptm zInzZpYCO+i#yUQ{!sMy!_=mBc^Q17=;dTWPw{qxgrY4)`wL2rPsPjznOamIy#b^Gsyl`Gbj&{+9#r6mLU~lJUmeQ8AY(8H% zgUZyeJg0b(F0wsdGIDY6;VwX`ggsxJ z5oGhRXuc_>?$$+2Euwn3F_s>lwk_6|w3z@eU;afpWU`v+w}+s8YXNGW7*zb*&n)|6?{0*@I!w|^|ZrE(yj2I5xThb_2GoJmdKm( zpuDZthMWc>_mA%z+YdY~^}A);es@P694l-aPx}o?mULP#vKhSkC1%<7X>J=Y5u14n z65ChgpfA%lYa3_Ts{duXUHQ@}i)@|>hcxXj)z?)zOVt3P4g zOX6hN$>WliWqo;IYh>T=4c5+bB0g!lvCs2yS0&;~V?)o_-#Wq1%G$*?_sSP>ACy{H ztmA6x&vPYg8u>oZdK=`Tt$QN z@dRjn!|P^}5YKuN7Cr~jn=Va8wgJ;F9)mMV0SY;?NCmv<&Wl{8uCfE@Tc!f!lNOu! z#KnBUAhHdFS21o>zCt$HdLR9&W{u(1P$ZG(bhRN_KDR4(01 zS|V)RCP_@{AMPK#?b#*yhiZH+QRkA*yG#&y zzVKY~i$RNHI1ik7{_*LH&txjG?--pX%b$5r$$BPhZ$5YYcyDoxYz8qU`e$j|w)rt6 z(Qe+Ac}%e`e=;J8VenYC?|7=_3+CbVh!PNwQ+wRl9X3Ta(zCZKPJNEUBvrK`EH%C_ z>8EUOr?M%jd49uf>E$a$<*bml`FZjn|Eg-r1^miU#|Y9e#%5D+d1;#P}2BZPpKxC4M=kTI6<>zIp%W$ z+U$~?tszC^@LrY&O_$YIL-F_QhO#}Sa@#Lc`7`+}>GV(AqU*KoWxwYAp4wjPLHkqR zma=`i;14vp9d&y=)Pwz&twp{Kvc2uOg`madBP+Itw@4KY=Tr~R&4Y8Ih-{LC9NM$0 z#Ie>AHP{@NyzQ0f7e9X%*(k`8+MEZ3yme<1?uk=S=03L1iM8o4H7(0>sZ!S?`)!Hq zizi4)F#<%QZ`z)n7~lCMKVl!-z6m48?=IQZ^sirI?47~zfIgSg5OS-bY9v{S8_i0bs)fjc;g-4VG^Nz%mC`h{@~PhvB$-!K4oMf1 z%(gw*mvprhIXH5QY|Ua;CM_RMG;R4j;m(O?O?B9IXDbWqW;xaB%*1*lIV?f4^O?Ts zM_ZQu*(F`^+^cMT>uH^={X98%MWXjN-<>mg-*7?T zxk#cuP008nNCx@^S>mFb+bTs4m|+m{1U zRjkjQ$X`#NiN0GB8OfKQL*)8oIp{-{nU^b<_nD_sr^h$#S4m@Y zo=2^1&z(b$yK7~se~R_Hq)P@ZDBhPMmq+Jp>0Bvs8(0pI4v&+b@}m8LVMwpD^IQMJ4)Q+?DbWNFm}1EW!uL{uTl1CKj0^j+i82w zDB)!v?d*5n_Ib=r*68CVo2R|UQJ<4#K854CQCr+c)7%gD9P?nq#6YUSDomvl3Dym) ztLemBC(ajI+*@JifEs;d8^=kB6vEWHkq+kl5|KG{JEqtnl^$pwZGKiZaidFr+=QJR@(Nz#2p|Dgh1`zDDIHNQU*Wkk7TH|I z*lr(QazHSPhjPngoM)z#Eg|Cpuq1**(&W!_B!iYHsqNIJttm=*ZD6OWLB-UjZLMjxx9_$Nv zdb_G!Wka-?0LA^)dS8<`uny0Ibe(K=P4u?nol!rE7-_Y27zv5j&r4A+$-TGrNaDcP z9hS?{4B#2>tYrIYkNB&u2UlDo=Brc8fw~D zoFE=-H;L<wD1O(oe@N_j2V{(ImP5jnuavy9<}hNk-(&@mFg;KIX_n3zzf1Yk zn6zK>9dk*?EU&(P(W~3z#|HqiWi#nG*sws_cA7ORlVO&@`WE|vmM<<_?E+e(uN%)< z2E?bQjd!=g{VuVO$XNMcEQtaoU*qb*4UjiJUeEN}#WF=QGs=hj22D03E057O#7MVA zT$Ro0D1Xnkj&h~-Q=NY5u?@?%%uGUj99-k_#QRnCKHQd(H3M#6))q41bVlZ@1A0B1^hFRoM=r z(kYW~x4EADu5FyR`%Tim-1J+jhX)_q;15XC@#g~`*_P>TS>hNz)E>BfByw5baAnv| zkxU+Wo6Cee`U2jTP_8R2CAJeBh6E9YILSKGd&l@d0yS%!uWwdpoATrz~j>f3tC4_$GiK8*L`eZaSEU0@a|r%KddjXoR>g_ zbZ6(DEE`KmvwkZ1?#jWO9kabnas0M*y^_yYNz2oU!$BB+T?Y+izfbD3?Q2djT>GO{ zn@mSfnYX*f1wLN~B-PEss!H71{cbNxc)d?~4CuKo(Uy|hT(|XjYG>|yywj$#$mZ_a z*q5$q__yMBE{Wx{O40QCWbce|zl%##%iCFE(>FrBL_7F9qj#1xzUa;F^>Azzr)|?? zX<7O^+sW9>jq8;-Ua67Pe3>hWZ0VtC=b&x8UjltuL?-1Wp{B#xc%NhHrC3l?elesVDq^rng z!seld!~Ms@X-TJ+_pj|YSB9%>vLO2YTT(;0{dHSjbj0P~bDY2FKGZk$KiX2_R7=M` zwz;k=F1+q72Roq^n@}4&tR{+(+rWNncFu8NdblsTzOvS9@p<)okgKJ{QwuD|r?h{35OsLXnq_l8$Zf^n^L%hQE8E>cGTT^>#5t%nEwO(k?@Q|^p1;M~q1#)^ zRx#yjj00`H=-$tEZsM~|9xPUGkyK}zvgGFr>umP>t{?l_mR(#Dwf&f+NNWb#an>$Y zdD0z8>@2dG=c45=-LK4(1i!&vK{8L_LacnvQn*C>MdE(Wjk_cv9x;A2CJZ8(3N6KG z7rvvoK~hcr!+QqGhUfXXhUlB;E9q^C2`+NHWj*SPY@QF+bo1q-X7zVAanEKZBD{;ykG=Ur4>|BAaJSe#NzRvdz3ZFd9W39PseZ5h6(RxQGwa zc}DfRl%UW?!nX3trR&w$=~J+jv48-cQL& zG+jP9lQ;Z)py-9c_W6;KU@Ea}vyHALn>>pI!JP8IN&KSeHwzEH*t8A}duILE(o>bj zN z-Nm-)c5|QR{Z=_dhnTd1NIfmsn!oAoqM}koX5IJrQf`Y#9>p?Wy+2#K6;jR(ESHvh#1I=-`xADTl|v7 zOL4y}+wpD@LUNO}%NaSOi4*Ki+nQ`1HgOwJuGcBDAvfC9b}jS_hT7Kc^7(SJaY*re zPlo{yBcd1I%gc_fyleefv%j;>vQcWSe|bOlek!shoQqr@6S|3H54)txbM!^j@5j`# z_gtyZIaCw6+7FMdjH`q}Sk{VG$^ zWv}-|Ne?=U_pg_~930~Nx;5|tkoJ@IRf+YrtlkgVSG_6M*5f(H{mq5EZ7r`; z`tg}AADNMC_A~osIyi5ZxNLKsV*Rz*Z;M=RZQ-?{#yRY_`Gf7F63pYDD;LQ`D#Jnq^D))4A>fg$fI@J)!5^h8EU1+>mD3T05F;Jnqj>pH*QN zOO#@NLm@s&WpE?3ZRxtb?$xc4f*=;xc%@wvL zsfGlIH$Zj(9|<>W1LOTk>Wd{^sknY7z?*H5uobX*PIzX2Y~OsDgK|82M!MrjZojFj zjO& z6YY~JLcT4_Hr6!veZE85)0bjTN&LwJj~Rjl+m4Gir~RN;wsJ5)l&zng=gq1dr|#{2 z%z*={blZ@%flndwxoZ0x>l8`HXEMb$vk7@tvNX-U8Nu`?4sO-xecf!C#$%h?xf%H8 zbF9fGISa2vlHQ9Q|JqLOvwU8qV_=D09MA>jn9c2Z)$dl$&;x>Bt2F?Xj8sPmbAOqihPV+wl`DWP;=Sb zzwwkPiWeg%EuC*XG)wN@bjq;pembCRlGtXKWE?Lz?bd70vTyPG(%gr=(LtJD)p|eh zr!3KUAcxrUea;WPtUd?#XPJwq^TXYRs2}Y0|bAHVB6{K zYDrHJaRR26i+yM%c}h<^7&!F#=FLeKea74z4jlUL&5q0Mz7gvS-}c$csM~$-x{ht1 z`;H~8Be6Xsle|UagFmsr?@NoESG;U+=e6hy?sp!iy|0S*Juj)WTs50S+)wd#c#G|Q zpPu%U$Tl34*zd;rHFWK+2Y>q!4`18pckKcV?)`>E&mnZ!$MTlaO^xG@^5(P?bkw%yoHPMoxjZQFKo;-sj9V8Do!UVa~PJo+}IEg(!?o#oIYgAFfQKEz zSSbV~mqx2nzU@w~{{R?eqLy!)b{aKW=R}Dr-o=i4s?B-i-iPS>9YC>pB`qwxZHBSS zx86|+*SZgNjHbEl6)#pZksfsF1;TpNN;Cm4pBx1e`_`lZHk6GPZGv{wu%PH$$AXnl zWqLJOcX-$rvI*y&P?vB+c17V;@tZ&B59NTsMzZBd$&ybF;K4GCTXDF zL1*bN@DvmIxFU8OMneof?`{&)6`KdUVcp1zHOfE9?z*2A+xuLKw6)3Hnx}1h1tZay zR`NoJNaWB;rKv5C_wFsqiH_Z^9tgu@CH?qPYWXs z{v=3$)9Y9F&KpW@V{szZY8K#8m7U`xX~L9`LB|GPjT*?+rU?6WrAy5I`V<^TlzMY{ z5cYN*>SU{9HOC^?Pwe*~N9CI@)|Rv&D@MN#0~Sk{N!t+ZmzoA)WXsZ|#%$sByls)72~3 zFSomsZiro$Z@&IsJGw?-ihCcL@%CXydX4qZO9u6N0zgAYn)ZW+?Ss*MhrY_cA#bV) zx1h{BXjl|Y7xPi)oOyd(!dLphUgM{?=2)D!u>~p%Z&3FoJRv)QDA4?E2jeMineUY3 z?j(;8lNZ31i*oW)(z=8TOf|of2ew_bz9yh8=C=w3w0gBoTJvakq0H^(E$so;#Mt8z z=A%1Gr1e&G1aLKsDWI^HzwxErL~gvGEen)uB=M9C?=R{M+4+7hTK7#f0|7Qv`t-olgGiDhe&N6YS5Pq6d#?HasH5y9v?d zX>~+=@0RkWOF99FeiRs2lYTqy``5QnCJ}TGqH=U z7sOjcs_s(n!@}qHdDUZ@rPSxE}Rn207LvDtdmA#A%PC?w2xRZei%c`=xM_X|Z{g~ucF2OohX2DfIKan(=EF8RAFzew)1 z$IYtC=3S;y#NamX+KIaq0*TLm5Zp--)~loy%GSnCQ-G6z4J|o-JrDRzuH>*}Zx5Gr z#nx9&v^xInKcN1iX9`+N0vqXbU;2}~Mq7*)q;y!h*2w*D6Qg0Gta3m?_=@>N1^BFW zv0diuipOtgeId4A+t#KG(jG#~tYiQVst?|Igi_^e4yD-+8E%~X2Yjm>QN;-gIAc6? zl}#bRUvI!V>w04BJ1pn%rm+R>b}xAwq#iX*{>IvQg!{pJwc8e@@>v{~29^Sfypm@c z0_{m!?1N)M(;aWOyV}_YmQu7VV3g)60mD*9{ztRrcVN%Rr^dH|(afy-b`Y4`w{1)b zSV4S?leT>!r`IAd+lj4jaJsUoK;N7Zr@CBrD8_!k{UR8xqKzX>5bh(Oxly{lB&!;8 zepSgkVC*-0Jwq=IXnRETbfC1IuOYUg!0MkB%Rs!W)dxq-<;7E8Ojmyh*8I(? zBf!?C+$J6F8f_!sOsb2Nm^n_j+B7cTm(Pzg73x6}2UKR&k zsHKHbIi-KnWl?K0n3f@ksWtGBd~!@Z2a?}=GbzDB$q$8+sOmh)J~iJut6vEox+67#-E|b7+H;+W!4GsO%7xHZB)IZG z#=;P_v0{2pCAY%Zz2&h2qe08j`C=J0F3vr*>HHN7=`v7B;@NoS!N}J7vaUe-Lv2-> z6vHG7x*aIH>#8nltyRVqg!P7=Oltj8&Ppb`nzc5@wciQdL};(QuB9@E*b~3EZIm4w z5YGyzIuAEbL|2l;Dg)U9XdgNrD`0@nzKCt=65|b zUUQfJrbUZ(Rgt`A^kcAy;5L;QK{bAldFG@^8J*Iv=WAvim%g$ARvRTd_50ptvys43 zfmRo7<6IF%Hm!$2_+tfs?Me4H|0dh2g<^U5h#_v|P~X-*sn*q2wetS>uYPIMo-LRUb)1FRQ|MGuj+M?1a*?UvQ9v3%-yy)s2sw(+B| zd@Bb$&%<9eeP4&{OMs1e)P-GOr`2z=E$tl63R!BTb!x5mgf2Czt9mv^uNf`L7QzX5mmCm z5LFc4Az0);(`NFrO+F9Cs&=hf;n%T|Z)olP;Gw_y&`myDf=8)Nne>RIJz>>9<%HXT z=;5v7WltlP0TIeHi2dDuSG`{5#sy}x+F#Qp7;22B*+(0QD-oOX7;>ou<2QL7YfzqP z6PcUkA9B?7zx3ID^Aw%{chC^5j(4U^F z?}3jU?SELsQxmG|sf)M|+jbyQaFcGouS1e9+OIga&OA?XG%h{#Gjz2t+*d`O)O&Fw z7-S;$rFu{K0q>3q1L(n~F) zV-J;`mVnT4j#&l)f(A`6`=Hi2hu0JE>5SpbAM_R(d2!dYiGFH+FVwhKa8Yk{!yp~L zle0T1$Bqa7#&yO%IfsRuF;km&0M-W$`bh0OP!gs38<~CckiTmY8+DUDhCStvGgldI z9HVMX<)C`;g^c@9q)KX!y6r6J@E=x0j&`hYQ_T}9)8?ozPLKzt5*VsD$XfMU!WS_WXO zCHlT-=ydH8yfH9Cz9$R=O1$#W)|mKih@?uhl$j?OfUgdB!hh$xOx}Dst_7)nUBUhv zv%3&OhDW_12;H{)){cd(P|K^(z=Elz^6F9Bz1QX#{Eu1z6UvO%lD>e+OXqxSv#{xmB8LT$j>+sKKFE4?o+Mpoh zOTI~+_O9}ms}CZXHq2DR1l4uy-|A2jw6iyde$h+pj9nHC%8o5e-zn=J4eWp?h`Xe` z4oA-8?`wQic)6xrt%)px{#IAdzNSsva@G@yY`-+mu}1$qNX_D>;{!7ILVmlt7O4Rm zkR-aU`SnM=DSI==37jXn+{_slX49OuCJ7rSjkgTpzqc9yErnx3Oq@Uaw99R`mL{`w z!55v7&{2X)6x?gmMQ`r!tz{a|wvk*cg*aCQhDjGZJ)7>0%MG$#;=>8lZMR%O*X@TN zS@pQ*xlxLelOI>A&-VK&1yN5;;MdZtl)CY%P9M4*a*CyFS_(6j3;3+b(z7eQ-o7UwHlsR&*QwY^US7NlUU;> zihcc6hAGeT05Hr`w$oRI_0p!*!N%w4=oKaxcM4UWf(K?dgl(j?`E zeHjBVKffu?2(Ex4!G^XbA^k@AJ^gT13Z7=Ta_rJfbX1;|yDGQ~o6}a>E7{({v%-Hs z%%;f8GUkt%j$3)VJO1hWO5>-DH8-_N(0WEWn+Nju^*awc+wqpxNKD^@t5+S!@rN#( z%%-hzMZmB!77PvP7|X~BGcBcl7bf{8)){8udX-jO9doA@@mOTCHsC6oiFElCoogZN z`X8Eq>F#~%F94xvpY^R4EtQ%=&CX*N;P8Q{M1@gGt{%rph*GwE#tS;_2`)_**wG4d zL(DCe`3+6JWlsUT9m4Y@StEyTrQ2L~DG^^YBuoSGlZS|sXD75xFbMr4ui;f3YdBk0 z<$Qm-y3+H#?1ZxGtgcWr{tC-McX3R#7b%F2?jzGa{Q3pT#9m*zD{*vRNj2@y9xTmd zn$g`)S?R0rlz3B>-_r{f>K?p?WpSjw%rdQL(xIV(wqd?%GnS(_MJhP&)f}kFE52Kj zkSU5zILX{Y{*nZLGD~jV%rsq?_x!2hb)<=ouFM)(o7iL8U%F4I>0c{VE>I}6OnIhq zB(&!D^-%ylR8nA9wrs3~0R**U74eSPxv}P-GOsyC7_ut8W^-3_3cL9 zn3#1S!wwy6!EqEf$tJ#0^PUvR7xLogRrUnM6x$q9aG+VMUSVn&FS6&*}%9~WGkS><%O`HNnfDoU+T!j6Uk(U99&hk7!*DfBBk7V#U+qB zW~_|2#@}v3iUZK*fewxM`tdk_Z!ou>`mCva!nCXDPB@%TxcMtJ)8bq9InL#+T-HWz zN}K$&wDB-OCH`xTOGq=(@$0_pjOm2q{R!a`d+Uqs_KOKM--x?Uc*!bWNxJlN>Jkw^cFb zZK5*pZRFgVF1|4iSaLRdR;w!PgQ>#g2Fm0$f9#CjDctaVt}W8pPdD|-{dk_s#{Ark z5>V&UL!!kQHV~}WkKAhH(?FmNm+XtE)^&d8(+v*GetFg*;KUnTsURs`(ciH-dGb-3 zvnud0kA7eF^Cd#ao=L=Ey}`K(b?=63gYtQfJtUT__y#s7SQl%9{$KTb)R~Oa;J3B2Ov+1cNrHYyrj$y$&|TYCTna^ERq7X zoYFT&8%fogN~z*zbIG5}N93|QGn4xEnm?yEj=#MZ2s4IUR%IV}X7_szzVYWSAUDYD zh%qpwqPjYAHC)7nN@IYjJ5K9S-F%PY_3Sv;2Rzqge{2hllFYpN4&v7l7G$`K3X6f_ z03gd^kAvPf^h86P_!FC(`TGcD5pej-o#q#^@pErH(`xDed zRxBqx-$D~f`iYbRz~3_Q<7DG6%Y1r|1?Yv`@ugaBBR8SMYh6=y#Ud2*BQN>E zE&3P-tkwTvTP~R#Kh_*uwS|@Zu6k6zaK_t-|2gd+3wm#c4B`oMiIX z_5h38aB%uIcU?aKx2h?9fu?Mep8YbqSIq$B;9FFS~PDD zI)iD_Iazg0T##fQE|aOJi=CqANil56FF?dVtW_IXFvuW+)K5xFAGI=Hu`h()qO3LJ z;iX`ocYN`g9=Dm(oND^x@*S<{;W4yP7QA=gjMax%LScjU2%CElWv!fI+oFd+@uG~d!8*dny_r7plx9N< z<4pRpnMyL<(8K6*O5jrh>QQ0k`LN#6gpf}Lhp zYBIa98e6AB{rGnOh;jQyQb^NakX;;xk~?85q?HR!d)ckDz0pOH;haza(u4k2dG}bw zg`?2|Ji59@#}D3No%uj4m# zAe($iq9rXQlFKqOJsXxe^GXo&b8Tc!*{i{e%^vHV22kHNY)cebUGy7S{+;n3H@=za z{ge8XB_$u>2)JPO^vT{S!Qlm3qbV>8{k?L^5r?cuHoQq;BrBgIpIQDJC$>(3cE9x3pPn~b!}dX(tE3qkVwxiG~$)yYY!dE2W1)WQD_IEy&N@mNT#YdLMMt$;BB zAHa@=3PR4rX~zT%q*LYG(m;9#D98`M5m+)8V(fRI2{h_U%Ho(n{7%i*8FMqHJsm#I z={f7`=-=%!UFLu5`homeTL$Z;YNbt{S64uH$sTa#{F|le6EOD189OO8pg&++j~Fc_ zTI&oK6sz@K1q*^wOMF-xh@G`oaJ4=uVdGAwu3MptTWPo7vWJN(sto4RkrV}83m89o zn4m0cOKNLJN23Dg%?p&(&V92*15_$b%ubF&5KXgmzz?Thtu5X^{(Af$kw#5exr9gy2#zm!ZS8jjKM)a^NwCxp}4 z)DuOwKueh|2$l6UsOHF$y*#f?Jd*Phv>k!3tAmL%IC?E0_@HQNcNERRN z$mdKZ)Kn0}>s&S;Waz$A`7XMwR$`ciwUe*Qw&v-oww?)-3r}^!Go@Ci0x>8JaxsMI z@QUh~&0cM&Ic&*Ec9BxCIXxy-q-7qn*0{fYv#oc09S?P)Oayh4FTbV}8O0s-!UxNM z(SZ!bHYZDR)8c0(`q9NrW|X|ido6PHVw;jYqS7mdbSAHuEUg)9QXcD`4SZ+rG5#~H2yYB*RyGu$uhu~8Uw8~lOhY_YQUNoaS^R$cND6|`cf~d z{8bw`dA}j;bplAQw?2!&3?v)$D9X`^F9Tnr)KScl=HjVHO@L6*Z@3CU*NkrAHgOJz zLz{m_Gspr>DB5g7+rexzha*xfBDdds>!G8~5zQyXgC_p|zWd&64*0Jwx)IV>+#dAO zp#1LN%7ZfC#<~${3PCxWj*3+DC(^TuT@5g@XzYS-T3KmRHZSmopLqZetwWxcR8 zr4S~!keV)7R81IJqJ&eMK81f-%7b*J@6*=W5+CCyG~3jo5Qv`P$Q%7xtOsV39i4m3 z?zpo4ck@;!Ir!E9Q}RH;BOP->Upu{f=m+F$UdI13hXBre4HAyJbeycNjs&Bjen4pr4WWFD#Bj5?;q>L36%eS5z zT34>q2FI<^*TLfsnm#(q`C@{y2#Y+v}KVS6`*(UYPo%isB7y8)4lF5MQ}J|{}o8TCxhxc>!?&M)|&Nv6&RsuyH&sBlQRZcVA%8I@MA z_BP|P#-LrV^Vj7^LJ>m9mjb+M;;w-ilwx%_PLlLn^jwfvu9_EL@nJ>2Zos_mr zvp(u%`GBIMVE$)Wh(ApsvktvZEqNh)MRmkZh>qAP&^?W;8p3nYOMCm*OPO~bFyKE| zU|>Y5vXBNlwWD_vKdvp~?Zw*v#yQ|6Xfx^1jL;=}B><4kVAZjJ1LE$`6TbTSL&p7` zR*5Vje8Hr)@6r^O)TFbRA5w=)Hno+R?Q6|S_r_j8rWB+i;3nS#&COBOQMA*~CRdL8 zU*$jS3l@K&FU_z5#^>uba70r_++tTD6yo|GH1hQW<>Mg(aYt@$E^tGY{yT#=M5ymH z^>_Y87bgUoV5tIhW8BoLJTY! zT&X?TRUys%<}X_A*Fr8Mexwc`Eo>;d(!X68ub+7-VI7FXpg1s1-wBre7;=Orvt`_g zc&?L5vfd?UAYhy0@7pSo5MBu0uITJt{E}Jn8d7m9kcszzAuFSu58*G5}y0D^P82SBh$xe3;l}= z&6X=admSIxsVpg7^X1Y8D=M%a&9-VtHdxNqXfBMSXJe`*DP@Q)ZgN|sX$eY`UDz!|+`Xk9cpk?6 z2ZaDEF$8V5S%wF^{dvmWQ8HM5i7tFO3~3n~SxCeX zQ=$XaPxp9LV*IX(hNrM)me!my4BTjK_X?qu?qI4V{)T2w-L-VY?FgI+Wz%$SFna(~ zCe<(hK6$KST*Sm!UbFa)RH6*0rUk=Hjp9l@5uVvBG{)L6xo4Hpkf2f)Ht#Z__DTlR zmDG*azE;VTH7uDtqYaipkF>SYN-@uKG)LqA;kF;)EHnz9>aqkj+%45qErGBr<50&+ z=UEm@yg#0%zFSD^M=n{>9$;CvC-yGCc)ZYEaiRnVZwP5j;%FG#SlkI+^pyRQB-Di2 z%i!qOu868HBFfGtlO;0NxUfR?TqtBJz`ok04WIA2tpoCX4o;cj5i>%g0GZ9^bZO1N zsKh2=8j1j}v`hS?in%L^dFk2uEu!l|DoSs640tU#n@ky zygaRyy6S^Iu^-U&%a{?Xjyu=FS>!l5gG>vYEGY(_u?xLY7*-DSLb+_r<;2txLR@~! zPIlWb<+w+g(avi5HI9||Zx7H5S9)GPUvEP5nN+i0oOrH`D7|2d#QM_;iJ zd)QMY({CtzSRnMekpKUVPK19P$p6KujH=TRTqB6nUo`M*?AZR12m^_Ma_{l8ZS%~i z;oL3)JnW;A?@VkVR26(@X41h+;- zrUX?9%-m^sLpbYOWpHgeF7~9PmDELp^JWL7!L5zmndO&o=pBymul>yvtxEgZS+o-k zlBjOAg){9OQDbhyy59+~;sf3P`HeNq(DfTl z@N)p)d@^1XwETAdvrQ~VLd&#@&APE0yowfx#KEeRWKg6peED5{X0>NFgA}_hJ2mY) zKQX=~W%3+3&A`f*sjmrgT$D`^O4CqK$w}f;RsRC?$J$m@*Yg=8qC;LS2PV8O7W+#h zsJ6n5BC4JN52N^%#nRFWqsU?(|M<6SC=seH4|TO=&wdpkSo{=+SwXlG)Rl>a|Bb=y zU{l|S z)vOT~f{f}+pbirnNKE>?v<)sJhGmN=$++FzyECD0(b)KUw|$UJc&oe0(d08d3o((7 zX4}s2iqT$e6arqLKM;W?<=bumFmjh-Sl>h*rhpnz3~$6qzIXgPE>W-tHnj`aWg=~; zu5(YGd2wxrP+^9su4g)eTbvwTpVPRiQ>*L>`mp*JGesh#&wT$f3dYN32N z=DSNXv(iJ8mgd+TXHbuWE1a8~(dZ1Uo@5q<@TlB9PiI$kkSXLIa}^7Khx{zUJ7$!U?w}zvXl<*peA*h1qJ72#)P3iz>mW;Vk1y;<@PRcd}NXD%mPZ z<=1tUmuQ*rlvzB)UJ$M>w$ww4rr6n3{p?@&Nm2DKX0?>7tcTFPnJ(r+rJv?+@sY}> zyGcpN(HORTDFEEO(|oG2wFVm-A|?Z{;+eJ+7j)63r}*w#ebgu%Np`= z7P4Q(p@~c-Ji2vd)uNQqN3|X!&f*~Wugo+4?U?We)qO>I!0+|~Y%YRCn}Jw$=yow) z%Yd9V#&kWAXVHEigxnMi_?GqcPS}I?xt_)GE-oqbVHCc!0h@ppS|JyiI<=Th>6Pzl zg~6PP-Y(LMSf?eey%RFdv*scWcy#PcD$MMo`# z@nV0}Ps17q{xL;!IBR7@u^-*tH5Qcf8{Sn*x5`=+275$s3^9_3P963Ls3T`!s zy1Ta)aK_t6EXuf-9I@L|>1HNozAe9SQCQbdY;oW;>&o*~etU9QsFxXZtXML# zDA}7xKUBS)S6ZU2o%VUJ4b0Cq;#WCCs|l>-585Zae{XqMpfYu+l58d9^qnuD8zGho zgoEnyBYVgkBujdqcs3lU2W8yz9?Y&%X5FM0=z0dLp3UiOkC?Kwny|oe4=q=fp$FVu z_`e9l{2%MlrEf64jYS99KrZ7eElNRK8wjWcG5?#m9Fj%q1ytyYkW~4m@^+hLhi^P) zJaS-MOEMwvAy6ptYAm_!j=ro_iG|sJX?{rM`AjevpWk4|vELf5Mf->g3TH7qaK#Ue zJ47#Yi{16tZ{0?0e3eB?d@-CLs?a{4AhY;xnfU_uLy8?)iXUy{&Z2@+QA$)}tSv4+ zcgjsx2QJ^yJ;EXs{uimx`Z7^+I+83je`ARV4(y6I^T3u`uKo!_sf{cttsDNY0U%#{ zrvSMgVW(EX6Vpl#>~m2;-IOm#>;))A1BWqVe&}oi0?CMn>UMKk0FFUKDJ| zrV@7W)KOcpa6UohU9HZat(h79VE@tPr}a!OFko2JAQv~P8XgYSaxY1sGIXmMyq*+i zL*#snqb*h9yW^9VC&DWoEGZAa{`WK*#l@O-3n9Lgwcho6ZdTM@6$JZXYysy6^0qfc z4x#&4UEGs6DN^{LVgc+2`pCLnx_G#5ne>t8e>+kbwf}o>KMD|dl4@}|n(+kd?~bLi zT+bErt7Y9)m=D)yzu9<c1COSuTC! zv>EWd<7_^`{*HGN&A^js53lU5j{PvV<5YL=u7*q{yXJtprtjl&jd^kyBeR{9@B8`! zhFlNN^tRzK3joe(EiOq9U2B1TTt^1>jb_z>Hrm0>`J`TGR#FV$tBq$*se6SO=teDj zW}6+$f6YVJ_k>D5b1B%pa)o%5-;^tf@`2fTOCg*P<6g5K+9lTSK~2LF{MTq6k6Xbn zJAY{)9xP*y%=vFpuY0gN6+7Qgy7aT|8;aMaAM9-|x}G)uO22GNb9&uZyw(cb5AH%U zxc^$Eg21&yXvlXPB6D4Duve^VxTy=ELYr7HQxV-04(q%-{j)kFD zsE0+9b`x0!q>-wJ`lf@>EQ5+@T=6UYw}ezFt4rG#;JJRr%0xlRxu)ly+&ec&*#zW( z?QLtXUD0#p2`Z+$6$=jR&@3a)l}0m1w-|psuRgzPEe|O0jtlbSiu;MS;`wyh>l-g#ds$xRu{>ys ztHd6=vE}c`rtd1*&vS%gIiCuHheY9y-G)`Q(RIZerx*tZ-WE(Z-{}zXi;chUm}@@7 zg;s=wIr=)*9q+1F{;3r3SPa3V+m`cx%xao)9vr~!)?rBF*(Rb%PD0WV?Gouo_YDyRig3Beyqo3kzmCTxJQnb`R;G5uOf%y!i7Xnf?y+v?tWJnhbGO!T zOEnjN?_?1t+cG+X7{i;f#h;NoaW59@BXeu0@xHv^`I=JkkW-T9Wu;?)XoN-mHR_yo`QZil+M1>3)@`%RuCL`0c4Q`N2%g}@Xi0W$Q>6WEL-$cG1-o1qTXr-RI zi4SmE=5pV71r->V@a`p?5Swcs2+Cg2-Rnml>eub56_8N{9a`(gfufe1cWus8Tvs3S zea(Mg)dtq~asvj`h9wq(3q-mr3L@>#^# zEnpNnzcr=UaBV>`l@|XP(vRp*VN^$fp=wjUQ8-ox^5wecm`4jg$2}~3d{pf=$h+~O z8{UM8;Tk4KGdt-1uLNuK5AxqLJ;f3~p?S=vukY^SVj(`J8IBG?;0Brc|8cs6?Cnvq zmd$yp+<2z*7`N6ZyYO6WOK@1HdfbE+_-^lWqC@Q5($w{Kc0C>Kt9p*3I23SLCaR(~ zGr`2Aby46>^!34^$2eo&^0}Zq_IIk4GdwtWAfoEL5a<<*=8~=8X7c4mOa`Q!K^wI3 zyc9*X6NFbP0wC4Stf!kCve&$54uUjZ1mG@^RNc#u4RimB&GHmWBB_yNyeA)q!u4qq2M;8TA2mm6Y!(1lz zG1#SOI4u>LN}YZC?&pK9r#BB~#~h z%2OIt-BMx2DP9`b_0{^c9b17aw8nLHvc_Zcrp2|x>*D7i8BUkCw5NM(n_6lAuz$`8 zVh#U4G{!+z@dSiAS@aRs5ts zGV(#G8e}XN1non{0k4Gbo0u#cB`N+}rG=AN>*dfn<6{?`?+OzwO|L@b}iHS@?=$Wv9A=vK<+fQlzGKj;% zCGO?C?Lt+y#3tL|xsThS)`zr?=8CStq=ZF~ds zY<19aLe6s{Vk!7_EQO24LNb7l7Ut5-jvlIWUJ-KH@UfDzgd)ENTWf#NFJ9}2gL9eb zt}D21@d|ALU=gdpkF)q?-g5|c={?-5%nS8v#2;Xy^T9UpBOLO7Fyv+w^bnb(vAUwr z{re^Hd+#-c!8L{S^ZG;Q0~gxBL2}pYp4AD2@4>}uW-I^Cmxt>230t_QEiC@{3m4Z~{>!}qH}gd_~aFN`tn4Kc17kRW_0f(7gtGff>$ ztY?IHroV^8Z~4W&ko-+>QcYngNC`1$Ok*?UqEAEaY2s&^8@RC|DrPbda{hla8!qK8 zDl%@&%BOHXsK;0*$I#`{|?vZ47A6BrTq^5{tH9m0k1onf)Vs3byv8awJho>>r1 z?4u9!ormRCSMAay-@Y@#I()griGB)*00xFP@aNC>tGl=1T{Pbz(9@NH_5a1OJUrv@ z1;Fnb@Ww*f+gT_;) z#8)jo2jJ^l%F>eyewV7VXA`vZyEpf{hZn=~XonDz5Fq@>;#vgcOOLFK-xQIBM#)u>E_r@;6^1;+Y4bNxn$&CFVm!hNS{}A&Pqrc|| zd+NTMB+1Q1o!0z-<6B`K)6P4t6Fade6J+4S{J2UO6;-i$67Jz|N|AM!&SlEFxRu0T zL|EbCC;Pub=Z2Gm<+lk*d^t%nVQ|A{P=9sYv(l|OM6v|83_RQm{PGrI&f6=HKSm(% zbfn=}f}AaTb#Hi(Bcz7WR-pw>4x`w~ZS-rm-4DJI2VbFfd&* z+JpA9>p?up@h|LF)=OL5f&3C_@0QJ%#yCf^eIA{4m7n+$9ir7PCs?Sc>-EENjtP=Y z@{A3wIOCAIcftsD6Pi;NG@ROJqX8$FVRbBTZLMd`d59YW7 z#3e3qf&V$fGorA>9WEM2=TUm+aH0t*m?MYOJWn=*$4&li21<1u#qY5<-PCvR;*l_2 z(Y;1du(C7pCoKN)%??s=aHkM~wGAW;s=;HR!DI8=m}0KqzPazd5=Q+ho=Z5H^sG%%lwLUPZ_NlG66&6DXn}PDkhX|Eo&X=$?d3pgCwD9e?sJ6G zJ>4k|`6So=Em@^G=_*B_$1ynnWahg2(Uk~z0XIeiq(hBn41~k#vwZ^rtKKMS$ z(O)tkQ&>4AjV~ZerZ~0E#{yLTaSa9PKRuV}T~F!nxW~>QJBQS0zt^32TCKy!Md$Pv&>64k+mR!XZ26Eg22A)zfakVtZR$E4+Q=`U4MY2O6fGF zY1BVZLf=OOt}q00!OnEk+>EuTuEfZSc5IA7iurksR#KH3An3R@7Ygl%q?xKJdFz?4 z77dCi;ZRqma6D0B3AEt06;5*W%7Q~8yG2eW^WM-FT>4mZX~1QgrD9*c`ysi6O1f0o zJcnK!M8?W5Q;WTtPY|ltE7CB+p&k@j$t7|8ykmwl=4Z};5rv*8mgt2sGTRo1JQ|;WGnzSEMCe{>ZP;RCUOv|j$4s> z??uFLPz3DwE|qji`cs|GrJ>iyMwqWgaf|UC?qT)>G45M#eGz8d_VDSV7pUMAAL*bx z(bc3I-x?$sU=n5~2D;q0i-NWRX4#9%f@bbiyWp)WZ{~bgtO~+PH=BCeurthMPP_{I z1Jg|__ksr{EN~8+jBKQ5A z_<8qD2QUQ_bE-o~jC@Ds=eufUO)~pnoq&2f1(?w!>t6_olTXX4$uezi^->Y$`NO5vn=~3eyitr zyXQ0+r;Q1`SU z{@%{d=p2~Z6$X-#DOj|l=Opx&U1>rH10iP7qzx;QKW^Iuyo&yAAU%SgHFE{B!y zIIiM}pP;FuYvzsQ>ZR3lkw<+^unW`zp|Pk&oZG@92S#wjd*w~5m93C3vu5tJOi2ACq zwzjV8;!azj6ff@XUfkUwNb%tAQi>EPP9S(F?(R_B-8D$D;_m)&PS5-Q`yyAl$dkR+ zlrhH`6aSOkJA*#Yu|Xa7T{Y5|+?OQ70}2~XJ4%wooMwwscps{10Ui^MV#Glj8hB^R zqTZZd_Q+8MKnQnk&&jKH?y1+K#bAL`e;s_P4x)PKlciaR$FUtnthtb!DTeuUsqnt# zI(dcGeoyP|sGiPHkqSFk!mzlr>BI=?xsI@PWLqp9a?tOCqaX{r`F_&#FXQ!rq00!P#|`N^a^KUSPp%P&)!cR6bG@g4ItsiR;lc;}eltgvu|bmiw{< z-z=l`To<|uYccxN=Di%toxUY3dC(MS5t0A#ruK6EpMb{no1=ION^sIpKY+12Fg)k@LVmbLf znDXTAwZY)uaPHpv%-MMt2-c@Y9pIEZB`2Lt)zM`79|fVTAY|&jc#YrlP+xa8Jm~~K zmCjyFVF~9PAdsvJGO}S2|3e$%<>ICM9;Oth?hNPPH4NCQtMDuP>c7okZ&7*qYPNiMg?2jIr%t$GTtQzWx{SATTJW~ z4awgYn*~ygLOgng)V$|W8lC$?r_%XGV${+?7g{dq9g{d!qBv#DMF31fIU!6?(6}Kxuu$!t$Ou`Wi!ONC!+h zmH(zCBxi2H@rE6bD}Ek3lJL>7NB2S$o+x*~BF9`xlxg@B;_a8%SBhl%aC-l6FgO3OeZQ~kEyM%Q z%kr7!@4tp#>9`f^oJ=ZXo-E=TZY$8iu6^+!7Ls=#>GrOd9Avt@^Ok0b%yqS_mW!2; z6ziwHJhvF_pO^K|k4Rqq0;^e+_*;Z5g-4ayuQwh1A=q=1aEiMfC*#|)0KgC`wUgqAu?IZG`<+e;(1#@Bb|tV zeZmdC=b>z?HqyN*RqheS)U$*@IT#lAZ-n|KMwN@ixf$9VZTA;l-d3VPaYl$8j@o}*FrEm9u7Ev zyl!Y;`D~g}Qnbi}Mtb}D{r1xR^Ez1Q^QV{1uNogx;iAXCK3^0Ie_pjr20Pq= z`y&bcT#WrK6sXgE(mAi#P1Tk7LuXE1(~XV94bc2R4W#nZjzu6OM>-4w4^l3_Zxf@mvH&5BE6l>mJW2ECm3P8_h>vc8n zRs`dtMvVI>Eb9xisvL)gM|5^?QF|hL3C^@8&@MO|vo2;hifp{Y#r}5~0?kmEKKnng zT5*8Rz^R}MJJ|qB!lJkUtO=ev6{3}d?~eGZ)iV%w^^Z90(<_{2S#vz^J37DYtjvnl zq#qLnD}f(`WDD1@c}722*+)vaend4cwaG}uO^vo+UdB|4Gl12~Q{A%KJ}hS_&#-c@ zn#M3|&_LCIvq<@QJy@Kou8(-O-RY!n9>iXjc+ZXUb6pgycj=Bsy6mH$C2r=zLh|S-ca{)25AHZEkJP#(|5-z=l9FiQ z*QOP`>>me1(_7y+zTd3>JFUY;G$Er-=-t(W7qrW*4K<<1O6l8n1GuBro^L?aGdM;uXL+*zX99lnzS7D3V1Qzl)ENQ8Qi3fTAe8PxKPkO}O%$EHrDJnp3!Ua7-#w&UU1a#dig7mzC=X|Gv+V@(SlM)@XtWs&0?_T8FG7i21dk$6ssr9b($N`in!;`6L>8{j4lsIYyYM>6jD#pfiTU- zwwqSv_HGGLiR9Zq4$l@(7`e$H6w%=Kh{rbV*EViyE91|H&~(p^BNTqmq`^Rq#(W2gL;AWB@L6Gp;ye@ukr zXY1J2jBVVV!kizr)ZF_@)y<6Ap|}`p7t0>{HLO1OLfNi5rF(M)PS>M4NxE}x`!!>O zmk153o@=aY>(q1wM+NhC&zfEv@WeXfnjmN*p@WvjVJdT}cn4l&SNTk0h@|`U)%riT$ zg)=;TIs5z^`n|XXmvs2Ihi|`!?c-L#hYJUz*lV&v4wi5PiIylKeg6)`03*7>66Qp8H^?2mZ%>f;Tw^Y zhhf6&)nX5)lhQAzeX=blXISGTM3gCq@>1eh-)D2-51F0J;{0ZKmv6O%Wka>g)n=O9 zQq(5}3^?j#C#_d0rkXkEd*^Yf!XlLCd7%(yBHC8gI`on@W4T5#UE{#^?R)GCOcNby z?S{I|hZh^BM~?HJb)*yQ4!;BDyRUPB5GEcZ-bh1IWx?*hDL@g%cZhV5Z<%ODbAcD~ zd|BYbEAZwAJ>%O*;?ae1l|s>U+2T8)8C!+l0$g&3rf;CA53&#WC*rfFv8tjW1eD?-cnaX%dW) zS7S&Wg$v%84+YsyZr>ESJ~4uc8NrAX1pg^#@m!^6d@lJD^8FIiM=jn>nk2J=FI_m} z@EmDI=0D7N@!ui2j1CWTi=yWE$t>X=g~m%EF%2cR)a5STM7}WcrApZho-d2Nw`>%Q zrai@j$vUwYr)>el1+Srhsb1B7tqu~DYcm$E!swnDo4kN?1%K;NEYiVB9T7TA`;6Rd1k(;8yH`n!q+Qzmq`o zL3A*R94!PFJ2)a=SNCL-DckP%gFM!=dH#m5+}sa6a_{e2#t4B65P2_Q-pSY={ngxycwUJ=x5p>X_E(6Xwx# z9LoDAJSm$MKDFtu;@(24rg>8;Q>b!P=^W~XsvCKh2Jg!H%8*;!?N|Cr}HCTFIc z02cDA4fX4QANAY$n~2h!|4O$S&jJdss*_q32Fb*h$c7M2_`V*yWKIt{X~P|&C+i?@ zc0$MKW^RDB!Ffyc67r2w+*Bkx7I$SPqI(P`k{pV*lEFa(1ViHQROVT=52L_t^m zGk5Q)L@ySo1cQ`k5*%7+?zN+IT;t{_a-9-^##PI~b5uP@6T^Y%z3u>^BEs{bPwwi5 zZ1BKgfxxxa%vF_ek%}-`!z=7tQ1SFIdhif65bepN<6-{q*(^eXr$9j5LLa76POb=hMeJIIXs0Z4?8B~SnJA)$rX0n4SZnn%f378!%^m= zzO9pRui+9?(`m+nZyfHNOQtI&nO5S?I{X!vFQ%T?x07Q~xIivCRzOEL$>h6!X{?cfO&$o;)<)E=zODuI0eOOvKZjw|9~5Q za(AVQA|90dbty<)<~Bie=)`y>|8);d^G+0wyo-FeGBbSDpY*yxu&NT^-r+3^cT=Uy zwD<0wqkvSsf@kv)mYk!KTK1>616BQD%vpLa!%dOi5Go6)F@1 zfl%Kawu!*-@$0AgRaV7RHPdVphWCgLadt90txx4g2XMg#+P*Xqb^7-b9jVmUd-sv0 zWPrS-8eF{{l{$M35P){bW&pD^If%s|{9=Bn>`d^J$^T@&P{i2nJ{8B7 z;%AFIPo@^6T%>@2o4Zm!+?u&^%G#6`59~Q5&|0`;4^Sv>NR8Y#Ly4Rb@LoH|LdFPE zo(YN#u3ye*F$o?5S$^WAPX@qTpBX)Icqt7kwJ`|5>qtP zD{QhuF&ZqFat2MQ>F@Md-+EbM!|(BA(T|pmQK0_%q-&Bm7ukOEX<$3Z7KGd&tov@C z-4Du@?T7Q7`7C+jd<%vew@p7`cZj9b@7CXI19YPDCt`kH`$L`m@+FW^yIbYwKjR@p zMEg&d!nVbSAynx|Lnys}CK4fT@90_n6d$$ti8tXtPVv825j*4Jf--%GgozqYrz|9~ zQV$VzSC(5~PB^Vs0@DB>^^vAFJl*T-EfHET;YH)<_l`zKvU(F>Q;KsJS-Rl|K`~k3 z$V!#;!%-K;jY)RaUlSOLvg!-6W}K8OltYECbkJ?t+#<1^H}em@gDvADwRmHGPjeIw z$<2qjIK$X$`cH;kSSG=(iQ9A7ZPOy>#~(*gb%blUcX;OJk>;fQl*)xT$T9g9mVBO< zyo{RKK5Lo;^Ze!QpXLTtGg)V~Olg`eut)Qs*l48TvzK2+hLkNZa?gILv1^dfw{q?| zRE3PpPAHTizU5n!^}bD^>RRRoXj2}R;@ElQ^G;L4LOiP)WSb`K_lhqiDSqER4}kfw zC?@y;Q^;q(BoLgx^cIeW7ngH@+8UVX{(RTN!bu$0?Byo+*^_8i%@3n7CzElvskV1S ze6`ESmvcRE2bq{P7r<8tY5d5$zpnnW{yW+|&=1Y21!qb8OXz2LPG`xxp=?$DS9KGK zEx~oUaXf{zmE}k_O!nhpHpJSi@80{z-;0Kh?y7w<6Yem{XE)o04tTVDG+;W*hzHg< z;!xyi6j&|ta{D8i_~i5Mxf|7L(N>3_=T~J>+2Z?9GIgzLSlR@uPj0Y7XT?B5;JDa}P&kF1!ACgCyhhS#&t-d0OUZ%udz5Mr^0 z%V~8N1=|?ZEK1sHS=R7Q5RH14?&ZDMfe;rh{6if*uG~F;I%p*x&=$3tuatYp@(g?? zn3yH)9XCw7=sh@)v{`FqCAcY6)PH+;omB_nk$W3+Qz=;g!AxjiNV~LRP4k`tOo;KR z@;`~!vNb2lfjwNfFumw^&6xyn)RjsQ5TIpMXP!VhR4(R`S=V+z zDZY31^G-l~jaXH%?nGPdt+A#ZUb@7*chO^NKGGPoseA8hQkoVJeP%F&?m;^$MG;&xH0aLG8(6)>eIawR-t+^) zHqLs|Y&BqmSaN4sx#sa~eRBErvOYy}?<3-a`Zh)Kk>MPeVc+uh-ywa?+a9S*o4eK@ z0(dJ<9P|$EY6aU4zaCUC)nBP9-QA~6zyt~YJEK5f7N#{Wv$mlnK!N*JCI$X1Lbr1N zP`pd-*1*m_(LoW8H<72yX-d&a`kRZ7^oQio7fSRfB)eq<?>(w9Wi{o?`MeOkwPv7FxH61 z5~z>$tRYED_LR?pd(*^sTc}UyCEO!x)bO-*?`;WUxU1Ey{kpbf^m)#k=-@y$Bg*fd zt%*;j#r;1LK5jQw1T7yTLT=6cQA4&?^iVpv3V0khhiIiK;M7fsx;D-!A$$OjvIG59 zA$=>+ljG`mznnrjbu70K0ux2Cl5SLdqcyfCYE_CXzJLShCR|6;M*D=2dsPYJ1*K&N)tzpv84st*)~joJ5`9L zaptg;*H=7Zm-v&1e@V77Zh}LrT2kQe4ZTcsr0pxsYa0x{iMCrlJ=1K3(*!i>k{zEr z{TF^5<+JqVz1?MXc;2yOvx)yUAwkb5nOhz%eFmO#<^ z>Q00>E1iUDbha9?OunOt?J)=K#zG=)UCrD&JtW~!Ofp1 z3E$p08y+P{{x9$d(kG}Le|$UfMyw2C@z`GCH%3tU@%FiK>89_-JrCRFHZ!paH^N<2 ztYg-5e5xs2kN1$r;=>Fj)4{!EX(WYu;war=QNy_FuuKgli(FwrTdA6=l0CGeK&Cgz>3X{Q|#GWYxRjS>@oTuxPm(Y}M=htk`Gs&-S#~i{XdR{*-9f z-2;=n@BtyZ@{=J zlEGB=8ne#xkUz-=R|oUz*n93Xf2kz!aLoAkvUevu-L#Tmm!y30d7NasVf>6d;rD?~ ziW`dz%bdul{zdOVRsKI?0cg~?$0q%E&9&}Zr#>Kak;_Z)W*E{q79F(b z2@Lw%)}!!;Vh%$cdqd=wG)~(8TroUtfxOMw!N~S;)oE49^4bYpnhni}{O`5U`pYHW z1P}5SNVT$J?pCGBYe#l|Q0}QQX4tG^3?^;ghd;Yq#(H&`8lqBwJsd`vrx;uj9=JEjPlA#ETa#|To%g*zK=$}rhuiV{7R?D3@8^>B=LITd}D*mC%tPwIn zs83`~Y$9Q)EFhZW$u_yg?w7x|C60u~st24woK_?h^(n++AFR;SODFj`dopzGM#;aO zx*G_tG$V2*WJq7XQ>>kmF%c~lg~TXuLBvG!olD7f6i!wojoQepkfE1l#+Tb zFN@X9ks&=LN?LAxIdlW+E5&01rVNxdl>Hpx4Q{dCX*if zUSS8>n_uvg`Sb=QP@ZP5*6$zkl26Q-3gT)_TiqVyS`|kQNSgRMV9x8DO~dH1a((q6 zyp_Z4QueFdYo!Au|2t)f@QwlJzJhK`y~sqCJ_#5Kl!tURY4PjdQ9=iKm?)bpk;Q=(yDb{6>y*5lHyr|)yjA1qc6m1`a4Db18Wn7Mi_dNQa1V-C_)v!nEM<{a6 zB;(h}HntRc?BF6j%9+TcA?d!XicKGlfEY)maZnRmK`|-tU=m#AUZ(nEv#i}zTdYdY zHpycm!db^B0uWK!{#Bl3vhG*y=GGA*qM>_PQ;B&nwb-1>k^@p>w}aT#HdV>S^TAO# zXic}ArTmg?Ow_Lpf2!sZd);C|?BALh5wV=Og#=so5%54>eDi>7G{Z+7QA7j z*-q(mpkJxApI{avQMy_XR|M#SBnahvD<0mw=lR9BH>mw40{bWi%O1_Fl@SOQJ67+Upv@0l#(eF1{1AI9{UwIcbCC&F}Lu!>UnpO zbc>#&87eG`$5jVwqBs@`RtYU=)eOdqUw&=LKakE!w~+QQ74=b8_TsD_hErFqq*V*1 zhzk@ffh7uO89;NEoNouib>&8SW}b`}5x8PKj)I%-V)~k7a~-Aetm5%Gh&8w=KP{J?_9Q2A2ol~onIr|*5|Lv zXZW%x9~NVAH4z?8ml|4swvb#{2gTW~AH>@fHua98$vLgD!+9Ef)rTg|spQCZ;OB{d zWvP}uQ*>)mnndGF9e3G1iyZ1?KeUm00a6|=crj+iiH^7&1*fharLmVBEvXzgh0mJu z_^sSO+cpVxd_j=AL|O{ZA2l5coYC>gFM)N}|=={EG*nd11{WX2RA# zY>CZ-c@E6U!dw8;fF3F)nPLwck&uOp8Bv^ddDdf?&MXZ;ej5K+6e}N-@5Bbo9$8n= z8t%_iNfGLS&`pf8lc*(@lUPJcq{7$EF|kp<@0I9Y!P&wM_Q;GcN-%pMrTYUfwj>!K zIOu3ObeV3FP{~AcmJ?&|dekHdU%|2uG>w%i3G?i3FBD{L($RRE!J8dnmZW|{A2?MGG6kf||9uGXG} zW5R<#p3j&!lnf1r67!&+6FeL8(cR-K!{9oVM;ihS55d<%n6=5oK}+q{wbK~>BE3w|%J zQO7z|Kbxts=U1bN#Ny+$g1^|*GNuAr^GM!vM)4HwkKwfb6wn*K3gr*L`?QF?U)(5h zgSs-rv*J^rpF+SkvvDa zAVVp%-XpFa^2Ebzxz8a!Zx!1lzp|7SIbDs)zD0c7%st7D)l3$6CtyoXZbvo>u4smd z>_4=^0*z)R_Xe_MPQ$lFF?IWmQu%?+7GE``;9n!;X#>QJ0vG!CMy`E3Go~VhO%D1h znn0z1NcQ!~Nld5S2W~ilbvow@r<3NpwfeT_iZqQiIg8=p`R6CCy%>4>>qhw19X2n= ze|wAH8b4`1eN)WYK~p7&HT_K5vr>gpAahYx>~Nq)JK!$=gJVW9U1^R(F|w`0{cZD?Z!@UsSMRsEgjJ1-UT}?LyFi1oCv6Ls ztm^5_>c(e|?N&d8L_?aLlRGi#k1uH{=T||a zvu7se+*H#~G>TkSas2us?W42|ceXJypR8V8QF(sZ9wqHmT(!C!1>Kab`AL<3+MU+4 zVrJ1dGE(){h|5^^B}()A&xPi`-_jl?#5hL-*0S&{i$9B1C#2+qC@(5+m)BY zxgQ&<{LL4nv%|O8($`98-(Te)$K4a|oZ5J*G~xC*ulXqMy#S*UXSPcrbs7!hlpJ|0 z#6UU2K;U71&}#~XHNkL!UZd6S>4qxqH(gP$LQZQ9R zZNjf|TzmoXaCT^y+cG?y^60%z=db0_ur>=u+kybGpm(e7^#>ql8@wku;rD6yMRm~7 z`SJFq>y@CXx3y2ErNh$1q?IhX!=6>H483K}UB82a?ggpe{zXXBNff}Uv2A7-3^zS= z*9~V~%ogrW>P*CSR1b(>S=i_%4srA_{RSwj^Kl}mo5{>tN zZ14WQAL5UR$KNPqSR#qgyXS8q+3Wg6OERP+M#pRwPg?d)10tL7sywarbVT!Y{_UBsJgFj@I`VCR-HWrAR0f4pT z?;0s%`Q{y28j@)%OSe_F#ISVdsFZ~hXyNH&`Kh~a+Rxc zuyc8FGZ9)24qg^7J{*mg+c;%;G zw*jVXuNIph_#Hu1z{SG~b*a8QfmdCgvonq?ID$&LtQ_!`BOF(TG22Z+z1ni-RJ}&> zQk8?hKU&G|;I_!tcd)+1JJ7*xg@Fxhc}al? zGr^an-f56x0a?Yxp?E9+E_3_^+DK(-xuh8SMt9v1=ViE~?XwE=|16!C={SSp!>spk zwQ0L+T0yW79Ve+ud3JJSluP0FN3WRqE3pDT@6a6QtdJyfLDaybjW#Y_vJ3N9gy(Sn zyN~K$OW>{Q$6|AeSfq!)yNBsA6*Ey3)mYFk73G!LC0bxvwIwn5T3U6l+x6vkYz&7u zG*`71YcW3kebt6r-KYOoSp~ZBHZCqLCc9{KMIKeH@2=D&M?1iT!*h zH>}02$IHh0H8ecvkJI&pv*%~i6Ye`@@Vk5;K`SS!lxs{W=mb0sWj~dleeH>yCz}Gr zG3Zkn$$mw8G*38LU02pxb_I7%KO z`y~)p5r#RVgMB&UA+xNbmzqAl!v)q9GNPkaFG@Q@Ix5dzn6&T>qFA-WsQjfpNijE0 zJrLHX>-5GD^R~yq9r){iB>-G&zkKyeD(y}&3(8^2*hxY>WY?N}JcnNPL;VF^)HkyF~#W*%B-$ZfCkM@-Lc&?s`7>(`slCxDJe? zbCj(e7o6lzabyO+QRX)iV1u zSaZQLW;4(c-_T77BQ7gy!EHTNQ>@(&< zgMHFaa9J@E7JSMNZW`))SJ}OzO4(544g*G+25>FbrNuU9jP`sW>&J&3R=yD{n3*{w z3Hy?@+uEOcM(CRUf0M_I*bZ7CrA=c~{b1Dm5taqgYT|@3ldD0MhvYRabS=HKzmSB4 zF!dlgRbj10xwA&aO|sst7Plu&(G0iSLarIh{6hRdTdH}m!C;%)TDMiIxQAqVv1Q;8 zr}wkBO`(WO;Fo9$#rP4Fz*_IddENM!X81d9A1O*eQ59rfJ(0G-gyNO(`LXzqBsXQN zxwGNH_SO^U8^yrH%|WH?s-stGtpZehjTWS#)2IN z`nvs4zbX1O6Tf>q3_=TQ{1)rhjrOmsmep~8^3^`__{|$?9xn;%oQ#WhYF!j^=lJO7NxG)L;)**ezF9&J;z*Ukq27e)Fp z%*br1bpn^IwZy1Wn9|=M8})C|=Bsg_{5a>A+W5IK48;2x9QmTafB=W4Lo1lU=$Ze* zoIWy8kH4oG_0ev4hNB3|q;4<;nEuY;rA3vthCg~#65pd1$fDKacPZ7ZxE=ju9?5KG#_VH2NaI0R1u=K4z9I1E;9^vCn#R(HMb%pO>nk6ot#23 zZ-TaVSU>~Z%>Oro6F`dM6OWxrn~P9tzkEwOO+cZS$w<@IfKw2{9G%KOnmoQA^Sh_* z(ad`hYbhQbZA3w4t){Od8M`&%XI6Uff`!vf^~@!Yn=yZy;$nUs+=+m8I1NW&;da+K z5#(?*hS4A{e=1V7&!2nR<7(_2NZcafnf8M1^plusuum$u;IO8C)ubBig<`%ubo9_| z@iiO$+^W9dnz&?LOORuo_zmYJ4B?Zn)Klk4y@}`hCC~ z>pR}@0&MYi*;d#5aA5A16fCBE4F;;*E?Y20na_Mu0gE1;HAa`hd5I$XRU)6mM5(H}#GUlY=}2ykmBrMB~EX3*MrB0C&glAle zeEL?WEdTl43F2Y%JfMd4Z^eba?DT5gN*2O)Rv}i(Nk3@iD5v+{)QcIytIU^NZs`I| z$aVJ!z%ofC6H(+7ig8&L@n`wb8;wgf-rP)OpgST*{%RmggF%3A@1qV{$*P>{k>9|^ zvKn=~0+cGYz*QS?CN}KNPuD)xQ4S-7n0kk+qUp9nSt;k7?i~&kC^tywmxt;VKaK4r zV-fal*V^E(o9+Q{!!=3=E8Qm4)Wq`sjNwp0r7BN2mu)DrmCL?nCPhU(M#H*u$y_8e z^8U^CJ8(UGalgm7#fuNO<{L@gtX(cA$hAz>lN0?Dm zOUYb_MJ_8net}r27x%gTVkkaalEzL!P0UW?08qSCf4DG1)1kYxanrw(pt9jZuB_5Z@QDq+X+YC(9NJ?IX=6A=~T-k!AsF z40I^^v-@@o*8O+=Eq`pN$DY(@y4CLz%Vi1_$4wNJL$77`tS_x-rg*=MyPtp7_|F)y z1#YO|2ZHTy-^N~zonDzbuf&#{Mjafq*dy!OYD#AtdTtOz#nvXP)7#|3<|eI2mD>^y zv+?tBAG|W(>LB-qdJ>&2p^%c>OF8EvwE>%AN*rHTmnxrcEXfmW0D*oK4(L`-+N)#+ z*BG^0dIdOHq7hsqQKDeje0-$lPd=g+R0~S5yh=ea(mNhh%H_gBVkBB((@6JNnO2M# zY*v&^U0%m&hFiHu--Gw|_3Ya4^I(PG25vJyiZM%UEYobqfxn361E0U8kr?z=h4w&$j#!1yzW0jsOLT z0Dr&dpB;CsBV%5{8G}*v6=NbF;6HxvJJ~ zP9*+Ie!n4*o^4|!GO+KErz$l?FOBk=RhScysQ#_yr!{UI-`pNsM%l+$onD_x09T{m zDD>41ltau&E=n51gYZjSz)%Yhc`KH66J388NavPbtA)WXOYSCZHqNBdkZ% zNrUF>gUfX)GPL=;^uWvhXrn6a%7^XThO&MkX*8_-+%(o%DG!@s2uEMEmPJ>$SHp_n zqYjwpv}a3)I!=QS8PvGoEkETRA{8oAHp&$xF;92*({7YXmve@e9qKI4Te6ojri5}u z4#iF6tB)>ovI*PUpq*9^fjX$$8Y~`}*x(JMZLMKv=O}W8ea%z8w>O(W{2nU)ag#K# z74F2xXQfbahazb(Ou(?8J?!GINfij5v}g2bL^3BQT#qZVgl}i}8dFj+uy~Q7r}}}e z#8eP1B62cbrer%#!R7fQ4g3aAjX`oiD-4A*P*pc;tR92nJo4sH0uZQo?~@e*SCzR3 z$OkO(EUnV;Hv?|tWXs*)tDfE2rMo`F98Y3xf7Fiki4|h-seqTj_xcJEn1Jl|P- z2mNoh;w%csfrDl-!QXz~@jf>kd1IpaMVE3JPRDd#82Dsu`oP+p#HsoemFo^xDK8Rc zsdjoZEtTzEt}C?MtjeE7EjfBxNKSeFXp2n<#k#j!qr6R^p(vLjXyYixoB$|b09q&1 zQQc@(>_wG1tMC@hefI{bDcD8j;yA5Trot$(m+PydU8|b!*)guv7Qq^jW6Im?2b|%t z!H1l{`sH5K%R~_$1j8FSK|B$j-y*4-0T@BC#J--2SY+n^A@qp3P!0l3v!Zpyz7{M% z8Y}OIn1x9@oY7tdrR$t#`OGTHDWVTQWTtcgc-R{}^nH&IeG3#lc|F~H(j~e4QOxSx zbM9Y-3l8;TV>B0R?PN6=7F5Z@vB;YZt+Nth3eNhNVeB+0^;la1H>lI!D5lr_!+9sE zuHN`fEHfN9-&-n{MuX5~yQ+k=czh>PO{zJYOgBZ=fIkVN?`iZqIi8IjpmvOZAz>h9b!UF??rkjqckQX z;WA@hkNW%%gKoQ8t5>_9C_VLtIbL)t`oe66!SmuG@BINYPE`{_JQN9m?$;fj(Mr_mP|=wG=cFs>@4CieH0C-86A@=f zQJawohD1%|8SdpSmf}I!Sbbtg%Z)J&u%&`hH8U56lT-R?bG{P zNLMHyR^ZB0_&O| z*Q9whQBC*{w)&Q^i&(QmTRFb)NC^ltV4oy*W4kX+0RFO;UTxIg$$hP4Nlk^`&YRJ5 zHW4^Z`FiE~oC=-D_1IE-)oZLU<&o%C$EWWfF%TRn$g0WIn`2f--j3>p^!{?3ZMKi- z@xZ2em%0LnjcS?3(PzdOK{qMzVqPCcKx;00X~a|LM()cg$t_B=Dpk)_^P4%O9zYKL z>Cj6w$qrVqp4Mty3zk@BBy+Rr(gYyhkPOts`=+rja4*$9L8S#fB*_#1E3&^qQPh(o z_)rNHyX5A{KTB7k&zT?F2STLMa+hF5bJ-1GxNW2uM1hH4IMj_n1JudTI$OW7lqOg($U8=c3f9_n; z_wK~4!I7VL=n}9F9KKVG3KXV5DjbH59To(hg9)PN)487i_q)Cwzz`D>PGVUgUhyO& z@p?MPEWxxOh0zw`g|VVE(DgH^Hgx|+=q3O9AwscJh9LFUjOI8->%!?DnM^B|!WfG2i8;g_tT4V&IAh% z=y9EER=q0GizcfETp>+i{Q=ClaC#wX^aGR*jGujyLG^N6i(JNOj}2$K4H8zPIAj*J zw0k${IzIVajluh^7bzh5sHpSRsvGMq@Ut2Hj8|dDE^%bFDEso#b&FT@nsa>@5e>nx z*=!SaR~a^}WsO7FH9rX%>Bn{F>X<^3X7P^~^3csd%G&>+40LpT8s7D{))%WpXm8vW zw`_sviKRmpXSDdjsmrv-LIIr5z&d3c`PqY6 zHz+5gu`X9{!-Zlqsw%JPL*9+t$teM#bK$O= z^0+UN9Z?2O%3QIx@*q1NSYRlB2+G{61OK_36QDEV+RIDX%xQ9M$9Mm)J$m&3^rtmG zQlrB8GsP(SLzP{5LOMAyJLQ7xpg;*yvmz#j;BItj*hQGZY4lcA+6IZ{rHVxa^>z+8-NJtXEJ z2}8ocQoPQx!RE9ClisWszW2rk?XsC+GTyYZpSUvFb9~H-b<4#Hq16WYUWDc#+SmQ+ z*ADn}rrc8i%!1E)OnFkyINs@+7fN+5Htyg@U%4{DMd^b0rFn_5Ga6|odF#3t3w1`j z_%A2P$@-0pVmG=s?R9S5U2OlhFRZMmJztphMy+k15w~?oXCxUfiT)JTDShW6Gjw@C zU?R5H=wQdS5@#$%M5$nxxsYr@3*^R#Bb|~x8O25^B5B|YJEKl)m;@j?F)&Ws5=B0C zZfnG8ZTOgg!&A}edN21Cy=@;-zH*SE+Tusv*gCyjegdb>+GLLDVCgoHY?z5*?F2D? zr^2?%Tp1i=I1B=+mjl|%omsgF{$>r?Up%^*7|YRB3(DUeqd{(770{b zY^`ru1SinxuaElVp6DyD25P#Q{1u}7lW(zMHMgjW+UOy2JJFHQ(k?oGCgV-oYoX8b zz#KiyQPf&1$_*c0?Kt)Ao(&OsXGHZ7`_Q>4YN*D#uEA)JE|y}MsFRyX*0bBMno8Fy zXG(3hYIL>c;S^LK?IyPzlD;qosd{x?F01X_n?$`f*Mq)=aXY6@Daf5- zzuQoMju^|5yR;p0iTZvnkY=zr=zI|;)f1=G{pLoc0k3T&Jbkm|u48@~_lEmpv>s=? zwEUvYJ@{h@wT2a1(0?Sh*SoMXVMaGLo?A0*hJabECze^pz5cF5ab+s2XQ`A(!;8wQ zBrQC2u)Z_k(~K-tlIVTP;YYj0M(-8(>{_>^p|bZyDew@oWA>osJ4z8fzCvW>l-;Fa z?4Wt2y z7!!Qyd(L~l{bPR5HEZvsS?hkz2dHq!78WO{u)SkCRJD4S7JE zn=YD+3m3POR9NpPe{7-hoLaZaz-_KP!UT53*&-FGq!t3SHw(*IRXz1&>61+z74=fn z3Ry2T9sX@k%7HhuEhL8Jl1OJ=M(e%ERh6qd<#G!AS0#Z6uq7AHT41lapf6;y>%fvS z7Ys_2*3AEuFrS}_P_ennR(VRkHC8TZ>1sv_uc)x@%anI5^jA5p`DPZVsd65&I^;De zb76O8G#w+L z0NN0)|3s9azYN=)oz~D=MB*3@u=r5gW5JY8Jxct2rXIM1cjc@Ab z_=r6!bqOfj{w6LOY)mHn85Wb8|93o>Wf3}X{3ER;Nz4fN{gddx-ywzvEe6 zN;x)rmV#!Sr5pxbiT|JBQHJ(+m`{jF9YV8bG^C}etdR-Jj8w51&jRefOZetYc7EN?9~XlD|2Z=OzK>29a;3RUVo@>uv<}s?PcGg%zLEG$J4BkYY4b zsx{1EVJWJcHV_NPM1|NCF$LG+8*&mx2L{wf?dD_W`vmk@1*#8ixrySMh$SRc_ad3^}DTYxdLfd9?B_sdP#=1 z%YK|2VOjQu3R=8ee&rC&u_*>-y#9C^HTs$0+K5Ys`M|36#c5O9*Ovz0h}G?-{sB3_ zx3}@*iWo|M2RHNSZ4kz%lE+hdT5HRlmSXP9ijOzZ-sCx6mXa)NOU^|@=bGITyq`ro zAlqk-j>JFQ@tJO;V(ekwMf11XCr_KC ziVY1}_V!svWdIkwF_DvR7^Ol|y`9~{?f?|w*c!10Vn)sCA@LYSvR*$u?a~7z>Q5#g z$hSFwLb_~4PYb~@P~)~M6lcTAjJQvsWu|B6%#y@T6@QU%!z^W2S0xXpRiRb4ucjQw z;VeykV<{3h@#9*X5Iyy;4UqKPTY&nY2M8!x)R z`Wa08XtN=)_qQEAf~;G-g10RO4aq9~yzgSIPkvqM&VYRD0;7RFUk2p#rdPYDfY=D+ zJa?3MsVGlzy<&2SNEOQmTuFe|{#fVwWsbHWLL5I04-N+Ld@wpbJ3(UuXU_Lo_xDieif7E;sYT5q)nmekLd0 z`s(G0Pss4-Kh7sNH+RS3ceVDOT^^UK?#Dm;kL&Z4h@VyBdfv8bpn9|{l#E5n-@8?c zsw%Jxx)xO#$^nx<4$`5X7={|{cvlJRcN^6hj%w!^V>Qxq8LjjwCvv6uGb<+6@+dop z6p~oH384)uV!uDtEMo)p5i_X3Q=oP-%2nisGImb8%pc+pJUP-_KATqjQfG}@l>Xh~ zbLkkmixV=%1sJ4c60ypXnQ@-v*xzUt3{e+FFH`gXGJA6-*Yf6C`b%Evo*A*@6A-J}$xCiv}>p23@?pY=5xc=m3?_6X&FvVjn1 zO^mi3hK3P~aGH>J-^<=%W3E&t%KM}viXR1ilHKER>rmO7br8iRb0i27C>zh`?~)}u zmFwv9Y!svw7i~^8yeXC+ALmX=48UsSxDus@*|qc;qisW&mX*RP3AS(T;J|DND9chOV&8CYbj|Zt-h=P$~TM*7psi_dg@Se0r;$M_f+FJt*T6H)x#qbj_ z4|jqfoJ?Q;!+*KW+3LD%&GQ4b1p?ssASwh5br5R`^b;Q2YR(_$OjAO}n*pS`-Zj(s zS|1LN=Vg9F5r*vj8q}6aO)I&uYU(_t`h?F>X6$jjeq#p$grf^R!1!V!xF0( zUjeQJK08V6pcyOo&wr7;rYUsN%LLO-6}4p(hLgHb$e#-{h6YtA7fD1^zwU`r-g9kf z@(atr-u#MTEE*}U85fw9*G->)pIaX^I3&psKgmpKD546Ym{sr2M$x6o^N7wHa+{j8 zcs=rOlvgs&Cv)>cfY$@H-ZZ&~{9#RLCOgo^@-K0LYOKyvrVov_OQ*NxU4)H`#2d2k z(UC*Fh_vQY!E0BT8M>>yr}O)8liz=i=AwQR_w%5CbZw#opjI*pYNY8T`D-oDuitmA z)G2yMg%-kGa*cLFYq7y7xX!t^n9opjHOYJF$H+j_ennSKO|b7!$M|Z8d1@%vV`R7~ z=gz(yNPjN{CbA)3*qdUGBn~tyNWW>tz?b=Z3eU@RZtQk29UprC zg;jxGf`TJQq9ox}MESW*R$1RMorf%aJ)~Efo2M_yFeu+xMCZKy{OCRxS$i*Uapebv zZvKuE_N*T99dXNavPe}M!T<25N)e%p8F|yRoqxVPa_^QT@D^)>{mtxqIDzfG)Rgb8 zt3HO%0Aw=inW#S$pvL04MPUEm85H3q6Wx~{=~km0`}V+1`#Wcv{wm)LqTe~h z!VjGD=JsU_U(0?IM?$xN#Ap(u-gC-)a|L_e-^b5DV^EWH4MK>|EMc zmhh2h!nb2d{UVAPis}ds|4+hlL-HP7>RB-!(&;~|-mD)9Xm+=9isqGJ zV%@81kMO==F}}VRV|GhH@5QNB%GA)Od(pE^1Wmt(@vmvl1(&gmrZ$CM_ijTl`nlCPRh93dQTv8KtU z?QLs_%-y!w-1VSc&d-we+L*-IqNZ)qwdNel5mU0GLrJ<*_DbGqNwpHmLi|{oi1L;u zDB5LtI+s-Xhq`~wW=G{8`#ZWnaIjK9y^UK*B7BkRuch%+WH(@=-NCDB5nr@=dN)I0 ziAT4Lnd__f*6NxBZg$1|xCotlnh`}j*-|okNg4)Mgg_MsG-uqKVkaKs7PXr6HYmV3{iAfBaQ9QfaP>A23a=1)ZGY`h z7kcZx#_cTvwSO{_QbRcH5Fb8>{@F@^$D01q*&c5h6ne6wWTV+CGmB@X;t8CNN!^e?iEBGxvGt8_b)#Cj~;oYCI&853*ANm_-Lg2&ndzRlWYQfBO;11&}I60GM z|Hrf7hos@}al?0)=c%h#1Y#__k=MKKBgyu}#>u>+%JypYY?FaG6KQN{B#e zs^|1qkIegi!E4HYG$#n)i6$Jq25|Q!1NJ4=@Wr!MB8b5LL8#qv1DCH}^fk$TS6s2z zdB=5~>0jO02ds`8lwUk6ew+x7K?*(zZ)^s>CVVt(7H-|7w~h9GJboA~i*LPl2tFP) z8wPAc1_*7wKeW+)72dG=xcYeOH3UIv{GcUCFmUGD+YbiwZzP|&Jn1CMpdH7~RJ;$H zQ$IEYrW?l3A3yUTs@dVDDt{TlR1*vi0Z-JR$y(<1`YLqegctN>ue}GQp+7*Lgo$wV$?aQ>n$e}rR zN3iB3FQ8@8jQ$v@H2aCJ0ofmFYTP2)nI!}c*Cg7CW$Idp$k*BwGgU{=C6@H_l!tS` zIiUp`{8Bx0aXp7_0ZiLSJjO3d<-qjG_Bq$&*G%P^)7>z@f`La<>K=bq^wXdS`D#rj z+7>1HCIc*FYFWW^RzZ?y&u!idcIfpdz;pZu7E0iL!YeW*J{9CXju{Fk36foC$+s*% zziJql*M9)}_ubEQ^5);A+IkH)-+CJ6w9%#l{ez>`NfiqFd^~Ku17*|@uKz)J0(LM1 zgaeKv0;Jym7(Ub9bZ>#WHPyO=VczEqUu17+dv9np;bOwBgcEzu06ntrr-pC8P9r9N z!$18DeEE4FX8(_nhC%-E@htdZ`I!=-Ij2AfRjdYX-N|p2et)ZbYw0ZyrTP}Vma}<4t}y>;aE}kjPX?MYynm zF2T6VmEXq(@heKi7L$<|UwW`Dcmm~1UY(j?E2R5X6PxoF6e*e$tt**t;&zOq%s+lo zN4TqiQ(BJ{;)VPcbutR^ai%lkM_M{-X*cRMLV_)1WP;*pUgN_M%C(m5yY{rY{g(#a z&R82jU(c^lULY!zV@4h04#(owps1=e+9Ts2NfGAZ$u_E>t@2?X7DO=Tok*F!*av7P zgDC_T$RoW2zqXjS5MA;zZb(^u9|`%>YNviwLjBq31fPVFsd9I4Xjmar*ijNX%M?wr z&=8?ngbN%Q#Vyk;UivEW@&n}}iMKpoX~@HZ^`e`ey0zJ@IDm5~2qNpf?bE(hyD(tT z_Xsd_3%@@}(OW7eLAa0q$_h1u`zp<`48O4qJ=fKC=#VFiU1+fHEYUe!4x37@+_NX2 zZX~d{6yC-G0%~LHMK@3u2=$}Bd!L+9*&WYaNZI$wck34?Xu0R1#mDvSpF`5Scjj99 zn)JRy_-?F8DaNSMMK@(}lKl|~jitYc2C?x*4F?^SO#tqza)XT$a zc0)1bdxGE3g1PhdRueLRfE_FgAza=M zR1#iFpwbo4km2+-dzP`KS z+sKhIs6jt>+p}yB;_d4=oB`z$=ZXd$EEsfhE+p;hnp;a$QdTn|ij|JF)6@>XBwe4- zH%0AQ&QiV2h=dHjZ_S(g{v4kjMNFZeed?UJRPb^#O~F34%*YFaQ+UhRQ$gm}Y_uCO zbUQEZ1#Z%(&M)!V7Z&f`8*SDbNS_j*mIeVErW^e*yFe3}j;eXt8NJjF)x*hBs(h@W z30kH?y2?9L*~rEqcFSI|3SOTaYbs-!O!8$1`p08J`(GLJM_ri`f>$;3IptRv1

zv8=eos4mw({Ifyj>Bp0R??il&dvGSN%O5X>8JayeX)YG`>_T-oTl>d%jO?B{{whJGzlvUwZcHE?QHqYQ0~*b@%@4HP!yIk8k!g5^z3p z$I4e7_{{iW-V&2Y^n>m7?31>9@TuDnSPHyN5qwU$xq9E|O}PT~TmeXSf^WjNoJqq? z-Xq>;2yezLHC6U1lHXSaL6!&8uUUe7Rf22po*$Q6X$7E;f_Z@##*c$ut30aD{-x*( zOBgoaT=p8nXvw$r5*_RaGmzH_YdrUae{Lbpw4IZtYI5b8g&-{iQN7BK_1~g#(&2m> z^>~yw#)S|p-$~%Xq>Kc6j`*)Ssu`-vxiD%usQ&CH8&Ch`Jng{Q5d{d#&2s1jIj}Ji z4HbU1A2EUj_6dL&l@5Iy*))D5TE|K?+Lovjd0&xk5ULyuwHi+D{vO3R zW|bJIh0Rf9iG8&_UV7A1#z*>s zvPo?MZh}(StSQ+J`>T4$BSuT)vK@>Q=jgfW0=(_2Uh;Hj*y*If+sCRe;+NIds)@iW z)kXg+{{+ZLXyz{@RN7=z!ilkD*kqBJ&ThlVkoc~HOgyJl(ZzuyOmGlnP{Blnky04s zF(jGFUxjzVa65lZt$y6<`qp%7E?(50y3LygD%AlL^6IB6Pj$E6r`meV(@_8{{)S6Q z>u~~SSKJWheiJIQ7plu?m6C_!S((cYo6987Jo}8^3#wPb;bMX>vk3@}fgs?}=@%t! zVm#k&_h|C((NI z13o1a+UpV11K*YdyOB_&*vt<8v0+OaV0-4xS87}y?*ECC*rrKFe$Th2z{*c$@}PiP zQ6Q48@OCrTlP$a!EYNv{6R#60fce#@?+qHU&>q`_+hJOr_OK_?{#6^J4{7Vw*O_Qc zA>g%69D}4OKzleImMYw(bp4oeVcVLdZW?a3)~=DnD^-{z(JE}tUbdF{IW%ZRoI8jVh> z=v$?6jF=Wu_4^e!ciE+S(g#uYBDl*xk^TI1Rknv;u0T~vm@yFmA7-^lyj?nFl@}ku z{q)U))047uNKV#{c z4+zxPe<-?2mX>X0)L`Zd6~F;P5Q_+hS%L)8Z}fA&p;!gl`Ynphrce@Uhj-eEM<7WE zu*xw!jlB~YKz8`|!c^3jcNGuU%Ea|$|3Hb)SZdv24e<%6g(5C+Y|{DrIB>tsJRiwz zCqKR%dQUVRYt4!Uwqd%xanRLDc_UTa?;uw*BLmy z);6eHDpMGDgV9q5YP|SF*uN)tZ7$@S^`B1=N<_-SX6!$hl*}+1Ry=hX@+2qQkPbE8Q$wPIbverY_^1IP8_z*~%?1OD5XWV}BjD&%sI>(%fB{YIOHX09LG&Y#(R!_eWg5jRX1w@wx6m>KAZ@y3A$UQ-+B zw4-+W9YsYuqez%1xxZWX6>vKBQ0P=BrZ8HGCtM)=)>sN+Q0x|FrQEJ9)RvuQ?w@|D zISkV{bJ9vESGxmETp};|`bnY?^0|BwTk?0+)J$x@I}8PW3VdWNFGT{f!Y(S4ySQuO zWX^-(FRNqUQ^@VFy(EhLQ6dx{>Ph+qFrohxp2vvqJ(y%g@6-Nx!8-~;GenDVnm(HfM#oJpAfb!p6_bmIP@`pnr9^ZHxU14R=M+&{pXFwiT|!VRv#b< zLyO*KkD*u6urM|!p-$ zk;|3niO`rFjcqiIMKb$QY%A|mjpgwC^z?CYH9T5n82H?Gzt7`pf1#6u z>4Uy=Z%uhVpoq{Aq7?Hx@0;wq&{xZwrUDY#56)S%v+(fnjUVJCs*58}>JLN-A5-uR zFA0mJ_GHbG;PF+`i4n?(0ff~aUT>RApAeN z5P_*yC3zlrx5|*pg(WgKD@v)@*Hh*QgH zdW2@Iy0|)g@%^FL`AHXP%{57QYPx^f=x4jdsD`t&K<#EQoHpS%8Iz~G+B}8q7_LkBq(cYgtCg}&V%{f-LUtBcJ>y}U`sw!5` zV{W&oeb+X1w+)YcT=ZAd-<&3iCOOix4jHe&14tl{1#Qk0L`4T$zH#&^(4dmq3^T9- z_wk!%@6;c!tTY`PF`fb~72fMtlbNtUei0rnAFk5eDp{&w6&)_nB?G#OWgtThNdbSJ1u< zo3MaY;8H-~l7DK3kWGMqM}UAsKoe6$CtuNM=kHJ3lrjI`m@&fX9)E!ML#1*~>5CGh zRAdOdL#5Ct!Z}}0p6j(tuKxSt48r2 z&dvR3TJxF1EqTTt@oC3~vzJLETey9hBkwfg!=a7TZs`FMgH-oZTn# zfS-Dyx#M<2etMKFHH;Lg2?BmKTVbp5UDW<&etK!pExYWwwq}OHX%laMm@KTI~K3#gqwKgfF=;1ontc_vX z*V8?9gMrD`T}Qn#U*3%_p2!dlQWHcB3(^Q8$iY zzR+EB28+;rgq?~KUcD0@D?|6Z%lSJkTW|;P$RS(%#sd#SttfI+1RR*mQjZ1S}zX(DzvIn8>@&SoxRN1_% z)ZR+jSoGj-ncamFQ0f%als0e+)pPFW&WAm2_)=!rS(tqtCaZhC0h2I=`7{>_C+b5? z-F#NgJibTx@zk^whV`~q_ogE)Ml{s7dwl6V?fd6h2!m+EO}J9*9R*yQTu zR~z9@btmtq6%hru91G2CB+~DL2m1?Ud1$H0`4&}>>x$3}n~@dNGRiyJ${xS7=JgBw zDu&&|_(KnID(Jpr38b7v56B6u%S6+2^9=rs?hG(w=9y)`j0(a?#_TSerirP0)6+LZ zEy_d-Qf7Bipa~XQlhzfRPP3Es+Rc?bDmtb)AbR7D{u)4H>2>6zWB3$tc`zuio-}eW z04;P_gnZb4*g1Ts?LmFgJ31+~*+}28dq1+)Q;X>vL6CHZ!d zZw^K<@h4>iZaD*0dww&xExsNFTpoq{Zhc1%={U#$-ZoT?VkY`}OU2p5wU9v~aiMS9qt z3_ca??HyTh@=_Cav%WlyPBC6p&?(+IQtj{IEmS$S{Uz2;6okSPLv`{eR@c)lu==W0 z9;YXo&&M6Yb%TS5rnI1_D5u%i)dvx|?qD|6iM!TAe3c7p%Bsj_6#MYl$2Do=vLGZ) z;UD69_+&82q3-c>k5vGQuE4ui5K-XhRX?XD{1G~5?#A7WJ1IV03GA7jkq2>D@n4yiw24*g zRpOe9s8xaTn>jzH66vh^-Ho#@j>~OWFi}j5?8@(!ZIP9&ehsi1sRv4uwU`2`FSWzK zRhJH-kSREide3&SVJ8iOkk3jmdm$kQWAf2`zlrzFy)$03fj*7 zI8Jz%O10^UB^F=jIr5Ce-vGC+a#0RW*o7a4UZrmp>9l$6a2FcVI`V`C;djXJHJI&i z5fQm3vO=NSlp|}6;*=tpG@tt)a+hWoj3xexb8OoKVm-w+ISx;i z_H_#AyzVQA2md(W>==`m0omIo_H(UlDWrf)v6vMsXELpFoX7Oij<0Rj5V@dt_2xVh z1or?}Ji(S@phMAHIK%-(J33(3LX_l{HUq~K@<^vHHJ_VzY`0>n-X*F50O=N+%nqV8 z7S&(sleH2@^~VtkF{$bXpF@9Uz~r0~;8Lq}5y5x>r~fjiJu<39(dqf@=r3)m=+5e` z)4V+MYO>!8rvmDtQD(A=Zh`4Pkei>F&Xr`i1qx2Enq$1Id-^>}Uhrg7KLy;_k`ce) z*@=28lfy9DP!!=9FrI zgH{XPw5`$OQ6@2Y9+J&tpl$9`^6yHWK4rW=iLrkVCb@;h>VvjJhCVL%xBV(z?Ek4c zQuFN-BX)508o7gEDY_AFo@illOoiS(>c=A(bpX@hBey=kSHnrTda?DP}-Q7z*4C^J~o{mA0M8o4)0H_CU;5rfm5$#mW!z#685nw3rA@TnOKXC}sUjhdU=!}%%}PbN2TCDDogbil!vSB92H#F^G>!nWJni@)l{KD^!V@q z8?`=-xyQ>>a%u=P_Utw(zS-w0GUbY!_T-k&3GOBDN9c^O%W%*K3!hBky2(zV77}{| zBcIeU|D`h`F^iXka^+XDbdI%htm56C_xn0b^%zFEH9J>XPs&28-UE6^sum_9)gNX93kPXYDk?TH^^vBDTlK6zRGM}p-mKg| zq(oDRc^*Fn#!GbzACr6v73SA~eoeWM(!_uu*G7$Pzw5oBP55d{RIDk>k=ig0--8l! zW{;mE(;Jn-0pau%KlX05->C~gS+#BK0ml~1BWrPj)HG-=la0=KUfs?cIfzAojfO-9 zLc#f0Zsx%Hj?J)~D|R-tLV{b)ak3X8Z(uoJNi`vxX3uBkzbg_riS+Ud(tRfzlgEZn zTummE9~Ff;WRdz-xpY>eiGU0 zs*Ad`O(txho`%PyChAYzw&=66vKo~l<&V*3*Er}LIVUHz%e+8>q#5~u1H z9Oi)Qqwu$W0hM>ch~s?5%IOEtO=G6VO5Mc>0W%xe#%I+s<1{Ops(VY%OB#f9xWD*Q zN$jUpbn{Jr*h+_Vi<29wCnu+Rr>5Gjmi1n+OZ8Thgej!to1HHFoP4)0 z$$I<`6+~<`(^=|oHFNqN-I>0)FPLRMI5q*&qd-{~$qIdF`EQ)Ck?u^*P4GpvrX%^E z+C07%C{@>D_IQ2bBxPf(d5$-$9bLvS$d!e?LUC$1wdD1IP(nH{9phIVwDGb4?M!dZ zgU{C3=Mzx@bE{Ql(KFer=9c;6W><^>A4R2biW3PNr+foL?E90KQjLE8$%wcx_hS_WV4jB!_W_(A)cRr96v$ki>)L(YNT`5q_pSyYeCyv%#_u6vL%(V zfC}%q+n(Vqb~e24<8WS-VD{5{yEkDH^5dXb-gCgeFJ81m0%4}zH7WCZcT{btrQ*O| zBEKm(w^{wMJ)kaK&oT;$qB6CKO(H7(oM8jH^>0hmX=xb|Q|EL5SELBa?TU0XFzt%fTp#SkQVTlo{#0+Fi3-FWWK5;+J=pkN>pOG|b zI!;YQbl_Qd{pjYi+w^&4p|akGWCGG++)?cdiwC)F^K2fe(t~7L!>Y5Te_I)EvRy4n zU79uMkAWF``0;D40JqtGX^rIK5hD<%rls5CN_5Ju<^xMn@3GBSP#DROO^Z9d^km{a zC{Rm2ey{o1X4JE|9)nK9T4{|3fe;3}H*SIA%9kSkuItdJE*k9?$T~Lmm=Tn_DWazS@POlMDc@|H8oNhLVVaB+KbAQ zI~t8-hpFHd*99hajmff+b=2~OQ_qXu;rdotMOw|!O#hJmn#*1)lJ68<2GR$S&j}ur z@(8+#{_hzGQf9%1d6s7%>G;lZcH^yQRo^e8oChO;jcJGLJo}(E$CV3}-E~sLs~gq2 z#hX!>rlNJuli4q_Q^dZ^LBH&F;38+)c-((-fn?!NF=kQp*U|*W`AXeJb_HrJ08r=F z-F$X=hOE7L)?+9VpIj;Sdt*W2zl6Tcj#I-cS8NfE%4v^N2a#azH zDD!-xo&D#U{+;^3%zfIZp~Ig}6PnpkVY$DF%OTW@Yb+-JoW|b3%OXk+k}uU#;8 zLEBK_tX5)VKqtcKDtAbacBw|g@LTK(|09pL;dQv=pXmQHfB{!B^0qxsj%lHQUNT3m zDcUWl$*QN;o9kaQ`QJhK#y*yALAeyvMl;<3Dtsr=mQ}d+ zfPhww#YEv1(V`-2PA95#&;i>nX4M=lyLfdT`jcd1Aik6+&h8O~>0x0WT7{7lOsoH0 z^+6tSYK!kIL!$1ZVHUc(AT%sLrr3aTG8MrxfrslJ82;%vm&$=Ic_}pe;#sX1kagpf ztF`}o@rrh~k=7u{=gr_w1Xjvu43d#IgmWRaM;0up!_HK$(QxUTN%(i>Qt}Wqr0d@p z1SdC>)G9p!PLs3osvz`Q7cpVzW7DjFLa_=uqUTd_C8#QLn@w)SZcA_#59^R;+@_J0 z;~4CgK7iQ2c5^zZh5(iB{1Z6>TMoAX%v}@H z%oVib|9Q0G;i97G7OogjxTq+&x1wd{l;aq-;j0WTK?XWRHAY2VjA~&(sJ3bhTI+b{ z*%CVm2w=o2M{F%ZWjTZIhx=Fy6tNd!1hOrzET)3pQuhj)k*z_@hac7EVL8f$_r0v_ zR};;9r}<%TyJ;J3Qrc%TOQ}1*3Xa6%LyRHZyi@fCdw=QD!JNU)I0C`bEXv7h z1rPQAvFbm=#;Qd_r8ohwSdG)G4^wTIAmT6-4ly(c#m-|lAiTR>RT`85CIU!FEhvU( zM#u7q1fqt|PS+a0rmFB>>G&66;OTuyIwUcFwE}tVJ!tk9qn~AB^%dHYQ1ZfMfHguh zDs|(V{>(H$Sa{0vJLg`!5eynCai@HDX;>p= z#@UT_HjRqMk>*`8+yI$!0FsCMhX?tBb?fm^JS_te#dkN@+0 zeMBbZ?>c&2S>ENuhiC`~vC8_eLvlvW7&dQ-mQxPKh)uSm%92Xi0;Fc}?^5pJ$3HZbjDBeRcYk0aLqH1ilH2CMI)FlWX@k6Q1xiMW04H z0rT|7+7p?K(=6R2!#Zbo4X#m|+uboF^40A6{mTS}A&rDMH?nhEX41_gvwYHSYD zuq;d&?@c9XH$gSZMu*N!*o*kRyg!!ak`Wgjt-BuHlX)+B@2vGDv^g)3$RwL!w?%&RG zAx^gcId~Ax8Aq@JCf@(qaAGc&$pQ&tKP5j*4Y?bt5z@ z;Cs-cxzcH^HIni4&2kg+a~{*saL4e&aYT=RW+2k3qnH2-*~xn32aQ22T5iqCUM%hE zOs4&{p%a!Zp8J24Hr%gx$T_uOsFxPLSXRfp!-_o@16Kja@^V-iJ>Cky%;s*7f+ukfP~>AnZW*46p!VHT%% zCC%bms<^>!hGQ<$pGK2%Un(vA>wgmK&*0_sJrYG$Q>wDuS!=csSwlIQQkmp}XtMi- zwv!yEePSk-4P+utx{$7}JV7EO8F#=y>G4Ddqg<;mmajapZ&ilB)h1jyOCXZ~Yn&g! zgO)E^P29su-nc|R^ZKW{iMvK#rt7oW8e=$?$^jq1p{fonNPDSl3(;X$VXs+9HPL2g z2m+qwDYNAHRsn|Tlr8yBW8&ZS|K)W5Ib29$nG4krU-KZ+;xN>;turVAb6!In4T}@^ z+lGI|K9z`gxIRf0(3qfMmJWnPRQ=$sUF~(hzQ7Co(Sw$XW|iffXh@xE1l+8O>^M{3 zT3a{v<<*LI>_O>?zwkqllCxn?;cFCrSXD6e`L-=VZdDeK4!RG#QE6Vf@68mp<2zzW z`E=X^^5*d^R!RDW5*cFg|B3nkKAfNi%P5DsX={I!rKy=Hh|+M+jt!wn7Vum4dc@c? zCF#3@vc*X~VwPY_N;X}Tv(0FsIo4=!>?$14#zHwr9CVyCFYrYd9hnVE<&u5xITTgm zD|mabVLDpzy?foPr;|^`Q2-GHtEtxAO59?+)Y6>x8#2VEtxjZ3;h{F@$cunF))eJ3 zhDsc2O54&oC-M$Ywu9>leOVlqqW?M((Saf%|5dWI)R;Maq(L2Dq{V)T$)^xc_xC?+v_qki~{x7}`^r^Pi!ep9j!;lwndy8{v1nc_E3>1Tta_#Cw zf4@ISMn}%QAYrH}pfM)Q-8~29gx0qQVu#ayy({%|s*&Z9H@e*COg-#i=U28JT$-@Y zs%qmZaCwk$v~3enEjSWwQMg+}p*U&uCWIj_II~?!OB`z5Xe_p`YQ})kSsoXv-2oWh>FAhCH55E%u2fpTy%>LeUTUh?b+OWoY~5fh2k4~nr5g&U zL@~bRO)zuoOfoF6YA8%vf9{Hpw69s@lw5U!SE;7w8W4Kp2wP+FtZI_SC3<>Q*lf~| zP2!>)z2!@B)x67ko5vx-igS~taHA34{2W61p93i#4;4-)Ne?IIuVdC=5wRkL(m3oh zUWu8%?lUg&^E(iQHJax~Gzyy5pjzmlY ztv2DU4_TAJU(j$kZ>I%^;=-AorR9R*n(N410HFD-wfcBUTUbc2uH?H!ehA85p{4C< zTh$8mfp=GNi*jMMp!<;=tU=VJ9&b|NPomiNU-q)mH1wql z=Qzw7f_LlG(^S9HLxbk{td9B>c)##`?Qo-GOOgEv)Glw*7Kk3#z4ze-S!=>m5Hr}^ zbt;}PSV05n^5jm{nHZZdUF&H%@+IT-tTE#M8^}bz8gN9(gqvC?;K!}&OyPyP<$D{e zWO)+n56m{20Gwo8pp1(W>%9^w(=OADT9Ir(mue%XHL`x?P3FDYOJQ)jq};Jao(@z3 z@M6uz4dQlk>&N!Zq7T(Zy-c(bCo2y;`m&?J;cAan1(XM8M??j>e2qZ=BH!~v@f1auS*J)dEeGe6oSd>?{_b^-H zo5>mx&j95f7?RHBm;9M-_ln(4=9qXfmL55DWteppjzeOY9T=^z`Br^5^^@7xott~k zVal{AI(0M1r1F^VTjwvyB+}z-j(+Lt!$gB+T(+0Xr)CZ@W)*(-u#mUyX!LuLiL@Kt z7^f1D%F|EFSkDgDGs!B9q|~i1-1@75hUIh>^U}a5O45ZjEDH9oo&PO*6QvZmCBoU=WHDH2ARaz9PiW@U&e9=aKTb0Oc{7sQfnF+-N-A%(P{0MDDqC zdj(w)X~U|s<_+HB%Y|}Cvssw|;qrt;zf7jJToi79BxYkc*xXKJ$D9pxVy}%Jojz$G zcg8ZEYG3#$TU$!;)v(PhgO zDn0~Y(@@s2)rYkgpLNgEK`0-hx94dydhO^?_MaG%L%Cr&$5|@=oRi3fk)u2y3!Vl? z?&qs)@G4VcQag174cZ!moYC_dX!tLF~~{B%SIro-%sIcu;?BC+^SPe zhcDOiCFx1w1dgml`IpW(H^^uOM4bL*rN@gcQgM{vIQx|*ntJqXk{pp_Kfj>d`n<8` z1ys+oCdFt?7F>APefW#gje$bJ&uV?a?|@5BlQ6_XjD+YWI67S}_MWSz>YA<{Nbn%R-91QfcL^5UHMqOG z1b26L26va>65QP(FgU^OJKS$Q_5FaUf;ne;_wK&dTIicQd`Nj#ru!ZwDl2Bj&PD7t zrK=-$Go(87N>n}Mtb)PIPh*VjH^D#3*{uD!8uM(e;8}f_??eU(cDeEo?WtEby zzQN>cCElE~_M9zGRTJ-sSl|$c@R{M+aAhLfAouZ6zt{v<9S^6Dj`7opGg8E(XBujM zg{WLfv{Q(t+uuo<9eM4#5d8L8^7hn9JR!DcYn5jD(qD^*%RKb90ZW{yonw7P;VE7J zinep!9&*O66R70a|8IN?bV~Xk9_XW2N2L-BOU$avq#s!^+fv~`*HBI>*0;gXo;Jx* z2G#0{DV+iIBGSLIJr6!!Xyb>?ka^Dv5|B|1! z8M`jX7ZHV%5@@0&{jc!q>E+V+i?b^~Z z+*;{0L1pdMxYA;DDTl|TdS-Uo+IeI2TQi=v5pm)*E+O^wZ;!#gl#e4Q84)jy5xZf5 zA@)qe*JUaDXnTZaWF$+UDd#U)>#pb9KZ$&_+X#2-0qfSuk$9fDuDFxYsAI?e>p$G_ zADB~KO7i0Ve}6=bBOQzhawMjY)oygY&`Nn0j|k*aA=MH!`W}x3H2c0J#|#Vd&AEY# zu=RE(roS%+VU_B~oxWtYlDe-ieX&u>r4SK8-|4EDgT2%!eP>bQUeTdHzvF>S%j0qRkh@5CjTd_tGSR~@;A5z!&7^xQ z$+`K{q_wk?Ue{`nv$SneT{e@`{`cU3^R~wW(~qIa*~{IbKI0S}Om+^AzM|j50uvZ+_SY6en$IeR|=4=3!C0VdS>`GfOLN>8bFU1`On^sM=Q8k6u%6_kq#zE){zV zhkPOOk_{p9B@A>8$VB+3^o>!n)(j{PqH93NOra zZq2tH`41iU?04}tIWsK<)Ag>?7l$taUE>>*w)=h(vo6^^-tIcY@HLgs4ps0w-nI=W zM@?I9Ds2fSD2N^2HIAdHEvhK8cxZ(U(AM|ovS_9?fURGA6orDt z;&ks18$9wU;H;&td0wN2qtwWGG%GR7Y!oG};1P;LPAr;tBD~_F) z<7^g@BW~kR?Xgl+V^r?O;qN0}8It0xQY#Ify)D~Km~CUiufq)UVvQPALQ%(43>1=Y z!d*bscixSzcFAa2;!6%WGS=aZ?&a1QLf5%5)^yWv%CPP;G~X3HLrM?W2XIu(H%eJY z^!+czQ-VfHbU3slw-8`^|AqM8his=!dKG(donq--KvBIv_Z?Y#s_h0@+d`buTGr0Q|+|?9ByaCp|gGctg z?+a?U;>G_$TpS5@^*MB%NdHs$F`?``|5e=8a$pBx667BOZ2lNVpUa3o;1k@fPc=kq zz{{CvIk1Vi+on9siuQ@}vnR#W`=3CUbJqtMlooc>{j%B(XHR&mq2%zepEA}T9(FRo z>uzmkrXKT9c3FiMBg8j#OuTiscGafoW$vjq8&f`~J#5jO=#b|$Edy^7{Xv%|T6%9) z!qvRBQh5%|ItPaS35bN3I|-fm_9c@LKOpP)Jn9p!13W-%Wqgi1b0pEd#IbF0$WP?+W`o8C3swnB`@Tps$!+kMO%3h7-r@Q{Sbk>Fv`X!mvr?^-UF z0H=)1^5$|uQIbzLi-1mNpT$nzPPX(^Ccg`Kp4@9Yn9ZE%OkM!qMfEAk@+A+@BlP6_ zkhqSraUx3-wXZ}L`CAj2>&HTQ#``Uen_6B%;AK{T&ud@J$N0XN@mcucsWsJqWvt&` z;Ji-(#ca+ilqKO1SnB!oq|i#Sxsu97WJN593SwVuf{NGTe6ALn| z)yX+5JlP5M=EzUDZ;t3HJn;UqCiHO!v`yEA4xY4CS7(EZi0dWV+Iq3RZ+E^#U8k#E zKN5%fyqM@e)(yFexit@w$z%>v-sc6`i~|DdsVc zmZf6CRc9rj!xOtheVdFV*E_3tk>Q>#qRz_n*~b&6pxK4?mNAi0K9l$1a`SmF zeLv3=eO`67Ep=#Fi^1yCOz^6)#}a$8_P9Wki6YYvLgp^M>LG+9E0KuB4XTOXl~0l$ zmlR-)m`Q;iRE0%nF~0BKvLwl7X!#dA`*?ZnuR%0cD{v^qRD4I0+l;R}jJIAqF4cnb zgXMox<|=KR%e}3~u-I1lFT><`6LH&CyGVYY1KOG(iPP^lLBrkvl<0W>F1Z&e`^N)u z&dQzy!}nL{aJ(Fj#2wJYQ|3SAeqK9zMg#J)# z2BT#O6YQpJ3eTm6JcI~#q*{Z&-04)Pl`T_J-$>>QKsfUS1Vt@O z-DFq6KgwUoqxV|dH)`#e{1}VRUpCGWZN0%-eXWq65FMi5Qdb{6p+4Ps0(fp3 zQ6w&6*aiZz+~$qWCWRr7+7`A~p6|w?_lCP9vR|gpkgL~FUIi2Q!(|@vjSC*g?AZ>9&Qv~RhPL`FMy z@{m!zF^zwz&m-x7-OVd%0gECAg3o0BgiTSYste!|M+Vlp21GgS+FhiT$JK}M(~_4B z`q(QwRVndlJrNyK>}1wW^$dMiDH;*A8(po%hOJU?+M_nC^USHRY8927&k0&iCSJ_v z#-G(n`^$5tczKP>P4NR!lBfSn7b11>!k-lWWTn$Xh1YjPS&yY8Mw*q5t64QX^VaKR z?P>l@l}dIZ)cdztqI4={ruI{Hh@jf{Bws$4r0Lzhr0+WC?YU7;7JXfQFYs;3laHo< zyV1G1*8Sz*(QH2GGzi_{vrV;9Vk6c!B;G=DAR{E(FgLOCV?R>THBfrL-vcH2z7hPk z!}yxtz_N+p&h2ud-ZE}&LpBhR^G`B|y!o*8rW*?*svR=6&Cz<_PmozsC`=C~6kpdQ zln;3VQGn~PA;myx!#6s_*@Q92|6PZR7SXT!;d1+>1lCsIT;QDSwkTus`QS<&CS0uS z3xHxC5S_<7q{hIxwvOrSX|6v^xF**!`SxOYncLRuRr+6}h2xsa{b7QOH7{y`!1 z(GqIFuUJs)Ia7m-oJ%u-I}&>pY-LMBC_J7*heK$BmS$1AlXydU+`90^)}n2QRf~Vd zEfc%k!#eo&wnNZfr|4wDHRWKL`D;6AV~R<2V(5^yE9X0 zr!fZCp7sS&0(af9%LC8GB~okfnywy{#B+-Ga>~eka*`%2HF>6GXY>X0C8@m;hI_#C z=3UpF4o~sq>&awt?e4>dB%ev$??X?M+6R}vzTScTgP-&J ztJ^;s51BxA$V@H6?3xgD*n%m$WV$3Q%>jaj^(PJfRSIHQ)kb8U43-6w6Ez(h+7`Y! z7LV!J)D31lry?6oW{~|uof58ds-7__s)oN^qtMBKKgBR-m{6uC+ewj43)F&_D76)DaQWu3 zFumdtvDup9n|Uomx$k|``cdCGrz9w{2O&z}{+EEdWHvA&5m1HyKtc_+A@iMA^H#YB zj?3F5c(kNzp!Nd+ZqlC(Lhjsk@Rxu~!cQ88zP>hI{$aM`b!5P-MN0OD8}h2DxzoDn z4XZI5+=#U1U`i^ay&~KEtCmsTYq(L<^+(!=t!X|rV(7CVMz3wDtIylU9h+}2o84dC zq4#`QwzoUf{KNjd8@5zGuR$1WFMLr^UVm>n6Cvlbswj+L+YzB6!6!KP(P|iFSFZ{3 z^)@nVZ1+bwujXlHr?;;=mIvqizHjGmo$Ncvy;4q_LC`sID!xQ>C|+wr-rAf}-;OlI zh9UOSb{(jUN_;)tvcTh}DecN&-ic>CX9lkYU?Ow~E#5lpc&h{99V_yAnx>ygi@jqr zzp5>EN8x!iPjM*dBah{Gi5s;|;sN+i?GzL|)NH4x?wWK1*OZoAk1WUsA{ zYE>aQNm@(geww1Y(JHxL;KIIm*;hd}Pp5qltW)z}ohkR)QdiS`IaaG)35qow3`A`@ zj=-&h_)WTFy`DNz^mdn2?X^D|X*wvWPqJvs*xX1`;Z|1r?*y7sPW#YV6g}p$&G4mP z?m18bzY(7)N26ZJNRqsU`7R{i`!VW*OS4tw0}18zQR-3qbi+lG@9pn`Dm^90#Ezkkz8MQg5C5&pNFhE6wjL{aJOr1;vY4U=5O*PP^5qR z_&JEv4$%Ij=fvs>kxuWm5ws6pBPfg@Ui84o3;0|`%e{A98~*L;b$4JBSsrxBRox46 z3Uda|^A{h11V6X$++V676WU)~ICyDOk=>iapwNaKlkz&Ehp+BBHs>Op*^wshgmvD< ztsjM*<-+P&%0+XC28xKQf-2;ZN5dbQ{Wh_fGowCb^nzs&e|2G|Sm9w_9JIq}YMr%^ zhkC+X_I8U(RV0i}*HjBR5~nEkD8sp`g`S(=Fz0Y)TO(7kXQlO#;iik`yzK2moT zvQ|KX{@R2ua+8jw5{o=ReEOLc-ZpXOAfJ0teU-9<(|Kxbi-!a`e67!1vG^mm2I1sR zGlR+pgZQT1n}Oz48w&PCNGmz#yqN0=0`9AqtjkmTR_D_B=8kR+7U`J%o%C93LDNL^ z9ITT_A4Ombao<9z{b5X%Z5@Y@ zB$6}?a!$pQCOI3KCfn5<9Lzu0_=;>Ce&!1WNhrT`6WE$eBEHuYQzegOI<>|EKHMUD z1PkK3r>?Vyr<|B)RQ>Ks52GBmM>F#&~WVorUOJI!MA=Lk7hK{?ctf#5uzfee6aHKYVP4A2<;RS z6<<9J<7+lC*SsF|N}JZvRpY3IvSG$Vd0A<_|5hV3O`G zi4_dNR?ur*o7N|V-h%OIYnKmu9+$KHjOYr4=wNQJ&>u!oOjh`Bpl6%Kz{w$@SMMc+ z{(_bRN2e*v-fp4tDtF&6Ya{@ayF?a$yZ#}&5!BbioLleB-h!1-48qX$P=yy)s2}&G z<8sJLoERZVhHa`4P-fPqBgm_o6-!y!mUz(6w_ne1G?eEEwjS~+=1Or_7GdP6;`&pZ zVhTyizrUNIF+TtKV8@@|hc4ze_0@2cIijCs#rD!kNhOdY^Lumu{*f44b1TiV)Rbdy zFlfw!A|YKV9$Rxz3qpL!xCu=<+etX<3_9u6@J!XsA7Wd}`3bG}9`MBD@fvIP8taPq zMMrh7y`Aeb<_slSS`0Jpw?Fp${=G=_wpuWLzN}3Z941bnphTfQjit0gg`tNxd{WW) zt^P}uW)A$^1>+sFu&$utTp7l-_nZ6|%pQ($au}r~PC2TSDnwmVl`}U~{bBn!YmW4j z%*iK9fA1<%%)0=`q{TIdTs&L`lF&|$PkP_f=XPR!W6%yqB`k^w)^6LfQO?51#zr0z z4b@QutldvQjlJ=#+|6`iri*s)^;2RmCB@kE!J0Dabhx#&i&?WyQKE9ck{E>(BL+xS z2V7z2wGD0&Jko6G4p^~vzVh$yLH-+DjJ6KW&y!}seq>-#PVh!SP*6=o=Q|I@%HUZnqS0I~3aY|k6!-Q!%TRw74x<{*{5k4&q1z(3b zgHD9-xGEz0g%wt#N-OHgbImT7ki&whtCX6;P+T>)|GJRr`ep8^ zG`}orkRWO9HJ-hg8Z@1~7+S$JTATy|2X(i$7MdXI&I7b7L=AE!Ek5rkxaRX~l=j{E zo>0<`^C}y#Ua|}O{hD^A=oB0~th0By|Z@$MYK?A>pB5@jmP0(0drW zVsrfXUEZ=|C#QdUWvfLP1{A;Gt6X&jeJ> z$nn`}Zdvq@w!?v$*83h~^gHsEBv>dY*x}j30vm`ZDh@6Wq+;HymdC&Kg%mLOEru!d z_NaYgDJ5{olSKVq0!|m)Eo*~Mic1x+M{W#0qbD%2`!Mq>Yokm0Y{n3wi>+}KEnCZ^ zpqlyBfjo&53PSk*<$X<(?ikg!Z0}kanAz~J0Y2r zF_@&Pu(RV}{I>X_&4ITa5!p=vCO1PKu3pdM3{^|BR$J!aJPT5u2k_0?Fd?E@&<^HN z6LnXucwZ7;W^JspT$_lU=u+Zd7-63bY)4Dtz5W4|Qs#KYx6)G)_AvdtuiIO54rHq? zUM$RR=oD)j+{MDwr~ch;j%`cXcw$_wzWhy&A|&FaV5P42!ajr7Oulc<2jTg5Uh zu0rS|KEjMg4hi;Ql@}GwWV~4IHh)Z=pwL>y=c58+_{!$ ztO1XiqM(k;CEM%VO6M-SWX(i0?s=8Q#0sa!f17rD7}M(ld+bS~ExPib;zf>I(eji{ zYJ28yO@z4Sz#)nf%=~#8m-D>9;enktMr|5pH)g{Hn)wuXWxIJ@!dr}N*7M)+ke!_L z9du=kE1B)G=@Y8R&7B@G-bz695Mjr>;mDzO1>y9-ii(IZS5?xU`;9xc#ew5;%+sMX z-&7vE;3dJwf_B_kJ%`)}^ZtcmXqP4z%Zb0;Q4=#vbePBvUdy%ek6$%^VXiK`edPH( zzoRfw{52WX9Zful=cbq&jDzHzC7w9Ui(6$yC#%IkOl%qhsCEl={&8Q0cr+&jnCh8) ze<}L)wom~TqO#={^ZHNj&1s(-eZEpUYilei(~$5x1g=qq)Uscqp=(Pj1PRIF6QB!L zaJ!7q>tNnYBgbqLJ{!;)Z{!Ev5lyj%hE39mCSy8&w#aPKv+5=z3^)Fw-> zm|DK@3g|X=Xmh>JD}X0*9{R1a^|ED;{??UR?TK-}mNnUq0(^TRfl`e{vDMI@VaP_) z5(a8w-4rCsu1x}up^ubVlFWF&mP@mpFV}Md4*)u9_G`OpiG;Rd%P)`8AXlvs-HN$6 z{DD6CJ6mNHU8*&D4#jCQYp3~M{SoZk^RH_#6KOQH7l(JW{VacA{Z?_6lV?H3U2XnMiTb{W zpeXzl~W- zAPQTMNT?Ia>ym!s{1QTXv5{V_rk#><5=U^5Egjh)CD=s=eyH+h!dTM8LUM42c*C0` z**LqEyK1YU|FdKPnc!uMIe%Qej;(sMIZ}R{7T;0??lD*OvtIZ}ia|~U&Oj2vmc_W%lN!+X!k2U zLs@jN7|B~KuYUkOq1#_#-1?zfJ^Q-#$*)dK_IGUNi&UqMf9s%A`jwx>=kgowTL81B zL_cpbmP!1XT)Kt`O=*b>k@)0Im9U9b4dnPe<~%5IuP8dIqQs3yCc0x#-O%66j@r-F zr2|Y1D@^e2(WX3}Kh$(3HegM`)m-^If4t)A40AZU6ukSD*2lEz@nZkBHeIuK*UDwF zKZUpzgbXb1mrg8@6lzxDcTRf}RA_L>G&M&D%s6OwTTy~SCt+^t{-zddXvgTr7UQ*P@P7Ou$;jS~Ld(=KJI3PePr%Hd=CQZb+W)Us*F~ zLRqXh1(U~D>^?P9UsK+#^@OREVP4p4V^(OefP`*5Odo!`#->^eIiR~(E8KVLQ~T0v ze%aB)g*Z}IR*BPQeurZ2$)T<$@lRXQ8JaaN zMB+D_3}N{Hc4XlR6I8?#@9yZTCa5BxyLP^I?R1p+EWrUTfIjBduh)w{r;EB*<{x62 zDZj}9Zp(e|OCLhc?uXp1>)ZF2Q|HKYe`0viYkVVeolF5W>g%j}^bin*|H|+#$lIO?aL-UkrCw*-y#euzC0SQRJ@CW;w>qcH$qks`sP<(;KXwSXA1Y%cd`r0v zHw*r>)6on4K@rtbVm@STRuDKC*Aji0J4tRfuU1+)(ZM3}Gv4+A;jZCjfIFuT9WX(B zmUIJ>9q8cc^>SgLELgvo-v2yua^?R(;@413D(7X+TztJhKPc{p+f5sT3kA4MpNvIU zQx+*IjhdTH<g{{nH zyBO650+DvdcXgm2*<5H6RrBo|Hw@)C0oTGB)9Fi$dh@6CX ztLOHv37?i-$F81kE$wp^SI-IxE@zY<-oVlO@0<2)408Q}xHSk>8cX;ogwr+qF<$0BVnKf<&AQG>~^yCfWdXduQsdk~k zxa=FXGFFyajld$q&zdzu*-Z^g)n*^>kQq9=ejTr z80*=n*q3oCc1LY^i@PvG<|ZS;V|7N_-GgMoI-78jEQcwpqV}U3aa8XHy_G|RJHU_{ zv+eN9gHaUyEhMw5g1)2)CzhT%wd|7o@QMx_W$3fqbH7|*#`=lLw)V$-IGSSfaaz2O zwAlPkysas?#1kle@6&U=vYG;;8T>uGu5G-Dw9Ukp4pV`r<*#9Ag<-W0uKQjelW=Ap zD@^&mK95#EkB&Oy9G<_PooCm>VTk%Pg9AH*x??&u!x$(b$JL1^!MFl(ZT7Pn8x8I` z!0lP#pV}Qs)|}SU=m0#67|$tzSF$TGn3a_P$NAXzXJ_}{*0gld<`)Uxb(0f-RF8puK`}C zV5ePIEL)+w8!&Eu3y3xCDNFzYx#M%cRfh-f+Q4T&6fpF%f(LCD!$#=EoKy0VHxmz? z_tJReqIfx2wO77X))y)})%N$mwVgOe?=lu`F{XNxLbKSSZ=B*(zVHV^K`paz${l)< z{M-glT>M0zYtq_UsIA&`zL}aPYStp(TmFbB$9V4DF;A|>k8>Nzf2g7m0Da#%3?cUU zomfM1?bs#gYlZ#<75}MB8*2FaeFPp1nBnlJJX;$X^mGA$kj{$1IdndhT4N1AtXb5! zXwb+q?biO#hEV^;zz59*!)f*X?jpmYZm=WzPv+*)m}u`nn11i-n03t-yqejQChJY2 z@$RstG3q%T6;7+e?^$2(2 zq}IA-T9Zxl%0?QMTI7Fz72|v_YRo3rIus&XF;8n)y;P#OZK^sANjxZ1r($nl;w((Q zZ!7!iyOyo83N?9&E0Vdq8aBpCDEl%=HAgmXnJ-a3W5xGHEOX)6?1Y$!PL``bK5r~~ zAG?$Fj7)LOO}Mj;+&6MHA|yECr>F*QQQ#V{SUBO3v&n-++{Y~CJcfbrMbZl~(M8@9 z9BRQyAS1^0CZ`S`)mwQ$>tGyUEjBx6u1D#OhOha~-6!p;Ao6}m7*J?H*(8W$oAWwa zKQJG9?@#o&lW7f(}T!fQ9(U;5r#{W{7tZ{t!BSMc!=kLxfT@$>AVjYoUm{n#2 z?z<5cBgA(`)|k&ea>2vR;7B`AK=ig2=U=2dl%yQg(9bSk7ojJ(&nn^4jp;b_FBc#> zR%w($owHDORODuWjEiIB>G~s|cW?021H(@oe)NtcN`BxhP5L1GlS!LZc^R=(Gz7{B z&iM{thek(>$&<+i6IUK2*4&Py^>BTTOMEOUN24Ld^|w0#v?qcR!f*0Z4$Pp-RPtaZ zLe@XkFJ}C%OK3>5MnRL`g7TM`7UKA|6|MnVdi?d<@Jm5k{LyaxT7H=DQl?`~z4xD6?Sic|sfF4OSMGTU5`87_n`AaSH zSA78Ij8(5I$w@X3EnH6WnfK~7$kx`{G=Bxz*;+2%x@3Vmi1BGJSwkj^k)1UHW=sKe zN~ERG;!NZMwVzCCI5IYS&w_NuNrDA`@cf&%T$0%n#|RV~ugQ6Eyt^U)G;|MzdRU&6 z?g-aqHEjH!EtOGNf56x34#nqy;j+SSOpjD_2T zX@Fx3&@A91!Ow|ODz!g!wMyHkPJUR#ZHJE1#J;xKTG<&X7Mc<|=e0vBUGth~QC-v7 zqOzbGGt)s16QFL91&4zt#KwCXA(HhwK~P+NypAvdHsSvjThG`qyH}b$R%0Tk=+6`Q zQ8`&oLAMt66-Fw(i}7{y>Z=YsbyllF!DH7bpcJn8!(SQ_e|QK8;#4g(T56Q4#;>h{ zp{pIhBbYBsCls@|WF_R>Kd$^qa)%lQnw&n&EsB7qoenCpEyY;_-IZ8!o7W^vSLB1d z!-(9aS#s%_V-`a|Zm!1`Y5SC4h4fz@7cUn8gwU2J+ex&gi;4=k9FSp5j9=041VZY_4JxxB67=71$gK!D{- zPC&Y(&PmC&^A51}N$Xz4H-dMZUy0s!eJfsgt9rFI zA5}?)(Th+2Z4fTNe`wWGvD}mrgyQUzfvl~<$_SM*)DoG zt<9x#FoSA*w!!JHDWT;oLb0%>Lgue{jZoVDG~?op2{w=oNE8!ean6ivVTes(cTV-p zYB-K$0oA)ik}y??J?b;qp6g(+!G&vWp=qZnX4dqIKic}!rrtDs*9M`WMK?Ft@s#*P|TW430?S-FK9& zNCK=4Ns%~*d;$SRSE;K8fZ1;TcfN8TN{&M?NXr>9tuS|uXf7oxyb}~_N$`6EE>_}% ziPf0I-Wa20K_=5I(NTTiizmckJziMQlp{Uz$~cNvAE=#rOYvOFJ#kAmO~Y>iVhkE| zt&c0j2GHmoC}Y88P16~XLwbh_|CaueGRAkFG4JJbBM0Wn)^Bp9?-!ICHB$Fw?GF92 z#K|vgzovCk&y6xpb1y?MWrx#5Zph@K4-Qq&0PQ(4A;3U+Tz84jSX(Y!DX%(C2B(); zQG~|M7_MB(aeI7?6GuENzyymm{*ti(+k9&@I>~L&Ye!BuP1|1}P@p3aSf{dVBY=#>J!qrDgqn;rLq~Yw2NU?qfZ+FXj465%ssK~ACLLb+ z&K9v3xm7H@!@H0sQ~Cl*)(Dq9K*i__)X4!ycPn)st5hrY@D%s?CE#)xd@&Cs^1#@* z33GI+_${3z&0TN=>Fm5?(b`~@D6c<+*HN4IQThLf1{x3Q;kh=(X#*g`v7T~YExhKy zue#A~%VR~b{4m;#-jFhz^#M75mEROJ)%;0 zy*-70N?sd5>Y7VlbK*e;(oi=hL z$;IWNv?;{ATR@)AueFzP%>Lzqa#GJHqUq$ijIOKcVCk6Xz$QHIl8xL*w%t)Jie;Q` zg%VYY=KRzU4|c$e6sZIsn2P&T_D)QQ9AgeGl3XV!JG(~gwfnt8`1H&5?fZqoxg*D& z0C3;MT;RfyJD6us4apDggxrbT#6WF?rv|^r^xzR*rw(P#o&`_`15n1-fjWO@!z2ud zof~Ihz`0!;<9!n6s>k|v2wHtY|9B=!nxh6ho1d_hY_PJ4n zJFYw-2%y3w3U{|@b6E|SNf4fMEiPn#X3B6<*Q_JT6kZ6UNw(;zoA#=x8w&}=>0o>u za@M$AzRtXRE7c0B&R9EhS=3(_4Y6nT*RQK(xQf2sK|;^*U(l%m9Bn=x&u@-c07=4n6}y*8X3F#iMfL9Y~rOaidj zC;M@YOb7F%a#^F%@p5XrdE<=Bx^}?c3($y{O+ zoDe&ghkjke78bKw(eLOl7epsUv{I#z|2C0@J17Vnc160bPbH`b&>4?<`CIz?OUTrM z+TUHw3pyuAKk19vAJoF5uozs+|Cto);xI~p>{rPVp^Rj;&tn?(L8Z7ze-FUZdC=Io zuE+BaRtIhy=Sh<1Ha}PQdEiRxzP~irRiTma@BMNj-CIkgt{QU*&b+oo7&6yA1W;on zqf^J{&0(1WC0ENLJvw$o8cG?qI)6OFk{G9b7E$bi+=NuVuLbpZ>jh|qY=6%aO3iKVQT`vMAwFYl0`2m?&E58y^Ese14Es}QVSWg>~0*6Dt?fTJZ2KJ~e zKvmg0{VtZ~ht@%#w&jFv^9ZRqjSOWC@(DD$XSmj-PIdcg57$oG;Yt;FG?W5L?4|(m z0Ff%&Ch7RkCTs%c1`+>9UV+FuhV_wj2Vgl0CjFN{#S)s>XahcbmclJ6+{bje({0UhG;ONp}8zkf9bK zC2kxS`%wCOx6GgRF`HWks=&f@K0s6`&+~%q%fjdmtMYeTjT7-<5)&JdA%I4 zWiWe!$cCq~&796gbJp{9tTCmmkwf=I5TkZEy3cPcFaM=bmyc-eM2~qyni*9eh?DoI z9M!gJ{7T;knMct%KPH|Kx-SsaOth|B52N7{Q0aK?*Hi9{zeJ*o@PjobfFxaYQiVxWQu(LGsabsbMfy{g zIqT7U<(uWJc6#hJhxs3saW0qaJOj(wW8e`sN&BZJrqoJ9JM*z%a~gU&C?5|>D)B2X zi-XkH`$uz%%7wv!M;y(G>tH1mPo=t$%tI^qJFl;dhn(7+_1;$Ia|Afk3w}_L-FeHC zpa4uKXg2DPs1D}mwH5a@Vq%`fj1?P!MyEPWxi7IgDV z%C*_iE40Q|@=F`i&(ikN0hBwHpqu;#wT2!6Iwn_k8($ zaehRLFy`YQzW&bh6!{xfwm)=^wN8q)sd$FSTaxt-kX0@qZReOIN&G$(NpWU_v|YB* zUz@AIFsZ-TTdLO0{+lPG!_dd(K;+~rp6Q%`s*iQdUo0$6lB$+dm2JGh+nSYROovxG z=v0d<+~}!(vSw7^kz68GSOOwVZdxu|;i-tpZ-hzl2}O$<%dq^2Zdr(GS#HNtl`4?o zvZK^#vHLhJ{K)Az_jf$gG;7d%FpS*>N97u!TENsUBh3hQ#kSf)9D-Qd>yq);YB}#M zq_S-rKD#!);BVi4GC~p~qJ1~U!sv!j=ddsB{*$X5UVR)Ve@D8|KOC9PNE(rJ?K%_F z1ZeuH+3iCO^KSbK*+_EDSWl$;@M9i)v=))?^n z)v6AxVrJOtlOY^vs1hquac20XGKh#FM~>Psc4b&U%&TO;xm{6+h8=;TE7m+Y4_d^4 zWY}yG@Awz=P=ivQGhPEM?^sy&&5hr)&y@^et0sdHpYox#_31d12gS+|tMH_QNI!pF zT$0J~r9_=V0@(=OlHftQBq``FcHZK)voY(%30=|wqc+y6l8W~5qdErA1Uxt=mO+he zJuC3gNUd643HLqt#|fXn9imdVs0&?y^hSG01LnzdeN8Cgm+N^Zk5Wn zpe2iqL>W^fXj+YFk*Spyl>C{W=5+>7b{TZ3pCX@&OJ#W% zh%N_pRI+V0y)4%%W2Y=oh=aTzL&m>wlZ-89jtuw&4d13E3Ud~v+CIM&#>>|$e}Jzb zZuoWPt!0f(y>POdh`K;ef5p?O@-l+5SEflX1wNAJLVXqSUegxI7Wp@Kyo9CW&QQ2)tDw6GAKz&s( zWp@b?FRlse;_ooQmm?8b>h5SwvVvW5sKTmpk+(_;@Q; zEtIK$ZpBv%rk6!4jog)XwVv^oWxQYYmgN+bu%Ltd{P>s zTsdmw9MUvjqsm>dSiYmZV~0soH2saV-k}@i$WXD$JW|_RxvY6{J?$F)X80R4L7ov; z$;y{nrTw-=oS4D6{W}`?Jn=l)T_%d)sU+26>#w1_n)AohAC^f`qZ#eEQBcXgM_AMa zFS0RQ02HbAPsHDPPt(bSc1#chz9n;!>ph+~7a;=L;YSk6+b>kIHq`?O&{si{tgV|b zXH;Ubi_r7#RCd|#G7tK2CXIP?fvdYZqHiNTU>Ot6#V!r@_g~L#h?$i#Jx*HWps=u` zgxIjRSa3z3rm`_JYNl&pM`!@DG`oL^>TXGk|b6Z`9*I#Yr91qu6~NYp9zqAg*BBwh8X;-nhG4a0~8EaCdii3GVJL-M9vKcL)$XxCH`%z-w~e zz30{+f4$kes#dL^fSIC7(N^>8Mw??T60|eI`XCNwHQW5A|<-+B)CKHD;eB7rNaKGtkih@q(rHc&izNDYWUfw|0QAw*v+d#fkmT_!6x1yLxKQK15vD}qOV9Z!TmY%C) zUigAyiaFrQaJ2|}*}So~w;*Kwoz1Q7Q?vT5(T4p>hXnT@DI;#oJ zy}eOJeGc<-JufSOA3=YU{WXhJt{AaOqb9=ni=NUssV=u=k_Q!v61=-U zhNeRFW%|-pX?n6SINW}nvE!Oghfo8zkDXNsD6x9N`9ZAb!j1&9>}RN9lwmB)=U3^9 z-ov8&CWfDwVn`inUR{f6WO`kW0+0`zRjd7QE5u~FvsZz^&FYM_Q7p}4v zx`OTg&e-VO`Wqs?h=6bgPYhd}mPwQa`4!iNDbC)Ce6(vXyYo+mBu6V= zh&hD?9#FsKXLRsH1CPNLV%ftjv*~*xHtsiI<>Ng5#P;R^b8Xj`b>DQZXH4T6r_V$4pEf_?-TxaP9{A1uC{M(2*;1 z`U`v}=}*>$Ay--JQ|3AQb`~#uN_+`l-Ls%rUz0>W4`MgDAPs*8ANSM?V+&!KR_T8J zVfIB>$6Q z+tz~4jnXW4wB_$(p_Z6l@**ddt_|F1-|Jy){q<)gx8%F5Hclh!b| zLA#IBM3Z+G^5Hds9xhF+VzNVj!|uzxg+XzCz;Eb(Z=o-Fz`e=X)(Z4T;<`kWdU3(Z*g>E0rA?}cEea(k0f=SdHl zQSQp!skGdETkB*OPcdppG#^Il7orTR@%EWxXS%c5_s9ke0%nqKuzf${CnsvMYLwZ3 z*eUJYkX@3%){yYS2*z&<+LDYt%Dq!3rOSD1>^9$A7yhPN#ge0r0uw5i6?2(O@oCYy zv5Y_@!7}%g|JQ0~+{d-7HhNHCTRe>M?SjW&N_g&3vL$P&1+KAk=N= zQGw{LgAOujQWlSC`de@?wzrzQAm&B7jRxH3>#N_v_G;PsZ^+ zaOUJf?p8ctk+))qeIzJP;IN#&>C@ZvM}qpi_EDir!1QONm9^4;fUsR%RA($Vz(TD9 z4XJCAI|$Fl1D=71Qh;Ak>|THE!Qy!CGD57r?3I~AYdqnSR(~ng+VXBW)9R?jQCJ8S6-Fk zNcq}uur&F5@Em(V{GqOW_FDKd2isk*jYh|Ai9eRFOyl%&uZp^91NW%SC~IIdmG@Y6 zp07U~qFg6fVPHbN-iwQbtY0q1-DH@{pOPV#+olTMJo8?g8FteufD4QQB)|P!7%&}g zM1HTMPDkV4jKBb^EF&R$^6qk7rDU`OXMm1^lTN2u-1<$iK6%b>70aZ^-fan7Vz(9R zouaRx$^#qptSV34WP#?UKY}T~O#%Pqjp5Bp>Z#x8)U`3HNAuu|#;;E@a>Lu&(LiZR zsxgpbEjHP&@fAqXqI-nM(z3aG{5_w7K&-DvroUNlm*hW0`c z2&*d{0bSxrjXKt>Ljps!oU*SI&wU2{@U>1QJ>|6bJEj`qS-XdM}_Z?%q{4qel7w$ulV>;jNa_}j&uTs!&1^A|I2cV z%2>;^VGJ-Ov+rziYCN!advG~&!H{M2h)X10Xf<7*NUCmF_gff$s!*#O<(AyQeF;}S z$G2SPkIMuTo4I29X}re(@=f`?A&N&4SgZ5MXonBCm|1ILV#5WsHW&KK=402g8r6XQ zo^s1f#TnZ<7sJ7f?m8Qe&HaL-1C_c8IWzkQ(Oohe)O-;0=FG3wZ<-0MpZ6bhR5XFp zyvIXp)5Dv&Qb6Xk-1J0$T^dNOyks_2ZQ3T!s;vLA#KT3ymAJ@aN7gIEZ0-U}a8Alh zmT48#rZA1^-71u7CtJ`BYOWa`V$@TwHts1unWYG0Ve`{Jafc8QH>K|!x6H9x(dw^a z`+90l##h?1M+&Cl3BaAIBSO*WM&X(=(KgS7RCcfOB`sU1B=UeTre~`Nq!}0aPR~Dd zg|+<6uR8X&oiu;7Mla&hV>9RfIR@yf9H+of80B0x&6aJ`W}`ms+CBxf5zJ1fRfrdN zAUvSt5~aveuV9fGdb5+({4e|Y+Zf-Z>;=X+l>zN;e9I?k^&a(9YlR=Q;gQ?Nwa^j6 zrn|YY!wtrgfe*eaX=%-#4jomS{@RVk+8EL*nUiZqMc^O*q1}RKd3*z_rkh9c8T~OTsaSVefFRpvAUtD#wP8%$rllwyja2)j&^jb80%O z!3&RW=C3A1ZW3JcU|h*?V5eHi{>_YR=bB}r)>tMPu|JH@ANntK)(H+hp`7#h4FPaD z{hX_DSgOBvn~~C+A8j}BG*?bLdyjjk?6nx=JSUc~Mn^t1sa98q$>I?pVY|17buiEt zF9QynOt7Cd-75clPxMW_7A3E<+}B=$Jf_Lx%9IstHJW~bb_hp-hNkcdVCU<%h>cJk zxq)R5*H*Oxu2l}w(dB3cb+)Bso-Am`NfUqUkGgI;0H)<0Za6oQf?K!IJZ=9njc(B5 zpjqDwjfV*t4CKN6Yd?WYLuQy#b=$QtTMSD}O8&5FdDK|L6Ab*-3f%%r3@7Wsyi+CP zt)>mZxFk=DvncaG@3#pz1-6wE393V2{TW17dt}{YhT^@U5rPzKZm?wKst?*yH&}IT2}s;y)9M=an4r|HB@%ZSpT62W zt;uJg!+kOmTBP7p>Q*%V5p4>vw~sXtj}jir+MjN_3y;vwz(?Jj7GHh0W&LNMK+~ac z#=`h9d~;k@AT#4%;zVL>1>ZM^N$TfodmC>@qOA&dl*Vf(sIS$g9-Z*#Rc~1vE4v4d z+uL-jyDczxOWY2;VmdjI{P})5BnCsB$RaiF#}V-)W)P*CC#WhCi_Yd3g*p_ zYw=}eS!rBs%I!NVy#3F~atN2p`;(eX#*!+YAf}dHo#hKA<%?_b;Jg)AIG>>o_(5Np zJ(c2IYQLYrJi~1NL!8lz~%2+<96?pNGfw6{y2VHqUsr{ET!{7Ua0nHFu^Q z^9B?$)4x)mK;RfXNh$ZMOxq8`MpAUel}L!aol+*--7DB8Xt1O?(2=>mXPz}%C%>iH@T=bO)&;dZM@q=oq{J;r%Hfdyg0JzWQ(|tkJ$On?(jWH6(A<;?E~2<03}VO@=00UBF~w zpB|j!InS#liBav2>#)l~8&^ZMo5`IWh03l1v?&W5F-!*;BG0BE6?RaYrHp#C`my7>Y)~&}l-pd?ujtbZtAU!JsD``@y-Skvp z8;nxY?EI$o!Y$TW(L;!)+1V$z?EtHbc2{2llh`mb21>NP{twz$d@wkPu&mN}eXP=1 zf?<#F$%qa^zN*xv(Vmkhgl0%8$hweSss4!BOe;jFEkx}8%dxpnE`U!7gfeA_*y*^F zFqbUO-;$`kYfP7Q#y(%*3oA|p&uNCEI<6%1v+6>hmUmv>G7B4N864m^jYrnCx#6)1 zENs#v3pc$ftcn4DKV0cEdsh|&ftK-VYekY<1_6v$p7rTybb?bSBqseLheu|-yVhEp ziHpvODLH4a>k&CH&3}$^M`b^n3AFrQ67?uyq@7K=E+<7(o(_iCkzeIJfpLbcU3II` zZk7S{xJu7#kvf^CWoXOa%K?*JBb9FV;Ho*cr6weLZ&bP5o_vBWrTx`%8)@lQ_utM3 zooaMMNY#6W{`_x(&WsI|b=}fp{95D;av&V21(i91nxLBcaY_OVk}m$5%$m=>_0Uo0 zzU3TP>W-`zr%GdY@}T_LgCklXs#+T#-xxOzGBuwYu7tg?ziKTJdsoxIFh@rhAUwj2 zO@7L&Hmd1=x3w=nI|ed!UZ zRVzX_rItpVCJOi`R`9Ba~8M5FHwfKyz}Qy{VsLJ zfdOpLhHKGb^pV!LU#wz4`1MJ=GT`v1(N_(P*0ev$8a7dmY$Tb)647@OZDqXk7{Va} z`RfwJgle@hURq{X9XNmNt=UJM?R_BN&HdH4I?f=hO7ZrX=ohA^f~4AW*p1uxn55eEl=$v{>}{>O|x-pC^+){ zcL56dg%%c@<-#+1&4$SLqHM4MPW`~iTyLMyqzM<<;WN6}WomHIw-$uIWLzmR~H5e3Q z^S8v=ulgzb`(&eu+lCrjCdbOE3M!-x(pzj$zIvc6*!Vhe(carq92$>x!~XD*(_kOQ z*2KE*PoOZr1G5JW4q?FRhNk|KHO2HM0NiFQ)zQpe#J{Khrn z-)D#ncDo-`CCrn|$Of6o5(0dSjuioywmbvHWWT{OF6nAm>BPyaggF$&qM1;Y0I(7K zZ>5A$xANhXro&AC zjm6@Kp?=@S*UpJ$%C3^7JBVX))zY$LsMx^VwWri^cvtlJRE&ouSaf7s2^$%KN(2<< zgCq)g4Rf09M$eVD=O9KFb(Pm@phH(%U4{%!<%7YAz=^`A-BL-rl|CFLvWchGGo|T@ z;Y5q6j(ppU|2>MqShUTNMJwCm&nI&aogqO_A!`&rG1ZPn(G61x@Gl$q_AD}t0#aXh7o>nm1GCyHPLHga~^l0ONNb6T}C2Xei*G!$^u%0@;GvU7b zrrb+WmY|iG2_l^Fdel~01NgLM-I6|LP3U#hP@f2$HP-r%@*uzK-?_oV1b_(iA+WOz+sCu z^(^f`g>>2D1S$;tiD{u%?m7#?{mG(t>L!Sw%91aD)2P4nHL0fBJnXxu;5?^lQKRw$ z53)0R)pF*lP}1t#*fRTr#OLvJ$R0~DHuoRxa(_C4O%xaOngR!Z=;hazwD< zl)F#o_5yf^uGaD<9fF_e3{KZqKOgEhkTwiOEfl-QT6E2w9E82IQQ#*!#D!CCN1V4Bo@H*>e#|f?F833gFnTe z)KVgCIvO+Nhzy$jhcNmwmhCqy=vIsVG}u2U;0cUtugF_O(UVw7xo8xCz1DEzK5erH zp@qVFqZy*`El4njk3;PZ&OLN=gel$I=hgDPG#loL_q9LJFcd+qfF%z1^jU_*HG7X-v77;d*r355mRnl-N1&Ye?q~eCPxQ{`tR?s#jC-0V$L-i z=^fkdyFch1RQYbLKhPI`yZiLX5B#J2ypfA1I@o(_8hUjdGMw#&&p^Tq@32=m8pYhT z*`hGkx9Oc11+TlD9U|e!epH0$-k00SL%)vgcMSCCr@G79i^kiRi=9{d_rzhGcSmqs zprY#&Qsb5-*fTq6X+rbm&HKK_h^c^L#OHZ)$8ftOZ(D0*__ZV9J-&9vC-3(&@s*eK zBbeAV^lrXZ*sl1u3?stIE=LjAG9v!^#U&X$O*lk`54{ zs<%K-h^->(1$>6fZY(PG+k2S@sN2doJ|w5EW}UcDjEmZnmu0do`CGvbRcUi6y8*_- zw`>U_&WxE6T>A2SBPPe~&8z(R1EMSJ($Izpa-X6sM3mUGq^$E3=so7Rch1F<`SCcR z!!xQX_$zUry;=h7jt`IlGYc0M`W_~Jn6!RGvr$UG)`{(z59z_My}1!J@#<} z`8^&oJjF-exx?YOeUHQcu-Q22;z3>_#bn-|ea~B?gg64e<0^Gc0q+RpnnWs88L|UE zVltn1r{JtE%8743q9NM$^Y(9M+6pa0Z}Z3Z4+(##gyg=mrF8Wg{92V}JiP;R88D;3 z_QU#J#NHo>nwO#Wd=-RSKc!&F=K`NH0`U!>H&!FwZ>8b1^qnPjW;(!Ts8{26J}ubh zgo!yuIduOK)$7~DcpU5%;_-yH_cF=ejQbaen}5(|u=V70z$5WctLIc>7a0ua=FP}w zvKu`PZxZ||ikb``H>+*=@-Cb-%Om{$1BU(b9s8DU8aQ+8H>2ZyE>E5r=cFhQ91VFT zR8U8_^Ek4*D0~4<1O|`6a7cvP578?Y9#k3L*&QWVSP6nih|-+s*zzvlC0(L%ok_S< zC^XV^vWw+Y+v(R8<+}ZMi`H3|ZdAsas64eTn!!uFXi@a;%E7QzkiFVQbfgcXT8A%O zGZz^j_cV=vya$ZgAV-DkUIbpVil6kY&c-M$dbnT?ZuS~~>OtX|cyW`99ns{0fMP0V z!4yT7h+Csk;+(U*J&D|nO4;x$+w}VU#*9D$6tD668WHxfOaI8ek-vfiHec_@z0lV& zE`S2aQ6OK1^d6h$7hVQVJ-)_Yt(VQ5gWs^Cz&^sen7}1(ydO4Y{4W7t1oR~sV`lcQ(+~)Oj!zZ`27?o#Snw( zkOV|5i6XoM*x}YCcEP?W((ZH#@uPPgq+ZmIf1vU!-yOXrBLPdeCkDeC1eMP!f}S?t z2E)E&{kVjsZPuSP9ZiTnwK2n^%KaU}<`mB>T#9~cef;O$cHtsK3f6JgYM3?|y!4a4 z1f%&=3;*su78(TOZe96!w$z``(PvraVpUXBwYcYdX>(I#E@aDQy6vd7d}jES?%_x2ji*#{ zXu|k9&jcRX91>dBpau@MZVM=2AK5~(K1Kh2H6{g zfo#&~$@**kCHYK(m$czx@%6!b_v!70p6z>})7jweq~puCM(-g?0tiUDkan>~;oq*G zv>fgs$5!$#-72nQ`yOM(cmdKfXwEm!1I(se;Cphn02m8rZRaAMQ{r< z&T;!PCM~R*s_M(qgw`l5fl7r9@ztb*aE){YVg@?oPq@)zxY(;*;2IR!S)$zxw!r9p zy{C{0xP!9(p!D`j=`foZD()l3)X)c2A28&;u^UD-LLFioQRxRD%Wl#a(mUKtTt+Kc z7FiRm9zBNN6D`iKIQLGNH7iJ>gqASaBIFyN%QkpVQS>+twjFW~1XGvQqS#k4_23&! zpMo;Qo9*qoNxMDe=g7d2NK-Xqy%CR5foa?7|_V#vde>+vG$`X_Dvd-=&L*TdLtY`~|k&R*{nK+lTEFqQ?# z%ge=~VKWoCCqJv1F4~>gcFtrJ4lmKy{G#+r?tF;QhpLgwZvn%dMGuH@_AV-}RShZ> zCX;YkNFZZ%cRdrS=5{o4puZn2>PWY1x^@CV4UYgB&X%z_mD+bX& z?cl+olK=N{4c81IEq8SVdjx!I)cS!}D=KS;R1*QqixG~9kQ_O91SWU@**Hw(id$t3 zQTe5b!U?&_@p0cZo)7jCiZ$t`K~%YE{s-<+wz!uTy45y~y30t6R&Mkx&}H(;m00;3 zpAzIM5m(l4lFjh83s!2uMY~Dx`Dz3V^~dC+zT#S)S9jmGy{mW@z)G}EfJ}-EYJk9G zNf_l0yw?h(!u;B^;r_1yo`5sp+_L$FdWef!v0alC6feHCZHU1SY$6Z*Eq!I%Dn(nS z>{yv@qDTG0&g4`1nbWm4Z1aK3t41pI+}hir+Wpv&<2ka;meTUAaVEy%1h2qTm6F$) z{!)TMoLL|p5iZ52KqU>}>{1x5$ugp^PD|rLzsV@48O1H0^tzFK1h{dp_+EGcbvryw zZBMcDfKw)4474X&Cv-|e6 zR>p|jYbWasPRaW>XmDAdV>k_zvP-jbKjn1)qRuNPB0(mJR=yR*x)Sy7z(!H>SxCW= z!P^u~Ri;wG_f&%h#DAECFpD!4kx)t3D)SdK5ZaXo_rnnujoW&P3>tB0- zv*F&2fcpmTY1;#ubRw)X_a8)SG!p`^r~fIbpg?mzM!*L+SpsL8x6@JiL{nvwHSmR7 z)tGE_JNjmS`e(};L0%J9po&r`x>b+wFjD z5nU)Acj!X{?1`U5?#FgF4Um&aE=pPpach=8DA*=dzGI^IL_9nMN6?0LE3ng;^ZIR_X4wA z5sKZjE*ii?lj8OH^l|`fmH=wB1(!IGhDN=$;OLO(D})4wQ1OP?kaS&shBHxeX_A<~ zc$Cb^?X3(!=b(=4JU9iu&?Y-|)8mY=}$p zN7fUyt?zYu0KCSsal>_w(3VK7ijsdcLo9An+k>Q>WRXtVQV8b8fO#4o_0jEA(iC|h zYqLX*Zv89r_+ei(Z}=?EeF)2TqGvgPczZQlGM!Q-yo@Bv?UuOyoNW) z@5z4Zb6hR9n&z~OBtW;<*G^If|4xZUfBxY<^6@0xivxwh=74{rqmhKtyW-B$cP3AE zI)H}xUl&h0QeF*oN?4&Nd+8$J*nv4t;8lvwrx1m0vYDp2oerCnwyzCei*9^n&^i+; zRV{EYs+6m1Z-}IpLSvwjGfin69$q&1rLww3M{)|s1o_#IB0UU3h}Juu*nJ(6DYU?b zajTGWBW^>D=@*C+**UBI z8XQk~8N41F2~h}Mpny+*uaEbG#vgU4>>-QgRWjQ6x_?TtBxAuQbI`ut=?p&y)6FH) zJc!;xLHoQU?H(SsS7s7@T~)~9QiVsCxC#FvI!C5jS(4~%NAezG5`KP(m2$*Za4dj{ z*0Y6W+sXqm&1RQV@&OYzN03pE!j5lWS8_(ELq9*t3$N`z!}4R zIDm7R=102{_`ZuGMh?)U-9Qr=15}}}sXU0?ru;R&3UQU3B73s1*QTD!XS{6t{G-z} z`5gSEL_=asKC$rukfAh4^^o%|q8Ng{xYhys^?fhpoD4cHrMgvK@oD;=&!9zVE+F3? z8H}Ffo{*9b`k{~pW}hd0Wrv8LO?N0gS%qcp-+dh8zT-FPG!oq2L5(FxS6ql)@9n_d zpJdbRDfGhxGU#e8+Leo<7(+n!61`?sMl;4yn|aozW^HD_1u5lW9gV}EUU~3!et;m`<$T_-sS(jZm{Rmt1Eb!T!0!s{CE;9vhxl_J_~a%rl>{j7!_}_RNdQ$^e*rYlzsh?02yx(CjxniCgPS=Soq7; za^PVS>L&ozTjmk$vtZIW61vUBHN(F}l(i&H^hg)ae-boaGzJdl!TZieO_-_%YlQ0C zP}|oY%L4sLG6moHtYC{<5gG{Ei+%n>jBtv@Zhab~bc^{IvADB!+y>}Rddh~&zJ4CU zH>{JNj6}mR=<42@s#^l=Ow}#sf{}6&3h^VOlg{WGgFmvX-O{Ee`Hkn9O@?wB%uehw zM|slnrc)^w<;`vLQ<%uQ{V!j&HatrnVT5EEiHcDeVvjqpa6CFbDBtE+(1U8?LtD2@ zKFcft_BY6bT{BW6|zARw#a(99nvGX|k2@cfk*(LJHu;lH^(FjlYEQT;riT|}}w=k%2 zSM9!S47oDd3|+jHOcm|(7^j5Cr)x8>w$+O@G>WJ+BN$~`PoRNFul6keL0w3a?F?j# zZ$%h3a#-FoEzNGGcu0utbp=<$Szt_g7wN7ofYl{-UyL_MI^F`eU zg7CF>Es+z-7csnXtEgFm;9qSpS~7IT$d4!UU$Dw(tD4ARO}x^QJa#9X*&p*}>G z=zE^jNv3C0kGUdO^4)54{$hUsCwmtb53V;J6Tr1!8?(6nBcmI$0mq)M|VMLQaWngPIFDw|BLT(jYpants!78lLx4%hT; z4Ia(*b6U_EaoNOp)5YT?Wmbql?Bk}r-8X_d5nk}jS+b6e)N?oR=<-0*+qR{~O0-}U z7MzM1B!*wv^(>$U61EQp{SB4H53l)l&L}*RVEdut2-K0=c5#JNYts!*g#-VKFO~Uh zk1Vo?Xb#Joq$reV7ObdFeCBOV{h$m~8(h}CD~CL_`5RRyf!ja)mL8r5eB^w5HS%CB zuUKuRn^H@w^?zINp|}f44{!vRM=1kSl&Vy7o5xrx)dQ%P(5;~iDrAWsGt1awsz=H) zm4#-?RqqxNAnR(1IIZ~dRzsUTPxJB(3^GvlI?yMu_PBdL?z5?#yQg!cgbal3305DU ztWiGaHu(Cfm-0N8V?*o3AhYZcS@Li0UxYZ#@4@6~g)x+$ft)g&2UWa<$2?$U7-FdB zzdS0};n~q-{%PBR!9f#txEEjOVCutcU!KMLP&zk;ZUC#W^*vr)?6y{BUj6&lhj|WQ zgn?Pbi?vK|Rrh4PCJG`oz~is-Um?7VG6UAuyluppMPPALb1ebkYT_9zZ^hQOo?5yP ze&o+ra4uE?mgS!~OGwI{?av9~^Skt{bp4xCW>6Ttg7~-T*xQ&Si3f-EsodVCCa$R~ z;G8S)0M|e8de!OCmGq5tbIB9Nbhr-&w!4@(U(nrk5SCr-CYjpSW$Lc@X`M<}*bCjk z1dksY2lXQxS>fWN*@O_~omaB8G%>?NhFUeZ>fN(6N-3xsJpfBZOEJ50hX7o5!C4$c zJG^qd&kUE&Pv-rLCC zcO0R&(V$H3vo*<}9X0sP*)71=ZBS17gt+(M`bJW^+Z~}q;4%n%-1+QQc|Kn4roBC+ z3ptA4{OA#WMcGwjc@A?@7TIW&DijuYXzQ@Cr_^|!_u@q;|suc09mPQ{< zK$Tdz3ZU>F^@HF%Kp*r+!R7s8fci!*#|It{6WS7uHHupkdJx>1m0G7MMJ7aO*oTh5 zQWKPJq8u&zHvK`g>^h#U=(_$avCW7T<{|bzxQQtgLN=YF=nIiobUbZw@1E$fen-An zDl(ReNnMkPGM4EIz!V>1>LBbl-dWhi*WYQYweej$q)=%BFy#<;`Mv`&WudmPGOIuQ zb)ki|(A?{Vt}6N&!7CdtOF}UFtaFE=;ommy(?WBRf2KU%(jbjMpga250`E#qc@$TF=kVtY(ue6 z^w*YGUxGu+y^(zMoz0~+>PHI*GirAJs8gRwoZRR(I3ny1>9Ws{(19-OWD2sk%=~ik z+k}KKD+q^JnD+Zz^shQICb>E@Z%#6HX#M^uIAVc8n2KmJ2xDaKkGX)P^i6pv{I+dC zgQj3>{pJoknV46(5jZL+3ix>g#=gwjEJP>>3T})v=!-W7QcFVb29q0(n_erEde*O4E^y0pD30$N|9MlkH^?1YI{U(f%~GK zLl^|UIdEX>1N8@a{>&d>FPp15sb-kv5B+u=eHLmwDb@~ZG=s708pC_~4vTYC?A%%l zVBg1y?gAOsq+R|y{)sn4qen5ok-SKI$u}2a8rq{t%zAU}VDje!|KAe17l1!#e%qK> zu1lUH@beGbHru4B&NG311^xe~B1PCrX7rw2x*?#=KzqyfWHaBj_>R zR91WPPYn*jh7Y2LWvr6st0mMr%_M%bRC6AUCF)6a1D^-iZGD^olTXFpGqJ)Zdpu+A z6QXqMb>F<|A&hh|l^IiSRqvNUb&PX^eLG-S&TA6>qaEJBLz*3~`{p^h1)@qrbJA%Q zabWJeJg`dgh==`VEDv^3<|oCzgzV_Z{A}2liX?fOPPBZy49C0)WG#Xj2ug7DvbX%> z-<>yqz!Vx2q#3S=u5v`Q=%(XMI+t`XRuWwthV13lEq`LqJ9ypFI{|-(g7irS@FN}n zyx)t=qE!C;uvNJF7#Hl}MZ~vcOcY0*1Vo4wAb#m-EaxKH!L*vmYf6foE)D)Y&_^27 zG#_Nfas_#iigP?0@Bj*bylnmi$FsUc(2S?t2k1j{qE-$hJ*W#kH*zKFztj!5J}F|B9oD7D5w z$+m#`imm9W+wd0V2z`Azsik9ahh)_lm}zN+bNz~}nPRe8G3&8`vm8u1vMG)N{Wuww zBDCYu^i!6|RiNmByv6oE~X^IFE_3$K5 z)kf!qO;w-(lf}ev_(;bXo=iI0(=UUrMM8*XtYN&kBFCdxcqEdOx-LbS%|^@G%~ zb;t=>xtPx~!PI^VQ1BkaIh+7gGp5*N!&$jI!DW;-XoZlQN&9;fzI=QuWR%cuju(#P00 z)7x&fkB8ZY%-l!5X($zfTi>2wCQBp3HBOW>dh2>ycZ@}Bk(FPXa5;~Pk|Hfg##swv z20cq3^GnNrS&NBNhz~&|wNc?ZXioI{p~R-dj024H`zSQEM%KUbJ&cXWBhXifS`!J& zotp8z@#&G5`8dL$UonU?T!T(HPY{Exk-HQ4&3X{n?b+F$0pt!!z_3l zBBpryPuC3f&+o6E_oLE{+*>9)ne98HhnlI8*K}en2>Bt}{&THkjJQ&@OvmdoK)TuO z@_AbmLN32;M0*cZilw8rOA~FGe9%yQysSpKyBFV^w-c0LD#Wn$Lv<-p#B_#cgQ4XZ zNan=PdA3<3@lelZQ@M{lh8^Kf-+v)-M|9aD>5k{FHT}HbR!sxXJP{iNsnu*x^A zpMN1#ee2a&9`(<;lLkVSjHx{72)y4-K<(d@1;QQ+j$Nh0H0roeK|aNFBv>If600)6 z$|VEpl?P&8-g{i_((i7n*oVpR_DK^(<<{EhMLlGZYV5*S46+&i6J&V?gH>BA^Nr2K z4W(U(maWxp_D?gndww_HLN~aJH&{!@lr_wnOi~wyUUJFBv1}-2&mEP&B0bYWM;L4u zB%bMLcGHoiE}`7(n4Sa;K?|uQ#2Y}8#}hEg66QQ%k=@d`P`jHw@0XBIcdqi`>`{kD z`Z}uK&Z8iV1^kQ*sPtBtMJFp+!M_F*e%;6O9C!&U?;jhRyuUz#p@rZ_Y>Y||NP;=6 zX0zn0{6oYTJ6L?qf6wu|ez1jpd_5_L9i0U_T%74Lb^|mdI6bIZ4zg<~-HO2sC5&Ac zHuF=wf$6E5^WSQd+|sdiYQpW3>2*F|6!FaD&~t}o#KyF2Nu08<9~UY)8BCOQp!*e? z_>grTb6Pv^k!Y(HtWrKbYoef8+Eqj_6lx)&_dtqpU$||V0P+^Zk`mKWwU(;Mparamn(A>lEZNp4+B1%vFM)_C4iRARVH`s(ICWd{WkFrXV=-z{SU z-`gI>&`YOetw_H4)zwJFR5okCB-rwcUW|)v?@W6=iYCn*o)b={!KeISY_#l(;yVus zp&)Eb4`%8H+QUx88(v@2=C%w*oli00gOv98wba%k0XN)}+MM{}l=f-~kpxPX%L$m% zmfYB#V~;D6@`s)$c>(2ra))8#i+hry+rDzh_86Y0=qQkkN*{g6wQX%a5J4q~XP@Mo zm9^v@&E>rFC5;ZJ$$L0R}jN z@pyL&i4?~Rl#xoM{rvK0tw$={&>fV(2sOzghu z2s4Lq{UnQ92lbxcHE)RgkuRRrufp~BvwxN+xjr&qkNJ2;I*K!XAy9Mzk0ogM#(v)v zH~NXKdU;hpONeVw-K78%;lV6%`^=gmrcGeR5|DH(Ak;Q&{67-TRUd8N^Di@SEwYtq zV(D3qSt77DRuM!-8SvKe%|&r_deoOgPY>+@~H!T+T1MLR<0ybD79w3 zo8QssAwFkIYh93R-qAySPH-#e0Imx`@XZ~;Hlg^V8Cd56>xM%_OHP2ZRA-mmn3+c= z;wuGn9vzb~w$W9HKFMqqwlXe}9vnoR<%*eaQY{X4L@C96F2!iKhC=HqRDaKiXE{jd zq>z%|7{f#LhqAbxc4;u7ZuBYyH?!6P2*42#{~5eUK;dlvx!dQ&y4IBlfpYWSvKUbH z*gOIxlWa*QUPR{{jvbbtzM8d`-%qDclnQOYkb^c;!-E`@fjoA*1Mt(*#iu z^*i@2L~g>TbnUhz^sGN0fku|1H)EdZ^olCiYm7TpTQisI%2uJ_BJ!V-R1R^D`~cijO#~)U8rZ;bY-bSvY5n{8t5^ zeJ{;QmEs#3N1I+y-Ul-josux9Qxn82b9Bl%xFF#b{*vZ?oJ2kemu?*u_ECf-;(x%*ChSdv{Cv`!Fp zNgHUA^hmBe4a&C;^Zs^Cj{$?is~=(m=;oZ$o>o5jx;=UkrTG|dBfO38hOhX)LFy&u&i7gggEIFA0dbNG` zPLvB`y6nS&$C=Kq5U|L1S>-&}bBX1!GwO2c#TdS~(cNcO%G~E0UWu$W@jV~%OF+xL z2ORI8a{e2z{5N!?MXsJ#lr#@KvtJMqM0Gg!d!z(cj-*;#zVMr-+|dC?8#hNPOu9TA zqQMkj4t?{n>I9rSe?;ES6)Yj9a4Ad==(M`9R`Poe2_*gsuzvrEp1@n`CQeuJRTm`G#@aX)(f_=+a3u@08#8 zeQRdJs&i6L)e4o+1$FndS`HX5KmJY}t0vJ#c&H6f9MX;hI^}FEl_me%-~Q_pOzI{> zo$8ZnYwjeayY z#e47<0pwVRj#M%x8C&2pxXw2h>?2^GU*3!XlhsZi(pDpueyqiG0702LDr@gD0-={x z^q7i>CS?89M;3tND%9yIH{GT>8(f(ePaJi5MNBroQ>>n3E&+oV-1Q@%P z23Du}oYjG7ys(G;HvM(b_Z57Sb)BKcZzw#h`%f6BgHIGgRUB8eO)%_k8H|>xSnG~<@X|vak1B}q z0D2zQEZ0rHl;F^tvPopPmSXquR!dOhEC3U4oUMG@U^}>(U%I)c>Y#u zR%G=@7R^Vrtkpi}s(xLbA}I*)!{@B=Y}TBbPMY4~A#y=Y$NBX4@5x+U{>rIoekUvr zw=jh$7Ugx>%3@rLNP%dn9=yq(lPPwZLeO#=Y!$3gcVg1ldM2l0iAyWoRtbP^OMm-( zP}}re@ApG9R{0C5Eb+;wfdyuF15x3vwJ}p)R*zt5T?-LDH(IpA>iPW=)>oR`Z*>ka z?3Z~DE`pZjAzFd)LJh5{~VI5eGQt>Gm3tVKM6jR(FQWs*n&eQtcF4vv3pV^?dbE29P2hl`W#RBzI*<@*>8`t^|cY3<{muwoa17_N> zvN0C-5q5XfBu);heMe^!fT_^@oeyhIoOQ0JpUb~9xqp9 zwGFmVSfEc;gs`_Fg)CVXle>^hT8dI%wlJi(0X_)!uh&Eu8?exTJ7z6XuzVKztda!Zn8# z>Xn<8FskPqX&m24wL{xfwS#lBN_6$)!zI6qdEYFj9i0V%CEOy+!Hd5=hDU0EZ$__ zG-C_7?z=J|*3@jN&m&<0@?JA|Hocb*pmCo#tL-h^4PMl@Ni;S%)vVBZoj|~~Ec#iA zYOL%a>GDGN8+W7RDI($D1{jQmEbYus2bJez9{0ggXOw`!ZBc^&jBG-o3d@` z_(r(MQ4zNYLEeqhPah8K_14jH%UmF;xakp7RKodMl@qY&b9C- zn@9nRed8>}{Ldp&t%I3D6kEHOPn5o_*-#4DYaabu%9Z+JA8gdLA#>O@FSkfC^+Fy{DI8c*~mLUoN#8@-v#r(NWF zDKy~iHs_mU2LjzKZ|;gxr9$foxsV}A{1EfENTIoKmzgsh5_qZy&YaDgSu6J@lC3KcX^LF=u39`VURu>nH9VDbsNeW(2^vtvGiE6k$%x6I|YBC-|><<;dj5v8_IT5c%!i?77pA=e?5SUM!A zzo9ZlA4s~dhtmz}o3lRRRy<^tx`yxpVff-_z<<#N7uzRvX)IgWPN=GIk&mzP4J+mF zW8p?Q&z>W1W#tytDg;@!=x56pHM$LMcP=%wb1gDMnIP61Os1;I8n_n8%e#9_R4A9P z#lW&?mq=1Am4`Xpr@eH3stJl|F=3b8qKfnI|CtdhzLcG1~rb1E-WwmG-E z6w@y4TNfP{M@g8?52R+~Kz3EQ7+CFEsz=iCG9i=RTu}jhQ0DtA3#_o|bz??L7IBi> zX0&KUo=YuSm-jV_^vBf>4eUgd%FEF*=(1Yqs*@wR8j1(T(oU-^bKyoQ>%+ksxk>C6 z=tZW`r#a9B#6*i2+@9-pI7jdxMR2hchl`Gl{D(Zf-Qr_j|+e!PjRyb1Q0-o)Ne+P-x59gt7#{MAh2uHiBkQ2*$uEs>7d001KQyea_ zSih+R$HN{BQYyxx5-UZHoXXewdAIduct$*9LOEhFT=4;r=fwWKsvSk6ZNK2pOzW2P zj+VQzkA^rGM$nIVP$Z+{(w~HCrybH|!gHYjpkBaUX8o-tj+OlO$Pd_W09IK1*B9e$ zZ0lt}XD#d_Fk|2&N?Q@n?gS_0EUA<#NuFO}ZK`f0T2M`;)sx6;i`09yULb! zPUZw-ZaDbsTuq!t^`ZL>mRx!qYa?k0rxzWp&`sc)2DZc>J<*&Q#dh_}G_{?xtF7Sj zq8$e63z&5?+9>=?sv&UGVw>wPP71_4*p}=(GHOXR?3-qs5<>#(3`>RPF)NMHGxdC@ zLe05@UqmtubeM@LV3mIbII(ZNue;Ij@=pcmJ!t$c0iA7lt8@;+L6xqTGgQ28C|+Etc>8CNjXa{fiAa1j6;i z-HeM~Bxx|Kq_qw4l~o(NsO4weKR*Y**nzb@{MYmBnMDOr3bmP)g?)b(pPIK*4|6I2N@9n8~x@UhVubsp(_Y2Xk{YH+yZQV-!x zEl46Rdbv*WKp-`mJwrdg-inc(2_+O_iyA>mhdJPGS-zxtWOyRW+iz!=3<|r{sh|Db!fzH(_&pc{Jq`^XLiNhPN(*OFY

*XBGLb1}(aJ@==24S6JvCVmsUOM#f*f?&q6UF;g`7t(eTQ%}BVZ|-)H{$IT5g_7KEE5`2Wk~(DDW$mUk?NW|-Bjy`w z^GlVN{zf>$#$*&nAwz%Ie!_m2c^Dn7&^>>IsvRi_Bv>~0q@EbjbSyRQ$uA4NfpX4K zzUpVz(L5A3^SY6iW{3bb5dPZ&X!V-)nR z+PAIZC5gCc(pS&Fg9E4}D!>d-r><%>w8#5HC7Jf zLF@A!65b4(t;#MH7z992z{4m<)wnQYSn8#6u(#hUtv7=d(z`(6_P4cqXChUYb4lyi zJR{$sM5%<)p+)?*=oi~(v2P(qp}6DRca+MFz!NdIsAMXFQBA35oQ=IU`}s9;u-w&u zO;2OC`JKqiQ$}KXMve()4+}w4;VieN` zW}k3#l;TgGz{vDQpH_HRmr^XK1`YI1Xo!9nz!4SYOIZwS>5Xjm2Ubl@Cg|HJ8?npX zlH8zbnyr-&ufom}o?|CXF>AoXd;*pdLBDDD9zIC3!v(!A^}*opl{C+S4d~VN1$Y(T z!_N@1!+tylVE2>RQAztteE%UI@D_sxMaqb?DDMsak0@C3zzbE=Q%Q%#xOjioIZcQ} zI>mH}2%XXN8*FHX5C&ex9w&O#VJTzzAp7d|cwE83>~a`8493^@v`d50UGbMFRFR}C zSWd@X0p1h9ySO>8>*e9j?p~d&ty=jOF*lBAN|J|5)EaPoAEc1+#k|}~#x-U66JV21 z=F#-sr`Gdx%8mU@e(awqDMf_Q4wuTr)NP%Xdl$~G-h>dvdl5l*@T4VNXpFMuO%I_n zQzh7yO}U4XX=0ynjaWBzGHMbO+aoXHhVcD2fypd<^cm|Q4??mJPyzxv2{|OE1QKG7>#D`VO6K z(XWk}o|53*a_=(RXO z1Qj7n`mmY5^5~m7h*Qjfc5LS7Ts+E;uWulhpCsq31lK9cUcci?>G}F6f%cxW~%wMy6KQG0UZkP<-1l+xP@>qIuX9*)%_aZOj2S=MWVB>=NjC9>qA>EpInU zHI@lOy$(8-MS0K@+5iP>ZY^$$b%s#0qDXw4e`q>pg;qGN0P3E`23OK&HaunkF%lRZ zN&U>4o2d%bO05Tpd54>P$&t~toN8Eyl1`iAKE{#>W(RED_a@;+@BCwrmYj77$H`Sr zw<>a#zt(+%q!Z~04!H*Zl@lI5mqQxgz%oJoC9?|-l>j!kEgxT39KgYlc$_*oCCy9{ z&v|5bEx5GztFF35aK1=Sw^a1gg+s@GSb8aV8|&qThkLaxgv5zNL+-nlgdZWQV%l_R z?Iz=2o=N(%tSsT0g(r6(a$05Udi@jV@L`RSqMBWko=jKoApM2kf>g?Msj01zGVOpH zrHg$$D~$pljbI`4-Ziv|gFJ&R(l9Aa_BA5cgb`C4;t%alvcCa}6XyVv`wC(VR@ zu&Or7VtieDRb-+cS7kvxIuat`oBOV0_{=3R3*N^s#j$YYKLDdZ?+}f-`Ar)Pjmww) zM%<)_c-M0O(qxZbsQ1Y3;Qz!o5+}X`|$u`dv9dMTi z8>3Y{l;?>w^Pw1jxM7)8%^OGjJe|mfU5NsGOfjRR|M0p4EO+@47C68Mi|F&cv$Dw=LkRdtbD ze{U;Z^2>2GhaQa!)&G)C;Roox?9BA)NgG>Oe9Kz~p6^nO-&U<%xILr7Do6Gd-PyTH zJb9mau=ua*=~mscTl3K@7dj%5c^>BY%gx?ji{Uw`L`GEF0I|f@+4#XQRhjfjYaJ zOaM2AU>yYejffz+K`wCHwP)>`@WP|ZPOW;d9-DQNUh|p3m#%Ic(wj3dw<5zHc*n_{9 zI{qUZKB@tZst^_(s-8Hhj?jLXGB%w^e!?Rwf}jMrC%AhSkIgUgsrFguvd3a;_u0Se zjuq0)NH;h_rj|zyr$tELfocUOX$jqu>)D-LUQnEekCh;HkJ>#U>JhEen=Bok@8$c$ z97+rPy;PUXM9)`sxLtHI&L6k$=v)4}bojz0Smz64cp7j6EBf22BEJ3#uKP-MM8p%8 z%|XE%b>v88v*%3}d(h#yUg2#yOw=;ByK1b^RHB!DF3u809ula7v0O@8+>Qx(xM154 zYqlhsuzwuBKiPl!@#(mPtKtCOnM%la{8ItVh`VDMiJ|*~znR8uP?~!#klhwc-K^_S zduOINacInat)jlUVsNEk{44w-g?*(f{2bMHVr9ejZNIou7aOJb!Xm3dxamtcN-(wW zKs$X!h{&UsU;2_0vh6cz>(Z?~oO$RfSuph0ZplM+?Si>DfD6O)FJ7rI9c_miJ#F+6n)efkB4|RmKKnJahP2H9lVo8!;jhyPbgG($W zyT#r9Wzf$%0<)XzVpLQWa=|0%<`}DK00U@r5`gC{l0YA35Ga)Dxcx^<+z6hLgN$I- z!FkM5_lmV&PR|MpOE=F*e)@-)v^%PzKJX+}c%kNtowX3wx5Cdbt6R_Y$Ev@RfqyL1 zLLp24c1(CuxA2=$A0JCW8^J|WEXL_znF4~c=O(;-7KwZbkB@KN3YB-ATcFCVAp%x4 zDF^bQ&sDX(3Mq8v&$jOh)C|4nVp(SRWU9S{(LwU%TC=vaxwLj|%UR%veefz9E2UIW z5=X31Yb{|k#I?1uJIrt&Q}mhO8t~6j`#WCRVoQ)M?0vudJevHh45IV(7X&klfl{ zW(h7@)ODRhFx|^Zyfr313*HQZB>;a%_{$Ui_F@qwxqm#J5)*?JPPsWOka(pwlTJi? zl*T`}jImk3+&2ruivLAHOZOc6a#HgR)iso6Y^Qjssa7?Cj*iWhSA6|R$MZSGw0;1g zMK3}}&%k#pTh@a(ppwj>Yz2ei>4{)cl>bXUJ~IttSl0Q4gw<-nLa>(Z(fyyox71v= zBDCH9aP;EdQ4nMQKt{|s`i{$!R6=4CxFq~_sQ9K4X{}{I#y&SaL9yHSfk-h8S0hB7 znWsA1ONe{CV81}edtWdju@H9AI5g9B(pSUX6NRkA)4lU&fgHzIricB9@<}0^?MDz zTnE*Tg`lPB-b(8R68X+tEjZ560@rzAF}wPNGy&(Oo#S~`yMS^Z0F9on9LXVHy7|7_ z-W5mul%m~1do!5mvrtEIe>B1?7o2sobe_bsuqnR@MS>ceWUo!F*2Q%s7{T?n=C|1O zT;8ODuY=s(_i=07LIDojGs#eguTyTh8^cv%KP=|_&fAmj5>d?(TxsY{&E9faetsxD z$NwhQ#3jd@buUWvpJFvaGV$lbX}II7U5X9$v;qU%hNTg7t)1aPIwPiL2x42?tjKJDzeTghB4f0;d^t8k&5#KB?0y5VeVnodFNdiYtSlynFs8Q>Kn znp}k*Nt|rPz&Tp4+(P*hrcj5J6o;zP2el)%wz$`R*?ekzT8r=%tu+$5T{*~h)=Q>w zlT4rdn|=$w*;<6g1-lXn*&--zI77N63#z7oW00B;;6hlhuh zemV8?2{npeuFBLA1C(cybR&4C5hv9Eg(=`Adi@Q&k@Cg3L{P*><86<@KU#smT&h{W)po6V013CRd5d0QqR9+X@ zqt%~PHCHbe?_=^+2K*~||F;JEhXpgLG5O?(I-m67?&-5X8}8rcogU`J^vp0Z)~kQ@ z8xV)0oLB>a7~%nzWu$3{lojzrTBwAToh{cu7*s1uW5`g;DzHpKclZQb-h`KfjJnvLC#Fj9vF+{IHo~i``5AV`w-&5~cOWCGnR6Xl_ zB@yL2>Hr}e2M_Wd99SA6eKcaIzdu5L#7%wko&98e;w3H)&W|6BOo_$)Oe34eIfULs zWHs%ss!RmKI>(&Hr>vd<>+~K>bb{Y}#*p@aNZB%|>!7rJG7Fol z%|-yY75bH(s>3~%m+=Tt-eb%=uIq-Sc1)=ZlA$N~DjvU=YrhCi@X(}`a(V_)j$i9> zQ_oe^o)2eC&FEg{rM=^STqC&wnuvh_fy}#Lzv1bW5Ih_yJmXb?; zrc!pl{^9E_%I};NUu`OXnaQqfMQ%?~ldWP8uhZTkAX z_WmGWla&R8{ZYC+PFxG(JO0tY!UDoHVChE5G#sj?ovNd|V5QK!z}LWsDS^GSx^(t2v3?koA=Vcz11?8L-0YgvThZu+)7nzj2r+`On-a@&`3IPDDQ+UUWcWwr{Wu8E0kl%MjH4*Bz&$5VPr^S@B;BBM<3mehhY$zeHycq zcpXRKJ|yPXQ7qd(>~v)N)_qd0*t5mpQ4JxyLdqouN#d1fE{=i^CCYfv$y=aX89@^q zS>PWAcT~>`u;&YWV4gL_s41|uRLX}ux1a3TwCRa-`i_S?z6FgXcaFU=vA~Y(V8i-} z2TzjZT=UG1ak2rdeym0VlL49?o8arNgD@@>)UXL1@j&xPAd;sYOCi`e?fG5(teoXq zuJm$m?N8Md`xRcxe(O7=?R){{B;Gt~tEVew;`7=BIdkuzCHTlTc&Y1vWAnN?Ixw5% z$un1cgr*FoE&Al^-DwFbd~|{++nz4$c<0OrZLg^jPl|vZm%6N3O{BV5(?jl0hKo4H zIDq5f9-dIg6)E`9;=yI`UDPu}c$XdUvH0%qA^5>BDg51_Qn(I})oL`bj_bQjhL65w znphM~@hA&>rO2$|!s!;>cii1Ea^vrlf<4=c}s5 zh??`gpbrbm<+_%iRcOm}qgH8=0!DXTf=4bzcBqE~uq1-Vuk7fUwRQzYaM$lc46$AP z{z55<3xT6#f#A&UH@qea<4te2NAOYM&}%HtYyeF0V50&EU&}KN5H*T^c=A^!*Y*1i zJ<1%uW2W|)n96v|0S=|*z$dy@M1Q{t$Y30t8mQ(sQ9sYGSV#GgG!SBZ`sG$Qy|@6& zr3Mk3M_U4`PEy^^trj!`H&V%WC!qZwR{Z?!xj2>XNf)Cwx=a!X(uHYZbR>zk%0)K( z+FM#RaFHmgiic1e@bU|)&F=#SiziVMs!7TPN5*xf{dkBh(LQ2+dh4YxSMfBRZo$$wV#v65y+j*i-uKyZ-pcr%x(p51ao44iYP5?kLpos_DF znItpzLZ8|`QyER7m$_QG&yl2hKrkW2$%sR7q6S-wuc&6Vgt#Rl9(FzFYA7cr z?9A2MKNY%|v$whrTW$GOfppe&0|0)GSW07|-+u49Vv^w>8L3->?}VY8+Y1k|9$Tn$ z2L8_-YX19p`sDi2eIf^f{oR4UxaVtSAuJI&d~#0H*MHX#W4n2I0AFmN)A|FDQt`xt=gUp`u{unCM;0_IuzmO0pEQ}H zk5!`tXRrO?PKNOpFg`Nk*%UB+b5^Nb*l_)+_!weEJ@)!pso-D~kEGi-dOT=DjJseW zlrSzvcXp1>n=~`UX2PtnKO9t2a(v=8!4aQ>lh-Pw>fDlcFz2CdzWdZ*{lzdpwv64w zsz~$DUYz=%yB81c*I0_{WPuMK*w0L25KjtS$)(e20Mipwm#lRhIlH^+MuISVHkJOG z=(5spDS|2{6*M5kIeI6OjTdc(a zfOhZy8{_xGndv$s8)$0UnN@3wByu~Q{=!Ty7-`O{B{5VPS##6(K_whwh;krehcv5Z zv1@E9VAr55)*`Isf@bv^>o&W^OG*roYMaVSQ`WLGFiDFUlDb{;9i7`Ql0L-~pMI*d zQ}Ga?fohaaml)cFBtBRrJ@@??*L=x{PzFoer*fepq9!TH$g0Ae5c2g2Y5F$H!~YAQ zy)wkjPhr+v>Ec@T<2L@#P?uNvkg>+&FUjC)#@e$W`ePgR9o@a^ql$y4jVVuXKyYq< zgPyi#-)6Q>ZA8`SU>k>}Fe=#vfl^jJpE;zDW*+p#@OuvOWRmjA;c`g{SJfjsid7Ce z{k;}?PQlT)9;^Dd&-l$6oJjBvjnL0)WGOZ#MiXm_j*%=CJQ7zK{_9}8opnuu3D9~$ zzNU=HRU^*OJ;wh!Wor526wlNh$-!OaF)PPY730`Nq(Jme+GBOLPSaNEX=9X=;=G3X z#FhCQ^1G2VeZl`PO^?h?7k8e+&Bc<<=&2x z9WI~NR|ht}*%L9(XAzCvU7+yi?4 zKLICKoUV)`eo$81pvKnQH3o|Hy-RVshsAARS{M>CMF}z1{dySta_Ga6@jqpth+jXZt60GVl z5Hu5^kBN8rka#i^wy>jFet$(x-dwaYUb$zVb#q10vkC8;4}HlWRG0-F8b+U2;a^`> z2xcLd@$+MZws38f{)%$-=`PrdZSm6AV)vZzAu7{6$`%y8oef9V2xV93HI3JDGf(n= zNu%`J`32gZkzgb~u65iw`=#dKDu8Z}s6z0t6T@=PL8s9@Y^@T05Qkvd33uJ-n%*HHA%%(lP2xVl5k{S7PStjzfn9b2_azwZ zM@0Yc1w?Ig@=;xLAEWKiT%F()L(`7A3{!%h+3ft_c4-6a7V8KCU_C%Yrn;wE7U0g@ zh!$C=lO6AJ+vIl=1=bao&SeP({RIH)-t|;#rQy&b%=@}K@BJ!;U!4t24aU)F-6Y|jfELigGKo<_G2|Hm-C)FYSuTufFni%E7 zRo)aXG!<3(T&;c1j|o_yH%l|xV1Ep>3}|<>H?VIh(5*nJgX)MSVe$Br)8X`6umArS zZc055PcbvQXc(BZyCYzt>OqlMvge_FOQKC|wBpSFr{& zQifyRE%`H5g=5aWJ9NhfZt|Mf-~d60JN;ss5m&*}fmrA4p{vX#6582qLVCJ!WdW;3 zdNi!VPk9}Ip=7dd^x7tcMA@HYLFdzMB{Dkkke=%(` zvBZK)TcWy|nFGXOh^<<+B2wS*hVjJ@ATF+m%9j7feWaHn?#vredk|KxV7$kd0IbF)|QwIh~+zakIQP35|K8y2{3R;0+sx$Ihh)w{qKZz%M{)rvGwx`p31UGk?+65xMH z=$p*$oXmiIh}zqK@9ZoF8@mfpKiXWETZm`-90c2x-LDwOwudD1@Mxn`72!Y-;!@-J zBUxD`?N~NXw^&nLiU9w%ZbH+W6k3BpM_+_BwF>?7-H|VJMPN8xA5{KXN#%*l^a9|? zIuuD_2Mo>X+a^{Um}}c^{sD;3x}kc=uNUjM28ozNT8Wdc<*Lx@L(D({F1-4u;nA=Iui1CTe|@~21{FdCEY%d*XpSr zYnh~|GCrvnvc0rgun#fCOt3GqSHoihq9fM)xHtFoPgs)aT(?)Epo=KA7)U@r*BDg) z(G`ql(bf_d>XuE<%#w%k4J?#0pbVcb887>5_Hxc;{aoiFt%RQT1{;OPpe8m0UAM!9 z>iv`iJ<)B(=#|?gDOYRVH1IX1@0|1VW~PW(vXEd>e{mAgOhqREEsyiZb#yF;!B`} zx@C!iVf~lZd*V*-Yft3Ud|q{m{rta}oWpA!0|wI4CTW;mOq*ewc%>8`gx1|;@4~3e zq&#}>02XcM#Fpo5c4tB>>>MOAL^)l^N>m#=AbHX&f>TECQv$Qh3xe)E>Rk=7m5k+8 z>$CPZKFxmAeYtnmSUBOaAwxNURX(eiKu*O6f1c&yek^ztm5sGlrhR-SS8$Gf?;JQr z%e9KrZ`?(q4b}M<2ncuS-5VhucnPsA;#u%jA5nihCtH>m4J2AQtwe+GUR$+$e(oiR zYKDhGE*Nc-Nf&=!PzOh~I+t^S@uYorWCW=FzBG{Wz3cpYY7xhj)>UIq-~68^WRyRz zw|URJ=}@dc;+dZS&fGraPew7DQi9lsF%T~)fe&dQY&r^CoxqEZjd*C@{00ed-_qE+ zmek-R@5kENb~{)uZ{9Vo+OJ@NKbgdGgn8iuo;hBKkq-ywD-I9UA zy8GuvKT6SmVn0{;{lt%C0g@%ZrBo)0Q*I=FtB6aGbge_;;UL_b;!D%2$ps%a9!IjX z(5@}W+|}v}+JY);#ll~NB)tcq|AbGS20Jbm{2@HOaw=zB_tOv8B!ccmU5)O~Ckk@D zX$w)x=}+P3hhe>}65~wmSh%fwg__vO)QQiIx~RGq4Xn^aqG0HJOc)SLNk?9J;5{JA zBxtX=wAj@cez%5dM7y0D#-GnI+<$!JpEX%0cWAg)?4vU^*Fdo*6+&i>wQ7 z3WTMRpJ92ITennvy$z?+$kq1ylerwJFqJ~yA}28{S0Gt+VxKv%lUBVzvT z>)#FMk>i~kc>8wH5gpMD_^W4co4UyuEKu~=`bC`mjQ zQLJKP9OTZlKpyKKNY>G>PLR|$#;0MUpiDS#5&l=#kGOVZ{>LQ6i)0N zSF1uwRL4)TmiH?h(z#a=ZgR)p?g!oTQ0C6?7o8B_hkR%2tRn+Ob+6@>cn688R}ri3 zuKG880z-@u5|!MP0BXMHZVRHJ`w-BGlsURZjC zG_XoIMBOIzn;^ebx-ay2j@&6Xr@)gYvfLnrHi*$uH_%=mPTKn-9@Jz2vC}#|1Ls?o zYX*0o5S-jM&BQQt#S-|_=laI!FMRj$yll;_;8D-?_34#`!#AS4pMUTK9IkJEx3}Q` zeKqOc+>`v7%ar4v)|kXjMz%jlf_CzW3yz18o3dFCo^*~$xQ=#CU76WWzRidomWFnC2&*OYANUn#+QNvixvG}9AJBEjzUO!Xhg*Iwh8ISj|AFL zID41LgTFlo>(3`(URMD+Gx0A^e-GULupKeoe;&psX(CceNhr2D7Pl(BYO8Pr zxe*Wi6>6MK1_w~h%?{`k^7DNHKavU1S+2Xl4%MI0q+Y3C6^Bk(fS*k(0_O3};C%Nu z_tcY1yM$*frGEJeC0(qA7@Qg`zig?S5i;r3bjckz+9)-mUQR;#tP40I8d#5( zAu---m2Xn}BWn1@8r~dXd7$11k3JC|Kw$yyHp9_(Zg2^%e6k!X8yJs zgN)sfHEtekK!6iaJVufTW%DJ>owlmV?|Ib=3lGfg$HHm#tL08*FRIa2aK$+aSvLlG zcnn=fUoB^+Zzv2~KF1r?NBqmSK-0&4XS(m&zTocr^C?yE_VY{pD}U=`{I$xb=fwzL z5YD-|{GjW!-a2N|VJn#Iu{AU8To9_jNt*6!mbStamttrRg>O+Kmg=L&vuyvyun3ko zOlsc_IRO6=gawQHHJ(48ZHcBzsqVYYCnWPO*QXcO-AQM_6Sd}h{mjv3FS?5giXENxxSAs7}1qD;V4&dppc0Avj-u4+^N zxt!fmLWYFY(`5Bf@w&>Y%P0Tn>e$QbdLnr7;Cz*9A1%3JIN1DxoI$KEfQ|- z{N^{`>yU&zxJ6{WvT?Y3vguf}9QZ3IH+3iklpc%d`^edIrqD-LzdNqP)))Xc^&MEg zjf|>_+W*y`CGEqkG~e#ZP{nAFsi*$S_|M0dTIv+wccf zU6wtc4~~M78_drrSC4(iW!K}SVgLDbaChU3{SV_$;h&s(GM65os)-dq-S-AfYlL)! zd4p9%^XmjZmnFYQiwht*ROrH{2Wxyv&FGxazAZ;V9^atlu^vsn29HogvonV^AXQ7{ z?%4;0G3p+4Sj>2)leBNpxyVto*b!2>77oIOS!FC#aWG2#l`2UgHj&*mATha`6fnE=@s}bW82HxfB>rVQ%p!)5WX-y#fp>Ns5TKHnQQy3@`mk$Bd`VLGE4z9GJoG!**EY?NZ>Xa|? zKc$O%f&PepQoI;t7hwNOEaeRKoBSEhKf+T^kpIT)Ipp)E0}=1>|KWQjIvE6S!tCjP zMyCGs>l9*+^)xfXx%sQ~SVs~X6j@cCDEL!RFoXRFcOQ+&t7vQ+E@z>>5!u0Icp}!* zcD}7-v+7Q>VY@eB)LX=Z7$+smekl0LN2YLXruxHTJH_zsW|&BFCo!WG^w`7&8CqjKM|z6z0kcliOcNli>F2&(+@~<=5GYk}vu9PTRFuo5s7EI-hghF?GE; z{?+-nPYc##s-Jch5Pkpt;pf6DJM;O%O(E83o3hilJ&{v)&K7Y@P3XFNC(`Vqq+8v} zNvdyIvWGJe|h3;oH+R$ zP(}4~L83d{@{pfJB#N|~>iDfM3c9DMsKQRIM>1eCIDL2?F%(bEUhp7Ut%|MAOi03;>Z%9wK(=`8*tn0Of>@7Be+$24fMjuPJAu-rwzg=h@c4n>{U}`$ z9TdrQ2~_y@fy>?GgU$<5eh<;@Em^JijjHTt4Dl{3NPN6<+dVIKM*}X< z)ZX)TG>O9DWwYhQn-E~ATe7?P&g)$Wt*nZ!fof|>rxhifh+~#+gr{F#C8;`dptGR$ zITl|M9V^j~C0apu?wu_qo?9w|QLY`kX!R}Vkh~X)G=E<6(KfqeA+h=CG45HdGqKa> zG<=GNhohW*?octpYiz(%A7!XunqHNXg@BgIEcx(Ozeo+T5R+XRVX@9779W|Bed$q4 zZdbLuLH$^lOGvM6yKp`QgA!lQp+F}F;DII4kX)9ZQ>fl;DX0pV$K)T`{;CP(JqY|i zrryE7(q`!zo|#N++qP}nw(W^++nhL=*v^h^+s?!`-hH3v+~4{7AK1UXc6U|vs#Rx*-evuJ|CzN6mzsi@!3Ox~X>IuyKCBiu97^ zk!k7fD}wWGDAOxcT=qzKI1}uG92K-+py1uQ8rl1{Ojh`-frEHBOZeiG=X8yMAAi)6 zf>b-dl9w?~q%gV+%=`aS7yZ*gCQuTa4g?zxEQc|I>afWV@hRX7BCwBnhav|~vM;p^ z#GI7Zt}_NA+PApcOzK0)K`d&W4=FZebT%01C%BFe=E>X)DGXSTySj3Rw&0;N!QWc5 z!P}4Rc`Q_0NCy!H#dj{+ zqpO${w}yJx>aZ&1nBkmxutrJ6m&~%$LVsUFE;o2dzqE9J4AxxUli`LJQVaVDFYh$k z;#p;acY3##b0qXOI*1C5d|{E3TDzshy+ju*ff3m>SmZVB#3bkembKkMno1)SS{@s|IQONH!Z8u=ISm& zTk-7xx6O$1$~UVe%vsO!6b!{`V|n67n$v?>ZbN%C8I7tbxTf{MW|MH6QFtp-fO+OI ztaUh!!)ocCcRLa{B+2Ut)I*AG(U066AFpWoY!FYj>%VlLa(QUoYykOO`GT6e^&5U@ zqctiY{V7F0V9!&}QPb_JjWHfUIgV|6fr_SoxDDZb1&FY%rKHA&&rEly`>Mo z*MBtn$B|9!5hYpdna25vGue^&3OxHw3-)u!;l*w5X4Z=3ys z)W`Xt%pK<6MRt|?HCP&yM73dm6Vxq)9*M#X%-wiIXBgAjQ+K8Y77xI1gGrCtK169Y zjZi;fWa$t{eUOf188WhpX0#s9VT4Nx>4Gt=%mM%*@Hf3?>{v9?{u3Kw1j^V4op=tb z1%-{x>aLLVdu6|?+DodTY_qmoZP%zkJi1IH;WYxEWL#x#5XUW)Yn2muCF4bXCko6` z&pt(=KZxn5cPfM9YYuNVj2wkuiviLuMdf0OqB?!>f{BuU*Hl_90;K(?7?s(wGZDuO zC9;89@?f|3t46g*B+v$UQpfnDcIy&$>#GZo6;7=kP7TQt-QX;qy;w2v+!yPo(;Z{a z7NI%;$M+87PN~G;*wE_I&xtE-nJM-}3v;ewM@F&Gt?CGegOK{S(Jf&k$0 z%nM}~GS#(~H8ru9eW+oVXk^!$%RCjHQBh+g8$gT)mk<@33ShcUH)wN>F;UL0fW2XA z+=dcRv=lWTcav>SDGL@H?b*0!s;IkZ0 z?8`*|9HUEl!;ZYNrF@^LPQ0vLcw84WeQEU=AdkyfeH2MPNKF;0j4DNR$xI&}dz`#t zNXVhK6r%!%^ZinQha*QL@v$H7Yb-mHC?yXbPH^2Sy%WN!==a{csDE8E zkET~9h??Rl6Zjg6pTlfv3dVNCi$mWC&IuNR1X~|DM0q`A7qr50GL#;<{=Vi-86$lP zpQ_^H6t*U=UFVC!toI4zo#OKzFv3yd@FNK6$UHt<9VV}lBA+56?;iT7#dJ@$=+ZI( z>1%Idin|UJOR?_1D*0h?bkbzZZzl679P1LPp%`ll*J0&w=pYC-?a!@Dt5CD@tN&6F z)%M7Lo#l;UBYWOhod40&8Rx0^`tMZN{@HgFFeXM+cEK_ewhuKG3&3y0%@vCLi)YJP z4Q|5Py@(T}d9FoFW6wW-cT!{HQT9q0yEGo5pe2|8n)ANBZk2b)pGr3b-|ydnnIhQ) z1iTc6rE0?}>E@?AEv=#Sy?yThBYA3IrH<>EJIi_;S}S&7n(?;CF6}EPQA`)lTtUQV zQvvWL#5x5?QMdnHnb4@1$^{QEAq4oKa379s4-L7`-GYLs8v2~t8{=5Q4KE% zB|JGg%llz*coGzG;OOmtG-Y-L{OA?h&Z{0Iiu~C@$4UZ@#bwD)QTA97Q2~ZS9DbG@ zPPe-}HL@&m^EKfo^M$$nxv9#U`QMS2Z<*zaBR=QUEkoTSJadU-ie)XFEX@VTYR)BK zZM0rU4PL(om2LGb)q_Y)tIiXQobY#=n|^R0Y*%wiFC5A(Ma@{R%sKOlw6pEG!{FHj z>n}M;OX`wGf%k&IDn)=&Pp?ql_b5bz&v~1nMWWhs)HJy~^gOoDKPR8u2+`o|vOhLqIPjPzKKLKAm7HF>dGMz{XUo$epBRG{=;Lp&V$$_llu_hMP3~O^?%OfQE09`$@ zYx_|7ZWfIf$Aw(un>YxzIQ<>!p4`JXOkRFGpS;mQA-Kxx%YzzQ$-z3}>*?S{mJk^qBCn)G&@* z;^RbR>u%sF-+gBH(aV*2+J^0kU&|MM9}kZN9)P%>EqDAldFP{RF>z{%jkY23*o=)- zNOmnx;`&&b0&%aaSq=Fyt7`Bd*ByezL`dACbcTWp_Fn~}9EcX;`-&)%V;pb{%j8VthGwqTXO4p$Fp0I zIDkmM#-qc?#`+c*wzh128kOLx$GrULy|CGCOLQYDVQi>Ru%@R#AsP%6O0DWLZ|n>m zgYo*QqP@b-P&Xfg*E5+>V)Csfj)oTGb5Csl=#e^-JVuQpM|?D*1p5soE}iG=I=!!^ zyLXJyd*PiY&y+o`sfBtJtFb}c(Ki>JS#ztq5S-Ecw$q5z>#F2~L}3NoHCZI)vgkVN zNRjG0o}GJr&|uDvL4qwWMaL9-4b&d~t;ehR!r;2kk7u_};W@OOFUs8d<&)K$gw5%C z;5g`h$~V94F#SCFlAFA)W1p@sr}lTY9bf|1#|`wz>Fr)CNo%libU2w0SS+%2HC2OJ zD26RsK>)~zOjajIW+j_gIv82i+%=c{`O&`FcY!BVAi4afYnoH5KrHEfrq)Qsw7kjb z%lQO13LTLP_+&gwBbYH*>Q6A^%MD_=+h-=r6L0d&MbzP~zlSXyCYN!|r@^m9`SKyF z+@>pgCM6s7dlw?HLlF!KwGh;~pf^SkH~Gxc7JN zu@8TKEpYbhLmO}|LVSVn$+==kG)3|jt$O+(w0MlkALbLJdfwXcXX|KK93`|w_IoQx0(;sz%2@nF#+1ATjUo&P0}8vmyK%k2~M5dY_>|7Y2gOI*b3@GHj#QA9hC zZq(Yh6E}P9#@8Wi_)Ptta^n|%yPBAg;K+&y0Cn3WTMsU6tHQu40%;r`bJ3hE*O3+i zf5%Bd)U{71+C^lvCB|7CaXTk93NA9T{-M1v11UKNA^H39LD%-NuYV~LRP?Y}$I|}g z0{xrN)Na1(k8SBk1ZZ%!Q2EyS0KAD5g6*x@Lk*AIh~uCTs-A1neSsPM5OB%_wU(VE zM>i6*=+u-Wqi+xCVI7k)#f0*E>HMTd?!Ch zu)$-E5xGY~t9h>Nce=kQtEjv$kh-TyUfFYc$8PA#TCkp995^Wn{;B2Qo{JxSKBGC` zqo>2`#WqE z<#a4~W9fXTnrHuJh6}~@_icORklsyx3~Qy0Urb5E%daODx3`+47Om8E9qJ3AaMKi- zZ(|H>-xS3HcGjqn9pgX-!S=_Y-vH7!7e* z#&&jneD7dmdW6R>NE@zO555{ab!zW9vy~jr&`W*pd7*}{nDTv-p2kIdeG2NkM_z?H zS*_L~=F4&*I@%%o16a%wL}GuJp&=?=9S2=QJv+T<{b^8nXL)Y%exr+MzlYq}hO5qh zzG%G;fs}r`N(f21lkS$8J}&;JnfdZP|F4?_cM9J{&=9&p4YL&x&s+RSbjspGoEf$X zL0)iNs*ZqRM74KjbVS3vw$IHYR83r++-&dVduTDRdV!;KZk`G)nan&cu1Ut)ZmVqG zM^<9OW?#Bz<;3tN2GfZGzul5>tjBFlOC;jX@IFm}7u*NagrN@YeyKTjBIgc3I{W$C zmJ}p2XTjh-j>0up&jN{C{eyw_t#Q%$mfMSWmB6IF!=Ft#=Dz=6yQ}HHZ z1^y6op^oa*@sS)ZfyA?KJvu82M)NZD#oC>Ab@Q=(6qS6n0$p48r7Zm@@cqI*ap(jX zr*&{)_>DQc8t`V+<7^;-geT7LN}LR_piDG6)Fko^?9AVXv0;!faZ5944lo(rg^XAS zi9yvdB7jup2|KR{?4ppUz%F^vP7<(~YsGk2x)j(%Lo||~ISZ$%` z-^D67B96_{2&R1kz(hW$dynn6|+7?kvF92r8_(SYd63BBl6@lSJ8v9}Thk{t840?yp+9k>O|lK%&K(uL}f)I{64FLi-L?_U-2z0j*=nY zP8}O-0q|Kl=N$Vxu=y%i$;Do7&<#w+J0l&RmeMyDb7fi$C5I-RfOvLac#W@JJgDU> zk?a@4i|sMK0^`{EA6yDWAequ*t1pD}#AyXZ4-BG|{vr)_=-tsviEDjhCUqgBmACGF8z_LAa9|SKZhijT||qv95RiM zzW2(uq&H5x&d|Mh3Yusx9^uBd4JVyfW#{*#;cU_ ze;~T^_oYgV`e>OGS}#}4I+b)&ke}=tPN)zCsx`3e5LG0)8is}^ZzYtJBS?bF$Zti6 zQW7Su=Py7{);Z=iGT^ul3wV}3!^8RocH+{k^@31|k(U)jFDAg2{#EoyMj_1`8(r%e z90~S{8RIH)Ib-d6-`l$hX%a;`$NUB6pr4xzYWG(5EgLv7)&_>+1`%dqzJr@gK3yr! zwrTxa@Drfm=mZb714NJfHe_6do=rp0uEktMij*9QKi!)d6TTHchJt3wB~DBqP|3SURQ5HLP3V?W~opX^_>zSlmV>wdZTxwGovB9>iKxzyc5PwfBs za-l2CML-{VcwI85eBFZYUG#x`@F&ZJRy8-s_Q`ucSCrXHWzk~#P>k)7>6&BozURdo zW6N`4W&XH0^Snb=2Xy8!+}r5}p-43+lrKAoa+D>Q-&v^dxjjPluU+P|5Witk;!u#F zAVc$SQ>N$y#qb-w+)Sz56;|;-A2|ybwuGwS#g_4|D||58Ygd|(E42aFzBnN_Iaw1)u^3(6$7Lo&JA%E9{(fu`$6NHg58CwP zp7q!N6rRl+2LKbJapX;u!=fUm>FA5A_)DgGo3nV*L;B*1UkTEirDP9r93u*Ku;J~O z|HEl`pMm_|Ws&I9eWscz5xA#ksopxa{RyhzVC3y>HtJvIzk_rG?tc1NWdvJxOwART zKV7phaq&*nNsVFo{XU1iEBQzH!~L#+rKTUs{kt!k&{fWM!Sz#>g@eF9=ofI+V2SvU z5togTs*c!(bbzY6Oxhuar6e8~oFJ-3g;D1a?b~&H8>1?lmz#0a*IE{w_87vM3E=5+Kz(Q{JV)ttJGpkN==iFai#TUrf zMpa3W`|b~Ug%`3)*aY824y)03z*;WGy+H7WX)gzjf3H&PPPX9eE8^H$a9yg{0j6^L zxBAs`1{HkhzGNZld#!M`icWqWHc`^a%zI;si@tuaJ=JW?S7fd?z0_|rb&YU8s_v8$NKdi2|No^5opZ2fv`)?UMh3@am|00kNa~6WP@9T7ksjmGKtDdDRl&#H(uQ zf0~c-Ot9Z=I2hK0sV6c|W-0q{Cp_!QloxPRj)yAItHs=)8dj5q>9w_h2GZHlV4Xrj z+|>rblql$Tc_9MgQFV8M<$B90F)vCQx@d>4qe2y4Nf4_avjfX>*+nFk&jv=@>OR z76Xzd_1;(8XM(t0DO(JjmfBtQhKF99%RgghMdUHc!J`rYgoa&cHFDmlzw+K%N6q)H zZa@5bj`xE~UuN9(U$jU7qwque1~6TZ5Por2%Yw*!w>7Q*cW^L`_8F8Al+yTSc#3Sp zD+F!O%ipc6v7(`-GfoH#U1_S;NFkY4eO@@ zT43x9+Tw08>8RKd&v#9in}3z=1lZAHPgA2iu4^e0s{CzwfXKbsPm1XrI;+XR=^hq& zV(r@Es3_}fDd^n_ui?eQ0`M9O@%~xtcM7~~-HE8ixA)*n2s@-s^nzB)k*Ba@Q;`d! z!JyY{;xqWQId^n`2sJfgtig-L{I zM84TaNcS3pgfV$BJTG!P`2zvNA{i`^Rq%}HMEakpa4Xyf{SHiVlKIrTc)5AnsK};x z2UGD{Tb1s}WA?(6!_<7$iOn)y6&A#OdYZpx4}S~ zs_Exn-7bBlPP1RUeiAna#(G!br%6u|MRUKx)?Ppu3x`RQ>`D(7XC@A`XfcP#5`-d- zkV!&v%%z*`cZ>uoasT_r1Cfnl$N-`U8tN&7 zp1Yd`#`SvNwsdE_JX^!GXz*2U6~^05d{5((FHY9eeT3cfVvb30iZ+q|0{V?MA62f1 zR=FpTbw^{-n_htZmfr8kyKl#tV*%^C3ia844N8N)ECiinBCg}qRL;>?cb< z9Vi*y5|&i(#WQ2f8a{_(v~SN(??`k(b&PavWJX>JmT$wM|}d?Jt|$b-nHz^;pfW78%C z*j4=2VwQ*v0-blwtWKyI(Wx2P^ol0N2Qlh)fjQSix%vT=aA`*EjQu4y!N>|xwG}aQ z9P-lRC(BDcTzmdL#e`qymt3A2N1fBjBA;P#Lx8fGl!9bgI#Hn&1!+x96^xHH_m*X$ z(+BLMhS<-kmmrzS@s z#lo|&L8bwfyEj%(*jzt<8Ez4)s(gBDYdE3zIu@l z{0X-r8w`+iS!d$p+g2L<+3q_LcU<{duM;awX)p8QE4}U}Uj_&b@96O6!&6`^8x$wT zar4w>`XkJWdW-Yo2yE1;C8qJ1=ZwQqPmCM;2BkYp+nvZmREmK&Uei@3O{_*V-{{pm z$cCW5o}~B1oKYGx{7+#Ep|i?Gm)wptk`dWRWIAI}5XHat)#^7!&c~))Xfm1X;oO@+B8Q9g3`2z7L@ zXCF1uH{g5x8_b}lS}YdqkHr^*N=O`Xt1AjuX)*u?1yll^M|}Y5X(Xy?S((W#Z#rxl zr@r*l#Sd;fhKs3Y^p$VK<~O6u3u~y3iCKo@RwBo8A9jfDw1R z@D|J`!u#7SRqvRSpD|*tii5KZ6HEWaL;iCy9LxA z5r5Z^R@gLDaIL$;vxWS;euFQEqwIe}OXT6c7@0 zv-L%6D#MD#% zIhV3m%G7woqGd2;DscD+Ie)+@FGwZgoU$7AjZw|o(m{2%cUF=zm4oXPuu>nxE`L)U0KD-@ zTwtbYHp{C*M)XtF^?O3ok#?B}qmQBVgtvB%-+=1N{mH-c;Q*|K_jwBRQMtk3&;^vQ z8Q@lUcBMY{w4%Z5g`mhs0uR8Pw*{_}YiH@xY$S>kqqsn!cOfK*Hxnu^&|qWvu$u{L zrWU*W#sc_kUlAZewsn58I76hK2YG=cNTzY4mrLL27G+sYa*qA=Yb5f;Sg~v$i=-o! zF1Xbuyu;nLdf0#Xn;cnYS+oaS1Lgn0{XoQ@hdbB_OxcF^SaXz~9|x8eer;jb;*>#Y z!Bfwu&UwLBYV})WAYJ1UUMeqYx)NDqcj$5dTsvZ_Zc?gT+g9^tR}$|kU@oUxF=f>H z;*87kY3bm@j1G`1<%8GRiclZpC`OVR%${D8tg#P7nFInX-C%xyl^QBC?{|*Xc?F=Y5`)@wnj* zp$a))!XqbTh^~ZK3tM#+H4FQ9X>$fB2T_raP}e-{DN|+Q!2!5y-pt?Q=5ZO@3%&9O z$|{$DPuKqn0OR;4(iAuls>RGa?9^tP)6d-n!$2NGyv`)Cwl5c~_^@&km;B&+qV?(v z?I7VvluLa|!eoGf_1DOBNA~m%9yZKcu{XwFe$=0v5@O~U#G#oHhgu* znW3=a^}BC1>|LlTC2gr(ap4UW`FP%m_ZF1;@G|J15s&{Ycd{NXYM?Je5_~(*5i`V{ z^357lU^#EiQ1;_e>CW0Hnc zXkwBL$qO`iD<7doa8#_haTqU+?W;uc(_H$;?CdNEU`dCYaN-ZOk8h(2wz#RL=e1~I z*_a14a`0D1N%0CxUsGZz!6?9vlLlySA)%-*xL#X4xta;Z;Hu{~87EF$$!@UdA(yp} zOE$tvZ*Tcs94xod%vZ{Ga{by}p>n*pTE$^#MS-=WWY!tvK(L_z#>L|pu3NDM~NyC|sJ7MDL3!??cpy_V}~>z2t~I zd88J>uH6FhJFvIUs)gCHmacWaiO@OOtic4eiTAVf)~GL!%8h@uf0gg(cv1~eXJD%6 zE)rp)d_wfff`U2W8K$~DUtImOfT!^%oQ%V03o+SZ7+^}SIO)ANdkqoUh%fc_DENet zx=((ofUo8x`WUyU>M8!=xcMN(XfoJSdUrHbA6rqEF`}zO`r)1L%6?T-%YhULL#lN z=|~`mr?Bd&wdhe?x~;s_HFRN$y;7v6D#|!{rCrJ5-v5icfE0IhQPO~kHiE<(#JVQ^ zqK2Pu-fozY}YODf#8pTv_uizR}+y@>J4zNg)}cg<(qgrhc?cVpkS#Mn`*ap!C# z1CHWRdmin^)wEDnrHCV;?D-cRd6_7jGtk@RT||QIebUjx$QEN6OBY>bEq1&ZfN03^ z4~gpk*0H}P_?nvaE`l`r2Ka?5oV7nuBeRv6^H1{rK&cBt$QFe2bQFuY);qc&CUrlXCsTW0A_A}aFmt)A_RW?MgRFpypyR51&oE^lN2Y~$ z?iDt>$p!QJCXF~mxN!R~Pq{%%{C7>^$rm-1;|DXzsH5!i<@AY57U!+(IRI}7GgnEb zWNkM2=eF9txwAdx^A7{z2JH?OY%nYtbig_+tGO0PYb!j_=7?-K7xm;ryV+j1>D+`! zcAEzzQ8WnlrQ&$7)+v}HQfP;Wyf2Nc>dYRt$IP72ugRxLH72Nk&oxsHZlVpF$N>X% zW-%Jfkj0>9C0+j$%oJ!Jj1lg}_gdSglNr1GW(V-;xYT=?^$2Y?up`ClXc^<*d zqVyeY#5aHsPvb~n76NVf@Eo_3KfDg?$DsGD^U=E;d~|W2vfAa)q;L#1KX5O`F7#K> zB4itDHQdU%6>AV$+*yt9L)7=UzHKfojV867K2l*WqbqiFljdIJt2#w4nWPq4N3obS zfA6l#;6ZzuvK$!T%z?3R{*C%~Gj-{hHKt}A^xKqj@e-PQxiCgJ7A%wOO#+GYLL7fl z?^(Bm^DE=v1837P{L5r1<(6OAGOl7U)a1`xvNxkMg>BjYmFhyi4`FEo2ZiF zj|z<20F%=BW-*5`86VE+Tb-7|h8^oE-Tw-8!bs-e4Lrn{m{mHB8&oFS1Ljs%{ zwOEnG7;Sku>cmAL4sSl~>nh)XjtV9{bR8(-F}h$$k2CPm(@Nt?2}wvs+L7Ci%}%Du zh4Oc+b5p3N$}oE2J)jLJAo~l6x(AsTx86#-j0KJw6pV9I!O=-#kUh-kuLWLB>4v$g zyBgFP{*`AbVhD!%fepGBsSvjgj{@EP&Bwr_ z;Qkv4nbWnDgyDWr(2AM$JJj4bNc~lsGR@jZyrDV2-&Ea$X2%Ymv3%Fb!d80wH`mizP4wK#Vs8PXIwjM>vSQuHgUjt zq0@t~aEjiI^|ALGA@U+>ZhBnH0uEyUENqg#?*vJ!(VXslB&L0ll|mxaMiq-v@0UQf zcAF?Q&4D>K**>lPu(4eRPtC3$SOTBoR+LJ+Yln%^gbn^;)?^}BB~?Fj414tsa)Zw^ zVa2R5**1+XbBCR}wAJy`eYsgDuu7HK50|MoRs_z@{QDjyZq03_iU}Kvx@fF&XXt^4 zz5M$fr9?_x-LI2l2AOk%X?B|Zaj(nQt-pM^OMA^jy@aKO(xcGnBJ{v=E7PZ6vd)cz z#g6uXZ|$=)B68jD<43+w9#iCOMSv3z#`cV~g#x2JA5 zK<9!aujWa#G}LY{!(w&tcD#_oHfKSKXIp$*j{_UxHqSOwYj%qRtrlyowq)(Zys00}cH-Y3nMzrJ*t%q+Eya_?M$IH7h>7prX-e{Ax z%)+#WEnmUzCo9!3GoQY9{Bay*&hHu5E;E<+>>A(agULrqm_Mgc{{k>0Kr;(cqD(Dl zFR{w8WSiZ{8m}TJ5E>az0Q^4YO{bfPM& zOjeS(H;DEQHrdnKPeSa|W8uGs36y~<=+sS?27}tjxF-&k?*Sfk#w}6s4V-$*y=02N5c+VkT1bQ|{}ax$A2cXBpG8mHi>8 z7b(1=tBXfW8UP&t2IFyvzffX~a}dw#oh?v0uI1=5$($a_89S`|C>6FPwbf@WiHE_o z$;!qVe5$8Tn8bl+l`23iEsZy4<{`4L*_3IVBoorbG3oDh;kkZm0+9%mLJ4>h|id6h1Ry>ZoBY{4t6+V5<-nbvs>wXAco=!r@rP z{Z9Z`nDUyhyUby{>fglC)B2RgW7s4MZf{>e9_2+XnGe~xVAjoAs@%Xzz%UYNs|u=L zry0{k(v}DD+^IOvD(}&$TOo75poJi*4Zd?s`QK2;d%7mCFqCOL zW<>FE`>pjRZg215|Lyra9;1VH%_WQLJj5@Bv{mpOhL(B(%~;(^+CF{>qA!ZfZLWGf ziEX09F(2AMX3RO?IMRxz7HvBxa(6I1@kOVkI(V+^x{OFb{;z}5T2xZg#~v9R1iNu5EQ7jYa$su`kaaNbdE%FxetLJtcsz8C7A*L^XY{xSSejvME4Io+dcTpkY=ydxe*5dp z(+AF!d12VM|6OS_i8H%vam098K`=~Rd&+3AW+dHczaS`B*7jz)RS&goDU$4BDi|%qSShliD@wk231t)5lSi5K6oLzQ zNgPlh$rEhzB)gVcQ?H)PXQEN-0vr2xCjbsf7bgLOuV=qSCL5B^nry*Z56?a`Bi7mde~n6V|2r*$@Xd@q?iY~vD`NFKx4 zE%F}T6;zX>>tWg~q3e8&DJ|}~7X||9*1k-;k_8Q;@rPT((x!LjRcut4i6hsBX^Y1X znUIYMA8N1%;Y}4p$VU%iJToMuLD2X3CW+o#O_=yMUsfJ2vfA2U=wi5w?E6yp1|`D{ zzt@X2UF@dsPm3s7y4rD=MQgwpDw`9(aE6QTBUW>!Jl+UX!dglx|14x@E0h3-!2WMd z2kGVic6+rxJK!C+MA;|r;L-p2l$l*_Ty*iUK6qaMuwb}UGoDrfQq zW`&hDHF!4x-GU7fR%n(_gMo72Mxw2GT<2YdDLrYfK-A`FgyYu8N#FBa;p_ODGt+Jn`7eVN}`yESnu zN)njeF)1y2u_>$q$g^=hOa3;ywvfGmpKkm+%5GlC!Y-6kJTEgZ)qMBTKTKm&LvrRp zZL6GGNe4eTgcu@WpP>@x;Xx?HxOXm)egm~uRQcR|fL!a3)v4;}o z;v>1VvrL?H-dj|BQ7G{MSrZl@a9hC$`xx5Dy9*ixjuNl^uM1Yf&3XL9As3>odF7=f zn+v-}q}ctXziHW7{wDjX8G>UAQ1a2hi+;!HmaA0hyCKA=D~x$C^>Cq~QsQ`gJ1U#( znAgxR#Ukl5DT#8UC}d)i;&pCJPd5?87F&MArGqXS-{zTNh5V-s730PZ<;~fc{6wph zfn_PGt_JZ7FDEPfTefQfc#Vka`?10uTo?^ByL70+GXfQhm8xB@oSQEa&p613P&uI4 z5y51qYA#qOXu{A9%sEN5FoTO$#@AlHTph;Ai!Ohyl>!iRy$>*~-<2f(NmtZQ9_TJCgz!nvx)6{Ubqe?0pu@j&tZ zS0?2uEeUaZ+lk?2X0wQ@_B!5oS0tK2f7x56j>BH;%0^yp@e}DKH5Q6G57V)Tqhl=* z2rn=V?sN{Oe4v!%S~(K0LJ4hZEYty(I(W>pP9I*y6b(p=v|5oS6rCAq2y*b}PnR?6oqP^H!a^YWMnE3|qo{i96WG`@pAw1>^-# zqGCBZcZTgM*GsYeG}jO}*Fh-OEBhZ&qtX9;Cjey}aQD1Rr4Bny^WSAg=q;5|TXZ#G za8p62?Jtq&c~pr@cr15owv5&nP7Q*!_RfvQe%r;sWchpLbId}c`Pe*(u^TPPZJqVm zjPt?sw|6CO6$hu2XSU3%ZMAY~SbEtKPE!!*tRIxfRK~HlPfbQJ`Nf|glNGs zN^q2kk?q4`)V6UgV|nczWyXG)LfJ2C&PXthRJm}g)LT$FZI(nI{lTtotUK;+J~nxb z8Usw#+Df=K9`bq#r^fm#*M}z);>KMxoRGP6$zkuIYH;iXh!O50D53+lMy0?_2Q}CH z;=-uLjgM7{Z(Qq4Z*dGCEG#;>{=6k~HoKS}ac3g6K3|1~kyCHgsi)-SE%NW? zfSU7D7yct$S4iQe!jt}Jgl&q2p)2H68a#o&>`G^^MU}H4YU-7&6Mx9xvuToH(ccm) zNAkk<=h#=3MS7KHr4N{pZ($%40jI`5l(RkvY%X<=ay0miq<%UgU1f6;5%hBppGUiZ zJpY!bz5T(5Dt{zkeU+IT2lba3Z=18ARYRH&{~U2~Q#Z2oI%}B)C$3bpnZ+U_cspbE zc2_v*E@)si2J-VHG~!kZFbg|xt)5Fd6su&$iWj6|e?HkmxJmt7H%0sWtKK8A`PgY|=}h9P?K0jHzS=w6HvCKtXv_;uR|q zaSP6*Yksry`aGLzfDKR=DyO#I?IiH@o(h6hnHySC$YYQ3EXbFLyEo{YzEHk-jPBE} zb*yFivJkx}D6z6Hc*}vc7(Zo(f({cUrMefGW0M*Uw0l+~6phPmO97e5A_b710gHAnUbQw4aL4>28JK`H*>Gu7R#9c{-`v2>$1XlFx?ungOq8D(n ztF4%yi9AtsB-+&d>WzdIZAA66n@2r$NpQdHMg{r>zTF4I^#HXm;8Hrb?jw=%qq!4Y z_h>~UY~>w+wwJgjv&6e5PVB$*!#83iql)lUBgC>d3)q_Rl<@S_%Op;twy-6}#WGQp z3@~Pnym~6LHEGUKNg-}$+wP@micB1&3ugx+X@hv|O_3HfS+R0>!kKAsM*%#Qh z4c$t&PORTwlVUVjPv<$|t4gAORobc;EMvroC;rtLqgh^B<4&~P6v`9~z0si|f7FTP zKJ^sHRp=k9W1Z7Y>LLW?VH-K)JNzj8coLa@NQDQ{MgA9J0Y**<9pv)WCIDNMjjFSW zS|783dkdhNvTZJ;mSVzX*4iMDg@c|1M%2nR&?QWj3hn+fxD|HnAlG&7A|xii~D2Kz-dTBrJvO^;GmI)BB|N`t_amgcSC` z1|u;pk(?a?H;hcdJ~7#}@rv>i3$RtFaO=qqj)EqvH4#_bDc_YR`y1CzoF}jsn>nkaGu>E+M6DR|=>*CZ-dv7yCBHT3d1wj42}<4jga^`9q71 zXR$kq!IWBT03)LaCrY#-^|2Po!Y32$$XgTM4lmF=9i{&3kYRKt3e&fmWYt75! zS0q*%@R55XmA9=m>yan8Bk2S6xLlX|Z+&^-q>)Js+;bF6|J04q3Pq z9Z!O@6spSb@A%Ol|A;(syE~u*^}{EY@TGY3XqW!s_(bpIM?QuIVp71<&#}9bOTFap z2Aki?z@qeN7qRR6&n_FzuMg_b5DGfB>7d}{DGj`ywasMAYw(rcb|6#UjPa_CSbby< zDW?XN77haWZLOEmc$@7+%Q_0Op_K|bvC8~O;$CjX7R=G*>u_>T3`n%R=a16xPARY2 zvEY^jxl#292mT>}KcLCHxPO6keH7=Z!#%S*4IDq@-UeMINxg>Oed2cc#prj){*$3| zW31mfZ`S2$`@v6zKXG1|rO+?=4?6s&&px4mkr4pp?*bBxk}W^t*!^9ykg5S46bOL3-%j3a0X6bSN(~Ph)1!>PWQ_MKhDI7hC7fh9?Up8sz zBD1gc71vW%zmi}M=xS-St+6bX?0AK3?7*U~c~fuA{qEiiu4!dx$SqI=%5S#!zDrCt zpW@2IV={Vo9=tc2op~6!BUcTVURWqhsC=NHE|X@syCR`Tc#hNmL)BNRwB=(@K&d*6 z9n=I#Fv^+Cn<&WUrIy%s#J;+$2h55g|^DXtwDFPIWLkCa7o6f;+vRL>64B{Rcy z3bPIt8)tP&y3xO5sMc?sl3TwyQOz8Z=TT3|FmcmKe+2)D!+&)K7?s*5XF#yNv?RGx zgw}wc+tTBUl{l<$yrei!PnNAXm6`&tQuhRYl^~CbrdHI3=Cn-yASO46o62Zr(9YiK zR5+(1R*eJK%&`fUEGBj1th;LVE+J$sJg-$^u9(KdO4j!8g2v0U0Q|EGN=34@S#M#P zPwOwokQjNEfrk*flm!zS{;Ug52KYTnPr`-{?+eA~bxAb@3TIuVDEF3Ge?2Gh%Q5?A zI0Rf%Ro`p8KDE75q2;YVzYq<_KSwfR@k%!2%{turyq%W_alOz{OMdhv(jMX53-xcj zZufs>wDoh_IUooMzO9Qek+CLCkG-7Dsa!iGI%N_`e$qyMEmON;3%}&wUA8g$GYj+} zeQXD*cJSVW)n?K z0%y2m%zRhM-(G1dL7K0M-(-P&DQ1Dv=COZ0AgI#{k{nE9iCe>MA+p7rxW)-D4QT8r z)VHk%+ee?L_hTyK;Wc`uWZ4_&ogw()fCn@HSv|h-W${~>c2f@lFm=crjxS%QbB%2w z+82N~7!++X5+T_|SPVSH(hw^#pE+%APjGSzH%K9NDbnl^h7PL?w;pkV&*s#M1Mn`& zo))6Y7P+|P!K3k}K0Y^zL>eH<4&PEx>k$)=-f2l8?aT;e#PdL<5#?5cG~bHA&5P7j zpr&5@&zG}erF)MJC_8kb7BIQM7H~qandu7djpBc&qtz5YU-<1U+XF(Po6w1j>GDSh zE@vx$tJ%2&N7jz&I%cMxYXWC`X7W~o3@}Bqxvu)>+H~gmY7yYny(Cs2nOIPc1E!z9 z<}tK}WDAvUbV$bYZx6o@XQbhKXFME`_FBv2qS0fAZUkHrMeypI4K0A1>GD+p0mw#LOR51i*ydvK*2teQJ5qCD60L?xz$jQq9ML95!n&|B~a%F zU+6)Ae`lcM`iwKD+bZTUAhc->?z>2|4f!;ho|Ha+c?CfpT<~#&RO;b3?R&K6RNH5F zY)&GkKc-amRsM@3Uo=1J@X`D+(eE89s4*vG$tK!;?m8d8c|AsQ+De zHyqYr!mPO^uKRSg0cN!9&Vz;YD{zZPEG;sbB>LtDrT6DPC z->wg*{SXC-+X0kEa!?f?q;ps!T$O-tbu-5D~dCgdx>#ztztG*D`3+O9mS(dlc$j)E>UfMP0jrFBU}Eg z!9zzil_dkdf-FE zC$6Qlq2Y>oU7-?y&c#s&-|Fipgf1Y#K_?USzf^NBJ z+MtC@6q;^ocpG^S6~Q4{n_N)AYKvAm?u%Llh^*CesRiDH|9B~=IcPfz66TT$3~M?qQ0o_r?^s0sd4)8n(lZdyz1_6_Cc$hR z6;5*&#F#2Kvf>pP()a738rPVv9vPXcTCuEa0`RMhLEogdU}F&MxIdlHml2kqkEo%=iN}rCh%W>W-8Ep zAXcORs zBK+3A$o(d8JAkPxyl8R{8#!Qu1^e^av1r{!f8LG2_4`F~L%CgQ&qFLFB@^3DI-+M8 zaLxl8risDDo{pc3V(&6N84bdFh~B-Yur{t}?5G67dytN>KIe5s?HIO1!N1!Oirv`D z5Jt+m7)>COyH)RIKigx3mTk$#=oFQY2?}F&{HA%w<(HDz-}U@|q?qWu*iLw3KYZkwhTnH0t6&;wN!+7W4c*$ALvxjpT zaOwK}qarr;_R0p);^%t@QCL9?$N7wx z@8|FM1n-)WTR-4ADK@gXX}6*DsX_h_r-F*J+`UUjP0?Ji$O@e(HmZdw#3>=Q@WtqO z(1G`e%beWyiZ`FV+?*rYwRH=+p2K=vJ^at#Ro)G*03Mxk`zIu-e+|c@Pt1rK#$8X_ zB(sTI0@b`YO;RqH<9_x(ctY%N_oYU?g^g#;#~Zb5&K)RR4d?5F4A;TU6%+EPqiQ{+ zq+HM-{0Sqj+7m+n|T5TXdg16xH5H# zL*~@)(ED3?jON}GD>=Gn>9U9Lr$)8Q=0=+ll~3-NujV65=XeX8;1d$Kvg`0Brr&QL zr68;zgCLV2Pd_H>xqU~lH@RMf+8&5QFwS@ck1qRa)*%wNmUjXuKUfd)%|Nj^or6$V zU#i||M^-hU7Ss6CT#ZBf7um=6%bApMm-jvi^~&xybc2bm6$6w#{4Ww&i>Cb08!xD* za;A7z4^RPWz1yd|t3jcQu`i12UduEY(_J1GbSH-io{L;cwwa{I6!~0*hH5C6j z9vD(cJ!MqTk2G&pZ#qp`X-CIpDkj#P#&1bpC>v`7cOt{uD$lW-rQYbqu5d;KgV4gP z-22cIh_Ww&l0Ib_(<*jsMT5mI+S9Nma3CCGMp|;h_pF~$=A0q$eK#yT&_d?kGwtHU zwlD<~-Mzvei-%7c9$_vFrDRR+ky9>r3`5qjs)7KWa%^6% zqG`Kmj*}Iv?p3t3q%jLlAidD@AC?2o)?e-d-daxq%2|zyg%Lh^-7h{LguUpMaxNhY zziz$lo%O!r1D^_?xkb83-LHnOaAr4)&<$R~hH>*#} z3W)L|N}TPqld7ZDZ@J7Fxtb&?7Gd&CB&ykW>%qBT zGhvg0LpQz9#aP5DAP+)?s>+3Q2sbP^f*v*=da=bc=dOIu@{c?bkyX-`jlu>s`@R;2 z8NsD%(K_J;E?Ap6ouORY^H7;cglgL4%<}vFdbS6D$r;D5Vu!2SpWo!2EmYz7-Gf+O zyc#B-yU=MJ^vZW1L35T+$Ur05MmT8xOTW`#oM9oho#mv){&NhmhDeNeysP!UL)nz? zy`=e$5fh$atlV2=Y6?*?{LQC=A32Y=?gC{o2Btneb-eGuH}`9v-s(^48e;#3W1CM2 z9gFGn_vghV+gpO`#}R|ojNF7TI$t1i$eMigR(IY+>ZklWwLZlMQhTERtWSJEW7~hT z&UBI&T{p|R7Bx8|2;8LESFwh{Q%`&4T@x)oIyT9>}o^Mc3wMJ=lY~xKuKqJmj%quNPoR=c>u_pAO$>0*Q4hKY;FWfD8KT z5(#Jhk^f}7?!Wo#XR7w~<{w&%o;*MwaYkQ9AXIa-j6;rxSBsg0rtKH-)Y+=&}b7zV!|J(aM}BJsH;0e_d2JaUkH9W`VsOuZ2$O%xAmH~0f(dK>G5DTHM17nFmB#H&<>bpR59?YRB#PBeS#bIyC1)_su zpdtjysshG9{x!UTjoqBbK$%~sA-h8gvYTVnrPAHq?L$FfI1}@5IQi_JA%;-(RtVWr zqkB;$IWj)7+g@*nr&0)Bxjce_ZfX5VE{3+pl^IupvT=wHQmQtbOQc`q>okR>R)vPD!cQSP+-8_2^bVH*exY;>*fIfqN5Ujp#NS z^-R7~!zIpL23_Z#f9f>WTyn6hi?JZD7L@--(=}bnOOSC~Y zKt*j?Qi(yBWTYPjX$1)vr;x%SqDFayDu%zm#WmQs8KLzILEu{x+gGd*){jl?z1Ye= z-qM8ihweEsXfP{S9#~s&Z(6uzZ8ysdPdtJ;IHuGIR0J%ohFq*OE$^4WIQGLUv9P~k z+8y8Lz57czMo@f#N<)?kR;8kVjO2NErg7cG66Yc;&lsHRG4 z8`Mxx`y5LsfVNeLnq^|Ev6&RuT(>~tJ}?z&P1XbpR=;On4k2%Oqky6BJO(Y*)|6w2 zX%QxCg4`xaJ`)G>tsx1v#wjXb?-qEa@ z(bu%@Gt{MI1!|G5QIYT+=5(HZK$E@UKBh|FMM5{IdMBo%=Ea8$_W7ud3Z4Ah@9LrJ z{`5RSX+?T-si6v|m*MLcs}a??7CIkRK@`*xz?x48nElFG7AiAE?r-QPy5ouiu^N|R zV=YEg8MPY(!6QGYOnbj1=F6yl@*T|Xz~oAOX)V6UeWk(V6W85tpBWs_1buN*vRJ*~ z1^{0yU)5EcK8#KNCqczLZI~Q5;zmnNBH3Kcpup3MLkGfG!q*Op)ARU@ox9I z^7(Y@+Fz}>@<)7tzYbZJLC^5YW_Wm$joAP)cANmeTlJaRdj*vDpuq`27O$`yV+Lj&PR1~^%o;gj+ z|H>CBlnD)<;{(>f^JfCA@q6wJ_t)}CxsQb**y}Ler@pzVF4Rqfe{QetMaA&Gs?!Jj z`=wYPNHm3v=yLA|v6P)UsC_u2MQzpszX@6mdo|d{x`h9HD+c>{``2bmRs2^_T#>9P z*1L=0W!q*EsS7iR%LhFf3zhTN$EkqH#s}z<)SjYYNAUBDI;JFN@~O~AKMK0E)m9)2 z#UGt%@j7;V(4o=gPhDI!uZsTuQkjX#gRjmqUKxXemNzsDPbte-HLD@Ll$S!b!~79>H8bP+w=x1u-f1bsCV{Hy zcaiWmp?#Q57a)|~ks(NC*wjqblYdHt4-1!=0reO8$T^WN7TR>GMEQZoe)-x<1f}K0 zW+_xMZ;zekbP^pplY~&jm=GqKYqE*ABLDJ~=7oJJQ10R0Qh?)(mXsv_=(s6Z^h5Lt zL3iaOpzTNc0m*J8GPPCA6UN8(siddOzlYw5B@*45}xBhp?LuhG?TAV<2N8Hu~AUA$%v@DSlod@ZgwLV zM@O?t4_33J%<11d8Nx+uw5@3jc})xH!j0}_T7^UQ;h9e!I{K#)nvku1vEJS$+6k*d z_Mk$5(`<>l|8h>`rSYU@buE1|$p?h_q2eIxEs+xR_WMw_5P%*`_s!wd&%Qsm`cBpY zi@N8E4ovGwcDN_&%5t+yI}i;)1rcA0Z#IenOSW1IF`X`GzLwou-=j&XG1fjjrlpo^ z|8kmZEfNyg)i+1?Z7B10gFNNx1`pkZa{8Z^g3;7uJQ>Gicr;{Vu@~)cF!j1?rH@@$ zj$=2_NftC^`qBxu4J}RhrXzwL01Cp4*f#=$d8om6@|4XC9pdrZSurR-+m>6PU1>a1M$Yyih$knMu5j}}k zJKaR~4@L#nd5?#Ba06K8z7t&b3Cx$U*0EEzSwwT%lC2!~t0AY4VogHvFK`EQAus9- zLS!A``>8t4Mv&8uu1zm7kaZU&vZwcnRE$x3U{8T~ z$|&}C+n;rk42&blAb~5~OW05>AxU=YS32K8L+V)JzsoIm5&;%91Xzw6p4h zD1$hjbeBY6Pw8v^E*z^L(;t9c{Cit*oz)sLiKJGy@4YRq3AFzjR7-j?Z-b@yD0L!) zxMo~`NX3sML(_PHH@-Xyv>0s&&YJD9gd40HpB8XCD0FPCs0V8BT`ATeatj5x4y6n@ z*eP_%W@-ZFP8-lHWgZOg_}1rwhq!0W=}4|H^WQNYfQeBz(~$$X#D@%mr=xQJh)k1- z>TY!dd?zNKDKIJCHxxi5tEAq(63@Rw-ps|pusODuGLe zjN?BwPf=I`X&$%hgucgq8HH$qpE)vuN0vdGTIY0su3}7cTiwhd_`9pJYhE^LOH%%- z=nxvglzT%XUN26vgO>GebW9WxS|4@ykF0xa z7;U@tKW6uYKb`Z=ISpb6_08QZD9h+2);Big@CrA=Ya?aq_%M{P@S|fF_-IMcefQeP z7=-X9rKG^f%0YKf?(?niT688+PP^i&Q~mqS{OKwmF{sd)4LU5t;F_4W$uGOT=Q4>Z=CvMXF~SBFqaXA{hCE~#wa7IT~C2{E1DsZp-qJ% zgyc3m%X z-}6ImO>|e8*W-mvkO9je6TSW2EUP9Gihu7b?6;MDf#`-Cio=P#LdT5inJd!jA$jC+ zbZD~9qEEaACimP=`N$zva!uYK0bv^M4eLaya zVZHT^X`ty0rFLXwZUGJoi$}eSq29&~FJ(YIm2AU-$c($fuLwJn67_VYn&AZ%YlJPH z<^Wfj9N5$Ns_^{yW+S*zd+~)!P)X$@xf{FeH*}EIhRCN1_X4{MsBMNrt_~f2{6X_S z4efphGhv`Rb|!Snjo$iA*dRs~^t4|e(>o!x?9i&wCUfLv>GuS~&bDVw7znN783e8b zxPA13GNwIA(df1^;8yeA_TWy#?M?P4{CssSZI@3h5@~TXJEEyhO+GM)@teNz>cP-h zS{fxvry+#7?|;W`SEM;$nsitl!*(}yD6x>C)z^TTCVT`0V?3>coRRe*^Re7X03^+5 zNN5o1W*%DY1nkoeD!|_Abv!-U*41l2CblIEC{&^>CZTnX^Rb(6aQK)MNOVN zIBvt7#_bK4gTCGS<9p8BCH|KFdJL=p@3f2)yXg7Qy1EJJZmQaz$S%5qsW(dU<)8uj z67TFVV}yR9B^AcA+i&hV>e;j(mhutUJrtSL7wDef&n0n_5y4;oK?5ALywiV2Gf*|k zQbExZ)fL8&O0$oUlJDkW$6YFkNAid?Y^@hETHp@8XrfTNbK2VT(BU5~igpx%=xBYqP4t2sH4Zj3(6N6r;$hG4|sd>q7R`&}s-{*mgrhCqVpz4n@+zhoyX{JEnw zs{JRVHAm{nu&lu-SQMGSoH%kUO{Ht8zV(Y)b^%s`h=g9k9Zxi#6lF2CYB?Q50{L<2BHLqlSv2H+kas`raDD58Y#cSzTNsjY*!#FF8A z66hx&!o`^5Y$mTSb4y(KGHKB?ASNzJnOzH2T|=v~TYqh#umgV#jj7d5CGZDJG3#$f zc|l9ZvC6Zfx#0x~jkb_OK4{rE4QdW33}d^Er3EhjiQ4Tl{%}*sxiBn~#YVuOX z2$DyfYg5K`8U2E0uJf}ziciQ$CHnxL888>eYrF7$N-YpNzmvn z_zP=y^efvBa`NH`r8)Sp*`MbK)E~U{!nPeXw6h@G?tJcSI|XR=n?TqbAHLF&^_;^s ztb;HIA%aYgAhUPvhjsho3Qcv}n|sXYU)!!vty30n#>LL#PF5k;g60%J>ZN_&XJ_(> zC``2lC*S(0AxWj#OgS0h_nM_PHsDq0NS3MuLbqGWjz^s&3u}i6&Bji zf;y+al{4R>F#&%ke38+_s-!Om*_YfP_GFIa0_LAxYS$so8x!&d6N>`%q7!ZEUkO$! z;|A=`Kv1U+2q=8xg>q*B$_e57lkL=BRz{Ga{WXY3$XIi2FqKSQdLiB=t+QC$^U}k5 z(0+7MHx0RUtM2`_c=peZUu>S6fG$JqsN=_Vmkd%&w24F(La{8h!)YIepX;X2j&~a) zio3O@79_1VG-6AS@8%lLBGyT*$~ubdl&vOtW(3^&A*_|nv--=%1~W@Tp_7i8#L>T$PFQ7ywO<%CNI z9=Mg{aU~gr!{m=|E>SuY`tcyHAK|W?O7xJ5lvdb(?!&!B2Nq%MGYi~&R+(h9EJEQZ z+B%Jpf127}IDwVmF~uut;i;8z~et&B%DB<`Ek` z4n7?LL{iuct_j>Jqe2$vOe4y90US#!>LDKb-Z6Mn{Eq1${v`l4(UW{STSlSMG^9G@ z0GYO@F$lVI2Xn{A-?5TPDVpH%I1tC~!m0E=sNDNKIAGr~5;eCoeGycn*HOQRym}dl z^SeVb{r4yBLD9Pex+PUr3kRQ7^Maw~a&9z1oLW~Afn9umx)k|h|0|{Sp03f;pU#ys z#ju7__>Sov-w^!_(L}STN*h(w{Z5&G8j&g*1BMcjf`&rC4hO>9)}7&DbX~p3i*NmI zOSn`JPcWylE3~JN^xYG*SW+Pv_|AoLAcnx4kr9=K@fjWi280$PG>!IeT!H9@v&=`Fx!yj=QAkF zX`maNVe7Rzf%5scj9V?GG=N^c60)m`_7E)9YZTg|@7BjW4ITfKoL7^0vbqaMjXnD%1Daw~|Btz;f&Rms)UxmOvs~*@;deC%!0j>#R2FS$9{G29jK*h##=Ujn(FK zi{M#;2G;3p^!RzRV3W{*!E;S)$Pi8BAuTeAqRJWRd6avuIu}0xo8SHU@%f@XYD!2W z;t~#vXYW)E1;0PH&drh{2B^{AQWpd{7g)no*f@U8qj&Y|?kCrWh9#wDCeXiXi+exF z{%?I6XuEVTMU(%5itCu+ymwFW31cs!8!xc5<*H!nH=@nBp0v$$v_WkWA{a-MuW6*= z#`u~6n*+Q5eZx*g>lZ=H`oyI?Tk5MtS9e zpzsWx?2wm3spB?das^=&px!qFFol}-;8TT&_Ot&}=~%`jUSkxu9X(?%=jjRRT@4lY z!ou+oV^T^X@fK7?e7iao+~cP23E~2~zIa{7$-+=nP!v%V+VsEK&)q17jA^Bl4QxU98RsUf z(c=+OkySb;6`fgkttCP+h^Vp&$HG_DVobtlp^{g)iIH8E@e;Ymgw(b0#E6&M>Oq41 zP#v-#D5k1IjBQ?Ll**_wQo%Pv-a}@#hCy!*I_zUJK>ek1%cFowZfthMaMw~TiY4?aQK9Wq?JapcFu`!>-6(r!aMwh)w}YWGCc(; zCiL4%sne>VX-B^4e4Ir7`_io1StWed)33-t9}x;@Z(+S>EYkkPF5burTGUpg#NClf zQnTRhk5|!g?nP9MYSOu`%VO4Rx0REg;f3+T@5Yz)Oe<`{m0b9bvjr#(R~()+H7h6w zguvm?aI;2_vD?LLK23cCY-D;O))usC&xY8@xggR}#gZaE;cF(01xml#$vMP{`YzRY= z6oQYk@lt%FDIdPcqdh}wekZas6a_>r7!C$W?o&_u@%_B0#;jvMu|nJ(SWSMe zS}tv|usht%Hl_sUmX^t&b^dHT)#Ip+yD+x>Gea_HJc(n4#GXFLce(EAUodo>RRgY| zVm-MB9yG$LDZ+1(DB}h3`m~tPpO~Nw+oBc_jZN9jZ@~DGuXm5FFFBGHmUxVPJD3qr zrkU#wS}tZ-%CQ^(T%ur@>HSwXcJ>G_n)R*$PImsPF1+z=;!K%zg%RwVS0EdpMf=eQV)hth!$@+K)e4Chn?>I=i*0#C4*b%ClKu8vbup~ruy{Pymx- zV3-P~3_L*&%2@rj_)PO0<^mO#VxWAcfgze_07bk1?>00fSXzhAp-q`x(to|I=?jW{ ztVs5E%Sy@;#FqGQCxO0FyjYS7zpUH?wJ}*=9_A)qei{k9&i0a(a>`^X`X&GJ$3IqR z@{>h$J%PDm)#AA*6y$;pZ`0hL-n?p2-Objc6Smkjm{%<$TU@^oBQA%!2a_1Jjpt?8A~Qb0%ez70 zWU(@ytWrjGaybN6&3YyhQS$*Chj+5$yp-^VI;n@O94b75a!c7NmpqF%mqjY80la8v z41{nJufJA_@{Y&$$J6HO=ebG`2B8Ix(iz8rd1>5Apa}n-lC5yJgJ% z+Wu&qO7z{w7vwX6=k~7IdrpTqXd%BOv{+BRsB{u!V%6+oOZ!G5OEo&l&B;@jj^1k~ zCq7B_*|2Is6yu!03J&F<2|X>@(E^J}m?O8R6w>JCTz=|lk`14x#T^NHTz5=m?E&Z` zSm)_`nEkE3S}Z?+%>I>Qj!%l*GlCzbV@;syW8*XvNKl{}?1{<%tQ2sik?Q#;{l}%h zC-2S)n2AJmFV$K!#DEQE1B_sT-Qv;phjVqR<_NSDH&8%gTZ<+6&gxGsYm@=0go)N{ zl9C5D9D3qHTjSZEjqmt-8K15`j&67bk1Op@>mL$i({}V>T2c-RUkjG~(PVAAIpL2WcHjG|D?**7RpEWbx6ATiX9r6(VE;_8=W<4brXxblgr%4WTA8=b2PkB zncY1=s0Beupk@=J#K=U)@HF9T-s%MJ_{1DDR0Hrnbq2N(XR)<&( zlt>QqST!J6hXFT4Jop0+Ozbrc@(F|mXu-R6?H2OdH$)FYyBsxE_(cIw{hn&` z=3OuuCt)mSzW{0jy%P~E_7gEaN{h$n1nlhr&sP*7K9(3 zCncz6#uk|){P|!SKRv1I%Ftj&Uzj^0U?LCc`vKTe5jrMreF8hmJ6NF_Yqdk@al7EC zdmKc9>Pf+0Z?@OY9ASzXa+4#hQaK`OhN-7jeTYB-klDpH^e9KAm^pOs(Gv~_)El(> z_OV^P>a0T&FdVhnBh`>$Au$lZ_YX{8Ct~C^*#)-vRVZh9!d@YEAH#iih5goEI z^JpnRRF_v^>Se^pejuKm9drHN_wt|U{CRi;i*HbF5LL}Ad9<|>=G7VvB99Zi;GrJF z-^iQ$#v^R>5m5gy!m+}&l(P0}QCiX(V5_!7`QYyqM@(E@8?pO6wmy1pnqGv46A z<*In%g0i#*Va()uaj&6~3f#b$oY*tAB~)6?RBo#fhlRR@>_{cVNFUfO1f(Jj@WFad zUS$nW1vMK(@9DLI<0GX|Fjj9;HnPdYsde54zx{FgZnPbEs$+crMs&7frc!(!C%!-$ z7xEl;Ap(0ZNik7Bzx?7!vBv&OBrWwmAEaaR3`%DP4DaB0`8{EN zf&p>0gF;|b^B*%Q?{pfHBZmb6MnG2WzaW%CN>U~a6@;SIS`CG|%AsyfoIrw;QJ$59 z?cStb^JB1Z<8kdTjicC;b;0Po%K>;Wy^>n*tQo&FZNUY*(jtqco4^>$eYc@r52gbG zxMmOhhj5x=+6D)sQ_j`GRmMtMROO#rIQUwiii75WwtuA?TE_$d#SX>NaHYvgSdU1X zbreA1eWROrG9tLj;@;I#l^H#tvVS#HhN^y)VKU;+x#mR~yfc&GjldnX5^(=$p1Vn` zbsY_2Ese>lfb?Z()pWkb_eLG|Jk-r&SoUZDOiMr>_uVlFOLo6|I)l`Q!sT>v{U&#(C?GHTVw%-nAmnbg!omzMu|Ow_@%>@(#V`4aI)*5U z@=V*as!e?P76o3bPu|Uq=zZ>IJeymOih)cD796ULkAy#G;Lo)>B)oHOOAUfXF%-3^ zRyw9KyLN2z#h5?%nZw`DSZ(p0F^ve3!w%LkM`w94CCM)(r}k|r;9Md}y=*qS z-v**pG8IacF$L7J>_9WJroN1|fA;b~QsfV^2YM7r-vLp4iZF8PyYPy(vuK#kC~Oqo zaW^so>RZ9DZ_xs2JP-VUTw~uk!THjFhqOah-}&&lpYM*-C!>o2{tXLqXrQaf5uxuaXj`|#Buq%VNFXH z{RxFYIimML6Zt?(RFhfK0NqXUpDC8oK^`xnOqer>)o~Xq6Bv;;zlY)NT2eSIv{6+E z5c%D^Qe<21-tx(kCC7=PZtvjDjqV#*;-L9#7m4g^zC=FgUn6Xv7_B zgnVrDwaZQ*2*m%KM5RjZ|C?aT+#eW;7D-?DO|PR_m;qM zo{s9;^+l6M#8S`oslf7uR#Ux0HIp4aG95LX+L<^OubFd9BmtJ_;IkbFyb1jcWcCmQ6 z87VEJV={AwRd!CI@JSl8hn)6w=jr>6b)VImxEN_h;Mx#5pBgb+BHGbf9OrI|@5LsU; zrD?a758;q64Iyr}dZ%j)?dqLEIonNzp=7_Kw!1A==K z_No8xAK&0tYv1rBC*~cu^<5M&#J6DEbyp0g^*o&OB7p<`eWjH7r*bqG5_<)R{4*K- zM4O8vh2`3a3r<%2Jum);C)4674XWKbw1>}nB^*1_!-?!QW`b9fWhV_LI8n|V@Y|7? z>TiZmduOr>J{^HMa2cZdKvNz+#Z&ywuPUj0Wr8&XsNq=IGBbr+&vjAc3T2S}J3HU3 z1xp0*4Z6{j3s22klt!b`kLyqv`+uLbuDB(iy@uI8V7I#i%OIg4mie*%*428aG`^R6 zaO}qMI30ZwXf5+R_!h)$&$h?%y)xbaHb4X9XT@1pp|o7v~hN)x~Kv8t6-yYKv8L(uk#?-ONjmk`vJ{{7BPg_7`mpR#PqH{P6&U3X7gU&eFJh_fPKMgO#L$0D! zC{OGTddb)nec2HJ}tP z9qvpQ88R3}=J7bW8n28Xr48@-5^iqR6|!!0#mcq<=?sRcjYyIn0lam5!Tu)C9Ip~X z!Tp&%zBod6`}&OK0@Lf~D93>x{*c%tHw?aMqsNYTzl!tC>afYRjle>kw+WWqhGiSX z5dI%cq-LqzC`x+=s<=naVA{n90G#d@my_53|HSb}jX|6}A~yPV8tl#`NM0xX521yP zaSm;7x><0F6rIH58wNRaA2<9PhTCo6#K{lIC}7?5eVnddzS#WDd+IuxN!ebGx-@ML9d%*Suo{@ z^2m1HMC(%|7?e{#DN!H27?|UVb@uKA9*MAn#U5(I=a{OoN~{@0Z&bq?xCg%o@%r*w zS>hg!J)<*~9!#KDb?#;IZ*@~G-djMmrI3)fO7m!E)~veLlQ~@xs%HX^Os{oUgCSM4 zdQ@*+#SU53OzQu}6AU;m#6vUhWrJ)?jUpSvy)Z@De~8~iehJLh-<;h!((3B*5_7b8 z;S!>VvvUd;ZqbQ@5#d5c2ulB1p(`=9+F%3b?S?>UjR?Bv>eIq69;;sOv6f=3$s~oN z*@XpN5%XjLcM2Hu+pVhw%}d;;WJ84PTrK6O7gn_36XIOv{oQ20{yI z{;FhSEs84aw+Hm)WOIum(s+}oi6=)i9H~c0Qi!JsE(&1m(dDj2si?Z{oE4ctpljx7 zxF_rxtQA>k#VzXDll%z5O#2=RxkiusK2|31Eff5N~2 zM2=$PBZlBzT(;>PHD$Jh{KFNl$wT~wc#X3;cDFHpxwHg~mu}M#<_I4Kk9$O7JRwRU z#D(LQQAzb%-)UT-RJ$ORWMVN#ez43*16Bf}97Xsp#8|Bz5>is0&@m${f5#m!Y*Z1> zRlT2J#jEpH2=cfZ#0UnQoSkZctwq3_dR$SVQ(>EKNH;Vi_iHR?1rJx~X6>75o=Y>u zFP0$b*gq9qPkvt&z~(&+V*+i7$jrq2+qIr=GD7FsD%2aijHetD>mDH)MS+P){<{)^ z^l?+P#3lz4VoCNox5PQRDDxPGi6+$jB%35l^_aiEvUn{@#Y2bNLW;zjU&3V(i9IO) z-Kvd>bX8j-wE$5n@RJ6m6p%D{Hl=OJt84IM+kNv8HYwex8e`$4hgMf(4KFVU#c|3U zoXPwSu9#4-sCGYr(p#@rPhOc|JziF>S}ktPk!TGV@tk0m>zJzLObnFnYk zj+SQyet)Wj&fsg6JY5a34?cIm^<*`z#y+!EmeWLcpTPMiPm#UIZuDRsBFXX|H*~=X z9_>feIT_0_RMO)Day^T?T7Ec`{=I!Ju(5-`KI%AM3U7S=U{C26n=a&COm+UW@95uX&# zx6A29jfTLNf#2Wv71eIDlts|FP0~72&9nt>xT9d)9`LczCVzl2(ZH+lB%QJ?cZd6v+c{noj>u3!-8%41e+25DL@QW;;yZxeOUq{of8Y3T?v|1-_8vhe^+fZ* zoyxR=FFpGUr@%KV`OxdLF+E8Y%H6#TsGmY!g$deD4UGE}(V9`P>czt8Y9a~lJ=juJ z;xurqz)A)sfIYh8N8D-MH59Xpi+0#c`0r593Q|MTOu%!7!m&g0{Et86$uq3()N{Fpas9S79l?IzBjl?g1RS&AWZt7<&q z{U~#CuH;<|9#OI@BJm;@zbOV4(Nkh6PuKXf;2P?WYh-2qV&F5`&1x3g?a%mk?|AJ-_dMnY#u>8w@@7Z0quChYksQPN3V;GhFEaSdH6_- zY60yGAEWGjhTx=xSe^qap}G$d0D)6*S$6kBzP|5=h!!P^aR6*{>zucv_t3f5?E`fFJ*fg9Le{~%=E5IPkrSL+eQ@i z3{g{@I>r7}55es7O6^X1-^LtCPC^l7YnL@%55A9)E*jn|=eZ3p_35mM{?Y_ph}Qi{ zo-Pf3u8;{PZGFiP%jmPy^8eEwIUEI&D@bi4!HvW(#kR5-3`W(b@+w>9<;0JB?@v0g zruefEiDH$$7FG@u9Cl)Sry}%rvCxB~PK$X-?n!hfrB-u z4qmHk4_WMHllbb#NZdu>CIC5SHXo(w&6n|rfg3Y!b>jZT@mX5NQK>o^C8j0$69Jx9Q+%9r8-791wt-@kq_ z;LYN?4^Wf;RNt~P@+aRbCTR;=QQbKyIV(|YG?$PL&rj5dJgnC2CjM-}6()H79LolE zD5u2L(HDE3pR&g8@RN7AM@)T9V7K0q))bkSVP4+R=DG3^3tCmi?|+x1EkLK%6{SUg z96^It7eKhdR&SMwBOamAZYF4l(mkZ?&cnQl-l$y37we0Fr}JIo0XGJq0M>u` z*zeL80Ve?n6M@+#ctz*P)OTb0xY5+ zHdj54d|Q@U%%hkf4aBpfU_rR|tRU}JZq1tc?LY;gV@t$15egCCx!w6x>;m7aN87hQ z&`tN-d@)U+&8)?iQ$CElRlq7FECIHQqC7d-46a6YT2v*kC{SEWh8e{fhWs2Qbcqk) zEBC{YzvhU~lyXtq-DXx_z0(n$Du<|HoV>-cazP_Kaz5|im=SO`{B}*tKjmOH2f{yf zK|I{tViasaRl(f$T^C>LvGEg?55?M#TQ@wrB4a&!&R4CL@yVs$lOR@%j?;y^L3q1r zovM&Ns<3qR?qQHVWqv;&bg3(oQjO&w*6w)5!f!RQuvT+0( zTk-7K;ri-#QPB*tO32=Oa%N^mj0#o*zI(0F)7Kt&(OAy?`@z5Jge45GI$4=R2ZkULi$j2&8?xH_LZ)*tRY;I|`BA zm@!yrnANvKzWO#J$b1>&ZuGECe^0j9CpMd1LabZ=HI9Gi!8T(^6v46Wn#VO=-4J5dnk$9WlwnNZdb3t=V_|q=b>fms@<@*@Z z;p`zjFf-A&lgORO($nsi?-i@nKQlqeO}R1j@mYZU(uaknhO<_{xSNHS^0YCAA?${9 zR0&^cXB_*fzyc)`KIoc)6lCUGAKe$`L+z^nckT;)QnPaFV;;tlOvHA^t*%GYKdGY- zosVD+0ya5e)~E1`gB1(Vb%1akqZe~IbNTYu3xn#{;+#i8c2I%GWC`a#NxsH$VEdEk za>=s6)&J$34`P@~Oo<5ljjaJ|fO@2jpPBokiQtXOMJ=F~Q2G-D3mOLua`mHfqZ@e9 z0&8sDBm8!;F*oKdN=8O(2Kx#f!-S^rToBuO)}otu%z+~okY7m`YzpaHrmPn}-R8BN z0}t@cmUNK0M|pFuTB4J`exqH_)O^4C%UyVBiG}dUIyE~+RxZ)BYECv{T`2V3yI1XI zW-9nLzSN;dD&s1Wj-q zRoI5OE>}maHjo8!PuPn7BI$98aT2{InCC>`Vyn zLuu5h-+@km9j*Q&gyM}pehG9a@GPytY5q~Uebhh0ZjTb(eYZw3t&-I-<|nrFELD&`MGG;8mg1 z6@?j750BO!_yi7w=Q8-R#8z5yrs&7#23h-Y?zj)AQ&9nwyF?WnaqH-UIdiY>RqM!j zH3~{>tXW8{+)KrcZ`X;AG?U*|{anD3cYp|Phw)h|HxwAJc!m4-%I_U>NQsvf+#SOi z>MEh84&#c_;gB7&z;h=%Ze%ob_0sB|__Z)M{g`jA9}ZL)F%gDA9^>lFvc$Ie%Dgfw zdS3iAvSovth(9ouP&ZrlR+xXpkaN$Ch35v5cF5Mo1o@JmUcNo&I2PW(Q=#4)73ABH zf%DT68<6>g6yD;$8K2{tKD5rnxA4#$;7DQ(Mvxw0 zJ2(?o6Qmg#3Y^wU3pjY@QUC{(0`cd%oFczigEC}p%1N}hyGpk8D)*R_6M^%Gr>gbzgqa0S zblRg)q)GT@*H_5cU!&irH@<%f$~%+~ZT>WXM>FJMBKc%`$${)?`bH!1uyjcGLUs&W zAQeQeXB-!)AXo7M{#bKF`!QAZ8!cu4$ce5(&MRN}1L?QT(aiJBR0)E`lMB`*{-(;Q zI~2uatm!c9qjc%AY?84CF!vS`m@ykVGky${&3SC6zymHDXtlpL>^-EoJX?aK)L6dJ z;nw)b2L$oZ1C)N95gXSXe~^`&g(cjy=(MnAb#qn_4!63LTdSyZJfrxfjnQ3 z%kk=PYez#q=;K4LML1DUlMZmuurrQ+{Q0x!S3??s`(9I=qgqq08dB_hW}W#yc6b&y zQq*A2%tp;mj1Xpy{A8Lo;)+MXM>EzDE*|llWz>zLsa1mcW-u?C?W~(=x8;!VVKRy0 zQNVsW9VPW@vAGvy_IFd=%fZO8%z~fE{eOI^Q_VpZ|nz*_m33={d*Uj>(jz z%bB%h>Q2}>NAJ6zSp=RB-7unwV_7!3$R2e>jUSibD{VjszG(Ne&IpV68_kHJm?(2G z-Y!!N3c8tVN*jL{CAg2prvi~|351qVh>zhK+$*v>P(ruT*9-ECo@ap0_cb4q!t^VT z%pRaZ*KA?=+!AfLqxv<;@bpYtk*_y~yKCIpb$v@r#ufZ3jjh76cNHmRw#!jz%=y%8 zK4DB6U9z4=)hN@RqfbH|4>^g|(oth~x_fu}3Doh}i;xs){LME&S6U%ff+QAQMAu1| z=hcVeJ2?R}GXYmBu(Ib8Wezq)>QRVVCn>KPgBM*smP^~_Tb59V>!WL&I+v7MPL6)* zc0)t07;Pvv(yGZO9dm;_>j`Mjv(KhRe`%?8%kSne?P$6+)$-!xThw^*nwR5a!my*n z0z*P4EIf5JPAB}eU~6UHzFc(r=0^EPfKdI0YkR2s-RoGwC7~astf+mC z&1`Yxug zmdsY8y`k2yt&sc`FSrRsa{eH_Cu5?Amfy4#ObbnUI_qXm`}jEbigrd-dOm3rwGVu;(9la$*_C5t%!M z)goG3>8F|}G;8v!B^xCL44jSKIbDb!H+-(Y#G7KiFi%i(Pi!x7?0KDFmtHbEf)3{l zZ@eO}B4Lv*ICruice>tL;pg5!DedY`3kky4`m7@Lh&wPDg0s5n1Fa??26T-nmhYl7 zukk3zcPCdd4c|w0EsxlhI6Wce`-Cb}5b7$wn8xGm)8xxaa|P+h|DZTxA{yY|e2O+} zpa0v(9Ixui5PyLfr(aJr!q9T`VpbmKp(X+>wToj}0X)8LAQ3>)cCqye2t#hX!Dh{% zNW0LpVtaFaXOKcy%6I#?$E|K35$}%BP8mH3gpkZ3q9zZ#eAgQ4CU+GctcP+>J~@NU zg0Nr1ZI4=qDTo%>8xcdFeG%gIH0h#oYBtyded(3eaI7361p76r0@OZfpx?w8@}XTY zItCZ_^VEKHF)9v-!*g!^G^tvrfo4_r7!pco#k2s~2AZ&WXmFVgRi5BRG0sOGw+=cF z_5vihx-;}Rz$#!$1rRD?wxI4-{LM_zprow{Xp~+*sSdHrO41g-_Z*9R8}YOh_;qUF z!@)K87J1>Jo=I}KZZ|%@>>%>n{M9z+-_!ddzL0a%2ueNhn7s(m*YPt0{c*3(XMuE^ zNo3u4DwSKe>y(i_*wU~}oF6~o=O252k0Y4W$wauY|FmaibA~y2iOc$@%mMbQXb82o zW)mwui!>;vG}j3EGaCRWF6&)kp_=^1WIEk*bsG+{Il3Sg4nibcL}CbqHWgrF$cVkw zwQmBqPKO1%#WncZ`&8c=w`E6WXw#Nd-uQjgfe3FwtufG1M2v~PI9K_`_GoEMM*!7& zPMVUL2eOXI9}@MvUpxJMUaNB2 zviDvg)eAvwURap5DxIwkJrez|am4rdw|{M6A5SkBw&z>UM&2Q|n3=xBzIKt&5~Y$E z;nfmHsIxsyuz8uQ6XlFa{-~hj=151L6a9tlr|4Nv{?lQg*sgs`o&I5z4r2+Bt87I67P4&F!P;Bqo(SOX z8ZRe_UEOYnMaV0)Sd2`V_{Yc66n_aVYo?fP^<0kZ)F0ck$p~2tHtSPF#dz%+V8}t4 zVxlbV_u^e4rEXMYB&36z`WYMk#m<=irZ9)A^3QSZx}UInX0*RGJaI^$l$6U#WcjF( zu^|h(9I*dvbR>{;+RY}Yb@=MMI4h(O}`f66brz`oQRDhN8pz z*_hmTh-N1unQYTb;Ud$vI1Sm&ZGRHp zt&h>_)l*pE!>z{ll@ zrqmZOivNiP$vO9qw5vZqyKq#;Rf20dcO=N%wJkkH8L>k6Dgm+H^5+(VuF;Y>iF%eg zo?r4J#gb&KYA#}kJXDw8NZf3jXmYEFTStvx2d-U*4ftHj~ z>vMV+T?9Y;N{ABm_wrIqi_|E%Wy-z+eP&he@X(3f@;H0tv>THt`j)?v9S$w51gJ73 zqNkE;?%4y^5|43DUsz4TBC~Aql8c<8{>nakn?pIZ5iiEYgR)05T2@gI7^g~TyOMT# zBY4=$Hpa+MiG==0{-`g+@Nj1*@|v5yFGf~%eICVft9|P{M~g{OeKCW`5Z%uzcDN0Zx=m44;FIU`l!2I1<{VD{Su3cg=qsK(Rfg@)&r+&4Mt2(2< z;}pmj<~3S+;EY^ZG;z`&N*5%ca_i=%(6Zxa>OA~_0D_~!TEo#V!myZ`4q_o>`$2f? z(pWN+2A>@Y^~V>k_QvS6C>>>^ay_=fTSNS7u9_}kix^Q_sJES#pA3$Kcp~gWQa&Y4 zjydKl8I-mKYlSQb-pP${z>ma;M`;$nfr&@J$eiNVnfMov2d?%2&EGKXfpxF#X9c$o zNb*itSJu8&kQvA=yHmTrr^*kB_k#s;+NmE>rtXZ}y##~s zy)-P7iXZbbgZzZv2xo=z{4o(caL^c##H-eAv^t`GW;v6cPBqOo>-ZEdDYkO~!QYntt1yADhAMb{QSV<0x#u}~cZU?HT!h%jgb_{__uo``*G!z)Ah z^TT>M4N}u$`%vSt%lwI=ZZdNGnuNnU)4|%Q56w2no8Ln>g*nCi$~nA=bab`ktak4N znk}^**_6ckgBxG4-t7}VKF!Ix6`NVM=S62H!Pme9CB63;V=;>t8W)-_(sJq0HV-CA zdHXENtYFJ&MLWKC--OMRncd5xX>NcI!FLo)re4&Vas$1LM`>-+mII+osUTw{cTS9s zE||vb@yuSM(>t4Eun|SyTi=DC-yry@3G#-VnR~O-Ln8Xmw~B7I3;F$G#@x3k`gSb7 zQy@l{$uOKn9zG~RJaK00D=-i;vl%cgPKsuN^h4H*VB-aL5kyB>0oq$*6dl8kH1joYwGGVlxZ*gnI zj;SW)hIRTRl-1-HAxwBon6;kF=XK)5;oTF$$^p6gNF`!G$$g->|0IO9Wk}atw>Xnw zjt>z(@U|Yr_b2&2Bj+?sEtSh_PpDj(R`+w+_ve-qaWQ@QmMi@4w3zmI=aaV@9x!mc z=?tUh7c+mID8`YxPu3HAVQ6Uj$MM*V)V{b|5iK5`cU(iqbfdWa0_l%~irBnNz{@35 zlU|oES6VlnT|*tPLh+e#|D6*8(?a4AOB5ZMD4pT>>V&y)8vXU z!8J%SSFSD5^FkC?Xo3#?(u#22=(4&^NI?hS+%eKr!=vBJ&aS8W>)WqA9M%ojd;ric zK=6$3HR2-o>Cr)H^9JrrBxaP_*JLhH5>dVR&mJd#~H9uTl4*bhA31z|e^ zzM{a8n3m12KOYb1(YS|r)%{)jLkrhI6f4rVnQ!*vkOw6b>d{yPNjEW4L70H`!SnUO z)Y&1NB?yYIy`f517wQS;Wbv2GNn<4GqD`Y`m0zL}_Vg?zZ@!DbjgjPXpqTfjFl4K% z{a|nkp6pH5?srbH>|!jSfaV-Q0(_)#TTbj{CwO?Hbs1U(icB zDVGm4YLD2dnp)(0Gr|96!TLam$Ugo4-uqiYe}P8NO8?z7OF>UP^ewhfCd$#X*yT@d z*M#p-0!QAYsj&B?apg5NIZ_w(opJ@VIbzhW(+q|Ty->9_J^!ENaS zy^iJURA<)g2(Bxt1d91`MNddVvvZHLH)ETK7BD9@c?rfKHA6~nSb^IPitc zx;r2%8=}Zj;%vZ(Nu%q@u)8A%lPe}8pe@&v#?J%EruQrz+4Fr0<&7mvir_l+Tz;9W z;@sKl1S34#T6GN|a9%%MXo{G>J|}=~x}9F1+b`1mT?Eu>3zworAsbi5fj7GZb#@=t z*0;?C;~~-;@AAWO+AU)|8_Xpi4Zqr0k3SjPyT>+Ozg@huZ1@S2b%C*){gvfcA%Fn4 z66IE2<#wQ_h2@=Ki*cWMe>VK&x%RljiM?U7ts^tay*Y!5OsXz&=bSQO8WSS zUzn8QDY~N@vym>N(QSa6M1MBF)sW+LRJVtvlS6k1*K<<6n=6T6fzwzP_}Bu9j_8G5 zaAEUmJw+yf+nnTD&xEh!CVZyK?18Rt7uGGUgUCdj+VFdZ33k8s`w2{t4aGD+N?Usq z>Wlu*9A@?W=AJ7F;L+Del6Anmbjd^3E#fiKEhCR~4%St_N29!o+x@D!ryWe6 z>EQ9j%#Ah-Q6gyRMhx^xE8H%U2$w;LhHL!a$$!=${k%X&lR`0-{w3Dn@n>Q6yr+q+R(qH0w3N8IdJW-NYfC_+yv5KgBDp{l9 z$c901KyDDz{Nd}=g|C=V;UCD-U4v#{>i3||CaK`K;8l`GyQ96J;TFc%YpM*O*s>i6 z?)W$8NEK2DSMbDFfx}0I?KeSMw*?<2ZXzTo+?X-K`!TPXt%kU?BW9V3^qdN`cr zg;mBOeS<$$2Fg1StDuY;SIlptDN#DIG(Uy%3hgWB<7KI3(Le~dI4hK2D4LSr907JJtC&u^=~7;qtC6q6%n;XZC?%Ix=R# z##SG40-_&CzU7p-qhM&2FJ>iRm|b!Q!EIeX@LHd=Vi+>UQvbNtHvJ;>ior`5z2!vH zdZ~+>QLz13%rKM8jiI1Tcbb2N-=DBPFV$0f)W*YdlhrM(Oglj=fWr}K#pz=a!k=t) zIVYgTPxuzPmLT_Xy_Y%BIkFJr$TtR~ zG)aL#p) z&<<7+x1V(N*Pa#vtTCN2%UlOcft4Ie!)H3F^Y!+UpYz97Lk*ylS3a=CMjs+a#g_5I7BG(i>pU%BaM23^zaCEDFk2PZWm_EcO=vvt7i^lef z_IjZ`bj- z0K91iO@E(>+0$}sfz-Yn>uCAwnHSdvVmWFXiHRh45&%qHtNk?4Z%UT3{kS9b9|xv$ z3k^n2tvT|pNaoqo9KAWP;9ud~oI8u$#18q;&zgVpP{~9fvjE}z9qm2f2C-cU5x8D8 z_(o4=T8tcTFfi@9Q9D8z;}3C8Xqa-VxXzBA>eet;!ILzKcot1R?^CuC7c4dhw>Byt zg5se4E+^>(-b@=bIw4x}#h7CH&B=9dIn?&ZQ{q)m{|p$zGZQ1hazUbla|OvZjQrIM zoXaHb-9J6TB)D)|nSyeFZnyVoG`W@*NI8~g};Hlme_p+Sx4|xztRQod(~Dmx=(Z! zulXOV%rP88oY%CkxeK!*IQvbUWCbxRTuVx4{Kd+c())+6YMpjp&P_ALq>60@d5@>Y zOF0ooARDVwN=-*an<>5>9DfQ@lgkH7N085Pes=RdmE@NF@gnPty-nXWGK*5P0r4?u zc%hgD^=tF<{DGaBsjt+gOWq&x?H+C0odjE`DMI?pLV?SN(}kBro#@9fPm7?v(&LiR zMlQf})&>6^?!P(+l2KvI02d3fWZ!3t-SuDkOGTW``}A7P@m$w3r6u8Q5P+P!Y*#T) zDRL=ntDDBJj`5#xFky);>BnV72Z1?&J+uy3{uaex34F;_gsUQkb{BrM)$(b+5q?{; zxdpRMY}m}Dapz8Btygwy-r1y1!b}x2MLhCN2(BMhoLYqNuc*xqS|6_iODxP8vz>%Fulx^{+ z{aQuE4*&jCQvP(;eEork+&8-pRPk$6n^xEFxcHHyC3m_|0QP3#4{FkUy9P#!!UZRugTA~atN6xlC8ZFe z7=JBjomZ|hX-=p&X@)G#BmTLF7qpAywhGTCeiOPE6giQt0J>81&n_DOyXtQzlAkaw zq$S}mxfZ(OVmt5mh$=rk|0TDJf=uB+606S%IW1%_sK&*pi!C4PC9V*1x-8p&F4$jQ zTYf*I7Vo^Csr;m$Cg~6o{=E<@zC&I(IN+6O-wF3IMet0^D?RbNJgN$+bp9_=xlIJ3 zIbztK5il|)5;bES*^q`A? zbEtbtJLb>6iV6gDDIWF~=if;V3Cj(%8FoqjIb4>V8xR+SNy27e9`aJMT@L?GZ=ju| z@%<_KVq7EQ2iKDOCi)gPrgoCZg_@aWIcBh=E)wOZINvp!5%!ktKSRvvb`73Lmq%HV(z3 zx~!*SCv0AUEA7lwrVm!D~;RcjmJE7X)~8ZC_+K*&G590&Q*Z0;oPL=DqF;{+wGT3*>%UoV`H z^U`@lg?4$6@&-g;cu_O<%W-Tn%-U?ly3w5X+jRi;gpi2$X}6QGX<&6eC$ep4O)+83 z=Iyh(eTwDYniRX4iM$nM!X{}G+Vt-vh zq->?=s`+rxt` zDFN$KbZMcLv_wShM6xrH5Q)COl9FCcpg6EcP5{lkIpe}_s#`d{4}8XQhN~*W1YY63uYG=M}$F0kt_3zq0u-+;vW>00qB_Pv&kxdV?JfP*70e{j?ePL-bQOE0sui101T{Mcy$F2SKkHsNFTfJc$z* z_9@;8g=ReH2Z?-`Z|H&(F8*<%vGqtklbXyph%dbTq6xB18FbF2P7F}CryMa=Bt&p~ zrpnHMJK4ab#zG>)3`Emu4q#j;LtU^WLfHIkerx=T=bW5PLO}UEDY>F(pNV*#wz!O~ ziC)?NheN?RLFVBnn*M&~C1X#Y9HZ(|p1#XlJurd~gL?QR`V^awsUv?e0O}N+24?FH zex_vYjBziwTk}{5Y0{L?Q8r zN3wHH73t}a5e}B}?yr@Lm|8-K#svl6K%216^AE1px!BB!@WDHnaC3BMgw?RbyCB{%#FRgRVU|C<*_m$mCgb<@=vs2P|uz?xmxI#Z8MunMD^wlgA zmpZm~GK4qebxCr1=CkP%DtUMs+VG-F>iOn)6s64fqTu=EbfQxvC7+$2<)NWbq#gay z-}C_YoJ*P{trddE9+3Kc?(2d`RUNd3IW+!91m#vb$)KKXD?&E+k`JXhr0a>7_<6ia z5p9Q(vFoGwzM{c0!qv6CYd@*!=?YM!gWZbj6UZ#jlX(S3gnHhE2reZhoC@F%r1MNt zSZ8NGP%%u`X#}*Zk+>@VT>)M8Oj_)E4}nXBIq<&l-V048=9D zEecgrD_m6qZfI<4Y(zZ%FUApp2bF5bJiEAtTb`ybri~9Lvx_62m!*idGV-&Vqdw9K;Xq0>vQbwIEaNnHAn;h_J3 zyw>rfBbd^g+u5VJ*B1s4>MSPCo2}+j=XZM}EUQ%Rju1n__$x=GOOgt*2?__CwQfp? zREG3RZXP;`j}J1iPEE_w0^v|K5DWfMC)NF~znr99sk;~N?oI671cJgM{v5U$vQ|#b z9MQzPfWmUsm#Q1B9>a-) zbQ&ddA{E@tBvugWz!F}=BJEOIgx`b>xeb5GO=yU9Op@hlW$m4Uba{on{v`(_V85eR*gApUe5Bc1B|p`z!-$0u$8)JSkg& zjQprCm!bNMc}0??r&x{R*?OZl0?(|&ech71Vp)y!)xDmYesV?FqXeX_bxOff6jHm@ zqxSVrvv~&OaNrD1=UY?>Nap-lw~;0GV)V>PAj^-j)a+QA2C?b?LAKcwW?A89!C(&h zPSS~T4(Jjlx#67NCF52K9jKXqnvn)%_gx5KQaXb)G!p6oAd;9d4j}9cB3DM@l7Q;W zjaw`rSX=@}w1C)-r&OAOw8XOeQ=%Y^8Lkx$kU8!U-IgM^%5Aw$YIFjRi+i5r}ilys&+*TY18{51{d0^ zm~19yj=A9gijr>vN$#2YGVH~(mssanK(X$s ztcRRT%iCkN>~O)vk{=#sU3M}vZp3Yh66J19 z;fXM~&O(`LpxsYwBJRdH77w$E5=LC(&zrq-E*=4x0&XGG=_lVjv(S*eaJ`VEvoNqO zh~V^GE=|2Q&_SFdBR1gW0vAL6Ip0cbPL*D8d|v(cfb^ks31GPCFaFXX!ZlA^boXof ze(#tfG!YcMXbwm$`yWACrEtf!Gez-KdcW=@lM3u{_PS%RCoC8(OuYUK5R~!0lB7zg zGkQ%+jLhQH=hXW6XvzqCj%j`KcmJ3_es8Cha+SMGZ8<$?-UcD;>ZgbIUehn-zMZ92Mdu>XRv`;SX@gvIPt1U?mi_Qkz z|6$w}vCLV%6sb&woRNAJNrbumZ9uoG9?4V_0%f_^Q3oo>Sxwo)eZ4Z#wJA+mOYzQ_ z#^q_7Uw+u=Z71vSRb^T2*Ri9`zc+u^;a8MVCf>cwu+JWGAy&VY4Z^fw;h3P{B};hC zTCh@&b#cMUy5Z2>az_KTTOz^}$V>;?o5X>j6pH`P>VB>pZ>a+W4;m}Fhbz#5v?nPP zUadBOlNkSHQX%tVyOB9{P>7+vs|kgYY0@9$zhVL zKq3!tdBvUzGYB&5`On_9V)ek!yrZ4^N-tX@w9`hM)t93Z8E|a&8r|2?^CJ96&s5qp zXsi1%UNFY)YpzqUU!0wSkMtins@ z^k}Cw#S{`Aw7VJ$xs-xQyJm4iblOai5(Rw2E`^k8yp?VRu9-WQW}DOR8(9Gy>1n>^&^6THeg@Mw&nBea;RBxnLrzs%Es+0-rE~f0$AP zYO#kG$bG$*D)?DdD|aw-SucnH$45l$xB5JQ#F|)H0fL*g1+yz*%kn^B>e0O-AZoKGoJ6&~Me@$Y|6VxSbxz#pQGz7% zB8whY(sRMd=lUcQG#ed@lH?h&S)k4rv8fCT>UPS0wXJdodMsS-20-a;_T+Y5>`S#B zxr&}=f$x*XX&4(9%#YAD#?lsI)6@=jB5(N&-fGS?Wyj>cLhdOo`K|19LFe&wz#bfc z$uDotf|Bm{nx7IS-n5rNmF9o#v==EDH%pOjdEF0z{nxLc>h;|t+KAG;I03&F89%Hg zWKac$ea>*mA|YGTKHwf>lxUt)bxCa@zQaB9L9aETl#h{cZjz|Z0EusEfeD9nUM!#- z2d2*t9ZEKsK;K;N^9F(wxATpW^wN$m$@%BB5#n!ij2k3GrrKRAb#_&wj3&=PQU(9D z&{zqvhbYjWOVwVe^**8$*xpw$&08Z1A@etC9_2Q8OG=y(8l}d-)cH~t?La?3hnQ7S zWP<~D``X#4n0Ukaj;rhx)pxv3>6=u}zXkG^^%BM?e$r$SoLa{^_ey>26lu^D@&A%6 z>n$w5V1WdvseauC>#xg^o=QboF&@w9 zPEdfG(rz>+q`&KZl$d1xa%r%_PK$2qJeguiOB?-8S&)ShCst5;FRU^a3x2NxBo$up zkohX5@-uU`7L}TU@?1?%WC-+BlU&}iE>r*v^9TdG*=Cj7Zv%K&=k!hfv@!cizSKQ( z*|AQ~_hIX=(F3u~kU_own8iM(-ZI?BF#$(T#7xm_e7Bu13OL5pyrsehmdc4PaJrSy zMPzam+>@LO>t^DLlBHvh6LO3Z0Ov=3i%e;5V^by0^ZpX_mcHNVp-by(_{BskYh5Ew zi9;GOJ0ZRL$}hMFiE?43rj@vG!01z;U4sWlbWPxS1fir;Q}mIM>Wz}FjL~QN< z#mGp6C$^HTSed)$Zz?e3C5_zOLsNr22Ypc4x4Cws_iE^rDdod^=u%ZgDm*fVcs&R> z^WLi=b)^}=H^B0wu-$3c37PX@$!DuyPMpfg>Mc1XdNmx6$+J&BA2+FjmlT1H}LyYxI6!KQF(v43B3~OD?PdjdOV^gqAq-2ZQ#soA%yuwbR$H zJ9%UwnN<`!&GbV5wSdd;RIOhTg}OaeJwB}YQ`S1lkA3idp!TTqRgin9iflr10!f#+ zikTO?to2GEVL;hpKly$|ctC+rc}WGgL9>ns#_a2*@-=ZYr9-cXL0%KwRohxSo#vU$IBwlDp*>-I#90BaNeC;a|&mZ+Be>`1dd|chuZ`#IjR+s4FfY}<|PG`5oljcqqJCTN3(6K8@6-g)kO zpZj5c^L3xI_FiZ0wf@jh&aIfHicZ%RuRN%WjW_}F<^zQ3X8&^J3k2j={@9pv4aIV+ zsFKdtCd}3grOy#F<#&i1-$WB9z%?h-(po`Sm`ck&gS$gm{{QfVQ)9B}#6hNNH5+(5x!j$pLgd>d(kVxfhad5=28-#xmAvQ~YLJ;`qc?|u)9IFm zAwo@^77>kE8_?7Bw3+3MNG$|jy@#qXIjo^wJdaPg(JxGlGK)}=xWI2ks%EsJsqhc& z==J)qqCOtvF_V)pEPfx-PtDHsOYmOqDvOoeR(5dG=EMFn`fjFUoS@sQ?pnzZZ8Tsj z*T{xDPr+2enZ&e_C-cNz==KblbH;`RFO9R$Rsg;p7zhPbylgGSJ>EMdRyZXdI#&Ey z&-ndd+(okEbRPtGPR@AXV8%Y@ylsRufp<1z}xaBwl0K;!a(#eG2IJeH8%H7UpnN|_YBeI)PRr$%c0RfdY8Mv z57TkxsLFb$ghR)SobmN2y>=%{?E1obid-62*k^xCGf5V~frM-oLSK38NcntZL{I!i ztbZ~Kq$&VSk}ll2BTGVXK3KIeTQ%kFWwZS5eRJ^ZPPQJvHWe_N$~a?0JEG_{_j?}q z%ykmpZ+HeY7~Z+vmLUvYQb zAVkmmpsV+eSm4QTJKG6o`p=KwL05ilr+-uJijC;aCE>;B@WCHCZ5QNp*D}8U+&O_P zZXzrY>!v0rd0!^!81kW`qMj&B6xBX;NACCtniTlrs=9jO8gg?Ie<;rp!?dN50 zjxHgB&p18cxXK2)=kB|@dPxTDi)j-r)@{xcYUpKh>`!bzTqM|A^XA{)hz)>~8L{9N z(WXg}IOfrV=;SV}z@FO={pG@3mmH(J)P(EH^3|+FL4niY2QAy`WK*iiXA0mf+KqmK zucL8SUeP~P90|~Kw+ZAb0O*?Is?kORrK%6@A_DgwyH;=cuLYLs7-GEaSzj(d+1EzE z8!I*FMFV~g!RgRCu&M9IEL2ul0I->M^tJN*9M&eEJE7H^M_h-y@jE40=0!+ZdB;9I z>PyFd(P-xvedi~`f>Xh-`JKP=*l3o0<;|bb6HZ2u@6a`QX3fNDoPV=GA(b{&nRnn;^rxal|);Sg`pmIJ&az>;D?Ub4DvT9~+jdu;|0 zKa<+_TKyJkb-UfJ#&no`ZR`V=m*Za{KQ27tK_3eALB!3^pDmH~q(uWcEy%WO<`uog zZmtWs^(7E@aSJtY8(i?l+H2SPCK3v5V3R^c>Dj>f{wdh{*zdCk?FCERL>r#1PozWR zOO%YhRqAj|Qxjg3|2&oAmr5&zpt|EQHr~b^jrS!XpxZ>xF!X#di!wOj^tXSS!#n7W z7@1V_ja^JHC1cy>=53Tzsx`vJpf$ zY&9$+5arS$jdB}bYz7XkQ60z~~XIy+@e~zX2%w6h0beqZ1TD$*DVH+OR zIVGo`K&Iec?zI6Bxx9!_+2C)@^Kw9A{>4}7FqPgI4iRkgcyMv|5ljys+zJ~a zs6XDSOWdk!5b1ddey!^R6LAItVSX5sHGffiYrVUET|4c<9e(m9nYSD4@+UF85A0hJ ze1GN}@>1_01bhov)iG)-kh{SNdNJfgV5?bG?j7~s$xB~+0fiw^p!_pG3ugIR%FE+ho`?8ix1h7X3P_KgZRo0LZn^GuRucEf(yU zbNv@o^b1_C-lY^fu>@#~+>j6+{Csg^E1$#@uD`v@>c0z~8%``(GlJldWds^M9b66o z_fp#&1jAD1G=umH5t+!j6DGE^SF3Z^(%)Ty?BT}@y&L?aCc&yOYHThl%UAzkvaZW&GY(5T+fBp{FmvF zzDp;@`b&m6BWiIE+F>Y?LoXJYiS0Mcl3|QUOO`D*QaV;+4K|h&Zd9@#XVYIW^jBv% zRRT_2vv`}Uc!03lMq9?I7i$TZdu?FLADS}35`g{AnnYflV-FvZDS5Q^A013&||ZbGiw=lX7!JRSyt-?C4X_WW+;cV0cG_gFU!V{czp z%gkK4vf56o{p*{}phMhx$Jv4gV13}@Ditv|Lib=b3>W?pH9v zA?09hH^y+!;$$ZJJ~#cmt-+JY@gDj*iKl+hNzh!%FZ{>2TFO4YL{s@+oN0T!6oLtY z`CfxW<4*!$_!I{-SuKo!b-~42^puJ!3|ohLlCX6U8Ne}>_gXvLiP5- z+Xku1=4*p+R<@T4vLEM|JA|@*g;_yg`^RzLx?DYXpau;jwe{|HxBk~OniEhXqz38k zZDZzGy0~8V%PpoYw4GM#kp=&vfZnDGy~$Zfs)HL`t zvh2limEw^MdzHJ1=cwg3jN|EZNa`FYfcb0bkf+dN)TcylM}T;l$)HYKud~+EpV2r#uJl*zwo_*)h#q6x%#FIauWx%$f3Wlu%L;;nQ|khA z+fqjAc@P}zrzg3*{F4ir<9d$yxi$VxE83eZI<5N7S#rARmn`b z>Yl%9o{OBA2Vx%Of#y&RfxKoN>mvpB#;F@#&fz};_9=@zloJvWhQP>VIj%Gt$twq| zPNKD&Z!9p}uD{+2KstRY;x8?%Z9Hv}M!)5-c`q4g0FV^EJ#b;*AAFzf!ZsQu ziW%-$5ciK~8507X7NnfRTDwZ&_gb0x-+X)&$Y&4~+eJ&G_(yXW3 zp;roCu^aRhDnk41DKuxd=H+G5KO6$$A7d>H6neOHcyUTf|5$7bMsu9(HU?%`C)^~e zp$GNcjQU!k8uQpz%yoI~0KpF66ttT=GF17r@92HM#wxuwZuKDf0T@0#1w_9?7fQv) zKIQ-;3a%CH)d_Sok1~j zeZ4X2wyW))*CHZ;KjdV8iPqZ4WH;BB(Ta9(>P>hct&OMj@_1^GV{{Jt@U%W}kv4Tm zos=uln6S@R(EQVyd~>#qp%-o4D%AkRlKN55AI?I#ofX-X&(+#zw z0^@!Q@PhTm_;xAaSAyVYZkjL=+S;b7fj>Uw9O*BLC;Pf%Rq2WM@{^U~2gc4Td{;O& z>kfRcLW0|bnmEUc&-l<_Vt^so65Jy}A;n1O|B5bzvOr%r-yI$iu?8KZz>a)KyPH$M?;wk=8v2~qs0MxK<-17}MP7b%{gx=nl7(fdmkjZ= zO}_Lye`|HEo0A`~wA0rM6u^>uM`b%7zWU>5+->)frW<+pIs0`-=a`nh_N>ZI`fC-} zpo!F$E540`Zw5ed{^>iBa|eKQ&K@7Cbyi_BBix^ua;&%n%BH&+MqcKZ1$MWr@)KvJB$WJ*rdVYft%_Vw0giHqYsZ+tcK=G{N$5 zWb5g~Y+i!{O^0vKRhPOiNZmt6V<13gqmOaO%E0pB5g1#bQoAiN(X~Ocv>nWeZVSvX zPUFxtP|llcruammclzg&+BSw`_(Ptezdun$cb>0OOrXrM&wz&_9lXL$T(>5iI7$7f zCmM)%3abahx~}_m5LS{?NlqH(j{l}OSEaG@*nJ_Wz%9zI5ngxOQ{T8qF z_HQ@vau*6w-)|x12-p+fA8K0K<%sS)$3t_O4S`)u@{GE}9vEpPVqUre_5H{lfgs5^aPJ2ho*&I`gH}Rz;bkU^1qnHS+QN_S>Dp z#1fQD(zkPYpuy)e-x{XnFCq%!rtZaGu74I11&7V=q>rd%c>=7an`+5LFzi3ea&=#2jf4u_>um(9Je{w{ne z<~pQ#;dIPl+-r_XqP_t-*X<8Aiyb2e9 zvthTSm?y?X;oj6&FpbgNrIr>rJsP1Lx~Nn9oX}-dgZ(%saZ@_7Zh<27xpRoLynGc6 z+M2ESm>V%+6T_w?KqWVY<{xIGAIcTxyNFX>I+mIPmB6DdGe3nNJPC>b*X;8bv~&>{ zum9*%0!^N50fT#fyLabq(P|DpDz>7AUpp+YJbOBsK^m%-p0@>Y5h={>lGE@5-h?}a zbY|DUEnoh*rAf>+lf4#FU(nk;qxL0LuRc_?*+uE~UhkYW0;4d`J_ud+7Owm~4L*O8 z7VHmnKt3vc@vQ?S(sgdvYIPdO|{u{?OcS)z?ESxv6+P(Of^ zU$W)lwtg;0#O}*YpSM5l?x8C`1oB+mYOX$7xc5kQskRTHd%+=?>#-7Yp2D`>D@M2C z(Gb=n5Py{Y=H?pb%rgUr5QK)COktGJNlCo49GMarC-1IR^1=u3!jTc%H9{-r;515_ z9%xK%lCnQm6nu%s{97ZR<1-SAAtjCD=Y%q05ucl|iG=54fLGnZ3RFpmlPSaS4D7!$ z%J)=k7y3zc4Kt6bS3|&Di>77WkB1TO;K~R{}dl*dPcxdx`aJ;?g@e zgeouBBX{QFitawO#j6Aw`C=}Z5xJ0E=H?MHTP4a(^u-_729DkGr$NO32=-r*oiv`E z7$Dd3#mUCfIgeNMHBh96%y_d_&01H*JAQl8bL>O0tqk)`R<>{fAi*JXYQroWs4wM_ zV1i`~Rg6|79n{Xfe*MUmA*F*4#op@)J_xybvPa~nXai1*!}W3QJ(gvi%R8_f=Hz_x;&_LtXM*Umo^)10ZZ=WYNvBwN*Fd8^S;1%T7!-v$`_>}Vo5i%NPInkFR$-T85y{8!k+2T}axiq*qh|9t%vh4fxRqXl@p~9qMH7DXEmYvtD$H!~|2~3D%4f(2 zYr!6+E?Sk3)oeZW%5C7KM_m2jztuP0YEq;TcZtv6MXSgct2Xu~%jPZvJ{CFTm0C}h zBk8jxF^WG)T`~0rwBc!TEu?osr4^L0d(Hg1yOrpqKA##lgWezqqmGAAEFuuC$2@2X zeE+8SW>_!8T23lav6_*nfD7l4a+_yr%R$};a>=%x*m&*r=}P~?WGx>kLTLzW_Er-h z>{YCJme#a*G;#V7Z^&rlaj<@!YeFz@H`eAf`RcmSma=e>y5cQg@8YZE_pQ!2SZnhF zlI8!x^PrQ~-^ZHQD^PKGRss>`{Oh!A)_d9GCFd-`qoKel}U|@WPmexulg~HZkMrQTT@}I%S}t zOlP*HL(;K_ph03o6HkS*C2U`X(1?-0q>IaM;XQIn{t*=mnPD&FJX_V7z0T^Pf-fiz zreBsXReot4p0$kfzF1EiHj*Hdp-N^jgonN;>>;;RjiAXkSse+AfB&m}`hGQ&iI`CL zV3x$GNx7ieL5Au)P&pgvr34r&LRHHa95`VCiS(SgJ}L)#@{&z!4LCxytv~LpK1m|X zv6}agK%}9A_rqv9$DpQy!)e9bkCR>w^)GC;S)hxim*-EdU8Y(Z4~?Lgw;k|}Tws7) zI@%4>5Bzrpmtc(?Cf(dT#-YyF&l8KBGwpRYj?Y%EZNbN{eQ8RHU+Td*h8BUjIy_nv z3=~f|w+zqYN*ds~JK-pOnIW&zRS(KS-Fd5i{xQc8xEDPE({LJbQquk_E8DgN+s%i(HXw@UfRTFf+>h8k;WkQZ@n*d`SsAKKGwCcp(ncgR!@^Z<715 zQ-r4M*wdkW7Ex2?C&v?+@ReQiUszrTYpbc?ELH1;Gu93G4Lu+I<%-@KI%6FB9D-V< zhFH&PvUm#ZDn$HZtAsw~DI89@i^qVa@f4jM*<7tuAEWUNoRRGRzYpKreqd~K|cXMG- zPUP(~;cxiXH~UnVM+V!VAL(p;hJGSBp&u8`+UR`mWut4P%`HDiRJT`~I!Pwa9wqPx ztavidt$GCXkmea1V|B+ngK8L&xr>k-jTXib0&)Olf zR(spIK?Pl30?7@#-uY1tEo%QX?5n|n8fCE|^5dO2Hrwra@+65=jl2CZrE*t%(O17{ zO18Yxh%IO5(K@r`IKv%Qje~ho6%P60VQe98 zCjg^1EcN?pe&;Q+!fA5cn%4IYo_5gx7=#d+Zw(ZGap}sl?v;F5SjTHUVy{=66z&1t zUduDLa(w#dqIssx*|a1$*gKJ#qdExM6H86+VkBgy~>2!BT?3M zy|~EJNWGE#`!ADyTNz_zMy&N05_?{uMjzVxtWIkpPya0D_E&2^v7t2upxFkFV=*x?GiYloY)qE+EHNH8-dy;jz9i^QSXdbqz1MFEy&b5x`M$ zk=cSTYJpDJ$uqG?IU_04=BFi8pGaDxF)x;8;Wc~Bk6arN$L3*_n1ry?g=KR?OLkLW zhW0mgX>*cYuIH9zCB&abnaZDQpOvY(_~#q=qVvo5IX!(lWh*L@d&lzV(Xf|-)QiW2 z8ZMTdQrGM_P!li1Vgd2}J{1k2A3uw0ZqNJ5%Y-=qE!j-zrT~OOtqrrqV1}r#0_1aZ zyV`wMWXB{9aGWBKf!X=~a){6odixJ&ivRrkxkMt~Ue8E7KJ;&0oLQO&iwuUTB>voM z&9xAlwAn=pfX*eBbe*O-)q+P?B`NG>ie0rT9o7wf3{?Tgw;JtO9&;))k^+-^VU&Y> z%ow-EPn_gA!sEvxWkQk|U$YsVpKPq8JI{M*72qnGQUF{(jW{Xk2<=H>gei>vMLk{kC45r1ACjA0ly0+OltX*oYi0e@iZ993t zm5dNGt)WNiD2+?Pe90p+rR(c%UVCJn-J4X>cD8g!Y7L0~uqsuvIX){}X|NS07QPjA zOEC5H*-sh#uG}3LBYX@*U+l5xLV@mRREA~0dTyr8+r&?mJw497sR zY5J!fxHxoavxc}hTy#V5oD8y(KZtK;ZpNhR$g2EDmmT3iZbE{mOyV7FTwUMNj%AE; znB0D=l7smsw)qZMnxzXDVZ?2#{_*}!v~c?^O%26=*VJC}rDdff6Bk7p-t)qh%J58% zI9|uCa+!&ETD38q@b;62E1~fujfXS_b9+eV-)vq|P7{5WdHG9cV9$-JU3W_H`5>{_ z7H4v;r!$gDQBf#8+I4N*A0L-A+10|{*u3ZMEXHyq$(kizdc)!-@S{s`c zbHID>6;D+j2v_`*$T{m*S#cV%|KcAu`G-AJK5;gs@zd+_UblxuL0r z5)4HQL^yOZ4H({wD9h;d3E1kCZ8W>IF2Nsf>Ei|=u!3`rtZ3UWJfUsAtK=j4TETYNAl)E`}fiE*ELN9{nRM5X#SQ$L6! z;U^kh`aN1@0J+1jIbJsrY^uep==N1h5sZJZL^s2=1~Dh>PyztUyTK%KJ`4hLJ>$cC zn62#=N^z={!UUhtZaBRM*Vjq+Ci)|6!7ldR=bxM?1MRZd!V*r6xTn-_ibMg2HNxgHa2A*wh_c%8lB8mXTtW0;=OO9;7IrD_gc3~a~4 z#Z<#|3y{TM+bPpq26_R5%Q&qeX@{7}45k_Z%A8o=5H^^1UGF z{8Gs_%67+9mAah^ODWf}^<#;AIAvzH(Np}>n|qQI!K^O}>ON<+pi(C738=Ts?(%Gc zICt}Rt^>XOFSnYCSQHyFFZ4SpC}joQ4vRU(#Q-&C@w?0`BieF1hW`( z2G*^aqEzkSrK`2?N%ZSb4RbAOEE5s=%0=nY%i$;Qkh4TVeR6xj=`%G!aU;y-4QHAJU?Rdbt) zV^p(0HKLIpXY%V6UEU9(r`!n7S<`gS9rJ;rt8eGOy^diA^RI8K~7e6NEUS)svopJh{_sj!# zxj9+iLjZraWs_D~@UTOEvOvWWkg=7~>Jn$e$@%G;JD#~p@$lqJ2GVcvX3v{Us$?P+l@D5&FPmpH+4|lC9{|&xk7H`TERVU^CY~mWi82jv-WO-cPlya9Ang3DX!a;& zB>?zb5?7Mh`RfxT!nZ)IyFsOQ=&h68eU%Qn<<4E!@U|Qp-5ZPmGgpoO$@w#}#`vry z=kJ_7yJ;!Ad9dlV@a$+X^e3w`^KQjsmB5Iq?Nt{noZ51IGM#XIcZ$C_9y;E}pG*6b6g*BRlyus~%jyV2YH49QxEo6I{mPlWDN2MblXlFDzXAB37nRdEiWODE;Z4`avx-%~+)LM>h+okw*#f1XrGo zx8#O>uG5hJdX7iMvpn(^D+lnSh1Vl5MZSCd=oN=BDDuYquX2h5lbtafkYAx-?jF^_ zwtr%5SE9uxbcZOn={#@AFt?+IiK>NADzs#84>WNBhe`^oZmZ2f5xcvGy+{+MHNFc$$g?e8$<1-V;1L-KboX0ZN-_YV2QL(k}QVbIO9_3QL$*)5WqE*~`9AN?_2|H|BddwiGa z=lSu5K`(1>@1xO?O=;ZOXVKUZ-*wv}DDlGv;rFEunYr)J;Bn=m^!@O3N0bSWsxnul z30m>#ZFVOrS#4J|WhV<}x#3~!&T6xaz4r;fe;Sv%hyX98uHU$^;IkqDTSkMm8YZ7K zCrJzN{k*WcF&@Dg5w^=Bv`+^6#pPDNL-QH%kAA_(iLy?ct*H_Hrodd^o!ejZl#5pY zrs|cjT#~7ov4B2YIwb)E`8dbmPVthm^(Cu{01lsq<%x+Sr$@13nmxg01}vMgOg3Mf zTZnHSs!I&lSD#GveFH~P$H!?wme?NA!+ftS3SO!X93F6RT?Da)cf|z2nAB==drZtb z$ivW_CB0Q^#-+33Zj?()iXz zANZBndA z8QViHJkpyaEe#VSwT3`{R;1>4!sd~BTiG$}An_Ja9>{ykkz#gM{)JzF@1w;bbl6Rl zbZ8US-sLy52v>fd>PX#5a}lj@$80ggL_$9Q(@!#YYr~P%pwurn5U5E?-H9;h4gGk; z_ftQj{*VNZRA+QkLC?1W90bx!{&+#U^?NfOq}Rsnv$&n{-SDh=CzsPL6TiLfGK%=< zr+u>&V%;BmX;BFrW``VgSt9zA6kD;v3pl6J^Po{r-ei|E+<<3#o59b#Y-y*`WdSPr z_82muRPT@@>9Pz`q0Vg9hc>5ddE|7!25k|_3;C$Ejv{ik(bVxR^^n;ZK!;_Udpp;D z4Z6tHr{maj5``puWj%~qHEHk}SbK)c{PR2k>nI(OaNNQh18W`{Oq z`%m+^r2-x#S_wZi*jr7x+b@Hgm`Y)+1cY|houaMA54=ge-L&tOmlzJVm+k7sCTR%m zx5&424(7I&mYA`5KY-m%^zlv4%CC+@C;2e0a$QM2@~%rbNBnjnoe4QBr+ZplMb13$wSLC( z5^$fa(ro?3cV70{)sU=2(Kgr*Q!=&1{gmD5h!k1hJnh~&A@)+t7JI>%k{p^}?jX6+ z%o=;QlGaMxXJ()nI#SxR)|Nxa;z$<#T6})`A#oH#M?mz&gmQqC^1u`%lZA{_cHzA= z3Srss!&lPIWbm;)`;f_ZWfOfI?@dn*i5pPo{dwBVd_^uwv#}`Ek{{n*rboo(6RF&t~7_fnx zak&ddg&FlJ?0F3{?2I$qU74d)F|8Xu1VJVR$V$giJ~3Gn=qh7-_-0K;o&@m6X6QQn zm3Q9VUl@p2Lr{(SE=d&*Oqihh7v3bH0rQ4Sj~xkDY#?rD-4Q4bAff7|_GIVGaNt8+ z;}t_lmH!Wh(cLY?4h)B&zD4DPkbVZcY9GEgi(l35#c@N8(N4vW^5xa#mVm}@yg1X; zo7xv?ZlLq8s(&O(4fh{-i|bh6TPo^&B+z6gs7c42B49=;yZdNu_Wh9I6Hv33@M+wJ z=l%>?=YX@~GE|U!zdQ+pzIwzoKG;Xm#DVbcARz27HyV&Ot;LL5sK6BOrG9c8(@NLD zU?anR&1y=CvGxhnPtt(+C$RrKCAzM9c3@=CX3`||t5w;VW5n-)g&4H&Frk(wv9@=G zipbFs*PYIk)JC$B5|5;1hhHS|144A)pR|0`2T5+P8hS~`Y*L{MXbg=~fU)CM+sP=( zT8i32&o5#TiV|6SQJ#Cx&k0;N$PNSKcTknl!N*SmsG{AAC=nCF@j5QxzYOsXEoeS1 zQ72uZ!83EAf)5$ZuT%gsGbsQL(jojyj->X0#z!}8v9W-WWow!R-)W-YRHs_tC;q$y4e^_1`C%v2II%S7zj+X$RHg71%Bdi;3`_C z(kQ{4{myH>SUwz5=HaoEwM7}yrE&5^mA&6}I`(8o%1?=QP2Pn zQPmoItClH9F=CUFv@a-b-y_bl!hXdAQ1+DD$s1}zhq7pe{MDQ)b-X*ZtJqhajTBKK z;&l$3Xo}6cwiZ(NM-e06Pc~7U=|2wl=CX$%99lHV*Jk=KQA=V*I#~Jz8bIuJCG#-( z@+&l0cE>o99BfaCDMeLeINi`zIZ|?03;+sFp4ACGqM;cc)T5F@P9Nj&_6Zn0R&Lo2 z5ZIYtdNRj3-%qoLmkqVZfic#{^8;3Z-RxUgmYd2Ov8%X*e3$EIyGCMce z4HRNz@sQf!GcL1>HLmElxh-tqv3F^iu&{Q+T;fgW`lym;mZlnWJxm+AI4lWkj8j)6 zKjT?H1QteQoF59dS!+0 zB`3Whu*Nb%>-!lz4`wJcYBiXa#jVd$)Hts*zxtgfb_P_&awxYDn-Gy$L2oSPE~;3e zP&{L`m3+>zC^6z`ij%X|(@PGY(!R@XHI~Mv6{8@7^Q$tj$ernDw)bbJ$|?5F7O@U1 zx`DB>3}#dtl0fqgImE_1JQ3fA6qD(1ZSOTiyd(|ygyV3r*AfR8xKp%t&WP==81^l} z=y@Y<@EuKxz52}mniS)~nVwwl=PExEoIg_~#fvL=gm1hK7164k=dG-2dKh`x{F+Ea z_e3kYrM%lmiSN+YB1A$@R!Po4`7oToIfR<>`JU3)jKsS+a$IVO*lXEq+%27(l4iEy z??%OYpX}sBd1Ex3jh)zZc44n~i+PLl7K215t-}MmUX|070h&cDu5Ge7$3w1UcV+7T z+C0y|Q0B(K^KCsw=JU(X#cxE1H?ifmHBwbYkBbvX^X1J;onU)-2JobsPv9nUYR#># zbLo7xauI+JNgI6VH~irE;WRyo9+dO-&+nR#Zafj0>?G<=Dm|lW>$-(ds6Gj-nxj>w zG#CiKLPme|ZB`yD-Jny3-n8Kp^_ECiOV;Ba1W9{+FSIz4CT_Y^S2pS2^qp6#sa4Sq=i`b>q z#tMTOmc7P0-ov)WCHpPM=MN2=R%C9@doAIfIjp`m^4^~XOIx8XPygX0{wW`LB{Opb zU>OkP73Y<<^76TWxF!Z)h20lxBvmGiVBXvRXa`9w^saKkjck(inv7(ou*B}*-5Z2P zhg3-uB6v`u*%E>=wSS@oBras+FCH~;Ca|RH69*F*?S?x}lfQK^8JfWdWVD>_&Fz(B zA$i<5+k zPBSt&VEESIcKy=07bwY(Qn2fmk$hp(vw>&E5YC=`SNfWIrO1Fe9fqp+pZOLl* z{cm7mFkB0?mGYnHIlGMuu3cQmwp|x8EQ;UDh6)IT=IKc`FV_U?MPy{r6KaH#s{pQP z=}IKh&L*r4-Z2Z(y1_0TP;e%6ItrpY$%%Q)c^BPB1eX!;)5knF-o1~)9BIjs+%;FF z+x{Ui?#AEpK%Z#0I)g`TUnAh5!_P1#CqK)az_Q`h#_`KYb-akPB(L;i241pc@Y4uB zw7RC^l(R|jr(|{jv_z(g+=?E2p`tA>qMbw8U0Kw!4Rgo)pUR8MYHPMOh)+1Y;{Bug z_Wsu5l9}eRl@}?s4_EA-at-DVLD2Q-bZu}^Sy6)pGu|2g_a!NOQ^GYYz#08ru3So? zHyHStrI))r19s*%74vTR*Q;qJf%j^1Ql~tLd5%E%6aiL;n17_yX?@bZm2~xwNGBlE}Z=_^4Bopa|LDb&M-qEdO{W@?$)P zhB@^9l6VE>Xowz{Q(cLZ+E>wLYtF@n$(gt-<}0i0uklWQsA)7V3bj@wKeOCyg(K|5 zhOow8>v8d|G7_t=pbFbd!8Ww$NVW#%oiieb9EbZ?VmKnZarOO<|DLJzWX-){+gh}- z{RU5yE|e$x&)iC_H^RJ892U7^Bo-QxVni4|c&E-hl1y?_%M~)li4V<(Spi*_Qpg_l z&1CB3MPmJQ8p^G?R_FWN1?Y16;h{vQ2e&BQV>Yvs{2LPvF8 zJ-ha>E8+ZPJTc7ZPXdJbM+B+$x@4WATovJvzU$DvS^Qn<4o{H_>EzHg7Wrs0?FgcQ zy`fpz;gDLXbF&&G83{kOUX={wi@i*l`n)NFus}@lO(_Y_XtQkKMbS)<(~q=y2Tztv zBhC;YoL(mRZ?N>Go2=tnEVd}fan0z!ZZWHAvvO3+YSUxM_Hnq>@F4Gf!4CqUh=84q1E)jMnEG6GtCKOM==ftdwjM1FZBdB45@DbFS`;l zN_QtnM?$clWKStOt4!B{8hMl^MmU}pnypqG{d?>wtKv1)udA|9Gwg9bjZ4%2u{9AK zrNSiUTvvIrI(Y~gi1)cSaWhAkURP~)N$01AQBW7pUo5uxBC7zh)UM`0IJJ8rG->1@ z3M--dI`z<{A$C)!RKZIgJ@Ma^16hej%B6jJ1%uFTY2CfF7-SQ1m_MUY&eY3~ zMhML2LxLJRDwO}51#iZQ>)E-3I&JkK!U3uj4el1Qq+o(+Mi*C}hliQUfHV;TXr%sqcI_5@8+QMR` zT{@_1m!m}9nvM~>b*zYb>hh$OAxirZs7SY|y^v9Sgk@Dw1%Ep&XekNt1b#A!M?+glhO&Ap`jdByoLYEs=Gz7Sr(%KW zd?WG+{&mM(mRznC{?eiykv=+`_%JK71gq1iVC?!s0aAkkFqXP98u?NdZ z7Ohc&-H1+ZjA|3MT0C5Zjg!Df%bBq#6QffQ$ov0JEyIaIwUJ{c2KI&c%}Y$q-~Y7G z+Z{b2QC>4kz0(B5bU0O8Nzo~UFY>AWf=S8k_8~cm_}Cq|o91yqYEeL%cNrqmH?0_2hT+Zu1VLKMRiPKMV5yyTF`?n<|^#@=pSxT-6!3T1Zm5 zgfzEx%k1%;Ozyxr>(tjQ&X$fza=O6#S)grG&7t5|*nq?c z&W0yM4F-QDoZu|m!H|LPyE!4OJ#Ng!?Ha&ALvXOwV@B8G;F5 zJ(+(jF~gz88F) zGW8JeS>u%)2CXU3{r%}(({tVQ@3B1nz2Ec|+0*)sJzjuX;4aR40|K-1?TI9M8g{^qR99J7h0lXZi$Bwn=A{HSI2d0>hxB>2a5s8w!)_DU zD7lAyJgr{%Lq;%vmiOx{z6QwsmiJolg zk6%^s^^ABwK&a(g&6iQxSz57z;oK7#LlGslGl?N##rX8VVwazb*lk$jd?Fc$|8B7K zcfuC_r|)9Jk-tCrD>K-sRv>EB)!Mnz85LOD7r)okP3cIqvYga#{a$ zE9ew2O;_eGwE5T*m`-*ilY7xm>3I1%CUC&%hl#z%MN7K0APJbg!AzWH>#MQ9bnbwK?P4^9l8*%aHSw#uys6LA@f-EZ4>@u>iM1;8K zDI%VWGlT4b@A(XgQepBpvHn|OsIJ>D~&%? zX)(!)mrK6g3SFq;w|-KUH%3JPh!$HpeM70mof!RTV@>4D8$CoX7Jnj~=2~_C^y7|z z7^`wocBNUv(bZJnD~Pk~&Q)7%5=B=Z0`ef)I~8^P1J_otGz&N6u-(l?ewYr6f`0ausWJxYZP~f;1=8o!QI{6AvnR^ z-JRg>?(Tt&1PJc#?(S?hT=Jdso?AcX$2>jVtEQ@}SJ&Ft&db7CjdWcm%M|QHH@I=& zIC2(luXtL?QpozUeLBIUw{OUhvBJY0=8-veo@piLlJa_`R5zms%||2(ZYypfT0Rp^ zdP~NMJ0l-lsL&wc5w4^)PCC3%=fd;IA7`|#aelniyvJq_WpU{J%W=-Lt#G#l!=!$h;kdC_g zB!JKKNSE`x+p^w(HW)O$-!RT158L0|7$@2GBq7<^%3S9;Evp;ekjrchF!zSrHlc<5 zFRiH&j=bt@=j|2OC~_e_U4L%m(ziLcc6tip!fb^?-HGT&jH8kshy^JxdAXZ7?sT$? z-N2R~XKPH;bS4$^2vYD1F+nr0eg7tTyhGT2BvRKiJr7?UqfLqnUH-urEAzeZ#K}9A zT&g{ut?--z-Qh$s=ptXm`Ef_$q?kGQ`3r9Gt=zaK!NmIu=4`~}SFS0=1q`7O(1z#Y9sh$;w^XR};=m+Y1{ z_JqiW#xPa&X?se56JM1==kLnz+RCtXqPWd9D?(bTmEUnR_)Ny+{x;)$1lVTH zW$YMX&rQlDeE{9WUsurR{x5504BQynX|!_&FW;>uhE-4qxi}fku|(_?leKQ$T~pZ{ z!saAo4n1GFrS7HeDvS-I%{1lvRNZvc1AZ4)C8FU`lN}zJf6%A=EBVBKR=;DJ+mU2J z{>(=51~zq?5mC~f@;yutZ!b-lHg4!ZDKTD_j9xcwWUML3ma_5}94a@hO`n^*LrMp{hrT@LgPm@@O{J`8r=#Qun~-Bl791rmwF@ zd5RNpN!3i#oDoShAb28hzgCv|)wxlJ21S(w9s!w_2wm!t??E}dv0JsIsRH`1`IoiL zbvy77b3vdq`wd`t@dXyM)=7@q*tqxd=Kp$TA@p`}QS(Ls;)-Kq1+Bh^OVvHk9hC~$ z1Ez?5IX%b4=Zx1ls1@&iTsbVet@`0c=6IDtCmW&JARU=1l=z8>vR>#}Mu<-L_m3=? z|8n~_EdJVqQZ~OM)s)+*V&kos!=I^U zs;YxJdE(omcl=|!(uAsy-(|JLx3>c%)>>2L5a(GL8v)!h=jkw--xr?@mSYoN&)ncX z+4tEs0X!R8jo5HO>66a*2(hgQSlBY33J>2?h0G?W92bAdZA=+Ql_dFzMKq65;6aBk zmr2gU$DFuvfY@HEB^_Gx`3S(D)?oiz;VcPR8oQd`3D7m8jqSnU2X?y(;VV}2e`1ko zQSnVZQ@$a~jgMTc<;fOTjz~yJl}|{ zEo2{y?mw-ddlHYhNIBX^R!-e%X~B`Izi!ub%^WGU`F@XIYaA}Xyv<%MIMeA$W49sb z7D{jnN5tf*{>tPnDFYYBAd7J1k6O_2hJBrav-bbvLDGO;3A!s#c8|l)ux_YT7iZ=bk1Q0o-i5OPWRt(+5TJ zZFN;c2xONS}}=hOtrjaNEz2|igqz^}qg#a0wz ze^^Po_~T`6KC)DSyw)xYaJp$3au(Psx;F{6q=(hRxc@p4sYFt03JkbCUmJo1Mkrdy zK=D!fw!`8Jq^)-=MZl`_AhD31S+6SmSKA%hL1^^IAym^H>|{#K?u*>HUDt={gTYJ- zE&w;MBHx?*dI!XGb|-AtUFJN?n3&a#ma2X04B{}Ta+04eK;cHZibPmFx0G0=IlbA5wms-G3 zm!dA$yi6NqNQ?wGu5w{sCf+MdfeBdxD8@)E*>mV)nnCUz7= zFfL!)teT+*<8whae=36TU?zUB1=nYm|4$A~vrf!_rbIy78o_hq1R-(ehhXx90F|{$ z!m?Z;u8#=01_@ohapv3F>6WD{WhN&^6?9Az&WkJAL=Vxmu5}!QaC(p@2SNCpa=UMh zeLnq)EZGuuCxgi0pSLNmzrr_jQ)1>8-+rD(F1&*6z;K7DWc!Z867x_0MFAHc8V{L0 zhuY^$K4U+ptT9eX)VUPHXH8M>OjevF$O_N%s=@90-^tVsdH$5UW&G%E=*{BIvDTg% zbCSESy%8ke+->+%ASKH-&iZwYCXd=psmYR%L!n#)y~R7gUlCKPEM8gVB)e*{$|Y7E zU4&ct^G`EFHjX`CO{$bn!iI5ig+NInt#mG`nK=UoZ9&=Q6qsV7Fs_-niCiQTgKwTX zvgX?*!SoZlzz%-J*?`S7Fx2ETkO`y1#=5bPt@n3!!zE7j_1}bX{th&FY)qS}+3uL~ zt>w3a1^~MNvVgZSfX!5$UWW(?Xof^Sls7({*ek-|sdLHp!>5br_nON(e=#8eYIzN@<~mwsY`slv6&D?LZGn|u*zZ>;`c{J0mgBD^ZvIdU zBz(3wF3TR-J8VD0(&Nhm5;&|Q&bssYg zY%s_ASk!~6ze)NjUUd0ytwg zQ_Lsvxfb?X>e@7$QdCelc8vEX{0vTG(B%yt`8jR0{g#auSMthhFgSguv;>T&wUP@p zwM=!B^uoVmP3sb6KJ#i}LL&45$cr1#XtwYEE-VEntw{&MSsTGzF8XMDDc<%1zWQn6 zZei^`(amw9V_4uV2HCwTyvxB!rs|P5gd=zy1YD2ZC&c56;703PZ5%ro%-Tx>mVbA} zUVi~Om~zQ4*0k%dE^2InhyFvbO~v^Z`k&3Y$qdaO<3g|~^ivxhu$YLG?|rVG{Fp)S zpC4vlrK8pEs@Lt6VsM+7mhA=lQs)8P{W4+CLHVk%0YLOnCgwMKPPc|!v z)s`?SsP+HCHjj*kY75ZYktj{TsZqPFA3fbPX8D;jV6`(^fBE|D2@|qGJo5UhE8cuC zw=@xwQBD{>Qb{JiqK=X6Kt>q$mL+8zORqv>r=ES~vrQ`omzh^fDe=I3_vEq4-*Z+{ z9vtXbNWp8!6I4)vpv$4R;7gLAG~g2==2PlM@6C$9X4W)d0O~Usou0t4&9WV{t-qp7 zjNgN%e~Q4ps9>@k&OOQr_g;o zpR?{2X!FYV=lszU;Qc`&^^2EW#F0XpY~@tO?J~Ey>*kT(Bu5kC7h#buvl*UCgYBr=bN67DEc*eYAg_d{!Q?G5YF!( zS>%&0y!mZYDcdI;_E3gzgN8nbXFLLEmPB2Z`DsL_u$ukVN_nq$c^^KnHzXg9S(b03 zb#J4g_omWwBG*2S=xKYcYFVydMaM8e}5G-Xl77 zN$$%5U$QY1LOEC6XAE^a>v$|gyj#q(CLH!*5N5f2yue)X(cLdxD{zndLxbJ}=~tBP zt5&)Ej@NfvLJl4vDUlrcq&F7W8+`T;|o#Ww^3VpCCA*lfe(fsuE6olJ^syL zCIsTU^xze9`JB52C#JkUy>{67YGIHpcmn|4A+Hgj;HP?L4YLMtO~1yM&*iv^FO z5g*E{MIV|n7p~D4`B#pWFM6yOx}L_#&(f-?gZ#@?@q>#w7}}2?$M&R?GnIcN9W2EY zlkj*;o9C4BjcV#kl2CPwiYo9~(_Y@#1G72lr-05=5pm73k7K82N1mHI;VTY`E#ebxqdnKb#Ty9Edt7*gs+ z5AFB-8+oO$<^Hz`bZ;$M(n|{s#}{s)c6Yv8PrfJJn`hljHYyPLN1Ez#>h2GBpdFI; zowHVbJ=0@Af_u+h&LQUF6i(htQtmwk(1(rRv~zpq{MG&~*50Rxe~AUK1?!HnaE(j(g45QO0Tn99x8rnT;@1U zL<<&}7-vJY7YF|%uUv=P`>mLFc|%2G`OmwwSbZbrXQ_HK@ zZ~8M_9^8|B?D;qE61Mp6CqscCG#XzlKF;$>6kn1RJ5HE!DD(X<>Okz|MF^n6SDcSs z!)&tYj~BrYi`TfTXB2W&9Y<7}4yBY!=C@G54_au8ow~OafCvDz2C$v_SP=wTJRZEn zf7}Rilra0Vob~u$U4gpq|Gnoo75w7eb=~^{pfC^Ek;iH+{CGjfy~OfX0=Q3;WB*+q zRU0QewW9cOuShU6JwGCWvKtrU4c;Jj_yJRl$w^Vqbffx##~cB=17S+6?=9TIC6$r3(XzK zdydSjKk_7MNf07hdiXU|IOa3J=@Xpe%-{CP8B#uA|M6&#x?B1F@NX!9!>vK)_g&`I zVuga@_kSNoytb5#x*ZQ{5HyF<`6vZkto2MhRwYsTY zE)ciqBsb_5aiUWV#8vOH^z#yTK)Y$#1fDhn_II|ZzD9iuOK4$z%T|zxSfgH%cb;plg zR#y2$Z1r;*KDcuB zMaiF&te%rB8{=XM*@Ziw%PZ`1jEcRbWid1gkWKRViA9l2p#sMJRtCnr;5F=fKtf?O z42baF9s$RQ(nAL9i&EjkR>r&TB7i8T*8tIrI2nvCaDuM!9YiNQAC94$<6_BjlIH)C zwx$mkN;mx%G&#M5{w3{783_wNsoa0SOHSt3!({mT#bv=+&wUAVEXeOU z+~0#{uhWhgm8!-|Z`nS~@9Dgu`bZbWHTvM;Igpo`oP0klt+#&{`ON_M*P)IOHhY?>IcB{e(mPKnBk+WmQpI6C8ok(K=ZyF#Bs&>Hu3sw8@)jz{cjyc^bNt6TeJzU*|GW!&Oa!zwZzKu;yZCGJ_t*SX^P4^4d( zn$qtNZ2GhC&`~%KSBmk{d1w1lAGRj_s3iu}6tDK+vDz+q76lQtr@wC1xhDNr zz=rxsB5r5+@qyB3E;c#()&5chS_dHaz)>p#aQJxr^moxaB+%^3FVq(_)t@Q7bjj&{ zCux2s?6z2VGBFD<1r$Pn>w84?EOMVd9G|}Hcuo7G_jyH7GUHwL5#KPJQ|-_avomFl zOmeel%x0(N?yDoolcNQlECMB#Rl_s6|9<=*oT^p4WCzfT;K$L$m_cNEVzpd;iqtVq zvxvHmJnM`Llu%@I<&poZ;>S;d@JicCDg#r9nj`NAaJ5_VG9O}GKh}!k}sbo4N+lN{_ zUNSOU;qtyo<$GmPM^mTBX%1eFOaDMsh?eb_7nFqetfi591*{a(v&Z|bb_Irou{zXH zx((9lSI-Sgl;-yhx))7|n!G+{&@3}v#+E2G$2CjUsyBbjmri{w=2OqU7gdOwx~uMu zSNE!@o_F1u8%wcWZ77{9l^+lVCf!odS)u*iF;t|^n)!RNoACqHtunbC)}v7)`j8O0 z<@XoceDMS;xq2R<@}?TbT{37!^k8~f{KfX4vb~tFtdf6-JkK4QY#C}@yaT_oU)?p< zCiJj$7254>7ywQOqp6c#J2}OFsF=ZRy!H!x-DpB8!Hrd!{tMOh_!q6ytC$8qWRRS` z=lraHGLQ{r?-P z>$kDwtH1vJ9n*hTF*`%M2BFj|bthv{w+U5pGdbO7LDP4E_M@Dxu2Kr}S`fl33)e(> z*D+OJd}NK}E!h8;z{j27R%gQovf*zYi5#RAXj33(4c+~F>T|5n$8<_|uP(>|6|G5E zoQZVKKh2^dv@-Y9q7Jh}@wHg7^APIncRm!x%G9gYU-=p9SdEujR&2{tN6BP}FRGxkPf7@(u&v)c3U z!A7?n;c>P%oU7Q-x_l4`yxV9MwRA1CTT5u6o!IxBoWCHgxmP^i_MBTZ9b|Ab)*B_K zU%guM4&Q6oP}#RFSb?{5)?t`%t`=(j?2K{t{=Kak$%z-*S+jIva_20NRIvg*+Scb3 zp^0CQJ9nq8)YRMncn-swbFEf406X$^qfI+3;dvn%al*LYJqMyDin&MVUX~$gv?+fy z)sME4u9Ozsi~d2UvI?W=-x3C+yM)pORPt4wwmdPO;bc_GAowckvgaM>4!ODJRnpNV z__qgfsh2%LM+Lmf1+Q=ct4A-0WnNS6>2Br_Ti?5B`^GqMLA>)pdT2)Kv2`i65bC;5RR(6heE3Ha!`1H1 z_x6$({FPQ!AgF18E}9?6=Nqj*TfV8m7xQj~V~_iU1v?P0&`i;L_t}>FCzk~Go6L~} zgsP`zn?!o&der5#d6v@KQ7Wo+KwIr4_n}en2K0q#hL;*Kn|>=XRqc{WQ8+fcg+_uz zVb)!nZmi~L$7D@YK@F=byKb6#i_fje7PU*SPX*mp;p~h7VhpF>pt1`@#; zn|8Jy`-i(G`U+&wqT<+~w-X8{)M!N+vJ=55jV#bDLBl0~UbaA8{uYJ?GBEG!c|hl- zYCFxu0LNoNjt5CY#7^60f<N_$xb2^ngTWJ0OH_E|4g2I4V-SqYQsj7 zI~EcXF*SSNE!%}xnQXOCjR(92w^zK)m_cFqUz-9`P$xR=`lj-X)K+QAid9~kfID6Q zD*|6i@tH+21+Z}FzQPxBA{L7$f%k0d*E`YiQSoru^hhPflwAIUJ))^vnr3wWfcVo1 z;KiD~p+kQfJ|#|cq_T3#s^#y8z`gq~4N!LNlA$l_v~}jYw&BrGpnVosG*QBgmb(=B?1E;o!@ z>IBspYT|A^bE<|1FxYo}YS%W?TAG7Gp)`{zR7YMiQEt&C z(Wx&{D$IAo%E6_#G(1OoO$4RuE zh>o-vfTi&Lbj(zEWtt@l%BHTJh7gj2n+n?8(Xw%3;@DY`CXj0ENu|zRa;Dy%fysO- zAJ!?rPMQM#o{DBEx)dtFRBw^u*O|7hqN`gX@810Rss6X%buk;+B{J2pS<`vkvGWS<%e9gwn1xu1cmD5FHOC5H`uQ7qD#XQQTI+C@RR4Og25jl%%Dpl_Jo?dmea$d_r-VM~5Hv-ME7cyNC4Ujw3Y={8pJ5N?)L zUS3?LQAx_Dvf&@Zgg{7%M3ACE1jFU^1wbBJ5Z1@`t-u6B4uRVQ$)^f)__$^EuNHMTCdG9x()+aq?Y&x3f#NY%SH#e>;EWtx$l)(8e z_&-qdrrI;3cOvZj65jiSpc#sCou0xm_9PKC=H?O+RZ0aas3tb?X6 zqMow)6)KZlyjxbYOOuz*#)8W{-@MWrr`OUib2}U*@7PvNwsqm}QDp5eHFmR#w-7J<5Bn$a^KD^zu2u zdNP{6wgF34_W|2G`*#44qJTzBlNa%LN5d!fwj+w?Iemn%%@3Us+=fjJd7Z!K^wa@y zesz#FeoUf39<4J z0dqaMAJ0A7u4`dWPYv=rw~>Qxt!H$1#xK$EzA(s(jAjLoE6HyvF@b{vF2eD@37VJs z&InD>ItHbhy{lFfXpF^vH>UFw=wa!{RZYp9r|3CXG6Pa!Scuh2LRQEH3n@+J1+b1o zv{drCB<*N!=#>&AI7?EJYEQ6paa8qv&1iLh3$%%ql~SH zhEiM43x*)|myx_T>CLB}r|x(tpc(~2Hp2E5hBS3OYJg=wsuvGdUlRCVmR3waFjwHf zW0yZ}+Sv`XAZInWl&fPM%U`xywd@nwd(t~QZ-^W5#q2vpkSu= zq5Rxk_l3I=;aN`AwFbMl$z6LKAUwsRT1wcqJAB90b5<`rbp#+uFA7v&@0?~ zs$&W=0J9wdrYqilY{kJzT}jCSS8hrTB@H0i)?)2bz&VcsVevh6oM1i4+I7=d)JM7o zYu;)QgUcpC8m3CJ*`tdqwI`{hF4XbR#pPmI;-awm5tjsGKJ!X=!i`+)Djdb6D@r2_ zQk}W(j=IHY8!5jwOE}RTG1P)nRB$+am#ZXLcM8O@ z|2B;ZQZoGAs17y~ zWEE%or^GOu%?7621|>d>H!a`^l^U|=g7jxDwGRgb6h0+w{ak>QJ$5Xt3O}*J+;Rb{ zw=3Lf*>GFOX(T=K^M>H+Qu^DTsk@#CL|t2+SOAuthSi>Js8VRbbZ{~Wykr-?E8F(| zDyBpIgvSL}!VR#M?q*kOQ?q(36x;@qL6a*Ej4NT8p`t85e|tReZKi`B1`c!zh|Ibt z*X?rQ5hKHPMk((d;Sgq+SVdIDP1lX}+2B3gtT(Zs73qVJ<%#{AP-7J*rn!t1 z^sS)Fmz|Hif{aOnkvQz2g=4QUdV08AGLt!<;6*`CC!W0V=e~#O>7{fNC(7bv^PWT3 zUx!lV6^$uutA=eQ|%8zaI`Q@I5~AcXQh6`^Lnjr(Jpe2q;hp?+n#USBX#}wEz9zvzS1_0POqTU0yo-gqSzx_S0KVNSn zpF6&$>ks$aBy$Rb?3i1AKjw`kABt_O(6qsT@x4E|dE%QvT4At=2$Le|zwp*m5Bn|b zL#1?FhS^LQTUFP>?%(9}zdUNqc3SS1e>ut+6RNR?lzfol!?3O%$gLYA{<*l`W68eJ zZ=5`YA(ZHyHO}?X~T#M;)sk1ojwXa`PfyJ|ZG)`ce-eVH)e;3(ZyO7u2-^GC# zuFL9RW6xL;Y}GV+2+|H;)?l*(m4|T}iMau>P?%L5D+FK0(3+x;W$~fj6W0Dz0mKOq zRw6f{xWh%bynf&!rfGbhvHRzwc?i2DAO#e|lyfMxY`z_GKB+#m^To7^JmnH23VD4K zgTr_q;_OKvDHa4a3632U9YcxX7JoStw!slVLpKYjGig2w)Y=P19c{J%8(4#*|0{vl zNsY*c({sdsRWJ)H0{_9f7>erWvmErBe035Yz7?7p>xk@!zp&{R!R9qg;>w+Zny!y!%qy2O~+R27Qo;|#nw1` z;~fqldP4@Bs@Lsq!C{$aLkXT~Po45#)LRJqJ|-c*5t|4Sqx^DFEbV0hixPa7Xt4i0 zm;xxr;gHsNOkERqxI)(*nAV8kDnWq{k3c1x{xREa*#!+dnvydMK9bD(3LX0DUJDS? zPB#AY!@iu>QzH&n&d|QnHEjz?GqJ^~U`Bj}>>=%A$TjGWZ8jOXPv#iS#7aY+{tG&50_aJTIgVTB>X!`g5ZqMl&(t${kElctwi7VDSE+58< z*~YY*+YldA&dRon_4&#+^w*tC?s+9F#!z1j{V`eRd&xp9|NeC%tQ2U7FnFvwfbw6{f}8s15rv^ltIJ4J{kM)=CMZ z3*ZF^?LqyKHLvfs8(ALr3Npb_Xx#uJ{b;{8Z7ca)y?N3XQ{o8vH>6@~Y()!Y$C3F} zG#5CwETYBku?!5m7! zCz(~kWMU5q1<&L-2QNjEUl#wIBm;}HFOl}RL2Sdd{B@n*Vq;yQ&D5j2P1Ao*g{Ud3 zn`oL?lDYBZ&XxU_QKCUKe?ni@I`gN1uRcEQ2wk7Ix6!F2#{)|Rk(p{2k(k<7i{JWi zpp20fx36d<-#71FDN3+|eg757nO`op-fca{bT>s>@MMaUEPmL5FiqQdP?$fLvnOrwvO_}MsEAY2-XZ4IUq}$vmS*x zi)%Jn@N%sh8o3TNHu5y<930e#z1ObSAHPSGz6+GT& z3>m~hMY=bO0{d=Wuo{?byrL_uAbh&63DB;9-sBWR-0kj*#DzmwsMXsT+#>&r;g{#2 z;!Fn3qKk|sMN_vtwQRA6J#GU1$o;P1&AM*Au-tt}#2OKn%Kv7#uj`|Hb4>f%u zSfRg8PwPH6%l7ZJhV~q?{&O{D4!CMZje1TOGSQWBbr%ca^i z^9t6=dM)5P8I(WBVl+uoWoCkswJb_<=S!w3hzH1$J6$Nd887V2>*XBp9Vy5fbRHLl zdg0t3Fi|;kMn^TJ5NFYMM)BV6*_jfc055;DFBr3L;1XB6z}1gHEk$}CsQ9px4J&*E z|6Dh4+x_54csRH4=YwlI5(EAyNV*-uWIH$ONGXaPApHu1sgGQDDnQ1}k@?*BGzQ;A zd-(RNku}cYhNGfxnevF|S2p+oJOYWpxzOLZ&#rBugQU)q1Uh^v*9gXX5HJN? z%IdX0&cGMG0IHsKp>6~S5X2|hegyYRe?xhH_T|qpvo@Dx;Ts9U- ziR=g}LLxVX^nyunY91$&k+nYRzQ*CuPX|Yg$=gh;34%Pxojj9t%D%V!P ze*OAh(Mp1(0`ZKrdVZ=Z{0RUEMH>w!%~;sJjUZu4ZA(O$kIIS; zjc`+8f|>=f*nHW8ltnsZM;CQ*W*3~6AA^zZSsfPTm40w6=|x{d3Va3`olfmx@D`sB zM;Sf^No32aTF|feon4gv3}W3L*M#5#2@W{t%E*lB`F?m84n;s+i4z(s=(-Gm8R?^H zPZ_78L>38)XqE!!Li%7C7YZufECej<+bSf2gUk0Mily}F^EmvQ=BM%GaxFsgdNLgjp;0@yd14Wp*@gqALpp0_!J?XV&R_ke9JFP zvFekz6#wLR`c^&!nAzuyg*fTbO?(j?dYNi1vO!GVcgnBlHO)JP7|2(9+JBzT?iIl7 zWMy{2Eq13ISO2K=;DG#+O`&%|WMpc5L>VlzB{r3J2=eO)@Ux_)`82Kpp&*pqQ!uSM zzEhOWT2V4Ig=T1rXL_f%Q&z){}b(}gDD?uJgm5V zy|Qbb`1fBH>V+5LTMz?w7@ zWAli8&rh{#AwA!xzKD6XvePxxS4Ik;hex}%UOOtI%PIlp`CWgVWrwepu`v8nzjtD;MBTL_b^!zWSnwm`o?;W7h# z!sAd|Mx2?gt%taU;yb13JcX*#r#VOja>w3N%gr8LT~9FmrXK zVWEPL(O>9Lajx@}$?r3`V{W+-X|;WqtAN zT%XL9XoIn)%#fq$?ku@{K`blD&VwBCdL4wtH-*V%72S4b8qsY+>~fk`mEf|VWwO*3 z1W%aCEoCifD<4wG{Cmzu_;

L056lZ#{g5e<(}(bF&g5=~%5OAid0Z#IS?~GMzs}Y1A{f`LSGCwWGtovaOr-&eKZHqVDO=I1=m*i;~(Q=k99p zU=B6iTKSF3L?<~rOgoF!CS>}|lj75^?j|0>u28ymVl;?%1b-d0Xn19^st}!`U9TmdSuxi4y`J^aY{OGb_^N0Qyaor|S7sOj~;m!$spq48>E2pFRKn zRsU>zA8j0ITP))mMu<6*qet^)T({m1<>qUjjKmPi7iapKQIyGM$>J2g`Hu4P2MA#t zfj#WPqyx5PH%Sxk5eViW#8q?N84PUw9y{|>TUzyc81)wG2+=Y>k72v`3A_5go^@yiDtOHnJ`?0|Ht~I!J(2^t<5173II6?SecY%P-|lIrqbBP z!Ctvr>rX!EN~ct?OQzr?q%Mu;t5t}A%XgIq?Tn9;)>Vsz;fI8hJ+F+Lp+!X%-1+~R zgR#Ty86R`{oX{+qF48YiDm(&k6{mwXDm>Q%_j7EiqK~CPq|9v33AyWsGz)?=U!v>9>D(q@K?hl*?r$Z`jI+&R*G6~zG18pUzDKX2-(K^yh?UjsLe4X zPSe0U-xz2l`0Lcp65-sva7~yG$1D4AX1=q-4i-G2jX}^HY4(aCn2hNW#>1QRw-#A6{Dvb0w9Oc%+eQ<0a?3Z#mpIaGUHIff0cp|VSAeDBN zE7}n)%N5`k(~1r^wO4Cz=h)* zF7N~*zT>&|Ohmev{&HbIj~V3~JR~|%q`kK1yk(o)O_*O~(I~;WsNb{EQC-i9=@x9^ z;-i_1ML(45J#inpFvo%-7K7GPnB|k(k6<2@bM42*^3klJQ``DL!SXR>ehTAi zgQ45ss2EPD6qJMNfMFNBT*!+wu;aW|Gu{`stiOMM)I$75C0$%E-9M8zI7@Gq60f7a z=dJy}K|=}4GWIAREcK{P4g$sbK>a~G5~kWPL$<+z1-0sgUs#;ZOQ=ln`NWpB>!NPD z1AhZsUmVC`vAPGCOZ;k43E*ni6209xP`MdMLOP>~y*Ym)EJYHOPs*#4>`F^3`J4rQ zQ*E6T!--;RN?Bo2ec(UprW73qBWB2qlMiX|N3WFAohQ8bQuN$u%FNa3<&TMyv5B=N z{XME+kBx$>D+lU_uTn))%U_E4CQ%P}oRZ}{#Qzs~5D*lIM>bkrppjbFwl(xgXxP$3 zuA$f)+=S`6`(~L{6kCA`*m&mmY`%=Ey?M*Mg{d{$j{1a&+=c9Dv)HpOiymBBvd_5W zLmq?}Ts(-=5!ijOKndAP{5w#rS8;)o_o3^1N91o7!S-4Yh=lLTiwZZtl_g?c6!B3n zL%)`-@G&wiSYNcRa9WX%VuWF&+IPan4Kk;(W^iCA%f%*vuE-jk$MV7E%Ad*x4=yGr*Os zw7VM1udPJ?TR@z4tUa%<7Vms7XQ5eAWX|U87Xqao{NQ(|{zPo?RnW}302#m1L@d;R z0oKQ@?_t&Xhuz<{mQr(S>f@HyudN(W;bsh^WJ@XDD$hIq<=OUcHiJU{Nvu67qPjxfxrSq=0)>92s ztIcUm^5ZO2Dpx(N+?Ay$!OCu;sh2xq!hTJP34;C)M+gc~!h`BC%EgwWbrqBOp04ce z(8o^KnOD=p=xfN`T%)AX+iLPPeS{#{oO@@7)|ekqzhyWGEhi(EQHf8t^U6kd-WS8b zld29l_#%#A320_s`G6kG{^<6)zCiQW<=jf8}x2+xdnB7zj8ii8E!cY+r< z@V}Y`NmS(?R^s!Gk*PkQX>3$yRrQ?v-A{e*Yg+=NMjBx3=x3v2DAJ8{2N|72CFLqp|H3+cp~~ z4H`DK@vZJ}KhJ*W@AYHNV~%l;>&7^*N5-@h4Or&RsX1kKnwIeJRh|WIw1La#f*~$- z+m|wDEs&I`5T8LSc*(lbN7#%p@n~rY9~x;Z1^gB?t?QGEAF3kcKRa5A8OAW#QtXZe z;=hC1?1$VhJ!4BeXsuxcJ@5332EwRTqzD7}0QG_W;D5q>Ao^zyHLD6oyCj${=nSej zP7U5b02Qw`+3n}k**1=5bIw)e&%g-F&6MnDbZ$l$VI8yBkue_f2stTQtD1AAiA0P) zQuQW+B0l$6!pi0AMHbHdyf`wj$T{$Y^v%Mz@Rc$kIelr5{Af+=3byr$29m_#9!2yx~7SZFHWEhbJpAhhKM zA^FbAv*EbSqFwan2%&0RB462oLOi zh5TGy=aQG#{)srWO12y-*IFMnLN^aB`c|TnwVXL5WU9Yvg!>Sit_0xp8lSJ8PF?&x zYI59aUD@H1*{HNJ2K(qMh|Or zGmzb~-~Vp|KxTuRZp9nu%PX%wh$MgRL(d>FdJV4VH|bPOuKOwtz)x za?;@A7(a^DrkSYtQ16#3LUllKv?cnk+8*0f#fLB$dln_h)GKuzMJdz4Ltw@FKlP3Z zjY&tTET~k!|6r0~aoj!bD>VcU-c--2dXKk{3N5wT_KG*dKmG~n`nG2xV19%~0ybB9u*3N2jWlsCbo$GWE;sa~DX}oC8$+kvSmzwR zT6PuNNXJ3IP?c7m)svDE#)3dhxD+@>drLd#>zYt&9s|3euP+%aVnqOmzx#(Uqjpq3I=C{o5WL&lf zHJ4&m{lmhVoD98oM%2gdv2@x7#o;5H`|H?WX(Fa)giduZ#XhVuhF_^7BArhcoC?5x zN|EzgszzofjJO(9IC45m#7dBMDAq;h+KVbjW@;i;!Y;fp&lQ(G$6Ls)2f+VYg&iZ< z9ugmY3QgND5~wT}pJ+B1P>EFecaI?Xbk@R^_@FuC{klkSabcU~i&rOaRV6mzKsav8 zsLm&vi_~1`%e(PBLTfxEdFoY~exDkZA=pdVsynWh!)Imx5n2HUs>7P+MJIh~v!w+8Pn-cMK;=nu={-3Tx8VjP} z6$^SoW8WJoF}I3}Ehi?zpdRbPN0>t6b%hQ#_)_NzEwsRo;+Mtn-DiR3He-#z*jS`_ z-Dtf9U2W6JaCLqqb~*#5>5K_i>O$NUigI4^sVsW1QgI;7i?vxcG#7i*1s=BRI#ULV zLz!Jo2@%(uVo0HIs0vRmmioZwKI5DOeC(bT(VHIY*a91)_9a@RV;i zbC?BTz1)->R+cS`a1IyPeX0@)^N*U4Lo-Oz*%bi(hJ%!MfpHERS|%zqz?>tC5aVUx z9#U*_E!9pmUJtM1w76%+w4=kQ6UOkd)e9Fk+{DwfvFP&e0BFAfX~;Jw6%JD0x zP;84)p6*<_w+WIaZ&Wg4HkW&O)xb5Zt?#HI60=tE#sM0cwczCpyRrmfYJ1UwD*DlG?u3deA%N)g|hmM@6ph3E?T?IfB3*P&Ro?00l8+@_Zu(M9M#w zi5_MXUyxtblTIr*V;cP7q#aA=o0&R;PKLm#dOxy1#WjJ&R zkF86I2UU?XqaIAVBJq$mczCH2$47t2`mco+8>?rFP476ufw`EsB0*Gfnb;W#q|M*b z^sRMt-M&M0$M0Il&{Oo~3?PWL))Vf&02#Ty@rWPtsD%dfB%Br091QcpY>K}-Td8iw z`yp(Y#i!^kmAEw^^BW>onSQFprPB!{Kf4 zy0p2a5`7TT+Ktg*F!&h8{5SlgBq9{h6o&C#_Tj-LMHJJu2l)fkeH`&1W>;MKo?Ol) zW3gDft5#jqk9a}g@7(@+1@f72-=8P6BlwX`t) zbHv^Wd8Y;7nM&d6c>tI?>$i3C~p*)n%y$oGH|1 zOKqbUbvSq@z9cf>;JW%X4u?FhMsf`%Kei7Iu#^;v?Xmd+daLl@t8a&`O={hFW^$0^ z3TSKGqGd4tw3~ho<^=FwyF#)JKb8}l&L}t9P;1N$-YZ!KqIoRo)C;mm3bvfvW zLwG0FG)DW$L!IBSin6~i%DnjhLAezhkv7^!ySWXf%j()6(?xe9gZrMbGvwLSa-NLG7lGJBp6A>S)Tcqwj`#{*c=qp%aYJ$tRI#Kz{R&K+3acv zn4H0HPDnL46aA^&N;#vtU#seECVI!Rd{!O&!uc}hP@SHS_JW#Dqz2^yF?dtM=%s|C znB`YtqJfMult9D9*)WVp<+y9D3se8#7U)8qgJ+dK9o)Z#=rNKzHP{CAw0fDxs`WCV zjKKxrFSCI_ln1>6qEktxocfhfUR zAt9-B1B?S3;G#uiBlj8$h&{P=>Xk6bYxSq2+JeXG#a2|si*9~XEi$pfTyLyS+sJ;) zNu%rlhOVIdb%&b>tWO7#oz=Su?3DAQe`GSAPKqbnpELa*eJch2V_X}`I=H!rg&H_M z)1Du&R-em<8$`iI+m<~vX3-zo9A9gWFN*1%RK&LSDVM4j5?!KlQU|pfK@8VhRjX-8 z(!~tbJtaet4P$s+=Eveml}HSI6&}S}fz6W8NvFZ$k*HIyDl~AA4$<2dd=XccA;9Oi z6jlCMr+Ttk{C@BS`namF-E*j2yBr$_)RQNsyY2i+9DfNCBM#(O_-Ywr7qV^}s+6~l z@~jJXx^VA&@?Mu>Ax#b!Je!a z)2~=X=mQ-2q`)E_i_JPktA9ODNc*gl5p=NXeG-Gv~C_rVD0Ab zIJS!|Ks7CsRRE6du`ZzGIIFD;tInUDUAmM0czPW+1b40p&9=xPJpRG*)k!VSVnVWA zsca-oWiQ&Ol-B<+O^FBN*tM^ue-kSpxo)t?fYyK`^XD|G3Nf~ATatc@jr-!g2H$~m z@mW$ z$wf0Z4NiGKa|7TsGa+da3LUq8VE+jO>wp>59i%BGEgm@BTrincgef_;o^xMwgptmi zB&`bh>MR=$Bwhf7!QHVnnwkvna(j8}s8eik%d8|Gvd7N^t~?^P&lTyzeUQbAx>i=24CpcEL16V`W+SU^hNE&UTLQ8pZnPXB5t5xM z>A?fdzPQpb+bmo}Xa5e0l$++ zdy?n77rfGr@g-zA1=x3C15TPrjsKb{r`BpH&`08N0AGDnSCWyXYPrT)^(xwPoO5}7 zRecWeoOSrfj}*+EX87w#%RCjDF1t`Y%Zd+Ndeh1_C|b;q;U9^>6}%~|$U+~~h~+)a zMG)>hJ8pA4&fw_D5q8L|)eb4usNz$YhlkUyi*P2U7=0OVDlY5eEs+m zXT^qWgV|nmsyh`QDlGXxtn^3-?%AVX45kOj<>n3gv@#|GK4*q73%~&s9csflDuYpb zEahzMY?_7dfVP|_=`vh5XhR^4ljC>s>T>+Pb!0KV*-iU_i!j%b|0VMw$Oz&Sj<(US z^%E5L?dFbVf|U`tbL-Hwvi|UD;n2*dfQk1k1`qp?DcRDPD`D=c8D6m6`27Q5A4Kbxf00+mv`DtQjnYv z+#}GMv@xKcAvPzqieTt~T-^oC%#q6h?YraFjDv3vyeyyrHc({@b>Gc{ zDiVvV@R5WVYe4>dJgdhV1qB72B#Y%}KO@Ol&M{ieMKoANNDqz7r;C)ul#rX4Y*@uG z)h3%=<~ik(@d3Uj(iU5#?n!UH0E2P|vl{=#Rr~?6JItl1VrOew#+w_U^dYgS z;m)U2EVNMd9pSAc>F#8&AeYejZ56VMmQLpzdJ8heZm72ksr+HJZOi z@k@o>1k#|9%>3a4$)CqM=ESpR6k?J+i`ZhjN1n+|=l`i9+}kX=I}02cQxS@~A#y8p zm+$Wq?~v!%yoRkme;{P)p*u{3g$k2|aouWGgf8{@3MFf5DgXGSZa9W-N(}qVg6XLA zC$J`blV$4vG@VIS~xMv-**W0|iUbqC@5sAow@viObzrvuf=nQ!%VsDLPL zyRG&5R8fZY&r5`|0huDJ=me`N5@=+4nGEHGWYbfJ)>%A@?Nae!V!pX-57m^^u-_QG zem**j`w^+2GGJ#yPp-x)+zrH#WYbWx{)kJtAG%Rv@0r*#?M4o`tk}i|XNUmye?MFn zL1G6)U<~@&!~Do-dJL8tg1r<>NlZI}VexNTKsxGQ#ynewRbpdvPSm)p zsSa;kWhL%J(~y4+(%Eq=vh&x8r#O`^hAPBaMCJ8cD>e5J!1wj~T#5yKQSKWSB()&~ z8~{rx8dUeeL)`nC#Fy_pW=c4B?``*V?wNnEBiTtV-=)v28IwPAdZ7macmk6v;}_h- z3uv3{FYAiO|7jm?7YR1CMh!6Kb3vKcRLl0tU;&^bmo#yLC3DYH=rDt#nE`XKL{?Wr zr<6h)j^R~vu_b6x95NEska4D{LYOTy^CZJgcY@S}tb2L-wMFr>S6nt!Th4kUs^H@+ zo{B}^I>KmHSlQwwD{Vr6(K7A$)-svOH1^wXYbd?ovqeE z%hMspy=oamEZfM|6swg@{%6-=WFgGAB2cb95PhV`BvaK94NVPA=j4g9wq}z9hTM({ znLQ0pi7a61abS$gBPm^qn2f(WdE{&hy|0!ZTYtst8)f=rf}4VT2WSCQE}n(*fnT|) z9vx*aA^mVQXPWH9!9r=J#k-4c+ymJXqj4CTP+dgaf=zks*zims_*AGN$&oGbM(MM& zlAVc*c)tse-VQaoRzaB1!1LkHV)f^ty3r|>hczNCA66##fSa)aUo0-BD4n0?U0|xsBC9}*W7#dSXq0Bd0(|?i5HtmO&^=F`t_w9FPIW^ zK^Zmrji7|jd*bGj7E5i0*t)vARf==%5f8`251NaA&a@=)Vp~Z2dVN^SU8#ngo3=7o ztkpKifwj`01K|NLZCtZG8KO~JOXtzrM1H*tSAFtv+jT(}1DIN)NhyE8H8e0|i~*)u z&1FW`OQ+OQ))8Ugvr`H6alYJv@#u$agFgZqQ3DyFh=^)?AM=f);Pwd}D@XKUT1_I1 zvJh~=obao~@`k%}`0^C<%|j<;mWaK{vq4LMMw-7^AZDDrs#w+-336Fg?VsqY?OnLEnVb|TP)yT zRf4PF!(LPGnUDl25MI%-*2QOEC%q1~>P^_OdGI}!p258&U?zfQL`NmpIXF9Y%%&QV z;CO}j3^PmK`pBfDMw3+syl9QKO~}E3PuKs`G7Xlx%qnNv*wx??8}&!_!uOQ<1jz#B zim`|tkT=)XugMOdV<)6=D8RJ*FgSIiWm#gCYqZ!V3{K@!Vj8T@$&W(Jvfk7JZI1dT zlw&*nnKqgkgqo7)4V*c)Ek{+vwi%LkbRODo6T>Jb>Q+&1rDjNdny`O@fHV(3Dvx>Z z7|bA+Xuska&}JA0>+B>G>@4Bu>~EX39vV@X#vi2sG%rb@PWZ%O{^E1h*;@*T1hbe zR9}KIx?AJsOS`QaXm~0U%*2za-&mECzwgQ(Vihl&8W6@8H))$Gp*z9Hm5pGZj~P-= z4)HAI-yu&XOa5npRtHepL*Q~jS4oEO|6x^edbqKZtLkzOnNWleFLgC13!CbnE63tyt`7VXBi3MBd@;Uf?4qA z$OsCPx$50T^RvA%5^T&YNtfSEpDYe%m!~#*D~LuN z3-6J-Wfj`^-hp)^(ULA?QpCws;XMKWh%iX%NDMmEn0Esgfg3}=Yo!ij-bvG+lD;B( zuKl_60vTBF@OdQPVp;XgkMY7Q0nrACp;N7F{ofhngjB~^3NHoI)>Se6e;Ed59cqF= zl0+25Lu=-U`2wCue3@|V^WK=LeI{(G#+J{6=uH5v+`j=8RzzYQ z)EOi{I+FGeSytjqEU{z~LR5=ks2mFC6@P?4TQF~^_27zZWDl40bDScIGf!|M41++| zs{3QL#E$kS21&!)7l8nSWL=?J$UR?NT`jHtDE@%}XDchPWXPd}k#;(&N@*FeGL%O) zXy9G*ZBpYy=5Dj|6b_?ZYov#8jHS&&mt_BhX8r|CZSw%K-|BNnIwM^$e-R$v?~LM7 zGm;Hdv>5xJ)l(xy?Z;hIKem&SG?atrHMGLOvESq#p3mM-LsDWRu~5z!kyUbjKK>%W zu3zYrYz7TEwf~>~C0^6MZM;``B!R}S>XfMM4a81a6`4ifgl@BVQH`!~_+8(KJ+)2^ z%5?hWf^!uX9j4DS8_OBU^qMn>AsG%6d)ZEjr$026vy5iU6z349*Gm><=C$b8bCgU@WgDG|7#)u=U-t78p(J5tpuoK=X->OS_$-{@U6~TyBh0gn(%qVGuc8}N0MM4(#+pHvO z3!&^g!qUxHvvaS^!O5uk%DW`85n`x>ym*kh7D5F7r`pi8as#%n2Dfdut>r+@vshZ( z(1M|1#n=Dq#8qq=uY(@#1rYs$+$63QUT2BH4 zFS8uRFaQ8VX62zZ(?3+{qe0;E37CjVllqadJJ>o@i)@dUSz6Hz8lrj=AXI>+c*3|g zRT5@1u$LncO{CuYHpY!5shfJO-7p2Q-c#}8rU*b*X}2Ex9uIYom}3DE1rgyP!Pe-k zfOx)uqni#8k5be&kP91JO8@Cq>#nlo9nL{#$=~Q{cm3<8jmkye!N%|t2lm8DlIBfW zf~rQv|GGa&Io4394J=@o8&$BT1mQ9UKlQ^UYwDdwBFC^ai++m&CY9;20;)ommds`G zLnOXXJ1V!5TYecyH!3K>9|&Z7n^Qm(xuN+z*(cA~c*7U|f6aN*29a zGAXg$<_DP{v!aNyB~4DWv);JcnQrkE4dH@OT8pk$ABI#)2$r*0v7D~DsbEQJ9hehP+BA;r zf`zEM3EvKb1KTn?pQJxGKzDAW<3`mHE*1)2FcoJ(^F6{Uy-_F=(o;GRiKscf63%5M z_Sh*C)4FUN7I7Uz<1nyJfWo%6i>dsOyAL>-Ve6irdTnEs&cL9xty?(_i{dcRI&3wY zgAOzB0-4pAE-WP8DA5f*?~%!&aozLt+1sTw5tQCyxs?@sWs9%t4W|*gCa7+F3h@LelaSau?fZHIbex`Wfxr7ju}KOnOxKa_A4M#ISLkBoHkFcWR_P9?)9FV*@k@w z)2$LFvyNm-J`$(qESG7yKewJcNzvS%e8z0hz0Dx5Qq(zB+T47@Hbu{+%&4b4(o%Nq0W9`>O%yK3^ z@LPHh)s`}o!t^f!RiH9e0oRP>*ft#@jzK0d&m7Jk(u`fS3^e6b#?{8Lf*}ss4+}8V z2X0y|F-?LsxGOzum0p_;Hu2x8C86>(6f=Y*%he6X(HBk0wT34Kn5@lhXKEqRPQD1Q z>o#gRJGGyvLnYd!>gC$J%i9#NqI_8GtcuBg^Oay}{4(d3n9XJ?Z}~QjbnU5Q(V3M} zPQd{GN^AHPhdFv}Ef9Nn1=TsU;R{rdFFI&qFJz7L^ZC(&(bT1&Kw;l1-0!o=poBo( z&$Im)(=L{??DwiRL4jBnBMh#&pJg+&9*aW(RKw6nRBeX=YWyBAGUtA+NnXU!_02i3 zPMKRt>{7QmxL$IX3W+rFGV)X;vqB6VwJ8$XOQS}>%)JZz)jGb*Z~q+o;2Vf}t22{7 zCAo9O{ZkW4AW83{(wcrccC%i~vBZ#DnsBUF6;j#uKCk!R5U_&97I9kUG^mxeiis{0 z20drbsliOcP1M0P)fy9wZpn`V%zpXqXL;(ijc3H z*r%pDL3`|b-+7%#h7?tuLe9<~eNHwq-fN_Q&aR_-ljbhr87V?mWJZ%8%h!wEVFMP71+ z&4t{exV3W8dza>W_xJf0(vp1R{qJDV>V@9VPEJpcX4u#1GxYVxS*iz5bzOovoN&`Z zN>tmOVnmy?*H2#DPrXx4t0{|rZkRoMpLbLC9OAOZ@6=_}vNir&3eJnL7{6#o;SAb;xb#i zyv5gRHCCI#oU&7*q-WJ_muX`>({mHi%_j_#;_J?5rN%;n`Y+MH6dEc`#|h7VW@<3_ zT^a}@ubw*<1nJ;OzQqP zp<|>-w}6IkMl=;Xh`BUy>M$Qh>>y(=VezOKl#*n%${GjoW7xh3m)v;-2Dhi_udbAU zN7jbU%6`k&3jF4(zkTTJ%8jzJJ+vKS0_Ye=4#k+x?mgwBS2d_CDDz7PTlz7X ze+e|}=i7%D86>15l-$x^VEHX=t~HEQ{p8(@WvZmbhOYZrzbO1AD=FJSl)P)q@GP-> z&343Js;eoDTPkSul*p>8tfBqFX(9Gg;OlIT+U8Zai^szVXi_*AiH9TjgU>_fvHcFH z25hfNVf5-($+`J~=~?oh@s4R(?_=fMM7ORJo$K3D0iB%NB5En2zAKV84i%henE+-$ zC(~F)v3;2$i6#a?=21M_J`F#)ki@xLQnaczdue2H0|Td{n`QsLzTM z+FhEtI@HOgkQQXaSuOaNs9ngy<67pOmZ>{av%EmF%C{74D9ys)4c%sB~NA53M4$%!cm=UYir1 z_EmmLHD%13FU56}qvrc!9bxPV?Q3$GEQ|IS$f1}i)vVhYHZLLGA_W*o*Z2vOiWlRI z<`SmwFBWZSY8}@}RypLoX)!+RTBkeX#X(`lJf#@mp-`2S(g!wb71Rb&TChTA z4t#8J^RDJ?r;P6q?fAa5Y&7Y&53dGlouBSaZ}E}L_#6xCUT@eq&UrL>!%mz1-Gbgy zpZOP7t%onLqpCJsG=&|2U^PZQKG}M-w6w3>y8d?hdR0K+xG{5r&3bdT%gg7Qi*2DM z+^nOM4a?f1*mMULZWzgUF3B%ct|gn_W@Solzgd+y)raq@Nik|8lMKzJB=}+TsMroT zS{a~G%m{@+OJ#)=22?GF8cv$_zitJN*NIcW2QpPm(koB(3D7_Aq&*$W0qicw)ouRR z3x+&uVns0iKKa}Sti%LlXcxdb20{%idas9ga8)9xr8T*c44;z%a&*P)pbfcZ(UDF< zn8uCza1zrfFBNPi9*Bzw_^TO5tN3f&>r!yJbGQ;bI;7Eb4o7|(2mVTku!%9}tyBtD z(g{MyH0U&FDqLcpFA1>j8vEggLO`>(t#6VFHVJ+CpXJ^mO+SFEux_Dj zus=q22z5^!eZ3<;wI}P>OoMzDYex7m!Y@X*skPa`;I_Fu&e5&kvEe>N@$orF$LsMg z0(sBp!fVTc@6fB~!E3+sW}4?vU;YwY zJqF2%NJpZst}vDoLC^^gy;~gnV4s)!F`lg_IijtLwnHL-mR1rWkLzgDvM#OO+oB%5 zJc#=pEoy@hfAFir?Pvcr$elv#FVCFIe%qgpo_-7xTf6tLXB(0+p0=h_srfU>5y*z3 zr#}f3dg_G$O zpi0|e_1y^lJKxWOj!|CrU(KTkM{4UuRwZ;le@~2`=V2imV1{Eg_%AM|ujfmlpgMy> zqN7o&*kVLv0RfY-lfKx+HE)tpBU;1BS#dH6-7A#2iMkd@DHq;BzuAb_=IQCC3tXvTLg(M3h^~u1G%wIcH1+5 ziRXgXH3lxdX;YGxrzX(k#EgVlbQuq}{rj}E?)5$`0c%#47{znXYw8Et$6tQguIDt* z<`q)gaYf~G?#EHwd$_L9Hh%XGzWX*FIP~Vl$NR-$z&XgTBMXH6e(fin`8z(&iGLa9 z_ZW7)_3`4DI^%u7lF1J>QtJoev6k~5a?KfS@@ zqgTJ-^&SIX?|R;s+hR~y-uFTO1hj}W!dAyAdG$PYn0y=BfB>e%)`vb z>r9gK?w&rm|LI2z2y-0!$SqIV{&JDp_+nr~7F?R{vPos=%FdBWmr%Hb!=q_ACp!An zCI=e5pTj^n5T+d~i5VzJP+UJ-ioqJg%?rdq8O$RNh1&Jij9aJsEMG$V+JXqp$%%9G z;NCq;#JoF6>@!2MI|j?%&!BcQ^TI5^H$z9Ekk42^Pr1LYOtt!LGjj$LILdg_{d%)G zsdqy76e{H#M2rFRn_`Bd_Jkr&f5&Ci5|14igzOnM=o)jjn)J-e{2}-`3Nv(a5?K;G z<>q}wT+BTI)5JNL7;qmWqUyoguJw0sQd zz;c&QvhP`q=RF*}<}|yu;}~VV)iSV|s1xLFk^*A=Z|y$B`J5W{g#V{Jl6BU) zpQ3f-0);#9?fk2}2i!-<^Yf8}K*pBe^UvH8l*?U^LkEQ&$AFB>`_C7}E9Fki{@jtl;G?_Vy zK(TTE<2CnV;G!;DVI~gZ68Q1f^U?A;&-jK0I;N);D(%|;P;JK93}6O>Y>CSJvNVz#cs(ec;=x0t`42JG9 z%mc#+kJr+e$ytld98*%Oz`p&~VsntZ7!6BMYvr6Sxokm`!4OvCf{OoT5J8_Gk#t7J zk)&pqbWMipZXeM3s^4vA=L>tOdy!H+Cd=Nyv71JUHkW~+ZnEUVs5|vnncr5YW zJHWl;yQC@Y7;OzV&BPiNc{EIb9R*d9h50A;3dA~&%QUf;@igSejKz~`RM%SjJO{SU zIuy@d-G^R##f(0G6p7t-La?PlHPOCQxciGO`8#yWZwsrbJ+J#J=^u}AA88PH0t5G# z5AT~>yNk{1wjD9-k$%_m?_;Q`@C7fGJ=v*JbcfzvumLaC`E+ z%<6e@`v7`_d`GC|6<%H5`-oq`wurssmY@D2?+YZIxj~mMs(^)0GZUwyErRH@l%t!t z{TVhtPUsKTNc*|ijRgs#Wk|4c%PR5e8d~-1v zT;T-IA@=LqOM}@`QycG%tHv(pe9{`L_>0@#kak*Xc51rReNi`^z(Y>wJG8N;UUF)f zeGFSn4ZK42!{NS8c^es^Zkifw{pLd_#jlm4X1nh-uZnd9`WlKa4P@<_m$Uy#?L)zr z%<+o(yP4Ncw@eyQNog<-bx%A)M*Siw(@wx; z54T4|-tRP#_vY%m<3MtZwhN$PKgh2(t8@45gctRwz2U?dGPTt{o_7tKU;pp8f47SL z!!Nf9Xn{u|U*yCWhQ zr_SkCVw&ccaGk}_0{Cx`q8A`FGoz@wJLSI&{TZ=W?YaQF>!@s|?>TF`=W&R!M0Q}T zp+_zYXPf_1&5eznS7w%NnH_+(Ej>pu{kO$Fjmi)Dkf7y`ax+VH7^Bk#x?Z1HMvT&0 ziK2Ar2va0)8xAHO#nb?7)J_wZU`k#8l4*##7w zpO50(3+b+gWJN~*F-WO!c97RyKD-We9cQha0|^b%JoHWjOK3M4b2fQ-XA+cPf(jQ) z^z5IL9&+3y;VIZ(=FKAlxH3c7a3Dne*=c$!Bakp1IZ?WfIGrpsILqc+pYpU2hvht= zm9J#){lj>@2hA!*F=Kfi6~$oZZ;ATtr2tNi|bL$gM!mO`uMCUBmBuF2TU zjfad4_Wmm3wAyx-*Lmx)ejRYSF%dNk_Gi=UVjmjz2LiNh5I6YtfFX<6=NyhQoX72c z%BgG~xAEds-Eria0eZ49@RNZ&kdTNe~3L`SsgDpNTou z^?aIm55C258F#rOosV!|l7~kk7 zlXv)w{bax**hnAfEW7GNZibFq9Qw4$wyfQ-!+iPt_878z8KK<;_2)f3_X*dQWeJ?@ zH6Pgrv;^_}4mR9(V>0<|t2lGN^a*%CU^4T_j@g4XoR|`j$n*tqGD8P5E`k%$R2xX$ zN>wVwL?0sENApht$*DzOzws@2q-z4vjOWVTqYMT|-HnSzt;s#acS(v>M8AooLoLfZ z96k^Vq2W+;g4z#F>0z7kTk9QvT!58;&F&YKhU!b-i-qw0*T<5vf^VLNI3XNUaqG0T zZcHKMy(N8@$KVzFroW?GV=9C`FgG_>Yf}3b&H>lGwej>{uC{(hMZ^PDxFT~txI?h88 zG_EPAK41~Nm>dEk)$osOM=u0k%s!$A5uvq5J|2lbM122Z`G|!%ncAsA z`HAe~vOO#k1g0@wFmg~xn#_p*q&N)+zaam0LY)05G%?0G_+S7L^qhLGX=>|L z_T8(<7#fB5)dxgSq<+}Crr#1)y>;KAo;F%1h$Z%w;Uw5ycj#O}Aem8Ex#GSe2#1u* z%Y9xbou3XwcdK%1SdLf@H{$RgA+!VCjfLJe7=9+T?se*#vH)yLMk@s z(1@O+47O&v*pmMT&-99K5YrzgQh&#~;B4;EZf=@xg3F;fh9c9O9w3z?{>#BN7Hb=F zjOC`#W-dY`#HsSYwh>1xC4F_ngc7gZkz%Euaf_ox+oI-h`h!e>8~yajQ12ZdVDKZG z!IQGaE{<~w!8KMefwQqfmpr4!aFLxI>3BA={!!+K$f8uL$whnMe32}}N^dh?n%K6D zN63Q-nol=YuJNDF@kIA*4uW@!Dojf!9mcO*vA&`CLMsZzBFyrQUl(A?Tp1D4j0gHe z8W>pyaC=5$RGo}yoDBJoW_v8&U=3|Vahu|&ia6Sw;;-mv-g@u5-+ZjDyX7aFvwDeQ z@oTIk^F~q-y4?$D!ZojTcb3A8;8=;b>dq^2pidtqH}FTuHBzII=N zfvX4wK>c#AftB)yn1A;z0)$-!u_N3WuL1&*dAw1!g9uMXP8`T$-Xct%?9qM{ z{S`=jsnBRTKaK(e3&H&kTj4@>aow4DhoG*Cx=4%px$5jZiSxKnK7AfQ6Lz>naRos48U2JGT@{`|BQ z22pARIr^UJvzqEaECLgNjsH6|$%o*>KFgIBE_RIinJ5dx`V$icVGXz}lFY*F7eiP7 zg=4th-QzLQOvv4pawiCB=c& zJ{|a6VGRsz$R^>9;tKC2sQARvR`+1JCfAb-mCs~|si7YfQyLy+Ce&;LWd;i+#B*H4uBld;t^=d(qte5X75L#z2WPPKM3^@aswHM2?(;&uDAtkaL}0;|ZfoK= zlhHt3d^?Kh3|Y|cUoVu57+kolE)(RdCo;F1hC_RlBa(|rYTU<=xcm;WbA=e?gLt2Y z%R6_$PfCq>;>W=RZpj&}%G`6M^wFNpdjpPeXWXG|J}Z2+CxJ?TZ4h{2*+dS(Oyv}^ z=HT8P2}AGxYvh>{IUt6uGZ^%3^c$2!ZRgfg_SREDw8}>4T>vjX$TO9yZ}3{kG5?3) zH{UZWz3+^KR6|!gEvT1$p~y%C{(s?~3DV3z?5r7o9Q!>Ye~?Bn^DCethxFl;?Z|D{ z#gMcM+$jr)sd`m1D~$Axec~@d-Gb;LTZ%(R5|!Qohu7s7g*D8~FWPma;~%^jE45KwDluZEj1t>X1;rCyKV2i@42z8;lydqwhl1+5O1!p(vkU5V|j)9lV2E9mU(BC4<4+u*xyd zX5eqQG6gbje1ilsge%N(g0Z(rX}H$vX^Q#-zAteEXG{ja?(gX z4OJhFgb%H1%;YH}639D{DMKtJR^!+)%2=`r6mUCs5ceXLV?6TsQ&oNFZJwh^5X;qrSyPNv^YaAd^Lc9H` zcbgJh&C!LbwZ(5|0fA|=hi6=Cvw>>swcyOG(Hv5GWXE!3r#B1?hLyo1aP%{dri0)R zms0arBwGdiyqs7Y6u-lZSHg;saE;FaAsHtSc+dcC_Mtu$XI!0t--@m6BEkE{^ z7|A*(i5;s0q6>lL*k8S$kt(jU4<+QgPpdcPvfE@}h~GW*%|1R98`a@Er-6U}73id! zs$cYv42*Cz&g0f+>EqtIE#XiE1g_l_3OpAQ{BYRr>f?6@!94qs9lW=(LARIetv@~W zJM0L&hJExV8NX(?MMhyjmt~O*{x!7{vsx1mo@?QpG`JEPK>Pn#ItRwM9;XY(R^!G=W81cxq>XLc<~C}aG&b7UHa5w|HaE5#>rH>} zf4{=qJ3DvgoaY2A4c4~BuExR;eQe~};A9zRSe#GPid4FXDUPP1^Z@qWYH5nHqJ+5w@6qx$u}Bc{bwG}#uTzjFVbg|aDMy3sM}6HidkPH2f==sV^AyjoO1>p zLY_|QQ=ozBbfk8>kv^BvfnJak_=#=2)Ojl7?wj+cwplDxMqFHdIyo8+sc9q{(ejNM zXYO6mO^-$Kjn}Jm?&Mr?MH;cL>mvr2M=6>29U(_wrnoi_irf}+=98PsO&NGiEcL^a z>Z~j_2KXVmGUYg!`1T~ooH)v;7>Mp%(~4M8ImpZA;L+F`Iy)FWkAYx(dW>IMq)J54 z-o=Eab!i^==#oVaJ(ew>M6+tq>_`_p>4TCYV@9k7pWiMy(Lffe_y0-{_oBb0h474B zmt&mN#|8}rUFEd(0EG-4%TklvS`t)lVKobF!~#ToJ%Ve2Y9iFk6WtYgf1FI(rJzRx zDU)(bM^S8V8%aP9esUs&6m}TXhv3x+N!zx;SE8>86kTBJ@Z-#cA%XWr{^cLAf?oPfU3&Vf5XB=~k`y-#^MDzO>ss!V9`~zG{ zXBzOnb7Wu4C!S+!zfQ1|oMjh+$7lLHX?VtT-j!$y0uWr5VTsiB<+t|2pI(P-K=|>W zh`%LZMw9PMmNV<@`OuGnTu?=i@&uUI@o@J|=$Ijlv)$P&=rw`^6T()k)zFh6-%rfx zSJY^w3nWs*%;Go@w0Q1L1qM#}b|T|)MpPYi`2VCvg9NY3q)Aq0hlvfBo_K{i{F^uD zhv9{5nGJ7G&n$QCA!?58)OTci%vV$)Pjh3MoI5tT+s%CHG!Z^lPT0H}4{|5X{;SPbKhY~t(wdzxQN_>X zBx*c*B~bl9o_?Uua{drbr$JO`Nc=+_2t~eG#^yovv1h=u*g{l@?RsNIKd=ox0+*dB z{?9NfnB&*!Ay!$Y2%ezN*fe_}7DnU0LW2CLi&k!gkz64lJ z6PdL6v2a$XUI?@^XdgKXHkW{^@~Sqkdc?@a9A;Xa@GjPC?V3k>#SJqpt!%b8*DZIaTq5;RfDY_++&C zYlZ@65kJF3O$a47desy%K!~s%7FaVafwrkYNVl*mNm+E`7cgyC><%#F5y4G`%&;g8 zq;eW}(=S?=N&|ly>t|N$!;Z$|i&gK>&xz#__iY=4rLKvqS+z+q_VYtPy{u z0b>qW(5!x_?FbO;i0WSD5NuHO)H#2)Ms9dhH-&~Es;pvyv0&!Bz92b%`5tZF%N*t( zY9WPyi|-cZ5=a>|)>ci_Px!VAWUHcsAHIJ@$mLXXK%h79f!>Zw%8W5lzVZ&{!yJnh z4VFHJKlLvm7V&50j-(KJN+|Z{AS(=&`iM%h;iILsX*W{miEx=oY)|7HY^h=+!Iw+> zR!Yn<>_DxWUYOXf-fQ(KSu=6h!Xz3B2L;*0fqA}66ky7X%X9F2G5mb~?XmuPmU6^h zXf7mPxtvfdl2e%CZ0(E968zbCHr))rS4kGhCcyO?C+r&xk<2Z=M9+tU*hixFy#ZGr}_D7Cv&pSlxcmTsDs#I~UU-)AId8 zJ=V$@{{m=2522K7Yi5wY^D@R4RD46a!fZ2Zoo?)$8LVca4OX%^FtTdSVl%?pys1^a zfF6vT;vng!)YS9y`wH+ngcV?D-M^N0=(Kf~XQW0q3w@sT9ODQ!NPr4O-vYbf%!Qej z5{B#e3O3f|nEphUQmg~&HqZCmf$GgM^Dy zxbvTuVQ;SK-kze$d3RiO<8dPi&L87r?<;0^nS~T`I~T0+Y47@mr-hyPCXB09 zQVr7?F+b=PO$M3gke_mj#|v+6E)G|xS`_w$sHpEAxo^0*b?abxCzPZLUAgB2TB ztO3z9kLutQHEI7m74iq4M>_a3VX*IUU{yZ1Dkqvp6UCbI!)3Tk#y1+3CXTBxe&UX{)e0g% zb=K4aVkPrcCZePlX~sDL|38;Kz{-vrz=;ce@cqT{sIB)^z2>fDl?TgLz}nxTNWqq7 z)9*tXx`S5&cU^Ild)7Qm!%fK?j>UY0VDC|~=F2r=gW8iI%_sc}AH2@Fv=Wh=2tE|9 z5J}(&D5;*SkM0! z!WYAF9u)8TxUju0s0o;Ftjo8{B+I#BYDtr7R9O?)y0FwocTAbk*JlHqmTI^gb(oei zqU~PNAHG%|q9_pS`!>f3a%f9iSTSN0nY9 zmvqQc_qf!iIOvtZsYIqj_i@4ehjnn zYPLkK^IoSom)%|OcY(XAiW^Id@}sY{=Nm%K>2moF@UcSo8*|5cyd1xI5vaLc^?hDP zZemhd#B=Y3E$slvRY|Hk}ur& zv@_;|2HFC~b@+)ruh3kX&eR3ixvJ?De-$}2<5R;0a)OOUjdyxJE7n9O^mF{u@#Hpp zSAs|66C0)$=c&v6j7V_1-0T-{GQ5cI1<1WDDHVL1R&c4KDbUnk81hGVPyZ0=Rf$(u z(L7!jA6!4kL(;kYR3&g@gL=YelV$yUt z|GnG+Ye1azI*(xmh}Oh~ivnjLuF*q3!>UI(0qs@Vor!>m5Mf7Psli%=wiNG*d{nK51hGBwe)AXvBybOU4u zpd1Fp>1CUfOAr>Z34E5)9e?FlxW)Hbc^O-23R z+72p-7QtW(Ed!+M7Q#O+_x{8Tt<9JZuWgSDQW`s^8~d}CJ3Ya^dOzzvJ2i>CfqNh^ zQT=%zc7k97$qBU6{B%tJ0fKnC;D*nPL? z<@cimf+EJ0CU((_DWdsC?F)vvSGQOVD99+dSLmMfxvQrZB?TwmX=!?GBxHwmhDphC zcJo0C74x0&W67yjU|b16io2iopCNJe>?jyXfiP>yOv(;X1Z0wnK}Ykl#sI@$tOrUe z?Q7Uwh=uSw(4Hn`g`yu%IHi%+a3@dKL%wth- z?`t`^uH}6A^z_TgTE#izFI1CmX#18`;m;EI^zGS%%{oi#BJf>@Th+kj=4Gyu!MF}G zlFd)5xs0ifLr88LQ_Sa+EO#p%Q^ej9_m&G)mSif$g%uWbL;-poB|kOL+;w*Gh?{ID zTvN`HVx*L4R+$h4rqVV8tPbG79_b%j$^nU8XW;kGLvv<3otU1igvh13N= zpL>cOa&x~w56V+dviSCTv_4($Alq%CESZyS^B8On(Ks$Y1^0Er7x=v)SFX7M)s(25 zYWOywH@8wpoLHetr5UjQEIWhyg4g_Q9$s%S0x&kl{j7HE`Ciymj9D9F?NoO~CAND0 zHbZ+L_Tz2=p(KJ#jRY>Yf}!6Mj!m3Nf8jp-twF=s_w2gKetlhPDi*#iePMkpt9~kD z`M#Kt{L>#g3SQVqH0vt#z<7kDj8CUNFBaT%b#o==*I?g&a`{@XjSWut?}WP_G!K#b zBKGT3W0+|)-Vd_;k4u>s%3Df)3CU!1D0JYe%+vEs|_hk!33v8#Tc2~p*Dv=B@p zcc=`|5pH-DNJAhhXsp=R?I6(2)xDVKju*S`8rSetHuU{LT@ZBPWl;>C;UJsXICYTf zG-`C5ab|YjLoETwly<#^AEA+m1_6x`8VW&)Hg+!7cVv3iT~$O8pIpy|YoD+t(ou;wlsK1=U7Hd#Bj z0ryq25^AliHHO!h?19V&J3Gd8JXDtewh8CQ`0anf>3^%-qAsB$N8jYS-Yl%Tatm0b} zvm_(T%LjMIH0J?9gH5Qv5;`!TOBC0D{-LJlhI;3!0RR`9%Hgrhr`uk1L$UMG_N^*d|$0b|g^pk!epiL3M=D`BGg zxD;o9`e`ArxPJHxHN21p|8~xosi=Xr>ph*<=`7!RFJsJ|mML)u+cJwgSLz%)0<(F; zJPIM(`zynkCf!N?zi*0l#-l+@1xegdO)Y^5=fh?~d$||m_iIUuwW&9r?)7nN=Emx4 z4Hjx;WWG*>S&CknVtENmTR+{@iH{SgA@QizM76KxB8CopqZVv(&8N(P=1 zFX5?X5rrU(Drt{tmNGA@Yk;<0{?y{_qIM*@btfjja1B=kkk1|$bi`5};imtX6If!D z0D7f%#}oFE58~(ul$E%1q5f2tq`Kr!a!|BUq8_E(q5ax>oyPY)lQguLKN+`7v=J>i zn|s$?&n00FFLwideZG%=lHFK<1xb?Fi1V0pAax51kS`3%LX``UGSp-4}{&-fsh+ zirx3AoEY|QioE@~M$HCzvCvSG5tRj}N?7zk|@T~v9 z2#Q#epx5YC#~eq+-uk=+FtK{s0M!?b7T z2=@%*D?BxOCb|tr9@~UQj#XcsZaRbEQW2YO7z$%%3{$0dp1^B!Sy&F{?xEQDaUGQY zqRsD;PsHDW2kfT5WLMX74Jiv&-6*i&oXn(5ZC4mOcPaqXS5u|={R-d8jFzX8AJd^x zcX=hmCfrslT|rz5swbKUhtOUE9kWw0VRr5${qtP0f;X!y4KyHJO`Mqr^X z!|xrK?RV$u+5{!s!Be-kYvsFFHhTTL(>9hx^-0TVRo`B?HU2+mI|qbbZ?DQEkah*iEuEDeF`2lcE77y|Zk7)GFWvx(3tQugUx;*Y5ZzFzVv zm(iu7@1=7acop67TdWIz=$g_+Cs(1h^8?a=Kfx&qw~gs<>#2x0kL=k$7-yOoC5&fQ z=R(-k)=~xsjK8^{7$A?&tMWYc{o-5_+OH@79UD?_xvgDpVp!DIb7a9P2|>0jtas?_ zd7e~tVjrD}3~9U+5!Z{XH^H(!(_wrpQJT*S2$J|xJZa9f*)84<7gB5bH>MH8K{}0W zRQW=5kq?VY_7Xt1g=?M0?c2%i~$V)p_Uu`rh zr}3+Y!>ZQ6US?N>%O>!f$Hmz$SSL~FxzKMbIHlce@s12nUu87(U@Nt9C7zP!vH9%J zNybZ-(%tzcKgZwmj*8`w_1ynViB3LOW0u0I0b6(hTTy~t#h5j1LVI+|eDz)HAxyzY zl?x9jeIEq29~|u%(s$ZAfgIK2UJ~ zx7IDY9BN z4H5m?Z(a3eH4BX&cxS>o{NU75sPa_A z>o7|BTX`etTH%kUUgBguw}5}{&h#EpWy<_`3e^jHr*>1f&i6G zXZ+71vJ)I^_SY`?q9E>-ngZ5seBd!#(wWrSM|8+tgcG=C+AN0CY0juADwGQ)mbjA* z?dzISY_{TkTK}#9n)2_Bp|a0|HT%AjD|!oqN@aA`rfFxQ)FYa^ z3V(BlCv}HePQK>^D`p^HPiiL`ic8-xb|3zJ@u^kKlPr(pnL2^OG~hrHU>!j_(KH-~ zadsPumKK)@K%3CO2NQD*5#GiReKG4m2tzksKugK+&nT>J>sY)eUGjnQH_(VSyo|_YQ;c*=be_cJ@I~3f#L*Dwg{ou9B(oiE5y@5!vpP#wAC6SncK&sx9YUnEu zrwbS>K_kAhN5Xe9nx*sqE$QI?go2cf`7s|2iqFT0Nz?ooM-4{Ph`%hB+`hGby&eZT^YgM?QPv?l(#T5OTzMP4e_OBWFePWyk!*FtNMapmQ=rS3ng6hahq z*Kg%p#I4=}K+WpiC4V{`nYkEaCt);P-_0Va1O`3J@FK#Kg z-^89-7pWsqwnlWF8~fImbny3u(thQ0bfK2sL27BD77to#hkt{~gC(sPQ}WCo_*!zd3U1%<5^OX3GU!H-ah zY$#%T60)V}CI#8g5OnkMPVvrK0gEf}4PAu4ef`#a0_N7!B{mY9jROq^H-za-jh)A8 z3BAtdysPpf-deU(jU^yWH# z1(&icx~K}X&D7yXXy3L9Sb}Kx0}! z1oH#5%-9MxADQIzmMBA<<&N(XE3l*T#*mOJ(=IExa!a$mp!{t$T|*fD3#t-+u#|^&1{zJwXl(5e*QtZm!qEHsuyvBOPr+d zi&ae2w89Q&39AS6btVk#cpDtFO^~i=D{yEVTO*mO*6e@-!y2JCju*JG-*Zo6Bx5YaD20_lrp&M^jWUF>5 zOG8=5w8W0E>X`jUz6L115`@3|n&k^XSZ##A38_`m@?}Yu5{$o4&N_%1RPc|F-&~Xr zTCAiUn2tVpPo$r;_tg3h>a!!VcnxAGG!AmNg$}Odu4@S`)R2)RJ@_=}H~xp9P>2-y z2JI>(=_kQAyj6jlFvGM-eC82lzIaWv;lRu=GqQw2gNf#|QROwc9d8`!GS(=I*t`Wc zCLJZ$1=$k}VuamHYzkYFavvK@0^-68(TqepOtyI*(IikPh7oW$`?Xy!4k}1Fi}@zll|`m)e`b z#~k9=lOtP*_;UU>r|v z1Is^DkVd4(2+<&QtdX9(P7-7Z%}Q7}a~sqoWO#9p;p|Z`e2BgD(Pw`W7{s)+RUpWi ziEifdTwcE{2|Z3kcJZ3sha$^5C143zPwX^Mn0cqoiq#n_f?UAC7Fi_Je661B`Djdt z>?9x|)ppCyf@;b~1oHqBm+BE~sEufQg*tQa+g^`(BD@F9vZoRCy@=lE2k&9zH{Nmo zu?AH4VSQ`jL8CYSZmJza1zo!b_WvN!PF2B;D?}tPH0jp1WS#ZmuzYnGELLne=2V56 z80s5NNK-Q(WLT*48FhZW2y@1nj1_OOwt9R9F117G{bGXy6yh$&_Vsk-0AUt_H3z=; zr~@_waj6Z?hm20`DJc_5mztQViji8vE>DJ!O}Y|lJ?-r0tP9&LpJty}|ZG_7-?2chkjd`I>!u?>d9H+$OOFR6gIxL+1p>%Ueo*rw# z=^^ylqiS4pa{og5Hm7$-65#^E!!T@S)-=!?#Uc?(zVXzk!(qXK4j8I|3%*kz7HEP+ zV-s!=U>A5YOSVFx`XA^#OE+TQT}4V8`5W)J1)DorpR7dGkeAYaI4>ItW0S5e*#e$NoI6uf?Ts&UiCt?+uA}V17Co7np|VJfV=N9m8;9$5&fTPhb_NBDn&cW*0P21fxwY?5z=SQa3`xU3Bs4O-eogbL(@VRb6WD&RKNkA~?~~@qzj8|*F-+QD~LKN(6JYTg6UJX(` zo+}oJTpy*sT(vLa+lDh9DsO~Q_tC}$?n*Lu<1-0^ERuStaLhEmSVNygcpO3TO~rM2 zP-q%%u;M+X2plj*@M#AyV*q}iLktAk~+x2S*@R-zPds>J9y=7 zD^48p!QP!y$Qgsx@#N*n^kR9!wNzWIm`Wj~FWHUV{Y96|=ilBks29H);!j!MmanIj(nQjzkn)emKD-1Tv zO?ra=4i_K41A-DB0HbCOWT%lLF%t^v1FmfN3Y*b}sF^d!2;uAkLEAcPCy&bc6yF^M z65HIHj@CN1;znaqs^nII!ltKq#+(GPuN*fK`gQ&~4K8>Q$4agcCWaFQpFi+6P~gi3 z8>)d-Ew5n2SsHAAdB%7p{`9Km%WxLZbyAjVjyU$qX~CY%bC?V9A4wWtl$1z*Ngm_p zI_rZWGyb9*bL;CYzf~KJWRB$+1`5jI(YBpFioj}tG=#=VVHc+lBZBM?(bTrehV8bD zfV5aDMsJn{y9z`X`9gZ`-rsohb+(pNw&?au-rbK2tsRg0@OyhKE6nX4YS-6z4oSsA z9`qMamu-HR`>m(L^4Hn-mh7FDA9F`rf^C)EZiqf$J2P>G>#kB-TVL59Ptb2d({SM* z+hzA9yYUf_=y$yky4KyERN42XuAq~X3FiMX<6u3Bx0E zGNwH)xr^!4kGsfVnIR(@w57s8pY7+s?)3w~ihne3?rVbc(0Bc`ZAFquJp&ES9eV9P zZztHlRK|THNzL~{G};3OOP2|Kosi=S2n$>Zl|GQ@Juk*5`pcHHKiyLX2~`<(n6ENm zjcP~q(n}#yaKX~Q!L&SOJO>lIBCloSC^tBpDE0GU(Cv@X$-2A2=K3j9YD)^@Rh42; zBc9+#t^La%Ygcb!w-jaeqt$@v6GbaZ%5L#(ljUfQxVbdQH>U6cbGA!@D?^A=v#3QD zdYQu~ujrG_4Qv?CFD&fZ0lQqP-h(?SEjcGuSwnkxT-RwQ2iGZ{)x-9lUpV-OX&iKi z^wSjq*oH1*<6-@;(pd-lu3_^#by4)KM{T8giSOCmZPgfTUY-Hx#GWiocF*}%hJkx# zqpA}Bi(XPf?@={vkGCn{T9__x5pb6ToXw;5k>FA;5)iOyV#?;ashz;r@fvYgCA-ml z+l1|-7R_lyaTH2gN`4vQcO4!>wH@m?8#lT4(BaO?5haGx4jWo6?i(h%obj+*RT6RK zp;&v^Kxk?=R3(_RYYqo@Q7^~m`sQU?MC9JsI5wj-Qwwi5c^B_y5ZYljd!(bQMs=<) z{UKcTNt3%b8T{MYLB!~k?SbNDAg*KC1b1EdyOq^(^jGyCd-Q$c1|}Bh+s$Y5?-wJR z%HAonKO%iBLwpqY=Y;ohzEZC)m2T{UjA82S7rpo(aS|;1+@C*go!7m}6VLuF>RO}E z*Qkynw0OCi!(XZwG!plI$0r|BdGD%YJuX>V5;1lNVOdk!_|Y=V9+-3aaVz-4@&@#R zT|a7q)%rB^6y-OJeinJX_0sW(OAT?Zh3K-=7=!($_aE5F>71Nu+(C&vU8PKW$SQ4L z#i)yz!&V&$*E{tv9nCsl%5%)UN-GS02Bi+6E49%Q$Nk&POrL7!$n(PLYJE+!MBBi@ z&CbQBA9JE5v&Hi`ZeMt*=JNJ2KOxeOp4Bv6DEZ?UlT=Z`tC^<+0Sp!Z? z{iF9R`_QBgYosAKHSfysUl6&gm*$LZ&6ZWU(^`!G*!I1UQvjMJXGCi{aoJ*aU9|YV zPkHo_rCOH0^}bw8ytE+0_VaJwG2SlTHg)Ky)9rJh(~k`KpT`sOZh6bchdJsI|8rAfaCBk?2(0f~M%3!N4oEV0)+De9*4*;VT?)KkwJ#jH!tr^4pDl%~wW}s2pR!4{ z3X%LT0NH{an#zszD_rzG4b`Jad^6%@K#xfsjS`W!<m<>|rTYSY=Xh9Q!i z`yy(8TQm1!nGFs4oC-y`nT^9I(iHgudN;vnSL(P6)+CP^!9&jP#g8@LejtGJQM*3L z9lW1iZiW|T*-d#q}lAI zxtW<4&Z8K}r_vp3+Y+Ka5BD%wZ33G1dTYCy{P4~qMa|hw25XVD(Ad>aL|PvhUC9#dKrdO`j2ew*JlTa zt}oZiGR<0rbDO&yXlC!u7_1jyIuby%qsj>Yksc)j0b zK^K0=8?)fBNZEN@)N_qtWMOf9>fnC=z2Uj(?{~hkLg2XC<=_5~n&I-emwF>VTnlm1 zaggKN(oqPx>Tx@-d2<0>-7FNl|Ft9K!+{(!+T(Gt+1+huFl!gk2<{1ncDd-hzxF*% zD)r3y_bre`E%Ge&(-VJOD6fa1_g=Ez zT|&?fc^4Xq;Ly&?KWmri$wamQk3L>(E4tt%L^tRliZqI+@8CnYuK2dUmh!<~8cv65 z!6^U;csNPge*QGUEv8?UWevxL4H*6AeRpbwwwQ1H(P7cGcvQ-otqv%s$>xT996kv` zvTFj75~O;DrP5PR=grf!banZO>~Z(M#qO8%fXI05%vV7cXEq2uV85IM^Xq3HE9Tw2 zd{;8$sUFkR)F0nQJ6qdo;l#c4({mSz3Akf$u7z)SI2Nl3xd{gCYwoOsc8t~TPhWSE zBL61x-IoiJr4^x&=V}+-uv>S9*mEpdV|wh`due5W0}g)*9a-UIakjUJ4lZsnRNH-k zdo3>bCb7L-f+7S<$Hz97yDi~T4I)O)BK@9XRaN)vxJN0U&dTdP?t)J;c8V%AWHXD3 z-iXjez8t6Po5M0990!Qg8c-Jk*QJx2&OSFS&MH0!0-JQr8$RX#v2E}=z_6gd9h3g& zuGdY?oiXqd7}oGZ`{t?o)XE>glRY_QpBcP_tq-D!*4wV~+Yfktj4`XgU|zEGrCJGx zjw|WM4>E}hiB=r9Y$V{4^utH&k}d-+%xZA;Q`5=+gW%NdImAthnrh%v`X6*c?#*}e z+2C;rB)q_K@!@{o`0L%g8PMLbh3W{t7rBHM=~?_29^=FxWV-ebo$%NLFHGipcREZY zCdE#agqLtJkK$^OB8dONr-RrdcmdcVUH!6ExsGhk111eIs)QVIqS zbm-aryna2b9FQm}oW9-C&>;E!rNUAuDi<#t>*>Dp!vDI;vok6eM~hlA;-4nt`qWb7 zIcn%>-E)(vI^edv^{dDOK!ny3lMGm6WJL=4HT)T@EuZNA9bKRFrfPi6G`)a6kw>)T zU|p;YI%D|P78=fi!DNTh zyx!5^U6(@+FBy!5O5q-T)OpR`jL!3g@9xmIc*gOxnL_F2i0|84YV1rr5V3#!^~nmn zAb@wBa$hlQ`1-_8DyTH}g-w_8v7ZUq+h}s3{Fc|&_t-R*N4-lK$H}O#7gfHmwyaKt zB;ejNFcsb$LLP32Jmq$IG9YD_0GsV%G!zZP8!b}>FGWwOecw292|-d@b|WUA*#237~-w75%0X%43|Ngf2yeEAPs@1?`$mS0_r0*?j9;^10+sc01SaII% zHG+P`$N{IIfYbQg{Gyi+YzqJU-%N)#xvrD;E`@)il@hFQvrq2J8dG(Or|{*s$F7qu zgRb|d>Qw8-#swjwaFM6!xI{sTW&a}u!xe_67Sw)%B3zJjNe4(g&sh-OJFEsWQBb2w z9ktz_d(FoLT4Mxa{)^HnDBuMFj5!0+=EU5Bj$ zRAIxM4+h;ecl$8mIXp!BsLp!W3^Gp6w;t$xLoog?8C> zI?m!y$INJyGGi4RCR>nE&ACr`*@&-07#w z%~FpuaEB*qHRo|LSsW;0lCCk?&eq^AkYfqlbddW{lvDgFbJ zrdKhld@e}$CXZX71_k1{!4S3|N70VZfTampBas7jg;VidYUcCl=b_*~F}S;*JEDOh zvnry_g0k+u_6dRE`?(Vx``p>y^}-1f zNN;`ML?ijID{V7wUnm*VyX_`I?MQR4)3rpt9`(-PsbSZjg=8p%YMdk1>g$6Os|k28 zJtO|Jt$LCb!qo_=N~4!ahxBu^N{fFL`y~|UVjToJGZE~^$fBU|GtXkkazzR+qgnHw%zMr zue5Jk=Yq83wJJse-^a@LdY28B>A_l2`5nm2EkPi?7hhg7gC0ysMqHU@C2`b2w$G9; zPM9d*CDhD;d%wy1OQa32Aaq=G2+e!$YaNNo!6tD1;Rgd1#u0e`w#@cc;S|T&G|)`w z(Tg7V0Q~#iG+_UfmRz-vHECk32GY3xY@vh~03feJ>!TUEEf5f`Lz%{^PROxd@{@8j zbi9~|+st*GloSZahykz(#Uoy-jq&Bv5vpbuatO@Om_9sjE&3r-N_`%GnZ>iqfCW7M++`e z_2S;9$Q8VqZ#^RoGRlgW`{1HQPqO4#HpC@6DI(c!)%P9VSZovs6QnMutZ-JSg04^6 z_Y7Gw^5jr#ARL8wW4{2fE3jH6RwPqT3v#%^TJ_9IEZ7GXIX4$O%NmAN`36UZ(mKc? znD

  • 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�S532IxNlV@3iw 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%0DHf{&rlhZJ9vUQ zzEovkqRpP{&dJ%=?DhtdjLq$*vFpK&X#6OTOpJrLL$Wolpyk|nI zon2k^lD`kjT*br{)>kelAYL@Q5s+B;x8UeUvHh|F$oki?UMj$^;9=nZ zNneUYk~?KT_i1Xn1Y%Z4EyziK*;=d!YH((lk=6Pq?r*?aL%TbdiX$KC&L{{h(Fb2q~)wT{LfR zdTd9log<#mxTYkWX7GN`MAP4zaN=;D3WA0FF}=W#paXsxIf$!koOa;~1i8sg9Rob2 zD(V}XY==nB>L<1fUL=bw)FrKWA3*hjgr*uEYLOsVuoUV8wz>S9Ym~8w zM`**s+i9{Hn4nCK?hz1zbd#S)vFjwZTZiuQlJ*janKkaJ9`5TfGSehC#J;_Je zW9X=`LTvvbS59t@FA(ah?eKQxBOA*dY>ya=F3+15QH;)*`2lv<30m1p5fl%NhQveL zm~Yh}Xz9fe2Cq`EqMh7fxI%7#uHfcic8h}Gzl4)tE4RQGCL%Xtiysz-t9s5D+4;s; zn0*x70`Hlf7dda!azqeQ?H39x2{j><|!GSpoI>Y8u2*A_bxeISA2j4+OoY~N(ek0y&fc$ zvj^@0)gFA{V*{2TMKP4KN`{$B)$9Cfhr{EatOCy5H5Qlc?63~{CCFa49}zXco7=wA zmp?Nz^Mb|R)t-$~<`FdvZ-L4?us8ewE}ZzUS4RmG|KI9$LgTkLhEBh=%s6>64v(+O z!K9Wfx;ee@6ue}<zb&Jf|)u|e*gFDCR4>- z+(m^*o_$?fW1Dk$a!DQ&$VqstWlw+@JGxTgvM{&WB~N{r9vdPE#q`|E>jj)5^CYPO z4`gg5JPhWGNnsN=@J_c!+cf|r&)Ba*B}!C^ssMZLF2v>@kRN>q2>{`iPq3>_S`zGc zL`@lYSpGB2V>N95|4eNd{CM#mt!g<%VkSl7?xZ2EjI=AdP&e~^d>ty}fAzq2^53#oI zs25ivcjm(?)~5qAV~rq&9HZ{BejdL(h7GL|*eH%-K$zLurwrgDOaD$0e@4|xv1=}JB$qMl*{xtQ_YkUQB9hG^iS)&82QkgXHg zkGoZnpCgDqI_P{8u`2NcDU;h}-S-9)pufoL>AU+MF+*QZ?Uk9bl z6{St7>ifKwliVl-j|D_9gq6rg(0P&@)hSSQ|DFC(Ho*HFQMf12aIQ1YDtIrnB;i58t?Y+ax zqUy2KRa#}a!`sov;6G2KSzB}!?90Q-RjsF}*_aHf;ckgKcytykDTld0h$|6RS>CU; zQa?Vk4@=KMhW>uhB0Ik4=~@3bx8{v`nMhF@1_f-Lnh*YYFK1V^aA?eRHFh*dPJVQT z3t=vWRqKlNv2kpdv#SonYc1UZsXZt^n_y?!?02Fc9+bp1RUMzk)jc&SZHut~@TxSf z_Vubs?tyT_soLkBZ1*RB1Ro)7H$^nMptU#HAGXKjNzca6tr?#;+}X7|n8auZ)2W7$ z%`jaR{s^d9yw#rl;Us_+8_TpeYPc*F=UKtmO-4{v8}z|91>c(2qJZ!AU22Ul>TyM6 z@t|lx{LrrTs<_`%>cz}>8jv-Dyq3gmP?E6)GcqO)+M?{r*x4^>1%m~X$HIO&LL{~j z67=((a~$-HsDG}m7&N0aL^Te*Q*O6hs`x(!bR%3zc`L(8g7)buE0HF6Z z(7~R+gW&A;1l*r|M4L|wFBp!pc{wKhh?(qmC${oU9#L?@%9RNq#~m#HvU9_MQ9&0h zW|lML;)+tM<1+}6hkp5VVCAxrhnW2x7TQ`QfV4Y~-<2u+z)@||diim`8y1Zh7b!)# zoKNU*%YPd_&49iJ#%bFgC0r()?vdrkKiszQZ|Xs*RlFIBxYUT2{5*mxyXtZ3M}kN7 zIARwxB@iqw6qSgP^PJFKjj3VzPn0%@tg!|Q{)4|t!mF^Nz=`$GCQ*7la+(TvNHuSM zoJ-{qG`|n!b8Y3ZMLyiM*=xk9n@b36%85_}Q%^Y+m@|8Rz6ih{znB=$zk%X$?f66h zl@=Ejwpe)1oPF;`@;!7PL$^CO{9G472d~E_23ZuOg4e?)@~R@Gi}x$6PqQozB6%LD za`!@|CSaQD#`$?5 zqDYKp7f*6ibRc@4OXMwW&wIWeCZi!88P<>52>iJMb}_0K-U8Mc55{}PmaH?~74BMf zoNlW}4FnwBwH~D6su|aKF-f7Rrl{Lg+yvXdC07Z5)b=wOQzmlq9_)HXbS(R+ReY}= zmU_Pf(0uyQz!%Z@{Tq@srsZ-J#SS3tG<5sXJV0AAw|Ql>rjuz;3kCmgCL6ozG~n}6 zy#`Fab|gy{-8LnvIHzxFT2vC_cJjj<7lf8T{oe^s9^#gHr}|BD=;A40ARD8F$8IJR z89fJ)Hpr!xrAqxhXq8;;!%X(tUToIAo>t-xaduF5Icg?5sz$L{qe9y*1z`mypqL?k zE1?$n;fu~X14MeTt*`_eq##TW814Kv#p;vtqQ3YTe5Nsh07sw_IFk>g=FU3@lFC<-thO8Ftj+Z7|G!ilNGiIF+S z4!?mTw8qM}0*ZUaOHogkt04oXFmgpi7Qsv6VtxXB!;?lDI&iu8H0h7d7R{m1u_chsFlPCf+Relc^`5~5GMTxvl4p;^Q5}kYF_JxYetYr09 z{Gw!{>>?gjwZ-cO)=|4fZBX^s<=*}}MgY*HMJ{LypVSk6 zFNknj*MxYvC`k5i$K&Y{u7PfvckK6*XTQPMJylxpOA4(e%r0B)`Sq1S{w?SPhJ|g&%Nl(PUw@C#IIav6HG(NOuq`5wI^Q2g01Q!RQ6e@%nJsUK98#ju7W~5A z`G}W9(IE58sw&b7*+$R{RC|8O$gDV66Jfs`qQaR8>9vKU8T*z>=FH6qvNK9m9&HP~ zB61W5-z7!Gb$6I$V>GCF?(hEn`Xl}z%gpcY?dzu9rP9_sC0pPM+Rrt-;TjDNQl3Ol&m(Nn#CR1N=5PIiEf zZoeadQn^hcNqs^N1g&{Lhg?A<4t(g2vb-(wxQN62Ur(9HBE%ID@~%Rb5pA_h>LQwr zR9JD_GJp6ZS?PT9B?|EPiI35ZC0Bpzg(?6>+VCeEaFjQzp}xNA+(Lf5)&7pZ_p>Wm zRCWEBMPC}gm3f{YMmyHW^8to90o z&c`-`@y}zR`vU8yl57R{St6qiCf}rOTOZWQXE>YX|#Q;-wpsEYt*Y ztKBYjrX52iXssW^N8ACu&g|=8nhRJ~qi1{LIo#RRzTKtLhm64cHx1x1WpMROA0Es- z#sOe}sBvEP4b2@<$nQ-MY?oG|Yvyyazu#XBj)}jy398(UG=_<#OUh5MCN6*&( zv*u75Pu{jv%fxl*aM zQM+Ef>J#6=zdB#f8Y|COW&ky#ArU>i-1>enx9P1Qr|LF1-&)DFE#ZNv)P~Ca}nQuS$9lc)!7i2CgWGYx(<>7avH|n}k{O)P>UB%}>m=kO| zEaS$3k@HL}MWH!1J=DefA~`CXq|NoW-a6qC*x$NCvUr8bm}5K7e0v&%mgE^e++mN- z+hX8Cb7qNlQs3*9(h2i}!^x}Jou@FiVxa3BA?-v(&vkMXUhg7Pb;UCMJ4KN6-tx=& zTOj*12j|HLx^Nc2&s%#`=X z5xwQ*9QuD_a-YrC%F#2V#o`nq3_1ri_;F955rgll18QhBMuR#l?UknU_B`+y(g!*>o_&lU2|=o>`1L;0A}S1<|)s8&si#Jcsmzk z`f)j3GC`If!?;xqSq2^~mzIdz3xRW`%=1A%+AWuqEVf7@ap2fjCW6BCZiF4v@fni_ zrEu)`5j}}6k`zSH9%1CS6Zg+HHk9`rYdVxP)VE7( z+{r=+Q8%3-)U{z{94E3JZ1V6#TLzp#@7tQ;_ud0#M*&ob=6@yCXB+0LnYn!^o8TZf z2h2c*O)w9NUl$E|ZjuKz@Z&-LbCS!d8z+!is;@v2mNhP0X!?O33Ff3^>8-cn zREIP#jNZkyxP96}in_Lj(W%*%PjN(r08so=S!vi3?P7@T-rU~sd`3PXRoNcvSQLfW z@jL|)21uFka_+0X%yec;+{4}wbHo$3!zLu&b1#XN?eXaHt7(X=;OFwF=lbWzV*zG| zQV~7HagAlm+{nVp*FHU+Jpny^BB;m%#_#p7Q%%At<5NeSbm+q_Q~+L$vxDkN<9)4q zwYj7ys+*zu`G+(6*UXdM@7l&ARDt%rg{W;6#-@w^^?xr_`Krs_h{`t{ZTl#2*8g%aj!q(CtUHD@yG!|7%I1D}~XCVcYcb zuc_|5NWX53$VbwTSQb}vsK4JS24j^DP)Xq`custaxDonJG-*M6lIs%NPeIYX zk60fm7*|5bOu!ysZ^iS@`A0-HA0s2O5|^i5c=HwXo|?-dYG-%DH4>@jBo;)1{k3BWOGx)W zfYZ{cLJ{0qcI!9XY&iHN1-5{a8XH<3H8&%HM}wLy?g-K-DGLpWnp1!|d41@OSAd)HW+a=?Y|yq{-B{`_w%ObY~U+L>JpVWb}r$GbFMR zh($-m7;CgxL%xgV&FSI!?ZB*30InUtOE?xUdQcddV3u>R>KvSd%0qBSn_QK&ZFw87 z@6LIPX^LA)C#=Hx1EP=Me8mC^r|0B`;xz0LDaMfA-9rYfU`0BhJ!He}8@nC< zW%R_}2B^ohi0??#4zl${1+TY}`=7b*Cd8lt`5i>^ogZ<~H3z8}d{37}qrhJ>jsJYg z>b~DsVlkmgt|haK+4>jR?2!To_?x(Y^iQJe2DrW7K)m7ZR)9zE3!%Eplw5kS@4%b+ zWU?l{bfYQ|C|DOL0cN>4hWW|=H9L5*4NySb(t=L~87y<@oF(~M^lkP=A(kq89w-RB z+tM$g&27gZFOBVfNvJ)H^u`i(3$0qcGe_o z1&;rk_LtF$nyv z-h67^(cLOaDY+{VuW@8T*ZEE3f6ZfHc+R-zen`m!NzL8%L5#ET{&%;b2->3ml_6RJ z_^AxXeJl8pQmS~2y^7I=^mJ>;zB;+w7d^gtiTaxF_E+Cq7Glad$$I~9Y(qFxYHFlq z%s~rMYp27-&G@S;a|GoZ0Cu&ls9K+&c-Jp0;S$Lw=M02wR?0V%>>Sf0*RS{iF3c%G z>EIa$()QX&i7##+;NYa{RkAl4sKsi92(K@1m3HL+JiSmm^7WX`%xY73$N~IXL%j~# zpmrp_IV9IdxJV>rne0&N?b0R}z6Hn3(Une$Qa_FBpx$EC9LWdmQ5NK?kfj4*z5E&$ zB()jmb|Of=1{ad5r`54uM{<-3n_E}%K|o)AOsGP~(?c6?m38G9rt22;`5;?=3_PNZ ziF6agQ>;Lq2S2J-I8a)Bu`$bQHQr87bRfOl_FkmHg-pRFs`v?=Tat>^yg#+2Fp;bD z%KccnD_6t8|BSpd*DfNeCW}KmXDh6Q>$|=Gu?T$7t_6C`a@TTxfFbhX%5oAX+*o)J zcsn)6oxi0`e(d)nyLP6)5WAg+Y)W26dNRhi1^6W9wC0iTugbS-&jWi+eESQQwgEsw zUd$Tc(-y6wDb4Ug8O0(SoU!`p>7$4M5zHct4K6GsFg~ zrV?{vpxOZ;ro<~gTiS;Dy%2;xj*d1UF88%s0xVKli+BdL&>qHl3g_rCdL^T$jZSMV zf*lo8Ti_nyX#?^eT>c2{yes86@O#7JzR`%2Yy(kXpHcd!F1KGq4L!n~`a}cuh&PAe zYB{Q&`2azG-!U%e>+dHkv(M8&3N!?7EtZ~u)k5WSK#bgqhmQ#8X$Sve7LVNjno(v= z_K_eD&-fabiu{vZey*RBCfj&({j4%Z|e#N|Nz5p|8DDA8Y$UW1<)>w)SXhH=fxZDNWVOh%78rHESKwTXM~ zC=|J_Qmk?&2eu7O_{5vJj+N-=S`$43=XBkepVgSp@AR(sp|p3LLHjd(a0%mAo6rG5 z69u?1gaZA;+MMf!f6bAhypp0uzpGiP>I251cd~}QoZg?6-$t^)*Y6zv1pJgI8q=g- znox~hIv+e*y8icf#pEMBy|nhHZ|S5-JZLDPAu4Tk|9R&Lr|^&|j=>VyE|(D^%HDt!=gOV?)nGvhUUaI7J!X5-O`aAYje}6S zX+v_lJWXq)A}bHtrea5wmC)nii2pT$Bg7frv;L*UD>1M`Dc5Q5l1_w%2}=YgvMXK2 zWWczD+gt}uKY-mYtEWG&fA?+YYe^wF`#9H@@o{c%3(E z0S`e2NRkz3{nYey?k{qkJzU5awv)MC6&ubVjdyY!{E*$v-LB1pKVQM{LADR|!t7I< zJo=Ry|4$Pvegqg-aY?nq52&f>Rndx)a!aqJ`?Cn8FoU)v0TRE;^$P$w&CoZ=?ckc2 z-;9g40U^28!4K&IKg?qN>-lh>#aKdy47>dqPeNl?BAAoSg0s*&73gU5RTUanDBu~8#bY~_N)HrgjKcfk7 z2S>AB5_G~hwMMy^IkAY#fBM$W zQ+sjS7<27b6R{DCDNF3Z#L3Y92RmBVB(hc|%*R`8vaCY&;ZgD$rD=1$8ACFZ`?p{K z!U7}Ma&>8!8^Toc=i$z(R3zt_N#*W~A&->Fnrf@#wnbDm02^A#VL2@5j6g{Rkx;-` zV>KZE3rYA`l9d3JZ}fmIfB|5xr zX4GS#7tAG1L4G}znh-;-_FT zP)P@`{@SJNe##|4vhC$J)S=v{<36e=NC?7u9s4IjFD?Bu<F{MI zX#4B@)Y3ctg>KhQqQDs|Ea2w<<~EbA?ZXq`))sIprgdh{I5FuzdHw$UJy`a}=5y-B`1zkX3$0s9HL!slK35-IPJ=0f<-=9ISP40!hy{Kb75o9RecGK$ zWwo3ub{itS8eev?&7YfQd_nn)>ZRA8YX2yrN8deHgkg_U8<9WZ_;J{v%TI)I3zZPf zZSXB`;}9Q2l-hicFx4J{M^Ae$q{1>U<8*r5>L20OGl>ju;NBAO^zeu6yKK0Ff+fjuz+x(bqt`gS3!kZ{D#N3V()&A_a8jGgmYVlSS?NLB z8z0BCfia?VSXQ;%aX;XV9x3PmnUkYktck|6dFZ)DRxUYZ%xGV1t~I%NTp=HS!;7W* zRN5+zlzk4$HG0nBU15#&G{Qi)%b1CDhuw1&S6_R2?NE_^ECjbVSiFx3icmO~@f3k_ zgBRumkmaCv z(R7E6;_%cfc>?P3Io;$}b^;WA*W6sizsVofke1kVgkJgoro{U?j!9AcP35_hY=auP zT)Uv+YCS_JMe~G}M^icAFO9(1bCdp^K+q3lL>Ft+rXC_bpTSn|CQW*>F) zw4vm(g&Rp5u`-z%?{u~Iw^U~zQO%1v`sN!CEjZvgKQ~rW#&lcE3uafZZ!(o4y0p8c zpFMJwCfjHWM{_A(BS*X7E} zb0QVC-Otz0b)Xh&Wn!mqh zF%4Q0+6;#qZzZ+Ooa`%!_{_zP%b%q?CiGvGZ8J4*+;6|Dxs`YYT~}saJ;Q29BTy4CG)bOs@2CI!6( ztIZbA#eNa~b@|`1O23!!XVCo#ZO9Sq3$VR7qowt4X^Qe6^MM6i_Li*qM9_Yfhm$m< zyIw(N>$qAx)3So{$OiK?kz+c`NSZ-pg$^bu9F0*On$V`9uHOgQNx{?`Wfr~;<^!PQek{AyyrY`8PFsRA;&o|{L_P#U zG5rxxMrK{bIfCSH7UZAkl8g5l+)+nPS5_Bx@~Y69h#wRtJm!b_v8$@K$LqO)h{;F% zGJ-GZExe8z`WZo=P!qzcV^18^O?WDSh8LH~Vs@EuO0DB$CY@eGJ0-tu>@OqK7*)I1 z_e!PE^xqVf1Hr`1(Fse(x0KJE4FX#4+SC@^j{Nu=yjJc=O&dBPJ*~(5)3wh+Ej9^U zT{T&Ss}_EV|Aem0r-X-9bb}vCgk^r2l1FPDaN70r)kW0K#Ybd%cA&wB%s_qN;Lk&3 zQ!F{fKf!r$E1}{B6u2V<#Ycuq2eMgw*jffLfJh|q05Cyd9?ih4NGB!mY#%~!hyqZt znL+^333>O(he%=to-Y5LRCRF(1w5aIsWD(HT+&y9S1a~D@c;lxnZ zjs^4b+|ZT!AJmDY=mK!$3b<$$(?I0x_nauJx<{<**!ZQg#y_InnO+} zUwZl84yPZEqMo&4cXP#*Oq<)3rlPvviyESLc6M8ge+wlYyRyWR!1%QEF9{Lzd55qs z{>_c7xwf5KfBV4r9&sJe*nTfVtXvIuy*=30%2Sf&e(AX10E>nJY{Kpo%w_t8r?~o4AdIy?D;r*|9 z{Pyma^>m%`?fjb?NuSVyqM~;DIxS*K+t4~8v<)#Cl&0CV3M=dfkF?8J21QaXS#8vM zP@Zc<(tY_dsoXaW_|b&jmPGZt6jSFsgYx_7y|Sx_GatiDw4s<}T9!)_3bbvZsPFe;tOc^B#l!fpUR=T$hN@BC zt+@jiL$hF(-ioKE2cg`R;gBZJst{8YNfgcq(Bu%T*R1bp^NeIKw_eEk7q{?ZX8?zG z$|uyZ9kSM+BN51;xf2LT3mc|5RMYn|7|P$0%q%c63Iy?ujSXRQ*V-k`h98Rv4b(jL zR&=H?aOuSMRq9*0T@_PrBLgy0GwozrBH*MDJ&#dGBm)XNlT6kZ!8!56lRq0` z>W9a`9;QWt1Nj?AV$Ht41r{Zjt8k~Ln`y%u)5w0Ec+3IH4(4AZU3%e4QbtQR z;ciK3gyf{$8V!+BSDko6AwvC10eBvePtlU7IPBE@2vpP&bmSBGLY!V(JJ|X9GbmnF zMbW;(tJ6TXU>-88wY&_()KK>j*pONL<|n#}@oS3-(X{QFN6T^HW^qBvAl3#BN7BX5 zOi^x9_{Ib-eweX zr80WYDRuwR;~eRPuyQ3&#J0@+y#;q5gS(=Y=W&uQR>Qi$z}U^w^s%n z-Jm2%a(FJ7QljqJVAVvFs1vH>+=vx@+_)Uv7B3UgyQ$lNx$G zBa9EY)*O`EL_I@ZQ}xPTKW6A;Hnfku|I#ORr}VrsGGNelW;0dh3MrHL9W(QWIvS*W zN?jV?sTn_jeEvHOi)3^>$8&A1(wX-N0_uw)0_(X2nE{*C%ozAGSOu%9yn#FF0KORq5 zv@fOH_R;_0A7m5nF$Y9K`>^Gkb@42?qi}*f!um=*7Cf%PR+cN#CTjb|xb;*GX9#2X zXyo!E_9)z>{gR1lCulR4tt^HX=Gon#Jr3OR`X0S#Qn9JB*3AbFZ1U;l7X6$k1>w8s z>D-dP?&{5(q8`QMFAApARz84buD72^ohO5`EQ$A?m=7NnC`{ikRUel^RR>!$w5E3F zYpC*ePxH#z-ooA%7v)J}nvayhzp9RpjCE=`!bBLSEyjV~?-WNS5FU6Yhv)-~+o zA;HwP(zagvRm_Os&o>K1gm6_x55t{Vm9k%D+>WMjC@$;b`!enyWsIJpVR#1e07c8A zBuifQcLgdK$L4LmPr_$E;Gqmk=IM-x>*I%IxyB9#iJ9Q?Z{m1od$@dUmOB7TCT#9^ z?i^PV)@vm$=x}z74=AITE;W6Xnm?h{KIO822o3H`DEwnt^k8Hm)M4YsKRNb!M)|7X z9I^+#%|eLT`VzSf75+&!#_=djd^htLhcv3Qto`uGR@H8Te>YJ z^%#k{sRH}k#SIkBipH6~vHdICI0(4Fm+e4FjGf}KGC1k_AdUM7j?orWcO}l$jZFl^ zH#=?dN5l2@IaZJQAens|2@mH1K+}b8ApO3w$c<;>!#>MkW#HF;{~80?FR)(b=YAHd zB04)@fbVZ$T;+few&S}-8`4n35BrMfeGNdT0WG8k*s4HsSrm#5hY+kHeU@_C;4`b` zC#|=N8oMz0c;1Q-=(M{j9YS{dpFN#2a1m}J;cvfsgSnG7K&4@-R!!ct!4|amo>`yp z2_Qq;XK_L}o$|Q)krC{5u5a=$0w{>O@|Lyh8~JA;XwVH4pj@Q*9*(MDqTx+X&gYW6 zQ5vOcI3#XA1R-XVABwl8G%x+JP)@@31eMF~`do#B(8H0fD6@WH6D{3#u^#*&CO*gO zcijBrAI8V>_&Fbs5>ttZ*k#B(GOO#M=J~zh)dRX7x3ePNweu&p6957P&)0jhP`)t> zJ+ExdD>2C@EB;ZF?)FiBsEF%6*(&mK#Pzefj;VBt+*{6~_eXu z#4!Bp@7#mBe6QXRg5!sx3e1D-1bEdUa56f+f{59>qb3~V>EnG;t92Uz=EeX<#jlhU zHh|)dk5=0oD2Y#N>L{s=9HA}(ob;CcmmNOp#kA$Z(<5S81@>$+k)DGPio}B7`zK|td9QHzj^3tdpASSYU02mA*fkCmZc9HB-?xQ~ zlln%4S7!>Yj_@g~XV<=G(Ly(8a#4D{h{e|J>k6x+$zOUo$g{2I7 zLnFw5kx3+mo;7pbw7ze#TLWbt?4$eA2(C--^zkn|tJ8X`L^8bskd{IXMILgtJ^fJX5&+X}=s?aXQ) z4N&zJc;^&DjB%5rN`5(o)*W;pOXaQ*8;x$ z+b%VUczRM@Zg{2f3tVzHh|dp~8U~-bt?iF-S?%a)8|5EVoD{s1b4UAj2kOR7u9C6d zL$mhZ#X6_C0(N(Dj*vw5aVp7y_}rxgwBGYsTIpE##p1a1dkNc|9$)Z@+Lb@A4U~}4 zLc%P=++$@40eV_DWUYiQt;H@|0Rc1*O!F<;jt%C0fg%-6p2hWR^ zZ~t1_&=yEvj);6PjP{A;|6;;r4TGGeMGO-(oLRr47)PqjZMO~4%#EV^{hh5f+)M}i z_m$PGuGwi>ihAW|rM%Dv=S~uDEiN;cUak;0c)^Qyg6ZU6J74GiSSLF;tl|p7hy{E` zd$K3ekGQLm6uc?UY{*n-k8*|V8^3m9SHimFgy~69I?!P~Vm+%E%0s5I}l9lz0}rd&Q`f5Lrgm_4g59~06$3k3U3gAotq`WXDM#aluB z8NU^bl1t0ZnP_103ne^>tN9A2U^XA02ymP?I<-~zRx}#QSLoT+dQ?I0D`O~2&1t!{ zWY9o7U<&l@T0cJY)+I*^NN%IBl; zE@G}Q3?lrpU_f{we!p#s4?i&q?pgFzwDq&*8cao$;)MHSCkXfeRs6NQLta-7 z%$y27w2`Y2<=(qwji~%2r zw8w(-mDUZpy2}25WZ320BEk>&=n3EHB@833GUHR?wf~zOiu;m25y7oBHF>@AHr^VP z&_FcUf*b+mckSEw*Ps@}0UH$TAL!2JAauOiLQRcTeKWe_o#k?#=oTX(ZCy`zf{eSA zKMXl*ZV8Pu-kqlA{qUDP6a;^CNal{mmlXSjAmRb0XP56=IIU1qD?~SBo-y$CRbFqh zpZ9FBXvEE;rd#<#0|1^_9K0_E@MR|^jLZ91hO2XeVsomgS_^S1!kq4x!T$e3q=(j< z3rBKlWyu$Q54JP6q&T{=^w&Iw?nnq*=>D1q_3CP59e!I z3}Y_4DNz7g0gqlq;0p_8ffb6@mL9AdOn)OU$-{H6>mHE`Xru7n)x~c(TUM|e}lhm2-B>;p!OIi9#qfwkzJX|eM3{B%H`q~kJ*IEKx zy;rY)HRfsSy+=&;D$sjqQ~;)60ut%1ULabP1>t$>S@O!H<@PELHaz4rHlpLGB4ip* zK%~|#I!edUhwLUSA|YdZ-?ZDvaP8}Y#bVkH@=q;w1EL$8e;y{#`tPoru^{h$fTS?_ z2O0(U&rF8+f7=$7{NNKmMfVXt6tlXD(l06g|`NO2?=Bdutpuz@~f;&Kz>DxuIVYnXHbS*haI0KIyX%P_c zmJva4ytumv_#{Nkwp~&+ky?l>H6Xg_od+9M>Hz~FpCrA{7~VN>j$E8yAlO1Ezdj@z zTld+K&u&aD-j3`WqHKm{3GWWCRC#3)-B9|l`9MgmIO}q?$ zFzwj}YQ~l!(=!*7>b{#EE2`u9AvMcLkL`Z+^UWIRZ$2Udmlj0pUvYJhNJ_*zbxQh` z6~a6sw@Am{5>8dt-iYRq?VY~JkZg;Q32ah$sYJlfI)TV0tQX$VX5eNMO~v{ux>$A6 zL6x$DsWZ*-YI*X|GfISv{jhGLRQD7i)T7Ll>{dl?dC(_GUJ=qq>Wo?GxtE8dir#yKts@C!{JOKXpM}rE335!}%DwU15>&~v{ zD0ry!Q*5y5#xLpBl**P4f|h{sqimLCa$*ekr7t2pRXzHvDZ85Wene$btLyH=Hi0Fn zN?U=gnnBKUI>LJGLC>6XZHS8!;@uuhaMr*V@QY{F7^J_M3;;-s!^! zQ32b(=$Ja#?9I@eY&nD5nr6T$Quc5bXerO?2t4|x9Yd*2HDhQ_m^C;@p2)>5Bc`N7 zoPjlc?DmzvLcpBLR4|u$m0%`lly@VG!!hQ@I|?M9OXH)u5$_UI%dU`(D5C}DiE_5E zWpT@8o8Prf&>MRm0<{~suI40GBl|qGSPMkypEQF2k^?ly(5l3|iq8w~<}G}#i|_5t zyFV7Cs<$OH^-tMb$8a%rN(KdU=AkD@4@0NNQ{$LPmVObqT?bdyIv$mhW)#q!SkU{Z z&vs~>#EFk}hWG>yoR+7|xTu3dJ}9;7h?%MAi52h}qT_!tPCsD(e1KV$Di2+ZqIPtM zq=9IU9?CBJ@v3I`YLx()-^%EKogaFGV|TBp+LcmixP zbTSQ)ylNOs$98CJs|oRtVJTx~wjy}2zK3(R`EyxCD$r%GW0qJ3PN#mpg)5*C7j6ro zFK(6vDvi4P#W-&qAIeNH{q%K6N8=8VYAU@HG~83TKu(zcO*YT`WRvLtTjP(sY^>51 zAt^^r$T}`W3pdsh+W94}4pE0wUU0fNzsnUVok{_wYGAizss1%f@N<oqxOmb(H8m@ZRwgi|H4Szz7Llo&4!MWV>x{Y_CaxZSLPAB z4J_=Ceb7n9E1x%oKw(LmKMo5*p*Z6+wvN!ChWopnlva6|H76kAFf6`G=kG#tU1O7JeZNwiGa-6xmbOT^FF`7>F6xjz4gcPF>&MA^N6|2=*? zu4`-;KMZ%ST(ELb(~0HmeK_UtYp2NEr-oD)%icaZkpr5^tjr5miSZV{Ik4_pe$f*$2191=2{2b?%k#o61eKNosOjG$+5X{#}4^U21 zZ+dI~?tc*@YYi}Mbi`Q_sY>8F0B+uTL_TC$qlQEdK+lW56biE9-f?pIMyU@F`6&5H z{}^eJwQQ(KZ8wf&VMwR2YB;3lsz1bqsk#9w#y@I?7F7_AIt1MbGS4Lf=w#BGWp$1^ zTJa>eZJ$`C>NYky->H^YN6WS^1a$7t#4>GGI%1l|fXaVWylEYk2=EH-2_w5i@uTsV zONYru#M(9;e9@ z=o&i$e(@qxo=BVdXvS2_ZFvM>bBReyYxbS((r3>JoB_ET^?p}YEwLjwjlQ}P!Gl%w zTJa-rW6P)kh$m!m%jdZn(NLN-m4Udp4P#2!`18At^h09I&f6Pk#ib_Wge`~FL|#0c zZq;LhKGu!GUzW`9=UXtxo6;!i%}swc1liKCeYO$q8Ht*8uS~^}rFVHt`(8zUOeGG1 zXM)NZ9{${NVa(2FkIdc1MLjAu)=94`JYvPY$r-9hL0cRVdp?YuYkawFi1ewu@i#duk3( z&Ki-*q3I4-j7Zd&9A+$YiBou!$_T$84HyiWJu-z6PAZr##~aMH)#kXS7H4$6?GPP` zpF_DIBA=(=^D2P+c^HHZbhYuVYM!gOS>?HkzP0&-_EbCHRh2)A9c@+!AmpN&LtDFP z3x||}_zEv}qI=&?rUNAwW$#nMH5IQE5l!wkdrjjfC9b8cU6hMYoMus@uR$M|k7_o}SU(<+bI#GD3>eKCYJQUpiQDgIJ06$Y%3}Tkr6^yMheS@V{8U=hXM%E6B@WiT z+%qoyZ?gMejgjgz55BmM_y?bDyFH$C(R^j)AL}H7I{QCJ2HG}a+TSm29xM?sDnHm+ z08S2c7`*NtVoYvu)UELy89q6fp_ycLj=ND@b}-SyjR%rW)O~03Z-a~RsZwkI1`xvM z0`*E`3M(~-aO}S15BtHR{&cQn2E_kZ4W(;oN~Ie|gD8`Q1nD4s8Xf!@E()hev^}edYvpn}_C*y);!_R&y1@G}+Bt=l()o6JiH#VA@s56pC+Lx2$lOSWK zTL0*V@c#qBKt8`JnyM46NDEvBkjko+2Zs$ow+7xE$OBIF#^Fb1`e+>xx$czcV^P?= zW~-?~jDkLmVCpQUXVmO4cwK)p&>^mq5`~>wD8PfVhQR1)R(AQ>i5n!WAJlR{e}akwM%6%>y#;y8Xa)iq(2{^I)UX)g*CIT6pWie~lC_3UnnCNi$fHq=?1 zAO{DdEdLDsDZR89-f8y{U<&=6^T*(lFq`9+PmnXw z$^~hn0D+t>WY^*vsgX3W(d#gX!2&7?C2@qC)<6(GqqF<{DU?)lpDjy(Jx1H3jIKGm zss)7;gS%Q}05;_Mn#xe-)yWQ%?I0}6+8qvYLLfY+34p0B z!NkJD1%Q<4{^)fU-!w;_I4uD`W&`tTszbY8PPwpr9Za{0L8-$r1oTi>{6OKdFtcei zb`|evaA)D)_AU{=qJv#8u9azZ+2i~0MbE-XpQAch^kSmr!=Dm8enRx_`vP3C5B9`)`_{X1 zY^aEKzv>{-GEuv{qWStj%SS(!GL`)Wm3#eOewtl9Rnc#Dn>%|*a%$bDJ?_hwMwi~J zUew`MnMo{s-oKal+25Gks)J|@S``NInCg5wYPoHJ!AJ6Hy)SD~#xEmCd(alPu!XN% zm?&flf6ZajY1+aTzJ`I9x?kpcf>|zOjSW+>C3f1lZZgI(-04@W2LUEZ>TE!sIBcp*huh5$sRrs4`-8NN;YY+?_8?I z-isa)$!T-^rIN;muqzo<>c24nrKiqWU!V~GhS}AsX$bZEJr1jfc`>nQ*wY3Y2{=W? z9E9|@*+44+FPK$@G)PZ9RNZXVU(&y7Y^-9=Qm{#@x^ba1a6F_89_c0K=5}kF`;)Z~ z7)=FvEXwj>c9$}DiZYm(wIqPqAVT0QVlIrKadcBsPi|8t)+DFZu{w(Oc?O-BnI()N z2J;pLfJCF#m`!MhBIi4NcOk7XZgu|sMTKBBw#B3{OlTD_R zB>*3)%Ggh+x@vjh`efI)S)ducVlXgC>a=KEkTJp_{LT5``)&+4*6!IHYLMVu3&>bMe295ZSwgj|LpxVyxNrMcKS4n^iQA{kDyfCvlu#y-y z4(`~b*OoOPGbD&LUyCwO0w(aiGo~nkFBGAq`v9Z~P*DRY!FA9rLRw%$VS<$oV|3CL zaAgUCgO)+AlIkYck>grz4FqLC$XP)pF_u!f@#-5MOVk-yMJ+Lr05aZngXF(Vp+!J` zGMmYFMCNdLYj<`k6iO!tdyrR|$cJbc>_i~HL&_)0iX%?vR6n4*qP(bQVNA{IlOE8n zXmdGz$tNRbK=h zg#A=|n)Us+mDME^NdNxg5tT>%J7zhtIQhN%q9s+n`CU?<@~R9g2AjANSZnI~N*-7! zOJT6?-d^gdG0TaC{kRP}K-cU@-e%&JbhZ~4$ufM2oO8(!rP*=|TiC+aCh)jtQ0Uiu zl2n86>lU`Kg|A=WWz|T(Q5W5x6`0Y{7-+B!TQM0}o2k_x;S9G6}ZiIQ+ku&mq*#Uk7d z0T?Z2!va*|_=W_^;cz9~2hs$*C0Y$879$Cxx$CWDMHaO&pqGn{Yxgo~v=VHG8(jvJ zh-gK+kh0iDIVn>K44oNTOVHUe8Q zDTQQdgK1SXOIn0tU7|VIfI3z0&`deRS771>DevoR%`VFz}SUWdIeKXn89JnxU_dyDwIQgPRQYZ7GAL<47>ep_5pcgSaGXU_5`s-Jz$t@1OPb8mgoKI0CkYB;-hmZ_ z1mcht4krt=1iVlJY4WuIP&~~V*BgTCUI#4b;?G)0wWhL^0EnFZ)7C&6 zfT-opHA`-rTz65u!8}UWW`FBO>w0vQIoB;kTFBctM4XuT$;wl$ouQ)zkO>bGFtA*! zsl@|^Qh&hTkbtVmjy2s4YmhB7uZF>Q0aW;VOgl0Fpsa33`<)p$JM#~3#$=l2c&MVj zu$#jt!SN)-z|+NP0A5H71MA+IGM!FMZ)%#1T3eJ!b#>G+^6Exi_W0yFe$X*{Xm?67 z%wP3u9``tyf_!?Wz~$SIs6D@>`Na(_&#nc%7f$2UCy4jnr~1|tntt@5J}}N0?761$ zxPwq9Cwg)PpKK@YhW)pzX9lhvrR?SGg346~nhqTpd-+mb{MdEw{6f?3zbkOsZ~Nra ze*GyGoE*aS#N{}uMpe7)dAqs|kddneE_6q0&o3)*`)sVHEo|ZI8-&44zuvI9&uw7~ zU*GU$t}EDGXP(V<$pE9#zysI}Fm9WjT*sS&=9M7q^lV2-D6yUeZ3TABY(Ma4=)Qs} zRFl;yKt-jMXNfHL4Yh!~*<@>NVN9j>=1PU>2cDP$sL-W~c!K zzlCjMb}Se&&O)j+F@cJUg)abR7*LrFbnt_t)b(yL4Ik|WYAY5ywT&T?QJ7MC4+EPM z-1SI>sh8CJi*>1BDQO+*Nol5wWI#-_dXrPxYG@UafjGF1f0GmBq)Ig(u4&caZfGat z(O_Vl6k5LH_(w@&v~-HO|0#GkS-((W?&j`@IK@vGRFk?gI!np`oMv^;Zg}2eNDT$mWkU|fkOqbR3IMZrldTieGZ>ZnqNRD?g!ODr8wCR zcs@?;@RCR^F#v5KEF}grCL?I-IH*DaUR0u$A1q4jjHI?^ z%20_KaTd;<`!FcspW9M=hzCFx7*f)Qsuc_!k^-@I#!v~w9utk8VHok`JR#(X)zAAI zXvO04GN-r!t^uqugxDvRcIUd)#6mHV?vq1iNfUCS(S{Fo(+2{8JOF^st88m93%|h? z(2wX`LTDT8A23$XFS0K*4-6cKN~w(ARwc^I?{{NVHp&FrF8Y%QZcp)W6L+klAW)?k zS=bF>U`2R;HBPbc2P8$A5GVJs`)69eoF=o9SODCCyIJGm&kQ6nb4l_sD{F_tlLwYB z16KIH`xgpi@jVPg2^Du#9VQ-Q>|Vw$b++@MT0$(U&27-*|6vD)u2itQymg-n>pC&w z|6A{O5a+xhZm#<&z%@_*CEdPS4*9 z1&|IMyn6bB4iY^jdhmqkvp?5%zP=)Q@l3_nt1;7<<;3qn!{g)KlW)yB=3t#*2>zoHs=k!V3t6ISy#?@y^)WW+k=_KCxDp^wwe|awY$Fci81W!ZrMVdJgL0(J=**zvyntB+29|A(iWNO7 zLuh4(!T8N=Ch>q+!lqdPMP{|54zsq^Xa(o@LtWo1-<2?52L+t;#5rTFB!U2g8G{{O zVM4;L|&;f~Q2;3s0GD!AJIe|> zPf1)IE^>A7ZePuf4rLIKQNSH-XW+jaH($4t69o0XrQv#GD)qlZPeVYz)|@fr&`g2= zG8ct^=V5+a?qI_$a8(AD#u~7YcI!&p3 zlqO&RI)NxVSTR`H=QQY?^+a01ppLoN-Vy)}SvnZB(n)d|3b}u;01*?dG^LQI43EoK z)&3|oI{ucJfI)ACJ`{(%8v&Fae#+P>0s0KU2q!|c1m?|&twfdv>O7;KQblbI*kK0+ z45}&VA5{aI7!=~cEvLJU_Hdi*!O%jF>Gnvg6YMxn{%$|U1C8C>SYPO9yBBf_-XauZ zub-vG^%Dk=waewL)vJM`{P1Ofb-Sr^$cxaevKeyYdUmhZ7z=qHseJbzDZs>S#zN*`m$9Z4 zx9R=+dTQOpneL`>8=UObg|4?nJJ;_SbZYyZv(nmCkMnFtUkcZp1NWWH)nyA?*ut`2 zPW(y%+k{)#!WO>PfnD^=Z|VSozb+UMAqM}Vr_ZrWLv!nq)YVRug8@kB+OvrZlC7zC z*pzL-C~5{4O=&&P=`FTam-VpOHd$(fO4x<`HQdZNPLZWZE^7RGoWrJY5UR!8%K75^ z;3Xk;0-0Sj$Ena9?_}(W91n+T*GfEd*p^~nr44T~`=y#K2&PoA^hC0wng0-xlP5}w z0^|VLcf#Hp!ls={xZUN45$smes1?(DQnxt;pQ`8&RRduhM=W8Ek)2K^ZN2D8g>_VS zF=aa~s@+tInfR!M*`#cW6|g3*TwhfmYB1MEIn|AUFDoP8&)4mDJ1Gmt z#V6#owm7gOz*YsAIB0j#ln6Rd-k9>G-ZVGGF>oT?G%L7F(jTNId%D#aM!|a8ja9By zAWZvo z=gOMRBk?3wk-INZOhAQb1vEe$8X3u~A~2S$p&Vp!P^YZ#DJh>d6$XXRpo(?L*O*dE z0u3rAhg=5+s30EelmW6?s{phDlLug1D!zkU-U}_QV(+md;FD!S*cVh_RGkBYJ_8!a zwYyu6N-JiR=W&T$5KOMk;=seBS%R30Mx96 z?u_p*F0|i-`^*-$u!X;Uf#+amH=WS077))Cwy=dQ0C+C+g#|ei2}e-LhBoQOpI-ow zg4C{I@F9UO1-1az=-NrLezH7I-zd?yg=ZteHW9gO==c1xLuir==~yL^Ag+YS7^sjIm-aT?U9qRu;Rq zC}<)8jM(gCx<~fARc0|DM!5rO2dgVql{~7>l5{KqMik2{0=CSH+K|ecf=iyARL8Dt z2ENn*d9yxTin;a`PgAPRE29XTM*txM43`AHa9!_42!;1=+FU7&clysin49*IYzQyqR5H4P=uO?5yvHc2hA1Sk+PPM$CDW&z_?NddPqbfQ>v60My7V7i1X#1ze~w(EzE(oXj85l>)o9I3SEO_;z$Bn?@bQJ9Ts~!u%6by?mPvJnFefk!`9t|IiNoz|F!0JF zXi4g9TSbNIy(OeU*Gfd z>_+->dFvk4lZU#X=!3j`;~owD-g-7t{l>dAKmCZx`2)tNl-?vxTi61E5o*53(C%*D&T3R=Fs6Vh!v^S1 zr6t-p_(p7V`Hh8qqg$9bwl;3IK7}2DwKwbuoDjEAP$FQjz{>$^@yRA$N#AS$xWK`` z2WdbOg9Fy#BGOG7H?*AK*8}5YO8J~qe4Lj(>?~WmBmKPL?Hi&kK10C9K`08_u->sb zaZ0ru9MRVH5>B536ARb-rn;5QWKr~#vO$0!2uX_;5GPlXHW8_dlr(`ci{u(+*DMAE znf+7EwQDdPByIXdG@Gh<9SO^b6g-e3l#-l}inzN~u{#uK5nx5~tGZC5xvni{3yVOv zm~F~95-_G|kyVq)+}?`n7b!zZa-c4PR0Panw=pnlhG8dlqFPj{>!hxIgPGD`IaL?y zUW|Rk;yXNC;TU}zXm73 zaR;ynss%}oiQJdxIe=3NjJ1e>tP_gu-^lfvntfg)wPZjo)2D28j#f1lLJ5rN7NGwiqb%@v_`HSOrn%ti%E%yR;;2@)|G>@ zXNC}%K|r_>uh2a!y{22_fS0utkaGy+ISsZ(oTbs4$zH?3puL1T&4AK8 zz1{}spP~^}RcEQSI8{ROWBrOB1m}DGe8f?yRwv>oevEo$V2IzJt4kW65GO3B;~f2& zb*0=#y(*By@iE}Ub?~TzJx?xFSe(ZipG5bKC-QxJ(eJzI@zoyx%L6SPbRyPmy0}l} z{t49%IMKZaG(SI3dHXSydF*5RO!8IV`%VXV&P3#Ue)(|+opw?;%d?M!K27^GT3D-z zfuvo}(O$68&}p6Vnd#_HjU+g zHFn8seZF?ANqfnd32tY!_2xIc0^DvRV>#9J-);;LrnL2XGKXw?Ayiw3bC5Dolyylq z9_*z35O%?C2z^$m7mLG54VDfF4ms`5^?FJ67rt*4VBg9vq_`f>8?ZzGg29HBboE|l zsx<*U76C5YEv+iJwAiLR04M71)VQ zUFHOwGP_cM9k6_)xLOe!w*n|F5jS!*hEO*u&HAHZ0B}i5^d1x9)-ae}6@Z{=Y1RaK zf*Qa-uWmz0mV^UnTGjQDxLh310Fb8);*Pgie`v|HcF@Rx9$}P`fWfM%s%@eK$P53 zjG?gtMW97Q2_VAcBlpZ+H4acM6oA=c*W!V&7+4~dGO&&st&SG0HFTah09?uGxQk+m zGUF7md2vQbf~2D!=OxNFjAk!9_teUxXM~k%<|dUQKtNm zk<&1i*gfGk$fD<{Blv+klgR}Au)BaZ!sHH@N7*&7b7;XpLY;G-8K~msVpb8ycXNyf z7^laq90Aj2{W~>KiWAzK+ZcmK%z|QYXo9f#&i+h4iZz=q&Q5XV6Q9I*?p>lFc<9ly+ zkf?uts{>A#&!|0rLG#BS3NThrcl`l&ptirD<-`9!)sqVv9==8Ooo`aT_k@;LAN0EE z_cGJzo4?!J@<#h5pF($aN%Zd9>gsp6(SE>f&8~M`K1TFk^}KO8nc2kskKF+6m`*u; zkN;T9iRsRb^^O7_aSL&YRI){0N9tK*X!AIC8{EeWa(uaoWduNdQvOE(-?JtZc4f9F1M2 z+ET0+)eMXQS7Q>9Zcckvohk#rL{M9XQ8WE2ZEpcpOtVQel*T~?wK>hEBI?Rc>SZ;S z74(z1`~nb?X(|;hx72|-&+&WyZ8vIL9!=m|*iP1exos;MOKFK=2mp?(t$4WH8ed{R z#Bd<%vd0!AKXk`^B|qS?*pQzvJuD%OU6M@&=wMtDIBk7R6P@F#HV2+!)g3UHoYIyC zE?LP23PTQm&Qc*-A-hN)zy|;sP&@~3MVkytxK}BhNh|ggMoMNvq1X^02dDx-0?d?v zE2Aug&SYO=!peG3qgBu}y>7gw*{Xq6%N zZ63nKixM#7^=ffZ+7?bPcRkcQ5-RkS}j$@BKuAg4f@VLj_zoy6Epylb0Y53-MX}SDKe{j7;!~MrS zTelrtdW+iec?YOEfOPq3&)kQU0@Wg=M?a}qv3cJ;v4qESDaM&0KTiC)DUJJj%ywLogEo@;6 zTd-hD*DvZE6ydiBTgNuVv5v-?l{Y$D^@DJlY01hme>9QfYU_6}oW{DI+i<9oZ1k2` z$K#}Hw{%Og`I-SOgRTYP^#OOdp<0@@_7?X(@=aEEsl%Z?oTsU+NeOcan?UT15Tc;Wzf!kT|FyDHmn*z8ApJDDN@Q?M&hZ!24{njM|(H~pHD zJuYi#W+0jUg!f=PEf9+?Ci`6N#5yynijg2xfZCj|gt?>!-C|{$E#C?jnbr|BhD@e} zf13dSlDWQ7n7s3BT16DyQOz{V12cSP1+uF38|@NRt9xNHN;Qw9`AX6>lBQ4_Yc#f& zXf*MOaaBPh(PCPR-9viP9fw^V1<+&Hw>sGAc{B@%wlmRfWnihz%l8TMX{g4s()t+( zV`3TLG#j|Ybp&?VankR&9V!D*jWwV~DKmp^vzeKkNpT+}Ydh;c0-gx~RlGeIiW)~f zInY^7r1ePxUYWP2w2yVOVjPRMhDCsJX1HRsp*3044v80Jqt#&=u$z`Nl!OpT=-1Yf z0V?@~g89Ne7x1NkOb_W=fJQPvN&=x;L_j4_WBVx(;spL!Wv>`f)Kc1MV7GJ+YD{XP z(8~1<6JE;7>i*2~X$e@OJ89+MC*L*Af6WuhoyjMMLhIiI)M%+*Qdv=SOj7vG!Lbsc z8!onq#$EGt1VlYRaL>i}EZ$*=H~>UR8&jWy`G|90F0wlj#ko16xG}STL4a3%w3KtogMt}HsJ^1VP4nsGQ~p)pcQ{U zOfky99s|SbU;>3_=5}^{ODj2oynZ?8c z-EW;*WPQ=_gDWcE+X*l=^n2#d`tJ+3bKe1(kN)s$9pEbbAnNJ24qh#N@lkiWBed|2 z3A@c*ykm9Q<9ONusP}(cZ{~Pdo9SiG+r>R&&F#qj{COb*YO(kqkB0X1PTG`Mh5dSV zsbgXBFN4{%flXW3!WQlZ?9mkZ#ltqIY+(yqxEtJ#`GSIhBRE9f!HsDS>5iu0LW411 z;jTmEk{AGs1`4~zk&d*EZJ^0eY%EW6viA&F!P@UNuL@f~W8*iou-ro<8Kk(DgR_yu zu%Ok6;t(kU@B_ON_6*s5VxtUULx71LLT?eBrP0_jwRlrI>xqGz z!(DmS<+yp=mCe@m_DKu3%8d=hG5)JyVKqi}Z{>g&v( zMk|bm&_R8m<~Ymx;*5M_!Y&j#Qn~gWe6cj(8V80D3Ykyna=@zv0F$rr9fW0lEhbz5 zKPBZQ>l=X~v?S0BTQNER-Zu)lyP^YpoHwh)mhy+X=ish|B^YJ%yv}bm-{~Ir1s2vpI?RSvaXGv|Vj?=GLy`c;r+#t2<^NF#rXPB>_{avW%E! z%ixck=4KCa9)Y_W3_ZEdsLEhqG0!aDpa^8%J5TiC)Dwt(;j&kK?i zYi2fOXfy^Lu=a*q8rGp=<5jy)*Y8Ev(nQX@K>$z%`wbi2*woEl3h-KJW}$*rC4Wn7 z)?*WOtl{nk(@M@zH`Y6+w;z(70IS$+j>!lMIHMifOxbhAX&H0auSgGUshaE@JB7{D zs_FW)&mEMGB{XSaF9CFN5(2F77B(lY;Q)r0)urMRW`n{kGnY95bc%u-M+IgU@Jo1~u(d@|f(44|UysnP1Aa(+ST>7h=^ya!rRtGJZLlE#5_q@SaW`#@_OF^k(D2&vNFryZA z86?xEGN9DKFS7gq*lWe|87Bck>q*+W0XnF5h@)PM?#mfEE($oo0~S1#ow1qV7zGt0 zFrwh8h;EuOVFj{irQuH$!)RWCP2_z+fnET*7(|8@ATgaBD82yLW7j$$Ai!0c44md@ zkpO(i5TF293B{7xKUyjCzW}Vj?X0B90U#4jp+lO0 z{c`-;yVjEp%8*aD<0yQBq6Ajui5vHqV9vB$HwN}#6wV2#DrH^&nEa7dAAl*0sV>N_ zX(j3wK-FZuZZM>>7_j5M%Qm^9yfs0e;8TvqsM^_X2!J;~1!7Kud8uqT7L-yuvHKaP zUy_g^|8}wuKn#D_>1b9SIeKwcJt_WtOdyz3d}n1TEjytfD}Xh|@ADjO&iAoUxa<=sn>Eo@;6zgocDQ0NyAf6M2TEo@;6zoGC2 z&JALi^8Ce{ejH3Iv)He^dFf0hW3Lng2Z}RfT5KqT&H!yD*cg6dU5he;z2s6QgKZ!$ z$=1mBTQiA`d2PFa5CD&dy$PEi<~1OMIA8-F=MVrVi<`kScLTIF{g^h(hY|z;tljw) zOr&f}CKFW&6LwP#K*((_vD!(0t7f@#5ywI+Vu7zm3#~kQ48Uo2jl2e-$;@HfxNXI~ z_dLth7)~an&F#%DWC~(Hh#TMxnF38od<88-GrQY`zUA9uIHkI_Ew;YbJQf4P$b%5f zD1ef=DGP_eBt1B=IuKW!A_L}6F-Iz#v}Xd`a{8O*xx-VcXVjM9i17WU?>mcI9dw*# zPh&AAR3l?YNmC6rhE#|fcKKMUoycbbk(s>;i0p>obE$OB!f4pRA0YmL?%ewNuEM zRDPXn@!0PU)DSt>thMzd<9&biVg0+b>JosGQz#3q926b-rkJ>vR3O}kGC_kVvw^$< zTexQ7=+OKd(sR0J0)H}Mk5LagsppAkrAsA~9FV~w0ck#K#3YZrI;v>tqLJ{MyF%Gd zU~X~14gn@UhfA$Y5c_5@qAfwIm3-G|3#7-9~1O^?G>RE9UXfdC#Bqw)%d5^Nh4Fb_|lf-K+NZdK~Apg)MC1 zmkqzlyzmv7Q?{^$Equko7dsXnISm z@ppEx5dbexG+;YnBi(JAgYh>@hJ(pJ zuuw7pFBE zrI%03;Dp;=42t%or6RNvK+~Vr#eQH-9O5W|KIxS2mSLm`y=T;>Xmr^XW!C(+W+}K_Xq1=kcz(Jd;aHN z_P=-gyZ>H)9Q`h>0opBWVGF@hVjT4YhOV9ODK1pvKNXQT{S<`Dub;6+HXiPQ9zw`r+1H$Sp0aI~GoS(I3 z!v^^V6d=Q^7!fbQI&(|#10|E5!pzSQKnAP?hb9HjoD7pOoraU-Q1f;T23Jdw1;iD< zM=mG;4nH-mc}F3jw^@YEsfspA$V#1=Lonj;p`_~y#pHw5$mqOxrV_6$0VeLuxCG!s z&2bV3CmO{>EiGy2brzkwi&DFQc-k?@`SkROXoW+*{?W^YxK}QGDUOmSWXm5WeT;&6 z4i*#2l37PBl|APN@zzwH39$s`b&i`P($INu%vY|_Ws$8_Kgx)*ZB4YXy1j4_>&@TJmf8Oihe?`hrx-lT> z>aV%yC;j^WUH|(dS^-TSc+>JH{qKLRpeSAquJBcw7u4V~DfrgVXh~`+kW*aAO!eirjtZQ2^-^U@ZvE{zBwzu`8 zudnk)rP!oqO90q%u&TzPKG|Lpg=8~uVHcvTa0!G*3$%*wxQMO1T0j!uwelFih|)t~ z#V4yCwzeOfgLw4!;yO{qO-9(208tc_8EECk$5E&DlQlV5HBvl1DGfHGY8VxF%UT>% z(lxuPp)_YQxtM%4<_?+>`2^U>;LK1R(1G4l2#!aV=e9VIRm|1SQnP*d(W`PcCX$L` zS9uTSkD9i2%ATAcH<*~Wx*C#|9{`f3qit8rolA9-2EEr@AN^ydlv2x0y(0}sN=n$liC++y+R%gVJ75m34#y(G! z1_-T7m~8YIC3tD{+8KKQFGP{6MJ?33U`iV3EA|m;CZ(Ctblg+5!88v5Y|=eG86|)O zab;z#EUYc}XMm7BP3_uiV0Gr@lxjnToI_Q$>924Sb z;7?{yVUomm4tUgFK3@$A0Rf;nd?l8MMvcyO7iB zmXG^R6<5W@NcG!~h|c?@*RLx}kGpiV>4S;Nqf=@>ensUwC&E%H`}c@0Kj_zuy}P_a z^zdOX!VN8-{e?V~hWCH3-_P`AV=kRt=mL$tzX^Iex zF_Zuh!M^Z~`z6-%uo1v2;x*?q#YV5#J9hn;>_4nKkrlYyNjXeQMlPx3jHX&b&_--# zSn9B~TGShCK@K?mJA2>2{uMhq*(ZyEMF}8U%)SFdM*t?<*p<`AjFna_eigu%D}WS=aSob+FN>!;NPQ^SGY%?DKK6^bOajgs^Qwua)K1t~5Lf4lQnpid z*EyI0+KfHEZr|c`x@K(vW)qBC6dDaDwMm~ zwSFIx@)p0Rc*&@HJ%$lWf*o zLL7dQ0(6-$iL?Ncz$7W;1KjhI8CA|%OL4>zL)DfJOd_z>3~O>wMQTKqaM z0a^_P(y%7dJOv;I&}_fA_znRCGEj0%AQA&d40>=pNS7aQ%t{*4?!dAn7}y@Ltkd+; zC@Avz%z&s9Wi3IFQ2LbtVfW7Ungu&CO|N0#iG{=O_c&g4MB$XRkFHfaYB3t}qYJ&B z8BY9=f7P%5=#~EdVy61I-^-d!d^GBxyrA;c z_sa%4ZD9*v)xhgEUS}O9GmpA)JDQ75Q^9Bs&5n{PlNs3jGbAjqsm|e;Hh>|*&4IrY z`3JL0j!EAXwgWcCv58wPZqYL$a*7DQPHBxkn!Ur00%HP|u;J}Yph9u-9huc@2k*eX zfrogMHaVAq@guEE$l;%a9ARTF7%WgoFE1~#2Ak@$YzxNd*|)bJ<09e+{HhMX-8W@!!m8rDtP z@riXdMuMrOfYh$rP$DMI@%8@6*G;BzR!pdTGK!F}lG2#t|0V+vi?P)f1JMTS^OLav z2V-+V{|YD0ofLA8%dP|)X)ugO+Kb}5#jc&r>}9XeX}QyN$-G}Jt_g7zBj-`5)nMoe z!4Fz(HjMm7N*IJxUQNH-XUBIiaje{lY~VQMa9Kx+0So37ZYtGd1iT8GYO}7F3z(fOubO**zo;1nfGy^DKIUC6G-cp)NqJcVspDWoyM(j}>U@-u} z{1{A26Ayq4z5#rqG{FOi!pU$A^;&|}gSZsNUW0A$bLQ`toIjiBa-7r>9hJzHOiftz5-Z9w`!0jM{o2dtp220qEG zQKKJc-*>Y5HioM311oc8-(b&%=r_pRq2Kd1JSE7fFT&%OH>-4J0wtAjt!`*+tb`=P z+SYRHE_Vk8mio1gSzi1=V?gTusUrKcQN6zofpNzFW3Saev(Fu9s{e7Xr~g+|7rLJj zxH{XP(`wrIc|!X^Zw3|+f0wqfg)O{kf#-#P-sg`0g1)%W_c%mf?mz4c+kaG6kN7QY zVGCc805*?pkTw`_SRc<#*IjJnf;=!NV_vu(huL*gS&K#t=^<^R!p#ipdO0KA04c~@ zK)Y28%o@j5!3G$HU^vOzn$|i_l3xal(&oHa#z_YX8@H0twgPHgg9)FBqWn(bv3n%G zgq}mYHtPf9^fJ;$N;R>iVBVbos4AAP=vP+xgN%6Ag+DRlJmo@7}d9*o^oeUUZL{~=FtkH*r zKLP@fcg7ZWytSR$@=M@6l*YC@X=025CR?$2@LQfl&}(yCEe58-h%u35&8B${Mh^<- zC&ZPQ7$~KkQD}Td2hFW8+o=cQtDDMz3DUug8u#MZK7c*&5jK=oud_-8SOL%_gCciB z^O3!-AH+Z15!9O-txld0c9Ik};zGz&&XVg91-7~u8tRbp*P`rbLpXOzyDvdO^#{lj ztf#!&0nMnPhOf8RODaF8v;ebYL|A!cK(_z}cP6$?_TZ>i&S6h0HW}(tc5BAy3+SKX zW=K)T@D+#t6Mz>10tfv(8|WmI;vMyA$(IegN0 zH2RkH@wtqFg{}pJs%=!zh^v2oLFJu4==tuz)A_lvnR@&WAL~PdMJHLm>A^kKVNyOP zx$7h9yIEM=>@e3xlCAD+6xq}SG1gzbOZ090W1%+)t3E66qn_OIxBK+$Y=mT zx>xq(H_oD061k;3$osej^uHignJ!X*V_)$%yC=m<|6V8?CO{BWHMWTLX{w}iLG^5` z+R%^vAF8D)Me7LlvCU_gZHhHH=Vo9ay~xykx4a}fFV0UmgOrifjKg5iOc3InrW2v< zuP-YDu7NuLh{Nao&Dao2t7j3=lz3}2)tD~J%yC2To>a$^P1fan!07AbG|8!SDmXGD zW%SRtl1pLslQw8dc6Qpr?(~2yv>1xbD&}}eC2R?{Z!ZT5 zxEgv%GG+&8x@9R|4LAs$fN!B*U_^1V>1Y?eQMXQn*JimZMJal>d1EGJ0M#h0Y!_P$ zm7`G|4h$SIok%x+Hk~-Sp%o6@CCq&ToCkAmQFP^k`0l*G%90rCr48>hcGJ6ARa zeWP_pNST+Pd~!T<-~ccO%|3fG;_2A{4X~R8%t<^CQJrLjR!pLw)BUyW15FdWdm)Xn zxujD?1`^Pqfz4K=4hZ)v$RJH zanHe2D#QC7URw}7%ZjSxQFj|1hD&xdh7tMCiEbJ3eVG6-Y4o?(3xiq=XE=N+*z&2l z4L0=Vg@wl((cMJQY|nC?)i)0)1!k&AE{7t%yifMS$uhrn1tveAtZy46HG#nKIEUOK z9eZAzI1B&YU#cnh9?z6YeLqmAE;dd!=R{_*bd;X%`84@c%%8wS3v$mJ94Xbk_D)ry zLfI?$r4-1;GD)?quZO%4NIM%j-b#@Yy`L3ey7?871gA2% zhyOO?__iLJZ+k4wI{&4Rq5n|N^N+Tz7j%1R6u@|gI(nV2ZoU?vhGVt&OT%Z7KR#Hc zH4U3)z_$6HIlDUzCz<5QSSAg@$amK4e=pP+X-$B5>O%dIlk!H^!N%Yf35t=X$mf*M8Gd4e}MKg955i5U^XF9qp3O>R zmul&IX;%VULZ>!Wh4C&sSTsjFX5TN$j2V`)z<24XJFc8;loott_hAJXkneI+Dvb7% zgAN2%8K9KmfGyeYj!cMgi@NlpkEvDWXiA~U!(`EF6EP}cy|lcb>rmYn=;je(_0L-N z4b(=ltms7gC5D;cPr@`5^%GN3TKon2Qe=4%oj9`+qYb_=U=;cU2cT4QGK4iu;ygyW5G(t5iHl)fDIMEo4AQ1`LT*Q3zLRM5Z^|cyj`)lilNLtjOM4)$%}dYpw`GWfx>}PDFBFU(pIZ` zAKm$End%FN4F3`cc@qg@xNVyfA2=xDfSN$jAc;}bOV%ahjDlOetIv@0zrY%T&gH9) zUcfMMvUIxREUZWws`yw&Xs(Te&){*!S4e=KSNF(6q01hr-;uED1gEAmhge!5VeN1w zA15OR@L2TiELIACIZ!B;izyYrs4s3-t&@@=I*cb{W=K?ChTYfkCyc4@0$$Y9&iy>= z#I4Pcm6X`oqf);B))Rj^M6Wbfl9fagsWUDC4==}q8y(vaM05`d>4GU zmEPu4{E+qx@~_)oX4if5X-mEZ2EM(Ee?ZrXZ)6`m!PG`#v$np#c!!+)+}m=U-HY16 zlYIl~X8PnG@x7JIU*rvZcN0k3GGahxg?V>7@K5=*YNB#dEBLxWgn}qt#liVy@;0TN z;Uj-x$acVX4>`OFvX^de>tJ=1$ETz0e46TiJ;V*YhvIe*Dsv-i>ObX9&kUB={K|Nd zw@{8Iq;avib`h^kD9?#;v$#;E>r|{>YJ2(8qF5O4vntO76v_E!IIWIpozG>3TV#Nf z45xe(SF#0}AVEH2t=6_IYUkTHArh4!y2WiOr+bsq zp-EBm--48`0A7hQK<;d=okRQsn;yS(e!tZj@OiZB9DB_|u!&J&KtU2TbXPZ#bHnW@#Fnz@ab68WT4KpWY}Bpuzd5~+I81uBnTH?_{I$)3zFs=0X(evf+MGNFSBaat*Uj`h`&=_zu zU)#DW@6_AMWo5bw9`9tXQvJF3$jd|$;AMD`Kh#rNJE84DDDudGhjo!4kc%I3y%NMx zaB?J*E^=@tE1FNxfl04Y*PFUsav_VXUQ{o8`@~dx1s-!v!QFX-AzEs}{t>ty*)lJR z)S@jIPcMFKn+pni5(-+3&BKwot{xsTa83t{M%3bB`?JfG_Pc zD*H$jq(hQ?BUWP!{^`opldD1M9NOdad0nV*2#vG~N`ED$eCS4z#;MB6oDF6i@_lRut*JDDe$#9fHX9Wv z#cHSHJ3uP#0Da{2dy7#e&EZTev$(wrdP_N7A}&Qr3Kazar$!y))qYdPF^^b5!OxUY zY~HNG=X~gJ!SZPn?y$nY0Jm!DqLQ#P_4bzI9XVC(m`L>$gA0_MI0#-{NsX|kv4W8) zHaO<&8?Jo9`fF>;EgLA8fR6x(RL>SCv$2U|$|?2#5fL7g=U3`k;I7~ko{6MB)da_t zm{Uf{EQTW}+-7B~Gwspj+06Y#1>(2GeBa|P=RPZ-tdx%RDJQCTt_tk+V*}P6faY(` zt+`@qOxlT>H&aRr5pT^#DSdF}Xf32o@D12T?-z|g3jFCG_>~MWz&(Hbmaa~Ps6Y#w zPPLArc}=FyUJ}>AK8czGcGb*2Qx4z{Z|(fL7(HG#RE~&FCq@8u9i=)Xh^EUJVbn<- zD7kco;0(i9T-e;74~JbZxqH~WfavnYVi$^k>JT-PK^V@vrs6L}D6ko&$s75r*(}d6 ziF7&E11n7rebWjM65Y-gPH-5=5pfZ-x(%S`0^s^o>6#|YpGUi4a`YQO>)ToxWfxv0TTa~3+x)UDr{~~b@~y&TUu$^5-7_}xH^rXymIp^N1Li;W?z^(jq}l1( zoc&eXr8e6s80P5vkLwgW@)OS@){o2B;SHXmtF(vWx8HY}&uq3?^4^QU?4rt_g>iCO zmQY{FmV5gz_>Ll?8=hZ~WY4&N*7J}EqrMdIWxkap?(8dmeKkFR6I@$0Vi{06F*W}Ih5Zjc>-u30}z3Qn4I^;U* zqJHB9xpm-weuV#Qa#-!;IVmIY5V96R$EDyV@vuuyf@ESxQ?RqyuMaG(4VKIN%{esU z2>EaO0IywK)K zg~307)OCVR#AAQE>P$Z3jpX)Uz*~`be623T2B0*^dorsqBWP1R39#kf!HxlbLz)Q-nYb3H6R&CIHm*ZJP<`-!ua=zX3cx;x@6-{H6w?mC|5`CHtyc{CNg`5 zQl=h+M+Z0A9p%xQl(6OeO7l)=^%|uWfkatIZXix``g(` zeOi&sm5=O-ZzGnt$-WYD4749406`eSm2pc*0^S}tdc~Yy^TZ~Dc=}h4wgpTU<}m0T zT$QJ*l^T}sP0RnKn8T7F%Ped)Xw<(Da@uGf(S(_oHJ^}j-ScHpq-pfze(-64%98T{ zt-^HdlLDJiY>EA5sX%{JCv2vuVA1iT7EQ?%CGe!Cz7ZPv8)IN+HVxZ!oToL6NV4L5 zeW+p0TJq|rRo({%9VVM}VK4bN($!`65x$o|pcaCYOn=&4d$XAboXg?8~zt`OS{v-gXKT3foY5bvVnr?M8>N{`lqx59?6ik{2< zg#x}}dmGO#FpYHyDETh~Mjfk+sH&nIm#v-ASTzg+c>wm52e)hgr1ZZCCcEF=F; z1BG);9@ORoG{ev36TmY>6KvIjMqCQ@OAeES^xm=zx8%X||4QBxy!9f_d4haOF4x{4 zJl`&&!*pTB1t?At7rAr(R}{7ekChL7mkmcF@Eh6xiD6v;!}iWZTwu36YEi@)p;3TRASr6PWHG?vxa1? zupE>$!PqaY`FAVu7?6p2jihP8QWNZ4x{^2li8S*O3!F#Y}wsr zi@%tbekW7!HRmg)VOli}AwZG(ynnH5J(VrgFk2K?dsQx5IGUcS&;l_MTLp?SYV;(Sge9TDvbU9+l#%zd5x?mk8~l*oZ<_Mc4b)#$i@bWu+37sI zt$}FqE{ya>%NOA!feu?R4Qxfb?A{*VL@an9FWfU@v1nI-V6n)xpubPzPnuO6ZzR!d za?j~QQ~7ImjCHL%R)=5?;B_3{iy=3_m#&BqInnX6=-4neCSjmqPgdb}0m*0K9u>`! zE6+dShpHBPf0I0riUmi$iU>(ju#{5vZSmkbYF`MBrXP==AKa%Pxm4bHJUaAju@Xk( zp4JCxhib8g(|)`XLKZh=L}PIr1c}x?amc$*wG%{v3;2QxQPhLUH3v6Qi@ZJYJrwf%jT-s`wr?N!qSOdq-TMGaz8^J#Y;qKH7TiaH zxB16z+t<7o_DqIH3gUeq`GGtg3lw`D8^8kJCk>clo0t66otJgogbU~1otY#)xfdkB zUGrm`Ev4n@dr9coSk^0U-HgxK9@km>)eGO*M6tH-G_bs5rekO3nESBTN2kS;u=d!O z;l;H5Txs22ZaOQ;6S(G1uI=J`dO2vj;lpB-P94 zVu{mJq(l2~PU$1?eX`{^;XydJ1Iy0ipW4YsRp`mu|4uU;EEv#PzR~~Fo~`sTQ{DA0 z%{vNgITQ0!N*u7)yb}MciR$ceGR90%c18(hxc@K>E$_z~{icx@RXFh5+1{Sf(wO10 zpK<<5$%HwfN-KK@j!)&CcvI=m=T(Sb5Jqfk*?WF}el7aZjBqW9R+Uw!(77S5LJm!5 ztG|Fy0$k{=mbeCl_0^iTnXOcN)2zM$wB)1x2rZozN3k$jOc^%=mKZ@Id2FS9)`O6= z*BpQXYTUG9t_eVWI)JUWLZ5nntqf&&9X3vbKs!6uUuV!B~_cyX8(S`+U85b+>k)MtdV0@tf$}$?;BF>b|2mA3b z3x40PTyw1@L25_Tnj$y|OL>j>rZ}Y*Gck3|to51~!w(>kKsuE?5n;j^^e4MFrUgxf zyR)!r0tE;A^a_AAXTjd^fI^ZAys*4lM`|eON;#repU+ekbaRvbP8hY-CdpJiuiD&; zqJYBXZgGI|q)~0(m?wIT4iz%ut)k_t0 z=xWBe&&b{nZ`1pfh?oXQEX~YB$SOKj=r2Ke6&Ytn8#xpXtu3iIMs#OCFbr>L?HyL- zg#}RtLLgp$FzjzM5z5}fEchZL&?=#Q*e!_|3O| zC$X`7SP`yDEzE;Os$cB8d?ud>uR)Ezc6gsEN^Vk)g671Wv&1{gdATYGkhoZzxoAQ_ z=J|PZm5>EYC7ix|11eOGe+hHf9I_5O-Grz2M+E_|`3W_O_r<9pAyW-1rv z^K3}n@rLzBj=R%o{l@+xVEN{&OLlS6!+L|+(=AL}_;c>wmM0vo{T4lsnX}K-j2Hdc zED{WLkvHjioQUUCM+;ZZ^`?Jqbm;G_TT6ObPgLwV$W4#Unt3e?4j7MbAWy3G!f8^C!RXyyZr?Oj<3;RH$f)CA_^AG!fAfl z+wh6FI4Oz*gqfcB)rIGbK*HEIReOl_&XL?!D*46)-2SGMAwhi{$1+e;VOWgqM%^lb zOpv18x#o}QE@mwu1w0BFH3C<0?d~HTgE;uFZz1Fl*q92kDznKAI6DsT3syGoYvo7H9HW$M+C&JsuP|$yAj)f6UJ%2OSPbAiLPNEyu{q+J>Q=$9NBI>UC9j#@~ ze$!2T)l=8x1Zgg<`97V6qDqtRxUE&eDNQXBAd*(7yxIOwlvS42%~I-&}z4WX*0 zZ6^-^<$A~gCJ@C18qPjh)6MmIqdoWuR^Ea*Q0=tEdq4b^q4KO@Y8%Tr2`l@XtLaZh zM^eu3?TLVDPJki=wn=R}HZ3_E@emI(wA|d);lt&HO}r97X_}jT(B3wOmGjf0@-6?C z5AZz~XA+4QL)>iWHTJFl)WVtLkLtWVEU~ItUqEb4;?IShK|m0q9X12QiPt_l_9dU2 znV+wJ3mEgoqhFQYt5+<Fv39x(+}QYa>z1`XX=kD-H=6HJ@)Xn8@BT~vwGWWBL8Z<9RrmJK zFJauT567<$&l?*j&d-L^=;v-f@AfGPVT`J;oNjymYg%ltMc)3;-p|uNo+|Hr-&br8 zTiZSfadb%5VQgL}mUJ!=aRqx<#g=qsA3;n?L1X#fG=%W&3oQGOI-xrPc}4!kRU{d~ zd=Z9qnaw^?j_RBDI7b9}S}~nqrL|TCRY@RXp`W&@cGq9mO1TLV$H!AF<{Kq8u6VFn zr8B|$GYt8lB+mcHVUbq#DLau|EWKWu9$ROt$^&P7!ADDbv9ZNu0d5kR7WGTaslv$q z76r9E51$)P)I$QZlnpfJ~;EJftc)#C7b^ zUw$khkHc3~1*gce9J4K$hgD2PiuTfG4LxJacJDl2&76Adn^c~VQf#W|Lu<6zzpv!F zVi8iL=FgWOOV~HXvlU#O@Xev{z;vQyDHkx88O>w}(a*prL1Uz3Vx=^4tiG2a$_tE7 zGtVdN_2JsOtcfUGM0sIoW{3y(kN}YZ>`<#Qa+M=E)M+I++jePA#6#zp>}hQWZ54dY z4tTmvEslzLbJgtoXgA_ub$IB%h;W;TeLSi^zA>N4dq}`nBuXRwQ9V}$d_lvMpV;mZ zEb4`%g0=hP9i@m_dmu(j=-K=vOh(&YC*2T4>LH2MRa(fMX!=U8_XWVo8199}WM`}! z+ha)fuq4_#z3m7PizoU*C*$P6Hx<4HS)MybB*EqlBVf@344*!O|9V86#XP=Cl z1rjb}OjKxP4ZqQuwSILc&0V0EsfGq6Vn<&L14O6T-d5OGbzQzEv3gwgiXbMj zaxu3T71Wi|)Jyw)J!pF(uE2Gw!`CJ6+>+tN*49<;7%Z`N%3W3eI!8ALBRudoDt6-f z41UVgg0EvF%ln6OYtFUXRIR&M<45s9$V~g(0XQ<~Zz2-&oOs7t2m93J5*AU}iPQWJ zFV{@-&czcX_f$5s2iv#B%8K7}klp~UAlc$oZ%slT#)N_CGz^~*XO+}uw|3Ev_Dk8y z`7s%r&&}sEw26$*&tJNabpM~~w(U@d?}W&4ga@wHN^jD~Y|PNNZeHYeG8LT6x4ec} zF_5iCW;c#@1PBapEs6z13KEeXaX0Okmp5Rym{NwZ3)ebu1!ioz=c%6<3gb1GU4l{* ztLF5u&-yRj{OKTHKv%*^n3430;#Hucdu~+VJ*+f6jm2PAyr65iw+lgj{zz7BE zF82{W$;}iYxZlMs3CH(l%6h!!Z*++;OqG_fN=?*u!u=S-2kPeTgIzGE5rJj_fp?o` zJKRDgrJDw7ifn>OYiX51R0uF%HzN^^vwySU9d1W5(EL+v5MmLA6Fg6 zX#)-Y6wd`cwCkg&Lw*)MtX?Vx{|_1vPT6R#kOPkZ{pBVB=AUgtWEykIBt5aspt-|5 zxgEw02p`b_)vrV(NhFQ|liZoNs}CMI$Gz!F5_NE>TQV$K6Bd~rF9vkU^hG&#`&TAYGZdzT;a4P{DfM1mig zv(6+J5Du~9TLtTqTZ+XjvL-a~Ow2a4dXOhfUL4RRKUR!N)E0sV&KTI73FK+VmL_`Q zgp~h{dVZEl-l>wOD7{@-=NUebqRR%z;zi%`h)3_lT$kc2s2`x&Xjz#GuiLFiX1fN^ zkzp6t%R4kb2~Edi?iT6CQ-WD4Bt0nEpxSozS6S&y7zjuAA}V>97{gyQ2?Gth-1F>5 zuF6~N`T~D)g5H{|JO4kAhUYYou&X%;rj?j)a2+5_#^X%#wmj@RaG>uH7 z-poh#AG`GotVpb_yti8WNO*S=C%V2L>+K!)@C?s<0_UnDK5}O>6CTd@o(4xzx~Pe*3pbqz#cImqi*1!17w%L&^Z?}*XDXgGy13`$1lWS8 zPVp&A>Zkkw4$63nx2zBIDfGkf!wBTahw2tYIpdBp-P!;3l>g0TT8}HJ;Y`|+cYzj? zVCO2_w!X4OlM@GhmJ}_mE{SRNM8tX_B&?`(Bx`nv1a;pDi2>E*k6=~6=x`UV(#Z0; zKi@a}8XirS#8aXwaA*E7g=qwbu;xaz{cP)s&*a?LI!zT&@C?8QI`^tdx?GT4EUvPy zit5mkRo^Gmc+@qCov@}>=p~a)?eRvrGue-vjhV}8?DbFl-DXpAz3rmMgjnQGWfOZ) zieZXlSr}|mxcCu`zPCF4fIc|+I*qQGoVc{xU~^y&THR!<>zrM5madk4QcPX;5 z&PYtllGRsh>{)o(O-s1&Fv5S$ilrDfzh#wx z(Zez0U+54!dH5=p^Z%BF#RI+=LDhT&;JAXvCCdATHg=T5-oB8UnL zf{5v#IxN&fTa?KT%jJNW68Dj}3Ej50%WwbAiQ=A(Vo@77zja>V0>cpxf7SGM$M}{uQ4&FmX50T3-az2v9$CtGl zw)0@qC|tO>Y4r(GH+SJ6r6%4C27zxVaV+tMAYtstewm^r8}t<&2UgG0wk&sws3IuV zGBY9J#(s5=CldFGWA{2OvaoKpWjZK$c_zaqQMV-6Zm~wE;IF&7KdlU)5 z>M(691+!@J5)|vKep`@fX;n!Qf}U<9l$|Jcnyk>Ly}Za8sPROHSeQ%ta$TEs-|@K| zr5mw&o5{=&p#1;U>Ge@!L3I43*8XG5c}4V1Qr1uf?I`Lje$#vPu70aDFFFRR;-UGS zIjaB#L}S>huZd_bkuST-#{1uN>Yeu)v&ramK@DVpZ^SsB;D~J46a05Nc3XYNFgC58 z?E<581D&R1?*JJex5~z9OA_CZxPB-ql9`yg6T$G#RD!1sMEx$cMSW}Qjxwv1?W^iP zbkYT)bRLZEy#&_B&>>_u)2#`8QbJbOz|+EaIMicJRjY3bk~x5|Ri^+A9pMU`cS@z{ zy{!wqan^qou=_rxNV$H17$P270;*7>_|m`~&acW5Ki4k8*)*=aE!o`1+Nv-)qL+-V zGt-2yj#W7xd|eUduL=lZCRwo^-4yj5+CkTZ$I#-rYv0#2dPn}T8At!wDOPD04&h(c zSNlUL8%L9>0UfysH_({~=Z)>Stzd7@#Z*Skd^9fVOP zp%~y?UBY;yO@WQ;Dl%CZcPheGgb33FFraa$^ZZ;93?JH#jjRP|FtZpx5eU!WZ)^v< z#6;#&b)uBy^uGIFV#-Cgfa9jD3WbjBMBS4BS=)?15iu?g0@W<(8%x51d{=D zepN|>p6zOYqk@{f#iK4z?1dru>>OfwdWNXw5K;1|qJdx7%(mM^Yy{y6BOwBgrxy{$Ei6y*FV7~* z$G!)!ALp+Xfd${2Ovbd~5RO^yWaZ3A*0)QQ3pCy%S=I-y`5APsy$Zgp-@Z++Q$@-X z6;2gi5BRldzb3D9164z!pO<%7taD8_imo4BJ(j7%b$d5lD4ex%tUTtsie92$RwsR1 z3>vuROsLDIdQoqKL_M7g22KnPB3~q67S@%^j!dD??<=2AzTS6qSRAAEICoaK_%;X> z`ZRRtPF51}lkuTOPyZL4@L$hy`xdz9T;H#FWk4G(#pwLVvlK(_Txr7mz411zxrbd!&5=`?vlTSsVb+OQK* ztD&$)L|z%8DU6lM<{W|TSkfda%cD($@I+zH$`?{VT8(x*52;}$se7KCh@3&P+B>T>6gWx<{6Z&i`-CkkUFn!O`5Qd=l$>cN>fapu zSo|Fr%TaoDCYDukHTzE3!7-gkU<=)_z!6)r^zOyJbC)anoi!vA2dzIlY>y&Gcllyk z+mO8PXNwR-rBu`iJ4_&%P6ytHc#BAgI*9h2U~jRYE-?!XKKz9rx$TqBpNJ$y+g-*3 z-PZg_d7HItQ;1Hx=EAiWKyTHBPHT%u*S<1mWN+EU@7AVH!l_|0nOJDO#Oq+z^O`(H z|MKE=G8|^yG4V`bF|kY}W;&z4ZZMHb`4j%sa5(}pG^2~I@Bl4?`~4L+dkmCynh>M- zw$^HN&Ek33{WVEllRw3D6j{wSx&>V0SNm^rH?hn0`D>VpjKTgSK7L~(&2(2N1gA%e zbRPrCYU7OHml|?(6E5iZMLJnMFUN=-3lDpV^ViW1unFoigolyc621~QAfd+h#Tdr1 zA_Y^~S#_V=u$)}>Bv^<>3!zGN#*{;GeOZXy<5L6sxWN7M_V6UyaWDmYw$7i^PtmP& zH+1(lgM@K6bbI@Tr0P9?V=sI&gIuz{acB8R;#!f$uv4+4&tM+YdJ*3?>;I2bxNM>_2uDZ<0Z(aVAwtUspg~YuC8?NzqEwoiASIR+x)mq8k;yS5NSZ+=#l!N zl?mnveu)q>QcSC~hrgAno)01f0e>x0g3B+2AUgYL=SQmIUOThU7P{Fx_bbuir+-P^ zL$f>G?AzXtI8>s5pT4+Diuk9R;SggPG2lWr*p3hir_6skN^;7Q|7@sIFCXbLrUu8G z0%Z2r_s;fBvYWXBs%J-a1(o~Q{tWtsH!l3+PRU;KO0p1R(SrFg)3T%v5u#O{a{8S3 zFN*SrQIsWnh2;D%gdnP{(4X26#ek7%oB_*1XXVwu2hLoFh1p02g{%f)nQUa$8a}D| zzNxb1K(EolgnS!!mDM$A7rxJhmJV5bqq|N|f8wK*bEBW!7U&<9$XJ>svpvBaBiej4 z#H5;1UoP%}@`XWe!tbUYNM^WXHG^Lt(>1Xgbis(f~62(!;Nfy4e_& zeKfR_I@&Nyj>NbgAKhAU%aCjTNer`-ep>h_Y%<GV81hycPNrHPX(pz$^PpdnKxN`#Mai1ZbZdz+Q}x?edFR9#&6$ z6;H``n}YEmLZ|c5XLV$$nHkk_mgQFu#>Uh%FOLGi47Zk`n>SSk;1%kWr3<}-kQXfWDJciJW|HfN4mu<`+VRT2%(^zDaSo{^T#n)jpKaX&CNbL?&93s%}a7*1SIpd z3Vst25&3y`>B3gPLufCHm-xAMZ{~O()<3tsdnei*~E*p*Yv9Is^vl2?MO)0qNQ!H4EWQu`H1)1_M!US_2^EC z>gnpK=l!_|gQojGVOdvC5aNEsN*B;NxH~2f#_RhHjQHvISJa3c!%Ci`wJLD`Qg5@4 zW`5aVU0Ek7g0*kzG`6t$U(I-0x9YN?VQVml;JoCpi?Wy$VACkAAhF6?3?NNLYwa6^ zNYbygNX=*t$q2F>+V-ndE73<+mE1@G^y?K7f5qf(&> z9pXSd90)=95^@k%Kbn15geu8`E5zw!nSA>AE-f+UP1L?B7Fx+_!&Y(X$<(nrV38Te zO@wBKUtI+OX{=4E#QL0D<34j&bB5QlZ`9eEa+a_&-vVM0dc&igm|FLbz6#>wv`yi; zb6(#!oen6yS{7Gf+XYCiSu}dLZ6ibHFX%P-E+Q%7C+D3WljlDCxCJg!ckx4>=Z8iM zz+7U>lPMk!ZZd9zdzpmKV8m=#K0s4Kx;%vUq+^&Ww}f8DY6fp;_t*M@Z7$9kT^Q%r zW;AqUc_SF(;COvf@p^?GJagvH~(OFOzw7aTT== zwCupy%Dy>8?Q+O3;uz*H?!hh$%4-B7T4rOMxquXTWZm(WJvncn&vd5j%*op>oM*Dp zXJRPC&s?6k@>PzQmIaav5LwKaA6G9}ib7m^1W%;Zvj$Rjad6MH+M1WQ#xL6rNqkOP zb8q4}R)^7a<^_#$NL?ix5%tS+}1_ zz*uJLSbl7U+-po6MBqcEa-NT5zPIt!bu{O?w8+1mM->WqE@eHvwZQPneZAKypD-!C zxE);QFS?&mb9#Lwt2+sD`RJoi_U-WXb&$M^OA?_4)p<$otzAqyJ;evdA&C*%6cZvV z6ZuBK9h}Wc0h9AHh`gl)qCKgTCK9^GQqG-Ki1@DGyR*?h3Lz3?Mq2aR--1K#vEMGT z-qNuJ&KBM)J^9`yJlA;he{t3#zkp6b-oMt#%xC^95xc_~uP5Y3$kB<9rq><*wReWJ zQM&(bn~utlx;c|af7P8`&;?gqRgYqAx>pW5v4A*QB_+%5wwMGlPQ0Ki$yv;w9MbvK z><_1Epi5XCo7@J8a?=Hf^a=&!)F!=kNWOEGCJkBhZ_r$ij&jq>=$4 zEo+zgGob6N1_hRpEn{?&4CA@jSKqS@cKQTz!P?46Oawy*(*=v>)Qx1QWqp8y zAsny|07(*4levcsEvwF2y{si zW)ohevF9{YMP>snSCWX><M@bmbj%TM42+%oup$4o5ve5DXu?d|8 zu4W}7>|&|lo72M-UDj0~A7eKPdkqAIb-Oyf4crFHrUJ3Uxl#4TwZ|G?J?9Xw+nLA( z?0G{A$%vFgeVde2Pisb%M%!PjR+el~;AjMA_>WN$ShxsniVzCZ3`4VT=ut%Grn1zF zU-dS&lo99cw|P%#KStBHFCJ5q2wy#%|8;L_o491ORC90g>dJc1cBy3fb$H6&HH?S7 zA1C&^%I>;NK7G7|C!VAry@+amhPu6b_{zOdP_U+}r0U++>fSYuUnsS_D%U06N1H!A zMi-BJEw6sV%X&qsi}U1u8V|7x;q{%q@SeYD%Dp}kr~Wzg)Zv!hy;%cdNfVo)^#+!ex{2Yfu-N38YWo%&Am`$oRWY%xgGWplpzJUPhrto+ZR*#|`qeybx?13eZh5L53o*`7PsuTrVcD z$c58q|_gKj-~a5 z+yp1f`2=NOnTmS>Q&xW|qZH^a;DubNrl~Sbr|OPv#klS!7!{>*mR*0GvJj&gY!eiv z=0&887W`tNe2Jv2%^3~ulWz%x{{FPPuag{9yCvUiL4VzV!)dxUq9I8J zTq=9eh9kCM>;>FYio2qv2o0yR`M>HTa{#NRrpRGg5-YCJeq4UsK29T#OXT92cjb+oC-Vj03>7b`3Y3J*0jIW@CzI)2@J5mIP;_yONnHNwlLOboN_7y47kR%F|{$Y|(P zftItz$sa8g-v|ZT6=yT=QhnT{MvnZ4^GI2uczZ=YNM%N}b(G4@89SWKjNebH;c6 zC=AgpIG?|d`0cy)@s0{>xmCyb4^`uQ>RF=GsNo~M))%b)D$scI^52$`;nHw7>!JE1 znR$yBPVFAx7ET78}!qVoG3}9FCO3&W32o-dt_8) zvLZF<$HN|5HZ#Kg{%JI$t_)OZod1VfmQiI*0*M3w78Mw$5gMTgJg!*LCD+i*poz*I zkQ(2|{u1ws^*4u|L0zhVZ8R&;AC`!D z1Wh`iONeZ77d5TrRO`Sv3=@K}L1jEhJ%+K)n6h|o_s>?Dn)jAgL!Q~26U>C15&9n%Vc znk<}qFp{<^ZxTi{%N#Bdo!Gc_phx=P>opw^z?GhqMs+;>wbqfKtFyx%??s2zh(uGt zb??h;hEA5>&<@5O@j&}|?8z3Biw@E*vbR7N$W-q7;Kg$-XWNqD`Q-rS<%xb zy~w?>(BZwLg1+J6U}~)T>BKo(MDPHM%Oa04&Hv zTS4Tr+cJJ&#L3Hb>T=&wM$tMx9{t$`Ho~8}g zs=lg!egBx9t=e^75;&^-KZcqN+)LIKA43>_;fB}Zs>3ICz0$;Q%^+LSKa<*)Xq|bd z{kJikjLIUumDdV%1?@=lAC;=Y0exatL_zwQk^YFZ9-pm`I>1~up(elfB7%0i!&VB@7mCRr96`z)7)`iYH7Qq`b7 zvT=mxn}7OzTvth<9y%_$ctM#U>LL!^k`!b03Ys7K8W2~Hdd{dH7CM=+(cwUs)mGJo z1QZB>5Q2gPpb3Q2d4I}YABHYDmcJ~?ySwpdF>bW~` z>)ccnam)6d!E@>kg*ibdhktRqfOoubEH||(Mlv!}v-QBs2FTx+s;RH2HycTpn!uVr zQ0^NP{Vg$xSAB*+=v@}>%GzF=u^&y8R(-Kn1I^-&DZ9|odJqc!c)NB+kUbmwE|8iM z=6N&eM1&0WCCDqM90>E>A>WazCQr4GDyCT3iC)uSboa2RJ!qW9doa)21W6*0r zIxB%F(~ndzA*CrECgvG=d zt9$>GEt3X2YuGO9HTh?Flo%eeZ;ITl*nt~?ddq)=TOK0$uH*9;vdG6XdAv9u@o7r_ zyWC<3lfa$&@rRG@t^S$tJ}cGyeY22)ZL%7qvh(uoGslqV%=aNZc*9q^XI0@DDT$%- zxqEw?PqQLx#>=~af9v4lGYBWuh6ly@Ny%b@KYQ;OS7yR@GLG-WZ@TUMZ6NjG(`xe7 zk|XE*sg{dBC~9QDd6Z(({+FA3-U2f^0)2?BLzVs|JPfXI)(u~vqyRZoLi@(+^8LWu z)_urxwO5`%>$}AN30&%r^xp3NUmI*~Z-zgIF%<;Fc^Vj(EB}@GKNaBcx0URWc)9Du zyOL!BsLmNPdMAM6s!uVA?OzSyNv(j7IBlsa-i0)9MUqr1J6*M|ZR*F|&{)fs<7qBh zy-7Wxeg{I>Q_LCf;Vu18`|q_ML-&DH2b!|~hLO@{0N-j>3}?~}2-RqbK=_HHcJ}xj zf@EW|3iV3iHQ%Mb2Z}Lr=x^_PdzgMfR4{(`{$t_S1mi`oMpX&}fD{a|3V7Z+FByVb zWsJi!KSgBK-$(p#sq*0dt_#YK1SVecl_JiL#?y~O0Uz^pnz)pH;z+BadQ5AhVxfOj z_fH!H>4d(r@-)d+%!RuYqEUtRZSS6q$7JyD*= z_nkP@GDiieM#;avbS+2hbafrA&r$kiOK#yMwOocFV!wMQRS0(s6UVm7oh9WUg~m@{ z>~kT5SGa-~$FKRh&k!e#nd$ajv21;WiySXdmqpvr~$*)wg!CMwqDRb6Plapjr38_Vnuf^f}x%?{MH zRpRmfd3)eH+uYxY;qpuddDWB&=8e?9Wv{*U-8_>E)uWibC`5C&~T)O>rCQdVa-(g^SYTMY2j| z#v?wLY(;Jj$?TIB86y|Q8JZA{8q>}Ao3Ow21Z^+Jo(+nBTf@l>sLwb8Li?V0)&@*C z=GSsFEG!+PLVz`GOx1cH(k_}rl>>4zQgGcr4B%7N0DOBJ z^~Jj#zw0KJdi%3eqT84LaIRi6*m(NWfWUY;WG`&2>8CU$O8>aut#p5m8{x&m3MS>{_H7<%Wjcq=o^*grpJaODb@34ZXX;j}Lyr%4@Y65ioR$>fJiH4M(M^-F|fE z^p>BY>0v5thq2yeekR&a?PXMFeRi=V_J2}GVi8@?|BtG-Y>NZvvPHXbcMI=g!MsLbGuNv6O$>9BYN$6u7uaJQ_lDad()y1$lsnZ;wMBd6iq`SjlfsB7_``imfU z25N1AjWT+20EQ=>Hiy9ZFrU^RuYYpd2Y;jlQanH^I$R@Q$s!T5=pw?Pp1=!9h$doO zmF3#CKrO)G)+f8ERt#$pEdlCTEAxo1lmUHdfi=a|MDAkk&?{m5{mjs?xA4l+z_P$z z4aS_=G+~dGHb{0ebq37G?7@>2qOXYUB^X-D1;IZKeq5RGP?FX_Qy_e;6UVgVPKlNP zE0EYw1$u@ottroK!3&!7*oxF}g`<>Ax;Wwk499JMY_kEVg7$|_yf)BtQd_+~ZU~^W z(>e!`7;q5&qEFXWZ4Vs#{ngt!ysj2P{?OSF*Q!9fC);&2wrgbgNa0+!ic; z>+*N6g;aj<;%?*nD<(OT+E93qs8oLUY~a-ybB6HU-Q{fblI0@8ZQkRfGsnOXOYpVY zpvKyYN6CAmbiu%H13b-@9QCqev=$!<5E*tW9ggyy5c90Qy?~*LUCc`=& z>0H)D;Gq7c9(ZN4<;)!vOl}Y^{V92Z(SvQ9YL{f0*ui-CYg0JFl^*NNEp+C~9n$Mi zktE-ZF({FEVa2&y14A5fUA4kg)O|VDLI3#R3(q2lUT(LW@Rt?hq+q9+cBZ~)Ti4+` z0<8fSw|k?ZdZIjs0Iv#lFLr0cZcHi73R8kXv)V;drBpX2s8Y&r@h5Yq%_wtf1B%_2 zY<74K=JoiUnvVt2(!qUb6E^v_Jrq`%-|pq*zx5!Vlxwcj^EOl^M_u-BF8&3$nC+^b;VWP zjyk{9oP9A(;7oqVpq^yDyMGKojVdG&>H-D(JeT|&<1TT`SUNMxy0~VpKt<~!URz*= z!g9!O2R)ePun7i4mS>>(TT6d`KDE<3>OLR)+=pcfqK|86WF>m!ZLjX{y~FC$)VY$U zp0H{B82UrbzziM#Zl7md--ojf^X|U~TLS7exejAMI-|yx_uU@i8*wl_$vY{am-Y^Xb#cb3F^|-F!`6^>QM|lB$alh8gH#F{(IjmN#x%Tp`hH-W7OCy88ED%oD7E zv?Ampk{TGTTJNMmJvCE@b>3)6H{&RD#|{nJ8i6rgANgxM6<0DSFNNhe=AQm=3&RKL z_TQQ^)@1k^D-!c@YgH|$LSuV}RDNdtFmFsdZj4yjV4BR{Q228ZMqVykI>H)Wv{=O| zCl@yUcjU^Yo60kKH>HExM-K2mJ`fM(w|q%`_7#ef^2P0V!VLJFuMxsK8~6puH>v&n zOGfNXv#MaxKk#)neSezXFx*G-A?0vwgSyCg?MA;dKhOZvyTI^z_gnjQmJ#r07x0#z1>?<8vq2uFAQMUL-wv}u7#&gFTsjeB`GQG%ea$sF zDfFnvkN_Qxoahd+eD_tchT>=ORgM6%#CgK%v##MLnD7$I>}5Tck=7)>14JYCA`?y~ zBGE#C_o2XD>qL2uY+JODB$GEkVrZ%h|rQyRgc9M~D)ZMS4&Y@VfE9O{h);myDiT7KF60-*3t4*Gl zzp~lVOs(gdSdlPCa%MT|HBkhOQ!daA*E=Ikzb$OZZE_my2j9EN?7B@RVOwSKiBJxsPS57UuNI{k({}8o*OX5W!nj7?V1*@9x3^> zfCM~sc0zGLRLc3uCLrF-b1rITr zy>b=a%rv6;R@(^p{$4k1BV1tDxb;Y&N1GkTWoW*huXE``aJS@KRy zJ>jkdG-V5ZBc+G{2&ff6J}>FS>H!Kc09Y{b1OUX7Nw(SZ^=MEqB?(Bx6XRDa{=I3h zgK`M?{akK!gxyw>Vd46Wa^d~uEN#;8%eSs8g0DKqG-9OLRg}P081MXkpK7lYdGuvIXY8){gNqKHO)^j^h-wXH z=T2??p8I1qH)sAmnY%ph!@hwb7hC;i1~jU=9Ru>Cb+a8r_yQkExVIC>3$wjw)7nHS^g}m)IbOIggf{pw51NM z?I>iGB~HvL-Pl7fciy>^1uJC+P`vX>`U}a@jhxPlc(BX{zh~TGDOY9Z!X71}YoJx%yS+5?mD-^wHW^oY zL+6PVBg{S$XaMP1|BHpKTf*T%4FysL!r?9JPd-~dS5_Klfs!8$K?@vAF~uR7-;w86 zdJz%O1}5L6d$@`IZlue!DfhsE8*uQ#%>RJ?js4xAL|!U=qmd_PFTv<8hmed|UsJ9| z=}_Q|K@dvT0$D#8N+J;wlbG1!)nz<-11ogewjBu}Tt+L02DckuvPKY`^%wW*#?CQ5 zE!JalA;R1WrhyqqEdfI~dC)bbnN*}n?NTq{r^bt%GtJq+WmVmT!$6${0lhzG*0JD) zG*xN2KLfmEg=%{}oZ{0?tvm?0{78?zvi`Ts@|7w9yp*u_`ev1AR0u`M7?7izt*SrV ziDax#0wsItWwft|skxeGB9jwo=D}x$gma>?I>lT$KWAabD2s1@G!%U*&ZCQprE_-d z;JUKZxMUa%3YuZY0={ROz&P>ssK51ZTw37oH*8FOeq(|yTjdrZ3?+_`7 zEh^&AXY#a1i*8eQQ0DWW?~<|Z3pu#67FHx1s=na4e=oHbA4QPTB5ei#-eXXv`NJ1l zLx1$~pTg@ezx%?L*y-fDJo_%cEt70V9;&u)&c5qj&iK5rF%b#U_iMwZJ?8T;Y_?uX zHjkUD6Z_TvJrBu}<`H~UWLYllg!k49W3c**Du8z1Quli;n5JNHpa*l)U;aXN#C0yP z)yYxOFjd)4S1kYjSfvN$0 z5Xh8pEfa&Ak3N&9erkw=VscS|n!k!xLYeVY>Li8+gds9^od_gssBy>VIYZVs4V)dC zilP1ixHAZR6?))l&;$B_t7t1zGGRW&%Rk+K$-}_Hj0C!GbX^Ri8--;MuZMODW`7w+ zT_gnUxLX8%!$f-QyY&Nl;YIIS)H{1v5?zm=n5QA7{EWsfFy$<(tZ#68;#=k6r+Puq zK*sn?6Pa8fs{DO2X9$4ru*9O(QfCSZeaE7=JTyt(LD;q?SY|>{bw`TVuP-tI^Qs~u zK66z7p={yzCOrbV=`VSnm(<>xkb)>cXdwK>b}_#=!Ii7>Qb2^av3{B-3>e?GmkdnL zT<$j`Yyz*=`Gt^NQIV@xg6cZx9(tkP%6cweP^*Jby1i`{fx~L`V%XhO$bgVkN7b@F zpDmAF?~nW!@E2z1=%oM%YW4tKZ8D@UBVBcA3$(?@NivZ#l?N(X5WlV%o6~reO^Ivs zw__qPs|hhwJZHYqV>ODIu>oxYwh|!KnXcna?|mDpKDn0{pDyb)vkL_=B~S?YG$2jO zXamGDT=R16O*7I_F-#xgy);!pb!D3I?5@wW&8y;}OEriX7;gu7dEvodVYAS1sr`($TT%m#LqC`^1Gl@#x7~HXeUqjq=Oj4BBguer%jac}^#o~#nys7LtCB%zV^Ei5F zeIRqmw_ZDaGz<*8a52lx?xO+~5$R`71i)kHR5S1`9?=hVtBx-x#0Y1oX0Sc@X*Eaq ze%9LE1>Zsn+QmFF=R2L*87@0Cl0U5_1fjlzaOB^8>I4+!Ot=%Z=%Uz5X0SN+b=A4>&uBGtBa|2RYgXmT6gr zhBTN~*9?0}YDdFV0M4p3a(b}ec2^+Uobd{(6&wb*sISnwP5t0QktU2mrP}Dje56ze zad*F!Tda;HTC~HwB4WNHRa8k-z)SkzmPk&=A+BW%hi=I?T@2E4?a2`hz_u%eQ&|zE zYpdFd%kw)w>fpP!!7ytr1i8UWkR0ci{s15x2Qh@Z0A&FlV{0OnGDGiq^ZS*MFh;~B{&e8{q_xYZ6Hta|A30lpD7Nq3 z*?ZB3H^68WrA(9H!e}<=0vPoffzlf^g|Db>-a;Kk>{cDDO4YUBfs;B&f9hm5-t#_n z$Wb6ZlH-%q5YugyA?H$h+&E5@WX9ePpA$&8Hhn-z+5-i=>eUEPJKpDznit@p-$Zq< zort|^V!FJ@nh%0=Q=JbCEYIL0%;@;}nL*l}@kT5%%!zJ@b~>#j;~UFTs7loj>wm|OnL z237VZy!h!3M|KN>#FEEE)nxt^G(n7d*5+YfXS-(m^pwoj;eTP0R-BY-eckz{Xu$;p zjG7B$K=X>yk$kzjj%cnqg)kM*_9O;`GXfDI=wJ(>A8M(4#Tg#h&2f`Q>~qej

    k` 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≤5$RU zcrN*d_j=p?naEwJ#>Ohob*ZTnLA~C4W|5q_?Dkh;t038Y^lLJs2S|G-u}nAnh2QST@w2J*rBKkP{g$FqaW83FhG7Dpp*@3yoz#%p%+y z-R%HENq0^ocvZ=p5#Wot#cBHdn*k>VSr#ofH}_tf86Xl|?!YL@p^R`JtK=YE+yHS2 zW;pKQ-Rl5c?X#Cv;0V(Fp3wEG_lGIu!|9%98h9JCnx$MO` zrELRU648SV8&MVz0HdjZx?-0)=n5dj}^zu2MPl z<}g8GQayF31yf1V4!VtuDa0uQByf*CgXP^dYtRvNr?BDn#JR;9}&bfC>+-WgS66=mx+R9#oQ! zf{mFRUSG6s7%7M22$mvm25XLm#s^sOhuf`!EqeuLxWC^l9Uy{pGV6DD*6*ElhYkj* zxU9UZ901Z107Nb<>m1+|sKlT{@t5e;>mF5(<2A_+dAAD4t^NFZXn@kwPRIVv zwSY_c-RD#ph`a4CH|g;e%@>uHHw{>N{|U{U_r)x6oFt#`F0|~9Er|v+wW1wc9=leS zdH4BoqpRPd4)!qU!vN90(wgHd>8;nMueX0b=uZa;!VUROyZ=}EJ&6G#-T~?x)D6c! zXy0t;z0@=q=xNAB&z>W zJO4fUp^|=(TUCe$7n{4uP7BLi!VYI3XUDdcvwZE$^YEg4|1bt8UAmY^DJ==R^!XhgcK@sxe zh@C}Je_u9l30>S4t6tendp)+E6g1ixRT=$=Lz^p&LBzA$26%@!rNo$xplMU3!85|_ zFtxg)?@wen;lANo8qfw9)x945oVn4FU#}?}M}qKo3RO#9E!zM*tm#WJG7a&SBHKmM ztZrISG(&?ahqAz3u4Ecq=FEIcek}zxn)_Bz Sbtfn%mHi8pES_@?dFou1+kavU^ z_Q7;swTU9uC|zD}1knvUvjI2)SSD7d{+7VndLP`skRXCzV}B_Nluix`p^Hd}-O|%W zOd|kUM?H40<~iqqSWR-j=BKak=6qF%9&}kSkbr;gw2UavQOf`~H`2Tm?HFFa!O8+u z0&uNrI`JL^UY9V^)%(+wM!m19Q~o{{Fq>=(%R~c<2M(2|qa?b^4fl{66vj-FF6Cf> zaq7s^%0a7eo0?{_=8QB)j1Fw^F88g@x`|~l5U2Pg1Besn&|~n2l(}e2=rn@pX#iAm z7?sje*;`GD6QItd^#>RdnPAhTz({dW3X@~x?$zRxDd6K0+DOu@I%8-VChd8ce4#gp z?Tl9($dc<9LK{I%&Rk(|AGF^&C-A(`pOTuzNwGC-ah{Yc|4~W*NJhs_sQK;*nnh!>0W<- zmHH9}Gl;lOVe|KD-{Fq>{StGD-SIdt?j!7u!)5vrz$SLx`&7g7 z)yeORM4N1~$nNB|adUH+S5i%wi`lP@fCfzC z=<;TuP8!t?iLP{~=TR@WgIIKn42zkp68-K5w9#UQz-7@@m^bEe)6cthvER^J7wZw z#|}QnAkt;~oS%!kTXzBE_)qQc`J&hH=0;)niw^isT&H$#KI_KAdHehPq6dDx;X1Ty zuG@*{4J>M4XZ`Tl8HciK+AQbOm!Gs^PE=mERke9X1>&dlKec?nb{7ezRfskNo=d>w1pa48N|| zw05ZfUot>SVkA8Y>EEegi1dH6eg5^-UHx(>*ERiU^858+;ByxC9R1z!o4fS~w8T4 ziT>a&w$l&X^x!dR&N$=%30(6K>|G)NrvT9CF~AaF1;z66hw~G5PZ}9kFb(ZEjYK}~ zGGIA@CAFfidE9NTh7WXXU|u?@J2{9}s^_zW{Nu1j`N2Ai5YJxErP$t~-4xMKf?s6o zNNLO1gROT)JPY5+d${#f3ZLY?$v_X3AO+N$98yi8_m_3dGsoVSLpnOKL`~VGs#-BO zzCr{452FARqH!0)$N&Hp>k9WL8F~&?GFs<&7ULFRQ*tJfA zil22?HwJ?kD1sXzGoBb2;`|vTV)waa_4rS$6TSzaqkv4RH;EyoLCU#KkIp;bB-_uH z&T!&mTz&vi0^F4z2a5CF^LeJ#v@|%NH~(gLw#&!u8V0UfeoxxpPp&%SitG5SUB~g5 znEk}y)jjKcqWsn8l)mHWw3*l})E$#neqK*!p*zGnJ_r??^mjSQXjAd>U{@eDsLZ-Ck3;RzJ8J@8+M4d#MgaC|K?rmB6#yv z(s54R@I#uvlsbcoLK+bq;&bzOUflKGW{AE;IDS<9$CZY~3m!J#A`cGEMPt2u4mTd$ zWRst}@Nm0(%ZJ@#|7klJ=9I*4Z+G}S*s>}{Dy!@h1gZX9I6Vcq_J6r zsibM3tk5;T^2R;VaNOX4P-b*3=uzSpB`*hnF3YEQ{!6GsggvOzz>c!Dq2y*X;!Vxk zR-6xiVu0?6yzPPI!VHdJAwV_wk4>Tdk)E{&x4)*1?wiR$KyZx!%0`RczhLQq@;c8)z2J(Q0lw$WO~9-6`= zgPp``4kM$`5L3qhFP-Fs)k0xZRX-PlV%zR@L0`~QR`ADM`6NLyXvDMmVJ1R`I`>1UjtnONaHUCG!PlgRuQzvU}b>YMs~{!=_A$?7Xf=DVwGWqPPBcn zTCtJ?Ht@xcGYq25@wE)TaQgenA>;)8Y5R*yns$F~E6xYOvsnaCnLWPPR9&yL=0VAU z8U}6wRU?UxMP_^-Z4sg-yA@Q&*9bDCQH(zAB&8xA=BIRc!Tn$zD- zeG72iZBAmRaD0>BtQ|OFuR%8IzR|vapM2LhHsJnNdgy*~*?}61>x8lMi!@9D0z7du z-qA2`raym@9_Rm@I+(=6mjem^Dw(TY{YHC9{X0Z&b+LJyu#x+J$|Hf*e-_MOB-4Xa>4VAiab|JuJJ zgDTeF?2JmbU6@(riOh8_g>zCa3g%kB#v;k!Zmk+bs>v!YLzKH1al~u z9y%Ic+q`HS)ZpMz;=oQCA4ILvN@W`;wDg~-bE))-Tg2l5M0LXNsZ{>9+{ z)ECZ5z(Qf_BjymA!2lC-;+YWlC;%G)RAD!?T>$imTn_`crFO80^Ok_%L>9VKo5rwC zeAbXo4fTf$9N&k)=CWrNIXr?70;V&Fbf&|Kk0ChsKKY}&Xnp6<58`{hnDXdD)%#Bk z_g8WNuA**fWKkt@zzI4RCRZj4++I|#Me{%!#snk1NAF((95N5^pMgy1(ON6GC!uBr z3u-PU2k@qj;8d++YaEXy5aLHsNRXY4`U?wt53J6B7rWXWlwQI!)p2%Uc2-^SbvO4t@8jbAkO8ep z%Z^Fd-Q^ery6H11?>fh2;QDiUIF1toWWvsBXM*Y-a&gyF02N%`uRGo{PRwt60DD-Fb}H|?dR={YyRu6)EG(;4M2wX{Fx z#2X;RZh9Pm{U*#6p9lG3Q_;WO46*-b_0@jH@cte-g9zTh5RU8q-);K(D>MQ~EDR3) zGWA$Y0H(y)8!t=mv^*bG1G!KZ0hXe0K=HrWuKiZ+wUIV)6)Ix$=&=m*<_QSOn3_Y<`21s=ZN2E}jrvyFXrdqdne zW+wg~`CB!!D0%$<5LY_6?yK$d2jLjcRll0tZEurJHu#{e|3U}tN9R zKY8=>O-rad{}an$(N9PjMXFykA(%w`JMNq&d8tWRv@lD61J(f87}i*64Be&If&_;= zEY?%%{b3>pi=gA;w>xWz$Xy>b9UuwdCjiXJA%D{)xhbJ6A_MJWj3__@&>cUfkK`a7 zAcoNWdSG*+0nChGRt{;y6;RrWT%RnnbLYqbo!ArOgk*!Vx>3KDlb)^ycTw$ufdkM7 zy%n{sbsRl_Rb^BJYb(L12Y7+&Qy5+3y&4q&=glx3ZFWqW=F}KQfyHVHqeZSwQO`a<}kArEst?D$pI(nO>qWP zHk3^CP_BYeNEr2QfRsx?D?S7^4rZ|_fMWnxpfdo5i(;?> z4^JI1fo|~Nn@+6wMD9+F~&)hEHMt z%SH7d!NjMqv!KqdtPci_FbguP=?Hz#9Xw(eIk2|=jsN-@}keFW{_Wi^1=BB&e2|FCz7vn`>21UsZ#U zL7V(3Nq(iJ^{g6$fOn1K^atVY_~)tTiE{jfx6EDC%g11cZoi=x(txtB$&=yBcHMeny2Xxa?WO*Z-La!I2*?57Ba)T1x9@%R_p z-@im2Lk{xQcKqLJ=l^aSuN+SfZ-+d8e7TL)f3?j6|F7z=hx<<%`#iP$7WFv-Z`EID z$G%RRY_iFpnk2W{#Tf4|6$~b&_8yq9@evknz zu5lywsM;FTV`>YgdL)P;4+{6ySkI7UbOVeIJk^tM>@RtJ&=sKTn9B!XfsOmAPO1h37=}pUe_t`J&&tHqn0-w5N|6FjWdqqpT~9pcxDc;Nk{GK z5OWIpj_!~t&PqvHN})d12ep0C-z;y{XVq&F3|;t+JMR}uaR!v=F|hAebwgVeWI~j= zsxx#=4&DhDRP&i8-)XLBT^SHMC=jH97b9)CyC}AEuWwz)Tu!bZ14#%n&Wt8z5AiVp ziL{?`)tSx2KoJ8!>@EkU76VJXYaQ$7@goJlTG}t)blq?aCI*zAJ@Jv$)XJmR>rt$Q>KbN}O&DTSIKgca+?nWnZAoFil zVR1=X9J}Rf=|?hb3te(H0ndlTH5ggm;fiAy9W!1S9Rj&e90a!J_Z6X@PGO z>7pqX_tlnx1zFRs_WXEnRlkQS_Vv=M(X&w*J&06&arqxXrU2q`dA$=PRG7fur(%p5>6bK47f zCv78P4yAtI+pHHvsfqQGjm`CM24eP__Y!opYPwwBo$3g?IkXFuj2VV*uYk zAiua;oGH{nhd1&>U1Oii~L-#YaSgf0c^z4WFS!S7LL$qJ<(<&zuv;(M{N@ge6pyA zq~tVAcs2^Pe36L+YAF~)a(;5qB7$}(yG??n8|8qjDa;}%=-YOBL+=Rsr4u~?3h%Rb zsB>%wa{garPT39=Y3!}gR|x=7d~l#n*!d51*yt_@=2O%^-1;o9>^g?5OJH4PeHiV0 zMg^2ZKaEQnGk*Xb0)KojRB z0lpPz!me=)x^N!+;N9z9)eIzY{#;gegEQ)J`{&senQNoOpi-AU2A9qb3f#@!PcBq1 zTt2)gu<(87$|kzK?{TSK-}H1C(2}ihYu67SXC0g|h{kSjcRl^*Pc+6B>uc@?$?cJ9 zdl5-3-}>F_2Bt7esvY}?LtI`3WTbC|ikh zfbkzA1eSnE|5AGU$#>{e?bWvmyW#!%k9Ef;n{4u<2tR(_rJt@au>2dJGG5>P5yn0n z75_1LF!9F|%Bma+zl zgY6`t)f|qK2h^|*SZZ2Qs&6PvlqC4HGMDiIDgh_&?sNt9~d59iF`~g>i z|2d-h$pM(7b}IlUIplw??PCln?F*5svQ)<(c`baeCl;7rp?uL4FGPd06ovG_R08;f zVPU+G8r=Rg&7=N}-SbdSxZA}Kc8=~ZCPK>Lz&@oQkrOV3SSLeq@ioHQ+TUBpiUl57F4!z2RL zBOlS6GK2^?1{j^$`tJTd;rNn~fPSX}bxlYN5e}aE@`S>IjP;mk^i`Gu_m$6-Lk8^I`x7b(1`c z$2}s|6ZEmFH!Qb<09gU_bG=zgvHfnn>O&S{%m{32_ElT;_-YIQO9q-`SB^u!!O0bbwa&{2P+}vOr z>0&eXVP`Q{yp;xE3h9ZJI=STvqCnP@i5yL!Ohb%a1F;efzEnAs7pRV^$OHROFZZhX zSpD?8S?WZQ9fK;=U}z&LF_DS`ND*wQ6ihIfQ4c~Aoo`}VS=0RD(iYl5e_FRI=3IGPC zeAfB_7~cNAR`FD+$fB$RSka99uJ^}yjM+OGAJk688Ag-N00nHI;k0cM=i!O zXDhtBkPXqu!blw$Lp6X6T0pB??V~Efi${&_e^yUzqf;n2>J>~K$N;m4vcOpt;8zMW5*S>7HxUibe5$91u7(uqYrzOr zzo~>eCkIhvWGYw~g8Hr#?MiDEFKf+I(94N$n2>gyZ3;Rd`J8}7o03i9p zGJLqA@5`&=$$#KTX*c4mJ_e793~lFg(Xo!u1^ED7u{{8i6y8@x-y3 z7RF-Otqr>ux39<)pTJ>V02TlU?y1pVKCrY_iEF|C|f&RQd0{<@mGq zDEh0^Hx<}qlTH5gBv|{hv)oS6i}p^J)D{s|lUm_beV>xrZAo966DSgU-AqPog2~@s zS;Q+~_IGf^ZQ^&rSsDL1`GRrumVsrTV6{`QO{&{J=C@T16zlb@0Z9qQQS^ua;xYw$ z#}=WGG=z0s<8Yv0LMF#V*E*IJ_b)Z*5E`!J#abmUMcT!)^52`0MD8Bm~U<&z#`P+#(TO?e!VYXH!o8{_DO@?*=- z6m)E(pn7_6?TRx+6ys3FNmvtt8(idPM!3=dxDwha`!{9}b0wp|L_%FdS!-+$Eyt=X zATY*6m)Z3nnN337ogelW^mH6sb+!VS5(k6oV|+Kg1+wDdnYv>IgrotW(;45v*t*<_PV{%i?9gT73%`=;!CxydG* z{OL-2XROM+Px?+@nYG^lr4A7A@!J1xAW>2qrZO0X#belDlP=1ZU{B0bu#>V|@6pba zFb|oAxKKa~R<)VzdyGc4PJk2gvaKadbcu#t)uL_^dAi0D%C2XuFq}f< zV131HN(cA3>Hr=nshE8%=2BdJm|TbQaLME39=VUPZuW309#aUjBJ#7gC1n?7XEI3{ zieEQ}ne?y(I&F5{0^Y_hh?zXC6#4;a`HoA7ZN%-RyB%I=Aa53io*5+Jm_}LCW>+$> zra(GmIQyV%sJS}yk1{i>NkAxdnG=>%@gR#iw9BOi|K6)R;O!e9Q(Ol=51nw?IpFuA z0MtQuq&wVq;0M6p7oHPc-*ylQ#~57VwtM}?SFYd^DLPVC+T#m%U1Ug|b$fL#=NKT8 zt#rSCP4(${XGHD#v?9CVX#n`4|sj#7Zatgw(s|7lT9}H=Rm5)AgaooPmw~R z;W&DDR^~8un}}WODi}z7JhLlZu)--=_$7qbkkh%d| zZ}Yyudts$vpCu3*O-}H_@tm-<9AfQ!SdHb*b|A! z&Am0G1C>Q=FPh|5hBAT{QU~(;ZI7tx-j87HfYoI8g{F%6a50z2Wzc&8q_9=TCLNeg zh!0l(l;EpgkCYTh! zjmQb)(QZNMbaKm~w^c1ztqxE~3oEc9Ju-Qn<~%Zdq{WDgs#(KKKy@rjqQIOf!102{=Qcj5WFbz*jS1tN83v5)FWtaU03?92r;38UV9I15Xh>ZVsD4@^y>4MUEy=Hv>0o@uv{~w&44S@86%{_A)!b71Z#$~5BZYUrP{#(CoMa!jUGK_Jd!r@+(YMUUSE_qfa4sm&K! z7QW|j=3`KTgWvb-84#P*+j!QU?Ven8{c*V%RATo!c9;9o)4t2e&20zOa^KaYzC85Z z>MqYHwVAE9*(mXEjtlS($8PF0ZC*JF&!A2IEXWmgmzhmA*~H~vgo*4ct(sriX6#Ki z`R7&m{fo`BG$nI+lM%b?p$%&4F`4+iju#9CbOMP$7lf1TCQW3na*+TK7mx3C)d`FE z@PLs#CmOsm6wKi$3wiWIdQy^IC zKqWA(8TI2+<~dEyur`>mle1qW%dmzjDNSxx4!hDNFZQyIrZ~WuA@YfupHl=(CI_R$ z{0aAq*-bR!bwnc}Ei@YM zbztjkau5j49$+=W{mVEl)pxi%#V^qX1k54b;84p6h7k0zXqs?oLp!4FVdgU!gKjwm zaHx<2UeR!=OeENdX1}jWms#XxR5Ky6i!K zl7p?X2G|%NP7OUmfC7{~v>;e9$S3ACQ2?BDVi{rzg;8jbaPi~Og{x}$z%W`B2DAw% z%rtPFsyZ7a0Ac{Xw6$#%rx8ej?f_E3WeU9yWik*ekph=B$7KFl`!cVtl6eF1ce zTQgwx2AisCpBGm@9hr0J6Oy*N0orbF#hFUnbgn2p7GG6PEhvRwy#IDQi9Ql<_}@!y>8YYp)I=*-z- zqAPa0)ABIr^+tD|BCpPo5>kmYr z9fNtV?|ONE-eO~QlT9|+LVV^U=)ZXWlZbFl=$gGRv zb=CMyX60jHn*3c4VIkmYfi1|Q=SK>a<#)#2>}U!iQ{&y4-wW?ow*yGefo_vE9TxNO zNDtrtVb4XW!NSJFzz|F=9vSFn`|P@ff&sdq zUb5#?0ArBmPP-$YlNe5_KVfT;o8X8YC9s8lAIQsO5qrpY%`1rp#~IWaQG2Wnst>z~ zoK+?DtgZ{`>iRca=Ad4=FFRO;{%H8K-V25l_&{hEFsj4^H4wyTjYD@JdYdJdPHKbw zAgw!uUJyqlm`gt>@|bCjXmBo+iKUGBN5Ke;Bimf%s>6(OEzw(9BjZu5B3ZgOK^t+P>0zJyh|Jd zo948g4Rq4uVDH44=5%JXhY9)hllpWc+8Q2Yc3b*y*!!1WOR{8L6fzq==Zeg#I-4V| zfGZ(E0}x1b_z`qy(&vZr3y^3bBqSaU?rk8B1iFuN&fb+7Yfc|_rkB3nPH>aaV3h+z_j{Kqg0kz{Col{0 ziwHpoADPa!^@|g%xCnrlFV^RzotT;KaN~GMKA%17;Ft%rU%1t!{sQ=UZ{Q5w;K#QE zi1KIdxqA~ap|8@aqU$2!Dm|N`jCfPGg!mDY3DADIVP?2E^wDRYR_As7V+IM| z65v9b5r--J^LtK(0F!>W6JTZmeg!rA_1Ru!j#KIWhyQT6&$-SV{QBeD5li6xA>;M^ z^x)use#+Nhh{@N_^O8$0`Tpf^nHPp!a>*r^d{eQCGpe#2(1VTAaGyKY?!)GaCdB$( z+WR7&+j_nH%)@$ywasK35d|!LYx3b(gVI|3o@HR0iY1o%w5c7*z^cYUu>RC zN2wVt>oBDOALH@}iLO-?Do45Eah<6GWhRT1t6kLrs?2AN)D9C3CSOHgK|a=i1$R5- zTcM4mL9vl_geiiq1<)kSw6U7>0pO%L8KyS61j3~$J)eKy+TqW7(4#dBy_s63dIT|gP)pD&qZQ@~ZWK6S>nJMM zIg!P+3EJN2lsmKJ(gV8%lP%)%V06`Y)N_zFpC*U< z{Rfj`%?6oganEM!ihB$T^d8KSvBs&2Sqp&2Mm;YXN!6x97ULSnp`v{3+Q)b_?R>Oj zvh@rC{c;d)cCy7Nj5?rAX1=UukIdR-(6ukJ`xNmh{0v{OD3-Nd7HOv4S22~+HY^Jtb{-J`ipByZ zs+Pw}3{cbOIQl zP_#o&=}R{a_P=Y79#tBxZaA>)R2Bw~V45-~B5=q_>LUG&1bEPVAgeJW2BM@Zgct?@ zQ_*mw(?WBANrWzOXMRXFGk_Dgvw;*BQBS11%7abEOb^`?<#Rv^u4Hg+dk}a7Gg_si z?w}iTcw@ijOrf`|102EB$mF#K95u^}<_!2i`h+^P$vycG)xOefDUlW0xoJCMK_^0B z8SZEJW1~KS=fqmSfJVS?D0jSh0 z5x!G;d;%ZZZ{NFpEk_6X3r3M?>+!L7TgLt9D|pe&K51^Jz7=K7yq@U}r`&e*!iw8z zbZF`pFS~Ll%>vkh;=6G??|{#H5+*$jFTda8ZgS2#^7P*FIj@5=oE&)a;NR}D_j3jo_cRdL z8$i~demw3&U~ms)?yRPN|4&C=UZMZ&kL?!dnf>GEWOZdd9kBA*oqu*lQ@ z5C2p!y&<5yyXnpS*lGK>KZ^eQKL^P4x*>F64t~ALG~T|!X>?z4@ps83m;6d3Usr#X zU|nEx$t9OuQlfwDobp$Q731AcojaIBh5~pzr`usYYcldJ*SiYl637D)+F=}*A3@-5)O#n$!*Bkihszsc zLLrDZ@_**NFUzU|g`@jU>9QaQIZW+h3`s)?lKDV44v1mt0{68~neI&eK66NI)Xg_C z|Irp`Tdv)M%$>67qRBb|U* z^9I&Zmh)lVyGwV}a&WrUY<1o>Kr5EJ+UVYn*`WZONx+$Ot4iJ@G}A4S=d6Lnjb@G8 z)-jWq(F8q38MJmhDl=vi)!(SBcH3j70FX5jT-%huL!N;o-PoRejU9WTCvX{eg3Y0i za$B_451350x9;FV2e#1zK+o;ycX;k|#wx;nh#du1ROvYLzl{cJ!4AS0;kLi!>2&@) z;!XK!a|ovHpp4@>{?f)Nc@At94S7GV1*2&k_xtq#g|dteDB)zed!D`*Og=k+D4z`- zsPZ~G81=&^DbA}0CkJaQ7*N?BKmO^+=RZots?zP1_P_m8Z)JF$|HDslzd5_gXLtu+ z9U%J;8A$x!{P4;~BaXl~2@!sQO_yA9$yxq}d0~kvaLFZ?T;hT?{?`(mP44lSd<}a$ zSm)}onZh>k&A;0S2~A2U5wmrT`;q1(loaHCV&!?#vBBBm4wrEPwSw7JLTXNAd?R&#kr zs;s~k$}vy-`LltFZ0Rscqk zy?#E8K7lUGFOyLrbtoqyn)6O1YCxCh6uWE2xUDUHv-i za&$4{?(omdhA`9%QkAZDfhv?Ef^-{=7yu?Bloo*utl0uV5=a>Z-sO6U z<1aiNFT;=bBc2po{QSq$jR576E_bgFfBHieH)hSpLjX{!m+c=8c2&@~9h8dl=)rbM z_eOoy$NudfMSuMI0m5~YOD?(Ol121y0ik0amrE|WtTbZ!)q;QWXot#DUMJG?ZvQ>EtTtVoQz$8S!mq}Yy6f>e_Vqtfta(g=2!A@f-RA2 zS%tkJt-nf3KI)VS5q7%Hwy4p=BkKYL$F!F46&M`W{gfr&F99n?#SrQD$zn359mj4+ zm8{8v(!f08@VVza$G>Ki!(;)Ms+>luSNXvAKve*MG4Fzs7Cuo<>3)$m=}`YcHmjXX z;q#BnUE4$|K*JiCn|bfky6d5dAE>vS%C2jZ=A{FWJJq{f@}!+shV+m?*^)XC^0qbj zrgJ1*bMo&7#X?pC;v}e8Mx#Q^C_9hKi z=t*~Q9s-~yN^CFbGTo}Rc4}4xnMO}k>s|nGNvH%hI)nzS^#ml;GiIhaqzC^JwTq;B z<&cFQDIy=i4C|Vm0T$)oUuXrf9(~LsERZ7sgeL(B>~`nEu66lHxd5W&Ij}d-bUq1h z;p35h*ohKMrAcMN?t$>AmJSw;xI#|`s&eF4FT+kjvUNBw_PK`C%-}j04Y#A7)h;4S zsD9+RkuDIci)X5MZX*CE`((M-pL?RO|p=|D2 z=fKf^BVh3L{6GHr0IrIOln~JPjgKT>mEUOphK7?Y{_R)P^3B> zPa1fs@=;dE4(S!cM^D^~(T|6^^WKd>rNxXhGOhAKt|H8DfH2snhq;d8F(0JD9TUh^ z&PAh}3#K8TD90cf>`vr~*ihgO6h}mwScawoNK$!cIi9H#n4RDV7}XIvF-p_#3;z|9 zF@in2W6$tvCLnOgyU6LBt{d(5soo6v0HmvopfS_jXl9su&sq_-e{?LU zw~Vdja^HHa+@PDi-zo@XAMu{{cKpSQ`-gk?F)!O&#!^BaiWxJ@v$O~8Mty!JgU}9E zlQWn6QUCn&=i|D^V!|^x0^!HEa;@{&O!|0%MP8Rb{1_+DMc{Ds884FqRq_0O#J;lk zS26dam;k@Nm0X75C6`Da)ZW#XXs*1pw`Bs=)*-Q_xm$D9k5Y_wJF`!ca0Y|FW!`w3)_XcIz>e zi_$?HpMn#l%_I&k&CIDV&tO_w0~KZJr#=cKN|P0sr(cK5+fF*)$b{tbp=&+KLaUG4?DTb?b8Ni4_z;7&3aFSf5 z0Ue|NS?4>2fG=s#jd56n$3{AEWyC0Y#|LZv1H4=3?C{hKra=$425Pc*8fk>VAqeyUr-_e8h~Ya77#9bmKT!| z;KC(%_FTac&ZBzFkP4>FMFjf>e{-O9jPU2A*??vlZh4n49UF>g*qsgfF1AdLY7W@$ zKu-BvOOKwK9jWK#j!DmE2zah5&lCDH9@%^A5^iJ&bUqm%^ZV}v9!Za!N2UQ_XiAS< zOV9-ly=JpP@b}aOPPM!B z$#$O-P#VdQ^Jm_Y{p7*QKOIC*_3MB5BW z=mZ$Lngs+|KFp2`X<>Qa5()%%l#u;Xo(c%JVGR`Rw0?$tg9DRxvby1}$MIQ>YzhPx z2Z_N2mOJbNpAPC^}-m5avW*0nY>7v-DSU{s8beJ z$=O6L@W|S1Sqyxm^o2}$=`wJXnX~!G^$2rH+k8ZH4ulPsOq)3=DyO$I zOm&eDm~0W_2ARCtW^%BeW}ie?Cbl)mF@?-h`rak>@bP_iA2^7>oW#&YFmHfl1;m&v zv8(uC`bjzf?`7ai>GZ?ABAx%72hSs>(;L;r(|~IpmKl#M$1dHTk*+qdguFHD|IA^u zQ)4}Cg>fQ^3z@W_%?0hL}mQYXk6VtL}M3d9&kX9IkTcdny$REMhQ`i{fKnb%Fkf_(*;a zz#60e@rSp3Ho?SOy1pSO_so9bj3>#qjaWaORTsdiiA^SjL>y zL=*rrRqQ6j(e<@jvFNehrhBF}s~$|EvWLia0_z{71b{(8pj8xiw1r<0tFv+(!h|=u zMb>rrnbyLz86Xm_D{@>2vnI{a&QlhgtAxyes1L7RP<^l;FIVMsNxwi00;v;z>M|8 z?u1b?5qV03Av@};YaI$U$hHJ%0=;98NVS2jcM*>1e#BSObT&cW*1Uszmf5{aqfFX7 zhbQG(CRnEF{p<$a%VCxTXCckin-Wlz{o#>1!Edx<4FOpJF`Oz_k!)n`TcHDOP;1gP z5iNzi!Jy}0%HHZ}qhy#xQ`AKCO6!)0@6-5vlKT)!A(Wdx2lmG}O$af<^r#vp z6$Y&~1~T%wWJnw}n%(08^D+Y~T8l?Kc5{}c2 zC}o-(rM*m?D)OZmHsu0F^(e%D5|E^z{RTL)5(zv88dzC71jz%4hZ|3Jd#l$ciX@u-Wo6PMbsaWpVdQ0Oq0P zWA%3?#iz(&{mA>~bz&XTqbl(pJWMl~s`dpFdQx+gIhoj7nQR0=mDw^$EmS=?RDiLg zRDHO_!Td$@kQH9$5%Q?=5~gVN-^zthsvDb_#oW>rj6(eRCz)e6=`tgva)lS8wC1S{ zgUnKN4QB25&wJ`|370D;IDpJ)v;j9)`BI{av;l3sr%vHf!F7u zCqp2}fA~R`ZH=!jcoSUgrb{mQeaa@ktWkK)DVJPwiCdQ6VrlbQ^QnVPu+7ZU1nTv< zZd-X|XvHYaHFwceU`xmwsS;3Pm^{Ate9p(S}xwxdVpd)YuVuu6V43$BBMiEBjRMc{4Ni(8M zQRy@(=_Ri+rKQ!tjHb#bZ-5CjP8;wrN0%RIRU0)K%$+jR=10}XbE8e?GNY$_aAW}= z0`6o;JNC-0y8`s*9|8WH>nWs*3Ot^sFq}T(yELW4w37y+U}7VY0DZK166{h3llCdy zMZ?sZN8Rsv4}y)~wHZk+d+Ofvde5@s1iXS+?0Amb7>|KD<>GE`4vf8183A@ZVLD6K zIT%xVfJnC?i>n{1FVgjXBR&UU6kx^$dze&+;bMBpQGQ9i-aY_DN>E#I?`AVTkTPlw zU;wRwuNgpLCJ*zu%FRvM0OuG;JW-gej1a;ssu%AG6d))!CQM9hxDT#i{wz)=10x14 zTCuFO0i)YI3^PGhF*WmIw3eS15-8fsmwg`IC5O{&Rh2Y zo-vA9rL8-Y?p0`~0^MfERzkRYz{7aY(15L>>HUhqvH(W*PCL{9Sf9$V)GKvk`BJml z2?&f^zUF4UUSMNm;8=p$X7pwZV_K| zW&F&;#yi%K3g$jo^-2H`wnaK?tAwq$cGd-+jgQVGmUn2koC(WGGcY|`IX z`O`+L*h{&@=TqiTF6?o`92(YTDZvA>f+{Yz(R>6n&y!3} zu!mqusx*jFahC`rK{wc`{9f(?gyhh}Tbjghq6%Ot%ZV~@%%hp+XACXC2hxBMVm{@_ z>Nc4T>OmLIT0)?6%l7`^)`84}t1yoH;v6cK#n}s{HuMo}Fan-9pgQ_4PQ}A@9OziV z(MLV#ALZAdEp1=-GYZWKeSAo|($G%6?Z10aAp zVJ4Z@!Kt~OyK&bAceMxll4d-ktTP@f{c^t2?TZ;J`oyFy0&9j5Mj-bmyx?#T`jZhb z#{E2E9L??@bu@!DbLf4i>$@y)uz$qM`O~eLQgFw6ONLUp7D~S?0Se+oLokn=DFhHS zSV!8xl0OpwjvVYHubanX@&y0Q566876b_K-4?i4#gKZTvC1O}5X!Q?&5cyL+@9Fw8 zh%5E^0J-$$iF1h6QCO+=}9aafmG{Jh0 zunf6jig>-grhOZY$iUr>={jKO!!FR1oe*UiotLGlyVEn@TLe>&pqMsXV*FOFcUO|!cTfcpDCu5(PCh9fOG%e8A))-3{dXT zpyQVJ0?@kW=eJvraR-vn1{Ey)ez@KJ;Zy2h9M8V)jRlN9mdFC4Vm<>yk?@`F`b>cFX-sB{n<{5JvGgY!(NU zoPn-KOsCik#AdC+Uenb+10ql!7P+>)-`W~xzZwy;f5^6&`F+`Z(zXE($^l&kKSn4^ ziFNv0dIuz=o4%Mvd|jnY#M4iCK*GEmt?Du*&nBNpou37> zC{(O#b<5n(=Yqpx-9_A#aj>J}1xbTi09nfHly2^)bvYOu(tIA_8^gq-Hb!{jTNeCH z8#FS47=Xz>X5Wwa_NWioPjEde=q2mT@uJXOYq-b(NV(iompk19Q?*?E5(InF&>PxQ z6a$mYyaPKd^4EiqI4B>a2OS9IkG6KXRV#3>PIS9a;`0P@POV(ytZNY0bK-)#lVv_S zWQn;{BQ-U~j+Pt~3B6ck)k^(M01fxpV%tdf0`}d-=!Gtz1C0K7E(^>j(&1(p(3)ua zFj>%e9`y@fa5fk6?327q&iNhGgtFK9jr=f^W2{VkS4gHnP<7<(dfp^at7>lwsUKY zn(golY3Nm{qzkoB{JEyXz%g4xk`viv`%s~RBsU?q)i zLobTQUILiK{U2bPv#Hz#4s5nR{XlQ3?BhFu%h;9j+0tV!`RAWLA7yrcES|Rm=<>YX z=l_qNQdb!V0O0K(4i~!$RPM0^JRqe9u1pZi0vMBTFRu1iQS4RKc4ofhl1qM*;^Q4h zzWhD2@j2x+{eCo2y{uZ-)sn`ow2)FI)&}(T1n0;iX zjZ-sCTg}pf9%h3mRex>MC>&Jgac$`?dVoOTFhV9V$2|i713(i_fUCTPT=??ZlllOH zjC({B&kJcgScBM2IMt5Z3;@ts4&0G)Er zBU%Q0M+OME*P)lO>)%E?rwOK>MX)~yLlCov!8xP46bF#>hk9dX8Z>j_LyW$BVSu>? zJr|HC&0#c}CGve!aj2H^OS_(p1au~~N9b0FC#nY!0w-CVaE7$DWIch61lCu;&7Bnw&W;G!en!C_F zA4eS?`JCtXDeJVQo1DAx{lkxuuO@J8Uf&3ae$*$rpD%zM)Vt>*olN|!Tz&ZgE7 zQliU-ut<9XRD&2zFdzN<0DflSt83I5IRGU#zn_^lLX3gggeW@Yx~E-Mw`iV}C+>6c z^0=qwPW5Csq8MA+agRKmhjiUh-wo}W!}v-10;shq;klV^IHY9$@4#_0UF^MKC<4ww zj54HAtg*}x!wU>2)Gy9Ri8?6VSuRJ^Q$^MuxG*&aO&rpru9`YO5B^U3d0DgRGR)J2 zF&feh4a}|~dhi+as=&^pM|8Dpeg?pRk;WqyUg(k3r`oZTLeHC$tDh+KQtXM*{ zGT^vWnE4Pu+tauMK=Nl{sbv4gbpUa2{`GiC4q74THh@Nep||Y!5vlHWfJZ-M#9G|* z>DKLwyqFXD3w*C;*by0Hy~VN1MFex{W_4807kUB%tTB$ES1y z^jWU*-wvkI=cgbTdicA)?o|&Uj??g5Ugh!cs=lwqiS6{Z{noNw#^EKG{ANXSe1FY& zN?!}uy5y2ezL~_l^1Z}<%$P{Ag!b?Tcf@fz*-(j2Xd(!j9vtTJov==RBuxku8D7ZW z2W?Cu^Gui5Q0Kp0a2hu3kU|`kY?MLYZyjc%&=6SJ+V{Z>4KRuAL;el>Lgfq~h3s^% zk$G&V`+#QX0UFIy=-Tt1KC)#;5(XV$UKY5che`1P#8Faf8vmRJHxXHN^?OotD5)E1 zipd8L#sZJH3rPb|$P0Xpf)7*{UIm-dQ5MPGPr&k>xBb|8d|3oITJ1Zgda&$;gWOR{js!)!*N^?GA+bp!qNpn$x)wTl~Eh98zRQc zt<=YmAiF&2X=VTj!tg00+vjT{o#u02dn8ykVR&noObOUwu2xNo)T3c|11g0ULFeT1 zm$T$(7ESePj_OAPrPGMsE zs8MU@*-fco$^2Cx*Yy}*(g*s>Vzt*5m19nTpw)Ot0OfUpT88Zq6=Xdk(O9?b`o0Bb zat1_xX27ZOI;1eZKs|jQ=B>gZESr$O>=c}jGF_dZZuJHdJP<-t49CB^jen2VLiZ!m zglqN8t`r4Bv!X6^Yu#dMl{7Wogutpt1~+-m2C;K%rl&XhSMxFo^Qe8Rn(*c7;k6eJ z+Rafe5|4gz>xs@h4-U=ChR^L8x5VWAfpvguR6VrMeO#ViREj{>7z)JvicTh;PzuUXj84<z5-$zF71j{L8O{u^1daA7-cCbP>sf5;F#_{3`7bSClkFNCmv-cxP;Koc4 z5QIey><+^lK%wkx>1t>Ty|uMnfhL^YTTBqhNHu43+m_0num-V+m1pXnr@!>+M2PJ9 z)4S!v*nt5x9I{7=Oz3#P`szO?N8qM8uNpsI;$GR3NwyFA3o!4bhY> zVB=hG>Nrpon=79C#jQl51e#a$HEvMzreUnd&OQ&GEaE4luG>Wa!??FXzpDPJPvB+Az#MWzssvYtuIV?2vu z-wSXR=g$LqWu{Usl6)=t2wXf1X_c;-&4Ff1p~BNnX$ZKCL_&xV6V~*m1TX_W zQ_I^69pYT=n-DoIUO|Qy_KmJh6cbj^@kf2t?dT_`4D0XNB6ml={0EEcpoUaIqSFcu{QF-GlK$67gipJuupz5zadRhv5^b-7UIIlBk=??-lv;Ad zx~ZyYn?bYW338~&pFIpCd_UfnQaLYGV&;~Z(?m68|i=JpN9 z;J^uiN9?E`+#ksQQqDbs-B;aT0dY0<9lH$Q4IjN)c0*Yo8Kfz7z^%mmu*5G`x5Q6V z8+5;TP{AL)+_z?`C>_%Ow+S8x125}#VcRw4*ul)hQ5R{y9o;4076up$;SU5L1@nI6 ziUDd;Q9CJ-VAJ z@H$V*!#S}b3=UMRQ}0_IQ`zt?IY&gV5Cm3K0#K&Qp5L_h)r511@?+AF59)V#P>0^s zO5vxEZ@iiVn)nKo>bT*VcH;NgPzawc{9bt=e(4B6=eN3&I654anaJ^|jhi>&kYa7k zxVsQQo%#&8IB}}!bdz`-)A`-p`e9UryKfd1r{#ON{?|HE^hjRuz=0uC>}dhDObh=$ z@q28KFH=L4&|)^9TpGwkeEO0rAuiIi5I3ACfa9*IB>1qA9a@5)mEV>-1oKL-QQ*=Vc_jH_ID&2>g0zvKwhovP=Vl~2YdJqX&tc^tvi67f|80suzY8(eQTZbzm zGkP_!f~1t7WBjYF-57AeWiOi<68U?Qy|hj2{?GZ^v=yl^&!3}Kw>^dZQhDt^R#0+*d01MHf?yN0&dp)_<= zznMK`%%{C-Bc^j_O=0;)JI->()L3eQpSAYm)E*pIiAXc1*ml(-ybbt3sUKh0)6OfT zpxR*{VTH$qf)ZTC^6GOf?Y{Gw8f%ztA3`wWEp$cgu7q5I~B{c_c)Y1Iub%`WY4b_a;AZ79wGjzGSwQF#CH ze@jvsK@q#Y*&{7bU8OVRvjCi9|AYw7FUTAxk)%@=eDnRwd~o!FNz5%Xr9=_o}lCKe2- z7T2~{%ah%NyHIN~u`8CWb>0tw8Ub^*YF}wIDU1E=j32d=BO)&e{>ReTvtBTJl0wg4 z19HuQhTGU_kP;30oqKP&J+O!mC-3qAx1vpHfP;j|_PS?unMDiz&8`4skPvEW(g(`A z0KTwK$%VVRaw8Q>7yFCCrGkAzT*tBmqR0CCE#>{wlUA({*XvE(&R++cOI_^xga2me zZ*r&dFUK}X7gE61tpA82Z z&$a!$IsS`~n7(veXQ8ghIs9!xXJ5D00||@m@XIj|NvPc$L1~359z^FCLjV2oX9%HS z8kBQ5MQ!8#FT+_6<9Tyyp)x-gClTdKGjxZuIOL0Fg#Q}52{=URLh!HjYUE%%Jij$? zyjaSf*i*4pxy18|dEn8-EGewW(=#V-unD|&6$y=uOW8#VN=j_ z8+J&XTHX3&RMxUSKn2XAHx_`g`Jh4csDlC6ijeC#*u_;+NADi?cPOhQCvcUz=o24S zpKXm!&_m^v6_VK$`ytnyZwj1A#l!KNv&nZs)T0f2-eUyj8@5K^QPt@BrsV!p@Vv#2 zRvxCPlF9IY=U$~EAU1nT5HI{G+53mu8soGE+HN$#I7fW4%BkAtO zfdnNl205K+rnC<74W}!np&RE<(UA0x@N8zlaB0-;%X7U_kNL{zzeIIAf}s9|LpFzH z#I8vg!%d9h*~*{XbFo2&!J1mmE_s(ra+AKIXb$X|d0!4J`J$}$ah)VFpWB1<94_P} z3k6X(>enUJKT~qjGf)#t>G@_4;qPo? z=;G_Q(KBY=K#r&XQQjBHx(A+76mwn<#D^UbYQtEj8+(0XzEdKp*zWW0kNx%K zPwxf9l4vf6a0M8@vJ^pF?0d|`=xu!ih>|Kv37^pD9@8C0B?rnk*c~1EGs*V@2dt7V z%)Gv1ykEy`AeogMvRsm(PT8$e(8L2dcttJ29v<#18&ZR0Z1>E^t}aUoHN=_U|cEXi8&5bh>=Yj48V17Q)`T zOkV5M-GiNOwZpF`Kt`eYvCSE4dC?)uLS~=1|BO0dG9G#oiW!{($j@zo@#nA#3|E?b zE|9YLEj+F1_~=ZxTgG3KMRWS4z&63i-Ul9^0at@Scq{*}C~u`qY#IJM-Q94lLtE?& zT;RBRT{&FfeycPK@PPNkK~I#opoU;*vU%VfLaLh(1pMp1E0h~eyOSUpEFEXQMB`ECTM-6uC4_A&ijfm`!=8bzV4`*MiNpK(tZD9 zd)TN^-Fa_sXp7=1zk%UE#mk`HfA2YvqV}VRsGP~)I;_$@)y&?c377#wUq7LttnO>( zf%xmB!#^okhkVCn3r7Vw9h;sQgll0v`YPGT$GG@2wM?IK=7qRZirUpe_d$h3L-u=n zu=e`@NJtM<%qqQ!udxSMa=$roWr@jen%sV%MAh4xW@v3VYsM`tLEr5+c|497Y}dg| zT^I{wAZ#HU1$U*J2=yI4?i9!basD~*j5+f+;+pvv?rO7yD!R~~xK4BZb(RGm1>qX|rIl-Za~o>{fc|MVu1f(%)76gy>~F1) ziZslu1!d7YTDnP%=lcsDV^Z77wY@>YXjz0obWxOTcGn}n)N}FC~ zL!lr~>vT1f?d6I}a$wgESkPdkGE$)p8_|rgsA@~Cz(wVk4c4tE9BI4E(KzU4HVIwX zE*I=yt|3oJks6aYbZ7k5+a2iU*flm`$_i4v#{}K=io|=kpyaL>ih}E~DDHW8gq_)G0N&Rl&XCL5ckYjA5N3|<)ciy1Gw|z-e(kT@*LMCLS9n@DKWKdx(`TrKoY$@ zm>LFthKSkO_|`y*Z3dN zcq?`p11wutIp9i`0aYtkgaX21Bm4Uwi-nP;KgdTS0F15yLQT^OvZpLZIzYffd0Fb+uQ z5Otm2zZ`s`mN|lYqV}wumUA2ADON)%kfUzHh&e>^dsDQh&&?{ICdou!V<0_XhVfBh z`Y2H@73u(Gr;SOpkP`6}kra}`l)6b%r6_=qSv7ylDP_(L>(@fj2C8ykAAKtW7V6cv z@}!ZKLi5^RMz;>_v%RopFt^k?{VIi5TxM)XdgUPWWOO@|@`D>F@V4Pz1kRCS1*tz0 z`yd5?pNE!nmW!=pIyC*T#iaAoQ~>R#@Pt1n9$b)OA0iZAb^7-O@-|X;>lSV>yHYF< zg+NlkUzh$mG>{(n5+ymmd!$8ENh}p`$wF{}?834}%!Gcz36qC*7Iod-!({=DjhSZ_ zW{X_gz}z7bU3>;$9pdkdK%Ad%LDHALgS|LiUel5srNvAg)VeF7BxDR_JXWa^j` zBdrOA@nw$y!JfU2=*MxOANmrlC%Fx!e{#+bsh)vb;2Muw2c?~Zyj>C4WFy*3Fa5je zJYpkm(9|x-5dFqF$?=u@&}5Wf2&n$K;-Pc>keYe1ha1*|Eq0lQ$@Fr@o~Ls<`bq0K z1FsHm#vZ<0oas}T!2jWxTr`}dpPHBlBzv#ly@S<4TUOH5D?hBAm+(CtXkPmAHnZMP zfkF0qf;T2c?o3E0K~cxtbfxJgGus_O3euFqr1RsVZPSthW0R!cN%5Ny*Un7L|P{u$ihk2_}nWgn3*O%?RL=XAuL zBUF$sJ=`IdH1o`0O+F`0N)BKy*C~0}zmlzX?oS32=|bSw8XA}_EUQ@Je>R9yfecX2 zmp2;|s6Wsrt3=lUM7``cs9faEF+FTYH{GhG?6raZnu^wl1m1y1I}g5- z_fV^2bMC4{JGX*7U^RYBQ^;?LcO5igO}l!r7_8j=)mp9!CJ}>>Chg}UpJr6zMGVW4 z1B@VdFEVH%wj1iL&KdG}Lk)Sw@cAJ|kgTbZ;@5OCz!>R4?-DM^o!Hex$cR~xnYS+= zge$mAzLZk=m^6#Y>VjX@b0C9*TS28OC)iP2s@RTogl@MClA>3G$!4HPm^A9*Q z_&WrJ?bv0MESS}|dl!x?zEI`>;2pM!1)axErS>&_Mi!i2zy6u! zm)_s8&qrzlzgd~eh?kl@btv(gxZ(3@#$1Fl{}@Z=fRcJxE1)ax;!GDiqX$1LGz1D! zf8Xu*>wfV*W?t^W6)hT=D&AV}vn}XRxV_~PK3S7~Zkl8uzFu)k+Q2&?UkIEE8r|Kb zEBM;{u+j~nD$%SE`yuK39)>LTvC4&KUv!pC1)(Gru%ftn5b5i#a$YnCO{RKp@~+Vw zQi;R{4yx5C1e;j(ckgTxyfH}OIkd9P3O6r-sa>~uRBW7dCos zF}T60wj5_sXI1Er?r4_M24jn?sR$c8tA@uEJHYlMEH(-&EzeiBl@d{EMn6<$hY1> zrziWta#O$_HYHVWl*g2@{|4XOc%G`G)YhQX!u!K|iZqR!Zv5%0UprE*V*99uGgz7W z|&qp?rvCiw%vY{)?P zH@g7Q;c(>$)L!TTo*p5vI}#t}fQR~6V7@bHk84IV+J?r@bd@mWa>%FRs+pZcho%8Z z0@?>$#-9Dc2_(qnDk0h)+02&=F!r3`vh*8vlkclw#0ASQMtF;2O%Qp|*f~KN?oaf3 zKge%}>BDW}1)I@mxw%CCJ!lUHi*mFKWK7WuG7T?P=;7`W1aJmPo-3~6lyUxYqT-{S zEv-Y>nag%H_7X-JQfb`I4d|)R&2760X8GtIe=ePMg&8aa_D$KhS?~4r3tS90D3a7I zx^6N=Z4mQWO`Y=r9`B{*Di~0Xbc5*MGbw)Kk@&fG6#S>m|D+I1vtdtLIy(4o3*;nZ zk*LSW;fOvIvy`nm@rW2O?|3-ttw6+<=JC>w0zIxTk!mruM9rYBtavZCLfFm18y?!9 zb!{Wn`ndVv&6sxBq{#K0C2@q28-R?x=uQ+nWx}*WoWQ|}S@Tyii>LP&8uULg&{C&t z(qe1*0yi-v9}+i~GPx~wEsv)lqFixiUD{vuyO4*3j}Ts}j7L$|G*0p{BKfR5|J$vh zu+qJyb3!GAJAFc<;s87sHXSHb)9r@rp|A2-H8-o=8TAbv<3Our_{XC9bCW-K>t#@# z4jTZUz)|BfHw|$0D-sGyuGn+X*h6icDG+zC?(_*O&YBq(0=@QE^yl@uGStnO(5||m zQh#1d0abnbg-UKT!)DXXuXLpqMtA=pvwIrmI^{`iyaqDc^r$KToG0A_uAQX=@{BeP zwsIn0wf_ghMEkCB2-T9vm*MH?_@vn8*{E71iZe+m9WU+;v~K+c$`>QmM@PL8X0 z8VtI_pSPpZ_0I@?6sHc3jbrl6#k^c=-5) zb*E7w^1YwI5+8=KTPD1B3f(&6N8@@<`ySbp(XK?;zlD`+d7>`ZgKleM!d(SUxkpbf zk>KbAQ5TkH1u<-=VwSenW=}*31Zci&A>L00n*6)_JW$mV5chib7HBb0^~%Z_%nhMI zNOB)RSOaLz!bnHve5;~`F-zXJr*KwNGTR3Jim3E@$~efkqRkLW=y453PiV3+MM;7m zocqq?AW@kwkme1sBjG`{ekA7CCi#O&IH z6oHzWXGwz%D=T=Y!wng4bB%aaY7C3;HUn(ckYeQF`cgtdTEYvERqbGz;=+YNJi3F9g)|SH{Xuf zj-lV!AmR7;*m~c2gpI#ZJ-D6l7a{26&X6BByRNGMU=GagZIu%rWV9(}A9xN`je%?d zt^!d{=D+E_UT|air=5N?|7xqi=X#bO}|$b|I%H~W5j@uBb8E(6|I}e|0%Hl^Y~Rk#Y04K7HN@~ zAy*+9a`p07DTrG};UGN@E1q0_#VSilz>p*Z<}3*nGBu$y4%_>yoxVA3jts>&)1*#Q zx3B2u$6lIqGEgluF?Qc>n1Yh9557kIk((U^2FVU*hUHFBq}2C9^bYJI0dgjEYyl7w zyJmrz512v_zrLi0;3cLY+N*Q~%Xx7hlQNvHYCPoI zCDK;k_Bv9V6Ma<_--z4aa&gdF>ksEi&U+L5=?uvge23mU!Jiw{{wf4e_*=OE&ei5z z$a3>8WG1MB_{M!*&Q51J{f2@qP%?g<`xGpb`taBV80(D}tt{@SMgy+o>s-|ne#VBW ze3%voN*W~}G>raP3)G-#=!za%ok}W%7Eu~>F}P1Ch6$8mHI2BXP)8t-LGT&+SKC_C z_owk(Vhj*e+2ye*WQDXP;|(4yTPvuPDzPRFw3_s7T%ss*BJTMz^9+#$0I5^Tamj zi%y#!_CN2{s>E^R{++g$>@O|BJn{`oLp4MFQ8hzf3#Bxb;5FU}qBzta{4?6^#AnNt zuQYsmbJ_?=n)vS9aIZ_!C|1O7cX?ldM_ty9-?ZRkbM$kT7peu5_rdyNxq7g&ux=*` z&?-{6exYqIG_cuXh9(hBNl%zN5$GxR)@hsbpzZrWW6YPM^bkeuGez#tJ+`agGiIE( zp~rO9wK|x^zVqESjQC-q9;|hzm zMu}LzudEK@Ybyez1Wz8m3S^s3y?`~F<90p2MNsg)`t@pDQd}`yh}@d)vlheG0~AKc zKXOe*`=iB*anmaXgA+ONcGwI9&FvtQC~2|P^nRnYfUvz`Q&DCSZz3bXCau6u`P9q) z!1`$cMnX7aL|)do#1=>8JZ4((#*bx<72z zYcqh3q`h$Q?yIe@B1ma-wL$(T{%qjMY%N$#d+HE2wTGO^RtNu1YO|(yXLgD@<>JRF z5-}-~sZJ4bBtP^b5Rv-=|MQ>zcIl4LfAFg&u5J@I^ z3r!{=?JiTnHCU{_3&2+VR7qe zu4>_}RpWSgKhN{!_4SkW7L1!f9#TazM8r-K=~gVh91n4%s%mZEwLV+eAa%*r@>Vfs@%Qi!UF6*6g?k%7H=>}m}SpExA~Xb0O? z)z(q7yHvJ3G=pmIYbJb>K_kAHG&OK1#Y>yNjDqcKRW%{h>z67jiG#D=gdtRZ-5>TC z^0G<~l!DrJKv}C~wz&ZRitD8IaU?0;S<5~b<1iw$wRlwdkC<$+=rJqN_Sz;H9c;6d z_~qraRUKYO;L?DUdTh+ORA#Gv{6^`>pJyXZO@sIYCzG_d;us;aQZy*s3ONY-m0m=b zn0CHtNVBP=u_$h7w8v2!(0i8ZswGg*n4=4DOS-RWkPl#&K{ddFo}`=jL@pOQ!GX<( zyq0;5oEeJxfmT>eX!+#}NV!UR`y)Xv84~KEh9Q9J*`KnR0$Awl&&ZgO7w{4iDroM z*$0@5Rs$m{_if{_so1kd?E2+4z5#8n@d$+s>%VaUB0S7l-*rf4F%`3nsWFQ(0yr9R zE=>Gcf^Yy^Qxpg~&2mjCj7hi8%O*S(HIpCU3!ds!vo|rW=0GKaDFn}Bx-4(*djnM; zx~W(CCsi!#>9QqV zDX)2Ubea;;Hf!9I@O$yOE&Xq8F+LFt?6QVb)e{l;F6rDU^lIg|)t&f}{IIRf1~7*7 zhT`FAQZ5FgUvX_iEFtmP7az0}5zI&^eC-FE{H)D-{hc+7FL*{-#36u1GrKH7yT!fA zEDUd8e7a(7!BgF5gH}-TRy;L8LG8?0=|IzvngrJJ+rmInjhGb_0!x;=IQsGWz$mNd z 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

    &LkWDLJ*LYZDkeN0*--(J#5 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μ<8W5T7D#H)2OvN)iy7ZGLi-v48&@9v}QshzW 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 <% if (!process.env.REACT_APP_SKIP_CSP) { %> <% let cspConfig = require('./csp.json'); %> + + <% let cspStyleNonce = require('crypto').randomUUID().replaceAll('-','') %> <% if (process.env.REACT_APP_STAGING) { %> <% const cspDevConfig = require('./vercel-csp.json'); %> @@ -26,7 +28,7 @@ <% } %> <% } %> diff --git a/apps/web/public/nfts-sitemap.xml b/apps/web/public/nfts-sitemap.xml index 5d053264215..c1c43d73ed0 100644 --- a/apps/web/public/nfts-sitemap.xml +++ b/apps/web/public/nfts-sitemap.xml @@ -2,647 +2,682 @@ https://app.uniswap.org/nfts/collection/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x60e4d786628fea6478f785a6d7e704777c86a7c6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0xed5af388653567af2f388e6224dc7c4b3241c544 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x34d85c9cdeb23fa97cb08333b511ac86e1c4e258 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x99a9b7c1116f9ceeb1652de04d5969cce509b069 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x49cf6f5d44e70224e2e23fdcdd2c053f30ada28b - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0xb7f7f6c52f2e2fdb1963eab30438024864c313f6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x23581767a106ae21c074b2276d25e5c3e136a68b - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x8a90cab2b38dba80c64b7734e58ee1db38b8992e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0xba30e5f9bb24caa003e9f2f0497ad287fdf95623 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0xbd3531da5cf5857e7cfaa92426877b022e612cf8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x7bd29408f11d2bfc23c34f18275bbf23bb716bc7 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x306b1ea3ecdf94ab739f1910bbda052ed4a9f949 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x1a92f7381b9f03921564a437210bb9396471050c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x5cc5b05a8a13e3fbdb0bb9fccd98d38e50f90c38 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x5af0d9827e0c53e4799bb226655a1de152a425a5 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x3bf2922f4520a8ba0c2efc3d2a1539678dad5e9d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0xe785e82358879f061bc3dcac6f0444462d4b5330 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x76be3b62873462d2142405439777e971754e8e77 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0xfd43af6d3fe1b916c026f6ac35b3ede068d1ca01 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x1cb1a5e65610aeff2551a50f76a87a7d3fb649c6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0xff9c1b15b16263c61d017ee9f65c50e4ae0113d7 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x6339e5e072086621540d0362c4e3cea0d643e114 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0xb932a70a57673d89f4acffbe830e8ed7f75fb9e0 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x79fcdef22feed20eddacbb2587640e45491b757f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0xa3aee8bce55beea1951ef834b99f3ac60d1abeeb - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x769272677fab02575e84945f03eca517acc544cc - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x4db1f25d3d98600140dfc18deb7515be5bd293af - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x34eebee6942d8def3c125458d1a86e0a897fd6f9 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x59468516a8259058bad1ca5f8f4bff190d30e066 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x394e3d3044fc89fcdd966d3cb35ac0b32b0cda91 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x60bb1e2aa1c9acafb4d34f71585d7e959f387769 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x28472a58a490c5e09a238847f66a68a47cc76f0f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x341a1c534248966c4b6afad165b98daed4b964ef - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x82c7a8f707110f5fbb16184a5933e9f78a34c6ab - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0xccc441ac31f02cd96c153db6fd5fe0a2f4e6a68d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x764aeebcf425d56800ef2c84f2578689415a2daa - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x160c404b2b49cbc3240055ceaee026df1e8497a0 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0xd2f668a8461d6761115daf8aeb3cdf5f40c532c6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x39ee2c7b3cb80254225884ca001f57118c8f21b6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0xd774557b647330c91bf44cfeab205095f7e6c367 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x1792a96e5668ad7c167ab804a100ce42395ce54d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0xf87e31492faf9a91b02ee0deaad50d51d56d5d4d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x04afa589e2b933f9463c5639f412b183ec062505 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0xe75512aa3bec8f00434bbd6ad8b0a3fbff100ad6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x348fc118bcc65a92dc033a951af153d14d945312 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x892848074ddea461a15f337250da3ce55580ca85 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x5946aeaab44e65eb370ffaa6a7ef2218cff9b47d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x282bdd42f4eb70e7a9d9f40c8fea0825b7f68c5d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x4b15a9c28034dc83db40cd810001427d3bd7163d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x7ea3cca10668b8346aec0bf1844a49e995527c8b - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0xb852c6b5892256c264cc2c888ea462189154d8d7 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x9378368ba6b85c1fba5b131b530f5f5bedf21a18 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x2acab3dea77832c09420663b0e1cb386031ba17b - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x0c2e57efddba8c768147d1fdf9176a0a6ebd5d83 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x08d7c0242953446436f34b4c78fe9da38c73668d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x8943c7bac1914c9a7aba750bf2b6b09fd21037e0 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x364c828ee171616a39897688a831c2499ad972ec - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x7f36182dee28c45de6072a34d29855bae76dbe2f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0xf61f24c2d93bf2de187546b14425bf631f28d6dc - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x797a48c46be32aafcedcfd3d8992493d8a1f256b - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x123b30e25973fecd8354dd5f41cc45a3065ef88c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x6632a9d63e142f17a668064d41a21193b49b41a0 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0xf4ee95274741437636e748ddac70818b4ed7d043 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x57a204aa1042f6e66dd7730813f4024114d74f37 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0xd1258db6ac08eb0e625b75b371c023da478e94a9 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x75e95ba5997eb235f40ecf8347cdb11f18ff640b - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0xd532b88607b1877fe20c181cba2550e3bbd6b31c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0xa1d4657e0e6507d5a94d06da93e94dc7c8c44b51 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0xedb61f74b0d09b2558f1eeb79b247c1f363ae452 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x7d8820fa92eb1584636f4f5b8515b5476b75171a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x231d3559aa848bf10366fb9868590f01d34bf240 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0xad9fd7cb4fc7a0fbce08d64068f60cbde22ed34c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x0e9d6552b85be180d941f1ca73ae3e318d2d4f1f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0xb716600ed99b4710152582a124c697a7fe78adbf - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0xaadc2d4261199ce24a4b0a57370c4fcf43bb60aa - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x4e1f41613c9084fdb9e34e11fae9412427480e56 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x79986af15539de2db9a5086382daeda917a9cf0c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0xc99c679c50033bbc5321eb88752e89a93e9e83c5 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0xc36cf0cfcb5d905b8b513860db0cfe63f6cf9f5c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x9c8ff314c9bc7f6e59a9d9225fb22946427edc03 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x3110ef5f612208724ca51f5761a69081809f03b7 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x036721e5a769cc48b3189efbb9cce4471e8a48b1 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x524cab2ec69124574082676e6f654a18df49a048 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x7ab2352b1d2e185560494d5e577f9d3c238b78c5 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x32973908faee0bf825a343000fe412ebe56f802a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x7daec605e9e2a1717326eedfd660601e2753a057 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0xc1caf0c19a8ac28c41fe59ba6c754e4b9bd54de9 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x33fd426905f149f8376e227d0c9d3340aad17af1 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x466cfcd0525189b573e794f554b8a751279213ac - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x6be69b2a9b153737887cfcdca7781ed1511c7e36 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x80336ad7a747236ef41f47ed2c7641828a480baa - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x9401518f4ebba857baa879d9f76e1cc8b31ed197 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x4b61413d4392c806e6d0ff5ee91e6073c21d6430 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0xc3f733ca98e0dad0386979eb96fb1722a1a05e69 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x09233d553058c2f42ba751c87816a8e9fae7ef10 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x960b7a6bcd451c9968473f7bbfd9be826efd549a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x36d30b3b85255473d27dd0f7fd8f35e36a9d6f06 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x698fbaaca64944376e2cdc4cad86eaa91362cf54 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x497a9a79e82e6fc0ff10a16f6f75e6fcd5ae65a8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x41a322b28d0ff354040e2cbc676f0320d8c8850d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0xa9c0a07a7cb84ad1f2ffab06de3e55aab7d523e8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x942bc2d3e7a589fe5bd4a5c6ef9727dfd82f5c8a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x8821bee2ba0df28761afff119d66390d594cd280 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x8c6def540b83471664edc6d5cf75883986932674 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x8d9710f0e193d3f95c0723eaaf1a81030dc9116d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x86825dfca7a6224cfbd2da48e85df2fc3aa7c4b1 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x629a673a8242c2ac4b7b8c5d8735fbeac21a6205 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x9a534628b4062e123ce7ee2222ec20b86e16ca8f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0xc2c747e0f7004f9e8817db2ca4997657a7746928 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x73da73ef3a6982109c4d5bdb0db9dd3e3783f313 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0xc92ceddfb8dd984a89fb494c376f9a48b999aafc - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x3248e8ba90facc4fdd3814518c14f8cc4d980e4b - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x67d9417c9c3c250f61a83c7e8658dac487b56b09 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0xb6a37b5d14d502c3ab0ae6f3a0e058bc9517786e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x86c10d10eca1fca9daf87a279abccabe0063f247 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x4b3406a41399c7fd2ba65cbc93697ad9e7ea61e5 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0xb0640e8b5f24bedc63c33d371923d68fde020303 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0xd3d9ddd0cf0a5f0bfb8f7fceae075df687eaebab - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0xa5c0bd78d1667c13bfb403e2a3336871396713c5 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x4d7d2e237d64d1484660b55c0a4cc092fa5e6716 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0xfcb1315c4273954f74cb16d5b663dbf479eec62e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x66d1db16101502ed0ca428842c619ca7b62c8fef - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x128675d4fddbc4a0d3f8aa777d8ee0fb8b427c2f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x19b86299c21505cdf59ce63740b240a9c822b5e4 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0xacf63e56fd08970b43401492a02f6f38b6635c91 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0x0bebad1ff25c623dff9605dad4a8f782d5da37df - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.7 https://app.uniswap.org/nfts/collection/0xdceaf1652a131f32a821468dc03a92df0edd86ea - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x273f7f8e6489682df756151f5525576e322d51a3 + 2024-07-05T19:43:14.783Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x77372a4cc66063575b05b44481f059be356964a4 + 2024-07-05T19:43:14.783Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0xf5b0a3efb8e8e4c201e2a935f110eaaf3ffecb8d + 2024-07-05T19:43:14.783Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x22c36bfdcef207f9c0cc941936eff94d4246d14a + 2024-07-05T19:43:14.783Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x59325733eb952a92e069c87f0a6168b29e80627f + 2024-07-05T19:43:14.783Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x0e3a2a1f2146d86a604adc220b4967a898d7fe07 + 2024-07-05T19:43:14.783Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x3af2a97414d1101e2107a70e7f33955da1346305 + 2024-07-05T19:43:14.783Z 0.7 \ No newline at end of file diff --git a/apps/web/public/pools-sitemap.xml b/apps/web/public/pools-sitemap.xml index 0903b0778ae..a63ab564281 100644 --- a/apps/web/public/pools-sitemap.xml +++ b/apps/web/public/pools-sitemap.xml @@ -2,4357 +2,5652 @@ https://app.uniswap.org/explore/pools/ethereum/0xcbcdf9626bc03e24f779434178a73a0b4bad62ed - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x4e68ccd3e89f51c3074ca5072bbac773960dfa36 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x4585fe77225b41b697c938b018e2ac67ac5a20c0 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xc63b0708e2f7e69cb8a1df0e1389a98c35a76d52 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x99ac8ca7087fa4a2a1fb6357269965a2014abc35 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x11b815efb8f581194ae79006d24e0d814b7697f6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xa6cc3c2531fdaa6ae1a3ca84c2855806728693e8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x5777d92f208679db4b9778590fa3cab3ac9e2168 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x1d42064fc4beb5f8aaf85f4617ae8b3b5b8bd801 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xc2e9f25be6257c210d7adf0d4cd6e3e881ba25f8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x11950d141ecb863f01007add7d1a342041227b58 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xc5c134a1f112efa96003f8559dba6fac0ba77692 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x109830a1aaad605bbf02a9dfa7b0b92ec2fb7daa - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x1df4c6e36d61416813b42fe32724ef11e363eddc - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x12d6867fa648d269835cf69b49f125147754b54d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x3416cf6c708da44db2624d63ea0aaef7113527c6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xe8c6c9227491c0a8156a0106a0204d881bb7e531 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x04708077eca6bb527a5bbbd6358ffb043a9c1c14 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x9db9e0e53058c89e5b94e29621a205198648425b - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xf239009a101b6b930a527deaab6961b6e7dec8a6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xfe0df74636bc25c7f2400f22fe7dae32d39443d2 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xf4c5e0f4590b6679b3030d29a84857f226087fef - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x5764a6f2212d502bc5970f9f129ffcd61e5d7563 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xa3f558aebaecaf0e11ca4b2199cc5ed341edfd74 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x99132b53ab44694eeb372e87bced3929e4ab8456 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x6c6bc977e13df9b0de53b251522280bb72383700 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x9d96880952b4c80a55099b9c258250f2cc5813ec - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x3afdc5e6dfc0b0a507a8e023c9dce2cafc310316 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x290a6a7460b308ee3f19023d2d00de604bcf5b42 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xac4b3dacb91461209ae9d41ec517c2b9cb1b7daf - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x60594a405d53811d3bc4766596efd80fd545a270 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x331399c614ca67dee86733e5a2fba40dbb16827c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x4b5ab61593a2401b1075b90c04cbcdd3f87ce011 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x844eb5c280f38c7462316aad3f338ef9bda62668 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xe936f0073549ad8b1fa53583600d629ba9375161 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x2f62f2b4c5fcd7570a709dec05d68ea19c82a9ec - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x381fe4eb128db1621647ca00965da3f9e09f4fac - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x97e7d56a0408570ba1a7852de36350f7713906ec - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xcd423f3ab39a11ff1d9208b7d37df56e902c932b - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xe15e6583425700993bd08f51bf6e7b73cd5da91b - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x69d91b94f0aaf8e8a2586909fa77a5c2c89818d5 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xe42318ea3b998e8355a3da364eb9d48ec725eb45 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xad9ef19e289dcbc9ab27b83d2df53cdeff60f02d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x3b685307c8611afb2a9e83ebc8743dc20480716e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x7bea39867e4169dbe237d55c8242a8f2fcdcc387 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x7b1e5d984a43ee732de195628d20d05cfabc3cc7 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x7858e59e0c01ea06df3af3d20ac7b0003275d4bf - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xae2a25cbdb19d0dc0dddd1d2f6b08a6e48c4a9a9 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x21b8065d10f73ee2e260e5b47d3344d3ced7596e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x0d4a11d5eeaac28ec3f61d100daf4d40471f1852 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x517f9dd285e75b599234f7221227339478d0fcc8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xa43fe16908251ee70ef74718545e4fe6c5ccec9f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x0af81cd5d9c124b4859d65697a4cd10ee223746a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xca7c2771d248dcbe09eabe0ce57a62e18da178c0 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x09d1d767edf8fa23a64c51fa559e0688e526812f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x7b73644935b8e68019ac6356c40661e1bc315860 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x180efc1349a69390ade25667487a826164c9c6e4 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x9c4fe5ffd9a9fc5678cfbd93aa2d4fd684b67c4c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xa478c2975ab1ea89e8196811f51a7b7ade33eb11 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xbb2b8038a1640196fbe3e38816f3e67cba72d940 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x9ec9367b8c4dd45ec8e7b800b1f719251053ad60 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xc91ef786fbf6d62858262c82c63de45085dea659 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x197d7010147df7b99e9025c724f13723b29313f8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x25647e01bd0967c1b9599fa3521939871d1d0888 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x2f0b1417aa42ebf0b4ca1154212847f6094d708d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x6ada49aeccf6e556bb7a35ef0119cc8ca795294a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x2a6c340bcbb0a79d3deecd3bc5cbc2605ea9259f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xda2d09fbbf8ee4b5051a0e9b562c5fcb4b393b18 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x48d20b3e529fb3dd7d91293f80638df582ab2daa - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x4028daac072e492d34a3afdbef0ba7e35d8b55c4 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xc2eab7d33d3cb97692ecb231a5d0e4a649cb539d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xc5be99a02c6857f9eac67bbce58df5572498f40c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xe4b8583ccb95b25737c016ac88e539d0605949e8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x8dbee21e8586ee356130074aaa789c33159921ca - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x43de4318b6eb91a7cf37975dbb574396a7b5b5c6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x9ff68f61ca5eb0c6606dc517a9d44001e564bb66 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xa29fe6ef9592b5d408cca961d0fb9b1faf497d6d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x1b1137dd16faa651e38a9dfb5d9ffff7767fdf62 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x470e8de2ebaef52014a47cb5e6af86884947f08c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x8fb8e9921922d2ffb529a95d28a0d06d275d7a59 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xd3d2e2692501a5c9ca623199d38826e513033a17 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x97e1fcb93ae7267dbafad23f7b9afaa08264cfd8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xa5e9c917b4b821e4e0a5bbefce078ab6540d6b5e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x2cc846fff0b08fb3bffad71f53a60b4b6e6d6482 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x959873fb4fc11825fba83c80c4c632db1e936e15 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xa7480aafa8ad2af3ce24ac6853f960ae6ac7f0c4 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xc7e6b676bfc73ae40bcc4577f22aab1682c691c6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x570febdf89c07f256c75686caca215289bb11cfc - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x343fd171caf4f0287ae6b87d75a8964dc44516ab - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xcaa004418eb42cdf00cb057b7c9e28f0ffd840a5 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xe3d3551bb608e7665472180a20280630d9e938aa - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xb6b0c651c37ec4ca81c0a128420e02001a57fac2 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x4e34da137f0b317c633838458e0c923a5e088752 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xfe9e7931e55c514c33d489c88582fa36e84bd8e3 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x5281e311734869c64ca60ef047fd87759397efe6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x149148acc3b06b8cc73af3a10e84189243a35925 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x8ef79d6c328c25da633559c20c75f638a4863462 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x14af1804dbbf7d621ecc2901eef292a24a0260ea - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x80a9ae39310abf666a87c743d6ebbd0e8c42158e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xc31e54c7a869b9fcbecc14363cf510d1c41fa443 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x2f5e87c9312fa29aed5c179e456625d79015299c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xc6962004f452be9203591991d15f6b388e09e8d0 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xc6f780497a95e246eb9449f5e4770916dcd6396a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x641c00a822e8b671738d32a431a4fb6074e5c79d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x92c63d0e701caae670c9415d91c474f686298f00 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x1aeedd3727a6431b8f070c0afaa81cc74f273882 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xcda53b1f66614552f834ceef361a8d12a0b8dad8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x35218a1cbac5bbc3e57fd9bd38219d37571b3537 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x17c14d2c404d167802b16c450d3c99f88f2c4f4d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x468b88941e7cc0b88c1869d68ab6b570bcef62ff - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xdbaeb7f0dfe3a0aafd798ccecb5b22e708f7852c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x149e36e72726e0bcea5c59d40df2c43f60f5a22d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xbaaf1fc002e31cb12b99e4119e5e350911ec575b - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xa67f72f21bd9f91db2da2d260590da5e6c437009 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x92fd143a8fa0c84e016c2765648b9733b0aa519e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x7cf803e8d82a50504180f417b8bc7a493c0a0503 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x81c48d31365e6b526f6bbadc5c9aafd822134863 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x446bf9748b4ea044dd759d9b9311c70491df8f29 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xc82819f72a9e77e2c0c3a69b3196478f44303cf4 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x50c7390dfdd3756139e6efb5a461c2eb7331ceb4 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x1dfc1054e0e2a10e33c9ca21aad5aa8a1cce91e3 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xc91b7b39bbb2c733f0e7459348fd0c80259c8471 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x59d72ddb29da32847a4665d08ffc8464a7185fae - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x09ba302a3f5ad2bf8853266e271b005a5b3716fe - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xa77d77c9773c35e910acc2e30cefe52b54a58414 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x8da66e470403b3d3eee66c67e2c61fda6e248ad1 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x2f020e708811c054f146eebcc4d5a215fd4eec26 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x7e7fb3cceca5f2ac952edf221fd2a9f62e411980 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x68c685fd52a56f04665b491d491355a624540e85 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xa8328bf492ba1b77ad6381b3f7567d942b000baf - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xc0cf0f380ddb44dbcaf19a86d094c8bba3efa04a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xa169d1ab5c948555954d38700a6cdaa7a4e0c3a0 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x1862200e8e7ce1c0827b792d0f9546156f44f892 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x05bbaaa020ff6bea107a9a1e06d2feb7bfd79ed2 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xd02a4969dc12bb889754361f8bcf3385ac1b2077 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xc24f7d8e51a64dc1238880bd00bb961d54cbeb29 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x7c06736e41236fecd681dd3353aa77ecd19ea565 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xc473e2aee3441bf9240be85eb122abb059a3b57c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x14353445c8329df76e6f15e9ead18fa2d45a8bb6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x2039f8c9cd32ba9cd2ea7e575d5b1abea93f7527 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xd3e11119d2680c963f1cdcffece0c4ade823fb58 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x8e295789c9465487074a65b1ae9ce0351172393f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x97bca422ec0ee4851f2110ea743c1cd0a14835a1 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xbe3ad6a5669dc0b8b12febc03608860c31e2eef6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x56ebd63a756b94d3de9cea194896b4920b64fb01 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xe2ddd33585b441b9245085588169f35108f85a6e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x84436a2af97f37018db116ae8e1b691666db3d00 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x21b8065d10f73ee2e260e5b47d3344d3ced7596e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x0d4a11d5eeaac28ec3f61d100daf4d40471f1852 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x517f9dd285e75b599234f7221227339478d0fcc8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xa43fe16908251ee70ef74718545e4fe6c5ccec9f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x0af81cd5d9c124b4859d65697a4cd10ee223746a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xca7c2771d248dcbe09eabe0ce57a62e18da178c0 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x09d1d767edf8fa23a64c51fa559e0688e526812f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x7b73644935b8e68019ac6356c40661e1bc315860 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x180efc1349a69390ade25667487a826164c9c6e4 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x9c4fe5ffd9a9fc5678cfbd93aa2d4fd684b67c4c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xa478c2975ab1ea89e8196811f51a7b7ade33eb11 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xbb2b8038a1640196fbe3e38816f3e67cba72d940 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x9ec9367b8c4dd45ec8e7b800b1f719251053ad60 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xc91ef786fbf6d62858262c82c63de45085dea659 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x197d7010147df7b99e9025c724f13723b29313f8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x25647e01bd0967c1b9599fa3521939871d1d0888 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x2f0b1417aa42ebf0b4ca1154212847f6094d708d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x6ada49aeccf6e556bb7a35ef0119cc8ca795294a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x2a6c340bcbb0a79d3deecd3bc5cbc2605ea9259f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xda2d09fbbf8ee4b5051a0e9b562c5fcb4b393b18 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x48d20b3e529fb3dd7d91293f80638df582ab2daa - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x4028daac072e492d34a3afdbef0ba7e35d8b55c4 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xc2eab7d33d3cb97692ecb231a5d0e4a649cb539d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xc5be99a02c6857f9eac67bbce58df5572498f40c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xe4b8583ccb95b25737c016ac88e539d0605949e8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x8dbee21e8586ee356130074aaa789c33159921ca - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x43de4318b6eb91a7cf37975dbb574396a7b5b5c6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x9ff68f61ca5eb0c6606dc517a9d44001e564bb66 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xa29fe6ef9592b5d408cca961d0fb9b1faf497d6d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x1b1137dd16faa651e38a9dfb5d9ffff7767fdf62 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x470e8de2ebaef52014a47cb5e6af86884947f08c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x8fb8e9921922d2ffb529a95d28a0d06d275d7a59 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xd3d2e2692501a5c9ca623199d38826e513033a17 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x97e1fcb93ae7267dbafad23f7b9afaa08264cfd8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xa5e9c917b4b821e4e0a5bbefce078ab6540d6b5e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x2cc846fff0b08fb3bffad71f53a60b4b6e6d6482 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x959873fb4fc11825fba83c80c4c632db1e936e15 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xa7480aafa8ad2af3ce24ac6853f960ae6ac7f0c4 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xc7e6b676bfc73ae40bcc4577f22aab1682c691c6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x570febdf89c07f256c75686caca215289bb11cfc - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x343fd171caf4f0287ae6b87d75a8964dc44516ab - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xcaa004418eb42cdf00cb057b7c9e28f0ffd840a5 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xe3d3551bb608e7665472180a20280630d9e938aa - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xb6b0c651c37ec4ca81c0a128420e02001a57fac2 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x4e34da137f0b317c633838458e0c923a5e088752 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xfe9e7931e55c514c33d489c88582fa36e84bd8e3 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x5281e311734869c64ca60ef047fd87759397efe6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x149148acc3b06b8cc73af3a10e84189243a35925 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x8ef79d6c328c25da633559c20c75f638a4863462 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x68f5c0a2de713a54991e01858fd27a3832401849 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x4533bad2dc588f0fadf8d2e72386d4cd6a19b519 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x85149247691df622eaf1a8bd0cafd40bc45154a9 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x0392b358ce4547601befa962680bede836606ae2 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x1c3140ab59d6caf9fa7459c6f83d4b52ba881d36 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xd1f1bad4c9e6c44dec1e9bf3b94902205c5cd6c3 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x03af20bdaaffb4cc0a521796a223f7d85e2aac31 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x73b14a78a0d396c521f954532d43fd5ffe385216 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xac85eaf55e9c60ed40a683de7e549d23fdfbeb33 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x04f6c85a1b00f6d9b75f91fd23835974cc07e65c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x730691cdac3cbd4d41fc5eb9d8abbb0cea795b94 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x535541f1aa08416e69dc4d610131099fa2ae7222 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xfc1f3296458f9b2a27a0b91dd7681c4020e09d05 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x85c31ffa3706d1cce9d525a00f1c7d4a2911754c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xd52533a3309b393afebe3176620e8ccfb6159f8a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xff7fbdf7832ae524deda39ca402e03d92adff7a5 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xb589969d38ce76d3d7aa319de7133bc9755fd840 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xf334f6104a179207ddacfb41fa3567feea8595c2 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x1fb3cf6e48f1e7b10213e7b6d87d4c073c7fdb7b - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xd4344ea0c5ade7e22b9b275f0bde7a145dec5a23 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x5b42a63d6741416ce9a7b9f4f16d8c9231ccddd4 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x252cbdff917169775be2b552ec9f6781af95e7f6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x2ab22ac86b25bd448a4d9dc041bd2384655299c4 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xc858a329bf053be78d6239c4a4343b8fbd21472b - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xa73c628eaf6e283e26a7b1f8001cf186aa4c0e8e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xb533c12fb4e7b53b5524eab9b47d93ff6c7a456f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x2ae3d6096d8215ac2acddf30c60caa984ea5debe - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x19ea026886cbb7a900ecb2458636d72b5cae223b - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x6f32061f59a21086c334d0d45f804089ce374aaf - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xfaf037caafa9620bfaebc04c298bf4a104963613 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xadb35413ec50e0afe41039eac8b930d313e94fa4 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xe9e3893921de87b1194a8108f9d70c24bde71c27 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xf1f199342687a7d78bcc16fce79fa2665ef870e1 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xf44acaa38be5e965c5ddf374e7a2ba270e580684 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x36e42931a765022790b797963e42c5522d6b585a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x5adba6c5589c50791dd65131df29677595c7efa7 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x3249e3e3e4133ee18e65347daf586610cc265f54 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xca1b837c87c6563910c2befa48834fa2a8c3d72d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x6ef7b14bcd8d989cef8f8ec8ba4bf371b2ac95fd - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x37ffd11972128fd624337ebceb167c8c0a5115ff - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xe62bd99a9501ca33d98913105fc2bec5bae6e5dd - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xb2ac2e5a3684411254d58b1c5a542212b782114d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xb0efaf46a1de55c54f333f93b1f0641e73bc16d0 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xd0fa3b5264ccde31e8b094b86bca4a1e97d3c603 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xad4c666fc170b468b19988959eb931a3676f0e9f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x790fde1fd6d2568050061a88c375d5c2e06b140b - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xaefc1edaede6adadcdf3bb344577d45a80b19582 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xa8a5356ee5d02fe33d72355e4f698782f8f199e8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x55bc964fe3b0c8cc2d4c63d65f1be7aef9bb1a3c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x95d9d28606ee55de7667f0f176ebfc3215cfd9c0 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x21b8065d10f73ee2e260e5b47d3344d3ced7596e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x0d4a11d5eeaac28ec3f61d100daf4d40471f1852 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x517f9dd285e75b599234f7221227339478d0fcc8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xa43fe16908251ee70ef74718545e4fe6c5ccec9f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x0af81cd5d9c124b4859d65697a4cd10ee223746a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xca7c2771d248dcbe09eabe0ce57a62e18da178c0 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x09d1d767edf8fa23a64c51fa559e0688e526812f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x7b73644935b8e68019ac6356c40661e1bc315860 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x180efc1349a69390ade25667487a826164c9c6e4 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x9c4fe5ffd9a9fc5678cfbd93aa2d4fd684b67c4c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xa478c2975ab1ea89e8196811f51a7b7ade33eb11 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xbb2b8038a1640196fbe3e38816f3e67cba72d940 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x9ec9367b8c4dd45ec8e7b800b1f719251053ad60 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xc91ef786fbf6d62858262c82c63de45085dea659 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x197d7010147df7b99e9025c724f13723b29313f8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x25647e01bd0967c1b9599fa3521939871d1d0888 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x2f0b1417aa42ebf0b4ca1154212847f6094d708d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x6ada49aeccf6e556bb7a35ef0119cc8ca795294a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x2a6c340bcbb0a79d3deecd3bc5cbc2605ea9259f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xda2d09fbbf8ee4b5051a0e9b562c5fcb4b393b18 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x48d20b3e529fb3dd7d91293f80638df582ab2daa - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x4028daac072e492d34a3afdbef0ba7e35d8b55c4 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xc2eab7d33d3cb97692ecb231a5d0e4a649cb539d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xc5be99a02c6857f9eac67bbce58df5572498f40c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xe4b8583ccb95b25737c016ac88e539d0605949e8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x8dbee21e8586ee356130074aaa789c33159921ca - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x43de4318b6eb91a7cf37975dbb574396a7b5b5c6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x9ff68f61ca5eb0c6606dc517a9d44001e564bb66 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xa29fe6ef9592b5d408cca961d0fb9b1faf497d6d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x1b1137dd16faa651e38a9dfb5d9ffff7767fdf62 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x470e8de2ebaef52014a47cb5e6af86884947f08c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x8fb8e9921922d2ffb529a95d28a0d06d275d7a59 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xd3d2e2692501a5c9ca623199d38826e513033a17 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x97e1fcb93ae7267dbafad23f7b9afaa08264cfd8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xa5e9c917b4b821e4e0a5bbefce078ab6540d6b5e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x2cc846fff0b08fb3bffad71f53a60b4b6e6d6482 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x959873fb4fc11825fba83c80c4c632db1e936e15 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xa7480aafa8ad2af3ce24ac6853f960ae6ac7f0c4 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xc7e6b676bfc73ae40bcc4577f22aab1682c691c6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x570febdf89c07f256c75686caca215289bb11cfc - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x343fd171caf4f0287ae6b87d75a8964dc44516ab - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xcaa004418eb42cdf00cb057b7c9e28f0ffd840a5 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xe3d3551bb608e7665472180a20280630d9e938aa - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xb6b0c651c37ec4ca81c0a128420e02001a57fac2 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x4e34da137f0b317c633838458e0c923a5e088752 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xfe9e7931e55c514c33d489c88582fa36e84bd8e3 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x5281e311734869c64ca60ef047fd87759397efe6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x149148acc3b06b8cc73af3a10e84189243a35925 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x8ef79d6c328c25da633559c20c75f638a4863462 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x45dda9cb7c25131df268515131f647d726f50608 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x50eaedb835021e4a108b7290636d62e9765cc6d7 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x167384319b41f7094e62f7506409eb38079abff8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xa374094527e1673a86de625aa59517c5de346d32 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x86f1d8390222a3691c28938ec7404a1661e618e0 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xeda1094f59a4781456734e5d258b95e6be20b983 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x847b64f9d3a95e977d157866447a5c0a5dfa0ee5 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x94ab9e4553ffb839431e37cc79ba8905f45bfbea - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x0e44ceb592acfc5d3f09d996302eb4c499ff8c10 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x1e5bd2ab4c308396c06c182e1b7e7ba8b2935b83 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x9b08288c3be4f62bbf8d1c20ac9c5e6f9467d8b7 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xb6e57ed85c4c9dbfef2a68711e9d6f36c56e0fcb - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x3e31ab7f37c048fc6574189135d108df80f0ea26 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xd36ec33c8bed5a9f7b6630855f1533455b98a418 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xdac8a8e6dbf8c690ec6815e0ff03491b2770255d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xfe343675878100b344802a6763fd373fdeed07a4 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x0a28c2f5e0e8463e047c203f00f649812ae67e4f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x88f3c15523544835ff6c738ddb30995339ad57d6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x98b9162161164de1ed182a0dfa08f5fbf0f733ca - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xeef1a9507b3d505f0062f2be9453981255b503c8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xc4c06c9a239f94fc0a1d3e04d23c159ebe8316f1 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x849ec65748107aedc518dbc42961f358ea1361a7 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x2db87c4831b2fec2e35591221455834193b50d1b - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xa4d8c89f0c20efbe54cba9e7e7a7e509056228d9 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x642f28a89fa9d0fa30e664f71804bfdd7341d21f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x2aceda63b5e958c45bd27d916ba701bc1dc08f7a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x781067ef296e5c4a4203f81c593274824b7c185d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x4ccd010148379ea531d6c587cfdd60180196f9b1 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xd866fac7db79994d08c0ca2221fee08935595b4b - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x941061770214613ba0ca3db9a700c39587bb89b6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xa9077cdb3d13f45b8b9d87c43e11bce0e73d8631 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xa01f64fa1b923dd9c5c7618b39a6ba8098a88863 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xa830ff28bb7a46570a7e43dc24a35a663b9cfc2e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x8837a61644d523cbe5216dde226f8f85e3aa9be3 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xca5d44977d6de1846530eb434167b208752fba7d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x4d05f2a005e6f36633778416764e82d1d12e7fbb - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x41e64a5bc929fa8e6a9c8d7e3b81a13b21ff3045 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x3ea34cfc9322273311f7843826a2581c4a00fd39 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x785061ed819414dc4269d2a5d5974069c0daea96 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x3f5228d0e7d75467366be7de2c31d0d098ba2c23 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x2e3f22e9a1c2470b2e293351f48c99e1fd788f32 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x2a08c38c7e1fa969325e2b64047abb085dec3756 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xe6c36eed27c2e8ecb9a233bf12da06c9730b5955 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xefa98fdf168f372e5e9e9b910fcdfd65856f3986 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x76fa081e510f43ac8335efdb4db88c9ff1894413 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xc6832ef0af793336aa44a936e54b992bff47e7cd - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x865f456479a21e2b3d866561d7171a3d0a7b112d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xbd934a7778771a7e2d9bf80596002a214d8c9304 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x9ab9f658104467604b5afa9a3e1df62f35f7b208 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x6e430d59ba145c59b73a6db674fe3d53c1f31cae - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x21b8065d10f73ee2e260e5b47d3344d3ced7596e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x0d4a11d5eeaac28ec3f61d100daf4d40471f1852 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x517f9dd285e75b599234f7221227339478d0fcc8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xa43fe16908251ee70ef74718545e4fe6c5ccec9f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x0af81cd5d9c124b4859d65697a4cd10ee223746a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xca7c2771d248dcbe09eabe0ce57a62e18da178c0 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x09d1d767edf8fa23a64c51fa559e0688e526812f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x7b73644935b8e68019ac6356c40661e1bc315860 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x180efc1349a69390ade25667487a826164c9c6e4 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x9c4fe5ffd9a9fc5678cfbd93aa2d4fd684b67c4c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xa478c2975ab1ea89e8196811f51a7b7ade33eb11 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xbb2b8038a1640196fbe3e38816f3e67cba72d940 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x9ec9367b8c4dd45ec8e7b800b1f719251053ad60 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xc91ef786fbf6d62858262c82c63de45085dea659 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x197d7010147df7b99e9025c724f13723b29313f8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x25647e01bd0967c1b9599fa3521939871d1d0888 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x2f0b1417aa42ebf0b4ca1154212847f6094d708d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x6ada49aeccf6e556bb7a35ef0119cc8ca795294a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x2a6c340bcbb0a79d3deecd3bc5cbc2605ea9259f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xda2d09fbbf8ee4b5051a0e9b562c5fcb4b393b18 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x48d20b3e529fb3dd7d91293f80638df582ab2daa - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x4028daac072e492d34a3afdbef0ba7e35d8b55c4 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xc2eab7d33d3cb97692ecb231a5d0e4a649cb539d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xc5be99a02c6857f9eac67bbce58df5572498f40c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xe4b8583ccb95b25737c016ac88e539d0605949e8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x8dbee21e8586ee356130074aaa789c33159921ca - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x43de4318b6eb91a7cf37975dbb574396a7b5b5c6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x9ff68f61ca5eb0c6606dc517a9d44001e564bb66 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xa29fe6ef9592b5d408cca961d0fb9b1faf497d6d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x1b1137dd16faa651e38a9dfb5d9ffff7767fdf62 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x470e8de2ebaef52014a47cb5e6af86884947f08c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x8fb8e9921922d2ffb529a95d28a0d06d275d7a59 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xd3d2e2692501a5c9ca623199d38826e513033a17 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x97e1fcb93ae7267dbafad23f7b9afaa08264cfd8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xa5e9c917b4b821e4e0a5bbefce078ab6540d6b5e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x2cc846fff0b08fb3bffad71f53a60b4b6e6d6482 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x959873fb4fc11825fba83c80c4c632db1e936e15 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xa7480aafa8ad2af3ce24ac6853f960ae6ac7f0c4 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xc7e6b676bfc73ae40bcc4577f22aab1682c691c6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x570febdf89c07f256c75686caca215289bb11cfc - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x343fd171caf4f0287ae6b87d75a8964dc44516ab - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xcaa004418eb42cdf00cb057b7c9e28f0ffd840a5 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xe3d3551bb608e7665472180a20280630d9e938aa - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xb6b0c651c37ec4ca81c0a128420e02001a57fac2 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x4e34da137f0b317c633838458e0c923a5e088752 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xfe9e7931e55c514c33d489c88582fa36e84bd8e3 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x5281e311734869c64ca60ef047fd87759397efe6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x149148acc3b06b8cc73af3a10e84189243a35925 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x8ef79d6c328c25da633559c20c75f638a4863462 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x9e37cb775a047ae99fc5a24dded834127c4180cd - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x48413707b70355597404018e7c603b261fcadf3f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xade9bcd4b968ee26bed102dd43a55f6a8c2416df - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xda679706ff21114ac9fac5198bff24543f357a16 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xba3f945812a83471d709bce9c3ca699a19fb46f7 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xc9034c3e7f58003e6ae0c8438e7c8f4598d5acaa - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x4c36388be6f416a29c8d8eee81c771ce6be14b18 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xa1b2457c0b627f97f6cc892946a382451e979014 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x4b0aaf3ebb163dd45f663b38b6d93f6093ebc2d3 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xae2ce200bdb67c472030b31f602f0756c9aeb61c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x3bc5180d5439b500f381f9a46f15dd6608101671 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x5122e02898ece3bc62df8c1efdb29a9e914244d3 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x24e1cbd6fed006ceed9af0dce688acc7951d57a9 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x2556230ac694093d4d3b7b965a2f2d77d4c403a4 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xdaca082c2c7d052a96fa83ea9d3a7b6839e39586 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xa555149210075702a734968f338d5e1cbd509354 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x10648ba41b8565907cfa1496765fa4d95390aa0d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x00bcec1526dae1e170a53017b8775a93b7810d7c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x20e068d76f9e90b90604500b84c7e19dcb923e7e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x6b93950a9b589bc32b82a5df4e5148f98a7fae27 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xd9caa6dbe6791fcb7fc9fb59d1a6b3dd8c1c2339 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x62e81e93136ac42a1ada48d4098f5f9e703e7455 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x84206d33845c9d811438b6fe4e7a0c634748dc50 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xd0b53d9277642d899df5c87a3966a349a798f224 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xcfa7c4bb565915f1c4f9475e2a0536d31efad776 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xa7de21f28ca460b45373b217cd4eb111c3faeff8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xb64dff20dd5c47e6dbb56ead80d23568006dec1e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xad4e969f4193878e5cc89cefb57faf6c7c0048da - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xdf5eb97e3e23ca7f5a5fd2264680377c211310ba - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xf16baaae8eb7b37f4280e72924479f69e7a61f32 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xe745a591970e0fa981204cf525e170a2b9e4fb93 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x64b74c66b9ba60ca668b781289767ae7298f37ae - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x17e1ebd791e7253a5e606fd94c5b66c14d873136 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x46715bd57b9ec01deadb35fe096fb44acda79414 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x3447accd4b8e735329d1065244aad2ed630f0122 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x2feb7f3ffc243f7de94d5ea5975533d301584e07 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x0d5959a52e7004b601f0be70618d01ac3cdce976 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x2170ca774e48a3f51559917ada6f9d7ae8f7bfea - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x62a76dfa8951aefcff787e790782db3633ebf422 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x8073679e0b3b2d1d665777cf1b2b5b1c2d3d2d0c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x143f1a6f3fb32e6ab3f22d3cc6b417b5c2197599 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x82ad659c2f152aad59bb37cbc5e7663a2de0c607 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xa4efe9e8e2a2d5a2ac46805f233b8e49d0e11955 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xfcc89a1f250d76de198767d33e1ca9138a7fb54b - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x2faa2b42b782d578a160f61bb7cd763a17476730 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xdd44c0e83c2570062d1e6fdd440b4724862e8f31 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xe3930a14641786e123e7bbe842d701fa1cbfe2df - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x6d03360ce4764e862ed81660c1f76cc2711b14b6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xc055f66f228105072315247785c00299d0ce27e8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xcae1d141ab11cef0a415cf0440025e1e5e962e06 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x21b8065d10f73ee2e260e5b47d3344d3ced7596e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x0d4a11d5eeaac28ec3f61d100daf4d40471f1852 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x517f9dd285e75b599234f7221227339478d0fcc8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xa43fe16908251ee70ef74718545e4fe6c5ccec9f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x0af81cd5d9c124b4859d65697a4cd10ee223746a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xca7c2771d248dcbe09eabe0ce57a62e18da178c0 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x09d1d767edf8fa23a64c51fa559e0688e526812f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x7b73644935b8e68019ac6356c40661e1bc315860 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x180efc1349a69390ade25667487a826164c9c6e4 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x9c4fe5ffd9a9fc5678cfbd93aa2d4fd684b67c4c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xa478c2975ab1ea89e8196811f51a7b7ade33eb11 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xbb2b8038a1640196fbe3e38816f3e67cba72d940 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x9ec9367b8c4dd45ec8e7b800b1f719251053ad60 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xc91ef786fbf6d62858262c82c63de45085dea659 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x197d7010147df7b99e9025c724f13723b29313f8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x25647e01bd0967c1b9599fa3521939871d1d0888 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x2f0b1417aa42ebf0b4ca1154212847f6094d708d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x6ada49aeccf6e556bb7a35ef0119cc8ca795294a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x2a6c340bcbb0a79d3deecd3bc5cbc2605ea9259f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xda2d09fbbf8ee4b5051a0e9b562c5fcb4b393b18 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x48d20b3e529fb3dd7d91293f80638df582ab2daa - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x4028daac072e492d34a3afdbef0ba7e35d8b55c4 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xc2eab7d33d3cb97692ecb231a5d0e4a649cb539d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xc5be99a02c6857f9eac67bbce58df5572498f40c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xe4b8583ccb95b25737c016ac88e539d0605949e8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x8dbee21e8586ee356130074aaa789c33159921ca - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x43de4318b6eb91a7cf37975dbb574396a7b5b5c6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x9ff68f61ca5eb0c6606dc517a9d44001e564bb66 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xa29fe6ef9592b5d408cca961d0fb9b1faf497d6d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x1b1137dd16faa651e38a9dfb5d9ffff7767fdf62 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x470e8de2ebaef52014a47cb5e6af86884947f08c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x8fb8e9921922d2ffb529a95d28a0d06d275d7a59 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xd3d2e2692501a5c9ca623199d38826e513033a17 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x97e1fcb93ae7267dbafad23f7b9afaa08264cfd8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xa5e9c917b4b821e4e0a5bbefce078ab6540d6b5e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x2cc846fff0b08fb3bffad71f53a60b4b6e6d6482 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x959873fb4fc11825fba83c80c4c632db1e936e15 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xa7480aafa8ad2af3ce24ac6853f960ae6ac7f0c4 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xc7e6b676bfc73ae40bcc4577f22aab1682c691c6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x570febdf89c07f256c75686caca215289bb11cfc - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x343fd171caf4f0287ae6b87d75a8964dc44516ab - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xcaa004418eb42cdf00cb057b7c9e28f0ffd840a5 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xe3d3551bb608e7665472180a20280630d9e938aa - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xb6b0c651c37ec4ca81c0a128420e02001a57fac2 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x4e34da137f0b317c633838458e0c923a5e088752 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xfe9e7931e55c514c33d489c88582fa36e84bd8e3 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x5281e311734869c64ca60ef047fd87759397efe6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x149148acc3b06b8cc73af3a10e84189243a35925 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x8ef79d6c328c25da633559c20c75f638a4863462 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x0f338ec12d3f7c3d77a4b9fcc1f95f3fb6ad0ea6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x4eaa90264d6a3567228dcb5cfc242200da586437 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x6fe9e9de56356f7edbfcbb29fab7cd69471a4869 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xf420603317a0996a3fce1b1a80993eaef6f7ae1a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x47a90a2d92a8367a91efa1906bfc8c1e05bf10c4 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x41bf5eeae051fbd2e97b76b5f8f0fdcc1a1e526b - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x28df0835942396b7a1b7ae1cd068728e6ddbbafd - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xa3f3664a52f01b42557524bd14556e379daf5669 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x1fd22fa7274bafebdfb1881321709f1219744829 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xe39cfc1a2e51a09ecbd060a24ee4eef5a97697bb - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x06396509195eb9e07c38a016694dc9ff535b128a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x5a1c486edefda2f09d3b349fadc38524f1743826 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x5bf1cf153c102a79d9e18b7fb7c79ba57fa70d0c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x2c3c320d49019d4f9a92352e947c7e5acfe47d68 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x4141325bac36affe9db165e854982230a14e6d48 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x17507bef4c3abc1bc715be723ee1baf571256e05 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x8149b92ea743cc382aada523b68b8834733b9015 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xc98f01bf2141e1140ef8f8cad99d4b021d10718f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x7f9d307973cdabe42769d9712df8ee1cc1a28d10 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x5c87da28a45e5089b762dcbbd86f743d14c54317 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x2cd97604ef77bbcb1fa0cff47545dff8ec7def08 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x7862d9b4be2156b15d54f41ee4ede2d5b0b455e4 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x554548b404213c7efcdbab933f52edfe3c581834 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x63008c5ea4e47f5421e0e1428b1c5043a507d0d0 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x0350ca994791c4b07a5b02b08aaf9d6fc8ab510e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x32776ed4d96ed069a2d812773f0ad8ad9ef83cf8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x84f3ca9b7a1579ff74059bd0e8929424d3fa330e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x5289a8dbf7029ee0b0498a84777ed3941d9acfec - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xb2bc284ab4c953b7f7a06d59c0ceb2de26405f22 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x508acf810857fefa86281499068ad5d19ebce325 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xccdfcd1aac447d5b29980f64b831c532a6a33726 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x4fb87838a29b37598099ef5aa6b3fbeeef987c50 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x515e94dc736b9d8b7d28ecf1cece0aba3d75da97 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xfd6e5b7c30538dff2752058e425ad01a56b831cc - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xcb99fe720124129520f7a09ca3cbef78d58ed934 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xd2f21358c1549be193537b2a4c5dc7f0228ae011 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x93094ed1c907e4bca7eb041cb659da94f7e1b58e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xd37e6ecb991d1a0e7610c89666817665713362a7 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x73234630bd159384c8d43f145407312d64614f43 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xad1ddf00c4ae50573e4dc98e6c5ee93baa04a0c4 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xa765593c821f7df9ad81119509a37961e7ffa6c5 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x9b501a7ad3087d603ceb34424b7b2a6c348ad0b7 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xafebb7cfa1a15fcac4121b609b456cbce3137c20 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x0adaf134ae0c4583b3a38fc3168a83e33162651e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xf9878a5dd55edc120fde01893ea713a4f032229c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x84e47c7f2fe86f6b5efbe14fee46b8bb871b2e05 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xf3e5bec78654049990965f666b0612e116b94fb2 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x33e59edd3214e97cb68450c6d3d6c167de072aba - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x2ca76c7e466e560e0cb11a91269bb953e41254bc - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xbb124e35ab9e85f8d59ba83500e559dc052b9368 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x21b8065d10f73ee2e260e5b47d3344d3ced7596e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x0d4a11d5eeaac28ec3f61d100daf4d40471f1852 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x517f9dd285e75b599234f7221227339478d0fcc8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xa43fe16908251ee70ef74718545e4fe6c5ccec9f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x0af81cd5d9c124b4859d65697a4cd10ee223746a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xca7c2771d248dcbe09eabe0ce57a62e18da178c0 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x09d1d767edf8fa23a64c51fa559e0688e526812f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x7b73644935b8e68019ac6356c40661e1bc315860 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x180efc1349a69390ade25667487a826164c9c6e4 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x9c4fe5ffd9a9fc5678cfbd93aa2d4fd684b67c4c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xa478c2975ab1ea89e8196811f51a7b7ade33eb11 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xbb2b8038a1640196fbe3e38816f3e67cba72d940 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x9ec9367b8c4dd45ec8e7b800b1f719251053ad60 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xc91ef786fbf6d62858262c82c63de45085dea659 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x197d7010147df7b99e9025c724f13723b29313f8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x25647e01bd0967c1b9599fa3521939871d1d0888 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x2f0b1417aa42ebf0b4ca1154212847f6094d708d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x6ada49aeccf6e556bb7a35ef0119cc8ca795294a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x2a6c340bcbb0a79d3deecd3bc5cbc2605ea9259f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xda2d09fbbf8ee4b5051a0e9b562c5fcb4b393b18 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x48d20b3e529fb3dd7d91293f80638df582ab2daa - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x4028daac072e492d34a3afdbef0ba7e35d8b55c4 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xc2eab7d33d3cb97692ecb231a5d0e4a649cb539d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xc5be99a02c6857f9eac67bbce58df5572498f40c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xe4b8583ccb95b25737c016ac88e539d0605949e8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x8dbee21e8586ee356130074aaa789c33159921ca - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x43de4318b6eb91a7cf37975dbb574396a7b5b5c6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x9ff68f61ca5eb0c6606dc517a9d44001e564bb66 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xa29fe6ef9592b5d408cca961d0fb9b1faf497d6d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x1b1137dd16faa651e38a9dfb5d9ffff7767fdf62 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x470e8de2ebaef52014a47cb5e6af86884947f08c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x8fb8e9921922d2ffb529a95d28a0d06d275d7a59 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xd3d2e2692501a5c9ca623199d38826e513033a17 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x97e1fcb93ae7267dbafad23f7b9afaa08264cfd8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xa5e9c917b4b821e4e0a5bbefce078ab6540d6b5e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x2cc846fff0b08fb3bffad71f53a60b4b6e6d6482 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x959873fb4fc11825fba83c80c4c632db1e936e15 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xa7480aafa8ad2af3ce24ac6853f960ae6ac7f0c4 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xc7e6b676bfc73ae40bcc4577f22aab1682c691c6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x570febdf89c07f256c75686caca215289bb11cfc - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x343fd171caf4f0287ae6b87d75a8964dc44516ab - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xcaa004418eb42cdf00cb057b7c9e28f0ffd840a5 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xe3d3551bb608e7665472180a20280630d9e938aa - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xb6b0c651c37ec4ca81c0a128420e02001a57fac2 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x4e34da137f0b317c633838458e0c923a5e088752 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xfe9e7931e55c514c33d489c88582fa36e84bd8e3 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x5281e311734869c64ca60ef047fd87759397efe6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x149148acc3b06b8cc73af3a10e84189243a35925 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x8ef79d6c328c25da633559c20c75f638a4863462 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0xd88d5f9e6c10e6febc9296a454f6c2589b1e8fae - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0xb90fe7da36ac89448e6dfd7f2bb1e90a66659977 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0xbd6313d0796984c578cae6bc5b5e23b27c5540c5 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x1f18cd7d1c7ba0dbe3d9abe0d3ec84ce1ad10066 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x7da99753ff017f1b7afb2c8c0542718dc9f15f21 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x079e7a44f42e9cd2442c3b9536244be634e8f888 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x1c8dafd358d308b880f71edb5170b010b106ca60 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0xbd0f6f34baa3c1329448a69bab90111a20756f01 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x3420720e561f3082f1e514a4545f0f2e0c955a5d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0xea3fb6e3313a2a90757e4ca3d6749efd0107b0b6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0xf130f72f8190f662522774c3367e6e8814f5e219 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x4a46c053bd5c10a959aea258228217b9d3405f3d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0xb83258bf5940c98abf54f26c5a02710bd6b83b2c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x6a209c5329f0a225fa1890d4177823c096016f34 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0xdb24905b1b080f65dedb0ad978aad5c76363d3c6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0xddff2cdad11898b901a661e32e9fa010780263a0 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x72dd8fe09b5b493012e5816068dfc6fb26a2a9e6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x54fc722a66abfb6500a36d8b7b2646129d0e836a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x53b612b32233c80ec439a64325a29766ce95be7f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0xe5edcbe72d1bc223097a1bed1fe6c0e404b4290c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0xb928c37b8bd9754d321dc3d3c6ef374d332fe761 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x2d70cbabf4d8e61d5317b62cbe912935fd94e0fe - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x953e2937f0515c43ca7995e80c84aedcbbb9385e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x84394d80830ae963b599ded7d9149b90059f182f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0xa1777e082fa1746eb78dd9c1fbb515419cf6e538 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x112466c8b6e5abe42c78c47eb1b9d40baa3f943c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x9491d57c5687ab75726423b55ac2d87d1cda2c3f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x978799f1845c00c9a4d9fd2629b9ce18df66e488 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0xdc55d1fd1c04e005051a40bd59c5f95623257bc5 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x34757893070b0fc5de37aaf2844255ff90f7f1e0 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x7faf167615419228f3f7d71d52d840dab154913c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0xa4d7b6a50dd4c55334ca6f175dbc6561f269d264 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x0ed413cefde954d8e5c54d981d7d182b587e98e3 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x524375d0c6a04439128428f400b00eae81a2e9e4 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x4b7a4530d56ff55a4dce089d917ede812e543307 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x84bb5b9bf1b6782c87cfa3e396f2f571c8e49646 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x723292eea7e1576ae482a5c317934054c0199e24 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x9b42940e8184d866aac6595a91f8d8952a59d3b9 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x37622453c614f625d288151101ffe48fd222ced1 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x4a94130b9e8eb0a0959c2c0f1ee9583213773fd9 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x51514b3dc24afc1db95586242b99f0063bea17c5 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0xc130254e9196d48bbd9f91240390a6e8203132e9 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x60ac25da2ada3be14a2a8c04e45b072bed965966 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x4e392a3883a84225260ff857318517eb50e5d128 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0xca0aa06385a42242fe9523cd7015f6d01cd8f6b2 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x3e448c17043ce1481bbe53c0fd19481bad8b98a6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x81060e6bf2a683f208b8799a33c7c09830cabed1 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x463fe9f646b61ccfb43a022bf947075411cd71c7 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x21b8065d10f73ee2e260e5b47d3344d3ced7596e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x0d4a11d5eeaac28ec3f61d100daf4d40471f1852 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x517f9dd285e75b599234f7221227339478d0fcc8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0xa43fe16908251ee70ef74718545e4fe6c5ccec9f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x0af81cd5d9c124b4859d65697a4cd10ee223746a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0xca7c2771d248dcbe09eabe0ce57a62e18da178c0 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x09d1d767edf8fa23a64c51fa559e0688e526812f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x7b73644935b8e68019ac6356c40661e1bc315860 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x180efc1349a69390ade25667487a826164c9c6e4 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x9c4fe5ffd9a9fc5678cfbd93aa2d4fd684b67c4c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0xa478c2975ab1ea89e8196811f51a7b7ade33eb11 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0xbb2b8038a1640196fbe3e38816f3e67cba72d940 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x9ec9367b8c4dd45ec8e7b800b1f719251053ad60 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0xc91ef786fbf6d62858262c82c63de45085dea659 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x197d7010147df7b99e9025c724f13723b29313f8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x25647e01bd0967c1b9599fa3521939871d1d0888 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x2f0b1417aa42ebf0b4ca1154212847f6094d708d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x6ada49aeccf6e556bb7a35ef0119cc8ca795294a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x2a6c340bcbb0a79d3deecd3bc5cbc2605ea9259f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0xda2d09fbbf8ee4b5051a0e9b562c5fcb4b393b18 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x48d20b3e529fb3dd7d91293f80638df582ab2daa - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x4028daac072e492d34a3afdbef0ba7e35d8b55c4 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0xc2eab7d33d3cb97692ecb231a5d0e4a649cb539d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0xc5be99a02c6857f9eac67bbce58df5572498f40c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0xe4b8583ccb95b25737c016ac88e539d0605949e8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x8dbee21e8586ee356130074aaa789c33159921ca - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x43de4318b6eb91a7cf37975dbb574396a7b5b5c6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x9ff68f61ca5eb0c6606dc517a9d44001e564bb66 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0xa29fe6ef9592b5d408cca961d0fb9b1faf497d6d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x1b1137dd16faa651e38a9dfb5d9ffff7767fdf62 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x470e8de2ebaef52014a47cb5e6af86884947f08c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x8fb8e9921922d2ffb529a95d28a0d06d275d7a59 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0xd3d2e2692501a5c9ca623199d38826e513033a17 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x97e1fcb93ae7267dbafad23f7b9afaa08264cfd8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0xa5e9c917b4b821e4e0a5bbefce078ab6540d6b5e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x2cc846fff0b08fb3bffad71f53a60b4b6e6d6482 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x959873fb4fc11825fba83c80c4c632db1e936e15 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0xa7480aafa8ad2af3ce24ac6853f960ae6ac7f0c4 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0xc7e6b676bfc73ae40bcc4577f22aab1682c691c6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x570febdf89c07f256c75686caca215289bb11cfc - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x343fd171caf4f0287ae6b87d75a8964dc44516ab - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0xcaa004418eb42cdf00cb057b7c9e28f0ffd840a5 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0xe3d3551bb608e7665472180a20280630d9e938aa - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0xb6b0c651c37ec4ca81c0a128420e02001a57fac2 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x4e34da137f0b317c633838458e0c923a5e088752 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0xfe9e7931e55c514c33d489c88582fa36e84bd8e3 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x5281e311734869c64ca60ef047fd87759397efe6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x149148acc3b06b8cc73af3a10e84189243a35925 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x8ef79d6c328c25da633559c20c75f638a4863462 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x0f23d49bc92ec52ff591d091b3e16c937034496e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x0f23d49bc92ec52ff591d091b3e16c937034496e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xbf16ef186e715668aa29cef57e2fd7f9d48adfe6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x0f23d49bc92ec52ff591d091b3e16c937034496e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x5645dcb64c059aa11212707fbf4e7f984440a8cf - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x0f23d49bc92ec52ff591d091b3e16c937034496e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x3ad4913fa896391c9822a81d8d869cc0d783bdd7 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x0f23d49bc92ec52ff591d091b3e16c937034496e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x0f23d49bc92ec52ff591d091b3e16c937034496e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x0f23d49bc92ec52ff591d091b3e16c937034496e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x7a415b19932c0105c82fdb6b720bb01b0cc2cae3 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x9b3423373e6e786c9ac367120533abe4ee398373 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x4a25dbdf9629b1782c3e2c7de3bdce41f1c7f801 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xbe80225f09645f172b079394312220637c440a63 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x059615ebf32c946aaab3d44491f78e4f8e97e1d3 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x435664008f38b0650fbc1c9fc971d0a3bc2f1e47 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x4b62fa30fea125e43780dc425c2be5acb4ba743b - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xc3db44adc1fcdfd5671f555236eae49f4a8eea18 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xddd23787a6b80a794d952f5fb036d0b31a8e6aff - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xa86aca6d7c393c06dcdc30473ea3d1b05c358dff - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x1ffec7119e315b15852557f654ae0052f76e6ae1 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x0f027d40c80d8f70f77d3884776531f80b21d20e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x69c66beafb06674db41b22cfc50c34a93b8d82a2 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xeedff72a683058f8ff531e8c98575f920430fdc5 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x811cfb75567a252bea23474e2ccd1286927bfe0a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x2caccf71bdf8fff97c06a46eca29b611b1a74b5e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0xf07a84f0732dfe8eea0d3961bcd8f62c761ff508 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/ethereum/0x8c1c499b1796d7f3c2521ac37186b52de024e58c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xe5cf22ee4988d54141b77050967e1052bd9c7f7a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x7f580f8a02b759c350e6b8340e7c2d4b8162b6a9 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x48b0ab72c2591849e678e7d6f272b75ef9b863f7 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x74d0ae8b8e1fca6039707564704a25ad2ee036b0 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x5969efdde3cf5c0d9a88ae51e47d721096a97203 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xe32efff8f8b5fdc53803405aa3f623f03f8a8767 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xe8629b6a488f366d27dad801d1b5b445199e2ada - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x066b28f0c160935cf285f75ed600967bf8417035 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xddd23787a6b80a794d952f5fb036d0b31a8e6aff - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xa86aca6d7c393c06dcdc30473ea3d1b05c358dff - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x1ffec7119e315b15852557f654ae0052f76e6ae1 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x0f027d40c80d8f70f77d3884776531f80b21d20e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x69c66beafb06674db41b22cfc50c34a93b8d82a2 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xeedff72a683058f8ff531e8c98575f920430fdc5 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x811cfb75567a252bea23474e2ccd1286927bfe0a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x2caccf71bdf8fff97c06a46eca29b611b1a74b5e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0xf07a84f0732dfe8eea0d3961bcd8f62c761ff508 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/arbitrum/0x8c1c499b1796d7f3c2521ac37186b52de024e58c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x146b020399769339509c98b7b353d19130c150ec - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xd28f71e383e93c570d3edfe82ebbceb35ec6c412 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xadab76dd2dca7ae080a796f0ce86170e482afb4a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x0fb07e6d6e1f52c839608e1436d2ea810cf07257 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xddd23787a6b80a794d952f5fb036d0b31a8e6aff - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xa86aca6d7c393c06dcdc30473ea3d1b05c358dff - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x1ffec7119e315b15852557f654ae0052f76e6ae1 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x0f027d40c80d8f70f77d3884776531f80b21d20e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x69c66beafb06674db41b22cfc50c34a93b8d82a2 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xeedff72a683058f8ff531e8c98575f920430fdc5 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x811cfb75567a252bea23474e2ccd1286927bfe0a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x2caccf71bdf8fff97c06a46eca29b611b1a74b5e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0xf07a84f0732dfe8eea0d3961bcd8f62c761ff508 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/optimism/0x8c1c499b1796d7f3c2521ac37186b52de024e58c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x95d2483d2a0fff034004f91c53d649623d993896 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x19c5505638383337d2972ce68b493ad78e315147 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xc143161ed3ed8049bb63d8da42907c08a10e2269 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xc3286373599dd5af2a17a572ebb7561f05f88bec - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xbb98b3d2b18aef63a3178023a920971cf5f29be4 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x647fb01a63de9a551b39c7915693b25e6bcec502 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xa90c1c009dc8292bd04ced30f9b53a5ff7a806a0 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xddd23787a6b80a794d952f5fb036d0b31a8e6aff - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xa86aca6d7c393c06dcdc30473ea3d1b05c358dff - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x1ffec7119e315b15852557f654ae0052f76e6ae1 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x0f027d40c80d8f70f77d3884776531f80b21d20e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x69c66beafb06674db41b22cfc50c34a93b8d82a2 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xeedff72a683058f8ff531e8c98575f920430fdc5 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x811cfb75567a252bea23474e2ccd1286927bfe0a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x2caccf71bdf8fff97c06a46eca29b611b1a74b5e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0xf07a84f0732dfe8eea0d3961bcd8f62c761ff508 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/polygon/0x8c1c499b1796d7f3c2521ac37186b52de024e58c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xfb765ff72a14735550f1d798a5efd1311f2ddee7 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x3537f2a5f99f08f59eb1417073db1fadbebf0c74 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xde8ed0277ee0e84c25756a73ffa7374e4aeadf46 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xd8f3a72d2b2220a5067abe8c38aea57dc2d69a5e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x7ec18abf80e865c6799069df91073335935c4185 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x14b1911dd6b451c2771661ae8cd70637d726c356 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x9ae8084c21752971d867597c07f2673765d949a1 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xcfaf75a3d292c3535ea3acdb16ed2ee58c2bb091 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x8055e6de251e414e8393b20adab096afb3cf8399 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xffec10fe1355c2d8df4f62affcdeffdb04f06569 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xc16454420f100b2e771d8bc4c5b6200068129a34 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x046f405e4ae1d0e786eda4959adadbd417d13ad8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xeccb34691c06c1c9c31ceb2228b22cbd242b5879 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xe22a2dfaaaaec8a7b2b7acb4909eaaa5c5bd6e64 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xe2dda0911e227e73d9fd94745b851c8bc6504610 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x0f082a7870908f8cebbb2cd27a42a9225c19f898 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x69d667281778db0c3bc8177efea3a91ee95c3068 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x30d61bb28a6789f9f49d8c7fb198d63b6aba4b61 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x090f3fd9110621df127c3f9be5c6f58c02f2d5eb - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xd56f086e7b796b313d49f2bc926fac4bdd2a2b0b - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x7eb847a214192aab8fa1b503f4d4c9ddd2a08db6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x81b3bc0ef974c16d71b8614adb8c22ccc045da01 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xc9b44ca4159dbaf5722a3dc8618e9d4b5f39d5b2 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xbeef35a63fc62a3334630d9d3b4db27093d95317 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x3d5d143381916280ff91407febeb52f2b60f33cf - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x68c9325cc268df8b9ed4a06429587f28471b5f84 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xa00cc1fb7ac185222294777c6b23a13c013f07ce - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x77021e63bcbd3c5296b0cdd8a3c3770fb0ea8fa2 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xcc28456d4ff980cee3457ca809a257e52cd9cdb0 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xec0b7e8e44c9d60efd67a89dba1d4a6e02a7a4a0 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x0c8fed5dd65542ca5f0add1acab14c2e470c9110 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xd56da2b74ba826f19015e6b7dd9dae1903e85da1 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x5482c2b11951bbb92b87858242e17abde802b398 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xd95bae63641d822dc591bd4aca7a64e53eac76f9 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x06959273e9a65433de71f5a452d529544e07ddd0 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x24bf2ee2e09477082d1ddf2f0603baa460b3f5f3 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x56d8f846415e08c5e663d89505e79f522d33f947 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x548e923281f372d28a40287d3a2d30dce482fc66 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x9d744d3d905897608d24c1b8c1c7db0d30c36cd4 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xddd23787a6b80a794d952f5fb036d0b31a8e6aff - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xa86aca6d7c393c06dcdc30473ea3d1b05c358dff - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x1ffec7119e315b15852557f654ae0052f76e6ae1 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x0f027d40c80d8f70f77d3884776531f80b21d20e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x69c66beafb06674db41b22cfc50c34a93b8d82a2 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xeedff72a683058f8ff531e8c98575f920430fdc5 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x811cfb75567a252bea23474e2ccd1286927bfe0a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x2caccf71bdf8fff97c06a46eca29b611b1a74b5e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0xf07a84f0732dfe8eea0d3961bcd8f62c761ff508 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/base/0x8c1c499b1796d7f3c2521ac37186b52de024e58c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xab46d39cb398fb3649ecba781180016fef75f50b - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x25048028ad87484b7fce99bc4e22dcb6c3307470 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xdb2177fee5b0ebdc7b8038cb70f3964bb6d14143 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x42d749f736051d8933b118324cded52d1f92bec1 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xb1a1b707b143b911c36e1a0f4f901c5017791aca - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x3319a81a316abd4c086f7048904e31ff86648b38 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x4a978a2d4fb7393063babfb0cee741b8bcd4dd4b - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xea403e36fb592fdfdc342c38e94284ddbb0d2105 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xe3fb01794d6912f0773171e32e723471ee8df061 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x916d7f23ccbb1d10118dcfc6ad5a10b6446ff73e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xddd23787a6b80a794d952f5fb036d0b31a8e6aff - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xa86aca6d7c393c06dcdc30473ea3d1b05c358dff - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x1ffec7119e315b15852557f654ae0052f76e6ae1 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x0f027d40c80d8f70f77d3884776531f80b21d20e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x69c66beafb06674db41b22cfc50c34a93b8d82a2 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xeedff72a683058f8ff531e8c98575f920430fdc5 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x811cfb75567a252bea23474e2ccd1286927bfe0a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x2caccf71bdf8fff97c06a46eca29b611b1a74b5e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0xf07a84f0732dfe8eea0d3961bcd8f62c761ff508 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/bnb/0x8c1c499b1796d7f3c2521ac37186b52de024e58c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x6cde5f5a192fbf3fd84df983aa6dc30dbd9f8fac - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0xd80d28850bebe6208433c298334392bc940b4fc7 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x7f7c4335ccac291ddedcef4429a626c442b627ed - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x628cb3a5a206956423d158009612813b64b19dab - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x116361f4f45e310347b43cd098fdfa459760ea7f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x5dc631ad6c26bea1a59fbf2c2680cf3df43d249f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x1a810e0b6c2dd5629afa2f0c898b9512c6f78846 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0xac1cb6d3d419da9ead0b53e62d6fb4bb53473523 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x0115d04a88990889471a88e85817aac9e961c07b - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0xd3409b7f3f54bb097433d0f4cd31c48ac33e569b - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x493bfc1adb2e60805693197f23132350ffd2a04e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0xcf4f103759770c21f945413781ca787620316988 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0xb135ebde27d366b0d62e579bae4118cb991b820e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0xecbc2f008c20729b9239317408367377c5473812 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x96e0c440d3377c2dfe4f2a82add0b045e46cbe64 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x6f5304c22ac77e228e8af4732ac6677c46e09030 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0xcb037f27eb3952222810966e28e0ceb650c65cd9 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0xddd23787a6b80a794d952f5fb036d0b31a8e6aff - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0xa86aca6d7c393c06dcdc30473ea3d1b05c358dff - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x1ffec7119e315b15852557f654ae0052f76e6ae1 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x0f027d40c80d8f70f77d3884776531f80b21d20e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x69c66beafb06674db41b22cfc50c34a93b8d82a2 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0xeedff72a683058f8ff531e8c98575f920430fdc5 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x811cfb75567a252bea23474e2ccd1286927bfe0a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x2caccf71bdf8fff97c06a46eca29b611b1a74b5e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0xf07a84f0732dfe8eea0d3961bcd8f62c761ff508 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/pools/celo/0x8c1c499b1796d7f3c2521ac37186b52de024e58c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x7baece5d47f1bc5e1953fbe0e9931d54dab6d810 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x83abecf7204d5afc1bea5df734f085f2535a9976 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x4eefe02fce5b53ca33c7717bbd8ad3c9cb0609f1 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xaf996125e98b5804c00ffdb4f7ff386307c99a00 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x7924a818013f39cf800f5589ff1f1f0def54f31f + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xb2eb5849e2606f99fc492e9add0103c667f806d3 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x53c6ca2597711ca7a73b6921faf4031eedf71339 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x4eefe02fce5b53ca33c7717bbd8ad3c9cb0609f1 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xaf996125e98b5804c00ffdb4f7ff386307c99a00 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x7924a818013f39cf800f5589ff1f1f0def54f31f + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xd35937ecd47b04a1474f8569f457fc5ac395921a + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x4eefe02fce5b53ca33c7717bbd8ad3c9cb0609f1 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xaf996125e98b5804c00ffdb4f7ff386307c99a00 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x7924a818013f39cf800f5589ff1f1f0def54f31f + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x6b75f2189f0e11c52e814e09e280eb1a9a8a094a + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xb372b5abdb7c2ab8ad9e614be9835a42d0009153 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xf369277650ad6654f25412ea8bfbd5942733babc + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x4eefe02fce5b53ca33c7717bbd8ad3c9cb0609f1 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xaf996125e98b5804c00ffdb4f7ff386307c99a00 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x7924a818013f39cf800f5589ff1f1f0def54f31f + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x4898cf312fbff8814cab80a8d7f6ee5ad0dc73fb + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x5e78afc6c804d4382bede3a0712d210e657e9b4f + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x86b211ca7915a0c8d4659dd98242d9e801d88ab4 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xb637f7c82fd774c280e23cebc725e7cd807c66d0 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xd249c43faabc58d6dd4b0a4de598b5a956c5d8d7 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x1fbae785ce68b79f7ed4f7b27c3af3ef0e0bc3d4 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x3c1376fb8487da57d4ffb263d9d01b578c7b586b + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x7b24bed19856f4bb1d4c0421cfb328026cd936bd + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x7cf887a863d81e6a483ee947dee05cb51914923c + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x588c8cf031809486f015908864ee8699b44017e4 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x3987d38a4ff8520a8ef6bcc6f98d6da8bcd69b89 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x4eefe02fce5b53ca33c7717bbd8ad3c9cb0609f1 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xaf996125e98b5804c00ffdb4f7ff386307c99a00 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x7924a818013f39cf800f5589ff1f1f0def54f31f + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0xde67d05242b18af00b28678db34feec883cc9cd6 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x4eefe02fce5b53ca33c7717bbd8ad3c9cb0609f1 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0xaf996125e98b5804c00ffdb4f7ff386307c99a00 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x7924a818013f39cf800f5589ff1f1f0def54f31f + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x4a5a8b0108f446df7c1c8a459fcfb54e844b7343 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xf6ba006abf768ab2d1b5bba2d22d9f13eb1269d4 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x4eefe02fce5b53ca33c7717bbd8ad3c9cb0609f1 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xaf996125e98b5804c00ffdb4f7ff386307c99a00 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x7924a818013f39cf800f5589ff1f1f0def54f31f + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x92c2fc5f306405eab0ff0958f6d85d7f8892cf4d + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xcbe856765eeec3fdc505ddebf9dc612da995e593 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x92c2fc5f306405eab0ff0958f6d85d7f8892cf4d + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xcbe856765eeec3fdc505ddebf9dc612da995e593 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xc1738d90e2e26c35784a0d3e3d8a9f795074bca4 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x92c2fc5f306405eab0ff0958f6d85d7f8892cf4d + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xcbe856765eeec3fdc505ddebf9dc612da995e593 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xda908c0bf14ad0b61ea5ebe671ac59b2ce091cbf + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x254aa3a898071d6a2da0db11da73b02b4646078f + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x92c2fc5f306405eab0ff0958f6d85d7f8892cf4d + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xcbe856765eeec3fdc505ddebf9dc612da995e593 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x41824081f2e7beb83048bf52465ddd7c8e471da2 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xa0c2ce1723b3939f47ad01a293292f2f75dc629d + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xc42442f6402b68626e791a447d87b35cb1c6236e + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x84537db6f6aaa2afdb71f325d14b9f5f7825bef1 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x13933689ed2c6c66e83aed64336df14896efb7e2 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x92c2fc5f306405eab0ff0958f6d85d7f8892cf4d + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xcbe856765eeec3fdc505ddebf9dc612da995e593 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x039df62583ddc1c5fda75db152b87113d863b6d6 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x92c2fc5f306405eab0ff0958f6d85d7f8892cf4d + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0xcbe856765eeec3fdc505ddebf9dc612da995e593 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x92c2fc5f306405eab0ff0958f6d85d7f8892cf4d + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xcbe856765eeec3fdc505ddebf9dc612da995e593 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xc39e83fe4e412a885c0577c08eb53bdb6548004a + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xdbac78be00503d10ae0074e5e5873a61fc56647c + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xc1cd3d0913f4633b43fcddbcd7342bc9b71c676f + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x6c4c7f46d9d4ef6bc5c9e155f011ad19fc4ef321 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xb2c86ff752f18499b70e8f642b3421405d50d6e9 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x16588709ca8f7b84829b43cc1c5cb7e84a321b16 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xd0a4c8a1a14530c7c9efdad0ba37e8cf4204d230 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xf92f2e3fca01491baba0975264362cc38b1cab7b + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x3e6e23198679419cd73bb6376518dcc5168c8260 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x531b6a4b3f962208ea8ed5268c642c84bb29be0b + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x553e9c493678d8606d6a5ba284643db2110df823 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xe3170d65018882a336743a9c396c52ea4b9c5563 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x1385fc1fe0418ea0b4fcf7adc61fc7535ab7f80d + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x5cd0ad98ba6288ed7819246a1ebc0386c32c314b + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xe945683b3462d2603a18bdfbb19261c6a4f03ad1 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xa1bf0e900fb272089c9fd299ea14bfccb1d1c2c0 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xe46935ae80e05cdebd4a4008b6ccaa36d2845370 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x3041cbd36888becc7bbcbc0045e3b1f144466f5f + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x0ad1e922e764df5ab6d636f5d21ecc2e41e827f0 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xe945683b3462d2603a18bdfbb19261c6a4f03ad1 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xa1bf0e900fb272089c9fd299ea14bfccb1d1c2c0 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xe46935ae80e05cdebd4a4008b6ccaa36d2845370 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x3041cbd36888becc7bbcbc0045e3b1f144466f5f + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x6b3a3d6ed64faf933a7a4b1bd44b2efba47614ac + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x4ce4a1a593ea9f2e6b2c05016a00a2d300c9ffd8 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x0843e0f56b9e7fdc4fb95fabba22a01ef4088f41 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x8323d063b1d12acce4742f1e3ed9bc46d71f4222 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xe945683b3462d2603a18bdfbb19261c6a4f03ad1 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xa1bf0e900fb272089c9fd299ea14bfccb1d1c2c0 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xe46935ae80e05cdebd4a4008b6ccaa36d2845370 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x3041cbd36888becc7bbcbc0045e3b1f144466f5f + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xe30e4dfdbb10949c27501922f845e20cfa579f09 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x7e02ae3f794ebade542c92973eb1c46d7e2e935d + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xfa22d298e3b0bc1752e5ef2849cec1149d596674 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x8066ee17156e4184d69277e26fa8cbca3a845edf + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x418de8e0ab58abfe916a47821a055c59b9502deb + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xfb9caae5a5c0ab91f68542124c05d1efbb97d151 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xb68606a75b117906e06caa0755896ad2b3dd0272 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x6e33c0f5e16b45114679eac217e0c0138cefcd2e + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xd64fb39a5681908ad488b487d65f5d8479cb235c + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xe945683b3462d2603a18bdfbb19261c6a4f03ad1 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xa1bf0e900fb272089c9fd299ea14bfccb1d1c2c0 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xe46935ae80e05cdebd4a4008b6ccaa36d2845370 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x3041cbd36888becc7bbcbc0045e3b1f144466f5f + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x0217fc17c642d29b890bcf888e21be2378493e01 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x099d23a43da5a8a9282266dbefeaaef958150300 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xd92e0767473d1e3ff11ac036f2b1db90ad0ae55f + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xe945683b3462d2603a18bdfbb19261c6a4f03ad1 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xa1bf0e900fb272089c9fd299ea14bfccb1d1c2c0 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xe46935ae80e05cdebd4a4008b6ccaa36d2845370 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x3041cbd36888becc7bbcbc0045e3b1f144466f5f + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x40c547e7fd88f60d94788953b83d9342d8d133c6 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x397433498c7befde4b4049b98a7ff081a2c17387 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0xf9be03505869d719ba194757943575ed2af001f2 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x18c40bb9281a07627ff25cea45b7511f68fd0076 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x270d89e983d9821a418bf193684736414fab78c5 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0xb125aa15ad943d96e813e4a06d0c34716f897e26 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x813c0decbb1097fff46d0ed6a39fb5f6a83043f4 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x9a7ac628ba9f330341486380af729c8975388959 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0xf2c9339945bff71dd0bffd3c142164112cd05dc6 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x12a4619c0bd9710732fbc458e9baa73df6c3d35f + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x96530dac7817f186390b64ba63d13becd079b28d + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x18fc1e95adb68b556212ebbad777f3fbb644db98 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0xabbeb324b090550ca6d15ec71019915813f54f90 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x86d708404d0db1d97843e66d4ed6b86d11be705b + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0xbfbba3de6a260c8374f8299c38898312c2d6e9a6 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0xe945683b3462d2603a18bdfbb19261c6a4f03ad1 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0xa1bf0e900fb272089c9fd299ea14bfccb1d1c2c0 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0xe46935ae80e05cdebd4a4008b6ccaa36d2845370 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x3041cbd36888becc7bbcbc0045e3b1f144466f5f + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xe945683b3462d2603a18bdfbb19261c6a4f03ad1 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xa1bf0e900fb272089c9fd299ea14bfccb1d1c2c0 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xe46935ae80e05cdebd4a4008b6ccaa36d2845370 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x3041cbd36888becc7bbcbc0045e3b1f144466f5f + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xd31d41dffa3589bb0c0183e46a1eed983a5e5978 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x391e8501b626c623d39474afca6f9e46c2686649 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xd0fc8ba7e267f2bc56044a7715a489d851dc6d78 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x4fd47e5102dfbf95541f64ed6fe13d4ed26d2546 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xe9033c0011f35547fa90d3f8a6ad4b666a590759 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x0c3561d3b72e17378d99684414aa8669daeb8bd0 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x14653ce9f406ba7f35a7ffa43c81fa7ecd99c788 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x3204e9734a56a4d7c6f4f5822e14182d9d1a43c4 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x43faefd4c0c25e969ac211cd97a4a51e52c729b7 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xa652ab3be697c7a01fbdce4d73f8e8acd990251c + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x29962083891241aad61ad97bae46d032c9c0c55c + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x26bf3601b77be9c31b13b22ebca02914db9c7468 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x0d2edd335982f56662d772b93d86901eb9bd2ff9 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xbaed273edd493930711fe88690ebd1f30f7f55ab + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x16033643947bf4d8a1ae37b055edf57cb183106a + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xf59abf32c1e8c5d2c6e3faa2131533bbcd466194 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x0312187403bf72b8d2d80729894d6ac3300bd63f + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x416fdbc4fb8d4d1f48d0d3778c59dfa5352e9b15 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x90908e414d3525e33733d320798b5681508255ea + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x5918aca9ae924e6eaaa3d293bb92bdec9ab79338 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x8270e64d22cf13e92c641c4006408c7d7e3ff341 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x16503510c58da73486950b72a12ead3d1d8355dd + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x90908e414d3525e33733d320798b5681508255ea + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x7505159f644ddc5eae21c119e328d0d5bee574b0 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xe870bfe4aacb6e234b645e535d26c53790d50e78 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x2e2d190ad4e0d7be9569baebd4d33298379b0502 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x90908e414d3525e33733d320798b5681508255ea + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xb834093d7e46f7644be45e77281394d31003e866 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xb5a1fd804342cfb679bd8ada75718bc3ec43097e + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x90908e414d3525e33733d320798b5681508255ea + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x9e71e2b14d7e6d30811628ab0965f28e4e2edbce + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xa011da4a0c9261ecf4694bf73a74d113aa261133 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x7ab922c1bfdf7df977c7531c5782074d866f3adc + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xe2d2050430e341a8f3988e2726e44d9370f8cd3a + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xed66ba3ea44425805a085b1ca80d00467b055b38 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x40dade19adc198125ec237a2c48b3408568b2f81 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x166bc40da621d3cb978e24334f844b84ddef25f8 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x76bf0abd20f1e0155ce40a62615a90a709a6c3d8 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x90908e414d3525e33733d320798b5681508255ea + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x6948d6c8532c6b0006cb67c6fb9c399792c8ac91 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x90908e414d3525e33733d320798b5681508255ea + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x4e40cf4a7d8724e5adc2b791bbf9451d1e260b93 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x90908e414d3525e33733d320798b5681508255ea + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xc0067d751fb1172dbab1fa003efe214ee8f419b6 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xc3d7aa944105d3fafe07fc1822102449c916a8d0 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xd6b4cce96ddf8aab2e5750983af9a901f17fbc36 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x4cef551255ec96d89fec975446301b5c4e164c59 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xc0067d751fb1172dbab1fa003efe214ee8f419b6 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xc3d7aa944105d3fafe07fc1822102449c916a8d0 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xdd0c6bae8ad5998c358b823df15a2a4181da1b80 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xc0067d751fb1172dbab1fa003efe214ee8f419b6 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xc3d7aa944105d3fafe07fc1822102449c916a8d0 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xc0067d751fb1172dbab1fa003efe214ee8f419b6 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xc3d7aa944105d3fafe07fc1822102449c916a8d0 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x5e6ff2fa4ca244b6b33c7286d368120822eacc11 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x98efd62b4bfbde6393b18b063c506ce5a77f4810 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x3c5096df639262db0a6cd0172f08709d4161094b + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xae31f0e673fc5f33cfc0e9abb426d8051404a7c5 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xc0067d751fb1172dbab1fa003efe214ee8f419b6 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xc3d7aa944105d3fafe07fc1822102449c916a8d0 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0xc0067d751fb1172dbab1fa003efe214ee8f419b6 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0xc3d7aa944105d3fafe07fc1822102449c916a8d0 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xd10456ce05b9af05c8eede0f93ea8aa80a0daa2f + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x065c22a16f6531706681fabbc8df135fe6eb1c2e + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x8ab8d851c6b31d8a4d42fd7d3e47b20861b025f2 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xc0067d751fb1172dbab1fa003efe214ee8f419b6 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xc3d7aa944105d3fafe07fc1822102449c916a8d0 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x2982d3295a0e1a99e6e88ece0e93ffdfc5c761ae + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xc593fe9193b745447e86b45ea0bf62565ee030cc + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x88051b0eea095007d3bef21ab287be961f3d8598 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xaf21b0ec0197e63a5c6cc30c8e947eb8165c6212 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x9c84f58bb51fabd18698efe95f5bab4f33e96e8f + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xb31273fd2dfc05e6fd91a3b8a2a681aeb0fbcf48 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xaf7b48ae2f4773fd44f9208cca3db5ae7bfa7e37 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xc2125a452115ff5a300cc2a6ffae99637f6e329d + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xb08a8794a5d3ccca3725d92964696858d3201909 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xaf21b0ec0197e63a5c6cc30c8e947eb8165c6212 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x9c84f58bb51fabd18698efe95f5bab4f33e96e8f + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xae99efe6b04bbe5b8b4ad567946fb84b35681abb + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xaf21b0ec0197e63a5c6cc30c8e947eb8165c6212 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x9c84f58bb51fabd18698efe95f5bab4f33e96e8f + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xaf21b0ec0197e63a5c6cc30c8e947eb8165c6212 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x9c84f58bb51fabd18698efe95f5bab4f33e96e8f + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x6696710b8e3dc0d844c8b9244767962a4a61ad97 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xcde77ef185a8f886d03b109573cc1dcdcf3cf1f8 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xaf21b0ec0197e63a5c6cc30c8e947eb8165c6212 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x9c84f58bb51fabd18698efe95f5bab4f33e96e8f + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x35f5387decce5a234da1a32ca3c9e338a48bcf37 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x4178dd7eb2eb983ba7f7e41648cf91db6be20190 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0xaf21b0ec0197e63a5c6cc30c8e947eb8165c6212 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x9c84f58bb51fabd18698efe95f5bab4f33e96e8f + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xb6c8f9490314394cfc6edacb8717bfdc1eb8dab5 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x1625fe58cdb3726e5841fb2bb367dde9aaa009b3 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xb1ed164c736909ba7ddbc1feb7ced4eaad854a87 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x95faa9a91cd6c1c018e4b1a6fc4c89d4f1695e5d + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xa143ccf73c25eec6f38bd1b741043ebea228b8e9 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x2e067e0eab7fd31c01473c0f56f3295afb82e461 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xbc83c60e853398d263c1d88899cf5a8b408f9654 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xaf21b0ec0197e63a5c6cc30c8e947eb8165c6212 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x9c84f58bb51fabd18698efe95f5bab4f33e96e8f + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x202a6012894ae5c288ea824cbc8a9bfb26a49b93 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x744159757cac173a7a3ecf5e97adb10d1a725377 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x127452f3f9cdc0389b0bf59ce6131aa3bd763598 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x5ced44f03ff443bbe14d8ea23bc24425fb89e3ed + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x5ced44f03ff443bbe14d8ea23bc24425fb89e3ed + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x2264ba9dc0b257c69eeae7782e8ff608cc65d6a7 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x5ced44f03ff443bbe14d8ea23bc24425fb89e3ed + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x00a59c2d0f0f4837028d47a391decbffc1e10608 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x5ced44f03ff443bbe14d8ea23bc24425fb89e3ed + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xad6e8f6a34087bddfb03815e2c10e4f7bfd4395b + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xd5bb156cb73bfca62f68dc3dff7e5ec4e305b861 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xc0d8f259578c985947a050802fb4857261af0bf3 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x5ced44f03ff443bbe14d8ea23bc24425fb89e3ed + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x74f7a360eb36a46b675ea932ea07094a3ace441f + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x626761cc5b9fafe4696bf8def4aa015576bb4bef + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x5ced44f03ff443bbe14d8ea23bc24425fb89e3ed + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xc767c0b2e2e56c455fd29f9ee9b6e6f035c71ed4 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x625cb959213d18a9853973c2220df7287f1e5b7d + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x5ced44f03ff443bbe14d8ea23bc24425fb89e3ed + 2024-07-10T19:43:34.135Z 0.8 \ No newline at end of file diff --git a/apps/web/public/tokens-sitemap.xml b/apps/web/public/tokens-sitemap.xml index e727529afef..24e9f6d35e0 100644 --- a/apps/web/public/tokens-sitemap.xml +++ b/apps/web/public/tokens-sitemap.xml @@ -2,3182 +2,5187 @@ https://app.uniswap.org/explore/tokens/ethereum/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xdac17f958d2ee523a2206206994597c13d831ec7 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x2260fac5e5542a773aa44fbcfedf7c193bc2c599 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x6982508145454ce325ddbe47a25d4ec3d2311933 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x6b175474e89094c44da98b954eedeac495271d0f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x6123b0049f904d730db3c36a31167d9d4121fa6b - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x1f9840a85d5af5bf1d1762f925bdaddc4201f984 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xcf0c122c6b73ff809c693db761e7baebe62b6a2e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xfaba6f8e4a5e8ab82f62fe7c39859fa577269be3 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x58cb30368ceb2d194740b144eab4c2da8a917dcb - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x4c9edd5852cd905f086c759e8383e09bff1e68b3 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xaaee1a9723aadb7afa2810263653a34ba2c21c7a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x514910771af9ca656af840dff83e8264ecf986ca - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x5b7533812759b45c2b44c19e320ba2cd2681b542 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xae78736cd615f374d3085123a210448e74fc6393 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xb9f599ce614feb2e1bbe58f180f370d05b39344e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xd5f7838f5c461feff7fe49ea5ebaf7728bb0adfa - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xd31a59c85ae9d8edefec411d448f90841571b89c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x6a7eff1e2c355ad6eb91bebb5ded49257f3fed98 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x576e2bed8f7b46d34016198911cdf9886f78bea7 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x1258d60b224c0c5cd888d37bbf31aa5fcfb7e870 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x62d0a8458ed7719fdaf978fe5929c6d342b0bfce - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x77e06c9eccf2e797fd462a92b6d7642ef85b0a44 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x24fcfc492c1393274b6bcd568ac9e225bec93584 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x27702a26126e0b3702af63ee09ac4d1a084ef628 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xd46ba6d942050d489dbd938a2c909a5d5039a161 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xbe9895146f7af43049ca1c1ae358b0541ea49704 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x72f713d11480dcf08b37e1898670e736688d218d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x0001a500a6b18995b03f44bb040a5ffc28e45cb0 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x9e9fbde7c7a83c43913bddc8779158f1368f0413 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x5f98805a4e8be255a32880fdec7f6728c6568ba0 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x2b591e99afe9f32eaa6214f7b7629768c40eeb39 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x1ae7e1d0ce06364ced9ad58225a1705b3e5db92b - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x046eee2cc3188071c02bfc1745a6b17c656e3f3d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x84018071282d4b2996272659d9c01cb08dd7327f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x12970e6868f88f6557b76120662c1b3e50a646bf - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xaea46a60368a7bd060eec7df8cba43b7ef41ad85 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x6de037ef9ad2725eb40118bb1702ebb27e4aeb24 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xc01154b4ccb518232d6bbfc9b9e6c5068b766f82 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x5a98fcbea516cf06857215779fd812ca3bef1b32 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x102c776ddb30c754ded4fdcc77a19230a60d4e4f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x72e4f9f808c49a2a61de9c5896298920dc4eeea9 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x467719ad09025fcc6cf6f8311755809d45a5e5f3 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xf19308f923582a6f7c465e5ce7a9dc1bec6665b1 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x710287d1d39dcf62094a83ebb3e736e79400068a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xf951e335afb289353dc249e82926178eac7ded78 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xf017d3690346eb8234b85f74cee5e15821fee1f4 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x8c282c35b5e1088bb208991c151182a782637699 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xeaa63125dd63f10874f99cdbbb18410e7fc79dd3 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xde342a3e269056fc3305f9e315f4c40d917ba521 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x2dff88a56767223a5529ea5960da7a3f5f766406 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x626e8036deb333b408be468f951bdb42433cbf18 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xdd66781d0e9a08d4fbb5ec7bac80b691be27f21d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xb23d80f5fefcddaa212212f028021b41ded428cf - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xbaac2b4491727d78d2b78815144570b9f2fe8899 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xf8ebf4849f1fa4faf0dff2106a173d3a6cb2eb3a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xb90b2a35c65dbc466b04240097ca756ad2005295 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x1614f18fc94f47967a3fbe5ffcd46d4e7da3d787 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xf1df7305e4bab3885cab5b1e4dfc338452a67891 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x91fbb2503ac69702061f1ac6885759fc853e6eae - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xa9e8acf069c58aec8825542845fd754e41a9489a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x2c95d751da37a5c1d9c5a7fd465c1d50f3d96160 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xe453c3409f8ad2b1fe1ed08e189634d359705a5b - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x89d584a1edb3a70b3b07963f9a3ea5399e38b136 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x4507cef57c46789ef8d1a19ea45f4216bae2b528 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xd1d2eb1b1e90b638588728b4130137d262c87cae - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xe92344b4edf545f3209094b192e46600a19e7c2d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x8a0a9b663693a22235b896f70a229c4a22597623 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x1bbe973bef3a977fc51cbed703e8ffdefe001fed - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xa41d2f8ee4f47d3b860a149765a7df8c3287b7f0 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x761d38e5ddf6ccf6cf7c55759d5210750b5d60f3 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xc18360217d8f7ab5e7c516566761ea12ce7f9d72 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xe28b3b32b6c345a34ff64674606124dd5aceca30 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x168e209d7b2f58f1f24b8ae7b7d35e662bbf11cc - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xb131f4a55907b10d1f0a50d8ab8fa09ec342cd74 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x3472a5a71965499acd81997a54bba8d852c6e53d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x7dd9c5cba05e151c895fde1cf355c9a1d5da6429 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x19efa7d0fc88ffe461d1091f8cbe56dc2708a84f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x14fee680690900ba0cccfc76ad70fd1b95d10e16 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x3c3a81e81dc49a522a592e7622a7e711c06bf354 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xa1290d69c65a6fe4df752f95823fae25cb99e5a7 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x92f419fb7a750aed295b0ddf536276bf5a40124f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x2c06ba9e7f0daccbc1f6a33ea67e85bb68fbee3a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x3d658390460295fb963f54dc0899cfb1c30776df - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x8e870d67f660d95d5be530380d0ec0bd388289e1 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x853d955acef822db058eb8505911ed77f175b99e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x1294f4183763743c7c9519bec51773fb3acd78fd - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x4e15361fd6b4bb609fa63c81a2be19d873717870 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x695d38eb4e57e0f137e36df7c1f0f2635981246b - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x40a7df3df8b56147b781353d379cb960120211d7 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xaaef88cea01475125522e117bfe45cf32044e238 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x163f8c2467924be0ae7b5347228cabf260318753 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x30672ae2680c319ec1028b69670a4a786baa0f35 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xc944e90c64b2c07662a292be6244bdf05cda44a7 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x15e6e0d4ebeac120f9a97e71faa6a0235b85ed12 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x7d225c4cc612e61d26523b099b0718d03152edef - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x82af49447d8a07e3bd95bd0d56f35241523fbab1 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xaf88d065e77c8cc2239327c5edb3a432268e5831 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xff970a61a04b1ca14834a43f5de4533ebddb5cc8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x912ce59144191c1204e64559fe8253a0e49e6548 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x2f2a2543b76a4166549f7aab2e75bef0aefc5b0f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x5979d7b546e38e414f7e9822514be443a4800529 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x35751007a407ca6feffe80b3cb397736d2cf4dbe - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xda10009cbd5d07dd0cecc66161fc93d7c9000da1 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xeb466342c4d449bc9f53a865d5cb90586f405215 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xfc5a1a6eb076a2c7ad06ed22c90d7e710e35ad0a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x0c880f6761f1af8d9aa9c466984b80dab9a8c9e8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xf97f4df75117a78c1a5a0dbb814af92458539fb4 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x9623063377ad1b27544c965ccd7342f7ea7e88c7 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x539bde0d7dbd336b79148aa742883198bbf60342 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x3082cc23568ea640225c2467653db90e9250aaa0 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x18c11fd286c5ec11c3b683caa813b77f5163a122 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x289ba1701c2f088cf0faf8b3705246331cb8a839 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x4cb9a7ae498cedcbb5eae9f25736ae7d428c9d66 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x00cbcf7b3d37844e44b888bc747bdd75fcf4e555 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xfa7f8980b0f1e64a2062791cc3b0871572f1f7f0 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xd79bb960dc8a206806c3a428b31bca49934d18d7 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x3096e7bfd0878cc65be71f8899bc4cfb57187ba3 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x13ad51ed4f1b7e9dc168d8a00cb3f4ddd85efa60 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x4e352cf164e64adcbad318c3a1e222e9eba4ce42 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x11cdb42b0eb46d95f990bedd4695a6e3fa034978 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xba5ddd1f9d7f570dc94a51479a000e3bce967196 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xc8ccbd97b96834b976c995a67bf46e5754e2c48e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xd07d35368e04a839dee335e213302b21ef14bb4a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x323665443cef804a3b5206103304bd4872ea4253 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x83d6c8c06ac276465e4c92e7ac8c23740f435140 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x87aaffdf26c6885f6010219208d5b161ec7609c0 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x1b8d516e2146d7a32aca0fcbf9482db85fd42c3a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xafccb724e3aec1657fc9514e3e53a0e71e80622d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x4425742f1ec8d98779690b5a3a6276db85ddc01a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xec70dcb4a1efa46b8f2d97c310c9c4790ba5ffa8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x3419875b4d3bca7f3fdda2db7a476a79fd31b4fe - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x3b60ff35d3f7f62d636b067dd0dc0dfdad670e4e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x58b9cb810a68a7f3e1e4f8cb45d1b9b3c79705e8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xfa5ed56a203466cbbc2430a43c66b9d8723528e7 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x95146881b86b3ee99e63705ec87afe29fcc044d9 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x088cd8f5ef3652623c22d48b1605dcfe860cd704 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xbfd5206962267c7b4b4a8b3d76ac2e1b2a5c4d5e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x6daf586b7370b14163171544fca24abcc0862ac5 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x9d2f299715d94d8a7e6f5eaa8e654e8c74a988a7 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x580e933d90091b9ce380740e3a4a39c67eb85b4c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x655a6beebf2361a19549a99486ff65f709bd2646 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x9e64d3b9e8ec387a9a58ced80b71ed815f8d82b5 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x2297aebd383787a160dd0d9f71508148769342e3 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x6694340fc020c5e6b96567843da2df01b2ce1eb6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x772598e9e62155d7fdfe65fdf01eb5a53a8465be - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x431402e8b9de9aa016c743880e04e517074d8cec - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xd74f5255d557944cf7dd0e45ff521520002d5748 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x6fd58f5a2f3468e35feb098b5f59f04157002407 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x561877b6b3dd7651313794e5f2894b2f18be0766 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xf9ca0ec182a94f6231df9b14bd147ef7fb9fa17c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xd77b108d4f6cefaa0cae9506a934e825becca46e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xd56734d7f9979dd94fae3d67c7e928234e71cd4c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xf1264873436a0771e440e2b28072fafcc5eebd01 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x5575552988a3a80504bbaeb1311674fcfd40ad4b - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x0341c0c0ec423328621788d4854119b97f44e391 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x764bfc309090e7f93edce53e5befa374cdcb7b8e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xaaa6c1e32c55a7bfa8066a6fae9b42650f262418 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x9e20461bc2c4c980f62f1b279d71734207a6a356 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x7fb7ede54259cb3d4e1eaf230c7e2b1ffc951e9a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x3a18dcc9745edcd1ef33ecb93b0b6eba5671e7ca - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x000000000026839b3f4181f2cf69336af6153b99 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x8b0e6f19ee57089f7649a455d89d7bc6314d04e8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x31c91d8fb96bff40955dd2dbc909b36e8b104dde - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x25d887ce7a35172c62febfd67a1856f20faebb00 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xd4d42f0b6def4ce0383636770ef773390d85c61a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xf8388c2b6edf00e2e27eef5200b1befb24ce141d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x619c82392cb6e41778b7d088860fea8447941f4c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x94025780a1ab58868d9b2dbbb775f44b32e8e6e5 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xad4b9c1fbf4923061814dd9d5732eb703faa53d4 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xd7a892f28dedc74e6b7b33f93be08abfc394a360 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x3269a3c00ab86c753856fd135d97b87facb0d848 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x4568ca00299819998501914690d6010ae48a59ba - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x21e60ee73f17ac0a411ae5d690f908c3ed66fe12 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xd3188e0df68559c0b63361f6160c57ad88b239d8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x2b41806cbf1ffb3d9e31a9ece6b738bf9d6f645f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xf19547f9ed24aa66b03c3a552d181ae334fbb8db - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x35e6a59f786d9266c7961ea28c7b768b33959cbb - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x59a729658e9245b0cf1f8cb9fb37945d2b06ea27 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xb56c29413af8778977093b9b4947efeea7136c36 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x43ab8f7d2a8dd4102ccea6b438f6d747b1b9f034 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x1d987200df3b744cfa9c14f713f5334cb4bc4d5d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x3404149e9ee6f17fb41db1ce593ee48fbdcd9506 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x080f6aed32fc474dd5717105dba5ea57268f46eb - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xb5a628803ee72d82098d4bcaf29a42e63531b441 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x1622bf67e6e5747b81866fe0b85178a93c7f86e3 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x7dd747d63b094971e6638313a6a2685e80c7fb2e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xa2f9ecf83a48b86265ff5fd36cdbaaa1f349916c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x17a8541b82bf67e10b0874284b4ae66858cb1fd5 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xbcd4d5ac29e06e4973a1ddcd782cd035d04bc0b7 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x42069d11a2cc72388a2e06210921e839cfbd3280 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xbbea044f9e7c0520195e49ad1e561572e7e1b948 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xe85b662fe97e8562f4099d8a1d5a92d4b453bf30 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x3d9907f9a368ad0a51be60f7da3b97cf940982d8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x4e51ac49bc5e2d87e0ef713e9e5ab2d71ef4f336 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x4200000000000000000000000000000000000006 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x7f5c764cbc14f9669b88837ca1490cca17c31607 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x4200000000000000000000000000000000000042 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x0b2c639c533813f4aa9d7837caf62653d097ff85 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x1f32b1c2345538c0c6f582fcb022739c4a194ebb - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x68f180fcce6836688e9084f035309e29bf0a2095 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x94b008aa00579c1307b0ef2c499ad98a8ce58e58 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0xda10009cbd5d07dd0cecc66161fc93d7c9000da1 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0xdc6ff44d5d932cbd77b52e5612ba0529dc6226f1 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x8c6f28f2f1a3c87f0f938b96d27520d9751ec8d9 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x8700daec35af8ff88c16bdf0418774cb3d7599b4 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x920cf626a271321c151d027030d5d08af699456b - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x6c84a8f1c29108f47a79964b5fe888d4f4d0de40 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x9e1028f5f1d5ede59748ffcee5532509976840e0 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0xeb466342c4d449bc9f53a865d5cb90586f405215 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x350a791bfc2c21f9ed5d10980dad2e2638ffa7f6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x17aabf6838a6303fc6e9c5a227dc1eb6d95c829a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0xf467c7d5a4a9c4687ffc7986ac6ad5a4c81e1404 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x76fb31fb4af56892a25e32cfc43de717950c9278 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0xc5b001dc33727f8f26880b184090d3e252470d45 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x9560e827af36c94d2ac33a39bce1fe78631088db - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x9bcef72be871e61ed4fbbc7630889bee758eb81d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x50c5725949a6f0c72e6c4a641f24049a917db0cb - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0xf98dcd95217e15e05d8638da4c91125e59590b07 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x4b03afc91295ed778320c2824bad5eb5a1d852dd - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0xc40f949f8a4e094d1b49a23ea9241d289b7b2819 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x323665443cef804a3b5206103304bd4872ea4253 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x50bce64397c75488465253c0a034b8097fea6578 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x296f55f8fb28e498b858d0bcda06d955b2cb3f97 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x2598c30330d5771ae9f983979209486ae26de875 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x0994206dfe8de6ec6920ff4d779b0d950605fb53 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0xc3248a1bd9d72fa3da6e6ba701e58cbf818354eb - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x6fd9d7ad17242c41f7131d257212c54a0e816691 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x14778860e937f509e651192a90589de711fb88a9 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0xdfa46478f9e5ea86d57387849598dbfb2e964b02 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x9b88d293b7a791e40d36a39765ffd5a1b9b5c349 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x3eb398fec5f7327c6b15099a9681d9568ded2e82 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x217d47011b23bb961eb6d93ca9945b7501a5bb11 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0xbfd5206962267c7b4b4a8b3d76ac2e1b2a5c4d5e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x1cef2d62af4cd26673c7416957cc4ec619a696a7 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x9fd22a17b4a96da3f83797d122172c450381fb88 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0xaddb6a0412de1ba0f936dcaeb8aaa24578dcf3b2 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x2791bca1f2de4661ed88a30c99a7a9449aa84174 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x7ceb23fd6bc0add59e62ac25578270cff1b9f619 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x3c499c542cef5e3811e1192ce70d8cc03d5c3359 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x1bfd67037b42cf73acf2047067bd4f2c47d9bfd6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xc2132d05d31c914a87c6611c10748aeb04b58e8f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x53e0bca35ec356bd5dddfebbd1fc0fd03fabad39 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x61299774020da444af134c82fa83e3810b309991 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xd6df932a45c0f255f85145f286ea0b292b21c90b - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x2ad2934d5bfb7912304754479dd1f096d5c807da - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xc3c7d422809852031b44ab29eec9f1eff2a58756 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x8f3cf7ad23cd3cadbd9735aff958023239c6a063 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x750e4c4984a9e0f12978ea6742bc1c5d248f40ed - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x111111517e4929d3dcbdfa7cce55d30d4b6bc4d6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xd0258a3fd00f38aa8090dfee343f10a9d4d30d3f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x430ef9263e76dae63c84292c3409d61c598e9682 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xb33eaad8d922b1083446dc23f610c2567fb5180f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xdc3326e71d45186f113a2f448984ca0e8d201995 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x311434160d7537be358930def317afb606c0d737 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x0b3f868e0be5597d5db7feb59e1cadbb0fdda50a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xe3f2b1b2229c0333ad17d03f179b87500e7c5e01 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xac0f66379a6d7801d7726d5a943356a172549adb - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xf88332547c680f755481bf489d890426248bb275 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xe5417af564e4bfda1c483642db72007871397896 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xe261d618a959afffd53168cd07d12e37b26761db - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xe0b52e49357fd4daf2c15e02058dce6bc0057db4 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xbbba073c31bf03b8acf7c28ef0738decf3695683 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xe238ecb42c424e877652ad82d8a939183a04c35f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x3b56a704c01d650147ade2b8cee594066b3f9421 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x5fe2b58c013d7601147dcdd68c143a77499f5531 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x172370d5cd63279efa6d502dab29171933a610af - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x53df32548214f51821cf1fe4368109ac5ddea1ff - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xff76c0b48363a7c7307868a81548d340049b0023 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x6f8a06447ff6fcf75d803135a7de15ce88c1d4ec - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x50b728d8d964fd00c2d0aad81718b71311fef68a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x3a58a54c066fdc0f2d55fc9c89f0415c92ebf3c4 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x03b54a6e9a984069379fae1a4fc4dbae93b3bccd - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xd93f7e271cb87c23aaa73edc008a79646d1f9912 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x200c234721b5e549c3693ccc93cf191f90dc2af9 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x11cd37bb86f65419713f30673a480ea33c826872 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x8a16d4bf8a0a716017e8d2262c4ac32927797a2f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x9a71012b13ca4d3d0cdc72a177df3ef03b0e76a3 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xa1c57f48f0deb89f569dfbe6e2b7f46d33606fd4 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x190eb8a183d22a4bdf278c6791b152228857c033 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x2f6f07cdcf3588944bf4c42ac74ff24bf56e7590 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x235737dbb56e8517391473f7c964db31fa6ef280 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x0b220b82f3ea3b7f6d9a1d8ab58930c064a2b5bf - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x8bff1bd27e2789fe390acabc379c380a83b68e84 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xb58458c52b6511dc723d7d6f3be8c36d7383b4a8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x323665443cef804a3b5206103304bd4872ea4253 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x2760e46d9bb43dafcbecaad1f64b93207f9f0ed7 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x18ec0a6e18e5bc3784fdd3a3634b31245ab704f6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x431d5dff03120afa4bdf332c61a6e1766ef37bdb - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x6f7c932e7684666c9fd1d44527765433e01ff61d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xeee3371b89fc43ea970e908536fcddd975135d8a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xe5b49820e5a1063f6f4ddf851327b5e8b2301048 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xaa3717090cddc9b227e49d0d84a28ac0a996e6ff - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x62a872d9977db171d9e213a5dc2b782e72ca0033 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x381caf412b45dac0f62fbeec89de306d3eabe384 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xe0bceef36f3a6efdd5eebfacd591423f8549b9d5 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x23d29d30e35c5e8d321e1dc9a8a61bfd846d4c5c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x282d8efce846a88b159800bd4130ad77443fa1a1 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x74dd45dd579cad749f9381d6227e7e02277c944b - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x714db550b574b3e927af3d93e26127d15721d4c2 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xfa68fb4628dff1028cfec22b4162fccd0d45efb6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xe631dabef60c37a37d70d3b4f812871df663226f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xdb725f82818de83e99f1dac22a9b5b51d3d04dd4 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x3c59798620e5fec0ae6df1a19c6454094572ab92 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x0d0b8488222f7f83b23e365320a4021b12ead608 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xa380c0b01ad15c8cf6b46890bddab5f0868e87f3 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x8a953cfe442c5e8855cc6c61b1293fa648bae472 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x45c32fa6df82ead1e2ef74d17b76547eddfaff89 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x11cd72f7a4b699c67f225ca8abb20bc9f8db90c7 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x0c9c7712c83b3c70e7c5e11100d33d9401bdf9dd - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x77a6f2e9a9e44fd5d5c3f9be9e52831fc1c3c0a0 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xbfc70507384047aa74c29cdc8c5cb88d0f7213ac - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xfcb54da3f4193435184f3f647467e12b50754575 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x9a6a40cdf21a0af417f1b815223fd92c85636c58 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xe111178a87a3bff0c8d18decba5798827539ae99 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x82617aa52dddf5ed9bb7b370ed777b3182a30fd1 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x2ab0e9e4ee70fff1fb9d67031e44f6410170d00e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xa486c6bc102f409180ccb8a94ba045d39f8fc7cb - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xc4a206a306f0db88f98a3591419bc14832536862 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xf0059cc2b3e980065a906940fbce5f9db7ae40a7 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x16eccfdbb4ee1a85a33f3a9b21175cd7ae753db4 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x553d3d295e0f695b9228246232edf400ed3560b5 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x14af1f2f02dccb1e43402339099a05a5e363b83c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x7bdf330f423ea880ff95fc41a280fd5ecfd3d09f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x8505b9d2254a7ae468c0e9dd10ccea3a837aef5c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xe2aa7db6da1dae97c5f5c6914d285fbfcc32a128 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xb7b31a6bc18e48888545ce79e83e06003be70930 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x1631244689ec1fecbdd22fb5916e920dfc9b8d30 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xf6372cdb9c1d3674e83842e3800f2a62ac9f3c66 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x692ac1e363ae34b6b489148152b12e2785a3d8d6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x0266f4f08d82372cf0fcbccc0ff74309089c74d1 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x7fbc10850cae055b27039af31bd258430e714c62 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xa3fa99a148fa48d14ed51d610c367c61876997f1 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x9dbfc1cbf7a1e711503a29b4b5f9130ebeccac96 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x236aa50979d5f3de3bd1eeb40e81137f22ab794b - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xf86df9b91f002cfeb2aed0e6d05c4c4eaef7cf02 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x4200000000000000000000000000000000000006 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x6921b130d297cc43754afba22e5eac0fbf8db75b - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x5babfc2f240bc5de90eb7e19d789412db1dec402 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x532f27101965dd16442e59d40670faf5ebb142e4 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x833589fcd6edb6e08f4c7c32d4f71b54bda02913 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x4ed4e862860bed51a9570b96d89af5e1b0efefed - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x0d97f261b1e88845184f678e2d1e7a98d9fd38de - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x8129b94753f22ec4e62e2c4d099ffe6773969ebc - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x3f14920c99beb920afa163031c4e47a3e03b3e4a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x940181a94a35a4569e4529a3cdfb74e38fd98631 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x3419875b4d3bca7f3fdda2db7a476a79fd31b4fe - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0xa067436db77ab18b1a315095e4b816791609897c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0xafb89a09d82fbde58f18ac6437b3fc81724e4df6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x489fe42c267fe0366b16b0c39e7aeef977e841ef - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0xdc46c1e93b71ff9209a0f8076a9951569dc35855 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x91f45aa2bde7393e0af1cc674ffe75d746b93567 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x236aa50979d5f3de3bd1eeb40e81137f22ab794b - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0xf6e932ca12afa26665dc4dde7e27be02a7c02e50 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x524d524b4c9366be706d3a90dcf70076ca037ae3 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x5b5dee44552546ecea05edea01dcd7be7aa6144a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x2598c30330d5771ae9f983979209486ae26de875 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0xfa980ced6895ac314e7de34ef1bfae90a5add21b - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x469fda1fb46fcb4befc0d8b994b516bd28c87003 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x4e496c0256fb9d4cc7ba2fdf931bc9cbb7731660 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x27d2decb4bfc9c76f0309b8e88dec3a601fe25a8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0xbfd5206962267c7b4b4a8b3d76ac2e1b2a5c4d5e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x9e1028f5f1d5ede59748ffcee5532509976840e0 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x3c3aa127e6ee3d2f2e432d0184dd36f2d2076b52 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0xba5e6fa2f33f3955f0cef50c63dcc84861eab663 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x97c806e7665d3afd84a8fe1837921403d59f3dcc - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x8ee73c484a26e0a5df2ee2a4960b789967dd0415 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x00e57ec29ef2ba7df07ad10573011647b2366f6d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x8f019931375454fe4ee353427eb94e2e0c9e0a8c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x93e6407554b2f02640ab806cd57bd83e848ec65d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x55d398326f99059ff775485246999027b3197955 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x2170ed0880ac9a755fd29b2688956bd959f933f8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xfdc66a08b0d0dc44c17bbd471b88f49f50cdd20f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x7130d2a12b9bcbfae4f2634d864a1ee1ce3ead9c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x1d2f0da169ceb9fc7b3144628db156f3f6c60dbe - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xe9e7cea3dedca5984780bafc599bd69add087d56 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xfa54ff1a158b5189ebba6ae130ced6bbd3aea76e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x570a5d26f7765ecb712c0924e4de545b89fd43df - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x47c454ca6be2f6def6f32b638c80f91c9c3c5949 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xad86d0e9764ba90ddd68747d64bffbd79879a238 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xf8a0bf9cf54bb92f17374d9e9a321e6a111a51bd - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xd691d9a68c887bdf34da8c36f63487333acfd103 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x1af3f329e8be154074d8769d1ffa4ee058b1dbc3 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x1294f4183763743c7c9519bec51773fb3acd78fd - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xb04906e95ab5d797ada81508115611fee694c2b3 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x111111111117dc0aa78b770fa6a738034120c302 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xcc42724c6683b7e57334c4e856f4c9965ed682bd - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x90c97f71e18723b0cf0dfa30ee176ab653e89f40 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x0e09fabb73bd3ade0a17ecc321fd13a19e81ce82 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x2b72867c32cf673f7b02d208b26889fed353b1f8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x031b41e504677879370e9dbcf937283a8691fa7f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x1ce0c2827e2ef14d5c4f29a091d735a204794041 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xcf3bb6ac0f6d987a5727e2d15e39c2d6061d5bec - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x8ff795a6f4d97e7887c79bea79aba5cc76444adf - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x2dff88a56767223a5529ea5960da7a3f5f766406 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x003d87d02a2a01e9e8a20f507c83e15dd83a33d1 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x4b0f1812e5df2a09796481ff14017e6005508003 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xbf5140a22578168fd562dccf235e5d43a02ce9b1 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xca1c644704febf4ab81f85daca488d1623c28e63 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x51e72dd1f2628295cc2ef931cb64fdbdc3a0c599 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xbbca42c60b5290f2c48871a596492f93ff0ddc82 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x555296de6a86e72752e5c5dc091fe49713aa145c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x0808bf94d57c905f1236212654268ef82e1e594e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x8457ca5040ad67fdebbcc8edce889a335bc0fbfb - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xcebef3df1f3c5bfd90fde603e71f31a53b11944d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x90ed8f1dc86388f14b64ba8fb4bbd23099f18240 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x9840652dc04fb9db2c43853633f0f62be6f00f98 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xba2ae424d960c26247dd6c32edc70b295c744c43 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x0782b6d8c4551b9760e74c0545a9bcd90bdc41e5 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xbe2b6c5e31f292009f495ddbda88e28391c9815e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x8f0528ce5ef7b51152a59745befdd91d97091d2f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xffeecbf8d7267757c2dc3d13d730e97e15bfdf7f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x0eb3a705fc54725037cc9e008bdede697f62f335 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xf21768ccbc73ea5b6fd3c687208a7c2def2d966e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x0000028a2eb8346cd5c0267856ab7594b7a55308 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x76a797a59ba2c17726896976b7b3747bfd1d220f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xc79d1fd14f514cd713b5ca43d288a782ae53eab2 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xad29abb318791d579433d831ed122afeaf29dcfe - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x3203c9e46ca618c8c1ce5dc67e7e9d75f5da2377 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xdb021b1b247fe2f1fa57e0a87c748cc1e321f07f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x7083609fce4d1d8dc0c979aab8c869ea2c873402 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xc5f0f7b66764f6ec8c8dff7ba683102295e16409 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xe29142e14e52bdfbb8108076f66f49661f10ec10 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xb0d502e938ed5f4df2e681fe6e419ff29631d62b - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x6730f7a6bbb7b9c8e60843948f7feb4b6a17b7f7 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x1613957159e9b0ac6c80e824f7eea748a32a0ae2 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/celo/0x471ece3750da237f93b8e339c536989b8978a438 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/celo/0x765de816845861e75a25fca122bb6898b8b1282a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/celo/0x66803fb87abd4aac3cbb3fad7c3aa01f6f3fb207 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/celo/0xd8763cba276a3738e6de85b4b3bf5fded6d6ca73 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/celo/0x37f750b7cc259a2f741af45294f6a16572cf5cad - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/celo/0xd71ffd0940c920786ec4dbb5a12306669b5b81ef - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/celo/0xe8537a3d056da446677b9e9d6c5db704eaab4787 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/celo/0x4f604735c1cf31399c6e711d5962b2b3e0225ad3 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/celo/0x02de4766c272abc10bc88c220d214a26960a7e92 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/celo/0xceba9300f2b948710d2653dd7b07f33a8b32118c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/celo/0xc16b81af351ba9e64c1a069e3ab18c244a1e3049 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x728f30fa2f100742c7949d1961804fa8e0b1387d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x41ea5d41eeacc2d5c4072260945118a13bb7ebce - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xf21661d0d1d76d3ecb8e1b9f1c923dbfffae4097 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xb0ecc6ac0073c063dcfc026ccdc9039cae2998e1 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x00f932f0fe257456b32deda4758922e56a4f4b42 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0xa4af354d466e8a68090dd9eb2cb7caf162f4c8c2 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xba50933c268f567bdc86e1ac131be072c6b0b71a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xd29da236dd4aac627346e1bba06a619e8c22d7c5 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x1bfce574deff725a3f483c334b790e25c8fa9779 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x9e18d5bab2fa94a6a95f509ecb38f8f68322abd3 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xcd5fe23c85820f7b72d0926fc9b05b43e359b7ee - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xbf5495efe5db9ce00f80364c8b423567e58d2110 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x065b4e5dfd50ac12a81722fd0a0de81d78ddf7fb - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x57e114b691db790c35207b2e685d4a43181e6061 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x0b7f0e51cd1739d6c96982d55ad8fa634dd43a9c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xc56c7a0eaa804f854b536a5f3d5f49d2ec4b12b8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x594daad7d77592a2b97b725a7ad59d7e188b5bfa - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x8355dbe8b0e275abad27eb843f3eaf3fc855e525 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x2a961d752eaa791cbff05991e4613290aec0d9ac - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x38e68a37e401f7271568cecaac63c6b1e19130b4 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x1131d427ecd794714ed00733ac0f851e904c8398 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x1495bc9e44af1f8bcb62278d2bec4540cf0c05ea - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x808507121b80c02388fad14726482e061b8da827 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x44971abf0251958492fee97da3e5c5ada88b9185 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x320623b8e4ff03373931769a31fc52a4e78b5d70 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x6e5970dbd6fc7eb1f29c6d2edf2bc4c36124c0c1 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xd40c688da9df74e03566eaf0a7c754ed98fbb8cc - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x8afe4055ebc86bd2afb3940c0095c9aca511d852 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x9ce84f6a69986a83d92c324df10bc8e64771030f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xbe4d9c8c638b5f0864017d7f6a04b66c42953847 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x68bbed6a47194eff1cf514b50ea91895597fc91e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x69420e3a3aa9e17dea102bb3a9b3b73dcddb9528 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x7420b4b9a0110cdc71fb720908340c03f9bc03ec - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x03aa6298f1370642642415edc0db8b957783e8d6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xd533a949740bb3306d119cc777fa900ba034cd52 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xf14dd7b286ce197019cba54b189d2b883e70f761 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xa35923162c49cf95e6bf26623385eb431ad920d3 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x8cefbeb2172a9382753de431a493e21ba9694004 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x120a3879da835a5af037bb2d1456bebd6b54d4ba - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x69457a1c9ec492419344da01daf0df0e0369d5d0 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xf6ce4be313ead51511215f1874c898239a331e37 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x73d7c860998ca3c01ce8c808f5577d94d545d1b4 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xeff49b0f56a97c7fd3b51f0ecd2ce999a7861420 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x236501327e701692a281934230af0b6be8df3353 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x5026f006b85729a8b14553fae6af249ad16c9aab - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x66761fa41377003622aee3c7675fc7b5c1c2fac5 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x9f9c8ec3534c3ce16f928381372bfbfbfb9f4d24 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xd8c978de79e12728e38aa952a6cb4166f891790f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x7122985656e38bdc0302db86685bb972b145bd3c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x582d872a1b094fc48f5de31d3b73f2d9be47def1 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x504624040e0642921c2c266a9ac37cafbd8cda4e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xc548e90589b166e1364de744e6d35d8748996fe8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x4c11249814f11b9346808179cf06e71ac328c1b5 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x423f4e6138e475d85cf7ea071ac92097ed631eea - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0x8390a1da07e376ef7add4be859ba74fb83aa02d5 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xf94e7d0710709388bce3161c32b4eea56d3f91cc - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/ethereum/0xaa95f26e30001251fb905d264aa7b00ee9df6c18 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x2416092f143378750bb29b79ed961ab195cceea5 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x6c84a8f1c29108f47a79964b5fe888d4f4d0de40 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x71eeba415a523f5c952cc2f06361d5443545ad28 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x88a269df8fe7f53e590c561954c52fccc8ec0cfb - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x429fed88f10285e61b12bdf00848315fbdfcc341 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xb299751b088336e165da313c33e3195b8c6663a6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xf0a479c9c3378638ec603b8b6b0d75903902550b - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xb59c8912c83157a955f9d715e556257f432c35d7 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xba0dda8762c24da9487f5fa026a9b64b695a07ea - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xc24a365a870821eb83fd216c9596edd89479d8d7 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xa586b3b80d7e3e8d439e25fbc16bc5bcee3e2c85 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xef04804e1e474d3f9b73184d7ef5d786f3fce930 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x2e9a6df78e42a30712c10a9dc4b1c8656f8f2879 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x13a7dedb7169a17be92b0e3c7c2315b46f4772b3 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0x1dd6b5f9281c6b4f043c02a83a46c2772024636c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xc5102fe9359fd9a28f877a67e36b0f050d81a3cc - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xf525e73bdeb4ac1b0e741af3ed8a8cbb43ab0756 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xe4177c1400a8eee1799835dcde2489c6f0d5d616 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xed5740209fcf6974d6f3a5f11e295b5e468ac27c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/arbitrum/0xe10d4a4255d2d35c9e23e2c4790e073046fbaf5c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x10398abc267496e49106b07dd6be13364d10dc71 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x2218a117083f5b482b0bb821d27056ba9c04b1d3 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x395ae52bb17aef68c2888d941736a71dc6d4e125 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/optimism/0x9a601c5bb360811d96a23689066af316a30c3027 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xbac3368b5110f3a3dda8b5a0f7b66edb37c47afe - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x1d3c629ca5c1d0ab3bdf74600e81b4145615df8e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xe9c21de62c5c5d0ceacce2762bf655afdceb7ab3 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x658cda444ac43b0a7da13d638700931319b64014 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x3d2bd0e15829aa5c362a4144fdf4a1112fa29b5c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x3fb83a9a2c4408909c058b0bfe5b4823f54fafe2 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x00e5646f60ac6fb446f621d146b6e1886f002905 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x12a4cebf81f8671faf1ab0acea4e3429e42869e7 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x9ff62d1fc52a907b6dcba8077c2ddca6e6a9d3e1 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0xc61f39418cd27820b5d4e9ba4a7197eefaeb8b05 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x15b7c0c907e4c6b9adaaaabc300c08991d6cea05 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x7f67639ffc8c93dd558d452b8920b28815638c44 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/polygon/0x276c9cbaa4bdf57d7109a41e67bd09699536fa3d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x041fdf3f472d2c8a7ecc458fc3b7f543e6c57ef7 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x3c281a39944a2319aa653d81cfd93ca10983d234 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x96419929d7949d6a801a6909c145c8eef6a40431 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0xfea9dcdc9e23a9068bf557ad5b186675c61d33ea - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0xdb6e0e5094a25a052ab6845a9f1e486b9a9b3dde - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0xcde172dc5ffc46d228838446c57c1227e0b82049 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0xff0c532fdb8cd566ae169c1cb157ff2bdc83e105 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x9a26f5433671751c3276a065f57e5a02d2817973 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x3636a7734b669ce352e97780df361ce1f809c58c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x50c5725949a6f0c72e6c4a641f24049a917db0cb - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0xe3086852a4b125803c815a158249ae468a3254ca - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0xbeb0fd48c2ba0f1aacad2814605f09e08a96b94e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0xbc45647ea894030a4e9801ec03479739fa2485f0 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x768be13e1680b5ebe0024c42c896e3db59ec0149 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x928a6a9fc62b2c94baf2992a6fba4715f5bb0066 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0xbf4db8b7a679f89ef38125d5f84dd1446af2ea3b - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0xed899bfdb28c8ad65307fa40f4acab113ae2e14c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x1b6a569dd61edce3c383f6d565e2f79ec3a12980 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x76734b57dfe834f102fb61e1ebf844adf8dd931e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x4621b7a9c75199271f773ebd9a499dbd165c3191 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0xaf07d812d1dcec20bf741075bc18660738d226dd - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x7f12d13b34f5f4f0a9449c16bcd42f0da47af200 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x55a6f6cb50db03259f6ab17979a4891313be2f45 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x968d6a288d7b024d5012c0b25d67a889e4e3ec19 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x7a8a5012022bccbf3ea4b03cd2bb5583d915fb1a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0xcde90558fc317c69580deeaf3efc509428df9080 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x0028e1e60167b48a938b785aa5292917e7eaca8b - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x76e7447bafa3f0acafc9692629b1d1bc937ca15d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x15ac90165f8b45a80534228bdcb124a011f62fee - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x4045b33f339a3027af80013fb5451fdbb01a4492 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0xddf98aad8180c3e368467782cd07ae2e3e8d36a5 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x698dc45e4f10966f6d1d98e3bfd7071d8144c233 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x3c8665472ec5af30981b06b4e0143663ebedcc1e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x18a8bd1fe17a1bb9ffb39ecd83e9489cfd17a022 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0xba0dda8762c24da9487f5fa026a9b64b695a07ea - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x13741c5df9ab03e7aa9fb3bf1f714551dd5a5f8a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0xebff2db643cf955247339c8c6bcd8406308ca437 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0xfadb26be94c1f959f900bf88cd396b3e803481d6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x52c2b317eb0bb61e650683d2f287f56c413e4cf6 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x38d513ec43dda20f323f26c7bef74c5cf80b6477 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x33ad778e6c76237d843c52d7cafc972bb7cf8729 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x290814ad0fbd2b935f34d7b40306102313d4c63e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x5e432eecd01c12ee7071ee9219c2477a347da192 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0xbdf5bafee1291eec45ae3aadac89be8152d4e673 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0xff62ddfa80e513114c3a0bf4d6ffff1c1d17aadf - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x8c81b4c816d66d36c4bf348bdec01dbcbc70e987 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x6b82297c6f1f9c3b1f501450d2ee7c37667ab70d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x42069babe14fb1802c5cb0f50bb9d2ad6fef55e2 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x72499bddb67f4ca150e1f522ca82c87bc9fb18c8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x0578d8a44db98b23bf096a382e016e29a5ce0ffe - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x8fe815417913a93ea99049fc0718ee1647a2a07c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x7d12aeb5d96d221071d176980d23c213d88d9998 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0xb166e8b140d35d9d8226e40c09f757bac5a4d87d - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x8853f0c059c27527d33d02378e5e4f6d5afb574a - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0xf3c052f2baab885c610a748eb01dfbb643ba835b - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0xcd1cffa8ebc66f1a2cf7675b48ba955ffcb82d8e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0xde7a416ac821c77478340eebaa21b68297025ef3 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x2da56acb9ea78330f947bd57c54119debda7af71 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x8972ab69d499b5537a31576725f0af8f67203d38 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x88faea256f789f8dd50de54f9c807eef24f71b16 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x42069de48741db40aef864f8764432bbccbd0b69 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x9a27c6759a6de0f26ac41264f0856617dec6bc3f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0xfaa4f3bcfc87d791e9305951275e0f62a98bcb10 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0xfd9fa4f785331ce88b5af8994a047ba087c705d8 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x21eceaf3bf88ef0797e3927d855ca5bb569a47fc - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x7d9ce55d54ff3feddb611fc63ff63ec01f26d15f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x4229c271c19ca5f319fb67b4bc8a40761a6d6299 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x80f45eacf6537498ecc660e4e4a2d2f99e195cf4 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x1a475d06d967aeb686c98de80d079d72097aeacf - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x4fb9b20dafe45d91ae287f2e07b2e79709308178 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0xd3741ac9b3f280b0819191e4b30be4ecd990771e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x09579452bc3872727a5d105f342645792bb8a82b - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x8a24d7260cd02d3dfd8eefb66bc17ad4b17d494c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0xd88611a629265c9af294ffdd2e7fa4546612273e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x9a86980d3625b4a6e69d8a4606d51cbc019e2002 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x1c7a460413dd4e964f96d8dfc56e7223ce88cd85 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x776aaef8d8760129a0398cf8674ee28cefc0eab9 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x28e29ec91db66733a94ee8e3b86a6199117baf99 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0xb9898511bd2bad8bfc23eba641ef97a08f27e730 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x76baa16ff15d61d32e6b3576c3a8c83a25c2f180 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x2816a491dd0b7a88d84cbded842a618e59016888 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0xa7ea9d5d4d4c7cf7dbde5871e6d108603c6942a5 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/base/0x586e10db93630a4d2da6c6a34ba715305b556f04 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xf486ad071f3bee968384d2e39e2d8af0fcf6fd46 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x76d36d44dc4595e8d2eb3ad745f175eda134284f - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x1fa4a73a3f0133f0025378af00236f3abdee5d63 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xb3ed0a426155b79b898849803e3b36552f7ed507 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x0ef4a107b48163ab4b57fca36e1352151a587be4 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x62694d43ccb9b64e76e38385d15e325c7712a735 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xa2b726b1145a4773f68593cf171187d8ebe4d495 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0xf275e1ac303a4c9d987a2c48b8e555a77fec3f1c - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/bnb/0x11a31b833d43853f8869c9eec17f60e3b4d2a753 - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z 0.8 https://app.uniswap.org/explore/tokens/celo/0x48065fbbe25f71c9282ddf5e1cd6d6a887483d5e - 2024-05-20T17:20:52.753Z + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xbadff0ef41d2a68f22de21eabca8a59aaf495cf0 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x1fdd61ef9a5c31b9a2abc7d39c139c779e8412af + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x4ade2b180f65ed752b6f1296d0418ad21eb578c0 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x0c5cb676e38d6973837b9496f6524835208145a2 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xb69753c06bb5c366be51e73bfc0cc2e3dc07e371 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x8143182a775c54578c8b7b3ef77982498866945d + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x76e222b07c53d28b89b0bac18602810fc22b49a8 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x18aaa7115705e8be94bffebde57af9bfc265b998 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x7d8146cf21e8d7cbe46054e01588207b51198729 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xfe0c30065b384f05761f15d0cc899d4f9f9cc0eb + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x1ce270557c1f68cfb577b856766310bf8b47fd9c + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x793a5d8b30aab326f83d20a9370c827fea8fdc51 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xff836a5821e69066c87e268bc51b849fab94240c + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xf4d2888d29d722226fafa5d9b24f9164c092421e + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x8ed97a637a790be1feff5e888d43629dc05408f6 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x31c8eacbffdd875c74b94b077895bd78cf1e64a3 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xc55126051b22ebb829d00368f4b12bde432de5da + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xe0f63a424a4439cbe457d80e4f4b51ad25b2c56c + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x8881562783028f5c1bcb985d2283d5e170d88888 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x67466be17df832165f8c80a5a120ccc652bd7e69 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xd939212f16560447ed82ce46ca40a63db62419b5 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x88417754ff7062c10f4e3a4ab7e9f9d9cbda6023 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x5afe3855358e112b5647b952709e6165e1c1eeee + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x02e7f808990638e9e67e1f00313037ede2362361 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xd2bdaaf2b9cc6981fd273dcb7c04023bfbe0a7fe + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x112b08621e27e10773ec95d250604a041f36c582 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x32b053f2cba79f80ada5078cb6b305da92bde6e1 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x5ac34c53a04b9aaa0bf047e7291fb4e8a48f2a18 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x26ebb8213fb8d66156f1af8908d43f7e3e367c1d + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xe3b9cfb8ea8a4f1279fbc28d3e15b4d2d86f18a0 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x8207c1ffc5b6804f6024322ccf34f29c3541ae26 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x255f1b39172f65dc6406b8bee8b08155c45fe1b6 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x092baadb7def4c3981454dd9c0a0d7ff07bcfc86 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x53bcf6698c911b2a7409a740eacddb901fc2a2c6 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x2ac2b254bc18cd4999f64773a966e4f4869c34ee + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x17fc002b466eec40dae837fc4be5c67993ddbd6f + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xc8a4eea31e9b6b61c406df013dd4fec76f21e279 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x498bf2b1e120fed3ad3d42ea2165e9b73f99c1e5 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xe4dddfe67e7164b0fe14e218d80dc4c08edc01cb + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x7c8a1a80fdd00c9cccd6ebd573e9ecb49bfa2a59 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x1debd73e752beaf79865fd6446b0c970eae7732f + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xaf5db6e1cc585ca312e8c8f7c499033590cf5c98 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0x65559aa14915a70190438ef90104769e5e890a00 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0x7fb688ccf682d58f86d7e38e03f9d22e7705448b + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0x73cb180bf0521828d8849bc8cf2b920918e23032 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0x2e3d870790dc77a83dd1d18184acc7439a53f475 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0xa00e3a3511aac35ca78530c85007afcd31753819 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0x528cdc92eab044e1e39fe43b9514bfdab4412b98 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0x4f604735c1cf31399c6e711d5962b2b3e0225ad3 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x1c954e8fe737f99f68fa1ccda3e51ebdb291948c + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xf50d05a1402d0adafa880d36050736f9f6ee7dee + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xab0b2ddb9c7e440fac8e140a89c0dbcbf2d7bbff + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x8bc3ec2e7973e64be582a90b08cadd13457160fe + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x64060ab139feaae7f06ca4e63189d86adeb51691 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x5ec03c1f7fa7ff05ec476d19e34a22eddb48acdc + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x9627a3d6872be48410fcece9b1ddd344bf08c53e + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x1ed02954d60ba14e26c230eec40cbac55fa3aeea + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x8d3419b9a18651f3926a205ee0b1acea1e7192de + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0xb56d0839998fd79efcd15c27cf966250aa58d6d3 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x81f91fe59ee415735d59bd5be5cca91a0ea4fa69 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x87c211144b1d9bdaa5a791b8099ea4123dc31d21 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0xf4210f93bc68d63df3286c73eba08c6414f40c0d + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0xece7b98bd817ee5b1f2f536daf34d0b6af8bb542 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x4c96a67b0577358894407af7bc3158fc1dffbeb5 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x70737489dfdf1a29b7584d40500d3561bd4fe196 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x39353a32eceafe4979a8606512c046c3b6398cc4 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x92fb1b7d9730b2f1bd4e2e91368c1eb6fdd2a009 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x174e33ef2effa0a4893d97dda5db4044cc7993a3 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0xfdc944fb59201fb163596ee5e209ebc8fa4dcdc5 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x388e543a5a491e7b42e3fbcd127dd6812ea02d0d + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x56a38e7216304108e841579041249feb236c887b + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x1804e3db872eed4141e482ff74c56862f2791103 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x9de16c805a3227b9b92e39a446f9d56cf59fe640 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0xb8d98a102b0079b69ffbc760c8d857a31653e56e + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x5d6812722c3693078e4a0dbe3e9affc27a0b2768 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x255f1b39172f65dc6406b8bee8b08155c45fe1b6 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0xc2fe011c3885277c7f0e7ffd45ff90cadc8ecd12 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0xc1ffaef4e7d553bbaf13926e258a1a555a363a07 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x4e73420dcc85702ea134d91a262c8ffc0a72aa70 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0xecaf81eb42cd30014eb44130b89bcd6d4ad98b92 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x4eae52907dba9c370e9ee99f0ce810602a4f2c63 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x25d887ce7a35172c62febfd67a1856f20faebb00 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x382ea807a61a418479318efd96f1efbc5c1f2c21 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x6468e79a80c0eab0f9a2b574c8d5bc374af59414 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x3106a0a076bedae847652f42ef07fd58589e001f + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xd015422879a1308ba557510345e944b912b9ab73 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x5de8ab7e27f6e7a1fff3e5b337584aa43961beef + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xcf078da6e85389de507ceede0e3d217e457b9d49 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x1bbf25e71ec48b84d773809b4ba55b6f4be946fb + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x7039cd6d7966672f194e8139074c3d5c4e6dcf65 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x943af17c37207c9d7a27d12cb5055542a0b7afa8 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x6d68015171eaa7af9a5a0a103664cf1e506ff699 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x6942806d1b2d5886d95ce2f04314ece8eb825833 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x949d48eca67b17269629c7194f4b727d4ef9e5d6 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x9361adf2b72f413d96f81ff40d794b47ce13b331 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x3bb1be077f3f96722ae92ec985ab37fd0a0c4c51 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xdbb7a34bf10169d6d2d0d02a6cbb436cf4381bfa + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x66bff695f3b16a824869a8018a3a6e3685241269 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x85d19fb57ca7da715695fcf347ca2169144523a7 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x069d89974f4edabde69450f9cf5cf7d8cbd2568d + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x0fe13ffe64b28a172c58505e24c0c111d149bd47 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x111111111117dc0aa78b770fa6a738034120c302 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xdc7ac5d5d4a9c3b5d8f3183058a92776dc12f4f3 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x482702745260ffd69fc19943f70cffe2cacd70e9 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xc555d625828c4527d477e595ff1dd5801b4a600e + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x9eec1a4814323a7396c938bc86aec46b97f1bd82 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x87d73e916d7057945c9bcd8cdd94e42a6f47f776 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x067def80d66fb69c276e53b641f37ff7525162f6 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xdd157bd06c1840fa886da18a138c983a7d74c1d7 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xe80772eaf6e2e18b651f160bc9158b2a5cafca65 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xb6093b61544572ab42a0e43af08abafd41bf25a6 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x35ca1e5a9b1c09fa542fa18d1ba4d61c8edff852 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x83e60b9f7f4db5cdb0877659b1740e73c662c55b + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x4d01397994aa636bdcc65c9e8024bc497498c3bb + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xc3abc47863524ced8daf3ef98d74dd881e131c38 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x4d15a3a2286d883af0aa1b3f21367843fac63e07 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xfb7f8a2c0526d01bfb00192781b7a7761841b16c + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x3809dcdd5dde24b37abe64a5a339784c3323c44f + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x85955046df4668e1dd369d2de9f3aeb98dd2a369 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x554cd6bdd03214b10aafa3e0d4d42de0c5d2937b + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x4318cb63a2b8edf2de971e2f17f77097e499459d + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xab9cb20a28f97e189ca0b666b8087803ad636b3c + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x6a8ec2d9bfbdd20a7f5a4e89d640f7e7ceba4499 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x385eeac5cb85a38a9a07a70c73e0a3271cfb54a7 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x0169ec1f8f639b32eec6d923e24c2a2ff45b9dd6 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0xe161be4a74ab8fa8706a2d03e67c02318d0a0ad6 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x4d58608eff50b691a3b76189af2a7a123df1e9ba + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x420b0fa3de2efcf2b2fd04152eb1df36a09717cd + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x1cd38856ee0fdfd65c757e530e3b1de3061008d3 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0xfad8cb754230dbfd249db0e8eccb5142dd675a0d + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0xda761a290e01c69325d12d82ac402e5a73d62e81 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0xafb5d4d474693e68df500c9c682e6a2841f9661a + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x0b3e328455c4059eeb9e3f84b5543f74e24e7e1b + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0xfc5462143a3178cf044e97c491f6bcb5e38f173e + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0xed1978d01d4a8a9d6a43ac79403d5b8dfbed739b + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0xba71cb8ef2d59de7399745793657838829e0b147 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x10c1b6f768e13c624a4a23337f1a5ba5c9be0e4b + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x1b1514c76c54ce8807d7fdedf85c664eee734ece + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x58cd93c4a91c3940109fa27d700f5013b18b5dc2 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0xea6f7e7e0f46a9e0f4e2048eb129d879f609d632 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x30d19fb77c3ee5cfa97f73d72c6a1e509fa06aef + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0xe2dca969624795985f2f083bcd0b674337ba130a + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0xbb7d61d2511fd2e63f02178ca9b663458af9fc63 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x59f4f336bf3d0c49dbfba4a74ebd2a6ace40539a + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x62d0a8458ed7719fdaf978fe5929c6d342b0bfce + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xb8fda5aee55120247f16225feff266dfdb381d4c + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xca530408c3e552b020a2300debc7bd18820fb42f + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x3ffeea07a27fab7ad1df5297fa75e77a43cb5790 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xcfeb09c3c5f0f78ad72166d55f9e6e9a60e96eec + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x467bccd9d29f223bce8043b84e8c8b282827790f + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x2077d81d0c5258230d5a195233941547cb5f0989 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xa0bbbe391b0d0957f1d013381b643041d2ca4022 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xd1b89856d82f978d049116eba8b7f9df2f342ff3 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x62f03b52c377fea3eb71d451a95ad86c818755d1 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x3927fb89f34bbee63351a6340558eebf51a19fb8 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xacd2c239012d17beb128b0944d49015104113650 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x86b69f38bea3e02f68ff88534bc61ec60e772b19 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x6873c95307e13beb58fb8fcddf9a99667655c9e4 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x18084fba666a33d37592fa2633fd49a74dd93a88 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x6e79b51959cf968d87826592f46f819f92466615 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x80ee5c641a8ffc607545219a3856562f56427fe9 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x0414d8c87b271266a5864329fb4932bbe19c0c49 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xf57e7e7c23978c3caec3c3548e3d615c346e79ff + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xb0ffa8000886e57f86dd5264b9582b2ad87b2b91 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x1c986661170c1834db49c3830130d4038eeeb866 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x9ed7e4b1bff939ad473da5e7a218c771d1569456 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x7f9a7db853ca816b9a138aee3380ef34c437dee0 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x371c7ec6d8039ff7933a2aa28eb827ffe1f52f07 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xb1bc21f748ae2be95674876710bc6d78235480e0 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xadf5dd3e51bf28ab4f07e684ecf5d00691818790 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0x1eba7a6a72c894026cd654ac5cdcf83a46445b08 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x38022a157b95c52d43abcac9bd09f028a1079105 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xd2507e7b5794179380673870d88b22f94da6abe0 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xc708d6f2153933daa50b2d0758955be0a93a8fec + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x0052074d3eb1429f39e5ea529b54a650c21f5aa4 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x4e78011ce80ee02d2c3e649fb657e45898257815 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x7583feddbcefa813dc18259940f76a02710a8905 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xe78aee6ccb05471a69677fb74da80f5d251c042b + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x04f177fcacf6fb4d2f95d41d7d3fee8e565ca1d0 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xa6da8c8999c094432c77e7d318951d34019af24b + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x6d3b8c76c5396642960243febf736c6be8b60562 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x7cf7132ede0ca592a236b6198a681bb7b42dd5ae + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x3afeae00a594fbf2e4049f924e3c6ac93296b6e8 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x0a93a7be7e7e426fc046e204c44d6b03a302b631 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0xc9b6ef062fab19d3f1eabc36b1f2e852af1acd18 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x1754e5aadce9567a95f545b146a616ce34eead53 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0xdb173587d459ddb1b9b0f2d6d88febef039304a2 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x10a7a84c91988138f8dbbc82a23b02c8639e2552 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x92af6f53febd6b4c6f5293840b6076a1b82c4bc2 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0xeb9e49fb4c33d9f6aefb1b03f9133435e24c0ec6 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x1b2c141479757b8643a519be4692904088d860b2 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x4d25e94291fe8dcfbfa572cbb2aaa7b755087c91 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x8e0e798966382e53bfb145d474254cbe065c17dc + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x4b6f82a4ed0b9e3767f53309b87819a78d041a7f + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x004aa1586011f3454f487eac8d0d5c647d646c69 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x741777f6b6d8145041f73a0bddd35ae81f55a40f + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0xc6c58f600917de512cd02d2b6ed595ab54b4c30f + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x03aa6298f1370642642415edc0db8b957783e8d6 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x3ee2200efb3400fabb9aacf31297cbdd1d435d47 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x0d8ce2a99bb6e3b7db580ed848240e4a0f9ae153 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0xa697e272a73744b343528c3bc4702f2565b2f422 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x301af3eff0c904dc5ddd06faa808f653474f7fcc + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x776f9987d9deed90eed791cbd824d971fd5ccf09 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0xf7de7e8a6bd59ed41a4b5fe50278b3b7f31384df + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x19e6bfc1a6e4b042fb20531244d47e252445df01 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x4338665cbb7b2485a8855a139b75d5e34ab0db94 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x2940566eb50f15129238f4dc599adc4f742d7d8e + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0xbb73bb2505ac4643d5c0a99c2a1f34b3dfd09d11 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x4ea98c1999575aaadfb38237dd015c5e773f75a2 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/celo/0x1d18d0386f51ab03e7e84e71bda1681eba865f1f + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x57b96d4af698605563a4653d882635da59bf11af + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xd33526068d116ce69f19a9ee46f0bd304f21a51f + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x2a5fa016ffb20c70e2ef36058c08547f344677aa + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xbe0ed4138121ecfc5c0e56b40517da27e6c5226b + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x9fd9278f04f01c6a39a9d1c1cd79f7782c6ade08 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x054c9d4c6f4ea4e14391addd1812106c97d05690 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x7613c48e0cd50e42dd9bf0f6c235063145f6f8dc + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x614da3b37b6f66f7ce69b4bbbcf9a55ce6168707 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x069e4aa272d17d9625aa3b6f863c7ef6cfb96713 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x24da31e7bb182cb2cabfef1d88db19c2ae1f5572 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x7d4a23832fad83258b32ce4fd3109ceef4332af4 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xb58e61c3098d85632df34eecfb899a1ed80921cb + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x67c4d14861f9c975d004cfb3ac305bee673e996e + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x69babe9811cc86dcfc3b8f9a14de6470dd18eda4 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x32f0d04b48427a14fb3cbc73db869e691a9fec6f + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x4cff49d0a19ed6ff845a9122fa912abcfb1f68a6 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x51cb253744189f11241becb29bedd3f1b5384fdb + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xcf4c91ecafc43c9f382db723ba20b82efa852821 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x6968676661ac9851c38907bdfcc22d5dd77b564d + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x0d438f3b5175bebc262bf23753c1e53d03432bde + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xb98d4c97425d9908e66e53a6fdf673acca0be986 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x68a47fe1cf42eba4a030a10cd4d6a1031ca3ca0a + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x8a370c951f34e295b2655b47bb0985dd08d8f718 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x525574c899a7c877a11865339e57376092168258 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xd9a442856c234a39a81a089c06451ebaa4306a72 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x1c43d05be7e5b54d506e3ddb6f0305e8a66cd04e + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xb766039cc6db368759c1e56b79affe831d0cc507 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x18c14c2d707b2212e17d1579789fc06010cfca23 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xe0ee18eacafddaeb38f8907c74347c44385578ab + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x56659245931cb6920e39c189d2a0e7dd0da2d57b + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xb6a5ae40e79891e4deadad06c8a7ca47396df21c + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x04565fe9aa3ae571ada8e1bebf8282c4e5247b2a + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0xf8a99f2bf2ce5bb6ce4aafcf070d8723bc904aa2 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x3b9728bd65ca2c11a817ce39a6e91808cceef6fd + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x6797b6244fa75f2e78cdffc3a4eb169332b730cc + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0xe2c86869216ac578bd62a4b8313770d9ee359a05 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x47b464edb8dc9bc67b5cd4c9310bb87b773845bd + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x28a730de97dc62a8c88363e0b1049056f1274a70 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0xba5ede8d98ab88cea9f0d69918dde28dc23c2553 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x8319767a7b602f88e376368dca1b92d38869b9b4 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x461ee40928677644b8195662ab91bcdaae6ef105 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x24569d33653c404f90af10a2b98d6e0030d3d267 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x22222bd682745cf032006394750739684e45a5f8 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x9124577428c5bd73ad7636cbc5014081384f29d6 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0xaa6cccdce193698d33deb9ffd4be74eaa74c4898 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0xe095780ba2a64a4efa7a74830f0b71656f0b0ad4 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0xb59c8912c83157a955f9d715e556257f432c35d7 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x7771450ece9c61430953d2646f995e33a06c91f5 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0xc48823ec67720a04a9dfd8c7d109b2c3d6622094 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x9ec02756a559700d8d9e79ece56809f7bcc5dc27 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x3593d125a4f7849a1b059e64f4517a86dd60c95d + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0xb0ffa8000886e57f86dd5264b9582b2ad87b2b91 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x6985884c4392d348587b19cb9eaaf157f13271cd + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xa045fe936e26e1e1e1fb27c1f2ae3643acde0171 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xbeef698bd78139829e540622d5863e723e8715f1 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x426a688ee72811773eb64f5717a32981b56f10c1 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x873259322be8e50d80a4b868d186cc5ab148543a + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x661c70333aa1850ccdbae82776bb436a0fcfeefb + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x0a2c375553e6965b42c135bb8b15a8914b08de0c + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x6fba952443be1de22232c824eb8d976b426b3c38 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x1abaea1f7c830bd89acc67ec4af516284b1bc33c + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xb62132e35a6c13ee1ee0f84dc5d40bad8d815206 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xb60fdf036f2ad584f79525b5da76c5c531283a1b + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x5a3e6a77ba2f983ec0d371ea3b475f8bc0811ad5 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x55296f69f40ea6d20e478533c15a6b08b654e758 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x1a7e4e63778b4f12a199c062f3efdd288afcbce8 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x45804880de22913dafe09f4980848ece6ecbaf78 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xe5018913f2fdf33971864804ddb5fca25c539032 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x6985884c4392d348587b19cb9eaaf157f13271cd + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x2c650dab03a59332e2e0c0c4a7f726913e5028c1 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x9aee3c99934c88832399d6c6e08ad802112ebeab + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x439c0cf1038f8002a4cad489b427e217ba4b42ad + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0x6985884c4392d348587b19cb9eaaf157f13271cd + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x6985884c4392d348587b19cb9eaaf157f13271cd + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x6985884c4392d348587b19cb9eaaf157f13271cd + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0xb79dd08ea68a908a97220c76d19a6aa9cbde4376 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x4b61e2f1bbdee6d746209a693156952936f1702c + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x7480527815ccae421400da01e052b120cc4255e9 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x7466de7bb8b5e41ee572f4167de6be782a7fa75d + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x298d411511a05dc1b559ed8f79c56bee06687b14 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x8e16d46cb2da01cdd49601ec73d7b0344969ae33 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x18dd5b087bca9920562aff7a0199b96b9230438b + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x37f0c2915cecc7e977183b8543fc0864d03e064c + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x37f24b26bcefbfac7f261b97f8036da98f81a299 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0xacb5b33ce55ba7729e38b2b59677e71c0112f0d9 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x6985884c4392d348587b19cb9eaaf157f13271cd + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0xc71b5f631354be6853efe9c3ab6b9590f8302e81 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x7e744bbb1a49a44dfcc795014a4ba618e418fbbe + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x4e3fbd56cd56c3e72c1403e103b45db9da5b9d2b + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x0c04ff41b11065eed8c9eda4d461ba6611591395 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x636bd98fc13908e475f56d8a38a6e03616ec5563 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x590246bfbf89b113d8ac36faeea12b7589f7fe5b + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x80034f803afb1c6864e3ca481ef1362c54d094b9 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x73fbd93bfda83b111ddc092aa3a4ca77fd30d380 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xff33a6b3dc0127862eedd3978609404b22298a54 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xc770eefad204b5180df6a14ee197d99d808ee52d + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xa0385e7283c83e2871e9af49eec0966088421ddd + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xb2617246d0c6c0087f18703d576831899ca94f01 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xba386a4ca26b85fd057ab1ef86e3dc7bdeb5ce70 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x9ebb0895bd9c7c9dfab0d8d877c66ba613ac98ea + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xd12a99dbc40036cec6f1b776dccd2d36f5953b94 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x8ab2ff0116a279a99950c66a12298962d152b83c + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x420698cfdeddea6bc78d59bc17798113ad278f9d + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xa8c8cfb141a3bb59fea1e2ea6b79b5ecbcd7b6ca + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xd8e8438cf7beed13cfabc82f300fb6573962c9e3 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xb1c9d42fa4ba691efe21656a7e6953d999b990c4 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xdadeca1167fe47499e53eb50f261103630974905 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xa05245ade25cc1063ee50cf7c083b4524c1c4302 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x4fafad147c8cd0e52f83830484d164e960bdc6c3 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x4dd9077269dd08899f2a9e73507125962b5bc87f + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x8931ee05ec111325c1700b68e5ef7b887e00661d + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x26f1bb40ea88b46ceb21557dc0ffac7b7c0ad40f + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x642e993fa91ffe9fb24d39a8eb0e0663145f8e92 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x0c41f1fc9022feb69af6dc666abfe73c9ffda7ce + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0xf7ccb8a6e3400eb8eb0c47619134f7516e025215 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x2416092f143378750bb29b79ed961ab195cceea5 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0xf0268c5f9aa95baf5c25d646aabb900ac12f0800 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x0c067fc190cde145b0c537765a78d4e19873a5cc + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0xbe5614875952b1683cb0a2c20e6509be46d353a4 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x87a0233a8cb4392ec3eb8fa467817fc0b6a326dd + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0xdfbea88c4842d30c26669602888d746d30f9d60d + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0xb6fe221fe9eef5aba221c348ba20a1bf5e73624c + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x80b3455e1db60b4cba46aba12e8b1e256dd64979 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x747747e47a48c669be384e0dfb248eee6ba04039 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/celo/0x50e85c754929840b58614f48e29c64bc78c58345 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x02f92800f57bcd74066f5709f1daa1a4302df875 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x967da4048cd07ab37855c090aaf366e4ce1b9f48 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x729031b3995538ddf6b6bce6e68d5d6fdeb3ccb5 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x6dea81c8171d0ba574754ef6f8b412f2ed88c54d + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x97a9a15168c22b3c137e6381037e1499c8ad0978 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x5faa989af96af85384b8a938c2ede4a7378d9875 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x4691937a7508860f876c9c0a2a617e7d9e945d4b + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xb50721bcf8d664c30412cfbc6cf7a15145234ad1 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x037a54aab062628c9bbae1fdb1583c195585fe41 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xcb8b5cd20bdcaea9a010ac1f8d835824f5c87a04 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xdfb8be6f8c87f74295a87de951974362cedcfa30 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x354a6da3fcde098f8389cad84b0182725c6c91de + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x3f56e0c36d275367b8c502090edf38289b3dea0d + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x6f9590958ce2beaf9c92a3a8fca6d1ddf310e052 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0x3e5d9d8a63cc8a88748f229999cf59487e90721e + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0xecc68d0451e20292406967fe7c04280e5238ac7d + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xf1c1a3c2481a3a8a3f173a9ab5ade275292a6fa3 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xb5e0cfe1b4db501ac003b740665bf43192cc7853 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xffa188493c15dfaf2c206c97d8633377847b6a52 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xb5c064f955d8e7f38fe0460c556a72987494ee17 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x4f604735c1cf31399c6e711d5962b2b3e0225ad3 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xf0949dd87d2531d665010d6274f06a357669457a + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x14e5386f47466a463f85d151653e1736c0c50fc3 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xadac33f543267c4d59a8c299cf804c303bc3e4ac + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0xcfa3ef56d303ae4faaba0592388f19d7c3399fb4 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x67ce18961c3269ca03c2e5632f1938cc53e614a1 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x48164ea5df090e80a0eaee1147e466ea28669221 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x3054e8f8fba3055a42e5f5228a2a4e2ab1326933 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x42069d11a2cc72388a2e06210921e839cfbd3280 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x74ff3cbf86f95fea386f79633d7bc4460d415f34 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x2d6a3893966dda77749cc7e4003ab15f5cfa3cc1 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x51b75da3da2e413ea1b8ed3eb078dc712304761c + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x8ad5b9007556749de59e088c88801a3aaa87134b + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0xbd97693278f1948c59f65f130fd87e7ff7c61d11 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x3992b27da26848c2b19cea6fd25ad5568b68ab98 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x34980c35353a8d7b1a1ba02e02e387a8383e004a + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0xdebd6e2da378784a69dc6ec99fe254223b312287 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/celo/0x456a3d042c0dbd3db53d5489e98dfb038553b0d0 + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/celo/0x9995cc8f20db5896943afc8ee0ba463259c931ed + 2024-07-05T19:43:14.783Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x30d20208d987713f46dfd34ef128bb16c404d10f + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x19848077f45356b21164c412eff3d3e4ff6ebc31 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x53206bf5b6b8872c1bb0b3c533e06fde2f7e22e4 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x07ddacf367f0d40bd68b4b80b4709a37bdc9f847 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xbdbe9f26918918bd3f43a0219d54e5fda9ce1bb3 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xb9d09bc374577dac1ab853de412a903408204ea8 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xe72b141df173b999ae7c1adcbf60cc9833ce56a8 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x214549b0317564de15770561221433fb3e8c995c + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xc82e3db60a52cf7529253b4ec688f631aad9e7c2 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xf3dcbc6d72a4e1892f7917b7c43b74131df8480e + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x62e3b3c557c792c4a70765b3cdb5b56b1879f82d + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x2598c30330d5771ae9f983979209486ae26de875 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xd4f4d0a10bcae123bb6655e8fe93a30d01eebd04 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xa0995d43901551601060447f9abf93ebc277cec2 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x40379a439d4f6795b6fc9aa5687db461677a2dba + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x433cde5a82b5e0658da3543b47a375dffd126eb6 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x619c4bbbd65f836b78b36cbe781513861d57f39d + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x1e0bb24ed6c806c01ef2f880a4b91adb90099ea7 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x0dd7913197bfb6d2b1f03f9772ced06298f1a644 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0xfbb75a59193a3525a8825bebe7d4b56899e2f7e1 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0xc3de830ea07524a0761646a6a4e4be0e114a3c83 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x3792dbdd07e87413247df995e692806aa13d3299 + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x527856315a4bcd2f428ea7fa05ea251f7e96a50a + 2024-07-10T19:43:34.135Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x292fcdd1b104de5a00250febba9bc6a5092a0076 + 2024-07-10T23:20:47.940Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xd749b369d361396286f8cc28a99dd3425ac05619 + 2024-07-10T23:20:47.940Z 0.8 \ No newline at end of file diff --git a/apps/web/src/assets/images/extensionIllustration.jpg b/apps/web/src/assets/images/extensionIllustration.jpg deleted file mode 100644 index 48f2bc5f80601a16b60c4598d8792fe3478dcd5f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 244079 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 ?-Z50_)#oT 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

    2Y#29tjA=V5FrOF($Q9;HHZy}* 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 diff --git a/apps/web/src/assets/images/extensionIllustration.png b/apps/web/src/assets/images/extensionIllustration.png new file mode 100644 index 0000000000000000000000000000000000000000..2c53c063430a8597c95d3680178f9d81323e297c GIT binary patch literal 145206 zcmV(|K+(U6P)8NK_UrBp^V6017{-tuzUC6H=#D9lN#@JMqW+dG^`QbFVdL?=j}M z#<=Fa_jy0}KF8;Dj-!2^=eh5-=9+7MjcZ=xV-~qd|FOT>#eTW{Yq$SBY$COczt28C z+w1uI_CqfF_)M?4p11F|yryUwlGmTNKQ7z&{P1hE%w0dY{oC|8WqhWhpX>el*|XcV z*V`Wt)vp=1$X3ViC`Z53YhLv66VK6dwEXJN+%~V9KEt1**X!@sGCr^VomIb%CfP^T z`f>Yr8Mld7v3IYoUVo0yM4P9L&%|HTu5JFkH;v#``}ilDOx;kl*|h5CxTl@tdi|_! z<8!^nTQ2I=ygYkW&+A*yI_qaD+9cM+x10BiKdX#xt5nbG*Nfj@smrv?!>_Msi`(DD z-&;So_K+M_-QT-Ved2F_$MK5~DctXE+FQ1j4F~^Q@7I4Kb9vSKq4jtT|DShJp?>h^ zRBs1;wzi*sx0M|FEDg_U-d^7J-7C3m*)M1V>3QFV_Uzf#cuw!p6qBG`^3%=azL+oX@{tCx@d*YBe&AJ6y<^QfHJ4=AH}8Fr6;-_eoU z3I4lwt`4Ts=YfXLns*crc669tbDTK>tkWeIikNI(F`aa+I*UvUzIPgMN76zUF zJPt2M=E;Q4#9%7)xt4)JwC~vwJn8X0o>hE;ZIzyfGB;l^Y0z#pCsA~^*N+Fg(1K$3 zZQiC{SL@`O<@3Zv=SJByko9718~sjo;8t_-Z2TTxzkS0F}|MW(bIZ=<(6%fW^$1{CQ=E6W@*4HV0pN;B->#F2hZ0lh4b)445ZKF1$xz$NA zVa+m-B@#%_+RI;d$B^x*zw~`{IkA0~%;i;YL+E{sAy%O%^07b*;3 zmCXmj=|HlUoSrkP4D~5q54~rjarF4)guvug# z;gozAU{q{KDyLm*lm)(JXSOe`uZUj^D9X1Jq5` zE2-#vR{14xM6Wa2q-7sRJ&FUUA~VxI7uG?i=ddoxs)m0IG4nx0w%Ro5vXM6##|-74)P55=n4OfX;ruttn-b1SSDkT7sBH zfW{X4mPpJ{oJfKhf4N{@{Qeh*Bk!KdGA4lJ06I^Zb^y#;oNbAd)`{tAFjd{l!MF=H$H$z45sWb` z7#JHEqx+ukWD2j&N(l?=!x(RaiBWKH^?{`z<;9tubMfuGf`-jCS~R}5^xFYtGrdrXD~a9B1}LY%1~mmhH^%48{V=8^T~ly#KMTinq@g zqq%m3>5g=1CtZN&GMfT$5oD&Mt`nKp?^wS|>g|iK?~cC2It(3Apab_YALH+mZH%#I&$@Uy7YSw-a~o|G zjRwR7`p3<|$qLiI;)A35!ykQM1bBh+sey^CT(Bajr#37G3=}_`EI_qwHSZNkG|T`; zf480X;pO;1fs#4S1Wnz4K3p7?dKnjAs9=7vvK`Rq`#w0>2sX#-QeC<28-Xf+29zBZ zC(z)a8No^f&~POP+dDSh$J0DX%+}7PF3AN0U<|*j4L=(4931%ToTz~|$x`Qlf%d`HX` zn2v1RAK71Bl10Qg*y;?dV&eIDFAfSZ(Xlwa(wqd81V57IKtKV9{p;iJ3vs!ZN~@d< zu%dq--f6gB#|D5!r%3}k>!If4*XJlDxYgX|J#Wdv;C`*+Gapz-OYpJX4Zy2+iUAQ{ z*|0*NMZ%1v^*DUc1UKJvfZ9O1?Xt{yAlukj6zz+*yeYJbFLGfEWeZ?ga_m??m|u@C zq~1?Z5&?cI-D4A-7UJrQtK@nHTvwfL$3~9 zgVE>s75KNAT2MNX{fCUz_HZurN z(3ot{TX;7Br{2>)1_K}EgTZ1`2u~g?PB3}tWjg^e9tUA|bYzUhyb9^6iUB#=D%xKp zCJy&K7M8Nf9ah~(UkwykJi!U1JCh>WLAo-iLxnLS=U_VY`U8#bjcukPNlkiqM>AFUpl7Qbw_cVqQp&xqColb&(mPVz^WS z40zT?im`b;p|mjIcL!5&_N?CimFh-QdRRA`1Q_Iv37F=mU;xOhce0F|gs} zw%?h##J@w|!nQiuczrx6Sd2xsCtJfi6*$h?HTO!tB!CF;x6wHBcSl*B`Udl`(k_hw zHlBfZ(Yj(B#$r}-!UFK(nGlo-rtLha4O+G(%9ifyNFe_%6NPD%m=~ZEEWky}Mtt9G zQxA^L9~@o6(c<(*Hm0Tzpz+M9LUr6#VDi;#Zl}!GrKSV$)t`w;W~-dPtX_|W01Jg) z^VK6k)sr{&6$rBrc4zZN=DpMHBK{q~mZ5H6;q-aC1i&e`d+Uu>fepM|#{yTq$?D+? ztoG3{zEhwBhhJOg6ql)Qje-zg6svz{nZGj>DnY@2!$` zd5QIvY|GFuwEjIEq?d$P@;=54>Sgu0D<8200gfCPfdoFh?;dtC-TAVCcEmcZ!jX7j z0ENdqru!z72_Emx`ZUJZRcIhOAUXX7ul_PmE!Q)BF4iR|<|`()mw^cub}%S?uaJIx zb+!Z^+!%PcR_Iy-8GWYn0g4nTz}_5Q;FvV~OqT|j@Vdj4aJeKPutDPYs)?!898f3O z6@B~!Sis;8fV7Qw)Ms3ee14Z?o6u39g&$mTqRr6JY(K^hXlHa=6EraM6DGDTl`&Wg z07U)n*|T|J+_t26!wDK@xh~TVQTy!K1GTHSJ-`=eX9ZACKG-^eF}z>%aR9Rq*X2sg z6+5LdwF#43Ebe~2HdmMtL9g#J0S_>8F=j;~!Yvl~k|xSfcIYcy+OvBR5q5KsW(5#aHb}=mybAFh z>>!8N`Qj7w1~B>!HPqXC_A~LG8u-m0Mj!WF%uHmp@i|~~`5XX2K}W+FwKg6<4b)U; z-OgOc;9t#5u#Ll-W7w+Qd`&7o4(XL*N9%RQR(LDX%%1&}3k4&~~UZz6S#kX5}r1d!b>1 zIUeg&$u$6LUhUaW*pR}Ph(m`(@;zV{!P;27juA5MKaASRPCzHgV%!*&H@8-#zy&0N2+3{g3vmqAJq~Q7=KEqy^xL{ncRWyJVqkV!M|9o5 z8ps0}5iTOhqMSVyARfe2SSW-`?qn}p)G>C@q^FK&j^VM>e_Wh?-w*7FnVDQeX+ys6z~#$N z`J96qQ7e1+YIM+`(&sAZdGO~y`pyO~Y`!nvD9Y}@|Ay_M*yZfLdY(dPxes3+n{+am zBHJFy&`wt@Hr4GHt~~W(cfKvR`oaf6$7+4%k39yKYOdg{Wsh;cnV0rlIBkB^(SF_W zV)f01QRsWlp%|m5msN7?8RN3M;`S!Qn2NkDJ=fgu^(Z;s*sK*R46hiPw$EN9U#conKMm2m4TU;YjeHRT=F#w zps*7_0l;Xq!P#mQ6MzG2rU|V{@V8(^%bQKKC;X-O{n)g@>qzw>86ALeRb{ehT(<0E zv?omH@oqiW?2U9t@o~dzHzF!+O}blK#rxX89qG$1$$L2L`|wW5jhZ z+66w10LSQa-YnRA9NC}IJ|eSO4&z-PbjZXg74KVn7iT0f^6ht9hy23Rze7AfI!Y`5 zP{uk1g{Efq7Z}t&aIB0o2QAhx1|=92K$z%wD2T~pUdB3Yejt06iNPKdy%2Jyci4TQ zd%DiR1dVCyDbZZKISgd(&%HOH=%re&DL(AF9auEl)#E;PqQgJ|TZDH4Sm6l-27%eE z00JVDPfMXi!WJO!c!YG{`-H$8hMgsL6<~%s4P>5Nxc}<(BNkgQFL>{1(enGYJ;w)w zQlH0Z;_>@B0f?ZCYqBc1_9UPH!B~qBSRWe^0*VRhrnu{oZOE(@#+zQN%pU_iem}HP zuw}k*#m>CtWDFB2m>8IB04fxGy!()TG0;c`Oxo}xQLyl7cjz*-TP19}U%C$VLo4<^ zUd%gF%}<}+2UCM+r3+~Ypga8@Hh?Eb+xT3^)Wu)kBY^CG`rWfFHKnd>%ChK6rdr~G zkgI;Ki;p_^?zCcwKM&@y!?lzY()!*pwg%kHO2yyZ`>s5Co8wm4`*kgNkWg0rfj~2M zcrp|*n&Wi9gWWd!cU2niu|sa-dNn6u^HYhr0X2{44Y0}xrNvr`jYcPyI5rRB>$OO@ zNMZL|dNMX$a$T%6mW%%l@QwLP(<6N$4^lWa6(DxxFmqwU8bJq@+S$wF4VwA203i9j zF-=21O<(StxRL`~^GJNQdB_liQ3oDYz~e5e!RLB z#|<{U@FlcAsfkS;GgMu|S{`Gq=W~KVqp{F2=Co`z9qWJAFJ%Cj+OpwLps)G% z!AVLXb{IXC4F(l{BJ3Cw9jr^-b7mtW@eqf@+Jrmu^BCm*GGQ{1Tr|*eP)1OWw)UMl zC@>D#5eg2Q55ADaf^^WXLI~VY6ZO_FFfDR}> zoMu}tZt!YHd66T>^O%6vg3~W{c;WxzeS~LUaW8*+55t8LhM)1^DxM0;Om^ z7kjQN*1gU!BWzlPbdAiIT4ZMVAy9&t`%t3Y(HMiP*mQKdho%$Sl0gw1RjqALnKq0Q z#^AN9%cPL8CPM#Qw(KiReP&%M>9$z&+a0L!XK~_F{T~7t*`y>T+wz_az`I(zVQm)4 z?7USbcLfHpFw+^)Tjug$Y7Bz7jXA<*LaQ3JmHoq9!S`r~S|VRe_uznGy6f}6K^p}f z=br<3#+WL3eV1G>4^j};xMw2cPvs>HM+LUi?wthcGIr%X^#baGq(pcw zoS~w>dn$Dd77R+>%{6M1-4*Oc6DuZ5%eFzgVWQxDr6+r36YI`52D`;zs_xntlL0z3 z0Gdx~9?UTFZDc0H9ORv7WN`Eajm=YdKUPvI2#D!^U5pohe(c75f`y-hy!@_0bpnMQ zPSyUrE1BH@su(<4JqH-`jJ>x(`0j1+yyKbAl!?L0`SxWL_}r24WzUrr57N~{0nn!| zIj}&nVwHq5@YO#=o|fGk_Q_W*950|{+KTA3!RfWt+u!&4J==03w~|^JnqJFS=E$f6 zpTsY`6ZGAlN`kVtU7Om(lmWD?;cx%0)l-VL!OXnfoeI9mLm%&Y`@Gz)u|=~>CcNts zW*sOnrIKx)z)7?Ymd4CNoBzI#vB(>yN}B^qeAoxJ429rSfAMjm;6>jZ{#2@1F9`Sc za<@BxaWq;7da?yACW8Ye$&H=RT!I2I1|8$IIGF|o2xYZg!%k-!uaKZJaV~-g#Tzt;iTD|3%ZccDm z2)!q%tmp@{=b2-|+W#uS!emlAXJZ6q)1U~-yyY$cjS}{J8YJe->jM@j?CLM^1uA?Xs%ZRX^=NrYfuyiv3v6^J~z#yK76K(iMBqBd9*HE}(9 z18J2J4TZ1+%NgSxXOxwU>#)MVg?jbDD0NBtAsjrqvnGAi^IBq+FJ=$*4mO}C23j;y zNwfW`UA^DdjPfw5WWeLe7vp0ru-n@{F0ToWkp@DG`2T&Z69wrj3ZS>4wVS6gZ&Kn}?m%!)*o&X2~2#5$X zCfM|jMv_q0u}=21z()EGSI$@qpijl8RVZq1lPa<11q;!0$i~J~a-CuqgJ62ZO-0^^ z_F2z1M0*tPL%4Blq-!xogB@zAH>;$p8*e0m37)x+RSZV-6~K!FyDjV|6WU<^)DrusKbKc@J$+&KflJ@ZU~P=Ii* z5?_F%&WlvA0X26ry=C2CwgH2KfhjEq1hXQ{GI0B#y{)?(q_JZw?D2t~C23yNql--J z)<(8_X&JFFnUPQ@Z-3;T!DOGp@_K@h(uhSu|5CqTp~C0g$07O`Z6Mxm+ApT|sszob z647Sf1^_>?`}627V0bidD6rV>VHfBDDitvicn(K=XTGi6(4?s z3Hi(`f*dW6&VF2K`oF|?#{fZAsDlTb6 z%R7AnX_?4Bql>}wBOQc+)N#EX%;Jsyc;=u)4jLPHebisS){yJ>eKP1zL|Iu~+5kXT z{|U{!`vmlyGUW!kEZ2+A7xj^WLblI+JeIrg>e!_8R5+l&<1_!edOH^Ha|^3>|7s-Q z3I%q~3LLxkG;dV@71e3;7I z9#yi`iE41gK-qrh$%N(cD~U`j%YdqfY3KTFGpnS>o=qg<4+ z$)q&Hyd{Hjt#l3l9_9|OKN8r%%*0VOjGnu9Vo;c80z^Cm3_zg?1kZg7i{5h}z;}a_ z2V!Dkp|-W=%y;aPO1aVC#&?DKr+ZuJ^$(X+=;%{6QQ>{zWXxu)7|^sJ;jF9(e1WsG zxBqeGc*Fa5$?G+`NdFE$fS&iE&S17GJvaQgWVGdR%`qWV*U7Pyp0PQj6Ytq`Ota6H z)g>dT(wwACbX|KWm=%Z;RorjPA@!cR#iU2h+^~1{U-> z{i4=RhzKn{D_6#=LcsRXUEl;XFb;#n8-Jp z>lixW%v)q)JuEP+=A1Zr%`G{^_^{0gq3$vzp+lX*4Ak=-i?`wK!_59{`ECGW!Dy5; zXEDoT_thClz!&M{dTlb9$A{2d*Xcz_|O8eQ70Z`9>72n zNx+0jjtTHm4v{nZJ$)xyuE8QXVd7B6k`ti$BG%4y7TqfC(nZ2-#?Gvk_vVt(N>H5b zoJ~2H7y-O6c#0=J5{X3#M-52NO?B)VTp@>ll;oggrV06W0I)i>DHcHhvr==BHL7;Y z!5tp^+gv^tBlKCY7+4f&p^`>m!N+7Pxj3@JW%yb3x&Fbf)NC6pdSF?&eK1Ztms93& z>Uh3TdyKl)cqwdgo}Ug>{Bv`_?c?W{p8RYN9{}n@qX6j12LRZVi~p{J^}!zoKbz)d znSY8iIA3;e_V=8}{>pWL2Kd9oKy(1LmyDu%??``}7cE~lz?OH0R$J%NvznQS^>Z`Z z;JPm16ukLDgB=tcfAyNHon6JIA~FcVRw}1U%LxEz4#ToJg`9m4-3a z&geIPCUhp)DX=}A@x05C96bEa~~`&sr`Pr!!yU3_sv!XKD@rcA+EAH3#oQCv^>ViG1Sh;Z zK>J#mKIm~z<@VZg9H{WI55cDR^_4&b-i`fD(D&irBvg*%_cjjBd`I*#KrHV*#p@pL zSrTyYfo=>S`aD-6k}P~(sDGf48by0x!Uz5!KzsF*t7CrE>_6;qeIkz$WZS!KfDbh^ z|H_Mu;JJ7QsWYvbm@fWL3sA-divFRkUsnD z257GN%;)Zz`tCl!iu$V@-r*Oouhv_ESFDnopN?IUdC>zR5n1%U4-U)sQU(w*&f8l9 zn$%L+IP!b!sTo4c`16h!y%`;)RC6qlqwHe!5d%0Fo=_ZZlib{Sz9!oQIUqr}c5D*d zZ*6={mj)~jv&cBR?C`@VnPLYs8A`_Ij532@Y{8V^phcUbMdENlJ2TY3 z2eSkGYKj`EBfN>xoUq781YUPg?`;4ug}Pq0^xp^tHg3!8-#yuM%=0i;Nv=D*3Bcwv z=Dc@(IOVsTqcF?xhIFI(2$0~9iRpkjiDzAWgA0e?U}$^JcWh3&*Bi<|nC_mN7;IW# z9*+p0lGgxO!8aw=8E;3C?0?jo`}R1pJc~Z{zGh#SJzhHNt93^|Hd@DVIyLlc$Nm2I z^^mC8#2}RfJTNf^D=&9o5gp$o4t|Uyk9@uw6XQ1y&|hr+_S=gWG`?NjB=QaLX{eyJ z#IJqwO%r?#&54o}z_bbS)d7PnSO`ppnw^7A<&3FNmMGU0lKXBTAAzl72!-wGMEUqM zI_63~8bTcy4oLnPlw+jd_rV*~Eh#z<-=53@zOd|j4mK#_%?I1VC3owf{1iOF@i=^W zeBj=PAsgWG&rYv5zQ>@1Fen1hjMH6x*0FPN?C9Ja)_sAEVhfq%gp{{WLii%#-Ky^r z5izlq1S#48rL#jDXPN%;1VyApHBB>P67s2GX zZtaqbtJVD~VZ!4~&S29nCc=*ef<|oBJ`q|rVFGjc^9E-I<9=lAo4O{h#Gc|QM_CEQXUk56J@#Kpw7Jpj@ zkMZ1ij^6_o$hrn5p;IE7W#4lx7OVw@=C=P1YaZr-HC>HY4F_P<33*WvE#GN9K)IdP zXeiU2)`EyMX}M;egoVM3Zw~wrv|y1oM=EV7a&$F=5BLNXNwBEDa~cL_xiDA5%u`Yy zVeOv4RZnmO=C_xe-y5|x)>d9W8(SY}%D@5uaE8bwDKksKx&v*8O&iz^1ilo0H?IfQ z!`Ns6TuDxUv=y9h(PuWEO18^M_0n=IAg7S24C_4BW67Tw6tyN32iJrEWZn*it{2jQ z%)A8hHJ}2GhbWyu;obpIJQ!Tw58?NLYgHoRBdawAl`|~C0PLB@Ufbef&zjc2bHY}{b7IOv!bzf&@rq~`mDGjh-Idwr6Oe4;*` zfad8S=E+wL1sLiRqYr)CU-liuy7y#jUK}z}M(O~oN?jS2&-(qK&A#dy1gjvTj4YP7 zCj;`fBe1h(xhgnEN$=BLOa;*K@((nYjPisgqCV(7?=U+TL~;l8zOTAK+}2^fDr+ZN zn6)>y)^GQ^hP@8R(Lc8~pq2NamH)OnmfON*Z{+w8i(d7|>RCxRk=o8Y@^NFw6V&WS z%<!A5}BB9sk>f28H_CpTUPk+YBaU1V6yZ8NQn!LDAm9zDoWMpvHO%Rt2n?cxP--(0(W^04v_rziZB_ zY+in8IrXR+Cvb+k0!-R)sstLkgoF4Mz^fz}Lar?Q?!z(6`T!Oe=nJZkKJP7eyFf5X zx$6u2^OQM>9-2Zw!_M$X{T108{f)$fsAZGtm1KxS|rLf9497@WN}HC!9(2X+It z1`)u)dKG3ISN6ye!_-#u0siC)1O6$PY~hUS6MA#N(Wzc=_ynNP7sEB!>GKK7u?v#i z0jnT}-<)FiY-mgzT_nWOItB`#m%?U-01n;R@JjFD-#=VrLcn3t@Cgzr$@}~+<~WN1 z;|l^j_+cTZ8G_y6q%7dST01c*_i0L0K>NkX-kCODF6U;8UTPl z$2O|Ypn<)DH#^GE>v-qVdPXYo#KGdiS&^fUXki23jrrp51U3#iY%mY)JMzyl>UK|{ z7LjvsJa^EC`obRy_abTv^>FYrdK!l~YIrho#=q|?B96mVX5_Q(sQdPw5kU$l-ty12 z_Fr_T-td|%5{^SSBn9Eydrf~E z`$pQT6S!=-76Noc9ueIvc|Ms=8;tPx@o`@ESs4THHy?L!aBq@x2fhx4H%}P9U_6UV zzB2ZfTPjsd9Ns+xTtp(uo&Y}55J)OcIE`Zd;63#kn92_yM&bO6`6BsFlMg0ss<1A{ z0h(Q=2p|RqcreOEokIDxRDv*jY?5;jShchSLg?p*J=a0>I{+qcL_;Uix`;Yf(gU01 z(8ujDrX&@-y-UlsM%Upx_)r;M@MslQ>0#2ye)O45=ELt94QwHaWc zUm3viIaFbC_c7F|ju9B+Yh?BLBmk;szPPcYzKJhwG@?4>SL~?q!WW+?pceHRSAR)d z11DajU;xOm&wK!O>R@lbT&qOVCk2$FBu&XqvTyNaNVnBkxs&d0m} z^&EJSgG6L9ML?kkeRpO~VSIA56BcB&>xII&aTXR5gjL!_vZ)gb6m~j7 zV^e$q=baYDQX~Cs{#H*CQEPay916YY4uZi_ma-G zFWjS1|2=Hf(B24NcGJ|8uPS4a-qp5`vP-T9IEo=mBCZ^f z5xD#J2zQ2$fXy*q=)=L)=PZB{)(SWZLm6P+*x2`B1k8vzizt&w`iy32SqFBu#XJg! zPEx)wa|0Z}y1DtVgv%3{jtEokbJ@@vcvtD<$Kl^M*Hb8a?0RGlTZc0Bwu8ghe$**;X4&XTZZ)f4h;+@LP`P^1TvuUp$On3fewx57nyBn zC7F=e4fIr2VVc;eVM^|fF|c4}FyM4#{jhOF2ZRFeGKI@pXVx6BLLC8iC!D!@nq>gg zpa|oztu5aj%oUQT;Yj!ZEF$4x48i+e-5-bZvoM%zcRLDWnD*QSz@8gBMlfehCR1AH zw(8lZ)88fQ4AUOh1#TR1oeXdqAgJWz>XOeeU!@~=U5(n4O^!UtwEfI-vE`wf5ppTC zVERvhGxQ7UDKzHM2N+A|ncuU+Vj<-ajLVS#=yY>bIKX!YG4J~7gWZHODHeHj=1xN$ zZ?7%Eg38-Ym6_j6@iUnGAHtGhufz#%7QxhpQ?Sv5o7)x3SOA{<#hys>X@wE7gu!fp zs*j(rA8Q$3aXJcN*I#v15AdYVf9m-YdCaXby;p)RmcgQY%9nF2M9)eBr>BY#PIoL zY|nN41DL5`Cot|diEv|>R(8KvyxD*y;UHODxkqXXHz!27{yh#bhff15E`S!j=Aot{ zNB#9W^9LklWt%YijL&>J-NByo!vL6YumhfJ^UJ|pC{ZloTF8zlpip2~_TxmXZfiP+A)kkai5LSVkY0L0E1AVfr_s1}%eDuDo?Hz?R(aJIRg zm@Mj?%bjVjU2+i$N6wnS0zMS_PuoIGO>vZh$mHY<wHv)1P;ryt%}bHb=-J(aCdkbtw73?OySIpd$z zl3Py$QFX98>H`FO%%!H*v|SWHRvCY94v5qrMFEWg8A)zj0hDLwHCAT#Hh90KgNRkT z4CbYFH$9IKZ1MiJwZ#vN42MDN_Q2LYB!EHoUPNbb_HCH8ZKL3kh)`;cfCuP!uqWd4 z4$iod4ZLNUSi^*sy?1A@Kt49RWEmi<0OB?wAYg-AVjT&kO`wB)W$g1U*S_xEVe6g# zED||nfLm=zJtVT6!=DA#K$x1Xt^7II3{IP|LSO>pMkGVWx6~P6%*59XWNSyCGb0gx zIg*<-3=%Jptr8ZLlzIy#C>5=5bNA1oxr8 z;+P8R)5vJt^I1D>{Oq3>vTZbW&eZ#y79frfkZj9Xg7N-cg4G+>mQdu-a|Mmv+#d!F z8cF6v!ofFQmR}m0aC& z0$JI#&K>SgfrSC)0qd+n5g}Mi+RF?Mlw%_hVG7+B__`-s0}pZ}W{ZJYau8a;ggd^5 z9Z+)73T0NP&3Ya#7<0Q;)eO>@y0u%x_csGuk;_-tq6=P1aTnqOnrVa1U z3>n4_%!XQ5ut^UdK@ujZCgE?&=1}$(h;#-$4@z|(piu*a-aS7Oz$ohbia%7D8TDIv zr;cmhcMS&G6pfC_uZB4gYNRWXA4zUMgfiAd-PbmZHK6#&@a4C9l#hHvyHW(zmNzMg+GeD zmVV~Zc(#5yGXplGqDi!w{Kymi&Q9C*?~p*R`t^~O)?a(W?~Z3-o(i*#UkCGpjy>)A z3pN&AB1$Njl1gkrPrwn%2cXEGfwJ|w$eZJJz4&HL>!<9^)!zj;mCYkCYnXB^S?@5n zIHST&y5Uqp^MCrChF7IJbDLBy-`hFNY7R>9PB7z%q=Gl#Kw~c@VGe^bO zBCt|gFYO1Mxr(;oHYs#%r7?xN>~tS9Rh9ajH~*+(I4k>js6IBU$ejJ001N^#3i{B^ zptu+HW$)L{ zPg16lRkA0Fg@-ii0yBZ2Gxj_GnUddwMg*OP28Qnp1_!`O01nirj@>ppa)k*Bpn@DT zBSD|P1H9-t8dW5(ncAl$NW$jO)|oRE9UNx$nP4)S$g~q;@Q3$`vzfX~PP(xJaODoi z(;_mNTR^iGYFqRt>I^y@W)v;M(t-D!d;!+zGwf!jkZj3zLK~nhE44kYiA9tm&Tv$Z zij5g(xG*k?+%Z^ayW)(O0~efS0y|blB37xGksU|M{Os{*(X<5sT z+1%;YRfak3%3y`e52z(7*0#_x^m&J`@U}(V0?dwmaZHw3fzsa56wkFqVbZ^_B{cm3+l8;&rQ0eZ3u;13{b%*Jy~KPDorA85KYT||LN6!r&?)ByqVMX1Ntc(DPA}>WA#U``l`!X328~v733~Mje8ex6d zv;7S_p2U!ptTQ`O2m5zsa0T=%%I=cSFdY(q;K6BfVqYbxIevxe26n}Oy-n+}9stM# zloT`sl#C6mPL;un`g$1@p_kEo@Wsv+iingKCUp2@_#0oHZVl&D-=PFU;N>zKV1s?j zhDBCed*+=}oql*6ImlS4#%{PIg=z0n^E%Loh?J6&3JINNCwfvuN_g%|*2O?WOT80K zjz_uMjdk% zeCYGOjC=^$SH{ZdXXX&>*X^V!zye`n!ZGh~0Hn_jUzwHz&km0Ca$k63BV6Z&YdpRG zQF1H&=mYNJzvE0O>(Ft1kMt0uPW2qgwb~8%6sHBuPv+748$K~Qu3&c1#a5s3L}SP% zsEANng33F4Un}+ReWy)>Naj{?g;Ov-+xUFnh~W@dIzAuaJ5s&hrmVcK5SVcY@~lfH zIX&}pusnSQLzH2&_Aa%Qv;xu{63c>gEFs;XbjN};NY~QcAl)FSASp;lcb9Z`cQ5gA z-uFA_AIvlN%rngn2jH62UYbeY^f$jk!bTW0J|=P^Iy7 zwRUoZ*-Z-n5g;`vvU-#5BLLQE+y2rIM9a+|c31oK?H&H5TA>_VvBIiVmVE1(c05yz zOwA;~OF^&^i3eiV21w1$GfQ2&YRs8~m3RLkGOX?BlDrC9l*s{lD2vJ^&SbT`Lsyl% z$tavpFhhIn8h3EK?_Tk!P`)}^?z`DmuEm`vxF-QPE~o?7a(2D@4-Q+nldhD8W=P@+ zL*ME?eo}gMxk-@^K+*B^Kf6118XsMD5`s%(e9y(iQJF%)dE0%NBc?j&xv=+s(~B!_ zEl}zn#7w}Xe^Ar94`+Q)_p$K9^~C)VTz#6QCRTnSIYOt|l)si1_aqVB5~3e44bV z@~Tz-1(^XpFu%L1A4mAwRU`O7{AC{K@$liAixiyakrXs?=X}+vR?CH?#5f4WR9fQe z6IPJeHZO`sa-X?Zgh90|e+UwaT`3iyJ;APhT%DhlrfuA+NA*&hyFB*2E7s6E)tnEO zj(C>(XIDz}WvGs}(qgNkXE#7_CKBhv-=>+M86LOg-@i{}Et-P1LTT+VVrL)dFI6hE zLWvoP)2xw7lXb=mb4n^q)`VKOyl3xxJdFIc-qj5sQ5+Ps4hA7y)|#^K1!yNfi^}N7 zoyo0OW$r$^@fp7T&3E-vHqfjVR#IiRvWIp_vdZv4=Ww9ht#7@wQe*K%0w>zc6NJw; z2B(wa$BEzEm>cnv0Mh|n%+VJ77|nTA1_B0)MD1aC9c+_-$L(To={Gk1H1%!b1{(05 zIGTuJ;C1iD<)E$xcD^hocCD;AGIWAC3Kk>QtRWW5zZ(w;aH?os^ibj#Fz{}n=-0>> zgeNPM$P4lPd$0g2u(x(Csv@2Eftx61VG1SEK9f-mHDxOVHbNqrBz?m#{xX{f%K zzBKaQJ>5sU(lPC+d3!~%({z#|HpXzrcz*;1%Q43y@74V4H$>zV0bIdcq?Vis;T`Lz z)>4Av09Bwm5VP~-gH_AeAM`qfiSK#`kJowvu|;FT3yWDrI&L2m{JT5Tf5&cXc2tLhx*vvEoY7Ytjrih4|VN*=As!AEzqYeO}kH3 zYf*WzzU~a*+Vr}N+8||wKb&1dY5#>o=XWy_A4dcW9K|s9a=+RLfBSs9ba!z;*~z)z z*NA%(VzmwqSu(j^u6E!*5}OdTHDAY#-?0eBwHoF{BE+ggW21$tG`oymQMP~`rTN;{ z?OugB)Mke$t_TV%2fBA#jEeT^W0TRKB`1@!-Ne>PNzrdO&scjrBQ91>*_fqAV(>|g z`S@OY_G$+;CL%dPbzzq1d8?=OV~yxg00WL=kh5MU$iGAmCO_{A1=NV7%(|sMqbwlN z({$)Y%XJ^(5d6s>v5nka?$dq;&>Y(XM4sulQs!KxeF3W(LqHd_o)qMMKy9*r81f`v zc+ z2F%{Yt0{WGnFbyAx0Nb+c>Qirn1wREn&YsJJOCNybSvM!p1?m~Z5fMqrE8Pqu3I2R zxiHz}OR%>Pvy(Sc4ze_eebGjYlR6}?os$XG}qRD%HzBzQmsTCJ+G*5$Hw~=dFRrKp@PqL5x@^J z@V@Fk9A8k(EpC$&Pnv$M6WmGpt^x3)u5yuEV1&(F?;wy4+tzuIpW69?4&$>E!y4g@ zct^qvRQ#%ex<#%Q(H+YMSNZt2YXW~L@?*PBnwYzlTE6GljFpBZt*SV^#vb)80jUFjdG%4Ss6udA4LkJ9!Nq5W`9` zOzkH|(H8Cry5U^L%}biYZ^AuUFpt#kBw^P4dh5cAgjbp*;HdRhtJ|fIr?GX%b=SXH zER$oc)_$Wzq0$?mY!xJNV#?Gl^ajsM>%ki5aQ}}@43fd}aJ!!{woq?QBrb?=eXHuJ z3n^wGyr`z)nUb({Goz58>4tXAk!T1y*pBrhS_zC^f?SSbffS71MQk!oD=xI0S6P%IzQ$OWKBY#-bEGUkm#xyptZ`Ueknep5==z^k=)YGZU_JQsD40Y2{t;YJSKKfuG}PnuY4M;z zu*NHXyT)q9N#g+ueu!`0vP? zcyE^m%ZeBorAjx*4I~AOSC?hhX77E^PV|(CZndGSc^KVn%Q(z~J1!m*DxwthrMSs3 zezw%28(yPHAOv8mELUmlC(NTdJ`G4KO^S!S$J+*=^`hiareuXS9B-TJ*Bn*O0&Kga zO59QHJQ(mA&9vmL9rWwI+zxobxC#z_~`gnV}y-?ps$>?+j;9mWyX>IZHazZHgF z;n_mO6LJ)@D9&|MbCxG*6wd%Ff!YYRaXA$yff~#US7O#P;POsD8kqIvs@4kibsx+W zpKn2RM3z1*gf!i?^$Rwd6e?Q?tbjtfEfFd{2%J27joa!M+WxMSjoy#SkfcNn**K3| zjVkzK>q)jrTmDcJJvNkmqkrW!%|@L2(5rhivFs1z`Ku!BeG51l%U_Ekt4w=6l?-A3 z;KZz*AQa~g=BROdaFRdZ4gJtec@!t z#MX?J=bK+^)oNdO)pF4zv6Z^S>M#e%66^l^A)6&U4KCANJ~FoKxT`h)+jU@S!P{@e zG37n@zHBU1fFvRt5H8Eu_YQ^av#5)g=zaaR3LxJ2$4SroFE~E#ZHIZU%z{=3#;7N` zb4EAtG>BIDeM-HiU)$`UD1Tt|XIe!y4mMW#qcn>0J_M*Z%B5@WSF&zwnwHnIK1cD_ zRp=03UjAI%IJ9s{mdm-!4>e7$^RSK+9}>(>C3lY7 zdz5=&o_P?|s3P4&-C%gB)!(S7s8=$wJ}P_42MZQmG!LS82Zk$fgm2eC%>r{N%HnPm z4lTXvM4-8252lq&JTSaz(k4~;MB)p_=0QsT^yhwoA;D_|7qq`AXEWA)0}V2uEiiR> z!N4c5_t(MBWviC5^_-ss4?9?u?|**t<@qZOD*J`&+ht!$e(1Oh5%Q#QflRf{ znw6J!-gUs+1G_TT56m`C@FT*1NNj2UbH}*&wEO$(fv)$!+S_eB&cZy3$oj(RVc*(3 zI^(mn2QLzA4{vv>jH-lMfpo9J+Z$~yq;MWx#;6Jf0GG!n8WE2XK`b)P-P4z*q9U7S zWyt0Qp<=o$66v#ukHDxcU-Y}%W^8{(6=PVFE+CDISb?s1o?!;<>Z(%^RdZVV=Uqh| z7CU7*i}kkg{lxTZ>RH+~9vim6R!z3qDWL8fTIn^}P%i+_ksgFjHOopm9twUs;g1xf#nr~4}Sab4|O7FwT0Clu|*EX*)cF+uUyPUu0$ zdtW9;l`mdOAJCzNj4b3{o^ynUQarJn0Et_kdCEva#tdl+oRXC=EMSu8eASpl#F^#F zT~p8=(g6@J^g0_(d1ID6thqSq7O_&}wBUtEt2juvedD9rKo!qhRMo-!i_Ukg!~U%+ zuHfwu?~k$amEVfHqhPOgYNJ`}09%oRGXD=CmS@Ze3B&S4iF5>HH2q&$INc)ht%%y?t)=G*R z)$OIQAju?z$oxnYa7hXOt~dAfc}v{$>jG`0cf0W)0t!*p!kB?yQ|_;}OtA+G ze%{2nZj8VFHu)%W{+H8Zz{$9%v3B5x^J92F(i1s48Hjt-&ctO}oIqj-_(kx3p#&8N z8W(T9GGtn#c}ltCAzdq;>PQ)~H@KZgn6X9`^t(?dK{lKP!bQ~n$&;$yW}588&?g3K z8}vMob}=S^>dN-L&@}575qxY1krDNzI2CHb%rjWc?ewLCB#&}kj>wg zK*r2DD*|Hg7OSv7Z=d~nYoJu8%DVqN5g$`80JyqpQLrv zl0U(a9$PhOIhB_1&1+l5r!Z+{P`g~i8AgLNA5@WEIMsZCW6 z5N@p_TiPe2-uJInC@#%@<9p=_EBakzP`Hs3V%jV!$_!!ApzYHQz~fro$lI15&?iGB zb)ou8!?tm>=SRojt3#yNY}Ig7WBw)P6Vpj_iFA-Wz4D8c8phAV*#RQZ>c@gVXhV-zFyz>E_k8p%RxiviCQT} zpx8!^gc=A7rWH6xs7~~HINa`5Mzoi=BU}BcuD4jbJg^C5OY@m+;DHB1P%f3`T9BR7T7KT8C zz~Q{7t%V{AqICE0kCj&9Ew45;Kve2hw6tyK%c3vS9 z!dItg;Ez`FsJEZ*mdAJABdX6Azt8o2E}-xgc#GjnBnqzE=y|&qa$!$?h((l9u^`#J zM)qF+B)y-kme$7(`fj~=?t5ekw&I5bO=WY)O530S$!mB>jxo>{M^r3JEr6SA|1Z8! zkd#MwY{y037tLSG32PpQqjj}+BED~DgD3U`R5L!k$&zXcHj3su=wc3DiKU;kK<-ST zY*S8C69m$M|$ zRHBV2C;yF3p8KUhjN&Lr30_Hu|F$T9{lWyY+Fae;c5p%APl(tspu`kRy5g>Ul>R~Q z^2z;LN^zL1qrl}ySHy%a{;T?@=1iAMhs`3jaY11Zb&)YuVh7_afg&s<=Se`GRj5Hs zRyFuEXVv^hIV>GB_Dx)xu$6{U@{g6)@n9fc%|(Vh4u6e7pHJGrfTxNe^zE&4vtRh)ne056xEWFxAM-u{rj*YF54s`sic~4 zmJ0BT_SEv5ZfueYHOvwZc3NuIZ`gKsE9bKS!ksLzcmXE{NZ)V8AatZcz{)8Xiv2Vh zUwtwd&PR1+)##Rp1dWNV0FLvUXqXB1z^*ATyDL?_lV5^AuqNxDO_KF^c%^f~EDzQ@ z-30Jem#$uc^@ZvP`wEauO%y-`Wu%Q-Ol6fw@GXabq)!t7D;jcI*q3E2QvoPKE~#Kl zoPR!9xh$V7SB=Ql}+a>e24X*eZ%3e}wayR1EM zcV-z~olLI<+o>wBQ57AVkC)&-IyCUBU;Kb_b4@j=1OaXMBoDW!Hgt9dG#bW%KBaJF zDn@@vS`ds=LRRlihTH2&9?6Rjr9Dmu1mrkb-u<_95kpGz}Igdn0>W_*R= zqq^11X`uGC-*!||wS;ag1=Z?`wLfdS zIov$D*aYpZotPqYdDl4e@-FvRp+1Gy=wmtnIDdA+pJ#2jtV~fPf6adK#X|oJ&|h@9 zS5Zo|e^Bx>TIM^;$n(Z_O+Rc@Ubp;0p9KR$`VqAb0h_d_*NWjYxJ|G$`i@$9oC05% z{S7%JyDbFXJ!ZQ14V`o6>taJnP=rB)61!V*ki-$OZbe^p96B8+U~bgjvu zz35qj1)lPW>CmP3xZ`Liuhaq55*=n)nX#>U;AGJ!So0MKJlv?h6*;iIaxNyEYQhE3 zk7HTp(cfZ7v3yU%o1?WrlRFIpq6cv*=2sVPRHql(TQ$)_iqm>c;Rk0e;U2(KfE7z( zBn9=xev|oMX$p&zVFKF%dyj42VUe(qiyzP*F-M?C*WR6h?myF7Fl?TCaG~{g&!u zj9kYG@}vtRcnbOp0_(r3;PMgfOvG}|54#%9WKi9A1M0=vFXlw7&B=p1RQl?6uDRzqB4KG5cf6Y3La$#7r zHpb7iMx*nA-HhocRG}S5qD&+=8vd<<0HYP$s<_5tc9%oKZ7z8yA36F{S3tdL%e}`? zSYGh>8n~OU3a;{2v54ZLbeT8^5g=O~9JPI$9?g(k@b^H@C1ILY$|#U)&2`fLVwjF3 zY+IgKO83TuJ6Y?={z#@xr2_fD*yEO*jFLb1`k+kV&<|_^;PRsYFeD4ZlZ%7!bT1wo zJYZnM_8b6>@vASYk>Njgx%0g!6V?8TwASzef08yD9|pt}M6aC+XQk z=j@QF6G_t?1&`-KtAN*a8ib!g7oeoqS1~c9i~gCg?aFe{j~`;u8J{Tv+}sAs3T=aVw_gCZ$5&kIl~<1Q@Wxuoh=GRjp_A7)IQ}=}Sv6jA-Jmx|0{C-7fqqEZNJ1T!1YovL*l4wV zn;`h;>TdK-2hE$&n5wOtBjR+D=qPTN7$|v9XOaq1C94Ke9A`CC56RIi7$ZTP|DkqK zF*J)4`w46~dlP;kqsYig6=Zy0tB6V`epi_7p8fy>bhY6~Sa(UU4n0|iE31b>=a5jj zKfjrfBwcA;I?cvK7WSF@yO;L|H#b0a0`u)EQ$ggFmV@pU--_Uxfz~E?aMyhxc9%PH zfxze%fQaOKs4(ii$*Aa&NP8?1e0sZ^&~DF6Rs`Lv5v5sn$tyBl&5MPJUPOE!9e?0; zdwVSaCnR^YfMc4a#0OX0H?e>{M?t|=Y!=BDqI{5+8D7sZjCEw`;g2V>)dH;Z6JRrz zS;sf+KGN522Uxiz8Q*81SESpVuDf<|LFLxlMEYS7=$JFFrCd>8Z`8-QG6I5m&$b6n z*&`tP7KTF6G4Sfs>iks{#xf44(v{G?qTJk~^2_l> zNqv(Wn0P4YzF7`XziBJyckH?mK~TEZZG*r7lCBz`a!Z(hlN*`*v!A5-$K5$j3Nz4= zW8b0wY<0{~EvSvDR_<7bn0~i&NBc#{>(k@hTGHmqOoko$$^h+@A=5Y4I8ja%D9^qF zIQ415KwSrb`rZM(lFdsbK0huX;bBBfM!;CfQNjX6!6V45#O7O25K-ESw`(Phi1k(W zY``vFE3An6>#XYne=uE2D#nU955L9}Ef16)ghoX0bPQvCJ~qbEF{;q=WYM9NK%2_~ z)Q~7E|NB7=N}uw4Q3W{@$5FY2Hbc;`-Uta#?$->O3t}W?wh93@&82NNL}T(A4+Vgx z5LGfG4jq}S1z?g=rTXqgqFv(*>C~D&t0;>QR9`{0D_TP_Ephd*$UVaACM&CsrWYsl zO*E|l4*ph%Fl2r?j6PmephES^N#SVH7iYB;4d6ib>~5??kD%v89wM^eo2>Or6NbL( zkeuuN78K#x3z~S@CDLQKFR>TP@kSQAFR~Q{;ul{Q|GaeH=dUyTt~b)^>*ODM6Pqcq zSS{-nk0ufYxY?SsIPdTox^ zPyj^AD>uWg(#8{yikV?lVZwXpfqrdlfl6iJcMN#fKBHB5fh|>PY<1m5wX^c$cQH`N ze$)#E0dOpFkB_YZQKeZb&)W|eUUb-+afnc@?A^)t(SJSwJm}irq_G=CO*tOczwiAZ z=k`yLq!6e3Vc*OA?WYUP!*!8~zG(qeu*woSrQKqtS+!Jll-Q`-X=Bn-J5r2yl_cfG zhu$`i72~8)wHw9JOlV$ZZbYi}#{H*;S9T=wE>q*vn9Tm4c8d|?uL$}tYg=@ciFNhB z5-IrHR0Zqbuy5obfK`)v_LsdVUPCC$gY8g%g6mvQBf1c;JSE|GKpRwin#07@dCI!~xLGAZrsrOB$2!57P#QrD*@NoINkdGW zK+v-m@|mn@K8h10xT{`;stS0qJ1P5R#Z-G0F}$g2{OKIh`EZ{GZUMAp0@giCYihSo)|H>uO7Z;8M=z+f|K!_+&rZ5r_>C{^EkKXv^*w(Gy z*|QEBQEsB2SwXpyJx;)ZK ziA|vJveNAalLA`XYd8U1B6!3b@!s-`{7`n3OKrZ_Q<|(g@{#& zK0psT#$6_z=cJ_iJo=lWs78laOaFdE<=Ce*uZPTk_as5>3%J`1&($F7U2;!bN&-&> z_J`@=tdBh$a`H$O`vaQ2W4uJaQ*=GYPe(WP#B7!gjXX>xdl2W+Z^`W!KHY6paidm>q`#I)D4fDJa1>`od@v3 zOk~r{8P2KcQfyLU*ss3S?teF7~QWfs|yTMQKh*wSU9@W*5P*&j(j?Y;>V(3pxN|cGT_;@B`BM z2#9!%I=5w|2#s%F%N0}E#*3vYF)`y#G0zJPoP{8l8J5vPdQftWh;Dv2b98+5t~1g< zn1OGNiM1Cww$Pw4qaWnt%($!>PnP~hgwSp@%8+KCVX86JVzfC{+A*B?daG7l_fJkv zYU7aDvFlWu_a`z`C=h}7x7T_~-!U)x9d)Ch-=^&FaaUWiQOT>!MBfu&bKuqdkFg4(>MX7I68kHE9}$SrSDU4f;}!&mv8)FS zM`i&WQLE{!dXHcjX8I+JP?~$Qd>u3n*I=P~auZBJm$8IrCXuE}SW?2N>c<1ep}Mwb zjrpPgIyC02`~b;3q4IBq8Deq6lBHtW#YOwowuHViB?k%hVeMKab&jEGtNiE}b9*=9 zY1Zw36F(@An-((yx|(=OmVk_;RuJl-xpMrUsFr)#Yp-gcLob$oWxIPtx0RYRc`?IqMW1td$`)%l9O9dTmpSmw{7?m z4p^3v3SJpRQVd|1V5~^Q*FF?OH6nl5ti#YqQ}nPlEdcOdJ&iWMKg~1zKQm+Kx9(^b z{7*l!h&`vAR&hP{&`dZgXc&5;KgB12D4;@fnrVDXk9{*xJT^l?;s5~f09vVs! z`Z3V;^qk)t;-i)p04SB%R4(4lHO?R}Yjg5hL9Y#0{N4*}TRn`Gg;?qU;CPJI-Kpl0 zwBhjOR9`Q^cI`-pxhlFxDOf0|`MvXVHa)xp5t>wKe%lh5*2`p`s{Nk~Qt;c5dmj1_ zD|Wu*?Dq__zduA@iNSe(l{G*vuWtz;^dn(+iBIcrg>rEbGCcE=tEvNo+67J?G&MPr zZKJY7*IKJqK2TK&?2AG$p_YZERB(p0kRL*8NIn1`U^7>xdhh&XdsNC&;uZi7r0XD! zI@7OZ=axC7*FUgrloZ$z|HccULJha&4*IRO^5IK*{P)VDxK|WxjcIBB6NWnHRd}xV ze;>At@I94vEEBjsEn-9JPFzYM&PTgUVDTyyY#(7adABfTCgmmEui^}l>*aT!e`uYY z9b&uo49W^VOYiblLGiz;&`c!I{27G~rxhHaKl>l(APJ3hx~PT@^qB>D*>Yxjv<*?q zU=;+i29TWz8i{nCh`WMzRo6C<0G4AkSz4h$&9tLNSmdt}l)DSD_u5scrZ2|5c(7M` zO3U*3J7>7hT1blGKgp_IyE9g=KT+e3(&KKj?YSTc7T>9)d9SA%d;?qKkjjwoTs~Ng(y^@JPfmC|qQT|lXE&B0!=WzI>l$@))uLfjd3lrtaA5FCIPHC^L5$Ug#OrYMfBf< z5?y!k+~~?}mKVDGO5Rkn>FSi2qu81Z)TjVdCe!AhRM*Wu|LZj6SgQNx+Lg0kU3-P9 z86lj4{poJ0EaYz+RQY=`GC$*piM+}Gx04Mgzc1&lQcV5`k|@O=SE756@=L!KKMR;C}-yc>^TIwBz2&Zs5{JRL>Y( zXyrRsr4?yzRU2;#yGTT)xXp*N&*`t(+bir}M!S8xLlb2nw1VsW_=e0DCX{)5<+RQ_ zaNv1MGbD}O(lNAUi?a-5%IQ2Lf(f+S)_Ni|f0PUvBl)oo2cQi^aK}U{HaHr4NvsP? zy$L8br4B{$f)(;6%@Kv{a`T%ZC)XX;|6iW)DD#%P-Un- z>5%)=NA$W2V~B8c1qTQ$PcmwKuC-zwqw$YN;G(=&u{3rG^!@E9*gWZYa(C@ z8TMsLtUwe~ziWeU^zO#)Wq&giBT&+h0v)0(kUl+sWd|k*tWn;@@}KA3BRVf%&xPAm zL+>f~7kv^VDwE{!U?TlUT>tY2sopf>csx>?_uswTnt@o?S z|1bQ(vWTZq+15WvoazUi4{d&L`ZRwc5sqwbzLV7HNZt4XfO|WBQ{x8?+S*5`eunYv zUssbdKUh5Y;l$O&$p_{wJ(}&?%46gqw|(qq3u$J1W3jao#MPKX%KPeD|MJf*SMvSC ze-VAO<`1Z+O>rX=-tk0umQ69UjFvg8)DNtLBssuOuNcK36>}P`x80oAYR^J48UL*X zt{*6*5A00-{MA5M8B}K-sT+oZ(wz*yJ%!RqDe<`b3c9~AN(?{qQJC>r!{|= z+Tp3}xQshXp>XL|MEZdN@|XEh&g z6gcxEy5O%`gkpR?uJ-)|Y~=}a@n;VB1jr*{SwyYYBVDKi@Dqma)RV*~a&ZxTJc}nj z&Z=UfdFN+Nl~VU}CAp-kx{Cktr%z@<{@)!tps4-8VRsbq`NN%y5^W=IC{@7a$`cDW zyK7uq1TqzF9HXf}ekZ7lw^FC~7rXVb>Z!AO&qB-;Q&4#2G~glcP&5a2V~;`3n%;a& z=ZXVTw8iPD(M-nzlEaI@2-%i*-0Fz0dF+X^o+5?hkk0bOltVF6Z}Gz|EsSn0aM|isK3i+LZZpzVh#pMhl+D`-A>(|#SH}Bx7z`@4D}sd@@RzP%?hhB{;T64%Y3has1?9tYV^hYt z<*gDqGO(PM83v-laQfk6zyrN#4hrJ)VOcV^otlCN6`Fwm^&uxcT8Ll#d{e3~I8KE8E- zIzO9@t1xnMmwa%?D-=STu4i6g_r2&yCUuV)DUCNz`$WYl2Of+_(H}*j8gLYDt0*GC zS{K7t`kgSz>SIUiuShrDP4@*yus`$BXExxCAA6}7yi;zQCarCDj|32^qyS`HB&t)J zQfG1@YI}o|i9M-9)n?{=>MHgAgIND!%Pt+5?{)#IZ9r}LpBoFd+NW(LVER1T@N%n1 z%Ej}w!ZC9{Zt5J8 z;wzH`-Dl>+p*!li`pX-EhvskL)%*SNfabjA!!KC;;}&QFrwj!BMP2~5Y2GgCZ1=m* z|6hh3uaq1dB5;tl1v6T!_E7KH3R9{mXSJW(bP8ZMZ3V z-Xz|7sLD-!syP`}>{Lie)~Xh`$9}Kl+f-1NEGXJ~v+P~*mtC7D>Etdm2V;mMqr5&j z#}ca+Hw{Rwc&+cLA%wENdArE*#O<5I$R}9I$_t0} z&?}&*r&QP3k$vNIgP)mRYuk&zQ?M^G_e{+DUfI4#TH%6wXeU$%Q`>6cg5Xa2( zS*y1Ow&XbFa}VdhhW`aq@ze6C9HRAEM8NNmIJ7`LY@()Qz2#B(xwZ8^fO==)JxjYx zX_K?&&JG`j`KAl*#P9Na;@3lfQcX*<2j7i6NmEb<%XA7;S~|aoS1Lc@u4p?YR7N&E zpwDv$mk=BC_@*Z8&)kjBGs%-k^=m@O33RJ=a{I3wUniYv{ z7B^i1*=^BP^srE~jpm}koCNj02P~5dg8eV#DCBzAa%pqZRmlGV{`pa?$IDI4IGz~V zi;BPn|D$?^$Ly1VNioDs(er+x?epnQ(R23A);;W5a_)(&RdSO!XK?a)*I{7N|DJyds zbz|MGNp9pPz2+pTBo)2bX99tTh$?ooZ{&!eCi-*q- z{_>k>pGU-Q+_xSs{Iz1Aw?_LW#U8ab&srZf)R*TrZ)g0J?}?4XBnt%e)W8us`v_gn!Usz+;U;_t`FG^ki^H8vte{KkfG+6U-_tnCj{Tx$ABm zf;rpcTRqu8djdwE2Ua)kS6mR&y5>>mXvhzgk<}(T37j^DK~PX2N6;SnKahsTl73O{ zdnB4(0T`h)KVR2FTpfyTse3Alqj5l*N#x6`bw&SaDacAG$vZ@csG^>~{KxOZS|oEG zcX3~i>~JM@@c9%G6JsrQX7fhZ=PrRN@gDg^xqj}(|KO*(|FZ+Z-QCTWQs06@t6kfN z&f%3WI22b=dne0^^7XKEmVV!NUS``J2}sHOB;owYV<|kz2Q9_wVJ@7C^b3p{f__cg zv91Ao=wV8fJJ{51%K&m3LChxz(&W$w%bg?Olr*F%W7YcI-eQywzpNnarkxM{PJ5S) zfzDPQA`Z|pj=e2~AG@Md0#zHmq$cJjY{RU|l6>48(Nci2DqwVwWpk#*%WCvc>B4_u z-8nvb^+O8_OK!s=V6Z~rE^ImFQN=GVOYDll^VE0Ur2G9jrGgeV4m@}IMKnt0ufxjF5i?ibpB>EI5Uc)7vuv#x9`-Oy#IKWX+Kt< zAO9_TfAefp?PYJeqNO{{D45Rr@jAl{)plxAO9~rT(X^C%nJjnXa(^KIC}p+^!eu@^ zb#VCK13dkp6Al~A>940-s8aZ=;#hLOrDfE&LG|dAkVz`n;+z8GYg6~Tt+Dn}ZQvDn zy2?#GLWnUuzT>mpO*NFg1Fa-%JYU@uKVNEX&ONW)tR&lh^_|w3zutz=WX9jux6p6F zT5VgnwuXE-DZ)U323B9FP5F=IGYsPY#I!2{D3CB!rzO6n#kMwOy6ZyN_zzEoFN!R0 zXID3OPF3=we}97A?5t@tt%3D7EuINYsNVQ!5|pkEm2mW{pREuMZgO5-AbYWP*eBqacnC618Ueyp6u6*T$!xdY1nauHl<8TwDX^4`i*(SMGkg9M6mX z>I1mGpYB&4s%y!}G+H*q2TpD^wXvs|w(lMM+i5cfPJFZmhPyQVS9UC#s`x$|vhvkT z_3GR&WC~nf`)QjeW*0e{;YLbq!$NG#25)__rsgaGl7QnQr07p&yu$7oF_oAQ!6BzY(&*ITlh5^(0fu+nlH3+=PIviz@iw(eK% z+&9lup57-I`6lDLA>RJHrrCJc+R?<=(c`6holfqkvj^h;ZXIY6 ziZuhc1@O0tlZ>ufk-4FVy0Fof#L}hX$Kpg@1k2xFoWeNMd+C*=?irj1sBbV0dCuoA z*;0G=rK!<}6i=)%%^WrR7K;nEnQ*;vLoo`jJiaUYqFES+3a1YLZ_W+afFR$b?1wrz ze^~Dxowj@2=03oM5eZb}3394+KTw`7R#+S!{h!pnzhv8vsXg1!M(*vqiRWvrE!Pk| z|6Na0%?=dEgS;R-OfcLqj_0G)IF_A49Tj0*f4_&!@2-kN`Hf5Ri(< z(w{a=L9V#M6|P}pku)y1otdbJ^?z|tE=4hBV4ywVCY&(}rxtBlW_R~;^H={3Gu1L$gZJjC?`Sg;RQ%5Idkc7b9iMTh{{xu- z%xx^5rV~yFNbAQ>?`SevR|n!j;^EE=b6N5*E>gTgb04)F50tU)h4t&Dny?4qSiN`@ zbH~8YLdZbm=}zL>Sd8;*+cRnl_$BBtzj5eg{51abg8LE^WD9Z3W9O%V@&Ea*gaO~> zWCsPF=-ZwweTQ#_N3G{e>7PX@ulE-QO56pWwsI2=TF-0LTM%}h2b@p&0Pe?<)`$7D zG+Yst=u$sn2Y_WI&Rg9j8!jqRo1bbxIY$~vaisXP9L9^$*D|=a{fb?{XaGjnH(QkG zE+A^MNR9~zdd!KaW7clF091aK`&5dRo<<+=iab=sJuxH#1Ee``^A&ro4xJ9B8XM%} zZO)s!wp1r5bh@6Wkd|xwBwlAI^VL-aAL@Z}TkDMrE#LOm?RYDD-1E@03di%+NkW+a z+KZ6)*O!AD!b>xuC$e+tHoRtkc@d8;8K!^oIH{wffb^P1>=iEy4QX}?GFq_DhqfuH zV-gSm@in%xun`hMjob`>)qaBh6YVE*D?u%%qS0C^a^$agfuCNH&rLU-2&6BLL|+5` za??e}U&azlSd=!JPlzPDsP|n*8SaeBI5fPJpR^-nAGRXNn_c;`UI}f0jkHi=x`7;4ax|qNW!b-bl)kH{U zlAh_kZ1xz0Oi({JidG^4I-rA4*a7O0VdU-pOtUz0s^%L7#X6BSB;2Ry*Rtmq;5fNz zr4Ul)^E>?h#=IrK@`zB9hHwcz6nT=6g4(*egX!IW+ZjH$=eieNi;9dY&xT{i_Zaw<@!_G%HRpHu_Y9;%#s6 ziV{u9a^4sE#8ua4Jvs+UW>^$bYFL=&|0C)v1KL`HZ4-jKyK8X??ykXGv_SAu+@VE+ z6?b=vL!rgp-6;;my%a6(JkB}yzWX=7cJ}^ezBOys%(}iue5NAcpl@zq3H0R*p!&w-I7j1pJj9^9#E z%O;wwp(CIE3U)k<8S3nf;QiZTqKa?R(T?~tNOc=U{xUt8Ci6Xl;{Nu>QA?hMhk0AqN}hUhWw2|=e5LUSnnBn69@ zYg3k+)2{{q|x7-T-1hb{6$_rV-iKdZ`S`(r0CgNg7cU&7T)WJA+I`m zPsLnTp&z%S)}_$$?&fuEv?2o%A?ZgWD56xuuVGRk;g__;XjRAyGsueL zlq9bf(Lsu!@3~mS54G#-&joILK%e87S$aqM3CntS+iR(?Y}Y8&{Pf=&nf{!S+#p$L z>3Q~M@f1}y=jrs>l=;+nr(fbPo`A;rAtJQl<=k`lTHUXaIepOo&w9$Ut*5ua_43KJ z`csVNn#83;R{f>$4&5+b;(^5=Fz#71z2}>)a13Ie5FX$N-?ukZH2+Ax1DutHF;VM- z_>So!3Kak;h}-?!eH7VqQ`)5@L1#H5I`_uS*VP7QCdOGhE@kP~z~aLe0vH~JsqYbc*>j4XVl85r}_d6gUY zl>21Nocoy=uyb@i?|%<=c_MlN*}R&1@>pGJMqa!Z>C?CbSip z{0(KE^lpW^CM?^3GmH$w?_mLdF>ZzvcciEloPY=A1)TpDRrg&s)V^K_aP{%m7UP29I*rn?^Incy}hW<>v*cBu2hb`PxpN4bAO^Os|B(YK`4+{&)K8tXK^ zp3K_mbPG%X-T5~~mb@dY2H0<#ZzLO4Q~M*Lx7 zn;%#Z;os|C-)wFZ^mNMajkS~eVNhGRi|+5MpK@T90=yQ5(z-}S;HlxNG_i|sY^~N= zlP~50!DxN|HtIs@M>;xsu$exXTJk4NL^S06RL zMom_m>~1se5_Pa#y}b03g$a#U3#KI0WqD5@V{R#YHrkY+(w>>Wbi=0r``PX)8(i-=xnu-~G?iQ-9?5&PGQc^)^>* zAS1g_1(EQbKA!FGoXg}Ui+BRmw2AeY5)<2sZaq^FAnau>E{2V?ETP zU}Y%&Z`b9#SZNf*$=)*fK0XxpSVIS2g7W-WWK!IhHi#qE=Cd-9D!pg6e zjqt$K%)t4h_WoycR}AL4y*wHS4}-7n1|FzZTaR-Her8yIPA@V6K$G=(G%K2D5|O&+ zlw~M9a4$3=RKIK;m;u*`V?91XA=$$RP{Ty*p+crvK<<96@p`W4TuA}6WAde75W&PV z+6=Lu1PZyK$C{X`N|?{I%iQIl+AHMxpLoVVnLzrqJ@M_F+gfFai$r~@HMp0a0` zn?KAg&y|)Ko;17g<$45_wX7NGhwn4gQMpygM=FIrZzNT$mmMHNv35<_KtGv(cj>Dh zrJ?-{B@+tB!C?&rwMpAWHw}kFyta;)hC)Mg{zvlAW~KQv|7bK*V#R4#fOf8CfKbJY zNuKv|j0+7f)UK9lIOra+S$~(>30WIW_O$)zAThsYX1#ZNRzE#O=NlKpCjM~(1pW&~ zX89t%`GV43TDl&s|DKLjEqaziuZjGtaVX>Wpmn9&L${IGpwc4wXGw-rbA@#PC$00x zhUZ!SPi-clc|1<`3-%h#XZBC;a zU#=Ud_?w4UbLJfOeH!5Ew+fXpm==kSd*y33gIrqA4>X+&&dJMU(0ecQGWO^(We^WF zhX(i7x{06&`W`?`JGT0zNy~4wGpu9pkaxd?qYM&eiK$B8Z2kf$>f}g}bs>~!Zxes{ zBhE>iBvM=yi;DH&u|MOQrT?YfI{qL}p^o4ILGF4xLDO_#@&-<;>vusI}Q>Mi9&pM z3_Z8pzCXE7ZJ(kP$BZ8?PVfy7R_4xcNK;73WR#l27P@*O5wcdhc8B`a2p|5}X57~4 zt`!ST&!=a*S#8-pt}}3M=N&LUYn3QeIp3u1Xm_#ozh-K4{*;nHUezj0x;>p^R$$0Oe-*b?Ow3^en2{j8m!EsQ@EzfK#JMyi+N+o zq+^NrUENPMAoCr+%Ws5luY0z}0QvC+teK%4tq|tNIZq@&d7{#rgzb8i^(ef02CM-1 zQq$`K?2W(~4hU~Hs*H#mlAGc1X}oK3ElCAEbQ$OT*pe0%WuL#=iAREqyUBC?x39_< z7m+SQW9$FgxrwYJePPyr<~r%hk^Si0TRiCPL5VcDB^&}SpLW?=&f-m-oU!!Y0${gJ zimlft!B#P!dR!-5tv;T}q{$`Jd%-2T>N3_UG`&KRnj&q^%+Tq1^yy{r>1Bx$bl#b|QwLQT`i%%ZGWMVqp#ME}8U74dGZh}Mt17eNP< z+y&x!x1!)B9xfobh5>}T$6#iV_lQ#uFK_O;PWZ5>st9FU?A#I0Rc7i;VdkcgTu52i zq=ZqECs(bC9Ic5FK_h%^(ws>Vuwc~-tw%kyi5JJ-xSzGB(ciAJ_3bLSbc=W}c3j+d z6(SNc|7tB18osyR4gXIDmumskbIy&TJ#i}^Ll-HR4yW@r6ak}T0mIn~67x!#Qu1+r zGS|wKKVhJPB*NvwAArQajvyj;ON!4?GE|256Iu5BRMBL(ZPZ82mzQPnBK!z9X;|z& z+FA9|NjQ=deTa9b`>S_aV;l%^2M0m36l?)g(=O2BZ4HM{6o9t>)h93+@ax2o!a25( zcS&k((oVgXMH_}9oVDMjH-#hJy=wtcw)2Z8!lR@xFOD+;goaSeA>pi#ik(;u1w}QX zWvECp9uaDRlPGFkYq#&|)9hLx&%AUiNi@?$>z|_#*1jdD3sJnXYcC&q7eGmgP=K0H z-~!R@<+PylkQ+j&$CKHJ`N1SKNt?h-4s8{=?Qzq#Vw(4A@E=5aLks^H9)l|>Mne7D zW<`28*!(Xy$G{a+X50HXbF1|kCae3*KVPg1(69VWnM@*iX&DEebhk zi+e8pGoNgo?A-wYwgN7{+OZ73pdh8Y(z%T2C(5IvXO_g6^G9!bfnXc}jtTbyDBL{L zZ3*>irxzJ4xNP}o6l9Vjh*f6sp#dn!>9xKuyv)3!P-6slk&xPN{if`9JTgEEpqfEo z_@9a?LA@%A^y}u#<4_dZ7pZNmeDc;2$2jhb}ibskc~6KIVhE1Rx~qwIEUM%ms|EtI)`<;4UTxwfGbg zuZ`7DxyzxkJkX84(|pT;fTT4+Xh^>An_(*PeW$ZT@UyUp2S_6Ff6uhu0ijvt{f}j5 z>|&8srMlj+TAbFr8vBFMz_3ikN|S-f+K^hO+v(CS{RTlv9gLEhUY|R)gf_E7Hcohob4*diwqaiKVmKK`Wc3CpPbpF2MU$C%zum)gp zdiw>h%Ktg+8V8QeQ(SD`SQ>Le?9fJBaKd5J+f0F8)cL`RbDWs0^6q3 z;5LM5KIV1L8J(5&+u25X@~}+_S{p){x0<$|F>MJ}OjX=xqnjV-0mK&vOEXDD`KKOY zdp^PgDPdr`kP*D}$4E+!JHIfF2=s0~AjaCh=(On~jEq~3>)Y!0`V2Au=auF9e>&O! zNC}nVJzH{9>4($wnyysSF=HddiDNWw-D6rb57?g+F|S!S<4oXC@43lcHt7m-H$}gC zjF8u0%xL9~!o>mg!jqnOddj+CPVICg*&0H8>!5hGl7(-Hr)cC+$NCF6h~o+%*&IT+ zcLf{p#%iGI?-18mzw|8=!Fxw@7a@T`D&N0 zE=8;fR_G7K*X=|nlRF2z!FCTiGwr}s=Ff5>rbZ6Hjt{Q}6a*Yf>(fan(@7AC29bYR zn(TMogaB9yM}465+>GSE&^yU?22JD4H|KaBuxS2L!J@fEGi9xyxA$E{RxCfpFvr6n za4KIqll!Fxb`<{zjQI}qwOvezY!jSjXZ%kth^oHArQ0l(L-vW?{RcXjw#}&Xhka@q;tt9u zEqPr^ce!>iV!D~%TEiG`$6ypvF5-fswnl2_51JyuJK}tS=mgI zGp|>Hl{nb6S?Q0?-has4Vwg#Xtu$SIQB!FyXVGF^^x?zqxKku^WQ*gA}I068Z%=0nb(qb`h?(Ndz! zuSW*5h(dyJnJ!u)_Rk!DNq_{O7ZMZ%sEK04U$%}=UzgC8E8n@C`5nED_Rwq&X#Kw= z{_6_S7@(1ZX-+tz3M5QpyewgP2g80h6uU6CKaKnzhESdKxNN7a6`=q7gEu2iPkj6i zdzTzjqB_&Og4Hg&gLDQ2x}V*wcz#z}(2-api0n^@#37hY2B@r0uh#_T!BCLzEUwvm z^Gy+d3GmP<@kluU(XbK0gaEaR_d-Y^pIjTv>#(|#GLlKJ29H|_%-a7$kkw{hT_o{a zY)afnGq&eA^g^7~h|8Az-ECIP{3Fyf=Y^AD<82$GOlH7bumEU*SV6DrWN%1X$E zKN!Ab7sPc<2MdjnkS2xvw5?M){r@=}>TH$20#hn^&k#fnh@^cTY1JbT$AtNs^TW|5 zpZAknXXy1*r%E?q2-H2IZ!UQess%dq{c-wE>@#n8AJw-RnXoUJ%=5Bs4u>5Q4DSQg z*T6~3Su8Ao9c+aP5AbC}G%&-?SQT_b3UJbYX^6wnGxM4#xo!{+;p21*hW`J@(t#Y4 zKn2;mjZP1omW7Tb&XcKCj+G{w7~QSpXl0v+leiZit%t(vz51{~nOb0*oaeWL@5Gyb zQR*ggOFaqRL=KHohc1zcQZZJBndJ+^7?${XqLV9BL=q9C3X_t(3rUc>imsM?5W%?C zs|aVP@@2a?Sfw|c$AQ~eZ;#FtBoL*0Hw&VYt3CfsMLvN=#D?51(Dqd%NoG}HBKVgJh8dehs1&t)7Wi~u;hQUV- zM_y>q!kTv4%rj$a|Ku$zx|qTd9WofQTB{(l;bE+HHQIiNvkA$#Lg=H=7_xH;^vO_) z%qJdmWDr!cyAAniRm|-9d$*Up{jvW)k|9C8f|%y3^(ArB;;`mBt71#fecc@w&D}E0 zYQYL@%;Gb=03mu}B5MvOPJhgXK1{up=^Oen@QDIm*$u^eVvq}_27@n0WbWiMqxNFM zjz~!QnE$VMG_rsYS1Z+@{6jip)E4WB`oM2^OJPE%qe8h?@Ddb=g5)DHVa&*qbPC~L zC;rU1Jv9DrZbV5T+jc>R{e0N`PC7TH{8X%=5-ik;Z%qYw`xbhy84iwYflRbI<%|8U zppCKGs@lEPPncU;4VV|_z%8*F{2GcW**C6zLk>?&s-`ihK*+TWhuwqo?ufJ29{vk` zb|zgA$0`zbub&LltvLeGi!N%rb;zNaf_@*pcrL|*bQ6}Wo@EY@@z8Y^%4xJgibR%Ery>~2e7?HjW0 zUW{X^rD$13ll-7UfHW@Z+IdVV{m6JATdPwrg5XBd(^v(pA6CoyXo@bFWTB!qQn3t}ZGmXY zx|(B1agcIJ4a5kZeS}Zhg0Qe4iSuWbVs?G_X0z1;rUi4|{q3sjqee$ZCy&Zr)&EZ( zmf~a9KcUvb-07$7NDs4H1?qjhaaS zrp?a?qDCc}z{?4N=>ItT0jxr<+B^l#%%5!jOht;l{fn0uJuYZ5a`rh63k?>Bja}<; zpdTZT`nQ$RPgvm59Md{t)>>|8L4lzw7Zi_old~{G@-=avp zMiv(H3sM zXD=p%kh+BN_`n!k>fFqfu!sBnmih)d{(Sm*vNe-V?6yJ$#I?y(suj$&S7pIkKjvU+D{jtZz=g;&L|N}^~V^D{Ng(?r0# z)Ktte(O69^*#l6ja%-jD%wbdZ%!?Oe8P|2KEBWMmfulplM&2q>cw9A8> z-5;w;Bf&IGkyMEvKDUwbx$Q574@Auc4l%RG8u>~@USWj%AgDH(yfsSrlGh}gJv$8N zc??N5!h#I;7kQv$B*}h(Qhl=ZhD5qfD{biBh=R3VA)Z-y#AWFAv%DAoy;-h4ll~y) zf5D~Rh8|+F^3pe(Yk8LJM%@4Pi+jP2-T0)Q%Qbv4oreKmZY;DLtJZ z?70|D`GKzafpdQ!u(dE<@pzCxd7I?rYI_XACN?2@oc|r2wo5X=fk4Q&P$@zhaA>;N zN5;>+2a~A$WBT(Xgm13b!j$4Iur*S4KMRHRJI#!F?3;tS@}31sf+*1zIEInCZ-THA zf~l0~_!Xt(3r}vaG}rMuxB)DAfwIA|oRYlQnYAM+mcTsjv_O9>Yew#>LBzvk)JxvXra!PWiSoAN`6->pBc zo)*6KK0Q&rjny@}DL*~wd@-=}Yq#x~Z`}~7fAX(9lqj|Di0|AbtLR<$bUk`{QUCB9 z=VM-e01wVl-t_DfVk z-CUj_QDdjbsrNy!H;A%a5nFjHJ4Q5SKHx*aLjkWp%sISn$<3{@R$8Ui{&HmOG%iq1 z{ns;Pb=S7C;KtpQA!zci0Bf$UQDo;`t?u0EbFGg~2RLM3>amz~l`*CW%{v6azlH>s^nn&eOxkau2^`9}Sa>n;+%dPwp(F zzaGl3Z}a?K4Ay?i1JCZ%Z)?6ReJflrkttIuM#21VJkoL206*Z#d@6TE5eX0h-CZQDG z+iN*dsMe4Dt$Ods>tKI@=5HBD@K;8gp$+U$9=8D1M{WM1=?f@hO^erCZPzn6Jc@@= zYIF*kIrV3;wBi{O9?$Q_ulbn_N9X*CpXQQ^uqM_+%J7xb3#CC%V^ddo86c@%U)ZKzO>O%3oOL znBv8amaX{_keED#K}w^b8B=$m8PSjejO$}duFwWS!;Bw-Q4SkQs*s~9`4TJD{CC8nzHd6uoJFoV>7m*IpLK~zVB zD$F-&LXOHcQ$pg7pR{MB8dj1%w6bf>J%J;DI9fv#%CG8XFtzE&a}ArPvkxfp{5)g` zbacaNRW_lqxBxWV4sZ@uKkkTkV9QX|XWc!wIzSf;{07PnH9P;!1~PV((ne$+=i+%g9;y*->P`36~vs<)%>m1$}n8DFV; z9qn77ZSG}hRis@V2am?mkuUEdpArgMI0IONR67xC<+=4B%P4I2Um)@#BDsp>>y^!J9l4Y zYCO{g^>l_iLFTbfa!l;rs)4KXdzfzjR=`sEwtc)7zhNqShnFH5mNJ8-e3Wb`hv43iMg9ZAqnc zRb>y%__#8K8q5!7?azN8vDE_4EI8?Jafi|f#@3niDBEHi8C+V4bpnNyN81@?v{wgr z|8!FALz@$57~11kR#iQd8^U0z~g#}!bw^p!9o7fYU;lP3uN&C&$~8+VUd2|Vmwtd4m;(&z5f zb!7`m(s&{!Uj-3b#lGD_FFrXMPU+y%Dao0P4J$BwG8R=5#O!4&hNKAXTwA2%XOEzZ z(=v_m!?yP((Z7_@_l9^5N@WQ80Xk6StoF)AU*!iK-pwVx1Z{Q@?|&hNz?&Aoqm$k# zqu~y<87-*<5#64r0n*>jpeHw24+*ptM3MzZ5s~?*2mQ#hlZG15GafpcG>Onb4mFC) zMAQ^1Z!<|2oB-g=fq8C~{np=PARnRU^jdKP!qhDD*6^TjP_#Du@nYH&(3R-`+P%y? zqW~3Ils`Nof0g!C=Pt!sB-A6+JLQ*~^le{7#OrlVf{dqy{|-DE;#APdoVFYDFZeiI zmSpmdj2BMjQuX1Od&-?})wj=x^%H_HG$;ou8s%erhcWEk_@rfp_Q z7sPs})0hu-_TduK&}&DfLJd*Vhq0$AOc$O{GCO;nt%RTwE%XIJ!Twy4n}w&eD2j1| zyF1_v<$KJ7)9#N-4FY3w$$H;gEQ27H(TYD_3o##Umt234Na?@IN)L;)f^R7@vf*_P?Kp?s(fJzWmzUB^ZiLz)-% zUpFmdg7ApM;epYW8w-yv2S`lcyW(Ej_(8-eiR3ZUZo969Vq~U76<1*~FR+FU@Q;XQfw@*+$rvr};Oip} zsjps5{luMOUNB#_7(V}mRzxp`={Zg9NPv?i9KXi>(4WI7Df)I>hjO!Af~q$}>Wx@< zs#U_CN3$x$?6DAOqf3&KMhffE5R^gbOc8EIbV1a)_K9bKffT_Y9=UgSzJT|?V(-<# z5Aq!uIVJvsVoxzfe;-WR6!8ej!%YpeUy8)koBm1W0&H!0)NfytBZG=@>Ko7nS8v~ard9lWd6w~?c@|+jS*i*}3C;)mk3v3tA64T34e(GzO;xVQ;1{%ADNXLs zCGm%GYS|V%8IV=jld*7m((I;Ar0mW+E(!);9W^t9t{jLJr&4jq_~)=KvuoLnUf2Co zTzh8EVxgDT`&MfD%qQnO!!FdX(t`Si>r)hAse}gS?8E0l|KO&~xP=ZOt(hcBp}dwg zlJKvt%h`brWumGy0_pitJ^MQo!@3OQ=95v1Ek`^dpj@Qn?U$aVhLYSUZWen)NuOgS zwT7pd;SF&78QbWjbWFoT3iLxsV(T%yjk+|_D~<1;+WNAg?;q^>M3TkA} zSJRdP%)v2)8_4X4j;R*kQWy=+F_bzzhYbF?Hw5UHbxbl%qL`Z%i**q}1rm;^jcMDz z*DX9~E|5LT~ z`rZ5@nl?RwOTz|53|M`~O2H8D^^i}d<2cDiK$Yebia3X^S)>!RKryf-+9!BJ0iKG= zmnd!c7Q%QtEn7G*M*Y!7jaK2{AB354UY_WE^F+tPz^v%s@-)L;!Ki6TSkLlsYef0_ z&B4LrVAx{X z=AxE2Ywgb^$AKTcwQ*Dk%0~A-#&|xu8?LULXH3Azx;c0nH5YmfWTrGXX+P|iwT}+Xc<8&#KwZH<5LYw(;*Xy1qh^iil-1o&5-!bTvCb3$n)!(g-(O1) z!~_g2%`7z}{FqQG?KozdRyy=5LQ zb~{fGIR9eq$Xd4O%|vwf4jG!1N=i41x)1ffqd!63^awS+{ch(*s1>X}n(eQ`5H~-~ zglJ8xf0C-U$HS?s-qi8F1~dA*)H88qNnlQi8O)yjR7G4PZG>XGa%EtbOte&Wzn92C zqLXdC;|sqN8puHa^eXXp+WZZ&xw`zL^@f?;hp%Tv8a*%`6B9q;o3c(}jQ2ET z+S21UyJtmrdWQg1PVk6ij|hMwxW;k;k}O#0a)h^3kEw_1WOZ}v1o?RW%p*68u6boR zAOh^`Dno_%-d3{3!5&B8;5Up_lDap&I0AV3BG=vI%w53kz+U>s=&Y~pD~kELCAHeR-g~Sdo*pZe{~7>I&#bs5f3uTg%4GQ2FLgM5F=4b>Nofi9No^VP*u9;{ zGQ}MX*habgq@)oN#z9|+mbg^8VvU6vOg`ikCInHx_O0*GpHh1LwqAb$WEb)|7e&jR zCJ75zP(4ilfy93nHV^{R)~}cSG9Uj+JP%^@y+O+PdncR%6jN&1H%k_TDbZ04{BBO4 zSFhUx!z8W;pVNF_3MB3e&i7v)b5lEo7RJwap z%pdxJXi|z6y2k7&v=hGoaXjCw8uw>70mZU)I2~e8i?^~UT$lQKCwj=_79_jrE=jsj z)|Im9azACJXQocSUtzvI1L7h{?sKUqseeJ4lnn!q$Z0hT=9XGD-#=Y3$HJvOe$0Si zjzLpmSShd%CMAhKr=q8G7zz_D|HZZrk8XvldytWzf7!u@Uu@*EFiOi7X`CIV`Cl+PleQ+>-lu3RWm$Zm@$5!b zV)>ou(!3F;_~uHj_)S`PpwRgzK;Ccn%H7SR?3{6-4`*Xx*tGnp)Sxj1XZ~;r1#f3J zSm%tNXVZ2PM)TyI9h9ZEmpWP-Lf^p+AOZ-*wch{1DHrUtzYtx#=`rC`hg`F+q zufA4s_wg0qv2o438T|^7>mKHOIp-uBg#&J%*GfFpUN_$N{;pqV3A-yYxi8Y?5rhU_ zQUPY$+r$&68!?Rs#v_Cp7TXP#G@4id@D+N;wRG8fn|cvhEXP?=F{pZkw_cJ{zpS@9 zMY@nBwQwOopSbc%xrx00&iwQs;0h|m#!V5Ev1P0!*G>xlvynM!F+vqIl>PQ)k{xgtEjVjea~?;MNI!@Aid0eAwS%ca||J>a`DxpCo6TB=Gf zim|%a9Snn#_!^~eG&EtbPJ%)%=+91ql?`w^{xxANLmn19lGp`7dZARZ4Xk=!#6&JB5mj1ip?}Sejs^ zRVy&|m^}G?#WThIYOi=!ursn5EL6-_{?_$&=yz=#ls7Fr1%fapDyTypL-fX?0XN(n znHJJkIK%#Da`+?|B7+j&id~S=T2qyYvvMxczM=TUYI4}mR&-6 z9mj9vDJieot7|5pU;oLi?z_vFj%|2gx+i(4lql7>a#Z8aHZ^jA)jA%5rM_{0-Q!T~ z6{;5&rrE$WfS4^R!!@}yYe zug&%iqS*<^h_z+(!|RO5x$+(L{q^$1;^CcP$6hU!x zZgShzv~g7ewvruP!0sUY`k43h4b02mxWStrTH;SpB*POiwQ)rY2e6QS?ZteX^Ob{2 z$HQ~<6I3zZ(qTFG>8plGn?ds=t_;MJ4d`?A%OTIAn_)iIxIx3nd&IX7rDx6n0OG#T z147x+=ZuA}v#xMGgnicVg(W|BKf5j=+>6a6fPE`izzHyaz3#lRbHK>^(bn?gPvgzi zs@HaoiA-11%D&C5hg8(-X6e(6mziUd&TF8_^Uq3$m*)3f=xvXkj_!}GR8CK)twt{o z9Rq=$f@Bx^uS9@A-5V!OO-5PQ8o=uqlU>Wy1gTHkqz%fisIe5=A)v3^{8<4WE*9#uS1DC(QN$r)GN#NTtEGt;$ETa4)bFf0#v(yJZ}M`7rJy*CUUc3ckZIw6(g* z5}Ut6mj`MRbcB{nto@cCIaxn_p91Hh+V~>IQFK15+EpPdOfXh-f0jJ`;i!GV^*aI1 zXTJxs_1iB8ADhhiC=z4Pa`_wpHuuTlWg3h<1ey=r4)2ik({93xtQf}?MiSoOZMjcd zK`o9H6242N8TY4!vnVN%U@M6zxaxa2{~{4sYeQ~gTv*;|*-%FZKpLlvcz$7gy{_msX)d4ZVpXJm+Y2@nOK1b$M+q^L(qxzA+ zw$L&PI2asO72ox9w7?p)VULAj9w7O1!47F!B}M#KJ>|x}fZw*j3X{a0lIL6%r$#Jo z2*zq;iIPz9fUn(ITE@I8;hjN+q&ix)BPtpG%ow?6c+N z`j8`}&5B4Qg{{jjNsBR7#G6;0`LIFKUq=okWagN1V;nu4dM=}ix9XK zeiiz+Ay@HE^%KX~3T;xK}3!l!)+R`_`n) zIv`I%^aB5@0pof!fey(m6%%oit95yO-BUOG%$w+72B|NMcPdD71O#V5{NmrTG4R_f zd$iJ36aGnjtSFw{1PFUzAV2^&S>i2!`bKsn zTWEB5S9CS$K#Cq{co>bbubdf#KTkH} z6kl^`fHmTB~2+;5Pcr8ZIxJ{!#T(-5s=L}c4S z0HWvLO3Zj$o%{h3?&Sjz<0*VxMd?h%*#U3LsG`ISBaqNX3J|%%i~?6BUiM$wKF?M^ z-O;(O?{k7Zjr;cddZ^g28Ts+gQp}I72XBujOBubDy4Q}TTNKS+a5DZM#jPPcpb)~$T!I(>6rjP5AHlkc;Y5W!$qZ3THLKBVoa`|H;)FLdg?9D71K@D%W>mU{Q~R5R(-~sjdpI}{PIv=BUCtT zRU)0*>v568z%K`n&ADX-b_>rj`9-kaKFiy^Iaf%kAJfa^w4aTR)*bK_%mRk#$ z<>#D67IrR8K=FW!Uc?Zo0jVMHT_^n9*=z}Got^;xR+_Ynztb^bVb5#~v)u^Z6tUO| z!rwQegWNFwR!lB7NVp`?c~X`Hr?jCwt~YgcAZ*S8C# zzT@dTAXXbB*81v7L!c~<^M+`|?5>cW96ER@UTj~T^ghFdr!sCea#%wnASFb{&k{Rv zTg%x}Qrp;P=Iq-hWYGAPeDf~LKSqUkV6#7D7WG|g*?*qQHA|^e zy5nn@i(9Lww|KL`@1E#ea6#;e#)As>>Oa^q9(9!J!&78j)S698uDE>IQD>)f z4;u`1usQ%y^|Fv06>Y=YuRo161iyxGNNQ449QB#2@X8fLOS&pM)`N7S~o_P z{KdX9W>Kkt)VL~o2e%Smv)S7Ifylb~x5)(gNRU68=0}P3glT+mvSW&qusCiR;j!Tv z#KFxJ_-&i6FVTNKwyfWoiwsUwVMREvOjN&*4wu> z8J&TCIR6t9%G;*sr^RE)s%;@^Cu`Frdva!Tyd8>T>vg{dqBZSAxanwgh%my}3OTls zvabL&ntF7KGo^)l_WU-HTY99lq<{_OJf{`~t*>_IhmZ`tc5t;6B1e{vI$w{IUIgov zA2$p-NQ~q;QrpiY`JE--FU+0Sm#w5>+8(5o5{p`%{MWqVJLy1ae_0(4Q_NEA>$yDS z1)ffkFjD{N6i7LocGJuQ%L@L&U4dbSc|`LW7HBc*stYg+MtZ8s?G+EJn2L3r>fhF9!>M*YB8CGDU86X z;vZcDfTdLM zW+(Fu2ZiC;zv&L+0%P6K$7JzO-oBgc`_hE`ae$rzoLvx=M?06Gmti>_{=vH>po|BH zRiu_rpX}MEecM-1)C6)vxLs1}4iTqG**Uc7@k~rCqaa z2yYs(o(}s%j{yJvk&mo6)SE0-`i0D>j-mC_{pjU61*^7EW7-}1n-t}pKTg@|mvU@*mnG=5(5_N|~kEfAmo@x81)UmAuHzK}?3NL5z z94T~AR=Gk+W1e32BJE38Odb5ys=enECiN}<)=N0%Ku}HFT<+*YG*^%m$O^BCtia)m zw&SjwIyh{&8a#1s;Z=%7@YrC~db{^*`D^#(52vN^zJynZ@Ap=6As5k*>eaAomRyMy zt31NUdoy85-;Q@l7CgtH;UCAX5vJTdU_+vMGCkt#&K^8v6MN1knLCv@`PIa26aD8_ zDyHc=13n)sl76uyO@Ay#ktCKxDH#;}QhHHsD>Ih>3>vQD%<6D3mqDlB5G!fP`r~`) zM;upWTjK#j6dvir8%QgVlESw&Z9z`@A|99&r(Sk>olc zUVQmH6b0}F(A)AiPz3hw&fn{x;F790{TK;gOx~j7KV-ylMih6&_4UY9T&_@y{HP#u1lCZ|^~ZuTe!WqrAGaO{CjxMX7pwBbzB;W2&T+W09<<3aiOi;Ai*L*MKb%-XUu@jZ~@S zE(ZhSP<#g`+UeRkDj=laLg}xx=zrlX`F-68ESv(AO2A z4d8W8RO4kpFK5J$JKD|%q(=Y2wI78ah-ixS-aK=^G_c{#F-{@fA!$ks==&Q*!^1f6 z2x-aP$tkj&^|EJ+si$%p7IOZ7B%O6Y6W-UxH%2!CGP-m~cS|WE-JOF0(hbs#?vfgf z(%ndxFj86?L6L4r$#>u1`~Uvf?(RMJ+~@h6bMBjhAy?Uxsvu4tp+%alKQfU61&;rn z-lcl>;tP4VIeFI4m6yvVVo4Tm-u@TAhJZY2PiUy1zx-BQ?QSvZiZB1|{kwd~5Ok|& zN`tr#8*=qdf4xB9$0y$g1`%3Kh%L*Sm-_d}V(U7Iis*f|kY|-)4bkhU*RJ{`04pT^ z59Y^;ESqKBAwHkMI9Ftq)3vgk-j;7;9(HV}*9w{xMU;B&eAps#r z`o1h3&d+L}1XBg3oh!KC=zD699xmRvWGhOv?75DAu#UU^TosKvA#|*F4b1oK6J51E z0cF2Ce!2ZYo#e=X=O&luufRWn#JK*_{WN@^BLzI`*VNC*3V(k|7!0dUG00VeSb2U@ z$;_!p=q**ZhLF!tfndv2rrV@)vfRBi;dc{G$~0Kgv)1m*AZz7;rg>>Mw-HaLhV%Rx zt7!YPB86#QwY8#1^tiUgVa+?RieX0i7X{$^uRoj$^rp(BGRps~XnG^LR5+^Bw-v9d%bD@gpba|ObY zXLWqzDRx3pdPx&ij-nc=h>Ie|>*p22>Qf93SG-owhs1a)y%%Xh{w>N}Wg{t>003!L ze@p(HTqSA_!@*QrEo2dmn3YsX3jk&*Ib*84DAauaReN#nc`u@LfZiI+9BcVQ^Ys;a z8Q6w0LO)r%b+{Md^HV$1a-(+Hu3uEv8jpkGTHh@*dgN@wd#-q1OQ1r*=TlEOZXTci zWb)=-!WVA=di?vn;@r&863>tZOmq1~wCvN&`l%W7u=;0Ow*xFGUlZ>r7weiLJ&|(Y zx4gljOj#Y5CS)6g(Vji^LjEMas!9|Fmx=Lf7rczqlO6%D^JqIxES@0Lljwg^qk$qk z!F9F{9Nbnb%!4rebezmS5d#CkM(*5I(vI{Rwb5!5kC(@hyS_9REY`l6=5a`0uPWkC z3rj4JAyM-UtB&!rWVv&6nRh8bR31F3sK4HFq&<_?tj=|0Dor?Dl2_f_YDH@>WtOcd5Rax!#FYKB&{Pqe zGu_z!$FrY?xI=7#af}Kb`qcJwEL)QkL#8U$-XF4Z%G(uBMtEem%Vyl6mmWc%j*dR` ziXn(k3W01N7D!fEOE+hbNvI6n{davy$_$T->js@sK~wfvEkArDc{39m#7^`b{k&7M zU8{Iv|8`%6U9-X`YtPhwCTt1;#{t_o+!eNR67%heq)yl2{A_}})f@p`ebXmYeY;ox z{l|RobkREn{S2~uMC}+N^_5Z9&?0pQg~LQC1A3$&4{SUE$coZ9=F)WBD0%t|9ZdvI z*_qr2Sh?wEJ<1up6tFI zT47XM3R_Xz|MoDQ9-2U^+vxzV_vH&5fy{Fa?}V#EYEVxyVxr)cPT}uo1oF!aGl;0B z&1PWi9Q)wRYZ%kLQWs1PLwbHleu%u%Cgnd|h$S2DvtY1F4vf&=>08ZM-qp5KoXaR&bGP({7ALhc^1ae3PAT~?o-*jc zsT(iQ44i^R${N!M#KR0JKyP)LJ^Lg>_=|Eh;TWgB0WWc<@gqEIi!E_{=HfuG!~=sQGSS*3)pBZS@ivb zk~G^AD*ZEYCvy3ksgn&umS{g@c+C7z%wY9o{t!#rWUH+_YzGAtcbifY)=ccd_HFq- zyB{~%<`o+^k+svSmD>78b#rz~nzL2qtWT zBlkeemc0L0V9Mfz8kR56#lPla<&ec!GK^^3FGTpb9e7$a=phiTN*C`LW6H9{a3fr$ zPU#ku@2rVaU>_WU)L*-vy6J+UBRy@XG-V-pzsLl-)u0^r-jj5+yg3(s@@tO0_MiI% zn^nZX@b`iCmGL-Rtr@!)0?ZfllD#91X>?fh;boZ0B@Ip(^j2=KXPcFEZ>}&TTS%~}gbK3A7fEBQb#|B}vhNizH>H0W55&vZRfhCA zN7?2YdNO#~Vp{G)oFX^BW@87I$R#NVs-L=cBC#pq7Dr=XKPr_pHAw{wQBiupFa*f< z^BsfLwf)~|%9IB89aT{(qwusn&s*Mv1g%7tOG?FiyT;YDf+HAQW-Ovh>yOxXyU!0$c5tYuZYLJiWchuQ`^L zjGASS z7j6hk$(Zci+fHtXq7bq6pJxX&ACP-QNi1B4-;+^a`o-x#cXEpL-gB-ar4$Pnc@0*Z zJz^Qx8kBXe&qcVt)86j$StSJ%iZupd;G+8wrSO%E3KVFjziP}|GCgepb`Cv>ZdN^ zSlYyWX@$EOI>Q%C%@QL7I#fCpQ6*uKolQKIlq`|foFs=iC{yzlD|ql(Uont8MaV+M zFL0#YoY;7|o{_fAu*2(2RdL^R?(1?$X#blnXZV0W3l4r;ktu_yRZ^cXQd2689Hue2 z9Tqb)x+KSo@yu^gf~(Odp2X^#SpK%IU4cn>2&WSs+$}18mPb%>@H5uhT2{SSIhZWr zD48=uMPKuJ3* z2jjkXrwa!85b?Z{uNTRg2pXa|VhDE5tipdn2~)k}k;~R3-9$uO4LFSa?1EEjDh&5I zWHGEugEK;>3;JeDb?#wrl#v4~w%(RGHl)r}_TD!x#TWNE!%TCaVt5Ev6<3O>d{>E& zwF+4q)!nRp4CYG7L+YWwsX>;9GNuiicqFeFS9*fcNMmqAU{u0DwqU#!*Wu7W&5M2u z>y(_;Qh|6Y>G6T?ZEBcu(xo|{W5Qz&=k0S@Ev==8@8K1wyS#BmL9{Cs zwRu@cIwb9D(5*YYNTwRL>V8h;#0b3W+gd0Upqu~KrlweF&|{8fSc`OG6A-liq9Ie4 zMge=4FRYPa!TKtUc4$V?<)5G$Sb2ZZsaaK1%y(7aj1$Jg%ft` zwOBviH{Tnc=1!z>UZ@)QH_zZXMJCfMvZ&uectBGA=Q|fz)^kh(nHdv+e^s=?yc*j2 zqsQcn*@=rRyWd-qAr+dnjq)_{XU63s;3|sj(f0@7LgmK z&+whjTcX?z45qum*9>vXG${+W*P`L;|GTDF>$p{k@UHs$$;9@)%q^eWv&1t*x@2+k zs#}DZS+n}j`2s9GzU`$Z75gbK;<@nHD22#e(wCU9dWL*GD*fb8D)!06Fersj!e0v) zCY<=z1{i2(zIWfd!wnf>Iz@Lo6NIcZ_wTX`9 zrYew7^WFS0Jn?`z8zu#kEiZ_kb?+)_*)JN;7JQ?FO7gsh3i+~p=gzQ#PO**`0*@9} zuLDo6+wB)>+O8$=JF7ydpR^_?j{(1>ZtGUBC+N{18L$b~zG7@`?3k9VVa;LG=(68t z)2&+67xe7?ee>2fI?pj{d(Q7QF%}kVhvz=w9f%O83s2|Fw^`@@m{R%OIS)UIw-$B&2wKD%XlPT?%BdLJ3dm75?+C1qjC zXHB@8NZEmGv44MA@Aq`kNJD>H{7A;p`%aGp^v$95y_>Eo&Z)ACo~+!d8|l`XuG2XO zGsDtUFioq*E)jJ!UAZjgzRO%CpIT#EsO~ah&TOw#ufP5CqZj1US^;!LVpepP`~o;s z@q2Bpvce=wcc?$X^c5$q%$*bP!s@(#mCLgRZ1d;4NG$NcJ`lvca9&uy}fOv(>Ir*BF$kXzcaoNo7jQF9-wg> z#dgC3byXU+Unp0#X|-%94j^bGpqD~2)KFe)Bmxse(@S#Dta}!h4E#IVitwRLSc5P@ z9n9=>QulOIv9l7)9}=^~^71Fk3((&DGW>pndlVs&xyOpFBwc`@2E zU;W3vwyFi)&JyK_iy}7 zKWKaMH0%jQ(I(R)E+%n#$PgP?mu0>D5KjOV9G~XoI_T*-vwU$%n9kv@J~1Fp1|2~F zz_0Qn-h>Sec4Ha$^K3?m14Ka2{ z?aYDlg?!&218M#mVSx_|zgJ_MW&mG*RwH8&i`5?9XfLwzc=gtK6?_oE***jMl_5OH ziP^U^zeK|jY?%|4q(P_2BRmh^%Y2DzVf-?VI&w*HPphcnC0jKXcQj=AjGHU$3(|=x zOGFvQ&z2GfiEBM+%=F00lh%sr{RRl%i}zkPod};qpPznV3+cwf6|X`Y1*9&3441$6 zrts>i&<0`Z%phc0HxbS^5z*0ocV>@UX4?S(r@&KGL?9dP`3~ z#z#*~59d@sL_QJ8O<9Wz_3&zUKsJjxhzx)Yzy$!opk6eHoAP}A!{LX2&BZnxKP*n- z(|F*)TPU0O#9Zd|_6sY!gRpY?x+^6b6qf-j*gf=LU%jhJZRg+7CW)F8JhL0L9;7E= z3Q)vQx8_qt+=8_$#{NCxJZSeV{b+gd&Z|FoI`^Lf_rPhzaAACvtk(;Ap2iC=$1&9Y zz9lQ3_)yOWA#bMAm(O)_6rU{xXHT&fYT1mIDZ7n8wpF?e9DLGvUV8awuWIqu@=D^{ zuqk{;m?FdK(vSZt_>Qy0MN1q)sHgT)oB3q8=8Yn4d6;zq#7vMOrV`!tasi*b`t^kr z%D(~`Fa@kE^*clQrYi{*F1WQs@5-*HI}R7KVIPauJ?M1E38c|9Wo!XuMPsM8kwG_+ zYupbr+`Q^QM6cgrY@NrmUMw`gAHVJ$KS(c-_;2p|zuZn*v|fypv$#tD?;_nDqYDkq z$xzV!&_Bxc)B5=M1FXOetQ!gG>HmmNp5Yi_h)k&)i55kXRFL zPAUj-Rz8;Bf-Ce5BwpYEfa#^AQderhxGnFYzlCY>YDl4gp|)3*;r0F(h4fnwN$ve0 z=|N3dFtvV`^KFUqZ}*Kwp(`>Mfc!79dYWFW+Ik`c6`XkX><~AOdI5gELG_g$Amh~~ znwPj(^QqH=As+of$sp=6TtBv8JFk6vk(q|c_{NHDf@J~Gz%G=gr72a>c1W{;Ja$WOVgmD6?E-N`{WhT_x!p)d1 zFSU+>l^Gq8m>6`wdAtzxu)uQlf}rjN)Hd)hOOV_0qY>&TQ99foPCizlx{?1Q!DRG$ zjW_#4zw_UA_OWo#xv<)v1HdJ4OE1tW>rXuaI6qo}kL`J))!zUp^#EY8vwD2K`{Rzz zE5}9}Tj*c87;J`q_-H5^8~`9*C6oZO@ZL`2;e*;A<|%UxFye;ClYjC1FWLVP0ZeD%+O~yKjdGn`>oRv4`Il@+~NhOPv0e@A_Fdy7M4hdm8 zR1gEl2^l~<$$TW~*1WnHSbJ(VM@FTOm1<6Yi!Q z8A^djNvL73FO&noh|K{K4;zZ77KSkLPNbQpn1Iqj7XTXFi(spjo#Eic$6*DjL@rW{ zCY$}KSX3Q=*fiC*agKK{@grnI?uKRsOCM`?*^^uPS>vPx(v7-#jcqRfHaIEPqNTxQ@rhY~zO5t*Vi&G~_zOnlK4XXI;j@g$jH%578wuSN;f+I4=PNoyBsHdI|2YD5J3UzumbE} zjP~a_^wOZQIte(bDJYHsBkef$+AHudl6Oh0sGC@XUZ1AwZP7v)>~}kAs-zMI@CDG{ zisw8HrEd7M@`Sb#k}E)3t!Bl|ZA3n~nPB2oO;61w{-AIcfuFimrBAZlTy>?Q)gk%0qZ5(f!Rn@09LTdHQQKaW#ZnN=}yBZY( zNC!8H1LFe$hYHw%$;6(3_~O<<+1`g^xjWbUkHXFs9)vxv8iUY_NcTpNsrp8gg; z8?)(chp97`bgYhG@eo!)FDQB6aRz4Y;Em81#-RHUf7j`b_QHVj#s`2QjlD6XX(*ca zwD^b?jyqE6{|J7LbCdf6MfA>xv10D!TK0Z!cK&DeyO}S3Tc87K%8Gsb_KyWlU9tbT zXa>til`%3Lk*paFLm`P5Dg0_vp6HO2`ka(eS`e8RKITiYD-8)K$^go`#qp&L`SZyW zr;LvP_X`K?qw~27YxrQycdpB%hoqvfo$w5UBh}1#0xx>;HX+H-mWxM?4yX+hFhMkfS4aJTovgkY*}t;vn^D`$?IM8VayGjs7mziY)-b(IYvY45qSx*;h%mz3P@_kiP-ZQn4T;8#M#BWd$e$FEuzo<(snsuJWJM)(jFZs$P37|%F5qv zUg;q!=&H|C6n;EZ36 zR?AHls4HKfOr5>Tb$S*vaD#~<^p!CA1MVB$vkfP-D;htHnvx5t) zYmHm!&y(mwSv{+Ozih$U6c<7r_jacpwjrA|lU`K|2U=o?N?gB(ppLEko`=r!To04& zz5JbbJbXG1KW&8W({#}c>@ZST@$A3ZN@Z#fX}%b%8v3+nT4ipjAt&R4>xLZ2)qBY@ zA!f4aS&Z}zPqZy4e>3_H4nIH*m7BEPBBEb^-^Ujl5NmTRoLd;RCpe3N8VRh^iA2&Q zD1EFrNhbI?BO@~39(x@>Hb(xcS!^Ch#FxaB+7C8XKfW518#@lxucdaE^g_MIC!!1D zOwdubRbC`}V=X%Te-<|KfB?&W_Z#Z(TB-wQ}oh<%n^mxR-tt#Q8jnB28eYao^ z62n%l-D{qtCfU>7hpyVb{1jhH$waCx7w**Yw7ddBtFJ)ZU$I zXkxF3Mx1%yT(vYYhKZu7i$y@?@Nldr82t5RkcN=p0{Zsw*~a2!vDlA&li$*R9pbT+ zGN%Hjq=vNt_N=HXjPXrn4x<;v0}lmvgvqwjY-NYA+mWivOpWQ9`Sb=}lUV-m(XmzX z%TL1z=JE8U^O%PJ{={HQ-h+X@3F@Cwn#jbEDaR zM}~`UEhH_qSD%iY*G&F!L0+L$%vNo1cdLM3wB3uMgpF0D$ue!W{^4kBXI5sI+3B@i z@eJgrj#?(`nq_WH=4Z+x3u{Hkh+zRRyM@rxyi7ZN;bv8|;}EeHWIkmCoL!)dbD$gH z?clH=P;ba`=|UMPVh!`0LjTkwOYX!*q_zYBX)_K~lqnom?NyG{e@%pcn{jNd(G&=F zQn))3{3QANNFZGMKv)vw@Npg&E~gUSR(5vAweP4eP@P3X(@*8pexJ)TM42oU`1(OI z6rqF?tcoh#*jV2u@H1aVCS&W}J;RuPN3aiWJ$q+O9=IZxCyRP`eA~1KY0N#sd_Cuq8D|!loKV` zq%{~)Zfg)+NU(=w`gUXWC^ll8Eq+;&ZfF^Ek=LML<-MGZMV&x((=kS;yq^oD9iUo)%K`136%CJMli1k#P>u^om$)SHq^^&m*cjJwP+=o0i5#3XgRxnu9U@$el z*mserIL7vwjS}F?`0fNaMK|*qDdR^IW%5hh?nP03j1Hs+^tCRn!`*QQ^Lrj6+Hyt+ZK z#ipj!jkr2qEi>{F7Nh2C&(4tw(93(mar#0^L{W1HFQ4}-OH`anVGjcjzM#Q=PJ%zr z<(KHJ;Vt4GcpNpk#WYIsCF9{0Nn19jdc--#qOh>q__fhyHRZTU02Y?S^Z^z_xsJFN zt5)Oku$b4JAZD~~RnrBv;G2|>@hJypgTzlqSTOSt|-~Fr0|jSjuwcJuH?H76Lmw=Ze8nVAfWj!hNg@Qe%|dos+*ZfFJF{) z6=f&*13d{1(D=obt4Xm!_cIr$P;x&Q+)B2*JN?r7BSfyH!BgdW+H>1OGXd}2yz~FO z6Xey02_5Qb%-JygG&D$Hl>gq+3Lk&4(jDzDb2$e`3N4wEw0f8Y@+)pmOY9y6e_5j@ z?A(aQ1WRqP`86hthfH}%RFl`FQkmE+5iMs&VGXzDQ0GL^^s+s_h5Uu!y*%2(RV_F`ybYSm*CuhYkJ728MFl%RseVeK4Dali} zxz#QK;e$cylD@{msP2+(OZtxgW1FjZsbc?7d?pSY$YjHN4 zi|=J>{kiJ3G!VF);Q>=V*Y9CE&#KQ;H8o(4BZ8M46+%DG%a%mFYd2JwOXPLruYc*m zdB;!tk<}OR^F16?=awOj?Do2=`B6j&L&rm>C8%+a2P0-e!yBLJ|-T7P!t-8!N`EQRUJ zr|*xG>es`UTJI$k)j0_>WSzgQnk)~b%(U=Xyv zVQU}9J%|(jTUJYXfy$>Q)wZv`juX$*^PXG4%>+Q_%x%>n0dL}p@wui3+QjnNMJ6oF zkUh)FZJ5SoF%%w?eKwOO&r`d|xw|5`E9gFg;Vt*}pu z=8;A_C8IAYyMeR)tV}8&o3aAg$Y0laF`t>)ej4dmXd0K383KPv@{$-Xig$P8m$fp> ziM3@0Jfo@P+C(^X2}040K-0VliG6SlUTLUwk*+nYI1lxpf8WK!otq{g&e=TAOX!;~ zR_URc2<}*zM}G!CdJaqhiOmbPURDq<0x0>lOl8zquS17SV;~VqBYSO{)#Kd_GJrTw ziHAi@rU)(El<;An_8R1G9-~HYJLu}pctlo`t zE@Tofp?6di@+LtIb88ZHB%oh-!tSbR{OZ^kFzB;ol~>B-K2#53*#L({r<==ufc+VRnTf{q9mW zXZ~a@;0@-*gI;3dqbN$^jfv6S!4NCr`7g`@`U#u5y_7CmmAz;yy+sojA|(oaqKR|n z*Gb58t2!=2OoI6>uIi3iJiJie;N*X=9nTF5<342^{WQfX%e0?-b@fMsmN+)~D95wY zG>jr%ovVHVRT9ORtl3IA{9kysjZ*qp?b%$xmI)B9fHY>lAYk%FO~uj%LOCb(cQvMhjC7fa`@u2=$$TTrEC z=7JoNzCw+m=$k9Du)>0FiRvZmUKnC(_^~kQVvZztCvXSZ(FN!u_3eH?)UHb1ElB}z zSL?FJP2+>p+6fj%&due*rUAbUMRSEt@BZh~@}WpJ=Ao2?#@tjZljb%$hjMj5EKgt7sfVM5F)dy%n)GmLi-s-+G<246xs6hJIJ*7k zZ|!-8dg|ZZtMc`!yfTfb8p1n7$7#LjotVN87b7$P|BqGx8L$pr1!K){big@Hb^9Nh z`anQj)9dBMQbQR6eu*r$_UU0aK&+gnaO+0^JG!d=?B9;}T=#UrnTEjOXc~5Jv?QqI zZonDi@-HWryNfK-+ZjdWzdV6($Z=m0fS8g~sqlLWRE17T7NKnm>_mC*TQ-F1mZW2sKe8}(@Iws7lG7@ZK<0F*4S+0s@@Dh-2 z2wd7op?lwS?45LOnq4k0D(hOaXN})<*}h)&3>zx|e@(cjl!~@?IV)m2x&zuCLhbyu z#=G7!`Mg!*>~wl}lt_?k99M6ZLT(%+e;J;&D=JD@k>(P>v4Npfg7lt`#r>h%mEKBv zkbNTh}jQ8rXZ=g$;GkP|ft{3|^lm=Vq zF5XVx<#ipAm!y(oCg+o4GarC)$TzQ`VDJ0p;?cijMNO6KWrP!Nb{wfnkmGoV+$98R zs9x@2Prl~&*{7|8q_4J_V7H0sAR*N@lKU%uKU%yXvB<$wlFf5NA+*%-7vWzYPm~tc zS6h=O#Pcq=rbNDdSa7;2Bqw=$!QaL`M3rRdt1)ZTss`W>Pxs5n3i)=4po^*;4yjXb z;^0T!`e3!JWYmv-PZ+BYb!lb-HBlw86(u+G0xap z&sKVAoN;o8-?WGtDtW4E$hJVMb>u`Q>lqyHxb@wVvq)rHp5N>y zR^q4g*6Y$A>ngpNJq;(X=Xc+EJeAkfean7J9ft++^&91qxxkx`S&In0Ncf#5H68muey12 zb0aa9Rq|^n^oyg>&!EO7<*}=)D-@T|I`t@;7r&S**8WB;T0a&|Awg4sJa2~U^N~&W zWxaA%WpY!0fib?ipkj3G^LQ6{br_}=n#kJSCbwi47i)QcLt6k zw{VKAFMRXLMsRAZ5yAD%E8mDT^v(4VRaejZ-aq!(t^R>qS&8E&tDX2+Qxr-zc=9W0mM}gD4Nr8f6*>jy~K^KHX<-_H~P3CV2kv z)?TcxCgTNn!>vKa=iMZ1!`(DIfS4mL9m}LRDRc6}z36wSsTd3V>zvssdZ z!7KRdc`k^KA8VNu2R>TqvV{0z*kKVG4`e?NKJd@|k745feFECz9(s>w5qJ|Bw1_fw zA7;W^9c<{ItGF0I~h5@E$yiEg5;+Z#5 zo+|{i_W~~7dj}#mjfHx@+)vX#kVrWX`7r#)DIBXk{!v?fhX&X_=y^QgPx3*+x=iye zU^Nxr&wH*R!HB1Rt6_O&9}BvvK`QhJsi~4s>?^lvJ1{yGEuD>mV0LXe*S3BDtO~d( zBAZ^U;W9Nesrr-f4s952wW#!r0V-*Y>&7Dr6GsdHcR9$KT((juB1$xbp-O5%27o?C z#Ud`Clq;>Te!oN$s8c*-p$C1vYr1-3(n|1|mseLH{jt%0$5#A_uM2`K_AYmmi2p`U zfd1NUA!TfD!OMrlPYhRCBW9#{374QtTvMpY!j+rJni;i>i)Lv`18yjeDBnKF-_Gdpt{;!v1sA7QOI=_}_`QE@&|Yzdal!{--{zHd3{$Q_WT)r) zzo$3SB}_uWz4H1k=%xeEc%oUOy(ZmXF3kz)>XRvQ5mBU>*^g$6<29mXvx&@&vU$;HcGjIVeg0Q#QIT z!w*JGY%_~kzhFdqI`G#Nk?i~wZ!{L`u2_4wJ1wuNpvhqlm;aVwjc$L?d7FQEl0e?( zeu*pRxTehYw`@vowo=oYcL$B7R@-v!!NL6!@7CGCfOJ$XjjO7I-yr&>;o^#)2xR*| zH(pJppyw}pPd{lXCl(5103rL(QVvfZk5BT%CWM%Sah?w;F5xNP$deWaZx*dMB!0;D z>=mn%07yU2zyHIsCUt}L)NAshPHG|K{i z#bQXW3sqjItY4^O#Uap2}Y8ese=Q8M#__?$uo|VYoi7HMF(Qf>9d8`uUq&LO2Yd3=gjp zAe)isb%AJ06X~X%s?U1gxzfe zeXRrpR8Jbeqc)UIu5d=H_I~6%g6a%J6-B3m$b&h0J~wv0Im8Pd`|Epj%iVl`_$%mq zIy~Lc-u5yUuT73!6pP>8u@4H#(BE04TmeA1J`#~5Y-UD~-T$1iz%i-eJbjK-tYy;o z>O`u?aoY?WK}oQwBta#5`q(!$JIKrodPj2cl| z>KdtlL|4Z|@`b0bPgm=b%?@{x#S4iycR^1lCzkqw7w>~ER#8ziPU9^gO1j4_S-zPh zs`g>IHh@UPXDdw)F`@usEu)!qI%MN&{iKG!m=WJVULEBn!It2Q@to(=H&H{*tw9j} zxbpX(%aU0`uEm;3c3))PbxGq1XY}`S2!As51wL;o6cGMXK?@?h|rGL(6 zw|}(AiIXtpC&oi(AHvOk`_Y9qwhYMmXX3(tUAWWf&m1Y;gC;op^ms!ks2vr57r14; z<6E?5w!~yjNJhINWwaDb0||55$6o$dt>oC);qmX=KnvVM@3t4sVJK4a;&ZvCHFbf5 z-s~aTTWp%ms>R2vZam{d!$1vA9JGbFv!&*@t$w=Dsy)x*Yj%}-MgZVrcqYbjH)ezv zklf*;F2+ou01f))rxO1HgEpHY2Fwi%)*_SOt@2$8+hWw(7did(WkIC47CbJ5Xm5kz&k;o-3DfJ$ zSDWa8A}*a6TETW4%i#bt{p_exu_rlI{N@5m9a^b9Bg7*H=ZJL=|vtja~)B70J@NnA+ z6RTI5l^HWY*yhj!7A!5i z(-4nD3^167w_~7-C}ylE(v5BJA1;U|!wS~QDi}y^(@&J9y+r1M7NMV3ZOj0$mECEW zL574<4gXk9;<5SH@Y`7bk^yTs| zN$~m&sgmSu;$4ci2BCgec&bli3$*-Xk2+|CX2xyXX#wqo0LoJ~bfyi}d)>(@xnP0h zEAGD+#+r7R5$;oia{yNIU~$DC_~^p`>vk2E4A~Wq6K8Ohx0xH3%On)N>V4mM?2BYv zYt!D5)bUO-&Cv;a^ld<|HopDq6Ainp@}lA#Zxm6T(~LxSx5?R5QDhZFx0}aUtB|>r zXnw>ub{O2yaTY*Pk()X4JvcH`m#_M}|7BZ)Nck%)d)abywo$v44=HOOvesyk-6x+( z$;0tqPs@>^zOcaO*^RlQV)h;9Xd7Fv0U#A+TpGZJ=Bc@kA*SSnSo#_%9u16QSp0;l zPHJ?1w$(0EP#+9?2QZyrFZ@X^PY*nm-fhb@RD}7_hE0pQ)d`O)O240yOBf$On1-=? z`)0lX3gB}3IsJ&-lGmaj9(yAds*QO7)#lzLtJ3%U)YYRAUHn)^1@`VEW1DLZd|Oyj zloa6$970!myV7IWNE9DQfmfW|pTz(S%)NdszWHWIGpNWsnCrE^I$FFvr5_p@$Yh>n)ZFWcUB87lChYL-6%M}+d%U#~=TXM$E`qTp`C%&ErmbGsBXBZDNhmx8D z?gckFsN|MpaQ=mA#hoESgulSQRFVtuG2Adq_? zOZrO)4o|N*CKPi#rfoAzfwp%j+3=j+o|6B9A8Nu-_YOx$wqsSnkTxih>_+7BQ(LWS zqwzL=VMp`6i#7}dZ7_@l(k(^=aA#4Xy?zWW#xpB;Ac{%@fPFIyfGUT1?4?mlt(Cuj zuM8BZvqAi8tk%=cDyf7!nTw~%qlybB;;+T53-`YvFR%T$d?QkL!aCrz>;ieR7$h>O zd7mRbsmj8d<)2C z#hk&XWJGX0hdYLp_H(r39$SY-YKmww7k%m4a~`0VO@}sBaQ!de_v z>TKhKCkaCnEz}GJmLKg&NDsan?93p57Yn%L<*53$z&dh#jhNkk!E?gk?}8uZ1=@(v zrU+wa2Kg6rQ`P}PC(LK6YV^?ZFczhTI{BBPJr z%U7m{XYbTB!c>t7aG1K7vS&NkikX$9>o@Vw46~+HVPxf;Bf(~^{b3>UUb_5oc(wVY z#^h40*rr=vx&)F$@=C|O$Gwqa+sXZdLTLr!v%$;(9m7OsZd|E(0^bWX4(gB>{2>_f z|8CUFp2=yxfxpCPazoQv$fl(qh`SkFnQ1RLUzY+)SWl$c~PWh@URi&{rnZWoo`CE0~N?`{P}Z?`$`zV^4@?|LgM z7SZZ-1rg??wapf=GT>qpeFP183`@o{8ls7Q5e1oHH``QJ?vQi#MS{;LizR6hqy|}? zI2mmDDm}{MJRwy;LzEPftI+GKhXY4~Pl0hb_KCL0SSTwUMa~2z4HTw~#rgIAu_e#b zETVrsSq_;$rEI4iy;=_Xy4+1M z6Jodyj0(@I%iHYPe!Ob@_4j2aiV5V~hNb9aaE^1jP(8t-SJKt}A0vIh1^-gGyqAbO&{rzLg3U-%Bnv#Tkt|^E#^SEq={}k zp382lljgVmT*OS03W#0^qcXtAL zlnYo0uQw!fY_H$@hcj+th^^p5{z@2G&Gaf!^K4%#hQh`{RCPeN*%o@W`N5 zI|`3@bwT*QO>{iyyN3N$Rh}AGb?ahagdCfxdxrShpBRhU1-ANZ_8t&?GfZTm6viW2 zLE2_>;%%-(0w>y+rdQ@hL@T58P#3cGst%zDgC?eDU!8b7rV`@F(e%@+a1TG*1PFXs5ZOioQeR($}6Inv4<%5 z<`Uh*yCNMU?mL1K6^&~$xtFD;}Z$w#3I;QXMYHj)3JUk`WDA_ zsi{^#)mXGuz9wc>ij-yHZuq>%ek+$c1T!t!X}zylvrW^jnq5q(i=@lo9VTgjDp$* zsxSA&f!FtMUDvWFbr%7MtBCJ`jC)oB+{~ogKD!cUe#CbZz-AbH{#tnSHIlVQI~hB# zN9*p- zSj$k1@c_d_29xkX8|FZl1+nn)(i5?w%9+0p^#8;a}yW4 z%~vIGjbABL%4ngcbZlL{;U(2Ra^-$x$UTAB$9mO;{H-3f;0 zHuHnOKo@50iw?BBSGlyEqu*EpHtPmUVJ*w}sHj~m>Yg@lV9HVsAHy_|x{aff(0-06 zuc4JN0612t&I)M&$7JgDT?TW~cxrnp96#9Nd~-df1`}hDlS#A+z?CT3ssJG+8@+es zv0uol6SbA50O1ElbHCu%8Z`E~m@t%yCX_puGMiYj2L7N|UQ6Hn*sR?IyW;Xm{S3w@V2Zau!2Y0WX?INZ!Jmfb6zfK7Q8cvb1x6P7<7b)zfOsIe5M9uo<${>Xk9 zUOMVD2R!PlWcQi*jDzSF+|K7|LDU!*!XS1&|_ zRHiZJbRYeb`h~U%BSVB4kuz=4MY)yfKDSHM*ko*JV7dfW1wHtNK4{nosX^8)X)tw} zSKsy}euKSEb+?X?QtgU#nY(euNKdci!HK+>f?>lC<3TL5K>BEx&T!p{S61veFRpC@ z@BzSxzwIi^dPEC!Iru?YHBMm~g2pb@-b$7FkoW;6gSUGb4-Nqrdw7BC>_Z?b$=IL}a6dRiEZo(m#Wnn;#3w&F551FV9 z2-j8=dLzsb{WMFERokQ3Jgk!tZ>f4UoJDnN6d( zYkYC6)ymYG%K|iVceHY*bdl)VL^2bQ$m|4u*S~~a^|VGZVI}9oROF;vp0vmG)D8^h zN#N-XA)Bm{;c$N*`+L@Zj!}ivjVERhJ|6&sQ3Hdo3x2HpM45Npm7nJVu$lDXc!Pl7 zLWz<2TaX#{^iw*Ga=$#rYsp0vi4-922T3ODLU@9d0t*Vv)+HV4eb_OQ$Q>cN5Dd%s zF{6DRyVE~$|Nkq!1z_uvO?VAn+=a#*@7d&Q>2=_-Au6oY-?}@{B7l){I*J9zJ=hg^ z?@==nyj)`eHuc~AFoa$o)Rg z*q6MlV41tQ3zPCpU|3IRE+FMa;{yRG%#|)%4_$1Ztzf*x;~*wCE?fp-414bNOBIvp zI|?iaRrRl4x#i%?k;QiBp9fEk3eW|^?hjcnT<@U&qxc#Z|4W@aeOUTxyf12 z$GlP`lOs2L;7K83c(WVR(5&#?_Y1@CXZcoxl6(=;l*1n~Xfs5fk+&*r+~(bT9Y@Cs z<~!m;ziuRt_z8G_>7^csEm0Xkpe-g^;IflQ;Gx;bn4~jcI#{7hZQIzYjpjTZ3N&)R zoF}n28#W5t9Sb=hqdJY~iW4@r#IuDTB5NN@86Q=ittC@=$5g zBw=Jy*fSDLrQ(G%*(bxi>GK29IchuiY|{=Yq0$PK?J_`!3;&qJ#LB`L#3h85RDaTQ z3~duIuK=NyqO51LTX=6^wjLwmlm*0-;5wLWWX!c96r<#kk0IBP!hX#@o>esS@L5}@ zHJ%DJ1|@5AG|h94jkNYDNr#g_%gc1}Qxtdl4NH6D4O8T-6_%` zT2{O7AK0S$&$M(!aHu@HTu}n4L?28zx|DZJ;wBilt*Z5VdnVu*@XZ#E^WcRfFry}f zX_jbb`?)amCzYbPXMw=a4l{SX<&AdWt7*^deaKkzL(S6L}L6oy|3vP zk*-@+XKcRmQOiRLsONt=S1oW;)k{t4k&Z?>qMxv05_7fw)sz3&1II7muAfgrgRN(; zzft&Uxnd%JA2prB%N*RKu|!G&OQW-93x6gUi6*u-gOsL)3_0CO58>RiveY4?7A&?> ziKdb1oYB-U;{PqL;7?f`3;{@^^mFf_3L3mOu)%l!;b2!6jsp#Jqv;wZge_6gxl zy|%pD4w)Ib`XTdY9|mAVtqq77j0nEOB{4*qh{^V1q3@ysloZNMCDs!^{fOUIm0ws8 ztzSWX|CM1r+Nby)pXQ{)vRB_ufh0|4)1uF(zEjr8$`n?A3r;{hRr;u)Gv_t6X-Kbp z(immUp<+UN(eZt-_YuL9*llt)_f&oDXy)9hO@n7>(#}_8I;kB9`4oa@=SwW$7q|wp z1UT}BHU1Y?-=HcT3>%(@d5F90f4Rw~T(|SR4?4-33k@3nZSm|)X9`hU8&1LsdrP~g zi;SzNdG&4=G<9BTl5en9Z2ZakF!?;Teb$4;nDo+6eNzASB}|g|3a^6qgEA{>FIk_R zTSYQ;x_)oy8yPGlc&SXTw%XGZ9o?u8;e{MHarlTGh#eAq6h%tZC6)W-c{F0OVRLXO z`2o#~KdY9u8Sk=#eK@*F1?X!H;vol#bSIrQyE}ojy^ZD_qfCr&rVQC|tWI2TWZpM5 zAOr|>;tQEqrF@iG&AQ1VZ|Z&$73g-#v#H59#ycXR2-P5%kNM+pS|jpS!d-HU<^^Q*!S+isw8=Tn_4l86WN!3CC6C}XN3Xu+ zx`e;pxtKMtS&d+4%MBP=l4Pm{h_o?gt|S9)M<;vlAkr$s2ruL^h_{>Z}3&{H!^r_#nx2>CJJD z&e}U|GW|fE=1HKs+;oKp=(%`K#>P!-FSlQ-q$i5f)+;mYaqpXaEPQmbVB(f@EbbbR zra85>=8HS~H9I-I?Yy~rmw&b`%DAz>7iB5K6-iJ-PR3unzW~F^?cA%ah>(fV5lH%D za6A*k-9*I#3ERDX1aIo=y#I#{VGX6W$1&AUNGcln+t$Z5Y& zhPG(7So^EvTWIdB*^6Tj5tDIt=kXShI+x$kwS%R{oZ4xmodBp|>E)yyv2)gpgi0I6 zsC@Nwu|60cfX{TO0U#laM{M%-W<)L?@Jxq zi`3sx!4$T%PMIEF7`iB9MKZvEg^;dZ-gZvA$RocTo}Pt$jv62@MsqBg0C z8#5F#(tWFHO?8Oad;GkKYyT0||6Au%7954{^>EUT^LQ?ID{Fa}w&|>>Vn7)AYd^_r z>jhHwvm9SA{KL}-6dc$HxqBe>&{cb6U(L^*(;Pfa7BZc|k>=-4^r8^br)1ql>A$iTl!y-S;(FY7HgJJ?RxB=vb{JN+nu42D@yyG#90c91EHrmAET!p& zB`tK8?$-iQv6|6dsoKUIyD3dR!>4A3i*xQN`QQkm5lsFQ{4jT%_5ctNK$M8xd;iOY z3`?QUhk-|?WTo({-M$K$$(!{V&Am_kitPAM$5}pfl%g5bSDRJ4_M7ooNkKF!C zTS`AvOv9Xz`F^z3EeauDSJA#GzWXp0G^2XP%5}u0TH`8f^r-YYXJA$2FZ$*~lleac ziLdx2obd*KvxK>(L9Ds9@L7!xxZ#*j(;ZPPRQHjeu!{!AcQgBzV=v>(II35pE@Ovd zI0?K`Ko7@Ze0l}2_g4+WP$N{mDRsY42XSG5)VlH~uTipy(3xDXkIi16qepckM##!J z4db@3A~q^O65cf~??$jt;A5{f(!1$qAyTXbPS58F$thY!8y3-^XHMhWYirFxUyJZV zi|<+OOaW_Yq)u@iC#hTBW`nP=!YM(3z>$0}#Kt8i*R~>j`q$DZA1?NbOmG%Ph=9V2 zrq&nTS~jypsS-{Wa4m{lb>H9r>yjT-Ha$I4o`X*?axsL*fc{_+{3g#)CR%6ExFCVF zSYw?4Kn7%u4sUdqk!rFLUE<*ph}EX2>d*RzT#svS|L4yaDe*eDHf@9lp=FU8Pyia1 z93Fog*#K{0ebQ_QHS!x`>i!+`f|ckI{1d$@ zGmX(*2}OW`j}G2bHiMwvM_s!5m{_Dnie9H+j05PoTst`0NV?yy?^lZr`PNbB%M$FN z^weP5wmf@s)!i%eW@1h6)_N1!2V&ld7Tu3&^yC%aDFU~(UBwJz$AFgVNpUArZS zx>KMqXI(s6WK^D|^<^ZRxo=G~pt9=c9aQY(0d?HZR|)oZxL$46xWagyz-!GAV7sR3 zbAnW-&9jadj#eNe8cknuZ4K5}a23&Xl-F(w;pOwW=jccWzFuJdSwfsueu_%Op((7e zgY>YMmm(GO4|9NZusu#F9kLTJfAN~;)-FW^qSI&zz z-P_^U&z~%O93c;5|H8uXYp_plygNDK*eupcg7<#yX9C)@P)QNPKztbB~2FqFm8o$4q`yAy%J{;!8DqP#aI&q zESR0NImjU?pW2n_1no}*9pXMQ6}+YM*)ZlJj>3=mcnO^=ildv(anHQaNQ$}n(l1F1 z({#-{a5$#1RfO3%n=T8z3#$q)YOkrtTO%wIKW#6DRa54PJDEi!geh_?68)Jhm0ubi zAvh<;|CNWpC!}%l^R?c5%(OscR!wYvw@55zc!^Oy{LrvzWytU}{4WXdC+=teYcmym zo#y5UfSM)(K>ITH})K8^g8%@<2Z=}+l#F=@_WveX11;LY!S+)wZQ$eL=yZ3P{W z!d&OObqQoU;68?MDn+)X=_n7Jzr;-9DB{nSdep)fQ$ES##2-5r%^&8g3PpDoszeG`x zBvUMeHcT9K3niLr7_1y2BOhmNl&j8C!c66lMpxL#6u0riXbF}c?BEhr@xz zA|<*jM>E6C+(AVpjJqM9={8zqqw%0J4j!Y3Gn1qPh%A{3(hG^Hy_fyMr&I&VuzG_@9lBSai^A2uwWXfK(hLtzSm;L6& zA(3QfB82#XNV07-Fb7D>qs`jkVF793|ECId$baG8aW7$ooC_33>4SWs#*U^5CY*NC zmjhoKlyo z9t=97EbFK_ApJq}Mh%AqGTZJ|xP#eN&bDgPL@XT-6+3i7sNnT|nwl>4G7hpTKeSz_ z%wQhAH$5U?47GU1I3hVeW2~&&TUd+uk~Go5j7um%Z-6OZPb zbG4cRk{LDCri)kda`T?bo%6x<@Hy}!nT`%GJFEYbN^u0xL=!Zv{b3q@xv=r=>lzUa zDHT`0p?(<-bqj@4p!TTb0bn>q*+2Xad+m@n+{O$ss#vJAZAoIn9gZ)6*nF%8P)iuh zjRX8Bq+0?CRdarapVD=oG@^D#nb7SWWMTf{z!lh0eZ%?w$_3f2LeWXRN z&$)w#qUfPy3%TOIGmHS?n|g20WAtW8!~#JFM&%kvEx{E5Vi?$%y15gPVI=Z4^li8c z*Wx<+bVJsl*1XVh!c75Y_!ILA(lu};{Wdc!-B|pG;SL7PP;dULVni2S04CM!?2h5j zp8Da|Z@m(tu5n+_F`eG^0g9!U;kOwU0`j{IBm5^`ogF%uf*-VGJ9!Q1B@{MP_0OMuobL!t4MAny!fj?cB z@1K(gWJ2S9WFt+!Y;8XUUc#$&2PUun?n*t!R>6kg5T=cPdR3;yz}=F`w&PyX8VE$g&8fndP_Pbpp#oc=P~Rx;p_igoV7NI6M?= z%!9jj+>1%9tgH%LEYHedBqN+^FV5DQOk16<8D*%Pj{v8B=zABXE&T& zp}exvBV4GAtatI(TS`$G{K=uOWLAbkf#IF8BAat}n=Z?yf0376`o{wbxce@QWkH)O zcsMjP->pQK@c?BDIN`aSn{EN)Mhrq;0?ez2%Ls*kMP1hU?<@UR-JYQR#Ay+#!#>rL z5uu3`iY+-H3_E*dAa&&9N5HWcIqrcEx;{V!pbcvUTx;GtlSL92yk5!I*y3|b|2#Fy zO*15n?I5*s4A$NpF-#ma2mNYC6nHKe-E!M=m~Bt<((*u;AGsrjz1_aJdD%N=!}|1N zo}y!c z+g~$z!w^MPZ0Oz~OafH^RIo~tJf3EoCu`onCZT8Vi$UC-#Xxd#00jUY=i_u28z2C{ zi?c%9e`g;(jL#Id`YvYY*N@XVVn4D}n9hK`{& zEmHAxFTQN8`8eM=uK0e4zq+lzU|PC&WWbazqVs9)alw6HFEs*n=nlVTUADa6ZRSta z8mR;s9raF%MPqOL z1ZElZxMjVe{+(amgm+wy-}F&&&!-5krP=1$uDP#=CxL2x=mC*QiCj9%iNs*+%?>AJ zo-$%vo?I+&UkE|m(TsPt}^c>HaCW@!n~V@G-(HK;%H?48>;~GpoPM3zbL03I}dRfSe(fHYZ*( zg2+q+8I6gnLaVOf*BR7H-?AYof>wiz5Iv+1 zkgq*&$}Zj5K|LPr+(JWSm{XagKk^_69v}3d-zP=LjGz$fhn8MbH6;fR z<^f${p!}TCd*C=w4lym_;)V)<`2;u*gTlL6jN;*ZXVPI~V~i=Rq7N3npTTD7+up+o z+VY!)?Sp0pg0ZgW{jc%)dH4{v_OGr5gI~VAIz=g22=<0$O8VNwOjTN6g@Wi1H5F*k ztf`L$q~)Vo8Aa{U#>~oPx_<)00H0jntqySOh|rRgMA5*?-2{cl5geHS@k1Y$#sEr| zrjeJ4Vs9f%A!*?8BskK5c?HPL(lHkwY_mX(?8w~|oR{RkYg3xwSm~Uc>I}Y@GjP_G z%Wg|QM0*-GN)}4Q$X?viXwvW}SLQf|ux(UmYLI7f z$!?#jV*=ix_d`2Df!)auGGJVRn9lw}P5{snt;pKAZTLMb@;IA{lJrlB+QDm2*a+6B z4;U+8Y_q~>yp9?w(}d918U;NcxmR&{3BX90g})h zb3VC!{$z-rN^wHqryRrOf)rL=?BSgnJRF^O#>xbr>Fy6Qu3BxIcbQrm{9(6KO-XBP zjtjRL*=Ax$A~~F`9%nM-iaEw@3l6RCbS(-rHp4tcS;nIG+@TqbWJ*xT39i~K^ZPBr zyj!4bX&_Gy68^{V5`K=%#wU2bPvU`m;D#LRr51^>V|ldR_$oltl4A6FCC^RVCc6J2 za3FVix37Df1ba@Vx&-7m*q@^!yF<#b{;?4)U0;sY4RaR3NJJ1M&ARvqzd5(RUW@Ws z|8=P?T`B5SVs~(N6AD!(gU;|3B-zguRv+iPtm5P~ok|o;L7D061bXr^lqwTqQ zo}QjA9Z>;j08&*Jmos&4qxWx4M_bR=mO8tFry?08kXNwIKPG-!k=$;)0kuOy1Oe2B zB`IG>GH)e1*S)uXHab2D`$#6?PCAR*XP3b1B}0%yq~;?c*n}dVEkZv>zX?r%p@rw- z7DLdQvzXgLqFdF*Mx5(eygfGKV_oF0Oh8EdxY)-=3*G7c&e2-XMj(yDhsqhkU?Pyn z)TW)k8cV4J2vOo`HmS@y8bvrNoH+$OI6KSVN~;k+?3;(Y7Ef8OESI`5CTd)bZq!Fp zc{5e55ii}kb!$HW{p|3;tdp1;999;c$YW%upp;^luN?8vT6I{dFVWRuf zV&C^xR~?g|_b%4rLxIMB^*E57&*oCE-@3 z;N&O5=vT>y3u2XV&frAHNcQ&&Fjq|-akg)Vf=J%ynQtB0!)O~Ve|U(8k-3uVp{MQg z=R!+GNskI(It?d~l1`W0R&lp3Wh8#TRHx4&hEn{dRj=dH%nC09 z6zS~nhsT!^-d)NjvqI>3Dm>@B+%!L_lo|~{Y)jJ%xd0FbUVmL3pN1W2eajEs66|EhN+g_PC4lbITeDHQQ}J*k`h!HZjI zHSA8tb{-PxNEZkK@K)a=@i5CFY=S!xc9=8o5i59E=kEe<0$OUBK6C?#(MzO5xLFWi zmtKH1TSh=u^NTbR$b&b`XZe2yAB+f5-lss2V8TY5v;hqo5r?+Z&q)UUO%66iIo7@! zkBMT7qhR(I4$|j^d7svLD;WLIjwD9lKQa{j+(*3B6F}Z>dOx|qUC-PgyM4v-XTG&7 z(I$4a7dSkJ%k~WqX4vw}=U5|7<|HoL5FdvWGR#jKkz#jD2kXdXv|a0D$9CvhIT!pY z`Q{6itc8X|SwW3f%Y`H*ac{0b8>inUzb7(#;a3FYcYgL^ z$g&o~NRf~TE2=g8QlrCh%6j|yRGu4G%|XsvPW>K&g#q72=%Nh1Lb)$#)m=&A{<4mG z#$6d}YqQXpQ99Wsbml*O@)fA&Y(ByqP&mmqbZu9|24aLFC$BJ^#$Tzi5iRr#)w zx^PJp+c9ZkAVKk;Y0Of+9~?5MyK_kP%p5EYmq9jtG}d@m7Bt+qoK@5P)o2sM6ih{sAyEg{hpGsCL*ad=t8@eO*_(wU>2}^xmr0 z2#2CwXg)&1rq$+s3ZJYbbEl39=YQgxQs45<_xqXVr71_2I71KYc?jrg~)2 zdULT*BXYv?9Swxykz`iARM{12u4RH`u^(+d7{)SiMD|C|NwsfF^UH5O&x;$rp&7#L3sGTjQF_H)er@?iP>E@iR?&b?Av!tRA=X7-W3 z&!-R`H(W)>IiDc5AGPzYohx$|w{Iu+|fYM!&l~tua4jOdAI9 zOk{YI49ZosT2iW$1h)Kn$ab=OdcCk=1NcrtBK`GSb8FV`zL#9ilxeR8IK{7JY7`|yKR_MB6ZrJ|9cayKoy@Ay6c9DC|Ji0d7v`pzw;)NNz_z=UJv+y@8XH4q3qwvUs$ zP9_)h^sxP)$c8E5NyFl1IysBR}0MHs&aIlYCog5^Ft)|KI(nec}oI*@hL`!Cd% z_h#AVjRA!x=8CNyO8wJyb-%X?vEUMNAp5$0~lqqgQ5bopehAA~Dm4!}oBy1*5MHBtmmMW#I#r9v6=(QMd z&-p;%kJz5!4l`gh=fxR>W7B*A@N>`4&2Ptt+pEl7{dkT;ukbGeY;1g&u7s}&^hNx z1KJ?)a6elqxZ?EK?Uh!WS6nz~U%YoW@*$aZil5WnI2CDzqivZ>%l88YEMvxa9MZOs zEY0n{PJOlaNl6gklxUHx=)|(bhns9hXu#mAl8ATuy09VXs3q#l_>@Y0EAFHpcG}Q- zvvvW!#R`Z|HFp@OcAN0oL6p33SlfPNpC8e7fn&|+N z0MU`PdX@(S0EjEUbRQJw*sxmh`}47=sz5d!e*B-bcK$*giGl#$7ET+w#c8(;1=WTp z+Ex*b<;69`3}B(Y(k>7-%z;*vW#$=(z!YtS^Q&>kD)OQTK+4nYX>5;p+*)8rbb`$Z zo~o8=(#HG|k>LM{SHL>MfJe#LK?n+e%Ztbz>Br45Y5&*378QJJdhdzKu%v|2U@+(B zMl#q+Fm)?kZL++)Qqu(kn_jkX+EK){oGiEZyCZcGz%6jth=vNcTA&LaoAscRitjy} zfN6#R;sS@B#Wvx&0B*@5&T_}Dv;f<<dNULx11kP8?(njjbF+b&xjsmRY3AtK3l7v+= z#^|V;Pk!+JV@-wBYfE~S-L*_xa0<0u7iJQwVz&*d|Kh&J3!EPCRF+qMrv#fcw&)7T zkuecLHs5x#cvW>JQI()VbXB5TlDD1D!_GWyiD7P zSFyTZ!-u=)sf4=n z8kp-4ki|C?MV#|i!(;5C;uuQ(zr&lQkqQ@OG{xIhg}hX)xnNTp44r#vo$}-eQUbxR zsgMcPqq}l|;iP$$=yY4&T9XkxUTZyTML`(UcWfK6%7qephO>+91&^eJI7X*mCK}Y) z`0grn*O-#N{a)^MD6|_jeg&AyOT|o#XLF=L;}dr}d3Aw$zWN4Gsg05kW6h5Vd?n%n z_#(ySf_T!6S95raTX+VX?o*N)_@^Afo%|l7V7q@KAiZqOLxlc>UkyutBRE1 zXm`)_5@%Us%W+6oMSa=CT*_>oZ?c$043#}H=1tVh!MVeml&*os%tmKxkUv>vEBW!C z&hvVqgH@FO+|r3!>A$U11LskgYE;>-_%(fP{oJrzqtlcM*JJtJ&_{oNI&~>Jan*I= zGnf~@N4{+#_$ZR~NAw2utqYjfN!y&mIjO@F-LwiZ{VItxcd+IsmBf3)`o!sFPtF-T zEde%rBG*GS_JE53b)kwuSfjl~(I39LZ}>m!b@-bi$T`yZwd-Cxu5Oe?-zE=y zVtLHmO@n*)Zwo3{?>?SSvEe_%Y8Ycrp)HU*`kC>qO`orOdIj}Y^Wn=y8t5h3S}$?Z zs%i7~|v3HJs zHS9`N!Kp(eI%4wf#;C>jCZ~%`qqRj7yr+%;OLqCsC49AIXtk+>XR9Ts!Nskw0S4U6 zmsjZc^B>fL=|rwK#GI~I91xUl0&!929U{lql#f8KGVcAiD6owb zt$p^%!$MRnm0Sh^xY?F_>n~byCsB;C*?^+VfExk$NLQcoYea1gKX}T%C4>2uBW=Hc zOl>xNP%{di_^!)})wuY86gFZ^8t2q}{I(vWv*)QG6`bq!-Z~-v^I&{l9#EIID_hK# z7-Ty2-q#Ie8Q{tk_WhfHtqr=JiRj%K(iJaW)Cno_;szTU=v<`5!O{v@Hfqso|CueS z(R{oBUSu;OX!vD^0sauK6rI;4Oc~M?B2>8oNZd)i-$+L>YC)1^eLp0hkGjI2ArC$A zowsop&6IoWAm)SarI543m|2kDocRr0T*v}xZiYpFN@?>u02dLxKUV1zAL4KARQExT8Y%SQLxAToQC z)@7@4I~x;=NsS`Xivh|47|-IB_KvjXm7JptoVr}*hi_|b^#{2|`bHZO8--+NPJ=@@ z3`}YWOGx(P$-ys$p4oF#!>5ghikN~f8ucqw;i{%9s{gA8ypNS%8v4&^gvu$Vam$_i zA}jBV9;ULr47Pl{FF9KpJBLhSbsVU&Q`-tgNXIB90w-p&zSi-};B&8TKnWIjy2z^S zpTKXsqY4a*Q$F|bw+NzaaG_195A=R1;Q=Tug`tWgu;#{m&E=uls-pJ-eB%*K22 zQrs{rK=f(zGEo#FXkJ~s4=-C7BXwUwkw@nH=aHGlpbVX_$*D~)u68a#IBmzNalDnGe&yTXh=-?mj*&5Wc+;fA2% z@OEv{Ol3aFHCgm65}xTnq~y$+j$@B5un8Z4Kr>gUp@8(!(yaSIn_DCon!IxpKew-`wPQTuRI52#P;1I+;Vnh4jdi@^nJ~0Nw}Y^r zNC(m_dc|US6W2@Q5oP~cacjom*dDm# z9KZ5X-BY`2HfZO|ra=nicau}dTP>T2^JIOGYvftXI6|+2Nhv0SH|J4Gl52|oRl)VR zNf+!Y{Bq?rg~UUYr9zeh@o>1(D=Ze``W@Ypm7sL7~I z>qyO3Tv7lWA#ux)#jjGYJ$yI38o-xcWJkP(Pvu~irrrF6k9qosSkvEFnwr}hLgpe~z$O0!mpxu0bQZmeh_c1u(xTe@DI z)Jc6j17(02X3qCf)hDTZJ6!FaOmCuUUs()Dp97()s61|`(2H&_au^z&MG2(rOPNj7 ztfuiW^J=|9S+S>v>DYkqZ(gg za?}wzVfdBqp1$K}pZOHTtVD{l5Q@N`f9{ui_JA z5R+qPSROhzg3OU4$NLed1-)1Q3<~P6&nXu=P*oHn&;>(6DIdxk5l}gy$$G|R(*^n- zInli!73FKBKpJLPs%lSd+GyIPSsN_Ru))3uu4rClpwkfbQ4^v-N%uHz1WYuC%*vp; zX;{BY=UXv&5*J)g%!gR-jzeHJ1nH+*yH0}@F;5p0`T}4S%A`btJgpE4>e*uEb%O!9 z*t!6(8%;fNGssP3x(YMH8{@MX;8_a-ur;iMQKqO+@e8*i!0SdC8C<)glp7jqvY2#fYGMGocj?#bnkz!fV5BE71KkW_SiLRJ@P?sW$=I)@5hTDuf5Ii#40aYb zc#KszVn(H`J98qds*c5bZJ14eCyRw@S(Kdtt74T(oc5Z#==$3W*kMnZq+HuT z4VKLnq*!|b03$T0#NR)g?ZKIMF6GqRC>%hCFw02Tu%080G^cxxnj@)3>Nr_+=tRj< zGl20d-L09E0UOh@g<$QLluyH&o-TNg(nsNdw!N=C6N0ia&SemjV|N&6*>=r*EQ}2P zc%;5A$I~44Z4W>bb(6IMnnyW-8IbK<1};x?2pJ&)OT4eR20A#02AZt7wPivaEUeA3%KFfRRom4x-^79AROj&imzNr3=9$=Rf&IBG%m-!GG$rt z^LjlpK(UWmZX2K`bL`!s#&k|oI9~5Jx=Noc#c>y}n?iIi*k%r3vl4(aDpOOGF{Q+K z)Z4w1G*P8x+&JfYrN((q{@z)aQ^?O9`k0Lhh#~H!+s7aa+8DxZF0^1;7RRo0O+LJGHi8c)Fs(Ozn`<^I{x~^Td_y7Tqk(ryZy-he~pV$#khitexB&TA0n9?;q zsX7w>7SYB)oXdTY)X**k7TgA))$~vq(@oaGz}EH3ykPFfG5{Mh$1^uy+~!7&^h`6e zzps6fl)=Hepy%z{*v;B2xI%`mJ0hU6yiR}04Hrs}oG?0Ih$4EIhEq$TiP?9eI4YTu z8ToFCLT2{L9&3flYjomOkijqLMC(!J44;{p9A`x8j`=9errr}l!OQ4!Jj;P!jg8X% zwYzczbHvK=Y=`PMV0Pvn@}uHWeeR5-RR~df!nZWG7x}}-moer!3G`z_TlGAoSNFcjMF@cQo)(a}!FmBJc`2;HGOW2l$@gA8pv^Osk?c z{1Ee^ zz*=}zsnbTFk4*0(rPBk$nP&2YDH-HIt0W!8TfUQYZzxygiNz`?y;joe0{Ryqw7pvT zFf?j_(`R9Cj&ovH>gbWAlY)NZ&H(25iT3K>!ZUz*&Z5^ZC=VIC4DPPf(+wbokT&`z z1^B;o_Xr43s$vLS){LG=Xv*kS^u;}v{al2(G~2>=V1YLC%_W>Wro5$@uaz}RjyrbsKGu&!!! ze|7f7dma-&FGch?X6_0|63HaiX(*@680E}=c#>IDG;FAH&^B1Fl zQ)XnY7%6ahxGG66R`_+{`ZlnCiT5jl7W>>s3DrmCc#D__e5*g!#{>(r;Pw)lY(D zsUqscK(2@nYaL+vltP8nZJRWB)Cwc#V~I6)qSm$m%at6%m>&4Q7^A{oG8+-=nmz_h z4TGR2deQLVRffVsV7s#;XXRLT!(#%bYYPG}Fs8!~zhwUx&z>(0t71lNKX5`2W{%F( zTI9Wl$|dORI{K-+I_H|D0>I^ZE#=f;Uk*xj>C?`WhfQW;a_o+6D)&(dds#n`^)JtD zgv;nzR9BPZIgLjs)_pXbwRU! z->zulrWDfN>uMY~GCKpX(=O2od+z#8$Dk#^91r$w0VUNCimaJ&3H^PV zVcItP2&H+hPk$%Y0)U-!ojUS5H#J%`L9a90|2_(TH~NLJRD-%yBg5I2J~oE5rjB;3 zPMviDbRYn>3a7e`W!ex2+cK+W;y+tUYZYR4S1Lflc*+)jJclgTZCL-ZPrnAl#lWGM zS(uttFEjn`fY_RXfllf6!ooEL!I$eZK}`TJY>q?c+HX|c<0P%5HQyP;fIbu?Uxj0zlXvKYAgHMcWt zt}#mk{Y!iNMYmoZO$>~ub6XR}QJEQlhrW+OUZa@Zy zj4>TmmN6~F<GyQNdBsJMMxsY!0*wb;)Tt!LcP* zVvYJ5z!GUBc=)0f=bY0?g`|-dbdsCI-&`ZsW~nCun7Ol&8-&yn4usd6+&d}98L>^@ zM(}cVCMG0qk~yB^z&E7_{hWhtm*e>k_4|3KLrkAR?n(il76?Q=&~>p--~jfrFVN$< zv?D-jh*%n=j7ECxQARz=XWkeJ1~P8!7_e0Hmud1HIF*EpgG(=38KC<$zzD!j*R?DN z<_^(BU!b)Bj@uJ`!62uCNgghyCa|!e_q?1}Zn#T7H)vmKJsBt^ z+>4e@G_Qq?2|k0E9A`(Gi?(>EY*PN_{gn~s^>W@O`FG(fCazk7e-`U|`4Vf-4IIaz zlpaLHh?JG+R6rZ{nB zgM>lSP4@9?<+)m8{9DqpO* zwLtj+Iu1&!!RDc)`sIbP`C57fk#FfTh{i#Y)XVgkyN(?tUT8vdYyUjVJ^3A~i^8{8NOx)(V7Vp2!1 zB(x^1=af>KU8D4kh*d)|XMrx5GR(*>K@m0X#$Q0+?yg*0>wfvb{A7X1cs z<#q7Fq14=JbmRR|5Y@|#R3yFBNIHq>D76s=a>25G%+*Ob9vn=ojt^N6oy)1evf@D# zR|d3SIl(ieyYyFPeCF(8gD)E+&A5KJO&?@8ICaEj&i#rzNrQDkDySf4#TP-Uh`4L@ zmuViQ^ih4?wMTEWe+=3Zcs$m31+XCfR4iQB+~rhN*}O={UNdcieodZ^g%niT%ms+h zc}fB-=#M0+6inze(O09}^nv|j2BJUEG!ZEq0Ea1Uqkv-^Vkc|d&r-{?nSD_ZW5A{K zT1KsXmT+2h#)kd{$3QA+qrzU`18iq43|K2u$&an9)Id+!8VG`Mj>ALR2X4I`rGWsr zHVs|J0jH8W?m=C0ItkEa&!aMF##--EQ%ViJN8+jAwvaPo#DZ%+NbJnZa{?}O@h{G0 z(2aJJJfagr@Y|`l224y{WDt|%?AUHS@G$W>`$vYm^bbA!M_i7lCBhZg<#;~h3O{pe zJAenL8m<_etRNPk7>_x7+3|o05V-4Z+WmUkLK=+FBeU$FmlSeuvoVd zumW5(Y8ncf8x{kXk(n7(=hO}IJP@d;*WS(62Tt~`DTPD;5T)e|SQ@@ZqZ-hFfnn_r z?YtnisyS?&zh1lgW?;k?+ELmm-0Rh`k6yn*_jd^-NALHwG6@&ohYVtJ?2fZatgjpo z!Njm}*1w|qT6TOl0 zKGLJKWDQMSc>+YYR7hO0C~tW^Uh z<`|6DA>D@=`{O{&y9&a<6fEj38OW3>>up%fTeE|2cSH#&RD!j1KTD}lV)g}b*vQFo zLcA_?uwKiqn{0NM;lQ^4nx0?FtjzNm0nLR! z7vy)~qVXDiFn-MCcm`uv1IViYnt*f?O{yb-j-kEn>KcpzB|FBuu9{tQBY_%mvmFWS zY=kr59!3BxuSvzyxYQiWHTHjDQHc5pp!x_(^%`j-VsG4dLi?bpW2q)|UYbdwY@5(U zPe=eI+Bkk6Vp(+R9t#h%YY0QXIryHeS48n#}=gU^)O3N|7~mz=oKZ6IFFD ze8X0ELEE2mnhJoD?&aRKxf_<0O8PK?%sG_9r2v;&n=zrACD@l04dmKfguU>N;hOC} zgP0tg~s zSD5|~U;yXe6WX6Z4^beCgBNI$UlBmMPZK=-f)bGF>9wyLHojx_f&dOmu*=y#?SGG1 zri#}0eaqm2t9yc02B|ZzIq(`|!qTFkCUdG`F`+JEP|(tM5$G^OubB;H4&Tv`_5GZMWyHJ8>@#9;qVlu4!MS%1b#l z9{X-Vf5O95XpQv{G%vn-!C*7c&&eOsNsejFA=?-W&2$gVqhLSWNxK33@VN9Umssbx zKs=TX))$EIdopPipTf6kdVIu17UsOk>=E>B$?=rM8{z6d*Uz~e&vZl<<|>~>%qG)J z52i$l6lww`*BO|Uvk$O&og0XiY4#|cF(&|Xof}40enXRZFNSmuov3fojCs!Kv zG%vD83|HLrYX%8D=XzFjt~~}QLzr{_ABc%Nn)Tb&ZD~hf4HgSwGE%0csr@ARK!Zgv zHHG#|8m&~?2~oki7!~wlPPbM8=Ds^5o-o+I?P5ta_e2VIk7RkJv<4WRR6IN?W7BGD zg4IT=lqj9FxjT0^SfX3AM=#n30NmXt0J0CWJF7~*TVs0$Nhg`3Z<(f$l;z>RN=f&XFif-@jc!JR47Y+Cfo&GF{SxR)s*hL;gB?(v1Z@ne zlGeSnA-F!v>7*QI$F2{HCk0|y|A304Z$5h)|5v@Fo9-sd2=tt{PmX6ZI31LKu3u5H z>&w$s(%KN@4wO4#c4TwH0A{{8WZ=-@Jpqz#b5Bzj1(#Bf#G){$pnn3yyDUzbu?V+B zwG++oaNWiFzVD{Sh;_Y3=V}QM5Ksvt0~ndY0}FurMAu^A1Ti&t^y=N>G*zVDF0FdL zc%>c8LAO?|s|brF09JZUm*MrSw6O_$f@5&k>q2`gm9eeBpzgqxRyj>2+og88R^c#v z?Lq*&tXx6JqveEL!RB?=2!LRkJ49JEjz`Q($E_bl)C+$YPkF6!zY;*RqIIt~B4Rc# z7z~h~ooHephOV@9OPQhU6O-f2c<;9$d46cWn2W~SLD-fXXK&(P6Lu!NL2uTggYh46 zIiAA^5cIy`t`=bIvL@!4i}$j@BBs6U7b&*0uP7bV-DiLT%?tckMy*(K#J-f(oIu72 z&5xBfbPTShv=Uer=wv*A5CM|1R8j9hfCMeN2btNy(iHUChb2$;Wl^vrFMz{FyFeo~ zWNeEyQbfR^7u#65Gnf-L^EFpoWl8ELn$jUk12h00g@|v>#n*s`Q;p43hONM9qiu*d zr8J88*F0!2W*(oYZzq-r=YhtyfX4QS?hmDYd{=TmDy|VC&~Z#au<{L;Q{%obL(8IS zc=PTN7c*+NYOI?f6}Oh3XZ@mrl!dkyU^+-|(0v-rwMNhwJM|8Nyi{DBK}?P_W0$b^ zNii|ZsC?uC8){i~{Ee3Yu*p>^&r!U6j*$BS;XgwEe8S~;#v-yWreo@{BFH8!m&Uw9~zOg(2IGy02nk)!%PkQ+GYw$oCDn6^b-fcY3@Y)^noHTK55VYsWdf*$}R2zj9)t^t0WbinR5 zdo0}boGz-lguM+TyTL59yt<(WSeRpCwD#P>t%#$!;SO9)fYD#0)ZELpoESP7u1qSW z`Uorx9%YD?RL2TvdZVJstcl67JGL2^?dK;F%EEAE3d*T}o*w@^h`HT4fm9TynX(ol zM~-JcQZ39?K7TORcm0e3g!hA6qZ2hvw57)fL_1lIur7q&R28ua1*L-eJ8W0NWyiGr zeE%hyHwW&54Q&iWyG7ARY6YpOcFCpH+)AKZyQUVNpk~%Us(jkEoywzezBK`plciP) znP1BoMZVbP_*lJOw8{uz#+-WXINT;cTNN-U86^cZDlh``0*#GNQcE}wbTK#{w-8Vh zJ%lka(A0E&+-E+E4!^ka7om&c_Kvk%qTzrS&@uzCL28Z#ydj((k19>;%+-A#+p?ESEr0~ez3Nv0LO>uP zH3d*q6sOZb295-%#lA{G%dWw?08nxFY!|&|3|v_&0%mIflu%K1zu-cmWdPqY5^y2L zMJpCF?)}bdAvYBTQqxI(f%K3BvsTX;yR)e9y9`C zz|24&gV(E`ADM~Cac1lSE)Qe>eWm!06Je5_Yo5e&gGcr@K??yE;-0vny-V*m>Wfm*Brlz(p_$>2zhT3@ zlq3gK6XP;-Gaw2`ThXEb{g?)s%z5%x#)F$PYNzN*=V8;nSTz#R%}l?1nSO?ydNE8E{W4z<1)C7C!qIY&6V!xUhR&O zdh=tYmU=~Cz20}LA-|TZlX9FByBL@!ChWB_ynXg^`zQ;<%)qG^EeXQElS$40k6Fn1 zTt&*Ny&*sI9M5;W*R9uGF-mUk*b@k;2#xf^QhF$B-7-X`@N}u`LwehBHIwOgH|rx} zKn}PwHFvgl8`lj5fr$+Rol3!jFBUw`p&vx5GI_oTdr~WrVEu}Kq59GGRfk;_{LW@t z{_NDPcNG0_sbB&)3h5zcWN7)`o|aBmQ8&YtO-}YT*WE$E6hKl@Mps|XOnEo8JwO_Y ztCwclOGzDtp@9JyVyVxFnk_Iya-pnCrh5sH!X2v}z)Y{L1v<2AqSx%U;}&ktOijZj z*~HfD%@|;LurP=8Sd>ymeW2prwKa*-A19VI*0Q_EOiYdkNBYg@i5uUYkuo}!O2S{z zT`|XF7UqBS=TesRn!`LDb#!||u>ju%F6cKO?nmS^O{t#0*mf8Wpm;}B$ zGz2$)48p2}L>xS*HpX59M6uS!m>!dls35p3B5Bd9PYeSfO(Eg@Y_`|SIxsVZkK@F# zm&dUk0)Px21$U+_nSrUhzJ_QYwn><00`g zd(sptG_1jv_5yQlJpi!M=xbaEykU?=y1kO;WtW+l9J?d^pm;xfCqe(f<+a=K&F`I< z(3SuI#^*|u=P&T>@%#PcA9T^+3bm|_dZxn|;VXZELX$@bPMcaM;-zb5`!3L|&Z!s5 zV{nIP1|)Nuuvhfh5FnsQxAbC4hY6l$eUxH@mBB}1L-fjqazLe$0$^9zZ$*KGx!$r? zMnMZ$6SO=4JHw8>n(l#M7Gq%XtrG^vfR0lz;#7d#s>e&2_{Y>{M7I$z;#6A$aN_kG zR*{6UpaKc+xzrlj8x;!MEw)rrT}Pum6AvT0mVPD!xRu&%f>{3}eFP7fo;_p=1}kVY zfHnktjRZI5Zl+sSoKh-jyY-tZ7v3CP^FQoM^T$eEdV%E7xf*5HDCat?MbwKr?-?!I ztHsmC<;bxkcDeQLdjHtRfl27EYMSV-GBBUmGPL6+e?gXi#N~KiW1h86Ux~2|UM>%2 zxb}z{8BCvZLJaHGiFC5=M()|uP@V*7q|N%60D&}~T(1I)XA3o;v!;-o(&0*?mcd4+ zo-aYQEo~v7qLiYfXkbTgj!$Bl^q_!XFdvsfm=V*#aN5a7rlwVZ!@$QYpiq_tV8m;x zDH!0<<9po(DJ=jXJD0I9cAirEHCbg+tAJ=RSM%IDde``#K5Hp|g?5=0ouKEmoHZ9H z-HMB`3wIROoKiwV$P<_bGVH0iMFCE|S)w$(am9^FQb;dUg7R>IxQS|VBggX;UgxjvxU9HWJY7af!LZVn zk{gBi%9f3=2z@_F*JjBeO-zJK4ED+*ObiP*RNSa&wn{oms}t{#mPsci)#i>XU9+`H z$tMp@6LDF1;^GUC(yItKC};xUVK$|e?EqT9%z*9b$H?ArxwQ&**&j}U3j4;WQC`q( zSU;m6rqR{}Xk`#=?Ak1{8@&_E6Nn@U?7-_7qOj_g`&COxwJBDlDydKraHXvYJnjXt zE(JaOTvJ8|l`t`P=%QboD_{}*3#X9g+Ebw&_*_{NljF=d&APmQDWx1aa(pGmqxoVI z2J|BGqIIFyBb;@?$&_+@0GmQ{E>iwHYRUAB4f4#;&Z>y4S_n~_5VjX4#8OMfnrOxG zDk}@jT#?ED9 z$8xW!qG;NuX)$e2Unmn2#JGPdjf45l|f97bK^<2 z8px3&M-Gehb7xmG$cqN%!iLgDl~xiNTXab%RXBn@wPF0P{GMG9!(2 z|5(|KN*N7fDvGEVfoKu<;$e zKCzN2sG?P&+$$Znk!DX>6O&_SY&-S7FKq{MHdA{MjU$!W@6B5sbHyf z>HK|Kn%yC|ZNU)zWTxdRC}#PP8s9DUiAf0};H|akBc#0r-`UZL{hVAhfK5yPVR?r} zoXp6Kymm=RC%E+%1kTDuDd6el(v5GUr$Qm6)IeCBBCAS&?qFx&9fL}xKBtp%JRr6i zm?!PnnOuJcm^qz`m9VP(Y-JeCI#eF&=5`E zMm2&1`;oFZRN?!7^Uw?!G59J~ip zgmKsE|FT)T$yhIFfe(Y6r8a(C%6_AQlc9@(;Yr-fgHzDct0zm4XWSkU@#5cJl`b8n ziM{d@iJa0)Y3XyP0#++?HiJ6u_8cTl zgSivT28(Hxarxib``cj4va>u8Tx)-vk9#w-vPvbXgoH@((G)a{h24PY9yf>%F!l^? z3^5ZJCT0Y`2*(5_hzaPh!Qn7CV!#m|!w*~bM1aNSO!gL#ju|f0@rf|3|*>r>K zwx9qBl|Ua^RhfD3J!kK|dOh!YpS9k7^4`q*m06it>#4YP&e>=0wLi|uy`J;D&-=c; zrexD#3!lrVgoJ9$W++V?TE*8()QZvmNdPVdO}-x9BZT|lg}I_@T<=peNTgdsjll+Z1iM=3hj^4EqUwpM zxG%g)udJ6)QBhIxTH!o(Von#n6(7!Cc10;5U=vwplT zmvEI54xrQV7@lE2qgKb-sPAQAI$L9+9#Vpj)PljW!3TqEBW6=chB3oQhuS|$8=zAC zk(BC1>Axgio~|4R&An7;u+NPeh>x_RHkF&y_%iKtxS6uF!qiBBs1Xt~NRASem4$?s zTE}iiz0iwR!b#v~yru8@i1VU&t9~D8xmm9@Daw|R@D}X+62w2 z8PtrcM%Z5GS4mTDih#m?M)b%q(WhxjiZw$*AT%HqAR1W5rcKME?w8&g7Mnu`@u4az zcoakLpoU(pGn4%*qmDnNF|Dsf$QskX)hNtkIk+94FN$wRF2$%(8uioQ@6M-^N5`oj zXT0t6HN$~Gff+=T8O0yN@DcuC?Cqqd607;9zaR$SrKm7#Xf)^&-Y2iq^r@|zZceU$ zE;6YrZMuMqhCLyidpa(EWs^QUEy7d7NfjsHcw=w%#8gyNJPe%FzwNA^7-S=*e1wqI z42Dq{1`=bfDi;hdZMP*f!y z>#ekgjMrK)t%jJ`wqT=kH%-V3?J`Z`;*FnXG+rC2K|^Auw;COZMJL|3A7-l$VaAvvn&=iBj6c~1ZP8m%4Q*TNpS3vwF?2F`!Kc!AlCPrP>S0r6Ud~>QJLveIEi&=*u7c~R>_xBVz$xwy=hB^5w`^L zgBHJY43%kRUfG2-{4gMicpsH~QMnh)uk*9M4poV%I1_4}n2L&uhk?rq zVGB8lGcbsWghr(00Me|3q+Fx2t`HRs6|s!~He{U`PW%h)Hoet00PMFUq-H9FXQ`dj zM?jT5nlE9OOi6#v#1^KU4f+H1xrG`xU zZy~ROTSXG`nQ&V?GM!MI;QAqwoIbeq=dYcQ3XFea?>s8@qVOGjFbC?oUTON5RM(qQ zRo}5@Csmw)a^ncI%%rNsR8&+vY+U5NlSn`iA~Osm241^Q2e-FQ>P2Tt|MzN)6)(%C zba2KpshgrbjPM9W=1A2GRNEDTvPpy~s6sNS;b37giMbvB4{Pu()bNdA zqodzBjMu11O(g#iWsc*?M@2@8-r+?4=uRRvdQL7mt4pnlFBKIP4;z=&b^zE6 zK^i@dqZ(bMBCk)iU#8L=@y>yg3so5HTq(Gv`dln>Lz8`BtnopF@stzoDvK`vVd5LSLf5*Za^n?DT>#W-bz>p z8Aeb?NOAy86iS6=n-nUNRPDZaYew~~Fq{Qd1>{L;w;UGIl{;>M91rKD-Uu^Y)Z;U&^d-33( zT2&HLaWZTJNL#bvq3j#K3|@%)d&x;`);$;h-W6|X*ImWy4B_W?zIQN9#_xlFXZ2@2 z$*@19Sa1{R(q~Vx<`_<9WEy6@hRO$Ra45@t`D5zyq;*?he8oCm>$Nb&c~oKpG69ot zr!`q*U&}&hga|>lP-O-x4tgR9Y0#>>(oacS46x$wa#_MnELJDkMoMImWfZ1T-v$+W zbBjAAMG_ur)HJLn;PJK^dyi78I0)9^wh)%2ycA_>%&%Js7!9JjVA(4Ex3B5eD|uYU z$E_NFm+^VhZ=3|74kGzMp}UPVfm}#Cg4NW!Z8yd18&#VaLTYgF-`uAjO8c5;j?UI- zKd{h~6T{L_NleAr@Gy+M;b#1MUl8)*pLa58g%7(#YTgt7Ioxm+uOoyHIr$Fw=k#^_ zez+In-(j4X-c^arlMdVZK^s1LZBal{lM3E-LTo^IhRq})r0u6ix4a%H5(o?YFsRlW z--{ecJup70Ge>Aiut>FSTt&qx@KC)mABy{k6P5p(`1eg-{+jpbud)xEfB!;`4)5{) z;l1Clct9Tt?*VKE~dl2p%OqEV`RYDBccYnlB0#M3k~52xb4ks%K5d8brl~Y>WOwYmzpnI1n)zl1 zmwPZl6%r${hKXcC?-CIb)9;;>p~PA*FzVuY(9~&UG%dT5Dvq>gdSCK&b7^`0ESycQ zK6Gg7m^va&(jTc6t)*4dZwV>k@IYCxhIF`DRtQT_=7%ef3)#Yfe5)a z3qwe)svUua;q*1onHjTH0&;X%O8>)$8nhT))0FL+wqymf^EB2)n5pZFlER9=y-FV} zShqPS9jfWiInXZEY&U>Vm84OJvQOy;^{SJCcCIPmEQlK0TP2m7VIwL~UAc6k!bsY< zD{9PEjJd6ru}iL))bKGCxKqxjAtbMaCLI{7{ib4dPW&=>jXqSNi?k@R!R~e(7sc&f z9aFsPYM0_{Vg%Cu2z^tPn2LMjwW&Q6&F#Y2J!I6mz6X33*b2I+a`ktiv2zmOshv)yPxIe;6N~GE!9s) zPmk?h)bDRK1wWQJZ-T)TWe<sJII!Wh6ZmV=oBAOL6}oAve_27fHtiMFnK$UFjEgcz;Nf(u?}b zdZoXzS1McTGK__AgI5LTE@GnLstgdKN@5-#NzvN zy(%$}6Koj!JR$Ambx9N8c8h{{X$%k;$u45Z$Cn~^lM)pWhe4Y(9^OgWMxB~?MafXp zFiDG$AeX{AAU*5VT!E2VFs)`LEz%>~2Q;e6umImySp~9`5Iuo`m*FmWl0jaEJNnwG zQWIQm`Y6?Z;la5UiAz*u`n_~6yrtc7bJD`AYO|7h=W?bJ*GZ}kI#lm#f9j;?q`EM@ z=q{~RifJU38boI)Sx7!5n!6#gusy#PCG1sqAlF8l6hl$Q9z^ z^trwLwdOq#n?>Au(gbUmczX#XLCrbTVt0*dHIvlw?Q}gZAH)`%LP@}=VJ3{XfpK@Q zb<=-{N4f`JjJlbjyj;^9cR>3tR3)b3SnR4WwxjF=-}g7@FJ~w(Fk^y{yr?08cloTE z_#0}*U6o(fLBmE89&(BJ$nS)kX$%QZ9Wwz+N;f7qdM4iKDv5c7ASw!dO8OaP1?{Blu2-*W1Pk7^bLAtr(guKCqv^3}w4$!lj@Ux1bX4;G&7{FL){`I^QZ__*dG zjx_4?Ua1U1N>E>VNwrQo%~pIfX-`S1X*8Fq)zWuu(sW)OUGq#Yo)`b7__$OL9!y}H(e=& zw$d;JYRq5~d?)-gkSOOjK?z;GkO&T**I(<84*U&$LbYr)|BU1_$MY9m{`@3O-+smz z>Rlb}mJ84?GX$j9U_VOljCEeZi~iCj1(C(}?K zSy7iM)-w5PrCb!XVG)JSwdZw(KMww1MN8yfN7Ss3sQF zfti~0eAJK@l_-FjM+^-q-^soghKsC1L!y)Z*fR;sm-7EsD#th_CkQI_LTj)$IAmHO zGu(IMj^aPkK9V0P&IeCY{BIp)uFaS}0BI9(rFZ`Ak*pjAG5JcZnA_W=#vm&Rrrvex z7g0$}#i{VHEB3lw55zI>H=Go|n=LzL8&OqeM$Xf$Q}L+4zvmQLNV^8#-Ouu+6P8yz z2B0QzK2MD324Z#%seh5ice*~f%~5=1?rTs+*qxdLMvBL&!IpiANLE-;m*x7UVPjlx zxtHE=wT}{O#?W6PWNFeucDhiqo0=37Qht?<#)A?lRu~4^5kF0%2Hub=_aYOi-B;!0 zFd2b!^hN&|8lvBYuQ#p`7G80y%5*LzP^UvlwO}T3zMy$PuS$*A@q&bipw(d8RU|W` z5S~`{4DAJ<i470uUH5&LgE+ct6J6Dv7B$7Q4NZ zYR{yK#}YTShLB67SIBC`V}U8o-k!Yz6z8c|C7Cr{kU$5B%;qR)gT_c?U>Ko~v?)pl z$jIJ05r{jtk{<=z;BTv)rLnH`KiVd;;xzQLSf%9{L)rsqof#N;dnHGr`e?YxF|(*k zYK>O{1A^m~w2Yc1#o9BH9Tn0xcWx&IM%YCpE+Yt4T-Nk1GY~Bqlg4mKeZeLqDfGwm z`pA^(!LdkPIzee3E#8)ev>5R6YANe2q~$;mLepu|wHFKUBq`Uemd#sB#*&vXl2eyV z8%fL*FNW0FLEC}rU15A+celq>srQpdsSQD^-;3T?5>s(9NL;K6(=<<(7hF6xbz*yR zfzZ>p`k8O4;_<*qbz;tnG0u$AA|NDxM!)c)Y)I$oQA114Ws|S?S9}WPh{B59CGA#S=RDl_`#q+`z1B+$pHqEZpt@d6*7-_2^Ca5h!hO`Hw zG=A)~xSiI4L0bf{k3su9k+2MUhF%Cpvf?5Ex>wDm)Awqg-S@ZPE5rK*sT$BVPo&3%6*rz>sp zzW7k`-f_ZM_bD&orAlHdj>WDAriPU&&VqDChVlWj z!uA1^su*}K>WMFY7WO(~IBBB>Tok%P9U0j^lvHg|Y1&ldg*}gQ$7-kzllodT#ac3X zuw0LoRf92ii(;eUuCP@lr%y@@)Q-b#)H=3}HePg^$_!$%))3O9o|vVWXV=We(v(OLRFqu(w<~05Vrc0w9bcoB=Rg;> zpIJv0Cz6n{cZ*>iNkO71$^X*0G{rA8@j0n4gOHY$V}L2CHA$rjvR3k{43Z|@3k~U` zauPJLdKrle)RjqJ2MsTQ%E0%s6)Mxq-%@-sV=`0A|Iv6eC1S5MEqjP3b){K5f1?!@ z$=!@6cRilnTUN=)i&!1h7#MjMdSN*ajWxx(;sji}v6ue5%4A+2Xz%e}RcT&-ILBvh z3CnKHsqVRKYU^zz1T|`{msUegL8u0eB%Osrl+V}2 zk?vR;lF(}caF^~*X$fKJR2M|LLj?8Z`+NU|XP%k4_uS7p z)8$~wBY?m`e$&aG?LUcIBSloD95yZ-Vuu z4-8NY+l^|}G1Uak8QAJyKSh~2xt%=8F1#9YI%azki3HR=swbMfP@MCVKl<)jbNx`c zm;+kv=8RJgQ9EW41i#$lC6b;nb?NlL zFxhE1_mXi!)Cl&G|4BroOSV&8`;+9CKFvWS*!1Js^l*AMQF_I9w$zUm22xFH;yAp1 zc0HW=6KSMbQMZzgQPszw*P+^fr7p_-%NWss7_qbZfwYyWT64SmOmu`4bPfp!J#Q1* zH=S%`L<#}fmth*+Zd^h*7r^99QQ<%9mA$OYvLM>{#c?kqL9j+k>_hO;`inm?Ts~WI z%vE-QKfiXXy{EK5H?EEh5v`UOtNbyoVy>t8bg*^Sn#;EcKTu57`F|R+BEM5$Z*5@Q zOqtupp+5P_(U;nu^jaDp$xd6A?-IQ2hq$kZbQSG=X&+7MhgYJyk)@9roWej^rNcUc zp}gy0vGt{#Y(&! zI4XNPh#gwk0ga>9du@m6E2WMib~e}`h7^)f>sXEO|6HBwL*Yf47g`my1J+jv9$$f` z1&m&uk#HR(QRIa~hO@TOxOp9#5;GjwM9#|xGeihSQ|Z-u(5Q+qx>9vqREf9|Ka~_@ zm~W)gC&qI>DVeB8WS9G`v!z?MIq zKXGMV*MRN$z42WZ{;F6U@LoBVw0^4Jyvz5^>C0Bvbww*LejXqbXOM8{%`r<0y zW`C)wLV{0`CeJl)Y$B4}ZQH;zk*U-3V?bH2AeAvqy*2HSzf>_OIgHM`#5I%RmGEx8 zjhjgsH-UC=GFw)+?q!7w9!klP#nK@dU$71S&y$y7RF4z0Dk*ZLp#nWJx^7?R&oNJc znH+=|+l67ZHmiIibX zX|WgnHG-4sFX7jio_4Vhxe3-rZ?Rq}$YhD%XkK1iocpo}Eu=~r?*#Go#f`NJpdp+3 z%cG&zJWlf6rL-W3<4A9T8h0~kn?)~OWRsdm$|C7>RdPB*P5P5#%*^T$OXrB-=q3igv5cu`MT+i)7s^Pro+|*A+?)f*9ZC(`jwp@H1&Zy z$om`=?fdUMKE*0Dc%vuZLc;JPR8sT$WYcKi!iN1w>Dxb*k$F$0ZYQSzU5YG~I=hJG z^Xr!~ta#3YJ|h+4NU2i9B5y0p4?*n?n}bB|F7 z&(1F!3GB`w0Dq9Wm}e_*Uc3VcgAprnwHD4NX4fCm zemN_5{Ca$k#~gsiI6BZ#*P71wTRL=vPxlL~Qal(_&MKOljJ`LdHH)MLVz0+|2r5HU@Is@*7qxDwow6itm+2nxbq?w^0FFuOd`o&U3J-N{Js9 zkD47Xvu18HyQpg%+#1yRyH0?ZXQQ2emC1(1&VS@1e1d{<%KEw}q5Vr-`7^9bE=$IF z&JC6w=0PUdK*VlgK33JSN29}@T%!kp%*+QRs`wb{`3x@>3q;~{Z8e=b69IJi<#nRn z(3-K$T~7!0%#rH@Ybes%o0&T!H%zy?eTCv!qqz(jCnhD%a5s`O{DXVxA+L{Dp!D!m ze6n*i;s4kMxAso2CzP>az46Y;XW5#14Kf2U zv;jR5d@%AG@7F*Y#CxgqTmfjUlM4%<<^eU9P10+2ah=V2)#}7x|BJJHVLIkk%(Lm{ zhNoftzlo|`r65-6nZvi2%VLvHKFvr-PVEh_Dk9^KzR6X7MavB zQEEmwl29{A6q>zwAbBwGJPsdO`LQifU8ijWgPU|EE?mU+GF1V@3o_Gim?`Qu(4mGw z3X{K=Dti|yy0<%yn|*HY?Bd{ct-w!Q*sLg3I%!AJh|v0}5DByyYuf5KxeLZ8P>?Oi zNPt@c@f_j+^X4QyZraroc z@3~O#M=N`Vp!lQzW^R1W3n~e!7b8y?i`Ds~vOdd|{ZPRG4i-N#hhmj&ql_$5lZ=b) zx&X^#d#OfQ4y`q9w`K|)I*saVVi^dfY-A0A-_IqP6)a4r+btJnMkhZC<1+FUl*sPC zOgesUwbq}8m*zIA{A@9A&rH)}&kL+7BX^QvCW0{kEoI*8+h!;W??K5W+}ieYmPPuH z4$%bf@@eOhh^OL%m|Yh^mKCME&8zT93NPT~U=xADFD13+oaA#YazA>=FueJV3H3C79tVwTY^0dCPxTc$TU zCv*t5qgjvAR>=RMLqt=jemm$FF#EoA@*OKJ*aO|1gY{_0j>QIe5}`BHZ6LN-!C@|3 z$jbWbrN*pRQv+q7n6u)S#MF{Hs!6Xn`=Pl6LQVv8MV{*W;LXkCOjOp#O`30F^5kPS z8X%}L%S*VqjZT~wBt`XO^~AcM?w_2_jG>RobLT6e1hJ~4ALF~VjJLlPdv>t=8D#IN zwq(!`e}!){4ppx4FV}tEa52faF!u8ZsMV-JEr>h&GykjKBUXmJc7r8iF?n#Dd~=Lc zdu4>xc(kI3uH$bZ@9f#mFCEXGN3Je9!9zQ9P=EH2H4}3trWm$!!$sVnsX6Bob|_s7 z(lH`wIf?AU58V|f18omN=*vqTZFxhRdX!Jh&grY4+Tmr<&->{xI8-i*u?JM2w)0}h zD_{l-{VVL73DLKggP7_G#QEBhT{*NWb>fD5QldB&+B>i$x-?mt&C`nR)t-tOpAJG^ zZR7B{WQ|1A`*=XHz}FXB+Oq4F_@0Wm|bZ4>UcG@lNr2-8^j*nlq# z{tmOoU1nJ5*IZM%fCXDS_GPm`KfmeRZ7>?!A$yaod2LrIW>05RQ;|KCK?*5w9u0ES ztLmWsVu6Unbo`8cFsJ^T`O-7fsNn{rEw34cP&ZO}aFjHz`7NTR6sN?2l%mbON}&xl zbm56Gbt9v^8?9W4N5J85NgRJhDS|w*k{8{2g5}|1fr4<@(L&}|E$z7d3ZVP7XE4e8 zRN{clH4g-Rp2Wm!NEhz&-{}tc;K-^s+}h8C#Fm* zS`1*?w+V+fy36@uq%2vE{^3X|r_P}c5&wbFjNzwdiWyagoI03nh2VVY4888x@~GvQ zXWDCxVlX?^aOF>0h9AU|J1L3a3%`3OUwSd=?}$?(w^GJEB$a)5ss#*Ck`gK_0}cmc zUq-YCgm}0bG(bM`G`KMAz3tb)gk=R^TzdY@XV5SQzr1Ca>z9|w#grd3twM}=r|%j4$HWrL}Way_xxX+|EV@eUX$l8zzL7N>qy-ysJzi0V_c#(y}NmS8{dDxpv_h*qaiX~&iy=W zBq2}|7Ve9Ir(W>4cO_q>f;9hs@(XBeyL#e3kWDS~fixrh3pQd2@GqK%-bhi*mVP`R zs=iz46i!+&qmE-xYfUB}aL~YRql~}!!co45nT#&=1?2Ih%}WfDueaGQ6VHZeqwcC) zX%)l|!1dP-1QkHRW7?d>l!n$S2OgNZ&JC3Q^cUrFqUc?n{K(4B^P|HS?^4eXz z<%9lro{PZZ_{8HtYdt|X66-%s&8|qV>}n#cJy9hy6$fDo=LF zFYL{JARBXuwAz|Voh_!RS=C>a^B$oe1Sw`FrW@x|rLiwQfq7@Z#x3t`Kc0*gw9ZIX z+WjX_vLv&LJi%mexIfJ|64wVQnc@=$#RdY=B>5n=wnKgwB|KaOVEA&vLBm+UHC19K zf{8OTuS#>w+?vO7IrFJ$>wRfKX#aP0%eXs&^sUlH8>Xk0#1E~c9u54lfTmGjl;DMM zVyl)hG7de%GuF856+zBAE}v-BZ=izwQ8hAZ z%xp@pY>rS3>q&_;&WBvrKNV|1R{7}})tUe5!D>oMqr-BJ%bf`=(QQVq)`;EVnkIwz zO@}sU=3h4m(;&ZQ^{wMnvGP%qdj5x#CC&&q?k7`~21Vm%5r?U)%N2rvxG%w`5-=?!*Nq9);cJkQT#^0`<%X+ts_iEr0iqJX@+wbGzg((m%lX}e z9rFz9hJ?2^Z9Hs;`~s6j`&n0RGOFEx>{9;Do{L`l#;B@s$=!>01%R&};pD-e4u>}; z4luGKzQpm&(>&2;Lf?gW1T=jFHjC3MUZu;XeD3#r;{S9GC`hUd8Vip=?b9fO}kJqHL$9X3(zPafa% zHM>-`GNEy2N~4&vRD|uF?TO5lioPSM4`cq$OmuEnh!eux^%H+n_z^n!sR_y)V!K(8 z6PGH-9(HCUn8Ybd&1d4Y5ej{+{c;EQkl?kQ_-|bO07;HYTLibIMVhJ@{bc()o}k2) zB(TyidM8N)DpOKZe{jvW)CkkWuei}~bQsgsnMCAI-q$GYe>@Ck7A%Jg($_+`rvayL zbu1-fe~i93NNExpBUQH+#N`#kIIL&$u?f3m)3VV=Fta@h*poH(W@lG0Ona~P{Q2^N^w{1k@r z0-)|!IX-Bl?LId7dkT&_p`p&Bfo{;aR&Lj`?bwyH!r(x1J%l)dPjLKYkXOkf6-D`5 zmz+PT6BA{&&tHt36dQ4fVV96l2ZNpZvWs6JXBzkq2DWBo z{I+tJ_QCW~K-$7*ul9*Mqqzm@mUf5;SDBxG+xbNoRJWecjv@Ki{q;;U!avv0ooh|T&T#K)3}RQ`*0N~$GsF~ z1O{@a=PI$Fy)7KP+wMJ6fPlIsl+r0UuWTTyW17VE@29}P&=8fgPiG9T{$cU1FvGm~ z|Mp1%35jgY4s|sUh1AEU3^awy$iH)22S`^&7QWVo^jb5WJiG@5xMrTJ8OPW#L*V4; zEm=&E9qIUCGl#7}#;D70dU4gCEjQ82@?3!5OYP+Jl_zm8znnR-Z>A!18=N%DC8~MQ zMIv0j0wzaYfEtG;LnP@=39SOtZ+`g&tNbIgk|s@lds9~1T9vRO{tR&@7f7;WqJ;Gs zuQ_%154YZ)jeL>BG5705Z^zDZlasHdh-=ptbv7)R^0Lc9x7Fzj)r~MjbOw(Gnj4c) zecU6RfS+%gxwuL_3KZIoBYC%rg6f^iJUeyP%$1()P^$3btI*Lk1; zuitgTCeU<7X62inajOp(6VKYWfX z7XOnn?mh74bxAfW#g#W3Lr1je{Z4=K$;CD=(B`&s5*5AScz8vro-&4Imw-l!-+;C6 z{(X6UqzSYu^bglz=}06Bm4yeEPF5xoyjOJA2gf$5*XUs={W%alj^r71I%z%7;lg7C zsP(s<$c_F%y9_^@vxyhZ8k7$Ewwp!wt=OdpY*EZ-3P^he3VWr=3f%N$60x7m9eD;x zOY^wVs*BllqTI5cJ{dhrU&mrQQx!Q9h$@wwH_*8ng-VM|4iX^4#h)ed@DEjTBp3O< zhmJAExj>lPhuKE#JYh75bx@jA?B~D3 zs8Zj;apJ)Z#D6euS}f=ngNJk(FoW}I=)Jsh8Y9kjd3P6;3+jS*IJryRxxJx7a(n=33|JwLFRrE$d!S2hhs?l%t4M;W;EBBxb2zf9X%}!pGsH!xQBVphCNggRp zqU>38M-C10?qXY3tFy3Y0PsGtMMg$_$ko6jT4T(9!W-k**c|et`IDBTFTgr-&e{3t z>N?mutaaE_4l);`e1L#77QcjRkT_j@_1JcN3#wdEdAigG8R!{SQhMwh%!nx^z+0?V z7_nF^nfiKwljDXMY_2Y?@Jp3e6@)z4VU4sD*W}CtE&1dqNQSrKtX2oHQ#M{1-wtSl z)A83TXBW7jIA5ly!XwULl3Qh+4jdg{Yqyp2xl-pg~l92wucIu7ssIPId9I zgP}uN{ocpyxW_@XO?wTQ*wHqdSJQ)PNKS5Ek95_s%q)SYSmHV zF=OVgB#{JSeKApq$g}O4^(d`J8HLm2ogby9q%}$kD-IFAd4!g@Q@GO}_CcD^rliBz zjbUo&kW4S9^N;cP&`LwxlzXh`UDde$Om`62I`F`05%gj!Dm4QdNHYn)STi10)euRl z|JAn3%MW8nq*j?Rwx3pL&6R;Ht!H)j{^;`&+bC&gLW)jd9p2nBFO*XIH3#Lx#6@Vt zoDdrsF?ukf0Bf`45POU-7pc1GFs{q_k=xUr8xXLtFUHFK9uFCa%8{2Js%VAQguMT+ z5hQt@Q|3*^&antOx_P$j*M*ZtMp?z__(Lr$~)fTF$X;jZeym9&b)- zTdW`K@XK;GjsPb*S|`n%hMp{wF`?=6&uJzjPZEX1Hhyk_Hco`t`gt}8--F&mcVEziabC|vBmA#J?;Ty8UQs4HNgv&;+^u2rz=`WG3$EbUzxPZ$QG%o6!y>f#7}KrOb%K<>ccQ2T;>Dz*OSW; zX#xxVDOc5BmZ5MVCRuLLrAj8`cM81uZ}q|RdL8$D@K3#0`BkM(1I%hDgWp!Zqt<=; z=5a9Xwzyh8y(s&vTvI*D=*s(sYLy>{JWsFtIELSqH4809*r-&awAR9!{K^idAwDoj zlj>AuKt-UNdm&E2HST5PPVale-z?wBm=U~SaA7(X+O8cRxruTJ0djeouIM>8iMz&Yy;CF?_7x+kB#EpG9}B_MDwrb5W7B9-%j9I!^SRj#}X zy1#ho02zms0}!%Cmywmz$1-^LbBssgdu@X;Y0snO56rC}4Q{aBoeUF^$3>(FFO70+ zC)~a>JS2}eDoG-LuzY?JA8jb7Pg)h0N7YdFS#HK&j2!4V0yp+(=z95Gx+5I1%+@A_z2J*p+^blJ$Yt$S<;_b&;R#%qL)q9@~G71{4YqH zUtQ#(d&XE(KFEGQaYF%lJzROWXqN`fw^OwAomg0lC)+qR+^|$Co6n1$=o9R=Jza4X=f)8p*9gg*ULlwKNMLhC z_w|R@GYggUGx_P~S|$?--c7faYR!7@IAp#o1L*Z)6fUs;H&=KnsV@h+4NJVAe`MyT z)RZnAj12JyqrdBTkbC^f^C9Nz)4X;kI^$H{j$!;hQ=KAioC2HqxkkQFy%~Kveuk$h z(wsFRcp z)wacM&1(S{DA{#sQmUX19^VMGpX-0&1W!sTTu4dl{J1cV zbb}yKAoqMxjTOozyoakq6%B`nA1n_}|6>ii9M)}UgT92l{ByZ2PsA=`=^sMs^Fz|# z-=co}>;NbEUvc=vgw)cn2g6UcD(eqO2j(ys&_XBZS!B8L0oxXicj$i5z2&fnqyd}doIoftO05+h zx#aao#G1lrEC-k%H2?B%`NEMc9r-byKoRAW5H^-M(_;gavqBRIl#Z201+)M>!O&5M2em&*3Xuhq9|IMrG=&H@% zu29hxe(3oe<;T^lH4IL9J-gv6=gc6m#OClsSv-=)O>$J4#6YWu*jNQ1S(TBwg^Tf) z4HazyPHmL(?klL~Mf=h$V>MAyN6=h)8?`o*ElUI8Wwq_C9z zu(H_lQ#cHz&L{>BVR+MV0#D0wEhCrEb=%;?YmL!@PY{Rv5EFJ8m56B9!YbPsRKbCI zaNG>_sQC_H63r;pqxB2f+h8pn_h5fU9)g8ZT3<&ReMhjNLropd9JF*n^z=H6BP-@h z)TTYS?Wr$I>es-_imnnJ?e?Ig9Jerd4-$K~Ijn2r+D#r3U^hr`#~{c7?WpMeMgP>iurXX0jE+)EcA!fhVTZB!I-jo|mgM{vba;d&tYI0|B&20Q z&TI~J1{jwkHkhvV^MKyO!sBD?D?1GTu4GpF5t=Y`xv5Y)1B5I9NPwG2@#vC{MA8oP z12J*I72xsGzbSQq_^fE-(rpV9TYgJ+Yv%8NuoA`*M%gk6?k94w7PV1<$o&r&!Si2| zI%erL@t5Pq2L=QfD4DgYBiuKj6yUlGPgQb^q0qChMeNq`(!1^wH@A)_U{@ zY3*Q7kC*9{4UVrw^xl@Yg>Dia`X8{>F2)*3=61yQ8x7iL>(JU>ZSg1%Ydr7SLku(53X}%>7)=_trHTRGD zFLW9J5v#-@h%y_dXf_?cPNyrEEJeHQ>Ou&gu230s&MnaraD+;XqT|pftQ^@M^*Eh4 z0N$_K!_Z!{zx% ztIidc^4Dd`cW#GCY+XupvW`}BTs|`R%tk>u3Z8%)2NYXhB7U^_Xem-W`TQ2Hvl(~C zni}nA3R0&Res$*1W(Lz0Tq20jWs+aphY{;EipCRepXCE&zX#U+Hjh-}N_$3j2x6;B zuR8JRTkbI(W5l#xvwK&|#UQ#9sQ*PQ9>);#^!dWxEYkNS_|1vmPGt|MlY+*JGKFB)m}HNQZONK{sJ!AA`@X26XsLdpsu+~R zjCW?WD7`?@*ZAw<15~X)$b(w}tWJn0##}Apc4~Sx*5+^aSg=E*dE+pS%jn{BInEmR zT>X`SAw7Hb*Qnih)(_9rd#Yv|m(e|$u$j2hmlhpj_7Pk{6I|=n+BukTZT1m|U)YBv z!}1x?_QZPf2m4|dlWK%OE-+EIx=C~4x1g}_7A;LeCQHVKJTO>J=tGQ8v|_r{%}Ofd zUQ5A_&`y5dG77RBL+i4aq57`X?8!1c9>K$G@fNgj?XG$T)}MT>Yw+~OI5&TYAo@-< z;A+HD0`vO&_5CoM0QspMBX2PDy8qv)rQ_K8PQsS+ z_`+*`Di=*Tf+j5dtEWQs@5TPEBusXsEu&8W>29qC4~z@tzEZ4&WHD0;-dIGM*7~61 zqzyW)+Mn9T*hgo;A*IX}aL#J0&{)21q#oTN5ySjH+@pDCUd4~RViv$iB$jG)QB*qq zEoc5%x}_+Oa9-vr=2o)Azu6pPp>1onHI(0ZS8PZn@0p!d1t#x)+rdNg*`|)%8yv{( z53dJ?gSHtmn79)rGpSYhtBXjb32&inG!23~p?(-|L>Xa8$ z!Ms5P>Uqm3KNlALuIQ*ZSIb0-;1X@U`l14VzFanOF%*xREz{Ot_>LGL2gbx!I)<&j*t`E_UX421y3C{bus(7= zpG8v`4_4}Y3Bmy4`LrGe$zMoNq-?w-qH92)M_eCfY5XqyX7nkf0(&J(+zWQ5)UDlv z%uJhMe!W%oa->X~#fGS#Pbf#AR5jYzpjSYaB5G8VeGmL)%C{%u2@+%}&(1zJ@(6Z0 z>?M51%vixJl=SKi;AORtD_8OJ>bDSce}?=IW>1A|(`uHhAH95k_PGR@Zr2R{A(xZ* zukz#e=o`dlF9x%SLn!v04)lemm3l>!j& z40p*mQ&NNWi&|Uj0s&;cuqx&pOOEK^?kZD&@PoKlNSvkt^G73zbti|iHaj-T?!1O6 zLRe`T%Lgh2HlU)ZRIvvSKLN*6#!J{r7EkZ>kOg04UL+~ErByUVOO2wXF&t1aKAhZq z{V$b5i4y*l_V7F*gXgwC`#X$~OPDkAe~XG)e$Nq7jvhlN#0kj7)pm4@y=WTU606zF zW6C!5F7UM{8l=Lg9*l(MOR+eM+UlO^EasZHW*}>JLcX*cY-QQqmX`xKcO(Q0sWDtk zU(kXk6pUNh>Gu&?+J?FM$4JF544|L{tNIs7V5HL?*>=pKLddiNC6WHbmVqEXXhw*V zy+UGd^G4_8`%Ka>8em^`W^o~9b~1Cp7-Y^xH8SWb3tN9xk;zrzSkM(d zEb4^tSjhQVachK=!a7rd9s;gOliL?nZqMw~UA!GX8dx67M;4B3oY;DktG!369gk_-_OJ9_sI@ zBB4ZS`!DHXcEIun`XV*td!G!4Rk$y@(6&{xG6w(&m$z+nF3EE7(J{BDBK-vR4?$or z*K=3iAUrLeBkqxz-tWFyWMns$kjaQ4%^kSD(vX@BBEFZ>lg zm@!5};%){;;f#Ujy|g8P;ECSk#n9& zOFa`+k*ZQXukq_*SP%(FCe3OTA}RVOPwB9b*Q^S$2*mviS99hZRO1D(YkP9+xLs85 z{jyTDm5j~P;0w7k3@+9>sjD{7Fd{IJPZ%Q^{Al%!xD26tBwayM0XqN<51^ zk7PajZC4&nj;FG6TOTkbLhCJzIDN9S^S&3Z77*J-XK8W9_b}p;5Br2vuaswv8&qiV zA3>AO`T$OEuT>{mS?|m1c$ZEQ?Ka`+ydNEP2%qJ9X#QJj*0%N!zJxzuqUW`*UFPAZ zB{Fi%H3`?j5FS>&ZdYTE;EJhoE~37-Ukn;1rFd<&u))gs9w?hX_AAm*^h?(hSfMet zNs`fTudd6QO4%TzXc@2#Gg!$+*L>dlrhj$I2t)+@yj%G$J=k3QJJ6T}_RqLdu!v&V zE4@+_bh52cbJgx4a#FP%W#Z8%xUI^MhGtC`moMUY5R${;2}-DP8=)TN$NglFKW|wu zqxG9p7&h`-5X~1$AuXwEZ^RkLemTg}SOSkQv_rcisyfzmVRA-Fon#%y0qODRO7l7A z+RY6&t^n+W#*jfu=IulRc@{4rpgyPP8793sN z=(hGJ+W5BK(SRKJUDcOKITGLV>)o(2X@*IG^|V=y6sxsT7G)q5Zo!uc0kjFzz^mVmu?ec zprjW$6upA4oNio~8U=J4x`z9F|QL;SZkQ7Y9M{{ai+ zB!8K31U%2uW*26pX5Z`P^X^o5*WK2^jIZpBQYnsT!0C`y#lDNYW*#G%YFda z4UN&S*((f)(ntPkA{mJcP|2+I7|iBss&VeOC}EjHZq0uH2I};;(9cKqr`#BA4~|_) zg+L!DC5Nj-l+Kk6`MZ?5v&?Be*#3*-excj?N^v*AVj9HS>#8_kso2Q9_-OA`?PDV5 zuy~egCG$@&gQ}-P3yn8z{Z0m$U3?=;L5Cn)h|+I2QLaYX zPus?lb4;jl;w{&;GS3Azxl-!oHem%E6b{tWDo-#4UiII;(os@c$M)t97L62{f2)1L zVV%x9(E54&4uQQ!Zy@Hb8bB$%CVTbuNJp7Qr{8r9HMROZ~BvM`M+53Cq0uDOtQo--1-Xf@X62Tq!B@mXkKPXE&;Cj~^hb z#B6)^;5?vagKjKLkJ6StzT9$&a@laGOqYyoASU12D*^3Fq8eWwtGrT0NiRwNXc#Sb zvvQKH$}2yDXdbUB-%aQPb+mK7cUEQ+#Xn-EvF$L%SYC&vz+w$NvYAdu+A(8kSxGIG zwy>l6-H|01bCa5V;S?sXP>mB`S&ea1$A3SO#pnL|C%Vx;RdVuwK`M^J{s%>wBFUxg zp`E5a4+vrF!mN^81C;4=ylKZ1{S$?~sZr|Fp3x+k%p-O3qZ?T=TFAHewnft&$i)Eg z*mfnIb40$DQO6Ihw~{)ov(!;h+zWNurlA^6lWJhs&Lq!7MFlk{GzYsn-(oLrbOHBwmJb#JwZ3wd0F8mW*iXT3a_g(b5hwK=Bf)Hj1|H%7HRJ=gK8uhD6 z$0282k%YLVR|KIm+_V+)PS(pQnD{@JVkX?jiid}Xf#dj3v?Jl!TRZLeATFMuz5*z* z0#tv>24dO{k_A8P<7g>Dl zcl`;B#PfeoId;D{IqbT&EgwVhO9m92Pr)}Z*{KL8Q@?3VT6VXnor5VxURo{Gn=#{O zYt0wDye}^ma_K%XwG~KiovQ`8&x+Ndv$7H;I_G zj$89Zb&GpuiZ?c$b$efrs`ZZ@{OmO;bZPSfF`jzP^vu%$E9qzuU|s)jB@aU=uJXr@Q*RPxY{GFVnTuHufTVNM_00qrfD>q}PLTax!%> ztHTKw&$1$_T973?5;jpfgSAMVO6x$MssKT@rs5O4bXhH;CBH7`cZ~9khJ}jSps1r1 z@GxS>8z=K5*L0EgG!C$kv8=?BdciV!{U0tlfx?A&C!LUU;v-dhG(E6Z2fIX;=!eXC zicMB|=Ap?oW%Y}Dp*EAly+n4!r_e3Pm*wm-z*2S0*uBEGabIz#K*e7B(anFbk>WA9 zBWv!zqt$uS?D*7KaqMo}2Yv&1tN1ss(_+9&9by_CF~o1#|GwLA>t)~c-5-;3D=*!t z-|uFlezRosMcbE7mue#67qmQlG2joObsDS9_)9Sk#Tsfm&iV?*8NZJc#uXireBeu~Ssm-Ns-tQ&Jk=5MMPzLjg z5hYyRBU>M+^_JEr3&OsO09h4-dOWAi#bJp2DA?PdH`=wPnAifk!> zm$$A^BL-jCN-HYV-@1CZ9OMJ-H(hey_IV?Gg?kr2%t}Qmp&kSPFYXRGxP^Jy$=Y=U z^}?e-@1WHritD6ssU}kj3_kT*l=^|%(X))Qk>5fUwpeMQLa%`WZ{1rK?d>@J9_P&; zDY+&3MY$WID^&>042UFOoTb7^w`*B3!F`g~pWWX8lsyGjJ~|Y1+2PRKJoa9Ele9}5 zF0*!6qwtX_c?v?@ByVUdlh{;C`P*IdbThoL?C`n?I8!43@EBt2y~WY?!hU

    %t;^ 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
        VO*(} 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
        7QCu`6sZP>o#mYw*Z`(o}x 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 z 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
        ifi$QRVB&cKz(3up>mS0 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
        c7rm{z8XWn&;K%|jkMO>v*-EKM5Jhoi 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 >PsNZ;qKi#60INPp4(6o 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

        y= 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 M#Ib?H#;t+>0%2USwEzGB diff --git a/apps/web/src/assets/images/walletIllustration.png b/apps/web/src/assets/images/walletIllustration.png new file mode 100644 index 0000000000000000000000000000000000000000..d3cbab404230b2dbfebf1e5f039673efdfed9c29 GIT binary patch literal 199070 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

        1-kp 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 + {uniswapUsername ?? ENSName ?? shortenAddress(address)} + {uniswapUsername && } + + ) + + if (!enableCopyAddress) { + return AddressDisplay + } + + return ( + + {AddressDisplay} + + ) +} diff --git a/apps/web/src/components/AccountDrawer/ActionTile.tsx b/apps/web/src/components/AccountDrawer/ActionTile.tsx index 1d8ca7c9269..5df06d3915f 100644 --- a/apps/web/src/components/AccountDrawer/ActionTile.tsx +++ b/apps/web/src/components/AccountDrawer/ActionTile.tsx @@ -2,10 +2,10 @@ import { ButtonEmphasis, ButtonSize, LoadingButtonSpinner, ThemeButton } from 'c import Column from 'components/Column' import Row from 'components/Row' import Tooltip from 'components/Tooltip' +import styled from 'lib/styled-components' import { ReactNode, useReducer } from 'react' import { Info } from 'react-feather' import { Text } from 'rebass' -import styled from 'styled-components' import { ExternalLink } from 'theme/components' import { ThemedText } from 'theme/components/text' import { uniswapUrls } from 'uniswap/src/constants/urls' diff --git a/apps/web/src/components/AccountDrawer/AuthenticatedHeader.tsx b/apps/web/src/components/AccountDrawer/AuthenticatedHeader.tsx index 44f432efafa..1210e9e0875 100644 --- a/apps/web/src/components/AccountDrawer/AuthenticatedHeader.tsx +++ b/apps/web/src/components/AccountDrawer/AuthenticatedHeader.tsx @@ -3,6 +3,8 @@ import { CurrencyAmount, Token } from '@uniswap/sdk-core' import { ActionTile } from 'components/AccountDrawer/ActionTile' import IconButton, { IconHoverText, IconWithConfirmTextButton } from 'components/AccountDrawer/IconButton' import MiniPortfolio from 'components/AccountDrawer/MiniPortfolio' +import { EmptyWallet } from 'components/AccountDrawer/MiniPortfolio/EmptyWallet' +import { ExtensionDeeplinks } from 'components/AccountDrawer/MiniPortfolio/ExtensionDeeplinks' import { portfolioFadeInAnimation } from 'components/AccountDrawer/MiniPortfolio/PortfolioRow' import { useAccountDrawer } from 'components/AccountDrawer/MiniPortfolio/hooks' import { Status } from 'components/AccountDrawer/Status' @@ -18,7 +20,9 @@ import { LoadingBubble } from 'components/Tokens/loading' import { useTokenBalancesQuery } from 'graphql/data/apollo/TokenBalancesProvider' import { useDisableNFTRoutes } from 'hooks/useDisableNFTRoutes' import useENSName from 'hooks/useENSName' +import { useIsUniExtensionAvailable } from 'hooks/useUniswapWalletOptions' import { Trans, t } from 'i18n' +import styled from 'lib/styled-components' import { useProfilePageState, useSellAsset, useWalletCollections } from 'nft/hooks' import { ProfilePageStateType } from 'nft/types' import { useCallback, useState } from 'react' @@ -26,8 +30,10 @@ import { useNavigate } from 'react-router-dom' import { useCloseModal, useFiatOnrampAvailability, useOpenModal, useToggleModal } from 'state/application/hooks' import { ApplicationModal } from 'state/application/reducer' import { useUserHasAvailableClaim, useUserUnclaimedAmount } from 'state/claim/hooks' -import styled from 'styled-components' import { ThemedText } from 'theme/components' +import { ArrowDownCircleFilled } from 'ui/src/components/icons' +import { FeatureFlags } from 'uniswap/src/features/gating/flags' +import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import Trace from 'uniswap/src/features/telemetry/Trace' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { useUnitagByAddress } from 'uniswap/src/features/unitags/hooks' @@ -35,8 +41,8 @@ import { isPathBlocked } from 'utils/blockedPaths' import { NumberType, useFormatter } from 'utils/formatNumbers' import { useDisconnect } from 'wagmi' -const AuthenticatedHeaderWrapper = styled.div` - padding: 20px 16px; +const AuthenticatedHeaderWrapper = styled.div<{ isUniExtensionAvailable?: boolean }>` + padding: ${({ isUniExtensionAvailable }) => (isUniExtensionAvailable ? 16 : 20)}px 16px; display: flex; flex-direction: column; flex: 1; @@ -98,12 +104,15 @@ export default function AuthenticatedHeader({ account, openSettings }: { account const { ENSName } = useENSName(account) const navigate = useNavigate() const closeModal = useCloseModal() + const openReceiveModal = useOpenModal(ApplicationModal.RECEIVE_CRYPTO) const setSellPageState = useProfilePageState((state) => state.setProfilePageState) const resetSellAssets = useSellAsset((state) => state.reset) const clearCollectionFilters = useWalletCollections((state) => state.clearCollectionFilters) const shouldShowBuyFiatButton = !isPathBlocked('/buy') const { formatNumber, formatDelta } = useFormatter() + const isUniExtensionAvailable = useIsUniExtensionAvailable() + const forAggregatorEnabled = useFeatureFlag(FeatureFlags.ForAggregatorWeb) const shouldDisableNFTRoutes = useDisableNFTRoutes() const unclaimedAmount: CurrencyAmount | undefined = useUserUnclaimedAmount(account) @@ -137,19 +146,33 @@ export default function AuthenticatedHeader({ account, openSettings }: { account } = useFiatOnrampAvailability(shouldCheck, openFoRModalWithAnalytics) const handleBuyCryptoClick = useCallback(() => { + if (forAggregatorEnabled) { + accountDrawer.close() + navigate(`/buy`, { replace: true }) + return + } + if (!fiatOnrampAvailabilityChecked) { setShouldCheck(true) } else if (fiatOnrampAvailable) { openFoRModalWithAnalytics() } - }, [fiatOnrampAvailabilityChecked, fiatOnrampAvailable, openFoRModalWithAnalytics]) + }, [ + accountDrawer, + fiatOnrampAvailabilityChecked, + fiatOnrampAvailable, + forAggregatorEnabled, + navigate, + openFoRModalWithAnalytics, + ]) const disableBuyCryptoButton = Boolean( - error || (!fiatOnrampAvailable && fiatOnrampAvailabilityChecked) || fiatOnrampAvailabilityLoading + error || (!fiatOnrampAvailable && fiatOnrampAvailabilityChecked) || fiatOnrampAvailabilityLoading, ) const { data: portfolioBalances } = useTokenBalancesQuery({ cacheOnly: !accountDrawer.isOpen }) const portfolio = portfolioBalances?.portfolios?.[0] const totalBalance = portfolio?.tokensTotalDenominatedValue?.value + const isEmptyWallet = totalBalance === 0 && forAggregatorEnabled const absoluteChange = portfolio?.tokensTotalDenominatedValueChange?.absolute?.value const percentChange = portfolio?.tokensTotalDenominatedValueChange?.percentage?.value const [showDisconnectConfirm, setShowDisconnectConfirm] = useState(false) @@ -158,7 +181,7 @@ export default function AuthenticatedHeader({ account, openSettings }: { account const amount = unclaimedAmount?.toFixed(0, { groupSeparator: ',' } ?? '-') return ( - + @@ -209,34 +232,52 @@ export default function AuthenticatedHeader({ account, openSettings }: { account )} - - {shouldShowBuyFiatButton && ( - } - name={t('common.buy.label')} - onClick={handleBuyCryptoClick} - disabled={disableBuyCryptoButton} - loading={fiatOnrampAvailabilityLoading} - error={Boolean(!fiatOnrampAvailable && fiatOnrampAvailabilityChecked)} - errorMessage={t('common.restricted.region')} - errorTooltip={t('moonpay.restricted.region')} - /> - )} - {!shouldDisableNFTRoutes && ( - } - name={t('nft.view')} - onClick={navigateToProfile} - /> - )} - - - {isUnclaimed && ( - - - + {isUniExtensionAvailable ? ( + + ) : ( + <> + + {shouldShowBuyFiatButton && ( + } + name={t('common.buy.label')} + onClick={handleBuyCryptoClick} + disabled={disableBuyCryptoButton} + loading={fiatOnrampAvailabilityLoading} + error={Boolean(!fiatOnrampAvailable && fiatOnrampAvailabilityChecked)} + errorMessage={t('common.restricted.region')} + errorTooltip={t('moonpay.restricted.region')} + /> + )} + {!shouldDisableNFTRoutes && !forAggregatorEnabled && ( + } + name={t('nft.view')} + onClick={navigateToProfile} + /> + )} + {forAggregatorEnabled && ( + } + name={t('common.receive')} + onClick={openReceiveModal} + /> + )} + + {isEmptyWallet ? ( + + ) : ( + + )} + {isUnclaimed && ( + + + + )} + )} diff --git a/apps/web/src/components/AccountDrawer/DefaultMenu.tsx b/apps/web/src/components/AccountDrawer/DefaultMenu.tsx index 7e6388835f0..f3d925d21d4 100644 --- a/apps/web/src/components/AccountDrawer/DefaultMenu.tsx +++ b/apps/web/src/components/AccountDrawer/DefaultMenu.tsx @@ -2,13 +2,14 @@ import AuthenticatedHeader from 'components/AccountDrawer/AuthenticatedHeader' import LanguageMenu from 'components/AccountDrawer/LanguageMenu' import LocalCurrencyMenu from 'components/AccountDrawer/LocalCurrencyMenu' import { LimitsMenu } from 'components/AccountDrawer/MiniPortfolio/Limits/LimitsMenu' +import { UniExtensionPoolsMenu } from 'components/AccountDrawer/MiniPortfolio/Pools/UniExtensionPoolsMenu' import SettingsMenu from 'components/AccountDrawer/SettingsMenu' import Column from 'components/Column' import WalletModal from 'components/WalletModal' import { useAccount } from 'hooks/useAccount' import { atom, useAtom } from 'jotai' +import styled from 'lib/styled-components' import { useCallback, useEffect, useMemo } from 'react' -import styled from 'styled-components' import { InterfaceEventNameLocal } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' @@ -23,6 +24,7 @@ export enum MenuState { LANGUAGE_SETTINGS = 'language_settings', LOCAL_CURRENCY_SETTINGS = 'local_currency_settings', LIMITS = 'limits', + POOLS = 'pools', } export const miniPortfolioMenuStateAtom = atom(MenuState.DEFAULT) @@ -78,6 +80,8 @@ function DefaultMenu({ drawerOpen }: { drawerOpen: boolean }) { return case MenuState.LIMITS: return account.address ? : null + case MenuState.POOLS: + return account.address ? : null } }, [ account.address, diff --git a/apps/web/src/components/AccountDrawer/DownloadButton.tsx b/apps/web/src/components/AccountDrawer/DownloadButton.tsx index 2b0b702e16e..aea0f44b17d 100644 --- a/apps/web/src/components/AccountDrawer/DownloadButton.tsx +++ b/apps/web/src/components/AccountDrawer/DownloadButton.tsx @@ -1,6 +1,6 @@ import { InterfaceElementName } from '@uniswap/analytics-events' +import styled from 'lib/styled-components' import { PropsWithChildren, useCallback } from 'react' -import styled from 'styled-components' import { ClickableStyle } from 'theme/components' import { openDownloadApp } from 'utils/openDownloadApp' diff --git a/apps/web/src/components/AccountDrawer/GitVersionRow.tsx b/apps/web/src/components/AccountDrawer/GitVersionRow.tsx index be76d36c588..f19b7936c8f 100644 --- a/apps/web/src/components/AccountDrawer/GitVersionRow.tsx +++ b/apps/web/src/components/AccountDrawer/GitVersionRow.tsx @@ -1,7 +1,7 @@ import Tooltip from 'components/Tooltip' import useCopyClipboard from 'hooks/useCopyClipboard' import { Trans } from 'i18n' -import styled from 'styled-components' +import styled from 'lib/styled-components' import { ThemedText } from 'theme/components' const Container = styled.div` diff --git a/apps/web/src/components/AccountDrawer/IconButton.tsx b/apps/web/src/components/AccountDrawer/IconButton.tsx index e9aef490b94..e75b5b79311 100644 --- a/apps/web/src/components/AccountDrawer/IconButton.tsx +++ b/apps/web/src/components/AccountDrawer/IconButton.tsx @@ -1,7 +1,7 @@ import Row from 'components/Row' +import styled, { DefaultTheme, css } from 'lib/styled-components' import React, { forwardRef, useCallback, useEffect, useRef, useState } from 'react' import { Icon } from 'react-feather' -import styled, { DefaultTheme, css } from 'styled-components' import { TRANSITION_DURATIONS } from 'theme/styles' import useResizeObserver from 'use-resize-observer' @@ -35,10 +35,11 @@ const IconStyles = css<{ hideHorizontal?: boolean }>` :hover { background-color: ${({ theme }) => theme.surface2}; transition: ${({ - theme: { - transition: { duration, timing }, - }, - }) => `${duration.fast} background-color ${timing.in}, ${getWidthTransition}`}; + theme: { + transition: { duration, timing }, + }, + }) => `${duration.fast} background-color ${timing.in},`} + ${getWidthTransition}; ${IconHoverText} { opacity: 1; @@ -46,7 +47,9 @@ const IconStyles = css<{ hideHorizontal?: boolean }>` } :active { background-color: ${({ theme }) => theme.surface1}; - transition: background-color ${({ theme }) => theme.transition.duration.fast} linear, ${getWidthTransition}; + transition: + background-color ${({ theme }) => theme.transition.duration.fast} linear, + ${getWidthTransition}; } ` @@ -114,7 +117,8 @@ const TextWrapper = styled.div` const TextHide = styled.div` overflow: hidden; - transition: width ${({ theme }) => theme.transition.timing.inOut} ${({ theme }) => theme.transition.duration.fast}, + transition: + width ${({ theme }) => theme.transition.timing.inOut} ${({ theme }) => theme.transition.duration.fast}, max-width ${({ theme }) => theme.transition.timing.inOut} ${({ theme }) => theme.transition.duration.fast}; ` @@ -143,7 +147,7 @@ export const IconWithConfirmTextButton = ({ setShowTextWithoutCallback(val) onShowConfirm?.(val) }, - [onShowConfirm] + [onShowConfirm], ) const dimensionsRef = useRef({ diff --git a/apps/web/src/components/AccountDrawer/LocalCurrencyMenu.tsx b/apps/web/src/components/AccountDrawer/LocalCurrencyMenu.tsx index 30b1b29fb7b..aeb887d83d9 100644 --- a/apps/web/src/components/AccountDrawer/LocalCurrencyMenu.tsx +++ b/apps/web/src/components/AccountDrawer/LocalCurrencyMenu.tsx @@ -1,11 +1,11 @@ -import { MenuColumn, MenuItem } from 'components/AccountDrawer/shared' import { SlideOutMenu } from 'components/AccountDrawer/SlideOutMenu' -import { getLocalCurrencyIcon, SUPPORTED_LOCAL_CURRENCIES, SupportedLocalCurrency } from 'constants/localCurrencies' +import { MenuColumn, MenuItem } from 'components/AccountDrawer/shared' +import { SUPPORTED_LOCAL_CURRENCIES, SupportedLocalCurrency, getLocalCurrencyIcon } from 'constants/localCurrencies' import { useActiveLocalCurrency } from 'hooks/useActiveLocalCurrency' import { useLocalCurrencyLinkProps } from 'hooks/useLocalCurrencyLinkProps' import { Trans } from 'i18n' +import styled from 'lib/styled-components' import { useMemo } from 'react' -import styled from 'styled-components' const StyledLocalCurrencyIcon = styled.div` width: 20px; diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/ActivityRow.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/ActivityRow.tsx index 22d3daac8df..e73c9e4fb82 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/ActivityRow.tsx +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/ActivityRow.tsx @@ -9,9 +9,9 @@ import AlertTriangleFilled from 'components/Icons/AlertTriangleFilled' import { LoaderV2 } from 'components/Icons/LoadingSpinner' import Row from 'components/Row' import useENSName from 'hooks/useENSName' +import styled from 'lib/styled-components' import { useCallback } from 'react' import { SignatureType } from 'state/signatures/types' -import styled from 'styled-components' import { EllipsisStyle, ThemedText } from 'theme/components' import { TransactionStatus } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import Trace from 'uniswap/src/features/telemetry/Trace' @@ -26,7 +26,10 @@ const ActivityRowDescriptor = styled(ThemedText.BodySmall)` const StyledTimestamp = styled(ThemedText.BodySmall)` color: ${({ theme }) => theme.neutral2}; font-variant: small; - font-feature-settings: 'tnum' on, 'lnum' on, 'ss02' on; + font-feature-settings: + 'tnum' on, + 'lnum' on, + 'ss02' on; ` function StatusIndicator({ activity: { status, timestamp, offchainOrderDetails } }: { activity: Activity }) { @@ -46,8 +49,18 @@ function StatusIndicator({ activity: { status, timestamp, offchainOrderDetails } } export function ActivityRow({ activity }: { activity: Activity }) { - const { chainId, title, descriptor, logos, otherAccount, currencies, hash, prefixIconSrc, offchainOrderDetails } = - activity + const { + chainId, + title, + descriptor, + logos, + otherAccount, + currencies, + hash, + prefixIconSrc, + suffixIconSrc, + offchainOrderDetails, + } = activity const openOffchainActivityModal = useOpenOffchainActivityModal() @@ -82,6 +95,7 @@ export function ActivityRow({ activity }: { activity: Activity }) { {prefixIconSrc && } {title} + {suffixIconSrc && } } descriptor={ diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/CancelOrdersDialog.test.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/CancelOrdersDialog.test.tsx index ec4cf3f93d1..5f991b7ee97 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/CancelOrdersDialog.test.tsx +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/CancelOrdersDialog.test.tsx @@ -52,15 +52,15 @@ describe('CancelOrdersDialog', () => { isVisible={true} orders={[mockOrderDetails]} cancelState={CancellationState.REVIEWING_CANCELLATION} - /> + />, ) expect(document.body).toMatchSnapshot() expect(screen.getByText('Cancel order')).toBeInTheDocument() expect( screen.getByText( - 'Your swap could execute before cancellation is processed. Your network costs cannot be refunded. Do you wish to proceed?' - ) + 'Your swap could execute before cancellation is processed. Your network costs cannot be refunded. Do you wish to proceed?', + ), ).toBeInTheDocument() }) it('should render limit order text', async () => { @@ -73,15 +73,15 @@ describe('CancelOrdersDialog', () => { isVisible={true} orders={[{ ...mockOrderDetails, type: SignatureType.SIGN_LIMIT }]} cancelState={CancellationState.REVIEWING_CANCELLATION} - /> + />, ) expect(document.body).toMatchSnapshot() expect(screen.getByText('Cancel limit')).toBeInTheDocument() expect( screen.getByText( - 'Your swap could execute before cancellation is processed. Your network costs cannot be refunded. Do you wish to proceed?' - ) + 'Your swap could execute before cancellation is processed. Your network costs cannot be refunded. Do you wish to proceed?', + ), ).toBeInTheDocument() }) }) diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/CancelOrdersDialog.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/CancelOrdersDialog.tsx index 68f56891815..c1c51fb0bac 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/CancelOrdersDialog.tsx +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/CancelOrdersDialog.tsx @@ -1,20 +1,20 @@ import { CurrencyAmount } from '@uniswap/sdk-core' import { ConfirmedIcon, LogoContainer, SubmittedIcon } from 'components/AccountDrawer/MiniPortfolio/Activity/Logos' import { useCancelOrdersGasEstimate } from 'components/AccountDrawer/MiniPortfolio/Activity/hooks' -import GetHelp from 'components/Button/GetHelp' import Column from 'components/Column' import { Container, Dialog, DialogButtonType, DialogProps } from 'components/Dialog/Dialog' import { LoaderV3 } from 'components/Icons/LoadingSpinner' import Modal from 'components/Modal' +import { GetHelpHeader } from 'components/Modal/GetHelpHeader' import Row from 'components/Row' import { DetailLineItem } from 'components/swap/DetailLineItem' import { nativeOnChain } from 'constants/tokens' import { useStablecoinValue } from 'hooks/useStablecoinPrice' import { Plural, Trans, t } from 'i18n' +import styled, { useTheme } from 'lib/styled-components' import { Slash } from 'react-feather' import { SignatureType, UniswapXOrderDetails } from 'state/signatures/types' -import styled, { useTheme } from 'styled-components' -import { CloseIcon, ExternalLink, ThemedText } from 'theme/components' +import { ExternalLink, ThemedText } from 'theme/components' import { InterfaceChainId } from 'uniswap/src/types/chains' import { NumberType, useFormatter } from 'utils/formatNumbers' import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink' @@ -24,6 +24,9 @@ const GasEstimateContainer = styled(Row)` margin-top: 16px; padding-top: 16px; ` +const ModalHeader = styled(GetHelpHeader)` + padding: 4px 0px; +` export enum CancellationState { NOT_STARTED = 'not_started', @@ -38,7 +41,7 @@ type CancelOrdersDialogProps = Partial void - } + }, ) { const { orders, cancelState, cancelTxHash, onConfirm, onCancel } = props @@ -95,7 +98,7 @@ export function CancelOrdersDialog( const gasEstimate = useCancelOrdersGasEstimate(orders) if ( [CancellationState.PENDING_SIGNATURE, CancellationState.PENDING_CONFIRMATION, CancellationState.CANCELLED].includes( - cancelState + cancelState, ) ) { const cancelSubmitted = @@ -104,10 +107,7 @@ export function CancelOrdersDialog( return ( - - - - + {icon} {title} diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/Logos.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/Logos.tsx index 17159dfb1c3..9c5a6081336 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/Logos.tsx +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/Logos.tsx @@ -1,5 +1,5 @@ import { LoaderV3 } from 'components/Icons/LoadingSpinner' -import styled, { css, useTheme } from 'styled-components' +import styled, { css, useTheme } from 'lib/styled-components' import { FadePresence, FadePresenceAnimationType } from 'theme/components/FadePresence' export const LogoContainer = styled.div` diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/OffchainActivityModal.test.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/OffchainActivityModal.test.tsx index f826ba5e07c..2d9799a9f51 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/OffchainActivityModal.test.tsx +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/OffchainActivityModal.test.tsx @@ -44,7 +44,7 @@ describe('OrderContent', () => { settledOutputCurrencyAmountRaw: '106841079134757921', }, }} - /> + />, ) expect(container).toMatchSnapshot() expect(container).toHaveTextContent('Order executed') @@ -73,7 +73,7 @@ describe('OrderContent', () => { settledOutputCurrencyAmountRaw: '106841079134757921', }, }} - /> + />, ) expect(container).toMatchSnapshot() expect(container).toHaveTextContent('Order pending') @@ -104,7 +104,7 @@ describe('OrderContent', () => { settledOutputCurrencyAmountRaw: '106841079134757921', }, }} - /> + />, ) expect(container).toMatchSnapshot() expect(container).toHaveTextContent('Limit pending') diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/OffchainActivityModal.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/OffchainActivityModal.tsx index e1f15365645..2ad628133d6 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/OffchainActivityModal.tsx +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/OffchainActivityModal.tsx @@ -25,11 +25,11 @@ import { useUSDPrice } from 'hooks/useUSDPrice' import { Trans } from 'i18n' import { atom } from 'jotai' import { useAtomValue, useUpdateAtom } from 'jotai/utils' +import styled, { useTheme } from 'lib/styled-components' import { ReactNode, useCallback, useMemo, useState } from 'react' import { ArrowDown, X } from 'react-feather' import { useOrder } from 'state/signatures/hooks' import { SignatureType, UniswapXOrderDetails } from 'state/signatures/types' -import styled, { useTheme } from 'styled-components' import { Divider, ThemedText } from 'theme/components' import { UniswapXOrderStatus } from 'types/uniswapx' import { InterfaceEventNameLocal } from 'uniswap/src/features/telemetry/constants' @@ -61,7 +61,7 @@ export function useOpenOffchainActivityModal() { }) setSelectedOrder({ order, logos, modalOpen: true }) }, - [setSelectedOrder] + [setSelectedOrder], ) } @@ -136,7 +136,7 @@ export function useOrderAmounts(order?: UniswapXOrderDetails): inputAmount: CurrencyAmount.fromRawAmount(inputCurrency, swapInfo.inputCurrencyAmountRaw), outputAmount: CurrencyAmount.fromRawAmount( outputCurrency, - swapInfo.settledOutputCurrencyAmountRaw ?? swapInfo.expectedOutputCurrencyAmountRaw + swapInfo.settledOutputCurrencyAmountRaw ?? swapInfo.expectedOutputCurrencyAmountRaw, ), } } else { @@ -214,7 +214,7 @@ export function OrderContent({ const currencies = useMemo( () => [amounts?.inputAmount.currency, amounts?.outputAmount.currency], - [amounts?.inputAmount.currency, amounts?.outputAmount.currency] + [amounts?.inputAmount.currency, amounts?.outputAmount.currency], ) if (!amounts?.inputAmount || !amounts?.outputAmount) { @@ -343,7 +343,7 @@ export function OffchainActivityModal() { }, [setSelectedOrder]) const cancelOrder = useCancelMultipleOrdersCallback( - useMemo(() => [syncedSelectedOrder].filter(Boolean) as Array, [syncedSelectedOrder]) + useMemo(() => [syncedSelectedOrder].filter(Boolean) as Array, [syncedSelectedOrder]), ) return ( diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/OffchainOrderLineItem.test.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/OffchainOrderLineItem.test.tsx index 21f379de4ad..a6df97b6d6d 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/OffchainOrderLineItem.test.tsx +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/OffchainOrderLineItem.test.tsx @@ -18,7 +18,7 @@ describe('OffchainOrderLineItem', () => { inputAmount: CurrencyAmount.fromRawAmount(DAI, 1), outputAmount: CurrencyAmount.fromRawAmount(USDC_MAINNET, 1), }} - /> + />, ) expect(asFragment()).toMatchSnapshot() expect(screen.getByText('Rate')).toBeInTheDocument() @@ -50,7 +50,7 @@ describe('OffchainOrderLineItem', () => { addedTime: 1, expiry: 2, }} - /> + />, ) expect(screen.getByText('Expiry')).toBeInTheDocument() }) @@ -88,7 +88,7 @@ describe('OffchainOrderLineItem', () => { addedTime: 1, expiry: 2, }} - /> + />, ) expect(asFragment()).toMatchSnapshot() expect(screen.getByText('Transaction ID')).toBeInTheDocument() diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/OffchainOrderLineItem.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/OffchainOrderLineItem.tsx index 66ca61524c0..c01849fe635 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/OffchainOrderLineItem.tsx +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/OffchainOrderLineItem.tsx @@ -49,7 +49,7 @@ function useLineItem(details: OffchainOrderLineItemProps): LineItemData | undefi details.amounts?.inputAmount.currency, details.amounts?.outputAmount.currency, details.amounts?.inputAmount.quotient, - details.amounts?.outputAmount.quotient + details.amounts?.outputAmount.quotient, ) } /> diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/__snapshots__/CancelOrdersDialog.test.tsx.snap b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/__snapshots__/CancelOrdersDialog.test.tsx.snap index 26fb9013d9e..b5ea2769a18 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/__snapshots__/CancelOrdersDialog.test.tsx.snap +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/__snapshots__/CancelOrdersDialog.test.tsx.snap @@ -1,40 +1,80 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`CancelOrdersDialog should render limit order text 1`] = ` -.c5 { - box-sizing: border-box; - margin: 0; - min-width: 0; - width: 100%; - padding: 4px 0px; +.c2 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 24px; } -.c9 { - box-sizing: border-box; - margin: 0; - min-width: 0; +.c11 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 16px; } -.c6 { - width: 100%; +.c13 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - padding: 0; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 8px; +} + +.c18 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c3 { + width: 100%; -webkit-align-items: center; -webkit-box-align: center; -ms-flex-align: center; align-items: center; - -webkit-box-pack: end; - -webkit-justify-content: end; - -ms-flex-pack: end; - justify-content: end; - padding: 4px 0px; - gap: 10px; } -.c10 { +.c8 { + box-sizing: border-box; + margin: 0; + min-width: 0; +} + +.c9 { width: 100%; display: -webkit-box; display: -webkit-flex; @@ -52,7 +92,7 @@ exports[`CancelOrdersDialog should render limit order text 1`] = ` gap: 4px; } -.c20 { +.c19 { width: 100%; display: -webkit-box; display: -webkit-flex; @@ -69,7 +109,7 @@ exports[`CancelOrdersDialog should render limit order text 1`] = ` justify-content: flex-start; } -.c25 { +.c24 { width: 100%; display: -webkit-box; display: -webkit-flex; @@ -87,14 +127,14 @@ exports[`CancelOrdersDialog should render limit order text 1`] = ` gap: 12px; } -.c22 { +.c21 { -webkit-box-pack: justify; -webkit-justify-content: space-between; -ms-flex-pack: justify; justify-content: space-between; } -.c15 { +.c14 { color: #222222; -webkit-letter-spacing: -0.01em; -moz-letter-spacing: -0.01em; @@ -102,7 +142,7 @@ exports[`CancelOrdersDialog should render limit order text 1`] = ` letter-spacing: -0.01em; } -.c17 { +.c16 { color: #7D7D7D; -webkit-letter-spacing: -0.01em; -moz-letter-spacing: -0.01em; @@ -110,7 +150,7 @@ exports[`CancelOrdersDialog should render limit order text 1`] = ` letter-spacing: -0.01em; } -.c11 { +.c10 { color: #222222; cursor: pointer; -webkit-text-decoration: none; @@ -120,15 +160,15 @@ exports[`CancelOrdersDialog should render limit order text 1`] = ` transition-duration: 125ms; } -.c11:hover { +.c10:hover { opacity: 0.6; } -.c11:active { +.c10:active { opacity: 0.4; } -.c7 { +.c6 { -webkit-text-decoration: none; text-decoration: none; cursor: pointer; @@ -139,106 +179,15 @@ exports[`CancelOrdersDialog should render limit order text 1`] = ` font-weight: 500; } -.c7:hover { +.c6:hover { opacity: 0.6; } -.c7:active { +.c6:active { opacity: 0.4; } -.c8 { - width: -webkit-fit-content; - width: -moz-fit-content; - width: fit-content; - border-radius: 16px; - padding: 4px 8px; - font-size: 14px; - font-weight: 485; - line-height: 20px; - background: #F9F9F9; - color: #7D7D7D; - stroke: none; -} - -.c8:hover { - background: #22222212; - color: #222222; - opacity: unset; -} - -.c8:hover path { - fill: #222222; -} - -.c2 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - -webkit-box-pack: start; - -webkit-justify-content: flex-start; - -ms-flex-pack: start; - justify-content: flex-start; - gap: 24px; -} - -.c12 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - -webkit-box-pack: start; - -webkit-justify-content: flex-start; - -ms-flex-pack: start; - justify-content: flex-start; - gap: 16px; -} - -.c14 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - -webkit-box-pack: start; - -webkit-justify-content: flex-start; - -ms-flex-pack: start; - justify-content: flex-start; - gap: 8px; -} - -.c19 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - -webkit-box-pack: start; - -webkit-justify-content: flex-start; - -ms-flex-pack: start; - justify-content: flex-start; -} - -.c3 { - width: 100%; - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; -} - -.c29 { +.c28 { background-color: transparent; bottom: 0; border-radius: inherit; @@ -252,7 +201,7 @@ exports[`CancelOrdersDialog should render limit order text 1`] = ` width: 100%; } -.c26 { +.c25 { -webkit-align-items: center; -webkit-box-align: center; -ms-flex-align: center; @@ -287,30 +236,30 @@ exports[`CancelOrdersDialog should render limit order text 1`] = ` user-select: none; } -.c26:active .c28 { +.c25:active .c27 { background-color: #B8C0DC3d; } -.c26:focus .c28 { +.c25:focus .c27 { background-color: #B8C0DC3d; } -.c26:hover .c28 { +.c25:hover .c27 { background-color: #98A1C014; } -.c26:disabled { +.c25:disabled { cursor: default; opacity: 0.6; } -.c26:disabled:active .c28, -.c26:disabled:focus .c28, -.c26:disabled:hover .c28 { +.c25:disabled:active .c27, +.c25:disabled:focus .c27, +.c25:disabled:hover .c27 { background-color: transparent; } -.c30 { +.c29 { -webkit-align-items: center; -webkit-box-align: center; -ms-flex-align: center; @@ -345,26 +294,26 @@ exports[`CancelOrdersDialog should render limit order text 1`] = ` user-select: none; } -.c30:active .c28 { +.c29:active .c27 { background-color: #B8C0DC3d; } -.c30:focus .c28 { +.c29:focus .c27 { background-color: #B8C0DC3d; } -.c30:hover .c28 { +.c29:hover .c27 { background-color: #98A1C014; } -.c30:disabled { +.c29:disabled { cursor: default; opacity: 0.6; } -.c30:disabled:active .c28, -.c30:disabled:focus .c28, -.c30:disabled:hover .c28 { +.c29:disabled:active .c27, +.c29:disabled:focus .c27, +.c29:disabled:hover .c27 { background-color: transparent; } @@ -411,6 +360,30 @@ exports[`CancelOrdersDialog should render limit order text 1`] = ` border-radius: 20px; } +.c7 { + width: -webkit-fit-content; + width: -moz-fit-content; + width: fit-content; + border-radius: 16px; + padding: 4px 8px; + font-size: 14px; + font-weight: 485; + line-height: 20px; + background: #F9F9F9; + color: #7D7D7D; + stroke: none; +} + +.c7:hover { + background: #22222212; + color: #222222; + opacity: unset; +} + +.c7:hover path { + fill: #222222; +} + .c4 { background-color: #FFFFFF; outline: 1px solid #22222212; @@ -419,7 +392,7 @@ exports[`CancelOrdersDialog should render limit order text 1`] = ` width: 100%; } -.c13 { +.c12 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -438,14 +411,14 @@ exports[`CancelOrdersDialog should render limit order text 1`] = ` border-radius: 12px; } -.c16 { +.c15 { font-size: 24px; line-height: 32px; text-align: center; font-weight: 500; } -.c18 { +.c17 { font-size: 16px; font-weight: 500; line-height: 24px; @@ -456,7 +429,7 @@ exports[`CancelOrdersDialog should render limit order text 1`] = ` text-align: center; } -.c27 { +.c26 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -470,7 +443,7 @@ exports[`CancelOrdersDialog should render limit order text 1`] = ` border-radius: 12px; } -.c31 { +.c30 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -484,17 +457,21 @@ exports[`CancelOrdersDialog should render limit order text 1`] = ` border-radius: 12px; } -.c23 { +.c5 { + padding: 4px 0px; +} + +.c22 { cursor: auto; color: #7D7D7D; } -.c24 { +.c23 { text-align: right; overflow-wrap: break-word; } -.c21 { +.c20 { border-top: 1px solid #22222212; margin-top: 16px; padding-top: 16px; @@ -570,68 +547,71 @@ exports[`CancelOrdersDialog should render limit order text 1`] = ` class="c2 c3 c4" >

        - -
        - - - - Get help -
        -
        - - - - + + + + Get help +
        + + + + + +
        Cancel limit
        Your swap could execute before cancellation is processed. Your network costs cannot be refunded. Do you wish to proceed?
        Network cost
        - @@ -698,21 +678,21 @@ exports[`CancelOrdersDialog should render limit order text 1`] = `
        @@ -733,40 +713,80 @@ exports[`CancelOrdersDialog should render limit order text 1`] = ` `; exports[`CancelOrdersDialog should render order cancel correctly 1`] = ` -.c5 { - box-sizing: border-box; - margin: 0; - min-width: 0; - width: 100%; - padding: 4px 0px; +.c2 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 24px; } -.c9 { - box-sizing: border-box; - margin: 0; - min-width: 0; +.c11 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 16px; } -.c6 { - width: 100%; +.c13 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; display: flex; - padding: 0; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 8px; +} + +.c18 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c3 { + width: 100%; -webkit-align-items: center; -webkit-box-align: center; -ms-flex-align: center; align-items: center; - -webkit-box-pack: end; - -webkit-justify-content: end; - -ms-flex-pack: end; - justify-content: end; - padding: 4px 0px; - gap: 10px; } -.c10 { +.c8 { + box-sizing: border-box; + margin: 0; + min-width: 0; +} + +.c9 { width: 100%; display: -webkit-box; display: -webkit-flex; @@ -784,7 +804,7 @@ exports[`CancelOrdersDialog should render order cancel correctly 1`] = ` gap: 4px; } -.c20 { +.c19 { width: 100%; display: -webkit-box; display: -webkit-flex; @@ -801,7 +821,7 @@ exports[`CancelOrdersDialog should render order cancel correctly 1`] = ` justify-content: flex-start; } -.c25 { +.c24 { width: 100%; display: -webkit-box; display: -webkit-flex; @@ -819,14 +839,14 @@ exports[`CancelOrdersDialog should render order cancel correctly 1`] = ` gap: 12px; } -.c22 { +.c21 { -webkit-box-pack: justify; -webkit-justify-content: space-between; -ms-flex-pack: justify; justify-content: space-between; } -.c15 { +.c14 { color: #222222; -webkit-letter-spacing: -0.01em; -moz-letter-spacing: -0.01em; @@ -834,7 +854,7 @@ exports[`CancelOrdersDialog should render order cancel correctly 1`] = ` letter-spacing: -0.01em; } -.c17 { +.c16 { color: #7D7D7D; -webkit-letter-spacing: -0.01em; -moz-letter-spacing: -0.01em; @@ -842,7 +862,7 @@ exports[`CancelOrdersDialog should render order cancel correctly 1`] = ` letter-spacing: -0.01em; } -.c11 { +.c10 { color: #222222; cursor: pointer; -webkit-text-decoration: none; @@ -852,15 +872,15 @@ exports[`CancelOrdersDialog should render order cancel correctly 1`] = ` transition-duration: 125ms; } -.c11:hover { +.c10:hover { opacity: 0.6; } -.c11:active { +.c10:active { opacity: 0.4; } -.c7 { +.c6 { -webkit-text-decoration: none; text-decoration: none; cursor: pointer; @@ -871,106 +891,15 @@ exports[`CancelOrdersDialog should render order cancel correctly 1`] = ` font-weight: 500; } -.c7:hover { +.c6:hover { opacity: 0.6; } -.c7:active { +.c6:active { opacity: 0.4; } -.c8 { - width: -webkit-fit-content; - width: -moz-fit-content; - width: fit-content; - border-radius: 16px; - padding: 4px 8px; - font-size: 14px; - font-weight: 485; - line-height: 20px; - background: #F9F9F9; - color: #7D7D7D; - stroke: none; -} - -.c8:hover { - background: #22222212; - color: #222222; - opacity: unset; -} - -.c8:hover path { - fill: #222222; -} - -.c2 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - -webkit-box-pack: start; - -webkit-justify-content: flex-start; - -ms-flex-pack: start; - justify-content: flex-start; - gap: 24px; -} - -.c12 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - -webkit-box-pack: start; - -webkit-justify-content: flex-start; - -ms-flex-pack: start; - justify-content: flex-start; - gap: 16px; -} - -.c14 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - -webkit-box-pack: start; - -webkit-justify-content: flex-start; - -ms-flex-pack: start; - justify-content: flex-start; - gap: 8px; -} - -.c19 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - -webkit-box-pack: start; - -webkit-justify-content: flex-start; - -ms-flex-pack: start; - justify-content: flex-start; -} - -.c3 { - width: 100%; - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; -} - -.c29 { +.c28 { background-color: transparent; bottom: 0; border-radius: inherit; @@ -984,7 +913,7 @@ exports[`CancelOrdersDialog should render order cancel correctly 1`] = ` width: 100%; } -.c26 { +.c25 { -webkit-align-items: center; -webkit-box-align: center; -ms-flex-align: center; @@ -1019,30 +948,30 @@ exports[`CancelOrdersDialog should render order cancel correctly 1`] = ` user-select: none; } -.c26:active .c28 { +.c25:active .c27 { background-color: #B8C0DC3d; } -.c26:focus .c28 { +.c25:focus .c27 { background-color: #B8C0DC3d; } -.c26:hover .c28 { +.c25:hover .c27 { background-color: #98A1C014; } -.c26:disabled { +.c25:disabled { cursor: default; opacity: 0.6; } -.c26:disabled:active .c28, -.c26:disabled:focus .c28, -.c26:disabled:hover .c28 { +.c25:disabled:active .c27, +.c25:disabled:focus .c27, +.c25:disabled:hover .c27 { background-color: transparent; } -.c30 { +.c29 { -webkit-align-items: center; -webkit-box-align: center; -ms-flex-align: center; @@ -1077,26 +1006,26 @@ exports[`CancelOrdersDialog should render order cancel correctly 1`] = ` user-select: none; } -.c30:active .c28 { +.c29:active .c27 { background-color: #B8C0DC3d; } -.c30:focus .c28 { +.c29:focus .c27 { background-color: #B8C0DC3d; } -.c30:hover .c28 { +.c29:hover .c27 { background-color: #98A1C014; } -.c30:disabled { +.c29:disabled { cursor: default; opacity: 0.6; } -.c30:disabled:active .c28, -.c30:disabled:focus .c28, -.c30:disabled:hover .c28 { +.c29:disabled:active .c27, +.c29:disabled:focus .c27, +.c29:disabled:hover .c27 { background-color: transparent; } @@ -1143,6 +1072,30 @@ exports[`CancelOrdersDialog should render order cancel correctly 1`] = ` border-radius: 20px; } +.c7 { + width: -webkit-fit-content; + width: -moz-fit-content; + width: fit-content; + border-radius: 16px; + padding: 4px 8px; + font-size: 14px; + font-weight: 485; + line-height: 20px; + background: #F9F9F9; + color: #7D7D7D; + stroke: none; +} + +.c7:hover { + background: #22222212; + color: #222222; + opacity: unset; +} + +.c7:hover path { + fill: #222222; +} + .c4 { background-color: #FFFFFF; outline: 1px solid #22222212; @@ -1151,7 +1104,7 @@ exports[`CancelOrdersDialog should render order cancel correctly 1`] = ` width: 100%; } -.c13 { +.c12 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -1170,14 +1123,14 @@ exports[`CancelOrdersDialog should render order cancel correctly 1`] = ` border-radius: 12px; } -.c16 { +.c15 { font-size: 24px; line-height: 32px; text-align: center; font-weight: 500; } -.c18 { +.c17 { font-size: 16px; font-weight: 500; line-height: 24px; @@ -1188,7 +1141,7 @@ exports[`CancelOrdersDialog should render order cancel correctly 1`] = ` text-align: center; } -.c27 { +.c26 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -1202,7 +1155,7 @@ exports[`CancelOrdersDialog should render order cancel correctly 1`] = ` border-radius: 12px; } -.c31 { +.c30 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -1216,17 +1169,21 @@ exports[`CancelOrdersDialog should render order cancel correctly 1`] = ` border-radius: 12px; } -.c23 { +.c5 { + padding: 4px 0px; +} + +.c22 { cursor: auto; color: #7D7D7D; } -.c24 { +.c23 { text-align: right; overflow-wrap: break-word; } -.c21 { +.c20 { border-top: 1px solid #22222212; margin-top: 16px; padding-top: 16px; @@ -1302,68 +1259,71 @@ exports[`CancelOrdersDialog should render order cancel correctly 1`] = ` class="c2 c3 c4" >
        - -
        - - - - Get help -
        -
        - - - - + + + + Get help +
        + + + + + +
        Cancel order
        Your swap could execute before cancellation is processed. Your network costs cannot be refunded. Do you wish to proceed?
        Network cost
        - @@ -1430,21 +1390,21 @@ exports[`CancelOrdersDialog should render order cancel correctly 1`] = `
        diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/__snapshots__/OffchainActivityModal.test.tsx.snap b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/__snapshots__/OffchainActivityModal.test.tsx.snap index efa9a0d17f5..3378ff6b8ec 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/__snapshots__/OffchainActivityModal.test.tsx.snap +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/__snapshots__/OffchainActivityModal.test.tsx.snap @@ -1,6 +1,65 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`OrderContent should render without error, filled order 1`] = ` +.c0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c10 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 12px; +} + +.c12 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 4px; +} + +.c17 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 8px; +} + .c1 { box-sizing: border-box; margin: 0; @@ -117,65 +176,6 @@ exports[`OrderContent should render without error, filled order 1`] = ` background-color: #22222212; } -.c0 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - -webkit-box-pack: start; - -webkit-justify-content: flex-start; - -ms-flex-pack: start; - justify-content: flex-start; -} - -.c10 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - -webkit-box-pack: start; - -webkit-justify-content: flex-start; - -ms-flex-pack: start; - justify-content: flex-start; - gap: 12px; -} - -.c12 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - -webkit-box-pack: start; - -webkit-justify-content: flex-start; - -ms-flex-pack: start; - justify-content: flex-start; - gap: 4px; -} - -.c17 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - -webkit-box-pack: start; - -webkit-justify-content: flex-start; - -ms-flex-pack: start; - justify-content: flex-start; - gap: 8px; -} - .c20 { cursor: auto; color: #7D7D7D; @@ -543,6 +543,65 @@ exports[`OrderContent should render without error, filled order 1`] = ` `; exports[`OrderContent should render without error, limit order 1`] = ` +.c0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c10 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 12px; +} + +.c12 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 4px; +} + +.c17 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 8px; +} + .c1 { box-sizing: border-box; margin: 0; @@ -659,65 +718,6 @@ exports[`OrderContent should render without error, limit order 1`] = ` background-color: #22222212; } -.c0 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - -webkit-box-pack: start; - -webkit-justify-content: flex-start; - -ms-flex-pack: start; - justify-content: flex-start; -} - -.c10 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - -webkit-box-pack: start; - -webkit-justify-content: flex-start; - -ms-flex-pack: start; - justify-content: flex-start; - gap: 12px; -} - -.c12 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - -webkit-box-pack: start; - -webkit-justify-content: flex-start; - -ms-flex-pack: start; - justify-content: flex-start; - gap: 4px; -} - -.c17 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - -webkit-box-pack: start; - -webkit-justify-content: flex-start; - -ms-flex-pack: start; - justify-content: flex-start; - gap: 8px; -} - .c26 { background-color: transparent; bottom: 0; @@ -1201,6 +1201,65 @@ exports[`OrderContent should render without error, limit order 1`] = ` `; exports[`OrderContent should render without error, open order 1`] = ` +.c0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c10 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 12px; +} + +.c12 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 4px; +} + +.c17 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 8px; +} + .c1 { box-sizing: border-box; margin: 0; @@ -1298,65 +1357,6 @@ exports[`OrderContent should render without error, open order 1`] = ` background-color: #22222212; } -.c0 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - -webkit-box-pack: start; - -webkit-justify-content: flex-start; - -ms-flex-pack: start; - justify-content: flex-start; -} - -.c10 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - -webkit-box-pack: start; - -webkit-justify-content: flex-start; - -ms-flex-pack: start; - justify-content: flex-start; - gap: 12px; -} - -.c12 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - -webkit-box-pack: start; - -webkit-justify-content: flex-start; - -ms-flex-pack: start; - justify-content: flex-start; - gap: 4px; -} - -.c17 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - -webkit-box-pack: start; - -webkit-justify-content: flex-start; - -ms-flex-pack: start; - justify-content: flex-start; - gap: 8px; -} - .c26 { background-color: transparent; bottom: 0; diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/getCurrency.ts b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/getCurrency.ts index df3674fc9bd..0e5fbe98754 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/getCurrency.ts +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/getCurrency.ts @@ -1,5 +1,6 @@ import { Currency } from '@uniswap/sdk-core' import { SupportedInterfaceChainId, chainIdToBackendChain } from 'constants/chains' +import { COMMON_BASES } from 'constants/routing' import { NATIVE_CHAIN_ID, nativeOnChain } from 'constants/tokens' import { apolloClient } from 'graphql/data/apollo/client' import { gqlTokenToCurrencyInfo } from 'graphql/data/types' @@ -8,16 +9,23 @@ import { SimpleTokenQuery, Token, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { isSameAddress } from 'utilities/src/addresses' export async function getCurrency( currencyId: string, - chainId: SupportedInterfaceChainId + chainId: SupportedInterfaceChainId, ): Promise { const isNative = currencyId === NATIVE_CHAIN_ID || currencyId?.toLowerCase() === 'native' || currencyId?.toLowerCase() === 'eth' if (isNative) { return nativeOnChain(chainId) } + const commonBase = chainId + ? COMMON_BASES[chainId]?.find((base) => base.currency.isToken && isSameAddress(base.currency.address, currencyId)) + : undefined + if (commonBase) { + return commonBase.currency + } const { data } = await apolloClient.query({ query: SimpleTokenDocument, variables: { diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/hooks.ts b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/hooks.ts index 9d43256d4b9..21eca429e46 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/hooks.ts +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/hooks.ts @@ -69,7 +69,7 @@ export function useAllActivities(account: string) { const localMap = useLocalActivities(account) const remoteMap = useMemo( () => parseRemoteActivities(activities, account, formatNumberOrString), - [account, activities, formatNumberOrString] + [account, activities, formatNumberOrString], ) const updateCancelledTx = useTransactionCanceller() @@ -103,7 +103,7 @@ export function useOpenLimitOrders(account: string) { activities?.filter( (activity) => activity.offchainOrderDetails?.type === SignatureType.SIGN_LIMIT && - activity.status === TransactionStatus.Pending + activity.status === TransactionStatus.Pending, ) ?? [] return { openLimitOrders, @@ -137,7 +137,7 @@ export function useCancelOrdersGasEstimate(orders?: UniswapXOrderDetails[]): Gas chainId: orders[0].chainId, } : undefined, - [orders] + [orders], ) const cancelTransaction = useCreateCancelTransactionRequest(cancelTransactionParams) const gasEstimate = useTransactionGasFee(cancelTransaction, GasSpeed.Fast) diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/index.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/index.tsx index 08cca223e59..f1ddf89f468 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/index.tsx +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/index.tsx @@ -9,9 +9,9 @@ import { hideSpamAtom } from 'components/AccountDrawer/SpamToggle' import Column from 'components/Column' import { LoadingBubble } from 'components/Tokens/loading' import { useAtomValue, useUpdateAtom } from 'jotai/utils' +import styled from 'lib/styled-components' import { EmptyWalletModule } from 'nft/components/profile/view/EmptyWalletContent' import { useMemo } from 'react' -import styled from 'styled-components' import { ThemedText } from 'theme/components' const ActivityGroupWrapper = styled(Column)` @@ -63,7 +63,7 @@ export function ActivityTab({ account }: { account: string }) { {activityGroup.transactions.map( (activity) => - !(hideSpam && activity.isSpam) && + !(hideSpam && activity.isSpam) && , )} diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/parseLocal.test.ts b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/parseLocal.test.ts index 3f9a04f0117..963943ded32 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/parseLocal.test.ts +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/parseLocal.test.ts @@ -29,7 +29,7 @@ function mockSwapInfo( inputCurrency: Token, inputCurrencyAmountRaw: string, outputCurrency: Token, - outputCurrencyAmountRaw: string + outputCurrencyAmountRaw: string, ): ExactInputSwapTransactionInfo | ExactOutputSwapTransactionInfo { if (type === MockTradeType.EXACT_INPUT) { return { @@ -109,7 +109,7 @@ jest.mock('../../../../state/transactions/hooks', () => { MockUSDC_MAINNET, mockCurrencyAmountRawUSDC, MockDAI, - mockCurrencyAmountRaw + mockCurrencyAmountRaw, ), ...mockCommonFields('0x123', mockAccount1, TransactionStatus.Confirmed), } as TransactionDetails, @@ -121,9 +121,9 @@ jest.mock('../../../../state/transactions/hooks', () => { MockUSDC_MAINNET, mockCurrencyAmountRawUSDC, MockDAI, - mockCurrencyAmountRaw + mockCurrencyAmountRaw, ), - '0xswap_exact_input' + '0xswap_exact_input', ), ...mockMultiStatus( mockSwapInfo( @@ -131,9 +131,9 @@ jest.mock('../../../../state/transactions/hooks', () => { MockUSDC_MAINNET, mockCurrencyAmountRawUSDC, MockDAI, - mockCurrencyAmountRaw + mockCurrencyAmountRaw, ), - '0xswap_exact_output' + '0xswap_exact_output', ), ...mockMultiStatus( { @@ -142,7 +142,7 @@ jest.mock('../../../../state/transactions/hooks', () => { spender: mockSpenderAddress, amount: mockApprovalAmountRaw, }, - '0xapproval' + '0xapproval', ), ...mockMultiStatus( { @@ -151,7 +151,7 @@ jest.mock('../../../../state/transactions/hooks', () => { spender: mockSpenderAddress, amount: '0', }, - '0xrevoke_approval' + '0xrevoke_approval', ), ...mockMultiStatus( { @@ -160,7 +160,7 @@ jest.mock('../../../../state/transactions/hooks', () => { currencyAmountRaw: mockCurrencyAmountRaw, chainId: mockChainId, }, - '0xwrap' + '0xwrap', ), ...mockMultiStatus( { @@ -169,7 +169,7 @@ jest.mock('../../../../state/transactions/hooks', () => { currencyAmountRaw: mockCurrencyAmountRaw, chainId: mockChainId, }, - '0xunwrap' + '0xunwrap', ), ...mockMultiStatus( { @@ -181,7 +181,7 @@ jest.mock('../../../../state/transactions/hooks', () => { expectedAmountBaseRaw: mockCurrencyAmountRawUSDC, expectedAmountQuoteRaw: mockCurrencyAmountRaw, }, - '0xadd_liquidity_v3' + '0xadd_liquidity_v3', ), ...mockMultiStatus( { @@ -191,7 +191,7 @@ jest.mock('../../../../state/transactions/hooks', () => { expectedAmountBaseRaw: mockCurrencyAmountRawUSDC, expectedAmountQuoteRaw: mockCurrencyAmountRaw, }, - '0xremove_liquidity_v3' + '0xremove_liquidity_v3', ), ...mockMultiStatus( { @@ -201,7 +201,7 @@ jest.mock('../../../../state/transactions/hooks', () => { expectedAmountBaseRaw: mockCurrencyAmountRawUSDC, expectedAmountQuoteRaw: mockCurrencyAmountRaw, }, - '0xadd_liquidity_v2' + '0xadd_liquidity_v2', ), ...mockMultiStatus( { @@ -211,7 +211,7 @@ jest.mock('../../../../state/transactions/hooks', () => { expectedCurrencyOwed0: mockCurrencyAmountRawUSDC, expectedCurrencyOwed1: mockCurrencyAmountRaw, }, - '0xcollect_fees' + '0xcollect_fees', ), ...mockMultiStatus( { @@ -220,7 +220,7 @@ jest.mock('../../../../state/transactions/hooks', () => { quoteCurrencyId: MockDAI.address, isFork: false, }, - '0xmigrate_v3_liquidity' + '0xmigrate_v3_liquidity', ), ] }, @@ -237,7 +237,7 @@ describe('parseLocalActivity', () => { MockUSDC_MAINNET, mockCurrencyAmountRawUSDC, MockDAI, - mockCurrencyAmountRaw + mockCurrencyAmountRaw, ), hash: '0x123', status: TransactionStatus.Confirmed, @@ -265,7 +265,7 @@ describe('parseLocalActivity', () => { MockUSDC_MAINNET, mockCurrencyAmountRawUSDC, MockDAI, - mockCurrencyAmountRaw + mockCurrencyAmountRaw, ), hash: '0x123', status: TransactionStatus.Confirmed, @@ -292,7 +292,7 @@ describe('parseLocalActivity', () => { MockUSDC_MAINNET, mockCurrencyAmountRawUSDC, MockDAI, - mockCurrencyAmountRaw + mockCurrencyAmountRaw, ), hash: '0x123', status: TransactionStatus.Confirmed, @@ -575,8 +575,8 @@ describe('parseLocalActivity', () => { type: SignatureType.SIGN_UNISWAPX_ORDER, status: UniswapXOrderStatus.FILLED, } as SignatureDetails, - formatNumber - ) + formatNumber, + ), ).toBeUndefined() }) @@ -594,11 +594,11 @@ describe('parseLocalActivity', () => { MockUSDC_MAINNET, mockCurrencyAmountRawUSDC, MockDAI, - mockCurrencyAmountRaw + mockCurrencyAmountRaw, ), } as SignatureDetails, - formatNumber - ) + formatNumber, + ), ).toEqual({ chainId: 1, currencies: [MockUSDC_MAINNET, MockDAI], diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/parseLocal.ts b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/parseLocal.ts index 29cb6503dc6..3033e42c30e 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/parseLocal.ts +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/parseLocal.ts @@ -45,7 +45,7 @@ function buildCurrencyDescriptor( currencyB: Currency | undefined, amtB: string, formatNumber: FormatNumberFunctionType, - delimiter = t('for') + delimiter = t('for'), ) { const formattedA = currencyA ? formatNumber({ @@ -67,7 +67,7 @@ function buildCurrencyDescriptor( async function parseSwap( swap: ExactInputSwapTransactionInfo | ExactOutputSwapTransactionInfo, chainId: SupportedInterfaceChainId, - formatNumber: FormatNumberFunctionType + formatNumber: FormatNumberFunctionType, ): Promise> { const [tokenIn, tokenOut] = await Promise.all([ getCurrency(swap.inputCurrencyId, chainId), @@ -89,7 +89,7 @@ function parseWrap( wrap: WrapTransactionInfo, chainId: InterfaceChainId, status: TransactionStatus, - formatNumber: FormatNumberFunctionType + formatNumber: FormatNumberFunctionType, ): Partial { const native = nativeOnChain(chainId) const wrapped = native.wrapped @@ -100,7 +100,7 @@ function parseWrap( wrap.currencyAmountRaw, output, wrap.currencyAmountRaw, - formatNumber + formatNumber, ) const title = getActivityTitle(TransactionType.WRAP, status, wrap.unwrapped) const currencies = wrap.unwrapped ? [wrapped, native] : [native, wrapped] @@ -111,7 +111,7 @@ function parseWrap( async function parseApproval( approval: ApproveTransactionInfo, chainId: SupportedInterfaceChainId, - status: TransactionStatus + status: TransactionStatus, ): Promise> { const currency = await getCurrency(approval.tokenAddress, chainId) const descriptor = currency?.symbol ?? currency?.name ?? t('common.unknown') @@ -119,7 +119,7 @@ async function parseApproval( title: getActivityTitle( TransactionType.APPROVAL, status, - BigNumber.from(approval.amount).eq(0) /* use alternate if it's a revoke */ + BigNumber.from(approval.amount).eq(0) /* use alternate if it's a revoke */, ), descriptor, currencies: [currency], @@ -133,7 +133,7 @@ type GenericLPInfo = Omit< async function parseLP( lp: GenericLPInfo, chainId: SupportedInterfaceChainId, - formatNumber: FormatNumberFunctionType + formatNumber: FormatNumberFunctionType, ): Promise> { const [baseCurrency, quoteCurrency] = await Promise.all([ getCurrency(lp.baseCurrencyId, chainId), @@ -146,7 +146,7 @@ async function parseLP( quoteCurrency, quoteRaw, formatNumber, - t('common.and') + t('common.endAdornment'), ) return { descriptor, currencies: [baseCurrency, quoteCurrency] } @@ -155,7 +155,7 @@ async function parseLP( async function parseCollectFees( collect: CollectFeesTransactionInfo, chainId: SupportedInterfaceChainId, - formatNumber: FormatNumberFunctionType + formatNumber: FormatNumberFunctionType, ): Promise> { // Adapts CollectFeesTransactionInfo to generic LP type const { @@ -167,13 +167,13 @@ async function parseCollectFees( return parseLP( { baseCurrencyId, quoteCurrencyId, expectedAmountBaseRaw, expectedAmountQuoteRaw }, chainId, - formatNumber + formatNumber, ) } async function parseMigrateCreateV3( lp: MigrateV2LiquidityToV3TransactionInfo | CreateV3PoolTransactionInfo, - chainId: SupportedInterfaceChainId + chainId: SupportedInterfaceChainId, ): Promise> { const [baseCurrency, quoteCurrency] = await Promise.all([ getCurrency(lp.baseCurrencyId, chainId), @@ -189,7 +189,7 @@ async function parseMigrateCreateV3( async function parseSend( send: SendTransactionInfo, chainId: SupportedInterfaceChainId, - formatNumber: FormatNumberFunctionType + formatNumber: FormatNumberFunctionType, ): Promise> { const { currencyId, amount, recipient } = send const currency = await getCurrency(currencyId, chainId) @@ -210,7 +210,7 @@ async function parseSend( export async function transactionToActivity( details: TransactionDetails | undefined, chainId: SupportedInterfaceChainId, - formatNumber: FormatNumberFunctionType + formatNumber: FormatNumberFunctionType, ): Promise { if (!details) { return undefined @@ -266,7 +266,7 @@ export async function transactionToActivity( export function getTransactionToActivityQueryOptions( transaction: TransactionDetails | undefined, chainId: SupportedInterfaceChainId, - formatNumber: FormatNumberFunctionType + formatNumber: FormatNumberFunctionType, ) { return queryOptions({ queryKey: ['transactionToActivity', transaction, chainId], @@ -276,7 +276,7 @@ export function getTransactionToActivityQueryOptions( export function getSignatureToActivityQueryOptions( signature: SignatureDetails | undefined, - formatNumber: FormatNumberFunctionType + formatNumber: FormatNumberFunctionType, ) { return queryOptions({ queryKey: ['signatureToActivity', signature], @@ -296,7 +296,7 @@ function convertToSecTimestamp(timestamp: number) { export async function signatureToActivity( signature: SignatureDetails | undefined, - formatNumber: FormatNumberFunctionType + formatNumber: FormatNumberFunctionType, ): Promise { if (!signature) { return undefined diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/parseRemote.test.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/parseRemote.test.tsx index 91ecd7991f2..129bf90e388 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/parseRemote.test.tsx +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/parseRemote.test.tsx @@ -181,7 +181,7 @@ describe('parseRemote', () => { ...swapOrderTokenChanges, TokenTransfer: [mockTokenTransferOutPartsFragment], }, - jest.fn().mockReturnValue('100') + jest.fn().mockReturnValue('100'), ) expect(result).toEqual(undefined) }) @@ -195,7 +195,7 @@ describe('parseRemote', () => { ...swapOrderTokenChanges, TokenTransfer: [], }, // no token changes - jest.fn().mockReturnValue('100') + jest.fn().mockReturnValue('100'), ) expect(result).toEqual(undefined) }) @@ -204,7 +204,7 @@ describe('parseRemote', () => { const result = offchainOrderDetailsFromGraphQLTransactionActivity( { ...MockSwapOrder, details: { ...mockTransactionDetailsPartsFragment, __typename: 'TransactionDetails' } }, swapOrderTokenChanges, - jest.fn().mockReturnValue('100') + jest.fn().mockReturnValue('100'), ) expect(result).toEqual({ chainId: 1, diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/parseRemote.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/parseRemote.tsx index ee7a82408e3..1f0a8c0e7ac 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/parseRemote.tsx +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/parseRemote.tsx @@ -24,6 +24,8 @@ import { NftApprovalPartsFragment, NftApproveForAllPartsFragment, NftTransferPartsFragment, + OnRampTransactionDetailsPartsFragment, + OnRampTransferPartsFragment, TokenApprovalPartsFragment, TokenAssetPartsFragment, TokenTransferPartsFragment, @@ -90,7 +92,7 @@ const SPAMMABLE_ACTIVITY_TYPES = [TransactionType.Receive, TransactionType.Mint, function isSpam( { NftTransfer, TokenTransfer }: TransactionChanges, details: TransactionDetailsPartsFragment, - account: string + account: string, ): boolean { if (!SPAMMABLE_ACTIVITY_TYPES.includes(details.type) || details.from === account) { return false @@ -108,13 +110,16 @@ function callsPositionManagerContract(assetActivity: TransactionActivity) { // Gets counts for number of NFTs in each collection present function getCollectionCounts(nftTransfers: NftTransferPartsFragment[]): { [key: string]: number | undefined } { - return nftTransfers.reduce((acc, NFTChange) => { - const key = NFTChange.asset.collection?.name ?? NFTChange.asset.name - if (key) { - acc[key] = (acc?.[key] ?? 0) + 1 - } - return acc - }, {} as { [key: string]: number | undefined }) + return nftTransfers.reduce( + (acc, NFTChange) => { + const key = NFTChange.asset.collection?.name ?? NFTChange.asset.name + if (key) { + acc[key] = (acc?.[key] ?? 0) + 1 + } + return acc + }, + {} as { [key: string]: number | undefined }, + ) } function getSwapTitle(sent: TokenTransferPartsFragment, received: TokenTransferPartsFragment): string | undefined { @@ -187,12 +192,12 @@ type SwapAmounts = { // eslint-disable-next-line import/no-unused-modules export function parseSwapAmounts( changes: TransactionChanges, - formatNumberOrString: FormatNumberOrStringFunctionType + formatNumberOrString: FormatNumberOrStringFunctionType, ): SwapAmounts | undefined { const sent = changes.TokenTransfer.find((t) => t.direction === 'OUT') // Any leftover native token is refunded on exact_out swaps where the input token is native const refund = changes.TokenTransfer.find( - (t) => t.direction === 'IN' && t.asset.id === sent?.asset.id && t.asset.standard === NATIVE_CHAIN_ID + (t) => t.direction === 'IN' && t.asset.id === sent?.asset.id && t.asset.standard === NATIVE_CHAIN_ID, ) const received = changes.TokenTransfer.find((t) => t.direction === 'IN' && t !== refund) if (!sent || !received) { @@ -271,12 +276,12 @@ function parseLend(changes: TransactionChanges, formatNumberOrString: FormatNumb function parseSwapOrder( changes: TransactionChanges, formatNumberOrString: FormatNumberOrStringFunctionType, - assetActivity: TransactionActivity + assetActivity: TransactionActivity, ) { const offchainOrderDetails = offchainOrderDetailsFromGraphQLTransactionActivity( assetActivity, changes, - formatNumberOrString + formatNumberOrString, ) return { ...parseSwap(changes, formatNumberOrString), @@ -288,7 +293,7 @@ function parseSwapOrder( export function offchainOrderDetailsFromGraphQLTransactionActivity( activity: AssetActivityPartsFragment & { details: TransactionDetailsPartsFragment }, changes: TransactionChanges, - formatNumberOrString: FormatNumberOrStringFunctionType + formatNumberOrString: FormatNumberOrStringFunctionType, ): UniswapXOrderDetails | undefined { const chainId = supportedChainIdFromGQLChain(activity.chain) if (!activity || !activity.details || !chainId) { @@ -353,11 +358,12 @@ function parseLPTransfers(changes: TransactionChanges, formatNumberOrString: For } type TransactionActivity = AssetActivityPartsFragment & { details: TransactionDetailsPartsFragment } +type FiatOnRampActivity = AssetActivityPartsFragment & { details: OnRampTransactionDetailsPartsFragment } function parseSendReceive( changes: TransactionChanges, formatNumberOrString: FormatNumberOrStringFunctionType, - assetActivity: TransactionActivity + assetActivity: TransactionActivity, ) { // TODO(cartcrom): remove edge cases after backend implements // Edge case: Receiving two token transfers in interaction w/ V3 manager === removing liquidity. These edge cases should potentially be moved to backend @@ -415,7 +421,7 @@ function parseSendReceive( function parseMint( changes: TransactionChanges, formatNumberOrString: FormatNumberOrStringFunctionType, - assetActivity: TransactionActivity + assetActivity: TransactionActivity, ) { const collectionMap = getCollectionCounts(changes.NftTransfer) if (Object.keys(collectionMap).length === 1) { @@ -433,7 +439,7 @@ function parseMint( function parseUnknown( _changes: TransactionChanges, _formatNumberOrString: FormatNumberOrStringFunctionType, - assetActivity: TransactionActivity + assetActivity: TransactionActivity, ) { return { title: t('common.contractInteraction'), ...COMMON_CONTRACTS[assetActivity.details.to.toLowerCase()] } } @@ -441,7 +447,7 @@ function parseUnknown( type TransactionTypeParser = ( changes: TransactionChanges, formatNumberOrString: FormatNumberOrStringFunctionType, - assetActivity: TransactionActivity + assetActivity: TransactionActivity, ) => Partial const ActivityParserByType: { [key: string]: TransactionTypeParser | undefined } = { [TransactionType.Swap]: parseSwap, @@ -500,10 +506,87 @@ function parseUniswapXOrder(activity: OrderActivity): Activity | undefined { } } +function parseFiatOnRampTransaction(activity: TransactionActivity | FiatOnRampActivity): Activity { + const chainId = supportedChainIdFromGQLChain(activity.chain) + if (!chainId) { + const error = new Error('Invalid activity from unsupported chain received from GQL') + logger.error(error, { + tags: { + file: 'parseRemote', + function: 'parseRemote', + }, + extra: { activity }, + }) + throw error + } + if (activity.details.__typename === 'OnRampTransactionDetails') { + const onRampTransfer = activity.details.onRampTransfer + return { + from: activity.details.receiverAddress, + hash: activity.id, + chainId, + timestamp: activity.timestamp, + logos: [onRampTransfer.token.project?.logo?.url], + currencies: [gqlToCurrency(onRampTransfer.token)], + title: t('fiatOnRamp.purchasedOn', { + serviceProvider: onRampTransfer.serviceProvider.name, + }), + descriptor: t('fiatOnRamp.exchangeRate', { + outputAmount: onRampTransfer.amount, + outputSymbol: onRampTransfer.token.symbol, + inputAmount: onRampTransfer.sourceAmount, + inputSymbol: onRampTransfer.sourceCurrency, + }), + suffixIconSrc: onRampTransfer.serviceProvider.logoDarkUrl, + status: activity.details.status, + } + } else if (activity.details.__typename === 'TransactionDetails') { + const assetChange = activity.details.assetChanges[0] + if (assetChange?.__typename !== 'OnRampTransfer') { + logger.error('Unexpected asset change type, expected OnRampTransfer', { + tags: { + file: 'parseRemote', + function: 'parseRemote', + }, + }) + } + const onRampTransfer = assetChange as OnRampTransferPartsFragment + return { + from: activity.details.from, + hash: activity.details.hash, + chainId, + timestamp: activity.timestamp, + logos: [onRampTransfer.token.project?.logo?.url], + currencies: [gqlToCurrency(onRampTransfer.token)], + title: t('fiatOnRamp.purchasedOn', { + serviceProvider: onRampTransfer.serviceProvider.name, + }), + descriptor: t('fiatOnRamp.exchangeRate', { + outputAmount: onRampTransfer.amount, + outputSymbol: onRampTransfer.token.symbol, + inputAmount: onRampTransfer.sourceAmount, + inputSymbol: onRampTransfer.sourceCurrency, + }), + suffixIconSrc: onRampTransfer.serviceProvider.logoDarkUrl, + status: activity.details.status, + } + } else { + const error = new Error('Invalid Fiat On Ramp activity type received from GQL') + logger.error(error, { + tags: { + file: 'parseRemote', + function: 'parseFiatOnRampTransaction', + }, + extra: { activity }, + }) + throw error + } +} + function parseRemoteActivity( assetActivity: AssetActivityPartsFragment | undefined, account: string, - formatNumberOrString: FormatNumberOrStringFunctionType + formatNumberOrString: FormatNumberOrStringFunctionType, ): Activity | undefined { try { if (!assetActivity) { @@ -515,8 +598,12 @@ function parseRemoteActivity( return parseUniswapXOrder(assetActivity as OrderActivity) } - if (assetActivity.details.__typename === 'OnRampTransactionDetails') { - return undefined // TODO(WEB-4187): support onramp transactions + if ( + assetActivity.details.__typename === 'OnRampTransactionDetails' || + (assetActivity.details.__typename === 'TransactionDetails' && + assetActivity.details.type === TransactionType.OnRamp) + ) { + return parseFiatOnRampTransaction(assetActivity as TransactionActivity) } const changes = assetActivity.details.assetChanges.reduce( @@ -535,7 +622,7 @@ function parseRemoteActivity( return acc }, - { NftTransfer: [], TokenTransfer: [], TokenApproval: [], NftApproval: [], NftApproveForAll: [] } + { NftTransfer: [], TokenTransfer: [], TokenApproval: [], NftApproval: [], NftApproveForAll: [] }, ) const supportedChain = supportedChainIdFromGQLChain(assetActivity.chain) @@ -566,7 +653,7 @@ function parseRemoteActivity( const parsedFields = ActivityParserByType[assetActivity.details.type]?.( changes, formatNumberOrString, - assetActivity as TransactionActivity + assetActivity as TransactionActivity, ) return { ...defaultFields, ...parsedFields } } catch (e) { @@ -581,7 +668,7 @@ function parseRemoteActivity( export function parseRemoteActivities( assetActivities: (AssetActivityPartsFragment | undefined)[] | undefined, account: string, - formatNumberOrString: FormatNumberOrStringFunctionType + formatNumberOrString: FormatNumberOrStringFunctionType, ) { return assetActivities?.reduce((acc: { [hash: string]: Activity }, assetActivity) => { const activity = parseRemoteActivity(assetActivity, account, formatNumberOrString) diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/types.ts b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/types.ts index 1c709ea36a3..60d8ec7aa32 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/types.ts +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/types.ts @@ -23,6 +23,7 @@ export type Activity = { from: string nonce?: number | null prefixIconSrc?: string + suffixIconSrc?: string cancelled?: boolean isSpam?: boolean } diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/utils.test.ts b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/utils.test.ts index e612bb1cfb1..7183d388e41 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/utils.test.ts +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/utils.test.ts @@ -23,7 +23,7 @@ describe('createGroups', () => { transactions: expect.arrayContaining([ expect.objectContaining({ timestamp: expect.any(Number), status: TransactionStatus.Confirmed }), ]), - }) + }), ) expect(createGroups(mockActivities, true)).toEqual([]) }) @@ -43,7 +43,7 @@ describe('createGroups', () => { transactions: expect.arrayContaining([ expect.objectContaining({ timestamp: 1700000000, status: TransactionStatus.Pending }), ]), - }) + }), ) expect(result).toContainEqual( @@ -52,7 +52,7 @@ describe('createGroups', () => { transactions: expect.arrayContaining([ expect.objectContaining({ timestamp: expect.any(Number), status: TransactionStatus.Confirmed }), ]), - }) + }), ) }) }) diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/utils.ts b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/utils.ts index 25df5acf9d2..2fd81d01218 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/utils.ts +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/utils.ts @@ -94,20 +94,20 @@ export const createGroups = (activities: Array = [], hideSpam = false) function getCancelMultipleUniswapXOrdersParams( orders: Array<{ encodedOrder: string; type: SignatureType }>, - chainId: InterfaceChainId + chainId: InterfaceChainId, ) { const nonces = orders .map(({ encodedOrder, type }) => type === SignatureType.SIGN_UNISWAPX_V2_ORDER ? CosignedV2DutchOrder.parse(encodedOrder, chainId) - : DutchOrder.parse(encodedOrder, chainId) + : DutchOrder.parse(encodedOrder, chainId), ) .map((order) => order.info.nonce) return getCancelMultipleParams(nonces) } export function useCancelMultipleOrdersCallback( - orders?: Array + orders?: Array, ): () => Promise { const provider = useEthersWeb3Provider() const permit2 = useContract(permit2Address(orders?.[0]?.chainId), PERMIT2_ABI, true) @@ -173,7 +173,7 @@ async function cancelMultipleUniswapXOrders({ async function getCancelMultipleUniswapXOrdersTransaction( orders: Array<{ encodedOrder: string; type: SignatureType }>, chainId: InterfaceChainId, - permit2: Permit2 + permit2: Permit2, ): Promise { const cancelParams = getCancelMultipleUniswapXOrdersParams(orders, chainId) if (!permit2 || cancelParams.length === 0) { @@ -201,7 +201,7 @@ export function useCreateCancelTransactionRequest( orders: Array<{ encodedOrder: string; type: SignatureType }> chainId: InterfaceChainId } - | undefined + | undefined, ): TransactionRequest | undefined { const permit2 = useContract(permit2Address(params?.chainId), PERMIT2_ABI, true) const transactionFetcher = useCallback(() => { diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/EmptyWallet.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/EmptyWallet.tsx new file mode 100644 index 00000000000..1edf1419eee --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/EmptyWallet.tsx @@ -0,0 +1,87 @@ +import { Trans, t } from 'i18n' +import styled from 'lib/styled-components' +import { useCallback, useMemo } from 'react' +import { Flex, Text, useIsDarkMode } from 'ui/src' +import { CRYPTO_PURCHASE_BACKGROUND_DARK, CRYPTO_PURCHASE_BACKGROUND_LIGHT } from 'ui/src/assets' +import { ArrowDownCircle, Buy as BuyIcon } from 'ui/src/components/icons' +import { ActionCard, ActionCardItem } from 'uniswap/src/components/misc/ActionCard' +import { ElementName } from 'uniswap/src/features/telemetry/constants' + +export const EmptyWallet = ({ + handleBuyCryptoClick, + handleReceiveCryptoClick, +}: { + handleBuyCryptoClick: () => void + handleReceiveCryptoClick: () => void +}) => { + const isDarkMode = useIsDarkMode() + + const BackgroundImageWrapperCallback = useCallback( + ({ children }: { children: React.ReactNode }) => { + return ( + + {children} + + ) + }, + [isDarkMode], + ) + + const options: ActionCardItem[] = useMemo( + () => [ + { + title: t('home.tokens.empty.action.buy.title'), + blurb: t('home.tokens.empty.action.buy.description'), + elementName: ElementName.EmptyStateBuy, + icon: , + onPress: handleBuyCryptoClick, + BackgroundImageWrapperCallback, + }, + { + title: t('fiatOnRamp.receiveCrypto.title'), + blurb: t('fiatOnRamp.receiveCrypto.transferFunds'), + elementName: ElementName.EmptyStateReceive, + icon: , + onPress: handleReceiveCryptoClick, + }, + ], + [BackgroundImageWrapperCallback, handleBuyCryptoClick, handleReceiveCryptoClick], + ) + + return ( + + + + + + + + + + + {options.map((option) => ( + + ))} + + + ) +} + +const StyledBackgroundImage = styled.img` + width: 100%; + border-radius: 24px; + position: absolute; + z-index: -1; + height: 100%; + object-fit: cover; + filter: blur(2px); +` + +const BackgroundImage = ({ children, image }: { children: React.ReactNode; image: string }): JSX.Element => { + return ( + + + {children} + + ) +} diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/ExpandoRow.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/ExpandoRow.tsx index 4286a0317a2..691556bd2b7 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/ExpandoRow.tsx +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/ExpandoRow.tsx @@ -1,9 +1,9 @@ import Column from 'components/Column' import Row from 'components/Row' import { t } from 'i18n' +import styled from 'lib/styled-components' import { PropsWithChildren } from 'react' import { ChevronDown } from 'react-feather' -import styled from 'styled-components' import { ThemedText } from 'theme/components' const ExpandIcon = styled(ChevronDown)<{ $expanded: boolean }>` diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/ExtensionDeeplinks.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/ExtensionDeeplinks.tsx new file mode 100644 index 00000000000..192720fc63b --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/ExtensionDeeplinks.tsx @@ -0,0 +1,113 @@ +import { MenuState, miniPortfolioMenuStateAtom } from 'components/AccountDrawer/DefaultMenu' +import { useOpenLimitOrders, usePendingActivity } from 'components/AccountDrawer/MiniPortfolio/Activity/hooks' +import { useFilterPossiblyMaliciousPositionInfo } from 'components/AccountDrawer/MiniPortfolio/Pools' +import useMultiChainPositions from 'components/AccountDrawer/MiniPortfolio/Pools/useMultiChainPositions' +import { useAccountDrawer } from 'components/AccountDrawer/MiniPortfolio/hooks' +import { Pool } from 'components/Icons/Pool' +import { ExtensionRequestMethods, useUniswapExtensionConnector } from 'components/WalletModal/useOrderedConnections' +import { t } from 'i18n' +import { useUpdateAtom } from 'jotai/utils' +import { useTheme } from 'lib/styled-components' +import { useEffect, useState } from 'react' +import { Button, Flex, Image, Text } from 'ui/src' +import { UNISWAP_LOGO } from 'ui/src/assets' +import { ArrowRightToLine, RotatableChevron, TimePast } from 'ui/src/components/icons' +import { iconSizes } from 'ui/src/theme/iconSizes' + +const UnreadIndicator = () => { + const theme = useTheme() + + return ( + + + + + + ) +} + +const DeepLinkButton = ({ Icon, Label, onPress }: { Icon: JSX.Element; Label: string; onPress: () => void }) => { + return ( + + ) +} + +export function ExtensionDeeplinks({ account }: { account: string }) { + const theme = useTheme() + const uniswapExtensionConnector = useUniswapExtensionConnector() + const accountDrawer = useAccountDrawer() + const setMenu = useUpdateAtom(miniPortfolioMenuStateAtom) + const { openLimitOrders } = useOpenLimitOrders(account) + + const [activityUnread, setActivityUnread] = useState(false) + const { hasPendingActivity } = usePendingActivity() + useEffect(() => { + if (hasPendingActivity) { + setActivityUnread(true) + } + }, [hasPendingActivity]) + + const { positions } = useMultiChainPositions(account) + const filteredPositions = useFilterPossiblyMaliciousPositionInfo(positions) + + if (!uniswapExtensionConnector) { + return null + } + + return ( + + } + Label={t('extension.open')} + onPress={() => { + uniswapExtensionConnector.extensionRequest(ExtensionRequestMethods.OPEN_SIDEBAR, 'Tokens') + accountDrawer.close() + }} + /> + + + {activityUnread && } + + } + Label={t('common.activity')} + onPress={() => { + uniswapExtensionConnector.extensionRequest(ExtensionRequestMethods.OPEN_SIDEBAR, 'Activity') + accountDrawer.close() + setActivityUnread(false) + }} + /> + {filteredPositions.length > 0 && ( + } + Label={t('common.pools')} + onPress={() => setMenu(MenuState.POOLS)} + /> + )} + {openLimitOrders.length > 0 && ( + } + Label={t('common.limits')} + onPress={() => setMenu(MenuState.LIMITS)} + /> + )} + + ) +} diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/LimitDetailActivityRow.test.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/LimitDetailActivityRow.test.tsx index eb02d439866..f3e93e9ed1c 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/LimitDetailActivityRow.test.tsx +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/LimitDetailActivityRow.test.tsx @@ -58,7 +58,7 @@ describe('LimitDetailActivityRow', () => { order={{ ...mockOrder, offchainOrderDetails: undefined }} onToggleSelect={jest.fn()} selected={false} - /> + />, ) expect(container.firstChild?.firstChild?.firstChild).toBeNull() }) @@ -69,14 +69,14 @@ describe('LimitDetailActivityRow', () => { onToggleSelect={jest.fn()} selected={false} order={{ ...mockOrder, offchainOrderDetails: { ...mockOrderDetails, swapInfo: undefined as any } }} - /> + />, ) expect(container.firstChild?.firstChild?.firstChild).toBeNull() }) it('should render with valid details', () => { const { container } = render( - + , ) expect(container.firstChild).toMatchSnapshot() expect(screen.getByText('when 0.00042 WETH/DAI')).toBeInTheDocument() diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/LimitDetailActivityRow.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/LimitDetailActivityRow.tsx index 6d837625863..103c2f58e98 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/LimitDetailActivityRow.tsx +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/LimitDetailActivityRow.tsx @@ -12,10 +12,10 @@ import { parseUnits } from 'ethers/lib/utils' import { useCurrencyInfo } from 'hooks/Tokens' import { useScreenSize } from 'hooks/screenSize' import { Trans } from 'i18n' +import styled, { useTheme } from 'lib/styled-components' import { Checkbox } from 'nft/components/layout/Checkbox' import { useMemo, useState } from 'react' import { ArrowRight } from 'react-feather' -import styled, { useTheme } from 'styled-components' import { EllipsisStyle, ThemedText } from 'theme/components' import { UniswapXOrderStatus } from 'types/uniswapx' import { useFormatter } from 'utils/formatNumbers' @@ -70,8 +70,8 @@ export function LimitDetailActivityRow({ order, onToggleSelect, selected }: Limi return tradePrice.quote( CurrencyAmount.fromRawAmount( amounts.inputAmount.currency, - parseUnits('1', amounts.inputAmount.currency.decimals).toString() - ) + parseUnits('1', amounts.inputAmount.currency.decimals).toString(), + ), ) }, [amounts?.inputAmount, amounts?.outputAmount, amountsDefined]) diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/LimitsMenu.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/LimitsMenu.tsx index 3014f3d8387..06dbf0679ba 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/LimitsMenu.tsx +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/LimitsMenu.tsx @@ -11,9 +11,9 @@ import { ButtonEmphasis, ButtonSize, ThemeButton } from 'components/Button' import Column from 'components/Column' import { LimitDisclaimer } from 'components/swap/LimitDisclaimer' import { Plural, Trans, t } from 'i18n' +import styled from 'lib/styled-components' import { useMemo, useState } from 'react' import { UniswapXOrderDetails } from 'state/signatures/types' -import styled from 'styled-components' import { UniswapXOrderStatus } from 'types/uniswapx' const Container = styled(Column)` diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/OpenLimitOrdersButton.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/OpenLimitOrdersButton.tsx index 0602ba2d482..7e36e0aaebe 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/OpenLimitOrdersButton.tsx +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/OpenLimitOrdersButton.tsx @@ -1,20 +1,8 @@ import { useOpenLimitOrders } from 'components/AccountDrawer/MiniPortfolio/Activity/hooks' -import Column from 'components/Column' -import { TimeForwardIcon } from 'components/Icons/TimeForward' -import Row from 'components/Row' +import { TabButton } from 'components/AccountDrawer/MiniPortfolio/shared' import { Plural, Trans, t } from 'i18n' -import { ChevronRight } from 'react-feather' -import styled, { useTheme } from 'styled-components' -import { ClickableStyle, ThemedText } from 'theme/components' - -const Container = styled.button` - border-radius: 16px; - border: none; - background: ${({ theme }) => theme.surface2}; - padding: 12px 16px; - margin-top: 12px; - ${ClickableStyle} -` +import { useTheme } from 'lib/styled-components' +import { Clock } from 'react-feather' function getExtraWarning(openLimitOrders: any[]) { if (openLimitOrders.length >= 100) { @@ -40,27 +28,25 @@ export function OpenLimitOrdersButton({ const { openLimitOrders } = useOpenLimitOrders(account) const theme = useTheme() const extraWarning = getExtraWarning(openLimitOrders) + if (!openLimitOrders || openLimitOrders.length < 1) { return null } + return ( - - - - - - - - - {extraWarning && {extraWarning}} - - - - - + + } + icon={} + extraWarning={extraWarning} + onClick={openLimitsMenu} + disabled={disabled} + className={className} + /> ) } diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/__snapshots__/LimitDetailActivityRow.test.tsx.snap b/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/__snapshots__/LimitDetailActivityRow.test.tsx.snap index d3f174be935..0d27cc71a9e 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/__snapshots__/LimitDetailActivityRow.test.tsx.snap +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/__snapshots__/LimitDetailActivityRow.test.tsx.snap @@ -1,6 +1,29 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`LimitDetailActivityRow should render with valid details 1`] = ` +.c6 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c4 { + display: grid; + grid-auto-rows: auto; + -webkit-box-flex: 1; + -webkit-flex-grow: 1; + -ms-flex-positive: 1; + flex-grow: 1; +} + .c0 { box-sizing: border-box; margin: 0; @@ -65,29 +88,6 @@ exports[`LimitDetailActivityRow should render with valid details 1`] = ` letter-spacing: -0.01em; } -.c6 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - -webkit-box-pack: start; - -webkit-justify-content: flex-start; - -ms-flex-pack: start; - justify-content: flex-start; -} - -.c4 { - display: grid; - grid-auto-rows: auto; - -webkit-box-flex: 1; - -webkit-flex-grow: 1; - -ms-flex-positive: 1; - flex-grow: 1; -} - .c2 { gap: 12px; height: 68px; diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/__snapshots__/LimitsMenu.test.tsx.snap b/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/__snapshots__/LimitsMenu.test.tsx.snap index 61a187120b3..b029f5fc5af 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/__snapshots__/LimitsMenu.test.tsx.snap +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/__snapshots__/LimitsMenu.test.tsx.snap @@ -1,6 +1,44 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`LimitsMenu should render when there are two open orders 1`] = ` +.c0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c6 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 8px; +} + +.c17 { + display: grid; + grid-auto-rows: auto; + -webkit-box-flex: 1; + -webkit-flex-grow: 1; + -ms-flex-positive: 1; + flex-grow: 1; +} + .c13 { box-sizing: border-box; margin: 0; @@ -84,44 +122,6 @@ exports[`LimitsMenu should render when there are two open orders 1`] = ` opacity: 0.4; } -.c0 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - -webkit-box-pack: start; - -webkit-justify-content: flex-start; - -ms-flex-pack: start; - justify-content: flex-start; -} - -.c6 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - -webkit-box-pack: start; - -webkit-justify-content: flex-start; - -ms-flex-pack: start; - justify-content: flex-start; - gap: 8px; -} - -.c17 { - display: grid; - grid-auto-rows: auto; - -webkit-box-flex: 1; - -webkit-flex-grow: 1; - -ms-flex-positive: 1; - flex-grow: 1; -} - .c7 { background-color: #F9F9F9; border-radius: 12px; @@ -588,6 +588,44 @@ exports[`LimitsMenu should render when there are two open orders 1`] = ` `; exports[`LimitsMenu should render when there is one open order 1`] = ` +.c0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c6 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 8px; +} + +.c17 { + display: grid; + grid-auto-rows: auto; + -webkit-box-flex: 1; + -webkit-flex-grow: 1; + -ms-flex-positive: 1; + flex-grow: 1; +} + .c13 { box-sizing: border-box; margin: 0; @@ -671,44 +709,6 @@ exports[`LimitsMenu should render when there is one open order 1`] = ` opacity: 0.4; } -.c0 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - -webkit-box-pack: start; - -webkit-justify-content: flex-start; - -ms-flex-pack: start; - justify-content: flex-start; -} - -.c6 { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - -webkit-box-pack: start; - -webkit-justify-content: flex-start; - -ms-flex-pack: start; - justify-content: flex-start; - gap: 8px; -} - -.c17 { - display: grid; - grid-auto-rows: auto; - -webkit-box-flex: 1; - -webkit-flex-grow: 1; - -ms-flex-positive: 1; - flex-grow: 1; -} - .c7 { background-color: #F9F9F9; border-radius: 12px; diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/__snapshots__/OpenLimitOrdersButton.test.tsx.snap b/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/__snapshots__/OpenLimitOrdersButton.test.tsx.snap index 2980208014c..da72cda05b3 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/__snapshots__/OpenLimitOrdersButton.test.tsx.snap +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/__snapshots__/OpenLimitOrdersButton.test.tsx.snap @@ -56,14 +56,6 @@ exports[`OpenLimitOrdersButton should render if there are open limit orders 1`] gap: 12px; } -.c5 { - color: #222222; - -webkit-letter-spacing: -0.01em; - -moz-letter-spacing: -0.01em; - -ms-letter-spacing: -0.01em; - letter-spacing: -0.01em; -} - .c0 { border-radius: 16px; border: none; @@ -102,40 +94,55 @@ exports[`OpenLimitOrdersButton should render if there are open limit orders 1`] class="c1 c3" > - +
        -
        1 open limit -
        +
        +
        diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/NFTs/NFTItem.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/NFTs/NFTItem.tsx index a078d560417..3787f70b3e8 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/NFTs/NFTItem.tsx +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/NFTs/NFTItem.tsx @@ -2,14 +2,18 @@ import { InterfaceElementName, SharedEventName } from '@uniswap/analytics-events import { useAccountDrawer } from 'components/AccountDrawer/MiniPortfolio/hooks' import Column from 'components/Column' import Row from 'components/Row' +import { MouseFollowTooltip, TooltipSize } from 'components/Tooltip' +import { t } from 'i18next' +import styled from 'lib/styled-components' import { Box } from 'nft/components/Box' import { NftCard } from 'nft/components/card' import { detailsHref } from 'nft/components/card/utils' import { VerifiedIcon } from 'nft/components/icons' import { WalletAsset } from 'nft/types' import { useNavigate } from 'react-router-dom' -import styled from 'styled-components' import { ThemedText } from 'theme/components' +import { capitalize } from 'tsafe' +import { Chain } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { useTrace } from 'utilities/src/telemetry/trace/TraceContext' import { NumberType, useFormatter } from 'utils/formatNumbers' @@ -52,32 +56,44 @@ export function NFT({ const trace = useTrace() const navigateToNFTDetails = () => { - accountDrawer.close() - navigate(detailsHref(asset)) + if (asset.chain === Chain.Ethereum) { + accountDrawer.close() + navigate(detailsHref(asset)) + } } return ( - - sendAnalyticsEvent(SharedEventName.ELEMENT_CLICKED, { - element: InterfaceElementName.MINI_PORTFOLIO_NFT_ITEM, - collection_name: asset.collection?.name, - collection_address: asset.collection?.address, - token_id: asset.tokenId, - ...trace, - }) - } - mediaShouldBePlaying={mediaShouldBePlaying} - setCurrentTokenPlayingMedia={setCurrentTokenPlayingMedia} - testId="mini-portfolio-nft" - /> + + + sendAnalyticsEvent(SharedEventName.ELEMENT_CLICKED, { + element: InterfaceElementName.MINI_PORTFOLIO_NFT_ITEM, + collection_name: asset.collection?.name, + collection_address: asset.collection?.address, + token_id: asset.tokenId, + ...trace, + }) + } + mediaShouldBePlaying={mediaShouldBePlaying} + setCurrentTokenPlayingMedia={setCurrentTokenPlayingMedia} + testId="mini-portfolio-nft" + /> + ) diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/NFTs/index.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/NFTs/index.tsx index ebf0dd383e3..e23566da226 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/NFTs/index.tsx +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/NFTs/index.tsx @@ -1,28 +1,53 @@ import { NFT } from 'components/AccountDrawer/MiniPortfolio/NFTs/NFTItem' import { DEFAULT_NFT_QUERY_AMOUNT } from 'components/AccountDrawer/MiniPortfolio/constants' import { useAccountDrawer } from 'components/AccountDrawer/MiniPortfolio/hooks' +import { TabButton } from 'components/AccountDrawer/MiniPortfolio/shared' import { useNftBalance } from 'graphql/data/nft/NftBalance' +import { t } from 'i18n' +import styled from 'lib/styled-components' import { LoadingAssets } from 'nft/components/collection/CollectionAssetLoading' import { EmptyWalletModule } from 'nft/components/profile/view/EmptyWalletContent' -import { useState } from 'react' +import { useProfilePageState, useSellAsset, useWalletCollections } from 'nft/hooks' +import { ProfilePageStateType } from 'nft/types' +import { useCallback, useState } from 'react' import InfiniteScroll from 'react-infinite-scroll-component' -import styled from 'styled-components' +import { useNavigate } from 'react-router-dom' +import { Gallery } from 'ui/src/components/icons' +import { Chain } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { FeatureFlags } from 'uniswap/src/features/gating/flags' +import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' + +const StyledTabButton = styled(TabButton)` + width: calc(100% - 32px); + margin: 0 16px -4px; +` export default function NFTs({ account }: { account: string }) { + const forAggregatorEnabled = useFeatureFlag(FeatureFlags.ForAggregatorWeb) const accountDrawer = useAccountDrawer() - const { walletAssets, loading, hasNext, loadMore } = useNftBalance( - account, - [], - [], - DEFAULT_NFT_QUERY_AMOUNT, - undefined, - undefined, - undefined, - !accountDrawer.isOpen - ) + const navigate = useNavigate() + const setSellPageState = useProfilePageState((state) => state.setProfilePageState) + const resetSellAssets = useSellAsset((state) => state.reset) + const clearCollectionFilters = useWalletCollections((state) => state.clearCollectionFilters) + + const isL2NFTsEnabled = useFeatureFlag(FeatureFlags.L2NFTs) + const { walletAssets, loading, hasNext, loadMore } = useNftBalance({ + ownerAddress: account, + first: DEFAULT_NFT_QUERY_AMOUNT, + skip: !accountDrawer.isOpen, + chains: isL2NFTsEnabled ? [Chain.Ethereum, Chain.Zora] : undefined, + }) const [currentTokenPlayingMedia, setCurrentTokenPlayingMedia] = useState() + const navigateToProfile = useCallback(() => { + accountDrawer.close() + resetSellAssets() + setSellPageState(ProfilePageStateType.VIEWING) + clearCollectionFilters() + navigate('/nfts/profile') + }, [clearCollectionFilters, navigate, resetSellAssets, setSellPageState, accountDrawer]) + if (loading && !walletAssets) { return ( @@ -36,35 +61,44 @@ export default function NFTs({ account }: { account: string }) { } return ( - - - - ) - } - dataLength={walletAssets?.length ?? 0} - style={{ overflow: 'unset' }} - scrollableTarget="wallet-dropdown-scroll-wrapper" - > - - {walletAssets?.length - ? walletAssets.map((asset, index) => { - return ( - - ) - }) - : null} - - + <> + {forAggregatorEnabled && ( + } + onClick={navigateToProfile} + /> + )} + + + + ) + } + dataLength={walletAssets?.length ?? 0} + style={{ overflow: 'unset' }} + scrollableTarget="wallet-dropdown-scroll-wrapper" + > + + {walletAssets?.length + ? walletAssets.map((asset, index) => { + return ( + + ) + }) + : null} + + + ) } diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Pools/UniExtensionPoolsMenu.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/Pools/UniExtensionPoolsMenu.tsx new file mode 100644 index 00000000000..85ff437a5be --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Pools/UniExtensionPoolsMenu.tsx @@ -0,0 +1,20 @@ +import Pools from 'components/AccountDrawer/MiniPortfolio/Pools' +import { SlideOutMenu } from 'components/AccountDrawer/SlideOutMenu' +import Column from 'components/Column' +import { Trans } from 'i18n' +import styled from 'lib/styled-components' + +const Container = styled(Column)` + height: 100%; + position: relative; +` + +export function UniExtensionPoolsMenu({ onClose, account }: { account: string; onClose: () => void }) { + return ( + } onClose={onClose}> + + + + + ) +} diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Pools/cache.ts b/apps/web/src/components/AccountDrawer/MiniPortfolio/Pools/cache.ts index a24cb418210..f339e578793 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/Pools/cache.ts +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Pools/cache.ts @@ -49,10 +49,10 @@ export function useCachedPositions(account: string): UseCachedPositionsReturnTyp return cache } }), - POSITION_CACHE_EXPIRY + POSITION_CACHE_EXPIRY, ) }, - [account, setCachedPositions] + [account, setCachedPositions], ) return [cachedPositions[account], setPositionsAndStaleTimeout] } @@ -70,12 +70,12 @@ export function usePoolAddressCache() { const [cache, updateCache] = useAtom(poolAddressCacheAtom) const get = useCallback( (details: PositionDetails, chainId: InterfaceChainId) => cache[poolAddressKey(details, chainId)], - [cache] + [cache], ) const set = useCallback( (details: PositionDetails, chainId: InterfaceChainId, address: string) => updateCache((c) => ({ ...c, [poolAddressKey(details, chainId)]: address })), - [updateCache] + [updateCache], ) return { get, set } } @@ -89,7 +89,7 @@ function useTokenCache() { const entry = cache[buildCurrencyKey(chainId, address)] return entry ? deserializeToken(entry) : undefined }, - [cache] + [cache], ) const set = useCallback( (token?: Token) => { @@ -97,7 +97,7 @@ function useTokenCache() { setCache((cache) => ({ ...cache, [currencyKey(token)]: serializeToken(token) })) } }, - [setCache] + [setCache], ) return { get, set } } @@ -114,7 +114,7 @@ export function useGetCachedTokens(chains: InterfaceChainId[]): TokenGetterFn { Object.values(fetched).forEach(tokenCache.set) return fetched }, - [multicallContracts, tokenCache] + [multicallContracts, tokenCache], ) // Uses tokens from local state if available, otherwise fetches them @@ -130,7 +130,7 @@ export function useGetCachedTokens(chains: InterfaceChainId[]): TokenGetterFn { const fetched = await fetchRemoteTokens([...missing], chainId) return { ...local, ...fetched } }, - [fetchRemoteTokens, tokenCache] + [fetchRemoteTokens, tokenCache], ) return getTokens diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Pools/getTokensAsync.ts b/apps/web/src/components/AccountDrawer/MiniPortfolio/Pools/getTokensAsync.ts index a531adb61ca..ab807df7b58 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/Pools/getTokensAsync.ts +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Pools/getTokensAsync.ts @@ -46,13 +46,13 @@ function tryParseToken(address: string, chainId: InterfaceChainId, data: CallRes const name = nameData.success ? (Erc20.decodeFunctionResult('name', nameData.returnData)[0] as string) : nameDataBytes32.success - ? (Erc20Bytes32.decodeFunctionResult('name', nameDataBytes32.returnData)[0] as string) - : undefined + ? (Erc20Bytes32.decodeFunctionResult('name', nameDataBytes32.returnData)[0] as string) + : undefined const symbol = symbolData.success ? (Erc20.decodeFunctionResult('symbol', symbolData.returnData)[0] as string) : symbolDataBytes32.success - ? (Erc20Bytes32.decodeFunctionResult('symbol', symbolDataBytes32.returnData)[0] as string) - : undefined + ? (Erc20Bytes32.decodeFunctionResult('symbol', symbolDataBytes32.returnData)[0] as string) + : undefined const decimals = decimalsData.success ? parseInt(decimalsData.returnData) : DEFAULT_ERC20_DECIMALS return new Token(chainId, address, decimals, symbol, name) @@ -94,7 +94,7 @@ const TokenPromiseCache: { [key: CurrencyKey]: Promise | unde export async function getTokensAsync( addresses: string[], chainId: InterfaceChainId, - multicall: UniswapInterfaceMulticall + multicall: UniswapInterfaceMulticall, ): Promise { if (addresses.length === 0) { return {} @@ -123,7 +123,7 @@ export async function getTokensAsync( // Caches tokens currently being fetched for further calls to use formattedAddresses.forEach( (address) => - (TokenPromiseCache[buildCurrencyKey(chainId, address)] = calledTokens.then((tokenMap) => tokenMap[address])) + (TokenPromiseCache[buildCurrencyKey(chainId, address)] = calledTokens.then((tokenMap) => tokenMap[address])), ) const tokenMap = await calledTokens diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Pools/hooks.ts b/apps/web/src/components/AccountDrawer/MiniPortfolio/Pools/hooks.ts index 39082df73ee..e909758c2c8 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/Pools/hooks.ts +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Pools/hooks.ts @@ -30,7 +30,7 @@ type ContractMap = { [key: number]: T } export function useContractMultichain( addressMap: AddressMap, ABI: any, - chainIds?: InterfaceChainId[] + chainIds?: InterfaceChainId[], ): ContractMap { const account = useAccount() const { provider: walletProvider } = useWeb3React() @@ -48,8 +48,8 @@ export function useContractMultichain( walletProvider && account.chainId === chainId ? walletProvider : isSupportedChain(chainId) - ? RPC_PROVIDERS[chainId] - : undefined + ? RPC_PROVIDERS[chainId] + : undefined if (provider) { acc[chainId] = getContract(addressMap[chainId] ?? '', ABI, provider) as T } @@ -91,14 +91,14 @@ export function usePoolPriceMap(positions: PositionInfo[] | undefined) { } return acc }, {}) ?? {}, - [data?.tokens] + [data?.tokens], ) return { priceMap, pricesLoading: loading && !data } } function useFeeValue(token: Token, fee: number | undefined, queriedPrice: number | undefined) { - const stablecoinPrice = useStablecoinPrice(!queriedPrice ? token : undefined) + const { price: stablecoinPrice } = useStablecoinPrice(!queriedPrice ? token : undefined) return useMemo(() => { // Prefers gql price, as fetching stablecoinPrice will trigger multiple infura calls for each pool position const price = queriedPrice ?? (stablecoinPrice ? parseFloat(stablecoinPrice.toSignificant()) : undefined) diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Pools/index.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/Pools/index.tsx index ddf01b2ceaf..93913c03cb7 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/Pools/index.tsx +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Pools/index.tsx @@ -17,10 +17,10 @@ import { useAccount } from 'hooks/useAccount' import { useFilterPossiblyMaliciousPositions } from 'hooks/useFilterPossiblyMaliciousPositions' import { useSwitchChain } from 'hooks/useSwitchChain' import { t } from 'i18n' +import styled from 'lib/styled-components' import { EmptyWalletModule } from 'nft/components/profile/view/EmptyWalletContent' import { useCallback, useMemo, useReducer } from 'react' import { useNavigate } from 'react-router-dom' -import styled from 'styled-components' import { ThemedText } from 'theme/components' import Trace from 'uniswap/src/features/telemetry/Trace' import { NumberType, useFormatter } from 'utils/formatNumbers' @@ -31,20 +31,20 @@ import { NumberType, useFormatter } from 'utils/formatNumbers' * filters the PositionDetails data for malicious content, * and then returns the original data in its original format. */ -function useFilterPossiblyMaliciousPositionInfo(positions: PositionInfo[] | undefined): PositionInfo[] { +export function useFilterPossiblyMaliciousPositionInfo(positions: PositionInfo[] | undefined): PositionInfo[] { const tokenIdsToPositionInfo: Record = useMemo( () => positions ? positions.reduce((acc, position) => ({ ...acc, [position.details.tokenId.toString()]: position }), {}) : {}, - [positions] + [positions], ) const positionDetails = useMemo(() => positions?.map((position) => position.details) ?? [], [positions]) const filteredPositionDetails = useFilterPossiblyMaliciousPositions(positionDetails) return useMemo( () => filteredPositionDetails.map((positionDetails) => tokenIdsToPositionInfo[positionDetails.tokenId.toString()]), - [filteredPositionDetails, tokenIdsToPositionInfo] + [filteredPositionDetails, tokenIdsToPositionInfo], ) } @@ -149,7 +149,7 @@ function PositionListItem({ positionInfo }: { positionInfo: PositionInfo }) { pool_token_0_address: pool.token0.address, pool_token_1_address: pool.token1.address, }), - [chainId, pool.token0.address, pool.token0.symbol, pool.token1.address, pool.token1.symbol] + [chainId, pool.token0.address, pool.token0.symbol, pool.token1.address, pool.token1.symbol], ) return ( diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Pools/useMultiChainPositions.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/Pools/useMultiChainPositions.tsx index 4e5749a2b65..a3e35f8aa86 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/Pools/useMultiChainPositions.tsx +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Pools/useMultiChainPositions.tsx @@ -13,7 +13,7 @@ import { usePoolPriceMap, useV3ManagerContracts, } from 'components/AccountDrawer/MiniPortfolio/Pools/hooks' -import { L1_CHAIN_IDS, L2_CHAIN_IDS, TESTNET_CHAIN_IDS } from 'constants/chains' +import { PRODUCTION_CHAIN_IDS } from 'constants/chains' import { BigNumber } from 'ethers/lib/ethers' import { Interface } from 'ethers/lib/utils' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' @@ -32,7 +32,7 @@ function createPositionInfo( details: PositionDetails, slot0: any, tokenA: Token, - tokenB: Token + tokenB: Token, ): PositionInfo { /* Instantiates a Pool with a hardcoded 0 liqudity value since the sdk only uses this value for swap state and this avoids an RPC fetch */ const pool = new Pool(tokenA, tokenB, details.fee, slot0.sqrtPriceX96.toString(), 0, slot0.tick) @@ -51,10 +51,6 @@ type FeeAmounts = [BigNumber, BigNumber] const MAX_UINT128 = BigNumber.from(2).pow(128).sub(1) -const DEFAULT_CHAINS = [...L1_CHAIN_IDS, ...L2_CHAIN_IDS].filter((chain: number) => { - return !TESTNET_CHAIN_IDS.includes(chain) -}) - type UseMultiChainPositionsData = { positions?: PositionInfo[]; loading: boolean } /** @@ -66,7 +62,10 @@ type UseMultiChainPositionsData = { positions?: PositionInfo[]; loading: boolean * @param chains - chains to fetch positions from * @returns positions, fees */ -export default function useMultiChainPositions(account: string, chains = DEFAULT_CHAINS): UseMultiChainPositionsData { +export default function useMultiChainPositions( + account: string, + chains = PRODUCTION_CHAIN_IDS, +): UseMultiChainPositionsData { const pms = useV3ManagerContracts(chains) const multicalls = useInterfaceMulticallContracts(chains) @@ -87,27 +86,30 @@ export default function useMultiChainPositions(account: string, chains = DEFAULT const callData = positionIds.map((id) => pm.interface.encodeFunctionData('collect', [ { tokenId: id, recipient: account, amount0Max: MAX_UINT128, amount1Max: MAX_UINT128 }, - ]) + ]), + ) + const fees = (await pm.callStatic.multicall(callData)).reduce( + (acc, feeBytes, index) => { + const key = chainId.toString() + positionIds[index] + acc[key] = pm.interface.decodeFunctionResult('collect', feeBytes) as FeeAmounts + return acc + }, + {} as { [key: string]: FeeAmounts }, ) - const fees = (await pm.callStatic.multicall(callData)).reduce((acc, feeBytes, index) => { - const key = chainId.toString() + positionIds[index] - acc[key] = pm.interface.decodeFunctionResult('collect', feeBytes) as FeeAmounts - return acc - }, {} as { [key: string]: FeeAmounts }) setFeeMap((prev) => ({ ...prev, ...fees })) }, - [account] + [account], ) const fetchPositionIds = useCallback( async (pm: NonfungiblePositionManager, balance: BigNumber) => { const callData = Array.from({ length: balance.toNumber() }, (_, i) => - pm.interface.encodeFunctionData('tokenOfOwnerByIndex', [account, i]) + pm.interface.encodeFunctionData('tokenOfOwnerByIndex', [account, i]), ) return (await pm.callStatic.multicall(callData)).map((idByte) => BigNumber.from(idByte)) }, - [account] + [account], ) const fetchPositionDetails = useCallback(async (pm: NonfungiblePositionManager, positionIds: BigNumber[]) => { @@ -117,7 +119,7 @@ export default function useMultiChainPositions(account: string, chains = DEFAULT ({ ...pm.interface.decodeFunctionResult('positions', positionBytes), tokenId: positionIds[index], - } as unknown as PositionDetails) + }) as unknown as PositionDetails, ) }, []) @@ -127,7 +129,7 @@ export default function useMultiChainPositions(account: string, chains = DEFAULT const poolInterface = new Interface(IUniswapV3PoolStateJSON.abi) as UniswapV3PoolInterface const tokens = await getTokens( positionDetails.flatMap((details) => [details.token0, details.token1]), - chainId + chainId, ) const calls: Call[] = [] @@ -166,7 +168,7 @@ export default function useMultiChainPositions(account: string, chains = DEFAULT return acc }, []) }, - [account, poolAddressCache, getTokens] + [account, poolAddressCache, getTokens], ) const fetchPositionsForChain = useCallback( @@ -197,7 +199,7 @@ export default function useMultiChainPositions(account: string, chains = DEFAULT return [] } }, - [account, fetchPositionDetails, fetchPositionFees, fetchPositionIds, fetchPositionInfo, pms, multicalls] + [account, fetchPositionDetails, fetchPositionFees, fetchPositionIds, fetchPositionInfo, pms, multicalls], ) const fetchAllPositions = useCallback(async () => { @@ -241,7 +243,7 @@ export default function useMultiChainPositions(account: string, chains = DEFAULT const prices = [priceMap[currencyKey(position.pool.token0)], priceMap[currencyKey(position.pool.token1)]] return { ...position, fees, prices } as PositionInfo }), - [feeMap, positions, priceMap] + [feeMap, positions, priceMap], ) return { positions: positionsWithFeesAndPrices, loading: pricesLoading || positionsLoading } diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/PortfolioLogo.test.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/PortfolioLogo.test.tsx index 4f0f9818198..08933d9bf7e 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/PortfolioLogo.test.tsx +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/PortfolioLogo.test.tsx @@ -14,7 +14,7 @@ describe('PortfolioLogo', () => { it('renders with L2 icon', () => { const { container } = render( - + , ) expect(container).toMatchSnapshot() }) diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/PortfolioLogo.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/PortfolioLogo.tsx index bccd8611878..4af15148e66 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/PortfolioLogo.tsx +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/PortfolioLogo.tsx @@ -10,8 +10,8 @@ import { } from 'components/DoubleLogo' import Identicon from 'components/Identicon' import { ChainLogo } from 'components/Logo/ChainLogo' +import styled from 'lib/styled-components' import React from 'react' -import styled from 'styled-components' import { InterfaceChainId, UniverseChainId } from 'uniswap/src/types/chains' const UnknownContract = styled(UnknownStatus)` diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/PortfolioRow.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/PortfolioRow.tsx index 7e55431cea9..2569e566a31 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/PortfolioRow.tsx +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/PortfolioRow.tsx @@ -1,7 +1,7 @@ import Column, { AutoColumn } from 'components/Column' import Row from 'components/Row' import { LoadingBubble } from 'components/Tokens/loading' -import styled, { css, keyframes } from 'styled-components' +import styled, { css, keyframes } from 'lib/styled-components' export const PortfolioRowWrapper = styled(Row)<{ onClick?: any }>` gap: 12px; diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Tokens/index.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/Tokens/index.tsx index fec861d1c81..025b8e38146 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/Tokens/index.tsx +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Tokens/index.tsx @@ -14,10 +14,10 @@ import { useTokenBalancesQuery } from 'graphql/data/apollo/TokenBalancesProvider import { PortfolioToken } from 'graphql/data/portfolios' import { getTokenDetailsURL, gqlToCurrency } from 'graphql/data/util' import { useAtomValue } from 'jotai/utils' +import styled from 'lib/styled-components' import { EmptyWalletModule } from 'nft/components/profile/view/EmptyWalletContent' import { useCallback, useMemo, useState } from 'react' import { useNavigate } from 'react-router-dom' -import styled from 'styled-components' import { EllipsisStyle, ThemedText } from 'theme/components' import { PortfolioTokenBalancePartsFragment } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import Trace from 'uniswap/src/features/telemetry/Trace' @@ -37,7 +37,7 @@ export default function Tokens() { const { visibleTokens, hiddenTokens } = useMemo( () => splitHiddenTokens(tokenBalances ?? [], { hideSmallBalances, hideSpam }), - [hideSmallBalances, tokenBalances, hideSpam] + [hideSmallBalances, tokenBalances, hideSpam], ) if (!data) { @@ -55,12 +55,12 @@ export default function Tokens() { {visibleTokens.map( (tokenBalance) => - tokenBalance.token && + tokenBalance.token && , )} {hiddenTokens.map( (tokenBalance) => - tokenBalance.token && + tokenBalance.token && , )} diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/index.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/index.tsx index a93dcd4f03d..4cc631dd607 100644 --- a/apps/web/src/components/AccountDrawer/MiniPortfolio/index.tsx +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/index.tsx @@ -12,8 +12,8 @@ import { useDisableNFTRoutes } from 'hooks/useDisableNFTRoutes' import { useIsNftPage } from 'hooks/useIsNftPage' import { Trans } from 'i18n' import { atom, useAtom } from 'jotai' +import styled, { useTheme } from 'lib/styled-components' import { useEffect, useState } from 'react' -import styled, { useTheme } from 'styled-components' import { BREAKPOINTS } from 'theme' import { ThemedText } from 'theme/components' import Trace from 'uniswap/src/features/telemetry/Trace' diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/shared.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/shared.tsx new file mode 100644 index 00000000000..161a71452f9 --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/shared.tsx @@ -0,0 +1,46 @@ +import Column from 'components/Column' +import Row from 'components/Row' +import styled, { useTheme } from 'lib/styled-components' +import { ReactNode } from 'react' +import { ArrowRight } from 'react-feather' +import { ClickableStyle, ThemedText } from 'theme/components' +import { Text } from 'ui/src' + +const Container = styled.button` + border-radius: 16px; + border: none; + background: ${({ theme }) => theme.surface2}; + padding: 12px 16px; + margin-top: 12px; + ${ClickableStyle} +` + +interface TabButtonProps { + text: ReactNode + icon: ReactNode + extraWarning?: ReactNode + onClick: () => void + disabled?: boolean + className?: string +} + +export function TabButton({ text, icon, extraWarning, onClick, disabled, className }: TabButtonProps) { + const theme = useTheme() + + return ( + + + + {icon} + + + {text} + + {extraWarning && {extraWarning}} + + + + + + ) +} diff --git a/apps/web/src/components/AccountDrawer/SettingsMenu.tsx b/apps/web/src/components/AccountDrawer/SettingsMenu.tsx index ab6316f70db..a2259883571 100644 --- a/apps/web/src/components/AccountDrawer/SettingsMenu.tsx +++ b/apps/web/src/components/AccountDrawer/SettingsMenu.tsx @@ -11,9 +11,9 @@ import { LOCALE_LABEL } from 'constants/locales' import { useActiveLocalCurrency } from 'hooks/useActiveLocalCurrency' import { useActiveLocale } from 'hooks/useActiveLocale' import { Trans } from 'i18n' +import styled from 'lib/styled-components' import { ReactNode } from 'react' import { ChevronRight } from 'react-feather' -import styled from 'styled-components' import { ClickableStyle, ThemedText } from 'theme/components' import ThemeToggle from 'theme/components/ThemeToggle' import { FeatureFlags } from 'uniswap/src/features/gating/flags' diff --git a/apps/web/src/components/AccountDrawer/SettingsToggle.test.tsx b/apps/web/src/components/AccountDrawer/SettingsToggle.test.tsx index 8e7ac8f8f2f..67f292247f0 100644 --- a/apps/web/src/components/AccountDrawer/SettingsToggle.test.tsx +++ b/apps/web/src/components/AccountDrawer/SettingsToggle.test.tsx @@ -12,7 +12,7 @@ describe('SettingsToggle', () => { description="Test description" isActive={mockActive} toggle={mockToggle} - /> + />, ) expect(mockActive).toBeFalsy() diff --git a/apps/web/src/components/AccountDrawer/SettingsToggle.tsx b/apps/web/src/components/AccountDrawer/SettingsToggle.tsx index 39117ffdf28..5d4661d2d07 100644 --- a/apps/web/src/components/AccountDrawer/SettingsToggle.tsx +++ b/apps/web/src/components/AccountDrawer/SettingsToggle.tsx @@ -1,8 +1,8 @@ import Column from 'components/Column' import Row from 'components/Row' import Toggle from 'components/Toggle' +import styled from 'lib/styled-components' import { ReactNode } from 'react' -import styled from 'styled-components' import { ThemedText } from 'theme/components' const StyledColumn = styled(Column)` diff --git a/apps/web/src/components/AccountDrawer/SlideOutMenu.tsx b/apps/web/src/components/AccountDrawer/SlideOutMenu.tsx index e8ce58bacd2..2d8d5f040ec 100644 --- a/apps/web/src/components/AccountDrawer/SlideOutMenu.tsx +++ b/apps/web/src/components/AccountDrawer/SlideOutMenu.tsx @@ -1,7 +1,7 @@ import Column from 'components/Column' import { ScrollBarStyles } from 'components/Common' +import styled from 'lib/styled-components' import { ArrowLeft } from 'react-feather' -import styled from 'styled-components' import { ClickableStyle, ThemedText } from 'theme/components' const Menu = styled(Column)` diff --git a/apps/web/src/components/AccountDrawer/Status.tsx b/apps/web/src/components/AccountDrawer/Status.tsx index 07d981c554e..bf8eced156e 100644 --- a/apps/web/src/components/AccountDrawer/Status.tsx +++ b/apps/web/src/components/AccountDrawer/Status.tsx @@ -1,3 +1,4 @@ +import { AddressDisplay } from 'components/AccountDetails/AddressDisplay' import Column from 'components/Column' import { ENS } from 'components/Icons/ENS' import { EthMini } from 'components/Icons/EthMini' @@ -5,11 +6,10 @@ import StatusIcon from 'components/Identicon/StatusIcon' import Popover from 'components/Popover' import Row from 'components/Row' import { useOnClickOutside } from 'hooks/useOnClickOutside' +import styled from 'lib/styled-components' import { useRef, useState } from 'react' import { MoreHorizontal } from 'react-feather' -import styled from 'styled-components' -import { ClickableStyle, CopyHelper, EllipsisStyle, ThemedText } from 'theme/components' -import { Unitag } from 'ui/src/components/icons' +import { ClickableStyle, CopyHelper, ThemedText } from 'theme/components' import { shortenAddress } from 'utilities/src/addresses' const Container = styled.div` @@ -26,13 +26,6 @@ const Identifiers = styled.div` overflow: hidden; flex: 1 1 auto; ` -const IdentifierText = styled.span` - ${EllipsisStyle} - max-width: 120px; - @media screen and (min-width: 1440px) { - max-width: 180px; - } -` const SecondaryIdentifiersContainer = styled(Row)` position: relative; user-select: none; @@ -80,7 +73,7 @@ function SecondaryIdentifier({ ) } -function SecondaryIdentifiers({ +export function SecondaryIdentifiers({ account, uniswapUsername, ensUsername, @@ -130,26 +123,19 @@ export function Status({ account, ensUsername, uniswapUsername, + showAddressCopy = true, }: { account: string ensUsername: string | null uniswapUsername?: string + showAddressCopy?: boolean }) { return ( - - - {uniswapUsername ?? ensUsername ?? shortenAddress(account)} - {uniswapUsername && } - - + {(uniswapUsername || ensUsername) && ( diff --git a/apps/web/src/components/AccountDrawer/UniwalletModal.tsx b/apps/web/src/components/AccountDrawer/UniwalletModal.tsx index 960b07d1a11..170e3d11755 100644 --- a/apps/web/src/components/AccountDrawer/UniwalletModal.tsx +++ b/apps/web/src/components/AccountDrawer/UniwalletModal.tsx @@ -8,9 +8,9 @@ import { useConnectorWithId } from 'components/WalletModal/useOrderedConnections import { CONNECTION } from 'components/Web3Provider/constants' import { useConnect } from 'hooks/useConnect' import { Trans } from 'i18n' +import styled, { useTheme } from 'lib/styled-components' import { QRCodeSVG } from 'qrcode.react' import { useCallback, useEffect, useState } from 'react' -import styled, { useTheme } from 'styled-components' import { CloseIcon, ThemedText } from 'theme/components' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { isWebAndroid, isWebIOS } from 'utilities/src/platform' @@ -69,6 +69,8 @@ export default function UniwalletModal() { useEffect(() => { if (open) { sendAnalyticsEvent(InterfaceEventName.UNIWALLET_CONNECT_MODAL_OPENED) + } else { + setUri(undefined) } }, [open]) diff --git a/apps/web/src/components/AccountDrawer/__snapshots__/index.test.tsx.snap b/apps/web/src/components/AccountDrawer/__snapshots__/index.test.tsx.snap new file mode 100644 index 00000000000..ccd52f478d3 --- /dev/null +++ b/apps/web/src/components/AccountDrawer/__snapshots__/index.test.tsx.snap @@ -0,0 +1,1657 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AccountDrawer tests AccountDrawer styles when isUniExtensionAvailable is false 1`] = ` + + .c7 { + box-sizing: border-box; + margin: 0; + min-width: 0; + width: 100%; +} + +.c14 { + box-sizing: border-box; + margin: 0; + min-width: 0; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; +} + +.c8 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; +} + +.c15 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: flex-start; + -webkit-box-align: flex-start; + -ms-flex-align: flex-start; + align-items: flex-start; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c9 { + -webkit-flex-wrap: wrap; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; +} + +.c9 > * { + margin: !important; +} + +.c10 { + color: #222222; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c24 { + color: #7D7D7D; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c25 { + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; + color: #FC72FF; + stroke: #FC72FF; + font-weight: 500; +} + +.c25:hover { + opacity: 0.6; +} + +.c25:active { + opacity: 0.4; +} + +.c4 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c13 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 12px; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; +} + +.c22 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 12px; +} + +.c11 { + background-color: #FFFFFF; + -webkit-transition: width ease-in-out 125ms; + transition: width ease-in-out 125ms; + border-radius: 12px; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + cursor: pointer; + position: relative; + overflow: hidden; + height: 32px; + width: 32px; + color: #7D7D7D; + border: none; + outline: none; +} + +.c11:hover { + background-color: #F9F9F9; + -webkit-transition: 125ms background-color ease-in, width ease-in-out 125ms; + transition: 125ms background-color ease-in, width ease-in-out 125ms; +} + +.c11:active { + background-color: #FFFFFF; + -webkit-transition: background-color 125ms linear, width ease-in-out 125ms; + transition: background-color 125ms linear, width ease-in-out 125ms; +} + +.c12 { + width: 24px; + height: 24px; + margin: auto; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; +} + +.c19 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-flow: column nowrap; + -ms-flex-flow: column nowrap; + flex-flow: column nowrap; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; +} + +.c18 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + background-color: unset; + border: none; + cursor: pointer; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex: 1 1 auto; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + opacity: 1; + padding: 18px; + -webkit-transition: 125ms; + transition: 125ms; +} + +.c21 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-flow: row nowrap; + -ms-flex-flow: row nowrap; + flex-flow: row nowrap; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + color: #222222; + font-size: 16px; + font-weight: 535; + padding: 0 8px; +} + +.c20 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-flow: column nowrap; + -ms-flex-flow: column nowrap; + flex-flow: column nowrap; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; +} + +.c20 img { + border: 1px solid #22222212; + border-radius: 12px; +} + +.c20 > img, +.c20 span { + height: 40px; + width: 40px; +} + +.c17 { + -webkit-align-items: stretch; + -webkit-box-align: stretch; + -ms-flex-align: stretch; + align-items: stretch; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + position: relative; + width: 100%; + background-color: #F9F9F9; +} + +.c17:hover { + cursor: pointer; + background-color: #22222212; +} + +.c17:focus { + background-color: #22222212; +} + +.c26 { + font-weight: 535; + color: #7D7D7D; +} + +.c6 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-flow: column nowrap; + -ms-flex-flow: column nowrap; + flex-flow: column nowrap; + background-color: #FFFFFF; + width: 100%; + padding: 14px 16px 16px; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + gap: 16px; +} + +.c16 { + display: grid; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + grid-gap: 2px; + border-radius: 12px; + overflow: hidden; + opacity: 1; + max-height: 100vh; + -webkit-transition: max-height 125ms ease-in-out,opacity 125ms ease-in-out; + transition: max-height 125ms ease-in-out,opacity 125ms ease-in-out; +} + +.c23 { + padding: 0 4px; +} + +.c5 { + width: 100%; + height: 100%; +} + +.c1 { + z-index: 1040; + overflow: hidden; + top: 0; + left: 0; + position: fixed; + width: 100%; + height: 100%; + background-color: rgba(0,0,0,0.60); + opacity: 0; + pointer-events: none; +} + +.c3 { + overflow-y: auto; + overflow-x: hidden; + -webkit-scrollbar-width: thin; + -moz-scrollbar-width: thin; + -ms-scrollbar-width: thin; + scrollbar-width: thin; + -webkit-scrollbar-color: #22222212 transparent; + -moz-scrollbar-color: #22222212 transparent; + -ms-scrollbar-color: #22222212 transparent; + scrollbar-color: #22222212 transparent; + height: 100%; + -webkit-scrollbar-gutter: stable; + -moz-scrollbar-gutter: stable; + -ms-scrollbar-gutter: stable; + scrollbar-gutter: stable; + overscroll-behavior: contain; + border-radius: 12px; +} + +.c3::-webkit-scrollbar { + background: transparent; + width: 4px; + overflow-y: scroll; +} + +.c3::-webkit-scrollbar-thumb { + background: #22222212; + border-radius: 8px; +} + +.c0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + height: calc(100% - 2 * 8px); + overflow: hidden; + position: fixed; + right: 8px; + top: 8px; + z-index: 1030; +} + +.c2 { + margin-right: -320px; + height: 100%; + overflow: hidden; + border-radius: 12px; + width: 320px; + max-width: 320px; + font-size: 16px; + background-color: #FFFFFF; + border: 1px solid #22222212; + box-shadow: 8px 12px 20px rgba(51,53,72,0.04),4px 6px 12px rgba(51,53,72,0.02),4px 4px 8px rgba(51,53,72,0.04); + -webkit-transition: margin-right 250ms; + transition: margin-right 250ms; +} + +@media (max-width:960px) { + .c20 { + -webkit-align-items: flex-end; + -webkit-box-align: flex-end; + -ms-flex-align: flex-end; + align-items: flex-end; + } +} + +@media (max-width:960px) { + .c16 { + grid-template-columns: 1fr; + } +} + +@media only screen and (max-width:640px) { + .c1 { + opacity: 0; + pointer-events: none; + -webkit-transition: opacity 250ms ease-in-out; + transition: opacity 250ms ease-in-out; + } +} + +@media only screen and (max-width:640px) { + .c0 { + height: 100%; + top: 100%; + left: 0; + right: 0; + width: 100%; + overflow: visible; + } +} + +@media only screen and (max-width:640px) { + .c2 { + z-index: 1060; + position: absolute; + margin-right: 0; + top: 0; + height: calc(100% - 72px); + width: 100%; + max-width: 100%; + border-bottom-right-radius: 0px; + border-bottom-left-radius: 0px; + box-shadow: unset; + -webkit-transition: top 250ms; + transition: top 250ms; + } +} + +@media screen and (min-width:1440px) { + .c2 { + margin-right: -390px; + width: 390px; + max-width: 390px; + } +} + + + +
        +
        +
        +
        +
        +
        +
        +
        +
        + Connect a wallet +
        + +
        +
        +
        +
        +
        + +
        +
        + +
        +
        + +
        +
        +
        +
        +
        +
        + By connecting a wallet, you agree to Uniswap Labs’ + + Terms of Service + + and consent to its + + Privacy Policy. + +
        +
        +
        +
        +
        +
        +
        +
        +
        + + + +`; + +exports[`AccountDrawer tests AccountDrawer styles when isUniExtensionAvailable is true 1`] = ` + + .c7 { + box-sizing: border-box; + margin: 0; + min-width: 0; + width: 100%; +} + +.c15 { + box-sizing: border-box; + margin: 0; + min-width: 0; +} + +.c19 { + box-sizing: border-box; + margin: 0; + min-width: 0; + padding: 8px 0px; +} + +.c23 { + box-sizing: border-box; + margin: 0; + min-width: 0; + margin-left: 18px; + margin-right: 18px; +} + +.c27 { + box-sizing: border-box; + margin: 0; + min-width: 0; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; +} + +.c8 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; +} + +.c16 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 12px; +} + +.c18 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 4px; +} + +.c20 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + padding: 8px 0px; +} + +.c24 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c28 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: flex-start; + -webkit-box-align: flex-start; + -ms-flex-align: flex-start; + align-items: flex-start; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c9 { + -webkit-flex-wrap: wrap; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; +} + +.c9 > * { + margin: !important; +} + +.c10 { + color: #222222; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c36 { + color: #7D7D7D; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c37 { + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; + color: #FC72FF; + stroke: #FC72FF; + font-weight: 500; +} + +.c37:hover { + opacity: 0.6; +} + +.c37:active { + opacity: 0.4; +} + +.c4 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c13 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 16px; +} + +.c14 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 12px; +} + +.c26 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 12px; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; +} + +.c11 { + background-color: #FFFFFF; + -webkit-transition: width ease-in-out 125ms; + transition: width ease-in-out 125ms; + border-radius: 12px; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + cursor: pointer; + position: relative; + overflow: hidden; + height: 32px; + width: 32px; + color: #7D7D7D; + border: none; + outline: none; +} + +.c11:hover { + background-color: #F9F9F9; + -webkit-transition: 125ms background-color ease-in, width ease-in-out 125ms; + transition: 125ms background-color ease-in, width ease-in-out 125ms; +} + +.c11:active { + background-color: #FFFFFF; + -webkit-transition: background-color 125ms linear, width ease-in-out 125ms; + transition: background-color 125ms linear, width ease-in-out 125ms; +} + +.c12 { + width: 24px; + height: 24px; + margin: auto; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; +} + +.c32 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-flow: column nowrap; + -ms-flex-flow: column nowrap; + flex-flow: column nowrap; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; +} + +.c31 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + background-color: unset; + border: none; + cursor: pointer; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex: 1 1 auto; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + opacity: 1; + padding: 18px; + -webkit-transition: 125ms; + transition: 125ms; +} + +.c34 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-flow: row nowrap; + -ms-flex-flow: row nowrap; + flex-flow: row nowrap; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + color: #222222; + font-size: 16px; + font-weight: 535; + padding: 0 8px; +} + +.c33 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-flow: column nowrap; + -ms-flex-flow: column nowrap; + flex-flow: column nowrap; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; +} + +.c33 img { + border: 1px solid #22222212; + border-radius: 12px; +} + +.c33 > img, +.c33 span { + height: 40px; + width: 40px; +} + +.c30 { + -webkit-align-items: stretch; + -webkit-box-align: stretch; + -ms-flex-align: stretch; + align-items: stretch; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + position: relative; + width: 100%; + background-color: #F9F9F9; +} + +.c30:hover { + cursor: pointer; + background-color: #22222212; +} + +.c30:focus { + background-color: #22222212; +} + +.c38 { + font-weight: 535; + color: #7D7D7D; +} + +.c17 { + padding: 16px; + gap: 12px; + border-radius: 16px; + border: 1px solid #22222212; + overflow: hidden; + max-height: 72px; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + cursor: pointer; + position: relative; + z-index: 1; +} + +.c17:hover { + background: #22222212; +} + +.c6 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-flow: column nowrap; + -ms-flex-flow: column nowrap; + flex-flow: column nowrap; + background-color: #FFFFFF; + width: 100%; + padding: 0px 16px 16px; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + gap: 16px; +} + +.c29 { + display: grid; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + grid-gap: 2px; + border-radius: 12px; + overflow: hidden; + opacity: 1; + max-height: 100vh; + -webkit-transition: max-height 125ms ease-in-out,opacity 125ms ease-in-out; + transition: max-height 125ms ease-in-out,opacity 125ms ease-in-out; +} + +.c35 { + padding: 0 4px; +} + +.c21 { + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.c21:hover { + opacity: 0.6; +} + +.c21:active { + opacity: 0.4; +} + +.c22 { + height: 1px; + width: 100%; + background: #22222212; +} + +.c25 { + height: 20px; + width: 20px; + fill: #7D7D7D; + -webkit-flex-shrink: 0; + -ms-flex-negative: 0; + flex-shrink: 0; +} + +.c5 { + width: 100%; + height: 100%; +} + +.c1 { + z-index: 1040; + overflow: hidden; + top: 0; + left: 0; + position: fixed; + width: 100%; + height: 100%; + background-color: rgba(0,0,0,0.60); + opacity: 0; + pointer-events: none; +} + +.c3 { + overflow-y: auto; + overflow-x: hidden; + -webkit-scrollbar-width: thin; + -moz-scrollbar-width: thin; + -ms-scrollbar-width: thin; + scrollbar-width: thin; + -webkit-scrollbar-color: #22222212 transparent; + -moz-scrollbar-color: #22222212 transparent; + -ms-scrollbar-color: #22222212 transparent; + scrollbar-color: #22222212 transparent; + height: 100%; + -webkit-scrollbar-gutter: stable; + -moz-scrollbar-gutter: stable; + -ms-scrollbar-gutter: stable; + scrollbar-gutter: stable; + overscroll-behavior: contain; + border-radius: 12px; +} + +.c3::-webkit-scrollbar { + background: transparent; + width: 4px; + overflow-y: scroll; +} + +.c3::-webkit-scrollbar-thumb { + background: #22222212; + border-radius: 8px; +} + +.c0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + height: calc(100% - 2 * 8px); + overflow: hidden; + position: fixed; + right: 8px; + top: 8px; + z-index: 1030; + height: auto; + max-height: calc(100% - 88px); + right: 24px; + top: 72px; + -webkit-scrollbar-width: thin; + -moz-scrollbar-width: thin; + -ms-scrollbar-width: thin; + scrollbar-width: thin; + -webkit-scrollbar-color: #22222212 transparent; + -moz-scrollbar-color: #22222212 transparent; + -ms-scrollbar-color: #22222212 transparent; + scrollbar-color: #22222212 transparent; + height: 100%; +} + +.c0::-webkit-scrollbar { + background: transparent; + width: 4px; + overflow-y: scroll; +} + +.c0::-webkit-scrollbar-thumb { + background: #22222212; + border-radius: 8px; +} + +.c2 { + margin-right: -368px; + height: 100%; + overflow: hidden; + border-radius: 12px; + width: 320px; + max-width: 320px; + font-size: 16px; + background-color: #FFFFFF; + border: 1px solid #22222212; + box-shadow: 8px 12px 20px rgba(51,53,72,0.04),4px 6px 12px rgba(51,53,72,0.02),4px 4px 8px rgba(51,53,72,0.04); + -webkit-transition: margin-right 250ms; + transition: margin-right 250ms; + height: -webkit-max-content; + height: -moz-max-content; + height: max-content; + max-height: 100%; + width: 368px; + max-width: 368px; + border-radius: 20px; + box-shadow: 8px 12px 20px rgba(51,53,72,0.04),4px 6px 12px rgba(51,53,72,0.02),4px 4px 8px rgba(51,53,72,0.04); + -webkit-transform: scale(0.96); + -ms-transform: scale(0.96); + transform: scale(0.96); + -webkit-transform-origin: top right; + -ms-transform-origin: top right; + transform-origin: top right; + opacity: 0; + overflow-y: scroll; + -webkit-transition: -webkit-transform 125ms ease-in-out, opacity 125ms ease-in-out; + -webkit-transition: transform 125ms ease-in-out, opacity 125ms ease-in-out; + transition: transform 125ms ease-in-out, opacity 125ms ease-in-out; +} + +@media (max-width:960px) { + .c33 { + -webkit-align-items: flex-end; + -webkit-box-align: flex-end; + -ms-flex-align: flex-end; + align-items: flex-end; + } +} + +@media (max-width:960px) { + .c29 { + grid-template-columns: 1fr; + } +} + +@media only screen and (max-width:640px) { + .c1 { + opacity: 0; + pointer-events: none; + -webkit-transition: opacity 250ms ease-in-out; + transition: opacity 250ms ease-in-out; + } +} + +@media only screen and (max-width:640px) { + .c0 { + height: 100%; + top: 100%; + left: 0; + right: 0; + width: 100%; + overflow: visible; + } +} + +@media only screen and (max-width:640px) { + .c2 { + z-index: 1060; + position: absolute; + margin-right: 0; + top: 0; + height: calc(100% - 72px); + width: 100%; + max-width: 100%; + border-bottom-right-radius: 0px; + border-bottom-left-radius: 0px; + box-shadow: unset; + -webkit-transition: top 250ms; + transition: top 250ms; + } +} + +@media screen and (min-width:1440px) { + .c2 { + margin-right: -390px; + width: 390px; + max-width: 390px; + } +} + + + +
        +
        +
        +
        +
        +
        +
        +
        +
        + Connect a wallet +
        + +
        +
        +
        +
        + + + +
        +
        + + Uniswap Mobile + + + Scan QR code to connect + +
        +
        +
        +
        +
        +
        +
        +
        + + Other wallets + + + + + +
        +
        +
        +
        +
        +
        +
        + +
        +
        + +
        +
        +
        +
        +
        +
        + By connecting a wallet, you agree to Uniswap Labs’ + + Terms of Service + + and consent to its + + Privacy Policy. + +
        +
        +
        +
        +
        +
        +
        +
        +
        + + + +`; diff --git a/apps/web/src/components/AccountDrawer/index.test.tsx b/apps/web/src/components/AccountDrawer/index.test.tsx new file mode 100644 index 00000000000..9d041bf0e19 --- /dev/null +++ b/apps/web/src/components/AccountDrawer/index.test.tsx @@ -0,0 +1,33 @@ +import AccountDrawer, { DRAWER_WIDTH, MODAL_WIDTH } from 'components/AccountDrawer' +import { useIsUniExtensionAvailable, useUniswapWalletOptions } from 'hooks/useUniswapWalletOptions' +import { mocked } from 'test-utils/mocked' +import { render, screen } from 'test-utils/render' + +jest.mock('hooks/useUniswapWalletOptions', () => ({ + useIsUniExtensionAvailable: jest.fn(), + useUniswapWalletOptions: jest.fn(), +})) + +describe('AccountDrawer tests', () => { + it('AccountDrawer styles when isUniExtensionAvailable is false', () => { + mocked(useUniswapWalletOptions).mockReturnValue(false) + mocked(useIsUniExtensionAvailable).mockReturnValue(false) + + const { asFragment } = render() + expect(asFragment()).toMatchSnapshot() + const drawerWrapper = screen.getByTestId('account-drawer') + expect(drawerWrapper).toBeInTheDocument() + expect(drawerWrapper).toHaveStyleRule('width', DRAWER_WIDTH) + }) + + it('AccountDrawer styles when isUniExtensionAvailable is true', () => { + mocked(useUniswapWalletOptions).mockReturnValue(true) + mocked(useIsUniExtensionAvailable).mockReturnValue(true) + + const { asFragment } = render() + expect(asFragment()).toMatchSnapshot() + const drawerWrapper = screen.getByTestId('account-drawer') + expect(drawerWrapper).toBeInTheDocument() + expect(drawerWrapper).toHaveStyleRule('width', MODAL_WIDTH) + }) +}) diff --git a/apps/web/src/components/AccountDrawer/index.tsx b/apps/web/src/components/AccountDrawer/index.tsx index 10f22cb519f..110a4d2b04e 100644 --- a/apps/web/src/components/AccountDrawer/index.tsx +++ b/apps/web/src/components/AccountDrawer/index.tsx @@ -2,13 +2,17 @@ import { InterfaceEventName } from '@uniswap/analytics-events' import DefaultMenu from 'components/AccountDrawer/DefaultMenu' import { useAccountDrawer } from 'components/AccountDrawer/MiniPortfolio/hooks' import { ScrollBarStyles } from 'components/Common' +import { Web3StatusRef } from 'components/Web3Status' import { useWindowSize } from 'hooks/screenSize' import useDisableScrolling from 'hooks/useDisableScrolling' +import { useOnClickOutside } from 'hooks/useOnClickOutside' import usePrevious from 'hooks/usePrevious' +import { useIsUniExtensionAvailable } from 'hooks/useUniswapWalletOptions' +import { useAtom } from 'jotai' +import styled, { css } from 'lib/styled-components' import { useEffect, useRef, useState } from 'react' import { ChevronsRight } from 'react-feather' import { useGesture } from 'react-use-gesture' -import styled from 'styled-components' import { BREAKPOINTS } from 'theme' import { ClickableStyle } from 'theme/components' import { Z_INDEX } from 'theme/zIndex' @@ -16,10 +20,12 @@ import Trace from 'uniswap/src/features/telemetry/Trace' import { isMobile } from 'utilities/src/platform' const DRAWER_WIDTH_XL = '390px' -const DRAWER_WIDTH = '320px' +export const DRAWER_WIDTH = '320px' const DRAWER_MARGIN = '8px' const DRAWER_OFFSET = '10px' +export const MODAL_WIDTH = '368px' + const ScrimBackground = styled.div<{ $open: boolean; $maxWidth?: number; $zIndex?: number }>` z-index: ${({ $zIndex }) => $zIndex ?? Z_INDEX.modalBackdrop}; overflow: hidden; @@ -71,7 +77,7 @@ const AccountDrawerScrollWrapper = styled.div` border-radius: 12px; ` -const Container = styled.div` +const Container = styled.div<{ isUniExtensionAvailable?: boolean }>` display: flex; flex-direction: row; height: calc(100% - 2 * ${DRAWER_MARGIN}); @@ -81,6 +87,8 @@ const Container = styled.div` top: ${DRAWER_MARGIN}; z-index: ${Z_INDEX.fixed}; + ${({ isUniExtensionAvailable }) => isUniExtensionAvailable && ExtensionContainerStyles} + @media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) { height: 100%; top: 100%; @@ -91,8 +99,17 @@ const Container = styled.div` } ` -const AccountDrawerWrapper = styled.div<{ open: boolean }>` - margin-right: ${({ open }) => (open ? 0 : '-' + DRAWER_WIDTH)}; +const ExtensionContainerStyles = css` + height: auto; + max-height: calc(100% - ${({ theme }) => theme.navHeight + 16}px); + right: 24px; + top: ${({ theme }) => theme.navHeight}px; + ${ScrollBarStyles} +` + +const AccountDrawerWrapper = styled.div<{ open: boolean; isUniExtensionAvailable?: boolean }>` + margin-right: ${({ open, isUniExtensionAvailable }) => + open ? 0 : '-' + (isUniExtensionAvailable ? MODAL_WIDTH : DRAWER_WIDTH)}; height: 100%; overflow: hidden; @@ -126,6 +143,23 @@ const AccountDrawerWrapper = styled.div<{ open: boolean }>` box-shadow: ${({ theme }) => theme.deprecated_deepShadow}; transition: margin-right ${({ theme }) => theme.transition.duration.medium}; + + ${({ isUniExtensionAvailable }) => isUniExtensionAvailable && ExtensionDrawerWrapperStyles} +` + +const ExtensionDrawerWrapperStyles = css<{ open: boolean }>` + height: max-content; + max-height: 100%; + width: ${MODAL_WIDTH}; + max-width: ${MODAL_WIDTH}; + border-radius: 20px; + box-shadow: ${({ theme }) => theme.deprecated_deepShadow}; + transform: scale(${({ open }) => (open ? 1 : 0.96)}); + transform-origin: top right; + opacity: ${({ open }) => (open ? 1 : 0)}; + overflow-y: scroll; + transition: ${({ theme }) => `transform ${theme.transition.duration.fast} ${theme.transition.timing.inOut}, + opacity ${theme.transition.duration.fast} ${theme.transition.timing.inOut}`}; ` const CloseIcon = styled(ChevronsRight).attrs({ size: 24 })` @@ -155,6 +189,22 @@ function AccountDrawer() { const accountDrawer = useAccountDrawer() const wasAccountDrawerOpen = usePrevious(accountDrawer.isOpen) const scrollRef = useRef(null) + const modalRef = useRef(null) + const isUniExtensionAvailable = useIsUniExtensionAvailable() + const [web3StatusRef] = useAtom(Web3StatusRef) + + useOnClickOutside( + modalRef, + () => { + if (isUniExtensionAvailable) { + accountDrawer.close() + } + }, + // Prevents quick close & re-open when tapping the Web3Status + // stopPropagation does not work here + web3StatusRef ? [web3StatusRef] : [], + ) + useEffect(() => { if (wasAccountDrawerOpen && !accountDrawer.isOpen) { scrollRef.current?.scrollTo({ top: 0, behavior: 'smooth' }) @@ -226,8 +276,8 @@ function AccountDrawer() { }) return ( - - {accountDrawer.isOpen && ( + + {accountDrawer.isOpen && !isUniExtensionAvailable && ( @@ -236,6 +286,9 @@ function AccountDrawer() { )} ` align-items: center; border-radius: 1.25rem; border: 1px solid ${({ error, theme }) => (error ? theme.critical : theme.surface3)}; - transition: border-color 300ms ${({ error }) => (error ? 'step-end' : 'step-start')}, + transition: + border-color 300ms ${({ error }) => (error ? 'step-end' : 'step-start')}, color 500ms ${({ error }) => (error ? 'step-end' : 'step-start')}; background-color: ${({ theme }) => theme.surface1}; ` @@ -95,7 +96,7 @@ export default function AddressInputPanel({ const withoutSpaces = input.replace(/\s+/g, '') onChange(withoutSpaces) }, - [onChange] + [onChange], ) const error = Boolean(value.length > 0 && !loading && !address) diff --git a/apps/web/src/components/AddressQRModal/index.tsx b/apps/web/src/components/AddressQRModal/index.tsx new file mode 100644 index 00000000000..4c9ba407b31 --- /dev/null +++ b/apps/web/src/components/AddressQRModal/index.tsx @@ -0,0 +1,91 @@ +import { AddressDisplay } from 'components/AccountDetails/AddressDisplay' +import { SecondaryIdentifiers } from 'components/AccountDrawer/Status' +import { useAvatarColorProps } from 'components/AddressQRModal/useAvatarColorProps' +import Identicon from 'components/Identicon' +import { GetHelpHeader } from 'components/Modal/GetHelpHeader' +import { PRODUCTION_CHAIN_IDS } from 'constants/chains' +import useENSName from 'hooks/useENSName' +import { Trans } from 'i18n' +import styled from 'lib/styled-components' +import { useCallback } from 'react' +import { useModalIsOpen, useOpenModal, useToggleModal } from 'state/application/hooks' +import { ApplicationModal } from 'state/application/reducer' +import { ExternalLink, ThemedText } from 'theme/components' +import { AdaptiveWebModalSheet, Flex, QRCodeDisplay, Text, useSporeColors } from 'ui/src' +import { NetworkLogos } from 'uniswap/src/components/network/NetworkLogos' +import { useUnitagByAddress } from 'uniswap/src/features/unitags/hooks' + +const HelpCenterLink = styled(ExternalLink)` + font-size: 14px; + margin: 4px auto 0 auto; +` + +const UNICON_SIZE = 50 +const QR_CODE_SIZE = 240 + +export function AddressQRModal({ accountAddress }: { accountAddress: Address }) { + const colors = useSporeColors() + const toggleModal = useToggleModal(ApplicationModal.RECEIVE_CRYPTO_QR) + const isOpen = useModalIsOpen(ApplicationModal.RECEIVE_CRYPTO_QR) + const openReceiveCryptoModal = useOpenModal(ApplicationModal.RECEIVE_CRYPTO) + const { ENSName } = useENSName(accountAddress) + const { unitag } = useUnitagByAddress(accountAddress) + const hasSecondaryIdentifier = ENSName || unitag?.username + const { smartColor } = useAvatarColorProps(accountAddress) + + const goBack = useCallback(() => { + toggleModal() + openReceiveCryptoModal() + }, [toggleModal, openReceiveCryptoModal]) + + return ( + + + + + + {hasSecondaryIdentifier && ( + + + + )} + + + + + + + + + + + + + + + + + + + + ) +} diff --git a/apps/web/src/components/AddressQRModal/useAvatarColorProps.tsx b/apps/web/src/components/AddressQRModal/useAvatarColorProps.tsx new file mode 100644 index 00000000000..9cf7c4b2381 --- /dev/null +++ b/apps/web/src/components/AddressQRModal/useAvatarColorProps.tsx @@ -0,0 +1,82 @@ +import useENSAvatar from 'hooks/useENSAvatar' +import { useMemo } from 'react' +import { + GradientProps, + getUniconColors, + passesContrast, + useExtractedColors, + useIsDarkMode, + useSporeColors, +} from 'ui/src' +import { useUnitagByAddress } from 'uniswap/src/features/unitags/hooks' + +// Fetches avatar for address, in priority uses: unitag avatar, ens avatar, undefined +// Note that this hook is used instead of just useENSAvatar because our implementation +// of useENSAvatar checks for reverse name resolution which Unitags does not support. +// Chose to do this because even if we used useENSAvatar without reverse name resolution, +// there is more latency because it has to go to the contract via CCIP-read first. +function useAvatar(address: string | undefined): { + avatar: Maybe + loading: boolean +} { + const { unitag, loading: unitagLoading } = useUnitagByAddress(address) + const { avatar: ensAvatar, loading: ensLoading } = useENSAvatar(address) + const unitagAvatar = unitag?.metadata?.avatar + + if (!address) { + return { loading: false, avatar: undefined } + } + + if (unitagAvatar) { + return { avatar: unitagAvatar, loading: false } + } + + if (ensAvatar) { + return { avatar: ensAvatar, loading: false } + } + + return { avatar: undefined, loading: ensLoading || unitagLoading } +} + +type AvatarColors = { + primary: string + base: string + detail: string +} + +type ColorProps = { + smartColor: string + gradientProps: GradientProps +} + +export const useAvatarColorProps = (address: Address): ColorProps => { + const colors = useSporeColors() + const isDarkMode = useIsDarkMode() + const { color: uniconColor } = getUniconColors(address, isDarkMode) as { color: string } + const { avatar, loading: avatarLoading } = useAvatar(address) + const { colors: avatarColors } = useExtractedColors(avatar) as { colors: AvatarColors } + const hasAvatar = !!avatar && !avatarLoading + + const smartColor: string = useMemo(() => { + const contrastThreshold = 3 // WCAG AA standard for contrast + const backgroundColor = colors.surface2.val // replace with your actual background color + + if (hasAvatar && avatarColors && avatarColors.primary) { + if (passesContrast(avatarColors.primary, backgroundColor, contrastThreshold)) { + return avatarColors.primary + } + if (passesContrast(avatarColors.base, backgroundColor, contrastThreshold)) { + return avatarColors.base + } + if (passesContrast(avatarColors.detail, backgroundColor, contrastThreshold)) { + return avatarColors.detail + } + // Modify the color if it doesn't pass the contrast check + // Replace 'modifiedColor' with the actual color you want to use + return colors.neutral1.val as string + } + return uniconColor + }, [avatarColors, hasAvatar, uniconColor, colors.surface2.val, colors.neutral1.val]) + + return { smartColor, gradientProps: {} } +} diff --git a/apps/web/src/components/Badge/RangeBadge.tsx b/apps/web/src/components/Badge/RangeBadge.tsx index c1a21524d40..5befc16a237 100644 --- a/apps/web/src/components/Badge/RangeBadge.tsx +++ b/apps/web/src/components/Badge/RangeBadge.tsx @@ -1,7 +1,7 @@ import { MouseoverTooltip } from 'components/Tooltip' import { Trans } from 'i18n' +import styled, { useTheme } from 'lib/styled-components' import { AlertTriangle, Slash } from 'react-feather' -import styled, { useTheme } from 'styled-components' const BadgeWrapper = styled.div` font-size: 14px; diff --git a/apps/web/src/components/Badge/index.tsx b/apps/web/src/components/Badge/index.tsx index 50ebce1f4df..7a816e62739 100644 --- a/apps/web/src/components/Badge/index.tsx +++ b/apps/web/src/components/Badge/index.tsx @@ -1,6 +1,6 @@ +import styled, { DefaultTheme } from 'lib/styled-components' import { readableColor } from 'polished' import { PropsWithChildren } from 'react' -import styled, { DefaultTheme } from 'styled-components' export enum BadgeVariant { DEFAULT = 'DEFAULT', diff --git a/apps/web/src/components/Banner/Outage/OutageBanner.tsx b/apps/web/src/components/Banner/Outage/OutageBanner.tsx index 33bcbae705e..652b2f229e4 100644 --- a/apps/web/src/components/Banner/Outage/OutageBanner.tsx +++ b/apps/web/src/components/Banner/Outage/OutageBanner.tsx @@ -2,9 +2,9 @@ import { Container, PopupContainer, StyledXButton, TextContainer } from 'compone import { chainIdToBackendChain } from 'constants/chains' import { ChainOutageData } from 'featureFlags/flags/outageBanner' import { Trans } from 'i18n' +import styled, { useTheme } from 'lib/styled-components' import { useState } from 'react' import { Globe } from 'react-feather' -import styled, { useTheme } from 'styled-components' import { ExternalLink, ThemedText } from 'theme/components' import { capitalize } from 'tsafe' import { UniverseChainId } from 'uniswap/src/types/chains' diff --git a/apps/web/src/components/Banner/shared/styled.tsx b/apps/web/src/components/Banner/shared/styled.tsx index a9633549142..71fb9a6999c 100644 --- a/apps/web/src/components/Banner/shared/styled.tsx +++ b/apps/web/src/components/Banner/shared/styled.tsx @@ -1,6 +1,6 @@ import { OpacityHoverState } from 'components/Common' +import styled from 'lib/styled-components' import { X } from 'react-feather' -import styled from 'styled-components' import { BREAKPOINTS } from 'theme' import { Z_INDEX } from 'theme/zIndex' diff --git a/apps/web/src/components/BreadcrumbNav/index.test.tsx b/apps/web/src/components/BreadcrumbNav/index.test.tsx index a33299e783d..9758a3f0aad 100644 --- a/apps/web/src/components/BreadcrumbNav/index.test.tsx +++ b/apps/web/src/components/BreadcrumbNav/index.test.tsx @@ -15,7 +15,7 @@ describe('BreadcrumbNav', () => { symbol: 'WBTC', }) const { asFragment } = render( - + , ) expect(asFragment()).toMatchSnapshot() diff --git a/apps/web/src/components/BreadcrumbNav/index.tsx b/apps/web/src/components/BreadcrumbNav/index.tsx index ce151ee05ed..b74d044ff4e 100644 --- a/apps/web/src/components/BreadcrumbNav/index.tsx +++ b/apps/web/src/components/BreadcrumbNav/index.tsx @@ -4,10 +4,10 @@ import Tooltip, { TooltipSize } from 'components/Tooltip' import { useScreenSize } from 'hooks/screenSize' import useCopyClipboard from 'hooks/useCopyClipboard' import { Trans, t } from 'i18n' +import styled, { useTheme } from 'lib/styled-components' import { useCallback, useState } from 'react' import { Copy } from 'react-feather' import { Link } from 'react-router-dom' -import styled, { useTheme } from 'styled-components' import { ClickableStyle } from 'theme/components' import { shortenAddress } from 'utilities/src/addresses' diff --git a/apps/web/src/components/Button/GetHelp.tsx b/apps/web/src/components/Button/GetHelp.tsx index e09be814654..fa242990aef 100644 --- a/apps/web/src/components/Button/GetHelp.tsx +++ b/apps/web/src/components/Button/GetHelp.tsx @@ -1,7 +1,7 @@ import { EnvelopeHeartIcon } from 'components/Icons/EnvelopeHeart' import Row from 'components/Row' import { Trans } from 'i18n' -import styled from 'styled-components' +import styled from 'lib/styled-components' import { ExternalLink } from 'theme/components' import { uniswapUrls } from 'uniswap/src/constants/urls' diff --git a/apps/web/src/components/Button/index.tsx b/apps/web/src/components/Button/index.tsx index 8d751d77aa1..51c4e2d8a1b 100644 --- a/apps/web/src/components/Button/index.tsx +++ b/apps/web/src/components/Button/index.tsx @@ -1,9 +1,9 @@ import { RowBetween } from 'components/Row' +import styled, { DefaultTheme, useTheme } from 'lib/styled-components' import { darken } from 'polished' import { forwardRef } from 'react' import { Check, ChevronDown } from 'react-feather' import { ButtonProps as ButtonPropsOriginal, Button as RebassButton } from 'rebass/styled-components' -import styled, { DefaultTheme, useTheme } from 'styled-components' export { default as LoadingButtonSpinner } from './LoadingButtonSpinner' @@ -530,7 +530,7 @@ type ThemeButtonRef = HTMLButtonElement export const ThemeButton = forwardRef(function ThemeButton( { children, ...rest }, - ref + ref, ) { return ( diff --git a/apps/web/src/components/Card/index.tsx b/apps/web/src/components/Card/index.tsx index 9fb4767ff03..b53036bd0a0 100644 --- a/apps/web/src/components/Card/index.tsx +++ b/apps/web/src/components/Card/index.tsx @@ -1,5 +1,5 @@ +import styled from 'lib/styled-components' import { Box } from 'rebass/styled-components' -import styled from 'styled-components' const Card = styled(Box)<{ width?: string; padding?: string; border?: string; $borderRadius?: string }>` width: ${({ width }) => width ?? '100%'}; diff --git a/apps/web/src/components/Charts/ChartHeader.tsx b/apps/web/src/components/Charts/ChartHeader.tsx index 8104a75884f..138877d5c65 100644 --- a/apps/web/src/components/Charts/ChartHeader.tsx +++ b/apps/web/src/components/Charts/ChartHeader.tsx @@ -2,9 +2,9 @@ import { useHeaderDateFormatter } from 'components/Charts/hooks' import Column from 'components/Column' import Row from 'components/Row' import { getProtocolColor, getProtocolName } from 'graphql/data/util' +import styled, { useTheme } from 'lib/styled-components' import { UTCTimestamp } from 'lightweight-charts' import { ReactElement, ReactNode } from 'react' -import styled, { useTheme } from 'styled-components' import { EllipsisStyle } from 'theme/components' import { ThemedText } from 'theme/components/text' import { textFadeIn } from 'theme/styles' diff --git a/apps/web/src/components/Charts/ChartModel.tsx b/apps/web/src/components/Charts/ChartModel.tsx index 9a1dff5f76b..683cbf24c94 100644 --- a/apps/web/src/components/Charts/ChartModel.tsx +++ b/apps/web/src/components/Charts/ChartModel.tsx @@ -8,6 +8,7 @@ import { useActiveLocale } from 'hooks/useActiveLocale' import { useOnClickOutside } from 'hooks/useOnClickOutside' import { Trans } from 'i18n' import { useUpdateAtom } from 'jotai/utils' +import styled, { DefaultTheme, useTheme } from 'lib/styled-components' import { BarPrice, CrosshairMode, @@ -20,7 +21,6 @@ import { createChart, } from 'lightweight-charts' import { ReactElement, useEffect, useMemo, useRef, useState } from 'react' -import styled, { DefaultTheme, useTheme } from 'styled-components' import { ThemedText } from 'theme/components' import { textFadeIn } from 'theme/styles' import { Z_INDEX } from 'theme/zIndex' @@ -146,7 +146,7 @@ export abstract class ChartModel { /** Updates the chart without re-creating it or resetting pan/zoom. */ public updateOptions( { locale, theme, format, isLargeScreen, onCrosshairMove }: ChartModelParams, - nonDefaultChartOptions?: DeepPartial + nonDefaultChartOptions?: DeepPartial, ) { this.onCrosshairMove = onCrosshairMove @@ -253,7 +253,7 @@ export function Chart, TDataType e const { md: isLargeScreen } = useScreenSize() const modelParams = useMemo( () => ({ ...params, format, theme, locale, isLargeScreen, onCrosshairMove: setCrosshairData }), - [format, isLargeScreen, locale, params, theme] + [format, isLargeScreen, locale, params, theme], ) // Chart model state should not affect React render cycles since the chart canvas is drawn outside of React, so we store via ref diff --git a/apps/web/src/components/Charts/LiquidityChart/index.tsx b/apps/web/src/components/Charts/LiquidityChart/index.tsx index 08fe61c4f18..9e7369b18ba 100644 --- a/apps/web/src/components/Charts/LiquidityChart/index.tsx +++ b/apps/web/src/components/Charts/LiquidityChart/index.tsx @@ -128,7 +128,7 @@ async function calculateActiveRangeTokensLocked( sqrtPriceX96?: JSBI currentTick?: number liquidity?: JSBI - } + }, ): Promise<{ amount0Locked: number; amount1Locked: number } | undefined> { if (!poolData.currentTick || !poolData.sqrtPriceX96 || !poolData.liquidity) { return undefined @@ -159,7 +159,7 @@ async function calculateActiveRangeTokensLocked( poolData.sqrtPriceX96, tick.liquidityActive, poolData.currentTick, - mockTicks + mockTicks, ) // Calculate amount of token0 that would need to be swapped to reach the bottom of the range const bottomOfRangePrice = TickMath.getSqrtRatioAtTick(mockTicks[0].index) @@ -182,7 +182,7 @@ async function calculateTokensLocked( token0: Token, token1: Token, feeTier: FeeAmount, - tick: TickProcessed + tick: TickProcessed, ): Promise<{ amount0Locked: number; amount1Locked: number }> { try { const tickSpacing = TICK_SPACINGS[feeTier] @@ -300,7 +300,7 @@ export function useLiquidityBarData({ tokenB, feeTier, ticksProcessed[activeRangeIndex], - activePoolData + activePoolData, ) barData[activeRangeIndex] = { ...activeRangeData, ...activeTickTvl } } diff --git a/apps/web/src/components/Charts/LiquidityChart/renderer.tsx b/apps/web/src/components/Charts/LiquidityChart/renderer.tsx index 2b130f0e186..77dfece4b0a 100644 --- a/apps/web/src/components/Charts/LiquidityChart/renderer.tsx +++ b/apps/web/src/components/Charts/LiquidityChart/renderer.tsx @@ -79,7 +79,7 @@ export class LiquidityBarSeriesRenderer implemen this._data.barSpacing, renderingScope.horizontalPixelRatio, this._data.visibleRange.from, - this._data.visibleRange.to + this._data.visibleRange.to, ) const zeroY = priceToCoordinate(0) ?? 0 ctx.fillStyle = this._options.tokenAColor @@ -95,7 +95,7 @@ export class LiquidityBarSeriesRenderer implemen } const width = Math.min( Math.max(renderingScope.horizontalPixelRatio, column.right - column.left), - this._data.barSpacing * renderingScope.horizontalPixelRatio + this._data.barSpacing * renderingScope.horizontalPixelRatio, ) // Create margin to make visual bars thin diff --git a/apps/web/src/components/Charts/LoadingState.tsx b/apps/web/src/components/Charts/LoadingState.tsx index c09dcaaaf35..98db0c3e61d 100644 --- a/apps/web/src/components/Charts/LoadingState.tsx +++ b/apps/web/src/components/Charts/LoadingState.tsx @@ -3,9 +3,9 @@ import Column from 'components/Column' import Row from 'components/Row' import { MissingDataIcon } from 'components/Table/icons' import { Trans } from 'i18n' +import styled, { useTheme } from 'lib/styled-components' import { lighten } from 'polished' import { PropsWithChildren, ReactNode } from 'react' -import styled, { useTheme } from 'styled-components' import { ThemedText } from 'theme/components' import { textFadeIn } from 'theme/styles' import { opacify } from 'theme/utils' diff --git a/apps/web/src/components/Charts/PriceChart/RoundedCandlestickSeries/renderer.ts b/apps/web/src/components/Charts/PriceChart/RoundedCandlestickSeries/renderer.ts index bd8dd76df47..c273c65e547 100644 --- a/apps/web/src/components/Charts/PriceChart/RoundedCandlestickSeries/renderer.ts +++ b/apps/web/src/components/Charts/PriceChart/RoundedCandlestickSeries/renderer.ts @@ -75,7 +75,7 @@ export class RoundedCandleSeriesRenderer + visibleRange: Range, ): void { if (this._data === null || this._options === null) { return @@ -99,7 +99,7 @@ export class RoundedCandleSeriesRenderer, - radius: number + radius: number, ): void { if (this._data === null || this._options === null) { return @@ -117,7 +117,7 @@ export class RoundedCandleSeriesRenderer { data: T[] diff --git a/apps/web/src/components/Charts/SparklineChart/index.tsx b/apps/web/src/components/Charts/SparklineChart/index.tsx index 1388e9f5a11..58f8e927806 100644 --- a/apps/web/src/components/Charts/SparklineChart/index.tsx +++ b/apps/web/src/components/Charts/SparklineChart/index.tsx @@ -4,8 +4,8 @@ import { LoadingBubble } from 'components/Tokens/loading' import { curveCardinal, scaleLinear } from 'd3' import { SparklineMap, TopToken } from 'graphql/data/TopTokens' import { PricePoint } from 'graphql/data/util' +import styled, { useTheme } from 'lib/styled-components' import { memo } from 'react' -import styled, { useTheme } from 'styled-components' const LoadingContainer = styled.div` height: 100%; @@ -49,11 +49,11 @@ function _SparklineChart({ width, height, tokenData, pricePercentChange, sparkli const widthScale = scaleLinear() .domain( // the range of possible input values - [startingPrice.timestamp, endingPrice.timestamp] + [startingPrice.timestamp, endingPrice.timestamp], ) .range( // the range of possible output values that the inputs should be transformed to (see https://www.d3indepth.com/scales/ for details) - [0, 110] + [0, 110], ) const { min, max } = getPriceBounds(pricePoints) diff --git a/apps/web/src/components/Charts/StackedLineChart/index.tsx b/apps/web/src/components/Charts/StackedLineChart/index.tsx index ad297d7012e..904826ec0c6 100644 --- a/apps/web/src/components/Charts/StackedLineChart/index.tsx +++ b/apps/web/src/components/Charts/StackedLineChart/index.tsx @@ -3,6 +3,7 @@ import { Chart, ChartModel, ChartModelParams } from 'components/Charts/ChartMode import { StackedAreaSeriesOptions } from 'components/Charts/StackedLineChart/stacked-area-series/options' import { StackedAreaSeries } from 'components/Charts/StackedLineChart/stacked-area-series/stacked-area-series' import { getProtocolColor } from 'graphql/data/util' +import { useTheme } from 'lib/styled-components' import { CustomStyleOptions, DeepPartial, @@ -13,7 +14,6 @@ import { WhitespaceData, } from 'lightweight-charts' import { useMemo } from 'react' -import { useTheme } from 'styled-components' import { PriceSource } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' export interface StackedLineData extends WhitespaceData { diff --git a/apps/web/src/components/Charts/StackedLineChart/stacked-area-series/renderer.ts b/apps/web/src/components/Charts/StackedLineChart/stacked-area-series/renderer.ts index c1ee8e3900a..fdd7418c415 100644 --- a/apps/web/src/components/Charts/StackedLineChart/stacked-area-series/renderer.ts +++ b/apps/web/src/components/Charts/StackedLineChart/stacked-area-series/renderer.ts @@ -77,7 +77,7 @@ export class StackedAreaSeriesRenderer implements this._data.visibleRange, renderingScope, zeroY * renderingScope.verticalPixelRatio, - options.hoveredLogicalIndex + options.hoveredLogicalIndex, ) const areaPaths = this._createAreas(linesMeshed) @@ -151,7 +151,7 @@ export class StackedAreaSeriesRenderer implements visibleRange: Range, renderingScope: BitmapCoordinatesRenderingScope, zeroY: number, - hoveredIndex?: number | null + hoveredIndex?: number | null, ) { const { horizontalPixelRatio, verticalPixelRatio } = renderingScope const oddLines: LinePathData[] = [] diff --git a/apps/web/src/components/Charts/TimeSelector.tsx b/apps/web/src/components/Charts/TimeSelector.tsx index 9a8448e5c2f..4563ec74b7e 100644 --- a/apps/web/src/components/Charts/TimeSelector.tsx +++ b/apps/web/src/components/Charts/TimeSelector.tsx @@ -3,7 +3,7 @@ import { MEDIUM_MEDIA_BREAKPOINT } from 'components/Tokens/constants' import { TimePeriod } from 'graphql/data/util' import { atom } from 'jotai' import { useAtomValue } from 'jotai/utils' -import styled from 'styled-components' +import styled from 'lib/styled-components' export const refitChartContentAtom = atom<(() => void) | undefined>(undefined) const DEFAULT_TIME_SELECTOR_OPTIONS = ORDERED_TIMES.map((time: TimePeriod) => ({ time, display: DISPLAYS[time] })) diff --git a/apps/web/src/components/Charts/VolumeChart/CrosshairHighlightPrimitive.tsx b/apps/web/src/components/Charts/VolumeChart/CrosshairHighlightPrimitive.tsx index 580d00f5952..bdac192755e 100644 --- a/apps/web/src/components/Charts/VolumeChart/CrosshairHighlightPrimitive.tsx +++ b/apps/web/src/components/Charts/VolumeChart/CrosshairHighlightPrimitive.tsx @@ -36,7 +36,7 @@ export function positionsLine( positionMedia: number, pixelRatio: number, desiredWidthMedia = 1, - widthIsBitmap?: boolean + widthIsBitmap?: boolean, ): BitmapPositionLength { const scaledPosition = Math.round(pixelRatio * positionMedia) const lineBitmapWidth = widthIsBitmap ? desiredWidthMedia : Math.round(desiredWidthMedia * pixelRatio) @@ -77,7 +77,7 @@ class CrosshairHighlightPaneRenderer implements ISeriesPrimitivePaneRenderer { const margin = Math.min( Math.max(scope.horizontalPixelRatio, crosshairPos.length), - this._data.barSpacing * scope.horizontalPixelRatio + this._data.barSpacing * scope.horizontalPixelRatio, ) * 0.035 const crosshairXPosition = crosshairPos.position + margin @@ -88,7 +88,7 @@ class CrosshairHighlightPaneRenderer implements ISeriesPrimitivePaneRenderer { crosshairYPosition, crosshairPos.length, scope.bitmapSize.height - crosshairYPosition, - 9 + 9, ) ctx.fill() @@ -104,7 +104,7 @@ class CrosshairHighlightPaneRenderer implements ISeriesPrimitivePaneRenderer { crosshairXPosition + crosshairPos.length, crosshairYPosition, scope.bitmapSize.width - (crosshairXPosition + crosshairPos.length), - scope.bitmapSize.height - crosshairYPosition + scope.bitmapSize.height - crosshairYPosition, ) // reset global settings ctx.globalAlpha = 1 diff --git a/apps/web/src/components/Charts/VolumeChart/index.tsx b/apps/web/src/components/Charts/VolumeChart/index.tsx index 8311ed0fbb0..10a8df85615 100644 --- a/apps/web/src/components/Charts/VolumeChart/index.tsx +++ b/apps/web/src/components/Charts/VolumeChart/index.tsx @@ -10,8 +10,8 @@ import { useHeaderDateFormatter } from 'components/Charts/hooks' import { BIPS_BASE } from 'constants/misc' import { TimePeriod, toHistoryDuration } from 'graphql/data/util' import { t } from 'i18n' +import { useTheme } from 'lib/styled-components' import { useMemo } from 'react' -import { useTheme } from 'styled-components' import { ThemedText } from 'theme/components' import { HistoryDuration } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { NumberType, useFormatter } from 'utils/formatNumbers' @@ -140,7 +140,7 @@ export function VolumeChart({ height, data, feeTier, timePeriod, stale }: Volume const params = useMemo( () => ({ data, colors: [theme.accent1], headerHeight: 75, stale }), - [data, stale, theme.accent1] + [data, stale, theme.accent1], ) return ( diff --git a/apps/web/src/components/Charts/VolumeChart/renderer.tsx b/apps/web/src/components/Charts/VolumeChart/renderer.tsx index 4a73913a229..8338c073133 100644 --- a/apps/web/src/components/Charts/VolumeChart/renderer.tsx +++ b/apps/web/src/components/Charts/VolumeChart/renderer.tsx @@ -99,7 +99,7 @@ export class CustomHistogramSeriesRenderer im this._data.barSpacing, renderingScope.horizontalPixelRatio, this._data.visibleRange.from, - this._data.visibleRange.to + this._data.visibleRange.to, ) const zeroY = priceToCoordinate(0) ?? 0 for (let i = this._data.visibleRange.from; i < this._data.visibleRange.to; i++) { @@ -111,7 +111,7 @@ export class CustomHistogramSeriesRenderer im let previousY = zeroY const width = Math.min( Math.max(renderingScope.horizontalPixelRatio, column.right - column.left), - this._data.barSpacing * renderingScope.horizontalPixelRatio + this._data.barSpacing * renderingScope.horizontalPixelRatio, ) // Modification: increase space between bars diff --git a/apps/web/src/components/Charts/VolumeChart/utils.ts b/apps/web/src/components/Charts/VolumeChart/utils.ts index 99b8876c730..30be9b505f2 100644 --- a/apps/web/src/components/Charts/VolumeChart/utils.ts +++ b/apps/web/src/components/Charts/VolumeChart/utils.ts @@ -18,7 +18,7 @@ export function getCumulativeVolume(data: CustomHistogramData[]) { export function getVolumeProtocolInfo( data: StackedHistogramData | undefined, - sources: PriceSource[] + sources: PriceSource[], ): ChartHeaderProtocolInfo[] { const info = new Array() for (const source of sources) { @@ -131,7 +131,7 @@ export interface ColumnPosition { function calculateColumnPosition( xMedia: number, columnData: ColumnCommon, - previousPosition: ColumnPosition | undefined + previousPosition: ColumnPosition | undefined, ): ColumnPosition { const xBitmapUnRounded = xMedia * columnData.horizontalPixelRatio const xBitmap = Math.round(xBitmapUnRounded) @@ -173,7 +173,7 @@ export function calculateColumnPositionsInPlace( barSpacingMedia: number, horizontalPixelRatio: number, startIndex: number, - endIndex: number + endIndex: number, ): void { const common = columnCommon(barSpacingMedia, horizontalPixelRatio) let previous: ColumnPositionItem | undefined = undefined @@ -196,7 +196,7 @@ export function calculateColumnPositionsInPlace( const width = item.column.right - item.column.left + 1 return Math.min(smallest, width) }, - Math.ceil(barSpacingMedia * horizontalPixelRatio) + Math.ceil(barSpacingMedia * horizontalPixelRatio), ) if (common.spacing > 0 && minColumnWidth < alignToMinimalWidthLimit) { ;(items as ColumnPositionItem[]).forEach((item: ColumnPositionItem, index: number) => { diff --git a/apps/web/src/components/Charts/hooks.ts b/apps/web/src/components/Charts/hooks.ts index 096e84f6ecb..ac8d67af507 100644 --- a/apps/web/src/components/Charts/hooks.ts +++ b/apps/web/src/components/Charts/hooks.ts @@ -18,6 +18,6 @@ export function useHeaderDateFormatter() { } return new Date(time * 1000).toLocaleString(locale, headerTimeFormatOptions) }, - [locale] + [locale], ) } diff --git a/apps/web/src/components/Column/index.tsx b/apps/web/src/components/Column/index.tsx index a75d9d60837..9d18001eede 100644 --- a/apps/web/src/components/Column/index.tsx +++ b/apps/web/src/components/Column/index.tsx @@ -1,6 +1,7 @@ -import styled from 'styled-components' +import styled from 'lib/styled-components' import { Gap } from 'theme' +/** @deprecated Please use `Flex` from `ui/src` going forward */ export const Column = styled.div<{ gap?: Gap | string flex?: string @@ -11,6 +12,8 @@ export const Column = styled.div<{ gap: ${({ gap, theme }) => (gap && theme.grids[gap as Gap]) || gap}; ${({ flex }) => flex && `flex: ${flex};`} ` + +/** @deprecated Please use `Flex` from `ui/src` going forward */ export const ColumnCenter = styled(Column)` width: 100%; align-items: center; diff --git a/apps/web/src/components/Common/index.tsx b/apps/web/src/components/Common/index.tsx index 2def1d4a25c..150327fb54c 100644 --- a/apps/web/src/components/Common/index.tsx +++ b/apps/web/src/components/Common/index.tsx @@ -1,4 +1,4 @@ -import { css } from 'styled-components' +import { css } from 'lib/styled-components' export const ScrollBarStyles = css<{ $isHorizontalScroll?: boolean }>` // Firefox scrollbar styling diff --git a/apps/web/src/components/ConfirmSwapModal/Error.tsx b/apps/web/src/components/ConfirmSwapModal/Error.tsx index 3368a2c6864..5f2aa5d6449 100644 --- a/apps/web/src/components/ConfirmSwapModal/Error.tsx +++ b/apps/web/src/components/ConfirmSwapModal/Error.tsx @@ -101,7 +101,7 @@ export default function Error({ errorType, trade, showTrade, swapResult, onRetry href={getExplorerLink( swapResult.response.chainId, swapResult.response.hash, - ExplorerDataType.TRANSACTION + ExplorerDataType.TRANSACTION, )} color="neutral2" > diff --git a/apps/web/src/components/ConfirmSwapModal/Head.test.tsx b/apps/web/src/components/ConfirmSwapModal/Head.test.tsx index ac8f080596e..d70799edc2e 100644 --- a/apps/web/src/components/ConfirmSwapModal/Head.test.tsx +++ b/apps/web/src/components/ConfirmSwapModal/Head.test.tsx @@ -5,7 +5,7 @@ import { fireEvent, render, screen } from 'test-utils/render' describe('ConfirmSwapModal/Head', () => { it('should render correctly for a classic swap', () => { const { asFragment } = render( - + , ) expect(asFragment()).toMatchSnapshot() expect(screen.getByText('Review swap')).toBeInTheDocument() @@ -13,7 +13,7 @@ describe('ConfirmSwapModal/Head', () => { it('should render correctly for a Limit order', () => { const { asFragment } = render( - + , ) expect(asFragment()).toMatchSnapshot() expect(screen.getByText('Review limit')).toBeInTheDocument() @@ -22,7 +22,7 @@ describe('ConfirmSwapModal/Head', () => { it('should call the close callback', () => { const callback = jest.fn() const component = render( - + , ) const button = component.getByTestId('confirmation-close-icon') diff --git a/apps/web/src/components/ConfirmSwapModal/Head.tsx b/apps/web/src/components/ConfirmSwapModal/Head.tsx index f8b485e7b26..b052b9df029 100644 --- a/apps/web/src/components/ConfirmSwapModal/Head.tsx +++ b/apps/web/src/components/ConfirmSwapModal/Head.tsx @@ -1,17 +1,7 @@ -import GetHelpButton from 'components/Button/GetHelp' import { ConfirmModalState } from 'components/ConfirmSwapModal' -import Row from 'components/Row' +import { GetHelpHeader } from 'components/Modal/GetHelpHeader' import { Trans } from 'i18n' -import { X } from 'react-feather' -import styled from 'styled-components' -import { ClickableStyle, ThemedText } from 'theme/components' -import { FadePresence } from 'theme/components/FadePresence' -const CloseIcon = styled(X)<{ onClick: () => void }>` - color: ${({ theme }) => theme.neutral1}; - cursor: pointer; - ${ClickableStyle} -` export function SwapHead({ onDismiss, isLimitTrade, @@ -21,21 +11,12 @@ export function SwapHead({ isLimitTrade: boolean confirmModalState: ConfirmModalState }) { + const swapTitle = isLimitTrade ? : return ( - - {confirmModalState === ConfirmModalState.REVIEWING && ( - - - - {isLimitTrade ? : } - - - - )} - - - - - + ) } diff --git a/apps/web/src/components/ConfirmSwapModal/Modal.tsx b/apps/web/src/components/ConfirmSwapModal/Modal.tsx index 821629c4f81..cdf26104a70 100644 --- a/apps/web/src/components/ConfirmSwapModal/Modal.tsx +++ b/apps/web/src/components/ConfirmSwapModal/Modal.tsx @@ -2,9 +2,9 @@ import { InterfaceModalName } from '@uniswap/analytics-events' import { AutoColumn } from 'components/Column' import { ConfirmModalState } from 'components/ConfirmSwapModal' import Modal from 'components/Modal' +import styled from 'lib/styled-components' import { PropsWithChildren, useRef } from 'react' import { animated, easings, useSpring } from 'react-spring' -import styled from 'styled-components' import { TRANSITION_DURATIONS } from 'theme/styles' import Trace from 'uniswap/src/features/telemetry/Trace' import useResizeObserver from 'use-resize-observer' diff --git a/apps/web/src/components/ConfirmSwapModal/Pending.test.tsx b/apps/web/src/components/ConfirmSwapModal/Pending.test.tsx index e7b5ad08ffd..3a86fa4384b 100644 --- a/apps/web/src/components/ConfirmSwapModal/Pending.test.tsx +++ b/apps/web/src/components/ConfirmSwapModal/Pending.test.tsx @@ -93,11 +93,11 @@ describe('Pending - classic trade titles', () => { revocationPending={revocationPending} wrapTxHash={wrapTxHash} swapResult={swapResult} - /> + />, ) expect(asFragment()).toMatchSnapshot() expect(screen.getByText(expectedTitle)).toBeInTheDocument() - } + }, ) }) @@ -117,10 +117,10 @@ describe('Pending - uniswapX trade titles', () => { revocationPending={revocationPending} wrapTxHash={wrapTxHash} swapResult={swapResult} - /> + />, ) expect(asFragment()).toMatchSnapshot() expect(screen.getByText(expectedTitle)).toBeInTheDocument() - } + }, ) }) diff --git a/apps/web/src/components/ConfirmSwapModal/Pending.tsx b/apps/web/src/components/ConfirmSwapModal/Pending.tsx index dbb545556c0..44bc3149000 100644 --- a/apps/web/src/components/ConfirmSwapModal/Pending.tsx +++ b/apps/web/src/components/ConfirmSwapModal/Pending.tsx @@ -13,12 +13,12 @@ import { useAccount } from 'hooks/useAccount' import { SwapResult } from 'hooks/useSwapCallback' import { useUnmountingAnimation } from 'hooks/useUnmountingAnimation' import { Trans, t } from 'i18n' +import styled, { css } from 'lib/styled-components' import { ReactNode, useMemo, useRef } from 'react' import { InterfaceTrade, TradeFillType } from 'state/routing/types' import { isLimitTrade, isUniswapXTradeType } from 'state/routing/utils' import { useOrder } from 'state/signatures/hooks' import { useIsTransactionConfirmed, useSwapTransactionStatus } from 'state/transactions/hooks' -import styled, { css } from 'styled-components' import { ExternalLink } from 'theme/components' import { AnimationType } from 'theme/components/FadePresence' import { ThemedText } from 'theme/components/text' diff --git a/apps/web/src/components/ConfirmSwapModal/ProgressIndicator.tsx b/apps/web/src/components/ConfirmSwapModal/ProgressIndicator.tsx index 49ad86b5b7a..fc1c1c2f270 100644 --- a/apps/web/src/components/ConfirmSwapModal/ProgressIndicator.tsx +++ b/apps/web/src/components/ConfirmSwapModal/ProgressIndicator.tsx @@ -10,12 +10,12 @@ import { useColor } from 'hooks/useColor' import { SwapResult } from 'hooks/useSwapCallback' import { t } from 'i18n' import useNativeCurrency from 'lib/hooks/useNativeCurrency' +import styled, { useTheme } from 'lib/styled-components' import { useEffect, useMemo, useState } from 'react' import { InterfaceTrade } from 'state/routing/types' import { isLimitTrade, isUniswapXSwapTrade, isUniswapXTradeType } from 'state/routing/utils' import { useOrder } from 'state/signatures/hooks' import { useIsTransactionConfirmed, useSwapTransactionStatus } from 'state/transactions/hooks' -import styled, { useTheme } from 'styled-components' import { colors } from 'theme/colors' import { Divider } from 'theme/components' import { UniswapXOrderStatus } from 'types/uniswapx' @@ -163,7 +163,7 @@ export default function ProgressIndicator({ : uniswapUrls.helpArticleUrls.howToSwapTokens, }, }), - [inputTokenColor, nativeCurrency.symbol, trade, theme.accent1] + [inputTokenColor, nativeCurrency.symbol, trade, theme.accent1], ) if (steps.length === 0) { diff --git a/apps/web/src/components/ConfirmSwapModal/Step.tsx b/apps/web/src/components/ConfirmSwapModal/Step.tsx index 789d5e4e397..f0d398cd26b 100644 --- a/apps/web/src/components/ConfirmSwapModal/Step.tsx +++ b/apps/web/src/components/ConfirmSwapModal/Step.tsx @@ -2,8 +2,8 @@ import Column from 'components/Column' import { CheckMark } from 'components/Icons/CheckMark' import { LoaderV3 } from 'components/Icons/LoadingSpinner' import Row, { RowBetween } from 'components/Row' +import styled, { Keyframes, keyframes } from 'lib/styled-components' import { ReactElement, useEffect, useState } from 'react' -import styled, { Keyframes, keyframes } from 'styled-components' import { ExternalLink, ThemedText } from 'theme/components' export interface StepDetails { diff --git a/apps/web/src/components/ConfirmSwapModal/TradeSummary.tsx b/apps/web/src/components/ConfirmSwapModal/TradeSummary.tsx index 91971afa4fb..678b29c37fb 100644 --- a/apps/web/src/components/ConfirmSwapModal/TradeSummary.tsx +++ b/apps/web/src/components/ConfirmSwapModal/TradeSummary.tsx @@ -1,8 +1,8 @@ import CurrencyLogo from 'components/Logo/CurrencyLogo' import Row from 'components/Row' +import { useTheme } from 'lib/styled-components' import { ArrowRight } from 'react-feather' import { InterfaceTrade } from 'state/routing/types' -import { useTheme } from 'styled-components' import { ThemedText } from 'theme/components' import { useFormatter } from 'utils/formatNumbers' diff --git a/apps/web/src/components/ConfirmSwapModal/__snapshots__/Head.test.tsx.snap b/apps/web/src/components/ConfirmSwapModal/__snapshots__/Head.test.tsx.snap index 3ce8c20dd60..86623136beb 100644 --- a/apps/web/src/components/ConfirmSwapModal/__snapshots__/Head.test.tsx.snap +++ b/apps/web/src/components/ConfirmSwapModal/__snapshots__/Head.test.tsx.snap @@ -2,72 +2,13 @@ exports[`ConfirmSwapModal/Head should render correctly for a Limit order 1`] = ` - .c0 { - box-sizing: border-box; - margin: 0; - min-width: 0; - width: 100%; -} - -.c2 { + .c2 { box-sizing: border-box; margin: 0; min-width: 0; } -.c1 { - width: 100%; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - padding: 0; - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - -webkit-box-pack: start; - -webkit-justify-content: flex-start; - -ms-flex-pack: start; - justify-content: flex-start; -} - .c3 { - width: 100%; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - padding: 0; - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - -webkit-box-pack: left; - -webkit-justify-content: left; - -ms-flex-pack: left; - justify-content: left; -} - -.c6 { - width: 100%; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - padding: 0; - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - -webkit-box-pack: right; - -webkit-justify-content: right; - -ms-flex-pack: right; - justify-content: right; - gap: 10px; -} - -.c9 { width: 100%; display: -webkit-box; display: -webkit-flex; @@ -85,28 +26,25 @@ exports[`ConfirmSwapModal/Head should render correctly for a Limit order 1`] = ` gap: 4px; } -.c5 { +.c4 { color: #222222; - -webkit-letter-spacing: -0.01em; - -moz-letter-spacing: -0.01em; - -ms-letter-spacing: -0.01em; - letter-spacing: -0.01em; + cursor: pointer; + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; } -.c4 { - -webkit-transition: display 250ms ease-in-out, -webkit-transform 250ms ease-in-out; - -webkit-transition: display 250ms ease-in-out, transform 250ms ease-in-out; - transition: display 250ms ease-in-out, transform 250ms ease-in-out; - -webkit-animation: iCPeaJ 250ms ease-in-out forwards; - animation: iCPeaJ 250ms ease-in-out forwards; +.c4:hover { + opacity: 0.6; } -.c4.exiting { - -webkit-animation: bbnGid 250ms ease-in-out; - animation: bbnGid 250ms ease-in-out; +.c4:active { + opacity: 0.4; } -.c7 { +.c0 { -webkit-text-decoration: none; text-decoration: none; cursor: pointer; @@ -117,15 +55,15 @@ exports[`ConfirmSwapModal/Head should render correctly for a Limit order 1`] = ` font-weight: 500; } -.c7:hover { +.c0:hover { opacity: 0.6; } -.c7:active { +.c0:active { opacity: 0.4; } -.c8 { +.c1 { width: -webkit-fit-content; width: -moz-fit-content; width: fit-content; @@ -139,34 +77,16 @@ exports[`ConfirmSwapModal/Head should render correctly for a Limit order 1`] = ` stroke: none; } -.c8:hover { +.c1:hover { background: #22222212; color: #222222; opacity: unset; } -.c8:hover path { +.c1:hover path { fill: #222222; } -.c10 { - color: #222222; - cursor: pointer; - -webkit-text-decoration: none; - text-decoration: none; - cursor: pointer; - -webkit-transition-duration: 125ms; - transition-duration: 125ms; -} - -.c10:hover { - opacity: 0.6; -} - -.c10:active { - opacity: 0.4; -} - @@ -174,33 +94,29 @@ exports[`ConfirmSwapModal/Head should render correctly for a Limit order 1`] = ` class=" t_light _dsp_contents is_Theme" >
        -
        -
        - Review limit -
        -
        + Review limit +
        - .c0 { - box-sizing: border-box; - margin: 0; - min-width: 0; - width: 100%; -} - -.c2 { + .c2 { box-sizing: border-box; margin: 0; min-width: 0; } -.c1 { - width: 100%; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - padding: 0; - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - -webkit-box-pack: start; - -webkit-justify-content: flex-start; - -ms-flex-pack: start; - justify-content: flex-start; -} - .c3 { - width: 100%; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - padding: 0; - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - -webkit-box-pack: left; - -webkit-justify-content: left; - -ms-flex-pack: left; - justify-content: left; -} - -.c6 { - width: 100%; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - padding: 0; - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - -webkit-box-pack: right; - -webkit-justify-content: right; - -ms-flex-pack: right; - justify-content: right; - gap: 10px; -} - -.c9 { width: 100%; display: -webkit-box; display: -webkit-flex; @@ -335,28 +192,25 @@ exports[`ConfirmSwapModal/Head should render correctly for a classic swap 1`] = gap: 4px; } -.c5 { +.c4 { color: #222222; - -webkit-letter-spacing: -0.01em; - -moz-letter-spacing: -0.01em; - -ms-letter-spacing: -0.01em; - letter-spacing: -0.01em; + cursor: pointer; + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; } -.c4 { - -webkit-transition: display 250ms ease-in-out, -webkit-transform 250ms ease-in-out; - -webkit-transition: display 250ms ease-in-out, transform 250ms ease-in-out; - transition: display 250ms ease-in-out, transform 250ms ease-in-out; - -webkit-animation: iCPeaJ 250ms ease-in-out forwards; - animation: iCPeaJ 250ms ease-in-out forwards; +.c4:hover { + opacity: 0.6; } -.c4.exiting { - -webkit-animation: bbnGid 250ms ease-in-out; - animation: bbnGid 250ms ease-in-out; +.c4:active { + opacity: 0.4; } -.c7 { +.c0 { -webkit-text-decoration: none; text-decoration: none; cursor: pointer; @@ -367,15 +221,15 @@ exports[`ConfirmSwapModal/Head should render correctly for a classic swap 1`] = font-weight: 500; } -.c7:hover { +.c0:hover { opacity: 0.6; } -.c7:active { +.c0:active { opacity: 0.4; } -.c8 { +.c1 { width: -webkit-fit-content; width: -moz-fit-content; width: fit-content; @@ -389,34 +243,16 @@ exports[`ConfirmSwapModal/Head should render correctly for a classic swap 1`] = stroke: none; } -.c8:hover { +.c1:hover { background: #22222212; color: #222222; opacity: unset; } -.c8:hover path { +.c1:hover path { fill: #222222; } -.c10 { - color: #222222; - cursor: pointer; - -webkit-text-decoration: none; - text-decoration: none; - cursor: pointer; - -webkit-transition-duration: 125ms; - transition-duration: 125ms; -} - -.c10:hover { - opacity: 0.6; -} - -.c10:active { - opacity: 0.4; -} - @@ -424,33 +260,29 @@ exports[`ConfirmSwapModal/Head should render correctly for a classic swap 1`] = class=" t_light _dsp_contents is_Theme" >
        -
        -
        - Review swap -
        -
        + Review swap +
        { onSelect={onSelect} disabled={disabled} selected={selected} - /> + />, ) await act(() => userEvent.click(screen.getByText(num > 0 ? `+${num}%` : 'Market'))) expect(container.firstChild).toMatchSnapshot() diff --git a/apps/web/src/components/CurrencyInputPanel/LimitPriceInputPanel/LimitPriceButton.tsx b/apps/web/src/components/CurrencyInputPanel/LimitPriceInputPanel/LimitPriceButton.tsx index e8376bc892c..3553728d157 100644 --- a/apps/web/src/components/CurrencyInputPanel/LimitPriceInputPanel/LimitPriceButton.tsx +++ b/apps/web/src/components/CurrencyInputPanel/LimitPriceInputPanel/LimitPriceButton.tsx @@ -1,8 +1,8 @@ import { Percent } from '@uniswap/sdk-core' import Row from 'components/Row' import { Trans } from 'i18n' +import styled, { css } from 'lib/styled-components' import { X } from 'react-feather' -import styled, { css } from 'styled-components' import { ClickableStyle, ThemedText } from 'theme/components' import { useFormatter } from 'utils/formatNumbers' diff --git a/apps/web/src/components/CurrencyInputPanel/LimitPriceInputPanel/LimitPriceInputLabel.tsx b/apps/web/src/components/CurrencyInputPanel/LimitPriceInputPanel/LimitPriceInputLabel.tsx index 5ba0fbd5d64..d4ed05cd83e 100644 --- a/apps/web/src/components/CurrencyInputPanel/LimitPriceInputPanel/LimitPriceInputLabel.tsx +++ b/apps/web/src/components/CurrencyInputPanel/LimitPriceInputPanel/LimitPriceInputLabel.tsx @@ -3,7 +3,7 @@ import CurrencyLogo from 'components/Logo/CurrencyLogo' import Row from 'components/Row' import { PrefetchBalancesWrapper } from 'graphql/data/apollo/TokenBalancesProvider' import { Trans } from 'i18n' -import styled from 'styled-components' +import styled from 'lib/styled-components' import { ClickableStyle, ThemedText } from 'theme/components' import { Text } from 'ui/src' diff --git a/apps/web/src/components/CurrencyInputPanel/LimitPriceInputPanel/LimitPriceInputPanel.test.tsx b/apps/web/src/components/CurrencyInputPanel/LimitPriceInputPanel/LimitPriceInputPanel.test.tsx index 80dc02426dd..2354915fd01 100644 --- a/apps/web/src/components/CurrencyInputPanel/LimitPriceInputPanel/LimitPriceInputPanel.test.tsx +++ b/apps/web/src/components/CurrencyInputPanel/LimitPriceInputPanel/LimitPriceInputPanel.test.tsx @@ -16,6 +16,7 @@ const mockSwapAndLimitContextValue = { setSelectedChainId: jest.fn(), currentTab: SwapTab.Limit, setCurrentTab: jest.fn(), + isSwapAndLimitContext: true, } const mockLimitContextValue = { @@ -53,7 +54,7 @@ describe('LimitPriceInputPanel', () => { const { container } = render( - + , ) expect(screen.getByText('Limit price')).toBeVisible() expect(screen.getByPlaceholderText('0')).toBeVisible() @@ -67,7 +68,7 @@ describe('LimitPriceInputPanel', () => { - + , ) expect(screen.getByText('DAI')).toBeVisible() expect(screen.getByPlaceholderText('0')).toBeVisible() @@ -88,7 +89,7 @@ describe('LimitPriceInputPanel', () => { - + , ) expect(screen.getByText('DAI')).toBeVisible() expect(container.querySelector('.token-symbol-container')).toHaveTextContent('USDC') diff --git a/apps/web/src/components/CurrencyInputPanel/LimitPriceInputPanel/LimitPriceInputPanel.tsx b/apps/web/src/components/CurrencyInputPanel/LimitPriceInputPanel/LimitPriceInputPanel.tsx index 698b84b2f22..0e6f8c8d9ed 100644 --- a/apps/web/src/components/CurrencyInputPanel/LimitPriceInputPanel/LimitPriceInputPanel.tsx +++ b/apps/web/src/components/CurrencyInputPanel/LimitPriceInputPanel/LimitPriceInputPanel.tsx @@ -14,13 +14,13 @@ import CurrencySearchModal from 'components/SearchModal/CurrencySearchModal' import { parseUnits } from 'ethers/lib/utils' import { PrefetchBalancesWrapper } from 'graphql/data/apollo/TokenBalancesProvider' import JSBI from 'jsbi' +import styled from 'lib/styled-components' import { ReversedArrowsIcon } from 'nft/components/icons' import { LIMIT_FORM_CURRENCY_SEARCH_FILTERS } from 'pages/Swap/Limit/LimitForm' import { useCallback, useMemo, useState } from 'react' import { useLimitContext, useLimitPrice } from 'state/limit/LimitContext' import { useSwapAndLimitContext } from 'state/swap/hooks' import { CurrencyState } from 'state/swap/types' -import styled from 'styled-components' import { ClickableStyle, ThemedText } from 'theme/components' import { InterfaceEventNameLocal } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' @@ -112,7 +112,7 @@ export function LimitPriceInputPanel({ onCurrencySelect }: LimitPriceInputPanelP } const oneUnitOfBaseCurrency = CurrencyAmount.fromRawAmount( baseCurrency, - JSBI.BigInt(parseUnits('1', baseCurrency?.decimals)) + JSBI.BigInt(parseUnits('1', baseCurrency?.decimals)), ) const getAdjustedPrice = (priceAdjustmentPercentage: number) => { return new Price({ @@ -121,7 +121,10 @@ export function LimitPriceInputPanel({ onCurrencySelect }: LimitPriceInputPanelP // (100 + adjustmentPercentage) times the market quote amount for 1 input token quoteAmount: CurrencyAmount.fromRawAmount( quoteCurrency, - JSBI.multiply(JSBI.BigInt(100 + priceAdjustmentPercentage), marketPrice.quote(oneUnitOfBaseCurrency).quotient) + JSBI.multiply( + JSBI.BigInt(100 + priceAdjustmentPercentage), + marketPrice.quote(oneUnitOfBaseCurrency).quotient, + ), ), }) } @@ -145,7 +148,7 @@ export function LimitPriceInputPanel({ onCurrencySelect }: LimitPriceInputPanelP } const oneUnitOfBaseCurrency = CurrencyAmount.fromRawAmount( baseCurrency, - JSBI.BigInt(parseUnits('1', baseCurrency?.decimals)) + JSBI.BigInt(parseUnits('1', baseCurrency?.decimals)), ) const marketOutputAmount = adjustedPrice?.quote(oneUnitOfBaseCurrency) setLimitPrice( @@ -154,12 +157,12 @@ export function LimitPriceInputPanel({ onCurrencySelect }: LimitPriceInputPanelP type: NumberType.SwapTradeAmount, placeholder: limitPrice, locale: 'en-US', - }) + }), ) setLimitState((prev) => ({ ...prev, limitPriceEdited: true })) sendAnalyticsEvent(InterfaceEventNameLocal.LimitPresetRateSelected, { value: adjustmentPercentage }) }, - [baseCurrency, limitPrice, setLimitPrice, setLimitState] + [baseCurrency, limitPrice, setLimitPrice, setLimitState], ) const { currentPriceAdjustment } = useCurrentPriceAdjustment({ @@ -188,12 +191,15 @@ export function LimitPriceInputPanel({ onCurrencySelect }: LimitPriceInputPanelP amount: marketPrice .invert() .quote( - CurrencyAmount.fromRawAmount(quoteCurrency, JSBI.BigInt(parseUnits('1', quoteCurrency?.decimals))) + CurrencyAmount.fromRawAmount( + quoteCurrency, + JSBI.BigInt(parseUnits('1', quoteCurrency?.decimals)), + ), ), type: NumberType.SwapTradeAmount, placeholder: '', locale: 'en-US', - }) + }), ) } setLimitState((prev) => ({ ...prev, limitPriceInverted: !prev.limitPriceInverted, limitPriceEdited: true })) @@ -264,7 +270,7 @@ export function LimitPriceInputPanel({ onCurrencySelect }: LimitPriceInputPanelP } onCurrencySelect( limitPriceInverted ? invertCurrencyField(currencySelectModalField) : currencySelectModalField, - currency + currency, ) }} selectedCurrency={quoteCurrency} diff --git a/apps/web/src/components/CurrencyInputPanel/LimitPriceInputPanel/useCurrentPriceAdjustment.ts b/apps/web/src/components/CurrencyInputPanel/LimitPriceInputPanel/useCurrentPriceAdjustment.ts index 39795c4d163..fe36769aa9c 100644 --- a/apps/web/src/components/CurrencyInputPanel/LimitPriceInputPanel/useCurrentPriceAdjustment.ts +++ b/apps/web/src/components/CurrencyInputPanel/LimitPriceInputPanel/useCurrentPriceAdjustment.ts @@ -37,7 +37,7 @@ export function useCurrentPriceAdjustment({ } const oneUnitOfBaseCurrency = CurrencyAmount.fromRawAmount( baseCurrency, - JSBI.BigInt(parseUnits('1', baseCurrency?.decimals)) + JSBI.BigInt(parseUnits('1', baseCurrency?.decimals)), ) const marketQuote = marketPrice.quote(oneUnitOfBaseCurrency) diff --git a/apps/web/src/components/CurrencyInputPanel/SwapCurrencyInputPanel.tsx b/apps/web/src/components/CurrencyInputPanel/SwapCurrencyInputPanel.tsx index ef0558e9c16..ae3c666b5c6 100644 --- a/apps/web/src/components/CurrencyInputPanel/SwapCurrencyInputPanel.tsx +++ b/apps/web/src/components/CurrencyInputPanel/SwapCurrencyInputPanel.tsx @@ -18,13 +18,13 @@ import { useIsSupportedChainId } from 'constants/chains' import { PrefetchBalancesWrapper } from 'graphql/data/apollo/TokenBalancesProvider' import { useAccount } from 'hooks/useAccount' import { Trans } from 'i18n' +import styled, { useTheme } from 'lib/styled-components' import ms from 'ms' import { darken } from 'polished' import { ReactNode, forwardRef, useCallback, useEffect, useState } from 'react' import { Lock } from 'react-feather' import { useCurrencyBalance } from 'state/connection/hooks' import { useSwapAndLimitContext } from 'state/swap/hooks' -import styled, { useTheme } from 'styled-components' import { ThemedText } from 'theme/components' import { flexColumnNoWrap, flexRowNoWrap } from 'theme/styles' import { Text } from 'ui/src' @@ -265,13 +265,13 @@ const SwapCurrencyInputPanel = forwardRef { const [modalOpen, setModalOpen] = useState(false) const account = useAccount() const { chainId } = useSwapAndLimitContext() const chainAllowed = useIsSupportedChainId(chainId) - const selectedCurrencyBalance = useCurrencyBalance(account.address, currency ?? undefined, chainId) + const selectedCurrencyBalance = useCurrencyBalance(account.address, currency ?? undefined) const theme = useTheme() const { formatCurrencyAmount } = useFormatter() @@ -435,7 +435,7 @@ const SwapCurrencyInputPanel = forwardRef ) - } + }, ) SwapCurrencyInputPanel.displayName = 'SwapCurrencyInputPanel' diff --git a/apps/web/src/components/CurrencyInputPanel/index.tsx b/apps/web/src/components/CurrencyInputPanel/index.tsx index adf8244b277..53361fd4510 100644 --- a/apps/web/src/components/CurrencyInputPanel/index.tsx +++ b/apps/web/src/components/CurrencyInputPanel/index.tsx @@ -15,10 +15,10 @@ import { useIsSupportedChainId } from 'constants/chains' import { PrefetchBalancesWrapper } from 'graphql/data/apollo/TokenBalancesProvider' import { useAccount } from 'hooks/useAccount' import { Trans } from 'i18n' +import styled, { useTheme } from 'lib/styled-components' import { darken } from 'polished' import { ReactNode, useCallback, useState } from 'react' import { useCurrencyBalance } from 'state/connection/hooks' -import styled, { useTheme } from 'styled-components' import { BREAKPOINTS } from 'theme' import { ThemedText } from 'theme/components' import { flexColumnNoWrap, flexRowNoWrap } from 'theme/styles' diff --git a/apps/web/src/components/Dialog/Dialog.test.tsx b/apps/web/src/components/Dialog/Dialog.test.tsx index dfbcb1b8cc5..20caa693a01 100644 --- a/apps/web/src/components/Dialog/Dialog.test.tsx +++ b/apps/web/src/components/Dialog/Dialog.test.tsx @@ -35,7 +35,7 @@ describe('', () => { body={mockBody} onCancel={mockOnCancel} buttonsConfig={mockButtonsConfig} - /> + />, ) expect(document.body).toMatchSnapshot() @@ -58,7 +58,7 @@ describe('', () => { body={mockBody} onCancel={mockOnCancel} buttonsConfig={mockButtonsConfig} - /> + />, ) fireEvent.click(screen.getByTestId('Dialog-closeButton')) @@ -75,7 +75,7 @@ describe('', () => { body={mockBody} onCancel={mockOnCancel} buttonsConfig={mockButtonsConfig} - /> + />, ) fireEvent.click(screen.getByText('Left Button')) @@ -92,7 +92,7 @@ describe('', () => { body={mockBody} onCancel={mockOnCancel} buttonsConfig={mockButtonsConfig} - /> + />, ) fireEvent.click(screen.getByText('Right Button')) @@ -109,7 +109,7 @@ describe('', () => { description={mockDescription} body={mockBody} onCancel={mockOnCancel} - /> + />, ) expect(screen.queryByText('Left Button')).not.toBeInTheDocument() expect(screen.queryByText('Right Button')).not.toBeInTheDocument() @@ -134,7 +134,7 @@ describe('', () => { description={mockDescription} body={mockBody} onCancel={mockOnCancel} - /> + />, ) expect(document.body).toMatchSnapshot() diff --git a/apps/web/src/components/Dialog/Dialog.tsx b/apps/web/src/components/Dialog/Dialog.tsx index 841221f4e96..4b591f6f848 100644 --- a/apps/web/src/components/Dialog/Dialog.tsx +++ b/apps/web/src/components/Dialog/Dialog.tsx @@ -1,12 +1,12 @@ import { ButtonEmphasis, ButtonSize, ThemeButton } from 'components/Button' -import GetHelp from 'components/Button/GetHelp' import { ColumnCenter } from 'components/Column' import Modal from 'components/Modal' +import { GetHelpHeader } from 'components/Modal/GetHelpHeader' import Row from 'components/Row' +import styled, { DefaultTheme } from 'lib/styled-components' import { ReactNode } from 'react' -import styled, { DefaultTheme } from 'styled-components' import { Gap } from 'theme' -import { CloseIcon, ThemedText } from 'theme/components' +import { ThemedText } from 'theme/components' export const Container = styled(ColumnCenter)` background-color: ${({ theme }) => theme.surface1}; @@ -49,6 +49,10 @@ const StyledButton = styled(ThemeButton)<{ $color?: keyof DefaultTheme }>` border-radius: 12px; ` +const DialogHeader = styled(GetHelpHeader)` + padding: 4px 0px; +` + export enum DialogButtonType { Primary = 'primary', Error = 'error', @@ -151,10 +155,7 @@ export function Dialog(props: DialogProps) { return ( - - - - + diff --git a/apps/web/src/components/Dialog/__snapshots__/Dialog.test.tsx.snap b/apps/web/src/components/Dialog/__snapshots__/Dialog.test.tsx.snap index 92fe5e2910e..fb2ac6f8b3f 100644 --- a/apps/web/src/components/Dialog/__snapshots__/Dialog.test.tsx.snap +++ b/apps/web/src/components/Dialog/__snapshots__/Dialog.test.tsx.snap @@ -1,40 +1,13 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[` renders different button types 1`] = ` -.c5 { +.c8 { box-sizing: border-box; margin: 0; min-width: 0; - width: 100%; - padding: 4px 0px; } .c9 { - box-sizing: border-box; - margin: 0; - min-width: 0; -} - -.c6 { - width: 100%; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - padding: 0; - -webkit-align-items: center; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - -webkit-box-pack: end; - -webkit-justify-content: end; - -ms-flex-pack: end; - justify-content: end; - padding: 4px 0px; - gap: 10px; -} - -.c10 { width: 100%; display: -webkit-box; display: -webkit-flex; @@ -52,7 +25,7 @@ exports[` renders different button types 1`] = ` gap: 4px; } -.c19 { +.c18 { width: 100%; display: -webkit-box; display: -webkit-flex; @@ -70,7 +43,7 @@ exports[` renders different button types 1`] = ` gap: 12px; } -.c15 { +.c14 { color: #222222; -webkit-letter-spacing: -0.01em; -moz-letter-spacing: -0.01em; @@ -78,7 +51,7 @@ exports[` renders different button types 1`] = ` letter-spacing: -0.01em; } -.c17 { +.c16 { color: #7D7D7D; -webkit-letter-spacing: -0.01em; -moz-letter-spacing: -0.01em; @@ -86,7 +59,7 @@ exports[` renders different button types 1`] = ` letter-spacing: -0.01em; } -.c11 { +.c10 { color: #222222; cursor: pointer; -webkit-text-decoration: none; @@ -96,15 +69,15 @@ exports[` renders different button types 1`] = ` transition-duration: 125ms; } -.c11:hover { +.c10:hover { opacity: 0.6; } -.c11:active { +.c10:active { opacity: 0.4; } -.c7 { +.c6 { -webkit-text-decoration: none; text-decoration: none; cursor: pointer; @@ -115,15 +88,15 @@ exports[` renders different button types 1`] = ` font-weight: 500; } -.c7:hover { +.c6:hover { opacity: 0.6; } -.c7:active { +.c6:active { opacity: 0.4; } -.c23 { +.c22 { background-color: transparent; bottom: 0; border-radius: inherit; @@ -137,7 +110,7 @@ exports[` renders different button types 1`] = ` width: 100%; } -.c20 { +.c19 { -webkit-align-items: center; -webkit-box-align: center; -ms-flex-align: center; @@ -172,30 +145,30 @@ exports[` renders different button types 1`] = ` user-select: none; } -.c20:active .c22 { +.c19:active .c21 { background-color: #B8C0DC3d; } -.c20:focus .c22 { +.c19:focus .c21 { background-color: #B8C0DC3d; } -.c20:hover .c22 { +.c19:hover .c21 { background-color: #98A1C014; } -.c20:disabled { +.c19:disabled { cursor: default; opacity: 0.6; } -.c20:disabled:active .c22, -.c20:disabled:focus .c22, -.c20:disabled:hover .c22 { +.c19:disabled:active .c21, +.c19:disabled:focus .c21, +.c19:disabled:hover .c21 { background-color: transparent; } -.c24 { +.c23 { -webkit-align-items: center; -webkit-box-align: center; -ms-flex-align: center; @@ -230,53 +203,29 @@ exports[` renders different button types 1`] = ` user-select: none; } -.c24:active .c22 { +.c23:active .c21 { background-color: #B8C0DC3d; } -.c24:focus .c22 { +.c23:focus .c21 { background-color: #B8C0DC3d; } -.c24:hover .c22 { +.c23:hover .c21 { background-color: #98A1C014; } -.c24:disabled { +.c23:disabled { cursor: default; opacity: 0.6; } -.c24:disabled:active .c22, -.c24:disabled:focus .c22, -.c24:disabled:hover .c22 { +.c23:disabled:active .c21, +.c23:disabled:focus .c21, +.c23:disabled:hover .c21 { background-color: transparent; } -.c8 { - width: -webkit-fit-content; - width: -moz-fit-content; - width: fit-content; - border-radius: 16px; - padding: 4px 8px; - font-size: 14px; - font-weight: 485; - line-height: 20px; - background: #F9F9F9; - color: #7D7D7D; - stroke: none; -} - -.c8:hover { - background: #22222212; - color: #222222; - opacity: unset; -} - -.c8:hover path { - fill: #222222; -} - .c2 { display: -webkit-box; display: -webkit-flex; @@ -292,7 +241,7 @@ exports[` renders different button types 1`] = ` gap: 24px; } -.c12 { +.c11 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -307,7 +256,7 @@ exports[` renders different button types 1`] = ` gap: 16px; } -.c14 { +.c13 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -373,6 +322,30 @@ exports[` renders different button types 1`] = ` border-radius: 20px; } +.c7 { + width: -webkit-fit-content; + width: -moz-fit-content; + width: fit-content; + border-radius: 16px; + padding: 4px 8px; + font-size: 14px; + font-weight: 485; + line-height: 20px; + background: #F9F9F9; + color: #7D7D7D; + stroke: none; +} + +.c7:hover { + background: #22222212; + color: #222222; + opacity: unset; +} + +.c7:hover path { + fill: #222222; +} + .c4 { background-color: #FFFFFF; outline: 1px solid #22222212; @@ -381,7 +354,7 @@ exports[` renders different button types 1`] = ` width: 100%; } -.c13 { +.c12 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -400,14 +373,14 @@ exports[` renders different button types 1`] = ` border-radius: 12px; } -.c16 { +.c15 { font-size: 24px; line-height: 32px; text-align: center; font-weight: 500; } -.c18 { +.c17 { font-size: 16px; font-weight: 500; line-height: 24px; @@ -418,7 +391,7 @@ exports[` renders different button types 1`] = ` text-align: center; } -.c21 { +.c20 { display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -431,6 +404,10 @@ exports[` renders different button types 1`] = ` border-radius: 12px; } +.c5 { + padding: 4px 0px; +} + @media screen and (max-width:640px) { .c0[data-reach-dialog-overlay] { -webkit-align-items: flex-end; @@ -501,85 +478,88 @@ exports[` renders different button types 1`] = ` class="c2 c3 c4" >
        - -
        - - - - Get help -
        -
        - - - - + + + + Get help +
        + + + + + +
        Mock Icon
        Mock Title
        Mock Description @@ -591,24 +571,24 @@ exports[` renders different button types 1`] = `
        {!hideAcceptButton && ( )} diff --git a/packages/uniswap/src/features/tokens/safetyHooks.ts b/packages/uniswap/src/features/tokens/safetyHooks.ts new file mode 100644 index 00000000000..03ab0c1bf9c --- /dev/null +++ b/packages/uniswap/src/features/tokens/safetyHooks.ts @@ -0,0 +1,14 @@ +import { ThemeKeys } from 'ui/src' +import { SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' + +export function useTokenSafetyLevelColors(safetyLevel: Maybe): ThemeKeys { + switch (safetyLevel) { + case SafetyLevel.MediumWarning: + return 'DEP_accentWarning' + case SafetyLevel.StrongWarning: + return 'statusCritical' + case SafetyLevel.Blocked: + default: + return 'neutral2' + } +} diff --git a/packages/wallet/src/features/tokens/utils.ts b/packages/uniswap/src/features/tokens/utils.ts similarity index 78% rename from packages/wallet/src/features/tokens/utils.ts rename to packages/uniswap/src/features/tokens/utils.ts index 33e08d66719..aa692cf5f19 100644 --- a/packages/wallet/src/features/tokens/utils.ts +++ b/packages/uniswap/src/features/tokens/utils.ts @@ -1,10 +1,7 @@ import { AppTFunction } from 'ui/src/i18n/types' import { SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -export function getTokenSafetyHeaderText( - safetyLevel: Maybe, - t: AppTFunction -): string | undefined { +export function getTokenSafetyHeaderText(safetyLevel: Maybe, t: AppTFunction): string | undefined { switch (safetyLevel) { case SafetyLevel.MediumWarning: return t('token.safetyLevel.medium.header') diff --git a/packages/wallet/src/features/transactions/transactionState/types.ts b/packages/uniswap/src/features/transactions/transactionState/types.ts similarity index 91% rename from packages/wallet/src/features/transactions/transactionState/types.ts rename to packages/uniswap/src/features/transactions/transactionState/types.ts index 6c37c318225..81438a00d19 100644 --- a/packages/wallet/src/features/transactions/transactionState/types.ts +++ b/packages/uniswap/src/features/transactions/transactionState/types.ts @@ -1,4 +1,4 @@ -import { TradeableAsset } from 'wallet/src/entities/assets' +import { TradeableAsset } from 'uniswap/src/entities/assets' export enum CurrencyField { INPUT = 'input', diff --git a/packages/uniswap/src/features/transactions/transfer/types.ts b/packages/uniswap/src/features/transactions/transfer/types.ts new file mode 100644 index 00000000000..3b340c054f2 --- /dev/null +++ b/packages/uniswap/src/features/transactions/transfer/types.ts @@ -0,0 +1,4 @@ +export enum TokenSelectorFlow { + Swap, + Transfer, +} diff --git a/packages/uniswap/src/features/unitags/api.ts b/packages/uniswap/src/features/unitags/api.ts index 71d3e11388c..a32a0da7112 100644 --- a/packages/uniswap/src/features/unitags/api.ts +++ b/packages/uniswap/src/features/unitags/api.ts @@ -37,7 +37,7 @@ export const unitagsApolloClient = new ApolloClient({ export function addQueryParamsToEndpoint( endpoint: string, - params: Record + params: Record, ): string { const url = new URL(endpoint, uniswapUrls.apiOrigin) // dummy base URL, we only need the path with query params Object.entries(params).forEach(([key, value]) => { @@ -49,9 +49,7 @@ export function addQueryParamsToEndpoint( return url.pathname + url.search } -export function useUnitagQuery( - username?: string -): ReturnType> { +export function useUnitagQuery(username?: string): ReturnType> { return useRestQuery>( addQueryParamsToEndpoint('/username', { username }), { username }, // dummy body so that cache key is unique per query params @@ -61,13 +59,11 @@ export function useUnitagQuery( ttlMs: ONE_MINUTE_MS * 2, }, 'GET', - unitagsApolloClient + unitagsApolloClient, ) } -export function useUnitagByAddressQuery( - address?: Address -): ReturnType> { +export function useUnitagByAddressQuery(address?: Address): ReturnType> { return useRestQuery>( addQueryParamsToEndpoint('/address', { address }), { address }, // dummy body so that cache key is unique per query params @@ -77,13 +73,13 @@ export function useUnitagByAddressQuery( ttlMs: ONE_MINUTE_MS * 2, }, 'GET', - unitagsApolloClient + unitagsApolloClient, ) } export function useWaitlistPositionQuery( accounts: Address[], - skip: boolean + skip: boolean, ): ReturnType> { const addresses = accounts.join(',') return useRestQuery>( @@ -95,6 +91,6 @@ export function useWaitlistPositionQuery( ttlMs: ONE_MINUTE_MS * 2, }, 'GET', - unitagsApolloClient + unitagsApolloClient, ) } diff --git a/packages/uniswap/src/features/unitags/context.tsx b/packages/uniswap/src/features/unitags/context.tsx index 0845b1479be..89040e89bfa 100644 --- a/packages/uniswap/src/features/unitags/context.tsx +++ b/packages/uniswap/src/features/unitags/context.tsx @@ -7,9 +7,7 @@ type UnitagUpdaterContextType = { const UnitagUpdaterContext = createContext(null) -export function UnitagUpdaterContextProvider({ - children, -}: PropsWithChildren): JSX.Element { +export function UnitagUpdaterContextProvider({ children }: PropsWithChildren): JSX.Element { const [refetchUnitagsCounter, setRefetchUnitagsCounter] = useState(0) const triggerRefetchUnitags = (): void => { diff --git a/packages/uniswap/src/i18n/locales/source/en-US.json b/packages/uniswap/src/i18n/locales/source/en-US.json index 7302b328532..1ecd82fc906 100644 --- a/packages/uniswap/src/i18n/locales/source/en-US.json +++ b/packages/uniswap/src/i18n/locales/source/en-US.json @@ -129,6 +129,7 @@ "common.button.tryAgain": "Try again", "common.button.understand": "I understand", "common.button.view": "View", + "common.button.yes": "Yes", "common.card.error.description": "Something went wrong", "common.card.error.title": "Oops! Something went wrong.", "common.endAdornment": "and", @@ -147,9 +148,12 @@ "common.navigation.settings": "Settings", "common.navigation.systemSettings": "Settings", "common.text.connected": "Connected", + "common.text.contract": "Contract", "common.text.disconnected": "Disconnected", "common.text.error": "Error", "common.text.notAvailable": "N/A", + "common.text.recipient": "To", + "common.text.sender": "From", "common.text.unknown": "Unknown", "currency.aud": "Australian Dollar", "currency.brl": "Brazilian Real", @@ -171,17 +175,15 @@ "currency.usd": "United States Dollar", "currency.vnd": "Vietnamese Dong", "dapp.request.approve.action": "Approve", - "dapp.request.approve.fallbackTitle": "Approve spending tokens", - "dapp.request.approve.helptext": "Allow this site to access and spend this token from your wallet.", + "dapp.request.approve.fallbackTitle": "Approve this site to access tokens", + "dapp.request.approve.helptext": "Allow this site to access and spend this token for transactions. Make sure you trust this site.", "dapp.request.approve.label": "Wallet", - "dapp.request.approve.title": "Approve spending {{tokenSymbol}}", + "dapp.request.approve.title": "Approve access to {{tokenSymbol}}", "dapp.request.base.title": "Transaction request", "dapp.request.connect.helptext": "Allow this site to view your wallet address, balance, and request approvals for transactions.", "dapp.request.connect.title": "Connect to site", "dapp.request.fallback.calldata.label": "Raw data", "dapp.request.fallback.function.label": "Function", - "dapp.request.fallback.recipient.label": "To", - "dapp.request.fallback.sending.label": "Sending", "dapp.request.permit2.description": "Permit2 manages token approvals across multiple dapps.", "dapp.request.permit2.header": "Sign Permit2", "dapp.request.reject.action": "Reject all", @@ -262,8 +264,6 @@ "extension.warning.storage.message": "Make sure to back up your recovery phrase to prevent losing access to your wallet and funds.", "extension.warning.storage.title": "Your browser is running out of storage", "fiatOnRamp.button.chooseToken": "Choose a token", - "fiatOnRamp.button.continueCheckout": "Continue to checkout", - "fiatOnRamp.checkout.button": "Checkout", "fiatOnRamp.checkout.title": "Checkout with", "fiatOnRamp.connection.message": "Connecting you to {{serviceProvider}}", "fiatOnRamp.connection.quote": "Buying {{amount}} worth of {{currencySymbol}}", @@ -349,6 +349,13 @@ "language.ukrainian": "Ukrainian", "language.urdu": "Urdu", "language.vietnamese": "Vietnamese", + "mobile.appRating.button.decline": "Not really", + "mobile.appRating.description": "Let us know if you’re having a good experience with this app", + "mobile.appRating.feedback.button.cancel": "Maybe later", + "mobile.appRating.feedback.button.send": "Send feedback", + "mobile.appRating.feedback.description": "Let us know how we can improve your experience", + "mobile.appRating.feedback.title": "We’re sorry to hear that.", + "mobile.appRating.title": "Enjoying Uniswap Wallet?", "notification.assetVisibility.hidden": "{{assetName}} hidden", "notification.assetVisibility.unhidden": "{{assetName}} unhidden", "notification.copied.address": "Address copied", @@ -373,6 +380,7 @@ "notification.transaction.approve.success": "Approved {{currencySymbol}} for use with {{address}}.", "notification.transaction.pending": "Transaction pending", "notification.transaction.swap.canceled": "Canceled {{inputCurrencySymbol}}-{{outputCurrencySymbol}} swap.", + "notification.transaction.swap.expired": "{{inputCurrencyAmountWithSymbol}} for {{outputCurrencyAmountWithSymbol}} swap expired.", "notification.transaction.swap.fail": "Failed to swap {{inputCurrencyAmountWithSymbol}} for {{outputCurrencyAmountWithSymbol}}.", "notification.transaction.swap.success": "Swapped {{inputCurrencyAmountWithSymbol}} for {{outputCurrencyAmountWithSymbol}}.", "notification.transaction.transfer.canceled": "Canceled {{tokenNameOrAddress}} send.", @@ -399,13 +407,14 @@ "notifications.scantastic.subtitle": "Continue on Uniswap Extension", "notifications.scantastic.title": "Success!", "onboarding.backup.manual.banner": "It’s best to write this on a piece of paper and store it in a safe place or in a secure password manager.", + "onboarding.backup.manual.error": "Invalid or misspelled word", "onboarding.backup.manual.placeholder": "Secret word", "onboarding.backup.manual.progress": "{{completedStepsCount}}/{{totalStepsCount}} completed", "onboarding.backup.manual.selectedWordPlaceholder": "Select word", - "onboarding.backup.manual.subtitle_one": "What’s the {{count}}st word in your recovery phrase?", - "onboarding.backup.manual.subtitle_two": "What’s the {{count}}nd word in your recovery phrase?", - "onboarding.backup.manual.subtitle_few": "What’s the {{count}}rd word in your recovery phrase?", - "onboarding.backup.manual.subtitle_other": "What’s the {{count}}th word in your recovery phrase?", + "onboarding.backup.manual.subtitle_one": "What’s the {{count}}st word in your recovery phrase?", + "onboarding.backup.manual.subtitle_two": "What’s the {{count}}nd word in your recovery phrase?", + "onboarding.backup.manual.subtitle_few": "What’s the {{count}}rd word in your recovery phrase?", + "onboarding.backup.manual.subtitle_other": "What’s the {{count}}th word in your recovery phrase?", "onboarding.backup.manual.title": "Let’s make sure you’ve recorded it correctly", "onboarding.backup.option.cloud.description": "Encrypt your recovery phrase with a secure password", "onboarding.backup.option.cloud.title": "{{cloudProviderName}} backup", @@ -414,7 +423,7 @@ "onboarding.backup.subtitle": "Backups let you restore your wallet if you delete the app or lose your device", "onboarding.backup.title.existing": "Back up your wallet", "onboarding.backup.title.new": "Choose a backup method", - "onboarding.backup.view.disclaimer": "I understand that if I lose my recovery phrase, Uniswap Labs cannot help me restore it", + "onboarding.backup.view.disclaimer": "I understand that if I lose my recovery phrase, Uniswap Labs cannot help me restore it.", "onboarding.backup.view.subtitle.message1": "Read the following carefully before continuing", "onboarding.backup.view.subtitle.message2": "You’ll need to enter all 12 of these secret words to recover your wallet.", "onboarding.backup.view.title": "Write down your recovery phrase", @@ -431,13 +440,13 @@ "onboarding.complete.pin.description": "Click the pin icon to add Uniswap Extension to your toolbar.", "onboarding.complete.pin.title": "Pin Uniswap Extension", "onboarding.complete.title": "You’re all set", - "onboarding.extension.connectMobile.button": "Import from your phone", - "onboarding.extension.connectMobile.title": "Have the Uniswap mobile app?", "onboarding.extension.getOnTheBetaWaitlist.subtitle": "Download the mobile app to claim a username", "onboarding.extension.getOnTheBetaWaitlist.title": "Get on the Beta waitlist", "onboarding.extension.password.subtitle": "You’ll need this to unlock your wallet and access your recovery phrase", "onboarding.extension.password.title.default": "Create password", "onboarding.extension.password.title.reset": "Reset your password", + "onboarding.extension.unsupported.description": "Uniswap Extension is only compatible with Chrome right now.", + "onboarding.extension.unsupported.title": "This browser is not supported (yet)", "onboarding.import.error.invalidWords_one": "1 word is invalid or misspelled", "onboarding.import.error.invalidWords_other": "{{count}} words are invalid or misspelled", "onboarding.import.method.import.message": "Enter your recovery phrase from another crypto wallet", @@ -461,7 +470,8 @@ "onboarding.importMnemonic.subtitle": "Type or paste your 12-word recovery phrase", "onboarding.importMnemonic.title": "Enter your recovery phrase", "onboarding.intro.button.alreadyHave": "I already have a wallet", - "onboarding.intro.title": "Welcome to \nUniswap Wallet", + "onboarding.intro.mobileScan.button": "Scan QR code to import", + "onboarding.intro.mobileScan.title": "Have the Uniswap app?", "onboarding.introBetaWaitlist.button.checkEligibility": "Check eligibility", "onboarding.introBetaWaitlist.button.letsGo": "Let’s go", "onboarding.introBetaWaitlist.checkEligibilityInstructions": "Enter your uni.eth username below to check if you’re eligible for the Beta.", @@ -470,7 +480,7 @@ "onboarding.introBetaWaitlist.ineligibleExplanation": "You’re still on the waitlist. We’ll notify you in the Uniswap mobile app when you become eligible!", "onboarding.introBetaWaitlist.unitagPlaceholder": "username", "onboarding.landing.button.add": "Add an existing wallet", - "onboarding.landing.button.create": "Create a new wallet", + "onboarding.landing.button.create": "Create a wallet", "onboarding.notification.permission.message": "To receive notifications, turn on notifications for Uniswap Wallet in your device’s settings.", "onboarding.notification.permission.title": "Notifications permission", "onboarding.notification.subtitle": "Get notified when your transfers, swaps, and approvals complete.", @@ -573,11 +583,9 @@ "send.recipient.warning.viewOnly.title": "You have this as a view-only wallet", "send.recipientSelect.search.empty.message": "When you send tokens to a wallet address, they’ll show up here", "send.recipientSelect.search.empty.title": "No wallets saved", - "send.recipientSelect.title": "To", "send.review.modal.title": "You’re sending", "send.review.summary.button.title": "Confirm send", "send.review.summary.sending": "Sending", - "send.review.summary.to": "To", "send.search.empty.subtitle": "The address you typed either does not exist or is spelled incorrectly.", "send.search.empty.title": "No results found", "send.search.placeholder": "Search ENS or address", @@ -612,20 +620,18 @@ "send.warning.viewOnly.title": "This wallet is view-only", "setting.recoveryPhrase.account.show": "Show recovery phrase", "setting.recoveryPhrase.action.hide": "Hide recovery phrase", - "setting.recoveryPhrase.remove.button": "Remove recovery phrase", - "setting.recoveryPhrase.remove.confirm.subtitle": "I understand that Uniswap Labs can’t help me recover my wallet if I failed to do so", + "setting.recoveryPhrase.remove": "Remove recovery phrase", + "setting.recoveryPhrase.remove.confirm.subtitle": "I understand that Uniswap Labs can’t help me recover my wallet if I failed to do so.", "setting.recoveryPhrase.remove.confirm.title": "I saved my recovery phrase", - "setting.recoveryPhrase.remove.initial.subtitle": "Make sure you’ve saved your recovery phrase. You will lose access to your funds otherwise", + "setting.recoveryPhrase.remove.initial.subtitle": "Make sure you’ve saved your recovery phrase. You will lose access to your wallets otherwise", "setting.recoveryPhrase.remove.initial.title": "Before you continue", "setting.recoveryPhrase.remove.password.error": "Wrong password. Try again", - "setting.recoveryPhrase.remove.password.input": "Enter password", - "setting.recoveryPhrase.remove.subtitle": "Enter your password to continue", + "setting.recoveryPhrase.remove.subtitle": "Enter your password to confirm", "setting.recoveryPhrase.remove.title": "You’re removing your recovery phrase", - "setting.recoveryPhrase.view.error": "Wrong password, try again", "setting.recoveryPhrase.view.warning.message1": "Anyone who knows your recovery phrase can access your wallet and funds", "setting.recoveryPhrase.view.warning.message2": "View this in private", - "setting.recoveryPhrase.view.warning.message3": "Do not share this with anyone", - "setting.recoveryPhrase.view.warning.message4": "Never enter it to any websites or apps", + "setting.recoveryPhrase.view.warning.message3": "Do not share with anyone", + "setting.recoveryPhrase.view.warning.message4": "Never enter it to any websites or applications", "setting.recoveryPhrase.view.warning.title": "Before you continue", "setting.recoveryPhrase.warning.screenshot.message": "Anyone who gains access to your photos can access your wallet. We recommend that you write down your words instead.", "setting.recoveryPhrase.warning.screenshot.title": "Screenshots aren’t secure", @@ -646,7 +652,6 @@ "settings.section.wallet.button.viewLess": "View less", "settings.section.wallet.label.viewOnly": "View-only", "settings.section.wallet.title": "Wallet settings", - "settings.setting.appearance.option.auto": "Auto", "settings.setting.appearance.option.dark.subtitle": "Always use dark mode", "settings.setting.appearance.option.dark.title": "Dark mode", "settings.setting.appearance.option.device.subtitle": "Default to your device’s appearance", @@ -710,11 +715,9 @@ "settings.setting.privacy.analytics.description": "We use anonymous usage data to enhance your experience across Uniswap Labs products. When disabled, we only track errors and essential usage.", "settings.setting.privacy.analytics.title": "Allow analytics", "settings.setting.privacy.title": "Privacy", - "settings.setting.recoveryPhrase.remove": "Remove recovery phrase", + "settings.setting.recoveryPhrase.password.title": "Enter your password", "settings.setting.recoveryPhrase.title": "Recovery phrase", - "settings.setting.recoveryPhrase.view": "View recovery phrase", "settings.setting.smallBalances.title": "Hide small balances", - "settings.setting.theme.title": "Theme", "settings.setting.unknownTokens.title": "Hide unknown tokens", "settings.setting.wallet.action.editLabel": "Edit label", "settings.setting.wallet.action.editProfile": "Edit profile", @@ -729,6 +732,8 @@ "settings.version": "Version {{appVersion}}", "swap.button.max": "Max", "swap.button.review": "Review", + "swap.button.submitting": "Submitting swap...", + "swap.button.submitting.keep.open": "Keep your wallet open...", "swap.button.swap": "Swap", "swap.button.unwrap": "Unwrap", "swap.button.view": "View transaction", @@ -793,6 +798,13 @@ "swap.warning.offline.title": "You’re offline", "swap.warning.priceImpact.message": "Due to the amount of {{outputCurrencySymbol}} liquidity currently available, the more {{inputCurrencySymbol}} you try to swap, the less {{outputCurrencySymbol}} you will receive.", "swap.warning.priceImpact.title": "High price impact ({{priceImpactValue}})", + "swap.warning.queuedOrder.appClosed": "Your transaction wasn’t submitted because you closed the app.", + "swap.warning.queuedOrder.approvalFailed": "Your transaction wasn’t submitted because token approval failed.", + "swap.warning.queuedOrder.stale": "Your transaction wasn’t submitted because you closed the app or approval took too long.", + "swap.warning.queuedOrder.submissionFailed": "There was a problem submitting your transaction.", + "swap.warning.queuedOrder.title": "Swap cancelled", + "swap.warning.queuedOrder.wrap.message": "Your ETH will remain wrapped as WETH.", + "swap.warning.queuedOrder.wrapFailed": "Your transaction wasn’t submitted because the wrap transaction failed.", "swap.warning.rateLimit.message": "Please try again in a few minutes.", "swap.warning.rateLimit.title": "Rate limit exceeded", "swap.warning.router.message": "You may have lost connection or the network may be down. If the problem persists, please try again later.", @@ -805,7 +817,6 @@ "token.balances.other": "Balances on other networks", "token.balances.viewOnly": "{{ownerAddress}}’s balance", "token.error.unknown": "Unknown token", - "token.links.contract": "Contract", "token.links.title": "Links", "token.links.twitter": "Twitter", "token.links.website": "Website", @@ -835,6 +846,7 @@ "tokens.action.hide": "Hide Token", "tokens.action.unhide": "Unhide Token", "tokens.hidden.label": "Hidden ({{numHidden}})", + "tokens.nfts.action.viewOnExplorer": "View on {{blockExplorerName}}", "tokens.nfts.collection.error.load.title": "Couldn’t load NFT collection", "tokens.nfts.collection.label.items": "Items", "tokens.nfts.collection.label.owners": "Owners", @@ -884,6 +896,8 @@ "transaction.amount.unlimited": "Unlimited", "transaction.currency.unknown": "unknown token", "transaction.date": "Submitted on {{date}}", + "transaction.details.dappName": "App", + "transaction.details.from": "From", "transaction.details.networkFee": "Network cost", "transaction.details.transactionId": "Transaction ID", "transaction.network.all": "All networks", @@ -953,7 +967,9 @@ "transaction.status.send.successDapp": "Sent on {{externalDappName}}", "transaction.status.swap.canceled": "Canceled swap", "transaction.status.swap.canceling": "Canceling swap", + "transaction.status.swap.expired": "Swap expired", "transaction.status.swap.failed": "Failed to swap", + "transaction.status.swap.insufficientFunds": "Insufficient funds", "transaction.status.swap.pending": "Swapping", "transaction.status.swap.success": "Swapped", "transaction.status.swap.successDapp": "Swapped on {{externalDappName}}", @@ -1071,7 +1087,6 @@ "walletConnect.request.button.scrollDown": "Scroll down to sign", "walletConnect.request.button.sign": "Sign", "walletConnect.request.details.label.function": "Function", - "walletConnect.request.details.label.recipient": "To", "walletConnect.request.details.label.sending": "Sending", "walletConnect.request.error.insufficientFunds": "You don’t have enough {{currencySymbol}} to complete this transaction.", "walletConnect.request.error.network": "Internet or network connection error", diff --git a/packages/uniswap/src/react-native-dotenv.d.ts b/packages/uniswap/src/react-native-dotenv.d.ts index b6afc5f4cbb..aa54c55db59 100644 --- a/packages/uniswap/src/react-native-dotenv.d.ts +++ b/packages/uniswap/src/react-native-dotenv.d.ts @@ -3,9 +3,6 @@ declare module 'react-native-dotenv' { export const APPSFLYER_API_KEY: string export const APPSFLYER_APP_ID: string - export const MOONPAY_API_KEY: string - export const MOONPAY_API_URL: string - export const MOONPAY_WIDGET_API_URL: string export const UNISWAP_API_KEY: string export const INFURA_KEY: string export const INFURA_PROJECT_ID: string diff --git a/packages/uniswap/src/test/fixtures/index.ts b/packages/uniswap/src/test/fixtures/index.ts index def94ce4501..4a4a8b3333c 100644 --- a/packages/uniswap/src/test/fixtures/index.ts +++ b/packages/uniswap/src/test/fixtures/index.ts @@ -1 +1,2 @@ export * from './events' +export * from './wallet' diff --git a/packages/uniswap/src/test/fixtures/testIDs.ts b/packages/uniswap/src/test/fixtures/testIDs.ts new file mode 100644 index 00000000000..f5554a7350c --- /dev/null +++ b/packages/uniswap/src/test/fixtures/testIDs.ts @@ -0,0 +1,68 @@ +/** + * IDs for testing purposes + */ +export const TestID = { + AccountCard: 'account-card', + AccountHeaderAvatar: 'account-header-avatar', + AccountHeaderCopyAddress: 'account-header-copy-address', + ActivityTab: 'activity-tab', + AddCloudBackup: 'add-cloud-backup', + AddManualBackup: 'add-manual-backup', + AmountInputIn: 'amount-input-in', + AmountInputOut: 'amount-input-out', + Back: 'back', + Cancel: 'cancal', + ChooseInputToken: 'choose-input-token', + ChooseOutputToken: 'choose-output-token', + Confirm: 'confirm', + Continue: 'continue', + Copy: 'copy', + CreateAccount: 'create-account', + Done: 'done', + Edit: 'edit', + Favorite: 'favorite', + ImportAccount: 'import-account', + ImportAccountInput: 'import-account-input', + Next: 'next', + NFTsTab: 'NFTs-tab', + NotificationToastTitle: 'notification-toast-title', + OK: 'ok', + OnboardingImportSeedPhrase: 'onboarding-import-seed-phrase', + OpenDeviceLanguageSettings: 'open-device-language-settings', + QRCodeModalToggle: 'qr-code-modal-toggle', + PortfolioBalance: 'portfolio-balance', + PortfolioRelativeChange: 'portfolio-relative-change', + Remove: 'remove', + ReviewSwap: 'review-swap', + RestoreFromCloud: 'restore-from-cloud', + RestoreWallet: 'restore-wallet', + ReviewTransfer: 'review-transfer', + SearchEtherscanItem: 'search-etherscan-item', + SearchNFTCollectionItem: 'search-nft-collection-item', + SearchTokenItem: 'search-token-item', + SearchTokensAndWallets: 'search-tokens-and-wallets', + SelectRecipient: 'select-recipient', + Send: 'send', + SendReview: 'send-review', + SetMaxInput: 'set-max-input', + SetMaxOutput: 'set-max-output', + ShowHiddenTokens: 'show-hidden-tokens', + Skip: 'skip', + Submit: 'submit', + Swap: 'swap', + SwapFormHeader: 'swap-form-header', + SwapSettings: 'swap-settings', + SwitchCurrenciesButton: 'switch-currencies-button', + TokenSelectorToggle: 'token-selector-toggle', + TokensTab: 'tokens-tab', + TokenWarningAccept: 'token-warning-accept', + WalletCard: 'wallet-card', + WalletNameInput: 'wallet-name-input', + WalletSettings: 'wallet-settings', + WCDappNetworks: 'wc-dapp-networks', + WCDappSwitchAccount: 'wc-dapp-switch-account', + WatchWallet: 'watch-wallet', + // alphabetize additional values. +} as const + +export type TestIDType = (typeof TestID)[keyof typeof TestID] diff --git a/packages/wallet/src/test/fixtures/wallet/currencies.ts b/packages/uniswap/src/test/fixtures/wallet/currencies.ts similarity index 91% rename from packages/wallet/src/test/fixtures/wallet/currencies.ts rename to packages/uniswap/src/test/fixtures/wallet/currencies.ts index bba01e53745..a17797269c2 100644 --- a/packages/wallet/src/test/fixtures/wallet/currencies.ts +++ b/packages/uniswap/src/test/fixtures/wallet/currencies.ts @@ -1,10 +1,10 @@ import { SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' +import { NativeCurrency } from 'uniswap/src/features/tokens/NativeCurrency' +import { faker } from 'uniswap/src/test/shared' +import { createFixture } from 'uniswap/src/test/utils' import { UniverseChainId } from 'uniswap/src/types/chains' -import { NativeCurrency } from 'wallet/src/features/tokens/NativeCurrency' -import { faker } from 'wallet/src/test/shared' -import { createFixture } from 'wallet/src/test/utils' -import { currencyId } from 'wallet/src/utils/currencyId' +import { currencyId } from 'uniswap/src/utils/currencyId' export const MAINNET_CURRENCY = NativeCurrency.onChain(UniverseChainId.Mainnet) export const BASE_CURRENCY = NativeCurrency.onChain(UniverseChainId.Base) @@ -33,7 +33,7 @@ export const ethCurrencyInfo = createFixture()(() => currencyInfo({ nativeCurrency: MAINNET_CURRENCY, logoUrl: 'https://token-icons.s3.amazonaws.com/eth.png', - }) + }), ) export const uniCurrencyInfo = createFixture()(() => @@ -41,7 +41,7 @@ export const uniCurrencyInfo = createFixture()(() => nativeCurrency: MAINNET_CURRENCY, logoUrl: 'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/logo.png', - }) + }), ) export const daiCurrencyInfo = createFixture()(() => @@ -49,7 +49,7 @@ export const daiCurrencyInfo = createFixture()(() => nativeCurrency: MAINNET_CURRENCY, logoUrl: 'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x6B175474E89094C44Da98b954EedeAC495271d0F/logo.png', - }) + }), ) export const arbitrumDaiCurrencyInfo = createFixture()(() => @@ -57,14 +57,14 @@ export const arbitrumDaiCurrencyInfo = createFixture()(() => nativeCurrency: ARBITRUM_CURRENCY, logoUrl: 'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x6B175474E89094C44Da98b954EedeAC495271d0F/logo.png', - }) + }), ) export const usdcCurrencyInfo = createFixture()(() => currencyInfo({ nativeCurrency: BASE_CURRENCY, logoUrl: null, - }) + }), ) export const ETH_CURRENCY_INFO = ethCurrencyInfo() diff --git a/packages/uniswap/src/test/fixtures/wallet/index.ts b/packages/uniswap/src/test/fixtures/wallet/index.ts new file mode 100644 index 00000000000..3c851f79847 --- /dev/null +++ b/packages/uniswap/src/test/fixtures/wallet/index.ts @@ -0,0 +1 @@ +export * from './currencies' diff --git a/packages/uniswap/src/test/render.tsx b/packages/uniswap/src/test/render.tsx index 08df6441989..f6ed5852a44 100644 --- a/packages/uniswap/src/test/render.tsx +++ b/packages/uniswap/src/test/render.tsx @@ -1,5 +1,3 @@ -/* eslint-disable no-restricted-imports */ - import { render as RNRender, RenderOptions, RenderResult } from '@testing-library/react-native' import { PropsWithChildren } from 'react' import { TamaguiProvider } from 'ui/src' @@ -13,10 +11,7 @@ import 'uniswap/src/i18n/i18n' * @param preloadedState and store * @returns `ui` wrapped with providers */ -export function renderWithProviders( - ui: React.ReactElement, - renderOptions: RenderOptions = {} -): RenderResult { +export function renderWithProviders(ui: React.ReactElement, renderOptions: RenderOptions = {}): RenderResult { function Wrapper({ children }: PropsWithChildren): JSX.Element { return {children} } diff --git a/packages/wallet/src/test/shared.ts b/packages/uniswap/src/test/shared.ts similarity index 100% rename from packages/wallet/src/test/shared.ts rename to packages/uniswap/src/test/shared.ts diff --git a/packages/uniswap/src/test/test-utils.ts b/packages/uniswap/src/test/test-utils.ts index 0e953d01589..a7873920875 100644 --- a/packages/uniswap/src/test/test-utils.ts +++ b/packages/uniswap/src/test/test-utils.ts @@ -1,5 +1,3 @@ -/* eslint-disable no-restricted-imports */ - import { renderWithProviders } from 'uniswap/src/test/render' // re-export everything diff --git a/packages/wallet/src/test/utils/factory.ts b/packages/uniswap/src/test/utils/factory.ts similarity index 86% rename from packages/wallet/src/test/utils/factory.ts rename to packages/uniswap/src/test/utils/factory.ts index 72f171707b9..5e44e7812c1 100644 --- a/packages/wallet/src/test/utils/factory.ts +++ b/packages/uniswap/src/test/utils/factory.ts @@ -61,9 +61,13 @@ import { omit, pick } from 'lodash' */ // If there are no custom options export function createFixture(): { - (getValues: () => V): { + ( + getValues: () => V, + ): { // If some fields returned by getValues are overridden - >(overrides: O): V extends (infer I)[] + >( + overrides: O, + ): V extends (infer I)[] ? (Omit & O)[] // update type of each array element : Omit & O // update type of the object // If no fields are overridden @@ -73,11 +77,15 @@ export function createFixture(): { // If there are custom options with default values object export function createFixture( - defaultOptions: Required

        // defaultOptions is an object with default options + defaultOptions: Required

        , // defaultOptions is an object with default options ): { - (getValues: (options: P) => V): { + ( + getValues: (options: P) => V, + ): { // If some fields returned by getValues are overridden - >(overrides: O): V extends (infer I)[] + >( + overrides: O, + ): V extends (infer I)[] ? (Omit> & Omit)[] // update type of each array element : Omit> & Omit // update type of the object // If no fields are overridden @@ -87,11 +95,15 @@ export function createFixture( // If there are custom options with default values getter function export function createFixture( - getDefaultOptions: () => Required

        // getDefaultOptions is a function that returns an object with default options + getDefaultOptions: () => Required

        , // getDefaultOptions is a function that returns an object with default options ): { - (getValues: (options: P) => V): { + ( + getValues: (options: P) => V, + ): { // If some fields returned by getValues are overridden - >(overrides: O): V extends (infer I)[] + >( + overrides: O, + ): V extends (infer I)[] ? (Omit> & Omit)[] // update type of each array element : Omit> & Omit // update type of the object // If no fields are overridden @@ -100,21 +112,19 @@ export function createFixture( } export function createFixture( - defaultOptionsOrGetter?: Required

        | (() => Required

        ) + defaultOptionsOrGetter?: Required

        | (() => Required

        ), ) { return (getValues: (options?: P) => V) => { // eslint-disable-next-line @typescript-eslint/explicit-function-return-type return | Partial>(overrides?: O) => { // Get default options (if they exist) const defaultOptions = - typeof defaultOptionsOrGetter === 'function' - ? defaultOptionsOrGetter() - : defaultOptionsOrGetter + typeof defaultOptionsOrGetter === 'function' ? defaultOptionsOrGetter() : defaultOptionsOrGetter // Get overrides for options (filter out undefined values) const optionOverrides = Object.fromEntries( Object.entries(defaultOptions ? pick(overrides, Object.keys(defaultOptions)) : {}).filter( - ([, value]) => value !== undefined - ) + ([, value]) => value !== undefined, + ), ) // Get values with getValues function const mergedOptions = defaultOptions ? { ...defaultOptions, ...optionOverrides } : undefined diff --git a/packages/uniswap/src/test/utils/index.ts b/packages/uniswap/src/test/utils/index.ts new file mode 100644 index 00000000000..8fd24633732 --- /dev/null +++ b/packages/uniswap/src/test/utils/index.ts @@ -0,0 +1 @@ +export * from './factory' diff --git a/packages/uniswap/src/types/chains.ts b/packages/uniswap/src/types/chains.ts index 0426191a8e8..b28bc871391 100644 --- a/packages/uniswap/src/types/chains.ts +++ b/packages/uniswap/src/types/chains.ts @@ -101,7 +101,7 @@ export type InterfaceGqlChain = Exclude { const validAddress = '0x71C7656EC7ab88b098defB751B7401B5f6d8976F' diff --git a/packages/wallet/src/utils/addresses.ts b/packages/uniswap/src/utils/addresses.ts similarity index 93% rename from packages/wallet/src/utils/addresses.ts rename to packages/uniswap/src/utils/addresses.ts index bbc0a4efa9b..30001ca304a 100644 --- a/packages/wallet/src/utils/addresses.ts +++ b/packages/uniswap/src/utils/addresses.ts @@ -1,4 +1,5 @@ -import { logger, utils } from 'ethers' +import { getAddress } from '@ethersproject/address' +import { logger } from 'utilities/src/logger/logger' export enum AddressStringFormat { Lowercase, @@ -21,11 +22,7 @@ export enum AddressStringFormat { * @param log If logging is enabled in case of errors * @returns The normalized address or false if the address is invalid */ -export function getValidAddress( - address: Maybe, - withChecksum = false, - log = true -): Nullable { +export function getValidAddress(address: Maybe, withChecksum = false, log = true): Nullable { if (!address) { return null } @@ -34,7 +31,7 @@ export function getValidAddress( if (withChecksum) { try { - return utils.getAddress(addressWith0x) + return getAddress(addressWith0x) } catch (error) { if (log) { logger.warn('utils/addresses', 'getValidAddress', `Invalid address at checksum: ${address}`) diff --git a/packages/wallet/src/utils/clipboard.native.ts b/packages/uniswap/src/utils/clipboard.native.ts similarity index 91% rename from packages/wallet/src/utils/clipboard.native.ts rename to packages/uniswap/src/utils/clipboard.native.ts index 3080cd395e4..12c7fde9765 100644 --- a/packages/wallet/src/utils/clipboard.native.ts +++ b/packages/uniswap/src/utils/clipboard.native.ts @@ -1,6 +1,6 @@ import * as ExpoClipboard from 'expo-clipboard' +import { IClipboard } from 'uniswap/src/utils/clipboard' import { logger } from 'utilities/src/logger/logger' -import { IClipboard } from 'wallet/src/utils/clipboard' const Clipboard: IClipboard = { setClipboard: async (value: string) => { @@ -31,8 +31,7 @@ const Clipboard: IClipboard = { const base64Encoding = await blobToBase64(blob) // extract base64 encoding from result string - const formattedEncoding = - typeof base64Encoding === 'string' ? base64Encoding.split(',')[1] : null + const formattedEncoding = typeof base64Encoding === 'string' ? base64Encoding.split(',')[1] : null // if valid result, copy to clipboard if (formattedEncoding) { diff --git a/packages/wallet/src/utils/clipboard.ts b/packages/uniswap/src/utils/clipboard.ts similarity index 100% rename from packages/wallet/src/utils/clipboard.ts rename to packages/uniswap/src/utils/clipboard.ts diff --git a/packages/wallet/src/utils/clipboard.web.ts b/packages/uniswap/src/utils/clipboard.web.ts similarity index 94% rename from packages/wallet/src/utils/clipboard.web.ts rename to packages/uniswap/src/utils/clipboard.web.ts index c81df0ef0d0..a0157827985 100644 --- a/packages/wallet/src/utils/clipboard.web.ts +++ b/packages/uniswap/src/utils/clipboard.web.ts @@ -1,5 +1,5 @@ +import { IClipboard } from 'uniswap/src/utils/clipboard' import { logger } from 'utilities/src/logger/logger' -import { IClipboard } from 'wallet/src/utils/clipboard' const Clipboard: IClipboard = { setClipboard: async (value: string) => { diff --git a/packages/wallet/src/utils/colors.test.ts b/packages/uniswap/src/utils/colors.test.ts similarity index 98% rename from packages/wallet/src/utils/colors.test.ts rename to packages/uniswap/src/utils/colors.test.ts index 8c70391740c..b8c6de3014e 100644 --- a/packages/wallet/src/utils/colors.test.ts +++ b/packages/uniswap/src/utils/colors.test.ts @@ -6,7 +6,7 @@ import { getColorDiffScore, hexToRGB, opacify, -} from 'wallet/src/utils/colors' +} from 'uniswap/src/utils/colors' it('returns an hex color with opacity', () => { expect(opacify(10, '#000000')).toEqual('#0000001a') diff --git a/packages/wallet/src/utils/colors.tsx b/packages/uniswap/src/utils/colors.tsx similarity index 94% rename from packages/wallet/src/utils/colors.tsx rename to packages/uniswap/src/utils/colors.tsx index 7de80525536..e789d0f24bf 100644 --- a/packages/wallet/src/utils/colors.tsx +++ b/packages/uniswap/src/utils/colors.tsx @@ -18,9 +18,7 @@ export function opacify(amount: number, hexColor: string): string { } if (hexColor.length !== 7) { - throw new Error( - `opacify: provided color ${hexColor} was not in hexadecimal format (e.g. #000000)` - ) + throw new Error(`opacify: provided color ${hexColor} was not in hexadecimal format (e.g. #000000)`) } if (amount < 0 || amount > 100) { @@ -60,9 +58,7 @@ export function useNetworkColors(chainId: WalletChainId): { * @param backgroundColor The hex value of the background color to check contrast against * @returns either 'sporeWhite' or 'sporeBlack' */ -export function getContrastPassingTextColor( - backgroundColor: string -): '$sporeWhite' | '$sporeBlack' { +export function getContrastPassingTextColor(backgroundColor: string): '$sporeWhite' | '$sporeBlack' { const lightText = colorsLight.sporeWhite if (hex(lightText, backgroundColor) >= MIN_COLOR_CONTRAST_THRESHOLD) { return '$sporeWhite' @@ -115,7 +111,7 @@ const ColorVariant = { */ export function adjustColorVariant( colorName: string | undefined, - adjustmentType: AdjustmentType + adjustmentType: AdjustmentType, ): keyof GlobalPalette | undefined { if (!colorName) { return undefined @@ -161,17 +157,14 @@ export function findNearestThemeColor(hexString: string): keyof GlobalPalette | } as { colorDiff: number | undefined colorName: keyof GlobalPalette | undefined - } + }, ).colorName } /** * Returns a number representing the difference between two colors. Lower means more similar. */ -export function getColorDiffScore( - colorA: string | null, - colorB: string | null -): number | undefined { +export function getColorDiffScore(colorA: string | null, colorB: string | null): number | undefined { if (!colorA || !colorB) { return undefined } diff --git a/packages/uniswap/src/utils/currency.ts b/packages/uniswap/src/utils/currency.ts index 704ee7336e5..527c47f0cfe 100644 --- a/packages/uniswap/src/utils/currency.ts +++ b/packages/uniswap/src/utils/currency.ts @@ -1,3 +1,7 @@ +import { Token } from '@uniswap/sdk-core' +import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' +import { UniverseChainId } from 'uniswap/src/types/chains' + const DEFAULT_MAX_SYMBOL_CHARACTERS = 6 export function getSymbolDisplayText(symbol: Maybe): Maybe { @@ -9,3 +13,14 @@ export function getSymbolDisplayText(symbol: Maybe): Maybe { ? symbol?.substring(0, DEFAULT_MAX_SYMBOL_CHARACTERS - 3) + '...' : symbol } + +export function wrappedNativeCurrency(chainId: UniverseChainId): Token { + const wrappedCurrencyInfo = UNIVERSE_CHAIN_INFO[chainId].wrappedNativeCurrency + return new Token( + chainId, + wrappedCurrencyInfo.address, + wrappedCurrencyInfo.decimals, + wrappedCurrencyInfo.symbol, + wrappedCurrencyInfo.name, + ) +} diff --git a/packages/uniswap/src/utils/currencyId.ts b/packages/uniswap/src/utils/currencyId.ts new file mode 100644 index 00000000000..ac060f784e7 --- /dev/null +++ b/packages/uniswap/src/utils/currencyId.ts @@ -0,0 +1,99 @@ +import { Currency } from '@uniswap/sdk-core' +import { getNativeAddress, getWrappedNativeAddress } from 'uniswap/src/constants/addresses' +import { DEFAULT_NATIVE_ADDRESS } from 'uniswap/src/constants/chains' +import { toSupportedChainId } from 'uniswap/src/features/chains/utils' +import { UniverseChainId, WalletChainId } from 'uniswap/src/types/chains' +import { CurrencyId } from 'uniswap/src/types/currency' +import { areAddressesEqual } from 'uniswap/src/utils/addresses' + +export function currencyId(currency: Currency): CurrencyId { + return buildCurrencyId(currency.chainId, currencyAddress(currency)) +} + +export function buildCurrencyId(chainId: WalletChainId, address: string): string { + return `${chainId}-${address}` +} + +export function buildNativeCurrencyId(chainId: WalletChainId): string { + return buildCurrencyId(chainId, getNativeAddress(chainId)) +} + +export function buildWrappedNativeCurrencyId(chainId: WalletChainId): string { + return buildCurrencyId(chainId, getWrappedNativeAddress(chainId)) +} + +export function areCurrencyIdsEqual(id1: CurrencyId, id2: CurrencyId): boolean { + return id1.toLowerCase() === id2.toLowerCase() +} + +export function currencyAddress(currency: Currency): string { + if (currency.isNative) { + return getNativeAddress(currency.chainId) + } + + return currency.address +} + +export const NATIVE_ANALYTICS_ADDRESS_VALUE = 'NATIVE' + +export function getCurrencyAddressForAnalytics(currency: Currency): string { + if (currency.isNative) { + return NATIVE_ANALYTICS_ADDRESS_VALUE + } + + return currency.address +} + +export const isNativeCurrencyAddress = (chainId: WalletChainId, address: Maybe

        ): boolean => { + if (!address) { + return true + } + + return areAddressesEqual(address, getNativeAddress(chainId)) +} + +// Currency ids are formatted as `chainId-tokenaddress` +export function currencyIdToAddress(_currencyId: string): Address { + const currencyIdParts = _currencyId.split('-') + if (!currencyIdParts[1]) { + throw new Error(`Invalid currencyId format: ${_currencyId}`) + } + return currencyIdParts[1] +} + +function isPolygonChain(chainId: number): chainId is UniverseChainId.Polygon | UniverseChainId.PolygonMumbai { + return chainId === UniverseChainId.PolygonMumbai || chainId === UniverseChainId.Polygon +} + +function isCeloChain(chainId: number): chainId is UniverseChainId.Celo { + return chainId === UniverseChainId.Celo +} + +// Similar to `currencyIdToAddress`, except native addresses are `null`. +export function currencyIdToGraphQLAddress(_currencyId?: string): Address | null { + if (!_currencyId) { + return null + } + + const address = currencyIdToAddress(_currencyId) + const chainId = currencyIdToChain(_currencyId) + + if (!chainId) { + return null + } + + // backend only expects `null` for the native asset, except Polygon & Celo + if (isNativeCurrencyAddress(chainId, address) && !isPolygonChain(chainId) && !isCeloChain(chainId)) { + return null + } + + return address.toLowerCase() +} + +export function currencyIdToChain(_currencyId: string): WalletChainId | null { + return toSupportedChainId(_currencyId.split('-')[0]) +} + +export function isDefaultNativeAddress(address: string): boolean { + return areAddressesEqual(address, DEFAULT_NATIVE_ADDRESS) +} diff --git a/packages/uniswap/src/utils/env/index.ts b/packages/uniswap/src/utils/env/index.ts index 30b6f15a2d9..fc0b2e6c907 100644 --- a/packages/uniswap/src/utils/env/index.ts +++ b/packages/uniswap/src/utils/env/index.ts @@ -10,9 +10,7 @@ export function isDevEnv(): boolean { if (isInterface) { return process.env.NODE_ENV === 'development' } else if (isExtension) { - return ( - __DEV__ || chrome.runtime.id === EXTENSION_ID_DEV || chrome.runtime.id === EXTENSION_ID_LOCAL - ) + return __DEV__ || chrome.runtime.id === EXTENSION_ID_DEV || chrome.runtime.id === EXTENSION_ID_LOCAL } else { throw createAndLogError('isProdEnv') } diff --git a/packages/uniswap/src/utils/link.native.ts b/packages/uniswap/src/utils/link.native.ts new file mode 100644 index 00000000000..cd902a09a94 --- /dev/null +++ b/packages/uniswap/src/utils/link.native.ts @@ -0,0 +1,9 @@ +import { Linking } from 'react-native' + +export async function openURL(url: string): Promise { + await Linking.openURL(url) +} + +export async function canOpenURL(url: string): Promise { + return await Linking.canOpenURL(url) +} diff --git a/packages/uniswap/src/utils/link.ts b/packages/uniswap/src/utils/link.ts new file mode 100644 index 00000000000..ff72b86f0f2 --- /dev/null +++ b/packages/uniswap/src/utils/link.ts @@ -0,0 +1,9 @@ +import { NotImplementedError } from 'utilities/src/errors' + +export function openURL(_url: string): Window | null { + throw new NotImplementedError('See `.native.ts` and `.web.ts` files.') +} + +export function canOpenURL(_url: string): boolean { + throw new NotImplementedError('See `.native.ts` and `.web.ts` files.') +} diff --git a/packages/uniswap/src/utils/link.web.ts b/packages/uniswap/src/utils/link.web.ts new file mode 100644 index 00000000000..821670c9081 --- /dev/null +++ b/packages/uniswap/src/utils/link.web.ts @@ -0,0 +1,8 @@ +export function openURL(url: string): Window | null { + // eslint-disable-next-line security/detect-non-literal-fs-filename + return window.open(url) +} + +export function canOpenURL(_url: string): boolean { + return true +} diff --git a/packages/uniswap/src/utils/linking.ts b/packages/uniswap/src/utils/linking.ts new file mode 100644 index 00000000000..5c21768a372 --- /dev/null +++ b/packages/uniswap/src/utils/linking.ts @@ -0,0 +1,60 @@ +import * as WebBrowser from 'expo-web-browser' +import { colorsLight } from 'ui/src/theme' +import { canOpenURL, openURL } from 'uniswap/src/utils/link' +import { logger } from 'utilities/src/logger/logger' + +const ALLOWED_EXTERNAL_URI_SCHEMES = ['http://', 'https://'] + +/** + * Opens allowed URIs. if isSafeUri is set to true then this will open http:// and https:// as well as some deeplinks. + * Only set this flag to true if you have formed the URL yourself in our own app code. For any URLs from an external source + * isSafeUri must be false and it will only open http:// and https:// URI schemes. + * + * @param openExternalBrowser whether to leave the app and open in system browser. default is false, opens in-app browser window + * @param isSafeUri whether to bypass ALLOWED_EXTERNAL_URI_SCHEMES check + * @param controlsColor When opening in an in-app browser, determines the controls color + **/ +export async function openUri( + uri: string, + openExternalBrowser = false, + isSafeUri = false, + // NOTE: okay to use colors object directly as we want the same color for light/dark modes + controlsColor = colorsLight.accent1, +): Promise { + const trimmedURI = uri.trim() + if (!isSafeUri && !ALLOWED_EXTERNAL_URI_SCHEMES.some((scheme) => trimmedURI.startsWith(scheme))) { + // TODO: [MOB-253] show a visual warning that the link cannot be opened. + logger.error(new Error('User attempted to open potentially unsafe url'), { + tags: { + file: 'linking', + function: 'openUri', + }, + extra: { uri }, + }) + return + } + + const isHttp = /^https?:\/\//.test(trimmedURI) + + // `canOpenURL` returns `false` for App Links / Universal Links, so we just assume any device can handle the `https://` protocol. + const supported = isHttp ? true : await canOpenURL(uri) + + if (!supported) { + logger.warn('linking', 'openUri', `Cannot open URI: ${uri}`) + return + } + + try { + if (openExternalBrowser) { + await openURL(uri) + } else { + await WebBrowser.openBrowserAsync(uri, { + controlsColor, + presentationStyle: WebBrowser.WebBrowserPresentationStyle.FULL_SCREEN, + windowFeatures: 'popup=false', + }) + } + } catch (error) { + logger.error(error, { tags: { file: 'linking', function: 'openUri' } }) + } +} diff --git a/packages/uniswap/src/utils/useKeyboardLayout.native.ts b/packages/uniswap/src/utils/useKeyboardLayout.native.ts index 0b07a7e27d1..99d55240770 100644 --- a/packages/uniswap/src/utils/useKeyboardLayout.native.ts +++ b/packages/uniswap/src/utils/useKeyboardLayout.native.ts @@ -19,13 +19,13 @@ export function useKeyboardLayout(): KeyboardLayout { }), Keyboard.addListener('keyboardDidHide', (e: KeyboardEvent) => { setKeyboardPosition(e.endCoordinates.screenY) - }) + }), ) } else { keyboardListeners.push( Keyboard.addListener('keyboardWillChangeFrame', (e: KeyboardEvent) => { setKeyboardPosition(e.endCoordinates.screenY) - }) + }), ) } diff --git a/packages/uniswap/src/utils/usePlatformBasedFetchPolicy.ts b/packages/uniswap/src/utils/usePlatformBasedFetchPolicy.ts new file mode 100644 index 00000000000..a794b4cfb24 --- /dev/null +++ b/packages/uniswap/src/utils/usePlatformBasedFetchPolicy.ts @@ -0,0 +1,19 @@ +import { WatchQueryFetchPolicy } from '@apollo/client' +import { usePlatformBasedValue } from 'uniswap/src/utils/usePlatformBasedValue' + +type Props = { + fetchPolicy: WatchQueryFetchPolicy | undefined + pollInterval: number | undefined +} + +export function usePlatformBasedFetchPolicy(props: Props): Props { + return usePlatformBasedValue({ + defaultValue: props, + extension: { + windowNotFocused: { + fetchPolicy: 'cache-only', + pollInterval: 0, + }, + }, + }) +} diff --git a/packages/uniswap/src/utils/usePlatformBasedValue.native.ts b/packages/uniswap/src/utils/usePlatformBasedValue.native.ts new file mode 100644 index 00000000000..e4da3cb168c --- /dev/null +++ b/packages/uniswap/src/utils/usePlatformBasedValue.native.ts @@ -0,0 +1,5 @@ +import type { UsePlatformBasedValue } from 'uniswap/src/utils/usePlatformBasedValue' + +export function usePlatformBasedValue({ defaultValue, mobile }: UsePlatformBasedValue): T { + return mobile?.defaultValue ?? defaultValue +} diff --git a/packages/uniswap/src/utils/usePlatformBasedValue.ts b/packages/uniswap/src/utils/usePlatformBasedValue.ts new file mode 100644 index 00000000000..1fae79004c5 --- /dev/null +++ b/packages/uniswap/src/utils/usePlatformBasedValue.ts @@ -0,0 +1,33 @@ +import { useIsChromeWindowFocusedWithTimeout } from 'uniswap/src/extension/useIsChromeWindowFocused' +import { isExtension } from 'utilities/src/platform' +import { ONE_SECOND_MS } from 'utilities/src/time/time' + +export type UsePlatformBasedValue = { + defaultValue: T + mobile?: { + defaultValue?: T + } + web?: { + defaultValue?: T + } + extension?: { + defaultValue?: T + windowNotFocused?: T + } +} + +export function usePlatformBasedValue({ defaultValue, web, extension }: UsePlatformBasedValue): T { + // We add a 30s delay before we trigger the `windowNotFocused` state to avoid unnecessary state changes when the user is quickly switching back-and-forth between windows. + // Without this delay, we could end up triggering too many unnecessary API calls every time the window regains focus. + const isChromeWindowFocused = useIsChromeWindowFocusedWithTimeout(30 * ONE_SECOND_MS) + + if (isExtension) { + if (!isChromeWindowFocused) { + return extension?.windowNotFocused ?? defaultValue + } + + return extension?.defaultValue ?? defaultValue + } + + return web?.defaultValue ?? defaultValue +} diff --git a/packages/utilities/package.json b/packages/utilities/package.json index d4714d0f7ec..4daa293d9bb 100644 --- a/packages/utilities/package.json +++ b/packages/utilities/package.json @@ -7,6 +7,7 @@ "@amplitude/analytics-react-native": "1.4.0", "@amplitude/analytics-types": "0.13.0", "@apollo/client": "3.10.4", + "@datadog/browser-logs": "^5.20.0", "@ethersproject/abstract-signer": "5.7.0", "@ethersproject/address": "5.7.0", "@ethersproject/constants": "5.7.0", @@ -16,7 +17,7 @@ "@sentry/react": "7.80.0", "@sentry/react-native": "5.5.0", "@uniswap/analytics": "1.7.0", - "@uniswap/analytics-events": "2.32.0", + "@uniswap/analytics-events": "2.34.0", "@uniswap/sdk-core": "5.3.0", "aws-appsync-auth-link": "3.0.7", "aws-appsync-subscription-link": "3.1.3", @@ -25,8 +26,10 @@ "jsbi": "3.2.5", "react": "18.2.0", "react-native": "0.73.6", + "react-native-device-info": "10.0.2", "react-test-renderer": "18.2.0", "subscriptions-transport-ws": "0.11.0", + "uuid": "9.0.0", "zen-observable-ts": "1.2.5" }, "devDependencies": { @@ -34,6 +37,7 @@ "@testing-library/react-hooks": "7.0.2", "@types/chrome": "0.0.254", "@types/react": "^18.0.15", + "@types/uuid": "9.0.1", "@uniswap/eslint-config": "workspace:^", "eslint": "8.44.0", "jest": "29.7.0", @@ -45,6 +49,7 @@ "scripts": { "build": "tsc -b", "check:deps:usage": "depcheck", + "format": "../../scripts/prettier.sh", "lint": "eslint . --ext ts,tsx --max-warnings=0", "lint:fix": "eslint . --ext ts,tsx --fix", "test": "jest", diff --git a/packages/utilities/src/addresses/addresses.test.ts b/packages/utilities/src/addresses/addresses.test.ts index 450ab64f0f9..b98febcaf7c 100644 --- a/packages/utilities/src/addresses/addresses.test.ts +++ b/packages/utilities/src/addresses/addresses.test.ts @@ -8,18 +8,12 @@ describe('utils', () => { }) it('returns the checksummed address', () => { - expect(isAddress('0xf164fc0ec4e93095b804a4795bbe1e041497b92a')).toBe( - '0xf164fC0Ec4E93095b804a4795bBe1e041497b92a' - ) - expect(isAddress('0xf164fC0Ec4E93095b804a4795bBe1e041497b92a')).toBe( - '0xf164fC0Ec4E93095b804a4795bBe1e041497b92a' - ) + expect(isAddress('0xf164fc0ec4e93095b804a4795bbe1e041497b92a')).toBe('0xf164fC0Ec4E93095b804a4795bBe1e041497b92a') + expect(isAddress('0xf164fC0Ec4E93095b804a4795bBe1e041497b92a')).toBe('0xf164fC0Ec4E93095b804a4795bBe1e041497b92a') }) it('succeeds even without prefix', () => { - expect(isAddress('f164fc0ec4e93095b804a4795bbe1e041497b92a')).toBe( - '0xf164fC0Ec4E93095b804a4795bBe1e041497b92a' - ) + expect(isAddress('f164fc0ec4e93095b804a4795bbe1e041497b92a')).toBe('0xf164fC0Ec4E93095b804a4795bBe1e041497b92a') }) it('fails if too long', () => { @@ -41,9 +35,7 @@ describe('utils', () => { }) it('renders checksummed address', () => { - expect(shortenAddress('0x2E1b342132A67Ea578e4E3B814bae2107dc254CC'.toLowerCase())).toBe( - '0x2E1b...54CC' - ) + expect(shortenAddress('0x2E1b342132A67Ea578e4E3B814bae2107dc254CC'.toLowerCase())).toBe('0x2E1b...54CC') }) it('allows undefined', () => { @@ -52,13 +44,9 @@ describe('utils', () => { it('allows custom amounts of start/end chars', () => { expect(shortenAddress('0x2E1b342132A67Ea578e4E3B814bae2107dc254CC', 2)).toBe('0x2E...54CC') - expect(shortenAddress('0x2E1b342132A67Ea578e4E3B814bae2107dc254CC', 6)).toBe( - '0x2E1b34...54CC' - ) + expect(shortenAddress('0x2E1b342132A67Ea578e4E3B814bae2107dc254CC', 6)).toBe('0x2E1b34...54CC') expect(shortenAddress('0x2E1b342132A67Ea578e4E3B814bae2107dc254CC', 2, 2)).toBe('0x2E...CC') - expect(shortenAddress('0x2E1b342132A67Ea578e4E3B814bae2107dc254CC', 2, 6)).toBe( - '0x2E...c254CC' - ) + expect(shortenAddress('0x2E1b342132A67Ea578e4E3B814bae2107dc254CC', 2, 6)).toBe('0x2E...c254CC') }) }) }) diff --git a/packages/utilities/src/apollo/SubscriptionLink.ts b/packages/utilities/src/apollo/SubscriptionLink.ts index d692dfde0d9..0f2e35c8471 100644 --- a/packages/utilities/src/apollo/SubscriptionLink.ts +++ b/packages/utilities/src/apollo/SubscriptionLink.ts @@ -79,7 +79,7 @@ export function createSubscriptionLink( region = '', // left blank for a custom domain name (eg realtime.gateway.uniswap.org) token, }: SubscriptionLinkConfig, - client: ApolloClient + client: ApolloClient, ): SubscriptionLink { const auth: AuthOptions = { type: AUTH_TYPE.AWS_LAMBDA, token } // Order is intentional here - the header must be set before sending the subscription. diff --git a/packages/utilities/src/apollo/splitSubscription.ts b/packages/utilities/src/apollo/splitSubscription.ts index 367f8e15744..5553fd33b32 100644 --- a/packages/utilities/src/apollo/splitSubscription.ts +++ b/packages/utilities/src/apollo/splitSubscription.ts @@ -2,10 +2,7 @@ import { ApolloLink, HttpLink, split } from '@apollo/client' import { getMainDefinition } from '@apollo/client/utilities' import { SubscriptionLink } from 'utilities/src/apollo/SubscriptionLink' -export function splitSubscription( - subscriptionLink: SubscriptionLink, - httpLink: HttpLink -): ApolloLink { +export function splitSubscription(subscriptionLink: SubscriptionLink, httpLink: HttpLink): ApolloLink { // Use the subscriptionLink for subscriptions, and the httpLink for everything else; // see https://www.apollographql.com/docs/react/api/link/introduction/#directional-composition. return split( @@ -14,6 +11,6 @@ export function splitSubscription( return definition.kind === 'OperationDefinition' && definition.operation === 'subscription' }, subscriptionLink, - httpLink + httpLink, ) } diff --git a/packages/utilities/src/contracts/getContract.ts b/packages/utilities/src/contracts/getContract.ts index e532450262e..ac02a2ebf22 100644 --- a/packages/utilities/src/contracts/getContract.ts +++ b/packages/utilities/src/contracts/getContract.ts @@ -8,7 +8,7 @@ export function getContract( address: string, ABI: ContractInterface, provider: JsonRpcProvider, - account?: string + account?: string, ): Contract { if (!isAddress(address) || address === AddressZero) { throw Error(`Invalid 'address' parameter '${address}'.`) diff --git a/packages/utilities/src/device/locales.native.test.ts b/packages/utilities/src/device/locales.native.test.ts index b15e0b5093b..ef1b21d6a8e 100644 --- a/packages/utilities/src/device/locales.native.test.ts +++ b/packages/utilities/src/device/locales.native.test.ts @@ -25,8 +25,6 @@ jest.mock('expo-localization', () => ({ describe(getDeviceLocales, () => { it('should return the device locale', () => { expect(getDeviceLocales).not.toThrow() - expect(getDeviceLocales()).toEqual([ - { languageCode: MOCK_LANGUAGE_CODE, languageTag: MOCK_LANGUAGE_TAG }, - ]) + expect(getDeviceLocales()).toEqual([{ languageCode: MOCK_LANGUAGE_CODE, languageTag: MOCK_LANGUAGE_TAG }]) }) }) diff --git a/packages/utilities/src/device/locales.native.ts b/packages/utilities/src/device/locales.native.ts index b2a064218ce..894c2320906 100644 --- a/packages/utilities/src/device/locales.native.ts +++ b/packages/utilities/src/device/locales.native.ts @@ -1,10 +1,6 @@ // eslint-disable-next-line no-restricted-imports import { getLocales } from 'expo-localization' -import { - DEFAULT_LANGUAGE_CODE, - DEFAULT_LANGUAGE_TAG, - DeviceLocale, -} from 'utilities/src/device/constants' +import { DEFAULT_LANGUAGE_CODE, DEFAULT_LANGUAGE_TAG, DeviceLocale } from 'utilities/src/device/constants' import { logger } from 'utilities/src/logger/logger' export function getDeviceLocales(): DeviceLocale[] { diff --git a/packages/utilities/src/device/locales.web.test.ts b/packages/utilities/src/device/locales.web.test.ts index 427ec6625df..4eaf46d8289 100644 --- a/packages/utilities/src/device/locales.web.test.ts +++ b/packages/utilities/src/device/locales.web.test.ts @@ -8,9 +8,7 @@ describe(getDeviceLocales, () => { it('should return the device locale', () => { expect(getDeviceLocales).not.toThrow() - expect(getDeviceLocales()).toEqual([ - { languageCode: MOCK_LANGUAGE, languageTag: MOCK_LANGUAGE }, - ]) + expect(getDeviceLocales()).toEqual([{ languageCode: MOCK_LANGUAGE, languageTag: MOCK_LANGUAGE }]) }) it('should return the default locale if an error occurs', () => { @@ -19,8 +17,6 @@ describe(getDeviceLocales, () => { }) expect(getDeviceLocales).not.toThrow() - expect(getDeviceLocales()).toEqual([ - { languageCode: DEFAULT_LANGUAGE_CODE, languageTag: DEFAULT_LANGUAGE_TAG }, - ]) + expect(getDeviceLocales()).toEqual([{ languageCode: DEFAULT_LANGUAGE_CODE, languageTag: DEFAULT_LANGUAGE_TAG }]) }) }) diff --git a/packages/utilities/src/device/locales.web.ts b/packages/utilities/src/device/locales.web.ts index 50c7b8e486b..76c10e82eb6 100644 --- a/packages/utilities/src/device/locales.web.ts +++ b/packages/utilities/src/device/locales.web.ts @@ -1,8 +1,4 @@ -import { - DEFAULT_LANGUAGE_CODE, - DEFAULT_LANGUAGE_TAG, - DeviceLocale, -} from 'utilities/src/device/constants' +import { DEFAULT_LANGUAGE_CODE, DEFAULT_LANGUAGE_TAG, DeviceLocale } from 'utilities/src/device/constants' import { logger } from 'utilities/src/logger/logger' export function getDeviceLocales(): DeviceLocale[] { diff --git a/packages/utilities/src/environment/constants.ts b/packages/utilities/src/environment/constants.ts new file mode 100644 index 00000000000..bf5ea7d2404 --- /dev/null +++ b/packages/utilities/src/environment/constants.ts @@ -0,0 +1,3 @@ +export const isDetoxBuild = Boolean(process.env.DETOX_MODE) +export const isJestRun = !!process.env.JEST_WORKER_ID +export const isNonJestDev = __DEV__ && !isJestRun diff --git a/packages/uniswap/src/utils/env/index.native.ts b/packages/utilities/src/environment/index.native.ts similarity index 52% rename from packages/uniswap/src/utils/env/index.native.ts rename to packages/utilities/src/environment/index.native.ts index 6965974cf5e..ac4a649cf5e 100644 --- a/packages/uniswap/src/utils/env/index.native.ts +++ b/packages/utilities/src/environment/index.native.ts @@ -2,6 +2,10 @@ import DeviceInfo from 'react-native-device-info' const BUNDLE_ID = DeviceInfo.getBundleId() +export function isTestEnv(): boolean { + return !!process.env.JEST_WORKER_ID || process.env.NODE_ENV === 'test' +} + export function isDevEnv(): boolean { return BUNDLE_ID.endsWith('.dev') } @@ -13,3 +17,13 @@ export function isBetaEnv(): boolean { export function isProdEnv(): boolean { return BUNDLE_ID === 'com.uniswap.mobile' } + +export function getEnvName(): 'production' | 'staging' | 'development' { + if (isBetaEnv()) { + return 'staging' + } + if (isProdEnv()) { + return 'production' + } + return 'development' +} diff --git a/packages/utilities/src/environment/index.ts b/packages/utilities/src/environment/index.ts index 0c6aa8d503e..48ceef16a20 100644 --- a/packages/utilities/src/environment/index.ts +++ b/packages/utilities/src/environment/index.ts @@ -1,3 +1,52 @@ -export const isJestRun = !!process.env.JEST_WORKER_ID -export const isNonJestDev = __DEV__ && !isJestRun -export const isDetoxBuild = process.env.DETOX_MODE +import { createAndLogError } from 'utilities/src/logger/logger' +import { isExtension, isInterface } from 'utilities/src/platform' + +const EXTENSION_ID_LOCAL = 'ceofpnbcmdjbibjjdniemjemmgaibeih' +const EXTENSION_ID_DEV = 'afhngfaoadjjlhbgopehflaabbgfbcmn' +const EXTENSION_ID_BETA = 'foilfbjokdonehdajefeadkclfpmhdga' +const EXTENSION_ID_PROD = 'nnpmfplkfogfpmcngplhnbdnnilmcdcg' + +export function isTestEnv(): boolean { + return !!process.env.JEST_WORKER_ID || process.env.NODE_ENV === 'test' +} + +export function isDevEnv(): boolean { + if (isInterface) { + return process.env.NODE_ENV === 'development' + } else if (isExtension) { + return __DEV__ || chrome.runtime.id === EXTENSION_ID_DEV || chrome.runtime.id === EXTENSION_ID_LOCAL + } else { + throw createAndLogError('isProdEnv') + } +} + +export function isBetaEnv(): boolean { + if (isInterface) { + // This is set in vercel builds and deploys from web/staging. + return Boolean(process.env.REACT_APP_STAGING) + } else if (isExtension) { + return chrome.runtime.id === EXTENSION_ID_BETA + } else { + throw createAndLogError('isBetaEnv') + } +} + +export function isProdEnv(): boolean { + if (isInterface) { + return process.env.NODE_ENV === 'production' && !isBetaEnv() + } else if (isExtension) { + return chrome.runtime.id === EXTENSION_ID_PROD + } else { + throw createAndLogError('isProdEnv') + } +} + +export function getEnvName(): 'production' | 'staging' | 'development' { + if (isBetaEnv()) { + return 'staging' + } + if (isProdEnv()) { + return 'production' + } + return 'development' +} diff --git a/packages/utilities/src/format/convertScientificNotation.ts b/packages/utilities/src/format/convertScientificNotation.ts index a433e325d0d..4c40c7e2ecb 100644 --- a/packages/utilities/src/format/convertScientificNotation.ts +++ b/packages/utilities/src/format/convertScientificNotation.ts @@ -17,17 +17,14 @@ export function convertScientificNotationToNumber(value: string): string { x *= Math.pow(10, decimalPlaces) } try { - convertedValue = JSBI.multiply( - JSBI.BigInt(x), - JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(e)) - ).toString() + convertedValue = JSBI.multiply(JSBI.BigInt(x), JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(e))).toString() } catch (error) { // If the numbers can't be converted to BigInts then just do regular arithmetic (i.e. when the exponent is negative) logger.debug( 'convertScientificNotation', 'convertScientificNotationToNumber', 'BigInt arithmetic unsuccessful', - e + e, ) convertedValue = (x * Math.pow(10, e)).toString() } diff --git a/packages/utilities/src/format/localeBased.test.ts b/packages/utilities/src/format/localeBased.test.ts index e06ec332b0d..009ae53c610 100644 --- a/packages/utilities/src/format/localeBased.test.ts +++ b/packages/utilities/src/format/localeBased.test.ts @@ -2,279 +2,131 @@ import { formatNumber } from 'utilities/src/format/localeBased' import { NumberType } from 'utilities/src/format/types' it('formats token reference numbers correctly', () => { - expect( - formatNumber({ input: 1234567000000000, type: NumberType.TokenNonTx, locale: 'en-US' }) - ).toBe('>999T') - expect(formatNumber({ input: 1002345, type: NumberType.TokenNonTx, locale: 'en-US' })).toBe( - '1.00M' - ) - expect(formatNumber({ input: 1234, type: NumberType.TokenNonTx, locale: 'en-US' })).toBe( - '1,234.00' - ) - expect(formatNumber({ input: 0.00909, type: NumberType.TokenNonTx, locale: 'en-US' })).toBe( - '0.009' - ) - expect(formatNumber({ input: 0.09001, type: NumberType.TokenNonTx, locale: 'en-US' })).toBe( - '0.090' - ) - expect(formatNumber({ input: 0.00099, type: NumberType.TokenNonTx, locale: 'en-US' })).toBe( - '<0.001' - ) + expect(formatNumber({ input: 1234567000000000, type: NumberType.TokenNonTx, locale: 'en-US' })).toBe('>999T') + expect(formatNumber({ input: 1002345, type: NumberType.TokenNonTx, locale: 'en-US' })).toBe('1.00M') + expect(formatNumber({ input: 1234, type: NumberType.TokenNonTx, locale: 'en-US' })).toBe('1,234.00') + expect(formatNumber({ input: 0.00909, type: NumberType.TokenNonTx, locale: 'en-US' })).toBe('0.009') + expect(formatNumber({ input: 0.09001, type: NumberType.TokenNonTx, locale: 'en-US' })).toBe('0.090') + expect(formatNumber({ input: 0.00099, type: NumberType.TokenNonTx, locale: 'en-US' })).toBe('<0.001') expect(formatNumber({ input: 0, type: NumberType.TokenNonTx, locale: 'en-US' })).toBe('0') }) it('formats token transaction numbers correctly', () => { - expect(formatNumber({ input: 1234567.8901, type: NumberType.TokenTx, locale: 'en-US' })).toBe( - '1,234,567.89' - ) - expect(formatNumber({ input: 765432.1, type: NumberType.TokenTx, locale: 'en-US' })).toBe( - '765,432.10' - ) - - expect(formatNumber({ input: 7654.321, type: NumberType.TokenTx, locale: 'en-US' })).toBe( - '7,654.32' - ) - expect(formatNumber({ input: 765.4321, type: NumberType.TokenTx, locale: 'en-US' })).toBe( - '765.432' - ) - expect(formatNumber({ input: 76.54321, type: NumberType.TokenTx, locale: 'en-US' })).toBe( - '76.5432' - ) - expect(formatNumber({ input: 7.654321, type: NumberType.TokenTx, locale: 'en-US' })).toBe( - '7.65432' - ) - expect(formatNumber({ input: 7.60000054321, type: NumberType.TokenTx, locale: 'en-US' })).toBe( - '7.60' - ) + expect(formatNumber({ input: 1234567.8901, type: NumberType.TokenTx, locale: 'en-US' })).toBe('1,234,567.89') + expect(formatNumber({ input: 765432.1, type: NumberType.TokenTx, locale: 'en-US' })).toBe('765,432.10') + + expect(formatNumber({ input: 7654.321, type: NumberType.TokenTx, locale: 'en-US' })).toBe('7,654.32') + expect(formatNumber({ input: 765.4321, type: NumberType.TokenTx, locale: 'en-US' })).toBe('765.432') + expect(formatNumber({ input: 76.54321, type: NumberType.TokenTx, locale: 'en-US' })).toBe('76.5432') + expect(formatNumber({ input: 7.654321, type: NumberType.TokenTx, locale: 'en-US' })).toBe('7.65432') + expect(formatNumber({ input: 7.60000054321, type: NumberType.TokenTx, locale: 'en-US' })).toBe('7.60') expect(formatNumber({ input: 7.6, type: NumberType.TokenTx, locale: 'en-US' })).toBe('7.60') expect(formatNumber({ input: 7, type: NumberType.TokenTx, locale: 'en-US' })).toBe('7.00') - expect(formatNumber({ input: 0.987654321, type: NumberType.TokenTx, locale: 'en-US' })).toBe( - '0.98765' - ) + expect(formatNumber({ input: 0.987654321, type: NumberType.TokenTx, locale: 'en-US' })).toBe('0.98765') expect(formatNumber({ input: 0.9, type: NumberType.TokenTx, locale: 'en-US' })).toBe('0.90') - expect(formatNumber({ input: 0.901000123, type: NumberType.TokenTx, locale: 'en-US' })).toBe( - '0.901' - ) + expect(formatNumber({ input: 0.901000123, type: NumberType.TokenTx, locale: 'en-US' })).toBe('0.901') expect(formatNumber({ input: 1e-9, type: NumberType.TokenTx, locale: 'en-US' })).toBe('<0.00001') expect(formatNumber({ input: 0, type: NumberType.TokenTx, locale: 'en-US' })).toBe('0') }) it('formats fiat estimates on token details pages correctly', () => { - expect( - formatNumber({ input: 1234567.891, type: NumberType.FiatTokenDetails, locale: 'en-US' }) - ).toBe('$1.23M') - expect( - formatNumber({ input: 1234.5678, type: NumberType.FiatTokenDetails, locale: 'en-US' }) - ).toBe('$1,234.57') - expect( - formatNumber({ input: 1.048942, type: NumberType.FiatTokenDetails, locale: 'en-US' }) - ).toBe('$1.049') + expect(formatNumber({ input: 1234567.891, type: NumberType.FiatTokenDetails, locale: 'en-US' })).toBe('$1.23M') + expect(formatNumber({ input: 1234.5678, type: NumberType.FiatTokenDetails, locale: 'en-US' })).toBe('$1,234.57') + expect(formatNumber({ input: 1.048942, type: NumberType.FiatTokenDetails, locale: 'en-US' })).toBe('$1.049') - expect( - formatNumber({ input: 0.001231, type: NumberType.FiatTokenDetails, locale: 'en-US' }) - ).toBe('$0.00123') - expect( - formatNumber({ input: 0.00001231, type: NumberType.FiatTokenDetails, locale: 'en-US' }) - ).toBe('$0.0000123') + expect(formatNumber({ input: 0.001231, type: NumberType.FiatTokenDetails, locale: 'en-US' })).toBe('$0.00123') + expect(formatNumber({ input: 0.00001231, type: NumberType.FiatTokenDetails, locale: 'en-US' })).toBe('$0.0000123') - expect( - formatNumber({ input: 1.234e-7, type: NumberType.FiatTokenDetails, locale: 'en-US' }) - ).toBe('$0.000000123') - expect( - formatNumber({ input: 9.876e-9, type: NumberType.FiatTokenDetails, locale: 'en-US' }) - ).toBe('<$0.00000001') + expect(formatNumber({ input: 1.234e-7, type: NumberType.FiatTokenDetails, locale: 'en-US' })).toBe('$0.000000123') + expect(formatNumber({ input: 9.876e-9, type: NumberType.FiatTokenDetails, locale: 'en-US' })).toBe('<$0.00000001') }) it('formats fiat estimates for tokens correctly', () => { - expect( - formatNumber({ input: 1234567.891, type: NumberType.FiatTokenPrice, locale: 'en-US' }) - ).toBe('$1.23M') - expect(formatNumber({ input: 1234.5678, type: NumberType.FiatTokenPrice, locale: 'en-US' })).toBe( - '$1,234.57' - ) + expect(formatNumber({ input: 1234567.891, type: NumberType.FiatTokenPrice, locale: 'en-US' })).toBe('$1.23M') + expect(formatNumber({ input: 1234.5678, type: NumberType.FiatTokenPrice, locale: 'en-US' })).toBe('$1,234.57') - expect(formatNumber({ input: 0.010235, type: NumberType.FiatTokenPrice, locale: 'en-US' })).toBe( - '$0.0102' - ) - expect(formatNumber({ input: 0.001231, type: NumberType.FiatTokenPrice, locale: 'en-US' })).toBe( - '$0.00123' - ) - expect( - formatNumber({ input: 0.00001231, type: NumberType.FiatTokenPrice, locale: 'en-US' }) - ).toBe('$0.0000123') + expect(formatNumber({ input: 0.010235, type: NumberType.FiatTokenPrice, locale: 'en-US' })).toBe('$0.0102') + expect(formatNumber({ input: 0.001231, type: NumberType.FiatTokenPrice, locale: 'en-US' })).toBe('$0.00123') + expect(formatNumber({ input: 0.00001231, type: NumberType.FiatTokenPrice, locale: 'en-US' })).toBe('$0.0000123') - expect(formatNumber({ input: 1.234e-7, type: NumberType.FiatTokenPrice, locale: 'en-US' })).toBe( - '$0.000000123' - ) - expect(formatNumber({ input: 9.876e-9, type: NumberType.FiatTokenPrice, locale: 'en-US' })).toBe( - '<$0.00000001' - ) + expect(formatNumber({ input: 1.234e-7, type: NumberType.FiatTokenPrice, locale: 'en-US' })).toBe('$0.000000123') + expect(formatNumber({ input: 9.876e-9, type: NumberType.FiatTokenPrice, locale: 'en-US' })).toBe('<$0.00000001') }) it('formats fiat estimates for token stats correctly', () => { - expect(formatNumber({ input: 1234576, type: NumberType.FiatTokenStats, locale: 'en-US' })).toBe( - '$1.2M' - ) - expect(formatNumber({ input: 234567, type: NumberType.FiatTokenStats, locale: 'en-US' })).toBe( - '$234.6K' - ) - expect(formatNumber({ input: 123.456, type: NumberType.FiatTokenStats, locale: 'en-US' })).toBe( - '$123.46' - ) - expect(formatNumber({ input: 1.23, type: NumberType.FiatTokenStats, locale: 'en-US' })).toBe( - '$1.23' - ) - expect(formatNumber({ input: 0.123, type: NumberType.FiatTokenStats, locale: 'en-US' })).toBe( - '$0.12' - ) - expect(formatNumber({ input: 0.00123, type: NumberType.FiatTokenStats, locale: 'en-US' })).toBe( - '<$0.01' - ) + expect(formatNumber({ input: 1234576, type: NumberType.FiatTokenStats, locale: 'en-US' })).toBe('$1.2M') + expect(formatNumber({ input: 234567, type: NumberType.FiatTokenStats, locale: 'en-US' })).toBe('$234.6K') + expect(formatNumber({ input: 123.456, type: NumberType.FiatTokenStats, locale: 'en-US' })).toBe('$123.46') + expect(formatNumber({ input: 1.23, type: NumberType.FiatTokenStats, locale: 'en-US' })).toBe('$1.23') + expect(formatNumber({ input: 0.123, type: NumberType.FiatTokenStats, locale: 'en-US' })).toBe('$0.12') + expect(formatNumber({ input: 0.00123, type: NumberType.FiatTokenStats, locale: 'en-US' })).toBe('<$0.01') expect(formatNumber({ input: 0, type: NumberType.FiatTokenStats, locale: 'en-US' })).toBe('-') }) it('formats gas USD prices correctly', () => { - expect(formatNumber({ input: 1234567.891, type: NumberType.FiatGasPrice, locale: 'en-US' })).toBe( - '$1.23M' - ) - expect(formatNumber({ input: 18.448, type: NumberType.FiatGasPrice, locale: 'en-US' })).toBe( - '$18.45' - ) - expect(formatNumber({ input: 0.0099, type: NumberType.FiatGasPrice, locale: 'en-US' })).toBe( - '<$0.01' - ) + expect(formatNumber({ input: 1234567.891, type: NumberType.FiatGasPrice, locale: 'en-US' })).toBe('$1.23M') + expect(formatNumber({ input: 18.448, type: NumberType.FiatGasPrice, locale: 'en-US' })).toBe('$18.45') + expect(formatNumber({ input: 0.0099, type: NumberType.FiatGasPrice, locale: 'en-US' })).toBe('<$0.01') }) it('formats USD token quantities prices correctly', () => { - expect( - formatNumber({ input: 1234567.891, type: NumberType.FiatTokenQuantity, locale: 'en-US' }) - ).toBe('$1.23M') - expect(formatNumber({ input: 18.448, type: NumberType.FiatTokenQuantity, locale: 'en-US' })).toBe( - '$18.45' - ) - expect(formatNumber({ input: 0.0099, type: NumberType.FiatTokenQuantity, locale: 'en-US' })).toBe( - '<$0.01' - ) - expect(formatNumber({ input: 0, type: NumberType.FiatTokenQuantity, locale: 'en-US' })).toBe( - '$0.00' - ) + expect(formatNumber({ input: 1234567.891, type: NumberType.FiatTokenQuantity, locale: 'en-US' })).toBe('$1.23M') + expect(formatNumber({ input: 18.448, type: NumberType.FiatTokenQuantity, locale: 'en-US' })).toBe('$18.45') + expect(formatNumber({ input: 0.0099, type: NumberType.FiatTokenQuantity, locale: 'en-US' })).toBe('<$0.01') + expect(formatNumber({ input: 0, type: NumberType.FiatTokenQuantity, locale: 'en-US' })).toBe('$0.00') }) it('formats Swap text input/output numbers correctly', () => { - expect( - formatNumber({ input: 1234567.8901, type: NumberType.SwapTradeAmount, locale: 'en-US' }) - ).toBe('1234570') - expect(formatNumber({ input: 765432.1, type: NumberType.SwapTradeAmount, locale: 'en-US' })).toBe( - '765432' - ) - - expect(formatNumber({ input: 7654.321, type: NumberType.SwapTradeAmount, locale: 'en-US' })).toBe( - '7654.32' - ) - expect(formatNumber({ input: 765.4321, type: NumberType.SwapTradeAmount, locale: 'en-US' })).toBe( - '765.432' - ) - expect(formatNumber({ input: 76.54321, type: NumberType.SwapTradeAmount, locale: 'en-US' })).toBe( - '76.5432' - ) - expect(formatNumber({ input: 7.654321, type: NumberType.SwapTradeAmount, locale: 'en-US' })).toBe( - '7.65432' - ) - expect( - formatNumber({ input: 7.60000054321, type: NumberType.SwapTradeAmount, locale: 'en-US' }) - ).toBe('7.60') - expect(formatNumber({ input: 7.6, type: NumberType.SwapTradeAmount, locale: 'en-US' })).toBe( - '7.60' - ) + expect(formatNumber({ input: 1234567.8901, type: NumberType.SwapTradeAmount, locale: 'en-US' })).toBe('1234570') + expect(formatNumber({ input: 765432.1, type: NumberType.SwapTradeAmount, locale: 'en-US' })).toBe('765432') + + expect(formatNumber({ input: 7654.321, type: NumberType.SwapTradeAmount, locale: 'en-US' })).toBe('7654.32') + expect(formatNumber({ input: 765.4321, type: NumberType.SwapTradeAmount, locale: 'en-US' })).toBe('765.432') + expect(formatNumber({ input: 76.54321, type: NumberType.SwapTradeAmount, locale: 'en-US' })).toBe('76.5432') + expect(formatNumber({ input: 7.654321, type: NumberType.SwapTradeAmount, locale: 'en-US' })).toBe('7.65432') + expect(formatNumber({ input: 7.60000054321, type: NumberType.SwapTradeAmount, locale: 'en-US' })).toBe('7.60') + expect(formatNumber({ input: 7.6, type: NumberType.SwapTradeAmount, locale: 'en-US' })).toBe('7.60') expect(formatNumber({ input: 7, type: NumberType.SwapTradeAmount, locale: 'en-US' })).toBe('7.00') - expect( - formatNumber({ input: 0.987654321, type: NumberType.SwapTradeAmount, locale: 'en-US' }) - ).toBe('0.98765') - expect(formatNumber({ input: 0.9, type: NumberType.SwapTradeAmount, locale: 'en-US' })).toBe( - '0.90' - ) - expect( - formatNumber({ input: 0.901000123, type: NumberType.SwapTradeAmount, locale: 'en-US' }) - ).toBe('0.901') - expect(formatNumber({ input: 1e-9, type: NumberType.SwapTradeAmount, locale: 'en-US' })).toBe( - '0.000000001' - ) + expect(formatNumber({ input: 0.987654321, type: NumberType.SwapTradeAmount, locale: 'en-US' })).toBe('0.98765') + expect(formatNumber({ input: 0.9, type: NumberType.SwapTradeAmount, locale: 'en-US' })).toBe('0.90') + expect(formatNumber({ input: 0.901000123, type: NumberType.SwapTradeAmount, locale: 'en-US' })).toBe('0.901') + expect(formatNumber({ input: 1e-9, type: NumberType.SwapTradeAmount, locale: 'en-US' })).toBe('0.000000001') expect(formatNumber({ input: 0, type: NumberType.SwapTradeAmount, locale: 'en-US' })).toBe('0') }) it('formats Swap prices correctly', () => { - expect(formatNumber({ input: 1234567.8901, type: NumberType.SwapPrice, locale: 'en-US' })).toBe( - '1234570' - ) - expect(formatNumber({ input: 765432.1, type: NumberType.SwapPrice, locale: 'en-US' })).toBe( - '765432' - ) - - expect(formatNumber({ input: 7654.321, type: NumberType.SwapPrice, locale: 'en-US' })).toBe( - '7654.32' - ) - expect(formatNumber({ input: 765.4321, type: NumberType.SwapPrice, locale: 'en-US' })).toBe( - '765.432' - ) - expect(formatNumber({ input: 76.54321, type: NumberType.SwapPrice, locale: 'en-US' })).toBe( - '76.5432' - ) - expect(formatNumber({ input: 7.654321, type: NumberType.SwapPrice, locale: 'en-US' })).toBe( - '7.65432' - ) - expect(formatNumber({ input: 7.60000054321, type: NumberType.SwapPrice, locale: 'en-US' })).toBe( - '7.60' - ) + expect(formatNumber({ input: 1234567.8901, type: NumberType.SwapPrice, locale: 'en-US' })).toBe('1234570') + expect(formatNumber({ input: 765432.1, type: NumberType.SwapPrice, locale: 'en-US' })).toBe('765432') + + expect(formatNumber({ input: 7654.321, type: NumberType.SwapPrice, locale: 'en-US' })).toBe('7654.32') + expect(formatNumber({ input: 765.4321, type: NumberType.SwapPrice, locale: 'en-US' })).toBe('765.432') + expect(formatNumber({ input: 76.54321, type: NumberType.SwapPrice, locale: 'en-US' })).toBe('76.5432') + expect(formatNumber({ input: 7.654321, type: NumberType.SwapPrice, locale: 'en-US' })).toBe('7.65432') + expect(formatNumber({ input: 7.60000054321, type: NumberType.SwapPrice, locale: 'en-US' })).toBe('7.60') expect(formatNumber({ input: 7.6, type: NumberType.SwapPrice, locale: 'en-US' })).toBe('7.60') expect(formatNumber({ input: 7, type: NumberType.SwapPrice, locale: 'en-US' })).toBe('7.00') - expect(formatNumber({ input: 0.987654321, type: NumberType.SwapPrice, locale: 'en-US' })).toBe( - '0.98765' - ) + expect(formatNumber({ input: 0.987654321, type: NumberType.SwapPrice, locale: 'en-US' })).toBe('0.98765') expect(formatNumber({ input: 0.9, type: NumberType.SwapPrice, locale: 'en-US' })).toBe('0.90') - expect(formatNumber({ input: 0.901000123, type: NumberType.SwapPrice, locale: 'en-US' })).toBe( - '0.901' - ) - expect(formatNumber({ input: 1e-9, type: NumberType.SwapPrice, locale: 'en-US' })).toBe( - '<0.00001' - ) + expect(formatNumber({ input: 0.901000123, type: NumberType.SwapPrice, locale: 'en-US' })).toBe('0.901') + expect(formatNumber({ input: 1e-9, type: NumberType.SwapPrice, locale: 'en-US' })).toBe('<0.00001') expect(formatNumber({ input: 0, type: NumberType.SwapPrice, locale: 'en-US' })).toBe('0') }) it('formats NFT numbers correctly', () => { - expect( - formatNumber({ input: 1234567000000000, type: NumberType.NFTTokenFloorPrice, locale: 'en-US' }) - ).toBe('>999T') - expect( - formatNumber({ input: 1002345, type: NumberType.NFTTokenFloorPrice, locale: 'en-US' }) - ).toBe('1.00M') - expect(formatNumber({ input: 1234, type: NumberType.NFTTokenFloorPrice, locale: 'en-US' })).toBe( - '1.23K' - ) - expect( - formatNumber({ input: 12.34467, type: NumberType.NFTTokenFloorPrice, locale: 'en-US' }) - ).toBe('12.34') - expect( - formatNumber({ input: 0.00909, type: NumberType.NFTTokenFloorPrice, locale: 'en-US' }) - ).toBe('0.009') - expect( - formatNumber({ input: 0.09001, type: NumberType.NFTTokenFloorPrice, locale: 'en-US' }) - ).toBe('0.090') - expect( - formatNumber({ input: 0.00099, type: NumberType.NFTTokenFloorPrice, locale: 'en-US' }) - ).toBe('<0.001') + expect(formatNumber({ input: 1234567000000000, type: NumberType.NFTTokenFloorPrice, locale: 'en-US' })).toBe('>999T') + expect(formatNumber({ input: 1002345, type: NumberType.NFTTokenFloorPrice, locale: 'en-US' })).toBe('1.00M') + expect(formatNumber({ input: 1234, type: NumberType.NFTTokenFloorPrice, locale: 'en-US' })).toBe('1.23K') + expect(formatNumber({ input: 12.34467, type: NumberType.NFTTokenFloorPrice, locale: 'en-US' })).toBe('12.34') + expect(formatNumber({ input: 0.00909, type: NumberType.NFTTokenFloorPrice, locale: 'en-US' })).toBe('0.009') + expect(formatNumber({ input: 0.09001, type: NumberType.NFTTokenFloorPrice, locale: 'en-US' })).toBe('0.090') + expect(formatNumber({ input: 0.00099, type: NumberType.NFTTokenFloorPrice, locale: 'en-US' })).toBe('<0.001') expect(formatNumber({ input: 0, type: NumberType.NFTTokenFloorPrice, locale: 'en-US' })).toBe('0') - expect( - formatNumber({ input: 1234576, type: NumberType.NFTCollectionStats, locale: 'en-US' }) - ).toBe('1.2M') - expect( - formatNumber({ input: 234567, type: NumberType.NFTCollectionStats, locale: 'en-US' }) - ).toBe('234.6K') - expect(formatNumber({ input: 999, type: NumberType.NFTCollectionStats, locale: 'en-US' })).toBe( - '999' - ) + expect(formatNumber({ input: 1234576, type: NumberType.NFTCollectionStats, locale: 'en-US' })).toBe('1.2M') + expect(formatNumber({ input: 234567, type: NumberType.NFTCollectionStats, locale: 'en-US' })).toBe('234.6K') + expect(formatNumber({ input: 999, type: NumberType.NFTCollectionStats, locale: 'en-US' })).toBe('999') expect(formatNumber({ input: 0, type: NumberType.NFTCollectionStats, locale: 'en-US' })).toBe('0') }) diff --git a/packages/utilities/src/format/localeBased.ts b/packages/utilities/src/format/localeBased.ts index 9c4ca82d4c4..9189c562b79 100644 --- a/packages/utilities/src/format/localeBased.ts +++ b/packages/utilities/src/format/localeBased.ts @@ -104,9 +104,7 @@ export function formatPercent(rawPercentage: Maybe, locale: str return '-' } const percentage = - typeof rawPercentage === 'string' - ? parseFloat(rawPercentage) - : parseFloat(rawPercentage.toString()) + typeof rawPercentage === 'string' ? parseFloat(rawPercentage) : parseFloat(rawPercentage.toString()) return formatNumber({ input: percentage / 100, type: NumberType.Percentage, locale }) } @@ -129,8 +127,8 @@ export function addFiatSymbolToNumber({ ? parts[1]?.value : '' : parts[parts.length - 2]?.type === 'literal' - ? parts[parts.length - 2]?.value - : '' + ? parts[parts.length - 2]?.value + : '' return isSymbolAtFront ? `${currencySymbol}${extra}${value}` : `${value}${extra}${currencySymbol}` } @@ -146,10 +144,7 @@ export type FiatCurrencyComponents = { * Helper function to return components of a currency value for a specific locale * E.g. comma, period, or space for separating thousands */ -export function getFiatCurrencyComponents( - locale: string, - currencyCode: string -): FiatCurrencyComponents { +export function getFiatCurrencyComponents(locale: string, currencyCode: string): FiatCurrencyComponents { const format = TwoDecimalsCurrency.createFormat(locale, currencyCode) // See MDN for official docs https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/formatToParts diff --git a/packages/utilities/src/format/truncateToMaxDecimals.test.ts b/packages/utilities/src/format/truncateToMaxDecimals.test.ts index aff5db25669..e924e08bec0 100644 --- a/packages/utilities/src/format/truncateToMaxDecimals.test.ts +++ b/packages/utilities/src/format/truncateToMaxDecimals.test.ts @@ -1,7 +1,4 @@ -import { - maxDecimalsReached, - truncateToMaxDecimals, -} from 'utilities/src/format/truncateToMaxDecimals' +import { maxDecimalsReached, truncateToMaxDecimals } from 'utilities/src/format/truncateToMaxDecimals' describe('truncateToMaxDecimals', () => { it('returns the same string if it has less than max decimals', () => { @@ -11,7 +8,7 @@ describe('truncateToMaxDecimals', () => { truncateToMaxDecimals({ value: valueWith1Decimal, maxDecimals: 1, - }) + }), ).toBe(valueWith1Decimal) const valueWith18Decimals = '1.999999999999999999' @@ -20,7 +17,7 @@ describe('truncateToMaxDecimals', () => { truncateToMaxDecimals({ value: valueWith18Decimals, maxDecimals: 18, - }) + }), ).toBe(valueWith18Decimals) }) @@ -29,14 +26,14 @@ describe('truncateToMaxDecimals', () => { truncateToMaxDecimals({ value: '1.01', maxDecimals: 1, - }) + }), ).toBe('1.0') expect( truncateToMaxDecimals({ value: '1.123456789', maxDecimals: 3, - }) + }), ).toBe('1.123') }) @@ -45,7 +42,7 @@ describe('truncateToMaxDecimals', () => { truncateToMaxDecimals({ value: '1.', maxDecimals: 8, - }) + }), ).toBe('1.') }) }) @@ -56,14 +53,14 @@ describe('maxDecimalsReached', () => { maxDecimalsReached({ value: '1.1234', maxDecimals: 4, - }) + }), ).toBe(true) expect( maxDecimalsReached({ value: '1.12345', maxDecimals: 4, - }) + }), ).toBe(true) }) @@ -72,14 +69,14 @@ describe('maxDecimalsReached', () => { maxDecimalsReached({ value: '1.123', maxDecimals: 4, - }) + }), ).toBe(false) expect( maxDecimalsReached({ value: '1', maxDecimals: 4, - }) + }), ).toBe(false) }) }) diff --git a/packages/utilities/src/format/urls.test.ts b/packages/utilities/src/format/urls.test.ts index bb2d8b9f4d9..47c761dc351 100644 --- a/packages/utilities/src/format/urls.test.ts +++ b/packages/utilities/src/format/urls.test.ts @@ -19,9 +19,7 @@ describe(uriToHttpUrls, () => { ]) }) it('returns ipfs gateways for wrongly formated ipfs:// urls', () => { - expect( - uriToHttpUrls('ipfs://ipfs/QmSP4nq9fnN9dAiCj42ug9Wa79rqmQerZXZch82VqpiH7U/image.gif') - ).toEqual([ + expect(uriToHttpUrls('ipfs://ipfs/QmSP4nq9fnN9dAiCj42ug9Wa79rqmQerZXZch82VqpiH7U/image.gif')).toEqual([ 'https://cloudflare-ipfs.com/ipfs/QmSP4nq9fnN9dAiCj42ug9Wa79rqmQerZXZch82VqpiH7U/image.gif/', 'https://ipfs.io/ipfs/QmSP4nq9fnN9dAiCj42ug9Wa79rqmQerZXZch82VqpiH7U/image.gif/', ]) diff --git a/packages/utilities/src/logger/Datadog.ts b/packages/utilities/src/logger/Datadog.ts new file mode 100644 index 00000000000..82ae814b006 --- /dev/null +++ b/packages/utilities/src/logger/Datadog.ts @@ -0,0 +1,75 @@ +import { datadogLogs } from '@datadog/browser-logs' +import { getEnvName, isTestEnv } from 'utilities/src/environment' +import { LogLevel, LoggerErrorContext } from 'utilities/src/logger/types' +import { v4 as uuidv4 } from 'uuid' + +// setup user information +const USER_ID_KEY = 'datadog-user-id' + +export function setupDatadog(): void { + if (isTestEnv()) { + return + } + if (!process.env.REACT_APP_DATADOG_CLIENT_TOKEN) { + // eslint-disable-next-line no-console + console.error(`No datadog client token, disabling`) + return + } + + datadogLogs.init({ + clientToken: process.env.REACT_APP_DATADOG_CLIENT_TOKEN, + site: 'datadoghq.com', + forwardErrorsToLogs: true, + }) + + let userId = localStorage.getItem(USER_ID_KEY) + if (!userId) { + localStorage.setItem(USER_ID_KEY, (userId = uuidv4())) + } + datadogLogs.setUser({ + id: userId, + }) + + datadogLogs.setUserProperty('env', getEnvName()) + datadogLogs.setUserProperty('version', process.env.REACT_APP_GIT_COMMIT_HASH) +} + +export function logToDatadog( + message: string, + { + level, + ...options + }: { + level: LogLevel + args: unknown[] + fileName: string + functionName: string + }, +): void { + if (isTestEnv()) { + return + } + datadogLogs.logger[level](message, options) +} + +export function logErrorToDatadog(error: Error, context?: LoggerErrorContext): void { + if (isTestEnv()) { + return + } + if (error instanceof Error) { + datadogLogs.logger.error(error.message, { + error: { + kind: error.name, + stack: error.stack, + }, + ...context, + }) + } else { + datadogLogs.logger.error(error, { + error: { + stack: new Error().stack, + }, + ...context, + }) + } +} diff --git a/packages/utilities/src/logger/Sentry.native.ts b/packages/utilities/src/logger/Sentry.native.ts index ec0a8000613..f6a7f32dfd3 100644 --- a/packages/utilities/src/logger/Sentry.native.ts +++ b/packages/utilities/src/logger/Sentry.native.ts @@ -21,12 +21,7 @@ export function captureException(error: unknown, captureContext?: CaptureContext * @param extraArgs Key/value pairs to enrich logging and allow filtering. * More info here: https://docs.sentry.io/platforms/react-native/enriching-events/context/ */ -export function captureMessage( - level: SeverityLevel, - context: string, - message: string, - ...extraArgs: unknown[] -): void { +export function captureMessage(level: SeverityLevel, context: string, message: string, ...extraArgs: unknown[]): void { SentryRN.captureMessage(message, { level, tags: { mobileContext: context }, diff --git a/packages/utilities/src/logger/Sentry.ts b/packages/utilities/src/logger/Sentry.ts index 8e0ba6d4096..ba87f0444d8 100644 --- a/packages/utilities/src/logger/Sentry.ts +++ b/packages/utilities/src/logger/Sentry.ts @@ -1,6 +1,6 @@ import { SeverityLevel } from '@sentry/types' import { NotImplementedError } from 'utilities/src/errors' -import { LoggerErrorContext } from 'utilities/src/logger/logger' +import { LoggerErrorContext } from 'utilities/src/logger/types' export type BreadCrumb = { message: string @@ -14,12 +14,7 @@ export type BreadCrumb = { /** Dummy Sentry logging class. Overridden by mobile or extension related code. */ export interface ISentry { captureException(error: unknown, captureContext: LoggerErrorContext): void - captureMessage( - level: SeverityLevel, - context: string, - message: string, - ...extraArgs: unknown[] - ): void + captureMessage(level: SeverityLevel, context: string, message: string, ...extraArgs: unknown[]): void addBreadCrumb(breadCrumb: BreadCrumb): void } diff --git a/packages/utilities/src/logger/Sentry.web.ts b/packages/utilities/src/logger/Sentry.web.ts index 8ed7cc38664..f7087c079f0 100644 --- a/packages/utilities/src/logger/Sentry.web.ts +++ b/packages/utilities/src/logger/Sentry.web.ts @@ -21,12 +21,7 @@ export function captureException(error: unknown, captureContext?: CaptureContext * @param extraArgs Key/value pairs to enrich logging and allow filtering. * More info here: https://docs.sentry.io/platforms/react-native/enriching-events/context/ */ -export function captureMessage( - level: SeverityLevel, - context: string, - message: string, - ...extraArgs: unknown[] -): void { +export function captureMessage(level: SeverityLevel, context: string, message: string, ...extraArgs: unknown[]): void { SentryReact.captureMessage(message, { level, tags: { webContext: context }, diff --git a/packages/utilities/src/logger/console.ts b/packages/utilities/src/logger/console.ts index 89e17208316..3d642959bf3 100644 --- a/packages/utilities/src/logger/console.ts +++ b/packages/utilities/src/logger/console.ts @@ -55,8 +55,7 @@ function isIgnoredMessage(msg: unknown, arg: unknown): boolean { return IGNORED_MESSAGES.some((ignoredMessage) => { const result = msg.includes(ignoredMessage.message) && - (!ignoredMessage.firstArgValues || - ignoredMessage.firstArgValues.some((argVal) => argVal === arg)) + (!ignoredMessage.firstArgValues || ignoredMessage.firstArgValues.some((argVal) => argVal === arg)) return result }) } diff --git a/packages/utilities/src/logger/logger.ts b/packages/utilities/src/logger/logger.ts index 3221f6b54a5..7acb1b21757 100644 --- a/packages/utilities/src/logger/logger.ts +++ b/packages/utilities/src/logger/logger.ts @@ -1,6 +1,7 @@ -/* eslint-disable no-console */ -import { Extras, ScopeContext } from '@sentry/types' +import { Extras } from '@sentry/types' +import { logErrorToDatadog, logToDatadog } from 'utilities/src/logger/Datadog' import { Sentry } from 'utilities/src/logger/Sentry' +import { LogLevel, LoggerErrorContext } from 'utilities/src/logger/types' import { isInterface, isWeb } from 'utilities/src/platform' // weird temp fix: the web app is complaining about __DEV__ being global @@ -11,7 +12,6 @@ import { isInterface, isWeb } from 'utilities/src/platform' // perhaps because the declarations are not applying to external packages // but somehow its also not picking up the declarations here declare global { - // @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore its ok const __DEV__: boolean @@ -19,14 +19,8 @@ declare global { const SENTRY_CHAR_LIMIT = 8192 -type LogLevel = 'debug' | 'info' | 'warn' | 'error' - -export type LoggerErrorContext = Omit, 'tags'> & { - tags: { file: string; function: string } -} - /** - * Logs a message to console. Additionally sends log to Sentry if using 'error', 'warn', or 'info'. + * Logs a message to console. Additionally sends log to Sentry and Datadog if using 'error', 'warn', or 'info'. * Use `logger.debug` for development only logs. * * ex. `logger.warn('myFile', 'myFunc', 'Some warning', myArray)` @@ -43,8 +37,7 @@ export const logger = { logMessage('info', fileName, functionName, message, ...args), warn: (fileName: string, functionName: string, message: string, ...args: unknown[]): void => logMessage('warn', fileName, functionName, message, ...args), - error: (error: unknown, captureContext: LoggerErrorContext): void => - logException(error, captureContext), + error: (error: unknown, captureContext: LoggerErrorContext): void => logException(error, captureContext), } function logMessage( @@ -56,6 +49,7 @@ function logMessage( ): void { // Log to console directly for dev builds or interface for debugging if (__DEV__ || isInterface) { + // eslint-disable-next-line no-console console[level](...formatMessage(level, fileName, functionName, message), ...args) } @@ -69,6 +63,15 @@ function logMessage( } else if (level === 'info') { Sentry.captureMessage('info', `${fileName}#${functionName}`, message, ...args) } + + if (isInterface) { + logToDatadog(message, { + level, + args, + functionName, + fileName, + }) + } } function logException(error: unknown, captureContext: LoggerErrorContext): void { @@ -76,6 +79,7 @@ function logException(error: unknown, captureContext: LoggerErrorContext): void // Log to console directly for dev builds or interface for debugging if (__DEV__ || isInterface) { + // eslint-disable-next-line no-console console.error(error) } @@ -94,12 +98,16 @@ function logException(error: unknown, captureContext: LoggerErrorContext): void } Sentry.captureException(error, updatedContext) + if (isInterface) { + logErrorToDatadog(error instanceof Error ? error : new Error(`${error}`), updatedContext) + } } interface RNError { nativeStackAndroid?: unknown userInfo?: unknown } + // Adds extra fields from errors provided by React Native function addErrorExtras(error: unknown, captureContext: LoggerErrorContext): LoggerErrorContext { if (error instanceof Error) { @@ -127,13 +135,10 @@ function formatMessage( level: LogLevel, fileName: string, functionName: string, - message: string + message: string, ): (string | Record)[] { const t = new Date() - const timeString = `${pad(t.getHours())}:${pad(t.getMinutes())}:${pad(t.getSeconds())}.${pad( - t.getMilliseconds(), - 3 - )}` + const timeString = `${pad(t.getHours())}:${pad(t.getMinutes())}:${pad(t.getSeconds())}.${pad(t.getMilliseconds(), 3)}` if (isWeb) { // Simpler printing for web logging return [ @@ -152,3 +157,14 @@ function formatMessage( return [`${timeString}::${fileName}#${functionName}`, message] } } + +export function createAndLogError(funcName: string): Error { + const e = new Error('Unsupported app environment that failed all checks') + logger.error(e, { + tags: { + file: 'utilities/src/environment/index.ts', + function: funcName, + }, + }) + return e +} diff --git a/packages/utilities/src/logger/mocks.ts b/packages/utilities/src/logger/mocks.ts new file mode 100644 index 00000000000..3260a1dfea1 --- /dev/null +++ b/packages/utilities/src/logger/mocks.ts @@ -0,0 +1,7 @@ +// mock this since it errors about multiple SDK instances in test mode +jest.mock('@datadog/browser-logs', () => ({ + datadogLogs: { + // leave it empty as we should avoid it in test mode + logger: {}, + }, +})) diff --git a/packages/utilities/src/logger/types.ts b/packages/utilities/src/logger/types.ts new file mode 100644 index 00000000000..fa9a08c709d --- /dev/null +++ b/packages/utilities/src/logger/types.ts @@ -0,0 +1,7 @@ +import { type ScopeContext } from '@sentry/types' + +export type LogLevel = 'debug' | 'info' | 'warn' | 'error' + +export type LoggerErrorContext = Omit, 'tags'> & { + tags: { file: string; function: string } +} diff --git a/packages/utilities/src/platform/index.ts b/packages/utilities/src/platform/index.ts index 7209025e438..3857be9527f 100644 --- a/packages/utilities/src/platform/index.ts +++ b/packages/utilities/src/platform/index.ts @@ -26,9 +26,7 @@ export const isMobile: boolean = export const isWebIOS: boolean = typeof document !== 'undefined' && typeof navigator !== 'undefined' && - (['iPad Simulator', 'iPhone Simulator', 'iPod Simulator', 'iPad', 'iPhone'].includes( - navigator.platform - ) || + (['iPad Simulator', 'iPhone Simulator', 'iPod Simulator', 'iPad', 'iPhone'].includes(navigator.platform) || // iPad on iOS 13 detection (navigator.userAgent.includes('Mac') && 'ontouchend' in document)) @@ -43,10 +41,8 @@ export const isTouchable = ('ontouchstart' in window || navigator.maxTouchPoints > 0) // Browser -export const isChrome: boolean = - typeof navigator !== 'undefined' && /Chrome/.test(navigator.userAgent || '') -export const isSafari: boolean = - typeof navigator !== 'undefined' && /Safari/.test(navigator.userAgent || '') +export const isChrome: boolean = typeof navigator !== 'undefined' && /Chrome/.test(navigator.userAgent || '') +export const isSafari: boolean = typeof navigator !== 'undefined' && /Safari/.test(navigator.userAgent || '') export const isMobileWebSafari: boolean = isTouchable && isSafari export const isMobileWebAndroid: boolean = isTouchable && isWebAndroid diff --git a/packages/utilities/src/primitives/array.ts b/packages/utilities/src/primitives/array.ts index 7a1b8f98756..63cbeff0cfa 100644 --- a/packages/utilities/src/primitives/array.ts +++ b/packages/utilities/src/primitives/array.ts @@ -2,10 +2,7 @@ function onlyUnique(value: T, index: number, self: T[]): boolean { return self.indexOf(value) === index } -export function unique( - array: T[], - isUnique: (value: T, index: number, self: T[]) => boolean = onlyUnique -): T[] { +export function unique(array: T[], isUnique: (value: T, index: number, self: T[]) => boolean = onlyUnique): T[] { return array.filter(isUnique) } @@ -19,11 +16,7 @@ export function next(array: T[], current: T): T | undefined { // get items in `array` that are not in `without` // e.g. difference([B, C, D], [A, B, C]) would return ([D]) -export function differenceWith( - array: T[], - without: T[], - comparator: (item1: T, item2: T) => boolean -): T[] { +export function differenceWith(array: T[], without: T[], comparator: (item1: T, item2: T) => boolean): T[] { return array.filter((item: T) => { const inWithout = Boolean(without.find((otherItem: T) => comparator(item, otherItem))) return !inWithout diff --git a/packages/utilities/src/primitives/objects.test.ts b/packages/utilities/src/primitives/objects.test.ts index ad5b9c1b350..eefa345856b 100644 --- a/packages/utilities/src/primitives/objects.test.ts +++ b/packages/utilities/src/primitives/objects.test.ts @@ -5,13 +5,7 @@ const DAI = new Token(1, '0x6b175474e89094c44da98b954eedeac495271d0f', 18, 'DAI' const USDC = new Token(1, '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', 6, 'USDC', 'USD//C') -const USDC_ARBITRUM = new Token( - 42161, - '0xff970a61a04b1ca14834a43f5de4533ebddb5cc8', - 6, - 'USDC', - 'USD//C' -) +const USDC_ARBITRUM = new Token(42161, '0xff970a61a04b1ca14834a43f5de4533ebddb5cc8', 6, 'USDC', 'USD//C') describe(flattenObjectOfObjects, () => { it('correctly flattens', () => { @@ -29,7 +23,7 @@ describe(flattenObjectOfObjects, () => { 42161: { [USDC_ARBITRUM.address]: USDC_ARBITRUM, }, - }) + }), ).toEqual([DAI, USDC, USDC_ARBITRUM]) expect(flattenObjectOfObjects({ 1: { '0x1': [1, 2, 3], '0x2': 4 } })).toEqual([[1, 2, 3], 4]) diff --git a/packages/utilities/src/primitives/string.test.ts b/packages/utilities/src/primitives/string.test.ts index 82902de8513..22d97002359 100644 --- a/packages/utilities/src/primitives/string.test.ts +++ b/packages/utilities/src/primitives/string.test.ts @@ -1,9 +1,4 @@ -import { - concatStrings, - escapeRegExp, - normalizeTextInput, - trimToLength, -} from 'utilities/src/primitives/string' +import { concatStrings, escapeRegExp, normalizeTextInput, trimToLength } from 'utilities/src/primitives/string' describe(trimToLength, () => { it('handles empty string', () => { diff --git a/packages/utilities/src/primitives/string.ts b/packages/utilities/src/primitives/string.ts index 2d3d696951d..a3e994a395b 100644 --- a/packages/utilities/src/primitives/string.ts +++ b/packages/utilities/src/primitives/string.ts @@ -37,9 +37,7 @@ export function containsNonPrintableChars(msg: string): boolean { const regex = /[\p{C}\p{Z}]/gu if (regex.test(msg)) { - return ![...msg].every( - (char) => char === '\n' || char === '\r' || char === '\t' || !/\p{C}/u.test(char) - ) + return ![...msg].every((char) => char === '\n' || char === '\r' || char === '\t' || !/\p{C}/u.test(char)) } return false diff --git a/packages/utilities/src/react/hook.test.tsx b/packages/utilities/src/react/hook.test.tsx index 6e99cf88289..c10bdfade0b 100644 --- a/packages/utilities/src/react/hook.test.tsx +++ b/packages/utilities/src/react/hook.test.tsx @@ -71,9 +71,7 @@ describe('useAsyncData', () => { const asyncCallback = jest.fn().mockResolvedValue('data') const onCancel = jest.fn() - const { unmount, rerender, waitForNextUpdate } = renderHook(() => - useAsyncData(asyncCallback, onCancel) - ) + const { unmount, rerender, waitForNextUpdate } = renderHook(() => useAsyncData(asyncCallback, onCancel)) await act(async () => { rerender() @@ -93,7 +91,7 @@ describe('useAsyncData', () => { ({ asyncCallback, onCancel }) => useAsyncData(asyncCallback, onCancel), { initialProps: { asyncCallback: initialCallback, onCancel: cancel }, - } + }, ) expect(initialCallback).toHaveBeenCalledTimes(1) @@ -117,7 +115,7 @@ describe('useAsyncData', () => { ({ asyncCallback, onCancel }) => useAsyncData(asyncCallback, onCancel), { initialProps: { asyncCallback: initialCallback, onCancel: cancel }, - } + }, ) const newCallback = jest.fn().mockResolvedValue('data') @@ -136,12 +134,9 @@ describe('useAsyncData', () => { it('enters loading state and returns new callback result when the callback changes', async () => { const initialCallback = jest.fn().mockImplementation(() => createPromise('data1')) - const { rerender, result, waitForNextUpdate } = renderHook( - ({ asyncCallback }) => useAsyncData(asyncCallback), - { - initialProps: { asyncCallback: initialCallback }, - } - ) + const { rerender, result, waitForNextUpdate } = renderHook(({ asyncCallback }) => useAsyncData(asyncCallback), { + initialProps: { asyncCallback: initialCallback }, + }) expect(result.current).toEqual({ data: undefined, isLoading: true }) @@ -201,11 +196,11 @@ describe('useMemoCompare', () => { (props) => useMemoCompare( () => props, - () => true + () => true, ), { initialProps: initialValue, - } + }, ) rerender({ a: 1 }) @@ -217,11 +212,11 @@ describe('useMemoCompare', () => { (props) => useMemoCompare( () => props, - () => false + () => false, ), { initialProps: { a: 1 }, - } + }, ) const newValue = { a: 2 } diff --git a/packages/utilities/src/react/hooks.ts b/packages/utilities/src/react/hooks.ts index b31ce71f874..dc86fa90b92 100644 --- a/packages/utilities/src/react/hooks.ts +++ b/packages/utilities/src/react/hooks.ts @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useRef, useState } from 'react' +import { RefObject, useEffect, useMemo, useRef, useState } from 'react' // modified from https://usehooks.com/usePrevious/ export function usePrevious(value: T): T | undefined { @@ -19,7 +19,7 @@ export function usePrevious(value: T): T | undefined { // above link contains example on how to add delayed execution if ever needed export function useAsyncData( asyncCallback: () => Promise | undefined, - onCancel?: () => void + onCancel?: () => void, ): { isLoading: boolean data: T | undefined @@ -110,3 +110,39 @@ export function useMemoCompare(next: () => T, compare: (a: T | undefined, b: // Finally, if equal then return the previous value if it's set return isEqual && previous ? previous : nextValue } + +export function useOnClickOutside( + node: RefObject, + handler: undefined | (() => void), + ignoredNodes: Array> = [], +): void { + const handlerRef = useRef void)>(handler) + + useEffect(() => { + handlerRef.current = handler + }, [handler]) + + useEffect(() => { + const handleClickOutside = (e: MouseEvent): void => { + const nodeClicked = node.current?.contains(e.target as Node) + const ignoredNodeClicked = ignoredNodes.reduce( + (reducer, val) => reducer || !!val.current?.contains(e.target as Node), + false, + ) + + if ((nodeClicked || ignoredNodeClicked) ?? false) { + return + } + + if (handlerRef.current) { + handlerRef.current() + } + } + + document.addEventListener('mousedown', handleClickOutside) + + return () => { + document.removeEventListener('mousedown', handleClickOutside) + } + }, [node, ignoredNodes]) +} diff --git a/packages/utilities/src/telemetry/analytics/analytics.native.ts b/packages/utilities/src/telemetry/analytics/analytics.native.ts index bfe358b324d..c2a7a910f9c 100644 --- a/packages/utilities/src/telemetry/analytics/analytics.native.ts +++ b/packages/utilities/src/telemetry/analytics/analytics.native.ts @@ -1,13 +1,5 @@ /* eslint-disable no-restricted-imports */ -import { - Identify, - flush, - getUserId, - identify, - init, - setDeviceId, - track, -} from '@amplitude/analytics-react-native' +import { Identify, flush, getUserId, identify, init, setDeviceId, track } from '@amplitude/analytics-react-native' import { ANONYMOUS_DEVICE_ID } from '@uniswap/analytics' import { ApplicationTransport } from 'utilities/src/telemetry/analytics/ApplicationTransport' import { Analytics, UserPropertyValue } from 'utilities/src/telemetry/analytics/analytics' @@ -33,7 +25,7 @@ export const analytics: Analytics = { transportProvider: ApplicationTransport, allowed: boolean, _initHash?: string, - userIdGetter?: () => Promise + userIdGetter?: () => Promise, ): Promise { try { allowAnalytics = allowed @@ -47,7 +39,7 @@ export const analytics: Analytics = { ...AMPLITUDE_SHARED_TRACKING_OPTIONS, ...AMPLITUDE_NATIVE_TRACKING_OPTIONS, }, - } + }, ) userId = userIdGetter ? await userIdGetter() : getUserId() diff --git a/packages/utilities/src/telemetry/analytics/analytics.ts b/packages/utilities/src/telemetry/analytics/analytics.ts index 95eb4a8c442..482dedc41a4 100644 --- a/packages/utilities/src/telemetry/analytics/analytics.ts +++ b/packages/utilities/src/telemetry/analytics/analytics.ts @@ -13,7 +13,7 @@ export interface Analytics { transportProvider: ApplicationTransport, allowed: boolean, initHash?: string, - userIdGetter?: () => Promise + userIdGetter?: () => Promise, ): Promise setAllowAnalytics(allowed: boolean): Promise sendEvent(eventName: string, eventProperties: Record): void @@ -26,7 +26,7 @@ export const analytics: Analytics = { _transportProvider: ApplicationTransport, _allowed: boolean, _initHash?: string, - _userIdGetter?: () => Promise + _userIdGetter?: () => Promise, ): Promise { throw new NotImplementedError('initAnalytics') }, diff --git a/packages/utilities/src/telemetry/analytics/analytics.web.ts b/packages/utilities/src/telemetry/analytics/analytics.web.ts index f8c41e0b321..82a1b283e26 100644 --- a/packages/utilities/src/telemetry/analytics/analytics.web.ts +++ b/packages/utilities/src/telemetry/analytics/analytics.web.ts @@ -1,12 +1,4 @@ -import { - flush, - getUserId, - Identify, - identify, - init, - setDeviceId, - track, -} from '@amplitude/analytics-browser' +import { flush, getUserId, Identify, identify, init, setDeviceId, track } from '@amplitude/analytics-browser' // eslint-disable-next-line no-restricted-imports import { ANONYMOUS_DEVICE_ID } from '@uniswap/analytics' // eslint-disable-next-line no-restricted-imports @@ -66,7 +58,7 @@ export const analytics: Analytics = { transportProvider: ApplicationTransport, allowed: boolean, initHash?: string, - userIdGetter?: () => Promise + userIdGetter?: () => Promise, ): Promise { // Set properties commitHash = initHash @@ -80,7 +72,7 @@ export const analytics: Analytics = { transportProvider, // Used to support custom reverse proxy header // Disable tracking of private user information by Amplitude trackingOptions: AMPLITUDE_SHARED_TRACKING_OPTIONS, - } + }, ) userId = userIdGetter ? await userIdGetter() : getUserId() @@ -123,11 +115,7 @@ export const analytics: Analytics = { loggers.flushEvents() flush() }, - async setUserProperty( - property: string, - value: UserPropertyValue, - insert?: boolean - ): Promise { + async setUserProperty(property: string, value: UserPropertyValue, insert?: boolean): Promise { if (!(await getAnalyticsAtomDirect())) { return } diff --git a/packages/utilities/src/telemetry/analytics/logging.ts b/packages/utilities/src/telemetry/analytics/logging.ts index f3e11a85acb..3be0ceb34e3 100644 --- a/packages/utilities/src/telemetry/analytics/logging.ts +++ b/packages/utilities/src/telemetry/analytics/logging.ts @@ -1,4 +1,4 @@ -import { isNonJestDev } from 'utilities/src/environment' +import { isNonJestDev } from 'utilities/src/environment/constants' import { logger } from 'utilities/src/logger/logger' // eslint-disable-next-line no-restricted-imports import { UserPropertyValue } from 'utilities/src/telemetry/analytics/analytics' diff --git a/packages/utilities/src/telemetry/trace/AnalyticsNavigationContext.tsx b/packages/utilities/src/telemetry/trace/AnalyticsNavigationContext.tsx index 4a265a34212..477a50fc62a 100644 --- a/packages/utilities/src/telemetry/trace/AnalyticsNavigationContext.tsx +++ b/packages/utilities/src/telemetry/trace/AnalyticsNavigationContext.tsx @@ -19,9 +19,5 @@ export function AnalyticsNavigationContextProvider({ shouldLogScreen, children, }: PropsWithChildren): JSX.Element { - return ( - - {children} - - ) + return {children} } diff --git a/packages/utilities/src/telemetry/trace/Trace.tsx b/packages/utilities/src/telemetry/trace/Trace.tsx index 4ba506ae9f3..5b116c8df20 100644 --- a/packages/utilities/src/telemetry/trace/Trace.tsx +++ b/packages/utilities/src/telemetry/trace/Trace.tsx @@ -8,11 +8,7 @@ import { useAnalyticsNavigationContext } from 'utilities/src/telemetry/trace/Ana import { ITraceContext, TraceContext, useTrace } from 'utilities/src/telemetry/trace/TraceContext' import { getEventHandlers } from 'utilities/src/telemetry/trace/utils' -export function getEventsFromProps( - logPress?: boolean, - logFocus?: boolean, - logKeyPress?: boolean -): string[] { +export function getEventsFromProps(logPress?: boolean, logFocus?: boolean, logKeyPress?: boolean): string[] { const events = [] if (logPress) { events.push(isWeb ? 'onClick' : 'onPress') @@ -70,8 +66,7 @@ function _Trace({ }: PropsWithChildren): JSX.Element { const id = useId() - const { useIsPartOfNavigationTree, shouldLogScreen: shouldLogScreen } = - useAnalyticsNavigationContext() + const { useIsPartOfNavigationTree, shouldLogScreen: shouldLogScreen } = useAnalyticsNavigationContext() const isPartOfNavigationTree = useIsPartOfNavigationTree() const parentTrace = useTrace() @@ -135,8 +130,8 @@ function _Trace({ events, eventOnTrigger ?? SharedEventName.ELEMENT_CLICKED, element, - properties - ) + properties, + ), ) }) } @@ -154,16 +149,14 @@ function _Trace({ combinedProps={combinedProps} directFromPage={directFromPage} logImpression={logImpression} - properties={properties}> + properties={properties} + > {modifiedChildren} ) } -type NavAwareTraceProps = Pick< - TraceProps, - 'logImpression' | 'properties' | 'directFromPage' | 'eventOnTrigger' -> +type NavAwareTraceProps = Pick // Internal component to keep track of navigation events // Needed since we need to rely on `navigation.useFocusEffect` to track @@ -186,7 +179,7 @@ function NavAwareTrace({ analytics.sendEvent(eventOnTrigger ?? SharedEventName.PAGE_VIEWED, eventProps) } } - }, [combinedProps, directFromPage, eventOnTrigger, logImpression, properties, shouldLogScreen]) + }, [combinedProps, directFromPage, eventOnTrigger, logImpression, properties, shouldLogScreen]), ) return <>{children} diff --git a/packages/utilities/src/telemetry/trace/utils.ts b/packages/utilities/src/telemetry/trace/utils.ts index 1e837d3f419..3af4d5c9244 100644 --- a/packages/utilities/src/telemetry/trace/utils.ts +++ b/packages/utilities/src/telemetry/trace/utils.ts @@ -15,7 +15,7 @@ export function getEventHandlers( triggers: string[], eventName: string, element?: string, - properties?: Record + properties?: Record, ): Partial void>> { const eventHandlers: Partial void>> = {} for (const event of triggers) { @@ -23,16 +23,11 @@ export function getEventHandlers( // Some interface elements don't have handlers defined. // TODO(WEB-4252): Potentially can remove isInterface check once web is fully converted to tamagui if (!child.props[event] && !isInterface) { - logger.info( - 'trace/utils.ts', - 'getEventHandlers', - 'Found a null handler while logging an event', - { - eventName, - ...consumedProps, - ...properties, - } - ) + logger.info('trace/utils.ts', 'getEventHandlers', 'Found a null handler while logging an event', { + eventName, + ...consumedProps, + ...properties, + }) } // call child event handler with original arguments diff --git a/packages/utilities/src/time/date.ts b/packages/utilities/src/time/date.ts index 52c86c12dcd..304212ffcbd 100644 --- a/packages/utilities/src/time/date.ts +++ b/packages/utilities/src/time/date.ts @@ -1,7 +1,3 @@ export function areSameDays(a: Date, b: Date): boolean { - return ( - a.getDate() === b.getDate() && - a.getMonth() === b.getMonth() && - a.getFullYear() === b.getFullYear() - ) + return a.getDate() === b.getDate() && a.getMonth() === b.getMonth() && a.getFullYear() === b.getFullYear() } diff --git a/packages/utilities/src/time/timing.test.ts b/packages/utilities/src/time/timing.test.ts index 88efd04a71c..50000107b49 100644 --- a/packages/utilities/src/time/timing.test.ts +++ b/packages/utilities/src/time/timing.test.ts @@ -13,7 +13,7 @@ jest.useFakeTimers() const timedPromise = (duration: number, shouldResolve = true): Promise => new Promise((resolve, reject) => - setTimeout(() => (shouldResolve ? resolve('resolve') : reject(new Error('reject'))), duration) + setTimeout(() => (shouldResolve ? resolve('resolve') : reject(new Error('reject'))), duration), ) describe('promiseTimeout', () => { diff --git a/packages/utilities/src/time/timing.ts b/packages/utilities/src/time/timing.ts index e518cf1adbf..3f0da1008b0 100644 --- a/packages/utilities/src/time/timing.ts +++ b/packages/utilities/src/time/timing.ts @@ -6,10 +6,7 @@ export function sleep(milliseconds: number): Promise { return new Promise((resolve) => setTimeout(() => resolve(true), milliseconds)) } -export async function promiseTimeout( - promise: Promise, - milliseconds: number -): Promise { +export async function promiseTimeout(promise: Promise, milliseconds: number): Promise { // Create a promise that rejects in milliseconds const timeout = new Promise((resolve) => { const id = setTimeout(() => { @@ -27,10 +24,7 @@ export async function promiseTimeout( * @param promise to execute * @param milliseconds length of minimum delay time in ms */ -export async function promiseMinDelay( - promise: Promise, - milliseconds: number -): Promise { +export async function promiseMinDelay(promise: Promise, milliseconds: number): Promise { const minDelay = new Promise((resolve) => { const id = setTimeout(() => { clearTimeout(id) @@ -43,11 +37,7 @@ export async function promiseMinDelay( } // https://usehooks-typescript.com/react-hook/use-interval -export function useInterval( - callback: () => void, - delay: number | null, - immediateStart?: boolean -): void { +export function useInterval(callback: () => void, delay: number | null, immediateStart?: boolean): void { const savedCallback = useRef<() => void | null>() // Remember the latest callback. @@ -81,7 +71,7 @@ type Timeout = ReturnType // https://medium.com/javascript-in-plain-english/usetimeout-react-hook-3cc58b94af1f export const useTimeout = ( callback: () => void, - delay = 0 // in ms (default: immediately put into JS Event Queue) + delay = 0, // in ms (default: immediately put into JS Event Queue) ): (() => void) => { const timeoutIdRef = useRef() @@ -136,7 +126,7 @@ export function useDebounceWithStatus(value: T, delay: number = DEFAULT_DELAY export function debounceCallback void>( func: T, - wait: number + wait: number, ): { triggerDebounce: () => void; cancelDebounce: () => void } { let timeout: NodeJS.Timeout diff --git a/packages/wallet/jest-setup.js b/packages/wallet/jest-setup.js index 87339720ed9..b5070eb35a0 100644 --- a/packages/wallet/jest-setup.js +++ b/packages/wallet/jest-setup.js @@ -1,4 +1,5 @@ import 'uniswap/src/i18n/i18n' // Uses real translations for tests +import 'utilities/src/logger/mocks' import { localizeMock as mockRNLocalize } from 'react-native-localize/mock' import { AppearanceSettingType } from 'wallet/src/features/appearance/slice' @@ -20,7 +21,8 @@ jest.mock('wallet/src/features/appearance/hooks', () => { } }) -jest.mock('uniswap/src/utils/env', () => ({ +jest.mock('utilities/src/environment', () => ({ + isTestEnv: jest.fn(() => true), isDevEnv: jest.fn(() => false), isBetaEnv: jest.fn(() => false), isProdEnv: jest.fn(() => false), diff --git a/packages/wallet/package.json b/packages/wallet/package.json index b4259338e97..cf92884db91 100644 --- a/packages/wallet/package.json +++ b/packages/wallet/package.json @@ -3,6 +3,7 @@ "version": "0.0.0", "scripts": { "check:deps:usage": "depcheck", + "format": "../../scripts/prettier.sh", "lint": "eslint . --ext ts,tsx --max-warnings=0", "lint:fix": "eslint . --ext ts,tsx --fix", "test": "jest", @@ -25,7 +26,7 @@ "@reduxjs/toolkit": "1.9.3", "@sentry/types": "7.80.0", "@shopify/flash-list": "1.6.3", - "@uniswap/analytics-events": "2.32.0", + "@uniswap/analytics-events": "2.34.0", "@uniswap/permit2-sdk": "1.3.0", "@uniswap/router-sdk": "1.9.2", "@uniswap/sdk-core": "5.3.0", @@ -38,7 +39,6 @@ "axios": "1.6.5", "dayjs": "1.11.7", "ethers": "5.7.2", - "expo-clipboard": "5.0.1", "expo-web-browser": "12.8.2", "fuse.js": "6.5.3", "graphql": "16.6.0", @@ -46,7 +46,7 @@ "jsbi": "3.2.5", "lodash": "4.17.21", "mockdate": "3.0.5", - "qrcode": "1.5.1", + "no-yolo-signatures": "0.0.2", "react": "18.2.0", "react-i18next": "14.1.0", "react-native": "0.73.6", @@ -61,8 +61,6 @@ "react-native-svg": "15.1.0", "react-native-webview": "11.23.1", "react-redux": "8.0.5", - "react-virtualized-auto-sizer": "1.0.20", - "react-window": "1.8.9", "redux": "4.2.1", "redux-persist": "6.0.0", "redux-saga": "1.2.2", @@ -72,7 +70,6 @@ "uniswap": "workspace:^", "utilities": "workspace:^", "uuid": "9.0.0", - "wcag-contrast": "3.0.0", "zod": "3.22.4", "zxcvbn": "4.4.2" }, @@ -85,7 +82,6 @@ "@testing-library/react-hooks": "7.0.2", "@testing-library/react-native": "11.5.0", "@types/react": "^18.0.15", - "@types/react-window": "1.8.2", "@types/zxcvbn": "4.4.2", "@uniswap/eslint-config": "workspace:^", "depcheck": "1.4.7", diff --git a/packages/wallet/src/components/CurrencyLogo/CurrencyLogo.test.tsx b/packages/wallet/src/components/CurrencyLogo/CurrencyLogo.test.tsx index f8c70b69205..c8807d9cb73 100644 --- a/packages/wallet/src/components/CurrencyLogo/CurrencyLogo.test.tsx +++ b/packages/wallet/src/components/CurrencyLogo/CurrencyLogo.test.tsx @@ -1,9 +1,5 @@ import { CurrencyLogo } from 'uniswap/src/components/CurrencyLogo/CurrencyLogo' -import { - ARBITRUM_DAI_CURRENCY_INFO, - UNI_CURRENCY_INFO, - arbitrumDaiCurrencyInfo, -} from 'wallet/src/test/fixtures' +import { ARBITRUM_DAI_CURRENCY_INFO, UNI_CURRENCY_INFO, arbitrumDaiCurrencyInfo } from 'uniswap/src/test/fixtures' import { render } from 'wallet/src/test/test-utils' describe(CurrencyLogo, () => { @@ -30,7 +26,7 @@ describe(CurrencyLogo, () => { it('is rendered if hideNetworkLogo is false', () => { const { queryByTestId } = render( - + , ) const networkLogo = queryByTestId('network-logo') @@ -39,9 +35,7 @@ describe(CurrencyLogo, () => { }) it('is not rendered if hideNetworkLogo is true', () => { - const { queryByTestId } = render( - - ) + const { queryByTestId } = render() const networkLogo = queryByTestId('network-logo') diff --git a/packages/wallet/src/components/CurrencyLogo/LogoWithTxStatus.test.tsx b/packages/wallet/src/components/CurrencyLogo/LogoWithTxStatus.test.tsx index c016ed118f7..a5c3d6ef2a1 100644 --- a/packages/wallet/src/components/CurrencyLogo/LogoWithTxStatus.test.tsx +++ b/packages/wallet/src/components/CurrencyLogo/LogoWithTxStatus.test.tsx @@ -1,8 +1,7 @@ -import { - UniverseChainId, - WALLET_SUPPORTED_CHAIN_IDS, - WalletChainId, -} from 'uniswap/src/types/chains' +import { AssetType } from 'uniswap/src/entities/assets' +import { ETH_CURRENCY_INFO, ethCurrencyInfo } from 'uniswap/src/test/fixtures/wallet/currencies' +import { createFixture } from 'uniswap/src/test/utils' +import { UniverseChainId, WALLET_SUPPORTED_CHAIN_IDS, WalletChainId } from 'uniswap/src/types/chains' import { WalletConnectEvent } from 'uniswap/src/types/walletConnect' import { DappLogoWithTxStatus, @@ -10,11 +9,9 @@ import { LogoWithTxStatus, LogoWithTxStatusProps, } from 'wallet/src/components/CurrencyLogo/LogoWithTxStatus' -import { AssetType } from 'wallet/src/entities/assets' import { TransactionStatus, TransactionType } from 'wallet/src/features/transactions/types' -import { ETH_CURRENCY_INFO, ethCurrencyInfo } from 'wallet/src/test/fixtures/wallet/currencies' import { render } from 'wallet/src/test/test-utils' -import { createFixture, randomChoice, randomEnumValue } from 'wallet/src/test/utils' +import { randomChoice, randomEnumValue } from 'wallet/src/test/utils' const currencyLogoProps = createFixture()(() => ({ assetType: AssetType.Currency, @@ -42,7 +39,7 @@ describe(LogoWithTxStatus, () => { txStatus: TransactionStatus.Pending, chainId: UniverseChainId.Mainnet, })} - /> + />, ) expect(tree).toMatchSnapshot() @@ -51,7 +48,7 @@ describe(LogoWithTxStatus, () => { describe('logo', () => { it('shows Moonpay logo for fiat purchase', () => { const { queryByTestId } = render( - + , ) expect(queryByTestId('moonpay-logo')).toBeTruthy() @@ -80,23 +77,21 @@ describe(LogoWithTxStatus, () => { describe('transaction summary network logo', () => { it('shows network logo if chainId is specified and is not Mainnet', () => { const { queryByTestId } = render( - + , ) expect(queryByTestId('network-logo')).toBeTruthy() }) it('does not show network logo if chainId is not specified', () => { - const { queryByTestId } = render( - - ) + const { queryByTestId } = render() expect(queryByTestId('network-logo')).toBeFalsy() }) it('does not show network logo if chainId is Mainnet', () => { const { queryByTestId } = render( - + , ) expect(queryByTestId('network-logo')).toBeFalsy() @@ -116,21 +111,18 @@ describe(LogoWithTxStatus, () => { TransactionType.Unknown, ] const transactionWithoutIcons = Object.values(TransactionType).filter( - (txType) => !transactionWithIcons.includes(txType) + (txType) => !transactionWithIcons.includes(txType), ) const nftAssetTypesWithIcons = [AssetType.ERC721, AssetType.ERC1155] const nftAssetTypesWithoutIcons = Object.values(AssetType).filter( - (assetType) => !nftAssetTypesWithIcons.includes(assetType) + (assetType) => !nftAssetTypesWithIcons.includes(assetType), ) for (const txType of transactionWithIcons) { it(`shows icon for ${txType}`, () => { const { queryByTestId } = render( - + , ) expect(queryByTestId('status-icon')).toBeTruthy() @@ -144,7 +136,7 @@ describe(LogoWithTxStatus, () => { {...currencyLogoProps({ chainId: UniverseChainId.Mainnet })} assetType={assetType} txType={TransactionType.NFTTrade} - /> + />, ) expect(queryByTestId('status-icon')).toBeTruthy() @@ -157,10 +149,7 @@ describe(LogoWithTxStatus, () => { jest.spyOn(console, 'warn').mockImplementation(consoleWarnMock) const { queryByTestId } = render( - + , ) expect(queryByTestId('status-icon')).toBeFalsy() @@ -168,7 +157,7 @@ describe(LogoWithTxStatus, () => { expect.anything(), expect.anything(), expect.stringContaining('Could not find icon for transaction type:'), - txType + txType, ) }) } @@ -183,7 +172,7 @@ describe(LogoWithTxStatus, () => { {...currencyLogoProps({ chainId: UniverseChainId.Mainnet })} assetType={assetType} txType={TransactionType.NFTTrade} - /> + />, ) expect(queryByTestId('status-icon')).toBeFalsy() @@ -191,7 +180,7 @@ describe(LogoWithTxStatus, () => { expect.anything(), expect.anything(), expect.stringContaining('Could not find icon for transaction type:'), - TransactionType.NFTTrade + TransactionType.NFTTrade, ) }) } @@ -203,7 +192,7 @@ describe(LogoWithTxStatus, () => { // (this is needed because native implementation is not used by default // with our test setup where we exclude files with native extensions) jest.mock('wallet/src/features/images/ImageUri', () => - jest.requireActual('wallet/src/features/images/ImageUri.native.tsx') + jest.requireActual('wallet/src/features/images/ImageUri.native.tsx'), ) describe(DappLogoWithTxStatus, () => { @@ -314,9 +303,7 @@ describe(DappLogoWithWCBadge, () => { }) it('renders wallet connect logo if chain is Mainnet', () => { - const { queryByTestId } = render( - - ) + const { queryByTestId } = render() expect(queryByTestId('network-logo')).toBeFalsy() expect(queryByTestId('wallet-connect-logo')).toBeTruthy() diff --git a/packages/wallet/src/components/CurrencyLogo/LogoWithTxStatus.tsx b/packages/wallet/src/components/CurrencyLogo/LogoWithTxStatus.tsx index 4729ae1ec54..cdd442fe9b1 100644 --- a/packages/wallet/src/components/CurrencyLogo/LogoWithTxStatus.tsx +++ b/packages/wallet/src/components/CurrencyLogo/LogoWithTxStatus.tsx @@ -5,30 +5,20 @@ import type { IconProps } from 'ui/src' import { Flex, useSporeColors } from 'ui/src' import WalletConnectLogo from 'ui/src/assets/icons/walletconnect.svg' import MoonpayLogo from 'ui/src/assets/logos/svg/moonpay.svg' -import { - AlertTriangle, - Approve, - ArrowDownInCircle, - ArrowUpInCircle, - QuestionInCircle, -} from 'ui/src/components/icons' +import { AlertTriangle, Approve, ArrowDownInCircle, ArrowUpInCircle, QuestionInCircle } from 'ui/src/components/icons' import { borderRadii } from 'ui/src/theme' import { CurrencyLogo, STATUS_RATIO } from 'uniswap/src/components/CurrencyLogo/CurrencyLogo' import { TransactionSummaryNetworkLogo } from 'uniswap/src/components/CurrencyLogo/NetworkLogo' +import { AssetType } from 'uniswap/src/entities/assets' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' import { UniverseChainId, WalletChainId } from 'uniswap/src/types/chains' import { WalletConnectEvent } from 'uniswap/src/types/walletConnect' import { logger } from 'utilities/src/logger/logger' import { DappIconPlaceholder } from 'wallet/src/components/WalletConnect/DappIconPlaceholder' -import { AssetType } from 'wallet/src/entities/assets' import { ImageUri } from 'wallet/src/features/images/ImageUri' import { NFTViewer } from 'wallet/src/features/images/NFTViewer' import { RemoteImage } from 'wallet/src/features/images/RemoteImage' -import { - NFTTradeType, - TransactionStatus, - TransactionType, -} from 'wallet/src/features/transactions/types' +import { NFTTradeType, TransactionStatus, TransactionType } from 'wallet/src/features/transactions/types' interface LogoWithTxStatusBaseProps { assetType: AssetType @@ -93,7 +83,8 @@ function getLogo(props: LogoWithTxStatusProps): JSX.Element { height={size} overflow="hidden" testID="nft-viewer" - width={size}> + width={size} + > {props.nftImageUrl && } ) @@ -155,12 +146,7 @@ export function LogoWithTxStatus(props: LogoWithTxStatusProps): JSX.Element { useEffect(() => { if (!icon) { - logger.warn( - 'statusIcon', - 'GenerateStatusIcon', - 'Could not find icon for transaction type:', - txType - ) + logger.warn('statusIcon', 'GenerateStatusIcon', 'Could not find icon for transaction type:', txType) } }, [icon, txType]) @@ -195,9 +181,7 @@ export function DappLogoWithTxStatus({ const getStatusIcon = (): JSX.Element | undefined => { switch (event) { case WalletConnectEvent.NetworkChanged: - return chainId ? ( - - ) : undefined + return chainId ? : undefined case WalletConnectEvent.TransactionConfirmed: return case WalletConnectEvent.TransactionFailed: @@ -257,18 +241,22 @@ export function DappLogoWithWCBadge({ dappName, size, chainId, + hideWCBadge = false, + circular = false, }: { dappImageUrl: Maybe dappName: string size: number chainId: WalletChainId | null + hideWCBadge?: boolean + circular?: boolean }): JSX.Element { const dappImageSize = size const statusSize = dappImageSize * STATUS_RATIO const totalSize = dappImageSize + statusSize * (1 / 4) const dappImage = dappImageUrl ? ( + {dappImage} @@ -287,7 +275,7 @@ export function DappLogoWithWCBadge({ - ) : ( + ) : !hideWCBadge ? ( + right={-2} + > - )} + ) : null} ) } diff --git a/packages/wallet/src/components/CurrencyLogo/__snapshots__/LogoWithTxStatus.test.tsx.snap b/packages/wallet/src/components/CurrencyLogo/__snapshots__/LogoWithTxStatus.test.tsx.snap index d0853b45b13..6171553c043 100644 --- a/packages/wallet/src/components/CurrencyLogo/__snapshots__/LogoWithTxStatus.test.tsx.snap +++ b/packages/wallet/src/components/CurrencyLogo/__snapshots__/LogoWithTxStatus.test.tsx.snap @@ -69,8 +69,10 @@ exports[`DappLogoWithWCBadge renders without error 1`] = ` 'ethereum-logo.png') @@ -12,8 +12,6 @@ it('renders a currency logo without network logo', () => { }) it('renders a currency logo with network logo', () => { - const tree = renderWithProviders( - - ) + const tree = renderWithProviders() expect(tree).toMatchSnapshot() }) diff --git a/packages/wallet/src/components/DevelopmentOnly/UniconSampleSheet.tsx b/packages/wallet/src/components/DevelopmentOnly/UniconSampleSheet.tsx index 11842b89945..b2821a5afbe 100644 --- a/packages/wallet/src/components/DevelopmentOnly/UniconSampleSheet.tsx +++ b/packages/wallet/src/components/DevelopmentOnly/UniconSampleSheet.tsx @@ -13,11 +13,7 @@ const generateRandomEthereumAddresses = (numberOfAddresses: number): string[] => export const UniconSampleSheet = ({ onClose }: { onClose: () => void }): JSX.Element => { return ( - + {generateRandomEthereumAddresses(80).map((address) => { diff --git a/packages/wallet/src/components/ErrorBoundary/ErrorBoundary.tsx b/packages/wallet/src/components/ErrorBoundary/ErrorBoundary.tsx index bebf404109e..a737b235dce 100644 --- a/packages/wallet/src/components/ErrorBoundary/ErrorBoundary.tsx +++ b/packages/wallet/src/components/ErrorBoundary/ErrorBoundary.tsx @@ -1,13 +1,13 @@ import React, { ErrorInfo, PropsWithChildren } from 'react' import { useTranslation } from 'react-i18next' import { Image, StyleSheet } from 'react-native' +import { useDispatch } from 'react-redux' import { Button, Flex, Text } from 'ui/src' import { DEAD_LUNI } from 'ui/src/assets' import { logger } from 'utilities/src/logger/logger' import { restartApp } from 'wallet/src/components/ErrorBoundary/restart' import { useAccounts } from 'wallet/src/features/wallet/hooks' import { setFinishedOnboarding } from 'wallet/src/features/wallet/slice' -import { useAppDispatch } from 'wallet/src/state' interface ErrorBoundaryState { error: Error | null @@ -55,7 +55,7 @@ const LUNI_SIZE = 150 function ErrorScreen({ error }: { error: Error }): JSX.Element { const { t } = useTranslation() - const dispatch = useAppDispatch() + const dispatch = useDispatch() const accounts = useAccounts() // If there is no active account, we need to reset the onboarding flow @@ -64,13 +64,7 @@ function ErrorScreen({ error }: { error: Error }): JSX.Element { } return ( - + diff --git a/packages/wallet/src/components/QRCodeScanner/WalletQRCode.tsx b/packages/wallet/src/components/QRCodeScanner/WalletQRCode.tsx index 02a5f0eedb7..48ffb96dd99 100644 --- a/packages/wallet/src/components/QRCodeScanner/WalletQRCode.tsx +++ b/packages/wallet/src/components/QRCodeScanner/WalletQRCode.tsx @@ -1,17 +1,21 @@ import { useTranslation } from 'react-i18next' -import { Flex, Text, isWeb, useMedia, useSporeColors } from 'ui/src' -import { spacing } from 'ui/src/theme' +import { Flex, QRCodeDisplay, Text, isWeb, useMedia, useSporeColors } from 'ui/src' +import { iconSizes, spacing } from 'ui/src/theme' +import { NetworkLogos } from 'uniswap/src/components/network/NetworkLogos' +import { LearnMoreLink } from 'uniswap/src/components/text/LearnMoreLink' import { uniswapUrls } from 'uniswap/src/constants/urls' import { WALLET_SUPPORTED_CHAIN_IDS } from 'uniswap/src/types/chains' -import { QRCodeDisplay } from 'wallet/src/components/QRCodeScanner/QRCode' +import { useQRColorProps } from 'wallet/src/components/QRCodeScanner/useQRColorProps' +import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon' import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' -import { NetworkLogos } from 'wallet/src/components/network/NetworkLogos' -import { LearnMoreLink } from 'wallet/src/components/text/LearnMoreLink' +import { useAvatar } from 'wallet/src/features/wallet/hooks' export function WalletQRCode({ address }: { address: Address }): JSX.Element | null { const colors = useSporeColors() + const { avatar } = useAvatar(address) const { t } = useTranslation() const media = useMedia() + const { smartColor } = useQRColorProps(address) const QR_CODE_SIZE = media.short ? 220 : 240 const UNICON_SIZE = QR_CODE_SIZE / 4 @@ -26,7 +30,8 @@ export function WalletQRCode({ address }: { address: Address }): JSX.Element | n justifyContent={isWeb ? 'flex-start' : 'center'} mb="$spacing8" px={isWeb ? '$spacing16' : '$spacing60'} - py={isWeb ? '$spacing60' : '$spacing24'}> + py={isWeb ? '$spacing60' : '$spacing24'} + > + > + + {t('qrScanner.wallet.title')} diff --git a/packages/wallet/src/components/QRCodeScanner/custom-qr-code-generator/src/genMatrix.js b/packages/wallet/src/components/QRCodeScanner/custom-qr-code-generator/src/genMatrix.js deleted file mode 100644 index b601cba8c2c..00000000000 --- a/packages/wallet/src/components/QRCodeScanner/custom-qr-code-generator/src/genMatrix.js +++ /dev/null @@ -1,14 +0,0 @@ -const QRCode = require('qrcode'); - -export default (value, errorCorrectionLevel) => { - const arr = Array.prototype.slice.call( - QRCode.create(value, { errorCorrectionLevel }).modules.data, - 0 - ) - const sqrt = Math.sqrt(arr.length) - return arr.reduce( - (rows, key, index) => - (index % sqrt === 0 ? rows.push([key]) : rows[rows.length - 1].push(key)) && rows, - [] - ) -} diff --git a/packages/wallet/src/components/QRCodeScanner/useQRColorProps.ts b/packages/wallet/src/components/QRCodeScanner/useQRColorProps.ts new file mode 100644 index 00000000000..8edd8ad698e --- /dev/null +++ b/packages/wallet/src/components/QRCodeScanner/useQRColorProps.ts @@ -0,0 +1,53 @@ +import { useMemo } from 'react' +import { + GradientProps, + getUniconColors, + passesContrast, + useExtractedColors, + useIsDarkMode, + useSporeColors, +} from 'ui/src' +import { useAvatar } from 'wallet/src/features/wallet/hooks' + +type AvatarColors = { + primary: string + base: string + detail: string +} + +type ColorProps = { + smartColor: string + gradientProps: GradientProps +} + +export const useQRColorProps = (address: Address): ColorProps => { + const colors = useSporeColors() + const isDarkMode = useIsDarkMode() + const { color: uniconColor } = getUniconColors(address, isDarkMode) as { color: string } + const { avatar, loading: avatarLoading } = useAvatar(address) + const { colors: avatarColors } = useExtractedColors(avatar) as { colors: AvatarColors } + const hasAvatar = !!avatar && !avatarLoading + + const smartColor: string = useMemo(() => { + const contrastThreshold = 3 // WCAG AA standard for contrast + const backgroundColor = colors.surface2.val // replace with your actual background color + + if (hasAvatar && avatarColors && avatarColors.primary) { + if (passesContrast(avatarColors.primary, backgroundColor, contrastThreshold)) { + return avatarColors.primary + } + if (passesContrast(avatarColors.base, backgroundColor, contrastThreshold)) { + return avatarColors.base + } + if (passesContrast(avatarColors.detail, backgroundColor, contrastThreshold)) { + return avatarColors.detail + } + // Modify the color if it doesn't pass the contrast check + // Replace 'modifiedColor' with the actual color you want to use + return colors.neutral1.val as string + } + return uniconColor + }, [avatarColors, hasAvatar, uniconColor, colors.surface2.val, colors.neutral1.val]) + + return { smartColor, gradientProps: {} } +} diff --git a/packages/wallet/src/components/RecipientSearch/RecipientList.tsx b/packages/wallet/src/components/RecipientSearch/RecipientList.tsx index a7d914a50a2..0a7002ce089 100644 --- a/packages/wallet/src/components/RecipientSearch/RecipientList.tsx +++ b/packages/wallet/src/components/RecipientSearch/RecipientList.tsx @@ -5,12 +5,13 @@ import { FadeIn, FadeOut } from 'react-native-reanimated' import { Text, TouchableArea, isWeb, useDeviceInsets } from 'ui/src' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { spacing } from 'ui/src/theme' +import { SearchResultType } from 'uniswap/src/features/search/SearchResult' import { WalletEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { ViewOnlyRecipientModal } from 'wallet/src/components/RecipientSearch/ViewOnlyRecipientModal' import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' import { SearchableRecipient } from 'wallet/src/features/address/types' -import { SearchResultType, extractDomain } from 'wallet/src/features/search/SearchResult' +import { extractDomain } from 'wallet/src/features/search/SearchResult' import { AccountType } from 'wallet/src/features/wallet/accounts/types' interface RecipientListProps { @@ -19,14 +20,9 @@ interface RecipientListProps { onPress: (recipient: string) => void } -export function RecipientList({ - onPress, - sections, - renderedInModal = false, -}: RecipientListProps): JSX.Element { +export function RecipientList({ onPress, sections, renderedInModal = false }: RecipientListProps): JSX.Element { const insets = useDeviceInsets() - const [selectedViewOnlyRecipient, setSelectedViewOnlyRecipient] = - useState(null) + const [selectedViewOnlyRecipient, setSelectedViewOnlyRecipient] = useState(null) const onRecipientPress = useCallback( (recipient: SearchableRecipient) => { @@ -37,7 +33,7 @@ export function RecipientList({ onPress(recipient.address) } }, - [onPress] + [onPress], ) const onConfirmViewOnlyRecipient = useCallback(() => { @@ -91,7 +87,8 @@ function SectionHeader(info: { section: SectionListData }): entering={FadeIn} // TODO(EXT-526): re-enable `exiting` animation when it's fixed. exiting={isWeb ? undefined : FadeOut} - py="$spacing8"> + py="$spacing8" + > {info.section.title} @@ -108,15 +105,9 @@ interface RecipientProps { onPress: (recipient: SearchableRecipient) => void } -export const RecipientRow = memo(function RecipientRow({ - recipient, - onPress, -}: RecipientProps): JSX.Element { +export const RecipientRow = memo(function RecipientRow({ recipient, onPress }: RecipientProps): JSX.Element { const domain = recipient.name - ? extractDomain( - recipient.name, - recipient.isUnitag ? SearchResultType.Unitag : SearchResultType.ENSAddress - ) + ? extractDomain(recipient.name, recipient.isUnitag ? SearchResultType.Unitag : SearchResultType.ENSAddress) : undefined const onPressWithAnalytics = (): void => { diff --git a/packages/wallet/src/components/RecipientSearch/ViewOnlyRecipientModal.tsx b/packages/wallet/src/components/RecipientSearch/ViewOnlyRecipientModal.tsx index 24734de1ac1..be9009c75c7 100644 --- a/packages/wallet/src/components/RecipientSearch/ViewOnlyRecipientModal.tsx +++ b/packages/wallet/src/components/RecipientSearch/ViewOnlyRecipientModal.tsx @@ -10,10 +10,7 @@ type ViewOnlyRecipientModalProps = { onCancel: () => void } -export function ViewOnlyRecipientModal({ - onConfirm, - onCancel, -}: ViewOnlyRecipientModalProps): JSX.Element { +export function ViewOnlyRecipientModal({ onConfirm, onCancel }: ViewOnlyRecipientModalProps): JSX.Element { const { t } = useTranslation() return ( @@ -25,7 +22,8 @@ export function ViewOnlyRecipientModal({ borderRadius="$rounded12" height={iconSizes.icon48} mb="$spacing8" - width={iconSizes.icon48}> + width={iconSizes.icon48} + > diff --git a/packages/wallet/src/components/RecipientSearch/filter.test.ts b/packages/wallet/src/components/RecipientSearch/filter.test.ts index e19f97cfc25..4ff5abcb7af 100644 --- a/packages/wallet/src/components/RecipientSearch/filter.test.ts +++ b/packages/wallet/src/components/RecipientSearch/filter.test.ts @@ -72,22 +72,14 @@ describe(filterRecipientsByAddress, () => { expect(filterRecipientsByAddress('0x', options)).toEqual(options) // Returns only the first one as it has exactly the same beginning - expect(filterRecipientsByAddress(options[0].data.address.slice(0, 3), options)).toEqual([ - options[0], - ]) + expect(filterRecipientsByAddress(options[0].data.address.slice(0, 3), options)).toEqual([options[0]]) // Returns only the second one as it has exactly the same beginning - expect(filterRecipientsByAddress(options[1].data.address.slice(0, 3), options)).toEqual([ - options[1], - ]) + expect(filterRecipientsByAddress(options[1].data.address.slice(0, 3), options)).toEqual([options[1]]) }) it('returns the same result irrespective of the casing', () => { - expect(filterRecipientsByAddress(options[0].data.address.toLowerCase(), options)).toEqual([ - options[0], - ]) - expect(filterRecipientsByAddress(options[0].data.address.toUpperCase(), options)).toEqual([ - options[0], - ]) + expect(filterRecipientsByAddress(options[0].data.address.toLowerCase(), options)).toEqual([options[0]]) + expect(filterRecipientsByAddress(options[0].data.address.toUpperCase(), options)).toEqual([options[0]]) }) }) diff --git a/packages/wallet/src/components/RecipientSearch/filter.ts b/packages/wallet/src/components/RecipientSearch/filter.ts index 86926b5264d..2be1e3ceb7f 100644 --- a/packages/wallet/src/components/RecipientSearch/filter.ts +++ b/packages/wallet/src/components/RecipientSearch/filter.ts @@ -25,7 +25,7 @@ const searchNameOptions: Fuse.IFuseOptions[] + list: AutocompleteOption[], ): AutocompleteOption[] { if (!searchPattern) { return [] @@ -40,7 +40,7 @@ export function filterRecipientsByName( export function filterRecipientsByAddress( searchPattern: string | null, - list: AutocompleteOption[] + list: AutocompleteOption[], ): AutocompleteOption[] { if (!searchPattern) { return [] @@ -55,7 +55,7 @@ export function filterRecipientsByAddress( export function filterRecipientByNameAndAddress( searchPattern: string | null, - list: AutocompleteOption[] + list: AutocompleteOption[], ): AutocompleteOption[] { if (!searchPattern) { return [] @@ -63,10 +63,7 @@ export function filterRecipientByNameAndAddress( // run both fuses and remove dupes return unique( - [ - ...filterRecipientsByAddress(searchPattern, list), - ...filterRecipientsByName(searchPattern, list), - ], - (v, i, a) => a.findIndex((v2) => v2.data.address === v.data.address) === i + [...filterRecipientsByAddress(searchPattern, list), ...filterRecipientsByName(searchPattern, list)], + (v, i, a) => a.findIndex((v2) => v2.data.address === v.data.address) === i, ) } diff --git a/packages/wallet/src/components/RecipientSearch/hooks.ts b/packages/wallet/src/components/RecipientSearch/hooks.ts index 042a1118a6b..d7246af11b3 100644 --- a/packages/wallet/src/components/RecipientSearch/hooks.ts +++ b/packages/wallet/src/components/RecipientSearch/hooks.ts @@ -3,6 +3,7 @@ import { useCallback, useMemo, useRef } from 'react' import { useTranslation } from 'react-i18next' import { useUnitagByName } from 'uniswap/src/features/unitags/hooks' import { UniverseChainId } from 'uniswap/src/types/chains' +import { getValidAddress } from 'uniswap/src/utils/addresses' import { useMemoCompare } from 'utilities/src/react/hooks' import { useDebounce } from 'utilities/src/time/timing' import { filterRecipientByNameAndAddress } from 'wallet/src/components/RecipientSearch/filter' @@ -16,7 +17,6 @@ import { selectRecipientsByRecency } from 'wallet/src/features/transactions/sele import { Account, AccountType } from 'wallet/src/features/wallet/accounts/types' import { selectInactiveAccounts } from 'wallet/src/features/wallet/selectors' import { useAppSelector } from 'wallet/src/state' -import { getValidAddress } from 'wallet/src/utils/addresses' const MAX_RECENT_RECIPIENTS = 15 @@ -27,7 +27,7 @@ type RecipientSection = { function useValidatedSearchedAddress( searchTerm: string, - debounceDelayMs?: number + debounceDelayMs?: number, ): { recipients: SearchableRecipient[] searchTerm: string @@ -40,11 +40,7 @@ function useValidatedSearchedAddress( name: dotEthName, } = useENS(UniverseChainId.Mainnet, searchTerm, true) - const { - loading: ensLoading, - address: ensAddress, - name: ensName, - } = useENS(UniverseChainId.Mainnet, searchTerm, false) + const { loading: ensLoading, address: ensAddress, name: ensName } = useENS(UniverseChainId.Mainnet, searchTerm, false) const { loading: unitagLoading, unitag } = useUnitagByName(searchTerm ?? undefined) @@ -108,7 +104,7 @@ function useValidatedSearchedAddress( searchTerm, loading: dotEthLoading || ensLoading || unitagLoading, }), - [memoRecipients, searchTerm, dotEthLoading, ensLoading, unitagLoading] + [memoRecipients, searchTerm, dotEthLoading, ensLoading, unitagLoading], ) // Debounce search results to prevent flickering const debouncedResult = useDebounce(memoResult, debounceDelayMs) @@ -120,7 +116,7 @@ function useValidatedSearchedAddress( export function useRecipients( pattern: string, - debounceDelayMs?: number + debounceDelayMs?: number, ): { sections: RecipientSection[] searchableRecipientOptions: { @@ -144,9 +140,9 @@ export function useRecipients( } return acc }, - { importedWallets: [], viewOnlyWallets: [] } + { importedWallets: [], viewOnlyWallets: [] }, ), - [inactiveLocalAccounts] + [inactiveLocalAccounts], ) const recentRecipients = useAppSelector(selectRecipientsByRecency).slice(0, MAX_RECENT_RECIPIENTS) @@ -202,7 +198,7 @@ export function useRecipients( (address) => { address, - } + }, ), }) } @@ -220,12 +216,10 @@ export function useRecipients( const searchableRecipientOptions = useMemo( () => - uniqueAddressesOnly([ - ...validatedAddressRecipients, - ...inactiveLocalAccounts, - ...recentRecipients, - ]).map((item) => ({ data: item, key: item.address })), - [recentRecipients, validatedAddressRecipients, inactiveLocalAccounts] + uniqueAddressesOnly([...validatedAddressRecipients, ...inactiveLocalAccounts, ...recentRecipients]).map( + (item) => ({ data: item, key: item.address }), + ), + [recentRecipients, validatedAddressRecipients, inactiveLocalAccounts], ) return useMemo( @@ -235,25 +229,21 @@ export function useRecipients( loading, debouncedPattern: searchTerm, }), - [loading, searchableRecipientOptions, sections, searchTerm] + [loading, searchableRecipientOptions, sections, searchTerm], ) } -export function useFilteredRecipientSections( - searchPattern: string, - debounceDelayMs?: number -): RecipientSection[] { +export function useFilteredRecipientSections(searchPattern: string, debounceDelayMs?: number): RecipientSection[] { const sectionsRef = useRef([]) const { sections, searchableRecipientOptions, loading, debouncedPattern } = useRecipients( searchPattern, - debounceDelayMs + debounceDelayMs, ) const getFilteredSections = useCallback(() => { - const filteredAddresses = filterRecipientByNameAndAddress( - debouncedPattern, - searchableRecipientOptions - ).map((item) => item.data.address) + const filteredAddresses = filterRecipientByNameAndAddress(debouncedPattern, searchableRecipientOptions).map( + (item) => item.data.address, + ) return filterSections(sections, filteredAddresses) }, [debouncedPattern, searchableRecipientOptions, sections]) diff --git a/packages/wallet/src/components/RecipientSearch/utils.test.ts b/packages/wallet/src/components/RecipientSearch/utils.test.ts index 886cee4c472..c222d41b8eb 100644 --- a/packages/wallet/src/components/RecipientSearch/utils.test.ts +++ b/packages/wallet/src/components/RecipientSearch/utils.test.ts @@ -2,11 +2,7 @@ import { faker } from '@faker-js/faker' import { SectionListData } from 'react-native' import { filterSections } from 'wallet/src/components/RecipientSearch/utils' import { SearchableRecipient } from 'wallet/src/features/address/types' -import { - SAMPLE_SEED_ADDRESS_1, - SAMPLE_SEED_ADDRESS_2, - recipientSection, -} from 'wallet/src/test/fixtures' +import { SAMPLE_SEED_ADDRESS_1, SAMPLE_SEED_ADDRESS_2, recipientSection } from 'wallet/src/test/fixtures' const recipientSections: ArrayOfLength<4, SectionListData> = [ recipientSection({ addresses: [SAMPLE_SEED_ADDRESS_1, SAMPLE_SEED_ADDRESS_2] }), diff --git a/packages/wallet/src/components/RecipientSearch/utils.ts b/packages/wallet/src/components/RecipientSearch/utils.ts index ae3ac923af0..07f00a083e9 100644 --- a/packages/wallet/src/components/RecipientSearch/utils.ts +++ b/packages/wallet/src/components/RecipientSearch/utils.ts @@ -3,7 +3,7 @@ import { SearchableRecipient } from 'wallet/src/features/address/types' export function filterSections( sections: SectionListData[], - filteredAddresses: string[] + filteredAddresses: string[], ): { title: string; data: SearchableRecipient[] }[] { return sections .map((section) => { diff --git a/packages/wallet/src/components/TokenSelector/SelectTokenButton.tsx b/packages/wallet/src/components/TokenSelector/SelectTokenButton.tsx index c313e2c120f..b8cb82057fa 100644 --- a/packages/wallet/src/components/TokenSelector/SelectTokenButton.tsx +++ b/packages/wallet/src/components/TokenSelector/SelectTokenButton.tsx @@ -12,11 +12,7 @@ interface SelectTokenButtonProps { testID?: string } -export function SelectTokenButton({ - selectedCurrencyInfo, - onPress, - testID, -}: SelectTokenButtonProps): JSX.Element { +export function SelectTokenButton({ selectedCurrencyInfo, onPress, testID }: SelectTokenButtonProps): JSX.Element { const { t } = useTranslation() return ( @@ -25,7 +21,8 @@ export function SelectTokenButton({ backgroundColor={selectedCurrencyInfo ? '$surface3' : '$accent1'} borderRadius="$roundedFull" testID={testID} - onPress={onPress}> + onPress={onPress} + > {selectedCurrencyInfo ? ( @@ -33,32 +30,16 @@ export function SelectTokenButton({ {getSymbolDisplayText(selectedCurrencyInfo.currency.symbol)} {isWeb && ( - + )} ) : ( - + {t('tokens.selector.button.choose')} {isWeb && ( - + )} )} diff --git a/packages/wallet/src/components/TokenSelector/TokenSelectorEmptySearchList.tsx b/packages/wallet/src/components/TokenSelector/TokenSelectorEmptySearchList.tsx deleted file mode 100644 index 8e7b4c174e3..00000000000 --- a/packages/wallet/src/components/TokenSelector/TokenSelectorEmptySearchList.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import { memo, useCallback, useMemo } from 'react' -import { useTranslation } from 'react-i18next' -import { Text, TouchableArea } from 'ui/src' -import { SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { GqlResult } from 'uniswap/src/data/types' -import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' -import { TokenSelectorList } from 'wallet/src/components/TokenSelector/TokenSelectorList' -import { - OnSelectCurrency, - TokenOption, - TokenSection, -} from 'wallet/src/components/TokenSelector/types' -import { getTokenOptionsSection } from 'wallet/src/components/TokenSelector/utils' -import { buildCurrency, gqlTokenToCurrencyInfo } from 'wallet/src/features/dataApi/utils' -import { SearchResultType, TokenSearchResult } from 'wallet/src/features/search/SearchResult' -import { clearSearchHistory } from 'wallet/src/features/search/searchHistorySlice' -import { selectSearchHistory } from 'wallet/src/features/search/selectSearchHistory' -import { usePopularTokens } from 'wallet/src/features/tokens/hooks' -import { useAppDispatch, useAppSelector } from 'wallet/src/state' -import { currencyId } from 'wallet/src/utils/currencyId' - -function searchResultToCurrencyInfo({ - chainId, - address, - symbol, - name, - logoUrl, - safetyLevel, -}: TokenSearchResult): CurrencyInfo | null { - const currency = buildCurrency({ - chainId, - address, - decimals: 0, // this does not matter in a context of CurrencyInfo here, as we do not provide any balance - symbol, - name, - }) - - if (!currency) { - return null - } - - const currencyInfo: CurrencyInfo = { - currency, - currencyId: currencyId(currency), - logoUrl, - safetyLevel: safetyLevel ?? SafetyLevel.StrongWarning, - // defaulting to not spam, as user has searched and chosen this token before - isSpam: false, - } - return currencyInfo -} - -function currencyInfosToTokenOptions( - currencyInfos: Array | undefined -): TokenOption[] | undefined { - return currencyInfos - ?.filter((cI): cI is CurrencyInfo => Boolean(cI)) - .map((currencyInfo) => ({ - currencyInfo, - quantity: null, - balanceUSD: undefined, - })) -} - -function ClearAll({ onPress }: { onPress: () => void }): JSX.Element { - const { t } = useTranslation() - return ( - - - {t('tokens.selector.button.clear')} - - - ) -} - -function useTokenSectionsForEmptySearch(): GqlResult { - const { t } = useTranslation() - const dispatch = useAppDispatch() - - const { popularTokens, loading } = usePopularTokens() - - const searchHistory = useAppSelector(selectSearchHistory) - - // it's a depenedency of useMemo => useCallback - const onPressClearSearchHistory = useCallback((): void => { - dispatch(clearSearchHistory()) - }, [dispatch]) - - const sections = useMemo( - () => [ - ...(getTokenOptionsSection( - t('tokens.selector.section.recent'), - currencyInfosToTokenOptions( - searchHistory - .filter( - (searchResult): searchResult is TokenSearchResult => - searchResult.type === SearchResultType.Token - ) - .map(searchResultToCurrencyInfo) - ), - - ) ?? []), - ...(getTokenOptionsSection( - t('tokens.selector.section.popular'), - currencyInfosToTokenOptions(popularTokens?.map(gqlTokenToCurrencyInfo)) - ) ?? []), - ], - [onPressClearSearchHistory, popularTokens, searchHistory, t] - ) - - return useMemo( - () => ({ - data: sections, - loading, - }), - [loading, sections] - ) -} - -function _TokenSelectorEmptySearchList({ - onSelectCurrency, -}: { - onSelectCurrency: OnSelectCurrency -}): JSX.Element { - const { t } = useTranslation() - - const { data: sections, loading, error, refetch } = useTokenSectionsForEmptySearch() - - return ( - - ) -} - -export const TokenSelectorEmptySearchList = memo(_TokenSelectorEmptySearchList) diff --git a/packages/wallet/src/components/TokenSelector/TokenSelectorSearchResultsList.tsx b/packages/wallet/src/components/TokenSelector/TokenSelectorSearchResultsList.tsx deleted file mode 100644 index c9bebf29384..00000000000 --- a/packages/wallet/src/components/TokenSelector/TokenSelectorSearchResultsList.tsx +++ /dev/null @@ -1,176 +0,0 @@ -import { memo, useCallback, useMemo } from 'react' -import { Trans, useTranslation } from 'react-i18next' -import { Flex, Text } from 'ui/src' -import { GqlResult } from 'uniswap/src/data/types' -import { UniverseChainId } from 'uniswap/src/types/chains' -import { - SectionHeader, - TokenSelectorList, -} from 'wallet/src/components/TokenSelector/TokenSelectorList' -import { - usePortfolioBalancesForAddressById, - usePortfolioTokenOptions, -} from 'wallet/src/components/TokenSelector/hooks' -import { OnSelectCurrency, TokenSection } from 'wallet/src/components/TokenSelector/types' -import { - formatSearchResults, - getTokenOptionsSection, -} from 'wallet/src/components/TokenSelector/utils' -import { useSearchTokens } from 'wallet/src/features/dataApi/searchTokens' -import { SearchResultType } from 'wallet/src/features/search/SearchResult' -import { addToSearchHistory } from 'wallet/src/features/search/searchHistorySlice' -import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' -import { useAppDispatch } from 'wallet/src/state' - -function EmptyResults({ searchFilter }: { searchFilter: string }): JSX.Element { - const { t } = useTranslation() - return ( - - - - }} - i18nKey="tokens.selector.search.empty" - values={{ searchText: searchFilter }} - /> - - - ) -} - -function useTokenSectionsForSearchResults( - chainFilter: UniverseChainId | null, - searchFilter: string | null, - isBalancesOnlySearch: boolean -): GqlResult { - const { t } = useTranslation() - const activeAccountAddress = useActiveAccountAddressWithThrow() - - const { - data: portfolioBalancesById, - error: portfolioBalancesByIdError, - refetch: refetchPortfolioBalances, - loading: portfolioBalancesByIdLoading, - } = usePortfolioBalancesForAddressById(activeAccountAddress) - - const { - data: portfolioTokenOptions, - error: portfolioTokenOptionsError, - refetch: refetchPortfolioTokenOptions, - loading: portfolioTokenOptionsLoading, - } = usePortfolioTokenOptions(activeAccountAddress, chainFilter, searchFilter ?? undefined) - - // Only call search endpoint if isBalancesOnlySearch is false - const { - data: searchResultCurrencies, - error: searchTokensError, - refetch: refetchSearchTokens, - loading: searchTokensLoading, - } = useSearchTokens(searchFilter, chainFilter, /*skip*/ isBalancesOnlySearch) - - const searchResults = useMemo(() => { - return formatSearchResults(searchResultCurrencies, portfolioBalancesById, searchFilter) - }, [searchResultCurrencies, portfolioBalancesById, searchFilter]) - - const loading = - portfolioTokenOptionsLoading || - portfolioBalancesByIdLoading || - (!isBalancesOnlySearch && searchTokensLoading) - - const sections = useMemo( - () => - getTokenOptionsSection( - t('tokens.selector.section.search'), - // Use local search when only searching balances - isBalancesOnlySearch ? portfolioTokenOptions : searchResults - ), - [isBalancesOnlySearch, portfolioTokenOptions, searchResults, t] - ) - - const error = - (!portfolioBalancesById && portfolioBalancesByIdError) || - (!portfolioTokenOptions && portfolioTokenOptionsError) || - (!isBalancesOnlySearch && !searchResults && searchTokensError) - - const refetchAll = useCallback(() => { - refetchPortfolioBalances?.() - refetchSearchTokens?.() - refetchPortfolioTokenOptions?.() - }, [refetchPortfolioBalances, refetchPortfolioTokenOptions, refetchSearchTokens]) - - return useMemo( - () => ({ - data: sections, - loading, - error: error || undefined, - refetch: refetchAll, - }), - [error, loading, refetchAll, sections] - ) -} - -function _TokenSelectorSearchResultsList({ - onSelectCurrency: parentOnSelectCurrency, - chainFilter, - searchFilter, - debouncedSearchFilter, - isBalancesOnlySearch, -}: { - onSelectCurrency: OnSelectCurrency - chainFilter: UniverseChainId | null - searchFilter: string - debouncedSearchFilter: string | null - isBalancesOnlySearch: boolean -}): JSX.Element { - const { t } = useTranslation() - const dispatch = useAppDispatch() - const { - data: sections, - loading, - error, - refetch, - } = useTokenSectionsForSearchResults(chainFilter, debouncedSearchFilter, isBalancesOnlySearch) - - const onSelectCurrency: OnSelectCurrency = (currencyInfo, section, index) => { - parentOnSelectCurrency(currencyInfo, section, index) - if (currencyInfo.currency.symbol && currencyInfo.currency.isToken) { - dispatch( - addToSearchHistory({ - searchResult: { - type: SearchResultType.Token, - chainId: currencyInfo.currency.chainId, - address: currencyInfo.currency.address, - name: currencyInfo.currency.name ?? null, - symbol: currencyInfo.currency.symbol, - logoUrl: currencyInfo.logoUrl ?? null, - safetyLevel: currencyInfo.safetyLevel ?? null, - }, - }) - ) - } - } - - const userIsTyping = Boolean(searchFilter && debouncedSearchFilter !== searchFilter) - - const emptyElement = useMemo( - () => - debouncedSearchFilter ? : undefined, - [debouncedSearchFilter] - ) - return ( - - ) -} - -export const TokenSelectorSearchResultsList = memo(_TokenSelectorSearchResultsList) diff --git a/packages/wallet/src/components/TokenSelector/TokenSelectorSendList.tsx b/packages/wallet/src/components/TokenSelector/TokenSelectorSendList.tsx deleted file mode 100644 index a94ea237248..00000000000 --- a/packages/wallet/src/components/TokenSelector/TokenSelectorSendList.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { skipToken } from '@reduxjs/toolkit/dist/query' -import { memo, useMemo } from 'react' -import { useTranslation } from 'react-i18next' -import { Flex, SpinningLoader, isWeb } from 'ui/src' -import { iconSizes } from 'ui/src/theme' -import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard' -import { GqlResult } from 'uniswap/src/data/types' -import { UniverseChainId } from 'uniswap/src/types/chains' -import { - SectionHeader, - TokenSelectorList, -} from 'wallet/src/components/TokenSelector/TokenSelectorList' -import { usePortfolioTokenOptions } from 'wallet/src/components/TokenSelector/hooks' -import { OnSelectCurrency, TokenSection } from 'wallet/src/components/TokenSelector/types' -import { getTokenOptionsSection } from 'wallet/src/components/TokenSelector/utils' -import { useFiatOnRampIpAddressQuery } from 'wallet/src/features/fiatOnRamp/api' -import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' - -function useTokenSectionsForSend(chainFilter: UniverseChainId | null): GqlResult { - const { t } = useTranslation() - const activeAccountAddress = useActiveAccountAddressWithThrow() - - const { - data: portfolioTokenOptions, - error: portfolioTokenOptionsError, - refetch: refetchPortfolioTokenOptions, - loading: portfolioTokenOptionsLoading, - } = usePortfolioTokenOptions(activeAccountAddress, chainFilter) - - const loading = portfolioTokenOptionsLoading - const error = !portfolioTokenOptions && portfolioTokenOptionsError - - const sections = useMemo( - () => getTokenOptionsSection(t('tokens.selector.section.yours'), portfolioTokenOptions), - [portfolioTokenOptions, t] - ) - - return useMemo( - () => ({ - data: sections, - loading, - error: error || undefined, - refetch: refetchPortfolioTokenOptions, - }), - [error, loading, refetchPortfolioTokenOptions, sections] - ) -} - -function EmptyList({ onEmptyActionPress }: { onEmptyActionPress?: () => void }): JSX.Element { - const { t } = useTranslation() - - const { data: ipAddressData, isLoading } = useFiatOnRampIpAddressQuery( - // TODO(EXT-669): re-enable this once we have an onramp for the Extension. - isWeb ? skipToken : undefined - ) - - const fiatOnRampEligible = Boolean(ipAddressData?.isBuyAllowed) - - return ( - - - - {isLoading ? ( - - - - ) : ( - - )} - - - ) -} - -function _TokenSelectorSendList({ - onSelectCurrency, - chainFilter, - onEmptyActionPress, -}: { - onSelectCurrency: OnSelectCurrency - chainFilter: UniverseChainId | null - onEmptyActionPress: () => void -}): JSX.Element { - const { data: sections, loading, error, refetch } = useTokenSectionsForSend(chainFilter) - const emptyElement = useMemo( - () => , - [onEmptyActionPress] - ) - - return ( - - ) -} - -export const TokenSelectorSendList = memo(_TokenSelectorSendList) diff --git a/packages/wallet/src/components/TokenSelector/TokenSelectorSwapInputList.tsx b/packages/wallet/src/components/TokenSelector/TokenSelectorSwapInputList.tsx deleted file mode 100644 index 54a2cadb128..00000000000 --- a/packages/wallet/src/components/TokenSelector/TokenSelectorSwapInputList.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { memo, useCallback, useMemo } from 'react' -import { useTranslation } from 'react-i18next' -import { GqlResult } from 'uniswap/src/data/types' -import { UniverseChainId } from 'uniswap/src/types/chains' -import { TokenSelectorList } from 'wallet/src/components/TokenSelector/TokenSelectorList' -import { - usePopularTokensOptions, - usePortfolioTokenOptions, -} from 'wallet/src/components/TokenSelector/hooks' -import { - OnSelectCurrency, - TokenSection, - TokenSelectorListSections, -} from 'wallet/src/components/TokenSelector/types' -import { - getTokenOptionsSection, - tokenOptionDifference, -} from 'wallet/src/components/TokenSelector/utils' -import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' - -function useTokenSectionsForSwapInput( - chainFilter: UniverseChainId | null -): GqlResult { - const { t } = useTranslation() - const activeAccountAddress = useActiveAccountAddressWithThrow() - - const { - data: portfolioTokenOptions, - error: portfolioTokenOptionsError, - refetch: refetchPortfolioTokenOptions, - loading: portfolioTokenOptionsLoading, - } = usePortfolioTokenOptions(activeAccountAddress, chainFilter) - - const { - data: popularTokenOptions, - error: popularTokenOptionsError, - refetch: refetchPopularTokenOptions, - loading: popularTokenOptionsLoading, - // if there is no chain filter then we show mainnet tokens - } = usePopularTokensOptions(activeAccountAddress, chainFilter ?? UniverseChainId.Mainnet) - - const error = - (!portfolioTokenOptions && portfolioTokenOptionsError) || - (!popularTokenOptions && popularTokenOptionsError) - - const loading = portfolioTokenOptionsLoading || popularTokenOptionsLoading - - const refetchAll = useCallback(() => { - refetchPortfolioTokenOptions?.() - refetchPopularTokenOptions?.() - }, [refetchPopularTokenOptions, refetchPortfolioTokenOptions]) - - const sections = useMemo(() => { - if (loading) { - return - } - - const popularMinusPortfolioTokens = tokenOptionDifference( - popularTokenOptions, - portfolioTokenOptions - ) - - return [ - ...(getTokenOptionsSection(t('tokens.selector.section.yours'), portfolioTokenOptions) ?? []), - ...(getTokenOptionsSection( - t('tokens.selector.section.popular'), - popularMinusPortfolioTokens - ) ?? []), - ] satisfies TokenSection[] - }, [loading, popularTokenOptions, portfolioTokenOptions, t]) - - return useMemo( - () => ({ - data: sections, - loading, - error: error || undefined, - refetch: refetchAll, - }), - [error, loading, refetchAll, sections] - ) -} - -function _TokenSelectorSwapInputList({ - onSelectCurrency, - chainFilter, -}: { - onSelectCurrency: OnSelectCurrency - chainFilter: UniverseChainId | null -}): JSX.Element { - const { data: sections, loading, error, refetch } = useTokenSectionsForSwapInput(chainFilter) - return ( - - ) -} - -export const TokenSelectorSwapInputList = memo(_TokenSelectorSwapInputList) diff --git a/packages/wallet/src/components/TokenSelector/filter.test.ts b/packages/wallet/src/components/TokenSelector/filter.test.ts index 86cb707e873..f5e8d698bff 100644 --- a/packages/wallet/src/components/TokenSelector/filter.test.ts +++ b/packages/wallet/src/components/TokenSelector/filter.test.ts @@ -1,10 +1,10 @@ import { Currency } from '@uniswap/sdk-core' +import { filter } from 'uniswap/src/components/TokenSelector/filter' +import { TokenOption } from 'uniswap/src/components/TokenSelector/types' +import { DAI, DAI_ARBITRUM_ONE } from 'uniswap/src/constants/tokens' +import { NativeCurrency } from 'uniswap/src/features/tokens/NativeCurrency' import { UniverseChainId, WalletChainId } from 'uniswap/src/types/chains' -import { filter } from 'wallet/src/components/TokenSelector/filter' -import { TokenOption } from 'wallet/src/components/TokenSelector/types' -import { DAI, DAI_ARBITRUM_ONE } from 'wallet/src/constants/tokens' -import { NativeCurrency } from 'wallet/src/features/tokens/NativeCurrency' -import { currencyId } from 'wallet/src/utils/currencyId' +import { currencyId } from 'uniswap/src/utils/currencyId' const ETH = NativeCurrency.onChain(UniverseChainId.Mainnet) @@ -44,7 +44,7 @@ const TEST_TOKEN_INPUT: TokenOption[] = [ const filterAndGetCurrencies = ( currencies: TokenOption[], chainFilter: WalletChainId | null, - searchFilter?: string + searchFilter?: string, ): Currency[] => filter(currencies, chainFilter, searchFilter).map((cm) => cm.currencyInfo.currency) describe(filter, () => { @@ -66,10 +66,7 @@ describe(filter, () => { it('filters by partial token name', () => { expect(filterAndGetCurrencies(TEST_TOKEN_INPUT, null, 'th')).toEqual([ETH]) - expect(filterAndGetCurrencies(TEST_TOKEN_INPUT, null, 'stable')).toEqual([ - DAI, - DAI_ARBITRUM_ONE, - ]) + expect(filterAndGetCurrencies(TEST_TOKEN_INPUT, null, 'stable')).toEqual([DAI, DAI_ARBITRUM_ONE]) }) it('filters by first characters of token address', () => { @@ -89,18 +86,10 @@ describe(filter, () => { it('filters by chainFilter and searchFilter', () => { expect(filterAndGetCurrencies(TEST_TOKEN_INPUT, UniverseChainId.Mainnet, 'DA')).toEqual([DAI]) - expect(filterAndGetCurrencies(TEST_TOKEN_INPUT, UniverseChainId.Mainnet, DAI.address)).toEqual([ - DAI, - ]) - expect(filterAndGetCurrencies(TEST_TOKEN_INPUT, UniverseChainId.ArbitrumOne, 'DAI')).toEqual([ + expect(filterAndGetCurrencies(TEST_TOKEN_INPUT, UniverseChainId.Mainnet, DAI.address)).toEqual([DAI]) + expect(filterAndGetCurrencies(TEST_TOKEN_INPUT, UniverseChainId.ArbitrumOne, 'DAI')).toEqual([DAI_ARBITRUM_ONE]) + expect(filterAndGetCurrencies(TEST_TOKEN_INPUT, UniverseChainId.ArbitrumOne, DAI_ARBITRUM_ONE.address)).toEqual([ DAI_ARBITRUM_ONE, ]) - expect( - filterAndGetCurrencies( - TEST_TOKEN_INPUT, - UniverseChainId.ArbitrumOne, - DAI_ARBITRUM_ONE.address - ) - ).toEqual([DAI_ARBITRUM_ONE]) }) }) diff --git a/packages/wallet/src/components/TokenSelector/flowToModalName.tsx b/packages/wallet/src/components/TokenSelector/flowToModalName.tsx index cd74d22b7e9..ff38fa38e6c 100644 --- a/packages/wallet/src/components/TokenSelector/flowToModalName.tsx +++ b/packages/wallet/src/components/TokenSelector/flowToModalName.tsx @@ -1,5 +1,5 @@ import { ModalName, ModalNameType } from 'uniswap/src/features/telemetry/constants' -import { TokenSelectorFlow } from 'wallet/src/features/transactions/transfer/types' +import { TokenSelectorFlow } from 'uniswap/src/features/transactions/transfer/types' export function flowToModalName(flow: TokenSelectorFlow): ModalNameType | undefined { switch (flow) { diff --git a/packages/wallet/src/components/TokenSelector/hooks.test.ts b/packages/wallet/src/components/TokenSelector/hooks.test.ts index 93404c35ab7..e1e0d2474e4 100644 --- a/packages/wallet/src/components/TokenSelector/hooks.test.ts +++ b/packages/wallet/src/components/TokenSelector/hooks.test.ts @@ -2,8 +2,15 @@ import { ApolloError } from '@apollo/client' import { toIncludeSameMembers } from 'jest-extended' import { PreloadedState } from 'redux' +import { createEmptyBalanceOption } from 'uniswap/src/components/TokenSelector/utils' +import { BRIDGED_BASE_ADDRESSES } from 'uniswap/src/constants/addresses' import { Chain } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { fromGraphQLChain } from 'uniswap/src/features/chains/utils' +import { tokenProjectToCurrencyInfos } from 'uniswap/src/features/dataApi/utils' +import { TokenSelectorFlow } from 'uniswap/src/features/transactions/transfer/types' +import { arbitrumDaiCurrencyInfo, ethCurrencyInfo, usdcCurrencyInfo } from 'uniswap/src/test/fixtures' import { UniverseChainId, WalletChainId } from 'uniswap/src/types/chains' +import { buildCurrencyId } from 'uniswap/src/utils/currencyId' import { useAllCommonBaseCurrencies, useCommonTokensOptions, @@ -15,17 +22,10 @@ import { usePortfolioBalancesForAddressById, usePortfolioTokenOptions, } from 'wallet/src/components/TokenSelector/hooks' -import { createEmptyBalanceOption } from 'wallet/src/components/TokenSelector/utils' -import { BRIDGED_BASE_ADDRESSES } from 'wallet/src/constants/addresses' -import { fromGraphQLChain } from 'wallet/src/features/chains/utils' -import { tokenProjectToCurrencyInfos } from 'wallet/src/features/dataApi/utils' -import { TokenSelectorFlow } from 'wallet/src/features/transactions/transfer/types' import { SharedState } from 'wallet/src/state/reducer' import { SAMPLE_SEED_ADDRESS_1, - arbitrumDaiCurrencyInfo, daiToken, - ethCurrencyInfo, ethToken, portfolio, portfolioBalance, @@ -35,12 +35,10 @@ import { tokenProject, usdcArbitrumToken, usdcBaseToken, - usdcCurrencyInfo, usdcToken, } from 'wallet/src/test/fixtures' import { act, createArray, renderHook, waitFor } from 'wallet/src/test/test-utils' import { portfolioBalancesById, queryResolvers } from 'wallet/src/test/utils' -import { buildCurrencyId } from 'wallet/src/utils/currencyId' expect.extend({ toIncludeSameMembers }) @@ -56,7 +54,7 @@ const favoriteTokens = [eth, dai, usdc] const favoriteTokenBalances = [ethBalance, daiBalance, usdcBalance] const favoriteCurrencyIds = favoriteTokens.map((t) => - buildCurrencyId(fromGraphQLChain(t.chain) ?? UniverseChainId.Mainnet, t.address) + buildCurrencyId(fromGraphQLChain(t.chain) ?? UniverseChainId.Mainnet, t.address), ) const preloadedState: PreloadedState = { @@ -80,16 +78,8 @@ const queryResolver = describe(useAllCommonBaseCurrencies, () => { const projects = createArray(3, tokenProject) - const nonBridgedTokens = [ - ethToken(), - daiToken(), - usdcToken(), - usdcBaseToken(), - usdcArbitrumToken(), - ] - const bridgedTokens = BRIDGED_BASE_ADDRESSES.map((address) => - token({ address, chain: Chain.Ethereum }) - ) + const nonBridgedTokens = [ethToken(), daiToken(), usdcToken(), usdcBaseToken(), usdcArbitrumToken()] + const bridgedTokens = BRIDGED_BASE_ADDRESSES.map((address) => token({ address, chain: Chain.Ethereum })) const projectWithBridged = tokenProject({ tokens: [...nonBridgedTokens, ...bridgedTokens] }) const tokenProjectWithoutBridged = { ...projectWithBridged, // Copy all props except tokens (leave only non-bridged tokens) @@ -496,7 +486,7 @@ describe(usePortfolioTokenOptions, () => { it.each(cases)('$test', async ({ input, output }) => { const { result } = renderHook( () => usePortfolioTokenOptions(...(input as Parameters)), - { resolvers } + { resolvers }, ) await waitFor(() => { @@ -532,9 +522,7 @@ describe(usePopularTokensOptions, () => { test: 'returns popular token options when there is data', input: { portfolios: [portfolio({ tokenBalances })], topTokens }, output: { - data: expect.toIncludeSameMembers( - tokenBalances.map((t) => portfolioBalance({ fromBalance: t })) - ), + data: expect.toIncludeSameMembers(tokenBalances.map((t) => portfolioBalance({ fromBalance: t }))), error: undefined, }, }, @@ -542,14 +530,11 @@ describe(usePopularTokensOptions, () => { it.each(cases)('$test', async ({ input, output }) => { const { resolvers } = queryResolvers( - Object.fromEntries( - Object.entries(input).map(([name, resolver]) => [name, queryResolver(resolver)]) - ) - ) - const { result } = renderHook( - () => usePopularTokensOptions(SAMPLE_SEED_ADDRESS_1, UniverseChainId.ArbitrumOne), - { resolvers } + Object.fromEntries(Object.entries(input).map(([name, resolver]) => [name, queryResolver(resolver)])), ) + const { result } = renderHook(() => usePopularTokensOptions(SAMPLE_SEED_ADDRESS_1, UniverseChainId.ArbitrumOne), { + resolvers, + }) expect(result.current.loading).toEqual(true) @@ -590,9 +575,7 @@ describe(useCommonTokensOptions, () => { tokenProjects: [tokenProject({ tokens })], }, output: { - data: expect.toIncludeSameMembers( - tokenBalances.map((t) => portfolioBalance({ fromBalance: t })) - ), + data: expect.toIncludeSameMembers(tokenBalances.map((t) => portfolioBalance({ fromBalance: t }))), error: undefined, }, }, @@ -616,14 +599,9 @@ describe(useCommonTokensOptions, () => { it.each(cases)('$test', async ({ input: { chainFilter = null, ...resolverResults }, output }) => { const { resolvers } = queryResolvers( - Object.fromEntries( - Object.entries(resolverResults).map(([name, resolver]) => [name, queryResolver(resolver)]) - ) - ) - const { result } = renderHook( - () => useCommonTokensOptions(SAMPLE_SEED_ADDRESS_1, chainFilter), - { resolvers } + Object.fromEntries(Object.entries(resolverResults).map(([name, resolver]) => [name, queryResolver(resolver)])), ) + const { result } = renderHook(() => useCommonTokensOptions(SAMPLE_SEED_ADDRESS_1, chainFilter), { resolvers }) expect(result.current.loading).toEqual(true) @@ -664,7 +642,7 @@ describe(useFavoriteTokensOptions, () => { }, output: { data: expect.toIncludeSameMembers( - favoriteTokenBalances.map((balance) => portfolioBalance({ fromBalance: balance })) + favoriteTokenBalances.map((balance) => portfolioBalance({ fromBalance: balance })), ), error: undefined, }, @@ -689,14 +667,12 @@ describe(useFavoriteTokensOptions, () => { it.each(cases)('$test', async ({ input: { chainFilter = null, ...resolverResults }, output }) => { const { resolvers } = queryResolvers( - Object.fromEntries( - Object.entries(resolverResults).map(([name, resolver]) => [name, queryResolver(resolver)]) - ) - ) - const { result } = renderHook( - () => useFavoriteTokensOptions(SAMPLE_SEED_ADDRESS_1, chainFilter), - { resolvers, preloadedState } + Object.fromEntries(Object.entries(resolverResults).map(([name, resolver]) => [name, queryResolver(resolver)])), ) + const { result } = renderHook(() => useFavoriteTokensOptions(SAMPLE_SEED_ADDRESS_1, chainFilter), { + resolvers, + preloadedState, + }) expect(result.current.loading).toEqual(true) diff --git a/packages/wallet/src/components/TokenSelector/hooks.ts b/packages/wallet/src/components/TokenSelector/hooks.tsx similarity index 51% rename from packages/wallet/src/components/TokenSelector/hooks.ts rename to packages/wallet/src/components/TokenSelector/hooks.tsx index b9b60ef3d21..3a1a83f7bad 100644 --- a/packages/wallet/src/components/TokenSelector/hooks.ts +++ b/packages/wallet/src/components/TokenSelector/hooks.tsx @@ -1,32 +1,44 @@ +/* eslint-disable max-lines */ import { useCallback, useEffect, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useDispatch } from 'react-redux' +import { Text, TouchableArea } from 'ui/src' +import { filter } from 'uniswap/src/components/TokenSelector/filter' +import { TokenOption, TokenSection } from 'uniswap/src/components/TokenSelector/types' +import { + createEmptyBalanceOption, + formatSearchResults, + getTokenOptionsSection, +} from 'uniswap/src/components/TokenSelector/utils' +import { BRIDGED_BASE_ADDRESSES } from 'uniswap/src/constants/addresses' +import { DAI, USDC, USDT, WBTC } from 'uniswap/src/constants/tokens' +import { SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { GqlResult } from 'uniswap/src/data/types' +import { useSearchTokens } from 'uniswap/src/features/dataApi/searchTokens' +import { useTokenProjects } from 'uniswap/src/features/dataApi/tokenProjects' +import { usePopularTokens } from 'uniswap/src/features/dataApi/topTokens' import { CurrencyInfo, PortfolioBalance } from 'uniswap/src/features/dataApi/types' +import { buildCurrency, gqlTokenToCurrencyInfo, usePersistedError } from 'uniswap/src/features/dataApi/utils' +import { SearchResultType } from 'uniswap/src/features/search/SearchResult' import { WalletEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' +import { TokenSelectorFlow } from 'uniswap/src/features/transactions/transfer/types' import { UniverseChainId } from 'uniswap/src/types/chains' -import { filter } from 'wallet/src/components/TokenSelector/filter' +import { areAddressesEqual } from 'uniswap/src/utils/addresses' +import { buildNativeCurrencyId, buildWrappedNativeCurrencyId, currencyId } from 'uniswap/src/utils/currencyId' import { flowToModalName } from 'wallet/src/components/TokenSelector/flowToModalName' -import { TokenOption } from 'wallet/src/components/TokenSelector/types' -import { createEmptyBalanceOption } from 'wallet/src/components/TokenSelector/utils' -import { BRIDGED_BASE_ADDRESSES } from 'wallet/src/constants/addresses' -import { DAI, USDC, USDT, WBTC } from 'wallet/src/constants/tokens' import { sortPortfolioBalances, usePortfolioBalances, useTokenBalancesGroupedByVisibility, } from 'wallet/src/features/dataApi/balances' -import { useTokenProjects } from 'wallet/src/features/dataApi/tokenProjects' -import { usePopularTokens } from 'wallet/src/features/dataApi/topTokens' -import { usePersistedError } from 'wallet/src/features/dataApi/utils' import { selectFavoriteTokens } from 'wallet/src/features/favorites/selectors' -import { TokenSelectorFlow } from 'wallet/src/features/transactions/transfer/types' +import { TokenSearchResult } from 'wallet/src/features/search/SearchResult' +import { addToSearchHistory, clearSearchHistory } from 'wallet/src/features/search/searchHistorySlice' +import { selectSearchHistory } from 'wallet/src/features/search/selectSearchHistory' +import { usePopularTokens as usePopularWalletTokens } from 'wallet/src/features/tokens/hooks' +import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' import { useAppSelector } from 'wallet/src/state' -import { areAddressesEqual } from 'wallet/src/utils/addresses' -import { - buildNativeCurrencyId, - buildWrappedNativeCurrencyId, - currencyId, -} from 'wallet/src/utils/currencyId' // Use Mainnet base token addresses since TokenProjects query returns each token // on each network @@ -60,9 +72,7 @@ export function useCurrencies(currencyIds: string[]): GqlResult } const { address } = currencyInfo.currency - const bridgedAsset = BRIDGED_BASE_ADDRESSES.find((bridgedAddress) => - areAddressesEqual(bridgedAddress, address) - ) + const bridgedAsset = BRIDGED_BASE_ADDRESSES.find((bridgedAddress) => areAddressesEqual(bridgedAddress, address)) if (!bridgedAsset) { return true @@ -77,12 +87,7 @@ export function useCurrencies(currencyIds: string[]): GqlResult export function useFavoriteCurrencies(): GqlResult { const favoriteCurrencyIds = useAppSelector(selectFavoriteTokens) - const { - data: favoriteTokensOnAllChains, - loading, - error, - refetch, - } = useTokenProjects(favoriteCurrencyIds) + const { data: favoriteTokensOnAllChains, loading, error, refetch } = useTokenProjects(favoriteCurrencyIds) const persistedError = usePersistedError(loading, error) @@ -98,7 +103,7 @@ export function useFavoriteCurrencies(): GqlResult { .filter((token: CurrencyInfo | undefined): token is CurrencyInfo => { return !!token }), - [favoriteCurrencyIds, favoriteTokensOnAllChains] + [favoriteCurrencyIds, favoriteTokensOnAllChains], ) return { data: favoriteTokens, loading, error: persistedError, refetch } @@ -106,7 +111,7 @@ export function useFavoriteCurrencies(): GqlResult { export function useFilterCallbacks( chainId: UniverseChainId | null, - flow: TokenSelectorFlow + flow: TokenSelectorFlow, ): { chainFilter: UniverseChainId | null searchFilter: string | null @@ -129,17 +134,14 @@ export function useFilterCallbacks( modal: flowToModalName(flow), }) }, - [flow] + [flow], ) const onClearSearchFilter = useCallback(() => { setSearchFilter(null) }, []) - const onChangeText = useCallback( - (newSearchFilter: string) => setSearchFilter(newSearchFilter), - [setSearchFilter] - ) + const onChangeText = useCallback((newSearchFilter: string) => setSearchFilter(newSearchFilter), [setSearchFilter]) return { chainFilter, @@ -175,14 +177,13 @@ export function useCurrencyInfosToTokenOptions({ : currencyInfos return sortedCurrencyInfos.map( - (currencyInfo) => - portfolioBalancesById?.[currencyInfo.currencyId] ?? createEmptyBalanceOption(currencyInfo) + (currencyInfo) => portfolioBalancesById?.[currencyInfo.currencyId] ?? createEmptyBalanceOption(currencyInfo), ) }, [currencyInfos, portfolioBalancesById, sortAlphabetically]) } export function usePortfolioBalancesForAddressById( - address: Address + address: Address, ): GqlResult | undefined> { const { data: portfolioBalancesById, @@ -205,27 +206,19 @@ export function usePortfolioBalancesForAddressById( export function usePortfolioTokenOptions( address: Address, chainFilter: UniverseChainId | null, - searchFilter?: string + searchFilter?: string, ): GqlResult { - const { - data: portfolioBalancesById, - error, - refetch, - loading, - } = usePortfolioBalancesForAddressById(address) + const { data: portfolioBalancesById, error, refetch, loading } = usePortfolioBalancesForAddressById(address) const { shownTokens } = useTokenBalancesGroupedByVisibility({ balancesById: portfolioBalancesById, }) - const portfolioBalances = useMemo( - () => (shownTokens ? sortPortfolioBalances(shownTokens) : undefined), - [shownTokens] - ) + const portfolioBalances = useMemo(() => (shownTokens ? sortPortfolioBalances(shownTokens) : undefined), [shownTokens]) const filteredPortfolioBalances = useMemo( () => portfolioBalances && filter(portfolioBalances, chainFilter, searchFilter), - [chainFilter, portfolioBalances, searchFilter] + [chainFilter, portfolioBalances, searchFilter], ) return { @@ -238,7 +231,7 @@ export function usePortfolioTokenOptions( export function usePopularTokensOptions( address: Address, - chainFilter: UniverseChainId + chainFilter: UniverseChainId, ): GqlResult { const { data: portfolioBalancesById, @@ -265,9 +258,7 @@ export function usePopularTokensOptions( refetchPopularTokens?.() }, [portfolioBalancesByIdRefetch, refetchPopularTokens]) - const error = - (!portfolioBalancesById && portfolioBalancesByIdError) || - (!popularTokenOptions && popularTokensError) + const error = (!portfolioBalancesById && portfolioBalancesByIdError) || (!popularTokenOptions && popularTokensError) return { data: popularTokenOptions, @@ -277,9 +268,33 @@ export function usePopularTokensOptions( } } +export function useAddToSearchHistory(): { registerSearch: (currencyInfo: CurrencyInfo) => void } { + const dispatch = useDispatch() + + const registerSearch = (currencyInfo: CurrencyInfo): void => { + if (currencyInfo.currency.symbol && currencyInfo.currency.isToken) { + dispatch( + addToSearchHistory({ + searchResult: { + type: SearchResultType.Token, + chainId: currencyInfo.currency.chainId, + address: currencyInfo.currency.address, + name: currencyInfo.currency.name ?? null, + symbol: currencyInfo.currency.symbol, + logoUrl: currencyInfo.logoUrl ?? null, + safetyLevel: currencyInfo.safetyLevel ?? null, + }, + }), + ) + } + } + + return { registerSearch } +} + export function useCommonTokensOptions( address: Address, - chainFilter: UniverseChainId | null + chainFilter: UniverseChainId | null, ): GqlResult { const { data: portfolioBalancesById, @@ -306,12 +321,11 @@ export function useCommonTokensOptions( }, [portfolioBalancesByIdRefetch, refetchCommonBaseCurrencies]) const error = - (!portfolioBalancesById && portfolioBalancesByIdError) || - (!commonBaseCurrencies && commonBaseCurrenciesError) + (!portfolioBalancesById && portfolioBalancesByIdError) || (!commonBaseCurrencies && commonBaseCurrenciesError) const filteredCommonBaseTokenOptions = useMemo( () => commonBaseTokenOptions && filter(commonBaseTokenOptions, chainFilter), - [chainFilter, commonBaseTokenOptions] + [chainFilter, commonBaseTokenOptions], ) return { @@ -324,7 +338,7 @@ export function useCommonTokensOptions( export function useFavoriteTokensOptions( address: Address, - chainFilter: UniverseChainId | null + chainFilter: UniverseChainId | null, ): GqlResult { const { data: portfolioBalancesById, @@ -352,12 +366,11 @@ export function useFavoriteTokensOptions( }, [portfolioBalancesByIdRefetch, refetchFavoriteCurrencies]) const error = - (!portfolioBalancesById && portfolioBalancesByIdError) || - (!favoriteCurrencies && favoriteCurrenciesError) + (!portfolioBalancesById && portfolioBalancesByIdError) || (!favoriteCurrencies && favoriteCurrenciesError) const filteredFavoriteTokenOptions = useMemo( () => favoriteTokenOptions && filter(favoriteTokenOptions, chainFilter), - [chainFilter, favoriteTokenOptions] + [chainFilter, favoriteTokenOptions], ) return { @@ -367,3 +380,165 @@ export function useFavoriteTokensOptions( loading: loadingPorfolioBalancesById || loadingFavoriteCurrencies, } } + +function searchResultToCurrencyInfo({ + chainId, + address, + symbol, + name, + logoUrl, + safetyLevel, +}: TokenSearchResult): CurrencyInfo | null { + const currency = buildCurrency({ + chainId, + address, + decimals: 0, // this does not matter in a context of CurrencyInfo here, as we do not provide any balance + symbol, + name, + }) + + if (!currency) { + return null + } + + const currencyInfo: CurrencyInfo = { + currency, + currencyId: currencyId(currency), + logoUrl, + safetyLevel: safetyLevel ?? SafetyLevel.StrongWarning, + // defaulting to not spam, as user has searched and chosen this token before + isSpam: false, + } + return currencyInfo +} + +function currencyInfosToTokenOptions(currencyInfos: Array | undefined): TokenOption[] | undefined { + return currencyInfos + ?.filter((cI): cI is CurrencyInfo => Boolean(cI)) + .map((currencyInfo) => ({ + currencyInfo, + quantity: null, + balanceUSD: undefined, + })) +} + +function ClearAll({ onPress }: { onPress: () => void }): JSX.Element { + const { t } = useTranslation() + return ( + + + {t('tokens.selector.button.clear')} + + + ) +} + +export function useTokenSectionsForEmptySearch(): GqlResult { + const { t } = useTranslation() + const dispatch = useDispatch() + + const { popularTokens, loading } = usePopularWalletTokens() + + const searchHistory = useAppSelector(selectSearchHistory) + + // it's a depenedency of useMemo => useCallback + const onPressClearSearchHistory = useCallback((): void => { + dispatch(clearSearchHistory()) + }, [dispatch]) + + const sections = useMemo( + () => [ + ...(getTokenOptionsSection( + t('tokens.selector.section.recent'), + currencyInfosToTokenOptions( + searchHistory + .filter((searchResult): searchResult is TokenSearchResult => searchResult.type === SearchResultType.Token) + .map(searchResultToCurrencyInfo), + ), + , + ) ?? []), + ...(getTokenOptionsSection( + t('tokens.selector.section.popular'), + currencyInfosToTokenOptions(popularTokens?.map(gqlTokenToCurrencyInfo)), + ) ?? []), + ], + [onPressClearSearchHistory, popularTokens, searchHistory, t], + ) + + return useMemo( + () => ({ + data: sections, + loading, + }), + [loading, sections], + ) +} + +export function useTokenSectionsForSearchResults( + chainFilter: UniverseChainId | null, + searchFilter: string | null, + isBalancesOnlySearch: boolean, +): GqlResult { + const { t } = useTranslation() + const activeAccountAddress = useActiveAccountAddressWithThrow() + + const { + data: portfolioBalancesById, + error: portfolioBalancesByIdError, + refetch: refetchPortfolioBalances, + loading: portfolioBalancesByIdLoading, + } = usePortfolioBalancesForAddressById(activeAccountAddress) + + const { + data: portfolioTokenOptions, + error: portfolioTokenOptionsError, + refetch: refetchPortfolioTokenOptions, + loading: portfolioTokenOptionsLoading, + } = usePortfolioTokenOptions(activeAccountAddress, chainFilter, searchFilter ?? undefined) + + // Only call search endpoint if isBalancesOnlySearch is false + const { + data: searchResultCurrencies, + error: searchTokensError, + refetch: refetchSearchTokens, + loading: searchTokensLoading, + } = useSearchTokens(searchFilter, chainFilter, /*skip*/ isBalancesOnlySearch) + + const searchResults = useMemo(() => { + return formatSearchResults(searchResultCurrencies, portfolioBalancesById, searchFilter) + }, [searchResultCurrencies, portfolioBalancesById, searchFilter]) + + const loading = + portfolioTokenOptionsLoading || portfolioBalancesByIdLoading || (!isBalancesOnlySearch && searchTokensLoading) + + const sections = useMemo( + () => + getTokenOptionsSection( + t('tokens.selector.section.search'), + // Use local search when only searching balances + isBalancesOnlySearch ? portfolioTokenOptions : searchResults, + ), + [isBalancesOnlySearch, portfolioTokenOptions, searchResults, t], + ) + + const error = + (!portfolioBalancesById && portfolioBalancesByIdError) || + (!portfolioTokenOptions && portfolioTokenOptionsError) || + (!isBalancesOnlySearch && !searchResults && searchTokensError) + + const refetchAll = useCallback(() => { + refetchPortfolioBalances?.() + refetchSearchTokens?.() + refetchPortfolioTokenOptions?.() + }, [refetchPortfolioBalances, refetchPortfolioTokenOptions, refetchSearchTokens]) + + return useMemo( + () => ({ + data: sections, + loading, + error: error || undefined, + refetch: refetchAll, + }), + [error, loading, refetchAll, sections], + ) +} diff --git a/packages/wallet/src/components/TokenSelector/types.ts b/packages/wallet/src/components/TokenSelector/types.ts deleted file mode 100644 index 77620ac39bf..00000000000 --- a/packages/wallet/src/components/TokenSelector/types.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' - -export type TokenOption = { - currencyInfo: CurrencyInfo - quantity: number | null // float representation of balance, returned by data-api - balanceUSD: Maybe -} - -export type OnSelectCurrency = ( - currency: CurrencyInfo, - section: SuggestedTokenSection | TokenSection, - index: number -) => void - -export type TokenSection = { - title: string - data: TokenOption[] - rightElement?: JSX.Element -} - -export type SuggestedTokenSection = { - title: string - data: TokenOption[][] - rightElement?: JSX.Element -} - -export type TokenSelectorListSections = Array diff --git a/packages/wallet/src/components/WalletConnect/DappIconPlaceholder.tsx b/packages/wallet/src/components/WalletConnect/DappIconPlaceholder.tsx index b2fee91ed53..ac094d0f9fe 100644 --- a/packages/wallet/src/components/WalletConnect/DappIconPlaceholder.tsx +++ b/packages/wallet/src/components/WalletConnect/DappIconPlaceholder.tsx @@ -1,13 +1,7 @@ import { Flex, Text } from 'ui/src' import { iconSizes } from 'ui/src/theme' -export function DappIconPlaceholder({ - name, - iconSize, -}: { - name?: string - iconSize: number -}): JSX.Element { +export function DappIconPlaceholder({ name, iconSize }: { name?: string; iconSize: number }): JSX.Element { return ( - = iconSizes.icon40 ? 'subheading1' : 'body2'}> + width={iconSize} + > + = iconSizes.icon40 ? 'subheading1' : 'body2'}> {name && name.length > 0 ? name.charAt(0) : ' '} diff --git a/packages/wallet/src/components/WalletPreviewCard/WalletPreviewCard.test.tsx b/packages/wallet/src/components/WalletPreviewCard/WalletPreviewCard.test.tsx index 9b4a78992ff..ef5f02ecc45 100644 --- a/packages/wallet/src/components/WalletPreviewCard/WalletPreviewCard.test.tsx +++ b/packages/wallet/src/components/WalletPreviewCard/WalletPreviewCard.test.tsx @@ -3,8 +3,6 @@ import { SAMPLE_SEED_ADDRESS_1 } from 'wallet/src/test/fixtures' import { render } from 'wallet/src/test/test-utils' it('renders wallet preview card', () => { - const tree = render( - null} /> - ) + const tree = render( null} />) expect(tree).toMatchSnapshot() }) diff --git a/packages/wallet/src/components/WalletPreviewCard/WalletPreviewCard.tsx b/packages/wallet/src/components/WalletPreviewCard/WalletPreviewCard.tsx index 66f9d551629..6713ff6ec2f 100644 --- a/packages/wallet/src/components/WalletPreviewCard/WalletPreviewCard.tsx +++ b/packages/wallet/src/components/WalletPreviewCard/WalletPreviewCard.tsx @@ -16,7 +16,8 @@ interface Props { hideSelectionCircle?: boolean } -export const ADDRESS_WRAPPER_HEIGHT = 36 +// Some preview cards do not have a name (no unitag), so we need to set a default height to keep their height consistent. +export const WALLET_PREVIEW_CARD_HEIGHT = 72 export default function WalletPreviewCard({ address, @@ -36,16 +37,17 @@ export default function WalletPreviewCard({ borderColor={selected ? '$surface3' : '$surface2'} borderRadius="$rounded20" borderWidth={1} - px="$spacing16" - py="$spacing16" + height={WALLET_PREVIEW_CARD_HEIGHT} + p="$spacing12" shadowColor={selected ? '$shadowColor' : '$transparent'} shadowOpacity={0.05} shadowRadius={selected ? '$spacing8' : '$none'} onPress={(): void => onSelect(address)} - {...rest}> - + {...rest} + > + - + {Boolean(balance) && ( {balanceFormatted} diff --git a/packages/wallet/src/components/WalletPreviewCard/__snapshots__/WalletPreviewCard.test.tsx.snap b/packages/wallet/src/components/WalletPreviewCard/__snapshots__/WalletPreviewCard.test.tsx.snap index cf4449be389..5405122d489 100644 --- a/packages/wallet/src/components/WalletPreviewCard/__snapshots__/WalletPreviewCard.test.tsx.snap +++ b/packages/wallet/src/components/WalletPreviewCard/__snapshots__/WalletPreviewCard.test.tsx.snap @@ -31,11 +31,12 @@ exports[`renders wallet preview card 1`] = ` "borderTopRightRadius": 20, "borderTopWidth": 1, "flexDirection": "column", + "height": 72, "opacity": 1, - "paddingBottom": 16, - "paddingLeft": 16, - "paddingRight": 16, - "paddingTop": 16, + "paddingBottom": 12, + "paddingLeft": 12, + "paddingRight": 12, + "paddingTop": 12, "shadowColor": "rgb(0,0,0)", "shadowOffset": { "height": 0, @@ -54,7 +55,9 @@ exports[`renders wallet preview card 1`] = ` diff --git a/packages/wallet/src/components/accounts/AccountDetails.test.tsx b/packages/wallet/src/components/accounts/AccountDetails.test.tsx index 5147574946c..e17e2bde1be 100644 --- a/packages/wallet/src/components/accounts/AccountDetails.test.tsx +++ b/packages/wallet/src/components/accounts/AccountDetails.test.tsx @@ -10,9 +10,7 @@ describe(AccountDetails, () => { }) it('renders without error with chevron', () => { - const tree = renderWithProviders( - - ) + const tree = renderWithProviders() expect(tree.toJSON()).toMatchSnapshot() }) diff --git a/packages/wallet/src/components/accounts/AccountDetails.tsx b/packages/wallet/src/components/accounts/AccountDetails.tsx index df40affbdba..398ec625fd9 100644 --- a/packages/wallet/src/components/accounts/AccountDetails.tsx +++ b/packages/wallet/src/components/accounts/AccountDetails.tsx @@ -1,7 +1,7 @@ import { ColorTokens, Flex, Text } from 'ui/src' import { RotatableChevron } from 'ui/src/components/icons' +import { shortenAddress } from 'uniswap/src/utils/addresses' import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' -import { shortenAddress } from 'wallet/src/utils/addresses' export function AccountDetails({ address, @@ -32,14 +32,7 @@ export function AccountDetails({ {shortenAddress(address)} - {chevron && ( - - )} + {chevron && } ) diff --git a/packages/wallet/src/components/accounts/AccountIcon.tsx b/packages/wallet/src/components/accounts/AccountIcon.tsx index 6fe10b1484f..7ddc7a3677d 100644 --- a/packages/wallet/src/components/accounts/AccountIcon.tsx +++ b/packages/wallet/src/components/accounts/AccountIcon.tsx @@ -44,7 +44,8 @@ export function AccountIcon({ borderRadius="$roundedFull" borderWidth={showBorder ? borderWidth : 0} position="relative" - testID="account-icon"> + testID="account-icon" + > {avatarUri ? ( + testID="account-icon/view-only-badge" + > )} @@ -86,15 +88,7 @@ export const UniconGradient = ({ color, size }: { color: string; size: number }) - + ) } diff --git a/packages/wallet/src/components/accounts/AddressDisplay.tsx b/packages/wallet/src/components/accounts/AddressDisplay.tsx index 040f5112301..2de26e415a9 100644 --- a/packages/wallet/src/components/accounts/AddressDisplay.tsx +++ b/packages/wallet/src/components/accounts/AddressDisplay.tsx @@ -1,27 +1,22 @@ +import { SharedEventName } from '@uniswap/analytics-events' import { PropsWithChildren, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { FlexAlignType } from 'react-native' -import { - ColorTokens, - Flex, - HapticFeedback, - SpaceTokens, - Text, - TextProps, - TouchableArea, -} from 'ui/src' +import { useDispatch } from 'react-redux' +import { ColorTokens, Flex, HapticFeedback, SpaceTokens, Text, TextProps, TouchableArea } from 'ui/src' import { CopySheets } from 'ui/src/components/icons' import { fonts } from 'ui/src/theme' import { ElementName } from 'uniswap/src/features/telemetry/constants' +import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' +import { sanitizeAddressText, shortenAddress } from 'uniswap/src/utils/addresses' +import { setClipboard } from 'uniswap/src/utils/clipboard' import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon' import { DisplayNameText } from 'wallet/src/components/accounts/DisplayNameText' import { pushNotification } from 'wallet/src/features/notifications/slice' import { AppNotificationType, CopyNotificationType } from 'wallet/src/features/notifications/types' import { useAvatar, useDisplayName } from 'wallet/src/features/wallet/hooks' import { DisplayNameType } from 'wallet/src/features/wallet/types' -import { useAppDispatch } from 'wallet/src/state' -import { sanitizeAddressText, shortenAddress } from 'wallet/src/utils/addresses' -import { setClipboard } from 'wallet/src/utils/clipboard' type AddressDisplayProps = { address: string @@ -43,13 +38,7 @@ type AddressDisplayProps = { includeUnitagSuffix?: boolean textAlign?: FlexAlignType horizontalGap?: SpaceTokens - notificationsBadgeContainer?: ({ - children, - address, - }: { - children: JSX.Element - address: string - }) => JSX.Element + notificationsBadgeContainer?: ({ children, address }: { children: JSX.Element; address: string }) => JSX.Element gapBetweenLines?: SpaceTokens showViewOnlyLabel?: boolean showViewOnlyBadge?: boolean @@ -60,13 +49,10 @@ type CopyButtonWrapperProps = { backgroundColor?: string } -function CopyButtonWrapper({ - children, - onPress, -}: PropsWithChildren): JSX.Element { +function CopyButtonWrapper({ children, onPress }: PropsWithChildren): JSX.Element { if (onPress) { return ( - + {children} ) @@ -112,12 +98,11 @@ export function AddressDisplay({ gapBetweenLines = '$none', }: AddressDisplayProps): JSX.Element { const { t } = useTranslation() - const dispatch = useAppDispatch() + const dispatch = useDispatch() const displayName = useDisplayName(address, { includeUnitagSuffix, overrideDisplayName }) const { avatar } = useAvatar(address) - const showAddressAsSubtitle = - !hideAddressInSubtitle && displayName?.type !== DisplayNameType.Address + const showAddressAsSubtitle = !hideAddressInSubtitle && displayName?.type !== DisplayNameType.Address const onPressCopyAddress = async (): Promise => { if (!address) { @@ -129,15 +114,17 @@ export function AddressDisplay({ pushNotification({ type: AppNotificationType.Copied, copyType: CopyNotificationType.Address, - }) + }), ) + sendAnalyticsEvent(SharedEventName.ELEMENT_CLICKED, { + element: ElementName.CopyAddress, + }) } // Extract sizes so copy icon can match font variants const mainSize = fonts[variant].fontSize const captionSize = fonts[captionVariant].fontSize - const itemAlignment = - textAlign || (!showAccountIcon || direction === 'column' ? 'center' : 'flex-start') + const itemAlignment = textAlign || (!showAccountIcon || direction === 'column' ? 'center' : 'flex-start') const icon = useMemo(() => { return ( @@ -169,12 +156,9 @@ export function AddressDisplay({ return ( {showAccountIcon && - (notificationsBadgeContainer - ? notificationsBadgeContainer({ children: icon, address }) - : icon)} + (notificationsBadgeContainer ? notificationsBadgeContainer({ children: icon, address }) : icon)} - + + py={showCopyWrapperButton ? '$spacing4' : '$none'} + > {sanitizeAddressText(shortenAddress(address))} diff --git a/packages/wallet/src/components/accounts/AnimatedUnitagDisplayName.tsx b/packages/wallet/src/components/accounts/AnimatedUnitagDisplayName.tsx index c89ea24fe14..d746e292511 100644 --- a/packages/wallet/src/components/accounts/AnimatedUnitagDisplayName.tsx +++ b/packages/wallet/src/components/accounts/AnimatedUnitagDisplayName.tsx @@ -1,15 +1,22 @@ -import { useState } from 'react' +import { SharedEventName } from '@uniswap/analytics-events' +import { BaseSyntheticEvent, useState } from 'react' import { LayoutChangeEvent } from 'react-native' +import { useDispatch } from 'react-redux' import { AnimatePresence, Flex, HapticFeedback, Text, TouchableArea } from 'ui/src' import { CopyAlt, Unitag } from 'ui/src/components/icons' import { IconSizeTokens } from 'ui/src/theme' +import { ElementName } from 'uniswap/src/features/telemetry/constants' +import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' +import { ExtensionScreens } from 'uniswap/src/types/screens/extension' +import { MobileScreens } from 'uniswap/src/types/screens/mobile' +import { sanitizeAddressText, shortenAddress } from 'uniswap/src/utils/addresses' +import { setClipboard } from 'uniswap/src/utils/clipboard' +import { isExtension, isMobile } from 'utilities/src/platform' import { pushNotification } from 'wallet/src/features/notifications/slice' import { AppNotificationType, CopyNotificationType } from 'wallet/src/features/notifications/types' import { UNITAG_SUFFIX } from 'wallet/src/features/unitags/constants' import { DisplayName, DisplayNameType } from 'wallet/src/features/wallet/types' -import { useAppDispatch } from 'wallet/src/state' -import { sanitizeAddressText, shortenAddress } from 'wallet/src/utils/addresses' -import { setClipboard } from 'wallet/src/utils/clipboard' type AnimatedUnitagDisplayNameProps = { displayName: DisplayName @@ -22,7 +29,7 @@ export function AnimatedUnitagDisplayName({ unitagIconSize = '$icon.24', address, }: AnimatedUnitagDisplayNameProps): JSX.Element { - const dispatch = useAppDispatch() + const dispatch = useDispatch() const [showUnitagSuffix, setShowUnitagSuffix] = useState(false) const [textWidth, setTextWidth] = useState(0) const isUnitag = displayName?.type === DisplayNameType.Unitag @@ -35,19 +42,24 @@ export function AnimatedUnitagDisplayName({ setShowUnitagSuffix(!showUnitagSuffix) } - const onPressCopyAddress = async (): Promise => { + const onPressCopyAddress = async (e: BaseSyntheticEvent): Promise => { if (!address) { return } + e.stopPropagation() await HapticFeedback.impact() await setClipboard(address) dispatch( pushNotification({ type: AppNotificationType.Copied, copyType: CopyNotificationType.Address, - }) + }), ) + sendAnalyticsEvent(SharedEventName.ELEMENT_CLICKED, { + element: ElementName.CopyAddress, + screen: isExtension ? ExtensionScreens.Home : isMobile ? MobileScreens.Home : undefined, + }) } const isLayoutReady = textWidth > 0 @@ -66,11 +78,7 @@ export function AnimatedUnitagDisplayName({ So we set it to `position: absolute` on first render and then switch it to `relative` once we have the width. */} - + {UNITAG_SUFFIX} @@ -82,7 +90,13 @@ export function AnimatedUnitagDisplayName({ ) : null} {address && ( - + {sanitizeAddressText(shortenAddress(address))} diff --git a/packages/wallet/src/components/buttons/PlusMinusButton.tsx b/packages/wallet/src/components/buttons/PlusMinusButton.tsx index 4fe9e7f3787..98b61544623 100644 --- a/packages/wallet/src/components/buttons/PlusMinusButton.tsx +++ b/packages/wallet/src/components/buttons/PlusMinusButton.tsx @@ -26,7 +26,8 @@ export default function PlusMinusButton({ height={iconSizes.icon28} justifyContent="center" width={iconSizes.icon28} - onPress={(): void => onPress(type)}> + onPress={(): void => onPress(type)} + > {type === PlusMinusButtonType.Plus ? ( ) : ( diff --git a/packages/wallet/src/components/buttons/Switch.tsx b/packages/wallet/src/components/buttons/Switch.tsx index ef934e8e345..88ad5548ccf 100644 --- a/packages/wallet/src/components/buttons/Switch.tsx +++ b/packages/wallet/src/components/buttons/Switch.tsx @@ -30,7 +30,8 @@ export function Switch({ value, onValueChange, disabled }: SwitchProps): JSX.Ele false: disabledTrackColor, }, }} - onCheckedChange={disabled ? undefined : onValueChange}> + onCheckedChange={disabled ? undefined : onValueChange} + > ) @@ -56,7 +57,8 @@ export function WebSwitch({ value, onValueChange }: SwitchProps): JSX.Element { minHeight={TRACK_HEIGHT} minWidth={spacing.spacing60} p="$spacing4" - onCheckedChange={onValueChange}> + onCheckedChange={onValueChange} + > + {...rest} + > diff --git a/packages/wallet/src/components/gating/GatingOverrides.tsx b/packages/wallet/src/components/gating/GatingOverrides.tsx index 6a0ea3c6ed1..f3b31aca42d 100644 --- a/packages/wallet/src/components/gating/GatingOverrides.tsx +++ b/packages/wallet/src/components/gating/GatingOverrides.tsx @@ -2,11 +2,7 @@ import React from 'react' import { Accordion, Button, Flex, Separator, Text, isWeb } from 'ui/src' import { RotatableChevron } from 'ui/src/components/icons' import { Experiments } from 'uniswap/src/features/gating/experiments' -import { - FeatureFlags, - WALLET_FEATURE_FLAG_NAMES, - getFeatureFlagName, -} from 'uniswap/src/features/gating/flags' +import { FeatureFlags, WALLET_FEATURE_FLAG_NAMES, getFeatureFlagName } from 'uniswap/src/features/gating/flags' import { useFeatureFlagWithExposureLoggingDisabled } from 'uniswap/src/features/gating/hooks' import { Statsig } from 'uniswap/src/features/gating/sdk/statsig' import { Switch, WebSwitch } from 'wallet/src/components/buttons/Switch' @@ -60,7 +56,8 @@ export function GatingOverrides(): JSX.Element { Statsig.removeGateOverride() Statsig.removeConfigOverride() Statsig.removeLayerOverride() - }}> + }} + > Clear all gating overrides @@ -116,7 +113,8 @@ function ExperimentRow({ experiment }: { experiment: Experiments }): JSX.Element alignItems="center" gap="$spacing16" justifyContent="space-between" - paddingStart="$spacing16"> + paddingStart="$spacing16" + > {/* TODO(WEB-4164): implement experiment groups overrides */} diff --git a/packages/wallet/src/components/icons/Arrow.tsx b/packages/wallet/src/components/icons/Arrow.tsx index e9417007baa..a020e91236a 100644 --- a/packages/wallet/src/components/icons/Arrow.tsx +++ b/packages/wallet/src/components/icons/Arrow.tsx @@ -33,13 +33,7 @@ export function _Arrow({ size = 24, color = '#000000', direction = 'e' }: Props) } return ( - + ) } diff --git a/packages/wallet/src/components/icons/PlusCircle.tsx b/packages/wallet/src/components/icons/PlusCircle.tsx index 2437f514cd3..8fa293e4542 100644 --- a/packages/wallet/src/components/icons/PlusCircle.tsx +++ b/packages/wallet/src/components/icons/PlusCircle.tsx @@ -17,7 +17,8 @@ export function PlusCircle(): JSX.Element { shadowColor={isDarkMode ? '$shadowColor' : '$surface3'} shadowOffset={{ width: 0, height: 0 }} shadowRadius={10} - width={iconSizes.icon40}> + width={iconSizes.icon40} + > ) diff --git a/packages/wallet/src/components/input/AmountInput.test.tsx b/packages/wallet/src/components/input/AmountInput.test.tsx index be482b595a9..b9a0cf737e3 100644 --- a/packages/wallet/src/components/input/AmountInput.test.tsx +++ b/packages/wallet/src/components/input/AmountInput.test.tsx @@ -80,7 +80,7 @@ describe(parseValue, () => { parseValue({ value: ' 1234 ', ...defaultParams, - }) + }), ).toBe('1234') }) @@ -89,7 +89,7 @@ describe(parseValue, () => { parseValue({ value: '1,234.56', ...defaultParams, - }) + }), ).toBe('1234.56') expect( @@ -98,7 +98,7 @@ describe(parseValue, () => { ...defaultParams, decimalSeparator: ',', groupingSeparator: '.', - }) + }), ).toBe('1234.56') expect( @@ -107,7 +107,7 @@ describe(parseValue, () => { ...defaultParams, decimalSeparator: '.', groupingSeparator: ' ', - }) + }), ).toBe('1234.56') }) @@ -116,7 +116,7 @@ describe(parseValue, () => { parseValue({ value: ' example $1,234,567.123456789 example ', ...defaultParams, - }) + }), ).toBe('1234567.123456789') }) @@ -126,7 +126,7 @@ describe(parseValue, () => { value: '1,234.123456789123456789 WBTC', ...defaultParams, maxDecimals: 8, - }) + }), ).toBe('1234.12345678') expect( @@ -134,7 +134,7 @@ describe(parseValue, () => { value: '1,234.123456789123456789123456789 ETH', ...defaultParams, maxDecimals: 18, - }) + }), ).toBe('1234.123456789123456789') }) }) diff --git a/packages/wallet/src/components/input/AmountInput.tsx b/packages/wallet/src/components/input/AmountInput.tsx index d1a955ac0ac..31aa7b825c1 100644 --- a/packages/wallet/src/components/input/AmountInput.tsx +++ b/packages/wallet/src/components/input/AmountInput.tsx @@ -33,7 +33,7 @@ export function replaceSeparators({ if (groupingSeparator && groupingOverride != null) { outputParts = outputParts.map((part) => // eslint-disable-next-line security/detect-non-literal-regexp - part.replace(new RegExp(`\\${groupingSeparator}`, 'g'), groupingOverride) + part.replace(new RegExp(`\\${groupingSeparator}`, 'g'), groupingOverride), ) } return outputParts.join(decimalOverride) @@ -97,7 +97,7 @@ export const AmountInput = forwardRef(function _AmountIn fiatCurrencyInfo, ...rest }, - ref + ref, ) { const appFiatCurrencyInfo = useAppFiatCurrencyInfo() const targetFiatCurrencyInfo = fiatCurrencyInfo || appFiatCurrencyInfo @@ -123,7 +123,7 @@ export const AmountInput = forwardRef(function _AmountIn showSoftInputOnFocus, nativeKeyboardDecimalSeparator, maxDecimals, - }) + }), ) }, [ @@ -133,7 +133,7 @@ export const AmountInput = forwardRef(function _AmountIn nativeKeyboardDecimalSeparator, onChangeText, showSoftInputOnFocus, - ] + ], ) const formattedValue = replaceSeparators({ @@ -155,7 +155,7 @@ export const AmountInput = forwardRef(function _AmountIn ...rest, ...(adjustWidthToContent ? { width } : {}), }), - [ref, value, dimTextColor, formattedValue, handleChange, rest, width, adjustWidthToContent] + [ref, value, dimTextColor, formattedValue, handleChange, rest, width, adjustWidthToContent], ) useEffect(() => { const subscription = AppState.addEventListener('change', (nextAppState) => { @@ -199,10 +199,11 @@ export const AmountInput = forwardRef(function _AmountIn setWidth( Math.min( e.nativeEvent.layout.width, - typeof textInputProps.maxWidth === 'number' ? textInputProps.maxWidth : +Infinity - ) + typeof textInputProps.maxWidth === 'number' ? textInputProps.maxWidth : +Infinity, + ), ) - }> + } + > {value || textInputProps.placeholder} {textInputElement} @@ -213,8 +214,9 @@ export const AmountInput = forwardRef(function _AmountIn return textInputElement }) -const TextInputWithNativeKeyboard = forwardRef( - function _TextInputWithNativeKeyboard(props: TextInputProps, ref) { - return - } -) +const TextInputWithNativeKeyboard = forwardRef(function _TextInputWithNativeKeyboard( + props: TextInputProps, + ref, +) { + return +}) diff --git a/packages/wallet/src/components/input/MaxAmountButton.tsx b/packages/wallet/src/components/input/MaxAmountButton.tsx index c9da97d3411..ff010ad1c52 100644 --- a/packages/wallet/src/components/input/MaxAmountButton.tsx +++ b/packages/wallet/src/components/input/MaxAmountButton.tsx @@ -2,9 +2,10 @@ import { Currency, CurrencyAmount } from '@uniswap/sdk-core' import { useTranslation } from 'react-i18next' import { GestureResponderEvent, StyleProp, ViewStyle } from 'react-native' import { Text, TouchableArea } from 'ui/src' -import { ElementName } from 'uniswap/src/features/telemetry/constants' import Trace from 'uniswap/src/features/telemetry/Trace' -import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' +import { ElementName } from 'uniswap/src/features/telemetry/constants' +import { CurrencyField } from 'uniswap/src/features/transactions/transactionState/types' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { maxAmountSpend } from 'wallet/src/utils/balance' interface MaxAmountButtonProps { @@ -28,13 +29,12 @@ export function MaxAmountButton({ // Disable max button if max already set or when balance is not sufficient const disableMaxButton = - !maxInputAmount || - !maxInputAmount.greaterThan(0) || - currencyAmount?.toExact() === maxInputAmount.toExact() + !maxInputAmount || !maxInputAmount.greaterThan(0) || currencyAmount?.toExact() === maxInputAmount.toExact() const onPress = (event: GestureResponderEvent): void => { + event.stopPropagation() + if (!disableMaxButton) { - event.stopPropagation() onSetMax(maxInputAmount.toExact()) } } @@ -42,23 +42,20 @@ export function MaxAmountButton({ return ( + element={currencyField === CurrencyField.INPUT ? ElementName.SetMaxInput : ElementName.SetMaxOutput} + > - + testID={currencyField === CurrencyField.INPUT ? TestID.SetMaxInput : TestID.SetMaxOutput} + onPress={onPress} + > + {t('swap.button.max')} diff --git a/packages/wallet/src/components/input/RecipientInputPanel.tsx b/packages/wallet/src/components/input/RecipientInputPanel.tsx index 7a254b9422c..964954504f5 100644 --- a/packages/wallet/src/components/input/RecipientInputPanel.tsx +++ b/packages/wallet/src/components/input/RecipientInputPanel.tsx @@ -1,14 +1,14 @@ import { useTranslation } from 'react-i18next' import { Flex, Text, TouchableArea } from 'ui/src' import { RotatableChevron } from 'ui/src/components/icons' -import { ElementName } from 'uniswap/src/features/telemetry/constants' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' import { useAllTransactionsBetweenAddresses } from 'wallet/src/features/transactions/hooks/useAllTransactionsBetweenAddresses' import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' interface RecipientInputPanelProps { recipientAddress: string - onToggleShowRecipientSelector: () => void + onShowRecipientSelector: () => void } /** @@ -17,14 +17,10 @@ interface RecipientInputPanelProps { */ export function RecipientInputPanel({ recipientAddress, - onToggleShowRecipientSelector, + onShowRecipientSelector, }: RecipientInputPanelProps): JSX.Element { return ( - + diff --git a/apps/mobile/src/components/gradients/LandingBackground.mock.tsx b/packages/wallet/src/components/landing/LandingBackground.mock.tsx similarity index 100% rename from apps/mobile/src/components/gradients/LandingBackground.mock.tsx rename to packages/wallet/src/components/landing/LandingBackground.mock.tsx diff --git a/apps/mobile/src/components/gradients/LandingBackground.tsx b/packages/wallet/src/components/landing/LandingBackground.tsx similarity index 60% rename from apps/mobile/src/components/gradients/LandingBackground.tsx rename to packages/wallet/src/components/landing/LandingBackground.tsx index 504b542ae14..363e408c878 100644 --- a/apps/mobile/src/components/gradients/LandingBackground.tsx +++ b/packages/wallet/src/components/landing/LandingBackground.tsx @@ -1,5 +1,5 @@ -import { useFocusEffect } from '@react-navigation/core' -import React, { ReactElement, useCallback, useEffect, useState } from 'react' +import { EventConsumer, EventMapBase } from '@react-navigation/core' +import { ReactElement, useCallback, useEffect, useMemo, useState } from 'react' import { LayoutChangeEvent, Platform } from 'react-native' import Animated, { Easing, @@ -12,72 +12,38 @@ import Animated, { withSpring, withTiming, } from 'react-native-reanimated' -import { Circle, Defs, LinearGradient, Stop, Svg } from 'react-native-svg' -import { useAppStackNavigation } from 'src/app/navigation/types' -import { OpenseaElement } from 'src/features/unitags/ConfirmationElements' +import { Circle, Defs, Svg } from 'react-native-svg' +import { Flex, FlexProps, Image, isWeb, useIsDarkMode } from 'ui/src' +import { Jiggly } from 'ui/src/animations' +import { ONBOARDING_LANDING_DARK, ONBOARDING_LANDING_LIGHT, UNISWAP_LOGO } from 'ui/src/assets' +import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' +import { useDeviceDimensions } from 'ui/src/hooks/useDeviceDimensions' +import { imageSizes } from 'ui/src/theme' +import { isAndroid } from 'utilities/src/platform' +import { ONE_SECOND_MS } from 'utilities/src/time/time' +import { useTimeout } from 'utilities/src/time/timing' import { BuyElement, FroggyElement, HeartElement, + OpenseaElement, PolygonElement, ReceiveUSDCElement, SendElement, SwapElement, UniconElement, -} from 'src/screens/Onboarding/OnboardingElements' -import { Flex, Image, useIsDarkMode } from 'ui/src' -import { Jiggly } from 'ui/src/animations' -import { ONBOARDING_LANDING_DARK, ONBOARDING_LANDING_LIGHT, UNISWAP_APP_ICON } from 'ui/src/assets' -import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' -import { useDeviceDimensions } from 'ui/src/hooks/useDeviceDimensions' -import { imageSizes } from 'ui/src/theme' -import { isAndroid } from 'utilities/src/platform' -import { ONE_SECOND_MS } from 'utilities/src/time/time' -import { useTimeout } from 'utilities/src/time/timing' +} from 'wallet/src/components/landing/elements' +import { InnerCircleGradient, OuterCircleGradient } from 'wallet/src/components/landing/landingBackgroundGradients' import { Language } from 'wallet/src/features/language/constants' import { useCurrentLanguage } from 'wallet/src/features/language/hooks' -const INNER_CIRCLE_SIZE = 120 -const OUTER_CIRCLE_SIZE = 215 - -const INNER_PROPS = { radius: INNER_CIRCLE_SIZE, speed: -1, isInner: true } -const OUTER_PROPS = { radius: OUTER_CIRCLE_SIZE, speed: 1 } +const DEFAULT_INNER_CIRCLE_SIZE = 120 +const DEFAULT_OUTER_CIRCLE_SIZE = 215 const ROTATION_DURATION = 150000 const ACCELERATION_DURATION = ROTATION_DURATION / 50 -const ANIMATED_ELEMENTS_INNER = [ - { - element: , - coordinates: { deg: 3.2, ...INNER_PROPS }, - }, - { - element: , - coordinates: { deg: 2.0, ...INNER_PROPS }, - }, - { - element: , - coordinates: { deg: 0.3, ...INNER_PROPS }, - }, - { - element: , - coordinates: { deg: 5.5, ...INNER_PROPS }, - }, -] -const ANIMATED_ELEMENTS_OUTER = [ - { - element: , - coordinates: { radius: OUTER_CIRCLE_SIZE + 40, deg: 1, speed: 1, flatteningY: 0.85 }, - }, - { element: , coordinates: { deg: 2.2, ...OUTER_PROPS } }, - { element: , coordinates: { deg: 3.8, ...OUTER_PROPS } }, - { element: , coordinates: { deg: 4.8, ...OUTER_PROPS } }, - { - element: , - coordinates: { deg: 5.7, ...OUTER_PROPS }, - }, -] - +const LOGO_SIZE_WEB = 80 const LOGO_SCALE_DELAY = 0.5 * ONE_SECOND_MS const LOGO_SCALE_DURATION = 0.7 * ONE_SECOND_MS const ANIMATED_ELEMENTS_DELAY = LOGO_SCALE_DELAY + LOGO_SCALE_DURATION - 750 @@ -86,7 +52,15 @@ const OUTER_CIRCLE_SHOW_DELAY = 0.5 * ONE_SECOND_MS export const LANDING_ANIMATION_DURATION = ANIMATED_ELEMENTS_DELAY -const OnboardingAnimation = (): JSX.Element => { +const OnboardingAnimation = ({ + elementsStyle, + innerCircleSize, + outerCircleSize, +}: { + elementsStyle: FlexProps['style'] + innerCircleSize: number + outerCircleSize: number +}): JSX.Element => { const [boxWidth, setBoxWidth] = useState(0) const [showAnimatedElements, setShowAnimatedElements] = useState(false) @@ -95,10 +69,7 @@ const OnboardingAnimation = (): JSX.Element => { }, []) const uniswapLogoScale = useSharedValue(1.5) - const animatedStyle = useAnimatedStyle( - () => ({ transform: [{ scale: uniswapLogoScale.value }] }), - [uniswapLogoScale] - ) + const animatedStyle = useAnimatedStyle(() => ({ transform: [{ scale: uniswapLogoScale.value }] }), [uniswapLogoScale]) useEffect(() => { uniswapLogoScale.value = withDelay( @@ -106,7 +77,7 @@ const OnboardingAnimation = (): JSX.Element => { withTiming(1, { duration: LOGO_SCALE_DURATION, easing: Easing.elastic(1.1), - }) + }), ) }, [uniswapLogoScale]) @@ -115,15 +86,19 @@ const OnboardingAnimation = (): JSX.Element => { }, ANIMATED_ELEMENTS_DELAY) return ( - - {showAnimatedElements ? : null} + + {showAnimatedElements ? ( + + + + ) : null} @@ -133,7 +108,57 @@ const OnboardingAnimation = (): JSX.Element => { const INITIAL_ANIMATION_LENGTH = 0.1 -const AnimatedElements = ({ width }: { width: number }): JSX.Element | null => { +const AnimatedElements = ({ + width, + innerCircleSize, + outerCircleSize, +}: { + width: number + innerCircleSize: number + outerCircleSize: number +}): JSX.Element | null => { + const isDarkMode = useIsDarkMode() + + const animatedElementsInner = useMemo(() => { + const innerProps = { radius: innerCircleSize, speed: -1, isInner: true } + + return [ + { + element: , + coordinates: { deg: 3.2, ...innerProps }, + }, + { + element: , + coordinates: { deg: 2.0, ...innerProps }, + }, + { + element: , + coordinates: { deg: 0.3, ...innerProps }, + }, + { + element: , + coordinates: { deg: 5.5, ...innerProps }, + }, + ] + }, [innerCircleSize]) + + const animatedElementsOuter = useMemo(() => { + const outerProps = { radius: outerCircleSize, speed: 1 } + return [ + { + element: , + coordinates: { radius: outerCircleSize + 40, deg: 1, speed: 1, flatteningY: 0.85 }, + }, + { element: , coordinates: { deg: 2.2, ...outerProps } }, + { element: , coordinates: { deg: 3.8, ...outerProps } }, + { element: , coordinates: { deg: 4.8, ...outerProps } }, + { + element: , + coordinates: { deg: 5.7, ...outerProps }, + }, + ] + }, [outerCircleSize]) + const rotation = useSharedValue(0) const innerAnimation = useSharedValue(0) const outerAnimation = useSharedValue(0) @@ -151,69 +176,61 @@ const AnimatedElements = ({ width }: { width: number }): JSX.Element | null => { duration: ROTATION_DURATION, easing: Easing.linear, }), - -1 - ) - ) + -1, + ), + ), ) - innerAnimation.value = withDelay(INNER_CIRCLE_SHOW_DELAY, withSpring(1)) - outerAnimation.value = withDelay(OUTER_CIRCLE_SHOW_DELAY, withSpring(1)) + innerAnimation.value = withDelay(INNER_CIRCLE_SHOW_DELAY, withSpring(0.8)) + outerAnimation.value = withDelay(OUTER_CIRCLE_SHOW_DELAY, withSpring(0.8)) }, [innerAnimation, outerAnimation, rotation]) const innerCircleStyle = useAnimatedStyle(() => { return { opacity: innerAnimation.value, } - }) + }, [innerAnimation]) const outerCircleStyle = useAnimatedStyle(() => { return { opacity: outerAnimation.value, } - }) + }, [outerAnimation]) return ( - - - - - - + - - - - - - + - {ANIMATED_ELEMENTS_INNER.map(({ element, coordinates }, index) => ( + {animatedElementsInner.map(({ element, coordinates }, index) => ( { rotation={rotation} /> ))} - {ANIMATED_ELEMENTS_OUTER.map(({ element, coordinates }, index) => ( + {animatedElementsOuter.map(({ element, coordinates }, index) => ( @@ -286,18 +302,35 @@ const RotateElement = ({ ) } -export const LandingBackground = (): JSX.Element | null => { - const navigation = useAppStackNavigation() +export const LandingBackground = ({ + navigationEventConsumer, + elementsStyle, + innerCircleSize = DEFAULT_INNER_CIRCLE_SIZE, + outerCircleSize = DEFAULT_OUTER_CIRCLE_SIZE, +}: { + navigationEventConsumer?: EventConsumer + elementsStyle?: FlexProps['style'] + innerCircleSize?: number + outerCircleSize?: number +}): JSX.Element | null => { const [blurred, setBlurred] = useState(false) const [hideAnimation, setHideAnimation] = useState(false) const language = useCurrentLanguage() useEffect(() => { - return navigation.addListener('blur', () => { + return navigationEventConsumer?.addListener('blur', () => { // set this flag on blur (when navigating to another screen) setBlurred(true) }) - }, [navigation]) + }, [navigationEventConsumer]) + + useEffect(() => { + return navigationEventConsumer?.addListener('focus', () => { + // reset animation when focusing on this screen again + setBlurred(false) + setHideAnimation(false) + }) + }, [navigationEventConsumer]) // callback to turn off the animation (so that we can turn it back // on on focus) @@ -312,12 +345,6 @@ export const LandingBackground = (): JSX.Element | null => { // transition animation happens useTimeout(turnAnimationOff, 500) - // reset animation when focusing on this screen again - useFocusEffect(() => { - setBlurred(false) - setHideAnimation(false) - }) - if (hideAnimation) { // resets the animation to restart when the screen is mounted again (eg. going back) return null @@ -333,7 +360,13 @@ export const LandingBackground = (): JSX.Element | null => { return } - return + return ( + + ) } const OnboardingStaticImage = (): JSX.Element => { @@ -341,11 +374,7 @@ const OnboardingStaticImage = (): JSX.Element => { const { fullHeight, fullWidth } = useDeviceDimensions() return ( ) diff --git a/apps/mobile/src/screens/Onboarding/OnboardingElements/BuyElement.tsx b/packages/wallet/src/components/landing/elements/BuyElement.tsx similarity index 51% rename from apps/mobile/src/screens/Onboarding/OnboardingElements/BuyElement.tsx rename to packages/wallet/src/components/landing/elements/BuyElement.tsx index 3761ac74dcb..b3bc4a6ee47 100644 --- a/apps/mobile/src/screens/Onboarding/OnboardingElements/BuyElement.tsx +++ b/packages/wallet/src/components/landing/elements/BuyElement.tsx @@ -1,10 +1,12 @@ import { useTranslation } from 'react-i18next' -import { Flex, Text } from 'ui/src' +import { Flex, Text, useIsDarkMode } from 'ui/src' import { Buy } from 'ui/src/components/icons' import { colors, opacify } from 'ui/src/theme' export const BuyElement = (): JSX.Element => { const { t } = useTranslation() + const isDarkMode = useIsDarkMode() + const mainColor = isDarkMode ? '$orange500' : '$orange300' return ( { gap="$spacing4" px="$spacing12" py="$spacing8" - style={{ backgroundColor: opacify(20, colors.orange200) }} - transform={[{ rotateZ: '-1deg' }]}> - - + style={{ backgroundColor: opacify(isDarkMode ? 10 : 20, colors.orange200) }} + transform={[{ rotateZ: '-1deg' }]} + > + + {t('common.button.buy')} diff --git a/apps/mobile/src/screens/Onboarding/OnboardingElements/ENSElement.tsx b/packages/wallet/src/components/landing/elements/ENSElement.tsx similarity index 64% rename from apps/mobile/src/screens/Onboarding/OnboardingElements/ENSElement.tsx rename to packages/wallet/src/components/landing/elements/ENSElement.tsx index 145c3079922..bae86afdaa8 100644 --- a/apps/mobile/src/screens/Onboarding/OnboardingElements/ENSElement.tsx +++ b/packages/wallet/src/components/landing/elements/ENSElement.tsx @@ -8,13 +8,9 @@ export const ENSElement = (): JSX.Element => { borderRadius="$rounded12" p="$spacing12" style={{ backgroundColor: opacify(20, colors.blue300) }} - transform={[{ rotateZ: '8deg' }]}> - + transform={[{ rotateZ: '8deg' }]} + > + ) } diff --git a/apps/mobile/src/screens/Onboarding/OnboardingElements/EmojiElement.tsx b/packages/wallet/src/components/landing/elements/EmojiElement.tsx similarity index 90% rename from apps/mobile/src/screens/Onboarding/OnboardingElements/EmojiElement.tsx rename to packages/wallet/src/components/landing/elements/EmojiElement.tsx index 95933ab4fbc..de93a0776d1 100644 --- a/apps/mobile/src/screens/Onboarding/OnboardingElements/EmojiElement.tsx +++ b/packages/wallet/src/components/landing/elements/EmojiElement.tsx @@ -7,7 +7,8 @@ export const EmojiElement = ({ emoji }: { emoji: string }): JSX.Element => { borderRadius="$roundedFull" p="$spacing8" style={{ backgroundColor: opacify(20, colors.yellow200) }} - transform={[{ rotateZ: '5deg' }]}> + transform={[{ rotateZ: '5deg' }]} + > {emoji} diff --git a/apps/mobile/src/screens/Onboarding/OnboardingElements/FroggyElement.tsx b/packages/wallet/src/components/landing/elements/FroggyElement.tsx similarity index 66% rename from apps/mobile/src/screens/Onboarding/OnboardingElements/FroggyElement.tsx rename to packages/wallet/src/components/landing/elements/FroggyElement.tsx index a704aff2eaa..2c7148d28ed 100644 --- a/apps/mobile/src/screens/Onboarding/OnboardingElements/FroggyElement.tsx +++ b/packages/wallet/src/components/landing/elements/FroggyElement.tsx @@ -5,12 +5,7 @@ import { imageSizes } from 'ui/src/theme' export const FroggyElement = (): JSX.Element => { return ( - + ) } diff --git a/packages/wallet/src/components/landing/elements/HeartElement.tsx b/packages/wallet/src/components/landing/elements/HeartElement.tsx new file mode 100644 index 00000000000..e5a260cec0a --- /dev/null +++ b/packages/wallet/src/components/landing/elements/HeartElement.tsx @@ -0,0 +1,19 @@ +import { Flex, useIsDarkMode } from 'ui/src' +import { Heart } from 'ui/src/components/icons' +import { colors, iconSizes, opacify } from 'ui/src/theme' + +export const HeartElement = (): JSX.Element => { + const isDarkMode = useIsDarkMode() + + return ( + + + + ) +} diff --git a/apps/mobile/src/screens/Onboarding/OnboardingElements/OpenseaElement.tsx b/packages/wallet/src/components/landing/elements/OpenseaElement.tsx similarity index 50% rename from apps/mobile/src/screens/Onboarding/OnboardingElements/OpenseaElement.tsx rename to packages/wallet/src/components/landing/elements/OpenseaElement.tsx index 28d8d9b54d4..e000a717df1 100644 --- a/apps/mobile/src/screens/Onboarding/OnboardingElements/OpenseaElement.tsx +++ b/packages/wallet/src/components/landing/elements/OpenseaElement.tsx @@ -4,13 +4,8 @@ import { imageSizes } from 'ui/src/theme' export const OpenseaElement = (): JSX.Element => { return ( - - + + ) } diff --git a/apps/mobile/src/screens/Onboarding/OnboardingElements/PolygonElement.tsx b/packages/wallet/src/components/landing/elements/PolygonElement.tsx similarity index 55% rename from apps/mobile/src/screens/Onboarding/OnboardingElements/PolygonElement.tsx rename to packages/wallet/src/components/landing/elements/PolygonElement.tsx index 0c4f021b222..5ee72bd7697 100644 --- a/apps/mobile/src/screens/Onboarding/OnboardingElements/PolygonElement.tsx +++ b/packages/wallet/src/components/landing/elements/PolygonElement.tsx @@ -1,14 +1,18 @@ -import { Flex } from 'ui/src' +import { Flex, useIsDarkMode } from 'ui/src' import { PolygonPurple } from 'ui/src/components/logos' import { colors, imageSizes, opacify } from 'ui/src/theme' export const PolygonElement = (): JSX.Element => { + const isDarkMode = useIsDarkMode() + return ( + style={{ backgroundColor: opacify(isDarkMode ? 10 : 20, colors.violet300) }} + transform={[{ rotateZ: '-20deg' }]} + > ) diff --git a/apps/mobile/src/screens/Onboarding/OnboardingElements/ReceiveUSDCElement.tsx b/packages/wallet/src/components/landing/elements/ReceiveUSDCElement.tsx similarity index 62% rename from apps/mobile/src/screens/Onboarding/OnboardingElements/ReceiveUSDCElement.tsx rename to packages/wallet/src/components/landing/elements/ReceiveUSDCElement.tsx index 18ea6b39973..b1b2f181cc6 100644 --- a/apps/mobile/src/screens/Onboarding/OnboardingElements/ReceiveUSDCElement.tsx +++ b/packages/wallet/src/components/landing/elements/ReceiveUSDCElement.tsx @@ -1,27 +1,26 @@ -import { Flex, Image, Text } from 'ui/src' +import { Flex, Image, Text, useIsDarkMode } from 'ui/src' import { USDC_LOGO } from 'ui/src/assets' import { colors, imageSizes, opacify } from 'ui/src/theme' export const ReceiveUSDCElement = (): JSX.Element => { + const isDarkMode = useIsDarkMode() + return ( + transform={[{ rotateZ: '-1deg' }]} + > +100 - + ) } diff --git a/packages/wallet/src/components/landing/elements/SendElement.tsx b/packages/wallet/src/components/landing/elements/SendElement.tsx new file mode 100644 index 00000000000..1901fe0dbcd --- /dev/null +++ b/packages/wallet/src/components/landing/elements/SendElement.tsx @@ -0,0 +1,19 @@ +import { Flex, useIsDarkMode } from 'ui/src' +import { SendAction } from 'ui/src/components/icons' +import { colors, iconSizes, opacify } from 'ui/src/theme' + +export const SendElement = (): JSX.Element => { + const isDarkMode = useIsDarkMode() + + return ( + + + + ) +} diff --git a/apps/mobile/src/screens/Onboarding/OnboardingElements/SwapElement.tsx b/packages/wallet/src/components/landing/elements/SwapElement.tsx similarity index 50% rename from apps/mobile/src/screens/Onboarding/OnboardingElements/SwapElement.tsx rename to packages/wallet/src/components/landing/elements/SwapElement.tsx index 36c925d02b0..c678b335c03 100644 --- a/apps/mobile/src/screens/Onboarding/OnboardingElements/SwapElement.tsx +++ b/packages/wallet/src/components/landing/elements/SwapElement.tsx @@ -1,31 +1,27 @@ -import { Flex, Image, Text, useSporeColors } from 'ui/src' +import { Flex, Image, Text, useIsDarkMode, useSporeColors } from 'ui/src' import { DAI_LOGO, ETH_LOGO } from 'ui/src/assets' import { RightArrow } from 'ui/src/components/icons' import { colors, iconSizes, imageSizes, opacify } from 'ui/src/theme' export const SwapElement = (): JSX.Element => { const sporeColors = useSporeColors() + const isDarkMode = useIsDarkMode() + + const componentOpacity = isDarkMode ? 0.8 : 1 + const backgroundColorOpacity = isDarkMode ? 15 : 20 + return ( - + - + style={{ backgroundColor: opacify(backgroundColorOpacity, colors.blue200) }} + > + ETH @@ -36,14 +32,11 @@ export const SwapElement = (): JSX.Element => { row borderRadius="$roundedFull" gap="$spacing8" + opacity={componentOpacity} p="$spacing8" - style={{ backgroundColor: opacify(20, colors.yellow100) }}> - + style={{ backgroundColor: opacify(backgroundColorOpacity, colors.yellow100) }} + > + DAI diff --git a/apps/mobile/src/screens/Onboarding/OnboardingElements/TextElement.tsx b/packages/wallet/src/components/landing/elements/TextElement.tsx similarity index 100% rename from apps/mobile/src/screens/Onboarding/OnboardingElements/TextElement.tsx rename to packages/wallet/src/components/landing/elements/TextElement.tsx diff --git a/apps/mobile/src/screens/Onboarding/OnboardingElements/UniconElement.tsx b/packages/wallet/src/components/landing/elements/UniconElement.tsx similarity index 57% rename from apps/mobile/src/screens/Onboarding/OnboardingElements/UniconElement.tsx rename to packages/wallet/src/components/landing/elements/UniconElement.tsx index 0d749a825a1..36b948f4960 100644 --- a/apps/mobile/src/screens/Onboarding/OnboardingElements/UniconElement.tsx +++ b/packages/wallet/src/components/landing/elements/UniconElement.tsx @@ -1,14 +1,18 @@ -import { Flex } from 'ui/src' +import { Flex, useIsDarkMode } from 'ui/src' import { OnboardingUnicon } from 'ui/src/components/icons' import { colors, iconSizes, opacify } from 'ui/src/theme' export const UniconElement = (): JSX.Element => { + const isDarkMode = useIsDarkMode() + return ( + style={{ backgroundColor: opacify(isDarkMode ? 10 : 20, colors.violet200) }} + transform={[{ rotateZ: '-4deg' }]} + > ) diff --git a/apps/mobile/src/screens/Onboarding/OnboardingElements/index.tsx b/packages/wallet/src/components/landing/elements/index.tsx similarity index 100% rename from apps/mobile/src/screens/Onboarding/OnboardingElements/index.tsx rename to packages/wallet/src/components/landing/elements/index.tsx diff --git a/packages/wallet/src/components/landing/landingBackgroundGradients.tsx b/packages/wallet/src/components/landing/landingBackgroundGradients.tsx new file mode 100644 index 00000000000..56b7a24261e --- /dev/null +++ b/packages/wallet/src/components/landing/landingBackgroundGradients.tsx @@ -0,0 +1,42 @@ +import { ReactElement } from 'react' +import { LinearGradient, Stop } from 'react-native-svg' +import { useIsDarkMode } from 'ui/src' + +// Exported from figma +export function InnerCircleGradient({ id }: { id: string }): ReactElement { + const isDarkMode = useIsDarkMode() + + return isDarkMode ? ( + + + + + + ) : ( + + + + + + + ) +} + +export function OuterCircleGradient({ id }: { id: string }): ReactElement { + const isDarkMode = useIsDarkMode() + + return isDarkMode ? ( + + + + + + ) : ( + + + + + + + ) +} diff --git a/packages/wallet/src/components/legacy/CurrencyInputPanelLegacy.tsx b/packages/wallet/src/components/legacy/CurrencyInputPanelLegacy.tsx index defe9f67a9f..82d5d963dcc 100644 --- a/packages/wallet/src/components/legacy/CurrencyInputPanelLegacy.tsx +++ b/packages/wallet/src/components/legacy/CurrencyInputPanelLegacy.tsx @@ -1,16 +1,12 @@ import { Currency, CurrencyAmount } from '@uniswap/sdk-core' import { memo, useCallback, useEffect, useMemo, useRef } from 'react' import { useTranslation } from 'react-i18next' -import { - NativeSyntheticEvent, - TextInput, - TextInputProps, - TextInputSelectionChangeEventData, -} from 'react-native' +import { NativeSyntheticEvent, TextInput, TextInputProps, TextInputSelectionChangeEventData } from 'react-native' import { Flex, FlexProps, SpaceTokens, Text, TouchableArea } from 'ui/src' import { fonts } from 'ui/src/theme' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' -import { ElementName } from 'uniswap/src/features/telemetry/constants' +import { CurrencyField } from 'uniswap/src/features/transactions/transactionState/types' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { NumberType } from 'utilities/src/format/types' import { SelectTokenButton } from 'wallet/src/components/TokenSelector/SelectTokenButton' import { AmountInput } from 'wallet/src/components/input/AmountInput' @@ -18,7 +14,6 @@ import { MaxAmountButton } from 'wallet/src/components/input/MaxAmountButton' import { useAppFiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' import { Warning, WarningLabel } from 'wallet/src/features/transactions/WarningModal/types' -import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' import { useDynamicFontSizing } from 'wallet/src/utils/useDynamicFontSizing' type CurrentInputPanelProps = { @@ -61,7 +56,7 @@ interface DynamicSwapPanelPaddingValues { const getSwapPanelPaddingValues = ( isOutputBox: boolean, - hasCurrencyValue: boolean + hasCurrencyValue: boolean, ): { outerPadding: DynamicSwapPanelPaddingValues; innerPadding: DynamicSwapPanelPaddingValues } => { const outerPadding: DynamicSwapPanelPaddingValues = hasCurrencyValue ? { @@ -119,16 +114,11 @@ export function _CurrencyInputPanel(props: CurrentInputPanelProps): JSX.Element const inputRef = useRef(null) const { convertFiatAmountFormatted, formatCurrencyAmount } = useLocalizationContext() - const insufficientBalanceWarning = warnings.find( - (warning) => warning.type === WarningLabel.InsufficientFunds - ) + const insufficientBalanceWarning = warnings.find((warning) => warning.type === WarningLabel.InsufficientFunds) const showInsufficientBalanceWarning = insufficientBalanceWarning && !isOutput - const formattedFiatValue = convertFiatAmountFormatted( - usdValue?.toExact(), - NumberType.FiatTokenQuantity - ) + const formattedFiatValue = convertFiatAmountFormatted(usdValue?.toExact(), NumberType.FiatTokenQuantity) const formattedCurrencyAmount = currencyAmount ? formatCurrencyAmount({ value: currencyAmount, type: NumberType.TokenTx }) : '' @@ -148,7 +138,7 @@ export function _CurrencyInputPanel(props: CurrentInputPanelProps): JSX.Element const { onLayout, fontSize, onSetFontSize } = useDynamicFontSizing( MAX_CHAR_PIXEL_WIDTH, MAX_INPUT_FONT_SIZE, - MIN_INPUT_FONT_SIZE + MIN_INPUT_FONT_SIZE, ) // Handle native numpad keyboard input @@ -157,7 +147,7 @@ export function _CurrencyInputPanel(props: CurrentInputPanelProps): JSX.Element onSetFontSize(newAmount) onSetExactAmount(newAmount) }, - [onSetFontSize, onSetExactAmount] + [onSetFontSize, onSetExactAmount], ) // This is needed to ensure that the text resizes when modified from outside the component (e.g. custom numpad) @@ -176,7 +166,7 @@ export function _CurrencyInputPanel(props: CurrentInputPanelProps): JSX.Element onSetMax(amount) onChangeText(amount) }, - [onChangeText, onSetMax] + [onChangeText, onSetMax], ) const onSelectionChange = useCallback( @@ -184,14 +174,13 @@ export function _CurrencyInputPanel(props: CurrentInputPanelProps): JSX.Element nativeEvent: { selection: { start, end }, }, - }: NativeSyntheticEvent) => - selectionChange && selectionChange(start, end), - [selectionChange] + }: NativeSyntheticEvent) => selectionChange && selectionChange(start, end), + [selectionChange], ) const paddingStyles = useMemo( () => getSwapPanelPaddingValues(isOutput, Boolean(currencyInfo)), - [isOutput, currencyInfo] + [isOutput, currencyInfo], ) const handleToggleFiatInput = useCallback(() => { @@ -212,7 +201,8 @@ export function _CurrencyInputPanel(props: CurrentInputPanelProps): JSX.Element alignItems="center" justifyContent={!currencyInfo ? 'center' : 'space-between'} pb={innerPaddingBottom} - pt={innerPaddingTop}> + pt={innerPaddingTop} + > {isFiatInput && ( + mr="$spacing2" + > {fiatCurrencySymbol} )} {currencyInfo && ( - + )} - + @@ -278,9 +266,7 @@ export function _CurrencyInputPanel(props: CurrentInputPanelProps): JSX.Element - + {t('swap.form.balance')}:{' '} {formatCurrencyAmount({ value: currencyBalance, diff --git a/packages/wallet/src/components/legacy/DecimalPadLegacy.tsx b/packages/wallet/src/components/legacy/DecimalPadLegacy.tsx index 7a23c7d034e..93d5bb9db80 100644 --- a/packages/wallet/src/components/legacy/DecimalPadLegacy.tsx +++ b/packages/wallet/src/components/legacy/DecimalPadLegacy.tsx @@ -10,20 +10,7 @@ enum KeyAction { Delete = 'delete', } -export type KeyLabel = - | '1' - | '2' - | '3' - | '4' - | '5' - | '6' - | '7' - | '8' - | '9' - | '.' - | ',' - | '0' - | 'backspace' +export type KeyLabel = '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '.' | ',' | '0' | 'backspace' type KeyProps = { action: KeyAction @@ -122,7 +109,7 @@ export function _DecimalPad({ setValue={setValue} value={value} /> - ) + ), )} ) @@ -159,8 +146,7 @@ function KeyButton({ // should only be deleting/inserting at position 2 of "5.13" // except in the case where start === 0 then also just treat it as start of the non-prefixed string (to avoid -1 index) const prefixLength = hasCurrencyPrefix ? 1 : 0 - const start = - selection && selection.start > 0 && hasCurrencyPrefix ? selection.start - 1 : selection?.start + const start = selection && selection.start > 0 && hasCurrencyPrefix ? selection.start - 1 : selection?.start const end = selection?.end && hasCurrencyPrefix ? selection.end - 1 : selection?.end // TODO(MOB-140): in USD mode, prevent user from typing in more than 2 decimals @@ -224,7 +210,8 @@ function KeyButton({ testID={'decimal-pad-' + label} width={index % 3 === 1 ? '50%' : '25%'} onLongPress={onLongPress} - onPress={onPress}> + onPress={onPress} + > {label === 'backspace' ? ( I18nManager.isRTL ? ( diff --git a/packages/wallet/src/components/modals/WarningModal/WarningInfo.tsx b/packages/wallet/src/components/modals/WarningModal/WarningInfo.tsx index 49808191596..1ad8dfd3794 100644 --- a/packages/wallet/src/components/modals/WarningModal/WarningInfo.tsx +++ b/packages/wallet/src/components/modals/WarningModal/WarningInfo.tsx @@ -1,10 +1,7 @@ import { PropsWithChildren, ReactNode, useState } from 'react' import { Flex, TouchableArea, isWeb } from 'ui/src' import { InfoCircle } from 'ui/src/components/icons' -import { - WarningModal, - WarningModalProps, -} from 'wallet/src/components/modals/WarningModal/WarningModal' +import { WarningModal, WarningModalProps } from 'wallet/src/components/modals/WarningModal/WarningModal' import { WarningTooltip } from 'wallet/src/components/modals/WarningModal/WarningTooltip.web' import { WarningTooltipProps } from 'wallet/src/components/modals/WarningModal/WarningTooltipProps' @@ -31,11 +28,7 @@ export function WarningInfo({ if (isWeb) { return ( - + {children} ) diff --git a/packages/wallet/src/components/modals/WarningModal/WarningModal.tsx b/packages/wallet/src/components/modals/WarningModal/WarningModal.tsx index 1b26f53a88a..bce50177bc0 100644 --- a/packages/wallet/src/components/modals/WarningModal/WarningModal.tsx +++ b/packages/wallet/src/components/modals/WarningModal/WarningModal.tsx @@ -4,7 +4,8 @@ import { Button, Flex, Text, useSporeColors } from 'ui/src' import { AlertTriangle } from 'ui/src/components/icons' import { opacify } from 'ui/src/theme' import { BottomSheetModal } from 'uniswap/src/components/modals/BottomSheetModal' -import { ElementName, ModalNameType } from 'uniswap/src/features/telemetry/constants' +import { ModalNameType } from 'uniswap/src/features/telemetry/constants' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { isWeb } from 'utilities/src/platform' import { WarningColor, WarningSeverity } from 'wallet/src/features/transactions/WarningModal/types' @@ -57,14 +58,16 @@ export function WarningModal({ isDismissible={isDismissible} maxWidth={maxWidth} name={modalName} - onClose={onClose}> + onClose={onClose} + > + px={isWeb ? '$none' : '$spacing24'} + > {!hideIcon && ( + } + > {icon ?? } )} @@ -91,24 +94,14 @@ export function WarningModal({ )} {children} - + {closeText && ( )} {confirmText && ( - )} diff --git a/packages/wallet/src/components/modals/WarningModal/WarningTooltip.tsx b/packages/wallet/src/components/modals/WarningModal/WarningTooltip.tsx index ffaec260afb..e4c1235f4e7 100644 --- a/packages/wallet/src/components/modals/WarningModal/WarningTooltip.tsx +++ b/packages/wallet/src/components/modals/WarningModal/WarningTooltip.tsx @@ -3,7 +3,5 @@ import { NotImplementedError } from 'utilities/src/errors' import { WarningTooltipProps } from 'wallet/src/components/modals/WarningModal/WarningTooltipProps' export function WarningTooltip(_props: PropsWithChildren): JSX.Element { - throw new NotImplementedError( - 'current tooltip implementation does not work properly for native mobile' - ) + throw new NotImplementedError('current tooltip implementation does not work properly for native mobile') } diff --git a/packages/wallet/src/components/modals/WarningModal/WarningTooltip.web.tsx b/packages/wallet/src/components/modals/WarningModal/WarningTooltip.web.tsx index a39e2eb77c5..d377c39ebf0 100644 --- a/packages/wallet/src/components/modals/WarningModal/WarningTooltip.web.tsx +++ b/packages/wallet/src/components/modals/WarningModal/WarningTooltip.web.tsx @@ -18,10 +18,7 @@ export function WarningTooltip({ return ( {triggerPlacement === 'end' && children} - + {trigger} diff --git a/packages/wallet/src/components/network/NetworkFee.test.tsx b/packages/wallet/src/components/network/NetworkFee.test.tsx index b8c96d3fec9..84b4dea95eb 100644 --- a/packages/wallet/src/components/network/NetworkFee.test.tsx +++ b/packages/wallet/src/components/network/NetworkFee.test.tsx @@ -16,9 +16,7 @@ jest.mock('wallet/src/features/transactions/swap/hooks/useGasFeeHighRelativeToVa describe(NetworkFee, () => { it('renders a NetworkFee normally', () => { - const tree = render( - - ) + const tree = render() expect(tree).toMatchSnapshot() }) @@ -28,9 +26,7 @@ describe(NetworkFee, () => { }) it('renders a NetworkFee in an error state', () => { - const tree = render( - - ) + const tree = render() expect(tree).toMatchSnapshot() }) }) diff --git a/packages/wallet/src/components/network/NetworkFee.tsx b/packages/wallet/src/components/network/NetworkFee.tsx index 63d72401ddc..41dda44c8f7 100644 --- a/packages/wallet/src/components/network/NetworkFee.tsx +++ b/packages/wallet/src/components/network/NetworkFee.tsx @@ -1,6 +1,7 @@ import { Currency, CurrencyAmount } from '@uniswap/sdk-core' import { useTranslation } from 'react-i18next' -import { Flex, Text } from 'ui/src' +import { Flex, Text, UniswapXText } from 'ui/src' +import { UniswapX } from 'ui/src/components/icons' import { iconSizes } from 'ui/src/theme' import { NetworkLogo } from 'uniswap/src/components/CurrencyLogo/NetworkLogo' import { WalletChainId } from 'uniswap/src/types/chains' @@ -14,20 +15,22 @@ import { NetworkFeeWarning } from 'wallet/src/features/transactions/swap/modals/ export function NetworkFee({ chainId, gasFee, + preUniswapXGasFeeUSD, transactionUSDValue, }: { chainId: WalletChainId gasFee: GasFeeResult + preUniswapXGasFeeUSD?: number transactionUSDValue?: Maybe> }): JSX.Element { const { t } = useTranslation() const { convertFiatAmountFormatted } = useLocalizationContext() const gasFeeUSD = useUSDValue(chainId, gasFee.value ?? undefined) - const gasFeeFormatted = convertFiatAmountFormatted(gasFeeUSD, NumberType.FiatTokenPrice) + const gasFeeFormatted = convertFiatAmountFormatted(gasFeeUSD, NumberType.FiatGasPrice) + const preSavingsGasFeeFormatted = convertFiatAmountFormatted(preUniswapXGasFeeUSD, NumberType.FiatGasPrice) const gasFeeHighRelativeToValue = useGasFeeHighRelativeToValue(gasFeeUSD, transactionUSDValue) - const isLoading = gasFee.loading return ( @@ -37,18 +40,21 @@ export function NetworkFee({ {t('transaction.networkCost.label')} - - + + {(!preUniswapXGasFeeUSD || gasFee.error) && ( + + )} {gasFee.error ? ( {t('common.text.notAvailable')} + ) : preUniswapXGasFeeUSD ? ( + ) : ( + color={isLoading ? '$neutral3' : gasFeeHighRelativeToValue ? '$statusCritical' : '$neutral1'} + variant="body3" + > {gasFeeFormatted} )} @@ -56,3 +62,17 @@ export function NetworkFee({ ) } + +export function UniswapXFee({ gasFee, preSavingsGasFee }: { gasFee: string; preSavingsGasFee?: string }): JSX.Element { + return ( + + + {gasFee} + {preSavingsGasFee && ( + + {preSavingsGasFee} + + )} + + ) +} diff --git a/packages/wallet/src/components/nfts/NFTHiddenRow.tsx b/packages/wallet/src/components/nfts/NFTHiddenRow.tsx index 41974c8d0dd..3ce2fc484b0 100644 --- a/packages/wallet/src/components/nfts/NFTHiddenRow.tsx +++ b/packages/wallet/src/components/nfts/NFTHiddenRow.tsx @@ -10,14 +10,7 @@ export function HiddenNftsRowLeft({ numHidden }: { numHidden: number }): JSX.Ele const { t } = useTranslation() return ( - + {t('tokens.nfts.hidden.label', { numHidden })} @@ -25,13 +18,7 @@ export function HiddenNftsRowLeft({ numHidden }: { numHidden: number }): JSX.Ele ) } -export function HiddenNftsRowRight({ - isExpanded, - onPress, -}: { - isExpanded: boolean - onPress: () => void -}): JSX.Element { +export function HiddenNftsRowRight({ isExpanded, onPress }: { isExpanded: boolean; onPress: () => void }): JSX.Element { const { t } = useTranslation() const chevronRotate = useSharedValue(isExpanded ? 180 : 0) @@ -51,11 +38,7 @@ export function HiddenNftsRowRight({ }, [chevronRotate, onPress]) return ( - + + py="$spacing4" + > {isExpanded ? t('common.button.hide') : t('common.button.show')} - + diff --git a/packages/wallet/src/components/nfts/NFTTransfer.tsx b/packages/wallet/src/components/nfts/NFTTransfer.tsx index 9af8464a4ea..079a5eadef9 100644 --- a/packages/wallet/src/components/nfts/NFTTransfer.tsx +++ b/packages/wallet/src/components/nfts/NFTTransfer.tsx @@ -3,24 +3,14 @@ import { iconSizes } from 'ui/src/theme' import { NFTViewer } from 'wallet/src/features/images/NFTViewer' import { GQLNftAsset } from 'wallet/src/features/nfts/hooks' -export function NFTTransfer({ - asset, - nftSize, -}: { - asset: GQLNftAsset - nftSize?: number -}): JSX.Element { +export function NFTTransfer({ asset, nftSize }: { asset: GQLNftAsset; nftSize?: number }): JSX.Element { return ( - + {asset?.name} diff --git a/packages/wallet/src/components/nfts/NftsList.tsx b/packages/wallet/src/components/nfts/NftsList.tsx index 2028b93ff3e..f56453e76de 100644 --- a/packages/wallet/src/components/nfts/NftsList.tsx +++ b/packages/wallet/src/components/nfts/NftsList.tsx @@ -5,10 +5,7 @@ import { useTranslation } from 'react-i18next' import { ListRenderItemInfo, StyleProp, ViewStyle } from 'react-native' import { SharedValue } from 'react-native-reanimated' import { Flex, Loader } from 'ui/src' -import { - AnimatedBottomSheetFlashList, - AnimatedFlashList, -} from 'ui/src/components/AnimatedFlashList/AnimatedFlashList' +import { AnimatedBottomSheetFlashList, AnimatedFlashList } from 'ui/src/components/AnimatedFlashList/AnimatedFlashList' import { NoNfts } from 'ui/src/components/icons' import { useDeviceDimensions } from 'ui/src/hooks/useDeviceDimensions' import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard' @@ -44,7 +41,7 @@ type NftsListProps = Omit< footerHeight?: SharedValue isExternalProfile?: boolean renderedInModal?: boolean - renderNFTItem: (item: NFTItem) => JSX.Element + renderNFTItem: (item: NFTItem, index: number) => JSX.Element onPressEmptyState?: () => void loadingStateStyle?: StyleProp errorStateStyle?: StyleProp @@ -74,7 +71,7 @@ export const NftsList = forwardRef, NftsListProps>(function _ onRefresh, ...rest }, - ref + ref, ) { const { t } = useTranslation() const { fullHeight } = useDeviceDimensions() @@ -127,9 +124,9 @@ export const NftsList = forwardRef, NftsListProps>(function _ }, [hiddenNftsExpanded, numHidden]) const renderItem = useCallback( - ({ item }: ListRenderItemInfo) => { + ({ item, index }: ListRenderItemInfo) => { if (typeof item !== 'string') { - return renderNFTItem(item) + return renderNFTItem(item, index) } switch (item) { case LOADING_ITEM: @@ -145,7 +142,7 @@ export const NftsList = forwardRef, NftsListProps>(function _ return null } }, - [hiddenNftsExpanded, numHidden, onHiddenRowPressed, renderNFTItem] + [hiddenNftsExpanded, numHidden, onHiddenRowPressed, renderNFTItem], ) const onRetry = useCallback(() => refetch(), [refetch]) @@ -174,11 +171,7 @@ export const NftsList = forwardRef, NftsListProps>(function _ // empty view , NftsListProps>(function _ // we add a footer to cover any possible space, so user can scroll the top menu all the way to the top ListFooterComponent={ <> - {nfts.length > 0 && networkStatus === NetworkStatus.fetchMore && ( - - )} + {nfts.length > 0 && networkStatus === NetworkStatus.fetchMore && } {ListFooterComponent} } diff --git a/packages/wallet/src/components/settings/AnalyticsToggleLineSwitch.tsx b/packages/wallet/src/components/settings/AnalyticsToggleLineSwitch.tsx index db3ac66f91e..70e37ae29e5 100644 --- a/packages/wallet/src/components/settings/AnalyticsToggleLineSwitch.tsx +++ b/packages/wallet/src/components/settings/AnalyticsToggleLineSwitch.tsx @@ -1,13 +1,14 @@ import { useTranslation } from 'react-i18next' +import { useDispatch } from 'react-redux' import { Flex, Text, isWeb } from 'ui/src' import { Switch, WebSwitch } from 'wallet/src/components/buttons/Switch' import { selectAllowAnalytics } from 'wallet/src/features/telemetry/selectors' import { setAllowAnalytics } from 'wallet/src/features/telemetry/slice' -import { useAppDispatch, useAppSelector } from 'wallet/src/state' +import { useAppSelector } from 'wallet/src/state' export function AnalyticsToggleLineSwitch(): JSX.Element { const { t } = useTranslation() - const dispatch = useAppDispatch() + const dispatch = useDispatch() const analyticsAllowed = useAppSelector(selectAllowAnalytics) const onChangeAllowAnalytics = (enabled: boolean): void => { diff --git a/packages/wallet/src/components/settings/language/SettingsLanguageModal.native.tsx b/packages/wallet/src/components/settings/language/SettingsLanguageModal.native.tsx index 795dba09dcd..f4e969aa888 100644 --- a/packages/wallet/src/components/settings/language/SettingsLanguageModal.native.tsx +++ b/packages/wallet/src/components/settings/language/SettingsLanguageModal.native.tsx @@ -3,10 +3,11 @@ import { Linking } from 'react-native' import { Button, Flex, Text, useSporeColors } from 'ui/src' import { Language } from 'ui/src/components/icons' import { BottomSheetModal } from 'uniswap/src/components/modals/BottomSheetModal' -import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants' +import { ModalName } from 'uniswap/src/features/telemetry/constants' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' +import { opacify } from 'uniswap/src/utils/colors' import { isAndroid } from 'utilities/src/platform' import { SettingsLanguageModalProps } from 'wallet/src/components/settings/language/SettingsLanguageModalProps' -import { opacify } from 'wallet/src/utils/colors' const openLanguageSettings = async (): Promise => { if (isAndroid) { @@ -23,10 +24,7 @@ export function SettingsLanguageModal({ onClose }: SettingsLanguageModalProps): return ( - + @@ -39,10 +37,7 @@ export function SettingsLanguageModal({ onClose }: SettingsLanguageModalProps): {t('settings.setting.language.description.mobile')} - diff --git a/packages/wallet/src/components/settings/language/SettingsLanguageModal.web.tsx b/packages/wallet/src/components/settings/language/SettingsLanguageModal.web.tsx index cd803c1864e..1e051be068c 100644 --- a/packages/wallet/src/components/settings/language/SettingsLanguageModal.web.tsx +++ b/packages/wallet/src/components/settings/language/SettingsLanguageModal.web.tsx @@ -3,8 +3,8 @@ import { Button, Flex, Text, useSporeColors } from 'ui/src' import { Language } from 'ui/src/components/icons' import { BottomSheetModal } from 'uniswap/src/components/modals/BottomSheetModal' import { ModalName } from 'uniswap/src/features/telemetry/constants' +import { opacify } from 'uniswap/src/utils/colors' import { SettingsLanguageModalProps } from 'wallet/src/components/settings/language/SettingsLanguageModalProps' -import { opacify } from 'wallet/src/utils/colors' export function SettingsLanguageModal({ onClose }: SettingsLanguageModalProps): JSX.Element { const colors = useSporeColors() @@ -17,7 +17,8 @@ export function SettingsLanguageModal({ onClose }: SettingsLanguageModalProps): + style={{ backgroundColor: opacify(10, colors.DEP_blue300.val) }} + > diff --git a/packages/wallet/src/components/text/RelativeChange.test.tsx b/packages/wallet/src/components/text/RelativeChange.test.tsx index c691005922d..f6405bb87e7 100644 --- a/packages/wallet/src/components/text/RelativeChange.test.tsx +++ b/packages/wallet/src/components/text/RelativeChange.test.tsx @@ -38,7 +38,7 @@ it('renders a relative change', () => { const tree = renderer.create( - + , ) expect(tree).toMatchSnapshot() }) @@ -47,7 +47,7 @@ it('renders placeholders without a change', () => { const tree = renderer.create( - + , ) expect(tree).toMatchSnapshot() }) @@ -56,7 +56,7 @@ it('renders placeholders with absolute change', () => { const tree = renderer.create( - + , ) expect(tree).toMatchSnapshot() }) diff --git a/packages/wallet/src/components/text/RelativeChange.tsx b/packages/wallet/src/components/text/RelativeChange.tsx index 9082e9b82cd..1b365957b26 100644 --- a/packages/wallet/src/components/text/RelativeChange.tsx +++ b/packages/wallet/src/components/text/RelativeChange.tsx @@ -1,6 +1,7 @@ import { ColorTokens, Flex, Text } from 'ui/src' import { Caret } from 'ui/src/components/icons' import { IconSizeTokens, fonts } from 'ui/src/theme' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { NumberType } from 'utilities/src/format/types' import { useAppFiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' @@ -50,18 +51,17 @@ export function RelativeChange(props: RelativeChangeProps): JSX.Element { alignItems="center" gap="$spacing2" justifyContent={alignRight ? 'flex-end' : 'flex-start'} - testID="relative-change"> - {change !== undefined && ( - - )} + testID="relative-change" + > + {change !== undefined && } + testID={TestID.PortfolioRelativeChange} + variant={variant} + > {absoluteChange ? `${formattedAbsChange} (${formattedChange})` : formattedChange} diff --git a/packages/wallet/src/components/text/__snapshots__/RelativeChange.test.tsx.snap b/packages/wallet/src/components/text/__snapshots__/RelativeChange.test.tsx.snap index 6963f9ad0a0..7c064fcca64 100644 --- a/packages/wallet/src/components/text/__snapshots__/RelativeChange.test.tsx.snap +++ b/packages/wallet/src/components/text/__snapshots__/RelativeChange.test.tsx.snap @@ -93,6 +93,7 @@ exports[`renders a relative change 1`] = ` } } suppressHighlighting={true} + testID="portfolio-relative-change" > 12% @@ -193,6 +194,7 @@ exports[`renders placeholders with absolute change 1`] = ` } } suppressHighlighting={true} + testID="portfolio-relative-change" > $100.00 (12%) @@ -231,6 +233,7 @@ exports[`renders placeholders without a change 1`] = ` } } suppressHighlighting={true} + testID="portfolio-relative-change" > - diff --git a/packages/wallet/src/constants/tokens.ts b/packages/wallet/src/constants/tokens.ts index 73a8646075c..de5264dfba1 100644 --- a/packages/wallet/src/constants/tokens.ts +++ b/packages/wallet/src/constants/tokens.ts @@ -1,64 +1,62 @@ // Copied from https://github.com/Uniswap/interface/blob/main/src/constants/tokens.ts import { Token } from '@uniswap/sdk-core' -import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' -import { UniverseChainId, WalletChainId } from 'uniswap/src/types/chains' -import { UNI_ADDRESS } from 'wallet/src/constants/addresses' +import { UniverseChainId } from 'uniswap/src/types/chains' export const DAI = new Token( UniverseChainId.Mainnet, '0x6b175474e89094c44da98b954eedeac495271d0f', 18, 'DAI', - 'Dai Stablecoin' + 'Dai Stablecoin', ) export const DAI_ARBITRUM_ONE = new Token( UniverseChainId.ArbitrumOne, '0xda10009cbd5d07dd0cecc66161fc93d7c9000da1', 18, 'DAI', - 'Dai stable coin' + 'Dai stable coin', ) export const USDC = new Token( UniverseChainId.Mainnet, '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', 6, 'USDC', - 'USD//C' + 'USD//C', ) export const USDC_ARBITRUM = new Token( UniverseChainId.ArbitrumOne, '0xff970a61a04b1ca14834a43f5de4533ebddb5cc8', 6, 'USDC', - 'USD//C' + 'USD//C', ) export const USDBC_BASE = new Token( UniverseChainId.Base, '0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca', 6, 'USDbC', - 'USD Base Coin' + 'USD Base Coin', ) export const USDT_BNB = new Token( UniverseChainId.Bnb, '0x55d398326f99059ff775485246999027b3197955', 18, 'USDT', - 'TetherUSD' + 'TetherUSD', ) export const USDC_OPTIMISM = new Token( UniverseChainId.Optimism, '0x7f5c764cbc14f9669b88837ca1490cca17c31607', 6, 'USDC', - 'USD//C' + 'USD//C', ) export const USDC_POLYGON = new Token( UniverseChainId.Polygon, '0x2791bca1f2de4661ed88a30c99a7a9449aa84174', 6, 'USDC', - 'USD//C' + 'USD//C', ) export const USDC_GOERLI = new Token( @@ -66,7 +64,7 @@ export const USDC_GOERLI = new Token( '0x07865c6e87b9f70255377e024ace6630c1eaa37f', 6, 'USDC', - 'USD//C' + 'USD//C', ) export const USDT = new Token( @@ -74,31 +72,19 @@ export const USDT = new Token( '0xdac17f958d2ee523a2206206994597c13d831ec7', 6, 'USDT', - 'Tether USD' + 'Tether USD', ) -export const USDB = new Token( - UniverseChainId.Blast, - '0x4300000000000000000000000000000000000003', - 18, - 'USDB', - 'USDB' -) +export const USDB = new Token(UniverseChainId.Blast, '0x4300000000000000000000000000000000000003', 18, 'USDB', 'USDB') -export const CUSD = new Token( - UniverseChainId.Celo, - '0x765de816845861e75a25fca122bb6898b8b1282a', - 18, - 'CUSD', - 'CUSD' -) +export const CUSD = new Token(UniverseChainId.Celo, '0x765de816845861e75a25fca122bb6898b8b1282a', 18, 'CUSD', 'CUSD') export const USDC_AVALANCHE = new Token( UniverseChainId.Avalanche, '0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E', 6, 'USDC', - 'USDC' + 'USDC', ) export const USDzC = new Token( @@ -106,7 +92,7 @@ export const USDzC = new Token( '0xCccCCccc7021b32EBb4e8C08314bD62F7c653EC4', 6, 'USDzC', - 'USD Coin' + 'USD Coin', ) export const USDC_ZKSYNC = new Token( @@ -114,7 +100,7 @@ export const USDC_ZKSYNC = new Token( '0x1d17CBcF0D6D143135aE902365D2E5e2A16538D4', 6, 'USDC', - 'USDC' + 'USDC', ) export const WBTC = new Token( @@ -122,33 +108,5 @@ export const WBTC = new Token( '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599', 8, 'WBTC', - 'Wrapped BTC' + 'Wrapped BTC', ) - -export const UNI = { - [UniverseChainId.Mainnet]: new Token( - UniverseChainId.Mainnet, - UNI_ADDRESS[UniverseChainId.Mainnet], - 18, - 'UNI', - 'Uniswap' - ), - [UniverseChainId.Goerli]: new Token( - UniverseChainId.Goerli, - UNI_ADDRESS[UniverseChainId.Goerli], - 18, - 'UNI', - 'Uniswap' - ), -} - -export function wrappedNativeCurrency(chainId: WalletChainId): Token { - const wrappedCurrencyInfo = UNIVERSE_CHAIN_INFO[chainId].wrappedNativeCurrency - return new Token( - chainId, - wrappedCurrencyInfo.address, - wrappedCurrencyInfo.decimals, - wrappedCurrencyInfo.symbol, - wrappedCurrencyInfo.name - ) -} diff --git a/packages/wallet/src/contexts/WalletNavigationContext.tsx b/packages/wallet/src/contexts/WalletNavigationContext.tsx index e31f61c88ff..10d836c5855 100644 --- a/packages/wallet/src/contexts/WalletNavigationContext.tsx +++ b/packages/wallet/src/contexts/WalletNavigationContext.tsx @@ -1,11 +1,8 @@ import { createContext, ReactNode, useContext } from 'react' +import { CurrencyField, TransactionState } from 'uniswap/src/features/transactions/transactionState/types' import { WalletChainId } from 'uniswap/src/types/chains' import { NFTItem } from 'wallet/src/features/nfts/types' import { getSwapPrefilledState } from 'wallet/src/features/transactions/swap/hooks/useSwapPrefilledState' -import { - CurrencyField, - TransactionState, -} from 'wallet/src/features/transactions/transactionState/types' import { getSendPrefilledState } from 'wallet/src/features/transactions/transfer/getSendPrefilledState' type NavigateToTransactionFlowTransactionState = { @@ -34,53 +31,48 @@ export type NavigateToSendFlowArgs = | undefined function isNavigateToTransactionFlowArgsInitialState( - args: NavigateToSwapFlowArgs | NavigateToSendFlowArgs + args: NavigateToSwapFlowArgs | NavigateToSendFlowArgs, ): args is NavigateToTransactionFlowTransactionState { - return Boolean( - args && (args as NavigateToTransactionFlowTransactionState).initialState !== undefined - ) + return Boolean(args && (args as NavigateToTransactionFlowTransactionState).initialState !== undefined) } -function isNavigateToSwapFlowArgsPartialState( - args: NavigateToSwapFlowArgs -): args is NavigateToSwapFlowPartialState { +function isNavigateToSwapFlowArgsPartialState(args: NavigateToSwapFlowArgs): args is NavigateToSwapFlowPartialState { return Boolean(args && (args as NavigateToSwapFlowPartialState).currencyAddress !== undefined) } -function isNavigateToSendFlowArgsPartialState( - args: NavigateToSendFlowArgs -): args is NavigateToSendFlowPartialState { +function isNavigateToSendFlowArgsPartialState(args: NavigateToSendFlowArgs): args is NavigateToSendFlowPartialState { return Boolean(args && (args as NavigateToSendFlowPartialState).chainId !== undefined) } -export function getNavigateToSwapFlowArgsInitialState( - args: NavigateToSwapFlowArgs -): TransactionState | undefined { +export function getNavigateToSwapFlowArgsInitialState(args: NavigateToSwapFlowArgs): TransactionState | undefined { return isNavigateToTransactionFlowArgsInitialState(args) ? args.initialState : isNavigateToSwapFlowArgsPartialState(args) - ? getSwapPrefilledState(args) - : undefined + ? getSwapPrefilledState(args) + : undefined } -export function getNavigateToSendFlowArgsInitialState( - args: NavigateToSendFlowArgs -): TransactionState | undefined { +export function getNavigateToSendFlowArgsInitialState(args: NavigateToSendFlowArgs): TransactionState | undefined { return isNavigateToTransactionFlowArgsInitialState(args) ? args.initialState : isNavigateToSendFlowArgsPartialState(args) - ? getSendPrefilledState(args) - : undefined + ? getSendPrefilledState(args) + : undefined } export type NavigateToNftItemArgs = { owner?: Address address: Address tokenId: string + chainId?: WalletChainId isSpam?: boolean fallbackData?: NFTItem } +export type NavigateToNftCollectionArgs = { + collectionAddress: Address +} + export type ShareTokenArgs = { currencyId: string } @@ -96,6 +88,7 @@ export type WalletNavigationContextState = { // Action that should be taken when the user presses the "Buy crypto" or "Receive tokens" button when they open the Send flow with an empty wallet. navigateToBuyOrReceiveWithEmptyWallet: () => void navigateToNftDetails: (args: NavigateToNftItemArgs) => void + navigateToNftCollection: (args: NavigateToNftCollectionArgs) => void navigateToSwapFlow: (args: NavigateToSwapFlowArgs) => void navigateToTokenDetails: (currencyId: string) => void navigateToReceive: () => void @@ -104,9 +97,7 @@ export type WalletNavigationContextState = { handleShareToken: (args: ShareTokenArgs) => void } -export const WalletNavigationContext = createContext( - undefined -) +export const WalletNavigationContext = createContext(undefined) export function WalletNavigationProvider({ children, @@ -114,9 +105,7 @@ export function WalletNavigationProvider({ }: { children: ReactNode } & WalletNavigationContextState): JSX.Element { - return ( - {children} - ) + return {children} } export const useWalletNavigation = (): WalletNavigationContextState => { diff --git a/packages/wallet/src/data/apollo/usePersistedApolloClient.tsx b/packages/wallet/src/data/apollo/usePersistedApolloClient.tsx index 113bbc8c320..19bc09522d0 100644 --- a/packages/wallet/src/data/apollo/usePersistedApolloClient.tsx +++ b/packages/wallet/src/data/apollo/usePersistedApolloClient.tsx @@ -4,7 +4,7 @@ import { useCallback, useEffect, useState } from 'react' import { MMKV } from 'react-native-mmkv' import { WalletEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' -import { isNonJestDev } from 'utilities/src/environment' +import { isNonJestDev } from 'utilities/src/environment/constants' import { logger } from 'utilities/src/logger/logger' import { isMobileApp } from 'utilities/src/platform' import { useAsyncData } from 'utilities/src/react/hooks' @@ -28,9 +28,7 @@ export const apolloClientRef: ApolloClientRef = ((): ApolloClientRef => { let apolloClient: ApolloClient | null = null const listeners: Array< - ( - value: ApolloClient | PromiseLike> - ) => void + (value: ApolloClient | PromiseLike>) => void > = [] const ref: ApolloClientRef = { @@ -77,9 +75,7 @@ export const usePersistedApolloClient = ({ }): ApolloClient | undefined => { const [client, setClient] = useState>() - const apolloLink = customEndpoint - ? getCustomGraphqlHttpLink(customEndpoint) - : getGraphqlHttpLink() + const apolloLink = customEndpoint ? getCustomGraphqlHttpLink(customEndpoint) : getGraphqlHttpLink() const init = useCallback(async () => { const cache = await initAndPersistCache({ storage: storageWrapper, maxCacheSizeInBytes }) @@ -88,7 +84,7 @@ export const usePersistedApolloClient = ({ logger.debug( 'usePersistedApolloClient', 'usePersistedApolloClient', - `Using custom endpoint ${customEndpoint.url}` + `Using custom endpoint ${customEndpoint.url}`, ) } @@ -100,9 +96,7 @@ export const usePersistedApolloClient = ({ getErrorLink(), // requires typing outside of wallet package // eslint-disable-next-line @typescript-eslint/no-explicit-any - getPerformanceLink((args: any) => - sendAnalyticsEvent(WalletEventName.PerformanceGraphql, args) - ), + getPerformanceLink((args: any) => sendAnalyticsEvent(WalletEventName.PerformanceGraphql, args)), restLink, apolloLink, ]), diff --git a/packages/wallet/src/data/links.ts b/packages/wallet/src/data/links.ts index a9fb8fe3f07..38b17d7c9e4 100644 --- a/packages/wallet/src/data/links.ts +++ b/packages/wallet/src/data/links.ts @@ -5,11 +5,7 @@ import { config } from 'uniswap/src/config' import { uniswapUrls } from 'uniswap/src/constants/urls' import { REQUEST_SOURCE, getVersionHeader } from 'uniswap/src/data/constants' import { logger } from 'utilities/src/logger/logger' -import { - EnsLookupParams, - STUB_ONCHAIN_ENS_ENDPOINT, - getOnChainEnsFetch, -} from 'wallet/src/features/ens/api' +import { EnsLookupParams, STUB_ONCHAIN_ENS_ENDPOINT, getOnChainEnsFetch } from 'wallet/src/features/ens/api' import { BalanceLookupParams, STUB_ONCHAIN_BALANCES_ENDPOINT, @@ -18,12 +14,8 @@ import { // mapping from endpoint to custom fetcher, when needed function getCustomFetcherMap( - restUri: string -): Record< - string, - | ((body: BalanceLookupParams) => Promise) - | ((body: EnsLookupParams) => Promise) -> { + restUri: string, +): Record Promise) | ((body: EnsLookupParams) => Promise)> { return { [restUri + STUB_ONCHAIN_BALANCES_ENDPOINT]: getOnChainBalancesFetch, [restUri + STUB_ONCHAIN_ENS_ENDPOINT]: getOnChainEnsFetch, @@ -107,7 +99,7 @@ export function sample(cb: () => void, rate: number): void { export function getErrorLink( graphqlErrorSamplingRate = APOLLO_GRAPHQL_ERROR_SAMPLING_RATE, - networkErrorSamplingRate = APOLLO_NETWORK_ERROR_SAMPLING_RATE + networkErrorSamplingRate = APOLLO_NETWORK_ERROR_SAMPLING_RATE, ): ApolloLink { // Log any GraphQL errors or network error that occurred const errorLink = onError(({ graphQLErrors, networkError }) => { @@ -122,15 +114,14 @@ export function getErrorLink( }, extra: { message, locations, path }, }), - graphqlErrorSamplingRate + graphqlErrorSamplingRate, ) }) } if (networkError) { sample( - () => - logger.error(networkError, { tags: { file: 'data/links', function: 'getErrorLink' } }), - networkErrorSamplingRate + () => logger.error(networkError, { tags: { file: 'data/links', function: 'getErrorLink' } }), + networkErrorSamplingRate, ) } }) @@ -140,7 +131,7 @@ export function getErrorLink( export function getPerformanceLink( sendAnalyticsEvent: (args: Record) => void, - samplingRate = APOLLO_PERFORMANCE_SAMPLING_RATE + samplingRate = APOLLO_PERFORMANCE_SAMPLING_RATE, ): ApolloLink { return new ApolloLink((operation, forward) => { const startTime = Date.now() @@ -156,7 +147,7 @@ export function getPerformanceLink( duration, operationName: operation.operationName, }), - samplingRate + samplingRate, ) return data diff --git a/packages/wallet/src/data/tradingApi/api.json b/packages/wallet/src/data/tradingApi/api.json index 246b3d54675..2e793f34b75 100644 --- a/packages/wallet/src/data/tradingApi/api.json +++ b/packages/wallet/src/data/tradingApi/api.json @@ -1 +1 @@ -{"openapi":"3.0.0","servers":[{"description":"Uniswap trading APIs Beta","url":"https://beta.trade-api.gateway.uniswap.org/v1"},{"description":"Uniswap trading APIs","url":"https://trade-api.gateway.uniswap.org/v1"}],"info":{"version":"1.0.0","title":"Token Trading","description":"Uniswap trading APIs for fungible tokens."},"paths":{"/check_approval":{"post":{"tags":["Approval"],"summary":"Check if token approval is required","description":"Checks if the swapper has the required approval. If the swapper does not have the required approval, then the response will include the transaction to approve the token. If the swapper has the required approval, then the response will be empty. If the parameter `includeGasInfo` is set to `true`, then the response will include the gas fee for the approval transaction.","operationId":"check_approval","security":[{"apiKey":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApprovalRequest"}}}},"responses":{"200":{"$ref":"#/components/responses/ApprovalSuccess200"},"400":{"$ref":"#/components/responses/BadRequest400"},"401":{"$ref":"#/components/responses/ApprovalUnauthorized401"},"404":{"$ref":"#/components/responses/ApprovalNotFound404"},"419":{"$ref":"#/components/responses/RateLimitedErr419"},"500":{"$ref":"#/components/responses/InternalErr500"},"504":{"$ref":"#/components/responses/Timeout504"}}}},"/quote":{"post":{"tags":["Quote"],"summary":"Get a quote","description":"Get a quote according to the provided configuration. Optionally adds a fee to the quote according to the API key being used. The fee is **ALWAYS** taken from the output token. If there is a fee and the trade is `EXACT_INPUT`, then the output amount will **NOT** include the fee subtraction. For `EXACT_INPUT` swaps, use `portionBips` to calculate the fee from the quoted amount. If there is a fee and the trade is `EXACT_OUTPUT`, then the input amount will **NOT** include the fee addition to account for the fee. For `EXACT_OUTPUT` swaps, use `portionAmount` to get the fee. \n \n We also support Wrapping and Unwrapping of native tokens on their respective chains. Wrapping and Unwrapping only works for when `routingPreference` is `CLASSIC`, `BEST_PRICE`, or `BEST_PRICE_V2`. We do not support `UNISWAPX` or `UNISWAPX_V2` for these actions.","operationId":"aggregator_quote","security":[{"apiKey":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuoteRequest"}}}},"responses":{"200":{"$ref":"#/components/responses/QuoteSuccess200"},"400":{"$ref":"#/components/responses/BadRequest400"},"401":{"$ref":"#/components/responses/QuoteUnauthorized401"},"404":{"$ref":"#/components/responses/QuoteNotFound404"},"419":{"$ref":"#/components/responses/RateLimitedErr419"},"500":{"$ref":"#/components/responses/InternalErr500"},"504":{"$ref":"#/components/responses/Timeout504"}}}},"/order":{"post":{"tags":["Order"],"summary":"Create a gasless order","description":"Submits a new gasless encoded order. The order will be validated and if valid, will be submitted to the filler network. The network will try to fill the order at the quoted `startAmount`, and if not, the amount will start decaying until the `endAmount` is reached. While the order is within `decayEndTime`, the `orderStatus` is `open`. If the order does not get filled after the `decayEndTime` has passed, that is reflected in the `expired` `orderStatus`. then The order will be filled at the best price possible. Once the order is filled, `orderStatus` becomes `filled`.","operationId":"post_dutch_order","security":[{"apiKey":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderRequest"}}}},"responses":{"201":{"$ref":"#/components/responses/OrderSuccess201"},"400":{"$ref":"#/components/responses/BadRequest400"},"401":{"$ref":"#/components/responses/OrderUnauthorized401"},"419":{"$ref":"#/components/responses/RateLimitedErr419"},"500":{"$ref":"#/components/responses/InternalErr500"},"504":{"$ref":"#/components/responses/Timeout504"}}}},"/orders":{"get":{"tags":["Order"],"summary":"Get gasless orders","description":"Retrieve gasless orders filtered by query param(s). Some fields on the order can be used as query param.","operationId":"get_dutch_order","security":[{"apiKey":[]}],"parameters":[{"$ref":"#/components/parameters/orderIdParam"},{"$ref":"#/components/parameters/orderIdsParam"},{"$ref":"#/components/parameters/limitParam"},{"$ref":"#/components/parameters/orderStatusParam"},{"$ref":"#/components/parameters/swapperParam"},{"$ref":"#/components/parameters/sortKeyParam"},{"$ref":"#/components/parameters/sortParam"},{"$ref":"#/components/parameters/fillerParam"},{"$ref":"#/components/parameters/cursorParam"}],"responses":{"200":{"$ref":"#/components/responses/OrdersSuccess200"},"400":{"$ref":"#/components/responses/BadRequest400"},"404":{"$ref":"#/components/responses/OrdersNotFound404"},"419":{"$ref":"#/components/responses/RateLimitedErr419"},"500":{"$ref":"#/components/responses/InternalErr500"},"504":{"$ref":"#/components/responses/Timeout504"}}}},"/swap":{"post":{"tags":["Swap"],"summary":"Create swap calldata","description":"Create the calldata for a swap transaction (including wrap/unwrap) against the Uniswap Protocols. If the `quote` parameter includes the fee parameters, then the calldata will include the fee disbursement. The gas estimates will be **more precise** when the the response calldata would be valid if submitted on-chain.","operationId":"create_swap_transaction","security":[{"apiKey":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSwapRequest"}}}},"responses":{"200":{"$ref":"#/components/responses/CreateSwapSuccess200"},"400":{"$ref":"#/components/responses/BadRequest400"},"401":{"$ref":"#/components/responses/SwapUnauthorized401"},"404":{"$ref":"#/components/responses/SwapNotFound404"},"419":{"$ref":"#/components/responses/RateLimitedErr419"},"500":{"$ref":"#/components/responses/InternalErr500"},"504":{"$ref":"#/components/responses/Timeout504"}}}},"/swap/{txHash}":{"get":{"tags":["Swap"],"summary":"Get swap status","description":"Get the status of a swap transaction.","operationId":"get_swap_transaction","security":[{"apiKey":[]}],"parameters":[{"$ref":"#/components/parameters/transactionHashParam"}],"responses":{"200":{"$ref":"#/components/responses/GetSwapSuccess200"},"400":{"$ref":"#/components/responses/BadRequest400"},"404":{"$ref":"#/components/responses/SwapNotFound404"},"419":{"$ref":"#/components/responses/RateLimitedErr419"},"500":{"$ref":"#/components/responses/InternalErr500"},"504":{"$ref":"#/components/responses/Timeout504"}}}}},"components":{"responses":{"OrdersSuccess200":{"description":"The request orders matching the query parameters.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetOrdersResponse"}}}},"OrderSuccess201":{"description":"Encoded order submitted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderResponse"}}}},"QuoteSuccess200":{"description":"Quote request successful.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuoteResponse"}}}},"ApprovalSuccess200":{"description":"Check approval successful.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApprovalResponse"}}}},"CreateSwapSuccess200":{"description":"Create swap successful.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSwapResponse"}}}},"GetSwapSuccess200":{"description":"Get swap successful.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetSwapResponse"}}}},"BadRequest400":{"description":"RequestValidationError, Bad Input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err400"}}}},"ApprovalUnauthorized401":{"description":"UnauthorizedError eg. Account is blocked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err401"}}}},"ApprovalNotFound404":{"description":"ResourceNotFound eg. Token allowance not found or Gas info not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err404"}}}},"QuoteUnauthorized401":{"description":"UnauthorizedError eg. Account is blocked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err401"}}}},"QuoteNotFound404":{"description":"ResourceNotFound eg. No quotes available or Gas fee/price not available","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err404"}}}},"SwapBadRequest400":{"description":"RequestValidationError, Bad Input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err400"}}}},"SwapUnauthorized401":{"description":"UnauthorizedError eg. Account is blocked or Fee is not enabled.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err401"}}}},"SwapNotFound404":{"description":"ResourceNotFound eg. No quotes available or Gas fee/price not available","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err404"}}}},"OrderUnauthorized401":{"description":"UnauthorizedError eg. Account is blocked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err401"}}}},"OrdersNotFound404":{"description":"Orders not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err404"}}}},"RateLimitedErr419":{"description":"Ratelimited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err419"}}}},"InternalErr500":{"description":"Unexpected error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err500"}}}},"Timeout504":{"description":"Request duration limit reached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err504"}}}}},"schemas":{"NullablePermit":{"allOf":[{"$ref":"#/components/schemas/Permit"},{"type":"object","nullable":true}]},"TokenAmount":{"type":"string"},"SwapStatus":{"type":"string","enum":["pending","success","error"]},"GetSwapResponse":{"type":"object","properties":{"requestId":{"$ref":"#/components/schemas/RequestId"},"status":{"$ref":"#/components/schemas/SwapStatus"}},"required":["requestId","status"]},"CreateSwapRequest":{"type":"object","description":"The parameters **signature** and **permitData** should only be included if *permitData* was returned from **/quote**.","properties":{"quote":{"oneOf":[{"$ref":"#/components/schemas/ClassicQuote"},{"$ref":"#/components/schemas/WrapUnwrapQuote"}]},"signature":{"type":"string","description":"The signed permit."},"includeGasInfo":{"type":"boolean","default":false,"deprecated":true,"description":"Use `refreshGasPrice` instead."},"refreshGasPrice":{"type":"boolean","default":false,"description":"If true, the gas price will be re-fetched from the network."},"simulateTransaction":{"type":"boolean","default":false,"description":"If true, the transaction will be simulated. If the simulation results on an onchain error, endpoint will return an error."},"permitData":{"allOf":[{"$ref":"#/components/schemas/Permit"}]},"safetyMode":{"$ref":"#/components/schemas/SwapSafetyMode"},"deadline":{"type":"integer","description":"The deadline for the swap in unix timestamp format. If the deadline is not defined OR in the past then the default deadline is 30 minutes."},"urgency":{"$ref":"#/components/schemas/Urgency"}},"required":["quote"]},"Address":{"type":"string","pattern":"^(0x)?[0-9a-fA-F]{40}$"},"CreateSwapResponse":{"type":"object","properties":{"requestId":{"$ref":"#/components/schemas/RequestId"},"swap":{"$ref":"#/components/schemas/TransactionRequest"},"gasFee":{"type":"string"}},"required":["requestId","swap"]},"QuoteResponse":{"type":"object","properties":{"requestId":{"$ref":"#/components/schemas/RequestId"},"quote":{"$ref":"#/components/schemas/Quote"},"routing":{"$ref":"#/components/schemas/Routing"},"permitData":{"$ref":"#/components/schemas/NullablePermit"}},"required":["routing","quote","permitData","requestId"]},"QuoteRequest":{"type":"object","properties":{"type":{"$ref":"#/components/schemas/TradeType"},"amount":{"type":"string"},"tokenInChainId":{"$ref":"#/components/schemas/ChainId"},"tokenOutChainId":{"$ref":"#/components/schemas/ChainId"},"tokenIn":{"type":"string"},"tokenOut":{"type":"string"},"swapper":{"$ref":"#/components/schemas/Address"},"slippageTolerance":{"description":"For **Classic** swaps, the slippage tolerance is the maximum amount the price can change between the time the transaction is submitted and the time it is executed. The slippage tolerance is represented as a percentage of the total value of the swap. \n\n Slippage tolerance works differently in **DutchLimit** swaps, it does not set a limit on the Spread in an order. See [here](https://uniswap-docs.readme.io/reference/faqs#why-do-the-uniswapx-quotes-have-more-slippage-than-the-tolerance-i-set) for more information. \n\n **NOTE**: slippage is in terms of trade type. If the trade type is `EXACT_INPUT`, then the slippage is in terms of the output token. If the trade type is `EXACT_OUTPUT`, then the slippage is in terms of the input token.","type":"number"},"autoSlippage":{"$ref":"#/components/schemas/AutoSlippage"},"routingPreference":{"$ref":"#/components/schemas/RoutingPreference"},"spreadOptimization":{"$ref":"#/components/schemas/SpreadOptimization"},"urgency":{"$ref":"#/components/schemas/Urgency"}},"required":["type","amount","tokenInChainId","tokenOutChainId","tokenIn","tokenOut","swapper"]},"GetOrdersResponse":{"type":"object","properties":{"requestId":{"$ref":"#/components/schemas/RequestId"},"orders":{"type":"array","items":{"$ref":"#/components/schemas/UniswapXOrder"}},"cursor":{"type":"string"}},"required":["orders","requestId"]},"OrderResponse":{"type":"object","properties":{"requestId":{"$ref":"#/components/schemas/RequestId"},"orderId":{"type":"string"},"orderStatus":{"$ref":"#/components/schemas/OrderStatus"}},"required":["requestId","orderId","orderStatus"]},"OrderRequest":{"type":"object","properties":{"signature":{"type":"string","description":"The signed permit."},"quote":{"oneOf":[{"$ref":"#/components/schemas/DutchQuote"},{"$ref":"#/components/schemas/DutchQuoteV2"}]},"routing":{"$ref":"#/components/schemas/Routing"}},"required":["signature","quote"]},"Urgency":{"type":"string","enum":["normal","fast","urgent"],"description":"The urgency determines the urgency of the transaction. The default value is `urgent`.","default":"urgent"},"Err400":{"type":"object","properties":{"errorCode":{"default":"RequestValidationError","type":"string"},"detail":{"type":"string"}}},"Err401":{"type":"object","properties":{"errorCode":{"default":"UnauthorizedError","type":"string"},"detail":{"type":"string"}}},"Err404":{"type":"object","properties":{"errorCode":{"default":"ResourceNotFound","type":"string"},"detail":{"type":"string"}}},"Err419":{"type":"object","properties":{"errorCode":{"default":"Ratelimited","type":"string"},"detail":{"type":"string"}}},"Err500":{"type":"object","properties":{"errorCode":{"default":"InternalServerError","type":"string"},"detail":{"type":"string"}}},"Err504":{"type":"object","properties":{"errorCode":{"default":"Timeout","type":"string"},"detail":{"type":"string"}}},"ChainId":{"type":"number","enum":[1,10,56,137,8453,42161,81457,43114,42220,7777777,324]},"OrderInput":{"type":"object","properties":{"token":{"type":"string"},"startAmount":{"type":"string"},"endAmount":{"type":"string"}},"required":["token"]},"OrderOutput":{"type":"object","properties":{"token":{"type":"string"},"startAmount":{"type":"string"},"endAmount":{"type":"string"},"isFeeOutput":{"type":"boolean"},"recipient":{"type":"string"}},"required":["token"]},"CosignerData":{"type":"object","properties":{"decayStartTime":{"type":"number"},"decayEndTime":{"type":"number"},"exclusiveFiller":{"type":"string"},"inputOverride":{"type":"string"},"outputOverrides":{"type":"array","items":{"type":"string"}}}},"SettledAmount":{"type":"object","properties":{"tokenOut":{"$ref":"#/components/schemas/Address"},"amountOut":{"type":"string"},"tokenIn":{"$ref":"#/components/schemas/Address"},"amountIn":{"type":"string"}}},"OrderType":{"type":"string","enum":["DutchLimit","Dutch","Dutch_V2"]},"UniswapXOrder":{"type":"object","properties":{"type":{"$ref":"#/components/schemas/OrderType"},"encodedOrder":{"type":"string"},"signature":{"type":"string"},"nonce":{"type":"string"},"orderStatus":{"$ref":"#/components/schemas/OrderStatus"},"orderId":{"type":"string"},"chainId":{"$ref":"#/components/schemas/ChainId"},"quoteId":{"type":"string"},"swapper":{"type":"string"},"txHash":{"type":"string"},"input":{"$ref":"#/components/schemas/OrderInput"},"outputs":{"type":"array","items":{"$ref":"#/components/schemas/OrderOutput"}},"settledAmounts":{"type":"array","items":{"$ref":"#/components/schemas/SettledAmount"}},"cosignature":{"type":"string"},"cosignerData":{"$ref":"#/components/schemas/CosignerData"}},"required":["encodedOrder","signature","nonce","orderId","orderStatus","chainId","type"]},"SortKey":{"type":"string","enum":["createdAt"]},"OrderId":{"type":"string"},"OrderIds":{"type":"string"},"OrderStatus":{"type":"string","enum":["open","expired","error","cancelled","filled","unverified","insufficient-funds"]},"Permit":{"type":"object","properties":{"domain":{"type":"object"},"values":{"type":"object"},"types":{"type":"object"}}},"DutchInput":{"type":"object","properties":{"startAmount":{"type":"string"},"endAmount":{"type":"string"},"token":{"type":"string"}},"required":["startAmount","endAmount","type"]},"DutchOutput":{"type":"object","properties":{"startAmount":{"type":"string"},"endAmount":{"type":"string"},"token":{"type":"string"},"recipient":{"type":"string"}},"required":["startAmount","endAmount","token","recipient"]},"DutchOrderInfo":{"type":"object","properties":{"chainId":{"$ref":"#/components/schemas/ChainId"},"nonce":{"type":"string"},"reactor":{"type":"string"},"swapper":{"type":"string"},"deadline":{"type":"number"},"additionalValidationContract":{"type":"string"},"additionalValidationData":{"type":"string"},"decayStartTime":{"type":"number"},"decayEndTime":{"type":"number"},"exclusiveFiller":{"type":"string"},"exclusivityOverrideBps":{"type":"string"},"input":{"$ref":"#/components/schemas/DutchInput"},"outputs":{"type":"array","items":{"$ref":"#/components/schemas/DutchOutput"}}},"required":["chainId","nonce","reactor","swapper","deadline","validationContract","validationData","startTime","endTime","exclusiveFiller","exclusivityOverrideBps","input","outputs"]},"DutchOrderInfoV2":{"type":"object","properties":{"chainId":{"$ref":"#/components/schemas/ChainId"},"nonce":{"type":"string"},"reactor":{"type":"string"},"swapper":{"type":"string"},"deadline":{"type":"number"},"additionalValidationContract":{"type":"string"},"additionalValidationData":{"type":"string"},"input":{"$ref":"#/components/schemas/DutchInput"},"outputs":{"type":"array","items":{"$ref":"#/components/schemas/DutchOutput"}},"cosigner":{"$ref":"#/components/schemas/Address"}},"required":["chainId","nonce","reactor","swapper","deadline","validationContract","validationData","startTime","endTime","exclusiveFiller","exclusivityOverrideBps","input","outputs"]},"DutchQuote":{"type":"object","properties":{"encodedOrder":{"type":"string"},"orderId":{"type":"string"},"orderInfo":{"$ref":"#/components/schemas/DutchOrderInfo"},"portionBips":{"type":"number"},"portionAmount":{"type":"string"},"portionRecipient":{"$ref":"#/components/schemas/Address"},"quoteId":{"type":"string"},"slippageTolerance":{"type":"number"}},"required":["encodedOrder","orderInfo","orderId"]},"DutchQuoteV2":{"type":"object","properties":{"encodedOrder":{"type":"string"},"orderId":{"type":"string"},"orderInfo":{"$ref":"#/components/schemas/DutchOrderInfoV2"},"portionBips":{"type":"number"},"portionAmount":{"type":"string"},"portionRecipient":{"$ref":"#/components/schemas/Address"},"quoteId":{"type":"string"},"slippageTolerance":{"type":"number"},"deadlineBufferSecs":{"type":"number"}},"required":["encodedOrder","orderInfo","orderId"]},"TradeType":{"type":"string","enum":["EXACT_INPUT","EXACT_OUTPUT"]},"Routing":{"type":"string","enum":["DUTCH_LIMIT","CLASSIC","DUTCH_V2"]},"Quote":{"oneOf":[{"$ref":"#/components/schemas/DutchQuote"},{"$ref":"#/components/schemas/ClassicQuote"},{"$ref":"#/components/schemas/WrapUnwrapQuote"},{"$ref":"#/components/schemas/DutchQuoteV2"}]},"ApprovalRequest":{"type":"object","properties":{"walletAddress":{"$ref":"#/components/schemas/Address"},"token":{"$ref":"#/components/schemas/Address"},"amount":{"$ref":"#/components/schemas/TokenAmount"},"chainId":{"$ref":"#/components/schemas/ChainId"},"urgency":{"$ref":"#/components/schemas/Urgency"},"includeGasInfo":{"type":"boolean","default":false},"tokenOut":{"type":"string","pattern":"^(0x)?[0-9a-fA-F]{40}$","description":"relevant for if we go from a wrapped token to a native token (unwrapping)"},"tokenOutChainId":{"type":"number","enum":[1,10,56,137,8453,42161,81457,43114,42220,7777777,324],"default":1,"description":"relevant for if we go from a wrapped token to a native token (unwrapping)"}},"required":["walletAddress","token","amount"]},"ApprovalResponse":{"type":"object","properties":{"requestId":{"$ref":"#/components/schemas/RequestId"},"approval":{"$ref":"#/components/schemas/TransactionRequest"},"gasFee":{"type":"string"}},"required":["requestId","approval"]},"ClassicQuote":{"type":"object","properties":{"input":{"$ref":"#/components/schemas/ClassicInput"},"output":{"$ref":"#/components/schemas/ClassicOutput"},"swapper":{"$ref":"#/components/schemas/Address"},"chainId":{"$ref":"#/components/schemas/ChainId"},"slippage":{"type":"number"},"tradeType":{"$ref":"#/components/schemas/TradeType"},"gasFee":{"type":"string","description":"The gas fee in terms of wei. It does NOT include the additional gas for token approvals."},"gasFeeUSD":{"type":"string","description":"The gas fee in terms of USD. It does NOT include the additional gas for token approvals."},"gasFeeQuote":{"type":"string","description":"The gas fee in terms of the quoted currency. It does NOT include the additional gas for token approvals."},"route":{"type":"array","items":{"type":"array","items":{"oneOf":[{"$ref":"#/components/schemas/V3PoolInRoute"},{"$ref":"#/components/schemas/V2PoolInRoute"}]}}},"portionBips":{"type":"number","description":"The portion of the swap that will be taken as a fee. The fee will be taken from the output token."},"portionAmount":{"type":"string","description":"The amount of the swap that will be taken as a fee. The fee will be taken from the output token."},"portionRecipient":{"$ref":"#/components/schemas/Address"},"routeString":{"type":"string","description":"The route in string format."},"quoteId":{"type":"string","description":"The quote id. Used for analytics purposes."},"gasUseEstimate":{"type":"string","description":"The estimated gas use. It does NOT include the additional gas for token approvals."},"blockNumber":{"type":"string","description":"The current block number."},"gasPrice":{"type":"string","description":"The gas price in terms of wei for pre EIP1559 transactions."},"maxFeePerGas":{"type":"string","description":"The maximum fee per gas in terms of wei for EIP1559 transactions."},"maxPriorityFeePerGas":{"type":"string","description":"The maximum priority fee per gas in terms of wei for EIP1559 transactions."},"txFailureReasons":{"type":"array","items":{"$ref":"#/components/schemas/TransactionFailureReason"}},"priceImpact":{"type":"number","description":"The impact the trade has on the market price of the pool, between 0-100 percent"}}},"WrapUnwrapQuote":{"type":"object","properties":{"swapper":{"$ref":"#/components/schemas/Address"},"input":{"$ref":"#/components/schemas/ClassicInput"},"output":{"$ref":"#/components/schemas/ClassicOutput"},"chainId":{"$ref":"#/components/schemas/ChainId"},"tradeType":{"$ref":"#/components/schemas/TradeType"},"gasFee":{"type":"string","description":"The gas fee in terms of wei."},"gasFeeUSD":{"type":"string","description":"The gas fee in terms of USD."},"gasFeeQuote":{"type":"string","description":"The gas fee in terms of the quoted currency."},"gasUseEstimate":{"type":"string","description":"The estimated gas use."},"gasPrice":{"type":"string","description":"The gas price in terms of wei for pre EIP1559 transactions."},"maxFeePerGas":{"type":"string","description":"The maximum fee per gas in terms of wei for EIP1559 transactions."},"maxPriorityFeePerGas":{"type":"string","description":"The maximum priority fee per gas in terms of wei for EIP1559 transactions."}}},"TokenInRoute":{"type":"object","properties":{"address":{"$ref":"#/components/schemas/Address"},"chainId":{"$ref":"#/components/schemas/ChainId"},"symbol":{"type":"string"},"decimals":{"type":"string"},"buyFeeBps":{"type":"string"},"sellFeeBps":{"type":"string"}}},"V2Reserve":{"type":"object","properties":{"token":{"$ref":"#/components/schemas/TokenInRoute"},"quotient":{"type":"string"}}},"V2PoolInRoute":{"type":"object","properties":{"type":{"type":"string","default":"v2-pool"},"address":{"$ref":"#/components/schemas/Address"},"tokenIn":{"$ref":"#/components/schemas/TokenInRoute"},"tokenOut":{"$ref":"#/components/schemas/TokenInRoute"},"reserve0":{"$ref":"#/components/schemas/V2Reserve"},"reserve1":{"$ref":"#/components/schemas/V2Reserve"},"amountIn":{"type":"string"},"amountOut":{"type":"string"}}},"V3PoolInRoute":{"type":"object","properties":{"type":{"type":"string","default":"v3-pool"},"address":{"$ref":"#/components/schemas/Address"},"tokenIn":{"$ref":"#/components/schemas/TokenInRoute"},"tokenOut":{"$ref":"#/components/schemas/TokenInRoute"},"sqrtRatioX96":{"type":"string"},"liquidity":{"type":"string"},"tickCurrent":{"type":"string"},"fee":{"type":"string"},"amountIn":{"type":"string"},"amountOut":{"type":"string"}}},"TransactionHash":{"type":"string","pattern":"^(0x)?[0-9a-fA-F]{64}$"},"ClassicInput":{"type":"object","properties":{"token":{"$ref":"#/components/schemas/Address"},"amount":{"type":"string"}}},"ClassicOutput":{"type":"object","properties":{"token":{"$ref":"#/components/schemas/Address"},"amount":{"type":"string"},"recipient":{"$ref":"#/components/schemas/Address"}}},"RequestId":{"type":"string"},"SpreadOptimization":{"type":"string","enum":["EXECUTION","PRICE"],"description":"For **Dutch Limit** orders only. When set to `EXECUTION`, quotes optimize for looser spreads at higher fill rates. When set to `PRICE`, quotes optimize for tighter spreads at lower fill rates","default":"EXECUTION"},"AutoSlippage":{"type":"string","enum":["DEFAULT"],"description":"For **Classic** swaps only. The auto slippage strategy to employ. If auto slippage is not defined then we don't compute it. If the auto slippage strategy is `DEFAULT`, then the swap will use the default slippage tolerance computation. You cannot define auto slippage and slippage tolerance at the same time. \n\n **NOTE**: slippage is in terms of trade type. If the trade type is `EXACT_INPUT`, then the slippage is in terms of the output token. If the trade type is `EXACT_OUTPUT`, then the slippage is in terms of the input token.","default":"undefined"},"RoutingPreference":{"type":"string","description":"The routing preference determines which protocol to use for the swap. If the routing preference is `UNISWAPX`, then the swap will be routed through the UniswapX Dutch Auction Protocol. If the routing preference is `CLASSIC`, then the swap will be routed through the Classic Protocol. If the routing preference is `BEST_PRICE`, then the swap will be routed through the protocol that provides the best price. When `UNIXWAPX_V2` is passed, the swap will be routed through the UniswapX V2 Dutch Auction Protocol. When `V3_ONLY` is passed, the swap will be routed ONLY through the Uniswap V3 Protocol. When `V2_ONLY` is passed, the swap will be routed ONLY through the Uniswap V2 Protocol.","enum":["CLASSIC","UNISWAPX","BEST_PRICE","BEST_PRICE_V2","UNISWAPX_V2","V3_ONLY","V2_ONLY"],"default":"BEST_PRICE"},"TransactionRequest":{"type":"object","properties":{"to":{"$ref":"#/components/schemas/Address"},"from":{"$ref":"#/components/schemas/Address"},"data":{"type":"string","description":"The calldata for the transaction."},"value":{"type":"string","description":"The value of the transaction in terms of wei in hex format."},"gasLimit":{"type":"string"},"chainId":{"type":"integer"},"maxFeePerGas":{"type":"string"},"maxPriorityFeePerGas":{"type":"string"},"gasPrice":{"type":"string"}},"required":["to","from","data","value","chainId"]},"TransactionFailureReason":{"type":"string","enum":["SIMULATION_ERROR","UNSUPPORTED_SIMULATION"]},"SwapSafetyMode":{"type":"string","enum":["SAFE"],"description":"The safety mode determines the safety level of the swap. If the safety mode is `SAFE`, then the swap will include a SWEEP for the native token."}},"parameters":{"addressParam":{"name":"address","in":"path","schema":{"$ref":"#/components/schemas/Address"},"required":true},"tokenIdParam":{"name":"tokenId","in":"path","schema":{"type":"string"},"required":true},"cursorParam":{"name":"cursor","in":"query","schema":{"type":"string"},"required":false},"limitParam":{"name":"limit","in":"query","schema":{"type":"number"},"required":false},"chainParam":{"name":"chain","in":"query","schema":{"$ref":"#/components/schemas/ChainId"},"required":false},"addressPathParam":{"name":"address","in":"query","schema":{"$ref":"#/components/schemas/Address"},"required":false},"orderStatusParam":{"name":"orderStatus","in":"query","description":"Filter by order status.","required":false,"schema":{"$ref":"#/components/schemas/OrderStatus"}},"orderIdParam":{"name":"orderId","in":"query","required":false,"schema":{"$ref":"#/components/schemas/OrderId"}},"orderIdsParam":{"name":"orderIds","in":"query","required":false,"description":"ids split by commas","schema":{"$ref":"#/components/schemas/OrderIds"}},"swapperParam":{"name":"swapper","in":"query","description":"Filter by swapper address.","required":false,"schema":{"$ref":"#/components/schemas/Address"}},"fillerParam":{"name":"filler","in":"query","description":"Filter by filler address.","required":false,"schema":{"$ref":"#/components/schemas/Address"}},"sortKeyParam":{"name":"sortKey","in":"query","description":"Order the query results by the sort key.","required":false,"schema":{"$ref":"#/components/schemas/SortKey"}},"sortParam":{"name":"sort","in":"query","description":"Sort query. For example: `sort=gt(UNIX_TIMESTAMP)`, `sort=between(1675872827, 1675872930)`, or `lt(1675872930)`.","required":false,"schema":{"type":"string"}},"descParam":{"description":"Sort query results by sortKey in descending order.","name":"desc","in":"query","required":false,"schema":{"type":"string"}},"transactionHashParam":{"description":"The transaction hash.","name":"txHash","in":"path","required":true,"schema":{"$ref":"#/components/schemas/TransactionHash"}}},"securitySchemes":{"apiKey":{"type":"apiKey","in":"header","name":"x-api-key"}}},"security":[{"apiKey":[]}]} \ No newline at end of file +{"openapi":"3.0.0","servers":[{"description":"Uniswap trading APIs Beta","url":"https://beta.trade-api.gateway.uniswap.org/v1"},{"description":"Uniswap trading APIs","url":"https://trade-api.gateway.uniswap.org/v1"}],"info":{"version":"1.0.0","title":"Token Trading","description":"Uniswap trading APIs for fungible tokens."},"paths":{"/check_approval":{"post":{"tags":["Approval"],"summary":"Check if token approval is required","description":"Checks if the swapper has the required approval. If the swapper does not have the required approval, then the response will include the transaction to approve the token. If the swapper has the required approval, then the response will be empty. If the parameter `includeGasInfo` is set to `true`, then the response will include the gas fee for the approval transaction.","operationId":"check_approval","security":[{"apiKey":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApprovalRequest"}}}},"responses":{"200":{"$ref":"#/components/responses/ApprovalSuccess200"},"400":{"$ref":"#/components/responses/BadRequest400"},"401":{"$ref":"#/components/responses/ApprovalUnauthorized401"},"404":{"$ref":"#/components/responses/ApprovalNotFound404"},"419":{"$ref":"#/components/responses/RateLimitedErr429"},"500":{"$ref":"#/components/responses/InternalErr500"},"504":{"$ref":"#/components/responses/Timeout504"}}}},"/quote":{"post":{"tags":["Quote"],"summary":"Get a quote","description":"Get a quote according to the provided configuration. Optionally adds a fee to the quote according to the API key being used. The fee is **ALWAYS** taken from the output token. If there is a fee and the trade is `EXACT_INPUT`, then the output amount will **NOT** include the fee subtraction. For `EXACT_INPUT` swaps, use `portionBips` to calculate the fee from the quoted amount. If there is a fee and the trade is `EXACT_OUTPUT`, then the input amount will **NOT** include the fee addition to account for the fee. For `EXACT_OUTPUT` swaps, use `portionAmount` to get the fee. \n \n We also support Wrapping and Unwrapping of native tokens on their respective chains. Wrapping and Unwrapping only works for when `routingPreference` is `CLASSIC`, `BEST_PRICE`, or `BEST_PRICE_V2`. We do not support `UNISWAPX` or `UNISWAPX_V2` for these actions.","operationId":"aggregator_quote","security":[{"apiKey":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuoteRequest"}}}},"responses":{"200":{"$ref":"#/components/responses/QuoteSuccess200"},"400":{"$ref":"#/components/responses/BadRequest400"},"401":{"$ref":"#/components/responses/Unauthorized401"},"404":{"$ref":"#/components/responses/QuoteNotFound404"},"419":{"$ref":"#/components/responses/RateLimitedErr429"},"500":{"$ref":"#/components/responses/InternalErr500"},"504":{"$ref":"#/components/responses/Timeout504"}}}},"/order":{"post":{"tags":["Order"],"summary":"Create a gasless order","description":"Submits a new gasless encoded order. The order will be validated and if valid, will be submitted to the filler network. The network will try to fill the order at the quoted `startAmount`, and if not, the amount will start decaying until the `endAmount` is reached. While the order is within `decayEndTime`, the `orderStatus` is `open`. If the order does not get filled after the `decayEndTime` has passed, that is reflected in the `expired` `orderStatus`. then The order will be filled at the best price possible. Once the order is filled, `orderStatus` becomes `filled`.","operationId":"post_dutch_order","security":[{"apiKey":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderRequest"}}}},"responses":{"201":{"$ref":"#/components/responses/OrderSuccess201"},"400":{"$ref":"#/components/responses/BadRequest400"},"401":{"$ref":"#/components/responses/Unauthorized401"},"419":{"$ref":"#/components/responses/RateLimitedErr429"},"500":{"$ref":"#/components/responses/InternalErr500"},"504":{"$ref":"#/components/responses/Timeout504"}}}},"/orders":{"get":{"tags":["Order"],"summary":"Get gasless orders","description":"Retrieve gasless orders filtered by query param(s). Some fields on the order can be used as query param.","operationId":"get_dutch_order","security":[{"apiKey":[]}],"parameters":[{"$ref":"#/components/parameters/orderIdParam"},{"$ref":"#/components/parameters/orderIdsParam"},{"$ref":"#/components/parameters/limitParam"},{"$ref":"#/components/parameters/orderStatusParam"},{"$ref":"#/components/parameters/swapperParam"},{"$ref":"#/components/parameters/sortKeyParam"},{"$ref":"#/components/parameters/sortParam"},{"$ref":"#/components/parameters/fillerParam"},{"$ref":"#/components/parameters/cursorParam"}],"responses":{"200":{"$ref":"#/components/responses/OrdersSuccess200"},"400":{"$ref":"#/components/responses/BadRequest400"},"404":{"$ref":"#/components/responses/OrdersNotFound404"},"419":{"$ref":"#/components/responses/RateLimitedErr429"},"500":{"$ref":"#/components/responses/InternalErr500"},"504":{"$ref":"#/components/responses/Timeout504"}}}},"/swap":{"post":{"tags":["Swap"],"summary":"Create swap calldata","description":"Create the calldata for a swap transaction (including wrap/unwrap) against the Uniswap Protocols. If the `quote` parameter includes the fee parameters, then the calldata will include the fee disbursement. The gas estimates will be **more precise** when the the response calldata would be valid if submitted on-chain.","operationId":"create_swap_transaction","security":[{"apiKey":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSwapRequest"}}}},"responses":{"200":{"$ref":"#/components/responses/CreateSwapSuccess200"},"400":{"$ref":"#/components/responses/BadRequest400"},"401":{"$ref":"#/components/responses/SwapUnauthorized401"},"404":{"$ref":"#/components/responses/SwapNotFound404"},"419":{"$ref":"#/components/responses/RateLimitedErr429"},"500":{"$ref":"#/components/responses/InternalErr500"},"504":{"$ref":"#/components/responses/Timeout504"}}}},"/swap/{txHash}":{"get":{"tags":["Swap"],"summary":"Get swap status","description":"Get the status of a swap transaction.","operationId":"get_swap_transaction","security":[{"apiKey":[]}],"parameters":[{"$ref":"#/components/parameters/transactionHashParam"}],"responses":{"200":{"$ref":"#/components/responses/GetSwapSuccess200"},"400":{"$ref":"#/components/responses/BadRequest400"},"404":{"$ref":"#/components/responses/SwapNotFound404"},"419":{"$ref":"#/components/responses/RateLimitedErr429"},"500":{"$ref":"#/components/responses/InternalErr500"},"504":{"$ref":"#/components/responses/Timeout504"}}}},"/indicative_quote":{"post":{"tags":["IndicativeQuote"],"summary":"Get an indicative quote","description":"Get an indicative quote according to the provided configuration. The quote will not include a fee.","operationId":"indicative_quote","security":[{"apiKey":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IndicativeQuoteRequest"}}}},"responses":{"200":{"$ref":"#/components/responses/IndicativeQuoteSuccess200"},"400":{"$ref":"#/components/responses/BadRequest400"},"404":{"$ref":"#/components/responses/QuoteNotFound404"},"419":{"$ref":"#/components/responses/RateLimitedErr429"},"500":{"$ref":"#/components/responses/InternalErr500"},"504":{"$ref":"#/components/responses/Timeout504"}}}},"/send":{"post":{"tags":["Send"],"summary":"Create send calldata","description":"Create the calldata for a send transaction.","operationId":"create_send","security":[{"apiKey":[]}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSendRequest"}}}},"responses":{"200":{"$ref":"#/components/responses/CreateSendSuccess200"},"400":{"$ref":"#/components/responses/BadRequest400"},"401":{"$ref":"#/components/responses/Unauthorized401"},"404":{"$ref":"#/components/responses/SendNotFound404"},"429":{"$ref":"#/components/responses/RateLimitedErr429"},"500":{"$ref":"#/components/responses/InternalErr500"},"504":{"$ref":"#/components/responses/Timeout504"}}}}},"components":{"responses":{"OrdersSuccess200":{"description":"The request orders matching the query parameters.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetOrdersResponse"}}}},"OrderSuccess201":{"description":"Encoded order submitted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderResponse"}}}},"QuoteSuccess200":{"description":"Quote request successful.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuoteResponse"}}}},"ApprovalSuccess200":{"description":"Check approval successful.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApprovalResponse"}}}},"CreateSendSuccess200":{"description":"Create send successful.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSendResponse"}}}},"CreateSwapSuccess200":{"description":"Create swap successful.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSwapResponse"}}}},"GetSwapSuccess200":{"description":"Get swap successful.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetSwapResponse"}}}},"BadRequest400":{"description":"RequestValidationError, Bad Input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err400"}}}},"ApprovalUnauthorized401":{"description":"UnauthorizedError eg. Account is blocked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err401"}}}},"ApprovalNotFound404":{"description":"ResourceNotFound eg. Token allowance not found or Gas info not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err404"}}}},"Unauthorized401":{"description":"UnauthorizedError eg. Account is blocked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err401"}}}},"QuoteNotFound404":{"description":"ResourceNotFound eg. No quotes available or Gas fee/price not available","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err404"}}}},"SendNotFound404":{"description":"ResourceNotFound eg. Gas fee not available","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err404"}}}},"SwapBadRequest400":{"description":"RequestValidationError, Bad Input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err400"}}}},"SwapUnauthorized401":{"description":"UnauthorizedError eg. Account is blocked or Fee is not enabled.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err401"}}}},"SwapNotFound404":{"description":"ResourceNotFound eg. No quotes available or Gas fee/price not available","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err404"}}}},"OrdersNotFound404":{"description":"Orders not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err404"}}}},"RateLimitedErr429":{"description":"Ratelimited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err429"}}}},"InternalErr500":{"description":"Unexpected error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err500"}}}},"Timeout504":{"description":"Request duration limit reached.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Err504"}}}},"IndicativeQuoteSuccess200":{"description":"Indicative quote request successful.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IndicativeQuoteResponse"}}}}},"schemas":{"NullablePermit":{"allOf":[{"$ref":"#/components/schemas/Permit"},{"type":"object","nullable":true}]},"TokenAmount":{"type":"string"},"SwapStatus":{"type":"string","enum":["pending","success","error"]},"GetSwapResponse":{"type":"object","properties":{"requestId":{"$ref":"#/components/schemas/RequestId"},"status":{"$ref":"#/components/schemas/SwapStatus"}},"required":["requestId","status"]},"CreateSwapRequest":{"type":"object","description":"The parameters **signature** and **permitData** should only be included if *permitData* was returned from **/quote**.","properties":{"quote":{"oneOf":[{"$ref":"#/components/schemas/ClassicQuote"},{"$ref":"#/components/schemas/WrapUnwrapQuote"}]},"signature":{"type":"string","description":"The signed permit."},"includeGasInfo":{"type":"boolean","default":false,"deprecated":true,"description":"Use `refreshGasPrice` instead."},"refreshGasPrice":{"type":"boolean","default":false,"description":"If true, the gas price will be re-fetched from the network."},"simulateTransaction":{"type":"boolean","default":false,"description":"If true, the transaction will be simulated. If the simulation results on an onchain error, endpoint will return an error."},"permitData":{"allOf":[{"$ref":"#/components/schemas/Permit"}]},"safetyMode":{"$ref":"#/components/schemas/SwapSafetyMode"},"deadline":{"type":"integer","description":"The deadline for the swap in unix timestamp format. If the deadline is not defined OR in the past then the default deadline is 30 minutes."},"urgency":{"$ref":"#/components/schemas/Urgency"}},"required":["quote"]},"CreateSendRequest":{"type":"object","properties":{"sender":{"$ref":"#/components/schemas/Address"},"recipient":{"$ref":"#/components/schemas/Address"},"token":{"$ref":"#/components/schemas/Address"},"amount":{"$ref":"#/components/schemas/TokenAmount"},"chainId":{"$ref":"#/components/schemas/ChainId"},"urgency":{"$ref":"#/components/schemas/Urgency"}},"required":["sender","recipient","token","amount"]},"Address":{"type":"string","pattern":"^(0x)?[0-9a-fA-F]{40}$"},"ClassicGasUseEstimateUSD":{"description":"The gas fee you would pay if you opted for a CLASSIC swap over a Uniswap X order in terms of USD.","type":"number"},"CreateSwapResponse":{"type":"object","properties":{"requestId":{"$ref":"#/components/schemas/RequestId"},"swap":{"$ref":"#/components/schemas/TransactionRequest"},"gasFee":{"type":"string"}},"required":["requestId","swap"]},"CreateSendResponse":{"type":"object","properties":{"requestId":{"$ref":"#/components/schemas/RequestId"},"send":{"$ref":"#/components/schemas/TransactionRequest"},"gasFee":{"type":"string"},"gasFeeUSD":{"type":"number"}},"required":["requestId","send"]},"QuoteResponse":{"type":"object","properties":{"requestId":{"$ref":"#/components/schemas/RequestId"},"quote":{"$ref":"#/components/schemas/Quote"},"routing":{"$ref":"#/components/schemas/Routing"},"permitData":{"$ref":"#/components/schemas/NullablePermit"}},"required":["routing","quote","permitData","requestId"]},"QuoteRequest":{"type":"object","properties":{"type":{"$ref":"#/components/schemas/TradeType"},"amount":{"type":"string"},"tokenInChainId":{"$ref":"#/components/schemas/ChainId"},"tokenOutChainId":{"$ref":"#/components/schemas/ChainId"},"tokenIn":{"type":"string"},"tokenOut":{"type":"string"},"swapper":{"$ref":"#/components/schemas/Address"},"slippageTolerance":{"description":"For **Classic** swaps, the slippage tolerance is the maximum amount the price can change between the time the transaction is submitted and the time it is executed. The slippage tolerance is represented as a percentage of the total value of the swap. \n\n Slippage tolerance works differently in **DutchLimit** swaps, it does not set a limit on the Spread in an order. See [here](https://uniswap-docs.readme.io/reference/faqs#why-do-the-uniswapx-quotes-have-more-slippage-than-the-tolerance-i-set) for more information. \n\n **NOTE**: slippage is in terms of trade type. If the trade type is `EXACT_INPUT`, then the slippage is in terms of the output token. If the trade type is `EXACT_OUTPUT`, then the slippage is in terms of the input token.","type":"number"},"autoSlippage":{"$ref":"#/components/schemas/AutoSlippage"},"routingPreference":{"$ref":"#/components/schemas/RoutingPreference"},"spreadOptimization":{"$ref":"#/components/schemas/SpreadOptimization"},"urgency":{"$ref":"#/components/schemas/Urgency"}},"required":["type","amount","tokenInChainId","tokenOutChainId","tokenIn","tokenOut","swapper"]},"GetOrdersResponse":{"type":"object","properties":{"requestId":{"$ref":"#/components/schemas/RequestId"},"orders":{"type":"array","items":{"$ref":"#/components/schemas/UniswapXOrder"}},"cursor":{"type":"string"}},"required":["orders","requestId"]},"OrderResponse":{"type":"object","properties":{"requestId":{"$ref":"#/components/schemas/RequestId"},"orderId":{"type":"string"},"orderStatus":{"$ref":"#/components/schemas/OrderStatus"}},"required":["requestId","orderId","orderStatus"]},"OrderRequest":{"type":"object","properties":{"signature":{"type":"string","description":"The signed permit."},"quote":{"oneOf":[{"$ref":"#/components/schemas/DutchQuote"},{"$ref":"#/components/schemas/DutchQuoteV2"}]},"routing":{"$ref":"#/components/schemas/Routing"}},"required":["signature","quote"]},"Urgency":{"type":"string","enum":["normal","fast","urgent"],"description":"The urgency determines the urgency of the transaction. The default value is `urgent`.","default":"urgent"},"Err400":{"type":"object","properties":{"errorCode":{"default":"RequestValidationError","type":"string"},"detail":{"type":"string"}}},"Err401":{"type":"object","properties":{"errorCode":{"default":"UnauthorizedError","type":"string"},"detail":{"type":"string"}}},"Err404":{"type":"object","properties":{"errorCode":{"default":"ResourceNotFound","type":"string"},"detail":{"type":"string"}}},"Err429":{"type":"object","properties":{"errorCode":{"default":"Ratelimited","type":"string"},"detail":{"type":"string"}}},"Err500":{"type":"object","properties":{"errorCode":{"default":"InternalServerError","type":"string"},"detail":{"type":"string"}}},"Err504":{"type":"object","properties":{"errorCode":{"default":"Timeout","type":"string"},"detail":{"type":"string"}}},"ChainId":{"type":"number","enum":[1,10,56,137,8453,42161,81457,43114,42220,7777777,324]},"OrderInput":{"type":"object","properties":{"token":{"type":"string"},"startAmount":{"type":"string"},"endAmount":{"type":"string"}},"required":["token"]},"OrderOutput":{"type":"object","properties":{"token":{"type":"string"},"startAmount":{"type":"string"},"endAmount":{"type":"string"},"isFeeOutput":{"type":"boolean"},"recipient":{"type":"string"}},"required":["token"]},"CosignerData":{"type":"object","properties":{"decayStartTime":{"type":"number"},"decayEndTime":{"type":"number"},"exclusiveFiller":{"type":"string"},"inputOverride":{"type":"string"},"outputOverrides":{"type":"array","items":{"type":"string"}}}},"SettledAmount":{"type":"object","properties":{"tokenOut":{"$ref":"#/components/schemas/Address"},"amountOut":{"type":"string"},"tokenIn":{"$ref":"#/components/schemas/Address"},"amountIn":{"type":"string"}}},"OrderType":{"type":"string","enum":["DutchLimit","Dutch","Dutch_V2"]},"UniswapXOrder":{"type":"object","properties":{"type":{"$ref":"#/components/schemas/OrderType"},"encodedOrder":{"type":"string"},"signature":{"type":"string"},"nonce":{"type":"string"},"orderStatus":{"$ref":"#/components/schemas/OrderStatus"},"orderId":{"type":"string"},"chainId":{"$ref":"#/components/schemas/ChainId"},"quoteId":{"type":"string"},"swapper":{"type":"string"},"txHash":{"type":"string"},"input":{"$ref":"#/components/schemas/OrderInput"},"outputs":{"type":"array","items":{"$ref":"#/components/schemas/OrderOutput"}},"settledAmounts":{"type":"array","items":{"$ref":"#/components/schemas/SettledAmount"}},"cosignature":{"type":"string"},"cosignerData":{"$ref":"#/components/schemas/CosignerData"}},"required":["encodedOrder","signature","nonce","orderId","orderStatus","chainId","type"]},"SortKey":{"type":"string","enum":["createdAt"]},"OrderId":{"type":"string"},"OrderIds":{"type":"string"},"OrderStatus":{"type":"string","enum":["open","expired","error","cancelled","filled","unverified","insufficient-funds"]},"Permit":{"type":"object","properties":{"domain":{"type":"object"},"values":{"type":"object"},"types":{"type":"object"}}},"DutchInput":{"type":"object","properties":{"startAmount":{"type":"string"},"endAmount":{"type":"string"},"token":{"type":"string"}},"required":["startAmount","endAmount","type"]},"DutchOutput":{"type":"object","properties":{"startAmount":{"type":"string"},"endAmount":{"type":"string"},"token":{"type":"string"},"recipient":{"type":"string"}},"required":["startAmount","endAmount","token","recipient"]},"DutchOrderInfo":{"type":"object","properties":{"chainId":{"$ref":"#/components/schemas/ChainId"},"nonce":{"type":"string"},"reactor":{"type":"string"},"swapper":{"type":"string"},"deadline":{"type":"number"},"additionalValidationContract":{"type":"string"},"additionalValidationData":{"type":"string"},"decayStartTime":{"type":"number"},"decayEndTime":{"type":"number"},"exclusiveFiller":{"type":"string"},"exclusivityOverrideBps":{"type":"string"},"input":{"$ref":"#/components/schemas/DutchInput"},"outputs":{"type":"array","items":{"$ref":"#/components/schemas/DutchOutput"}}},"required":["chainId","nonce","reactor","swapper","deadline","validationContract","validationData","startTime","endTime","exclusiveFiller","exclusivityOverrideBps","input","outputs"]},"DutchOrderInfoV2":{"type":"object","properties":{"chainId":{"$ref":"#/components/schemas/ChainId"},"nonce":{"type":"string"},"reactor":{"type":"string"},"swapper":{"type":"string"},"deadline":{"type":"number"},"additionalValidationContract":{"type":"string"},"additionalValidationData":{"type":"string"},"input":{"$ref":"#/components/schemas/DutchInput"},"outputs":{"type":"array","items":{"$ref":"#/components/schemas/DutchOutput"}},"cosigner":{"$ref":"#/components/schemas/Address"}},"required":["chainId","nonce","reactor","swapper","deadline","validationContract","validationData","startTime","endTime","exclusiveFiller","exclusivityOverrideBps","input","outputs"]},"DutchQuote":{"type":"object","properties":{"encodedOrder":{"type":"string"},"orderId":{"type":"string"},"orderInfo":{"$ref":"#/components/schemas/DutchOrderInfo"},"portionBips":{"type":"number"},"portionAmount":{"type":"string"},"portionRecipient":{"$ref":"#/components/schemas/Address"},"quoteId":{"type":"string"},"slippageTolerance":{"type":"number"},"classicGasUseEstimateUSD":{"$ref":"#/components/schemas/ClassicGasUseEstimateUSD"}},"required":["encodedOrder","orderInfo","orderId"]},"DutchQuoteV2":{"type":"object","properties":{"encodedOrder":{"type":"string"},"orderId":{"type":"string"},"orderInfo":{"$ref":"#/components/schemas/DutchOrderInfoV2"},"portionBips":{"type":"number"},"portionAmount":{"type":"string"},"portionRecipient":{"$ref":"#/components/schemas/Address"},"quoteId":{"type":"string"},"slippageTolerance":{"type":"number"},"deadlineBufferSecs":{"type":"number"},"classicGasUseEstimateUSD":{"$ref":"#/components/schemas/ClassicGasUseEstimateUSD"}},"required":["encodedOrder","orderInfo","orderId"]},"TradeType":{"type":"string","enum":["EXACT_INPUT","EXACT_OUTPUT"]},"Routing":{"type":"string","enum":["DUTCH_LIMIT","CLASSIC","DUTCH_V2"]},"Quote":{"oneOf":[{"$ref":"#/components/schemas/DutchQuote"},{"$ref":"#/components/schemas/ClassicQuote"},{"$ref":"#/components/schemas/WrapUnwrapQuote"},{"$ref":"#/components/schemas/DutchQuoteV2"}]},"ApprovalRequest":{"type":"object","properties":{"walletAddress":{"$ref":"#/components/schemas/Address"},"token":{"$ref":"#/components/schemas/Address"},"amount":{"$ref":"#/components/schemas/TokenAmount"},"chainId":{"$ref":"#/components/schemas/ChainId"},"urgency":{"$ref":"#/components/schemas/Urgency"},"includeGasInfo":{"type":"boolean","default":false},"tokenOut":{"type":"string","pattern":"^(0x)?[0-9a-fA-F]{40}$","description":"relevant for if we go from a wrapped token to a native token (unwrapping)"},"tokenOutChainId":{"type":"number","enum":[1,10,56,137,8453,42161,81457,43114,42220,7777777,324],"default":1,"description":"relevant for if we go from a wrapped token to a native token (unwrapping)"}},"required":["walletAddress","token","amount"]},"ApprovalResponse":{"type":"object","properties":{"requestId":{"$ref":"#/components/schemas/RequestId"},"approval":{"$ref":"#/components/schemas/TransactionRequest"},"gasFee":{"type":"string"}},"required":["requestId","approval"]},"ClassicQuote":{"type":"object","properties":{"input":{"$ref":"#/components/schemas/ClassicInput"},"output":{"$ref":"#/components/schemas/ClassicOutput"},"swapper":{"$ref":"#/components/schemas/Address"},"chainId":{"$ref":"#/components/schemas/ChainId"},"slippage":{"type":"number"},"tradeType":{"$ref":"#/components/schemas/TradeType"},"gasFee":{"type":"string","description":"The gas fee in terms of wei. It does NOT include the additional gas for token approvals."},"gasFeeUSD":{"type":"string","description":"The gas fee in terms of USD. It does NOT include the additional gas for token approvals."},"gasFeeQuote":{"type":"string","description":"The gas fee in terms of the quoted currency. It does NOT include the additional gas for token approvals."},"route":{"type":"array","items":{"type":"array","items":{"oneOf":[{"$ref":"#/components/schemas/V3PoolInRoute"},{"$ref":"#/components/schemas/V2PoolInRoute"}]}}},"portionBips":{"type":"number","description":"The portion of the swap that will be taken as a fee. The fee will be taken from the output token."},"portionAmount":{"type":"string","description":"The amount of the swap that will be taken as a fee. The fee will be taken from the output token."},"portionRecipient":{"$ref":"#/components/schemas/Address"},"routeString":{"type":"string","description":"The route in string format."},"quoteId":{"type":"string","description":"The quote id. Used for analytics purposes."},"gasUseEstimate":{"type":"string","description":"The estimated gas use. It does NOT include the additional gas for token approvals."},"blockNumber":{"type":"string","description":"The current block number."},"gasPrice":{"type":"string","description":"The gas price in terms of wei for pre EIP1559 transactions."},"maxFeePerGas":{"type":"string","description":"The maximum fee per gas in terms of wei for EIP1559 transactions."},"maxPriorityFeePerGas":{"type":"string","description":"The maximum priority fee per gas in terms of wei for EIP1559 transactions."},"txFailureReasons":{"type":"array","items":{"$ref":"#/components/schemas/TransactionFailureReason"}},"priceImpact":{"type":"number","description":"The impact the trade has on the market price of the pool, between 0-100 percent"}}},"WrapUnwrapQuote":{"type":"object","properties":{"swapper":{"$ref":"#/components/schemas/Address"},"input":{"$ref":"#/components/schemas/ClassicInput"},"output":{"$ref":"#/components/schemas/ClassicOutput"},"chainId":{"$ref":"#/components/schemas/ChainId"},"tradeType":{"$ref":"#/components/schemas/TradeType"},"gasFee":{"type":"string","description":"The gas fee in terms of wei."},"gasFeeUSD":{"type":"string","description":"The gas fee in terms of USD."},"gasFeeQuote":{"type":"string","description":"The gas fee in terms of the quoted currency."},"gasUseEstimate":{"type":"string","description":"The estimated gas use."},"gasPrice":{"type":"string","description":"The gas price in terms of wei for pre EIP1559 transactions."},"maxFeePerGas":{"type":"string","description":"The maximum fee per gas in terms of wei for EIP1559 transactions."},"maxPriorityFeePerGas":{"type":"string","description":"The maximum priority fee per gas in terms of wei for EIP1559 transactions."}}},"TokenInRoute":{"type":"object","properties":{"address":{"$ref":"#/components/schemas/Address"},"chainId":{"$ref":"#/components/schemas/ChainId"},"symbol":{"type":"string"},"decimals":{"type":"string"},"buyFeeBps":{"type":"string"},"sellFeeBps":{"type":"string"}}},"V2Reserve":{"type":"object","properties":{"token":{"$ref":"#/components/schemas/TokenInRoute"},"quotient":{"type":"string"}}},"V2PoolInRoute":{"type":"object","properties":{"type":{"type":"string","default":"v2-pool"},"address":{"$ref":"#/components/schemas/Address"},"tokenIn":{"$ref":"#/components/schemas/TokenInRoute"},"tokenOut":{"$ref":"#/components/schemas/TokenInRoute"},"reserve0":{"$ref":"#/components/schemas/V2Reserve"},"reserve1":{"$ref":"#/components/schemas/V2Reserve"},"amountIn":{"type":"string"},"amountOut":{"type":"string"}}},"V3PoolInRoute":{"type":"object","properties":{"type":{"type":"string","default":"v3-pool"},"address":{"$ref":"#/components/schemas/Address"},"tokenIn":{"$ref":"#/components/schemas/TokenInRoute"},"tokenOut":{"$ref":"#/components/schemas/TokenInRoute"},"sqrtRatioX96":{"type":"string"},"liquidity":{"type":"string"},"tickCurrent":{"type":"string"},"fee":{"type":"string"},"amountIn":{"type":"string"},"amountOut":{"type":"string"}}},"TransactionHash":{"type":"string","pattern":"^(0x)?[0-9a-fA-F]{64}$"},"ClassicInput":{"type":"object","properties":{"token":{"$ref":"#/components/schemas/Address"},"amount":{"type":"string"}}},"ClassicOutput":{"type":"object","properties":{"token":{"$ref":"#/components/schemas/Address"},"amount":{"type":"string"},"recipient":{"$ref":"#/components/schemas/Address"}}},"RequestId":{"type":"string"},"SpreadOptimization":{"type":"string","enum":["EXECUTION","PRICE"],"description":"For **Dutch Limit** orders only. When set to `EXECUTION`, quotes optimize for looser spreads at higher fill rates. When set to `PRICE`, quotes optimize for tighter spreads at lower fill rates","default":"EXECUTION"},"AutoSlippage":{"type":"string","enum":["DEFAULT"],"description":"For **Classic** swaps only. The auto slippage strategy to employ. If auto slippage is not defined then we don't compute it. If the auto slippage strategy is `DEFAULT`, then the swap will use the default slippage tolerance computation. You cannot define auto slippage and slippage tolerance at the same time. \n\n **NOTE**: slippage is in terms of trade type. If the trade type is `EXACT_INPUT`, then the slippage is in terms of the output token. If the trade type is `EXACT_OUTPUT`, then the slippage is in terms of the input token.","default":"undefined"},"RoutingPreference":{"type":"string","description":"The routing preference determines which protocol to use for the swap. If the routing preference is `UNISWAPX`, then the swap will be routed through the UniswapX Dutch Auction Protocol. If the routing preference is `CLASSIC`, then the swap will be routed through the Classic Protocol. If the routing preference is `BEST_PRICE`, then the swap will be routed through the protocol that provides the best price. When `UNIXWAPX_V2` is passed, the swap will be routed through the UniswapX V2 Dutch Auction Protocol. When `V3_ONLY` is passed, the swap will be routed ONLY through the Uniswap V3 Protocol. When `V2_ONLY` is passed, the swap will be routed ONLY through the Uniswap V2 Protocol.","enum":["CLASSIC","UNISWAPX","BEST_PRICE","BEST_PRICE_V2","UNISWAPX_V2","V3_ONLY","V2_ONLY"],"default":"BEST_PRICE"},"TransactionRequest":{"type":"object","properties":{"to":{"$ref":"#/components/schemas/Address"},"from":{"$ref":"#/components/schemas/Address"},"data":{"type":"string","description":"The calldata for the transaction."},"value":{"type":"string","description":"The value of the transaction in terms of wei in hex format."},"gasLimit":{"type":"string"},"chainId":{"type":"integer"},"maxFeePerGas":{"type":"string"},"maxPriorityFeePerGas":{"type":"string"},"gasPrice":{"type":"string"}},"required":["to","from","data","value","chainId"]},"TransactionFailureReason":{"type":"string","enum":["SIMULATION_ERROR","UNSUPPORTED_SIMULATION"]},"SwapSafetyMode":{"type":"string","enum":["SAFE"],"description":"The safety mode determines the safety level of the swap. If the safety mode is `SAFE`, then the swap will include a SWEEP for the native token."},"IndicativeQuoteRequest":{"type":"object","properties":{"type":{"$ref":"#/components/schemas/TradeType"},"amount":{"type":"string"},"tokenInChainId":{"$ref":"#/components/schemas/ChainId"},"tokenOutChainId":{"$ref":"#/components/schemas/ChainId"},"tokenIn":{"type":"string"},"tokenOut":{"type":"string"}},"required":["type","amount","tokenInChainId","tokenOutChainId","tokenIn","tokenOut"]},"IndicativeQuoteResponse":{"type":"object","properties":{"requestId":{"$ref":"#/components/schemas/RequestId"},"input":{"$ref":"#/components/schemas/IndicativeQuoteToken"},"output":{"$ref":"#/components/schemas/IndicativeQuoteToken"},"type":{"$ref":"#/components/schemas/TradeType"}},"required":["requestId","input","output","type"]},"IndicativeQuoteToken":{"type":"object","properties":{"amount":{"type":"string"},"chainId":{"$ref":"#/components/schemas/ChainId"},"token":{"$ref":"#/components/schemas/Address"}}}},"parameters":{"addressParam":{"name":"address","in":"path","schema":{"$ref":"#/components/schemas/Address"},"required":true},"tokenIdParam":{"name":"tokenId","in":"path","schema":{"type":"string"},"required":true},"cursorParam":{"name":"cursor","in":"query","schema":{"type":"string"},"required":false},"limitParam":{"name":"limit","in":"query","schema":{"type":"number"},"required":false},"chainParam":{"name":"chain","in":"query","schema":{"$ref":"#/components/schemas/ChainId"},"required":false},"addressPathParam":{"name":"address","in":"query","schema":{"$ref":"#/components/schemas/Address"},"required":false},"orderStatusParam":{"name":"orderStatus","in":"query","description":"Filter by order status.","required":false,"schema":{"$ref":"#/components/schemas/OrderStatus"}},"orderIdParam":{"name":"orderId","in":"query","required":false,"schema":{"$ref":"#/components/schemas/OrderId"}},"orderIdsParam":{"name":"orderIds","in":"query","required":false,"description":"ids split by commas","schema":{"$ref":"#/components/schemas/OrderIds"}},"swapperParam":{"name":"swapper","in":"query","description":"Filter by swapper address.","required":false,"schema":{"$ref":"#/components/schemas/Address"}},"fillerParam":{"name":"filler","in":"query","description":"Filter by filler address.","required":false,"schema":{"$ref":"#/components/schemas/Address"}},"sortKeyParam":{"name":"sortKey","in":"query","description":"Order the query results by the sort key.","required":false,"schema":{"$ref":"#/components/schemas/SortKey"}},"sortParam":{"name":"sort","in":"query","description":"Sort query. For example: `sort=gt(UNIX_TIMESTAMP)`, `sort=between(1675872827, 1675872930)`, or `lt(1675872930)`.","required":false,"schema":{"type":"string"}},"descParam":{"description":"Sort query results by sortKey in descending order.","name":"desc","in":"query","required":false,"schema":{"type":"string"}},"transactionHashParam":{"description":"The transaction hash.","name":"txHash","in":"path","required":true,"schema":{"$ref":"#/components/schemas/TransactionHash"}}},"securitySchemes":{"apiKey":{"type":"apiKey","in":"header","name":"x-api-key"}}},"security":[{"apiKey":[]}]} \ No newline at end of file diff --git a/packages/wallet/src/data/utils.ts b/packages/wallet/src/data/utils.ts index ba715f1f87f..9c4f1b52605 100644 --- a/packages/wallet/src/data/utils.ts +++ b/packages/wallet/src/data/utils.ts @@ -31,26 +31,22 @@ export function isError(networkStatus: NetworkStatus, hasData: boolean): boolean } export function useRefetchQueries(): ( - include?: Parameters['refetchQueries']>[0]['include'] + include?: Parameters['refetchQueries']>[0]['include'], ) => void { const client = useApolloClient() return useCallback( - async ( - include: Parameters< - ApolloClient['refetchQueries'] - >[0]['include'] = 'active' - ) => { + async (include: Parameters['refetchQueries']>[0]['include'] = 'active') => { await client?.refetchQueries({ include }) }, - [client] + [client], ) } export async function createSignedRequestBody( data: T, account: Account, - signerManager: SignerManager + signerManager: SignerManager, ): Promise<{ requestBody: T & AuthData; signature: string }> { const requestBody: T & AuthData = { ...data, @@ -65,7 +61,7 @@ export async function createSignedRequestBody( export async function createSignedRequestParams( params: T, account: Account, - signerManager: SignerManager + signerManager: SignerManager, ): Promise<{ requestParams: T & AuthData; signature: string }> { const requestParams: T & AuthData = { ...params, @@ -80,12 +76,12 @@ export async function createSignedRequestParams( export async function createOnRampTransactionsAuth( limit: number, account: Account, - signerManager: SignerManager + signerManager: SignerManager, ): Promise { const { requestParams, signature } = await createSignedRequestParams( { limit }, // Parameter needed by graphql server when fetching onramp transactions account, - signerManager + signerManager, ) return { queryParams: objectToQueryString(requestParams), signature } } diff --git a/packages/wallet/src/features/activity/hooks.ts b/packages/wallet/src/features/activity/hooks.ts index f486ff5bd7a..7531f996a28 100644 --- a/packages/wallet/src/features/activity/hooks.ts +++ b/packages/wallet/src/features/activity/hooks.ts @@ -1,18 +1,13 @@ import { ApolloError, NetworkStatus } from '@apollo/client' import { useCallback, useMemo } from 'react' +import { PollingInterval } from 'uniswap/src/constants/misc' import { useFeedTransactionListQuery, useTransactionListQuery, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { PollingInterval } from 'wallet/src/constants/misc' +import { usePersistedError } from 'uniswap/src/features/dataApi/utils' import { isNonPollingRequestInFlight } from 'wallet/src/data/utils' -import { - LoadingItem, - SectionHeader, - isLoadingItem, - isSectionHeader, -} from 'wallet/src/features/activity/utils' -import { usePersistedError } from 'wallet/src/features/dataApi/utils' +import { LoadingItem, SectionHeader, isLoadingItem, isSectionHeader } from 'wallet/src/features/activity/utils' import { useLocalizedDayjs } from 'wallet/src/features/language/localizedDayjs' import { formatTransactionsByDate, @@ -27,7 +22,7 @@ const LOADING_DATA = [LOADING_ITEM(1), LOADING_ITEM(2), LOADING_ITEM(3), LOADING export function useFormattedTransactionDataForFeed( addresses: Address[], - hideSpamTokens: boolean + hideSpamTokens: boolean, ): { hasData: boolean isLoading: boolean @@ -74,7 +69,7 @@ export function useFormattedTransactionDataForFeed( const localizedDayjs = useLocalizedDayjs() const { pending, last24hTransactionList, priorByMonthTransactionList } = useMemo( () => formatTransactionsByDate(transactions, localizedDayjs), - [transactions, localizedDayjs] + [transactions, localizedDayjs], ) const hasTransactions = transactions && transactions.length > 0 @@ -84,8 +79,7 @@ export function useFormattedTransactionDataForFeed( const isError = usePersistedError(requestLoading, requestError) // show loading if no data and fetching, or refetching when there is error (for UX when "retry" is clicked). - const showLoading = - (!hasData && isLoading) || (Boolean(isError) && networkStatus === NetworkStatus.refetch) + const showLoading = (!hasData && isLoading) || (Boolean(isError) && networkStatus === NetworkStatus.refetch) const sectionData = useMemo(() => { if (showLoading) { @@ -108,7 +102,7 @@ export function useFormattedTransactionDataForFeed( } return accum }, - [] + [], ), ] }, [showLoading, hasTransactions, pending, last24hTransactionList, priorByMonthTransactionList]) @@ -127,8 +121,8 @@ export function useFormattedTransactionDataForActivity( hideSpamTokens: boolean, useMergeLocalFunction: ( address: Address, - remoteTransactions: TransactionDetails[] | undefined - ) => TransactionDetails[] | undefined + remoteTransactions: TransactionDetails[] | undefined, + ) => TransactionDetails[] | undefined, ): { hasData: boolean isLoading: boolean @@ -165,7 +159,7 @@ export function useFormattedTransactionDataForActivity( // for transactions, use the transaction hash as the key return info.id }, - [address] + [address], ) const formattedTransactions = useMemo(() => { @@ -182,7 +176,7 @@ export function useFormattedTransactionDataForActivity( const localizedDayjs = useLocalizedDayjs() const { pending, last24hTransactionList, priorByMonthTransactionList } = useMemo( () => formatTransactionsByDate(transactions, localizedDayjs), - [transactions, localizedDayjs] + [transactions, localizedDayjs], ) const hasTransactions = transactions && transactions.length > 0 @@ -192,8 +186,7 @@ export function useFormattedTransactionDataForActivity( const isError = usePersistedError(requestLoading, requestError) // show loading if no data and fetching, or refetching when there is error (for UX when "retry" is clicked). - const showLoading = - (!hasData && isLoading) || (Boolean(isError) && networkStatus === NetworkStatus.refetch) + const showLoading = (!hasData && isLoading) || (Boolean(isError) && networkStatus === NetworkStatus.refetch) const sectionData = useMemo(() => { if (showLoading) { @@ -216,7 +209,7 @@ export function useFormattedTransactionDataForActivity( } return accum }, - [] + [], ), ] }, [showLoading, hasTransactions, pending, last24hTransactionList, priorByMonthTransactionList]) diff --git a/packages/wallet/src/features/activity/useActivityData.tsx b/packages/wallet/src/features/activity/useActivityData.tsx index 725b32cebe5..ef41df84216 100644 --- a/packages/wallet/src/features/activity/useActivityData.tsx +++ b/packages/wallet/src/features/activity/useActivityData.tsx @@ -4,28 +4,22 @@ import { StyleProp, ViewStyle } from 'react-native' import { Flex, Loader, Text, isWeb } from 'ui/src' import { NoTransactions } from 'ui/src/components/icons' import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard' +import { TransactionState } from 'uniswap/src/features/transactions/transactionState/types' import { useWalletNavigation } from 'wallet/src/contexts/WalletNavigationContext' import { useFormattedTransactionDataForActivity } from 'wallet/src/features/activity/hooks' import { LoadingItem, SectionHeader } from 'wallet/src/features/activity/utils' import { AuthTrigger } from 'wallet/src/features/auth/types' import TransactionSummaryLayout from 'wallet/src/features/transactions/SummaryCards/SummaryItems/TransactionSummaryLayout' import { SwapSummaryCallbacks } from 'wallet/src/features/transactions/SummaryCards/types' -import { - ActivityItemRenderer, - generateActivityItemRenderer, -} from 'wallet/src/features/transactions/SummaryCards/utils' -import { - useCreateSwapFormState, - useMergeLocalAndRemoteTransactions, -} from 'wallet/src/features/transactions/hooks' +import { ActivityItemRenderer, generateActivityItemRenderer } from 'wallet/src/features/transactions/SummaryCards/utils' +import { useCreateSwapFormState, useMergeLocalAndRemoteTransactions } from 'wallet/src/features/transactions/hooks' import { useMostRecentSwapTx } from 'wallet/src/features/transactions/swap/hooks/useMostRecentSwapTx' -import { TransactionState } from 'wallet/src/features/transactions/transactionState/types' import { TransactionDetails } from 'wallet/src/features/transactions/types' import { useHideSpamTokensSetting } from 'wallet/src/features/wallet/hooks' -const SectionTitle = ({ title }: { title: string }): JSX.Element => ( +const SectionTitle = ({ title, index }: { title: string; index?: number }): JSX.Element => ( - + {title} @@ -81,16 +75,15 @@ export function useActivityData({ , SectionTitle, swapCallbacks, - authTrigger + authTrigger, ) }, [swapCallbacks, authTrigger]) - const { onRetry, hasData, isLoading, isError, sectionData, keyExtractor } = - useFormattedTransactionDataForActivity( - owner, - hideSpamTokens, - useMergeLocalAndRemoteTransactions - ) + const { onRetry, hasData, isLoading, isError, sectionData, keyExtractor } = useFormattedTransactionDataForActivity( + owner, + hideSpamTokens, + useMergeLocalAndRemoteTransactions, + ) const errorCard = ( @@ -105,9 +98,7 @@ export function useActivityData({ const emptyListView = ( (objectsWithAddress: T[]): T[] { // the input array must be objects that have an obj.address field // had to cast to any because ts doesn't recognize it as HasAddress... maybe issue with unique - return unique( - objectsWithAddress, - (v, i, a) => a.findIndex((v2) => v2.address === v.address) === i - ) + return unique(objectsWithAddress, (v, i, a) => a.findIndex((v2) => v2.address === v.address) === i) } diff --git a/packages/wallet/src/features/auth/saga.ts b/packages/wallet/src/features/auth/saga.ts index 961be7b7862..c0a3db70ce4 100644 --- a/packages/wallet/src/features/auth/saga.ts +++ b/packages/wallet/src/features/auth/saga.ts @@ -1,11 +1,8 @@ import { call } from 'typed-redux-saga' +import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants' +import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { logger } from 'utilities/src/logger/logger' -import { - AuthActionType, - AuthBaseParams, - AuthSagaError, - UnlockParams, -} from 'wallet/src/features/auth/types' +import { AuthActionType, AuthBaseParams, AuthSagaError, UnlockParams } from 'wallet/src/features/auth/types' import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring' import { createMonitoredSaga } from 'wallet/src/utils/saga' @@ -25,11 +22,19 @@ function* unlock({ password }: UnlockParams) { if (!success) { throw new Error(AuthSagaError.InvalidPassword) } + yield* call(sendAnalyticsEvent, ExtensionEventName.ChangeLockedState, { + locked: false, + location: 'sidebar', + }) } function* lock() { logger.debug('authSaga', 'lock', `Locking wallet`) yield* call(Keyring.lock) + yield* call(sendAnalyticsEvent, ExtensionEventName.ChangeLockedState, { + locked: true, + location: 'sidebar', + }) } export const { diff --git a/packages/wallet/src/features/auth/types.ts b/packages/wallet/src/features/auth/types.ts index 2dba010b526..a70c60c62e6 100644 --- a/packages/wallet/src/features/auth/types.ts +++ b/packages/wallet/src/features/auth/types.ts @@ -20,7 +20,4 @@ export interface LockParams extends AuthBaseParams { type: AuthActionType.Lock } -export type AuthTrigger = (args: { - successCallback: () => void - failureCallback: () => void -}) => Promise +export type AuthTrigger = (args: { successCallback: () => void; failureCallback: () => void }) => Promise diff --git a/packages/wallet/src/features/behaviorHistory/selectors.ts b/packages/wallet/src/features/behaviorHistory/selectors.ts index 5b934deb1e1..18ca836bc8a 100644 --- a/packages/wallet/src/features/behaviorHistory/selectors.ts +++ b/packages/wallet/src/features/behaviorHistory/selectors.ts @@ -1,11 +1,7 @@ -import { - ExtensionBetaFeedbackState, - ExtensionOnboardingState, -} from 'wallet/src/features/behaviorHistory/slice' +import { ExtensionBetaFeedbackState, ExtensionOnboardingState } from 'wallet/src/features/behaviorHistory/slice' import { SharedState } from 'wallet/src/state/reducer' -export const selectHasViewedReviewScreen = (state: SharedState): boolean => - state.behaviorHistory.hasViewedReviewScreen +export const selectHasViewedReviewScreen = (state: SharedState): boolean => state.behaviorHistory.hasViewedReviewScreen export const selectHasSubmittedHoldToSwap = (state: SharedState): boolean => state.behaviorHistory.hasSubmittedHoldToSwap @@ -19,6 +15,5 @@ export const selectHasCompletedUnitagsIntroModal = (state: SharedState): boolean export const selectExtensionOnboardingState = (state: SharedState): ExtensionOnboardingState => state.behaviorHistory.extensionOnboardingState -export const selectExtensionBetaFeedbackState = ( - state: SharedState -): ExtensionBetaFeedbackState | undefined => state.behaviorHistory.extensionBetaFeedbackState +export const selectExtensionBetaFeedbackState = (state: SharedState): ExtensionBetaFeedbackState | undefined => + state.behaviorHistory.extensionBetaFeedbackState diff --git a/packages/wallet/src/features/chains/utils.test.ts b/packages/wallet/src/features/chains/utils.test.ts deleted file mode 100644 index dc7b3ac4a60..00000000000 --- a/packages/wallet/src/features/chains/utils.test.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { BigNumber } from 'ethers' -import { Chain } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { UniverseChainId } from 'uniswap/src/types/chains' -import { PollingInterval } from 'wallet/src/constants/misc' -import { - chainIdToHexadecimalString, - fromGraphQLChain, - fromMoonpayNetwork, - fromUniswapWebAppLink, - getPollingIntervalByBlocktime, - hexadecimalStringToInt, - toSupportedChainId, - toUniswapWebAppLink, -} from 'wallet/src/features/chains/utils' - -describe(toSupportedChainId, () => { - it('handles undefined input', () => { - expect(toSupportedChainId(undefined)).toEqual(null) - }) - - it('handles unsupported chain ID', () => { - expect(toSupportedChainId(BigNumber.from(6767))).toEqual(null) - }) - - it('handles supported chain ID', () => { - expect(toSupportedChainId(UniverseChainId.Polygon)).toEqual(137) - }) -}) - -describe(fromGraphQLChain, () => { - it('handles undefined', () => { - expect(fromGraphQLChain(undefined)).toEqual(null) - }) - - it('handles supported chain', () => { - expect(fromGraphQLChain(Chain.Arbitrum)).toEqual(UniverseChainId.ArbitrumOne) - }) - - it('handles unsupported chain', () => { - expect(fromGraphQLChain(Chain.UnknownChain)).toEqual(null) - }) -}) - -describe(fromMoonpayNetwork, () => { - it('handles supported chain', () => { - expect(fromMoonpayNetwork(undefined)).toEqual(UniverseChainId.Mainnet) - expect(fromMoonpayNetwork(Chain.Arbitrum.toLowerCase())).toEqual(UniverseChainId.ArbitrumOne) - expect(fromMoonpayNetwork(Chain.Optimism.toLowerCase())).toEqual(UniverseChainId.Optimism) - expect(fromMoonpayNetwork(Chain.Polygon.toLowerCase())).toEqual(UniverseChainId.Polygon) - expect(fromMoonpayNetwork(Chain.Base.toLowerCase())).toEqual(UniverseChainId.Base) - }) - - it('handle unsupported chain', () => { - expect(fromMoonpayNetwork('unknown')).toBeUndefined() - }) -}) - -describe(getPollingIntervalByBlocktime, () => { - it('returns the correct value for L1', () => { - expect(getPollingIntervalByBlocktime(UniverseChainId.Mainnet)).toEqual(PollingInterval.Fast) - }) - - it('returns the correct value for L2', () => { - expect(getPollingIntervalByBlocktime(UniverseChainId.Polygon)).toEqual( - PollingInterval.LightningMcQueen - ) - }) -}) - -describe(fromUniswapWebAppLink, () => { - it('handles supported chain', () => { - expect(fromUniswapWebAppLink(Chain.Ethereum.toLowerCase())).toEqual(UniverseChainId.Mainnet) - expect(fromUniswapWebAppLink(Chain.Arbitrum.toLowerCase())).toEqual(UniverseChainId.ArbitrumOne) - expect(fromUniswapWebAppLink(Chain.Optimism.toLowerCase())).toEqual(UniverseChainId.Optimism) - expect(fromUniswapWebAppLink(Chain.Polygon.toLowerCase())).toEqual(UniverseChainId.Polygon) - // TODO: add Base test once Chain includes Base (GQL reliant) - }) - - it('handle unsupported chain', () => { - expect(() => fromUniswapWebAppLink('unkwnown')).toThrow('Network "unkwnown" can not be mapped') - }) -}) - -describe(toUniswapWebAppLink, () => { - it('handles supported chain', () => { - expect(toUniswapWebAppLink(UniverseChainId.Mainnet)).toEqual(Chain.Ethereum.toLowerCase()) - expect(toUniswapWebAppLink(UniverseChainId.ArbitrumOne)).toEqual(Chain.Arbitrum.toLowerCase()) - expect(toUniswapWebAppLink(UniverseChainId.Optimism)).toEqual(Chain.Optimism.toLowerCase()) - expect(toUniswapWebAppLink(UniverseChainId.Polygon)).toEqual(Chain.Polygon.toLowerCase()) - // TODO: add Base test once Chain includes Base (GQL reliant) - }) - - it('handle unsupported chain', () => { - expect(() => fromUniswapWebAppLink('unkwnown')).toThrow('Network "unkwnown" can not be mapped') - }) -}) - -describe(chainIdToHexadecimalString, () => { - it('handles supported chain', () => { - expect(chainIdToHexadecimalString(UniverseChainId.ArbitrumOne)).toEqual('0xa4b1') - }) -}) - -describe('hexadecimalStringToInt', () => { - it('converts valid hexadecimal strings to integers', () => { - expect(hexadecimalStringToInt('1')).toEqual(1) - expect(hexadecimalStringToInt('a')).toEqual(10) - expect(hexadecimalStringToInt('A')).toEqual(10) - expect(hexadecimalStringToInt('10')).toEqual(16) - expect(hexadecimalStringToInt('FF')).toEqual(255) - expect(hexadecimalStringToInt('ff')).toEqual(255) - expect(hexadecimalStringToInt('100')).toEqual(256) - }) - - it('converts hexadecimal strings with prefix to integers', () => { - expect(hexadecimalStringToInt('0x1')).toEqual(1) - expect(hexadecimalStringToInt('0xa')).toEqual(10) - expect(hexadecimalStringToInt('0xA')).toEqual(10) - expect(hexadecimalStringToInt('0x10')).toEqual(16) - expect(hexadecimalStringToInt('0xFF')).toEqual(255) - expect(hexadecimalStringToInt('0xff')).toEqual(255) - expect(hexadecimalStringToInt('0x100')).toEqual(256) - }) - - it('handles invalid hexadecimal strings', () => { - expect(hexadecimalStringToInt('')).toBeNaN() - expect(hexadecimalStringToInt('g')).toBeNaN() - expect(hexadecimalStringToInt('0x')).toBeNaN() - expect(hexadecimalStringToInt('0xg')).toBeNaN() - }) -}) diff --git a/packages/wallet/src/features/chains/utils.ts b/packages/wallet/src/features/chains/utils.ts deleted file mode 100644 index ecc5e39f5ea..00000000000 --- a/packages/wallet/src/features/chains/utils.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { BigNumber, BigNumberish } from 'ethers' -import { L2ChainId, L2_CHAIN_IDS } from 'uniswap/src/constants/chains' -import { Chain } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { - UniverseChainId, - WALLET_SUPPORTED_CHAIN_IDS, - WalletChainId, -} from 'uniswap/src/types/chains' -import { isJestRun } from 'utilities/src/environment' -import { PollingInterval } from 'wallet/src/constants/misc' - -// Some code from the web app uses chainId types as numbers -// This validates them as coerces into SupportedChainId -export function toSupportedChainId(chainId?: BigNumberish): WalletChainId | null { - // Support Goerli for testing - const ids = isJestRun - ? [UniverseChainId.Goerli, ...WALLET_SUPPORTED_CHAIN_IDS] - : WALLET_SUPPORTED_CHAIN_IDS - - if (!chainId || !ids.map((c) => c.toString()).includes(chainId.toString())) { - return null - } - return parseInt(chainId.toString(), 10) as WalletChainId -} - -export function chainIdToHexadecimalString(chainId: WalletChainId): string { - return BigNumber.from(chainId).toHexString() -} - -export function hexadecimalStringToInt(hex: string): number { - return parseInt(hex, 16) -} - -export const isL2Chain = (chainId?: UniverseChainId): boolean => - Boolean(chainId && L2_CHAIN_IDS.includes(chainId as L2ChainId)) - -export function fromGraphQLChain(chain: Chain | undefined): WalletChainId | null { - switch (chain) { - case Chain.Ethereum: - return UniverseChainId.Mainnet - case Chain.Arbitrum: - return UniverseChainId.ArbitrumOne - case Chain.EthereumGoerli: - return UniverseChainId.Goerli - case Chain.Optimism: - return UniverseChainId.Optimism - case Chain.Polygon: - return UniverseChainId.Polygon - case Chain.Base: - return UniverseChainId.Base - case Chain.Bnb: - return UniverseChainId.Bnb - case Chain.Blast: - return UniverseChainId.Blast - case Chain.Avalanche: - return UniverseChainId.Avalanche - case Chain.Celo: - return UniverseChainId.Celo - case Chain.Zora: - return UniverseChainId.Zora - case Chain.Zksync: - return UniverseChainId.Zksync - } - - return null -} - -export function getPollingIntervalByBlocktime(chainId?: WalletChainId): PollingInterval { - return isL2Chain(chainId) ? PollingInterval.LightningMcQueen : PollingInterval.Fast -} - -export function fromMoonpayNetwork(moonpayNetwork: string | undefined): WalletChainId | undefined { - switch (moonpayNetwork) { - case Chain.Arbitrum.toLowerCase(): - return UniverseChainId.ArbitrumOne - case Chain.Optimism.toLowerCase(): - return UniverseChainId.Optimism - case Chain.Polygon.toLowerCase(): - return UniverseChainId.Polygon - case Chain.Bnb.toLowerCase(): - return UniverseChainId.Bnb - // Moonpay still refers to BNB chain as BSC so including both BNB and BSC cases - case 'bsc': - return UniverseChainId.Bnb - case Chain.Base.toLowerCase(): - return UniverseChainId.Base - case Chain.Avalanche.toLowerCase(): - return UniverseChainId.Avalanche - case Chain.Celo.toLowerCase(): - return UniverseChainId.Celo - case undefined: - return UniverseChainId.Mainnet - default: - return undefined - } -} - -export function fromUniswapWebAppLink(network: string | null): WalletChainId | null { - switch (network) { - case Chain.Ethereum.toLowerCase(): - return UniverseChainId.Mainnet - case Chain.Arbitrum.toLowerCase(): - return UniverseChainId.ArbitrumOne - case Chain.Optimism.toLowerCase(): - return UniverseChainId.Optimism - case Chain.Polygon.toLowerCase(): - return UniverseChainId.Polygon - case Chain.Base.toLowerCase(): - return UniverseChainId.Base - case Chain.Bnb.toLowerCase(): - return UniverseChainId.Bnb - case Chain.Blast.toLowerCase(): - return UniverseChainId.Blast - case Chain.Avalanche.toLowerCase(): - return UniverseChainId.Avalanche - case Chain.Celo.toLowerCase(): - return UniverseChainId.Celo - case Chain.Zora.toLowerCase(): - return UniverseChainId.Zora - case Chain.Zksync.toLowerCase(): - return UniverseChainId.Zksync - default: - throw new Error(`Network "${network}" can not be mapped`) - } -} - -export function toUniswapWebAppLink(chainId: WalletChainId): string | null { - switch (chainId) { - case UniverseChainId.Mainnet: - return Chain.Ethereum.toLowerCase() - case UniverseChainId.ArbitrumOne: - return Chain.Arbitrum.toLowerCase() - case UniverseChainId.Optimism: - return Chain.Optimism.toLowerCase() - case UniverseChainId.Polygon: - return Chain.Polygon.toLowerCase() - case UniverseChainId.Base: - return Chain.Base.toLowerCase() - case UniverseChainId.Bnb: - return Chain.Bnb.toLowerCase() - case UniverseChainId.Blast: - return Chain.Blast.toLowerCase() - case UniverseChainId.Avalanche: - return Chain.Avalanche.toLowerCase() - case UniverseChainId.Celo: - return Chain.Celo.toLowerCase() - case UniverseChainId.Zora: - return Chain.Zora.toLowerCase() - case UniverseChainId.Zksync: - return Chain.Zksync.toLowerCase() - default: - throw new Error(`ChainID "${chainId}" can not be mapped`) - } -} diff --git a/packages/wallet/src/features/contracts/ContractManager.ts b/packages/wallet/src/features/contracts/ContractManager.ts index afcfe4371f9..a958957478f 100644 --- a/packages/wallet/src/features/contracts/ContractManager.ts +++ b/packages/wallet/src/features/contracts/ContractManager.ts @@ -1,9 +1,9 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import { Contract, ContractInterface, providers } from 'ethers' import { WalletChainId } from 'uniswap/src/types/chains' +import { getValidAddress } from 'uniswap/src/utils/addresses' +import { isNativeCurrencyAddress } from 'uniswap/src/utils/currencyId' import { logger } from 'utilities/src/logger/logger' -import { getValidAddress } from 'wallet/src/utils/addresses' -import { isNativeCurrencyAddress } from 'wallet/src/utils/currencyId' export class ContractManager { private _contracts: Partial>> = {} @@ -12,7 +12,7 @@ export class ContractManager { chainId: WalletChainId, address: Address, provider: providers.Provider, - ABI: ContractInterface + ABI: ContractInterface, ): Contract { if (isNativeCurrencyAddress(chainId, address) || !getValidAddress(address, true)) { throw Error(`Invalid address for contract: ${address}`) @@ -21,11 +21,7 @@ export class ContractManager { if (this._contracts[chainId]?.[address]) { throw new Error(`Contract already exists for: ${chainId} ${address}`) } else { - logger.debug( - 'ContractManager', - 'createContract', - `Creating a new contract for: ${chainId} ${address}` - ) + logger.debug('ContractManager', 'createContract', `Creating a new contract for: ${chainId} ${address}`) const contract = new Contract(address, ABI, provider) this._contracts[chainId]![address] = contract return contract @@ -37,7 +33,7 @@ export class ContractManager { logger.warn( 'ContractManager', 'removeContract', - `Attempting to remove non-existing contract for: ${chainId} ${address}` + `Attempting to remove non-existing contract for: ${chainId} ${address}`, ) return } @@ -57,7 +53,7 @@ export class ContractManager { chainId: WalletChainId, address: Address, provider: providers.Provider, - ABI: ContractInterface + ABI: ContractInterface, ): T { const cachedContract = this.getContract(chainId, address) return (cachedContract ?? this.createContract(chainId, address, provider, ABI)) as T diff --git a/packages/wallet/src/features/contracts/hooks.ts b/packages/wallet/src/features/contracts/hooks.ts index 0db8c8e96ab..d6b502a98ed 100644 --- a/packages/wallet/src/features/contracts/hooks.ts +++ b/packages/wallet/src/features/contracts/hooks.ts @@ -16,12 +16,7 @@ export function useIsErc20Contract(address: string | undefined, chainId: WalletC } const contract = new Contract(address, ERC20_ABI, provider) try { - await Promise.all([ - contract.name(), - contract.symbol(), - contract.decimals(), - contract.totalSupply(), - ]) + await Promise.all([contract.name(), contract.symbol(), contract.decimals(), contract.totalSupply()]) return true } catch (e) { return false diff --git a/packages/wallet/src/features/dataApi/balances.test.ts b/packages/wallet/src/features/dataApi/balances.test.ts index 32061d8d2ff..9bb2b929e3d 100644 --- a/packages/wallet/src/features/dataApi/balances.test.ts +++ b/packages/wallet/src/features/dataApi/balances.test.ts @@ -6,6 +6,14 @@ import { PortfolioBalanceDocument, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { PortfolioBalance } from 'uniswap/src/features/dataApi/types' +import { + ARBITRUM_CURRENCY, + BASE_CURRENCY, + MAINNET_CURRENCY, + OPTIMISM_CURRENCY, + POLYGON_CURRENCY, + currencyInfo, +} from 'uniswap/src/test/fixtures' import { sortPortfolioBalances, useHighestBalanceNativeCurrencyId, @@ -22,16 +30,10 @@ import { WalletState, initialWalletState } from 'wallet/src/features/wallet/slic import { ACCOUNT, ACCOUNT2, - ARBITRUM_CURRENCY, - BASE_CURRENCY, - MAINNET_CURRENCY, - OPTIMISM_CURRENCY, - POLYGON_CURRENCY, SAMPLE_CURRENCY_ID_1, SAMPLE_CURRENCY_ID_2, SAMPLE_SEED_ADDRESS_1, SAMPLE_SEED_ADDRESS_2, - currencyInfo, daiToken, ethToken, portfolio, @@ -69,9 +71,7 @@ describe(usePortfolioValueModifiers, () => { }, }) - const mockFavoritesState = ( - overrideTokensVisibility?: FavoritesState['tokensVisibility'] - ): FavoritesState => ({ + const mockFavoritesState = (overrideTokensVisibility?: FavoritesState['tokensVisibility']): FavoritesState => ({ ...initialFavoritesState, tokensVisibility: { ...initialFavoritesState.tokensVisibility, @@ -92,9 +92,7 @@ describe(usePortfolioValueModifiers, () => { }) it('returns multiple modifiers if multiple addresses are passed', () => { - const { result } = renderHook(() => - usePortfolioValueModifiers([SAMPLE_SEED_ADDRESS_1, SAMPLE_SEED_ADDRESS_2]) - ) + const { result } = renderHook(() => usePortfolioValueModifiers([SAMPLE_SEED_ADDRESS_1, SAMPLE_SEED_ADDRESS_2])) expect(result.current).toEqual([ { ...sharedModifier, ownerAddress: SAMPLE_SEED_ADDRESS_1 }, @@ -104,10 +102,9 @@ describe(usePortfolioValueModifiers, () => { describe('includeSmallBalances', () => { it('returns modifiers with includeSmallBalances set to true if hideSmallBalances settings is false', () => { - const { result } = renderHook( - () => usePortfolioValueModifiers([SAMPLE_SEED_ADDRESS_1, SAMPLE_SEED_ADDRESS_2]), - { preloadedState: { wallet: mockWalletState({ hideSmallBalances: false }) } } - ) + const { result } = renderHook(() => usePortfolioValueModifiers([SAMPLE_SEED_ADDRESS_1, SAMPLE_SEED_ADDRESS_2]), { + preloadedState: { wallet: mockWalletState({ hideSmallBalances: false }) }, + }) expect(result.current).toEqual([ { ...sharedModifier, ownerAddress: SAMPLE_SEED_ADDRESS_1, includeSmallBalances: true }, @@ -116,10 +113,9 @@ describe(usePortfolioValueModifiers, () => { }) it('returns modifiers with includeSmallBalances set to false if hideSmallBalances settings is true', () => { - const { result } = renderHook( - () => usePortfolioValueModifiers([SAMPLE_SEED_ADDRESS_1, SAMPLE_SEED_ADDRESS_2]), - { preloadedState: { wallet: mockWalletState({ hideSmallBalances: true }) } } - ) + const { result } = renderHook(() => usePortfolioValueModifiers([SAMPLE_SEED_ADDRESS_1, SAMPLE_SEED_ADDRESS_2]), { + preloadedState: { wallet: mockWalletState({ hideSmallBalances: true }) }, + }) expect(result.current).toEqual([ { ...sharedModifier, ownerAddress: SAMPLE_SEED_ADDRESS_1, includeSmallBalances: false }, @@ -130,10 +126,9 @@ describe(usePortfolioValueModifiers, () => { describe('includeSpamTokens', () => { it('returns modifiers with includeSpamTokens set to true if hideSpamTokens settings is false', () => { - const { result } = renderHook( - () => usePortfolioValueModifiers([SAMPLE_SEED_ADDRESS_1, SAMPLE_SEED_ADDRESS_2]), - { preloadedState: { wallet: mockWalletState({ hideSpamTokens: false }) } } - ) + const { result } = renderHook(() => usePortfolioValueModifiers([SAMPLE_SEED_ADDRESS_1, SAMPLE_SEED_ADDRESS_2]), { + preloadedState: { wallet: mockWalletState({ hideSpamTokens: false }) }, + }) expect(result.current).toEqual([ { ...sharedModifier, ownerAddress: SAMPLE_SEED_ADDRESS_1, includeSpamTokens: true }, @@ -142,10 +137,9 @@ describe(usePortfolioValueModifiers, () => { }) it('returns modifiers with includeSpamTokens set to false if hideSpamTokens settings is true', () => { - const { result } = renderHook( - () => usePortfolioValueModifiers([SAMPLE_SEED_ADDRESS_1, SAMPLE_SEED_ADDRESS_2]), - { preloadedState: { wallet: mockWalletState({ hideSpamTokens: true }) } } - ) + const { result } = renderHook(() => usePortfolioValueModifiers([SAMPLE_SEED_ADDRESS_1, SAMPLE_SEED_ADDRESS_2]), { + preloadedState: { wallet: mockWalletState({ hideSpamTokens: true }) }, + }) expect(result.current).toEqual([ { ...sharedModifier, ownerAddress: SAMPLE_SEED_ADDRESS_1, includeSpamTokens: false }, @@ -171,21 +165,18 @@ describe(usePortfolioValueModifiers, () => { }) it('includes token overrides in the result if tokensVisibility contains addresses visibility settings', () => { - const { result } = renderHook( - () => usePortfolioValueModifiers([SAMPLE_SEED_ADDRESS_1, SAMPLE_SEED_ADDRESS_2]), - { - preloadedState: { - wallet: { - ...initialWalletState, - accounts: { [SAMPLE_SEED_ADDRESS_1]: ACCOUNT, [SAMPLE_SEED_ADDRESS_2]: ACCOUNT2 }, - }, - favorites: mockFavoritesState({ - [SAMPLE_CURRENCY_ID_1]: { isVisible: false }, - [SAMPLE_CURRENCY_ID_2]: { isVisible: true }, - }), + const { result } = renderHook(() => usePortfolioValueModifiers([SAMPLE_SEED_ADDRESS_1, SAMPLE_SEED_ADDRESS_2]), { + preloadedState: { + wallet: { + ...initialWalletState, + accounts: { [SAMPLE_SEED_ADDRESS_1]: ACCOUNT, [SAMPLE_SEED_ADDRESS_2]: ACCOUNT2 }, }, - } - ) + favorites: mockFavoritesState({ + [SAMPLE_CURRENCY_ID_1]: { isVisible: false }, + [SAMPLE_CURRENCY_ID_2]: { isVisible: true }, + }), + }, + }) expect(result.current).toEqual([ { @@ -319,10 +310,9 @@ describe(usePortfolioBalances, () => { it('calls onCompleted callback when query completes', async () => { const onCompleted = jest.fn() - const { result } = renderHook( - () => usePortfolioBalances({ address: daiCurrencyId, onCompleted }), - { resolvers: portfolioResolvers } - ) + const { result } = renderHook(() => usePortfolioBalances({ address: daiCurrencyId, onCompleted }), { + resolvers: portfolioResolvers, + }) expect(result.current.loading).toEqual(true) expect(onCompleted).not.toHaveBeenCalled() @@ -348,10 +338,9 @@ describe(usePortfolioTotalValue, () => { }) it('returns loading set to true when data is being fetched', async () => { - const { result } = renderHook( - () => usePortfolioTotalValue({ address: Portfolio.ownerAddress }), - { resolvers: portfolioResolvers } - ) + const { result } = renderHook(() => usePortfolioTotalValue({ address: Portfolio.ownerAddress }), { + resolvers: portfolioResolvers, + }) expect(result.current).toEqual({ data: undefined, @@ -372,10 +361,7 @@ describe(usePortfolioTotalValue, () => { throw new Error('test') }, }) - const { result } = renderHook( - () => usePortfolioTotalValue({ address: Portfolio.ownerAddress }), - { resolvers } - ) + const { result } = renderHook(() => usePortfolioTotalValue({ address: Portfolio.ownerAddress }), { resolvers }) await waitFor(() => { expect(result.current).toEqual({ @@ -392,10 +378,7 @@ describe(usePortfolioTotalValue, () => { const { resolvers } = queryResolvers({ portfolios: () => [], }) - const { result } = renderHook( - () => usePortfolioTotalValue({ address: Portfolio.ownerAddress }), - { resolvers } - ) + const { result } = renderHook(() => usePortfolioTotalValue({ address: Portfolio.ownerAddress }), { resolvers }) expect(result.current.loading).toEqual(true) @@ -411,12 +394,9 @@ describe(usePortfolioTotalValue, () => { }) it('returns total value of all balances for the specified address', async () => { - const { result } = renderHook( - () => usePortfolioTotalValue({ address: Portfolio.ownerAddress }), - { - resolvers: portfolioResolvers, - } - ) + const { result } = renderHook(() => usePortfolioTotalValue({ address: Portfolio.ownerAddress }), { + resolvers: portfolioResolvers, + }) await waitFor(() => { expect(result.current).toEqual({ @@ -481,7 +461,7 @@ describe(useTokenBalancesGroupedByVisibility, () => { [daiPortfolioBalance.cacheId]: daiPortfolioBalance, [ethPortfolioBalance.cacheId]: ethPortfolioBalance, }, - }) + }), ) expect(result.current).toEqual({ @@ -493,9 +473,7 @@ describe(useTokenBalancesGroupedByVisibility, () => { describe(useSortedPortfolioBalances, () => { it('returns loading set to true when data is being fetched', () => { - const { result } = renderHook(() => - useSortedPortfolioBalances({ address: Portfolio.ownerAddress }) - ) + const { result } = renderHook(() => useSortedPortfolioBalances({ address: Portfolio.ownerAddress })) expect(result.current).toEqual({ data: { @@ -509,12 +487,9 @@ describe(useSortedPortfolioBalances, () => { }) it('returns balances grouped by visibility when data is fetched', async () => { - const { result } = renderHook( - () => useSortedPortfolioBalances({ address: Portfolio.ownerAddress }), - { - resolvers: portfolioResolvers, - } - ) + const { result } = renderHook(() => useSortedPortfolioBalances({ address: Portfolio.ownerAddress }), { + resolvers: portfolioResolvers, + }) await waitFor(() => { expect(result.current).toEqual({ @@ -556,12 +531,8 @@ describe(sortPortfolioBalances, () => { const result = sortPortfolioBalances([...balancesWithoutUSD, ...balancesWithUSD]) expect(result).toEqual([ - ...createArray(balancesWithUSD.length, () => - expect.objectContaining({ balanceUSD: expect.any(Number) }) - ), - ...createArray(balancesWithoutUSD.length, () => - expect.objectContaining({ balanceUSD: null }) - ), + ...createArray(balancesWithUSD.length, () => expect.objectContaining({ balanceUSD: expect.any(Number) })), + ...createArray(balancesWithoutUSD.length, () => expect.objectContaining({ balanceUSD: null })), ]) }) @@ -629,7 +600,7 @@ describe(usePortfolioCacheUpdater, () => { fields: { tokensTotalDenominatedValue: expect.any(Function), }, - }) + }), ) }) }) diff --git a/packages/wallet/src/features/dataApi/balances.ts b/packages/wallet/src/features/dataApi/balances.ts index 731bce86612..3710c35c1ef 100644 --- a/packages/wallet/src/features/dataApi/balances.ts +++ b/packages/wallet/src/features/dataApi/balances.ts @@ -1,5 +1,6 @@ import { NetworkStatus, Reference, useApolloClient, WatchQueryFetchPolicy } from '@apollo/client' import { useCallback, useMemo } from 'react' +import { PollingInterval } from 'uniswap/src/constants/misc' import { ContractInput, IAmount, @@ -9,22 +10,15 @@ import { usePortfolioBalancesQuery, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { GqlResult } from 'uniswap/src/data/types' +import { fromGraphQLChain } from 'uniswap/src/features/chains/utils' import { CurrencyInfo, PortfolioBalance } from 'uniswap/src/features/dataApi/types' +import { buildCurrency, currencyIdToContractInput, usePersistedError } from 'uniswap/src/features/dataApi/utils' import { CurrencyId } from 'uniswap/src/types/currency' +import { currencyId } from 'uniswap/src/utils/currencyId' +import { usePlatformBasedFetchPolicy } from 'uniswap/src/utils/usePlatformBasedFetchPolicy' import { logger } from 'utilities/src/logger/logger' -import { PollingInterval } from 'wallet/src/constants/misc' -import { fromGraphQLChain } from 'wallet/src/features/chains/utils' -import { - buildCurrency, - currencyIdToContractInput, - usePersistedError, -} from 'wallet/src/features/dataApi/utils' import { useCurrencyIdToVisibility } from 'wallet/src/features/transactions/selectors' -import { - useHideSmallBalancesSetting, - useHideSpamTokensSetting, -} from 'wallet/src/features/wallet/hooks' -import { currencyId } from 'wallet/src/utils/currencyId' +import { useHideSmallBalancesSetting, useHideSpamTokensSetting } from 'wallet/src/features/wallet/hooks' type SortedPortfolioBalances = { balances: PortfolioBalance[] @@ -44,22 +38,15 @@ interface TokenOverrides { export type PortfolioCacheUpdater = (hidden: boolean, portfolioBalance?: PortfolioBalance) => void -export function usePortfolioValueModifiers( - address?: Address | Address[] -): PortfolioValueModifier[] | undefined { +export function usePortfolioValueModifiers(address?: Address | Address[]): PortfolioValueModifier[] | undefined { // Memoize array creation if passed a string to avoid recomputing at every render - const addressArray = useMemo( - () => (!address ? [] : Array.isArray(address) ? address : [address]), - [address] - ) + const addressArray = useMemo(() => (!address ? [] : Array.isArray(address) ? address : [address]), [address]) const currencyIdToTokenVisibility = useCurrencyIdToVisibility() const hideSpamTokens = useHideSpamTokensSetting() const hideSmallBalances = useHideSmallBalancesSetting() - const { tokenIncludeOverrides, tokenExcludeOverrides } = Object.entries( - currencyIdToTokenVisibility - ).reduce( + const { tokenIncludeOverrides, tokenExcludeOverrides } = Object.entries(currencyIdToTokenVisibility).reduce( (acc: TokenOverrides, [key, tokenVisibility]) => { const contractInput = currencyIdToContractInput(key) if (tokenVisibility.isVisible) { @@ -72,7 +59,7 @@ export function usePortfolioValueModifiers( { tokenIncludeOverrides: [], tokenExcludeOverrides: [], - } + }, ) const modifiers = useMemo(() => { @@ -83,13 +70,7 @@ export function usePortfolioValueModifiers( includeSmallBalances: !hideSmallBalances, includeSpamTokens: !hideSpamTokens, })) - }, [ - addressArray, - tokenIncludeOverrides, - tokenExcludeOverrides, - hideSmallBalances, - hideSpamTokens, - ]) + }, [addressArray, tokenIncludeOverrides, tokenExcludeOverrides, hideSmallBalances, hideSpamTokens]) return modifiers.length > 0 ? modifiers : undefined } @@ -122,6 +103,12 @@ export function usePortfolioBalances({ fetchPolicy?: WatchQueryFetchPolicy }): GqlResult> & { networkStatus: NetworkStatus } { const valueModifiers = usePortfolioValueModifiers(address) + + const { fetchPolicy: internalFetchPolicy, pollInterval: internalPollInterval } = usePlatformBasedFetchPolicy({ + fetchPolicy, + pollInterval, + }) + const { data: balancesData, loading, @@ -129,10 +116,10 @@ export function usePortfolioBalances({ refetch, error, } = usePortfolioBalancesQuery({ - fetchPolicy, + fetchPolicy: internalFetchPolicy, notifyOnNetworkStatusChange: true, onCompleted, - pollInterval, + pollInterval: internalPollInterval, variables: address ? { ownerAddress: address, valueModifiers } : undefined, skip: !address, }) @@ -204,7 +191,7 @@ export function usePortfolioBalances({ const retry = useCallback( () => refetch({ ownerAddress: address, valueModifiers }), - [address, valueModifiers, refetch] + [address, valueModifiers, refetch], ) return { @@ -228,6 +215,12 @@ export function usePortfolioTotalValue({ fetchPolicy?: WatchQueryFetchPolicy }): GqlResult & { networkStatus: NetworkStatus } { const valueModifiers = usePortfolioValueModifiers(address) + + const { fetchPolicy: internalFetchPolicy, pollInterval: internalPollInterval } = usePlatformBasedFetchPolicy({ + fetchPolicy, + pollInterval, + }) + const { data: balancesData, loading, @@ -235,10 +228,10 @@ export function usePortfolioTotalValue({ refetch, error, } = usePortfolioBalancesQuery({ - fetchPolicy, + fetchPolicy: internalFetchPolicy, notifyOnNetworkStatusChange: true, onCompleted, - pollInterval, + pollInterval: internalPollInterval, variables: address ? { ownerAddress: address, valueModifiers } : undefined, skip: !address, }) @@ -260,7 +253,7 @@ export function usePortfolioTotalValue({ const retry = useCallback( () => refetch({ ownerAddress: address, valueModifiers }), - [address, valueModifiers, refetch] + [address, valueModifiers, refetch], ) return { @@ -281,8 +274,7 @@ export function usePortfolioTotalValue({ */ export function useHighestBalanceNativeCurrencyId(address: Address): CurrencyId | undefined { const { data } = useSortedPortfolioBalances({ address }) - return data?.balances.find((balance) => balance.currencyInfo.currency.isNative)?.currencyInfo - .currencyId + return data?.balances.find((balance) => balance.currencyInfo.currency.isNative)?.currencyInfo.currencyId } /** @@ -322,7 +314,7 @@ export function useTokenBalancesGroupedByVisibility({ } return acc }, - { shown: [], hidden: [] } + { shown: [], hidden: [] }, ) return { shownTokens: shown.length ? shown : undefined, @@ -471,7 +463,7 @@ export function usePortfolioCacheUpdater(address: string): PortfolioCacheUpdater }, }) }, - [apolloClient, address] + [apolloClient, address], ) return updater diff --git a/packages/wallet/src/features/dataApi/searchTokens.test.ts b/packages/wallet/src/features/dataApi/searchTokens.test.ts index c26e6d7e9c6..0f028a2c6dc 100644 --- a/packages/wallet/src/features/dataApi/searchTokens.test.ts +++ b/packages/wallet/src/features/dataApi/searchTokens.test.ts @@ -1,7 +1,8 @@ import { waitFor } from '@testing-library/react-native' -import { useSearchTokens } from 'wallet/src/features/dataApi/searchTokens' -import { useTokenProjects } from 'wallet/src/features/dataApi/tokenProjects' -import { gqlTokenToCurrencyInfo } from 'wallet/src/features/dataApi/utils' +import { useSearchTokens } from 'uniswap/src/features/dataApi/searchTokens' +import { useTokenProjects } from 'uniswap/src/features/dataApi/tokenProjects' +import { gqlTokenToCurrencyInfo } from 'uniswap/src/features/dataApi/utils' +// TODO: https://linear.app/uniswap/issue/WEB-4376/move-universepackageswalletsrcfeaturesdataapi-tests-to-uniswap-pkg import { token } from 'wallet/src/test/fixtures' import { createArray, renderHook } from 'wallet/src/test/test-utils' import { queryResolvers } from 'wallet/src/test/utils' diff --git a/packages/wallet/src/features/dataApi/tokenProject.test.tsx b/packages/wallet/src/features/dataApi/tokenProject.test.tsx index 35d3b93358a..14150b67ec2 100644 --- a/packages/wallet/src/features/dataApi/tokenProject.test.tsx +++ b/packages/wallet/src/features/dataApi/tokenProject.test.tsx @@ -1,6 +1,7 @@ import { waitFor } from '@testing-library/react-native' -import { useTokenProjects } from 'wallet/src/features/dataApi/tokenProjects' -import { tokenProjectToCurrencyInfos } from 'wallet/src/features/dataApi/utils' +import { useTokenProjects } from 'uniswap/src/features/dataApi/tokenProjects' +import { tokenProjectToCurrencyInfos } from 'uniswap/src/features/dataApi/utils' +// TODO: https://linear.app/uniswap/issue/WEB-4376/move-universepackageswalletsrcfeaturesdataapi-tests-to-uniswap-pkg import { SAMPLE_CURRENCY_ID_1, usdcTokenProject } from 'wallet/src/test/fixtures' import { renderHook } from 'wallet/src/test/test-utils' import { queryResolvers } from 'wallet/src/test/utils' diff --git a/packages/wallet/src/features/dataApi/topTokens.test.ts b/packages/wallet/src/features/dataApi/topTokens.test.ts index 4274fc82fad..c323a4e0e04 100644 --- a/packages/wallet/src/features/dataApi/topTokens.test.ts +++ b/packages/wallet/src/features/dataApi/topTokens.test.ts @@ -1,6 +1,7 @@ +import { usePopularTokens } from 'uniswap/src/features/dataApi/topTokens' +import { gqlTokenToCurrencyInfo } from 'uniswap/src/features/dataApi/utils' import { UniverseChainId } from 'uniswap/src/types/chains' -import { usePopularTokens } from 'wallet/src/features/dataApi/topTokens' -import { gqlTokenToCurrencyInfo } from 'wallet/src/features/dataApi/utils' +// TODO: https://linear.app/uniswap/issue/WEB-4376/move-universepackageswalletsrcfeaturesdataapi-tests-to-uniswap-pkg import { token } from 'wallet/src/test/fixtures' import { act, renderHook, waitFor } from 'wallet/src/test/test-utils' import { createArray, queryResolvers } from 'wallet/src/test/utils' diff --git a/packages/wallet/src/features/dataApi/utils.test.ts b/packages/wallet/src/features/dataApi/utils.test.ts index 469b9517b37..e1d50bdf04c 100644 --- a/packages/wallet/src/features/dataApi/utils.test.ts +++ b/packages/wallet/src/features/dataApi/utils.test.ts @@ -5,23 +5,19 @@ import { Token as GQLToken, TokenProject, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { fromGraphQLChain } from 'uniswap/src/features/chains/utils' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' -import { UniverseChainId } from 'uniswap/src/types/chains' -import { fromGraphQLChain } from 'wallet/src/features/chains/utils' import { buildCurrency, currencyIdToContractInput, gqlTokenToCurrencyInfo, tokenProjectToCurrencyInfos, usePersistedError, -} from 'wallet/src/features/dataApi/utils' -import { NativeCurrency } from 'wallet/src/features/tokens/NativeCurrency' -import { - SAMPLE_CURRENCY_ID_1, - SAMPLE_CURRENCY_ID_2, - ethToken, - usdcTokenProject, -} from 'wallet/src/test/fixtures' +} from 'uniswap/src/features/dataApi/utils' +import { NativeCurrency } from 'uniswap/src/features/tokens/NativeCurrency' +import { UniverseChainId } from 'uniswap/src/types/chains' +// TODO: https://linear.app/uniswap/issue/WEB-4376/move-universepackageswalletsrcfeaturesdataapi-tests-to-uniswap-pkg +import { SAMPLE_CURRENCY_ID_1, SAMPLE_CURRENCY_ID_2, ethToken, usdcTokenProject } from 'wallet/src/test/fixtures' import { renderHook } from 'wallet/src/test/test-utils' describe(currencyIdToContractInput, () => { @@ -52,7 +48,7 @@ describe(tokenProjectToCurrencyInfos, () => { symbol: token.symbol, name: project.name, }), - } as CurrencyInfo) + }) as CurrencyInfo it('converts tokenProject to CurrencyInfo', () => { const result = tokenProjectToCurrencyInfos([project]) @@ -64,9 +60,7 @@ describe(tokenProjectToCurrencyInfos, () => { const result = tokenProjectToCurrencyInfos([project], UniverseChainId.Polygon) expect(result).toEqual( - project.tokens - .filter((token) => token.chain === 'POLYGON') - .map((token) => getExpectedResult(project, token)) + project.tokens.filter((token) => token.chain === 'POLYGON').map((token) => getExpectedResult(project, token)), ) }) @@ -121,14 +115,14 @@ describe(buildCurrency, () => { chainId: null, address: '0x0', decimals: 18, - }) + }), ).toBeUndefined() expect( buildCurrency({ chainId: UniverseChainId.Mainnet, address: '0x0', decimals: null, - }) + }), ).toBeUndefined() }) }) diff --git a/packages/wallet/src/features/ens/api.ts b/packages/wallet/src/features/ens/api.ts index 567da73ddfb..5bca8337ef2 100644 --- a/packages/wallet/src/features/ens/api.ts +++ b/packages/wallet/src/features/ens/api.ts @@ -3,9 +3,9 @@ import { providers } from 'ethers' import { useMemo } from 'react' import { useRestQuery } from 'uniswap/src/data/rest' import { UniverseChainId, WalletChainId } from 'uniswap/src/types/chains' +import { areAddressesEqual } from 'uniswap/src/utils/addresses' import { ONE_MINUTE_MS } from 'utilities/src/time/time' import { createEthersProvider } from 'wallet/src/features/providers/createEthersProvider' -import { areAddressesEqual } from 'wallet/src/utils/addresses' // stub endpoint to conform to REST endpoint styles // Rest link should intercept and use custom fetcher instead @@ -89,7 +89,7 @@ export const getOnChainEnsFetch = async (params: EnsLookupParams): Promise; timestamp: number }, EnsLookupParams>( STUB_ONCHAIN_ENS_ENDPOINT, // will invoke `getOnChainEnsFetch` @@ -97,7 +97,7 @@ function useEnsQuery( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion { type, nameOrAddress: nameOrAddress!, chainId }, ['data'], - { ttlMs: 5 * ONE_MINUTE_MS, skip: !nameOrAddress } + { ttlMs: 5 * ONE_MINUTE_MS, skip: !nameOrAddress }, ) const { data, error } = result @@ -108,34 +108,22 @@ function useEnsQuery( data: data?.data, error, }), - [data, error, result] + [data, error, result], ) } export function useENSName(address?: Address, chainId: WalletChainId = UniverseChainId.Mainnet) { return useEnsQuery(EnsLookupType.Name, address, chainId) } -export function useAddressFromEns( - maybeName: string | null, - chainId: WalletChainId = UniverseChainId.Mainnet -) { +export function useAddressFromEns(maybeName: string | null, chainId: WalletChainId = UniverseChainId.Mainnet) { return useEnsQuery(EnsLookupType.Address, maybeName, chainId) } -export function useENSAvatar( - address?: string | null, - chainId: WalletChainId = UniverseChainId.Mainnet -) { +export function useENSAvatar(address?: string | null, chainId: WalletChainId = UniverseChainId.Mainnet) { return useEnsQuery(EnsLookupType.Avatar, address, chainId) } -export function useENSDescription( - name?: string | null, - chainId: WalletChainId = UniverseChainId.Mainnet -) { +export function useENSDescription(name?: string | null, chainId: WalletChainId = UniverseChainId.Mainnet) { return useEnsQuery(EnsLookupType.Description, name, chainId) } -export function useENSTwitterUsername( - name?: string | null, - chainId: WalletChainId = UniverseChainId.Mainnet -) { +export function useENSTwitterUsername(name?: string | null, chainId: WalletChainId = UniverseChainId.Mainnet) { return useEnsQuery(EnsLookupType.TwitterUsername, name, chainId) } diff --git a/packages/wallet/src/features/ens/parseENSAddress.ts b/packages/wallet/src/features/ens/parseENSAddress.ts index 3dba8acae14..2be5a1d9b07 100644 --- a/packages/wallet/src/features/ens/parseENSAddress.ts +++ b/packages/wallet/src/features/ens/parseENSAddress.ts @@ -2,9 +2,7 @@ const ENS_DOMAIN_REGEX = /^[a-zA-Z0-9-.]+\.eth$/ -export function parseENSAddress( - ensAddress: string -): { ensName: string; ensPath: string | undefined } | undefined { +export function parseENSAddress(ensAddress: string): { ensName: string; ensPath: string | undefined } | undefined { // Note: this was refactored from a regex into a function as the regex in question // contained a sequence of rules that result in O(n^2) complexity, thus potentially opening up // the mobile client to a DoS attack. diff --git a/packages/wallet/src/features/ens/useENS.ts b/packages/wallet/src/features/ens/useENS.ts index 96bc498ac2f..113890ac8da 100644 --- a/packages/wallet/src/features/ens/useENS.ts +++ b/packages/wallet/src/features/ens/useENS.ts @@ -1,9 +1,9 @@ // Copied from https://github.com/Uniswap/interface/blob/main/src/hooks/useENS.ts import { WalletChainId } from 'uniswap/src/types/chains' +import { getValidAddress } from 'uniswap/src/utils/addresses' import { useDebounce } from 'utilities/src/time/timing' import { useAddressFromEns, useENSName } from 'wallet/src/features/ens/api' -import { getValidAddress } from 'wallet/src/utils/addresses' /** * Given a name or address, does a lookup to resolve to an address and name @@ -12,7 +12,7 @@ import { getValidAddress } from 'wallet/src/utils/addresses' export function useENS( chainId: WalletChainId, nameOrAddress?: string | null, - autocompleteDomain?: boolean + autocompleteDomain?: boolean, ): { loading: boolean address?: string | null @@ -25,7 +25,7 @@ export function useENS( const { data: name, loading: nameFetching } = useENSName(validAddress ?? undefined, chainId) const { data: address, loading: addressFetching } = useAddressFromEns( autocompleteDomain ? getCompletedENSName(maybeName) : maybeName, - chainId + chainId, ) return { diff --git a/packages/wallet/src/features/favorites/selectors.ts b/packages/wallet/src/features/favorites/selectors.ts index 94bfc3386c4..64da22d7138 100644 --- a/packages/wallet/src/features/favorites/selectors.ts +++ b/packages/wallet/src/features/favorites/selectors.ts @@ -5,29 +5,20 @@ import { RootState } from 'wallet/src/state' export const selectFavoriteTokens = (state: RootState): string[] => unique(state.favorites.tokens) -export const selectHasFavoriteTokens = createSelector(selectFavoriteTokens, (tokens) => - Boolean(tokens?.length > 0) -) +export const selectHasFavoriteTokens = createSelector(selectFavoriteTokens, (tokens) => Boolean(tokens?.length > 0)) export const makeSelectHasTokenFavorited = (): Selector => createSelector( selectFavoriteTokens, (_: RootState, currencyId: string) => currencyId, - (tokens, currencyId) => tokens?.includes(currencyId.toLowerCase()) + (tokens, currencyId) => tokens?.includes(currencyId.toLowerCase()), ) const selectWatchedAddresses = (state: RootState): string[] => state.favorites.watchedAddresses -export const selectWatchedAddressSet = createSelector( - selectWatchedAddresses, - (watched) => new Set(watched) -) +export const selectWatchedAddressSet = createSelector(selectWatchedAddresses, (watched) => new Set(watched)) -export const selectHasWatchedWallets = createSelector(selectWatchedAddresses, (watched) => - Boolean(watched?.length > 0) -) +export const selectHasWatchedWallets = createSelector(selectWatchedAddresses, (watched) => Boolean(watched?.length > 0)) -export const selectNftsVisibility = (state: RootState): NFTKeyToVisibility => - state.favorites.nftsVisibility +export const selectNftsVisibility = (state: RootState): NFTKeyToVisibility => state.favorites.nftsVisibility -export const selectTokensVisibility = (state: RootState): CurrencyIdToVisibility => - state.favorites.tokensVisibility +export const selectTokensVisibility = (state: RootState): CurrencyIdToVisibility => state.favorites.tokensVisibility diff --git a/packages/wallet/src/features/favorites/slice.ts b/packages/wallet/src/features/favorites/slice.ts index 988c1f4bcf3..7b325045799 100644 --- a/packages/wallet/src/features/favorites/slice.ts +++ b/packages/wallet/src/features/favorites/slice.ts @@ -1,10 +1,10 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { Ether } from '@uniswap/sdk-core' +import { WBTC } from 'uniswap/src/constants/tokens' import { UniverseChainId } from 'uniswap/src/types/chains' import { CurrencyId } from 'uniswap/src/types/currency' +import { currencyId as idFromCurrency } from 'uniswap/src/utils/currencyId' import { logger } from 'utilities/src/logger/logger' -import { WBTC } from 'wallet/src/constants/tokens' -import { currencyId as idFromCurrency } from 'wallet/src/utils/currencyId' export type Visibility = { isVisible: boolean } export type CurrencyIdToVisibility = Record @@ -36,24 +36,14 @@ export const slice = createSlice({ name: 'favorites', initialState: initialFavoritesState, reducers: { - addFavoriteToken: ( - state, - { payload: { currencyId } }: PayloadAction<{ currencyId: string }> - ) => { + addFavoriteToken: (state, { payload: { currencyId } }: PayloadAction<{ currencyId: string }>) => { if (state.tokens.indexOf(currencyId) === -1) { state.tokens.push(currencyId.toLowerCase()) // normalize all IDs } else { - logger.warn( - 'slice', - 'addFavoriteToken', - `Attempting to favorite a token twice (${currencyId})` - ) + logger.warn('slice', 'addFavoriteToken', `Attempting to favorite a token twice (${currencyId})`) } }, - removeFavoriteToken: ( - state, - { payload: { currencyId } }: PayloadAction<{ currencyId: string }> - ) => { + removeFavoriteToken: (state, { payload: { currencyId } }: PayloadAction<{ currencyId: string }>) => { const newTokens = state.tokens.filter((c) => { return c.toLocaleLowerCase() !== currencyId.toLocaleLowerCase() }) @@ -62,59 +52,46 @@ export const slice = createSlice({ logger.warn( 'slice', 'removeFavoriteToken', - `Attempting to un-favorite a token that was not in favorites (${currencyId})` + `Attempting to un-favorite a token that was not in favorites (${currencyId})`, ) } state.tokens = newTokens }, - setFavoriteTokens: ( - state, - { payload: { currencyIds } }: PayloadAction<{ currencyIds: string[] }> - ) => { + setFavoriteTokens: (state, { payload: { currencyIds } }: PayloadAction<{ currencyIds: string[] }>) => { state.tokens = currencyIds }, addWatchedAddress: (state, { payload: { address } }: PayloadAction<{ address: Address }>) => { if (!state.watchedAddresses.includes(address)) { state.watchedAddresses.push(address) } else { - logger.warn( - 'slice', - 'addWatchedAddress', - `Attempting to watch an address twice (${address})` - ) + logger.warn('slice', 'addWatchedAddress', `Attempting to watch an address twice (${address})`) } }, - removeWatchedAddress: ( - state, - { payload: { address } }: PayloadAction<{ address: Address }> - ) => { + removeWatchedAddress: (state, { payload: { address } }: PayloadAction<{ address: Address }>) => { const newWatched = state.watchedAddresses.filter((a) => a !== address) if (newWatched.length === state.watchedAddresses.length) { logger.warn( 'slice', 'removeWatchedAddress', - `Attempting to remove an address not found in watched list (${address})` + `Attempting to remove an address not found in watched list (${address})`, ) } state.watchedAddresses = newWatched }, - setFavoriteWallets: ( - state, - { payload: { addresses } }: PayloadAction<{ addresses: Address[] }> - ) => { + setFavoriteWallets: (state, { payload: { addresses } }: PayloadAction<{ addresses: Address[] }>) => { state.watchedAddresses = addresses }, toggleTokenVisibility: ( state, - { payload: { currencyId, isSpam } }: PayloadAction<{ currencyId: string; isSpam?: boolean }> + { payload: { currencyId, isSpam } }: PayloadAction<{ currencyId: string; isSpam?: boolean }>, ) => { const isVisible = state.tokensVisibility[currencyId]?.isVisible ?? isSpam === false state.tokensVisibility[currencyId] = { isVisible: !isVisible } }, toggleNftVisibility: ( state, - { payload: { nftKey, isSpam } }: PayloadAction<{ nftKey: string; isSpam?: boolean }> + { payload: { nftKey, isSpam } }: PayloadAction<{ nftKey: string; isSpam?: boolean }>, ) => { const isVisible = state.nftsVisibility[nftKey]?.isVisible ?? isSpam === false state.nftsVisibility[nftKey] = { isVisible: !isVisible } diff --git a/packages/wallet/src/features/fiatCurrency/conversion.ts b/packages/wallet/src/features/fiatCurrency/conversion.ts index aeb8cf9d3ad..db513fc721a 100644 --- a/packages/wallet/src/features/fiatCurrency/conversion.ts +++ b/packages/wallet/src/features/fiatCurrency/conversion.ts @@ -1,12 +1,9 @@ import { useCallback, useMemo } from 'react' -import { - Currency, - useConvertQuery, -} from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { PollingInterval } from 'uniswap/src/constants/misc' +import { Currency, useConvertQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { FiatNumberType } from 'utilities/src/format/types' -import { PollingInterval } from 'wallet/src/constants/misc' import { FiatCurrency } from 'wallet/src/features/fiatCurrency/constants' import { getFiatCurrencyCode, useAppFiatCurrency } from 'wallet/src/features/fiatCurrency/hooks' import { LocalizationContextState } from 'wallet/src/features/language/LocalizationContext' @@ -79,18 +76,18 @@ const mapFiatCurrencyToServerCurrency: Record { amount: number; currency: FiatCurrency } + convertFiatAmount: (amount: number) => { amount: number; currency: FiatCurrency } convertFiatAmountFormatted: ( fromAmount: Maybe, numberType: FiatNumberType, - placeholder?: string + placeholder?: string, ) => string } // Temporary function for feature turned off -function convertFiatAmountDefault(_amount?: number): { amount: number; currency: FiatCurrency } { +function convertFiatAmountDefault(amount: number): { amount: number; currency: FiatCurrency } { return { - amount: 1, + amount, currency: FiatCurrency.UnitedStatesDollar, } } @@ -123,12 +120,10 @@ export function useFiatConverter({ const conversion = latestConversion || prevConversion const conversionRate = conversion?.convert?.value const conversionCurrency = conversion?.convert?.currency - const outputCurrency = conversionCurrency - ? mapServerCurrencyToFiatCurrency[conversionCurrency] - : undefined + const outputCurrency = conversionCurrency ? mapServerCurrencyToFiatCurrency[conversionCurrency] : undefined const convertFiatAmountInner = useCallback( - (amount = 1): { amount: number; currency: FiatCurrency } => { + (amount: number): { amount: number; currency: FiatCurrency } => { const defaultResult = { amount, currency: FiatCurrency.UnitedStatesDollar } if (SOURCE_CURRENCY === toCurrency || !conversionRate || !outputCurrency) { @@ -140,7 +135,7 @@ export function useFiatConverter({ currency: outputCurrency, } }, - [conversionRate, outputCurrency, toCurrency] + [conversionRate, outputCurrency, toCurrency], ) const convertFiatAmountFormattedInner = useCallback( (fromAmount: Maybe, numberType: FiatNumberType, placeholder = '-'): string => { @@ -159,7 +154,7 @@ export function useFiatConverter({ placeholder, }) }, - [convertFiatAmountInner, formatNumberOrString] + [convertFiatAmountInner, formatNumberOrString], ) return useMemo( @@ -167,6 +162,6 @@ export function useFiatConverter({ convertFiatAmount: featureEnabled ? convertFiatAmountInner : convertFiatAmountDefault, convertFiatAmountFormatted: convertFiatAmountFormattedInner, }), - [convertFiatAmountFormattedInner, convertFiatAmountInner, featureEnabled] + [convertFiatAmountFormattedInner, convertFiatAmountInner, featureEnabled], ) } diff --git a/packages/wallet/src/features/fiatCurrency/hooks.ts b/packages/wallet/src/features/fiatCurrency/hooks.ts index 2aa6f458a7e..e88d3e51a4a 100644 --- a/packages/wallet/src/features/fiatCurrency/hooks.ts +++ b/packages/wallet/src/features/fiatCurrency/hooks.ts @@ -40,10 +40,7 @@ export function useFiatCurrencyComponents(currency: FiatCurrency): FiatCurrencyC * @param currency target currency * @returns currency name */ -export function getFiatCurrencyName( - t: AppTFunction, - currency: FiatCurrency -): { name: string; shortName: string } { +export function getFiatCurrencyName(t: AppTFunction, currency: FiatCurrency): { name: string; shortName: string } { const currencyToCurrencyName = { [FiatCurrency.AustrialianDollor]: t('currency.aud'), [FiatCurrency.BrazilianReal]: t('currency.brl'), @@ -143,6 +140,6 @@ export const useLocalFiatToUSDConverter = (): ((fiatAmount: number) => number | const { amount: USDInLocalCurrency } = convertFiatAmount(1) return USDInLocalCurrency ? fiatAmount / USDInLocalCurrency : undefined }, - [convertFiatAmount] + [convertFiatAmount], ) } diff --git a/packages/wallet/src/features/fiatOnRamp/api.ts b/packages/wallet/src/features/fiatOnRamp/api.ts index f77fc39bbbd..d92d19fdabe 100644 --- a/packages/wallet/src/features/fiatOnRamp/api.ts +++ b/packages/wallet/src/features/fiatOnRamp/api.ts @@ -1,350 +1,43 @@ -import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' -import { MoonpayEventName } from '@uniswap/analytics-events' import dayjs from 'dayjs' -import { config } from 'uniswap/src/config' import { uniswapUrls } from 'uniswap/src/constants/urls' import { objectToQueryString } from 'uniswap/src/data/utils' -import { useFiatOnRampAggregatorTransferServiceProvidersQuery } from 'uniswap/src/features/fiatOnRamp/api' import { FOR_API_HEADERS } from 'uniswap/src/features/fiatOnRamp/constants' -import { - FORServiceProvider, - FORTransactionRequest, - FORTransactionResponse, -} from 'uniswap/src/features/fiatOnRamp/types' -import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' +import { FORTransactionResponse } from 'uniswap/src/features/fiatOnRamp/types' import { logger } from 'utilities/src/logger/logger' import { ONE_MINUTE_MS } from 'utilities/src/time/time' -import { createSignedRequestParams } from 'wallet/src/data/utils' -import { - FiatOnRampTransactionDetails, - FiatOnRampWidgetUrlQueryParameters, - FiatOnRampWidgetUrlQueryResponse, - MoonpayBuyQuoteResponse, - MoonpayCurrency, - MoonpayIPAddressesResponse, - MoonpayLimitsResponse, - MoonpayListCurrenciesResponse, - MoonpayTransactionsResponse, -} from 'wallet/src/features/fiatOnRamp/types' +import { FiatOnRampTransactionDetails } from 'wallet/src/features/fiatOnRamp/types' import { extractFiatOnRampTransactionDetails } from 'wallet/src/features/transactions/history/conversion/extractFiatOnRampTransactionDetails' -import { extractMoonpayTransactionDetails } from 'wallet/src/features/transactions/history/conversion/extractMoonpayTransactionDetails' -import { serializeQueryParams } from 'wallet/src/features/transactions/swap/utils' import { TransactionStatus } from 'wallet/src/features/transactions/types' -import { Account } from 'wallet/src/features/wallet/accounts/types' -import { walletContextValue } from 'wallet/src/features/wallet/context' -import { selectActiveAccount } from 'wallet/src/features/wallet/selectors' -import { SignerManager } from 'wallet/src/features/wallet/signing/SignerManager' -import { RootState } from 'wallet/src/state' -const COMMON_QUERY_PARAMS = serializeQueryParams({ apiKey: config.moonpayApiKey }) -const TRANSACTION_NOT_FOUND = 404 const FIAT_ONRAMP_STALE_TX_TIMEOUT = ONE_MINUTE_MS * 20 const FIAT_ONRAMP_FORCE_FETCH_TX_TIMEOUT = ONE_MINUTE_MS * 3 -// List of currency codes that our Moonpay account supports -// Manually maintained for now -const supportedCurrencyCodes = [ - 'eth', - 'eth_arbitrum', - 'eth_base', - 'eth_optimism', - 'eth_polygon', - 'weth', - 'wbtc', - 'matic_polygon', - 'polygon', - 'usdc_arbitrum', - 'usdc_base', - 'usdc_optimism', - 'usdc_polygon', -] - -export const fiatOnRampApi = createApi({ - reducerPath: 'fiatOnRampApi', - baseQuery: fetchBaseQuery({ baseUrl: config.moonpayApiUrl }), - endpoints: (builder) => ({ - fiatOnRampIpAddress: builder.query({ - queryFn: () => - // TODO: [MOB-223] consider a reverse proxy for privacy reasons - fetch(`${config.moonpayApiUrl}/v4/ip_address?${COMMON_QUERY_PARAMS}`) - .then((response) => response.json()) - .then((response: MoonpayIPAddressesResponse) => { - sendAnalyticsEvent(MoonpayEventName.MOONPAY_GEOCHECK_COMPLETED, { - success: response.isBuyAllowed ?? false, - networkError: false, - }) - return { data: response } - }) - .catch((e) => { - sendAnalyticsEvent(MoonpayEventName.MOONPAY_GEOCHECK_COMPLETED, { - success: false, - networkError: true, - }) - - return { data: undefined, error: e } - }), - }), - fiatOnRampSupportedTokens: builder.query< - MoonpayCurrency[], - { - isUserInUS: boolean - stateInUS?: string - } - >({ - queryFn: ({ isUserInUS, stateInUS }) => - // TODO: [MOB-223] consider a reverse proxy for privacy reasons - fetch(`${config.moonpayApiUrl}/v3/currencies?${COMMON_QUERY_PARAMS}`) - .then((response) => response.json()) - .then((response: MoonpayListCurrenciesResponse) => { - const moonpaySupportField = __DEV__ ? 'supportsTestMode' : 'supportsLiveMode' - return { - data: response.filter( - (c) => - c.type === 'crypto' && - c[moonpaySupportField] && - (!isUserInUS || - (c.isSupportedInUS && - (!stateInUS || c.notAllowedUSStates.indexOf(stateInUS) === -1))) - ), - } - }) - .catch((e) => { - return { data: undefined, error: e } - }), - }), - fiatOnRampBuyQuote: builder.query< - MoonpayBuyQuoteResponse, - { - quoteCurrencyCode: string - baseCurrencyCode: string - baseCurrencyAmount: string - areFeesIncluded: boolean - } - >({ - queryFn: ({ quoteCurrencyCode, baseCurrencyCode, baseCurrencyAmount, areFeesIncluded }) => - // TODO: [MOB-223] consider a reverse proxy for privacy reasons - fetch( - `${ - config.moonpayApiUrl - }/v3/currencies/${quoteCurrencyCode}/buy_quote?${serializeQueryParams({ - baseCurrencyCode, - baseCurrencyAmount, - areFeesIncluded, - apiKey: config.moonpayApiKey, - })}` - ) - .then((response) => response.json()) - .then((response: MoonpayBuyQuoteResponse) => { - return { data: response } - }) - .catch((e) => { - return { data: undefined, error: e } - }), - }), - fiatOnRampLimits: builder.query< - MoonpayLimitsResponse, - { - quoteCurrencyCode: string - baseCurrencyCode: string - areFeesIncluded: boolean - } - >({ - queryFn: ({ quoteCurrencyCode, baseCurrencyCode, areFeesIncluded }) => - // TODO: [MOB-223] consider a reverse proxy for privacy reasons - fetch( - `${config.moonpayApiUrl}/v3/currencies/${quoteCurrencyCode}/limits?${serializeQueryParams( - { - baseCurrencyCode, - areFeesIncluded, - apiKey: config.moonpayApiKey, - } - )}` - ) - .then((response) => response.json()) - .then((response: MoonpayLimitsResponse) => { - return { data: response } - }) - .catch((e) => { - return { data: undefined, error: e } - }), - }), - - fiatOnRampWidgetUrl: builder.query< - string, - FiatOnRampWidgetUrlQueryParameters & { - ownerAddress: Address - amount: string - currencyCode: string - baseCurrencyCode: string - redirectUrl?: string - } - >({ - query: ({ ownerAddress, amount, currencyCode, baseCurrencyCode, redirectUrl, ...rest }) => ({ - url: config.moonpayWidgetApiUrl, - body: { - ...rest, - defaultCurrencyCode: 'eth', - currencyCode, - baseCurrencyCode, - baseCurrencyAmount: amount, - redirectURL: redirectUrl, - walletAddresses: JSON.stringify( - supportedCurrencyCodes.reduce>((acc, code: string) => { - acc[code] = ownerAddress - return acc - }, {}) - ), - }, - method: 'POST', - }), - transformResponse: (response: FiatOnRampWidgetUrlQueryResponse) => response.url, - }), - }), -}) - -export const { - useFiatOnRampIpAddressQuery, - useFiatOnRampWidgetUrlQuery, - useFiatOnRampSupportedTokensQuery, - useFiatOnRampBuyQuoteQuery, - useFiatOnRampLimitsQuery, -} = fiatOnRampApi - -export const fiatOnRampAggregatorApi = createApi({ - reducerPath: 'fiatOnRampAggregatorApi-wallet', - baseQuery: fetchBaseQuery({ - baseUrl: uniswapUrls.fiatOnRampApiUrl, - headers: FOR_API_HEADERS, - }), - endpoints: (builder) => ({ - fiatOnRampAggregatorTransaction: builder.query({ - async queryFn(args, { getState }, _extraOptions, baseQuery) { - try { - const account = selectActiveAccount(getState() as RootState) - const signerManager = walletContextValue.signers - - if (!account) { - throw new Error('No active account') - } - const { requestParams, signature } = await createSignedRequestParams( - args, - account, - signerManager - ) - const result = await baseQuery({ - url: `/transaction?${objectToQueryString(requestParams)}`, - method: 'GET', - headers: { - 'x-uni-sig': signature, - }, - }) - if (result.error) { - return { error: result.error } - } - return { data: result.data as FORTransactionResponse } - } catch (error) { - return { error: { status: 'FETCH_ERROR', error: String(error) } } - } - }, - }), - }), -}) - -export const { useFiatOnRampAggregatorTransactionQuery } = fiatOnRampAggregatorApi - -/** - * Utility to fetch fiat onramp transactions from moonpay - */ -export function fetchMoonpayTransaction( - previousTransactionDetails: FiatOnRampTransactionDetails -): Promise { - return fetch( - `${config.moonpayApiUrl}/v1/transactions/ext/${previousTransactionDetails.id}?${COMMON_QUERY_PARAMS}` - ).then((res) => { - if (res.status === TRANSACTION_NOT_FOUND) { - // If Moonpay API returned 404 for the given external transaction id - // (meaning it was not /yet/ found on their end, e.g. user has not finished flow) - // we opt to put a dummy placeholder transaction in the user's activity feed. - // to avoid leaving placeholders as "pending" for too long, we mark them - // as "unknown" after some time - const isStale = dayjs(previousTransactionDetails.addedTime).isBefore( - dayjs().subtract(FIAT_ONRAMP_STALE_TX_TIMEOUT, 'ms') - ) - - if (isStale) { - logger.debug( - 'fiatOnRamp/api', - 'fetchFiatOnRampTransaction', - `Transaction with id ${previousTransactionDetails.id} not found.` - ) - - return { - ...previousTransactionDetails, - // use `Unknown` status to denote a transaction missing from backend - // this transaction will later get deleted - status: TransactionStatus.Unknown, - } - } else { - logger.debug( - 'fiatOnRamp/api', - 'fetchFiatOnRampTransaction', - `Transaction with id ${ - previousTransactionDetails.id - } not found, but not stale yet (${dayjs() - .subtract(previousTransactionDetails.addedTime, 'ms') - .unix()}s old).` - ) - - return previousTransactionDetails - } - } - - return res.json().then((transactions: MoonpayTransactionsResponse) => - extractMoonpayTransactionDetails( - // log while we have the full moonpay tx response - transactions.sort((a, b) => (dayjs(a.createdAt).isAfter(dayjs(b.createdAt)) ? 1 : -1))?.[0] - ) - ) - }) -} - /** * Utility to fetch fiat onramp transactions */ export async function fetchFiatOnRampTransaction( previousTransactionDetails: FiatOnRampTransactionDetails, forceFetch: boolean, - account: Account, - signerManager: SignerManager ): Promise { - // Force fetch if requested or for the first 3 minutes after the transaction was added - const shouldForceFetch = shouldForceFetchTransaction(previousTransactionDetails, forceFetch) - - const { requestParams, signature } = await createSignedRequestParams( - { sessionId: previousTransactionDetails.id, forceFetch: shouldForceFetch }, - account, - signerManager - ) - const res = await fetch( - `${uniswapUrls.fiatOnRampApiUrl}/transaction?${objectToQueryString(requestParams)}`, - { - headers: { - 'x-uni-sig': signature, - ...FOR_API_HEADERS, - }, - } - ) + const requestParams = { + sessionId: previousTransactionDetails.id, + // Force fetch if requested or for the first 3 minutes after the transaction was added + forceFetch: shouldForceFetchTransaction(previousTransactionDetails, forceFetch), + } + const res = await fetch(`${uniswapUrls.fiatOnRampApiUrl}/transaction?${objectToQueryString(requestParams)}`, { + headers: FOR_API_HEADERS, + }) const { transaction }: FORTransactionResponse = await res.json() if (!transaction) { const isStale = dayjs(previousTransactionDetails.addedTime).isBefore( - dayjs().subtract(FIAT_ONRAMP_STALE_TX_TIMEOUT, 'ms') + dayjs().subtract(FIAT_ONRAMP_STALE_TX_TIMEOUT, 'ms'), ) if (isStale) { logger.debug( 'fiatOnRamp/api', 'fetchFiatOnRampTransaction', - `Transaction with id ${previousTransactionDetails.id} not found.` + `Transaction with id ${previousTransactionDetails.id} not found.`, ) return { @@ -357,11 +50,9 @@ export async function fetchFiatOnRampTransaction( logger.debug( 'fiatOnRamp/api', 'fetchFiatOnRampTransaction', - `Transaction with id ${ - previousTransactionDetails.id - } not found, but not stale yet (${dayjs() + `Transaction with id ${previousTransactionDetails.id} not found, but not stale yet (${dayjs() .subtract(previousTransactionDetails.addedTime, 'ms') - .unix()}s old).` + .unix()}s old).`, ) return previousTransactionDetails @@ -373,23 +64,11 @@ export async function fetchFiatOnRampTransaction( function shouldForceFetchTransaction( previousTransactionDetails: FiatOnRampTransactionDetails, - forceFetch: boolean + forceFetch: boolean, ): boolean { const isRecent = dayjs(previousTransactionDetails.addedTime).isAfter( - dayjs().subtract(FIAT_ONRAMP_FORCE_FETCH_TX_TIMEOUT, 'ms') + dayjs().subtract(FIAT_ONRAMP_FORCE_FETCH_TX_TIMEOUT, 'ms'), ) const isSyncedWithBackend = previousTransactionDetails.typeInfo?.syncedWithBackend return forceFetch || (isRecent && !isSyncedWithBackend) } - -export function useCexTransferProviders(isEnabled: boolean): FORServiceProvider[] { - const { data, isLoading } = useFiatOnRampAggregatorTransferServiceProvidersQuery(undefined, { - skip: !isEnabled, - }) - - if (isLoading || !data) { - return [] - } - - return data.serviceProviders -} diff --git a/packages/wallet/src/features/fiatOnRamp/hooks.ts b/packages/wallet/src/features/fiatOnRamp/hooks.ts deleted file mode 100644 index 6970d049374..00000000000 --- a/packages/wallet/src/features/fiatOnRamp/hooks.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { FiatCurrencyInfo } from 'uniswap/src/features/fiatOnRamp/types' -import { FiatCurrency } from 'wallet/src/features/fiatCurrency/constants' -import { useAppFiatCurrencyInfo, useFiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks' - -// MoonPay supported fiat currencies (https://support.moonpay.com/hc/en-gb/articles/360011931457-Which-fiat-currencies-are-supported-) -const MOONPAY_FIAT_CURRENCY_CODES = [ - 'aud', // Australian Dollar - 'bgn', // Bulgarian Lev - 'brl', // Brazilian Real - 'cad', // Canadian Dollar - 'chf', // Swiss Franc - 'cny', // Chinese Yuan - 'cop', // Colombia Peso - 'czk', // Czech Koruna - 'dkk', // Danish Krone - 'dop', // Dominican Peso - 'egp', // Egyptian Pound - 'eur', // Euro - 'gbp', // Pound Sterling - 'hkd', // Hong Kong Dollar - 'idr', // Indonesian Rupiah - 'ils', // Israeli New Shekel - 'jpy', // Japanese Yen - 'jod', // Jordanian Dollar - 'kes', // Kenyan Shilling - 'krw', // South Korean Won - 'kwd', // Kuwaiti Dinar - 'lkr', // Sri Lankan Rupee - 'mad', // Moroccan Dirham - 'mxn', // Mexican Peso - 'ngn', // Nigerian Naira - 'nok', // Norwegian Krone - 'nzd', // New Zealand Dollar - 'omr', // Omani Rial - 'pen', // Peruvian Sol - 'pkr', // Pakistani Rupee - 'pln', // Polish Złoty - 'ron', // Romanian Leu - 'sek', // Swedish Krona - 'thb', // Thai Baht - 'try', // Turkish Lira - 'twd', // Taiwan Dollar - 'usd', // US Dollar - 'vnd', // Vietnamese Dong - 'zar', // South African Rand -] - -export function useMoonpayFiatCurrencySupportInfo(): { - appFiatCurrencySupportedInMoonpay: boolean - moonpaySupportedFiatCurrency: FiatCurrencyInfo -} { - // Not all the currencies are supported by MoonPay, so we need to fallback to USD if the currency is not supported - const appFiatCurrencyInfo = useAppFiatCurrencyInfo() - const fallbackCurrencyInfo = useFiatCurrencyInfo(FiatCurrency.UnitedStatesDollar) - const appFiatCurrencyCode = appFiatCurrencyInfo.code.toLowerCase() - - const appFiatCurrencySupported = MOONPAY_FIAT_CURRENCY_CODES.includes(appFiatCurrencyCode) - const currency = appFiatCurrencySupported ? appFiatCurrencyInfo : fallbackCurrencyInfo - - return { - appFiatCurrencySupportedInMoonpay: appFiatCurrencySupported, - moonpaySupportedFiatCurrency: currency, - } -} diff --git a/packages/wallet/src/features/fiatOnRamp/types.ts b/packages/wallet/src/features/fiatOnRamp/types.ts index ac1b1e6318b..81480ac34dd 100644 --- a/packages/wallet/src/features/fiatOnRamp/types.ts +++ b/packages/wallet/src/features/fiatOnRamp/types.ts @@ -1,31 +1,9 @@ -import { - FiatPurchaseTransactionInfo, - TransactionDetails, -} from 'wallet/src/features/transactions/types' +import { FiatPurchaseTransactionInfo, TransactionDetails } from 'wallet/src/features/transactions/types' export type FiatOnRampTransactionDetails = TransactionDetails & { typeInfo: FiatPurchaseTransactionInfo } -export type FiatOnRampWidgetUrlQueryParameters = { - colorCode: string - externalTransactionId: string -} - -export type FiatOnRampWidgetUrlQueryResponse = { url: string } - -/** @ref https://dashboard.moonpay.com/api_reference/client_side_api#ip_address_object */ -export type MoonpayIPAddressData = { - alpha3?: string - state?: string - isAllowed?: boolean - isBuyAllowed?: boolean - isSellAllowed?: boolean -} - -/** @ref https://dashboard.moonpay.com/api_reference/client_side_api#check_ip_address */ -export type MoonpayIPAddressesResponse = MoonpayIPAddressData - /** @ref https://dashboard.moonpay.com/api_reference/client_side_api#currencies */ export type MoonpayCurrency = { id: string @@ -41,131 +19,3 @@ export type MoonpayCurrency = { chainId: string } } - -/** @ref https://dashboard.moonpay.com/api_reference/client_side_api#list_currencies */ -export type MoonpayListCurrenciesResponse = MoonpayCurrency[] - -/** @ref https://dashboard.moonpay.com/api_reference/client_side_api#get_currency_buy_quote */ -export type MoonpayBuyQuoteResponse = MoonpayQuote - -type CurrencyLimit = { - code: string - minBuyAmount: number - maxBuyAmount: number -} - -/** @ref https://dashboard.moonpay.com/api_reference/client_side_api#get_currency_limits */ -export type MoonpayLimitsResponse = { - paymentMethod: string - quoteCurrency: CurrencyLimit - baseCurrency: CurrencyLimit - areFeesIncluded: boolean -} - -/** @ref https://dashboard.moonpay.com/api_reference/client_side_api#transactions */ -type MoonpayQuote = { - // A positive integer representing how much the customer wants to spend. The minimum amount is 20. - baseCurrencyAmount: number - // A positive integer representing the amount of cryptocurrency the customer will receive. Set when the purchase of cryptocurrency has been executed. - quoteCurrencyAmount: number - quoteCurrencyPrice: number - // A positive integer representing the fee for the transaction. It is added to baseCurrencyAmount, extraFeeAmount and networkFeeAmount when the customer's card is charged. - feeAmount: number - // A positive integer representing your extra fee for the transaction. It is added to baseCurrencyAmount and feeAmount when the customer's card is charged. - extraFeeAmount: number - // A positive integer representing the network fee for the transaction. It is added to baseCurrencyAmount, feeAmount and extraFeeAmount when the customer's card is charged. - networkFeeAmount: number - // A boolean indicating whether baseCurrencyAmount includes or excludes the feeAmount, extraFeeAmount and networkFeeAmount. - areFeesIncluded: boolean -} - -/** - * Transaction objects represent cryptocurrency purchases by your end users. - * @ref https://dashboard.moonpay.com/api_reference/client_side_api#transactions - */ -export type MoonpayTransactionsResponse = Array - -export type MoonpayTransactionResponseItem = MoonpayQuote & { - // Unique identifier for the object. - id: string - // Time at which the object was created. Returned as an ISO 8601 string. - createdAt: string - // Time at which the object was last updated. Returned as an ISO 8601 string. - updatedAt: string - baseCurrency: MoonpayCurrency - currency: MoonpayCurrency - // The transaction's status. - status: 'waitingPayment' | 'pending' | 'waitingAuthorization' | 'failed' | 'completed' - // The transaction's failure reason. Set when transaction's status is failed. - failureReason: string - // The cryptocurrency wallet address the purchased funds will be sent to. - walletAddress: string - // The secondary cryptocurrency wallet address identifier for coins such as EOS, XRP and XMR. - walletAddressTag: string - // The cryptocurrency transaction identifier representing the transfer to the customer's wallet. Set when the withdrawal has been executed. - cryptoTransactionId: string - // The URL provided to you, when required, to which to redirect the customer as part of a redirect authentication flow. - redirectUrl: string - // The URL the customer is returned to after they authenticate or cancel their payment on the payment method’s app or site. If you are using our widget implementation, this is always our transaction tracker page, which provides the customer with real-time information about their transaction. - returnUrl: string - // The cryptocurrency transaction identifier representing the transfer from the customer's wallet to MoonPay's wallet. Set when the deposit has been executed and received. - depositHash?: string - // An optional URL used in a widget implementation. It is passed to us by you in the query parameters, and we include it as a link on the transaction tracker page. - widgetRedirectUrl: string - // The exchange rate between the transaction's base currency and Euro at the time of the transaction. - eurRate: number - // The exchange rate between the transaction's base currency and US Dollar at the time of the transaction. - usdRate: number - // The exchange rate between the transaction's base currency and British Pound at the time of the transaction. - gbpRate: number - // For bank transfer transactions, the information about our bank account to which the customer should make the transfer. - bankDepositInformation: object - // For bank transfer transactions, the reference code the customer should cite when making the transfer. - bankTransferReference: string - // The identifier of the cryptocurrency the customer wants to purchase. - currencyId: string - // The identifier of the fiat currency the customer wants to use for the transaction. - baseCurrencyId: string - // The identifier of the customer the transaction is associated with. - customerId: string - // For token or card transactions, the identifier of the payment card used for this transaction. - cardId: string - // For bank transfer transactions, the identifier of the bank account used for this transaction. - bankAccountId: string - // An identifier associated with the customer, provided by you. - externalCustomerId: string - // The transaction's payment method. Possible values are credit_debit_card, sepa_bank_transfer, sepa_open_banking_payment, gbp_bank_transfer, gbp_open_banking_payment, ach_bank_transfer, pix_instant_payment and mobile_wallet - paymentMethod: string - // An identifier associated with the transaction, provided by you. - externalTransactionId: string - // The customer's country. Returned as an ISO 3166-1 alpha-3 code. - country: string - // The customer's state, if the customer is from the USA. Returned as a two-letter code. - state: string - // An array of four objects, each representing one of the four stages of the purchase process. The attributes of each stage are described below. - stages: Array<{ - stage: - | 'stage_one_ordering' - | 'stage_two_verification' - | 'stage_three_processing' - | 'stage_four_delivery' - status: 'not_started' | 'in_progress' | 'success' | 'failed' - failureReason: - | 'card_not_supported' - | 'daily_purchase_limit_exceeded' - | 'payment_authorization_declined' - | 'timeout_3d_secure' - | 'timeout_bank_transfer' - | 'timeout_kyc_verification' - | 'timeout_card_verification' - | 'rejected_kyc' - | 'rejected_card' - | 'rejected_other' - | 'cancelled' - | 'refund' - | 'failed_testnet_withdrawal' - | 'error' - // Sometimes| the customer is required to take an action or actions to further the purchase process| usually by submitting information at a provided URL. For each action| we pass an object with a type and a url. - actions: 'complete_bank_transfer' | 'retry_kyc' | 'verify_card_by_code' | 'verify_card_by_file' - }> -} diff --git a/packages/wallet/src/features/fiatOnRamp/utils.test.ts b/packages/wallet/src/features/fiatOnRamp/utils.test.ts deleted file mode 100644 index 6ffa0404858..00000000000 --- a/packages/wallet/src/features/fiatOnRamp/utils.test.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { - isFiatOnRampApiError, - isInvalidRequestAmountTooHigh, - isInvalidRequestAmountTooLow, -} from 'wallet/src/features/fiatOnRamp/utils' - -describe('isFiatOnRampApiError', () => { - test('returns true', () => { - const error = { - data: { - statusCode: 400, - errorName: 'InvalidRequestAmountTooLow', - message: 'Source amount is below the minimum allowed, which is 50.00 USD', - }, - } - const result = isFiatOnRampApiError(error) - expect(result).toBe(true) - }) - - test('returns false', () => { - const error = { - data: { - message: 'Source amount is below the minimum allowed, which is 50.00 USD', - }, - } - const result = isFiatOnRampApiError(error) - expect(result).toBe(false) - }) -}) - -describe('isInvalidRequestAmountTooHigh', () => { - test('returns true', () => { - const error = { - data: { - statusCode: 400, - errorName: 'InvalidRequestAmountTooHigh', - message: 'Source amount is above the maximum allowed, which is 50000.00 USD', - context: { - maximumAllowed: 50000, - }, - }, - } - const result = isInvalidRequestAmountTooHigh(error) - expect(result).toBe(true) - }) - - test('returns false when context has unexpected type', () => { - const error = { - data: { - statusCode: 400, - errorName: 'InvalidRequestAmountTooHigh', - message: 'Source amount is above the maximum allowed, which is 50000.00 USD', - context: { - randomProperty: 50000, - }, - }, - } - const result = isInvalidRequestAmountTooHigh(error) - expect(result).toBe(false) - }) - - test('returns false when statusCode is not 400', () => { - const error = { - data: { - statusCode: 404, - errorName: 'InvalidRequestAmountTooHigh', - message: 'Source amount is above the maximum allowed, which is 50000.00 USD', - context: { - maximumAllowed: 50000, - }, - }, - } - const result = isInvalidRequestAmountTooHigh(error) - expect(result).toBe(false) - }) - - test('returns false when errorName is not InvalidRequestAmountTooHigh', () => { - const error = { - data: { - statusCode: 400, - errorName: 'InvalidRequestAmountTooBig', - message: 'Source amount is above the maximum allowed, which is 50000.00 USD', - context: { - maximumAllowed: 50000, - }, - }, - } - const result = isInvalidRequestAmountTooHigh(error) - expect(result).toBe(false) - }) -}) - -describe('isInvalidRequestAmountTooLow', () => { - test('returns true', () => { - const error = { - data: { - statusCode: 400, - errorName: 'InvalidRequestAmountTooLow', - message: 'Source amount is below the minimum allowed, which is 50.00 USD', - context: { - minimumAllowed: 50, - }, - }, - } - const result = isInvalidRequestAmountTooLow(error) - expect(result).toBe(true) - }) - - test('returns false when context has unexpected type', () => { - const error = { - data: { - statusCode: 400, - errorName: 'InvalidRequestAmountTooLow', - message: 'Source amount is below the minimum allowed, which is 50.00 USD', - context: { - randomProperty: 50, - }, - }, - } - const result = isInvalidRequestAmountTooLow(error) - expect(result).toBe(false) - }) - - test('returns false when statusCode is not 400', () => { - const error = { - data: { - statusCode: 404, - errorName: 'InvalidRequestAmountTooLow', - message: 'Source amount is below the minimum allowed, which is 50.00 USD', - context: { - minimumAllowed: 50, - }, - }, - } - const result = isInvalidRequestAmountTooLow(error) - expect(result).toBe(false) - }) - - test('returns false when errorName is not InvalidRequestAmountTooLow', () => { - const error = { - data: { - statusCode: 400, - errorName: 'InvalidRequestAmountTooSmall', - message: 'Source amount is below the minimum allowed, which is 50.00 USD', - context: { - minimumAllowed: 50, - }, - }, - } - const result = isInvalidRequestAmountTooLow(error) - expect(result).toBe(false) - }) -}) diff --git a/packages/wallet/src/features/fiatOnRamp/utils.ts b/packages/wallet/src/features/fiatOnRamp/utils.ts deleted file mode 100644 index df1e2693bb0..00000000000 --- a/packages/wallet/src/features/fiatOnRamp/utils.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { FORLogo } from 'uniswap/src/features/fiatOnRamp/types' - -export interface FORApiError { - data: { - statusCode: number - errorName: string - message: string - context: object | undefined - } -} - -export interface InvalidRequestAmountTooLow extends FORApiError { - data: FORApiError['data'] & { - statusCode: 400 - errorName: 'InvalidRequestAmountTooLow' - context: { - minimumAllowed: number - } - } -} - -export function isInvalidRequestAmountTooLow( - error: FORApiError -): error is InvalidRequestAmountTooLow { - const e = error as InvalidRequestAmountTooLow - return ( - e.data.statusCode === 400 && - e.data.errorName === 'InvalidRequestAmountTooLow' && - typeof e.data.context?.minimumAllowed === 'number' - ) -} - -export interface InvalidRequestAmountTooHigh extends FORApiError { - data: FORApiError['data'] & { - statusCode: 400 - errorName: 'InvalidRequestAmountTooHigh' - context: { - maximumAllowed: number - } - } -} - -export function isInvalidRequestAmountTooHigh( - error: FORApiError -): error is InvalidRequestAmountTooHigh { - const e = error as InvalidRequestAmountTooHigh - return ( - e.data.statusCode === 400 && - e.data.errorName === 'InvalidRequestAmountTooHigh' && - typeof e.data.context?.maximumAllowed === 'number' - ) -} - -export interface NoQuotesError extends FORApiError { - data: FORApiError['data'] & { - statusCode: 400 - errorName: 'NoQuotes' - } -} - -export function isNoQuotesError(error: FORApiError): error is InvalidRequestAmountTooHigh { - const e = error as NoQuotesError - return e.data.statusCode === 400 && e.data.errorName === 'NoQuotes' -} - -export function isFiatOnRampApiError(error: unknown): error is FORApiError { - if (typeof error === 'object' && error !== null) { - const e = error as FORApiError - return ( - typeof e.data === 'object' && - e.data !== null && - typeof e.data.statusCode === 'number' && - typeof e.data.errorName === 'string' - ) - } - return false -} - -export function getServiceProviderLogo(logos: FORLogo, isDarkMode: boolean): string { - return isDarkMode ? logos.darkLogo : logos.lightLogo -} diff --git a/packages/wallet/src/features/gas/adjustGasFee.ts b/packages/wallet/src/features/gas/adjustGasFee.ts index 70ef601ba21..1d5cd879633 100644 --- a/packages/wallet/src/features/gas/adjustGasFee.ts +++ b/packages/wallet/src/features/gas/adjustGasFee.ts @@ -17,14 +17,12 @@ export type FeeDetails = export function getAdjustedGasFeeDetails( request: providers.TransactionRequest, currentGasFeeParams: NonNullable, - adjustmentFactor: number + adjustmentFactor: number, ): FeeDetails { // Txn needs to be submitted with legacy gas params if (request.gasPrice) { const currentGasPrice = - 'gasPrice' in currentGasFeeParams - ? currentGasFeeParams.gasPrice - : currentGasFeeParams.maxFeePerGas + 'gasPrice' in currentGasFeeParams ? currentGasFeeParams.gasPrice : currentGasFeeParams.maxFeePerGas return { type: FeeType.Legacy, @@ -37,14 +35,10 @@ export function getAdjustedGasFeeDetails( // Txn needs to be submitted with EIP-1559 params if (request.maxFeePerGas && request.maxPriorityFeePerGas) { const currentMaxFeePerGas = - 'maxFeePerGas' in currentGasFeeParams - ? currentGasFeeParams.maxFeePerGas - : currentGasFeeParams.gasPrice + 'maxFeePerGas' in currentGasFeeParams ? currentGasFeeParams.maxFeePerGas : currentGasFeeParams.gasPrice const currentMaxPriorityFeePerGas = - 'maxFeePerGas' in currentGasFeeParams - ? currentGasFeeParams.maxPriorityFeePerGas - : currentGasFeeParams.gasPrice + 'maxFeePerGas' in currentGasFeeParams ? currentGasFeeParams.maxPriorityFeePerGas : currentGasFeeParams.gasPrice return { type: FeeType.Eip1559, @@ -53,7 +47,7 @@ export function getAdjustedGasFeeDetails( maxPriorityFeePerGas: multiplyByFactor( request.maxPriorityFeePerGas, currentMaxPriorityFeePerGas, - adjustmentFactor + adjustmentFactor, ), }, } @@ -64,13 +58,11 @@ export function getAdjustedGasFeeDetails( function determineError( request: providers.TransactionRequest, - currentGasFeeParams: NonNullable + currentGasFeeParams: NonNullable, ): Error { - const isEIP1559Transaction = - request.maxFeePerGas !== undefined && request.maxPriorityFeePerGas !== undefined + const isEIP1559Transaction = request.maxFeePerGas !== undefined && request.maxPriorityFeePerGas !== undefined - const isEIP1559Params = - 'maxFeePerGas' in currentGasFeeParams && 'maxPriorityFeePerGas' in currentGasFeeParams + const isEIP1559Params = 'maxFeePerGas' in currentGasFeeParams && 'maxPriorityFeePerGas' in currentGasFeeParams const isLegacyTransaction = request.gasPrice !== undefined const isLegacyParams = 'gasPrice' in currentGasFeeParams @@ -84,12 +76,10 @@ function determineError( const transactionMissingGasInfo = !isEIP1559Transaction && !isLegacyTransaction if (transactionMissingGasInfo) { if (isEIP1559Params) { - return new Error( - 'Transaction is missing gas values, but gasParams were provided for an EIP-1559 transaction.' - ) + return new Error('Transaction is missing gas values, but gasParams were provided for an EIP-1559 transaction.') } else { return new Error( - 'Transaction is missing gas values, but currentGasFeeParams were provided for a legacy transaction.' + 'Transaction is missing gas values, but currentGasFeeParams were provided for a legacy transaction.', ) } } @@ -99,12 +89,10 @@ function determineError( if (missingGasParams) { if (isEIP1559Transaction) { return new Error( - 'currentGasFeeParams is missing gas fee parameters. Required: maxFeePerGas and maxPriorityFeePerGas for EIP-1559 transactions.' + 'currentGasFeeParams is missing gas fee parameters. Required: maxFeePerGas and maxPriorityFeePerGas for EIP-1559 transactions.', ) } else { - return new Error( - 'currentGasFeeParams is missing gas fee parameters. Required: gasPrice for legacy transactions.' - ) + return new Error('currentGasFeeParams is missing gas fee parameters. Required: gasPrice for legacy transactions.') } } @@ -112,7 +100,7 @@ function determineError( const EIP1559RequestMissingGasParams = isEIP1559Transaction && !isEIP1559Params if (EIP1559RequestMissingGasParams) { return new Error( - 'Transaction request specifies EIP-1559 gas values, but currentGasFeeParams lacks corresponding EIP-1559 parameters.' + 'Transaction request specifies EIP-1559 gas values, but currentGasFeeParams lacks corresponding EIP-1559 parameters.', ) } @@ -120,7 +108,7 @@ function determineError( const legacyRequestMissingGasParams = isLegacyTransaction && !isLegacyParams if (legacyRequestMissingGasParams) { return new Error( - 'Transaction request specifies Legacy gasPrice, but currentGasFeeParams lacks a corresponding gasPrice.' + 'Transaction request specifies Legacy gasPrice, but currentGasFeeParams lacks a corresponding gasPrice.', ) } @@ -129,11 +117,7 @@ function determineError( return new Error('Unable to determine gas fee structure.') } -function multiplyByFactor( - value: BigNumberish, - minValue: BigNumberish | null, - adjustmentFactor: number -): string { +function multiplyByFactor(value: BigNumberish, minValue: BigNumberish | null, adjustmentFactor: number): string { const baseValue = BigNumberMax(BigNumber.from(value), BigNumber.from(minValue ?? 0)) return Math.floor(baseValue.toNumber() * adjustmentFactor).toString() } diff --git a/packages/wallet/src/features/gas/api.ts b/packages/wallet/src/features/gas/api.ts index 7e7821b391c..eae93595aad 100644 --- a/packages/wallet/src/features/gas/api.ts +++ b/packages/wallet/src/features/gas/api.ts @@ -1,14 +1,14 @@ import { providers } from 'ethers' +import { PollingInterval } from 'uniswap/src/constants/misc' import { uniswapUrls } from 'uniswap/src/constants/urls' import { useRestQuery } from 'uniswap/src/data/rest' -import { PollingInterval } from 'wallet/src/constants/misc' -import { getPollingIntervalByBlocktime } from 'wallet/src/features/chains/utils' +import { getPollingIntervalByBlocktime } from 'uniswap/src/features/chains/utils' import { GasFeeResponse } from 'wallet/src/features/gas/types' export function useGasFeeQuery( tx: Maybe, skip?: boolean, - pollingInterval?: PollingInterval + pollingInterval?: PollingInterval, ): ReturnType> { return useRestQuery( uniswapUrls.gasServicePath, @@ -20,6 +20,6 @@ export function useGasFeeQuery( pollInterval: pollingInterval ?? getPollingIntervalByBlocktime(tx?.chainId), skip: skip || !tx, ttlMs: pollingInterval ?? getPollingIntervalByBlocktime(tx?.chainId), - } + }, ) } diff --git a/packages/wallet/src/features/gas/formatExternalTxnWithGasEstimates.tsx b/packages/wallet/src/features/gas/formatExternalTxnWithGasEstimates.tsx new file mode 100644 index 00000000000..83a1652b7a1 --- /dev/null +++ b/packages/wallet/src/features/gas/formatExternalTxnWithGasEstimates.tsx @@ -0,0 +1,43 @@ +import { providers } from 'ethers' +import { GasFeeResult } from 'wallet/src/features/gas/types' + +/** + * This util should be used for formatting all external txn requests with gas estimates. This is + * primarily WC transactions and dapp transactions on extension. + * + * We should always be using the estimates from a dapp if they are provided. `gasLimit` will not + * always be included along with fee estimates - use our limit in that case if missing. + * + * If no valid fee combination is found (for legacy type 1, or eip1559 type 2), we should use our own + * estimates instead. Our estimates come from a request to our gas service in both WC and Dapp interaction + * flows. + * + */ + +export function formatExternalTxnWithGasEstimates({ + transaction, + gasFeeResult, +}: { + transaction: providers.TransactionRequest + gasFeeResult: GasFeeResult +}): providers.TransactionRequest { + const { gasLimit: gasLimitDapp, gasPrice, maxFeePerGas, maxPriorityFeePerGas } = transaction + const requestHasLegacyGasValues = !!gasPrice + const requestHasEIP1559GasValues = !!maxFeePerGas && !!maxPriorityFeePerGas + const requestHasValidGasEstimates = requestHasLegacyGasValues || requestHasEIP1559GasValues + + if (requestHasValidGasEstimates) { + return { + ...transaction, + // Avoid `??` in case dapp passes empty string + gasLimit: gasLimitDapp || gasFeeResult?.params?.gasLimit, + } + } + + const formattedTxnWithGasEstimates: providers.TransactionRequest = { + ...transaction, + ...gasFeeResult.params, + } + + return formattedTxnWithGasEstimates +} diff --git a/packages/wallet/src/features/gas/hooks.ts b/packages/wallet/src/features/gas/hooks.ts index 9a53ccf2890..6bdceb6d2c4 100644 --- a/packages/wallet/src/features/gas/hooks.ts +++ b/packages/wallet/src/features/gas/hooks.ts @@ -1,19 +1,21 @@ import { BigNumber, providers } from 'ethers' -import { useMemo } from 'react' +import { useCallback, useMemo } from 'react' +import { PollingInterval } from 'uniswap/src/constants/misc' +import { NativeCurrency } from 'uniswap/src/features/tokens/NativeCurrency' import { WalletChainId } from 'uniswap/src/types/chains' import { logger } from 'utilities/src/logger/logger' -import { PollingInterval } from 'wallet/src/constants/misc' +import { useAsyncData } from 'utilities/src/react/hooks' import { TRANSACTION_CANCELLATION_GAS_FACTOR } from 'wallet/src/constants/transactions' import { FeeDetails, getAdjustedGasFeeDetails } from 'wallet/src/features/gas/adjustGasFee' import { useGasFeeQuery } from 'wallet/src/features/gas/api' import { FeeType, GasFeeResult, GasSpeed } from 'wallet/src/features/gas/types' -import { NativeCurrency } from 'wallet/src/features/tokens/NativeCurrency' +import { getCancelOrderTxRequest } from 'wallet/src/features/transactions/cancelTransactionSaga' import { useUSDCValue } from 'wallet/src/features/transactions/swap/trade/hooks/useUSDCPrice' import { isUniswapX } from 'wallet/src/features/transactions/swap/trade/utils' import { TransactionDetails } from 'wallet/src/features/transactions/types' import { ValueType, getCurrencyAmount } from 'wallet/src/utils/getCurrencyAmount' -type CancelationGasFeeDetails = { +export type CancelationGasFeeDetails = { cancelRequest: providers.TransactionRequest cancelationGasFee: string } @@ -22,7 +24,7 @@ export function useTransactionGasFee( tx: Maybe, speed: GasSpeed = GasSpeed.Urgent, skip?: boolean, - pollingInterval?: PollingInterval + pollingInterval?: PollingInterval, ): GasFeeResult { const { data, error, loading } = useGasFeeQuery(tx, skip, pollingInterval) @@ -65,10 +67,8 @@ export function useUSDValue(chainId?: WalletChainId, ethValueInWei?: string): st * Construct cancelation transaction with increased gas (based on current network conditions), * then use it to compute new gas info. */ -export function useCancelationGasFeeInfo( - transaction: TransactionDetails -): CancelationGasFeeDetails | undefined { - const cancelationRequest = useMemo(() => { +export function useCancelationGasFeeInfo(transaction: TransactionDetails): CancelationGasFeeDetails | undefined { + const classicCancelRequest = useMemo(() => { return { chainId: transaction.chainId, from: transaction.from, @@ -77,14 +77,21 @@ export function useCancelationGasFeeInfo( } }, [transaction]) - const baseTxGasFee = useTransactionGasFee(cancelationRequest, GasSpeed.Urgent) + const isUniswapXTx = isUniswapX(transaction) + + const uniswapXCancelRequest = useUniswapXCancelRequest(transaction) + const uniswapXGasFee = useTransactionGasFee(uniswapXCancelRequest?.data, GasSpeed.Urgent, !isUniswapXTx) + + const baseTxGasFee = useTransactionGasFee(classicCancelRequest, GasSpeed.Urgent, /* skip = */ isUniswapXTx) return useMemo(() => { - if (!baseTxGasFee.params) { - return + if (isUniswapXTx) { + if (!uniswapXCancelRequest.data || !uniswapXGasFee.value) { + return + } + return { cancelRequest: uniswapXCancelRequest.data, cancelationGasFee: uniswapXGasFee.value } } - // TODO(WEB-4295): handle uniswapx cancels - if (isUniswapX(transaction)) { + if (!baseTxGasFee.params) { return } @@ -93,7 +100,7 @@ export function useCancelationGasFeeInfo( adjustedFeeDetails = getAdjustedGasFeeDetails( transaction.options.request, baseTxGasFee.params, - TRANSACTION_CANCELLATION_GAS_FACTOR + TRANSACTION_CANCELLATION_GAS_FACTOR, ) } catch (error) { logger.error(error, { @@ -104,7 +111,7 @@ export function useCancelationGasFeeInfo( } const cancelRequest = { - ...cancelationRequest, + ...classicCancelRequest, ...adjustedFeeDetails.params, gasLimit: baseTxGasFee.params.gasLimit, } @@ -113,7 +120,14 @@ export function useCancelationGasFeeInfo( cancelRequest, cancelationGasFee: getCancelationGasFee(adjustedFeeDetails, baseTxGasFee.params.gasLimit), } - }, [baseTxGasFee.params, cancelationRequest, transaction]) + }, [ + isUniswapXTx, + baseTxGasFee.params, + classicCancelRequest, + transaction, + uniswapXCancelRequest.data, + uniswapXGasFee.value, + ]) } function getCancelationGasFee(adjustedFeeDetails: FeeDetails, gasLimit: string): string { @@ -124,3 +138,18 @@ function getCancelationGasFee(adjustedFeeDetails: FeeDetails, gasLimit: string): return BigNumber.from(adjustedFeeDetails.params.maxFeePerGas).mul(gasLimit).toString() } + +function useUniswapXCancelRequest(transaction: TransactionDetails): { + isLoading: boolean + data: providers.TransactionRequest | undefined + error?: Error +} { + const cancelRequestFetcher = useCallback(() => { + if (!isUniswapX(transaction)) { + return undefined + } + return getCancelOrderTxRequest(transaction) + }, [transaction]) + + return useAsyncData(cancelRequestFetcher) +} diff --git a/packages/wallet/src/features/gating/userPropertyHooks.ts b/packages/wallet/src/features/gating/userPropertyHooks.ts index 464666e1ea2..cf70155d25d 100644 --- a/packages/wallet/src/features/gating/userPropertyHooks.ts +++ b/packages/wallet/src/features/gating/userPropertyHooks.ts @@ -1,11 +1,11 @@ import { useEffect } from 'react' import { Statsig } from 'uniswap/src/features/gating/sdk/statsig' import { useUnitagByAddress } from 'uniswap/src/features/unitags/hooks' +import { getValidAddress } from 'uniswap/src/utils/addresses' import { logger } from 'utilities/src/logger/logger' import { useENSName } from 'wallet/src/features/ens/api' import { AccountType } from 'wallet/src/features/wallet/accounts/types' import { useActiveAccount } from 'wallet/src/features/wallet/hooks' -import { getValidAddress } from 'wallet/src/utils/addresses' export function useGatingUserPropertyUsernames(): void { const activeAccount = useActiveAccount() @@ -24,12 +24,7 @@ export function useGatingUserPropertyUsernames(): void { ens: ens?.split('.')[0], }, }).catch((error) => { - logger.warn( - 'userPropertyHooks', - 'useGatingUserPropertyUsernames', - 'Failed to set usernames for gating', - error - ) + logger.warn('userPropertyHooks', 'useGatingUserPropertyUsernames', 'Failed to set usernames for gating', error) }) } }, [activeAccount, ens, unitag?.username]) diff --git a/packages/wallet/src/features/images/ImageUri.native.tsx b/packages/wallet/src/features/images/ImageUri.native.tsx index 2c78808b89d..53120f4833b 100644 --- a/packages/wallet/src/features/images/ImageUri.native.tsx +++ b/packages/wallet/src/features/images/ImageUri.native.tsx @@ -16,9 +16,7 @@ export function ImageUri({ imageDimensions, ...rest }: ImageUriProps): JSX.Element | null { - const inputImageAspectRatio = imageDimensions - ? imageDimensions?.width / imageDimensions?.height - : 1 + const inputImageAspectRatio = imageDimensions ? imageDimensions?.width / imageDimensions?.height : 1 const [isError, setIsError] = useState(false) const isLoaded = useSharedValue(false) @@ -64,11 +62,7 @@ export function ImageUri({ uri, cache: FastImage.cacheControl.immutable, }} - style={[ - styles.image, - imageStyle ?? [styles.fullWidth, { maxHeight: maxHeight ?? '100%' }], - { aspectRatio }, - ]} + style={[styles.image, imageStyle ?? [styles.fullWidth, { maxHeight: maxHeight ?? '100%' }], { aspectRatio }]} onError={(): void => setIsError(true)} onLoad={({ nativeEvent: { width, height } }: OnLoadEvent): void => { isLoaded.value = true diff --git a/packages/wallet/src/features/images/ImageUri.web.tsx b/packages/wallet/src/features/images/ImageUri.web.tsx index 00612d30943..03ed0ac91d6 100644 --- a/packages/wallet/src/features/images/ImageUri.web.tsx +++ b/packages/wallet/src/features/images/ImageUri.web.tsx @@ -4,12 +4,7 @@ import { Flex, Loader } from 'ui/src' import { ImageUriProps } from 'wallet/src/features/images/ImageUri' import { RemoteImage } from 'wallet/src/features/images/RemoteImage' -export function ImageUri({ - uri, - fallback, - loadingContainerStyle, - imageDimensions, -}: ImageUriProps): JSX.Element | null { +export function ImageUri({ uri, fallback, loadingContainerStyle, imageDimensions }: ImageUriProps): JSX.Element | null { const [height, setHeight] = useState(imageDimensions?.height ?? null) const [width, setWidth] = useState(imageDimensions?.width ?? null) const [isError, setIsError] = useState(false) @@ -28,7 +23,7 @@ export function ImageUri({ }, () => { setIsError(true) - } + }, ) }, [imageDimensions, uri]) @@ -48,13 +43,5 @@ export function ImageUri({ } // TODO: get sizing and other params accounted for - return ( - - ) + return } diff --git a/packages/wallet/src/features/images/NFTPreviewImage.tsx b/packages/wallet/src/features/images/NFTPreviewImage.tsx index ded76ecffad..609b0a2f33a 100644 --- a/packages/wallet/src/features/images/NFTPreviewImage.tsx +++ b/packages/wallet/src/features/images/NFTPreviewImage.tsx @@ -8,22 +8,14 @@ type NFTPreviewProps = { imageProps: ImageUriProps } -export function NFTPreviewImage({ - contractAddress, - tokenId, - imageProps, -}: NFTPreviewProps): JSX.Element | null { +export function NFTPreviewImage({ contractAddress, tokenId, imageProps }: NFTPreviewProps): JSX.Element | null { const { data, loading } = useNftPreviewUri(contractAddress, tokenId) const imageUrl = data?.previews?.image_medium_url if (imageUrl || loading) { return ( - + ) } diff --git a/packages/wallet/src/features/images/NFTViewer.tsx b/packages/wallet/src/features/images/NFTViewer.tsx index a85ccdb71cd..66e0ee87e94 100644 --- a/packages/wallet/src/features/images/NFTViewer.tsx +++ b/packages/wallet/src/features/images/NFTViewer.tsx @@ -2,6 +2,7 @@ import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { StyleSheet } from 'react-native' import { Flex, Text } from 'ui/src' +import { isAddress, shortenAddress } from 'utilities/src/addresses' import { isGifUri, isSVGUri, uriToHttpUrls } from 'utilities/src/format/urls' import { ImageUri, ImageUriProps } from 'wallet/src/features/images/ImageUri' import { NFTPreviewImage } from 'wallet/src/features/images/NFTPreviewImage' @@ -47,21 +48,18 @@ export function NFTViewer(props: Props): JSX.Element { const { t } = useTranslation() const imageHttpUri = uri ? uriToHttpUrls(uri)[0] : undefined - const fallback = useMemo( - () => ( - - - {placeholderContent || t('tokens.nfts.error.unavailable')} + const fallback = useMemo(() => { + const isPlaceholderAddress = isAddress(placeholderContent) + return ( + + + {isPlaceholderAddress + ? shortenAddress(isPlaceholderAddress) + : placeholderContent || t('tokens.nfts.error.unavailable')} - ), - [placeholderContent, maxHeight, t] - ) + ) + }, [placeholderContent, maxHeight, t]) if (!imageHttpUri) { // Sometimes Opensea does not return any asset, show placeholder @@ -78,9 +76,7 @@ export function NFTViewer(props: Props): JSX.Element { const isGif = isGifUri(imageHttpUri) const formattedUri = - isGif && limitGIFSize - ? convertGIFUriToSmallImageFormat(imageHttpUri, limitGIFSize) - : imageHttpUri + isGif && limitGIFSize ? convertGIFUriToSmallImageFormat(imageHttpUri, limitGIFSize) : imageHttpUri const imageProps: ImageUriProps = { fallback, @@ -106,13 +102,7 @@ export function NFTViewer(props: Props): JSX.Element { } if (!props.showSvgPreview) { - return ( - - ) + return } // Display fallback if preview data is not provided @@ -120,13 +110,7 @@ export function NFTViewer(props: Props): JSX.Element { return fallback } - return ( - - ) + return } const style = StyleSheet.create({ diff --git a/packages/wallet/src/features/images/RemoteImage.tsx b/packages/wallet/src/features/images/RemoteImage.tsx index 8abf2c92325..8d16a21de94 100644 --- a/packages/wallet/src/features/images/RemoteImage.tsx +++ b/packages/wallet/src/features/images/RemoteImage.tsx @@ -45,7 +45,8 @@ export function RemoteImage({ height={height} overflow="hidden" testID={testID} - width={width}> + width={width} + > ) diff --git a/packages/wallet/src/features/images/RemoteSvg.tsx b/packages/wallet/src/features/images/RemoteSvg.tsx index d6a423b30eb..fc40ec9c6ed 100644 --- a/packages/wallet/src/features/images/RemoteSvg.tsx +++ b/packages/wallet/src/features/images/RemoteSvg.tsx @@ -41,7 +41,5 @@ export const RemoteSvg = ({ if (!svg) { return } - return ( - - ) + return } diff --git a/packages/wallet/src/features/images/WebSvgUri.web.tsx b/packages/wallet/src/features/images/WebSvgUri.web.tsx index a9d01ae4600..04e41dc2994 100644 --- a/packages/wallet/src/features/images/WebSvgUri.web.tsx +++ b/packages/wallet/src/features/images/WebSvgUri.web.tsx @@ -3,12 +3,5 @@ import { SvgUriProps } from 'wallet/src/features/images/WebSvgUri' export function WebSvgUri({ maxHeight, uri }: SvgUriProps): JSX.Element { // TODO: get sizing and other params accounted for - return ( - - ) + return } diff --git a/packages/wallet/src/features/images/hooks.ts b/packages/wallet/src/features/images/hooks.ts index b7fc15c4a24..2ea70e0db14 100644 --- a/packages/wallet/src/features/images/hooks.ts +++ b/packages/wallet/src/features/images/hooks.ts @@ -39,16 +39,13 @@ type PreviewsResponse = { } | null } -export function useNftPreviewUri( - contractAddress: string, - tokenId: string -): GqlResult { +export function useNftPreviewUri(contractAddress: string, tokenId: string): GqlResult { return useRestQuery( `/nfts/ethereum/${contractAddress}/${tokenId}`, { contractAddress, tokenId }, ['previews'], { ttlMs: 5 * ONE_MINUTE_MS }, 'GET', - apolloClient + apolloClient, ) } diff --git a/packages/wallet/src/features/images/utils.tsx b/packages/wallet/src/features/images/utils.tsx index e82cf8f4873..4207e58fddb 100644 --- a/packages/wallet/src/features/images/utils.tsx +++ b/packages/wallet/src/features/images/utils.tsx @@ -9,11 +9,7 @@ export type SvgData = { aspectRatio: number } -export async function fetchSVG( - uri: string, - autoplay: boolean, - signal?: AbortSignal -): Promise { +export async function fetchSVG(uri: string, autoplay: boolean, signal?: AbortSignal): Promise { const res = await fetch(uri, { signal }) const text = await res.text() @@ -30,8 +26,7 @@ export async function fetchSVG( let aspectRatio = FALLBACK_ASPECT_RATIO try { - aspectRatio = - viewboxHeight && viewboxWidth ? +viewboxWidth / +viewboxHeight : FALLBACK_ASPECT_RATIO + aspectRatio = viewboxHeight && viewboxWidth ? +viewboxWidth / +viewboxHeight : FALLBACK_ASPECT_RATIO } catch (e) { logger.debug('images/utils', 'fetchSVG', 'Could not calculate aspect ratio ' + e) } diff --git a/packages/wallet/src/features/language/LocalizationContext.tsx b/packages/wallet/src/features/language/LocalizationContext.tsx index b85e408237d..2bb2ad7fb75 100644 --- a/packages/wallet/src/features/language/LocalizationContext.tsx +++ b/packages/wallet/src/features/language/LocalizationContext.tsx @@ -16,8 +16,7 @@ export type LocalizationContextState = { export const LocalizationContext = createContext(undefined) export function LocalizationContextProvider({ children }: { children: ReactNode }): JSX.Element { - const { formatNumberOrString, formatCurrencyAmount, formatPercent, addFiatSymbolToNumber } = - useLocalizedFormatter() + const { formatNumberOrString, formatCurrencyAmount, formatPercent, addFiatSymbolToNumber } = useLocalizedFormatter() const { convertFiatAmount, convertFiatAmountFormatted } = useFiatConverter({ formatNumberOrString, @@ -39,7 +38,7 @@ export function LocalizationContextProvider({ children }: { children: ReactNode formatCurrencyAmount, formatNumberOrString, formatPercent, - ] + ], ) return {children} diff --git a/packages/wallet/src/features/language/formatter.ts b/packages/wallet/src/features/language/formatter.ts index 383cd50f54a..c30430ff763 100644 --- a/packages/wallet/src/features/language/formatter.ts +++ b/packages/wallet/src/features/language/formatter.ts @@ -1,5 +1,6 @@ import { Currency, CurrencyAmount } from '@uniswap/sdk-core' import { useCallback, useMemo } from 'react' +import { FormatNumberOrStringInput } from 'uniswap/src/features/language/formatter' // eslint-disable-next-line no-restricted-imports import { addFiatSymbolToNumber, @@ -10,12 +11,6 @@ import { import { NumberType } from 'utilities/src/format/types' import { useCurrentLocale } from 'wallet/src/features/language/hooks' -type FormatNumberOrStringInput = { - value: Maybe - type?: NumberType - currencyCode?: string - placeholder?: string -} type FormatCurrencyAmountInput = { value: CurrencyAmount | null | undefined type?: NumberType @@ -43,29 +38,24 @@ export function useLocalizedFormatter(): LocalizedFormatter { const locale = useCurrentLocale() const formatNumberOrStringInner = useCallback( - ({ - value, - type = NumberType.TokenNonTx, - currencyCode, - placeholder, - }: FormatNumberOrStringInput): string => + ({ value, type = NumberType.TokenNonTx, currencyCode, placeholder }: FormatNumberOrStringInput): string => formatNumberOrString({ price: value, locale, currencyCode, type, placeholder }), - [locale] + [locale], ) const formatCurrencyAmountInner = useCallback( ({ value, type, placeholder }: FormatCurrencyAmountInput): string => formatCurrencyAmount({ amount: value, locale, type, placeholder }), - [locale] + [locale], ) const formatPercentInner = useCallback( (value: Maybe): string => formatPercent(value, locale), - [locale] + [locale], ) const addFiatSymbolToNumberInner = useCallback( ({ value, currencyCode, currencySymbol }: AddFiatSymbolToNumberInput): string => addFiatSymbolToNumber({ value, currencyCode, currencySymbol, locale }), - [locale] + [locale], ) return useMemo( @@ -75,11 +65,6 @@ export function useLocalizedFormatter(): LocalizedFormatter { formatPercent: formatPercentInner, addFiatSymbolToNumber: addFiatSymbolToNumberInner, }), - [ - formatNumberOrStringInner, - formatCurrencyAmountInner, - formatPercentInner, - addFiatSymbolToNumberInner, - ] + [formatNumberOrStringInner, formatCurrencyAmountInner, formatPercentInner, addFiatSymbolToNumberInner], ) } diff --git a/packages/wallet/src/features/language/saga.ts b/packages/wallet/src/features/language/saga.ts index bdafe719feb..784bbdd1ac8 100644 --- a/packages/wallet/src/features/language/saga.ts +++ b/packages/wallet/src/features/language/saga.ts @@ -14,11 +14,7 @@ import { mapLocaleToLanguage, } from 'wallet/src/features/language/constants' import { getLocale } from 'wallet/src/features/language/hooks' -import { - selectCurrentLanguage, - setCurrentLanguage, - updateLanguage, -} from 'wallet/src/features/language/slice' +import { selectCurrentLanguage, setCurrentLanguage, updateLanguage } from 'wallet/src/features/language/slice' export function* appLanguageWatcherSaga() { yield* takeLatest(updateLanguage.type, appLanguageSaga) @@ -46,11 +42,7 @@ function* appLanguageSaga(action: ReturnType) { try { yield* call([i18n, i18n.changeLanguage], localeToSet) } catch (error) { - logger.warn( - 'language/saga', - 'appLanguageSaga', - 'Sync of language setting state and i18n instance failed' - ) + logger.warn('language/saga', 'appLanguageSaga', 'Sync of language setting state and i18n instance failed') } yield* call(restartAppIfRTL, localeToSet) @@ -82,11 +74,7 @@ function getDeviceLanguage(): Language { function restartAppIfRTL(currentLocale: Locale) { const isRtl = i18n.dir(currentLocale) === 'rtl' if (isRtl !== I18nManager.isRTL) { - logger.debug( - 'saga.ts', - 'restartAppIfRTL', - `Changing RTL to ${isRtl} for locale ${currentLocale}` - ) + logger.debug('saga.ts', 'restartAppIfRTL', `Changing RTL to ${isRtl} for locale ${currentLocale}`) I18nManager.forceRTL(isRtl) // Need to restart to apply RTL changes diff --git a/packages/wallet/src/features/language/slice.ts b/packages/wallet/src/features/language/slice.ts index ad994e6462c..2889ff3c5fe 100644 --- a/packages/wallet/src/features/language/slice.ts +++ b/packages/wallet/src/features/language/slice.ts @@ -24,10 +24,8 @@ const slice = createSlice({ export const { setCurrentLanguage, resetSettings } = slice.actions export const updateLanguage = createAction('language/updateLanguage') -export const syncAppWithDeviceLanguage = (): ReturnType => - updateLanguage(null) +export const syncAppWithDeviceLanguage = (): ReturnType => updateLanguage(null) -export const selectCurrentLanguage = (state: RootState): Language => - state.languageSettings.currentLanguage +export const selectCurrentLanguage = (state: RootState): Language => state.languageSettings.currentLanguage export const languageSettingsReducer = slice.reducer diff --git a/packages/wallet/src/features/nfts/hooks.ts b/packages/wallet/src/features/nfts/hooks.ts index c97d20ba19b..51b6c28bf76 100644 --- a/packages/wallet/src/features/nfts/hooks.ts +++ b/packages/wallet/src/features/nfts/hooks.ts @@ -1,10 +1,7 @@ import { useMemo } from 'react' -import { - NftsQuery, - useNftsQuery, -} from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { PollingInterval } from 'uniswap/src/constants/misc' +import { NftsQuery, useNftsQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { GqlResult } from 'uniswap/src/data/types' -import { PollingInterval } from 'wallet/src/constants/misc' import { selectNftsVisibility } from 'wallet/src/features/favorites/selectors' import { EMPTY_NFT_ITEM, @@ -19,11 +16,7 @@ export type GQLNftAsset = NonNullable< NonNullable[0]>['nftBalances']>[0] >['ownedAsset'] -export function useNFT( - owner: Address = '', - address?: Address, - tokenId?: string -): GqlResult { +export function useNFT(owner: Address = '', address?: Address, tokenId?: string): GqlResult { // TODO: [MOB-227] do a direct cache lookup in Apollo using id instead of re-querying const { data, loading, refetch } = useNftsQuery({ variables: { ownerAddress: owner }, @@ -34,11 +27,9 @@ export function useNFT( const nft = useMemo( () => data?.portfolios?.[0]?.nftBalances?.find( - (balance) => - balance?.ownedAsset?.nftContract?.address === address && - balance?.ownedAsset?.tokenId === tokenId + (balance) => balance?.ownedAsset?.nftContract?.address === address && balance?.ownedAsset?.tokenId === tokenId, )?.ownedAsset ?? undefined, - [data, address, tokenId] + [data, address, tokenId], ) return { data: nft, loading, refetch } @@ -47,7 +38,7 @@ export function useNFT( // Apply to NFTs fetched from API hidden filter, which is stored in Redux export function useGroupNftsByVisibility( nftDataItems: Array | undefined, - showHidden: boolean + showHidden: boolean, ): { nfts: Array numHidden: number @@ -73,7 +64,7 @@ export function useGroupNftsByVisibility( } return acc }, - { shown: [], hidden: [] } + { shown: [], hidden: [] }, ) return { nfts: [ diff --git a/packages/wallet/src/features/nfts/types.ts b/packages/wallet/src/features/nfts/types.ts index c7af4774ec3..7e17794e73e 100644 --- a/packages/wallet/src/features/nfts/types.ts +++ b/packages/wallet/src/features/nfts/types.ts @@ -1,7 +1,4 @@ -import { - Chain, - IAmount, -} from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { Chain, IAmount } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' export type NFTItem = { name?: string diff --git a/packages/wallet/src/features/nfts/useNftContextMenu.tsx b/packages/wallet/src/features/nfts/useNftContextMenu.tsx index c98c24dcf7a..1fbbbd293bf 100644 --- a/packages/wallet/src/features/nfts/useNftContextMenu.tsx +++ b/packages/wallet/src/features/nfts/useNftContextMenu.tsx @@ -2,8 +2,13 @@ import { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { NativeSyntheticEvent } from 'react-native' import { ContextMenuAction, ContextMenuOnPressNativeEvent } from 'react-native-context-menu-view' -import { GeneratedIcon, isWeb } from 'ui/src' +import { useDispatch } from 'react-redux' +import { GeneratedIcon, isWeb, useIsDarkMode } from 'ui/src' import { Eye, EyeOff } from 'ui/src/components/icons' +import { UNIVERSE_CHAIN_LOGO } from 'uniswap/src/assets/chainLogos' +import { WalletEventName } from 'uniswap/src/features/telemetry/constants' +import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' +import { WalletChainId } from 'uniswap/src/types/chains' import { ONE_SECOND_MS } from 'utilities/src/time/time' import { useWalletNavigation } from 'wallet/src/contexts/WalletNavigationContext' import { selectNftsVisibility } from 'wallet/src/features/favorites/selectors' @@ -12,7 +17,8 @@ import { getIsNftHidden, getNFTAssetKey } from 'wallet/src/features/nfts/utils' import { pushNotification } from 'wallet/src/features/notifications/slice' import { AppNotificationType } from 'wallet/src/features/notifications/types' import { useAccounts } from 'wallet/src/features/wallet/hooks' -import { useAppDispatch, useAppSelector } from 'wallet/src/state' +import { useAppSelector } from 'wallet/src/state' +import { getExplorerName } from 'wallet/src/utils/linking' interface NFTMenuParams { tokenId?: string @@ -20,6 +26,7 @@ interface NFTMenuParams { owner?: Address showNotification?: boolean isSpam?: boolean + chainId?: WalletChainId } type MenuAction = ContextMenuAction & { onPress: () => void; Icon?: GeneratedIcon } @@ -30,15 +37,17 @@ export function useNFTContextMenu({ owner, showNotification = false, isSpam, + chainId, }: NFTMenuParams): { menuActions: Array onContextMenuPress: (e: NativeSyntheticEvent) => void onlyShare: boolean } { const { t } = useTranslation() - const dispatch = useAppDispatch() + const dispatch = useDispatch() + const isDarkMode = useIsDarkMode() - const { handleShareNft } = useWalletNavigation() + const { handleShareNft, navigateToNftDetails } = useWalletNavigation() const accounts = useAccounts() const isLocalAccount = owner && !!accounts[owner] @@ -59,6 +68,14 @@ export function useNFTContextMenu({ return } + sendAnalyticsEvent(WalletEventName.NFTVisibilityChanged, { + tokenId, + chainId, + contractAddress, + isSpam, + // we log the state to which it's transitioning + visible: hidden, + }) dispatch(toggleNftVisibility({ nftKey, isSpam })) if (showNotification) { @@ -68,15 +85,33 @@ export function useNFTContextMenu({ visible: !hidden, hideDelay: 2 * ONE_SECOND_MS, assetName: 'NFT', - }) + }), ) } - }, [nftKey, dispatch, hidden, showNotification, isSpam]) + }, [nftKey, dispatch, isSpam, tokenId, chainId, contractAddress, hidden, showNotification]) + + const onPressNavigateToExplorer = useCallback(() => { + if (contractAddress && tokenId && chainId) { + navigateToNftDetails({ address: contractAddress, tokenId, chainId }) + } + }, [contractAddress, tokenId, chainId, navigateToNftDetails]) const menuActions = useMemo( () => nftKey ? [ + ...(isWeb && chainId + ? [ + { + title: t('tokens.nfts.action.viewOnExplorer', { blockExplorerName: getExplorerName(chainId) }), + onPress: onPressNavigateToExplorer, + Icon: isDarkMode + ? UNIVERSE_CHAIN_LOGO[chainId].explorer.logoDark + : UNIVERSE_CHAIN_LOGO[chainId].explorer.logoLight, + destructive: false, + }, + ] + : []), ...(!isWeb ? [ { @@ -88,9 +123,7 @@ export function useNFTContextMenu({ : []), ...((isLocalAccount && [ { - title: hidden - ? t('tokens.nfts.hidden.action.unhide') - : t('tokens.nfts.hidden.action.hide'), + title: hidden ? t('tokens.nfts.hidden.action.unhide') : t('tokens.nfts.hidden.action.hide'), ...(isWeb ? { Icon: hidden ? Eye : EyeOff, @@ -105,14 +138,24 @@ export function useNFTContextMenu({ []), ] : [], - [nftKey, t, onPressShare, isLocalAccount, hidden, onPressHiddenStatus] + [ + nftKey, + chainId, + t, + onPressNavigateToExplorer, + isDarkMode, + onPressShare, + isLocalAccount, + hidden, + onPressHiddenStatus, + ], ) const onContextMenuPress = useCallback( async (e: NativeSyntheticEvent): Promise => { await menuActions[e.nativeEvent.index]?.onPress?.() }, - [menuActions] + [menuActions], ) return { menuActions, onContextMenuPress, onlyShare: !!nftKey && !isLocalAccount } diff --git a/packages/wallet/src/features/notifications/buildReceiveNotification.ts b/packages/wallet/src/features/notifications/buildReceiveNotification.ts index 1a3568292ca..290e45e2592 100644 --- a/packages/wallet/src/features/notifications/buildReceiveNotification.ts +++ b/packages/wallet/src/features/notifications/buildReceiveNotification.ts @@ -1,14 +1,10 @@ -import { AssetType } from 'wallet/src/entities/assets' +import { AssetType } from 'uniswap/src/entities/assets' import { AppNotificationType, ReceiveCurrencyTxNotification, ReceiveNFTNotification, } from 'wallet/src/features/notifications/types' -import { - TransactionDetails, - TransactionStatus, - TransactionType, -} from 'wallet/src/features/transactions/types' +import { TransactionDetails, TransactionStatus, TransactionType } from 'wallet/src/features/transactions/types' /** * Based on notification type info, returns an AppNotification object for either NFT or Currency receive. @@ -20,7 +16,7 @@ import { export function buildReceiveNotification( transactionDetails: TransactionDetails, - receivingAddress: Address // not included in transactionDetails + receivingAddress: Address, // not included in transactionDetails ): ReceiveNFTNotification | ReceiveCurrencyTxNotification | undefined { const { typeInfo, status, chainId, hash, id } = transactionDetails @@ -32,17 +28,12 @@ export function buildReceiveNotification( const baseNotificationData = { txStatus: status, chainId, - txHash: hash, address: receivingAddress, txId: id, } // Currency receive txn. - if ( - typeInfo?.assetType === AssetType.Currency && - typeInfo?.currencyAmountRaw && - typeInfo?.sender - ) { + if (typeInfo?.assetType === AssetType.Currency && typeInfo?.currencyAmountRaw && typeInfo?.sender) { return { ...baseNotificationData, type: AppNotificationType.Transaction, @@ -55,10 +46,7 @@ export function buildReceiveNotification( } // NFT receive txn. - if ( - (typeInfo?.assetType === AssetType.ERC1155 || typeInfo?.assetType === AssetType.ERC721) && - typeInfo?.tokenId - ) { + if ((typeInfo?.assetType === AssetType.ERC1155 || typeInfo?.assetType === AssetType.ERC721) && typeInfo?.tokenId) { return { ...baseNotificationData, type: AppNotificationType.Transaction, diff --git a/packages/wallet/src/features/notifications/builtReceiveNotification.test.ts b/packages/wallet/src/features/notifications/builtReceiveNotification.test.ts index 3bebc29f49f..ce3f98d14a7 100644 --- a/packages/wallet/src/features/notifications/builtReceiveNotification.test.ts +++ b/packages/wallet/src/features/notifications/builtReceiveNotification.test.ts @@ -1,11 +1,7 @@ -import { AssetType } from 'wallet/src/entities/assets' +import { AssetType } from 'uniswap/src/entities/assets' import { buildReceiveNotification } from 'wallet/src/features/notifications/buildReceiveNotification' import { createFinalizedTxAction } from 'wallet/src/features/notifications/notificationWatcherSaga.test' -import { - ReceiveTokenTransactionInfo, - TransactionStatus, - TransactionType, -} from 'wallet/src/features/transactions/types' +import { ReceiveTokenTransactionInfo, TransactionStatus, TransactionType } from 'wallet/src/features/transactions/types' import { receiveCurrencyTxNotification, receiveNFTNotification, @@ -50,10 +46,9 @@ describe(buildReceiveNotification, () => { sender: receiveNftTypeInfo.sender, address: account.address, tokenAddress: receiveNftTypeInfo.tokenAddress, - txHash: testTransaction.hash, txId: testTransaction.id, txStatus: testTransaction.status, - }) + }), ) }) @@ -68,10 +63,9 @@ describe(buildReceiveNotification, () => { currencyAmountRaw: receiveCurrencyTypeInfo.currencyAmountRaw, sender: receiveCurrencyTypeInfo.sender, tokenAddress: receiveCurrencyTypeInfo.tokenAddress, - txHash: testTransaction.hash, txId: testTransaction.id, txStatus: testTransaction.status, - }) + }), ) }) }) diff --git a/packages/wallet/src/features/notifications/components/ApproveNotification.tsx b/packages/wallet/src/features/notifications/components/ApproveNotification.tsx index 2d1f7aedfb2..6a1aea5b1a8 100644 --- a/packages/wallet/src/features/notifications/components/ApproveNotification.tsx +++ b/packages/wallet/src/features/notifications/components/ApproveNotification.tsx @@ -1,12 +1,12 @@ +import { AssetType } from 'uniswap/src/entities/assets' +import { buildCurrencyId } from 'uniswap/src/utils/currencyId' import { LogoWithTxStatus } from 'wallet/src/components/CurrencyLogo/LogoWithTxStatus' import { useWalletNavigation } from 'wallet/src/contexts/WalletNavigationContext' -import { AssetType } from 'wallet/src/entities/assets' import { NotificationToast } from 'wallet/src/features/notifications/components/NotificationToast' import { NOTIFICATION_ICON_SIZE } from 'wallet/src/features/notifications/constants' import { ApproveTxNotification } from 'wallet/src/features/notifications/types' import { formApproveNotificationTitle } from 'wallet/src/features/notifications/utils' import { useCurrencyInfo } from 'wallet/src/features/tokens/useCurrencyInfo' -import { buildCurrencyId } from 'wallet/src/utils/currencyId' export function ApproveNotification({ notification: { address, chainId, tokenAddress, spender, txStatus, txType, hideDelay }, @@ -17,12 +17,7 @@ export function ApproveNotification({ const currencyInfo = useCurrencyInfo(buildCurrencyId(chainId, tokenAddress)) - const title = formApproveNotificationTitle( - txStatus, - currencyInfo?.currency, - tokenAddress, - spender - ) + const title = formApproveNotificationTitle(txStatus, currencyInfo?.currency, tokenAddress, spender) const icon = ( + ) : ( - + ) } title={ diff --git a/packages/wallet/src/features/notifications/components/NetworkChangedNotification.test.tsx b/packages/wallet/src/features/notifications/components/NetworkChangedNotification.test.tsx index 9bcc1aa27f3..db7df6f86f1 100644 --- a/packages/wallet/src/features/notifications/components/NetworkChangedNotification.test.tsx +++ b/packages/wallet/src/features/notifications/components/NetworkChangedNotification.test.tsx @@ -12,7 +12,7 @@ describe(NetworkChangedNotification, () => { chainId: UniverseChainId.Mainnet, flow: 'swap', }} - /> + />, ) const title = queryByText('Swapping on Ethereum') expect(title).toBeTruthy() @@ -26,7 +26,7 @@ describe(NetworkChangedNotification, () => { chainId: UniverseChainId.Mainnet, flow: 'send', }} - /> + />, ) const title = queryByText('Sending on Ethereum') expect(title).toBeTruthy() @@ -39,7 +39,7 @@ describe(NetworkChangedNotification, () => { type: AppNotificationType.NetworkChanged, chainId: UniverseChainId.Mainnet, }} - /> + />, ) const title = queryByText('Switched to Ethereum') expect(title).toBeTruthy() diff --git a/packages/wallet/src/features/notifications/components/NotSupportedNetworkNotification.test.tsx b/packages/wallet/src/features/notifications/components/NotSupportedNetworkNotification.test.tsx index 7ef2a1df222..6bcad82fc12 100644 --- a/packages/wallet/src/features/notifications/components/NotSupportedNetworkNotification.test.tsx +++ b/packages/wallet/src/features/notifications/components/NotSupportedNetworkNotification.test.tsx @@ -5,9 +5,7 @@ import { renderWithProviders } from 'wallet/src/test/render' describe(NotSupportedNetworkNotification, () => { it('renders without error', () => { const tree = renderWithProviders( - + , ) expect(tree).toMatchSnapshot() diff --git a/packages/wallet/src/features/notifications/components/NotificationToast.tsx b/packages/wallet/src/features/notifications/components/NotificationToast.tsx index 873cbad1506..4fcf07a8fe1 100644 --- a/packages/wallet/src/features/notifications/components/NotificationToast.tsx +++ b/packages/wallet/src/features/notifications/components/NotificationToast.tsx @@ -3,13 +3,9 @@ // the tamagui optimizer disabling optimization for now on this file import { useCallback, useEffect } from 'react' -import { - Directions, - FlingGestureHandler, - FlingGestureHandlerGestureEvent, - State, -} from 'react-native-gesture-handler' +import { Directions, FlingGestureHandler, FlingGestureHandlerGestureEvent, State } from 'react-native-gesture-handler' import { useAnimatedStyle, useSharedValue, withDelay, withSpring } from 'react-native-reanimated' +import { useDispatch } from 'react-redux' import { Flex, Text, @@ -23,10 +19,11 @@ import { } from 'ui/src' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { borderRadii, spacing } from 'ui/src/theme' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { useTimeout } from 'utilities/src/time/timing' import { selectActiveAccountNotifications } from 'wallet/src/features/notifications/selectors' -import { popNotification } from 'wallet/src/features/notifications/slice' -import { useAppDispatch, useAppSelector } from 'wallet/src/state' +import { popNotification, setNotificationViewed } from 'wallet/src/features/notifications/slice' +import { useAppSelector } from 'wallet/src/state' const NOTIFICATION_HEIGHT = 64 @@ -57,13 +54,14 @@ export interface NotificationToastProps extends NotificationContentProps { } // TODO(EXT-931): Consolidate mobile and web animation styles -const ToastEntryAnimation = styled(Flex, { +const WebToastEntryAnimation = styled(Flex, { animation: 'semiBouncy', y: 0, top: '$spacing12', - position: 'absolute', + // @ts-expect-error - It's Ok to ignore and use the web-only `fixed` value because this component is only used on web. + position: 'fixed', width: '100%', - zIndex: '$modal', + zIndex: '$overlay', opacity: 1, pointerEvents: 'none', @@ -73,6 +71,8 @@ const ToastEntryAnimation = styled(Flex, { }, }) +const SPRING_ANIMATION_DELAY = 100 + export function NotificationToast({ subtitle, title, @@ -85,7 +85,7 @@ export function NotificationToast({ smallToast, }: NotificationToastProps): JSX.Element { const isDarkMode = useIsDarkMode() - const dispatch = useAppDispatch() + const dispatch = useDispatch() const notifications = useAppSelector(selectActiveAccountNotifications) const currentNotification = notifications?.[0] const hasQueuedNotification = !!notifications?.[1] @@ -93,17 +93,29 @@ export function NotificationToast({ const showOffset = useDeviceInsets().top + spacing.spacing4 + (isWeb ? spacing.spacing12 : 0) const bannerOffset = useSharedValue(HIDE_OFFSET_Y) + // Run this only once to ensure that if a new notification is created it doesn't show on the next screen + useEffect(() => { + if (currentNotification?.shown) { + dispatch(popNotification({ address })) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [address, dispatch]) + useEffect(() => { if (currentNotification) { - bannerOffset.value = withDelay(100, withSpring(showOffset, SPRING_ANIMATION)) + bannerOffset.value = withDelay(SPRING_ANIMATION_DELAY, withSpring(showOffset, SPRING_ANIMATION)) + // delay to ensure the notification is shown if theres a quick navigation event + setTimeout(() => { + dispatch(setNotificationViewed({ address })) + }, SPRING_ANIMATION_DELAY * 2) } - }, [showOffset, bannerOffset, currentNotification]) + }, [showOffset, bannerOffset, currentNotification, dispatch, address]) const animatedStyle = useAnimatedStyle( () => ({ transform: [{ translateY: bannerOffset.value }], }), - [bannerOffset] + [bannerOffset], ) const dismissLatest = useCallback(() => { @@ -148,19 +160,13 @@ export function NotificationToast({ borderRadius={smallToast ? SMALL_TOAST_RADIUS : LARGE_TOAST_RADIUS} borderWidth={TOAST_BORDER_WIDTH} mx={smallToast ? 'auto' : '$spacing12'} - pointerEvents="auto"> + pointerEvents="auto" + > {smallToast ? ( - + ) : ( {notificationContent} + {notificationContent} ) : ( + zIndex="$modal" + > {notificationContent} @@ -208,7 +215,8 @@ function NotificationContent({ minHeight={NOTIFICATION_HEIGHT} p="$spacing12" onPress={onPress} - onPressIn={onPressIn}> + onPressIn={onPressIn} + > + justifyContent="flex-start" + > {icon} - + {title} {subtitle && ( @@ -243,12 +257,7 @@ function NotificationContent({ ) } -function NotificationContentSmall({ - title, - icon, - onPress, - onPressIn, -}: NotificationContentProps): JSX.Element { +function NotificationContentSmall({ title, icon, onPress, onPressIn }: NotificationContentProps): JSX.Element { return ( + onPressIn={onPressIn} + > {icon} - + {title} diff --git a/packages/wallet/src/features/notifications/components/PasswordChangedNotification.tsx b/packages/wallet/src/features/notifications/components/PasswordChangedNotification.tsx index e1d0315bc0b..404fda96484 100644 --- a/packages/wallet/src/features/notifications/components/PasswordChangedNotification.tsx +++ b/packages/wallet/src/features/notifications/components/PasswordChangedNotification.tsx @@ -8,7 +8,5 @@ export function PasswordChangedNotification({ notification: PasswordChangedNotificationType }): JSX.Element { const { t } = useTranslation() - return ( - - ) + return } diff --git a/packages/wallet/src/features/notifications/components/PendingNotificationBadge.tsx b/packages/wallet/src/features/notifications/components/PendingNotificationBadge.tsx index 58bef86ab21..60f4354084e 100644 --- a/packages/wallet/src/features/notifications/components/PendingNotificationBadge.tsx +++ b/packages/wallet/src/features/notifications/components/PendingNotificationBadge.tsx @@ -17,9 +17,7 @@ interface Props { size?: number } -export function PendingNotificationBadge({ - size = LOADING_SPINNER_SIZE, -}: Props): JSX.Element | null { +export function PendingNotificationBadge({ size = LOADING_SPINNER_SIZE }: Props): JSX.Element | null { const colors = useSporeColors() const activeAccountAddress = useAppSelector(selectActiveAccountAddress) const notifications = useAppSelector(selectActiveAccountNotifications) @@ -40,12 +38,10 @@ export function PendingNotificationBadge({ /*************** Pending in-app txn **************/ - const swapPendingNotificationActive = - currentNotification?.type === AppNotificationType.SwapPending + const swapPendingNotificationActive = currentNotification?.type === AppNotificationType.SwapPending const pendingTransactionCount = (sortedPendingTransactions ?? []).length const txPendingLongerThanLimit = - sortedPendingTransactions?.[0] && - Date.now() - sortedPendingTransactions[0].addedTime > PENDING_TX_TIME_LIMIT + sortedPendingTransactions?.[0] && Date.now() - sortedPendingTransactions[0].addedTime > PENDING_TX_TIME_LIMIT // If a transaction has been pending for longer than 5 mins, then don't show the pending icon anymore // Dont show the loader if the swap pending toast is on screen @@ -65,12 +61,7 @@ export function PendingNotificationBadge({ if (hasNotifications) { return ( - + ) } diff --git a/packages/wallet/src/features/notifications/components/SharedNotificationToastRouter.tsx b/packages/wallet/src/features/notifications/components/SharedNotificationToastRouter.tsx index 1e2566a4c1a..65c4a6792bb 100644 --- a/packages/wallet/src/features/notifications/components/SharedNotificationToastRouter.tsx +++ b/packages/wallet/src/features/notifications/components/SharedNotificationToastRouter.tsx @@ -1,4 +1,4 @@ -import { AssetType } from 'wallet/src/entities/assets' +import { AssetType } from 'uniswap/src/entities/assets' import { ApproveNotification } from 'wallet/src/features/notifications/components/ApproveNotification' import { ChangeAssetVisibilityNotification } from 'wallet/src/features/notifications/components/ChangeAssetVisibilityNotification' import { ChooseCountryNotification } from 'wallet/src/features/notifications/components/ChooseCountryNotification' @@ -19,11 +19,7 @@ import { WrapNotification } from 'wallet/src/features/notifications/components/W import { AppNotification, AppNotificationType } from 'wallet/src/features/notifications/types' import { TransactionType } from 'wallet/src/features/transactions/types' -export function SharedNotificationToastRouter({ - notification, -}: { - notification: AppNotification -}): JSX.Element | null { +export function SharedNotificationToastRouter({ notification }: { notification: AppNotification }): JSX.Element | null { switch (notification.type) { case AppNotificationType.Default: return diff --git a/packages/wallet/src/features/notifications/components/SwapNotification.tsx b/packages/wallet/src/features/notifications/components/SwapNotification.tsx index b39a473445b..9571bfc38b2 100644 --- a/packages/wallet/src/features/notifications/components/SwapNotification.tsx +++ b/packages/wallet/src/features/notifications/components/SwapNotification.tsx @@ -1,5 +1,5 @@ import { useTranslation } from 'react-i18next' -import { SplitLogo } from 'wallet/src/components/CurrencyLogo/SplitLogo' +import { SplitLogo } from 'uniswap/src/components/CurrencyLogo/SplitLogo' import { useWalletNavigation } from 'wallet/src/contexts/WalletNavigationContext' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' import { NotificationToast } from 'wallet/src/features/notifications/components/NotificationToast' @@ -39,7 +39,7 @@ export function SwapNotification({ outputCurrencyId, inputCurrencyAmountRaw, outputCurrencyAmountRaw, - tradeType + tradeType, ) const { t } = useTranslation() diff --git a/packages/wallet/src/features/notifications/components/SwapPendingNotification.tsx b/packages/wallet/src/features/notifications/components/SwapPendingNotification.tsx index de4dc70f531..5b93f2ca003 100644 --- a/packages/wallet/src/features/notifications/components/SwapPendingNotification.tsx +++ b/packages/wallet/src/features/notifications/components/SwapPendingNotification.tsx @@ -10,11 +10,7 @@ import { WrapType } from 'wallet/src/features/transactions/types' // and when a txn confirms it ll replace this toast. export const TRANSACTION_PENDING_NOTIFICATION_DELAY = 12 * ONE_SECOND_MS -export function SwapPendingNotification({ - notification, -}: { - notification: SwapPendingNotificationType -}): JSX.Element { +export function SwapPendingNotification({ notification }: { notification: SwapPendingNotificationType }): JSX.Element { const { t } = useTranslation() const notificationText = getNotificationText(notification.wrapType, t) diff --git a/packages/wallet/src/features/notifications/components/TransferCurrencyNotification.tsx b/packages/wallet/src/features/notifications/components/TransferCurrencyNotification.tsx index 7378ffa5166..bc004b141ae 100644 --- a/packages/wallet/src/features/notifications/components/TransferCurrencyNotification.tsx +++ b/packages/wallet/src/features/notifications/components/TransferCurrencyNotification.tsx @@ -1,3 +1,4 @@ +import { buildCurrencyId } from 'uniswap/src/utils/currencyId' import { LogoWithTxStatus } from 'wallet/src/components/CurrencyLogo/LogoWithTxStatus' import { useWalletNavigation } from 'wallet/src/contexts/WalletNavigationContext' import { useENS } from 'wallet/src/features/ens/useENS' @@ -8,7 +9,6 @@ import { TransferCurrencyTxNotification } from 'wallet/src/features/notification import { formTransferCurrencyNotificationTitle } from 'wallet/src/features/notifications/utils' import { useCurrencyInfo } from 'wallet/src/features/tokens/useCurrencyInfo' import { TransactionType } from 'wallet/src/features/transactions/types' -import { buildCurrencyId } from 'wallet/src/utils/currencyId' export function TransferCurrencyNotification({ notification, @@ -16,18 +16,8 @@ export function TransferCurrencyNotification({ notification: TransferCurrencyTxNotification }): JSX.Element { const formatter = useLocalizationContext() - const { - address, - assetType, - chainId, - tokenAddress, - currencyAmountRaw, - txType, - txStatus, - hideDelay, - } = notification - const senderOrRecipient = - txType === TransactionType.Send ? notification.recipient : notification.sender + const { address, assetType, chainId, tokenAddress, currencyAmountRaw, txType, txStatus, hideDelay } = notification + const senderOrRecipient = txType === TransactionType.Send ? notification.recipient : notification.sender const { name: ensName } = useENS(chainId, senderOrRecipient) const currencyInfo = useCurrencyInfo(buildCurrencyId(chainId, tokenAddress)) @@ -38,7 +28,7 @@ export function TransferCurrencyNotification({ currencyInfo?.currency, tokenAddress, currencyAmountRaw, - ensName ?? senderOrRecipient + ensName ?? senderOrRecipient, ) const { navigateToAccountActivityList } = useWalletNavigation() diff --git a/packages/wallet/src/features/notifications/components/TransferNFTNotification.tsx b/packages/wallet/src/features/notifications/components/TransferNFTNotification.tsx index 1d90a0e935b..3e3cd9025d2 100644 --- a/packages/wallet/src/features/notifications/components/TransferNFTNotification.tsx +++ b/packages/wallet/src/features/notifications/components/TransferNFTNotification.tsx @@ -9,16 +9,10 @@ import { formTransferNFTNotificationTitle } from 'wallet/src/features/notificati import { TransactionType } from 'wallet/src/features/transactions/types' import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' -export function TransferNFTNotification({ - notification, -}: { - notification: TransferNFTTxNotification -}): JSX.Element { - const { address, assetType, chainId, tokenAddress, tokenId, txType, txStatus, hideDelay } = - notification +export function TransferNFTNotification({ notification }: { notification: TransferNFTTxNotification }): JSX.Element { + const { address, assetType, chainId, tokenAddress, tokenId, txType, txStatus, hideDelay } = notification const userAddress = useActiveAccountAddressWithThrow() - const senderOrRecipient = - txType === TransactionType.Send ? notification.recipient : notification.sender + const senderOrRecipient = txType === TransactionType.Send ? notification.recipient : notification.sender const nftOwner = txType === TransactionType.Send ? notification.recipient : userAddress const { data: nft } = useNFT(nftOwner, tokenAddress, tokenId) const { name: ensName } = useENS(chainId, senderOrRecipient) @@ -29,7 +23,7 @@ export function TransferNFTNotification({ nft, tokenAddress, tokenId, - ensName ?? senderOrRecipient + ensName ?? senderOrRecipient, ) const { navigateToAccountActivityList } = useWalletNavigation() diff --git a/packages/wallet/src/features/notifications/components/UnknownNotification.tsx b/packages/wallet/src/features/notifications/components/UnknownNotification.tsx index 7c0ec4cc86e..c3c8b00dc38 100644 --- a/packages/wallet/src/features/notifications/components/UnknownNotification.tsx +++ b/packages/wallet/src/features/notifications/components/UnknownNotification.tsx @@ -1,7 +1,8 @@ import { AlertTriangle, CheckmarkCircle } from 'ui/src/components/icons' +import { AssetType } from 'uniswap/src/entities/assets' +import { buildCurrencyId } from 'uniswap/src/utils/currencyId' import { LogoWithTxStatus } from 'wallet/src/components/CurrencyLogo/LogoWithTxStatus' import { useWalletNavigation } from 'wallet/src/contexts/WalletNavigationContext' -import { AssetType } from 'wallet/src/entities/assets' import { useENS } from 'wallet/src/features/ens/useENS' import { NotificationToast } from 'wallet/src/features/notifications/components/NotificationToast' import { NOTIFICATION_ICON_SIZE } from 'wallet/src/features/notifications/constants' @@ -9,7 +10,6 @@ import { TransactionNotificationBase } from 'wallet/src/features/notifications/t import { formUnknownTxTitle } from 'wallet/src/features/notifications/utils' import { useCurrencyInfo } from 'wallet/src/features/tokens/useCurrencyInfo' import { TransactionStatus } from 'wallet/src/features/transactions/types' -import { buildCurrencyId } from 'wallet/src/utils/currencyId' export function UnknownTxNotification({ notification: { address, chainId, tokenAddress, txStatus, txType, hideDelay }, @@ -17,9 +17,7 @@ export function UnknownTxNotification({ notification: TransactionNotificationBase }): JSX.Element { const { name: ensName } = useENS(chainId, tokenAddress) - const currencyInfo = useCurrencyInfo( - tokenAddress ? buildCurrencyId(chainId, tokenAddress) : undefined - ) + const currencyInfo = useCurrencyInfo(tokenAddress ? buildCurrencyId(chainId, tokenAddress) : undefined) const title = formUnknownTxTitle(txStatus, tokenAddress, ensName) const icon = currencyInfo ? ( Unsupported network diff --git a/packages/wallet/src/features/notifications/notificationWatcherSaga.test.ts b/packages/wallet/src/features/notifications/notificationWatcherSaga.test.ts index 228becf215c..2da93b089bd 100644 --- a/packages/wallet/src/features/notifications/notificationWatcherSaga.test.ts +++ b/packages/wallet/src/features/notifications/notificationWatcherSaga.test.ts @@ -1,8 +1,8 @@ import { TradeType } from '@uniswap/sdk-core' import { expectSaga } from 'redux-saga-test-plan' +import { getNativeAddress } from 'uniswap/src/constants/addresses' +import { AssetType } from 'uniswap/src/entities/assets' import { UniverseChainId } from 'uniswap/src/types/chains' -import { getNativeAddress } from 'wallet/src/constants/addresses' -import { AssetType } from 'wallet/src/entities/assets' import { pushTransactionNotification } from 'wallet/src/features/notifications/notificationWatcherSaga' import { pushNotification } from 'wallet/src/features/notifications/slice' import { AppNotificationType } from 'wallet/src/features/notifications/types' @@ -23,9 +23,7 @@ const finalizedTxAction = finalizedTransactionAction() const txId = 'uuid-4' -export const createFinalizedTxAction = ( - typeInfo: TransactionTypeInfo -): ReturnType => ({ +export const createFinalizedTxAction = (typeInfo: TransactionTypeInfo): ReturnType => ({ payload: { ...finalizedTxAction.payload, typeInfo, @@ -42,7 +40,7 @@ describe(pushTransactionNotification, () => { spender: '0xUniswapDeployer', } const finalizedApproveAction = createFinalizedTxAction(approveTypeInfo) - const { chainId, from, hash } = finalizedApproveAction.payload + const { chainId, from } = finalizedApproveAction.payload return expectSaga(pushTransactionNotification, finalizedApproveAction) .withState({ @@ -60,13 +58,12 @@ describe(pushTransactionNotification, () => { txStatus: TransactionStatus.Success, address: from, chainId, - txHash: hash, type: AppNotificationType.Transaction, txType: TransactionType.Approve, tokenAddress: approveTypeInfo.tokenAddress, spender: approveTypeInfo.spender, txId, - }) + }), ) .silentRun() }) @@ -105,7 +102,7 @@ describe(pushTransactionNotification, () => { maximumInputCurrencyAmountRaw: '12000000000000000', } const finalizedSwapAction = createFinalizedTxAction(swapTypeInfo) - const { chainId, from, hash } = finalizedSwapAction.payload + const { chainId, from } = finalizedSwapAction.payload return expectSaga(pushTransactionNotification, finalizedSwapAction) .put( @@ -113,7 +110,6 @@ describe(pushTransactionNotification, () => { txStatus: TransactionStatus.Success, address: from, chainId, - txHash: hash, type: AppNotificationType.Transaction, txType: TransactionType.Swap, inputCurrencyId: swapTypeInfo.inputCurrencyId, @@ -122,7 +118,7 @@ describe(pushTransactionNotification, () => { outputCurrencyAmountRaw: swapTypeInfo.outputCurrencyAmountRaw, tradeType: swapTypeInfo.tradeType, txId, - }) + }), ) .silentRun() }) @@ -136,7 +132,7 @@ describe(pushTransactionNotification, () => { tokenAddress: '0xUniswapToken', } const finalizedSendCurrencyAction = createFinalizedTxAction(sendCurrencyTypeInfo) - const { chainId, from, hash } = finalizedSendCurrencyAction.payload + const { chainId, from } = finalizedSendCurrencyAction.payload return expectSaga(pushTransactionNotification, finalizedSendCurrencyAction) .put( @@ -144,7 +140,6 @@ describe(pushTransactionNotification, () => { txStatus: TransactionStatus.Success, address: from, chainId, - txHash: hash, type: AppNotificationType.Transaction, txType: TransactionType.Send, assetType: AssetType.Currency, @@ -152,7 +147,7 @@ describe(pushTransactionNotification, () => { currencyAmountRaw: '1000', recipient: sendCurrencyTypeInfo.recipient, txId, - }) + }), ) .silentRun() }) @@ -166,7 +161,7 @@ describe(pushTransactionNotification, () => { tokenId: '420', } const finalizedSendNftAction = createFinalizedTxAction(sendNftTypeInfo) - const { chainId, from, hash } = finalizedSendNftAction.payload + const { chainId, from } = finalizedSendNftAction.payload return expectSaga(pushTransactionNotification, finalizedSendNftAction) .put( @@ -174,7 +169,6 @@ describe(pushTransactionNotification, () => { txStatus: TransactionStatus.Success, address: from, chainId, - txHash: hash, type: AppNotificationType.Transaction, txType: TransactionType.Send, assetType: AssetType.ERC721, @@ -182,7 +176,7 @@ describe(pushTransactionNotification, () => { tokenId: '420', recipient: sendNftTypeInfo.recipient, txId, - }) + }), ) .silentRun() }) @@ -196,7 +190,7 @@ describe(pushTransactionNotification, () => { tokenAddress: '0xUniswapToken', } const finalizedReceiveCurrencyAction = createFinalizedTxAction(receiveCurrencyTypeInfo) - const { chainId, from, hash } = finalizedReceiveCurrencyAction.payload + const { chainId, from } = finalizedReceiveCurrencyAction.payload return expectSaga(pushTransactionNotification, finalizedReceiveCurrencyAction) .put( @@ -204,7 +198,6 @@ describe(pushTransactionNotification, () => { txStatus: TransactionStatus.Success, address: from, chainId, - txHash: hash, type: AppNotificationType.Transaction, txType: TransactionType.Receive, assetType: AssetType.Currency, @@ -212,7 +205,7 @@ describe(pushTransactionNotification, () => { currencyAmountRaw: '1000', sender: receiveCurrencyTypeInfo.sender, txId, - }) + }), ) .silentRun() }) @@ -226,7 +219,7 @@ describe(pushTransactionNotification, () => { tokenId: '420', } const finalizedReceiveNftAction = createFinalizedTxAction(receiveNftTypeInfo) - const { chainId, from, hash } = finalizedReceiveNftAction.payload + const { chainId, from } = finalizedReceiveNftAction.payload return expectSaga(pushTransactionNotification, finalizedReceiveNftAction) .put( @@ -234,7 +227,6 @@ describe(pushTransactionNotification, () => { txStatus: TransactionStatus.Success, address: from, chainId, - txHash: hash, type: AppNotificationType.Transaction, txType: TransactionType.Receive, assetType: AssetType.ERC1155, @@ -242,7 +234,7 @@ describe(pushTransactionNotification, () => { tokenId: '420', sender: receiveNftTypeInfo.sender, txId, - }) + }), ) .silentRun() }) @@ -253,7 +245,7 @@ describe(pushTransactionNotification, () => { tokenAddress: '0xUniswapToken', } const finalizedUnknownAction = createFinalizedTxAction(unknownTxTypeInfo) - const { chainId, from, hash } = finalizedUnknownAction.payload + const { chainId, from } = finalizedUnknownAction.payload return expectSaga(pushTransactionNotification, finalizedUnknownAction) .put( @@ -261,12 +253,11 @@ describe(pushTransactionNotification, () => { txStatus: TransactionStatus.Success, address: from, chainId, - txHash: hash, type: AppNotificationType.Transaction, txType: TransactionType.Unknown, tokenAddress: unknownTxTypeInfo.tokenAddress, txId, - }) + }), ) .silentRun() }) diff --git a/packages/wallet/src/features/notifications/notificationWatcherSaga.ts b/packages/wallet/src/features/notifications/notificationWatcherSaga.ts index fd40e4fe43f..9008d260fd5 100644 --- a/packages/wallet/src/features/notifications/notificationWatcherSaga.ts +++ b/packages/wallet/src/features/notifications/notificationWatcherSaga.ts @@ -1,7 +1,7 @@ import { call, put, takeLatest } from 'typed-redux-saga' +import { AssetType } from 'uniswap/src/entities/assets' import { WalletChainId } from 'uniswap/src/types/chains' import { WalletConnectEvent } from 'uniswap/src/types/walletConnect' -import { AssetType } from 'wallet/src/entities/assets' import { buildReceiveNotification } from 'wallet/src/features/notifications/buildReceiveNotification' import { pushNotification } from 'wallet/src/features/notifications/slice' import { AppNotificationType } from 'wallet/src/features/notifications/types' @@ -16,23 +16,17 @@ export function* notificationWatcher() { } export function* pushTransactionNotification(action: ReturnType) { - const { chainId, status, typeInfo, hash, id, from, addedTime } = action.payload + const { chainId, status, typeInfo, id, from, addedTime } = action.payload const baseNotificationData = { txStatus: status, chainId, - txHash: hash, address: from, txId: id, } if (typeInfo.type === TransactionType.Approve) { - const shouldSuppressNotification = yield* call( - suppressApproveNotification, - from, - chainId, - addedTime - ) + const shouldSuppressNotification = yield* call(suppressApproveNotification, from, chainId, addedTime) if (!shouldSuppressNotification) { yield* put( pushNotification({ @@ -41,7 +35,7 @@ export function* pushTransactionNotification(action: ReturnType { diff --git a/packages/wallet/src/features/notifications/selectors.ts b/packages/wallet/src/features/notifications/selectors.ts index 10df189787b..a4195a8130d 100644 --- a/packages/wallet/src/features/notifications/selectors.ts +++ b/packages/wallet/src/features/notifications/selectors.ts @@ -3,8 +3,7 @@ import { AppNotification } from 'wallet/src/features/notifications/types' import { selectActiveAccountAddress } from 'wallet/src/features/wallet/selectors' import { SharedState } from 'wallet/src/state/reducer' -const selectNotificationQueue = (state: SharedState): AppNotification[] => - state.notifications.notificationQueue +const selectNotificationQueue = (state: SharedState): AppNotification[] => state.notifications.notificationQueue export const selectActiveAccountNotifications = createSelector( selectNotificationQueue, @@ -15,20 +14,16 @@ export const selectActiveAccountNotifications = createSelector( } // If a notification doesn't have an address param assume it belongs to the active account return notificationQueue.filter((notif) => !notif.address || notif.address === address) - } + }, ) const selectNotificationStatus = ( - state: SharedState + state: SharedState, ): { [userAddress: string]: boolean | undefined } => state.notifications.notificationStatus -export const makeSelectHasNotifications = (): Selector< - SharedState, - boolean | undefined, - [Address | null] -> => +export const makeSelectHasNotifications = (): Selector => createSelector( selectNotificationStatus, (_: SharedState, address: Address | null) => address, @@ -37,11 +32,11 @@ export const makeSelectHasNotifications = (): Selector< return undefined } return notificationStatuses?.[address] - } + }, ) export const selectLastTxNotificationUpdate = ( - state: SharedState + state: SharedState, ): { [address: string]: number | undefined } => state.notifications.lastTxNotificationUpdate diff --git a/packages/wallet/src/features/notifications/slice.ts b/packages/wallet/src/features/notifications/slice.ts index 708ab18ad2b..2b9d22da093 100644 --- a/packages/wallet/src/features/notifications/slice.ts +++ b/packages/wallet/src/features/notifications/slice.ts @@ -27,29 +27,37 @@ const slice = createSlice({ if (!address) { state.notificationQueue.shift() } else { - const indexToRemove = state.notificationQueue.findIndex( - (notif) => notif.address === address - ) + const indexToRemove = state.notificationQueue.findIndex((notif) => notif.address === address) if (indexToRemove !== -1) { state.notificationQueue.splice(indexToRemove, 1) } } }, + setNotificationViewed: (state, action: PayloadAction<{ address: Maybe
        }>) => { + const { address } = action.payload + if (!address) { + if (state.notificationQueue[0]) { + state.notificationQueue[0].shown = true + } + } else { + const indexToChange = state.notificationQueue.findIndex((notif) => notif.address === address) + if (indexToChange !== -1) { + const itemToChange = state.notificationQueue[indexToChange] + if (itemToChange) { + itemToChange.shown = true + } + } + } + }, clearNotificationQueue: (state) => { state.notificationQueue = [] }, resetNotifications: () => initialNotificationsState, - setNotificationStatus: ( - state, - action: PayloadAction<{ address: Address; hasNotifications: boolean }> - ) => { + setNotificationStatus: (state, action: PayloadAction<{ address: Address; hasNotifications: boolean }>) => { const { address, hasNotifications } = action.payload state.notificationStatus = { ...state.notificationStatus, [address]: hasNotifications } }, - setLastTxNotificationUpdate: ( - state, - { payload }: PayloadAction<{ address: Address; timestamp: number }> - ) => { + setLastTxNotificationUpdate: (state, { payload }: PayloadAction<{ address: Address; timestamp: number }>) => { const { address, timestamp } = payload state.lastTxNotificationUpdate[address] = timestamp }, @@ -59,6 +67,7 @@ const slice = createSlice({ export const { pushNotification, popNotification, + setNotificationViewed, clearNotificationQueue, resetNotifications, setNotificationStatus, diff --git a/packages/wallet/src/features/notifications/testingUtils.ts b/packages/wallet/src/features/notifications/testingUtils.ts index 8d461cf87a1..0c18e65eb3d 100644 --- a/packages/wallet/src/features/notifications/testingUtils.ts +++ b/packages/wallet/src/features/notifications/testingUtils.ts @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react' +import { useDispatch } from 'react-redux' import { pushNotification } from 'wallet/src/features/notifications/slice' import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' -import { useAppDispatch } from 'wallet/src/state' export const exampleDisconnectedNotification = { type: 2, @@ -21,7 +21,6 @@ export const exampleSwapConfirmation = { export const exampleSwapSuccess = { txStatus: 'failed', chainId: 42161, - txHash: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', // address: '0x...', // txId: 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', type: 3, @@ -37,7 +36,7 @@ export const exampleSwapSuccess = { // easiest to use inside NotificationToastWrapper before any returns export const useMockNotification = (ms?: number): void => { const [sent, setSent] = useState(false) - const dispatch = useAppDispatch() + const dispatch = useDispatch() const activeAddress = useActiveAccountAddressWithThrow() useEffect(() => { @@ -51,7 +50,7 @@ export const useMockNotification = (ms?: number): void => { ...exampleSwapSuccess, hideDelay: ms ?? exampleSwapSuccess.hideDelay, address: activeAddress, - }) + }), ) setSent(true) } diff --git a/packages/wallet/src/features/notifications/types.ts b/packages/wallet/src/features/notifications/types.ts index e8d4b2a8afe..59b95790c28 100644 --- a/packages/wallet/src/features/notifications/types.ts +++ b/packages/wallet/src/features/notifications/types.ts @@ -1,13 +1,9 @@ import { TradeType } from '@uniswap/sdk-core' +import { AssetType } from 'uniswap/src/entities/assets' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' import { WalletChainId } from 'uniswap/src/types/chains' import { WalletConnectEvent } from 'uniswap/src/types/walletConnect' -import { AssetType } from 'wallet/src/entities/assets' -import { - FinalizedTransactionStatus, - TransactionType, - WrapType, -} from 'wallet/src/features/transactions/types' +import { FinalizedTransactionStatus, TransactionType, WrapType } from 'wallet/src/features/transactions/types' export enum AppNotificationType { Default, @@ -35,6 +31,7 @@ export interface AppNotificationBase { type: AppNotificationType address?: Address hideDelay?: number + shown?: boolean } export interface AppNotificationDefault extends AppNotificationBase { @@ -58,7 +55,6 @@ export interface TransactionNotificationBase extends AppNotificationBase { type: AppNotificationType.Transaction txType: TransactionType txStatus: FinalizedTransactionStatus - txHash: string txId: string chainId: WalletChainId tokenAddress?: string @@ -123,9 +119,7 @@ export interface UnknownTxNotification extends TransactionNotificationBase { txType: TransactionType.Unknown } -export type TransferCurrencyTxNotification = - | SendCurrencyTxNotification - | ReceiveCurrencyTxNotification +export type TransferCurrencyTxNotification = SendCurrencyTxNotification | ReceiveCurrencyTxNotification export type TransferNFTTxNotification = SendNFTNotification | ReceiveNFTNotification diff --git a/packages/wallet/src/features/notifications/utils.test.ts b/packages/wallet/src/features/notifications/utils.test.ts index c2a58a3b0d6..40c68319cfc 100644 --- a/packages/wallet/src/features/notifications/utils.test.ts +++ b/packages/wallet/src/features/notifications/utils.test.ts @@ -1,5 +1,5 @@ import { TradeType } from '@uniswap/sdk-core' -import { DAI, USDC } from 'wallet/src/constants/tokens' +import { DAI, USDC } from 'uniswap/src/constants/tokens' import { formSwapNotificationTitle } from 'wallet/src/features/notifications/utils' import { TransactionStatus } from 'wallet/src/features/transactions/types' import { mockLocalizedFormatter } from 'wallet/src/test/mocks' @@ -16,8 +16,8 @@ describe(formSwapNotificationTitle, () => { '1-USDC', '1000000000000000000', '1000000', - TradeType.EXACT_INPUT - ) + TradeType.EXACT_INPUT, + ), ).toEqual('Swapped 1.00 DAI for ~1.00 USDC.') }) @@ -31,8 +31,8 @@ describe(formSwapNotificationTitle, () => { '1-DAI', '1-USDC', '1000000000000000000', - '1200000' - ) + '1200000', + ), ).toEqual('Swapped 1.00 DAI for 1.20 USDC.') }) @@ -47,8 +47,8 @@ describe(formSwapNotificationTitle, () => { '1-USDC', '1000000000000000000', '1000000', - TradeType.EXACT_INPUT - ) + TradeType.EXACT_INPUT, + ), ).toEqual('Canceled DAI-USDC swap.') }) @@ -63,8 +63,8 @@ describe(formSwapNotificationTitle, () => { '1-USDC', '1000000000000000000', '1000000', - TradeType.EXACT_INPUT - ) + TradeType.EXACT_INPUT, + ), ).toEqual('Failed to swap 1.00 DAI for ~1.00 USDC.') }) }) diff --git a/packages/wallet/src/features/notifications/utils.ts b/packages/wallet/src/features/notifications/utils.ts index edb305999c2..be421dd45bc 100644 --- a/packages/wallet/src/features/notifications/utils.ts +++ b/packages/wallet/src/features/notifications/utils.ts @@ -1,16 +1,16 @@ import { Currency, TradeType } from '@uniswap/sdk-core' import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' +import { toSupportedChainId } from 'uniswap/src/features/chains/utils' import i18n from 'uniswap/src/i18n/i18n' import { WalletConnectEvent } from 'uniswap/src/types/walletConnect' +import { getValidAddress, shortenAddress } from 'uniswap/src/utils/addresses' import { getSymbolDisplayText } from 'uniswap/src/utils/currency' -import { toSupportedChainId } from 'wallet/src/features/chains/utils' +import { currencyIdToAddress } from 'uniswap/src/utils/currencyId' import { LocalizationContextState } from 'wallet/src/features/language/LocalizationContext' import { GQLNftAsset } from 'wallet/src/features/nfts/hooks' import { WalletConnectNotification } from 'wallet/src/features/notifications/types' import { TransactionStatus, TransactionType } from 'wallet/src/features/transactions/types' -import { getValidAddress, shortenAddress } from 'wallet/src/utils/addresses' import { getCurrencyDisplayText, getFormattedCurrencyAmount } from 'wallet/src/utils/currency' -import { currencyIdToAddress } from 'wallet/src/utils/currencyId' export const formWCNotificationTitle = (appNotification: WalletConnectNotification): string => { const { event, dappName, chainId } = appNotification @@ -41,7 +41,7 @@ export const formApproveNotificationTitle = ( txStatus: TransactionStatus, currency: Maybe, tokenAddress: Address, - spender: Address + spender: Address, ): string => { const currencyDisplayText = getCurrencyDisplayText(currency, tokenAddress) const address = shortenAddress(spender) @@ -51,13 +51,13 @@ export const formApproveNotificationTitle = ( address, }) : txStatus === TransactionStatus.Canceled - ? i18n.t('notification.transaction.approve.canceled', { - currencySymbol: currencyDisplayText, - }) - : i18n.t('notification.transaction.approve.fail', { - currencySymbol: currencyDisplayText, - address, - }) + ? i18n.t('notification.transaction.approve.canceled', { + currencySymbol: currencyDisplayText, + }) + : i18n.t('notification.transaction.approve.fail', { + currencySymbol: currencyDisplayText, + address, + }) } export const formSwapNotificationTitle = ( @@ -69,47 +69,49 @@ export const formSwapNotificationTitle = ( outputCurrencyId: string, inputCurrencyAmountRaw: string, outputCurrencyAmountRaw: string, - tradeType?: TradeType + tradeType?: TradeType, ): string => { - const inputCurrencySymbol = getCurrencyDisplayText( - inputCurrency, - currencyIdToAddress(inputCurrencyId) - ) - const outputCurrencySymbol = getCurrencyDisplayText( - outputCurrency, - currencyIdToAddress(outputCurrencyId) - ) + const inputCurrencySymbol = getCurrencyDisplayText(inputCurrency, currencyIdToAddress(inputCurrencyId)) + const outputCurrencySymbol = getCurrencyDisplayText(outputCurrency, currencyIdToAddress(outputCurrencyId)) const inputAmount = getFormattedCurrencyAmount( inputCurrency, inputCurrencyAmountRaw, formatter, - tradeType === TradeType.EXACT_OUTPUT + tradeType === TradeType.EXACT_OUTPUT, ) const outputAmount = getFormattedCurrencyAmount( outputCurrency, outputCurrencyAmountRaw, formatter, - tradeType === TradeType.EXACT_INPUT + tradeType === TradeType.EXACT_INPUT, ) const inputCurrencyAmountWithSymbol = `${inputAmount}${inputCurrencySymbol}` const outputCurrencyAmountWithSymbol = `${outputAmount}${outputCurrencySymbol}` - return txStatus === TransactionStatus.Success - ? i18n.t('notification.transaction.swap.success', { + switch (txStatus) { + case TransactionStatus.Success: + return i18n.t('notification.transaction.swap.success', { inputCurrencyAmountWithSymbol, outputCurrencyAmountWithSymbol, }) - : txStatus === TransactionStatus.Canceled - ? i18n.t('notification.transaction.swap.canceled', { + case TransactionStatus.Canceled: + return i18n.t('notification.transaction.swap.canceled', { inputCurrencySymbol, outputCurrencySymbol, }) - : i18n.t('notification.transaction.swap.fail', { + case TransactionStatus.Expired: + return i18n.t('notification.transaction.swap.expired', { + inputCurrencySymbol, + outputCurrencySymbol, + }) + default: + return i18n.t('notification.transaction.swap.fail', { inputCurrencyAmountWithSymbol, outputCurrencyAmountWithSymbol, }) + } } export const formWrapNotificationTitle = ( @@ -118,7 +120,7 @@ export const formWrapNotificationTitle = ( inputCurrency: Maybe, outputCurrency: Maybe, currencyAmountRaw: string, - unwrapped: boolean + unwrapped: boolean, ): string => { const inputCurrencySymbol = getSymbolDisplayText(inputCurrency?.symbol) const outputCurrencySymbol = getSymbolDisplayText(outputCurrency?.symbol) @@ -136,12 +138,12 @@ export const formWrapNotificationTitle = ( outputCurrencyAmountWithSymbol, }) : txStatus === TransactionStatus.Canceled - ? i18n.t('notification.transaction.unwrap.canceled', { - inputCurrencySymbol, - }) - : i18n.t('notification.transaction.unwrap.fail', { - inputCurrencyAmountWithSymbol, - }) + ? i18n.t('notification.transaction.unwrap.canceled', { + inputCurrencySymbol, + }) + : i18n.t('notification.transaction.unwrap.fail', { + inputCurrencyAmountWithSymbol, + }) } return txStatus === TransactionStatus.Success ? i18n.t('notification.transaction.wrap.success', { @@ -149,12 +151,12 @@ export const formWrapNotificationTitle = ( outputCurrencyAmountWithSymbol, }) : txStatus === TransactionStatus.Canceled - ? i18n.t('notification.transaction.wrap.canceled', { - inputCurrencySymbol, - }) - : i18n.t('notification.transaction.wrap.fail', { - inputCurrencyAmountWithSymbol, - }) + ? i18n.t('notification.transaction.wrap.canceled', { + inputCurrencySymbol, + }) + : i18n.t('notification.transaction.wrap.fail', { + inputCurrencyAmountWithSymbol, + }) } export const formTransferCurrencyNotificationTitle = ( @@ -164,7 +166,7 @@ export const formTransferCurrencyNotificationTitle = ( currency: Maybe, tokenAddress: string, currencyAmountRaw: string, - senderOrRecipient: string + senderOrRecipient: string, ): string => { const currencySymbol = getCurrencyDisplayText(currency, tokenAddress) const amount = getFormattedCurrencyAmount(currency, currencyAmountRaw, formatter) @@ -178,7 +180,7 @@ export const formTransferNFTNotificationTitle = ( nft: GQLNftAsset | undefined, tokenAddress: Address, tokenId: string, - senderOrRecipient: string + senderOrRecipient: string, ): string => { const nftName = nft?.name ?? `NFT ${shortenAddress(tokenAddress)} #${tokenId}` const shortenedAddressOrENS = getShortenedAddressOrEns(senderOrRecipient) @@ -188,7 +190,7 @@ export const formTransferNFTNotificationTitle = ( export const formUnknownTxTitle = ( txStatus: TransactionStatus, tokenAddress: Address | undefined, - ensName: string | null + ensName: string | null, ): string => { const address = tokenAddress && shortenAddress(tokenAddress) const target = ensName ?? address @@ -210,7 +212,7 @@ const formTransferTxTitle = ( txType: TransactionType, txStatus: TransactionStatus, tokenNameOrAddress: string, - walletNameOrAddress: string + walletNameOrAddress: string, ): string => { if (txType === TransactionType.Send) { return txStatus === TransactionStatus.Success @@ -219,13 +221,13 @@ const formTransferTxTitle = ( walletNameOrAddress, }) : txStatus === TransactionStatus.Canceled - ? i18n.t('notification.transaction.transfer.canceled', { - tokenNameOrAddress, - }) - : i18n.t('notification.transaction.transfer.fail', { - tokenNameOrAddress, - walletNameOrAddress, - }) + ? i18n.t('notification.transaction.transfer.canceled', { + tokenNameOrAddress, + }) + : i18n.t('notification.transaction.transfer.fail', { + tokenNameOrAddress, + walletNameOrAddress, + }) } return i18n.t('notification.transaction.transfer.received', { diff --git a/packages/wallet/src/features/onboarding/OnboardingContext.tsx b/packages/wallet/src/features/onboarding/OnboardingContext.tsx index eb2f3a87f80..a80e9f90d2a 100644 --- a/packages/wallet/src/features/onboarding/OnboardingContext.tsx +++ b/packages/wallet/src/features/onboarding/OnboardingContext.tsx @@ -1,9 +1,13 @@ +/* eslint-disable max-lines */ import { PropsWithChildren, createContext, useContext, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' +import { useDispatch } from 'react-redux' import { MobileEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { UnitagClaim } from 'uniswap/src/features/unitags/types' import { ImportType } from 'uniswap/src/types/onboarding' +import { ExtensionOnboardingFlow } from 'uniswap/src/types/screens/extension' +import { areAddressesEqual } from 'uniswap/src/utils/addresses' import { logger } from 'utilities/src/logger/logger' import { isExtension } from 'utilities/src/platform' import { setHasSkippedUnitagPrompt } from 'wallet/src/features/behaviorHistory/slice' @@ -13,36 +17,33 @@ import { createImportedAccounts } from 'wallet/src/features/onboarding/createImp import { createOnboardingAccount } from 'wallet/src/features/onboarding/createOnboardingAccount' import { useClaimUnitag } from 'wallet/src/features/unitags/hooks' import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring' -import { - EditAccountAction, - editAccountActions, -} from 'wallet/src/features/wallet/accounts/editAccountSaga' -import { - Account, - BackupType, - SignerMnemonicAccount, -} from 'wallet/src/features/wallet/accounts/types' +import { EditAccountAction, editAccountActions } from 'wallet/src/features/wallet/accounts/editAccountSaga' +import { Account, BackupType, SignerMnemonicAccount } from 'wallet/src/features/wallet/accounts/types' import { createAccountsActions } from 'wallet/src/features/wallet/create/createAccountsSaga' import { selectSortedSignerMnemonicAccounts } from 'wallet/src/features/wallet/selectors' -import { useAppDispatch, useAppSelector } from 'wallet/src/state' -import { areAddressesEqual } from 'wallet/src/utils/addresses' +import { useAppSelector } from 'wallet/src/state' export interface OnboardingContext { generateOnboardingAccount: (password?: string) => Promise - generateImportedAccounts: ( - mnemonicId: string, - backupType: BackupType.Cloud | BackupType.Manual - ) => Promise + generateImportedAccounts: (mnemonicId: string, backupType: BackupType.Cloud | BackupType.Manual) => Promise generateImportedAccountsByMnemonic: ( validMnemonic: string, password?: string, - backupType?: BackupType.Cloud | BackupType.Manual + backupType?: BackupType.Cloud | BackupType.Manual, ) => Promise addBackupMethod: (backupMethod: BackupType) => void hasBackup: (address: string, backupType?: BackupType) => boolean | undefined enableNotifications: () => void selectImportedAccounts: (accountAddresses: string[]) => Promise - finishOnboarding: (importType: ImportType, accounts?: SignerMnemonicAccount[]) => Promise + finishOnboarding: ({ + importType, + accounts, + extensionOnboardingFlow, + }: { + importType: ImportType + accounts?: SignerMnemonicAccount[] + extensionOnboardingFlow?: ExtensionOnboardingFlow + }) => Promise getAllOnboardingAccounts: () => SignerMnemonicAccount[] getOnboardingAccount: () => SignerMnemonicAccount | undefined getOnboardingAccountAddress: () => string | undefined @@ -67,8 +68,11 @@ const initialOnboardingContext: OnboardingContext = { hasBackup: () => undefined, enableNotifications: () => undefined, selectImportedAccounts: async () => [], - finishOnboarding: async (_importType: ImportType, _accounts?: SignerMnemonicAccount[]) => - undefined, + finishOnboarding: async (_params: { + importType: ImportType + accounts?: SignerMnemonicAccount[] + extensionOnboardingFlow?: ExtensionOnboardingFlow + }) => undefined, getAllOnboardingAccounts: () => [], getOnboardingAccount: () => undefined, getOnboardingAccountAddress: () => undefined, @@ -94,7 +98,7 @@ const OnboardingContext = createContext(initialOnboardingCont * to redux store. */ export function OnboardingContextProvider({ children }: PropsWithChildren): JSX.Element { - const dispatch = useAppDispatch() + const dispatch = useDispatch() const { t } = useTranslation() const claimUnitag = useClaimUnitag() const sortedMnemonicAccounts = useAppSelector(selectSortedSignerMnemonicAccounts) @@ -107,13 +111,9 @@ export function OnboardingContextProvider({ children }: PropsWithChildren importedAccounts - ?.sort( - (a, b) => - (a as SignerMnemonicAccount).derivationIndex - - (b as SignerMnemonicAccount).derivationIndex - ) + ?.sort((a, b) => (a as SignerMnemonicAccount).derivationIndex - (b as SignerMnemonicAccount).derivationIndex) .map((account: SignerMnemonicAccount) => account.address), - [importedAccounts] + [importedAccounts], ) /** @@ -122,6 +122,12 @@ export function OnboardingContextProvider({ children }: PropsWithChildren => { + if (isExtension) { + // Clear any stale data from Keyring + // Only used on web during onboarding + // Mobile has different legacy conditions + await Keyring.removeAllMnemonicsAndPrivateKeys() + } resetOnboardingContextData() setOnboardingAccount(await createOnboardingAccount(sortedMnemonicAccounts, password)) } @@ -159,7 +165,7 @@ export function OnboardingContextProvider({ children }: PropsWithChildren => { setImportedAccounts(undefined) setOnboardingAccount(undefined) @@ -169,8 +175,14 @@ export function OnboardingContextProvider({ children }: PropsWithChildren => { + if (isExtension) { + // Clear any stale data from Keyring + // Only used on web during onboarding + // Mobile has different legacy conditions + await Keyring.removeAllMnemonicsAndPrivateKeys() + } const mnemonicId = await Keyring.importMnemonic(validMnemonic, password, true) await generateImportedAccounts(mnemonicId, backupType) } @@ -191,14 +203,12 @@ export function OnboardingContextProvider({ children }: PropsWithChildren => { + const selectImportedAccounts = async (accountAddresses: string[]): Promise => { if (!importedAccounts) { throw new Error('No imported accounts available for toggling selecting imported accounts') } const filteredImportedAccounts = importedAccounts.filter((importedAccount) => - accountAddresses.includes(importedAccount.address) + accountAddresses.includes(importedAccount.address), ) const namedImportedAccounts = filteredImportedAccounts.map((acc, index) => ({ ...acc, @@ -246,9 +256,7 @@ export function OnboardingContextProvider({ children }: PropsWithChildren account.address === address) ?.backups?.some((backup) => - backupType - ? backup === backupType - : backup === BackupType.Cloud || backup === BackupType.Manual + backupType ? backup === backupType : backup === BackupType.Cloud || backup === BackupType.Manual, ) } @@ -286,10 +294,15 @@ export function OnboardingContextProvider({ children }: PropsWithChildren => { + extensionOnboardingFlow?: ExtensionOnboardingFlow + }): Promise => { const isWatchFlow = importType === ImportType.Watch const onboardingAccounts = isWatchFlow ? [] : accounts ?? getAllOnboardingAccounts() const onboardingAddresses = onboardingAccounts.map((a) => a.address) @@ -299,15 +312,12 @@ export function OnboardingContextProvider({ children }: PropsWithChildren - acc.backups?.includes(BackupType.Cloud) - ), - }) + const isExtensionNoAccounts = onboardingAddresses.length === 0 && isExtension + if (!isExtensionNoAccounts) { + // Send analytics events + sendAnalyticsEvent(MobileEventName.OnboardingCompleted, { + wallet_type: importType, + flow: extensionOnboardingFlow, + accounts_imported_count: onboardingAddresses.length, + wallets_imported: onboardingAddresses, + cloud_backup_used: Object.values(onboardingAccounts).some((acc: Account) => + acc.backups?.includes(BackupType.Cloud), + ), + }) + } // Reset data caused production ios app crashes and it is not necessary on mobile if (isExtension) { @@ -401,9 +415,7 @@ export function OnboardingContextProvider({ children }: PropsWithChildren { throwIfNotExtension() if (!mnemonic || (mnemonic.length !== 12 && mnemonic.length !== 24)) { - throw new Error( - 'Incorrect value of mnemonic parameted passed to addOnboardingAccountMnemonic function' - ) + throw new Error('Incorrect value of mnemonic parameted passed to addOnboardingAccountMnemonic function') } setOnboardingAccountMnemonic(mnemonic) } @@ -440,7 +452,8 @@ export function OnboardingContextProvider({ children }: PropsWithChildren + }} + > {children} ) @@ -473,20 +486,20 @@ export function useCreateOnboardingAccountIfNone(): void { * Triggers onboarding finish on screen mount * Extracted into hook for reusability. */ -export function useFinishOnboarding(callback?: () => void): void { +export function useFinishOnboarding(callback?: () => void, extensionOnboardingFlow?: ExtensionOnboardingFlow): void { const { finishOnboarding, getOnboardingAccountAddress } = useOnboardingContext() const onboardingAccountAddress = getOnboardingAccountAddress() const importType = onboardingAccountAddress ? ImportType.CreateNew : ImportType.RestoreMnemonic useEffect(() => { - finishOnboarding(importType) + finishOnboarding({ importType, extensionOnboardingFlow }) .then(callback) .catch((e) => { logger.error(e, { tags: { file: 'useFinishOnboarding', function: 'finishOnboarding' }, }) }) - }, [finishOnboarding, importType, callback]) + }, [finishOnboarding, importType, callback, extensionOnboardingFlow]) } // Checks if context function is used on the proper platform diff --git a/packages/wallet/src/features/onboarding/createImportedAccounts.ts b/packages/wallet/src/features/onboarding/createImportedAccounts.ts index c702a58fcc1..2209adc67ea 100644 --- a/packages/wallet/src/features/onboarding/createImportedAccounts.ts +++ b/packages/wallet/src/features/onboarding/createImportedAccounts.ts @@ -1,21 +1,17 @@ import dayjs from 'dayjs' import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring' -import { - AccountType, - BackupType, - SignerMnemonicAccount, -} from 'wallet/src/features/wallet/accounts/types' +import { AccountType, BackupType, SignerMnemonicAccount } from 'wallet/src/features/wallet/accounts/types' export const NUMBER_OF_WALLETS_TO_IMPORT = 10 export const createImportedAccounts = async ( mnemonicId: string, - backupType?: BackupType.Cloud | BackupType.Manual + backupType?: BackupType.Cloud | BackupType.Manual, ): Promise => { const addresses = await Promise.all( Array(NUMBER_OF_WALLETS_TO_IMPORT) .fill(null) - .map(async (_, index) => await Keyring.generateAndStorePrivateKey(mnemonicId, index)) + .map(async (_, index) => await Keyring.generateAndStorePrivateKey(mnemonicId, index)), ) const importedAccounts: SignerMnemonicAccount[] = addresses.map((address, index) => ({ type: AccountType.SignerMnemonic, diff --git a/packages/wallet/src/features/onboarding/createOnboardingAccount.ts b/packages/wallet/src/features/onboarding/createOnboardingAccount.ts index 82eb6ea4ee3..622b5acf453 100644 --- a/packages/wallet/src/features/onboarding/createOnboardingAccount.ts +++ b/packages/wallet/src/features/onboarding/createOnboardingAccount.ts @@ -1,21 +1,17 @@ import dayjs from 'dayjs' import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring' -import { - AccountType, - BackupType, - SignerMnemonicAccount, -} from 'wallet/src/features/wallet/accounts/types' +import { AccountType, BackupType, SignerMnemonicAccount } from 'wallet/src/features/wallet/accounts/types' /** * Takes a list of existing mnemonic accounts to use as reference for pulling the next derivation index */ export const createOnboardingAccount = async ( sortedMnemonicAccounts: SignerMnemonicAccount[], - password?: string + password?: string, ): Promise => { const { nextDerivationIndex, mnemonicId, existingBackups } = await getNewAccountParams( sortedMnemonicAccounts, - password + password, ) const address = await Keyring.generateAndStorePrivateKey(mnemonicId, nextDerivationIndex) return { @@ -31,7 +27,7 @@ export const createOnboardingAccount = async ( export async function getNewAccountParams( sortedAccounts: SignerMnemonicAccount[], - password?: string + password?: string, ): Promise<{ nextDerivationIndex: number mnemonicId: string diff --git a/packages/wallet/src/features/onboarding/createViewOnlyAccount.ts b/packages/wallet/src/features/onboarding/createViewOnlyAccount.ts index bbb00d732ac..44742339589 100644 --- a/packages/wallet/src/features/onboarding/createViewOnlyAccount.ts +++ b/packages/wallet/src/features/onboarding/createViewOnlyAccount.ts @@ -1,6 +1,6 @@ import dayjs from 'dayjs' +import { getValidAddress } from 'uniswap/src/utils/addresses' import { AccountType, ReadOnlyAccount } from 'wallet/src/features/wallet/accounts/types' -import { getValidAddress } from 'wallet/src/utils/addresses' export const createViewOnlyAccount = (address: string): ReadOnlyAccount => { const formattedAddress = getValidAddress(address, true) diff --git a/packages/wallet/src/features/onboarding/hooks/useSelectAccounts.tsx b/packages/wallet/src/features/onboarding/hooks/useSelectAccounts.tsx index 7c2f4ce09ed..e86046ee2fe 100644 --- a/packages/wallet/src/features/onboarding/hooks/useSelectAccounts.tsx +++ b/packages/wallet/src/features/onboarding/hooks/useSelectAccounts.tsx @@ -10,10 +10,7 @@ export function useSelectAccounts(accounts: ImportableAccount[] = []): { selectedAddresses: string[] toggleAddressSelection: (address: string) => void } { - const initialSelectedAddresses = useMemo( - () => accounts.map((account) => account.ownerAddress), - [accounts] - ) + const initialSelectedAddresses = useMemo(() => accounts.map((account) => account.ownerAddress), [accounts]) const [selectedAddresses, setSelectedAddresses] = useState(initialSelectedAddresses) const toggleAddressSelection = (address: string): void => { @@ -22,9 +19,7 @@ export function useSelectAccounts(accounts: ImportableAccount[] = []): { return } if (selectedAddresses.includes(address)) { - setSelectedAddresses( - selectedAddresses.filter((selectedAddress) => selectedAddress !== address) - ) + setSelectedAddresses(selectedAddresses.filter((selectedAddress) => selectedAddress !== address)) } else { setSelectedAddresses([...selectedAddresses, address]) } diff --git a/packages/wallet/src/features/portfolio/AnimatedNumber.tsx b/packages/wallet/src/features/portfolio/AnimatedNumber.tsx index 42554ac35e9..f1faa83c960 100644 --- a/packages/wallet/src/features/portfolio/AnimatedNumber.tsx +++ b/packages/wallet/src/features/portfolio/AnimatedNumber.tsx @@ -16,6 +16,7 @@ import { Flex, Shine, useSporeColors } from 'ui/src' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { TextLoaderWrapper } from 'ui/src/components/text/Text' import { fonts } from 'ui/src/theme' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { usePrevious } from 'utilities/src/react/hooks' import { useAppFiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks' @@ -26,7 +27,7 @@ export const ADDITIONAL_WIDTH_FOR_ANIMATIONS = 8 // TODO: remove need to manually define width of each character const NUMBER_WIDTH_ARRAY_SCALED = NUMBER_WIDTH_ARRAY.map( - (width) => width * (fonts.heading2.fontSize / fonts.heading1.fontSize) + (width) => width * (fonts.heading2.fontSize / fonts.heading1.fontSize), ) const isRTL = I18nManager.isRTL @@ -54,18 +55,16 @@ const RollNumber = ({ }): JSX.Element => { const colors = useSporeColors() const fontColor = useSharedValue( - nextColor || - (shouldFadeDecimals && index > chars.length - 4 ? colors.neutral3.val : colors.neutral1.val) + nextColor || (shouldFadeDecimals && index > chars.length - 4 ? colors.neutral3.val : colors.neutral1.val), ) const yOffset = useSharedValue(digit && Number(digit) >= 0 ? DIGIT_HEIGHT * -digit : 0) useEffect(() => { - const finishColor = - shouldFadeDecimals && index > chars.length - 4 ? colors.neutral3.val : colors.neutral1.val + const finishColor = shouldFadeDecimals && index > chars.length - 4 ? colors.neutral3.val : colors.neutral1.val if (nextColor && index > commonPrefixLength - 1) { fontColor.value = withSequence( withTiming(nextColor, { duration: 250 }), - withDelay(50, withTiming(finishColor, { duration: 310 })) + withDelay(50, withTiming(finishColor, { duration: 310 })), ) } else { fontColor.value = finishColor @@ -93,7 +92,8 @@ const RollNumber = ({ + style={[animatedFontStyle, AnimatedFontStyles.fontStyle, { height: DIGIT_HEIGHT }]} + > {char} ) @@ -117,11 +117,11 @@ const RollNumber = ({ style={[ animatedWrapperStyle, { - width: - (NUMBER_WIDTH_ARRAY_SCALED[Number(digit)] || 0) + ADDITIONAL_WIDTH_FOR_ANIMATIONS, + width: (NUMBER_WIDTH_ARRAY_SCALED[Number(digit)] || 0) + ADDITIONAL_WIDTH_FOR_ANIMATIONS, ...margin, }, - ]}> + ]} + > {numbers} ) @@ -129,7 +129,8 @@ const RollNumber = ({ return ( + style={[animatedFontStyle, AnimatedFontStyles.fontStyle, { height: DIGIT_HEIGHT }]} + > {digit} ) @@ -154,7 +155,8 @@ const Char = ({ entering={nextColor ? FadeIn : undefined} exiting={FadeOut} layout={Layout} - style={[{ height: DIGIT_HEIGHT }, AnimatedCharStyles.wrapperStyle]}> + style={[{ height: DIGIT_HEIGHT }, AnimatedCharStyles.wrapperStyle]} + > { { color: colors.neutral1.val, }, - ]}> + ]} + testID={TestID.PortfolioBalance} + > {amountOfCurrency[0]} + }} + > {currency.decimalSeparator} {amountOfCurrency[1]} @@ -262,11 +267,7 @@ const ReanimatedNumber = ({ const scaleWraper = useAnimatedStyle(() => { return { - transform: [ - { translateX: -SCREEN_WIDTH / 2 }, - { scale: scale.value }, - { translateX: SCREEN_WIDTH / 2 }, - ], + transform: [{ translateX: -SCREEN_WIDTH / 2 }, { scale: scale.value }, { translateX: SCREEN_WIDTH / 2 }], } }) @@ -309,11 +310,7 @@ const ReanimatedNumber = ({ {placeholderChars.map((_, index) => ( - + + {chars?.map((_, index) => ( + onLayout={fitBalanceOnLayout} + > {value} diff --git a/packages/wallet/src/features/portfolio/HiddenTokensRow.tsx b/packages/wallet/src/features/portfolio/HiddenTokensRow.tsx index ff18d3e588a..eefa35b997d 100644 --- a/packages/wallet/src/features/portfolio/HiddenTokensRow.tsx +++ b/packages/wallet/src/features/portfolio/HiddenTokensRow.tsx @@ -2,6 +2,7 @@ import { useTranslation } from 'react-i18next' import { Flex, ImpactFeedbackStyle, Text, TouchableArea } from 'ui/src' import { RotatableChevron } from 'ui/src/components/icons' import { iconSizes } from 'ui/src/theme' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' export function HiddenTokensRow({ padded = false, @@ -17,22 +18,18 @@ export function HiddenTokensRow({ const { t } = useTranslation() return ( - - + + {t('tokens.hidden.label', { numHidden })} {/* just used for opacity styling, the parent TouchableArea handles event */} - + + py="$spacing8" + > + variant="buttonLabel3" + > {isExpanded ? t('common.button.hide') : t('common.button.show')} @@ -83,7 +81,8 @@ const WebBalanceWithFadedDecimals = ({ value }: { value: string }): JSX.Element style={{ fontWeight: WEB_BALANCE_FONT_WEIGHT, }} - variant="heading2"> + variant="heading2" + > {amountOfCurrency[0]} {amountOfCurrency.length > 1 && ( diff --git a/packages/wallet/src/features/portfolio/PortfolioEmptyState.tsx b/packages/wallet/src/features/portfolio/PortfolioEmptyState.tsx index 17f1da3ef6d..c234aab7605 100644 --- a/packages/wallet/src/features/portfolio/PortfolioEmptyState.tsx +++ b/packages/wallet/src/features/portfolio/PortfolioEmptyState.tsx @@ -1,36 +1,19 @@ -import React, { useMemo } from 'react' +import React, { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' -import { - ImageBackground, - ImageSourcePropType, - StyleProp, - StyleSheet, - ViewStyle, - VirtualizedList, -} from 'react-native' -import { Flex, Text, TouchableArea, useIsDarkMode } from 'ui/src' +import { ImageBackground, ImageSourcePropType, StyleProp, StyleSheet, ViewStyle, VirtualizedList } from 'react-native' +import { Flex, useIsDarkMode } from 'ui/src' import { CRYPTO_PURCHASE_BACKGROUND_DARK, CRYPTO_PURCHASE_BACKGROUND_LIGHT } from 'ui/src/assets' import { ArrowDownCircle, Buy as BuyIcon, PaperStack } from 'ui/src/components/icons' import { borderRadii } from 'ui/src/theme' +import { ActionCard, ActionCardItem } from 'uniswap/src/components/misc/ActionCard' +import { useCexTransferProviders } from 'uniswap/src/features/fiatOnRamp/useCexTransferProviders' import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' -import Trace from 'uniswap/src/features/telemetry/Trace' -import { ElementName, ElementNameType } from 'uniswap/src/features/telemetry/constants' -import { useCexTransferProviders } from 'wallet/src/features/fiatOnRamp/api' +import { ElementName } from 'uniswap/src/features/telemetry/constants' import { ImageUri } from 'wallet/src/features/images/ImageUri' import { AccountType } from 'wallet/src/features/wallet/accounts/types' import { useActiveAccount } from 'wallet/src/features/wallet/hooks' -interface ActionCardItem { - title: string - blurb: string - icon: JSX.Element - elementName: ElementNameType - backgroundImage?: ImageSourcePropType - badgeText?: string - onPress?: () => void -} - enum ActionOption { Buy = 'Buy', Import = 'Import', @@ -47,11 +30,7 @@ type WalletEmptyStateProps = { onPressBuy?: () => void } -export function PortfolioEmptyState({ - onPressReceive, - onPressImport, - onPressBuy, -}: WalletEmptyStateProps): JSX.Element { +export function PortfolioEmptyState({ onPressReceive, onPressImport, onPressBuy }: WalletEmptyStateProps): JSX.Element { const { t } = useTranslation() const isDarkMode = useIsDarkMode() @@ -60,6 +39,17 @@ export function PortfolioEmptyState({ const cexTransferEnabled = useFeatureFlag(FeatureFlags.CexTransfers) const cexTransferProviders = useCexTransferProviders(cexTransferEnabled) + const BackgroundImageWrapperCallback = useCallback( + ({ children }: { children: React.ReactNode }) => { + return ( + + {children} + + ) + }, + [isDarkMode], + ) + const options: { [key in ActionOption]: ActionCardItem } = useMemo( () => ({ [ActionOption.Buy]: { @@ -67,10 +57,8 @@ export function PortfolioEmptyState({ blurb: t('home.tokens.empty.action.buy.description'), elementName: ElementName.EmptyStateBuy, icon: , - backgroundImage: isDarkMode - ? CRYPTO_PURCHASE_BACKGROUND_DARK - : CRYPTO_PURCHASE_BACKGROUND_LIGHT, onPress: onPressBuy, + BackgroundImageWrapperCallback, }, [ActionOption.Receive]: { title: t('home.tokens.empty.action.receive.title'), @@ -78,9 +66,7 @@ export function PortfolioEmptyState({ elementName: ElementName.EmptyStateReceive, icon: cexTransferProviders.length > 0 ? ( - provider.logos.lightLogo)} - /> + provider.logos.lightLogo)} /> ) : ( ), @@ -94,14 +80,12 @@ export function PortfolioEmptyState({ onPress: onPressImport, }, }), - [t, isDarkMode, onPressBuy, cexTransferProviders, onPressReceive, onPressImport] + [t, onPressBuy, BackgroundImageWrapperCallback, cexTransferProviders, onPressReceive, onPressImport], ) // Order options based on view only status, and wether we have a valid buy handler const sortedOptions = - isViewOnly && onPressImport - ? [options.Import] - : [...(onPressBuy ? [options.Buy] : []), options.Receive] + isViewOnly && onPressImport ? [options.Import] : [...(onPressBuy ? [options.Buy] : []), options.Receive] return ( @@ -112,51 +96,17 @@ export function PortfolioEmptyState({ ) } -const ActionCard = ({ - title, - blurb, - onPress, - icon, - elementName, - backgroundImage, -}: ActionCardItem): JSX.Element => ( - - - - - {icon} - - - {title} - - - {blurb} - - - - - - -) - -const BackgroundWrapper = ({ +const BackgroundImage = ({ children, - backgroundImage, + image, }: { children: React.ReactNode - backgroundImage?: ImageSourcePropType + image: ImageSourcePropType }): JSX.Element => { - return backgroundImage !== undefined ? ( - + return ( + {children} - ) : ( - {children} ) } @@ -170,7 +120,8 @@ function ReceiveCryptoIcon(): JSX.Element { style={{ ...styles.iconContainer, borderRadius: borderRadii.roundedFull, - }}> + }} + > + style={styles.iconContainer} + > void isLoading?: boolean padded?: boolean + index?: number } export const TokenBalanceItem = memo(function _TokenBalanceItem({ portfolioBalance, onPressToken, isLoading, + index, padded, }: TokenBalanceItemProps) { const { quantity, currencyInfo, relativeChange24 } = portfolioBalance @@ -37,10 +39,7 @@ export const TokenBalanceItem = memo(function _TokenBalanceItem({ } const shortenedSymbol = getSymbolDisplayText(currency.symbol) - const balance = convertFiatAmountFormatted( - portfolioBalance.balanceUSD, - NumberType.FiatTokenQuantity - ) + const balance = convertFiatAmountFormatted(portfolioBalance.balanceUSD, NumberType.FiatTokenQuantity) return ( + onPress={onPress} + > void } -export const TokenBalanceListContext = createContext( - undefined -) +export const TokenBalanceListContext = createContext(undefined) export function TokenBalanceListContextProvider({ owner, @@ -113,21 +108,17 @@ export function TokenBalanceListContextProvider({ onPressToken, refetch, rows, - ] + ], ) - return ( - {children} - ) + return {children} } export const useTokenBalanceListContext = (): TokenBalanceListContextState => { const context = useContext(TokenBalanceListContext) if (context === undefined) { - throw new Error( - '`useTokenBalanceListContext` must be used inside of `TokenBalanceListContextProvider`' - ) + throw new Error('`useTokenBalanceListContext` must be used inside of `TokenBalanceListContextProvider`') } return context diff --git a/packages/wallet/src/features/portfolio/api.ts b/packages/wallet/src/features/portfolio/api.ts index 86d98fa40b7..a8428ed65c0 100644 --- a/packages/wallet/src/features/portfolio/api.ts +++ b/packages/wallet/src/features/portfolio/api.ts @@ -2,12 +2,12 @@ import { Currency, CurrencyAmount, NativeCurrency as NativeCurrencyClass } from import { useMemo } from 'react' import ERC20_ABI from 'uniswap/src/abis/erc20.json' import { useRestQuery } from 'uniswap/src/data/rest' +import { getPollingIntervalByBlocktime } from 'uniswap/src/features/chains/utils' +import { NativeCurrency } from 'uniswap/src/features/tokens/NativeCurrency' import { WalletChainId } from 'uniswap/src/types/chains' -import { getPollingIntervalByBlocktime } from 'wallet/src/features/chains/utils' +import { currencyAddress as getCurrencyAddress } from 'uniswap/src/utils/currencyId' import { createEthersProvider } from 'wallet/src/features/providers/createEthersProvider' -import { NativeCurrency } from 'wallet/src/features/tokens/NativeCurrency' import { walletContextValue } from 'wallet/src/features/wallet/context' -import { currencyAddress as getCurrencyAddress } from 'wallet/src/utils/currencyId' import { ValueType, getCurrencyAmount } from 'wallet/src/utils/getCurrencyAmount' // stub endpoint to conform to REST endpoint styles @@ -40,19 +40,14 @@ export const getOnChainBalancesFetch = async (params: BalanceLookupParams): Prom } // erc20 lookup - const erc20Contract = walletContextValue.contracts.getOrCreateContract( - chainId, - currencyAddress, - provider, - ERC20_ABI - ) + const erc20Contract = walletContextValue.contracts.getOrCreateContract(chainId, currencyAddress, provider, ERC20_ABI) const balance = await erc20Contract.callStatic.balanceOf?.(accountAddress) return new Response(JSON.stringify({ balance: balance.toString() })) } export function useOnChainCurrencyBalance( currency?: Currency | null, - accountAddress?: Address + accountAddress?: Address, ): { balance: CurrencyAmount | undefined; isLoading: boolean; error: unknown } { const { data, error } = useRestQuery<{ balance?: string }, BalanceLookupParams>( STUB_ONCHAIN_BALANCES_ENDPOINT, @@ -67,24 +62,22 @@ export function useOnChainCurrencyBalance( pollInterval: getPollingIntervalByBlocktime(currency?.chainId), ttlMs: getPollingIntervalByBlocktime(currency?.chainId), skip: !currency, - } + }, ) return useMemo( () => ({ - balance: - getCurrencyAmount({ value: data?.balance, valueType: ValueType.Raw, currency }) ?? - undefined, + balance: getCurrencyAmount({ value: data?.balance, valueType: ValueType.Raw, currency }) ?? undefined, isLoading: !data?.balance, error, }), - [data, currency, error] + [data, currency, error], ) } export function useOnChainNativeCurrencyBalance( chain: WalletChainId, - accountAddress?: Address + accountAddress?: Address, ): { balance: CurrencyAmount | undefined; isLoading: boolean } { const currency = NativeCurrency.onChain(chain) const { balance, isLoading } = useOnChainCurrencyBalance(currency, accountAddress) diff --git a/packages/wallet/src/features/portfolio/useTokenContextMenu.tsx b/packages/wallet/src/features/portfolio/useTokenContextMenu.tsx index 9ce9cf30dec..39e2bf7808e 100644 --- a/packages/wallet/src/features/portfolio/useTokenContextMenu.tsx +++ b/packages/wallet/src/features/portfolio/useTokenContextMenu.tsx @@ -1,32 +1,28 @@ import { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { NativeSyntheticEvent } from 'react-native' -import type { - ContextMenuAction, - ContextMenuOnPressNativeEvent, -} from 'react-native-context-menu-view' +import type { ContextMenuAction, ContextMenuOnPressNativeEvent } from 'react-native-context-menu-view' +import { useDispatch } from 'react-redux' import { GeneratedIcon, isWeb } from 'ui/src' import { CoinConvert, Eye, EyeOff, ReceiveAlt, SendAction } from 'ui/src/components/icons' import { PortfolioBalance } from 'uniswap/src/features/dataApi/types' +import { WalletEventName } from 'uniswap/src/features/telemetry/constants' +import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' +import { CurrencyField } from 'uniswap/src/features/transactions/transactionState/types' import { UniverseChainId } from 'uniswap/src/types/chains' import { CurrencyId } from 'uniswap/src/types/currency' +import { areCurrencyIdsEqual, currencyIdToAddress, currencyIdToChain } from 'uniswap/src/utils/currencyId' import { ONE_SECOND_MS } from 'utilities/src/time/time' import { useWalletNavigation } from 'wallet/src/contexts/WalletNavigationContext' import { usePortfolioCacheUpdater } from 'wallet/src/features/dataApi/balances' import { toggleTokenVisibility } from 'wallet/src/features/favorites/slice' import { pushNotification } from 'wallet/src/features/notifications/slice' import { AppNotificationType } from 'wallet/src/features/notifications/types' -import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' -import { useAppDispatch } from 'wallet/src/state' -import { - areCurrencyIdsEqual, - currencyIdToAddress, - currencyIdToChain, -} from 'wallet/src/utils/currencyId' interface TokenMenuParams { currencyId: CurrencyId + isBlocked: boolean tokenSymbolForNotification?: Nullable portfolioBalance?: Nullable } @@ -35,6 +31,7 @@ type MenuAction = ContextMenuAction & { onPress: () => void; Icon?: GeneratedIco export function useTokenContextMenu({ currencyId, + isBlocked, tokenSymbolForNotification, portfolioBalance, }: TokenMenuParams): { @@ -42,11 +39,10 @@ export function useTokenContextMenu({ onContextMenuPress: (e: NativeSyntheticEvent) => void } { const { t } = useTranslation() - const dispatch = useAppDispatch() + const dispatch = useDispatch() const activeAccountAddress = useActiveAccountAddressWithThrow() - const { navigateToSwapFlow, navigateToReceive, navigateToSend, handleShareToken } = - useWalletNavigation() + const { navigateToSwapFlow, navigateToReceive, navigateToSend, handleShareToken } = useWalletNavigation() const activeAccountHoldsToken = portfolioBalance && areCurrencyIdsEqual(currencyId, portfolioBalance?.currencyInfo.currencyId) @@ -65,7 +61,7 @@ export function useTokenContextMenu({ // Do not show warning modal speed-bump if user is trying to swap tokens they own navigateToSwapFlow({ currencyField, currencyAddress, currencyChainId }) }, - [currencyAddress, currencyChainId, navigateToSwapFlow] + [currencyAddress, currencyChainId, navigateToSwapFlow], ) const onPressShare = useCallback(async () => { @@ -83,6 +79,11 @@ export function useTokenContextMenu({ */ updateCache(!isHidden, portfolioBalance ?? undefined) + sendAnalyticsEvent(WalletEventName.TokenVisibilityChanged, { + currencyId, + // we log the state to which it's transitioning + visible: isHidden, + }) dispatch(toggleTokenVisibility({ currencyId: currencyId.toLowerCase(), isSpam: isHidden })) if (tokenSymbolForNotification) { @@ -92,7 +93,7 @@ export function useTokenContextMenu({ visible: !isHidden, hideDelay: 2 * ONE_SECOND_MS, assetName: tokenSymbolForNotification, - }) + }), ) } }, [currencyId, dispatch, isHidden, tokenSymbolForNotification, updateCache, portfolioBalance]) @@ -101,6 +102,7 @@ export function useTokenContextMenu({ (): MenuAction[] => [ { title: t('common.button.swap'), + disabled: isBlocked, onPress: () => onPressSwap(CurrencyField.INPUT), ...(isWeb ? { @@ -154,6 +156,7 @@ export function useTokenContextMenu({ ], [ t, + isBlocked, onPressSend, navigateToReceive, onPressShare, @@ -161,14 +164,14 @@ export function useTokenContextMenu({ isHidden, onPressHiddenStatus, onPressSwap, - ] + ], ) const onContextMenuPress = useCallback( (e: NativeSyntheticEvent): void => { menuActions[e.nativeEvent.index]?.onPress?.() }, - [menuActions] + [menuActions], ) return { menuActions, onContextMenuPress } diff --git a/packages/wallet/src/features/providers/ProviderManager.ts b/packages/wallet/src/features/providers/ProviderManager.ts index a1e82d599f2..2144a9bceb0 100644 --- a/packages/wallet/src/features/providers/ProviderManager.ts +++ b/packages/wallet/src/features/providers/ProviderManager.ts @@ -74,11 +74,7 @@ export class ProviderManager { removeProviders(chainId: WalletChainId): void { const providersInfo = this._providers[chainId] if (!providersInfo) { - logger.warn( - 'ProviderManager', - 'removeProviders', - `Attempting to remove non-existent provider: ${chainId}` - ) + logger.warn('ProviderManager', 'removeProviders', `Attempting to remove non-existent provider: ${chainId}`) return } diff --git a/packages/wallet/src/features/providers/createEthersProvider.e2e.js b/packages/wallet/src/features/providers/createEthersProvider.e2e.js index c9cc2e15fe7..599175d5981 100644 --- a/packages/wallet/src/features/providers/createEthersProvider.e2e.js +++ b/packages/wallet/src/features/providers/createEthersProvider.e2e.js @@ -3,7 +3,6 @@ * Replaces `createEthersProvider.ts` when RN_SRC_EXT=e2e.js at runtime */ import { JsonRpcProvider } from '@ethersproject/providers' -import { WalletChainId } from 'uniswap/src/types/chains' export function createEthersProvider(chainId) { if (chainId === ChainId.Mainnet) { @@ -24,9 +23,7 @@ class TestProvider extends JsonRpcProvider { return block } catch (e) { if (e.reason === 'missing response') { - throw new Error( - 'Hardhat node is not running. Start it with `yarn hardhat`. [original error: ' + e - ) + throw new Error('Hardhat node is not running. Start it with `yarn hardhat`. [original error: ' + e) } throw e } diff --git a/packages/wallet/src/features/providers/createEthersProvider.ts b/packages/wallet/src/features/providers/createEthersProvider.ts index cc0f303d2f8..815691b0479 100644 --- a/packages/wallet/src/features/providers/createEthersProvider.ts +++ b/packages/wallet/src/features/providers/createEthersProvider.ts @@ -8,7 +8,7 @@ import { getInfuraChainName } from 'wallet/src/features/providers/utils' // Should use ProviderManager for provider access unless being accessed outside of ProviderManagerContext (e.g., Apollo initialization) export function createEthersProvider( chainId: WalletChainId, - rpcType: RPCType = RPCType.Public + rpcType: RPCType = RPCType.Public, ): ethersProviders.JsonRpcProvider | null { try { if (rpcType === RPCType.Private) { diff --git a/packages/wallet/src/features/scantastic/types.ts b/packages/wallet/src/features/scantastic/types.ts index 38db7081a41..e41e4c7e163 100644 --- a/packages/wallet/src/features/scantastic/types.ts +++ b/packages/wallet/src/features/scantastic/types.ts @@ -18,7 +18,7 @@ export const ScantasticParamsSchema = z.object({ invalid_type_error: 'Invalid public exponent', }), }, - { required_error: 'Public key is required' } + { required_error: 'Public key is required' }, ), vendor: z.string().nullish(), model: z.string().nullish(), diff --git a/packages/wallet/src/features/search/SearchBar.tsx b/packages/wallet/src/features/search/SearchBar.tsx index d1d62341305..bada0bdd118 100644 --- a/packages/wallet/src/features/search/SearchBar.tsx +++ b/packages/wallet/src/features/search/SearchBar.tsx @@ -1,23 +1,28 @@ +import { forwardRef } from 'react' +import { TextInput } from 'react-native' import { Flex, TouchableArea } from 'ui/src' import { RotatableChevron } from 'ui/src/components/icons' import { iconSizes } from 'ui/src/theme' -import { ElementName } from 'uniswap/src/features/telemetry/constants' -import { SearchTextInput, SearchTextInputProps } from 'wallet/src/features/search/SearchTextInput' +import { SearchTextInput, SearchTextInputProps } from 'uniswap/src/features/search/SearchTextInput' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' interface SearchBarProps extends SearchTextInputProps { onBack?: () => void } // Use instead of SearchTextInput when you need back button functionality outside of nav stack (i.e., inside BottomSheetModals) -export function SearchBar({ onBack, ...rest }: SearchBarProps): JSX.Element { +export const SearchBar = forwardRef(function _SearchBar( + { onBack, ...rest }, + ref, +): JSX.Element { return ( {onBack && ( - + )} - + ) -} +}) diff --git a/packages/wallet/src/features/search/SearchResult.ts b/packages/wallet/src/features/search/SearchResult.ts index 32f8bfd915e..d7dc214340a 100644 --- a/packages/wallet/src/features/search/SearchResult.ts +++ b/packages/wallet/src/features/search/SearchResult.ts @@ -1,21 +1,8 @@ import { SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { SearchResultType } from 'uniswap/src/features/search/SearchResult' import { WalletChainId } from 'uniswap/src/types/chains' -export type SearchResult = - | TokenSearchResult - | WalletSearchResult - | EtherscanSearchResult - | NFTCollectionSearchResult - -// Retain original ordering as these are saved to storage and loaded back out -export enum SearchResultType { - ENSAddress, - Token, - Etherscan, - NFTCollection, - Unitag, - WalletByAddress, -} +export type SearchResult = TokenSearchResult | WalletSearchResult | EtherscanSearchResult | NFTCollectionSearchResult export function extractDomain(walletName: string, type: SearchResultType): string { const index = walletName.indexOf('.') @@ -31,10 +18,7 @@ export interface SearchResultBase { searchId?: string } -export type WalletSearchResult = - | ENSAddressSearchResult - | UnitagSearchResult - | WalletByAddressSearchResult +export type WalletSearchResult = ENSAddressSearchResult | UnitagSearchResult | WalletByAddressSearchResult export interface WalletByAddressSearchResult extends SearchResultBase { type: SearchResultType.WalletByAddress diff --git a/packages/wallet/src/features/search/searchHistorySlice.ts b/packages/wallet/src/features/search/searchHistorySlice.ts index d4ff5de746c..e9a2a22def4 100644 --- a/packages/wallet/src/features/search/searchHistorySlice.ts +++ b/packages/wallet/src/features/search/searchHistorySlice.ts @@ -1,5 +1,6 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' -import { SearchResult, SearchResultType } from 'wallet/src/features/search/SearchResult' +import { SearchResultType } from 'uniswap/src/features/search/SearchResult' +import { SearchResult } from 'wallet/src/features/search/SearchResult' const SEARCH_HISTORY_LENGTH = 5 @@ -40,10 +41,7 @@ const slice = createSlice({ state.results.unshift({ ...searchResult, searchId }) // Filter out to only uniques & keep size under SEARCH_HISTORY_LENGTH state.results = state.results - .filter( - (result, index, self) => - index === self.findIndex((value) => value.searchId === result.searchId) - ) + .filter((result, index, self) => index === self.findIndex((value) => value.searchId === result.searchId)) .slice(0, SEARCH_HISTORY_LENGTH) }, clearSearchHistory: (state) => { diff --git a/apps/mobile/src/features/telemetry/hooks.ts b/packages/wallet/src/features/telemetry/hooks.ts similarity index 83% rename from apps/mobile/src/features/telemetry/hooks.ts rename to packages/wallet/src/features/telemetry/hooks.ts index 54c508e8ea0..8d0f12cd78a 100644 --- a/apps/mobile/src/features/telemetry/hooks.ts +++ b/packages/wallet/src/features/telemetry/hooks.ts @@ -1,9 +1,11 @@ import { useEffect, useMemo } from 'react' -import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { useDispatch } from 'react-redux' import { MobileAppsFlyerEvents, MobileEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent, sendAppsFlyerEvent } from 'uniswap/src/features/telemetry/send' import { logger } from 'utilities/src/logger/logger' import { areSameDays } from 'utilities/src/time/date' +import { ONE_SECOND_MS } from 'utilities/src/time/time' +import { useInterval } from 'utilities/src/time/timing' import { useAccountList } from 'wallet/src/features/accounts/hooks' import { selectAllowAnalytics, @@ -20,9 +22,10 @@ import { } from 'wallet/src/features/telemetry/slice' import { Account, AccountType } from 'wallet/src/features/wallet/accounts/types' import { useAccounts } from 'wallet/src/features/wallet/hooks' +import { useAppSelector } from 'wallet/src/state' -export function useLastBalancesReporter(): () => void { - const dispatch = useAppDispatch() +export function useLastBalancesReporter(): void { + const dispatch = useDispatch() const accounts = useAccounts() const lastBalancesReport = useAppSelector(selectLastBalancesReport) @@ -58,19 +61,14 @@ export function useLastBalancesReporter(): () => void { // Only trigger the first time a funded wallet is detected dispatch(recordWalletFunded()) sendAppsFlyerEvent(MobileAppsFlyerEvents.WalletFunded, { sumOfFunds }).catch((error) => - logger.debug('hooks', 'useLastBalancesReporter', error) + logger.debug('hooks', 'useLastBalancesReporter', error), ) } }, [dispatch, signerAccountValues, walletIsFunded]) const reporter = (): void => { if ( - shouldReportBalances( - lastBalancesReport, - lastBalancesReportValue, - signerAccountAddresses, - signerAccountValues - ) + shouldReportBalances(lastBalancesReport, lastBalancesReportValue, signerAccountAddresses, signerAccountValues) ) { const totalBalance = signerAccountValues.reduce((a, b) => a + b, 0) @@ -84,13 +82,13 @@ export function useLastBalancesReporter(): () => void { } } - return reporter + useInterval(reporter, ONE_SECOND_MS * 15, true) } // Returns a function that checks if the app needs to send a heartbeat action to record anonymous DAU // Only logs when the user has allowing product analytics off and a heartbeat has not been sent for the user's local day -export function useHeartbeatReporter(): () => void { - const dispatch = useAppDispatch() +export function useHeartbeatReporter(): void { + const dispatch = useDispatch() const allowAnalytics = useAppSelector(selectAllowAnalytics) const lastHeartbeat = useAppSelector(selectLastHeartbeat) @@ -105,5 +103,5 @@ export function useHeartbeatReporter(): () => void { } } - return reporter + useInterval(reporter, ONE_SECOND_MS * 15, true) } diff --git a/packages/wallet/src/features/telemetry/selectors.ts b/packages/wallet/src/features/telemetry/selectors.ts index 159fdb02466..99e689d1d96 100644 --- a/packages/wallet/src/features/telemetry/selectors.ts +++ b/packages/wallet/src/features/telemetry/selectors.ts @@ -1,7 +1,6 @@ import { SharedState } from 'wallet/src/state/reducer' -export const selectLastBalancesReport = (state: SharedState): number => - state.telemetry.lastBalancesReport +export const selectLastBalancesReport = (state: SharedState): number => state.telemetry.lastBalancesReport export const selectLastBalancesReportValue = (state: SharedState): number | undefined => state.telemetry.lastBalancesReportValue diff --git a/packages/wallet/src/features/telemetry/slice.ts b/packages/wallet/src/features/telemetry/slice.ts index cf6ad0f686e..2c83df3549a 100644 --- a/packages/wallet/src/features/telemetry/slice.ts +++ b/packages/wallet/src/features/telemetry/slice.ts @@ -35,10 +35,7 @@ export const slice = createSlice({ sendAnalyticsEvent(SharedEventName.HEARTBEAT) state.lastHeartbeat = Date.now() }, - recordBalancesReport: ( - state, - { payload: { totalBalance } }: PayloadAction<{ totalBalance: number }> - ) => { + recordBalancesReport: (state, { payload: { totalBalance } }: PayloadAction<{ totalBalance: number }>) => { state.lastBalancesReport = Date.now() state.lastBalancesReportValue = totalBalance }, @@ -76,7 +73,7 @@ export function shouldReportBalances( lastBalancesReport: number | undefined, lastBalancesReportValue: number | undefined, signerAccountAddresses: string[], - signerAccountValues: number[] + signerAccountValues: number[], ): boolean { const currentBalance = signerAccountValues.reduce((a, b) => a + b, 0) @@ -87,6 +84,5 @@ export function shouldReportBalances( return validAccountInfo && (didWalletGetFunded || balanceReportDue) } -export const { recordHeartbeat, recordBalancesReport, recordWalletFunded, setAllowAnalytics } = - slice.actions +export const { recordHeartbeat, recordBalancesReport, recordWalletFunded, setAllowAnalytics } = slice.actions export const { reducer: telemetryReducer } = slice diff --git a/packages/wallet/src/features/timing/selectors.ts b/packages/wallet/src/features/timing/selectors.ts index 10d17614520..02256288b53 100644 --- a/packages/wallet/src/features/timing/selectors.ts +++ b/packages/wallet/src/features/timing/selectors.ts @@ -1,4 +1,3 @@ import { SharedState } from 'wallet/src/state/reducer' -export const selectSwapStartTimestamp = (state: SharedState): number | undefined => - state.timing.swap.startTimestamp +export const selectSwapStartTimestamp = (state: SharedState): number | undefined => state.timing.swap.startTimestamp diff --git a/packages/wallet/src/features/timing/slice.ts b/packages/wallet/src/features/timing/slice.ts index 12a2ba210d9..c1947527e84 100644 --- a/packages/wallet/src/features/timing/slice.ts +++ b/packages/wallet/src/features/timing/slice.ts @@ -20,10 +20,7 @@ export const slice = createSlice({ name: 'timing', initialState: initialTimingState, reducers: { - updateSwapStartTimestamp: ( - state, - { payload: { timestamp } }: PayloadAction<{ timestamp?: number }> - ) => { + updateSwapStartTimestamp: (state, { payload: { timestamp } }: PayloadAction<{ timestamp?: number }>) => { state.swap.startTimestamp = timestamp }, }, diff --git a/packages/wallet/src/features/tokens/dismissedWarningTokensSelector.ts b/packages/wallet/src/features/tokens/dismissedWarningTokensSelector.ts index 72d0ad2368c..783cc7e26cb 100644 --- a/packages/wallet/src/features/tokens/dismissedWarningTokensSelector.ts +++ b/packages/wallet/src/features/tokens/dismissedWarningTokensSelector.ts @@ -3,7 +3,7 @@ import type { SharedState } from 'wallet/src/state/reducer' // selectors export const dismissedWarningTokensSelector = ( - state: SharedState + state: SharedState, ): { [currencyId: string]: boolean } => state.tokens.dismissedWarningTokens diff --git a/packages/wallet/src/features/tokens/hooks.ts b/packages/wallet/src/features/tokens/hooks.ts index 56b939198bb..2b62b40116d 100644 --- a/packages/wallet/src/features/tokens/hooks.ts +++ b/packages/wallet/src/features/tokens/hooks.ts @@ -1,12 +1,12 @@ import { useMemo } from 'react' +import { getWrappedNativeAddress } from 'uniswap/src/constants/addresses' import { Chain, SearchPopularTokensQuery, useSearchPopularTokensQuery, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { UniverseChainId } from 'uniswap/src/types/chains' -import { getWrappedNativeAddress } from 'wallet/src/constants/addresses' -import { areAddressesEqual } from 'wallet/src/utils/addresses' +import { areAddressesEqual } from 'uniswap/src/utils/addresses' export type TopToken = NonNullable[0]> @@ -35,8 +35,7 @@ export function usePopularTokens(): { return } - const isWeth = - areAddressesEqual(token.address, wethAddress) && token?.chain === Chain.Ethereum + const isWeth = areAddressesEqual(token.address, wethAddress) && token?.chain === Chain.Ethereum // manually replace weth with eth given backend only returns eth data as a proxy for eth if (isWeth && eth) { diff --git a/packages/wallet/src/features/tokens/safetyHooks.ts b/packages/wallet/src/features/tokens/safetyHooks.ts index 46759ede7d5..8c34dcaa8ba 100644 --- a/packages/wallet/src/features/tokens/safetyHooks.ts +++ b/packages/wallet/src/features/tokens/safetyHooks.ts @@ -1,21 +1,18 @@ import { useCallback } from 'react' -import { ThemeKeys } from 'ui/src' -import { SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { useDispatch } from 'react-redux' import { CurrencyId } from 'uniswap/src/types/currency' import { dismissedWarningTokensSelector } from 'wallet/src/features/tokens/dismissedWarningTokensSelector' import { addDismissedWarningToken } from 'wallet/src/features/tokens/tokensSlice' -import { useAppDispatch, useAppSelector } from 'wallet/src/state' +import { useAppSelector } from 'wallet/src/state' export function useTokenWarningDismissed(currencyId: Maybe): { tokenWarningDismissed: boolean // user dismissed warning dismissWarningCallback: () => void // callback to dismiss warning } { - const dispatch = useAppDispatch() + const dispatch = useDispatch() const dismissedTokens = useAppSelector(dismissedWarningTokensSelector) - const tokenWarningDismissed = Boolean( - currencyId && dismissedTokens && dismissedTokens[currencyId] - ) + const tokenWarningDismissed = Boolean(currencyId && dismissedTokens && dismissedTokens[currencyId]) const dismissWarningCallback = useCallback(() => { if (currencyId) { @@ -28,15 +25,3 @@ export function useTokenWarningDismissed(currencyId: Maybe): { dismissWarningCallback, } } - -export function useTokenSafetyLevelColors(safetyLevel: Maybe): ThemeKeys { - switch (safetyLevel) { - case SafetyLevel.MediumWarning: - return 'DEP_accentWarning' - case SafetyLevel.StrongWarning: - return 'statusCritical' - case SafetyLevel.Blocked: - default: - return 'neutral2' - } -} diff --git a/packages/wallet/src/features/tokens/useCurrencyInfo.ts b/packages/wallet/src/features/tokens/useCurrencyInfo.ts index d5cb8a00af3..5527574bb15 100644 --- a/packages/wallet/src/features/tokens/useCurrencyInfo.ts +++ b/packages/wallet/src/features/tokens/useCurrencyInfo.ts @@ -1,12 +1,9 @@ import { useMemo } from 'react' import { useTokenQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' +import { currencyIdToContractInput, gqlTokenToCurrencyInfo } from 'uniswap/src/features/dataApi/utils' import { WalletChainId } from 'uniswap/src/types/chains' -import { - currencyIdToContractInput, - gqlTokenToCurrencyInfo, -} from 'wallet/src/features/dataApi/utils' -import { buildNativeCurrencyId, buildWrappedNativeCurrencyId } from 'wallet/src/utils/currencyId' +import { buildNativeCurrencyId, buildWrappedNativeCurrencyId } from 'uniswap/src/utils/currencyId' export function useCurrencyInfo(_currencyId?: string): Maybe { const { data } = useTokenQuery({ diff --git a/packages/wallet/src/features/transactions/InsufficientNativeTokenWarning/InsufficientNativeTokenBaseComponent.tsx b/packages/wallet/src/features/transactions/InsufficientNativeTokenWarning/InsufficientNativeTokenBaseComponent.tsx index 1be13692377..7511aa08186 100644 --- a/packages/wallet/src/features/transactions/InsufficientNativeTokenWarning/InsufficientNativeTokenBaseComponent.tsx +++ b/packages/wallet/src/features/transactions/InsufficientNativeTokenWarning/InsufficientNativeTokenBaseComponent.tsx @@ -8,22 +8,16 @@ import { useInsufficientNativeTokenWarning } from 'wallet/src/features/transacti export function InsufficientNativeTokenBaseComponent({ parsedInsufficentNativeTokenWarning, }: { - parsedInsufficentNativeTokenWarning: NonNullable< - ReturnType - > + parsedInsufficentNativeTokenWarning: NonNullable> }): JSX.Element | null { const { nativeCurrency, networkColors, networkName, flow } = parsedInsufficentNativeTokenWarning const currencySymbol = nativeCurrency.symbol - const shouldShowNetworkName = - nativeCurrency.symbol === 'ETH' && nativeCurrency.chainId !== UniverseChainId.Mainnet + const shouldShowNetworkName = nativeCurrency.symbol === 'ETH' && nativeCurrency.chainId !== UniverseChainId.Mainnet const textComponentWithNetworkColor = ( - + ) return ( @@ -33,7 +27,8 @@ export function InsufficientNativeTokenBaseComponent({ backgroundColor={isWeb ? '$surface2' : undefined} borderRadius="$rounded12" gap="$spacing8" - p={isWeb ? '$spacing16' : '$none'}> + p={isWeb ? '$spacing16' : '$none'} + > {isWeb && ( diff --git a/packages/wallet/src/features/transactions/InsufficientNativeTokenWarning/InsufficientNativeTokenWarning.native.tsx b/packages/wallet/src/features/transactions/InsufficientNativeTokenWarning/InsufficientNativeTokenWarning.native.tsx index e3aca84f54d..7beef57a349 100644 --- a/packages/wallet/src/features/transactions/InsufficientNativeTokenWarning/InsufficientNativeTokenWarning.native.tsx +++ b/packages/wallet/src/features/transactions/InsufficientNativeTokenWarning/InsufficientNativeTokenWarning.native.tsx @@ -2,11 +2,11 @@ import { useState } from 'react' import { useTranslation } from 'react-i18next' import { Flex, Text, TouchableArea } from 'ui/src' import { CurrencyLogo } from 'uniswap/src/components/CurrencyLogo/CurrencyLogo' +import { LearnMoreLink } from 'uniswap/src/components/text/LearnMoreLink' import { uniswapUrls } from 'uniswap/src/constants/urls' import { ModalName } from 'uniswap/src/features/telemetry/constants' import { UniverseChainId } from 'uniswap/src/types/chains' import { WarningModal } from 'wallet/src/components/modals/WarningModal/WarningModal' -import { LearnMoreLink } from 'wallet/src/components/text/LearnMoreLink' import { InsufficientNativeTokenBaseComponent } from 'wallet/src/features/transactions/InsufficientNativeTokenWarning/InsufficientNativeTokenBaseComponent' import type { InsufficientNativeTokenWarningProps } from 'wallet/src/features/transactions/InsufficientNativeTokenWarning/InsufficientNativeTokenWarning' import { useInsufficientNativeTokenWarning } from 'wallet/src/features/transactions/InsufficientNativeTokenWarning/useInsufficientNativeTokenWarning' @@ -32,8 +32,7 @@ export function InsufficientNativeTokenWarning({ const { modalOrTooltipMainMessage, nativeCurrency, nativeCurrencyInfo, networkName } = parsedInsufficentNativeTokenWarning - const shouldShowNetworkName = - nativeCurrency.symbol === 'ETH' && nativeCurrency.chainId !== UniverseChainId.Mainnet + const shouldShowNetworkName = nativeCurrency.symbol === 'ETH' && nativeCurrency.chainId !== UniverseChainId.Mainnet return ( <> @@ -58,7 +57,8 @@ export function InsufficientNativeTokenWarning({ tokenSymbol: nativeCurrency.symbol, }) } - onClose={(): void => setShowModal(false)}> + onClose={(): void => setShowModal(false)} + > {modalOrTooltipMainMessage} diff --git a/packages/wallet/src/features/transactions/InsufficientNativeTokenWarning/InsufficientNativeTokenWarning.tsx b/packages/wallet/src/features/transactions/InsufficientNativeTokenWarning/InsufficientNativeTokenWarning.tsx index 4fa1a5ef153..cab6c756606 100644 --- a/packages/wallet/src/features/transactions/InsufficientNativeTokenWarning/InsufficientNativeTokenWarning.tsx +++ b/packages/wallet/src/features/transactions/InsufficientNativeTokenWarning/InsufficientNativeTokenWarning.tsx @@ -8,8 +8,6 @@ export type InsufficientNativeTokenWarningProps = { gasFee: GasFeeResult } -export function InsufficientNativeTokenWarning( - _: InsufficientNativeTokenWarningProps -): JSX.Element | null { +export function InsufficientNativeTokenWarning(_: InsufficientNativeTokenWarningProps): JSX.Element | null { throw new NotImplementedError('InsufficientNativeTokenWarning') } diff --git a/packages/wallet/src/features/transactions/InsufficientNativeTokenWarning/InsufficientNativeTokenWarning.web.tsx b/packages/wallet/src/features/transactions/InsufficientNativeTokenWarning/InsufficientNativeTokenWarning.web.tsx index 9bc66f420eb..eda448d120c 100644 --- a/packages/wallet/src/features/transactions/InsufficientNativeTokenWarning/InsufficientNativeTokenWarning.web.tsx +++ b/packages/wallet/src/features/transactions/InsufficientNativeTokenWarning/InsufficientNativeTokenWarning.web.tsx @@ -1,6 +1,6 @@ import { Flex, Text, Tooltip } from 'ui/src' +import { LearnMoreLink } from 'uniswap/src/components/text/LearnMoreLink' import { uniswapUrls } from 'uniswap/src/constants/urls' -import { LearnMoreLink } from 'wallet/src/components/text/LearnMoreLink' import { InsufficientNativeTokenBaseComponent } from 'wallet/src/features/transactions/InsufficientNativeTokenWarning/InsufficientNativeTokenBaseComponent' import type { InsufficientNativeTokenWarningProps } from 'wallet/src/features/transactions/InsufficientNativeTokenWarning/InsufficientNativeTokenWarning' import { useInsufficientNativeTokenWarning } from 'wallet/src/features/transactions/InsufficientNativeTokenWarning/useInsufficientNativeTokenWarning' diff --git a/packages/wallet/src/features/transactions/InsufficientNativeTokenWarning/useInsufficientNativeTokenWarning.tsx b/packages/wallet/src/features/transactions/InsufficientNativeTokenWarning/useInsufficientNativeTokenWarning.tsx index 4b0d4aa18c9..b4a0fac2b7b 100644 --- a/packages/wallet/src/features/transactions/InsufficientNativeTokenWarning/useInsufficientNativeTokenWarning.tsx +++ b/packages/wallet/src/features/transactions/InsufficientNativeTokenWarning/useInsufficientNativeTokenWarning.tsx @@ -3,25 +3,21 @@ import { useMemo } from 'react' import { Trans } from 'react-i18next' import { Text } from 'ui/src' import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' +import { toSupportedChainId } from 'uniswap/src/features/chains/utils' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' +import { NativeCurrency } from 'uniswap/src/features/tokens/NativeCurrency' import { UniverseChainId } from 'uniswap/src/types/chains' +import { useNetworkColors } from 'uniswap/src/utils/colors' import { NumberType } from 'utilities/src/format/types' -import { toSupportedChainId } from 'wallet/src/features/chains/utils' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' -import { NativeCurrency } from 'wallet/src/features/tokens/NativeCurrency' import { useNativeCurrencyInfo } from 'wallet/src/features/tokens/useCurrencyInfo' import { type InsufficientNativeTokenWarningProps } from 'wallet/src/features/transactions/InsufficientNativeTokenWarning/InsufficientNativeTokenWarning' import { INSUFFICIENT_NATIVE_TOKEN_TEXT_VARIANT } from 'wallet/src/features/transactions/InsufficientNativeTokenWarning/constants' import { Warning, WarningLabel } from 'wallet/src/features/transactions/WarningModal/types' import { useUSDCValue } from 'wallet/src/features/transactions/swap/trade/hooks/useUSDCPrice' -import { useNetworkColors } from 'wallet/src/utils/colors' import { ValueType, getCurrencyAmount } from 'wallet/src/utils/getCurrencyAmount' -export function useInsufficientNativeTokenWarning({ - flow, - gasFee, - warnings, -}: InsufficientNativeTokenWarningProps): { +export function useInsufficientNativeTokenWarning({ flow, gasFee, warnings }: InsufficientNativeTokenWarningProps): { gasAmount: CurrencyAmount | null | undefined gasAmountFiatFormatted: string nativeCurrency: Currency @@ -46,19 +42,14 @@ export function useInsufficientNativeTokenWarning({ getCurrencyAmount({ value: gasFee.value, valueType: ValueType.Raw, - currency: nativeCurrency?.chainId - ? NativeCurrency.onChain(nativeCurrency.chainId) - : undefined, + currency: nativeCurrency?.chainId ? NativeCurrency.onChain(nativeCurrency.chainId) : undefined, }), - [gasFee.value, nativeCurrency?.chainId] + [gasFee.value, nativeCurrency?.chainId], ) const gasAmountUsd = useUSDCValue(gasAmount) - const gasAmountFiatFormatted = convertFiatAmountFormatted( - gasAmountUsd?.toExact(), - NumberType.FiatGasPrice - ) + const gasAmountFiatFormatted = convertFiatAmountFormatted(gasAmountUsd?.toExact(), NumberType.FiatGasPrice) if (!warning || !nativeCurrency || !nativeCurrencyInfo) { return null diff --git a/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/ApproveTransactionDetails.tsx b/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/ApproveTransactionDetails.tsx new file mode 100644 index 00000000000..ed0a13a6e5d --- /dev/null +++ b/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/ApproveTransactionDetails.tsx @@ -0,0 +1,70 @@ +import { SharedEventName } from '@uniswap/analytics-events' +import { useTranslation } from 'react-i18next' +import { Flex, Text, TouchableArea, isWeb } from 'ui/src' +import { iconSizes } from 'ui/src/theme' +import { CurrencyLogo } from 'uniswap/src/components/CurrencyLogo/CurrencyLogo' +import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants' +import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' +import { getSymbolDisplayText } from 'uniswap/src/utils/currency' +import { NumberType } from 'utilities/src/format/types' +import { useWalletNavigation } from 'wallet/src/contexts/WalletNavigationContext' +import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' +import { useCurrencyInfo } from 'wallet/src/features/tokens/useCurrencyInfo' +import { ApproveTransactionInfo, TransactionDetails } from 'wallet/src/features/transactions/types' +import { buildCurrencyId } from 'wallet/src/utils/currencyId' + +const INFINITE_AMOUNT = 'INF' +const ZERO_AMOUNT = '0.0' +export function ApproveTransactionDetails({ + transactionDetails, + typeInfo, + onClose, +}: { + transactionDetails: TransactionDetails + typeInfo: ApproveTransactionInfo + onClose: () => void +}): JSX.Element { + const { t } = useTranslation() + const { formatNumberOrString } = useLocalizationContext() + const { navigateToTokenDetails } = useWalletNavigation() + const currencyInfo = useCurrencyInfo(buildCurrencyId(transactionDetails.chainId, typeInfo.tokenAddress)) + + const { approvalAmount } = typeInfo + + const amount = + approvalAmount === INFINITE_AMOUNT + ? t('transaction.amount.unlimited') + : approvalAmount && approvalAmount !== ZERO_AMOUNT + ? formatNumberOrString({ value: approvalAmount, type: NumberType.TokenNonTx }) + : '' + + const symbol = getSymbolDisplayText(currencyInfo?.currency.symbol) + + const onPressToken = (): void => { + if (currencyInfo) { + sendAnalyticsEvent(SharedEventName.ELEMENT_CLICKED, { + element: ElementName.TokenItem, + modal: ModalName.TransactionDetails, + }) + + navigateToTokenDetails(currencyInfo.currencyId) + if (!isWeb) { + onClose() + } + } + } + + return ( + + + {amount} + + + + {symbol} + + + + + ) +} diff --git a/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/HeaderLogo.tsx b/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/HeaderLogo.tsx index 1fd8ed88e25..5f74b0f7046 100644 --- a/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/HeaderLogo.tsx +++ b/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/HeaderLogo.tsx @@ -1,14 +1,12 @@ import { Flex, useIsDarkMode, useSporeColors } from 'ui/src' import { ContractInteraction } from 'ui/src/components/icons' import { iconSizes } from 'ui/src/theme' +import { SplitLogo } from 'uniswap/src/components/CurrencyLogo/SplitLogo' +import { AssetType } from 'uniswap/src/entities/assets' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' import { getOptionalServiceProviderLogo } from 'uniswap/src/features/fiatOnRamp/utils' -import { - DappLogoWithWCBadge, - LogoWithTxStatus, -} from 'wallet/src/components/CurrencyLogo/LogoWithTxStatus' -import { SplitLogo } from 'wallet/src/components/CurrencyLogo/SplitLogo' -import { AssetType } from 'wallet/src/entities/assets' +import { buildCurrencyId } from 'uniswap/src/utils/currencyId' +import { DappLogoWithWCBadge, LogoWithTxStatus } from 'wallet/src/components/CurrencyLogo/LogoWithTxStatus' import { useCurrencyInfo, useNativeCurrencyInfo, @@ -21,6 +19,8 @@ import { isNFTApproveTransactionInfo, isNFTMintTransactionInfo, isNFTTradeTransactionInfo, + isOnRampPurchaseTransactionInfo, + isOnRampTransferTransactionInfo, isReceiveTokenTransactionInfo, isSendTokenTransactionInfo, isSwapTransactionInfo, @@ -33,13 +33,15 @@ import { NFTApproveTransactionInfo, NFTMintTransactionInfo, NFTTradeTransactionInfo, + OnRampPurchaseInfo, + OnRampTransferInfo, ReceiveTokenTransactionInfo, SendTokenTransactionInfo, TransactionDetails, + UnknownTransactionInfo, WCConfirmInfo, WrapTransactionInfo, } from 'wallet/src/features/transactions/types' -import { buildCurrencyId } from 'wallet/src/utils/currencyId' const TXN_DETAILS_ICON_SIZE = iconSizes.icon40 @@ -75,7 +77,7 @@ const getLogoWithTxStatus = ({ /> ) -export const HeaderLogo = ({ transactionDetails }: HeaderLogoProps): JSX.Element => { +export function HeaderLogo({ transactionDetails }: HeaderLogoProps): JSX.Element { const { typeInfo } = transactionDetails const getHeaderLogoComponent = (): JSX.Element => { @@ -99,8 +101,10 @@ export const HeaderLogo = ({ transactionDetails }: HeaderLogoProps): JSX.Element return } else if (isWrapTransactionInfo(typeInfo)) { return + } else if (isOnRampPurchaseTransactionInfo(typeInfo) || isOnRampTransferTransactionInfo(typeInfo)) { + return } else { - return + return } } @@ -111,10 +115,10 @@ interface SpecificHeaderLogoProps extends HeaderLogoProps { typeInfo: T } -const SwapHeaderLogo = ({ +function SwapHeaderLogo({ transactionDetails, typeInfo, -}: SpecificHeaderLogoProps): JSX.Element => { +}: SpecificHeaderLogoProps): JSX.Element { const inputCurrency = useCurrencyInfo(typeInfo.inputCurrencyId) const outputCurrency = useCurrencyInfo(typeInfo.outputCurrencyId) @@ -128,13 +132,11 @@ const SwapHeaderLogo = ({ ) } -const ApproveHeaderLogo = ({ +function ApproveHeaderLogo({ transactionDetails, typeInfo, -}: SpecificHeaderLogoProps): JSX.Element => { - const currencyInfo = useCurrencyInfo( - buildCurrencyId(transactionDetails.chainId, typeInfo.tokenAddress) - ) +}: SpecificHeaderLogoProps): JSX.Element { + const currencyInfo = useCurrencyInfo(buildCurrencyId(transactionDetails.chainId, typeInfo.tokenAddress)) return getLogoWithTxStatus({ assetType: AssetType.Currency, currencyInfo, @@ -142,22 +144,16 @@ const ApproveHeaderLogo = ({ }) } -const FiatPurchaseHeaderLogo = ({ +function FiatPurchaseHeaderLogo({ transactionDetails, typeInfo, -}: SpecificHeaderLogoProps): JSX.Element => { +}: SpecificHeaderLogoProps): JSX.Element { const outputCurrencyInfo = useCurrencyInfo( typeInfo.outputCurrency?.metadata.contractAddress - ? buildCurrencyId( - transactionDetails.chainId, - typeInfo.outputCurrency?.metadata.contractAddress - ) - : undefined - ) - const serviceProviderLogoUrl = getOptionalServiceProviderLogo( - typeInfo.serviceProviderLogo, - useIsDarkMode() + ? buildCurrencyId(transactionDetails.chainId, typeInfo.outputCurrency?.metadata.contractAddress) + : undefined, ) + const serviceProviderLogoUrl = getOptionalServiceProviderLogo(typeInfo.serviceProviderLogo, useIsDarkMode()) return getLogoWithTxStatus({ assetType: AssetType.Currency, transactionDetails, @@ -167,54 +163,58 @@ const FiatPurchaseHeaderLogo = ({ }) } -const TokenTransferHeaderLogo = ({ +function TokenTransferHeaderLogo({ transactionDetails, typeInfo, -}: SpecificHeaderLogoProps< - ReceiveTokenTransactionInfo | SendTokenTransactionInfo ->): JSX.Element => { +}: SpecificHeaderLogoProps): JSX.Element { const currencyInfo = useCurrencyInfo( typeInfo.assetType === AssetType.Currency ? buildCurrencyId(transactionDetails.chainId, typeInfo.tokenAddress) - : undefined + : undefined, ) return getLogoWithTxStatus({ assetType: typeInfo.assetType, currencyInfo, transactionDetails, - nftImageUrl: - typeInfo.assetType !== AssetType.Currency ? typeInfo.nftSummaryInfo?.imageURL : undefined, + nftImageUrl: typeInfo.assetType !== AssetType.Currency ? typeInfo.nftSummaryInfo?.imageURL : undefined, }) } -const NFTHeaderLogo = ({ +function OnRampHeaderLogo({ transactionDetails, typeInfo, -}: SpecificHeaderLogoProps< - NFTApproveTransactionInfo | NFTMintTransactionInfo | NFTTradeTransactionInfo ->): JSX.Element => - getLogoWithTxStatus({ - assetType: AssetType.ERC721, +}: SpecificHeaderLogoProps): JSX.Element { + const currencyInfo = useCurrencyInfo(buildCurrencyId(transactionDetails.chainId, typeInfo.destinationTokenAddress)) + return getLogoWithTxStatus({ + assetType: AssetType.Currency, + currencyInfo, transactionDetails, - nftImageUrl: typeInfo.nftSummaryInfo.imageURL, }) +} -const WCConfirmHeaderLogo = ({ +function NFTHeaderLogo({ transactionDetails, typeInfo, -}: SpecificHeaderLogoProps): JSX.Element => ( - -) +}: SpecificHeaderLogoProps): JSX.Element { + return getLogoWithTxStatus({ + assetType: AssetType.ERC721, + transactionDetails, + nftImageUrl: typeInfo.nftSummaryInfo.imageURL, + }) +} -const WrapHeaderLogo = ({ - transactionDetails, - typeInfo, -}: SpecificHeaderLogoProps): JSX.Element => { +function WCConfirmHeaderLogo({ transactionDetails, typeInfo }: SpecificHeaderLogoProps): JSX.Element { + return ( + + ) +} + +function WrapHeaderLogo({ transactionDetails, typeInfo }: SpecificHeaderLogoProps): JSX.Element { const unwrapped = typeInfo.unwrapped const nativeCurrencyInfo = useNativeCurrencyInfo(transactionDetails.chainId) const wrappedCurrencyInfo = useWrappedNativeCurrencyInfo(transactionDetails.chainId) @@ -229,7 +229,21 @@ const WrapHeaderLogo = ({ ) } -const UnknownHeaderLogo = (): JSX.Element => { +function UnknownHeaderLogo({ + transactionDetails, + typeInfo, +}: SpecificHeaderLogoProps): JSX.Element { const colors = useSporeColors() - return + return typeInfo.dappInfo?.icon ? ( + + ) : ( + + ) } diff --git a/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/NftTransactionDetails.test.tsx b/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/NftTransactionDetails.test.tsx new file mode 100644 index 00000000000..86c96185d75 --- /dev/null +++ b/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/NftTransactionDetails.test.tsx @@ -0,0 +1,56 @@ +import { NftTransactionDetails } from 'wallet/src/features/transactions/SummaryCards/DetailsModal/NftTransactionDetails' +import { NFTMintTransactionInfo, TransactionDetails } from 'wallet/src/features/transactions/types' +import { ACCOUNT, preloadedSharedState } from 'wallet/src/test/fixtures' +import { render } from 'wallet/src/test/test-utils' + +const preloadedState = preloadedSharedState({ account: ACCOUNT }) +const nftTypeInfo = { + type: 'nft-mint', + nftSummaryInfo: { + tokenId: '9920dbad-ff24-47c8-814a-094566fc45ff', + name: 'voluptates repudiandae aliquid', + collectionName: 'inventore qui fugiat', + imageURL: 'https://loremflickr.com/640/480', + address: '0xaea14f6cccfeae34fea11d9a2ca6aabb112e8b8d', + }, +} as NFTMintTransactionInfo +const mockTransaction = { + id: '9920dbad-ff24-47c8-814a-094566fc45ff', + chainId: 81457, + routing: 'CLASSIC', + from: '0xee814caea14f6cccfeae34fea11d9a2ca6aabb11', + typeInfo: nftTypeInfo, + status: 'confirmed', + addedTime: 1719911758204, + options: { request: {} }, + hash: 'b568a9e9-bbe7-42fc-ab00-5070186c0600', + receipt: { + transactionIndex: 29529, + blockNumber: 17489, + blockHash: 'dfd3ad45-78e7-4124-90f2-92758b4499ba', + confirmedTime: 1719946653408, + confirmations: 57408, + gasUsed: 27844, + effectiveGasPrice: 2941, + }, +} as TransactionDetails + +// Mock the ImageUri component +jest.mock('wallet/src/features/images/ImageUri', () => ({ + ImageUri: jest.fn(() => null), +})) + +describe('NftTransactionDetails Component', () => { + it('renders NftTransactionDetails without error', () => { + const onClose = jest.fn() + + const tree = render( + , + { + preloadedState, + }, + ) + + expect(tree).toMatchSnapshot() + }) +}) diff --git a/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/NftTransactionDetails.tsx b/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/NftTransactionDetails.tsx new file mode 100644 index 00000000000..629c8b9df4f --- /dev/null +++ b/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/NftTransactionDetails.tsx @@ -0,0 +1,109 @@ +import { Flex, Text, TouchableArea, isWeb } from 'ui/src' +import { RotatableChevron } from 'ui/src/components/icons' +import { iconSizes } from 'ui/src/theme' +import { UniverseChainId } from 'uniswap/src/types/chains' +import { useWalletNavigation } from 'wallet/src/contexts/WalletNavigationContext' +import { NFTViewer } from 'wallet/src/features/images/NFTViewer' +import { + NFTApproveTransactionInfo, + NFTMintTransactionInfo, + NFTSummaryInfo, + NFTTradeTransactionInfo, + ReceiveTokenTransactionInfo, + SendTokenTransactionInfo, + TransactionDetails, +} from 'wallet/src/features/transactions/types' + +const MAX_NFT_IMAGE_HEIGHT = 375 + +export function NftTransactionDetails({ + transactionDetails, + typeInfo, + onClose, +}: { + transactionDetails: TransactionDetails + typeInfo: + | ReceiveTokenTransactionInfo + | SendTokenTransactionInfo + | NFTTradeTransactionInfo + | NFTMintTransactionInfo + | NFTApproveTransactionInfo + onClose: () => void +}): JSX.Element { + if (!typeInfo.nftSummaryInfo) { + return <> + } + + return ( + + ) +} + +export function NftTransactionContent({ + chainId, + nftSummaryInfo, + onClose, +}: { + chainId: UniverseChainId + nftSummaryInfo: NFTSummaryInfo + onClose: () => void +}): JSX.Element { + const { navigateToNftCollection, navigateToNftDetails } = useWalletNavigation() + + const onPressNft = (): void => { + navigateToNftDetails({ + address: nftSummaryInfo.address, + tokenId: nftSummaryInfo.tokenId, + }) + onClose() + } + + const onPressCollection = (): void => { + // Collection should not be clickable on L2s + if (chainId === UniverseChainId.Mainnet) { + navigateToNftCollection({ collectionAddress: nftSummaryInfo.address }) + onClose() + } + } + + const disableOnPressNftItem = isWeb + const disableOnPressNftCollection = isWeb || chainId !== UniverseChainId.Mainnet + + return ( + + + + + + {nftSummaryInfo.name} + + + + {nftSummaryInfo.collectionName} + + {!disableOnPressNftCollection && ( + + )} + + + + + ) +} diff --git a/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/OnRampTransactionDetails.tsx b/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/OnRampTransactionDetails.tsx new file mode 100644 index 00000000000..f5f7029a80a --- /dev/null +++ b/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/OnRampTransactionDetails.tsx @@ -0,0 +1,47 @@ +import { getSymbolDisplayText } from 'uniswap/src/utils/currency' +import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' +import { useCurrencyInfo } from 'wallet/src/features/tokens/useCurrencyInfo' +import { CurrencyTransferContent } from 'wallet/src/features/transactions/SummaryCards/DetailsModal/TransferTransactionDetails' +import { useFormattedCurrencyAmountAndUSDValue } from 'wallet/src/features/transactions/SummaryCards/DetailsModal/utils' +import { + OnRampPurchaseInfo, + OnRampTransferInfo, + TransactionDetails, + TransactionType, +} from 'wallet/src/features/transactions/types' +import { buildCurrencyId } from 'wallet/src/utils/currencyId' +import { ValueType } from 'wallet/src/utils/getCurrencyAmount' + +export function OnRampTransactionDetails({ + transactionDetails, + typeInfo, + onClose, +}: { + transactionDetails: TransactionDetails + typeInfo: OnRampTransferInfo | OnRampPurchaseInfo + onClose: () => void +}): JSX.Element { + const formatter = useLocalizationContext() + const currencyInfo = useCurrencyInfo(buildCurrencyId(transactionDetails.chainId, typeInfo.destinationTokenAddress)) + + const { amount, value } = useFormattedCurrencyAmountAndUSDValue({ + currency: currencyInfo?.currency, + currencyAmountRaw: typeInfo.destinationTokenAmount.toString(), + formatter, + isApproximateAmount: false, + valueType: ValueType.Exact, + }) + const symbol = getSymbolDisplayText(currencyInfo?.currency.symbol) + + const tokenAmountWithSymbol = symbol ? amount + ' ' + symbol : amount // Prevents 'undefined' from being displayed + + return ( + + ) +} diff --git a/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/SwapTransactionDetails.test.tsx b/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/SwapTransactionDetails.test.tsx new file mode 100644 index 00000000000..1ad288058ff --- /dev/null +++ b/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/SwapTransactionDetails.test.tsx @@ -0,0 +1,71 @@ +import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' +import { + ARBITRUM_DAI_CURRENCY_INFO, + BASE_CURRENCY, + ETH_CURRENCY_INFO, + OPTIMISM_CURRENCY, + POLYGON_CURRENCY, + currencyInfo, +} from 'uniswap/src/test/fixtures' +import { SwapTransactionDetails } from 'wallet/src/features/transactions/SummaryCards/DetailsModal/SwapTransactionDetails' +import { SwapTypeTransactionInfo } from 'wallet/src/features/transactions/SummaryCards/DetailsModal/types' +import { ACCOUNT, preloadedSharedState } from 'wallet/src/test/fixtures' +import { render } from 'wallet/src/test/test-utils' + +// Function to set up mocks +const getCurrencyInfoForChain = (chainId: number): CurrencyInfo => { + switch (chainId) { + case 1: // Mainnet + return ETH_CURRENCY_INFO + case 42161: // Arbitrum One + return ARBITRUM_DAI_CURRENCY_INFO + case 8453: // Base + return currencyInfo({ nativeCurrency: BASE_CURRENCY }) + case 10: // Optimism + return currencyInfo({ nativeCurrency: OPTIMISM_CURRENCY }) + case 137: // Polygon + return currencyInfo({ nativeCurrency: POLYGON_CURRENCY }) + default: + return ETH_CURRENCY_INFO // fallback to ETH + } +} + +jest.mock('wallet/src/features/tokens/useCurrencyInfo', () => ({ + useCurrencyInfo: (currencyIdString: string | undefined): Maybe => { + if (!currencyIdString) { + return null + } + const [, chainIdStr] = currencyIdString.split(':') + if (!chainIdStr) { + return null + } + const chainId = parseInt(chainIdStr, 10) + return getCurrencyInfoForChain(chainId) + }, +})) + +jest.mock('uniswap/src/features/gating/hooks', () => ({ + useDynamicConfigValue: jest.fn().mockReturnValue(1000), + useFeatureFlag: jest.fn().mockReturnValue(true), +})) + +const preloadedState = preloadedSharedState({ account: ACCOUNT }) +const swapTypeInfo = { + type: 'swap', + inputCurrencyId: '9920dbad-ff24-47c8-814a-094566fc45ff', + outputCurrencyId: 'ee612dba-c03f-44dd-9be7-e23fec01e671', + inputCurrencyAmountRaw: '83116', + outputCurrencyAmountRaw: '77261', +} as SwapTypeTransactionInfo + +describe('SwapTransactionDetails Component', () => { + it('renders SwapTransactionDetails without error', () => { + const onClose = jest.fn() + + const tree = render(, { + preloadedState, + }) + + expect(tree).toMatchSnapshot() + }) +}) diff --git a/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/SwapTransactionDetails.tsx b/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/SwapTransactionDetails.tsx index dc0157d1543..bb6b49de01d 100644 --- a/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/SwapTransactionDetails.tsx +++ b/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/SwapTransactionDetails.tsx @@ -1,9 +1,14 @@ +import { SharedEventName } from '@uniswap/analytics-events' import { TradeType } from '@uniswap/sdk-core' -import { Flex, Text, useSporeColors } from 'ui/src' +import { Flex, Text, TouchableArea, isWeb, useSporeColors } from 'ui/src' import { iconSizes } from 'ui/src/theme' import { CurrencyLogo } from 'uniswap/src/components/CurrencyLogo/CurrencyLogo' +import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' +import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants' +import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { getSymbolDisplayText } from 'uniswap/src/utils/currency' import { Arrow } from 'wallet/src/components/icons/Arrow' +import { useWalletNavigation } from 'wallet/src/contexts/WalletNavigationContext' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' import { useCurrencyInfo } from 'wallet/src/features/tokens/useCurrencyInfo' import { SwapTypeTransactionInfo } from 'wallet/src/features/transactions/SummaryCards/DetailsModal/types' @@ -11,18 +16,58 @@ import { useFormattedCurrencyAmountAndUSDValue } from 'wallet/src/features/trans import { getAmountsFromTrade } from 'wallet/src/features/transactions/getAmountsFromTrade' import { isConfirmedSwapTypeInfo } from 'wallet/src/features/transactions/types' -export const SwapTransactionDetails = ({ +export function SwapTransactionDetails({ typeInfo, + onClose, + disableClick, }: { typeInfo: SwapTypeTransactionInfo -}): JSX.Element => { - const colors = useSporeColors() - const formatter = useLocalizationContext() + onClose?: () => void + disableClick?: boolean +}): JSX.Element { const inputCurrency = useCurrencyInfo(typeInfo.inputCurrencyId) const outputCurrency = useCurrencyInfo(typeInfo.outputCurrencyId) const isConfirmed = isConfirmedSwapTypeInfo(typeInfo) const { inputCurrencyAmountRaw, outputCurrencyAmountRaw } = getAmountsFromTrade(typeInfo) + + return ( + + ) +} + +export function SwapTransactionContent({ + inputCurrency, + outputCurrency, + isConfirmed, + inputCurrencyAmountRaw, + outputCurrencyAmountRaw, + tradeType, + onClose, + disableClick, +}: { + inputCurrency: Maybe + outputCurrency: Maybe + isConfirmed: boolean + inputCurrencyAmountRaw: string + outputCurrencyAmountRaw: string + tradeType?: TradeType + onClose?: () => void + disableClick?: boolean +}): JSX.Element { + const colors = useSporeColors() + const formatter = useLocalizationContext() + const { navigateToTokenDetails } = useWalletNavigation() + const { tilde: inputTilde, amount: inputAmount, @@ -31,7 +76,7 @@ export const SwapTransactionDetails = ({ currency: inputCurrency?.currency, currencyAmountRaw: inputCurrencyAmountRaw, formatter, - isApproximateAmount: isConfirmed ? false : typeInfo.tradeType === TradeType.EXACT_OUTPUT, + isApproximateAmount: isConfirmed ? false : tradeType === TradeType.EXACT_OUTPUT, }) const { tilde: outputTilde, @@ -41,40 +86,75 @@ export const SwapTransactionDetails = ({ currency: outputCurrency?.currency, currencyAmountRaw: outputCurrencyAmountRaw, formatter, - isApproximateAmount: isConfirmed ? false : typeInfo.tradeType === TradeType.EXACT_INPUT, + isApproximateAmount: isConfirmed ? false : tradeType === TradeType.EXACT_INPUT, }) const inputSymbol = getSymbolDisplayText(inputCurrency?.currency.symbol) const outputSymbol = getSymbolDisplayText(outputCurrency?.currency.symbol) + const onPressInputToken = (): void => { + if (inputCurrency) { + sendAnalyticsEvent(SharedEventName.ELEMENT_CLICKED, { + element: ElementName.TokenItem, + modal: ModalName.TransactionDetails, + }) + + navigateToTokenDetails(inputCurrency.currencyId) + if (!isWeb) { + onClose?.() + } + } + } + + const onPressOutputToken = (): void => { + if (outputCurrency) { + sendAnalyticsEvent(SharedEventName.ELEMENT_CLICKED, { + element: ElementName.TokenItem, + modal: ModalName.TransactionDetails, + }) + + navigateToTokenDetails(outputCurrency.currencyId) + if (!isWeb) { + onClose?.() + } + } + } + return ( - - - - - {inputTilde} - {inputAmount} {inputSymbol} - - - {inputValue} - + + + + + + {inputTilde} + {inputAmount} {inputSymbol} + + + {inputValue} + + + - - + - - - - {outputTilde} - {outputAmount} {outputSymbol} - - - {outputValue} - + + + + + {outputTilde} + {outputAmount} {outputSymbol} + + + {outputValue} + + + - - + ) } diff --git a/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/TransactionDetailsInfoRows.tsx b/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/TransactionDetailsInfoRows.tsx new file mode 100644 index 00000000000..0572c2458fa --- /dev/null +++ b/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/TransactionDetailsInfoRows.tsx @@ -0,0 +1,233 @@ +import { PropsWithChildren } from 'react' +import { useTranslation } from 'react-i18next' +import { useDispatch } from 'react-redux' +import { + Flex, + Text, + TouchableArea, + UniswapXText, + UniversalImage, + UniversalImageResizeMode, + useIsDarkMode, +} from 'ui/src' +import { CopyAlt, ExternalLink, UniswapX, Unitag } from 'ui/src/components/icons' +import { borderRadii, iconSizes } from 'ui/src/theme' +import { NetworkLogo } from 'uniswap/src/components/CurrencyLogo/NetworkLogo' +import { useUnitagByAddress } from 'uniswap/src/features/unitags/hooks' +import { UniverseChainId } from 'uniswap/src/types/chains' +import { setClipboard } from 'uniswap/src/utils/clipboard' +import { openUri } from 'uniswap/src/utils/linking' +import { shortenAddress } from 'utilities/src/addresses' +import { useENS } from 'wallet/src/features/ens/useENS' +import { pushNotification } from 'wallet/src/features/notifications/slice' +import { AppNotificationType, CopyNotificationType } from 'wallet/src/features/notifications/types' +import { useNetworkFee } from 'wallet/src/features/transactions/SummaryCards/DetailsModal/hooks' +import { shortenHash } from 'wallet/src/features/transactions/SummaryCards/DetailsModal/utils' +import { ContentRow } from 'wallet/src/features/transactions/TransactionRequest/ContentRow' +import { isUniswapX } from 'wallet/src/features/transactions/swap/trade/utils' +import { TransactionDetails, TransactionType } from 'wallet/src/features/transactions/types' +import { ExplorerDataType, getExplorerLink } from 'wallet/src/utils/linking' + +export function TransactionDetailsInfoRows({ + transactionDetails, +}: { + transactionDetails: TransactionDetails +}): JSX.Element { + const rows = useTransactionDetailsInfoRows(transactionDetails) + + return ( + + {rows} + + ) +} + +export function useTransactionDetailsInfoRows(transactionDetails: TransactionDetails): JSX.Element[] { + const { t } = useTranslation() + const isDarkMode = useIsDarkMode() + + const { typeInfo } = transactionDetails + + const defaultRows = [ + , + , + ] + const specificRows: JSX.Element[] = [] + + switch (typeInfo.type) { + case TransactionType.Approve: + case TransactionType.NFTApprove: + case TransactionType.NFTMint: + if (typeInfo.dappInfo && typeInfo.dappInfo.name) { + specificRows.push() + } + break + case TransactionType.WCConfirm: + specificRows.push() + break + case TransactionType.Receive: + specificRows.push() + break + case TransactionType.Send: + specificRows.push() + break + case TransactionType.OnRampPurchase: + case TransactionType.OnRampTransfer: + specificRows.push( + + + {typeInfo.serviceProvider.name} + , + ) + break + case TransactionType.Swap: + case TransactionType.Wrap: + case TransactionType.FiatPurchase: + case TransactionType.NFTTrade: + // For now, these cases don't add any specific rows + break + + case TransactionType.Unknown: + if (typeInfo.dappInfo) { + if (typeInfo.dappInfo.name) { + specificRows.push( + , + ) + } + specificRows.push( + + {shortenAddress(typeInfo.dappInfo.address)} + => { + if (typeInfo.dappInfo?.address) { + await openUri( + getExplorerLink(transactionDetails.chainId, typeInfo.dappInfo.address, ExplorerDataType.ADDRESS), + ) + } + }} + > + + + , + ) + } + break + default: + break + } + + // Combine specific rows and default rows + // In the future, you can modify this logic to omit or change default rows for specific types + return [...specificRows, ...defaultRows] +} + +function NetworkFeeRow({ transactionDetails }: { transactionDetails: TransactionDetails }): JSX.Element { + const { t } = useTranslation() + const { value: networkFeeValue } = useNetworkFee(transactionDetails) + + const Logo = isUniswapX(transactionDetails) ? UniswapX : NetworkLogo + const GasText = isUniswapX(transactionDetails) ? UniswapXText : Text + return ( + + + {networkFeeValue} + + ) +} + +function TransactionHashRow({ transactionDetails }: { transactionDetails: TransactionDetails }): JSX.Element | null { + const { hash } = transactionDetails + const { t } = useTranslation() + const dispatch = useDispatch() + + if (!hash && isUniswapX(transactionDetails)) { + return null + } + + const onPressCopy = async (): Promise => { + if (!hash) { + return + } + + await setClipboard(hash) + dispatch( + pushNotification({ + type: AppNotificationType.Copied, + copyType: CopyNotificationType.TransactionId, + }), + ) + } + + return ( + + {shortenHash(hash)} + + + + + ) +} + +function InfoRow({ + label, + children, +}: PropsWithChildren & { + label: string +}): JSX.Element { + return ( + + + {children} + + + ) +} + +function DappInfoRow({ name, iconUrl }: { name: string; iconUrl?: string | null }): JSX.Element { + const { t } = useTranslation() + return ( + + {iconUrl && ( + + )} + {name} + + ) +} + +function TransactionParticipantRow({ address, isSend = false }: { address: string; isSend?: boolean }): JSX.Element { + const { t } = useTranslation() + const { name: ensName } = useENS(UniverseChainId.Mainnet, address, true) + const { unitag } = useUnitagByAddress(address) + const personDisplayName = unitag?.username ?? ensName ?? shortenAddress(address) + return ( + + {personDisplayName} + {unitag?.username && } + + ) +} diff --git a/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/TransactionDetailsModal.test.tsx b/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/TransactionDetailsModal.test.tsx new file mode 100644 index 00000000000..60fc542241b --- /dev/null +++ b/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/TransactionDetailsModal.test.tsx @@ -0,0 +1,121 @@ +import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' +import { + ARBITRUM_DAI_CURRENCY_INFO, + BASE_CURRENCY, + ETH_CURRENCY_INFO, + OPTIMISM_CURRENCY, + POLYGON_CURRENCY, + currencyInfo, +} from 'uniswap/src/test/fixtures' +import { TransactionDetailsInfoRows } from 'wallet/src/features/transactions/SummaryCards/DetailsModal/TransactionDetailsInfoRows' +import { + TransactionDetailsContent, + TransactionDetailsHeader, +} from 'wallet/src/features/transactions/SummaryCards/DetailsModal/TransactionDetailsModal' +import { TransactionDetails } from 'wallet/src/features/transactions/types' +import { ACCOUNT, preloadedSharedState } from 'wallet/src/test/fixtures' +import { render } from 'wallet/src/test/test-utils' + +const preloadedState = preloadedSharedState({ account: ACCOUNT }) +const mockTransaction = { + id: '9920dbad-ff24-47c8-814a-094566fc45ff', + chainId: 81457, + routing: 'CLASSIC', + from: '0xee814caea14f6cccfeae34fea11d9a2ca6aabb11', + typeInfo: { + type: 'approve', + tokenAddress: '0x2e8b8dafe7faa3aa2bbcd27cda50ebcdfbd8710c', + spender: '0xf097e7bed97db1bccd9b067a564aca3d4e5da1f4', + }, + status: 'confirmed', + addedTime: 1719911758204, + options: { request: {} }, + hash: 'b568a9e9-bbe7-42fc-ab00-5070186c0600', + receipt: { + transactionIndex: 29529, + blockNumber: 17489, + blockHash: 'dfd3ad45-78e7-4124-90f2-92758b4499ba', + confirmedTime: 1719946653408, + confirmations: 57408, + gasUsed: 27844, + effectiveGasPrice: 2941, + }, +} as TransactionDetails + +// Function to set up mocks +const getCurrencyInfoForChain = (chainId: number): CurrencyInfo => { + switch (chainId) { + case 1: // Mainnet + return ETH_CURRENCY_INFO + case 42161: // Arbitrum One + return ARBITRUM_DAI_CURRENCY_INFO + case 8453: // Base + return currencyInfo({ nativeCurrency: BASE_CURRENCY }) + case 10: // Optimism + return currencyInfo({ nativeCurrency: OPTIMISM_CURRENCY }) + case 137: // Polygon + return currencyInfo({ nativeCurrency: POLYGON_CURRENCY }) + default: + return ETH_CURRENCY_INFO // fallback to ETH + } +} + +jest.mock('wallet/src/features/tokens/useCurrencyInfo', () => ({ + useCurrencyInfo: (currencyIdString: string | undefined): Maybe => { + if (!currencyIdString) { + return null + } + const [, chainIdStr] = currencyIdString.split(':') + if (!chainIdStr) { + return null + } + const chainId = parseInt(chainIdStr, 10) + return getCurrencyInfoForChain(chainId) + }, +})) + +jest.mock('uniswap/src/features/gating/hooks', () => ({ + useDynamicConfigValue: jest.fn().mockReturnValue(1000), + useFeatureFlag: jest.fn().mockReturnValue(true), +})) + +jest.mock('wallet/src/features/language/localizedDayjs', () => ({ + useFormattedDateTime: jest.fn(() => 'January 1, 2023 12:00 AM'), + FORMAT_DATE_TIME_MEDIUM: 'MMMM D, YYYY h:mm A', +})) + +describe('TransactionDetails Components', () => { + it('renders TransactionDetailsHeader without error', () => { + const transactionActions = { + openActionsModal: jest.fn(), + openCancelModal: jest.fn(), + renderModals: jest.fn(), + menuItems: [], + } + + const tree = render( + , + { preloadedState }, + ) + + expect(tree).toMatchSnapshot() + }) + + it('renders TransactionDetailsContent without error', () => { + const onClose = jest.fn() + + const tree = render(, { + preloadedState, + }) + + expect(tree).toMatchSnapshot() + }) + + it('renders TransactionDetailsInfoRows without error', () => { + const tree = render(, { + preloadedState, + }) + + expect(tree).toMatchSnapshot() + }) +}) diff --git a/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/TransactionDetailsModal.tsx b/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/TransactionDetailsModal.tsx index b10609d8b23..c38cadab6f1 100644 --- a/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/TransactionDetailsModal.tsx +++ b/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/TransactionDetailsModal.tsx @@ -1,42 +1,42 @@ import dayjs from 'dayjs' import { useTranslation } from 'react-i18next' -import { Button, Flex, Separator, Text, TouchableArea } from 'ui/src' -import { CopyAlt, TripleDots } from 'ui/src/components/icons' -import { iconSizes } from 'ui/src/theme' -import { NetworkLogo } from 'uniswap/src/components/CurrencyLogo/NetworkLogo' +import { Button, ContextMenu, Flex, Separator, Text, TouchableArea, isWeb } from 'ui/src' +import { TripleDots, UniswapX } from 'ui/src/components/icons' import { BottomSheetModal } from 'uniswap/src/components/modals/BottomSheetModal' +import { AssetType } from 'uniswap/src/entities/assets' import { ModalName } from 'uniswap/src/features/telemetry/constants' +import { Routing } from 'wallet/src/data/tradingApi/__generated__/index' import { AuthTrigger } from 'wallet/src/features/auth/types' -import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' -import { - FORMAT_DATE_TIME_MEDIUM, - useFormattedDateTime, -} from 'wallet/src/features/language/localizedDayjs' -import { pushNotification } from 'wallet/src/features/notifications/slice' -import { AppNotificationType, CopyNotificationType } from 'wallet/src/features/notifications/types' -import { useCurrencyInfo } from 'wallet/src/features/tokens/useCurrencyInfo' +import { FORMAT_DATE_TIME_MEDIUM, useFormattedDateTime } from 'wallet/src/features/language/localizedDayjs' +import { ApproveTransactionDetails } from 'wallet/src/features/transactions/SummaryCards/DetailsModal/ApproveTransactionDetails' import { HeaderLogo } from 'wallet/src/features/transactions/SummaryCards/DetailsModal/HeaderLogo' +import { NftTransactionDetails } from 'wallet/src/features/transactions/SummaryCards/DetailsModal/NftTransactionDetails' +import { OnRampTransactionDetails } from 'wallet/src/features/transactions/SummaryCards/DetailsModal/OnRampTransactionDetails' import { SwapTransactionDetails } from 'wallet/src/features/transactions/SummaryCards/DetailsModal/SwapTransactionDetails' +import { TransactionDetailsInfoRows } from 'wallet/src/features/transactions/SummaryCards/DetailsModal/TransactionDetailsInfoRows' +import { TransferTransactionDetails } from 'wallet/src/features/transactions/SummaryCards/DetailsModal/TransferTransactionDetails' +import { WrapTransactionDetails } from 'wallet/src/features/transactions/SummaryCards/DetailsModal/WrapTransactionDetails' import { isApproveTransactionInfo, isFiatPurchaseTransactionInfo, isNFTApproveTransactionInfo, isNFTMintTransactionInfo, isNFTTradeTransactionInfo, + isOnRampPurchaseTransactionInfo, + isOnRampTransferTransactionInfo, isReceiveTokenTransactionInfo, isSendTokenTransactionInfo, isSwapTransactionInfo, + isUnknownTransactionInfo, isWCConfirmTransactionInfo, isWrapTransactionInfo, } from 'wallet/src/features/transactions/SummaryCards/DetailsModal/types' -import { useTransactionActionsCancelModals } from 'wallet/src/features/transactions/SummaryCards/DetailsModal/useTransactionActionsCancelModals' -import { useFormattedCurrencyAmountAndUSDValue } from 'wallet/src/features/transactions/SummaryCards/DetailsModal/utils' +import { useTransactionActions } from 'wallet/src/features/transactions/SummaryCards/DetailsModal/useTransactionActions' import { getTransactionSummaryTitle } from 'wallet/src/features/transactions/SummaryCards/utils' -import { TransactionDetails } from 'wallet/src/features/transactions/types' -import { useAppDispatch } from 'wallet/src/state' -import { setClipboard } from 'wallet/src/utils/clipboard' -import { buildCurrencyId } from 'wallet/src/utils/currencyId' -import { ValueType } from 'wallet/src/utils/getCurrencyAmount' +import { TransactionDetails, TransactionTypeInfo } from 'wallet/src/features/transactions/types' +import { getIsCancelable } from 'wallet/src/features/transactions/utils' +import { AccountType } from 'wallet/src/features/wallet/accounts/types' +import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks' type TransactionDetailsModalProps = { authTrigger?: AuthTrigger @@ -44,183 +44,170 @@ type TransactionDetailsModalProps = { transactionDetails: TransactionDetails } -const TransactionDetailsHeader = ({ - authTrigger, - onClose, +export function TransactionDetailsHeader({ transactionDetails, -}: TransactionDetailsModalProps): JSX.Element => { + transactionActions, +}: { + transactionDetails: TransactionDetails + transactionActions: ReturnType +}): JSX.Element { const { t } = useTranslation() - const { openActionsModal, renderModals } = useTransactionActionsCancelModals({ - authTrigger, - onNavigateAway: onClose, - transaction: transactionDetails, - }) - - const dateString = useFormattedDateTime( - dayjs(transactionDetails.addedTime), - FORMAT_DATE_TIME_MEDIUM - ) + const dateString = useFormattedDateTime(dayjs(transactionDetails.addedTime), FORMAT_DATE_TIME_MEDIUM) const title = getTransactionSummaryTitle(transactionDetails, t) + const { menuItems, openActionsModal } = transactionActions + return ( - <> - - - - + + + + + + {(transactionDetails.routing === Routing.DUTCH_V2 || + transactionDetails.routing === Routing.DUTCH_LIMIT) && } {title} - {dateString} + + {dateString} + + + {isWeb ? ( + + + + + + ) : ( - + - - {renderModals()} - + )} + ) } -const TransactionDetailsContent = ({ +export function TransactionDetailsContent({ transactionDetails, + onClose, }: { transactionDetails: TransactionDetails -}): JSX.Element => { + onClose: () => void +}): JSX.Element | null { const { typeInfo } = transactionDetails - const getContentComponent = (): JSX.Element => { + const getContentComponent = (): JSX.Element | null => { if (isApproveTransactionInfo(typeInfo)) { - return <> + return } else if (isFiatPurchaseTransactionInfo(typeInfo)) { return <> } else if (isNFTApproveTransactionInfo(typeInfo)) { - return <> + return } else if (isNFTMintTransactionInfo(typeInfo)) { - return <> + return } else if (isNFTTradeTransactionInfo(typeInfo)) { - return <> - } else if (isReceiveTokenTransactionInfo(typeInfo)) { - return <> - } else if (isSendTokenTransactionInfo(typeInfo)) { - return <> + return + } else if (isReceiveTokenTransactionInfo(typeInfo) || isSendTokenTransactionInfo(typeInfo)) { + return ( + + ) } else if (isSwapTransactionInfo(typeInfo)) { - return + return } else if (isWCConfirmTransactionInfo(typeInfo)) { return <> } else if (isWrapTransactionInfo(typeInfo)) { - return <> + return + } else if (isOnRampPurchaseTransactionInfo(typeInfo) || isOnRampTransferTransactionInfo(typeInfo)) { + return } else { - return <> + return null } } - return ( - - {getContentComponent()} - - ) + const contentComponent = getContentComponent() + if (contentComponent === null) { + return null + } + return {contentComponent} +} + +const isNFTActivity = (typeInfo: TransactionTypeInfo): boolean => { + const isTransferNft = + (isReceiveTokenTransactionInfo(typeInfo) || isSendTokenTransactionInfo(typeInfo)) && + typeInfo.assetType !== AssetType.Currency + const isNft = + isTransferNft || + isNFTApproveTransactionInfo(typeInfo) || + isNFTMintTransactionInfo(typeInfo) || + isNFTTradeTransactionInfo(typeInfo) + return isNft } -const TransactionDetailsInfoRows = ({ +export function TransactionDetailsModal({ + authTrigger, + onClose, transactionDetails, -}: { - transactionDetails: TransactionDetails -}): JSX.Element => { +}: TransactionDetailsModalProps): JSX.Element { const { t } = useTranslation() - const dispatch = useAppDispatch() - const { value: networkFeeValue } = useNetworkFee(transactionDetails) + const { typeInfo } = transactionDetails - const onPressCopy = async (): Promise => { - if (!transactionDetails.hash) { - return - } + // Hide both separators if it's an Nft transaction. Hide top separator if it's an unknown type transaction. + const isNftTransaction = isNFTActivity(typeInfo) + const hideTopSeparator = isNftTransaction || isUnknownTransactionInfo(typeInfo) + const hideBottomSeparator = isNftTransaction + + const { type } = useActiveAccountWithThrow() + const readonly = type === AccountType.Readonly + const isCancelable = !readonly && getIsCancelable(transactionDetails) + + const transactionActions = useTransactionActions({ + authTrigger, + onNavigateAway: onClose, + transaction: transactionDetails, + }) - await setClipboard(transactionDetails.hash) - dispatch( - pushNotification({ - type: AppNotificationType.Copied, - copyType: CopyNotificationType.TransactionId, - }) + const { openCancelModal, renderModals } = transactionActions + + const buttons: JSX.Element[] = [] + if (isCancelable) { + buttons.push( + , + ) + } + if (isWeb) { + buttons.push( + , ) } return ( - - - {t('transaction.details.networkFee')} - - - {networkFeeValue} - - - - {t('transaction.details.transactionId')} - - {shortenHash(transactionDetails.hash)} - - - + <> + + + + {!hideTopSeparator && } + + {!hideBottomSeparator && } + + {buttons.length > 0 && ( + + {buttons} + + )} - - - ) -} - -export const TransactionDetailsModal = ({ - authTrigger, - onClose, - transactionDetails, -}: TransactionDetailsModalProps): JSX.Element => { - const { t } = useTranslation() - return ( - - - - - - - - - - + + {renderModals()} + ) } - -function shortenHash(hash: string | undefined, chars: NumberRange<1, 20> = 4): string { - if (!hash) { - return '' - } - return `${hash.substring(0, chars + 2)}...${hash.substring(hash.length - chars)}` -} - -function useNetworkFee(transactionDetails: TransactionDetails): { - value: string - amount: string -} { - const formatter = useLocalizationContext() - - const currencyId = transactionDetails.networkFee - ? buildCurrencyId(transactionDetails.chainId, transactionDetails.networkFee.tokenAddress) - : undefined - const currencyInfo = useCurrencyInfo(currencyId) - - return useFormattedCurrencyAmountAndUSDValue({ - currency: currencyInfo?.currency, - currencyAmountRaw: transactionDetails.networkFee?.quantity, - valueType: ValueType.Exact, - formatter, - isApproximateAmount: false, - }) -} diff --git a/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/TransferTransactionDetails.test.tsx b/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/TransferTransactionDetails.test.tsx new file mode 100644 index 00000000000..ed59f766f04 --- /dev/null +++ b/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/TransferTransactionDetails.test.tsx @@ -0,0 +1,94 @@ +import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' +import { + ARBITRUM_DAI_CURRENCY_INFO, + BASE_CURRENCY, + ETH_CURRENCY_INFO, + OPTIMISM_CURRENCY, + POLYGON_CURRENCY, + currencyInfo, +} from 'uniswap/src/test/fixtures' +import { TransferTransactionDetails } from 'wallet/src/features/transactions/SummaryCards/DetailsModal/TransferTransactionDetails' +import { SendTokenTransactionInfo, TransactionDetails } from 'wallet/src/features/transactions/types' +import { ACCOUNT, preloadedSharedState } from 'wallet/src/test/fixtures' +import { render } from 'wallet/src/test/test-utils' + +const preloadedState = preloadedSharedState({ account: ACCOUNT }) +const transferTypeInfo = { + type: 'send', + assetType: 'currency', + recipient: '0xcd20dfedef359abb25e0c6789eb67ffee814caea', + tokenAddress: '0x14f6cccfeae34fea11d9a2ca6aabb112e8b8dafe', + currencyAmountRaw: '1000000000000000000', +} as SendTokenTransactionInfo +const mockTransaction = { + id: '9920dbad-ff24-47c8-814a-094566fc45ff', + chainId: 81457, + routing: 'CLASSIC', + from: '0xee814caea14f6cccfeae34fea11d9a2ca6aabb11', + typeInfo: transferTypeInfo, + status: 'confirmed', + addedTime: 1719911758204, + options: { request: {} }, + hash: 'b568a9e9-bbe7-42fc-ab00-5070186c0600', + receipt: { + transactionIndex: 29529, + blockNumber: 17489, + blockHash: 'dfd3ad45-78e7-4124-90f2-92758b4499ba', + confirmedTime: 1719946653408, + confirmations: 57408, + gasUsed: 27844, + effectiveGasPrice: 2941, + }, +} as TransactionDetails + +// Function to set up mocks +const getCurrencyInfoForChain = (chainId: number): CurrencyInfo => { + switch (chainId) { + case 1: // Mainnet + return ETH_CURRENCY_INFO + case 42161: // Arbitrum One + return ARBITRUM_DAI_CURRENCY_INFO + case 8453: // Base + return currencyInfo({ nativeCurrency: BASE_CURRENCY }) + case 10: // Optimism + return currencyInfo({ nativeCurrency: OPTIMISM_CURRENCY }) + case 137: // Polygon + return currencyInfo({ nativeCurrency: POLYGON_CURRENCY }) + default: + return ETH_CURRENCY_INFO // fallback to ETH + } +} + +jest.mock('uniswap/src/features/gating/hooks', () => ({ + useDynamicConfigValue: jest.fn().mockReturnValue(1000), + useFeatureFlag: jest.fn().mockReturnValue(true), +})) + +jest.mock('wallet/src/features/tokens/useCurrencyInfo', () => ({ + useCurrencyInfo: (currencyIdString: string | undefined): Maybe => { + if (!currencyIdString) { + return null + } + const [, chainIdStr] = currencyIdString.split(':') + if (!chainIdStr) { + return null + } + const chainId = parseInt(chainIdStr, 10) + return getCurrencyInfoForChain(chainId) + }, +})) + +describe('TransferTransactionDetails Component', () => { + it('renders TransferTransactionDetails without error', () => { + const onClose = jest.fn() + + const tree = render( + , + { + preloadedState, + }, + ) + + expect(tree).toMatchSnapshot() + }) +}) diff --git a/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/TransferTransactionDetails.tsx b/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/TransferTransactionDetails.tsx new file mode 100644 index 00000000000..dbf551c44f8 --- /dev/null +++ b/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/TransferTransactionDetails.tsx @@ -0,0 +1,101 @@ +import { SharedEventName } from '@uniswap/analytics-events' +import { Flex, Text, TouchableArea, isWeb } from 'ui/src' +import { iconSizes } from 'ui/src/theme' +import { CurrencyLogo } from 'uniswap/src/components/CurrencyLogo/CurrencyLogo' +import { AssetType } from 'uniswap/src/entities/assets' +import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' +import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants' +import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' +import { getSymbolDisplayText } from 'uniswap/src/utils/currency' +import { useWalletNavigation } from 'wallet/src/contexts/WalletNavigationContext' +import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' +import { useCurrencyInfo } from 'wallet/src/features/tokens/useCurrencyInfo' +import { NftTransactionDetails } from 'wallet/src/features/transactions/SummaryCards/DetailsModal/NftTransactionDetails' +import { useFormattedCurrencyAmountAndUSDValue } from 'wallet/src/features/transactions/SummaryCards/DetailsModal/utils' +import { + ReceiveTokenTransactionInfo, + SendTokenTransactionInfo, + TransactionDetails, +} from 'wallet/src/features/transactions/types' +import { buildCurrencyId } from 'wallet/src/utils/currencyId' + +export function TransferTransactionDetails({ + transactionDetails, + typeInfo, + onClose, +}: { + transactionDetails: TransactionDetails + typeInfo: ReceiveTokenTransactionInfo | SendTokenTransactionInfo + onClose: () => void +}): JSX.Element { + const formatter = useLocalizationContext() + const isCurrency = typeInfo.assetType === AssetType.Currency + const currencyInfo = useCurrencyInfo( + isCurrency ? buildCurrencyId(transactionDetails.chainId, typeInfo.tokenAddress) : undefined, + ) + + const { amount, value } = useFormattedCurrencyAmountAndUSDValue({ + currency: currencyInfo?.currency, + currencyAmountRaw: typeInfo.currencyAmountRaw, + formatter, + isApproximateAmount: false, + }) + const symbol = getSymbolDisplayText(currencyInfo?.currency.symbol) + + const tokenAmountWithSymbol = symbol ? amount + ' ' + symbol : amount // Prevents 'undefined' from being displayed + + return isCurrency ? ( + + ) : ( + + ) +} + +export function CurrencyTransferContent({ + tokenAmountWithSymbol, + currencyInfo, + value, + onClose, + showValueAsHeading = false, +}: { + tokenAmountWithSymbol: string | undefined + currencyInfo: Maybe + value: string + onClose: () => void + showValueAsHeading?: boolean +}): JSX.Element { + const { navigateToTokenDetails } = useWalletNavigation() + + const onPressToken = (): void => { + if (currencyInfo) { + sendAnalyticsEvent(SharedEventName.ELEMENT_CLICKED, { + element: ElementName.TokenItem, + modal: ModalName.TransactionDetails, + }) + + navigateToTokenDetails(currencyInfo.currencyId) + if (!isWeb) { + onClose() + } + } + } + + return ( + + + {showValueAsHeading ? value : tokenAmountWithSymbol} + + + + {showValueAsHeading ? tokenAmountWithSymbol : value} + + + + + ) +} diff --git a/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/WrapTransactionDetails.tsx b/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/WrapTransactionDetails.tsx new file mode 100644 index 00000000000..459a8a78270 --- /dev/null +++ b/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/WrapTransactionDetails.tsx @@ -0,0 +1,31 @@ +import { useCurrencyInfo } from 'wallet/src/features/tokens/useCurrencyInfo' +import { SwapTransactionContent } from 'wallet/src/features/transactions/SummaryCards/DetailsModal/SwapTransactionDetails' +import { TransactionDetails, WrapTransactionInfo } from 'wallet/src/features/transactions/types' +import { buildNativeCurrencyId, buildWrappedNativeCurrencyId } from 'wallet/src/utils/currencyId' + +export function WrapTransactionDetails({ + transactionDetails, + typeInfo, + onClose, +}: { + transactionDetails: TransactionDetails + typeInfo: WrapTransactionInfo + onClose: () => void +}): JSX.Element { + const nativeCurrency = useCurrencyInfo(buildNativeCurrencyId(transactionDetails.chainId)) + const wrappedNativeCurrency = useCurrencyInfo(buildWrappedNativeCurrencyId(transactionDetails.chainId)) + + const inputCurrency = typeInfo.unwrapped ? wrappedNativeCurrency : nativeCurrency + const outputCurrency = typeInfo.unwrapped ? nativeCurrency : wrappedNativeCurrency + + return ( + + ) +} diff --git a/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/__snapshots__/NftTransactionDetails.test.tsx.snap b/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/__snapshots__/NftTransactionDetails.test.tsx.snap new file mode 100644 index 00000000000..fd3c4ac432d --- /dev/null +++ b/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/__snapshots__/NftTransactionDetails.test.tsx.snap @@ -0,0 +1,128 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`NftTransactionDetails Component renders NftTransactionDetails without error 1`] = ` + + + + + voluptates repudiandae aliquid + + + + + inventore qui fugiat + + + + + +`; diff --git a/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/__snapshots__/SwapTransactionDetails.test.tsx.snap b/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/__snapshots__/SwapTransactionDetails.test.tsx.snap new file mode 100644 index 00000000000..1eb6efc3b7c --- /dev/null +++ b/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/__snapshots__/SwapTransactionDetails.test.tsx.snap @@ -0,0 +1,191 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SwapTransactionDetails Component renders SwapTransactionDetails without error 1`] = ` + + + + + + - + + + + - + + + + + + + + + + + + - + + + + - + + + + + +`; diff --git a/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/__snapshots__/TransactionDetailsModal.test.tsx.snap b/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/__snapshots__/TransactionDetailsModal.test.tsx.snap new file mode 100644 index 00000000000..6aa0acee7f8 --- /dev/null +++ b/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/__snapshots__/TransactionDetailsModal.test.tsx.snap @@ -0,0 +1,636 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TransactionDetails Components renders TransactionDetailsContent without error 1`] = ` + + + + + + + + + + +`; + +exports[`TransactionDetails Components renders TransactionDetailsHeader without error 1`] = ` + + + + + + + +
        +
        +
        + + + + + + + + + Approved + + + + January 1, 2023 12:00 AM + + + + + + + + + + + + + +`; + +exports[`TransactionDetails Components renders TransactionDetailsInfoRows without error 1`] = ` + + + + Network cost + + + +
        +
        +
        + + + - + + + + + + Transaction ID + + + + b568a9...0600 + + + + + + + + + + + +`; diff --git a/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/__snapshots__/TransferTransactionDetails.test.tsx.snap b/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/__snapshots__/TransferTransactionDetails.test.tsx.snap new file mode 100644 index 00000000000..edb02c6722f --- /dev/null +++ b/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/__snapshots__/TransferTransactionDetails.test.tsx.snap @@ -0,0 +1,86 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TransferTransactionDetails Component renders TransferTransactionDetails without error 1`] = ` + + + + - + + + + - + + + + +`; diff --git a/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/hooks.ts b/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/hooks.ts new file mode 100644 index 00000000000..ec16b910e5e --- /dev/null +++ b/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/hooks.ts @@ -0,0 +1,28 @@ +import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' +import { useCurrencyInfo } from 'wallet/src/features/tokens/useCurrencyInfo' +import { useFormattedCurrencyAmountAndUSDValue } from 'wallet/src/features/transactions/SummaryCards/DetailsModal/utils' +import { isUniswapX } from 'wallet/src/features/transactions/swap/trade/utils' +import { TransactionDetails } from 'wallet/src/features/transactions/types' +import { buildCurrencyId } from 'wallet/src/utils/currencyId' +import { ValueType } from 'wallet/src/utils/getCurrencyAmount' + +export function useNetworkFee(transactionDetails: TransactionDetails): { + value: string + amount: string +} { + const formatter = useLocalizationContext() + + const currencyId = transactionDetails.networkFee + ? buildCurrencyId(transactionDetails.chainId, transactionDetails.networkFee.tokenAddress) + : undefined + const currencyInfo = useCurrencyInfo(currencyId) + + return useFormattedCurrencyAmountAndUSDValue({ + currency: currencyInfo?.currency, + currencyAmountRaw: transactionDetails.networkFee?.quantity, + valueType: ValueType.Exact, + formatter, + isApproximateAmount: false, + isUniswapX: isUniswapX(transactionDetails), + }) +} diff --git a/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/types.ts b/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/types.ts index 5d433669a35..d978fba40a8 100644 --- a/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/types.ts +++ b/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/types.ts @@ -2,6 +2,10 @@ import { ConfirmedSwapTransactionInfo, ExactInputSwapTransactionInfo, ExactOutputSwapTransactionInfo, + OnRampPurchaseInfo, + OnRampTransferInfo, + UnknownTransactionInfo, + WrapTransactionInfo, } from 'wallet/src/features/transactions/types' export type SwapTypeTransactionInfo = @@ -22,60 +26,54 @@ import { WCConfirmInfo, } from 'wallet/src/features/transactions/types' -export function isApproveTransactionInfo( - typeInfo: TransactionTypeInfo -): typeInfo is ApproveTransactionInfo { +export function isApproveTransactionInfo(typeInfo: TransactionTypeInfo): typeInfo is ApproveTransactionInfo { return typeInfo.type === TransactionType.Approve } -export function isFiatPurchaseTransactionInfo( - typeInfo: TransactionTypeInfo -): typeInfo is FiatPurchaseTransactionInfo { +export function isFiatPurchaseTransactionInfo(typeInfo: TransactionTypeInfo): typeInfo is FiatPurchaseTransactionInfo { return typeInfo.type === TransactionType.FiatPurchase } -export function isNFTApproveTransactionInfo( - typeInfo: TransactionTypeInfo -): typeInfo is NFTApproveTransactionInfo { +export function isOnRampPurchaseTransactionInfo(typeInfo: TransactionTypeInfo): typeInfo is OnRampPurchaseInfo { + return typeInfo.type === TransactionType.OnRampPurchase +} + +export function isOnRampTransferTransactionInfo(typeInfo: TransactionTypeInfo): typeInfo is OnRampTransferInfo { + return typeInfo.type === TransactionType.OnRampTransfer +} + +export function isNFTApproveTransactionInfo(typeInfo: TransactionTypeInfo): typeInfo is NFTApproveTransactionInfo { return typeInfo.type === TransactionType.NFTApprove } -export function isNFTMintTransactionInfo( - typeInfo: TransactionTypeInfo -): typeInfo is NFTMintTransactionInfo { +export function isNFTMintTransactionInfo(typeInfo: TransactionTypeInfo): typeInfo is NFTMintTransactionInfo { return typeInfo.type === TransactionType.NFTMint } -export function isNFTTradeTransactionInfo( - typeInfo: TransactionTypeInfo -): typeInfo is NFTTradeTransactionInfo { +export function isNFTTradeTransactionInfo(typeInfo: TransactionTypeInfo): typeInfo is NFTTradeTransactionInfo { return typeInfo.type === TransactionType.NFTTrade } -export function isReceiveTokenTransactionInfo( - typeInfo: TransactionTypeInfo -): typeInfo is ReceiveTokenTransactionInfo { +export function isReceiveTokenTransactionInfo(typeInfo: TransactionTypeInfo): typeInfo is ReceiveTokenTransactionInfo { return typeInfo.type === TransactionType.Receive } -export function isSendTokenTransactionInfo( - typeInfo: TransactionTypeInfo -): typeInfo is SendTokenTransactionInfo { +export function isSendTokenTransactionInfo(typeInfo: TransactionTypeInfo): typeInfo is SendTokenTransactionInfo { return typeInfo.type === TransactionType.Send } -export function isSwapTransactionInfo( - typeInfo: TransactionTypeInfo -): typeInfo is SwapTypeTransactionInfo { +export function isSwapTransactionInfo(typeInfo: TransactionTypeInfo): typeInfo is SwapTypeTransactionInfo { return typeInfo.type === TransactionType.Swap } -export function isWCConfirmTransactionInfo( - typeInfo: TransactionTypeInfo -): typeInfo is WCConfirmInfo { +export function isWCConfirmTransactionInfo(typeInfo: TransactionTypeInfo): typeInfo is WCConfirmInfo { return typeInfo.type === TransactionType.WCConfirm } -export function isWrapTransactionInfo(typeInfo: TransactionTypeInfo): typeInfo is WCConfirmInfo { +export function isWrapTransactionInfo(typeInfo: TransactionTypeInfo): typeInfo is WrapTransactionInfo { return typeInfo.type === TransactionType.Wrap } + +export function isUnknownTransactionInfo(typeInfo: TransactionTypeInfo): typeInfo is UnknownTransactionInfo { + return typeInfo.type === TransactionType.Unknown +} diff --git a/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/useTransactionActions.tsx b/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/useTransactionActions.tsx new file mode 100644 index 00000000000..c12ebb73654 --- /dev/null +++ b/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/useTransactionActions.tsx @@ -0,0 +1,277 @@ +import { SharedEventName } from '@uniswap/analytics-events' +import { providers } from 'ethers' +import { useCallback, useEffect, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useDispatch } from 'react-redux' +import { IconProps, MenuContentItem, getTokenValue, isWeb, useIsDarkMode } from 'ui/src' +import { CopySheets, HelpCenter } from 'ui/src/components/icons' +import { UNIVERSE_CHAIN_LOGO } from 'uniswap/src/assets/chainLogos' +import { CurrencyLogo } from 'uniswap/src/components/CurrencyLogo/CurrencyLogo' +import { BottomSheetModal } from 'uniswap/src/components/modals/BottomSheetModal' +import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' +import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants' +import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' +import { CurrencyId } from 'uniswap/src/types/currency' +import { setClipboard } from 'uniswap/src/utils/clipboard' +import { useWalletNavigation } from 'wallet/src/contexts/WalletNavigationContext' +import { AuthTrigger } from 'wallet/src/features/auth/types' +import { pushNotification } from 'wallet/src/features/notifications/slice' +import { AppNotificationType, CopyNotificationType } from 'wallet/src/features/notifications/types' +import { useCurrencyInfo } from 'wallet/src/features/tokens/useCurrencyInfo' +import { isSwapTransactionInfo } from 'wallet/src/features/transactions/SummaryCards/DetailsModal/types' +import { CancelConfirmationView } from 'wallet/src/features/transactions/SummaryCards/SummaryItems/CancelConfirmationView' +import TransactionActionsModal, { + getTransactionId, + openSupportLink, +} from 'wallet/src/features/transactions/SummaryCards/SummaryItems/TransactionActionsModal' +import { cancelTransaction } from 'wallet/src/features/transactions/slice' +import { + BaseSwapTransactionInfo, + TransactionDetails, + TransactionStatus, + TransactionType, +} from 'wallet/src/features/transactions/types' +import { getIsCancelable } from 'wallet/src/features/transactions/utils' +import { AccountType } from 'wallet/src/features/wallet/accounts/types' +import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks' +import { openMoonpayTransactionLink, openTransactionLink } from 'wallet/src/utils/linking' + +export const useTransactionActions = ({ + authTrigger, + onNavigateAway, + transaction, +}: { + authTrigger?: AuthTrigger + onNavigateAway?: () => void + transaction: TransactionDetails +}): { + renderModals: () => JSX.Element + openActionsModal: () => void + openCancelModal: () => void + menuItems: MenuContentItem[] +} => { + const { t } = useTranslation() + const isDarkMode = useIsDarkMode() + const { navigateToTokenDetails } = useWalletNavigation() + + const { type } = useActiveAccountWithThrow() + const readonly = type === AccountType.Readonly + + const [showActionsModal, setShowActionsModal] = useState(false) + const [showCancelModal, setShowCancelModal] = useState(false) + const dispatch = useDispatch() + + const { status, addedTime, hash, chainId, typeInfo } = transaction + + const isCancelable = !readonly && getIsCancelable(transaction) + + const handleCancel = (txRequest: providers.TransactionRequest): void => { + if (!transaction) { + return + } + dispatch( + cancelTransaction({ + chainId: transaction.chainId, + id: transaction.id, + address: transaction.from, + cancelRequest: txRequest, + }), + ) + setShowCancelModal(false) + } + + const handleCancelModalClose = (): void => { + setShowCancelModal(false) + } + + const handleActionsModalClose = (): void => { + setShowActionsModal(false) + } + + const handleExplore = useCallback((): Promise => { + setShowActionsModal(false) + return openTransactionLink(hash, chainId) + }, [chainId, hash]) + + const handleViewMoonpay = (): Promise | undefined => { + if (transaction.typeInfo.type === TransactionType.FiatPurchase) { + setShowActionsModal(false) + return openMoonpayTransactionLink(transaction.typeInfo) + } + return undefined + } + + const handleViewTokenDetails = useCallback( + (currencyId: CurrencyId): void | undefined => { + if (typeInfo.type === TransactionType.Swap) { + setShowActionsModal(false) + sendAnalyticsEvent(SharedEventName.ELEMENT_CLICKED, { + element: ElementName.TokenItem, + modal: ModalName.TransactionDetails, + }) + + navigateToTokenDetails(currencyId) + if (!isWeb) { + onNavigateAway?.() + } + } + return undefined + }, + [navigateToTokenDetails, onNavigateAway, typeInfo.type], + ) + + const handleCancelConfirmationBack = (): void => { + setShowCancelModal(false) + } + + useEffect(() => { + if (status !== TransactionStatus.Pending) { + setShowCancelModal(false) + } + }, [status]) + + const openActionsModal = (): void => { + setShowActionsModal(true) + } + + const openCancelModal = (): void => { + setShowCancelModal(true) + } + + const renderModals = (): JSX.Element => ( + <> + {showActionsModal && ( + { + setShowActionsModal(false) + setShowCancelModal(true) + }} + onClose={handleActionsModalClose} + onExplore={handleExplore} + onViewMoonpay={ + typeInfo.type === TransactionType.FiatPurchase && typeInfo.explorerUrl ? handleViewMoonpay : undefined + } + onViewTokenDetails={typeInfo.type === TransactionType.Swap ? handleViewTokenDetails : undefined} + /> + )} + {showCancelModal && ( + + {transaction && ( + + )} + + )} + + ) + + const chainInfo = UNIVERSE_CHAIN_INFO[chainId] + const chainLogo = UNIVERSE_CHAIN_LOGO[chainId].explorer + + const isSwapTransaction = isSwapTransactionInfo(typeInfo) + const inputCurrencyInfo = useCurrencyInfo((typeInfo as BaseSwapTransactionInfo).inputCurrencyId) + const outputCurrencyInfo = useCurrencyInfo((typeInfo as BaseSwapTransactionInfo).outputCurrencyId) + + const menuItems = useMemo(() => { + const items: MenuContentItem[] = [] + + if (hash) { + items.push({ + label: t('transaction.action.viewEtherscan', { blockExplorerName: chainInfo.explorer.name }), + textProps: { variant: 'body2' }, + onPress: handleExplore, + Icon: isDarkMode ? chainLogo.logoDark : chainLogo.logoLight, + }) + } + + if (isSwapTransaction && inputCurrencyInfo && outputCurrencyInfo) { + const InputCurrencyLogo = ({ size }: IconProps): JSX.Element => ( + + ) + const OutputCurrencyLogo = ({ size }: IconProps): JSX.Element => ( + + ) + + items.push({ + label: t('transaction.action.view', { + tokenSymbol: inputCurrencyInfo?.currency.symbol, + }), + textProps: { variant: 'body2' }, + onPress: (): void => handleViewTokenDetails(inputCurrencyInfo.currencyId), + Icon: InputCurrencyLogo, + }) + + items.push({ + label: t('transaction.action.view', { + tokenSymbol: outputCurrencyInfo?.currency.symbol, + }), + textProps: { variant: 'body2' }, + onPress: (): void => handleViewTokenDetails(outputCurrencyInfo.currencyId), + Icon: OutputCurrencyLogo, + }) + } + + const transactionId = getTransactionId(transaction) + if (transactionId) { + items.push({ + label: t('transaction.action.copy'), + textProps: { variant: 'body2' }, + onPress: async () => { + await setClipboard(transactionId) + dispatch( + pushNotification({ + type: AppNotificationType.Copied, + copyType: CopyNotificationType.TransactionId, + }), + ) + }, + Icon: CopySheets, + }) + + items.push({ + label: t('settings.action.help'), + textProps: { variant: 'body2' }, + onPress: async (): Promise => { + await openSupportLink(transaction) + }, + Icon: HelpCenter, + }) + } + + return items + }, [ + hash, + isSwapTransaction, + inputCurrencyInfo, + outputCurrencyInfo, + transaction, + t, + chainInfo.explorer.name, + handleExplore, + isDarkMode, + chainLogo.logoDark, + chainLogo.logoLight, + handleViewTokenDetails, + dispatch, + ]) + + return { + openActionsModal, + openCancelModal, + renderModals, + menuItems, + } +} diff --git a/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/useTransactionActionsCancelModals.tsx b/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/useTransactionActionsCancelModals.tsx deleted file mode 100644 index c9a8b0cd631..00000000000 --- a/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/useTransactionActionsCancelModals.tsx +++ /dev/null @@ -1,152 +0,0 @@ -import { providers } from 'ethers' -import { useEffect, useState } from 'react' -import { BottomSheetModal } from 'uniswap/src/components/modals/BottomSheetModal' -import { ModalName } from 'uniswap/src/features/telemetry/constants' -import { CurrencyId } from 'uniswap/src/types/currency' -import { useWalletNavigation } from 'wallet/src/contexts/WalletNavigationContext' -import { AuthTrigger } from 'wallet/src/features/auth/types' -import { CancelConfirmationView } from 'wallet/src/features/transactions/SummaryCards/SummaryItems/CancelConfirmationView' -import TransactionActionsModal from 'wallet/src/features/transactions/SummaryCards/SummaryItems/TransactionActionsModal' -import { cancelTransaction } from 'wallet/src/features/transactions/slice' -import { - TransactionDetails, - TransactionStatus, - TransactionType, -} from 'wallet/src/features/transactions/types' -import { getIsCancelable } from 'wallet/src/features/transactions/utils' -import { AccountType } from 'wallet/src/features/wallet/accounts/types' -import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks' -import { useAppDispatch } from 'wallet/src/state' -import { openMoonpayTransactionLink, openTransactionLink } from 'wallet/src/utils/linking' - -export const useTransactionActionsCancelModals = ({ - authTrigger, - onNavigateAway, - transaction, -}: { - authTrigger?: AuthTrigger - onNavigateAway?: () => void - transaction: TransactionDetails -}): { - renderModals: () => JSX.Element - openActionsModal: () => void -} => { - const { navigateToTokenDetails } = useWalletNavigation() - - const { type } = useActiveAccountWithThrow() - const readonly = type === AccountType.Readonly - - const [showActionsModal, setShowActionsModal] = useState(false) - const [showCancelModal, setShowCancelModal] = useState(false) - const dispatch = useAppDispatch() - - const { status, addedTime, hash, chainId, typeInfo } = transaction - - const isCancelable = !readonly && getIsCancelable(transaction) - - const handleCancel = (txRequest: providers.TransactionRequest): void => { - if (!transaction) { - return - } - dispatch( - cancelTransaction({ - chainId: transaction.chainId, - id: transaction.id, - address: transaction.from, - cancelRequest: txRequest, - }) - ) - setShowCancelModal(false) - } - - const handleCancelModalClose = (): void => { - setShowCancelModal(false) - } - - const handleActionsModalClose = (): void => { - setShowActionsModal(false) - } - - const handleExplore = (): Promise => { - setShowActionsModal(false) - return openTransactionLink(hash, chainId) - } - - const handleViewMoonpay = (): Promise | undefined => { - if (transaction.typeInfo.type === TransactionType.FiatPurchase) { - setShowActionsModal(false) - return openMoonpayTransactionLink(transaction.typeInfo) - } - return undefined - } - - const handleViewTokenDetails = (currencyId: CurrencyId): void | undefined => { - if (transaction.typeInfo.type === TransactionType.Swap) { - setShowActionsModal(false) - navigateToTokenDetails(currencyId) - onNavigateAway?.() - } - return undefined - } - - const handleCancelConfirmationBack = (): void => { - setShowActionsModal(true) - setShowCancelModal(false) - } - - useEffect(() => { - if (status !== TransactionStatus.Pending) { - setShowCancelModal(false) - } - }, [status]) - - const openActionsModal = (): void => { - setShowActionsModal(true) - } - - const renderModals = (): JSX.Element => ( - <> - {showActionsModal && ( - { - setShowActionsModal(false) - setShowCancelModal(true) - }} - onClose={handleActionsModalClose} - onExplore={handleExplore} - onViewMoonpay={ - typeInfo.type === TransactionType.FiatPurchase && typeInfo.explorerUrl - ? handleViewMoonpay - : undefined - } - onViewTokenDetails={ - typeInfo.type === TransactionType.Swap ? handleViewTokenDetails : undefined - } - /> - )} - {showCancelModal && ( - - {transaction && ( - - )} - - )} - - ) - - return { - openActionsModal, - renderModals, - } -} diff --git a/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/utils.ts b/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/utils.ts index 20f568a4009..8152df36e11 100644 --- a/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/utils.ts +++ b/packages/wallet/src/features/transactions/SummaryCards/DetailsModal/utils.ts @@ -10,12 +10,14 @@ export function useFormattedCurrencyAmountAndUSDValue({ formatter, isApproximateAmount = false, valueType = ValueType.Raw, + isUniswapX = false, }: { currency: Maybe currencyAmountRaw: string | undefined formatter: LocalizationContextState isApproximateAmount?: boolean valueType?: ValueType + isUniswapX?: boolean }): { amount: string; value: string; tilde: string } { const currencyAmount = getCurrencyAmount({ value: currencyAmountRaw, @@ -24,10 +26,26 @@ export function useFormattedCurrencyAmountAndUSDValue({ }) const value = useUSDCValue(currencyAmount) + + if (isUniswapX) { + return { + tilde: '', + amount: `${formatter.formatNumberOrString({ value: 0 })}`, + value: formatter.formatNumberOrString({ value: 0, type: NumberType.FiatGasPrice }), + } + } + const formattedAmount = formatter.formatCurrencyAmount({ value: currencyAmount }) return { tilde: isApproximateAmount ? '~' : '', amount: `${formattedAmount}`, - value: formatter.formatCurrencyAmount({ value, type: NumberType.FiatTokenPrice }), + value: formatter.formatCurrencyAmount({ value, type: NumberType.FiatGasPrice }), + } +} + +export function shortenHash(hash: string | undefined, chars: NumberRange<1, 20> = 4): string { + if (!hash) { + return '' } + return `${hash.substring(0, chars + 2)}...${hash.substring(hash.length - chars)}` } diff --git a/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/ApproveSummaryItem.tsx b/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/ApproveSummaryItem.tsx index 84fe7159fce..63b04ad29e0 100644 --- a/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/ApproveSummaryItem.tsx +++ b/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/ApproveSummaryItem.tsx @@ -1,22 +1,15 @@ import { createElement } from 'react' import { useTranslation } from 'react-i18next' +import { AssetType } from 'uniswap/src/entities/assets' import { getSymbolDisplayText } from 'uniswap/src/utils/currency' +import { buildCurrencyId } from 'uniswap/src/utils/currencyId' import { NumberType } from 'utilities/src/format/types' import { LogoWithTxStatus } from 'wallet/src/components/CurrencyLogo/LogoWithTxStatus' -import { AssetType } from 'wallet/src/entities/assets' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' import { useCurrencyInfo } from 'wallet/src/features/tokens/useCurrencyInfo' -import { - SummaryItemProps, - TransactionSummaryLayoutProps, -} from 'wallet/src/features/transactions/SummaryCards/types' +import { SummaryItemProps, TransactionSummaryLayoutProps } from 'wallet/src/features/transactions/SummaryCards/types' import { TXN_HISTORY_ICON_SIZE } from 'wallet/src/features/transactions/SummaryCards/utils' -import { - ApproveTransactionInfo, - TransactionDetails, - TransactionType, -} from 'wallet/src/features/transactions/types' -import { buildCurrencyId } from 'wallet/src/utils/currencyId' +import { ApproveTransactionInfo, TransactionDetails, TransactionType } from 'wallet/src/features/transactions/types' const INFINITE_AMOUNT = 'INF' const ZERO_AMOUNT = '0.0' @@ -24,14 +17,13 @@ const ZERO_AMOUNT = '0.0' export function ApproveSummaryItem({ transaction, layoutElement, + index, }: SummaryItemProps & { transaction: TransactionDetails & { typeInfo: ApproveTransactionInfo } }): JSX.Element { const { t } = useTranslation() const { formatNumberOrString } = useLocalizationContext() - const currencyInfo = useCurrencyInfo( - buildCurrencyId(transaction.chainId, transaction.typeInfo.tokenAddress) - ) + const currencyInfo = useCurrencyInfo(buildCurrencyId(transaction.chainId, transaction.typeInfo.tokenAddress)) const { approvalAmount } = transaction.typeInfo @@ -39,12 +31,10 @@ export function ApproveSummaryItem({ approvalAmount === INFINITE_AMOUNT ? t('transaction.amount.unlimited') : approvalAmount && approvalAmount !== ZERO_AMOUNT - ? formatNumberOrString({ value: approvalAmount, type: NumberType.TokenNonTx }) - : '' + ? formatNumberOrString({ value: approvalAmount, type: NumberType.TokenNonTx }) + : '' - const caption = `${amount ? amount + ' ' : ''}${ - getSymbolDisplayText(currencyInfo?.currency.symbol) ?? '' - }` + const caption = `${amount ? amount + ' ' : ''}${getSymbolDisplayText(currencyInfo?.currency.symbol) ?? ''}` return createElement(layoutElement as React.FunctionComponent, { caption, @@ -59,5 +49,6 @@ export function ApproveSummaryItem({ /> ), transaction, + index, }) } diff --git a/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/CancelConfirmationView.tsx b/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/CancelConfirmationView.tsx index a39866bb2f4..64f036cd446 100644 --- a/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/CancelConfirmationView.tsx +++ b/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/CancelConfirmationView.tsx @@ -1,18 +1,17 @@ import { providers } from 'ethers' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' -import { ActivityIndicator } from 'react-native' -import { Button, Flex, HapticFeedback, Text, useSporeColors } from 'ui/src' -import SlashCircleIcon from 'ui/src/assets/icons/slash-circle.svg' -import { ElementName } from 'uniswap/src/features/telemetry/constants' +import { Button, Flex, FlexLoader, HapticFeedback, Separator, Skeleton, Text, isWeb } from 'ui/src' +import { SlashCircle } from 'ui/src/components/icons' +import { fonts } from 'ui/src/theme' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { NumberType } from 'utilities/src/format/types' -import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' import { AuthTrigger } from 'wallet/src/features/auth/types' import { useCancelationGasFeeInfo, useUSDValue } from 'wallet/src/features/gas/hooks' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' +import { useSelectTransaction } from 'wallet/src/features/transactions/hooks' +import { isUniswapX } from 'wallet/src/features/transactions/swap/trade/utils' import { TransactionDetails, TransactionStatus } from 'wallet/src/features/transactions/types' -import { useActiveAccount } from 'wallet/src/features/wallet/hooks' -import { shortenAddress } from 'wallet/src/utils/addresses' export function CancelConfirmationView({ authTrigger, @@ -25,16 +24,11 @@ export function CancelConfirmationView({ onCancel: (txRequest: providers.TransactionRequest) => void transactionDetails: TransactionDetails }): JSX.Element { - const colors = useSporeColors() const { t } = useTranslation() const { convertFiatAmountFormatted } = useLocalizationContext() - const accountAddress = useActiveAccount()?.address const cancelationGasFeeInfo = useCancelationGasFeeInfo(transactionDetails) - const gasFeeUSD = useUSDValue( - transactionDetails.chainId, - cancelationGasFeeInfo?.cancelationGasFee - ) + const gasFeeUSD = useUSDValue(transactionDetails.chainId, cancelationGasFeeInfo?.cancelationGasFee) const gasFee = convertFiatAmountFormatted(gasFeeUSD, NumberType.FiatGasPrice) const onCancelConfirm = useCallback(() => { @@ -54,8 +48,13 @@ export function CancelConfirmationView({ } }, [onCancelConfirm, authTrigger]) + // We don't currently support cancelling orders made from another device. + const isRemoteOrder = + useSelectTransaction(transactionDetails.from, transactionDetails.chainId, transactionDetails.id) === undefined && + isUniswapX(transactionDetails) + const disableConfirmationButton = - !cancelationGasFeeInfo?.cancelRequest || transactionDetails.status !== TransactionStatus.Pending + !cancelationGasFeeInfo?.cancelRequest || transactionDetails.status !== TransactionStatus.Pending || isRemoteOrder return ( + gap="$spacing16" + mt={isWeb ? '$spacing16' : '$none'} + px={isWeb ? '$none' : '$spacing24'} + py={isWeb ? '$none' : '$spacing12'} + > - + {t('transaction.action.cancel.title')} - + {t('transaction.action.cancel.description')} - - - {t('transaction.networkCost.label')} - {!gasFeeUSD ? : {gasFee}} - - {accountAddress && ( - - - - {shortenAddress(transactionDetails.from)} - - + + + + + + {t('transaction.networkCost.label')} + + {!gasFeeUSD ? ( + + + + ) : ( + {gasFee} )} - - diff --git a/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/FiatPurchaseSummaryItem.tsx b/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/FiatPurchaseSummaryItem.tsx index 30d1f23a310..f623ecb1e07 100644 --- a/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/FiatPurchaseSummaryItem.tsx +++ b/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/FiatPurchaseSummaryItem.tsx @@ -1,27 +1,22 @@ import React, { createElement } from 'react' import { useTranslation } from 'react-i18next' import { useIsDarkMode } from 'ui/src' +import { AssetType } from 'uniswap/src/entities/assets' import { getOptionalServiceProviderLogo } from 'uniswap/src/features/fiatOnRamp/utils' import { getSymbolDisplayText } from 'uniswap/src/utils/currency' +import { buildCurrencyId } from 'uniswap/src/utils/currencyId' import { NumberType } from 'utilities/src/format/types' import { LogoWithTxStatus } from 'wallet/src/components/CurrencyLogo/LogoWithTxStatus' -import { AssetType } from 'wallet/src/entities/assets' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' import { useCurrencyInfo } from 'wallet/src/features/tokens/useCurrencyInfo' -import { - SummaryItemProps, - TransactionSummaryLayoutProps, -} from 'wallet/src/features/transactions/SummaryCards/types' +import { SummaryItemProps, TransactionSummaryLayoutProps } from 'wallet/src/features/transactions/SummaryCards/types' import { TXN_HISTORY_ICON_SIZE } from 'wallet/src/features/transactions/SummaryCards/utils' -import { - FiatPurchaseTransactionInfo, - TransactionDetails, -} from 'wallet/src/features/transactions/types' -import { buildCurrencyId } from 'wallet/src/utils/currencyId' +import { FiatPurchaseTransactionInfo, TransactionDetails } from 'wallet/src/features/transactions/types' export function FiatPurchaseSummaryItem({ transaction, layoutElement, + index, }: SummaryItemProps & { transaction: TransactionDetails & { typeInfo: FiatPurchaseTransactionInfo } }): JSX.Element { @@ -43,16 +38,13 @@ export function FiatPurchaseSummaryItem({ const outputCurrencyInfo = useCurrencyInfo( outputCurrency?.metadata.contractAddress ? buildCurrencyId(chainId, outputCurrency?.metadata.contractAddress) - : undefined + : undefined, ) const cryptoSymbol = - outputSymbol ?? - getSymbolDisplayText(outputCurrencyInfo?.currency.symbol) ?? - t('transaction.currency.unknown') + outputSymbol ?? getSymbolDisplayText(outputCurrencyInfo?.currency.symbol) ?? t('transaction.currency.unknown') - const cryptoPurchaseAmount = - formatNumberOrString({ value: outputCurrencyAmount }) + ' ' + cryptoSymbol + const cryptoPurchaseAmount = formatNumberOrString({ value: outputCurrencyAmount }) + ' ' + cryptoSymbol const isDarkMode = useIsDarkMode() const serviceProviderLogoUrl = getOptionalServiceProviderLogo(serviceProviderLogo, isDarkMode) @@ -92,5 +84,6 @@ export function FiatPurchaseSummaryItem({ /> ), transaction, + index, }) } diff --git a/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/NFTApproveSummaryItem.tsx b/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/NFTApproveSummaryItem.tsx index 3bc4bac0538..259eeed0e31 100644 --- a/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/NFTApproveSummaryItem.tsx +++ b/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/NFTApproveSummaryItem.tsx @@ -1,19 +1,17 @@ import { NFTSummaryItem } from 'wallet/src/features/transactions/SummaryCards/SummaryItems/NFTSummaryItem' import { SummaryItemProps } from 'wallet/src/features/transactions/SummaryCards/types' -import { - NFTApproveTransactionInfo, - TransactionDetails, - TransactionType, -} from 'wallet/src/features/transactions/types' +import { NFTApproveTransactionInfo, TransactionDetails, TransactionType } from 'wallet/src/features/transactions/types' export function NFTApproveSummaryItem({ transaction, layoutElement, + index, }: SummaryItemProps & { transaction: TransactionDetails & { typeInfo: NFTApproveTransactionInfo } }): JSX.Element { return ( ), transaction, + index, }) } diff --git a/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/NFTTradeSummaryItem.tsx b/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/NFTTradeSummaryItem.tsx index 37fff8104cb..3e7cb4068d1 100644 --- a/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/NFTTradeSummaryItem.tsx +++ b/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/NFTTradeSummaryItem.tsx @@ -5,11 +5,13 @@ import { NFTTradeTransactionInfo, TransactionDetails } from 'wallet/src/features export function NFTTradeSummaryItem({ transaction, layoutElement, + index, }: SummaryItemProps & { transaction: TransactionDetails & { typeInfo: NFTTradeTransactionInfo } }): JSX.Element { return ( { return formatNumberOrString({ diff --git a/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/ReceiveSummaryItem.tsx b/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/ReceiveSummaryItem.tsx index 0a740002f37..1a0520597b4 100644 --- a/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/ReceiveSummaryItem.tsx +++ b/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/ReceiveSummaryItem.tsx @@ -9,11 +9,13 @@ import { export function ReceiveSummaryItem({ transaction, layoutElement, + index, }: SummaryItemProps & { transaction: TransactionDetails & { typeInfo: ReceiveTokenTransactionInfo } }): JSX.Element { return ( JSX.Element { return function OptionItem(): JSX.Element { return ( <> - + {label} @@ -67,7 +55,7 @@ export default function TransactionActionsModal({ transactionDetails, }: TransactionActionModalProps): JSX.Element { const { t } = useTranslation() - const dispatch = useAppDispatch() + const dispatch = useDispatch() const dateString = useFormattedDate(dayjs(msTimestampAdded), FORMAT_DATE_LONG) @@ -75,13 +63,9 @@ export default function TransactionActionsModal({ onClose() }, [onClose]) - const inputCurrencyInfo = useCurrencyInfo( - (transactionDetails.typeInfo as BaseSwapTransactionInfo).inputCurrencyId - ) + const inputCurrencyInfo = useCurrencyInfo((transactionDetails.typeInfo as BaseSwapTransactionInfo).inputCurrencyId) - const outputCurrencyInfo = useCurrencyInfo( - (transactionDetails.typeInfo as BaseSwapTransactionInfo).outputCurrencyId - ) + const outputCurrencyInfo = useCurrencyInfo((transactionDetails.typeInfo as BaseSwapTransactionInfo).outputCurrencyId) const options = useMemo(() => { const isSwapTransaction = transactionDetails.typeInfo.type === TransactionType.Swap @@ -95,7 +79,7 @@ export default function TransactionActionsModal({ render: renderOptionItem( t('transaction.action.view', { tokenSymbol: inputCurrencyInfo?.currency.symbol, - }) + }), ), }, { @@ -104,7 +88,7 @@ export default function TransactionActionsModal({ render: renderOptionItem( t('transaction.action.view', { tokenSymbol: outputCurrencyInfo?.currency.symbol, - }) + }), ), }, ] @@ -130,7 +114,7 @@ export default function TransactionActionsModal({ render: renderOptionItem( t('transaction.action.viewEtherscan', { blockExplorerName: chainInfo.explorer.name, - }) + }), ), }, ] @@ -148,7 +132,7 @@ export default function TransactionActionsModal({ pushNotification({ type: AppNotificationType.Copied, copyType: CopyNotificationType.TransactionId, - }) + }), ) handleClose() }, @@ -201,7 +185,8 @@ export default function TransactionActionsModal({ backgroundColor="$transparent" name={ModalName.TransactionActions} onClose={handleClose} - {...(isWeb && { alignment: 'top' })}> + {...(isWeb && { alignment: 'top' })} + > { +export async function openSupportLink(transactionDetails: TransactionDetails): Promise { + const params = new URLSearchParams() switch (transactionDetails.typeInfo.type) { case TransactionType.FiatPurchase: - return openLegacyFiatOnRampServiceProviderLink( - transactionDetails.typeInfo.serviceProvider ?? 'MOONPAY' - ) + return openLegacyFiatOnRampServiceProviderLink(transactionDetails.typeInfo.serviceProvider ?? 'MOONPAY') case TransactionType.OnRampPurchase: case TransactionType.OnRampTransfer: return openOnRampSupportLink(transactionDetails.typeInfo.serviceProvider) default: - return openUniswapHelpLink() + params.append('tf_11041337007757', transactionDetails.ownerAddress ?? '') // Wallet Address + params.append('tf_7005922218125', isWeb ? 'uniswap_extension_issue' : 'uw_ios_app') // Report Type Dropdown + params.append('tf_13686083567501', 'uw_transaction_details_page_submission') // Issue type Dropdown + params.append('tf_9807731675917', transactionDetails.hash ?? 'N/A') // Transaction id + return openUri(uniswapUrls.helpRequestUrl + '?' + params.toString()).catch((e) => + logger.error(e, { tags: { file: 'TransactionActionsModal', function: 'getHelpLink' } }), + ) } } -function getTransactionId(transactionDetails: TransactionDetails): string | undefined { +export function getTransactionId(transactionDetails: TransactionDetails): string | undefined { switch (transactionDetails.typeInfo.type) { case TransactionType.FiatPurchase: case TransactionType.OnRampPurchase: diff --git a/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/TransactionSummaryLayout.tsx b/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/TransactionSummaryLayout.tsx index 3177b319380..b3112c9012d 100644 --- a/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/TransactionSummaryLayout.tsx +++ b/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/TransactionSummaryLayout.tsx @@ -1,15 +1,14 @@ import { memo, useState } from 'react' import { useTranslation } from 'react-i18next' import { Flex, SpinningLoader, Text, TouchableArea, isWeb, useSporeColors } from 'ui/src' -import AlertTriangle from 'ui/src/assets/icons/alert-triangle.svg' import SlashCircleIcon from 'ui/src/assets/icons/slash-circle.svg' -import { UniswapX } from 'ui/src/components/icons' +import { AlertTriangle, UniswapX } from 'ui/src/components/icons' import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { DisplayNameText } from 'wallet/src/components/accounts/DisplayNameText' import { Routing } from 'wallet/src/data/tradingApi/__generated__/index' import { TransactionDetailsModal } from 'wallet/src/features/transactions/SummaryCards/DetailsModal/TransactionDetailsModal' -import { useTransactionActionsCancelModals } from 'wallet/src/features/transactions/SummaryCards/DetailsModal/useTransactionActionsCancelModals' +import { useTransactionActions } from 'wallet/src/features/transactions/SummaryCards/DetailsModal/useTransactionActions' import { TransactionSummaryTitle } from 'wallet/src/features/transactions/SummaryCards/SummaryItems/TransactionSummaryTitle' import { TransactionSummaryLayoutProps } from 'wallet/src/features/transactions/SummaryCards/types' import { @@ -33,6 +32,7 @@ function TransactionSummaryLayout({ caption, postCaptionElement, icon, + index, onRetry, }: TransactionSummaryLayoutProps): JSX.Element { const { t } = useTranslation() @@ -56,7 +56,7 @@ function TransactionSummaryLayout({ // Monitor latest nonce to identify queued transactions. const queued = useIsQueuedTransaction(transaction) - const { openActionsModal, renderModals } = useTransactionActionsCancelModals({ + const { openActionsModal, renderModals } = useTransactionActions({ authTrigger, transaction, }) @@ -89,9 +89,8 @@ function TransactionSummaryLayout({ ) : ( @@ -102,7 +101,7 @@ function TransactionSummaryLayout({ return ( <> - + + py="$spacing8" + > {icon && ( {icon} @@ -127,8 +127,9 @@ function TransactionSummaryLayout({ textProps={{ color: '$accent1', variant: 'body1' }} /> ) : null} - {(transaction.routing === Routing.DUTCH_V2 || - transaction.routing === Routing.DUTCH_LIMIT) && } + {(transaction.routing === Routing.DUTCH_V2 || transaction.routing === Routing.DUTCH_LIMIT) && ( + + )} {!inProgress && rightBlock} diff --git a/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/TransactionSummaryTitle.tsx b/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/TransactionSummaryTitle.tsx index 973ee4b0eaa..c245cf0b159 100644 --- a/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/TransactionSummaryTitle.tsx +++ b/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/TransactionSummaryTitle.tsx @@ -9,10 +9,7 @@ interface TransactionSummaryTitleProps { const ICON_SIZE = 14 -export const TransactionSummaryTitle: React.FC = ({ - transaction, - title, -}) => { +export const TransactionSummaryTitle: React.FC = ({ transaction, title }) => { const isDarkMode = useIsDarkMode() const onRampLogo = transaction.typeInfo.type === TransactionType.OnRampPurchase || diff --git a/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/TransferTokenSummaryItem.tsx b/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/TransferTokenSummaryItem.tsx index 1d4b8b52f4d..30d46a52ab7 100644 --- a/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/TransferTokenSummaryItem.tsx +++ b/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/TransferTokenSummaryItem.tsx @@ -1,18 +1,17 @@ import { createElement, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { Unitag } from 'ui/src/components/icons' +import { AssetType } from 'uniswap/src/entities/assets' import { useUnitagByAddress } from 'uniswap/src/features/unitags/hooks' import { UniverseChainId } from 'uniswap/src/types/chains' +import { shortenAddress } from 'uniswap/src/utils/addresses' import { getSymbolDisplayText } from 'uniswap/src/utils/currency' +import { buildCurrencyId } from 'uniswap/src/utils/currencyId' import { LogoWithTxStatus } from 'wallet/src/components/CurrencyLogo/LogoWithTxStatus' -import { AssetType } from 'wallet/src/entities/assets' import { useENS } from 'wallet/src/features/ens/useENS' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' import { useCurrencyInfo } from 'wallet/src/features/tokens/useCurrencyInfo' -import { - SummaryItemProps, - TransactionSummaryLayoutProps, -} from 'wallet/src/features/transactions/SummaryCards/types' +import { SummaryItemProps, TransactionSummaryLayoutProps } from 'wallet/src/features/transactions/SummaryCards/types' import { TXN_HISTORY_ICON_SIZE } from 'wallet/src/features/transactions/SummaryCards/utils' import { ReceiveTokenTransactionInfo, @@ -20,15 +19,14 @@ import { TransactionDetails, TransactionType, } from 'wallet/src/features/transactions/types' -import { shortenAddress } from 'wallet/src/utils/addresses' import { getFormattedCurrencyAmount } from 'wallet/src/utils/currency' -import { buildCurrencyId } from 'wallet/src/utils/currencyId' export function TransferTokenSummaryItem({ transactionType, otherAddress, transaction, layoutElement, + index, }: SummaryItemProps & { transactionType: TransactionType.Send | TransactionType.Receive otherAddress: string @@ -42,7 +40,7 @@ export function TransferTokenSummaryItem({ const currencyInfo = useCurrencyInfo( transaction.typeInfo.assetType === AssetType.Currency ? buildCurrencyId(transaction.chainId, transaction.typeInfo.tokenAddress) - : undefined + : undefined, ) const isCurrency = transaction.typeInfo.assetType === AssetType.Currency @@ -50,11 +48,7 @@ export function TransferTokenSummaryItem({ const currencyAmount = currencyInfo && transaction.typeInfo.currencyAmountRaw && - getFormattedCurrencyAmount( - currencyInfo.currency, - transaction.typeInfo.currencyAmountRaw, - formatter - ) + getFormattedCurrencyAmount(currencyInfo.currency, transaction.typeInfo.currencyAmountRaw, formatter) const icon = useMemo(() => { if (isCurrency) { @@ -115,5 +109,6 @@ export function TransferTokenSummaryItem({ icon, transaction, postCaptionElement: unitag?.username ? : undefined, + index, }) } diff --git a/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/UnknownSummaryItem.tsx b/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/UnknownSummaryItem.tsx index c567632b6de..c5549127b06 100644 --- a/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/UnknownSummaryItem.tsx +++ b/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/UnknownSummaryItem.tsx @@ -1,16 +1,16 @@ import { createElement, useMemo } from 'react' import { useSporeColors } from 'ui/src' import { ContractInteraction } from 'ui/src/components/icons' -import { - SummaryItemProps, - TransactionSummaryLayoutProps, -} from 'wallet/src/features/transactions/SummaryCards/types' +import { iconSizes } from 'ui/src/theme' +import { getValidAddress, shortenAddress } from 'uniswap/src/utils/addresses' +import { DappLogoWithWCBadge } from 'wallet/src/components/CurrencyLogo/LogoWithTxStatus' +import { SummaryItemProps, TransactionSummaryLayoutProps } from 'wallet/src/features/transactions/SummaryCards/types' import { TransactionDetails, UnknownTransactionInfo } from 'wallet/src/features/transactions/types' -import { getValidAddress, shortenAddress } from 'wallet/src/utils/addresses' export function UnknownSummaryItem({ transaction, layoutElement, + index, }: SummaryItemProps & { transaction: TransactionDetails & { typeInfo: UnknownTransactionInfo } }): JSX.Element { @@ -24,7 +24,18 @@ export function UnknownSummaryItem({ return createElement(layoutElement as React.FunctionComponent, { caption, - icon: , + icon: transaction.typeInfo.dappInfo?.icon ? ( + + ) : ( + + ), transaction, + index, }) } diff --git a/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/WCSummaryItem.tsx b/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/WCSummaryItem.tsx index 49cb69b4e4e..301ce9c23e0 100644 --- a/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/WCSummaryItem.tsx +++ b/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/WCSummaryItem.tsx @@ -1,15 +1,13 @@ import { createElement } from 'react' import { DappLogoWithWCBadge } from 'wallet/src/components/CurrencyLogo/LogoWithTxStatus' -import { - SummaryItemProps, - TransactionSummaryLayoutProps, -} from 'wallet/src/features/transactions/SummaryCards/types' +import { SummaryItemProps, TransactionSummaryLayoutProps } from 'wallet/src/features/transactions/SummaryCards/types' import { TXN_HISTORY_ICON_SIZE } from 'wallet/src/features/transactions/SummaryCards/utils' import { TransactionDetails, WCConfirmInfo } from 'wallet/src/features/transactions/types' export function WCSummaryItem({ transaction, layoutElement, + index, }: SummaryItemProps & { transaction: TransactionDetails & { typeInfo: WCConfirmInfo } }): JSX.Element { @@ -24,5 +22,6 @@ export function WCSummaryItem({ /> ), transaction, + index, }) } diff --git a/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/WrapSummaryItem.tsx b/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/WrapSummaryItem.tsx index f6e3e2b188a..a292cea8034 100644 --- a/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/WrapSummaryItem.tsx +++ b/packages/wallet/src/features/transactions/SummaryCards/SummaryItems/WrapSummaryItem.tsx @@ -1,14 +1,8 @@ import { createElement, useMemo } from 'react' -import { SplitLogo } from 'wallet/src/components/CurrencyLogo/SplitLogo' +import { SplitLogo } from 'uniswap/src/components/CurrencyLogo/SplitLogo' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' -import { - useNativeCurrencyInfo, - useWrappedNativeCurrencyInfo, -} from 'wallet/src/features/tokens/useCurrencyInfo' -import { - SummaryItemProps, - TransactionSummaryLayoutProps, -} from 'wallet/src/features/transactions/SummaryCards/types' +import { useNativeCurrencyInfo, useWrappedNativeCurrencyInfo } from 'wallet/src/features/tokens/useCurrencyInfo' +import { SummaryItemProps, TransactionSummaryLayoutProps } from 'wallet/src/features/transactions/SummaryCards/types' import { TXN_HISTORY_ICON_SIZE } from 'wallet/src/features/transactions/SummaryCards/utils' import { TransactionDetails, WrapTransactionInfo } from 'wallet/src/features/transactions/types' import { getFormattedCurrencyAmount } from 'wallet/src/utils/currency' @@ -16,6 +10,7 @@ import { getFormattedCurrencyAmount } from 'wallet/src/utils/currency' export function WrapSummaryItem({ transaction, layoutElement, + index, }: SummaryItemProps & { transaction: TransactionDetails & { typeInfo: WrapTransactionInfo } }): JSX.Element { @@ -35,24 +30,14 @@ export function WrapSummaryItem({ const { currency: inputCurrency } = inputCurrencyInfo const { currency: outputCurrency } = outputCurrencyInfo - const currencyAmount = getFormattedCurrencyAmount( - inputCurrency, - transaction.typeInfo.currencyAmountRaw, - formatter - ) + const currencyAmount = getFormattedCurrencyAmount(inputCurrency, transaction.typeInfo.currencyAmountRaw, formatter) const otherCurrencyAmount = getFormattedCurrencyAmount( outputCurrency, transaction.typeInfo.currencyAmountRaw, - formatter + formatter, ) return `${currencyAmount}${inputCurrency.symbol} → ${otherCurrencyAmount}${outputCurrency.symbol}` - }, [ - nativeCurrencyInfo, - transaction.typeInfo.currencyAmountRaw, - unwrapped, - wrappedCurrencyInfo, - formatter, - ]) + }, [nativeCurrencyInfo, transaction.typeInfo.currencyAmountRaw, unwrapped, wrappedCurrencyInfo, formatter]) return createElement(layoutElement as React.FunctionComponent, { caption, @@ -65,5 +50,6 @@ export function WrapSummaryItem({ /> ), transaction, + index, }) } diff --git a/packages/wallet/src/features/transactions/SummaryCards/types.ts b/packages/wallet/src/features/transactions/SummaryCards/types.ts index 3ea805d7314..a98393ccd22 100644 --- a/packages/wallet/src/features/transactions/SummaryCards/types.ts +++ b/packages/wallet/src/features/transactions/SummaryCards/types.ts @@ -1,6 +1,6 @@ +import { TransactionState } from 'uniswap/src/features/transactions/transactionState/types' import { WalletChainId } from 'uniswap/src/types/chains' import { AuthTrigger } from 'wallet/src/features/auth/types' -import { TransactionState } from 'wallet/src/features/transactions/transactionState/types' import { TransactionDetails } from 'wallet/src/features/transactions/types' export interface TransactionSummaryLayoutProps { @@ -10,6 +10,7 @@ export interface TransactionSummaryLayoutProps { caption: string postCaptionElement?: JSX.Element icon?: JSX.Element + index?: number onRetry?: () => void } @@ -18,6 +19,7 @@ export interface SummaryItemProps { transaction: TransactionDetails layoutElement: React.FunctionComponent swapCallbacks?: SwapSummaryCallbacks + index?: number } export interface SwapSummaryCallbacks { @@ -25,7 +27,7 @@ export interface SwapSummaryCallbacks { useSwapFormTransactionState: ( address: Address | undefined, chainId: WalletChainId | undefined, - txId: string | undefined + txId: string | undefined, ) => TransactionState | undefined onRetryGenerator?: (swapFormState: TransactionState | undefined) => () => void } diff --git a/packages/wallet/src/features/transactions/SummaryCards/utils.ts b/packages/wallet/src/features/transactions/SummaryCards/utils.ts index 427cba8b844..9773894d8e6 100644 --- a/packages/wallet/src/features/transactions/SummaryCards/utils.ts +++ b/packages/wallet/src/features/transactions/SummaryCards/utils.ts @@ -5,12 +5,7 @@ import { AppTFunction } from 'ui/src/i18n/types' import { iconSizes } from 'ui/src/theme' import { ONE_MINUTE_MS } from 'utilities/src/time/time' import { useInterval } from 'utilities/src/time/timing' -import { - LoadingItem, - SectionHeader, - isLoadingItem, - isSectionHeader, -} from 'wallet/src/features/activity/utils' +import { LoadingItem, SectionHeader, isLoadingItem, isSectionHeader } from 'wallet/src/features/activity/utils' import { FORMAT_DATE_MONTH_DAY, FORMAT_TIME_SHORT, @@ -44,25 +39,23 @@ export const TXN_HISTORY_ICON_SIZE = TXN_HISTORY_LOADER_ICON_SIZE export const TXN_STATUS_ICON_SIZE = iconSizes.icon16 export type ActivityItem = TransactionDetails | SectionHeader | LoadingItem -export type ActivityItemRenderer = ({ item }: { item: ActivityItem }) => JSX.Element +export type ActivityItemRenderer = ({ item, index }: { item: ActivityItem; index: number }) => JSX.Element export function generateActivityItemRenderer( layoutElement: React.FunctionComponent, loadingItem: JSX.Element, - sectionHeaderElement: React.FunctionComponent<{ title: string }>, + sectionHeaderElement: React.FunctionComponent<{ title: string; index?: number }>, swapCallbacks: SwapSummaryCallbacks | undefined, - authTrigger: - | ((args: { successCallback: () => void; failureCallback: () => void }) => Promise) - | undefined + authTrigger: ((args: { successCallback: () => void; failureCallback: () => void }) => Promise) | undefined, ): ActivityItemRenderer { - return function ActivityItemComponent({ item }: { item: ActivityItem }): JSX.Element { + return function ActivityItemComponent({ item, index }: { item: ActivityItem; index: number }): JSX.Element { // if it's a loading item, render the loading placeholder if (isLoadingItem(item)) { return loadingItem } // if it's a section header, render it differently if (isSectionHeader(item)) { - return createElement(sectionHeaderElement, { title: item.title, key: item.title }) + return createElement(sectionHeaderElement, { title: item.title, key: item.title, index }) } // item is a transaction let SummaryItem @@ -111,6 +104,7 @@ export function generateActivityItemRenderer( transaction: item, layoutElement, swapCallbacks, + index, }) } } @@ -124,13 +118,15 @@ export function generateActivityItemRenderer( */ function getTransactionTypeVerbs( typeInfo: TransactionDetails['typeInfo'], - t: AppTFunction + t: AppTFunction, ): { success: string pending?: string failed?: string canceling?: string canceled?: string + expired?: string + insufficientFunds?: string } { const externalDappName = typeInfo.externalDappInfo?.name @@ -144,6 +140,8 @@ function getTransactionTypeVerbs( failed: t('transaction.status.swap.failed'), canceling: t('transaction.status.swap.canceling'), canceled: t('transaction.status.swap.canceled'), + expired: t('transaction.status.swap.expired'), + insufficientFunds: t('transaction.status.swap.insufficientFunds'), } case TransactionType.Receive: return { @@ -304,17 +302,21 @@ function getTransactionTypeVerbs( } } -export function getTransactionSummaryTitle( - tx: TransactionDetails, - t: AppTFunction -): string | undefined { - const { success, pending, failed, canceling, canceled } = getTransactionTypeVerbs(tx.typeInfo, t) +export function getTransactionSummaryTitle(tx: TransactionDetails, t: AppTFunction): string | undefined { + const { success, pending, failed, canceling, canceled, expired, insufficientFunds } = getTransactionTypeVerbs( + tx.typeInfo, + t, + ) switch (tx.status) { case TransactionStatus.Pending: return pending case TransactionStatus.Cancelling: return canceling + case TransactionStatus.Expired: + return expired + case TransactionStatus.InsufficientFunds: + return insufficientFunds case TransactionStatus.Canceled: return canceled case TransactionStatus.Failed: @@ -346,8 +348,8 @@ export function useFormattedTime(time: number): string { // so for the first 30s it would show 0 minutes `${Math.ceil(localizedDayjs().diff(wrappedAddedTime) / ONE_MINUTE_MS)}m` // within an hour : localizedDayjs().isBefore(wrappedAddedTime.add(24, 'hour')) - ? wrappedAddedTime.format(FORMAT_TIME_SHORT) // within last 24 hours - : wrappedAddedTime.format(FORMAT_DATE_MONTH_DAY) // current year + ? wrappedAddedTime.format(FORMAT_TIME_SHORT) // within last 24 hours + : wrappedAddedTime.format(FORMAT_DATE_MONTH_DAY) // current year // eslint-disable-next-line react-hooks/exhaustive-deps }, [time, unixTime, localizedDayjs]) } diff --git a/packages/wallet/src/features/transactions/TransactionDetails/TransactionDetails.tsx b/packages/wallet/src/features/transactions/TransactionDetails/TransactionDetails.tsx index 8c65885edf0..1193296e5d0 100644 --- a/packages/wallet/src/features/transactions/TransactionDetails/TransactionDetails.tsx +++ b/packages/wallet/src/features/transactions/TransactionDetails/TransactionDetails.tsx @@ -24,6 +24,7 @@ interface TransactionDetailsProps { banner?: ReactNode chainId: WalletChainId gasFee: GasFeeResult + preUniswapXGasFeeUSD?: number showExpandedChildren?: boolean swapFeeInfo?: SwapFeeInfo showWarning?: boolean @@ -41,6 +42,7 @@ export function TransactionDetails({ showExpandedChildren, chainId, gasFee, + preUniswapXGasFeeUSD, swapFeeInfo, showWarning, warning, @@ -76,7 +78,8 @@ export function TransactionDetails({ borderRadius="$rounded16" gap="$spacing8" px="$spacing16" - py="$spacing8"> + py="$spacing8" + > @@ -87,11 +90,7 @@ export function TransactionDetails({ )} {gasFee.error && ( - + {t('swap.warning.expectedFailure')} )} @@ -105,22 +104,15 @@ export function TransactionDetails({ justifyContent="center" pb="$spacing4" pt="$spacing8" - onPress={onPressToggleShowChildren}> + onPress={onPressToggleShowChildren} + > {showChildren ? t('swap.details.action.less') : t('swap.details.action.more')} {showChildren ? ( - + ) : ( - + )} @@ -130,7 +122,12 @@ export function TransactionDetails({ {showChildren ? {children} : null} {feeOnTransferProps && } {displaySwapFeeInfo && } - + {AccountDetails} diff --git a/packages/wallet/src/features/transactions/TransactionHistoryUpdater.test.tsx b/packages/wallet/src/features/transactions/TransactionHistoryUpdater.test.tsx index cfe41557dfc..c2f09346012 100644 --- a/packages/wallet/src/features/transactions/TransactionHistoryUpdater.test.tsx +++ b/packages/wallet/src/features/transactions/TransactionHistoryUpdater.test.tsx @@ -4,7 +4,7 @@ import { AssetActivity, TransactionListQuery, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { fromGraphQLChain } from 'wallet/src/features/chains/utils' +import { fromGraphQLChain } from 'uniswap/src/features/chains/utils' import { TransactionHistoryUpdater, getReceiveNotificationFromData, @@ -95,7 +95,7 @@ const portfolioWithReceive = portfolio({ }) const { resolvers } = queryResolvers({ - portfolios: () => portfolios, + portfolios: (_, { ownerAddresses }) => portfolios.filter((p) => ownerAddresses.includes(p.ownerAddress)), }) describe(TransactionHistoryUpdater, () => { @@ -120,7 +120,10 @@ describe(TransactionHistoryUpdater, () => { it('updates notification status when there are new transactions', async () => { const reduxState = { - wallet: walletSlice, + wallet: { + ...walletSlice, + activeAccountAddress: account1.address, + }, notifications: { notificationQueue: [], notificationStatus: {}, @@ -213,7 +216,6 @@ describe(getReceiveNotificationFromData, () => { receiveCurrencyTxNotification({ address: account1.address, txStatus: TransactionStatus.Success, - txHash: receiveAssetActivity.details.hash, txId: receiveAssetActivity.details.hash, sender: assetChange.sender, tokenAddress: assetChange.asset.address, @@ -222,7 +224,7 @@ describe(getReceiveNotificationFromData, () => { // have to check if the calculation is correct in this test. // It's better to test the calculation in a separate test. currencyAmountRaw: expect.any(String), - }) + }), ) }) @@ -233,11 +235,7 @@ describe(getReceiveNotificationFromData, () => { // Ensure all transactions will be "new" compared to this const newTimestamp = 1 - const notification = getReceiveNotificationFromData( - txnDataWithoutReceiveTxns, - account1.address, - newTimestamp - ) + const notification = getReceiveNotificationFromData(txnDataWithoutReceiveTxns, account1.address, newTimestamp) expect(notification).toBeUndefined() }) diff --git a/packages/wallet/src/features/transactions/TransactionHistoryUpdater.tsx b/packages/wallet/src/features/transactions/TransactionHistoryUpdater.tsx index 63d8e88eb9b..23dc8ffb340 100644 --- a/packages/wallet/src/features/transactions/TransactionHistoryUpdater.tsx +++ b/packages/wallet/src/features/transactions/TransactionHistoryUpdater.tsx @@ -2,7 +2,8 @@ import { useApolloClient } from '@apollo/client' import dayjs from 'dayjs' import { useEffect, useMemo } from 'react' import { View } from 'react-native' -import { batch } from 'react-redux' +import { batch, useDispatch } from 'react-redux' +import { PollingInterval } from 'uniswap/src/constants/misc' import { TransactionHistoryUpdaterQueryResult, TransactionListQuery, @@ -11,7 +12,6 @@ import { } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { GQLQueries } from 'uniswap/src/data/graphql/uniswap-data-api/queries' import { ONE_SECOND_MS } from 'utilities/src/time/time' -import { PollingInterval } from 'wallet/src/constants/misc' import { buildReceiveNotification } from 'wallet/src/features/notifications/buildReceiveNotification' import { selectLastTxNotificationUpdate } from 'wallet/src/features/notifications/selectors' import { @@ -19,20 +19,13 @@ import { setLastTxNotificationUpdate, setNotificationStatus, } from 'wallet/src/features/notifications/slice' -import { - ReceiveCurrencyTxNotification, - ReceiveNFTNotification, -} from 'wallet/src/features/notifications/types' +import { ReceiveCurrencyTxNotification, ReceiveNFTNotification } from 'wallet/src/features/notifications/types' import { parseDataResponseToTransactionDetails } from 'wallet/src/features/transactions/history/utils' import { useSelectAddressTransactions } from 'wallet/src/features/transactions/selectors' import { TransactionStatus, TransactionType } from 'wallet/src/features/transactions/types' -import { - useAccounts, - useActiveAccountAddress, - useHideSpamTokensSetting, -} from 'wallet/src/features/wallet/hooks' +import { useAccounts, useActiveAccountAddress, useHideSpamTokensSetting } from 'wallet/src/features/wallet/hooks' import { selectActiveAccountAddress } from 'wallet/src/features/wallet/selectors' -import { useAppDispatch, useAppSelector } from 'wallet/src/state' +import { useAppSelector } from 'wallet/src/state' export const GQL_QUERIES_TO_REFETCH_ON_TXN_UPDATE = [ GQLQueries.PortfolioBalances, @@ -68,10 +61,7 @@ export function TransactionHistoryUpdater(): JSX.Element | null { skip: nonActiveAccountAddresses.length === 0, }) - const combinedPortfoliosData = [ - ...(activeAccountData?.portfolios ?? []), - ...(nonActiveAccountData?.portfolios ?? []), - ] + const combinedPortfoliosData = [...(activeAccountData?.portfolios ?? []), ...(nonActiveAccountData?.portfolios ?? [])] if (!combinedPortfoliosData.length) { return null @@ -85,13 +75,8 @@ export function TransactionHistoryUpdater(): JSX.Element | null { } return ( - - + + ) })} @@ -110,7 +95,7 @@ function AddressTransactionHistoryUpdater({ >['assetActivities'] > }): JSX.Element | null { - const dispatch = useAppDispatch() + const dispatch = useDispatch() const apolloClient = useApolloClient() const activeAccountAddress = useAppSelector(selectActiveAccountAddress) @@ -149,9 +134,7 @@ function AddressTransactionHistoryUpdater({ // Dont flag notification status for txns submitted from app, this is handled in transactionWatcherSaga. const confirmedLocally = localTransactions?.some( // eslint-disable-next-line max-nested-callbacks - (localTx) => - activity.details.__typename === 'TransactionDetails' && - localTx.hash === activity.details.hash + (localTx) => activity.details.__typename === 'TransactionDetails' && localTx.hash === activity.details.hash, ) if (!confirmedLocally) { dispatch(setNotificationStatus({ address, hasNotifications: true })) @@ -162,19 +145,10 @@ function AddressTransactionHistoryUpdater({ } }) - if (newTransactionsFound) { + if (newTransactionsFound && address === activeAccountAddress) { // Fetch full recent txn history and dispatch receive notification if needed. - if (address === activeAccountAddress) { - await fetchAndDispatchReceiveNotification( - address, - lastTxNotificationUpdateTimestamp, - hideSpamTokens - ) - } - - await apolloClient.refetchQueries({ - include: GQL_QUERIES_TO_REFETCH_ON_TXN_UPDATE, - }) + await fetchAndDispatchReceiveNotification(address, lastTxNotificationUpdateTimestamp, hideSpamTokens) + await apolloClient.refetchQueries({ include: GQL_QUERIES_TO_REFETCH_ON_TXN_UPDATE }) } }).catch(() => undefined) }, [ @@ -203,15 +177,15 @@ function AddressTransactionHistoryUpdater({ export function useFetchAndDispatchReceiveNotification(): ( address: string, lastTxNotificationUpdateTimestamp: number | undefined, - hideSpamTokens: boolean + hideSpamTokens: boolean, ) => Promise { const [fetchFullTransactionData] = useTransactionListLazyQuery() - const dispatch = useAppDispatch() + const dispatch = useDispatch() return async ( address: string, lastTxNotificationUpdateTimestamp: number | undefined, - hideSpamTokens = false + hideSpamTokens = false, ): Promise => { // Fetch full transaction history for user address. const { data: fullTransactionData } = await fetchFullTransactionData({ @@ -223,7 +197,7 @@ export function useFetchAndDispatchReceiveNotification(): ( fullTransactionData, address, lastTxNotificationUpdateTimestamp, - hideSpamTokens + hideSpamTokens, ) if (notification) { @@ -236,7 +210,7 @@ export function getReceiveNotificationFromData( data: TransactionListQuery | undefined, address: Address, lastTxNotificationUpdateTimestamp: number | undefined, - hideSpamTokens = false + hideSpamTokens = false, ): ReceiveCurrencyTxNotification | ReceiveNFTNotification | undefined { if (!data || !lastTxNotificationUpdateTimestamp) { return @@ -254,7 +228,7 @@ export function getReceiveNotificationFromData( tx.addedTime && tx.addedTime >= lastTxNotificationUpdateTimestamp && tx.typeInfo.type === TransactionType.Receive && - tx.status === TransactionStatus.Success + tx.status === TransactionStatus.Success, ) if (!latestReceivedTx) { diff --git a/packages/wallet/src/features/transactions/TransactionRequest/AddressFooter.tsx b/packages/wallet/src/features/transactions/TransactionRequest/AddressFooter.tsx index 1e3d62fde0d..00b5923f8ef 100644 --- a/packages/wallet/src/features/transactions/TransactionRequest/AddressFooter.tsx +++ b/packages/wallet/src/features/transactions/TransactionRequest/AddressFooter.tsx @@ -2,10 +2,10 @@ import { useTranslation } from 'react-i18next' import { Flex, Text, Tooltip } from 'ui/src' import { AlertTriangle } from 'ui/src/components/icons' import { iconSizes } from 'ui/src/theme' +import { areAddressesEqual } from 'uniswap/src/utils/addresses' import { isMobileApp } from 'utilities/src/platform' import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' import { ContentRow } from 'wallet/src/features/transactions/TransactionRequest/ContentRow' -import { areAddressesEqual } from 'wallet/src/utils/addresses' export function AddressFooter({ connectedAccountAddress, @@ -20,8 +20,7 @@ export function AddressFooter({ const currentAccountAddress = connectedAccountAddress || activeAccountAddress - const showWarning = - connectedAccountAddress && !areAddressesEqual(connectedAccountAddress, activeAccountAddress) + const showWarning = connectedAccountAddress && !areAddressesEqual(connectedAccountAddress, activeAccountAddress) return ( @@ -33,7 +32,8 @@ export function AddressFooter({ {showWarning && } - }> + } + > { borderColor="$surface3" borderRadius="$rounded16" borderWidth={1} - p="$spacing12"> + p="$spacing12" + > {t('dapp.request.warning.notActive.title')} {t('dapp.request.warning.notActive.message')} diff --git a/packages/wallet/src/features/transactions/TransactionRequest/ContentRow.tsx b/packages/wallet/src/features/transactions/TransactionRequest/ContentRow.tsx index f8ff2bbceb7..a9bc2863612 100644 --- a/packages/wallet/src/features/transactions/TransactionRequest/ContentRow.tsx +++ b/packages/wallet/src/features/transactions/TransactionRequest/ContentRow.tsx @@ -4,15 +4,17 @@ import { Flex, Text, TextProps } from 'ui/src' export function ContentRow({ label, variant = 'body4', + textColor = '$neutral2', children, }: PropsWithChildren<{ label: string | JSX.Element variant?: TextProps['variant'] + textColor?: TextProps['color'] }>): JSX.Element { return ( - + {typeof label === 'string' ? ( - + {label} ) : ( diff --git a/packages/wallet/src/features/transactions/TransactionRequest/NetworkFeeFooter.tsx b/packages/wallet/src/features/transactions/TransactionRequest/NetworkFeeFooter.tsx index 530c56b42ff..6269ddfbc6f 100644 --- a/packages/wallet/src/features/transactions/TransactionRequest/NetworkFeeFooter.tsx +++ b/packages/wallet/src/features/transactions/TransactionRequest/NetworkFeeFooter.tsx @@ -5,6 +5,7 @@ import { NetworkLogo } from 'uniswap/src/components/CurrencyLogo/NetworkLogo' import { WalletChainId } from 'uniswap/src/types/chains' import { NumberType } from 'utilities/src/format/types' import { isMobileApp } from 'utilities/src/platform' +import { UniswapXFee } from 'wallet/src/components/network/NetworkFee' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' import { ContentRow } from 'wallet/src/features/transactions/TransactionRequest/ContentRow' @@ -12,25 +13,32 @@ interface NetworkFeeFooterProps { chainId: WalletChainId showNetworkLogo: boolean gasFeeUSD: string | undefined + isUniswapX?: boolean } export function NetworkFeeFooter({ chainId, showNetworkLogo, gasFeeUSD, + isUniswapX, }: NetworkFeeFooterProps): JSX.Element | null { const { t } = useTranslation() const { convertFiatAmountFormatted } = useLocalizationContext() const variant = isMobileApp ? 'body3' : 'body4' + const formattedFiat = convertFiatAmountFormatted(gasFeeUSD, NumberType.FiatGasPrice) return ( {showNetworkLogo && } - - {convertFiatAmountFormatted(gasFeeUSD, NumberType.FiatGasPrice)} - + {isUniswapX ? ( + + ) : ( + + {formattedFiat} + + )} diff --git a/apps/mobile/src/components/WalletConnect/RequestModal/SpendingDetails.tsx b/packages/wallet/src/features/transactions/TransactionRequest/SpendingDetails.tsx similarity index 82% rename from apps/mobile/src/components/WalletConnect/RequestModal/SpendingDetails.tsx rename to packages/wallet/src/features/transactions/TransactionRequest/SpendingDetails.tsx index f9a9df10f7f..a40a37bcdaf 100644 --- a/apps/mobile/src/components/WalletConnect/RequestModal/SpendingDetails.tsx +++ b/packages/wallet/src/features/transactions/TransactionRequest/SpendingDetails.tsx @@ -1,4 +1,3 @@ -import React from 'react' import { useTranslation } from 'react-i18next' import { Flex, Text } from 'ui/src' import { iconSizes } from 'ui/src/theme' @@ -6,19 +5,16 @@ import { CurrencyLogo } from 'uniswap/src/components/CurrencyLogo/CurrencyLogo' import { WalletChainId } from 'uniswap/src/types/chains' import { getSymbolDisplayText } from 'uniswap/src/utils/currency' import { NumberType } from 'utilities/src/format/types' +import { isMobileApp } from 'utilities/src/platform' import { useUSDValue } from 'wallet/src/features/gas/hooks' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' import { useNativeCurrencyInfo } from 'wallet/src/features/tokens/useCurrencyInfo' import { ContentRow } from 'wallet/src/features/transactions/TransactionRequest/ContentRow' import { ValueType, getCurrencyAmount } from 'wallet/src/utils/getCurrencyAmount' -export function SpendingDetails({ - value, - chainId, -}: { - value: string - chainId: WalletChainId -}): JSX.Element { +export function SpendingDetails({ value, chainId }: { value: string; chainId: WalletChainId }): JSX.Element { + const variant = isMobileApp ? 'body3' : 'body4' + const { t } = useTranslation() const { convertFiatAmountFormatted, formatCurrencyAmount } = useLocalizationContext() @@ -39,11 +35,11 @@ export function SpendingDetails({ const fiatAmount = convertFiatAmountFormatted(usdValue, NumberType.FiatTokenPrice) return ( - + - {tokenAmountWithSymbol} - + {tokenAmountWithSymbol} + ({fiatAmount}) diff --git a/packages/wallet/src/features/transactions/TransactionReview/TransactionReview.tsx b/packages/wallet/src/features/transactions/TransactionReview/TransactionReview.tsx index 62c82cee702..69c2f02d79b 100644 --- a/packages/wallet/src/features/transactions/TransactionReview/TransactionReview.tsx +++ b/packages/wallet/src/features/transactions/TransactionReview/TransactionReview.tsx @@ -11,6 +11,7 @@ import { CurrencyLogo } from 'uniswap/src/components/CurrencyLogo/CurrencyLogo' import { TextInputProps } from 'uniswap/src/components/input/TextInput' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' import { ElementNameType } from 'uniswap/src/features/telemetry/constants' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { getSymbolDisplayText } from 'uniswap/src/utils/currency' import { NumberType } from 'utilities/src/format/types' import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' @@ -84,7 +85,7 @@ export function TransactionReview({ const formattedInputFiatValue = convertFiatAmountFormatted( inputCurrencyUSDValue?.toExact(), - NumberType.FiatTokenQuantity + NumberType.FiatTokenQuantity, ) return ( @@ -96,7 +97,8 @@ export function TransactionReview({ entering={FadeInUp} // TODO(EXT-526): re-enable `exiting` animation when it's fixed. exiting={isWeb ? undefined : FadeOut} - gap="$spacing4"> + gap="$spacing4" + > {currencyInInfo ? ( @@ -115,7 +117,7 @@ export function TransactionReview({ px="$spacing16" py="$none" showSoftInputOnFocus={false} - testID="amount-input-in" + testID={TestID.AmountInputOut} textAlign="center" value={formattedAmountIn} /> @@ -138,25 +140,15 @@ export function TransactionReview({ ) : null} - + {recipient ? ( {/* TODO gary to come back and fix this later. More complicated with nested components */} - {t('send.review.summary.to')} + {t('common.text.recipient')} - + @@ -167,7 +159,8 @@ export function TransactionReview({ // TODO(EXT-526): re-enable `exiting` animation when it's fixed. exiting={isWeb ? undefined : FadeOut} gap="$spacing12" - justifyContent="flex-end"> + justifyContent="flex-end" + > {transactionDetails} diff --git a/packages/wallet/src/features/transactions/cancelTransactionSaga.ts b/packages/wallet/src/features/transactions/cancelTransactionSaga.ts index 2d019babdb4..a06bef2887c 100644 --- a/packages/wallet/src/features/transactions/cancelTransactionSaga.ts +++ b/packages/wallet/src/features/transactions/cancelTransactionSaga.ts @@ -1,20 +1,91 @@ -import { call, select } from 'typed-redux-saga' +import { PERMIT2_ADDRESS } from '@uniswap/permit2-sdk' +import { CosignedV2DutchOrder, getCancelSingleParams } from '@uniswap/uniswapx-sdk' +import { Contract, providers } from 'ethers' +import { call } from 'typed-redux-saga' +import PERMIT2_ABI from 'uniswap/src/abis/permit2.json' +import { Permit2 } from 'uniswap/src/abis/types' +import { getValidAddress } from 'uniswap/src/utils/addresses' +import { logger } from 'utilities/src/logger/logger' +import { getOrders } from 'wallet/src/features/transactions/orderWatcherSaga' import { attemptReplaceTransaction } from 'wallet/src/features/transactions/replaceTransactionSaga' -import { makeSelectTransaction } from 'wallet/src/features/transactions/selectors' -import { ClassicTransactionDetails } from 'wallet/src/features/transactions/types' - -const selectTransaction = makeSelectTransaction() +import { signAndSendTransaction } from 'wallet/src/features/transactions/sendTransactionSaga' +import { isClassic } from 'wallet/src/features/transactions/swap/trade/utils' +import { TransactionDetails, UniswapXOrderDetails } from 'wallet/src/features/transactions/types' +import { getProvider, getSignerManager } from 'wallet/src/features/wallet/context' +import { selectAccounts } from 'wallet/src/features/wallet/selectors' +import { appSelect } from 'wallet/src/state' // Note, transaction cancellation on Ethereum is inherently flaky // The best we can do is replace the transaction and hope the original isn't mined first // Inspiration: https://github.com/MetaMask/metamask-extension/blob/develop/app/scripts/controllers/transactions/index.js#L744 -export function* attemptCancelTransaction(transaction: ClassicTransactionDetails) { - const { from, chainId, id } = transaction - const tx = yield* select((state) => - selectTransaction(state, { address: from, chainId, txId: id }) - ) - if (!tx?.cancelRequest) { - throw new Error('attempted to cancel a transaction without cancelRequest set') +export function* attemptCancelTransaction( + transaction: TransactionDetails, + cancelRequest: providers.TransactionRequest, +) { + if (isClassic(transaction)) { + yield* call(attemptReplaceTransaction, transaction, cancelRequest, true) + } else { + yield* call(cancelOrder, transaction, cancelRequest) + } +} + +function getPermit2Contract(): Permit2 { + return new Contract(PERMIT2_ADDRESS, PERMIT2_ABI) as Permit2 +} + +export async function getCancelOrderTxRequest( + tx: UniswapXOrderDetails, +): Promise { + const { orderHash, chainId, from } = tx + if (!orderHash) { + return undefined + } else { + const { encodedOrder } = (await getOrders([orderHash])).orders[0] ?? {} + if (!encodedOrder) { + return + } + + const nonce = CosignedV2DutchOrder.parse(encodedOrder, chainId).info.nonce + const cancelParams = getCancelSingleParams(nonce) + + const permit2 = getPermit2Contract() + const cancelTx = await permit2.populateTransaction.invalidateUnorderedNonces(cancelParams.word, cancelParams.mask) + return { ...cancelTx, from, chainId } + } +} + +function* cancelOrder(order: UniswapXOrderDetails, cancelRequest: providers.TransactionRequest) { + const { orderHash, chainId } = order + if (!orderHash) { + return + } + + try { + const accounts = yield* appSelect(selectAccounts) + const checksummedAddress = getValidAddress(order.from, true, false) + if (!checksummedAddress) { + throw new Error(`Cannot cancel order, address is invalid: ${checksummedAddress}`) + } + const account = accounts[checksummedAddress] + if (!account) { + throw new Error(`Cannot cancel order, account missing: ${orderHash}`) + } + const signerManager = yield* call(getSignerManager) + const provider = yield* call(getProvider, chainId) + + // UniswapX Orders are cancelled via submitting a transaction to invalidate the nonce of the permit2 signature used to fill the order. + // If the permit2 tx is mined before a filler attempts to fill the order, the order is prevented; the cancellation is successful. + // If the permit2 tx is mined after a filler successfully fills the order, the tx will succeed but have no effect; the cancellation is unsuccessful. + yield* call(signAndSendTransaction, cancelRequest, account, provider, signerManager) + + // At this point, there is no need to track the above transaction in state, as it will be mined regardless of whether the order is filled or not. + // Instead, the transactionWatcherSaga will either receive 'cancelled' or 'success' from the backend, updating the original tx's UI accordingly. + + // Activity history UI will pick the above transaction up as a generic "Permit2" tx. + } catch (error) { + logger.error(error, { + tags: { file: 'cancelTransactionSaga', function: 'cancelOrder' }, + extra: { orderHash }, + }) } - yield* call(attemptReplaceTransaction, transaction, tx.cancelRequest, true) } diff --git a/packages/wallet/src/features/transactions/contexts/SwapFormContext.tsx b/packages/wallet/src/features/transactions/contexts/SwapFormContext.tsx index bb7bc1d36c2..d8ac7264c06 100644 --- a/packages/wallet/src/features/transactions/contexts/SwapFormContext.tsx +++ b/packages/wallet/src/features/transactions/contexts/SwapFormContext.tsx @@ -9,15 +9,12 @@ import { useRef, useState, } from 'react' +import { getNativeAddress } from 'uniswap/src/constants/addresses' +import { AssetType, TradeableAsset } from 'uniswap/src/entities/assets' +import { CurrencyField, TradeProtocolPreference } from 'uniswap/src/features/transactions/transactionState/types' import { UniverseChainId } from 'uniswap/src/types/chains' -import { getNativeAddress } from 'wallet/src/constants/addresses' -import { AssetType, TradeableAsset } from 'wallet/src/entities/assets' import { useSwapAnalytics } from 'wallet/src/features/transactions/swap/analytics' import { useDerivedSwapInfo } from 'wallet/src/features/transactions/swap/trade/hooks/useDerivedSwapInfo' -import { - CurrencyField, - TradeProtocolPreference, -} from 'wallet/src/features/transactions/transactionState/types' export type SwapFormState = { customSlippageTolerance?: number @@ -95,7 +92,7 @@ export function SwapFormContextProvider({ setSwapForm((prevState) => ({ ...prevState, ...newState })) }, - [setSwapForm] + [setSwapForm], ) const derivedSwapInfo = useDerivedSwapInfo({ @@ -149,7 +146,7 @@ export function SwapFormContextProvider({ swapForm.txId, derivedSwapInfo, updateSwapForm, - ] + ], ) return {children} diff --git a/packages/wallet/src/features/transactions/contexts/SwapScreenContext.tsx b/packages/wallet/src/features/transactions/contexts/SwapScreenContext.tsx index 36fd4f8f343..7176a1a1118 100644 --- a/packages/wallet/src/features/transactions/contexts/SwapScreenContext.tsx +++ b/packages/wallet/src/features/transactions/contexts/SwapScreenContext.tsx @@ -29,7 +29,7 @@ export function SwapScreenContextProvider({ children }: { children: ReactNode }) screenRef, setScreen: wrappedSetScreen, }), - [screen, wrappedSetScreen] + [screen, wrappedSetScreen], ) return {children} diff --git a/packages/wallet/src/features/transactions/contexts/SwapTxContext.tsx b/packages/wallet/src/features/transactions/contexts/SwapTxContext.tsx index 2c60297e08f..089a2104890 100644 --- a/packages/wallet/src/features/transactions/contexts/SwapTxContext.tsx +++ b/packages/wallet/src/features/transactions/contexts/SwapTxContext.tsx @@ -1,42 +1,58 @@ -import { createContext, ReactNode, useContext, useMemo } from 'react' +import { createContext, PropsWithChildren, useContext } from 'react' +import { GasFeeResult } from 'wallet/src/features/gas/types' import { useSwapFormContext } from 'wallet/src/features/transactions/contexts/SwapFormContext' -import { useSwapTxAndGasInfoTradingApi } from 'wallet/src/features/transactions/swap/trade/tradingApi/hooks/useSwapTxAndGasInfoTradingApi' +import { + SwapTxAndGasInfo, + useSwapTxAndGasInfo, +} from 'wallet/src/features/transactions/swap/trade/api/hooks/useSwapTxAndGasInfo' +import { isClassic, isUniswapX } from 'wallet/src/features/transactions/swap/trade/utils' + +export type ValidatedSwapTxContext = Required & { + approvalError: false + gasFee: ValidatedGasFeeResult +} +function validateSwapTxContext(swapTxContext: SwapTxAndGasInfo): ValidatedSwapTxContext | undefined { + const gasFee = validateGasFeeResult(swapTxContext.gasFee) + if (!gasFee) { + return undefined + } + + if (!swapTxContext.approvalError && swapTxContext.trade) { + const { approvalError } = swapTxContext + if (isClassic(swapTxContext) && swapTxContext.trade && swapTxContext.txRequest) { + const { trade, txRequest } = swapTxContext + return { ...swapTxContext, trade, txRequest, approvalError, gasFee } + } else if (isUniswapX(swapTxContext) && swapTxContext.orderParams) { + const { trade, orderParams } = swapTxContext + return { ...swapTxContext, trade, gasFee, approvalError, orderParams } + } + } + return undefined +} + +type ValidatedGasFeeResult = GasFeeResult & { value: string; error: undefined } +function validateGasFeeResult(gasFee: GasFeeResult): ValidatedGasFeeResult | undefined { + if (gasFee.value === undefined || gasFee.error) { + return undefined + } + return { ...gasFee, value: gasFee.value, error: undefined } +} -type SwapTxContextState = { - txRequest: ReturnType['txRequest'] - approveTxRequest: ReturnType['approveTxRequest'] - approvalError: ReturnType['approvalError'] - gasFee: ReturnType['gasFee'] +export function isValidSwapTxContext(swapTxContext: SwapTxAndGasInfo): swapTxContext is ValidatedSwapTxContext { + // Validation fn prevents/futureproofs typeguard against illicit casts + return validateSwapTxContext(swapTxContext) !== undefined } -export const SwapTxContext = createContext(undefined) +export const SwapTxContext = createContext(undefined) -// Same as above, with different hook for data fetching. -export function SwapTxContextProviderTradingApi({ - children, -}: { - children: ReactNode -}): JSX.Element { +export function SwapTxContextProviderTradingApi({ children }: PropsWithChildren): JSX.Element { const { derivedSwapInfo } = useSwapFormContext() + const swapTxContext = useSwapTxAndGasInfo({ derivedSwapInfo }) - const { txRequest, approveTxRequest, gasFee, approvalError } = useSwapTxAndGasInfoTradingApi({ - derivedSwapInfo, - }) - - const state = useMemo( - (): SwapTxContextState => ({ - txRequest, - approveTxRequest, - gasFee, - approvalError, - }), - [approvalError, approveTxRequest, gasFee, txRequest] - ) - - return {children} + return {children} } -export const useSwapTxContext = (): SwapTxContextState => { +export const useSwapTxContext = (): SwapTxAndGasInfo => { const swapContext = useContext(SwapTxContext) if (swapContext === undefined) { diff --git a/packages/wallet/src/features/transactions/contexts/TransactionModalContext.tsx b/packages/wallet/src/features/transactions/contexts/TransactionModalContext.tsx index e90706d7b22..a1b00e2940e 100644 --- a/packages/wallet/src/features/transactions/contexts/TransactionModalContext.tsx +++ b/packages/wallet/src/features/transactions/contexts/TransactionModalContext.tsx @@ -11,9 +11,7 @@ export type TransactionModalContextState = { authTrigger?: AuthTrigger } -export const TransactionModalContext = createContext( - undefined -) +export const TransactionModalContext = createContext(undefined) export function TransactionModalContextProvider({ children, @@ -41,28 +39,17 @@ export function TransactionModalContextProvider({ openWalletRestoreModal, walletNeedsRestore, }), - [ - BiometricsIcon, - authTrigger, - bottomSheetViewStyles, - onClose, - openWalletRestoreModal, - walletNeedsRestore, - ] + [BiometricsIcon, authTrigger, bottomSheetViewStyles, onClose, openWalletRestoreModal, walletNeedsRestore], ) - return ( - {children} - ) + return {children} } export const useTransactionModalContext = (): TransactionModalContextState => { const context = useContext(TransactionModalContext) if (context === undefined) { - throw new Error( - '`useTransactionModalContext` must be used inside of `TransactionModalContextProvider`' - ) + throw new Error('`useTransactionModalContext` must be used inside of `TransactionModalContextProvider`') } return context diff --git a/packages/wallet/src/features/transactions/getAmountsFromTrade.ts b/packages/wallet/src/features/transactions/getAmountsFromTrade.ts index eade100401c..3b20d7f482c 100644 --- a/packages/wallet/src/features/transactions/getAmountsFromTrade.ts +++ b/packages/wallet/src/features/transactions/getAmountsFromTrade.ts @@ -7,10 +7,7 @@ import { } from 'wallet/src/features/transactions/types' export function getAmountsFromTrade( - typeInfo: - | ExactInputSwapTransactionInfo - | ExactOutputSwapTransactionInfo - | ConfirmedSwapTransactionInfo + typeInfo: ExactInputSwapTransactionInfo | ExactOutputSwapTransactionInfo | ConfirmedSwapTransactionInfo, ): { inputCurrencyAmountRaw: string; outputCurrencyAmountRaw: string } { if (isConfirmedSwapTypeInfo(typeInfo)) { const { inputCurrencyAmountRaw, outputCurrencyAmountRaw } = typeInfo diff --git a/packages/wallet/src/features/transactions/history/conversion/conversion.test.ts b/packages/wallet/src/features/transactions/history/conversion/conversion.test.ts index be123df48fe..c08758a8637 100644 --- a/packages/wallet/src/features/transactions/history/conversion/conversion.test.ts +++ b/packages/wallet/src/features/transactions/history/conversion/conversion.test.ts @@ -1,4 +1,6 @@ /* eslint-disable max-lines */ +import { getNativeAddress, getWrappedNativeAddress } from 'uniswap/src/constants/addresses' +import { DAI } from 'uniswap/src/constants/tokens' import { Chain, Currency, @@ -9,8 +11,6 @@ import { TransactionStatus, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { UniverseChainId } from 'uniswap/src/types/chains' -import { getNativeAddress, getWrappedNativeAddress } from 'wallet/src/constants/addresses' -import { DAI } from 'wallet/src/constants/tokens' import { extractOnRampTransactionDetails } from 'wallet/src/features/transactions/history/conversion/extractFiatOnRampTransactionDetails' import extractTransactionDetails from 'wallet/src/features/transactions/history/conversion/extractTransactionDetails' import parseApproveTransaction from 'wallet/src/features/transactions/history/conversion/parseApproveTransaction' @@ -19,11 +19,7 @@ import parseOnRampTransaction from 'wallet/src/features/transactions/history/con import parseReceiveTransaction from 'wallet/src/features/transactions/history/conversion/parseReceiveTransaction' import parseSendTransaction from 'wallet/src/features/transactions/history/conversion/parseSendTransaction' import parseTradeTransaction from 'wallet/src/features/transactions/history/conversion/parseTradeTransaction' -import { - NFTTradeType, - TransactionListQueryResponse, - TransactionType, -} from 'wallet/src/features/transactions/types' +import { NFTTradeType, TransactionListQueryResponse, TransactionType } from 'wallet/src/features/transactions/types' import { SAMPLE_SEED_ADDRESS_1, SAMPLE_SEED_ADDRESS_2 } from 'wallet/src/test/fixtures' /** @@ -243,6 +239,7 @@ describe(parseNFTMintTransaction, () => { collectionName: 'collection_name', imageURL: 'image_url', tokenId: 'token_id', + address: 'nft_contract_address', }, purchaseCurrencyId: `1-${ERC20_ASSET_ADDRESS}`, purchaseCurrencyAmountRaw: '1000000000000000000', @@ -258,6 +255,7 @@ describe(parseNFTMintTransaction, () => { collectionName: 'collection_name', imageURL: 'image_url', tokenId: 'token_id', + address: 'nft_contract_address', }, purchaseCurrencyId: `1-${getNativeAddress(UniverseChainId.Mainnet)}`, purchaseCurrencyAmountRaw: '1000000000000000000', @@ -343,6 +341,7 @@ describe(parseReceiveTransaction, () => { collectionName: 'collection_name', imageURL: 'image_url', tokenId: 'token_id', + address: 'nft_contract_address', }, }) }) @@ -395,6 +394,7 @@ describe(parseSendTransaction, () => { collectionName: 'collection_name', imageURL: 'image_url', tokenId: 'token_id', + address: 'nft_contract_address', }, }) }) @@ -503,6 +503,7 @@ describe(parseTradeTransaction, () => { collectionName: 'collection_name', imageURL: 'image_url', tokenId: 'asset_name', + address: 'nft_contract_address', }, purchaseCurrencyId: `1-${ERC20_ASSET_ADDRESS}`, purchaseCurrencyAmountRaw: '1000000000000000000', @@ -518,6 +519,7 @@ describe(parseTradeTransaction, () => { collectionName: 'collection_name', imageURL: 'image_url', tokenId: 'asset_name', + address: 'nft_contract_address', }, purchaseCurrencyId: `1-${ERC20_ASSET_ADDRESS}`, purchaseCurrencyAmountRaw: '1000000000000000000', diff --git a/packages/wallet/src/features/transactions/history/conversion/extractFiatOnRampTransactionDetails.ts b/packages/wallet/src/features/transactions/history/conversion/extractFiatOnRampTransactionDetails.ts index c6b8da05a23..522031446ae 100644 --- a/packages/wallet/src/features/transactions/history/conversion/extractFiatOnRampTransactionDetails.ts +++ b/packages/wallet/src/features/transactions/history/conversion/extractFiatOnRampTransactionDetails.ts @@ -1,9 +1,9 @@ import { TransactionType as RemoteTransactionType } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { fromGraphQLChain, toSupportedChainId } from 'uniswap/src/features/chains/utils' import { FORTransaction } from 'uniswap/src/features/fiatOnRamp/types' import { UniverseChainId, WalletChainId } from 'uniswap/src/types/chains' import { logger } from 'utilities/src/logger/logger' import { Routing } from 'wallet/src/data/tradingApi/__generated__/index' -import { fromGraphQLChain, toSupportedChainId } from 'wallet/src/features/chains/utils' import { FiatOnRampTransactionDetails } from 'wallet/src/features/fiatOnRamp/types' import parseOnRampTransaction from 'wallet/src/features/transactions/history/conversion/parseOnRampTransaction' import { remoteTxStatusToLocalTxStatus } from 'wallet/src/features/transactions/history/utils' @@ -17,7 +17,7 @@ import { } from 'wallet/src/features/transactions/types' function parseFiatPurchaseTransaction( - transaction: FORTransaction + transaction: FORTransaction, ): FiatPurchaseTransactionInfo & { chainId: WalletChainId } { const { sourceAmount: inputCurrencyAmount, @@ -61,7 +61,7 @@ function statusToTransactionInfoStatus(status: FORTransaction['status']): Transa } export function extractFiatOnRampTransactionDetails( - transaction: FORTransaction + transaction: FORTransaction, ): FiatOnRampTransactionDetails | undefined { try { const { chainId, ...typeInfo } = parseFiatPurchaseTransaction(transaction) ?? { @@ -90,9 +90,7 @@ export function extractFiatOnRampTransactionDetails( } } -export function extractOnRampTransactionDetails( - transaction: TransactionListQueryResponse -): TransactionDetails | null { +export function extractOnRampTransactionDetails(transaction: TransactionListQueryResponse): TransactionDetails | null { if (transaction?.details.__typename !== TransactionDetailsType.OnRamp) { return null } diff --git a/packages/wallet/src/features/transactions/history/conversion/extractMoonpayTransactionDetails.ts b/packages/wallet/src/features/transactions/history/conversion/extractMoonpayTransactionDetails.ts deleted file mode 100644 index 367950bffaa..00000000000 --- a/packages/wallet/src/features/transactions/history/conversion/extractMoonpayTransactionDetails.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { WalletChainId } from 'uniswap/src/types/chains' -import { logger } from 'utilities/src/logger/logger' -import { getNativeAddress } from 'wallet/src/constants/addresses' -import { Routing } from 'wallet/src/data/tradingApi/__generated__/index' -import { toSupportedChainId } from 'wallet/src/features/chains/utils' -import { - FiatOnRampTransactionDetails, - MoonpayTransactionsResponse, -} from 'wallet/src/features/fiatOnRamp/types' -import { - FiatPurchaseTransactionInfo, - TransactionStatus, - TransactionType, -} from 'wallet/src/features/transactions/types' - -const MOONPAY_ETH_CONTRACT_ADDRESS = '0x0000000000000000000000000000000000000000' - -function parseFiatPurchaseTransaction( - transaction: Partial -): FiatPurchaseTransactionInfo & { chainId: WalletChainId } { - const { - currency: outputCurrency, - baseCurrencyAmount: inputCurrencyAmount, - baseCurrency: inputCurrency, - quoteCurrencyAmount: outputCurrencyAmount, - } = transaction - - if (!outputCurrency) { - throw new Error('Expected output currency to be defined.') - } - if (!inputCurrency) { - throw new Error('Expected input currency to be defined.') - } - if (!inputCurrencyAmount) { - throw new Error('Expected inputCurrencyAmount to be defined') - } - if (outputCurrency.type !== 'crypto') { - throw new Error('Expected output currency to be crypto but received ' + outputCurrency.type) - } - - const moonpayChainId = outputCurrency.metadata?.chainId - const chainId = toSupportedChainId(moonpayChainId ?? undefined) - if (!chainId || !moonpayChainId) { - throw new Error('Unable to parse chain id' + outputCurrency.metadata?.chainId) - } - - const outputTokenAddress = - outputCurrency.metadata?.contractAddress === MOONPAY_ETH_CONTRACT_ADDRESS - ? getNativeAddress(chainId) - : outputCurrency.metadata?.contractAddress - if (!outputTokenAddress) { - throw new Error('Expected output currency address to be defined') - } - - return { - type: TransactionType.FiatPurchase, - id: transaction.id, - explorerUrl: formatReturnUrl(transaction.returnUrl, transaction.id), // Moonpay's transaction tracker page - inputCurrency: { type: inputCurrency.type, code: inputCurrency.code }, - inputCurrencyAmount, - outputCurrency: { - type: outputCurrency.type, - metadata: { chainId: moonpayChainId, contractAddress: outputTokenAddress }, - }, - outputCurrencyAmount, - // mark this local tx as synced given we updated it with server information - // this marks the tx as 'valid' / ready to display in the ui - syncedWithBackend: true, - chainId, - } -} - -function moonpayStatusToTransactionInfoStatus( - status: MoonpayTransactionsResponse[0]['status'] -): TransactionStatus { - switch (status) { - case 'failed': - return TransactionStatus.Failed - case 'pending': - case 'waitingAuthorization': - case 'waitingPayment': - return TransactionStatus.Pending - case 'completed': - // completed fiat onramp transactions show up in on-chain history - return TransactionStatus.Success - } -} - -// MoonPay does not always (ever?) return the transaction id inside `returnUrl` -// returnUrl": "https://buy-sandbox.moonpay.com/transaction_receipt -// This adds `transactionId` param if required -function formatReturnUrl( - providedReturnUrl: string | undefined, - id: string | undefined -): string | undefined { - if (!providedReturnUrl || !id) { - return - } - - if (providedReturnUrl.includes('?transactionId=')) { - return providedReturnUrl - } - - // TODO: [MOB-233] improve formatting when MoonPay provides us with more info - return `${providedReturnUrl}?transactionId=${id}` -} - -export function extractMoonpayTransactionDetails( - transaction?: MoonpayTransactionsResponse[0] -): FiatOnRampTransactionDetails | undefined { - if (!transaction) { - return - } - - // given that the `transaction` object is the raw Moonpay response, - // we wrap the extract block in a try-catch and log to Sentry - try { - const { chainId, ...typeInfo } = parseFiatPurchaseTransaction(transaction) ?? { - type: TransactionType.Unknown, - } - - return { - routing: Routing.CLASSIC, - id: transaction.externalTransactionId, - chainId, - hash: transaction.cryptoTransactionId, - addedTime: new Date(transaction.createdAt).getTime(), - status: moonpayStatusToTransactionInfoStatus(transaction.status), - from: transaction.walletAddress, - typeInfo, - options: { request: {} }, - } - } catch (error) { - logger.error(error, { - tags: { - file: 'extractFiatPurchaseTransactionDetails', - function: 'extractFiatOnRampTransactionDetails', - }, - }) - return - } -} diff --git a/packages/wallet/src/features/transactions/history/conversion/extractTransactionDetails.ts b/packages/wallet/src/features/transactions/history/conversion/extractTransactionDetails.ts index 49b42700d3c..ccd4156f76d 100644 --- a/packages/wallet/src/features/transactions/history/conversion/extractTransactionDetails.ts +++ b/packages/wallet/src/features/transactions/history/conversion/extractTransactionDetails.ts @@ -1,9 +1,9 @@ import { DEFAULT_NATIVE_ADDRESS } from 'uniswap/src/constants/chains' import { TransactionType as RemoteTransactionType } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { fromGraphQLChain } from 'uniswap/src/features/chains/utils' import { UniverseChainId } from 'uniswap/src/types/chains' import { Routing } from 'wallet/src/data/tradingApi/__generated__/index' import { SpamCode } from 'wallet/src/data/types' -import { fromGraphQLChain } from 'wallet/src/features/chains/utils' import parseApproveTransaction from 'wallet/src/features/transactions/history/conversion/parseApproveTransaction' import parseNFTMintTransaction from 'wallet/src/features/transactions/history/conversion/parseMintTransaction' import parseOnRampTransaction from 'wallet/src/features/transactions/history/conversion/parseOnRampTransaction' @@ -27,7 +27,7 @@ import { * @returns Formatted TransactionDetails object. */ export default function extractTransactionDetails( - transaction: TransactionListQueryResponse + transaction: TransactionListQueryResponse, ): TransactionDetails | null { if (transaction?.details.__typename !== TransactionDetailsType.Transaction) { return null @@ -70,19 +70,26 @@ export default function extractTransactionDetails( return false } }) ?? true + + const dappInfo = transaction.details.application?.address + ? { + name: transaction.details.application?.name, + address: transaction.details.application?.address, + icon: transaction.details.application?.icon?.url, + } + : undefined typeInfo = { type: TransactionType.Unknown, tokenAddress: transaction.details.to, isSpam, + dappInfo, } } const chainId = fromGraphQLChain(transaction.chain) const networkFee = - chainId && - transaction.details.networkFee?.quantity && - transaction.details.networkFee?.tokenSymbol + chainId && transaction.details.networkFee?.quantity && transaction.details.networkFee?.tokenSymbol ? { quantity: transaction.details.networkFee.quantity, tokenSymbol: transaction.details.networkFee.tokenSymbol, @@ -93,10 +100,7 @@ export default function extractTransactionDetails( : undefined return { - routing: - transaction.details.type === RemoteTransactionType.SwapOrder - ? Routing.DUTCH_V2 - : Routing.CLASSIC, + routing: transaction.details.type === RemoteTransactionType.SwapOrder ? Routing.DUTCH_V2 : Routing.CLASSIC, id: transaction.details.hash, // fallback to mainnet, although this should never happen chainId: chainId ?? UniverseChainId.Mainnet, diff --git a/packages/wallet/src/features/transactions/history/conversion/extractUniswapXOrderDetails.ts b/packages/wallet/src/features/transactions/history/conversion/extractUniswapXOrderDetails.ts index 4d8bcda3bb7..7286b1ef1bf 100644 --- a/packages/wallet/src/features/transactions/history/conversion/extractUniswapXOrderDetails.ts +++ b/packages/wallet/src/features/transactions/history/conversion/extractUniswapXOrderDetails.ts @@ -3,9 +3,10 @@ import { SwapOrderType, TokenStandard, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { fromGraphQLChain } from 'uniswap/src/features/chains/utils' import { UniverseChainId } from 'uniswap/src/types/chains' +import { buildCurrencyId } from 'uniswap/src/utils/currencyId' import { Routing } from 'wallet/src/data/tradingApi/__generated__/index' -import { fromGraphQLChain } from 'wallet/src/features/chains/utils' import { deriveCurrencyAmountFromAssetResponse } from 'wallet/src/features/transactions/history/utils' import { ConfirmedSwapTransactionInfo, @@ -15,20 +16,14 @@ import { TransactionStatus, TransactionType, } from 'wallet/src/features/transactions/types' -import { buildCurrencyId } from 'wallet/src/utils/currencyId' -export function extractUniswapXOrderDetails( - transaction: TransactionListQueryResponse -): TransactionDetails | null { +export function extractUniswapXOrderDetails(transaction: TransactionListQueryResponse): TransactionDetails | null { if (transaction?.details.__typename !== TransactionDetailsType.UniswapXOrder) { return null } const typeInfo = parseUniswapXOrderTransaction(transaction) - const routing = - transaction.details.swapOrderType === SwapOrderType.Limit - ? Routing.DUTCH_LIMIT - : Routing.DUTCH_V2 + const routing = transaction.details.swapOrderType === SwapOrderType.Limit ? Routing.DUTCH_LIMIT : Routing.DUTCH_V2 // TODO (MOB-3609): Parse and show pending limit orders in Activity feed if (!typeInfo || transaction.details.swapOrderType === SwapOrderType.Limit) { @@ -42,6 +37,7 @@ export function extractUniswapXOrderDetails( addedTime: transaction.timestamp * 1000, // convert to ms, status: remoteOrderStatusToLocalTxStatus(transaction.details.orderStatus), from: transaction.details.offerer, // This transaction is not on-chain, so use the offerer address as the from address + orderHash: transaction.details.hash, typeInfo, } } @@ -64,7 +60,7 @@ function remoteOrderStatusToLocalTxStatus(orderStatus: SwapOrderStatus): Transac } export default function parseUniswapXOrderTransaction( - transaction: NonNullable + transaction: NonNullable, ): ConfirmedSwapTransactionInfo | null { if (transaction?.details?.__typename !== TransactionDetailsType.UniswapXOrder) { return null @@ -88,7 +84,7 @@ export default function parseUniswapXOrderTransaction( transaction.chain, transaction.details.inputToken.address, transaction.details.inputToken.decimals, - transaction.details.inputTokenQuantity + transaction.details.inputTokenQuantity, ) const outputCurrencyAmountRaw = deriveCurrencyAmountFromAssetResponse( @@ -96,7 +92,7 @@ export default function parseUniswapXOrderTransaction( transaction.chain, transaction.details.outputToken.address, transaction.details.outputToken.decimals, - transaction.details.outputTokenQuantity + transaction.details.outputTokenQuantity, ) if (!inputCurrencyId || !outputCurrencyId) { diff --git a/packages/wallet/src/features/transactions/history/conversion/parseApproveTransaction.ts b/packages/wallet/src/features/transactions/history/conversion/parseApproveTransaction.ts index 49e4f7f98ef..38a85edb682 100644 --- a/packages/wallet/src/features/transactions/history/conversion/parseApproveTransaction.ts +++ b/packages/wallet/src/features/transactions/history/conversion/parseApproveTransaction.ts @@ -7,7 +7,7 @@ import { } from 'wallet/src/features/transactions/types' export default function parseApproveTransaction( - transaction: NonNullable + transaction: NonNullable, ): ApproveTransactionInfo | NFTApproveTransactionInfo | undefined { if (transaction.details.__typename !== TransactionDetailsType.Transaction) { return undefined @@ -25,11 +25,20 @@ export default function parseApproveTransaction( if (!(tokenAddress && spender)) { return undefined } + + const dappInfo = transaction.details.application?.address + ? { + name: transaction.details.application?.name, + address: transaction.details.application.address, + icon: transaction.details.application?.icon?.url, + } + : undefined return { type: TransactionType.Approve, tokenAddress, spender, approvalAmount, + dappInfo, } } return undefined diff --git a/packages/wallet/src/features/transactions/history/conversion/parseMintTransaction.ts b/packages/wallet/src/features/transactions/history/conversion/parseMintTransaction.ts index b43fe4fcf61..efb0fb02d6b 100644 --- a/packages/wallet/src/features/transactions/history/conversion/parseMintTransaction.ts +++ b/packages/wallet/src/features/transactions/history/conversion/parseMintTransaction.ts @@ -1,4 +1,5 @@ -import { fromGraphQLChain } from 'wallet/src/features/chains/utils' +import { fromGraphQLChain } from 'uniswap/src/features/chains/utils' +import { buildCurrencyId, buildNativeCurrencyId } from 'uniswap/src/utils/currencyId' import { deriveCurrencyAmountFromAssetResponse, parseUSDValueFromAssetChange, @@ -9,21 +10,16 @@ import { TransactionListQueryResponse, TransactionType, } from 'wallet/src/features/transactions/types' -import { buildCurrencyId, buildNativeCurrencyId } from 'wallet/src/utils/currencyId' export default function parseNFTMintTransaction( - transaction: NonNullable + transaction: NonNullable, ): NFTMintTransactionInfo | undefined { if (transaction.details.__typename !== TransactionDetailsType.Transaction) { return undefined } - const tokenChange = transaction.details.assetChanges?.find( - (change) => change?.__typename === 'TokenTransfer' - ) - const nftChange = transaction.details.assetChanges?.find( - (change) => change?.__typename === 'NftTransfer' - ) + const tokenChange = transaction.details.assetChanges?.find((change) => change?.__typename === 'TokenTransfer') + const nftChange = transaction.details.assetChanges?.find((change) => change?.__typename === 'NftTransfer') // Mints must include the NFT minted if (!nftChange || nftChange.__typename !== 'NftTransfer') { @@ -36,10 +32,11 @@ export default function parseNFTMintTransaction( const tokenId = nftChange.asset.tokenId const chainId = fromGraphQLChain(transaction.chain) const isSpam = nftChange.asset?.isSpam ?? false + const address = nftChange.asset.nftContract?.address let transactedUSDValue: number | undefined - if (!name || !collectionName || !imageURL || !tokenId || !chainId) { + if (!name || !collectionName || !imageURL || !tokenId || !chainId || !address) { return undefined } @@ -50,19 +47,26 @@ export default function parseNFTMintTransaction( tokenChange.tokenStandard === 'NATIVE' ? buildNativeCurrencyId(chainId) : tokenChange.asset?.address - ? buildCurrencyId(chainId, tokenChange.asset.address) - : undefined + ? buildCurrencyId(chainId, tokenChange.asset.address) + : undefined purchaseCurrencyAmountRaw = deriveCurrencyAmountFromAssetResponse( tokenChange.tokenStandard, tokenChange.asset.chain, tokenChange.asset.address, tokenChange.asset.decimals, - tokenChange.quantity + tokenChange.quantity, ) transactedUSDValue = parseUSDValueFromAssetChange(tokenChange.transactedValue) } + const dappInfo = transaction.details.application?.address + ? { + name: transaction.details.application?.name, + address: transaction.details.application.address, + icon: transaction.details.application?.icon?.url, + } + : undefined return { type: TransactionType.NFTMint, nftSummaryInfo: { @@ -70,10 +74,12 @@ export default function parseNFTMintTransaction( collectionName, imageURL, tokenId, + address, }, purchaseCurrencyId, purchaseCurrencyAmountRaw, transactedUSDValue, isSpam, + dappInfo, } } diff --git a/packages/wallet/src/features/transactions/history/conversion/parseOnRampTransaction.ts b/packages/wallet/src/features/transactions/history/conversion/parseOnRampTransaction.ts index cd49edc7f3c..07818ea9e10 100644 --- a/packages/wallet/src/features/transactions/history/conversion/parseOnRampTransaction.ts +++ b/packages/wallet/src/features/transactions/history/conversion/parseOnRampTransaction.ts @@ -1,4 +1,4 @@ -import { fromGraphQLChain } from 'wallet/src/features/chains/utils' +import { fromGraphQLChain } from 'uniswap/src/features/chains/utils' import { getAddressFromAsset } from 'wallet/src/features/transactions/history/utils' import { OnRampPurchaseInfo, @@ -10,7 +10,7 @@ import { } from 'wallet/src/features/transactions/types' export default function parseOnRampTransaction( - transaction: NonNullable + transaction: NonNullable, ): OnRampPurchaseInfo | OnRampTransferInfo | undefined { let change if (transaction.details.__typename === TransactionDetailsType.Transaction) { diff --git a/packages/wallet/src/features/transactions/history/conversion/parseReceiveTransaction.ts b/packages/wallet/src/features/transactions/history/conversion/parseReceiveTransaction.ts index a21d78404f7..16b55aaf720 100644 --- a/packages/wallet/src/features/transactions/history/conversion/parseReceiveTransaction.ts +++ b/packages/wallet/src/features/transactions/history/conversion/parseReceiveTransaction.ts @@ -1,6 +1,7 @@ +import { AssetType } from 'uniswap/src/entities/assets' +import { fromGraphQLChain } from 'uniswap/src/features/chains/utils' +import { areAddressesEqual } from 'uniswap/src/utils/addresses' import { SpamCode } from 'wallet/src/data/types' -import { AssetType } from 'wallet/src/entities/assets' -import { fromGraphQLChain } from 'wallet/src/features/chains/utils' import { deriveCurrencyAmountFromAssetResponse, getAddressFromAsset, @@ -13,7 +14,6 @@ import { TransactionListQueryResponse, TransactionType, } from 'wallet/src/features/transactions/types' -import { areAddressesEqual } from 'wallet/src/utils/addresses' // Non-exhaustive list of addresses Moonpay uses when sending purchased tokens const MOONPAY_SENDER_ADDRESSES = [ @@ -24,7 +24,7 @@ const MOONPAY_SENDER_ADDRESSES = [ ] export default function parseReceiveTransaction( - transaction: NonNullable + transaction: NonNullable, ): ReceiveTokenTransactionInfo | FiatPurchaseTransactionInfo | undefined { if (transaction.details.__typename !== TransactionDetailsType.Transaction) { return undefined @@ -61,6 +61,7 @@ export default function parseReceiveTransaction( collectionName, imageURL, tokenId, + address: tokenAddress, }, isSpam, } @@ -70,9 +71,7 @@ export default function parseReceiveTransaction( // Found ERC20 transfer if (change.__typename === 'TokenTransfer') { const sender = change.sender - const isMoonpayPurchase = MOONPAY_SENDER_ADDRESSES.some((address) => - areAddressesEqual(address, sender) - ) + const isMoonpayPurchase = MOONPAY_SENDER_ADDRESSES.some((address) => areAddressesEqual(address, sender)) const tokenAddress = getAddressFromAsset({ chain: change.asset.chain, @@ -85,15 +84,13 @@ export default function parseReceiveTransaction( change.asset.chain, change.asset.address, change.asset.decimals, - change.quantity + change.quantity, ) const transactedUSDValue = parseUSDValueFromAssetChange(change.transactedValue) // Filter out receive transactions with tokens that are either marked `isSpam` or with spam code 2 (token with URL name) - const isSpam = Boolean( - change.asset.project?.isSpam || change.asset.project?.spamCode === SpamCode.HIGH - ) + const isSpam = Boolean(change.asset.project?.isSpam || change.asset.project?.spamCode === SpamCode.HIGH) if (!(sender && tokenAddress)) { return undefined diff --git a/packages/wallet/src/features/transactions/history/conversion/parseSendTransaction.ts b/packages/wallet/src/features/transactions/history/conversion/parseSendTransaction.ts index ce19f059bb5..59a721e18eb 100644 --- a/packages/wallet/src/features/transactions/history/conversion/parseSendTransaction.ts +++ b/packages/wallet/src/features/transactions/history/conversion/parseSendTransaction.ts @@ -1,5 +1,5 @@ +import { AssetType } from 'uniswap/src/entities/assets' import { SpamCode } from 'wallet/src/data/types' -import { AssetType } from 'wallet/src/entities/assets' import { deriveCurrencyAmountFromAssetResponse, getAddressFromAsset, @@ -13,13 +13,22 @@ import { } from 'wallet/src/features/transactions/types' export default function parseSendTransaction( - transaction: NonNullable + transaction: NonNullable, ): SendTokenTransactionInfo | undefined { if (transaction.details.__typename !== TransactionDetailsType.Transaction) { return undefined } - const change = transaction.details.assetChanges?.[0] + let change = transaction.details.assetChanges?.[0] + + // For some NFT transfers, the first assetChange is an NftApproval followed by an NftTransfer + if ( + change?.__typename === 'NftApproval' && + transaction.details.assetChanges?.length && + transaction.details.assetChanges.length > 1 + ) { + change = transaction.details.assetChanges[1] + } if (!change) { return undefined @@ -50,6 +59,7 @@ export default function parseSendTransaction( collectionName, imageURL, tokenId, + address: tokenAddress, }, } } @@ -68,15 +78,13 @@ export default function parseSendTransaction( change.asset.chain, change.asset.address, change.asset.decimals, - change.quantity + change.quantity, ) const transactedUSDValue = parseUSDValueFromAssetChange(change.transactedValue) // Filter out send transactions with tokens that are either marked `isSpam` or with spam code 2 (token with URL name) // because send txs can be spoofed with spam tokens - const isSpam = Boolean( - change.asset.project?.isSpam || change.asset.project?.spamCode === SpamCode.HIGH - ) + const isSpam = Boolean(change.asset.project?.isSpam || change.asset.project?.spamCode === SpamCode.HIGH) if (!(recipient && tokenAddress)) { return undefined diff --git a/packages/wallet/src/features/transactions/history/conversion/parseTradeTransaction.ts b/packages/wallet/src/features/transactions/history/conversion/parseTradeTransaction.ts index 5d6b2702d06..11c442cdabe 100644 --- a/packages/wallet/src/features/transactions/history/conversion/parseTradeTransaction.ts +++ b/packages/wallet/src/features/transactions/history/conversion/parseTradeTransaction.ts @@ -5,7 +5,8 @@ import { TokenStandard, TransactionDirection, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { fromGraphQLChain } from 'wallet/src/features/chains/utils' +import { fromGraphQLChain } from 'uniswap/src/features/chains/utils' +import { buildCurrencyId, buildNativeCurrencyId, buildWrappedNativeCurrencyId } from 'uniswap/src/utils/currencyId' import { deriveCurrencyAmountFromAssetResponse, parseUSDValueFromAssetChange, @@ -19,11 +20,6 @@ import { TransactionType, WrapTransactionInfo, } from 'wallet/src/features/transactions/types' -import { - buildCurrencyId, - buildNativeCurrencyId, - buildWrappedNativeCurrencyId, -} from 'wallet/src/utils/currencyId' type TransferAssetChange = Extract< NonNullable< @@ -36,7 +32,7 @@ type TransferAssetChange = Extract< > export default function parseTradeTransaction( - transaction: NonNullable + transaction: NonNullable, ): ConfirmedSwapTransactionInfo | NFTTradeTransactionInfo | WrapTransactionInfo | undefined { // ignore UniswapX transactions for now if (transaction?.details?.__typename !== TransactionDetailsType.Transaction) { @@ -50,8 +46,7 @@ export default function parseTradeTransaction( const txAssetChanges = transaction.details.assetChanges?.filter( - (t): t is TransferAssetChange => - t?.__typename === 'TokenTransfer' || t?.__typename === 'NftTransfer' + (t): t is TransferAssetChange => t?.__typename === 'TokenTransfer' || t?.__typename === 'NftTransfer', ) ?? [] // for detecting wraps @@ -70,9 +65,7 @@ export default function parseTradeTransaction( } const isRefundInternalTx = - t?.__typename === 'TokenTransfer' && - t.asset.id === sent?.asset.id && - t.tokenStandard === TokenStandard.Native + t?.__typename === 'TokenTransfer' && t.asset.id === sent?.asset.id && t.tokenStandard === TokenStandard.Native if (isRefundInternalTx) { acc.refund = t @@ -85,7 +78,7 @@ export default function parseTradeTransaction( { refund: undefined, received: undefined, - } + }, ) // Invalid input/output info @@ -93,8 +86,7 @@ export default function parseTradeTransaction( return } - const onlyERC20Tokens = - sent.__typename === 'TokenTransfer' && received.__typename === 'TokenTransfer' + const onlyERC20Tokens = sent.__typename === 'TokenTransfer' && received.__typename === 'TokenTransfer' const containsNFT = sent.__typename === 'NftTransfer' || received.__typename === 'NftTransfer' // TODO: [MOB-235] Currently no spec for advanced transfer types. @@ -108,20 +100,20 @@ export default function parseTradeTransaction( sent.tokenStandard === TokenStandard.Native ? buildNativeCurrencyId(chainId) : sent.asset.address - ? buildCurrencyId(chainId, sent.asset.address) - : null + ? buildCurrencyId(chainId, sent.asset.address) + : null const outputCurrencyId = received.tokenStandard === TokenStandard.Native ? buildNativeCurrencyId(chainId) : received.asset.address - ? buildCurrencyId(chainId, received.asset.address) - : null + ? buildCurrencyId(chainId, received.asset.address) + : null let inputCurrencyAmountRaw = deriveCurrencyAmountFromAssetResponse( sent.tokenStandard, sent.asset.chain, sent.asset.address, sent.asset.decimals, - sent.quantity + sent.quantity, ) if (refund && refund.tokenStandard === sent.tokenStandard) { @@ -130,12 +122,10 @@ export default function parseTradeTransaction( refund.asset.chain, refund.asset.address, refund.asset.decimals, - refund.quantity + refund.quantity, ) - inputCurrencyAmountRaw = BigNumber.from(inputCurrencyAmountRaw) - .sub(refundCurrencyAmountRaw) - .toString() + inputCurrencyAmountRaw = BigNumber.from(inputCurrencyAmountRaw).sub(refundCurrencyAmountRaw).toString() } const outputCurrencyAmountRaw = deriveCurrencyAmountFromAssetResponse( @@ -143,7 +133,7 @@ export default function parseTradeTransaction( received.asset.chain, received.asset.address, received.asset.decimals, - received.quantity + received.quantity, ) const transactedUSDValue = parseUSDValueFromAssetChange(sent.transactedValue) @@ -192,26 +182,29 @@ export default function parseTradeTransaction( tokenChange.tokenStandard === TokenStandard.Native ? buildNativeCurrencyId(chainId) : tokenChange.asset?.address - ? buildCurrencyId(chainId, tokenChange.asset.address) - : undefined + ? buildCurrencyId(chainId, tokenChange.asset.address) + : undefined const purchaseCurrencyAmountRaw = deriveCurrencyAmountFromAssetResponse( tokenChange.tokenStandard, tokenChange.asset.chain, tokenChange.asset.address, tokenChange.asset.decimals, - tokenChange.quantity + tokenChange.quantity, ) const tradeType = nftChange.direction === 'IN' ? NFTTradeType.BUY : NFTTradeType.SELL const transactedUSDValue = parseUSDValueFromAssetChange(tokenChange.transactedValue) + const address = nftChange.asset.nftContract?.address + if ( !name || !collectionName || !imageURL || !tokenId || !purchaseCurrencyId || - !purchaseCurrencyAmountRaw + !purchaseCurrencyAmountRaw || + !address ) { return undefined } @@ -223,6 +216,7 @@ export default function parseTradeTransaction( collectionName, imageURL, tokenId, + address, }, purchaseCurrencyId, purchaseCurrencyAmountRaw, diff --git a/packages/wallet/src/features/transactions/history/utils.ts b/packages/wallet/src/features/transactions/history/utils.ts index bed90aeed60..a1ef1602be3 100644 --- a/packages/wallet/src/features/transactions/history/utils.ts +++ b/packages/wallet/src/features/transactions/history/utils.ts @@ -1,5 +1,6 @@ import { Token } from '@uniswap/sdk-core' import dayjs from 'dayjs' +import { getNativeAddress } from 'uniswap/src/constants/addresses' import { Amount, Chain, @@ -10,16 +11,12 @@ import { TokenStandard, TransactionListQuery, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { fromGraphQLChain } from 'uniswap/src/features/chains/utils' +import { NativeCurrency } from 'uniswap/src/features/tokens/NativeCurrency' import { CurrencyId } from 'uniswap/src/types/currency' -import { getNativeAddress } from 'wallet/src/constants/addresses' -import { fromGraphQLChain } from 'wallet/src/features/chains/utils' +import { buildCurrencyId } from 'uniswap/src/utils/currencyId' import { CurrencyIdToVisibility } from 'wallet/src/features/favorites/slice' -import { - FORMAT_DATE_MONTH, - FORMAT_DATE_MONTH_YEAR, - LocalizedDayjs, -} from 'wallet/src/features/language/localizedDayjs' -import { NativeCurrency } from 'wallet/src/features/tokens/NativeCurrency' +import { FORMAT_DATE_MONTH, FORMAT_DATE_MONTH_YEAR, LocalizedDayjs } from 'wallet/src/features/language/localizedDayjs' import { extractOnRampTransactionDetails } from 'wallet/src/features/transactions/history/conversion/extractFiatOnRampTransactionDetails' import extractTransactionDetails from 'wallet/src/features/transactions/history/conversion/extractTransactionDetails' import { extractUniswapXOrderDetails } from 'wallet/src/features/transactions/history/conversion/extractUniswapXOrderDetails' @@ -31,7 +28,6 @@ import { TransactionStatus, TransactionType, } from 'wallet/src/features/transactions/types' -import { buildCurrencyId } from 'wallet/src/utils/currencyId' import { ValueType, getCurrencyAmount } from 'wallet/src/utils/getCurrencyAmount' export interface AllFormattedTransactions { @@ -43,16 +39,16 @@ export interface AllFormattedTransactions { export function formatTransactionsByDate( transactions: TransactionDetails[] | undefined, - localizedDayjs: LocalizedDayjs + localizedDayjs: LocalizedDayjs, ): AllFormattedTransactions { // timestamp in ms for start of time periods const msTimestampCutoff24h = dayjs().subtract(24, 'hour').valueOf() const msTimestampCutoffYear = dayjs().startOf('year').valueOf() // Segment by time periods. - const [pending, last24hTransactionList, olderThan24HTransactionList] = ( - transactions ?? [] - ).reduce<[TransactionDetails[], TransactionDetails[], TransactionDetails[]]>( + const [pending, last24hTransactionList, olderThan24HTransactionList] = (transactions ?? []).reduce< + [TransactionDetails[], TransactionDetails[], TransactionDetails[]] + >( (accum, item) => { if ( // Want all incomplete transactions @@ -68,7 +64,7 @@ export function formatTransactionsByDate( } return accum }, - [[], [], []] + [[], [], []], ) const pendingSorted = pending.sort((a, b) => { @@ -97,7 +93,7 @@ export function formatTransactionsByDate( accum[key] = currentMonthList return accum }, - {} + {}, ) return { @@ -114,7 +110,7 @@ export function formatTransactionsByDate( export function parseDataResponseToTransactionDetails( data: TransactionListQuery, hideSpamTokens: boolean, - tokenVisibilityOverrides?: CurrencyIdToVisibility + tokenVisibilityOverrides?: CurrencyIdToVisibility, ): TransactionDetails[] | undefined { if (data.portfolios?.[0]?.assetActivities) { return data.portfolios[0].assetActivities.reduce((accum: TransactionDetails[], t) => { @@ -151,7 +147,7 @@ export function parseDataResponseToTransactionDetails( */ export function parseDataResponseToFeedTransactionDetails( data: FeedTransactionListQuery, - hideSpamTokens?: boolean + hideSpamTokens?: boolean, ): TransactionDetails[] | undefined { const allTransactions: TransactionDetails[] = [] @@ -190,7 +186,7 @@ export function deriveCurrencyAmountFromAssetResponse( chain: Chain, address: Maybe, decimals: Maybe, - quantity: string + quantity: string, ): string { const chainId = fromGraphQLChain(chain) if (!chainId) { @@ -201,8 +197,8 @@ export function deriveCurrencyAmountFromAssetResponse( tokenStandard === TokenStandard.Native ? NativeCurrency.onChain(chainId) : address && decimals - ? new Token(chainId, address, decimals) - : undefined + ? new Token(chainId, address, decimals) + : undefined const currencyAmount = getCurrencyAmount({ value: quantity, @@ -242,9 +238,7 @@ export function getAddressFromAsset({ * @param transactedValue Transacted value amount from TokenTransfer API response * @returns parsed USD value as a number if currency is of type USD */ -export function parseUSDValueFromAssetChange( - transactedValue: Maybe> -): number | undefined { +export function parseUSDValueFromAssetChange(transactedValue: Maybe>): number | undefined { return transactedValue?.currency === Currency.Usd ? transactedValue.value ?? undefined : undefined } @@ -269,7 +263,7 @@ function extractCurrencyIdFromTx(transaction: TransactionDetails | null): Curren export function remoteTxStatusToLocalTxStatus( type: RemoteTransactionType, - status: RemoteTransactionStatus + status: RemoteTransactionStatus, ): TransactionStatus { switch (status) { case RemoteTransactionStatus.Failed: diff --git a/packages/wallet/src/features/transactions/hooks.ts b/packages/wallet/src/features/transactions/hooks.ts index 4e0e358bdba..5abb149bdb7 100644 --- a/packages/wallet/src/features/transactions/hooks.ts +++ b/packages/wallet/src/features/transactions/hooks.ts @@ -1,33 +1,35 @@ import { Currency } from '@uniswap/sdk-core' import { BigNumberish } from 'ethers' import { useMemo } from 'react' +import { useDispatch } from 'react-redux' +import { TransactionState } from 'uniswap/src/features/transactions/transactionState/types' import { WalletChainId } from 'uniswap/src/types/chains' +import { ensureLeading0x } from 'uniswap/src/utils/addresses' +import { areCurrencyIdsEqual, buildCurrencyId } from 'uniswap/src/utils/currencyId' import { useCurrencyInfo } from 'wallet/src/features/tokens/useCurrencyInfo' -import { - makeSelectTransaction, - useSelectAddressTransactions, -} from 'wallet/src/features/transactions/selectors' +import { makeSelectTransaction, useSelectAddressTransactions } from 'wallet/src/features/transactions/selectors' import { finalizeTransaction } from 'wallet/src/features/transactions/slice' import { createSwapFormFromTxDetails, createWrapFormFromTxDetails, } from 'wallet/src/features/transactions/swap/createSwapFormFromTxDetails' import { isClassic, isUniswapX } from 'wallet/src/features/transactions/swap/trade/utils' -import { TransactionState } from 'wallet/src/features/transactions/transactionState/types' import { + QueuedOrderStatus, TransactionDetails, TransactionStatus, TransactionType, + UniswapXOrderDetails, isFinalizedTx, } from 'wallet/src/features/transactions/types' import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' -import { useAppDispatch, useAppSelector } from 'wallet/src/state' -import { ensureLeading0x } from 'wallet/src/utils/addresses' -import { areCurrencyIdsEqual, buildCurrencyId } from 'wallet/src/utils/currencyId' +import { useAppSelector } from 'wallet/src/state' + +type HashToTxMap = Map export function usePendingTransactions( address: Address | null, - ignoreTransactionTypes: TransactionType[] = [] + ignoreTransactionTypes: TransactionType[] = [], ): TransactionDetails[] | undefined { const transactions = useSelectAddressTransactions(address) return useMemo(() => { @@ -36,31 +38,64 @@ export function usePendingTransactions( } return transactions.filter( (tx: { status: TransactionStatus; typeInfo: { type: TransactionType } }) => - tx.status === TransactionStatus.Pending && - !ignoreTransactionTypes.includes(tx.typeInfo.type) + tx.status === TransactionStatus.Pending && !ignoreTransactionTypes.includes(tx.typeInfo.type), ) }, [ignoreTransactionTypes, transactions]) } +const ERRORED_QUEUE_STATUSES = [ + QueuedOrderStatus.AppClosed, + QueuedOrderStatus.ApprovalFailed, + QueuedOrderStatus.WrapFailed, + QueuedOrderStatus.SubmissionFailed, + QueuedOrderStatus.Stale, +] as const +export type ErroredQueuedOrderStatus = (typeof ERRORED_QUEUE_STATUSES)[number] +export type ErroredQueuedOrder = UniswapXOrderDetails & { + status: TransactionStatus.Pending + queueStatus: ErroredQueuedOrderStatus +} + +function isErroredQueuedOrder(tx: TransactionDetails): tx is ErroredQueuedOrder { + return Boolean( + isUniswapX(tx) && + tx.status === TransactionStatus.Pending && + tx.queueStatus && + ERRORED_QUEUE_STATUSES.some((status) => status === tx.queueStatus), + ) +} + +export function useErroredQueuedOrders(address: Address | null): ErroredQueuedOrder[] | undefined { + const transactions = useSelectAddressTransactions(address) + return useMemo(() => { + if (!transactions) { + return + } + const erroredQueuedOrders: ErroredQueuedOrder[] = [] + for (const tx of transactions) { + if (isErroredQueuedOrder(tx)) { + erroredQueuedOrders.push(tx) + } + } + return erroredQueuedOrders.sort((a, b) => b.addedTime - a.addedTime) + }, [transactions]) +} + // sorted oldest to newest -export function useSortedPendingTransactions( - address: Address | null -): TransactionDetails[] | undefined { +export function useSortedPendingTransactions(address: Address | null): TransactionDetails[] | undefined { const transactions = usePendingTransactions(address) return useMemo(() => { if (!transactions) { return } - return transactions.sort( - (a: TransactionDetails, b: TransactionDetails) => a.addedTime - b.addedTime - ) + return transactions.sort((a: TransactionDetails, b: TransactionDetails) => a.addedTime - b.addedTime) }, [transactions]) } export function useSelectTransaction( address: Address | undefined, chainId: WalletChainId | undefined, - txId: string | undefined + txId: string | undefined, ): TransactionDetails | undefined { const selectTransaction = useMemo(makeSelectTransaction, []) return useAppSelector((state) => selectTransaction(state, { address, chainId, txId })) @@ -69,19 +104,15 @@ export function useSelectTransaction( export function useCreateSwapFormState( address: Address | undefined, chainId: WalletChainId | undefined, - txId: string | undefined + txId: string | undefined, ): TransactionState | undefined { const transaction = useSelectTransaction(address, chainId, txId) const inputCurrencyId = - transaction?.typeInfo.type === TransactionType.Swap - ? transaction.typeInfo.inputCurrencyId - : undefined + transaction?.typeInfo.type === TransactionType.Swap ? transaction.typeInfo.inputCurrencyId : undefined const outputCurrencyId = - transaction?.typeInfo.type === TransactionType.Swap - ? transaction.typeInfo.outputCurrencyId - : undefined + transaction?.typeInfo.type === TransactionType.Swap ? transaction.typeInfo.outputCurrencyId : undefined const inputCurrencyInfo = useCurrencyInfo(inputCurrencyId) const outputCurrencyInfo = useCurrencyInfo(outputCurrencyId) @@ -104,7 +135,7 @@ export function useCreateWrapFormState( chainId: WalletChainId | undefined, txId: string | undefined, inputCurrency: Maybe, - outputCurrency: Maybe + outputCurrency: Maybe, ): TransactionState | undefined { const transaction = useSelectTransaction(address, chainId, txId) @@ -126,9 +157,9 @@ export function useCreateWrapFormState( */ export function useMergeLocalAndRemoteTransactions( address: Address, - remoteTransactions: TransactionDetails[] | undefined + remoteTransactions: TransactionDetails[] | undefined, ): TransactionDetails[] | undefined { - const dispatch = useAppDispatch() + const dispatch = useDispatch() const localTransactions = useSelectAddressTransactions(address) // Merge local and remote txs into one array and reconcile data discrepancies @@ -140,36 +171,49 @@ export function useMergeLocalAndRemoteTransactions( return remoteTransactions } - const txHashes = new Set() - const offChainFiatOnRampTxs: TransactionDetails[] = [] - - const remoteTxMap: Map = new Map() - remoteTransactions.forEach((tx) => { - if (tx.hash) { + // This map enables `getTrackingHash` to deduplicate UniswapX orders in the event that one source + // has a filled order (orderHash + txHash), while the other has it pending (orderHash only). + const orderHashToTxHashMap = new Map() + function populateOrderHashToTxHashMap(tx: TransactionDetails): void { + if (isUniswapX(tx) && tx.hash && tx.orderHash) { const txHash = ensureLeading0x(tx.hash.toLowerCase()) - remoteTxMap.set(txHash, tx) - txHashes.add(txHash) - } else { - offChainFiatOnRampTxs.push(tx) + const orderHash = ensureLeading0x(tx.orderHash.toLowerCase()) + orderHashToTxHashMap.set(orderHash, txHash) } - }) + } + remoteTransactions.forEach(populateOrderHashToTxHashMap) + localTransactions.forEach(populateOrderHashToTxHashMap) - const localTxMap: Map = new Map() - localTransactions.forEach((tx) => { + /** Returns the hash that should be used to deduplicate transactions. */ + function getTrackingHash(tx: TransactionDetails): string | undefined { if (tx.hash) { - const txHash = ensureLeading0x(tx.hash.toLowerCase()) - localTxMap.set(txHash, tx) - txHashes.add(txHash) + return ensureLeading0x(tx.hash.toLowerCase()) + } else if (isUniswapX(tx) && tx.orderHash) { + const orderHash = ensureLeading0x(tx.orderHash.toLowerCase()) + return orderHashToTxHashMap.get(orderHash) ?? orderHash + } + } + + const hashes = new Set() + const offChainFiatOnRampTxs: TransactionDetails[] = [] + function addToMap(map: HashToTxMap, tx: TransactionDetails): HashToTxMap { + const hash = getTrackingHash(tx) + if (hash) { + map.set(hash, tx) + hashes.add(hash) } else { offChainFiatOnRampTxs.push(tx) } - }) + return map + } + const remoteTxMap = remoteTransactions.reduce(addToMap, new Map()) + const localTxMap = localTransactions.reduce(addToMap, new Map()) const deDupedTxs: TransactionDetails[] = [...offChainFiatOnRampTxs] - for (const txHash of [...txHashes]) { - const remoteTx = remoteTxMap.get(txHash) - const localTx = localTxMap.get(txHash) + for (const hash of [...hashes]) { + const remoteTx = remoteTxMap.get(hash) + const localTx = localTxMap.get(hash) if (!localTx) { if (!remoteTx) { throw new Error('No local or remote tx, which is not possible') @@ -220,10 +264,7 @@ export function useMergeLocalAndRemoteTransactions( return deDupedTxs.sort((a, b) => { // If inclusion times are equal, then sequence approve txs before swap txs if (a.addedTime === b.addedTime) { - if ( - a.typeInfo.type === TransactionType.Approve && - b.typeInfo.type === TransactionType.Swap - ) { + if (a.typeInfo.type === TransactionType.Approve && b.typeInfo.type === TransactionType.Swap) { const aCurrencyId = buildCurrencyId(a.chainId, a.typeInfo.tokenAddress) const bCurrencyId = b.typeInfo.inputCurrencyId if (areCurrencyIdsEqual(aCurrencyId, bCurrencyId)) { @@ -231,10 +272,7 @@ export function useMergeLocalAndRemoteTransactions( } } - if ( - a.typeInfo.type === TransactionType.Swap && - b.typeInfo.type === TransactionType.Approve - ) { + if (a.typeInfo.type === TransactionType.Swap && b.typeInfo.type === TransactionType.Approve) { const aCurrencyId = a.typeInfo.inputCurrencyId const bCurrencyId = buildCurrencyId(b.chainId, b.typeInfo.tokenAddress) if (areCurrencyIdsEqual(aCurrencyId, bCurrencyId)) { diff --git a/packages/wallet/src/features/transactions/hooks/useAllTransactionsBetweenAddresses.ts b/packages/wallet/src/features/transactions/hooks/useAllTransactionsBetweenAddresses.ts index 537352a5464..0b8178447f0 100644 --- a/packages/wallet/src/features/transactions/hooks/useAllTransactionsBetweenAddresses.ts +++ b/packages/wallet/src/features/transactions/hooks/useAllTransactionsBetweenAddresses.ts @@ -9,7 +9,7 @@ import { TransactionDetails, TransactionType } from 'wallet/src/features/transac */ export function useAllTransactionsBetweenAddresses( sender: Address, - recipient: Maybe
        + recipient: Maybe
        , ): TransactionDetails[] | undefined { const txnsToSearch = useSelectAddressTransactions(sender) return useMemo(() => { @@ -17,8 +17,7 @@ export function useAllTransactionsBetweenAddresses( return } return txnsToSearch.filter( - (tx: TransactionDetails) => - tx.typeInfo.type === TransactionType.Send && tx.typeInfo.recipient === recipient + (tx: TransactionDetails) => tx.typeInfo.type === TransactionType.Send && tx.typeInfo.recipient === recipient, ) }, [recipient, sender, txnsToSearch]) } diff --git a/packages/wallet/src/features/transactions/hooks/useParsedTransactionWarnings.tsx b/packages/wallet/src/features/transactions/hooks/useParsedTransactionWarnings.tsx index b0ed7ce679d..f8ca869e4f6 100644 --- a/packages/wallet/src/features/transactions/hooks/useParsedTransactionWarnings.tsx +++ b/packages/wallet/src/features/transactions/hooks/useParsedTransactionWarnings.tsx @@ -4,10 +4,7 @@ import { isWeb } from 'ui/src' import { AlertTriangle } from 'ui/src/components/icons' import { useSwapFormContext } from 'wallet/src/features/transactions/contexts/SwapFormContext' import { useSwapTxContext } from 'wallet/src/features/transactions/contexts/SwapTxContext' -import { - isPriceImpactWarning, - useSwapWarnings, -} from 'wallet/src/features/transactions/hooks/useSwapWarnings' +import { isPriceImpactWarning, useSwapWarnings } from 'wallet/src/features/transactions/hooks/useSwapWarnings' import { useTransactionGasWarning } from 'wallet/src/features/transactions/hooks/useTransactionGasWarning' import { Warning, @@ -67,17 +64,11 @@ export function useParsedSendWarnings(allSendWarnings: Warning[]): ParsedWarning function useFormattedWarnings(warnings: Warning[]): ParsedWarnings { return useMemo(() => { const blockingWarning = warnings.find( - (warning) => - warning.action === WarningAction.DisableReview || - warning.action === WarningAction.DisableSubmit + (warning) => warning.action === WarningAction.DisableReview || warning.action === WarningAction.DisableSubmit, ) - const insufficientBalanceWarning = warnings.find( - (warning) => warning.type === WarningLabel.InsufficientFunds - ) - const insufficientGasFundsWarning = warnings.find( - (warning) => warning.type === WarningLabel.InsufficientGasFunds - ) + const insufficientBalanceWarning = warnings.find((warning) => warning.type === WarningLabel.InsufficientFunds) + const insufficientGasFundsWarning = warnings.find((warning) => warning.type === WarningLabel.InsufficientGasFunds) const priceImpactWarning = warnings.find((warning) => isPriceImpactWarning(warning)) return { @@ -92,9 +83,7 @@ function useFormattedWarnings(warnings: Warning[]): ParsedWarnings { }, [warnings]) } -function getReviewScreenWarning( - warnings: Warning[] -): ParsedWarnings['reviewScreenWarning'] | undefined { +function getReviewScreenWarning(warnings: Warning[]): ParsedWarnings['reviewScreenWarning'] | undefined { const reviewWarning = warnings.find((warning) => warning.severity >= WarningSeverity.Medium) if (!reviewWarning) { @@ -105,12 +94,8 @@ function getReviewScreenWarning( } // This function decides which warning to show when there is more than one. -function getFormScreenWarning( - warnings: Warning[] -): ParsedWarnings['reviewScreenWarning'] | undefined { - const insufficientBalanceWarning = warnings.find( - (warning) => warning.type === WarningLabel.InsufficientFunds - ) +function getFormScreenWarning(warnings: Warning[]): ParsedWarnings['reviewScreenWarning'] | undefined { + const insufficientBalanceWarning = warnings.find((warning) => warning.type === WarningLabel.InsufficientFunds) if (insufficientBalanceWarning) { return { @@ -122,8 +107,7 @@ function getFormScreenWarning( } const formWarning = warnings.find( - (warning) => - warning.type === WarningLabel.InsufficientFunds || warning.severity >= WarningSeverity.Low + (warning) => warning.type === WarningLabel.InsufficientFunds || warning.severity >= WarningSeverity.Low, ) if (!formWarning) { @@ -133,8 +117,7 @@ function getFormScreenWarning( return getWarningWithStyle({ warning: formWarning, displayedInline: - formWarning.type !== WarningLabel.InsufficientGasFunds && - (!isWeb || !isPriceImpactWarning(formWarning)), + formWarning.type !== WarningLabel.InsufficientGasFunds && (!isWeb || !isPriceImpactWarning(formWarning)), }) } diff --git a/packages/wallet/src/features/transactions/hooks/useSwapWarnings.test.ts b/packages/wallet/src/features/transactions/hooks/useSwapWarnings.test.ts index 81a7a0aa7da..80e4725018f 100644 --- a/packages/wallet/src/features/transactions/hooks/useSwapWarnings.test.ts +++ b/packages/wallet/src/features/transactions/hooks/useSwapWarnings.test.ts @@ -1,21 +1,16 @@ import { CurrencyAmount } from '@uniswap/sdk-core' +import { DAI, USDC } from 'uniswap/src/constants/tokens' +import { NativeCurrency } from 'uniswap/src/features/tokens/NativeCurrency' +import { CurrencyField } from 'uniswap/src/features/transactions/transactionState/types' import i18n from 'uniswap/src/i18n/i18n' +import { daiCurrencyInfo, ethCurrencyInfo } from 'uniswap/src/test/fixtures' import { UniverseChainId } from 'uniswap/src/types/chains' -import { DAI, USDC } from 'wallet/src/constants/tokens' -import { NativeCurrency } from 'wallet/src/features/tokens/NativeCurrency' import { WarningLabel } from 'wallet/src/features/transactions/WarningModal/types' import { getSwapWarnings } from 'wallet/src/features/transactions/hooks/useSwapWarnings' import { DerivedSwapInfo } from 'wallet/src/features/transactions/swap/types' -import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' import { WrapType } from 'wallet/src/features/transactions/types' import { isOffline } from 'wallet/src/features/transactions/utils' -import { - daiCurrencyInfo, - ethCurrencyInfo, - networkDown, - networkUnknown, - networkUp, -} from 'wallet/src/test/fixtures' +import { networkDown, networkUnknown, networkUp } from 'wallet/src/test/fixtures' import { mockLocalizedFormatter } from 'wallet/src/test/mocks' const ETH = NativeCurrency.onChain(UniverseChainId.Mainnet) @@ -110,12 +105,7 @@ describe(getSwapWarnings, () => { }) it('catches insufficient balance errors', () => { - const warnings = getSwapWarnings( - i18n.t, - formatPercent, - insufficientBalanceState, - isOffline(networkUp()) - ) + const warnings = getSwapWarnings(i18n.t, formatPercent, insufficientBalanceState, isOffline(networkUp())) expect(warnings.length).toBe(1) expect(warnings[0]?.type).toEqual(WarningLabel.InsufficientFunds) }) @@ -133,7 +123,7 @@ describe(getSwapWarnings, () => { i18n.t, formatPercent, incompleteAndInsufficientBalanceState, - isOffline(networkUp()) + isOffline(networkUp()), ) expect(warnings.length).toBe(2) }) @@ -144,22 +134,12 @@ describe(getSwapWarnings, () => { }) it('errors if there is no internet', () => { - const warnings = getSwapWarnings( - i18n.t, - formatPercent, - tradeErrorState, - isOffline(networkDown()) - ) + const warnings = getSwapWarnings(i18n.t, formatPercent, tradeErrorState, isOffline(networkDown())) expect(warnings.find((warning) => warning.type === WarningLabel.NetworkError)).toBeTruthy() }) it('does not error when network state is unknown', () => { - const warnings = getSwapWarnings( - i18n.t, - formatPercent, - tradeErrorState, - isOffline(networkUnknown()) - ) + const warnings = getSwapWarnings(i18n.t, formatPercent, tradeErrorState, isOffline(networkUnknown())) expect(warnings.find((warning) => warning.type === WarningLabel.NetworkError)).toBeFalsy() }) }) diff --git a/packages/wallet/src/features/transactions/hooks/useSwapWarnings.tsx b/packages/wallet/src/features/transactions/hooks/useSwapWarnings.tsx index ce1e34b4294..d74e34c7b55 100644 --- a/packages/wallet/src/features/transactions/hooks/useSwapWarnings.tsx +++ b/packages/wallet/src/features/transactions/hooks/useSwapWarnings.tsx @@ -4,12 +4,10 @@ import { TFunction } from 'i18next' import _ from 'lodash' import { useTranslation } from 'react-i18next' import { isWeb } from 'ui/src' +import { CurrencyField } from 'uniswap/src/features/transactions/transactionState/types' import { normalizePriceImpact } from 'utilities/src/format/normalizePriceImpact' import { useMemoCompare } from 'utilities/src/react/hooks' -import { - LocalizationContextState, - useLocalizationContext, -} from 'wallet/src/features/language/LocalizationContext' +import { LocalizationContextState, useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' import { getNetworkWarning } from 'wallet/src/features/transactions/WarningModal/getNetworkWarning' import { Warning, @@ -21,9 +19,8 @@ import { API_RATE_LIMIT_ERROR, NO_QUOTE_DATA, SWAP_QUOTE_ERROR, -} from 'wallet/src/features/transactions/swap/trade/tradingApi/hooks/useTradingApiTrade' +} from 'wallet/src/features/transactions/swap/trade/api/hooks/useTrade' import { DerivedSwapInfo } from 'wallet/src/features/transactions/swap/types' -import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' import { isOffline } from 'wallet/src/features/transactions/utils' const PRICE_IMPACT_THRESHOLD_MEDIUM = new Percent(3, 100) // 3% @@ -33,7 +30,7 @@ export function getSwapWarnings( t: TFunction, formatPercent: LocalizationContextState['formatPercent'], derivedSwapInfo: DerivedSwapInfo, - offline: boolean + offline: boolean, ): Warning[] { const warnings: Warning[] = [] @@ -142,10 +139,7 @@ export function useSwapWarnings(derivedSwapInfo: DerivedSwapInfo): Warning[] { // See for more here: https://github.com/react-native-netinfo/react-native-netinfo/pull/444 const offline = isOffline(networkStatus) - return useMemoCompare( - () => getSwapWarnings(t, formatPercent, derivedSwapInfo, offline), - _.isEqual - ) + return useMemoCompare(() => getSwapWarnings(t, formatPercent, derivedSwapInfo, offline), _.isEqual) } const formIncomplete = (derivedSwapInfo: DerivedSwapInfo): boolean => { @@ -164,7 +158,5 @@ const formIncomplete = (derivedSwapInfo: DerivedSwapInfo): boolean => { } export function isPriceImpactWarning(warning: Warning): boolean { - return ( - warning.type === WarningLabel.PriceImpactMedium || warning.type === WarningLabel.PriceImpactHigh - ) + return warning.type === WarningLabel.PriceImpactMedium || warning.type === WarningLabel.PriceImpactHigh } diff --git a/packages/wallet/src/features/transactions/hooks/useSyncFiatAndTokenAmountUpdater.tsx b/packages/wallet/src/features/transactions/hooks/useSyncFiatAndTokenAmountUpdater.tsx index d850bca334d..88dfb9c7cb7 100644 --- a/packages/wallet/src/features/transactions/hooks/useSyncFiatAndTokenAmountUpdater.tsx +++ b/packages/wallet/src/features/transactions/hooks/useSyncFiatAndTokenAmountUpdater.tsx @@ -1,11 +1,8 @@ import { useEffect } from 'react' +import { currencyIdToChain } from 'uniswap/src/utils/currencyId' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' import { useSwapFormContext } from 'wallet/src/features/transactions/contexts/SwapFormContext' -import { - STABLECOIN_AMOUNT_OUT, - useUSDCPrice, -} from 'wallet/src/features/transactions/swap/trade/hooks/useUSDCPrice' -import { currencyIdToChain } from 'wallet/src/utils/currencyId' +import { STABLECOIN_AMOUNT_OUT, useUSDCPrice } from 'wallet/src/features/transactions/swap/trade/hooks/useUSDCPrice' import { ValueType, getCurrencyAmount } from 'wallet/src/utils/getCurrencyAmount' // Used for rounding in conversion math @@ -20,20 +17,14 @@ const NUM_DECIMALS_DISPLAY_FIAT = 2 * amount. This allows us to toggle between 2 modes, without losing the entered amount. */ export function useSyncFiatAndTokenAmountUpdater(): void { - const { - isFiatMode, - updateSwapForm, - exactAmountToken, - exactAmountFiat, - derivedSwapInfo, - exactCurrencyField, - } = useSwapFormContext() + const { isFiatMode, updateSwapForm, exactAmountToken, exactAmountFiat, derivedSwapInfo, exactCurrencyField } = + useSwapFormContext() const exactCurrency = derivedSwapInfo.currencies[exactCurrencyField] const usdPriceOfCurrency = useUSDCPrice(exactCurrency?.currency ?? undefined) const { convertFiatAmount } = useLocalizationContext() - const conversionRate = convertFiatAmount().amount + const conversionRate = convertFiatAmount(1).amount const chainId = currencyIdToChain(exactCurrency?.currencyId ?? '') useEffect(() => { @@ -42,17 +33,14 @@ export function useSyncFiatAndTokenAmountUpdater(): void { } if (isFiatMode) { - const fiatAmount = - exactAmountFiat && !isNaN(parseFloat(exactAmountFiat)) ? parseFloat(exactAmountFiat) : 0 + const fiatAmount = exactAmountFiat && !isNaN(parseFloat(exactAmountFiat)) ? parseFloat(exactAmountFiat) : 0 const usdAmount = (fiatAmount / conversionRate).toFixed(NUM_DECIMALS_FIAT_ROUNDING) const stablecoinAmount = getCurrencyAmount({ value: usdAmount, valueType: ValueType.Exact, currency: STABLECOIN_AMOUNT_OUT[chainId]?.currency, }) - const tokenAmount = stablecoinAmount - ? usdPriceOfCurrency?.invert().quote(stablecoinAmount) - : undefined + const tokenAmount = stablecoinAmount ? usdPriceOfCurrency?.invert().quote(stablecoinAmount) : undefined updateSwapForm({ exactAmountToken: tokenAmount?.toExact() }) } diff --git a/packages/wallet/src/features/transactions/hooks/useTokenAndFiatDisplayAmounts.tsx b/packages/wallet/src/features/transactions/hooks/useTokenAndFiatDisplayAmounts.tsx index d0e4a4ac5d6..7c08f8bab1c 100644 --- a/packages/wallet/src/features/transactions/hooks/useTokenAndFiatDisplayAmounts.tsx +++ b/packages/wallet/src/features/transactions/hooks/useTokenAndFiatDisplayAmounts.tsx @@ -30,17 +30,13 @@ export function useTokenAndFiatDisplayAmounts({ isFiatMode, }: FormattedDisplayAmountsProps): string { const appFiatCurrency = useAppFiatCurrencyInfo() - const { convertFiatAmountFormatted, formatCurrencyAmount, addFiatSymbolToNumber } = - useLocalizationContext() + const { convertFiatAmountFormatted, formatCurrencyAmount, addFiatSymbolToNumber } = useLocalizationContext() const formattedCurrencyAmount = currencyAmount ? formatCurrencyAmount({ value: currencyAmount, type: NumberType.TokenTx }) : '' - const formattedFiatValue: string = convertFiatAmountFormatted( - usdValue?.toExact(), - NumberType.FiatTokenQuantity - ) + const formattedFiatValue: string = convertFiatAmountFormatted(usdValue?.toExact(), NumberType.FiatTokenQuantity) // In fiat mode, show equivalent token amount. In token mode, show equivalent fiat amount return useMemo((): string => { diff --git a/packages/wallet/src/features/transactions/hooks/useTokenFormActionHandlers.ts b/packages/wallet/src/features/transactions/hooks/useTokenFormActionHandlers.ts index 4bcb5636e42..e58e31579e4 100644 --- a/packages/wallet/src/features/transactions/hooks/useTokenFormActionHandlers.ts +++ b/packages/wallet/src/features/transactions/hooks/useTokenFormActionHandlers.ts @@ -1,7 +1,7 @@ import { AnyAction } from '@reduxjs/toolkit' import { useCallback } from 'react' +import { CurrencyField } from 'uniswap/src/features/transactions/transactionState/types' import { transactionStateActions } from 'wallet/src/features/transactions/transactionState/transactionState' -import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' /** Set of handlers wrapping actions involving user input */ export function useTokenFormActionHandlers(dispatch: React.Dispatch): { @@ -16,13 +16,13 @@ export function useTokenFormActionHandlers(dispatch: React.Dispatch): const onUpdateExactTokenAmount = useCallback( (field: CurrencyField, amount: string) => dispatch(transactionStateActions.updateExactAmountToken({ field, amount })), - [dispatch] + [dispatch], ) const onUpdateExactUSDAmount = useCallback( (field: CurrencyField, amount: string) => dispatch(transactionStateActions.updateExactAmountFiat({ field, amount })), - [dispatch] + [dispatch], ) const onSetExactAmount = useCallback( @@ -30,7 +30,7 @@ export function useTokenFormActionHandlers(dispatch: React.Dispatch): const updater = isFiatInput ? onUpdateExactUSDAmount : onUpdateExactTokenAmount updater(field, value) }, - [onUpdateExactUSDAmount, onUpdateExactTokenAmount] + [onUpdateExactUSDAmount, onUpdateExactTokenAmount], ) const onSetMax = useCallback( @@ -38,13 +38,11 @@ export function useTokenFormActionHandlers(dispatch: React.Dispatch): // when setting max amount, always switch to token mode because // our token/usd updater doesnt handle this case yet dispatch(transactionStateActions.toggleFiatInput(false)) - dispatch( - transactionStateActions.updateExactAmountToken({ field: CurrencyField.INPUT, amount }) - ) + dispatch(transactionStateActions.updateExactAmountToken({ field: CurrencyField.INPUT, amount })) // Unfocus the CurrencyInputField by setting focusOnCurrencyField to null dispatch(transactionStateActions.onFocus(null)) }, - [dispatch] + [dispatch], ) const onSwitchCurrencies = useCallback(() => { @@ -53,22 +51,13 @@ export function useTokenFormActionHandlers(dispatch: React.Dispatch): const onToggleFiatInput = useCallback( (isFiatInput: boolean) => dispatch(transactionStateActions.toggleFiatInput(isFiatInput)), - [dispatch] + [dispatch], ) - const onCreateTxId = useCallback( - (txId: string) => dispatch(transactionStateActions.setTxId(txId)), - [dispatch] - ) + const onCreateTxId = useCallback((txId: string) => dispatch(transactionStateActions.setTxId(txId)), [dispatch]) - const onFocusInput = useCallback( - () => dispatch(transactionStateActions.onFocus(CurrencyField.INPUT)), - [dispatch] - ) - const onFocusOutput = useCallback( - () => dispatch(transactionStateActions.onFocus(CurrencyField.OUTPUT)), - [dispatch] - ) + const onFocusInput = useCallback(() => dispatch(transactionStateActions.onFocus(CurrencyField.INPUT)), [dispatch]) + const onFocusOutput = useCallback(() => dispatch(transactionStateActions.onFocus(CurrencyField.OUTPUT)), [dispatch]) return { onCreateTxId, onFocusInput, diff --git a/packages/wallet/src/features/transactions/hooks/useTokenSelectorActionHandlers.ts b/packages/wallet/src/features/transactions/hooks/useTokenSelectorActionHandlers.ts index ed1b9bbfe58..ce1a4158995 100644 --- a/packages/wallet/src/features/transactions/hooks/useTokenSelectorActionHandlers.ts +++ b/packages/wallet/src/features/transactions/hooks/useTokenSelectorActionHandlers.ts @@ -1,19 +1,19 @@ import { AnyAction } from '@reduxjs/toolkit' import { Currency } from '@uniswap/sdk-core' import { useCallback } from 'react' +import { AssetType } from 'uniswap/src/entities/assets' +import { SearchContext } from 'uniswap/src/features/search/SearchContext' import { WalletEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' +import { CurrencyField } from 'uniswap/src/features/transactions/transactionState/types' +import { TokenSelectorFlow } from 'uniswap/src/features/transactions/transfer/types' +import { currencyAddress } from 'uniswap/src/utils/currencyId' import { flowToModalName } from 'wallet/src/components/TokenSelector/flowToModalName' -import { AssetType } from 'wallet/src/entities/assets' -import { SearchContext } from 'wallet/src/features/search/SearchContext' import { transactionStateActions } from 'wallet/src/features/transactions/transactionState/transactionState' -import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' -import { TokenSelectorFlow } from 'wallet/src/features/transactions/transfer/types' -import { currencyAddress } from 'wallet/src/utils/currencyId' export function useTokenSelectorActionHandlers( dispatch: React.Dispatch, - flow: TokenSelectorFlow + flow: TokenSelectorFlow, ): { onShowTokenSelector: (field: CurrencyField) => void onHideTokenSelector: () => void @@ -21,12 +21,12 @@ export function useTokenSelectorActionHandlers( } { const onShowTokenSelector = useCallback( (field: CurrencyField) => dispatch(transactionStateActions.showTokenSelector(field)), - [dispatch] + [dispatch], ) const onHideTokenSelector = useCallback( () => dispatch(transactionStateActions.showTokenSelector(undefined)), - [dispatch] + [dispatch], ) const onSelectCurrency = useCallback( @@ -39,7 +39,7 @@ export function useTokenSelectorActionHandlers( chainId: currency.chainId, type: AssetType.Currency, }, - }) + }), ) // log event that a currency was selected @@ -58,7 +58,7 @@ export function useTokenSelectorActionHandlers( // hide screen when done selecting onHideTokenSelector() }, - [dispatch, flow, onHideTokenSelector] + [dispatch, flow, onHideTokenSelector], ) return { onSelectCurrency, onShowTokenSelector, onHideTokenSelector } } diff --git a/packages/wallet/src/features/transactions/hooks/useTransactionGasWarning.tsx b/packages/wallet/src/features/transactions/hooks/useTransactionGasWarning.tsx index aaa1eb208db..f7717c95368 100644 --- a/packages/wallet/src/features/transactions/hooks/useTransactionGasWarning.tsx +++ b/packages/wallet/src/features/transactions/hooks/useTransactionGasWarning.tsx @@ -2,17 +2,17 @@ import { CurrencyAmount, NativeCurrency } from '@uniswap/sdk-core' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { isWeb } from 'ui/src' +import { CurrencyField } from 'uniswap/src/features/transactions/transactionState/types' import { useOnChainNativeCurrencyBalance } from 'wallet/src/features/portfolio/api' -import { DerivedSwapInfo } from 'wallet/src/features/transactions/swap/types' -import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' -import { DerivedTransferInfo } from 'wallet/src/features/transactions/transfer/types' -import { hasSufficientFundsIncludingGas } from 'wallet/src/features/transactions/utils' import { Warning, WarningAction, WarningLabel, WarningSeverity, } from 'wallet/src/features/transactions/WarningModal/types' +import { DerivedSwapInfo } from 'wallet/src/features/transactions/swap/types' +import { DerivedTransferInfo } from 'wallet/src/features/transactions/transfer/types' +import { hasSufficientFundsIncludingGas } from 'wallet/src/features/transactions/utils' import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' export function useTransactionGasWarning({ diff --git a/packages/wallet/src/features/transactions/orderWatcherSaga.ts b/packages/wallet/src/features/transactions/orderWatcherSaga.ts new file mode 100644 index 00000000000..bb7a34ce241 --- /dev/null +++ b/packages/wallet/src/features/transactions/orderWatcherSaga.ts @@ -0,0 +1,154 @@ +import axios from 'axios' +import { call, delay, fork, select, take } from 'typed-redux-saga' +import { uniswapUrls } from 'uniswap/src/constants/urls' +import { logger } from 'utilities/src/logger/logger' +import { ONE_SECOND_MS } from 'utilities/src/time/time' +import { GetOrdersResponse } from 'wallet/src/data/tradingApi/__generated__/index' +import { makeSelectUniswapXOrder } from 'wallet/src/features/transactions/selectors' +import { updateTransaction } from 'wallet/src/features/transactions/slice' +import { TRADING_API_HEADERS } from 'wallet/src/features/transactions/swap/trade/api/client' +import { ORDER_STATUS_TO_TX_STATUS } from 'wallet/src/features/transactions/swap/trade/api/utils' +import { isUniswapX } from 'wallet/src/features/transactions/swap/trade/utils' +import { + QueuedOrderStatus, + TransactionStatus, + UniswapXOrderDetails, + isFinalizedTxStatus, +} from 'wallet/src/features/transactions/types' + +// If the backend cannot provide a status for an order, we can assume after a certain threshold the submission failed. +const ORDER_TIMEOUT_BUFFER = 20 * ONE_SECOND_MS + +export async function getOrders(orderIds: string[]): Promise { + const orderIdsString = orderIds.join(',') + const ordersEndpoint = uniswapUrls.tradingApiUrl + uniswapUrls.tradingApiPaths.orders + const urlParams = `?orderIds=${encodeURIComponent(orderIdsString)}` + + return (await axios.get(ordersEndpoint + urlParams, { headers: TRADING_API_HEADERS })).data as GetOrdersResponse +} + +const selectUniswapXOrder = makeSelectUniswapXOrder() + +export class OrderWatcher { + private static listeners: { + [orderHash: string]: { + updateOrderStatus: (updatedOrder: UniswapXOrderDetails) => void + promise: Promise + } + } = {} + + // There is an issue on extension where the sagas are initialized multiple times. + // The first instance of this polling utility will not have access to the latest store. + // As a temporary fix, we can use an index to track & cancel the previous instance of the polling utility. + private static index = 0 + + static *initialize(): Generator { + OrderWatcher.index++ + yield* fork(OrderWatcher.poll, OrderWatcher.index) + } + + private static *poll(index: number): Generator { + if (index !== OrderWatcher.index) { + return + } + + yield* delay(2000) // Poll at 2s intervals + + const orderHashes = Object.keys(OrderWatcher.listeners) + if (!orderHashes.length) { + yield* fork(OrderWatcher.poll, index) + return + } + + try { + const data = yield* call(getOrders, orderHashes) + const remoteOrderMap = new Map(data.orders.map((order) => [order.orderId, order])) + + for (const localOrderHash of orderHashes) { + const remoteOrder = remoteOrderMap.get(localOrderHash) + const localOrder = yield* select(selectUniswapXOrder, { orderHash: localOrderHash }) + + if (!localOrder?.orderHash) { + continue + } + + // If submission fails, stop polling for this order. + if (localOrder.queueStatus === QueuedOrderStatus.SubmissionFailed) { + delete OrderWatcher.listeners[localOrder.orderHash] + continue + } + + // If the backend does not have data for an order marked locally as submitted and enough time has + // passed, we can assume the submission call to the tradingAPI failed and update state accordingly + if (!remoteOrder) { + if (Date.now() - localOrder.addedTime > ORDER_TIMEOUT_BUFFER) { + OrderWatcher.listeners[localOrderHash]?.updateOrderStatus({ + ...localOrder, + queueStatus: QueuedOrderStatus.SubmissionFailed, + }) + delete OrderWatcher.listeners[localOrder.orderHash] + } + continue + } + + const updatedStatus = ORDER_STATUS_TO_TX_STATUS[remoteOrder.orderStatus] + + const isUnchanged = updatedStatus === localOrder?.status + const isFinal = isFinalizedTxStatus(updatedStatus) + + // Ignore non-final order statuses if the tx is being cancelled locally; the backend is not yet aware of cancellation + const isOngoingCancel = !isFinal && localOrder?.status === TransactionStatus.Cancelling + + if (isUnchanged || isOngoingCancel) { + continue + } + + OrderWatcher.listeners[localOrder.orderHash]?.updateOrderStatus({ + ...localOrder, + status: updatedStatus, + hash: remoteOrder.txHash, + }) + delete OrderWatcher.listeners[localOrder.orderHash] + } + } catch (error) { + logger.error(error, { + tags: { + file: 'orderWatcherSaga', + function: 'orderWatcher', + }, + }) + } + + yield* fork(OrderWatcher.poll, index) + } + + static *waitForOrderStatus(orderHash: string, queueStatus: QueuedOrderStatus) { + // Avoid polling until the order has been submitted + if (queueStatus !== QueuedOrderStatus.Submitted) { + while (true) { + const { payload } = yield* take>(updateTransaction.type) + if ( + isUniswapX(payload) && + payload.orderHash === orderHash && + payload.queueStatus === QueuedOrderStatus.Submitted + ) { + break + } + } + } + + const existingListenerPromise = OrderWatcher.listeners[orderHash]?.promise + if (existingListenerPromise) { + return yield* call(() => existingListenerPromise) + } + + let resolvePromise: (value: UniswapXOrderDetails) => void + const promise = new Promise((resolve) => { + resolvePromise = resolve + }) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Must appease typechecker since resolvePromise is assigned inside promise scope + OrderWatcher.listeners[orderHash] = { updateOrderStatus: resolvePromise!, promise } + + return yield* call(() => promise) + } +} diff --git a/packages/wallet/src/features/transactions/refetchGQLQueriesSaga.ts b/packages/wallet/src/features/transactions/refetchGQLQueriesSaga.ts index df17438ffb6..0e82775e53f 100644 --- a/packages/wallet/src/features/transactions/refetchGQLQueriesSaga.ts +++ b/packages/wallet/src/features/transactions/refetchGQLQueriesSaga.ts @@ -1,22 +1,19 @@ import { ApolloClient, NormalizedCacheObject } from '@apollo/client' -import { call, delay } from 'typed-redux-saga' +import { call, delay, select } from 'typed-redux-saga' +import { getNativeAddress } from 'uniswap/src/constants/addresses' import { PortfolioBalancesDocument, PortfolioBalancesQuery, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { fromGraphQLChain } from 'uniswap/src/features/chains/utils' import { WalletEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { CurrencyId } from 'uniswap/src/types/currency' import { ONE_SECOND_MS } from 'utilities/src/time/time' -import { getNativeAddress } from 'wallet/src/constants/addresses' -import { fromGraphQLChain } from 'wallet/src/features/chains/utils' import { GQL_QUERIES_TO_REFETCH_ON_TXN_UPDATE } from 'wallet/src/features/transactions/TransactionHistoryUpdater' import { TransactionDetails, TransactionType } from 'wallet/src/features/transactions/types' -import { - buildCurrencyId, - buildNativeCurrencyId, - buildWrappedNativeCurrencyId, -} from 'wallet/src/utils/currencyId' +import { selectActiveAccountAddress } from 'wallet/src/features/wallet/selectors' +import { buildCurrencyId, buildNativeCurrencyId, buildWrappedNativeCurrencyId } from 'wallet/src/utils/currencyId' type CurrencyIdToBalance = Record @@ -38,7 +35,13 @@ export function* refetchGQLQueries({ apolloClient, }) - // when there is a new local tx wait 1s then proactively refresh portfolio and activity queries + const activeAddress = yield* select(selectActiveAccountAddress) + if (owner !== activeAddress) { + // We can ignore if the transaction does not belong to the active account. + return + } + + // when there is a new local tx wait REFETCH_INTERVAL then proactively refresh portfolio and activity queries yield* delay(REFETCH_INTERVAL) yield* call([apolloClient, apolloClient.refetchQueries], { @@ -50,7 +53,7 @@ export function* refetchGQLQueries({ } let freshnessLag = REFETCH_INTERVAL - // poll every second until the cache has updated balances for the relevant currencies + // poll every REFETCH_INTERVAL until the cache has updated balances for the relevant currencies for (let i = 0; i < MAX_REFETCH_ATTEMPTS; i += 1) { const currencyIdToUpdatedBalance = readBalancesFromCache({ owner, @@ -64,6 +67,13 @@ export function* refetchGQLQueries({ yield* delay(REFETCH_INTERVAL) + const currentActiveAddress = yield* select(selectActiveAccountAddress) + if (owner !== currentActiveAddress) { + // We stop polling if the user has switched accounts. + // A call to `refetchQueries` wouldn't be useful in this case because no query with the transaction's owner is currently being watched. + break + } + yield* call([apolloClient, apolloClient.refetchQueries], { include: GQL_QUERIES_TO_REFETCH_ON_TXN_UPDATE, }) @@ -78,9 +88,7 @@ export function* refetchGQLQueries({ } // based on transaction data, determine which currencies we expect to see a balance update on -function getCurrenciesWithExpectedUpdates( - transaction: TransactionDetails -): Set | undefined { +function getCurrenciesWithExpectedUpdates(transaction: TransactionDetails): Set | undefined { const currenciesWithBalToUpdate: Set = new Set() const txChainId = transaction.chainId @@ -97,9 +105,7 @@ function getCurrenciesWithExpectedUpdates( currenciesWithBalToUpdate.add(transaction.typeInfo.outputCurrencyId.toLowerCase()) break case TransactionType.Send: - currenciesWithBalToUpdate.add( - buildCurrencyId(txChainId, transaction.typeInfo.tokenAddress).toLowerCase() - ) + currenciesWithBalToUpdate.add(buildCurrencyId(txChainId, transaction.typeInfo.tokenAddress).toLowerCase()) break case TransactionType.Wrap: currenciesWithBalToUpdate.add(buildWrappedNativeCurrencyId(txChainId)) @@ -125,7 +131,7 @@ function readBalancesFromCache({ const currencyIdToBalance: CurrencyIdToBalance = Array.from(currencyIdsToUpdate).reduce( (currIdToBal, currencyId) => ({ ...currIdToBal, [currencyId]: 0 }), // assume 0 balance and update later if found in cache - {} + {}, ) const cachedBalancesData = apolloClient.readQuery({ @@ -157,10 +163,7 @@ function readBalancesFromCache({ return currencyIdToBalance } -function checkIfBalancesUpdated( - balance1: CurrencyIdToBalance, - balance2: Maybe -) { +function checkIfBalancesUpdated(balance1: CurrencyIdToBalance, balance2: Maybe) { if (!balance2) { return true } // if no currencies to check, then assume balances are updated diff --git a/packages/wallet/src/features/transactions/replaceTransactionSaga.test.ts b/packages/wallet/src/features/transactions/replaceTransactionSaga.test.ts index 40a67eb91a4..e6c8a0a5d25 100644 --- a/packages/wallet/src/features/transactions/replaceTransactionSaga.test.ts +++ b/packages/wallet/src/features/transactions/replaceTransactionSaga.test.ts @@ -6,25 +6,13 @@ import { expectSaga } from 'redux-saga-test-plan' import { call } from 'redux-saga/effects' import { Routing } from 'wallet/src/data/tradingApi/__generated__/index' import { attemptReplaceTransaction } from 'wallet/src/features/transactions/replaceTransactionSaga' -import { - sendTransaction, - signAndSendTransaction, -} from 'wallet/src/features/transactions/sendTransactionSaga' +import { sendTransaction, signAndSendTransaction } from 'wallet/src/features/transactions/sendTransactionSaga' import { addTransaction } from 'wallet/src/features/transactions/slice' import { TransactionStatus } from 'wallet/src/features/transactions/types' import * as TxnUtils from 'wallet/src/features/transactions/utils' -import { - getProvider, - getProviderManager, - getSignerManager, -} from 'wallet/src/features/wallet/context' +import { getProvider, getProviderManager, getSignerManager } from 'wallet/src/features/wallet/context' import { selectAccounts } from 'wallet/src/features/wallet/selectors' -import { - ACCOUNT, - ethersTransactionRequest, - getTxFixtures, - transactionDetails, -} from 'wallet/src/test/fixtures' +import { ACCOUNT, ethersTransactionRequest, getTxFixtures, transactionDetails } from 'wallet/src/test/fixtures' import { provider, providerManager, signerManager } from 'wallet/src/test/mocks' const NEW_UNIQUE_ID = faker.datatype.uuid() @@ -83,7 +71,7 @@ describe(sendTransaction, () => { transaction.options.request, ACCOUNT, provider as providers.Provider, - signerManager + signerManager, ), { transactionResponse: txResponse, populatedRequest: txRequest }, ], @@ -114,7 +102,7 @@ describe(sendTransaction, () => { maxFeePerGas: undefined, }, }, - }) + }), ) .silentRun() }) diff --git a/packages/wallet/src/features/transactions/replaceTransactionSaga.ts b/packages/wallet/src/features/transactions/replaceTransactionSaga.ts index 62ccab26a55..d0c404b0391 100644 --- a/packages/wallet/src/features/transactions/replaceTransactionSaga.ts +++ b/packages/wallet/src/features/transactions/replaceTransactionSaga.ts @@ -1,6 +1,7 @@ import { BigNumber, providers } from 'ethers' import { call, put } from 'typed-redux-saga' import i18n from 'uniswap/src/i18n/i18n' +import { getValidAddress } from 'uniswap/src/utils/addresses' import { logger } from 'utilities/src/logger/logger' import { pushNotification } from 'wallet/src/features/notifications/slice' import { AppNotificationType } from 'wallet/src/features/notifications/types' @@ -11,19 +12,15 @@ import { TransactionDetails, TransactionStatus, } from 'wallet/src/features/transactions/types' -import { - createTransactionId, - getSerializableTransactionRequest, -} from 'wallet/src/features/transactions/utils' +import { createTransactionId, getSerializableTransactionRequest } from 'wallet/src/features/transactions/utils' import { getProvider, getSignerManager } from 'wallet/src/features/wallet/context' import { selectAccounts } from 'wallet/src/features/wallet/selectors' import { appSelect } from 'wallet/src/state' -import { getValidAddress } from 'wallet/src/utils/addresses' export function* attemptReplaceTransaction( transaction: ClassicTransactionDetails, newTxRequest: providers.TransactionRequest, - isCancellation = false + isCancellation = false, ) { const { chainId, hash, options } = transaction logger.debug('replaceTransaction', '', 'Attempting tx replacement', hash) @@ -59,7 +56,7 @@ export function* attemptReplaceTransaction( request, account, provider, - signerManager + signerManager, ) logger.debug('replaceTransaction', '', 'Tx submitted. New hash:', transactionResponse.hash) @@ -92,7 +89,7 @@ export function* attemptReplaceTransaction( address: transaction.from, id: replacementTxnId, chainId: transaction.chainId, - }) + }), ) yield* put( @@ -102,7 +99,7 @@ export function* attemptReplaceTransaction( errorMessage: isCancellation ? i18n.t('transaction.notification.error.cancel') : i18n.t('transaction.notification.error.replace'), - }) + }), ) } } diff --git a/packages/wallet/src/features/transactions/selectors.ts b/packages/wallet/src/features/transactions/selectors.ts index 97baff1c9fb..3dc67af0f17 100644 --- a/packages/wallet/src/features/transactions/selectors.ts +++ b/packages/wallet/src/features/transactions/selectors.ts @@ -1,6 +1,7 @@ import { createSelector, Selector } from '@reduxjs/toolkit' import { useMemo } from 'react' import { WalletChainId } from 'uniswap/src/types/chains' +import { buildCurrencyId } from 'uniswap/src/utils/currencyId' import { unique } from 'utilities/src/primitives/array' import { flattenObjectOfObjects } from 'utilities/src/primitives/objects' import { SearchableRecipient } from 'wallet/src/features/address/types' @@ -8,16 +9,16 @@ import { uniqueAddressesOnly } from 'wallet/src/features/address/utils' import { selectTokensVisibility } from 'wallet/src/features/favorites/selectors' import { CurrencyIdToVisibility } from 'wallet/src/features/favorites/slice' import { TransactionStateMap } from 'wallet/src/features/transactions/slice' -import { isClassic } from 'wallet/src/features/transactions/swap/trade/utils' +import { isClassic, isUniswapX } from 'wallet/src/features/transactions/swap/trade/utils' import { + isFinalizedTx, SendTokenTransactionInfo, TransactionDetails, - TransactionStatus, TransactionType, + UniswapXOrderDetails, } from 'wallet/src/features/transactions/types' import { useAccounts } from 'wallet/src/features/wallet/hooks' import { RootState, useAppSelector } from 'wallet/src/state' -import { buildCurrencyId } from 'wallet/src/utils/currencyId' export const selectTransactions = (state: RootState): TransactionStateMap => state.transactions @@ -69,19 +70,17 @@ export const makeSelectAddressTransactions = (): Selector< tx2.options.request.chainId && tx2.options.request.chainId === tx.options.request.chainId && tx.options.request.nonce && - tx2.options.request.nonce === tx.options.request.nonce + tx2.options.request.nonce === tx.options.request.nonce, ) if (duplicate) { return tx.addedTime > duplicate.addedTime } return true }) - } + }, ) -export function useSelectAddressTransactions( - address: Address | null -): TransactionDetails[] | undefined { +export function useSelectAddressTransactions(address: Address | null): TransactionDetails[] | undefined { const selectAddressTransactions = useMemo(makeSelectAddressTransactions, []) return useAppSelector((state) => selectAddressTransactions(state, address)) } @@ -90,15 +89,13 @@ export function useCurrencyIdToVisibility(): CurrencyIdToVisibility { const accounts = useAccounts() const addresses = Object.values(accounts).map((account) => account.address) const manuallySetTokenVisibility = useAppSelector(selectTokensVisibility) - const selectLocalTxCurrencyIds: ( - state: RootState, - addresses: Address[] - ) => CurrencyIdToVisibility = useMemo(makeSelectTokenVisibilityFromLocalTxs, []) - - const tokenVisibilityFromLocalTxs = useAppSelector((state) => - selectLocalTxCurrencyIds(state, addresses) + const selectLocalTxCurrencyIds: (state: RootState, addresses: Address[]) => CurrencyIdToVisibility = useMemo( + makeSelectTokenVisibilityFromLocalTxs, + [], ) + const tokenVisibilityFromLocalTxs = useAppSelector((state) => selectLocalTxCurrencyIds(state, addresses)) + return { ...tokenVisibilityFromLocalTxs, // Tokens the user has individually shown/hidden in the app should take preference over local txs @@ -106,11 +103,7 @@ export function useCurrencyIdToVisibility(): CurrencyIdToVisibility { } } -const makeSelectTokenVisibilityFromLocalTxs = (): Selector< - RootState, - CurrencyIdToVisibility, - [Address[]] -> => +const makeSelectTokenVisibilityFromLocalTxs = (): Selector => createSelector( selectTransactions, (_: RootState, addresses: Address[]) => addresses, @@ -133,7 +126,7 @@ const makeSelectTokenVisibilityFromLocalTxs = (): Selector< }) return acc - }, {}) + }, {}), ) interface MakeSelectParams { @@ -142,11 +135,7 @@ interface MakeSelectParams { txId: string | undefined } -export const makeSelectTransaction = (): Selector< - RootState, - TransactionDetails | undefined, - [MakeSelectParams] -> => +export const makeSelectTransaction = (): Selector => createSelector( selectTransactions, (_: RootState, { address, chainId, txId }: MakeSelectParams) => ({ @@ -165,22 +154,41 @@ export const makeSelectTransaction = (): Selector< } return Object.values(addressTxs).find((txDetails) => txDetails.id === txId) - } + }, ) +interface MakeSelectOrderParams { + orderHash: string +} + +export const makeSelectUniswapXOrder = (): Selector< + RootState, + UniswapXOrderDetails | undefined, + [MakeSelectOrderParams] +> => + createSelector( + selectTransactions, + (_: RootState, { orderHash }: MakeSelectOrderParams) => ({ orderHash }), + (transactions, { orderHash }): UniswapXOrderDetails | undefined => { + for (const transactionsForChain of flattenObjectOfObjects(transactions)) { + for (const tx of Object.values(transactionsForChain)) { + if (isUniswapX(tx) && tx.orderHash === orderHash) { + return tx + } + } + } + }, + ) // Returns a list of past recipients ordered from most to least recent // TODO: [MOB-232] either revert this to return addresses or keep but also return displayName so that it's searchable for RecipientSelect export const selectRecipientsByRecency = (state: RootState): SearchableRecipient[] => { const transactionsByChainId = flattenObjectOfObjects(state.transactions) - const sendTransactions = transactionsByChainId.reduce( - (accum, transactions) => { - const sendTransactionsWithRecipients = Object.values(transactions).filter( - (tx) => tx.typeInfo.type === TransactionType.Send && tx.typeInfo.recipient - ) - return [...accum, ...sendTransactionsWithRecipients] - }, - [] - ) + const sendTransactions = transactionsByChainId.reduce((accum, transactions) => { + const sendTransactionsWithRecipients = Object.values(transactions).filter( + (tx) => tx.typeInfo.type === TransactionType.Send && tx.typeInfo.recipient, + ) + return [...accum, ...sendTransactionsWithRecipients] + }, []) const sortedRecipients = sendTransactions .sort((a, b) => (a.addedTime < b.addedTime ? 1 : -1)) .map((transaction) => { @@ -195,12 +203,7 @@ export const selectRecipientsByRecency = (state: RootState): SearchableRecipient export const selectIncompleteTransactions = (state: RootState): TransactionDetails[] => { const transactionsByChainId = flattenObjectOfObjects(state.transactions) return transactionsByChainId.reduce((accum, transactions) => { - const pendingTxs = Object.values(transactions).filter( - (tx) => - Boolean(!tx.receipt) && - tx.status !== TransactionStatus.Failed && - tx.status !== TransactionStatus.Success - ) + const pendingTxs = Object.values(transactions).filter((tx) => Boolean(!tx.receipt) && !isFinalizedTx(tx)) return [...accum, ...pendingTxs] }, []) } diff --git a/packages/wallet/src/features/transactions/sendTransactionSaga.test.ts b/packages/wallet/src/features/transactions/sendTransactionSaga.test.ts index b03225d0b2d..7b580debea3 100644 --- a/packages/wallet/src/features/transactions/sendTransactionSaga.test.ts +++ b/packages/wallet/src/features/transactions/sendTransactionSaga.test.ts @@ -4,18 +4,11 @@ import { expectSaga } from 'redux-saga-test-plan' import { call } from 'redux-saga/effects' import { UniverseChainId, WalletChainId } from 'uniswap/src/types/chains' import { Routing } from 'wallet/src/data/tradingApi/__generated__/index' -import { - sendTransaction, - signAndSendTransaction, -} from 'wallet/src/features/transactions/sendTransactionSaga' +import { sendTransaction, signAndSendTransaction } from 'wallet/src/features/transactions/sendTransactionSaga' import { addTransaction } from 'wallet/src/features/transactions/slice' import { TransactionStatus } from 'wallet/src/features/transactions/types' import { AccountType, ReadOnlyAccount } from 'wallet/src/features/wallet/accounts/types' -import { - getProvider, - getProviderManager, - getSignerManager, -} from 'wallet/src/features/wallet/context' +import { getProvider, getProviderManager, getSignerManager } from 'wallet/src/features/wallet/context' import { getTxFixtures, signerMnemonicAccount } from 'wallet/src/test/fixtures' import { noOpFunction, provider, providerManager, signerManager } from 'wallet/src/test/mocks' @@ -52,13 +45,7 @@ describe(sendTransaction, () => { [call(getProviderManager), providerManager], [call(getSignerManager), signerManager], [ - call( - signAndSendTransaction, - txRequest, - account, - provider as providers.Provider, - signerManager - ), + call(signAndSendTransaction, txRequest, account, provider as providers.Provider, signerManager), { transactionResponse: txResponse, populatedRequest: txRequest }, ], ]) @@ -87,7 +74,7 @@ describe(sendTransaction, () => { maxFeePerGas: undefined, }, }, - }) + }), ) .silentRun() }) @@ -104,8 +91,6 @@ describe(sendTransaction, () => { ...sendParams, account: readOnlyAccount, } - return expectSaga(sendTransaction, params) - .throws(new Error('Account must support signing')) - .silentRun() + return expectSaga(sendTransaction, params).throws(new Error('Account must support signing')).silentRun() }) }) diff --git a/packages/wallet/src/features/transactions/sendTransactionSaga.ts b/packages/wallet/src/features/transactions/sendTransactionSaga.ts index ea38fef563b..ee50915027b 100644 --- a/packages/wallet/src/features/transactions/sendTransactionSaga.ts +++ b/packages/wallet/src/features/transactions/sendTransactionSaga.ts @@ -15,10 +15,7 @@ import { TransactionType, TransactionTypeInfo, } from 'wallet/src/features/transactions/types' -import { - createTransactionId, - getSerializableTransactionRequest, -} from 'wallet/src/features/transactions/utils' +import { createTransactionId, getSerializableTransactionRequest } from 'wallet/src/features/transactions/utils' import { Account, AccountType } from 'wallet/src/features/wallet/accounts/types' import { getProvider, getSignerManager } from 'wallet/src/features/wallet/context' import { SignerManager } from 'wallet/src/features/wallet/signing/SignerManager' @@ -42,11 +39,7 @@ export function* sendTransaction(params: SendTransactionParams) { const { chainId, account, options } = params const request = options.request - logger.debug( - 'sendTransaction', - '', - `Sending tx on ${UNIVERSE_CHAIN_INFO[chainId].label} to ${request.to}` - ) + logger.debug('sendTransaction', '', `Sending tx on ${UNIVERSE_CHAIN_INFO[chainId].label} to ${request.to}`) if (account.type === AccountType.Readonly) { throw new Error('Account must support signing') @@ -61,7 +54,7 @@ export function* sendTransaction(params: SendTransactionParams) { request, account, provider, - signerManager + signerManager, ) logger.debug('sendTransaction', '', 'Tx submitted:', transactionResponse.hash) @@ -74,7 +67,7 @@ export async function signAndSendTransaction( request: providers.TransactionRequest, account: Account, provider: providers.Provider, - signerManager: SignerManager + signerManager: SignerManager, ): Promise<{ transactionResponse: providers.TransactionResponse populatedRequest: providers.TransactionRequest @@ -91,7 +84,7 @@ export async function signAndSendTransaction( function* addTransaction( { chainId, typeInfo, account, options, txId, analytics }: SendTransactionParams, hash: string, - populatedRequest: providers.TransactionRequest + populatedRequest: providers.TransactionRequest, ) { const id = txId ?? createTransactionId() const request = getSerializableTransactionRequest(populatedRequest, chainId) diff --git a/packages/wallet/src/features/transactions/slice.test.ts b/packages/wallet/src/features/transactions/slice.test.ts index 58b994d8a8d..c9eaf9d7696 100644 --- a/packages/wallet/src/features/transactions/slice.test.ts +++ b/packages/wallet/src/features/transactions/slice.test.ts @@ -59,7 +59,7 @@ describe('transaction reducer', () => { typeInfo: approveTxTypeInfo, status: TransactionStatus.Pending, addedTime: Date.now(), - }) + }), ) const txs = store.getState()[address] expect(txs?.[UniverseChainId.Mainnet]).toBeTruthy() @@ -90,7 +90,7 @@ describe('transaction reducer', () => { typeInfo: approveTxTypeInfo, status: TransactionStatus.Pending, addedTime: Date.now(), - }) + }), ) try { @@ -105,7 +105,7 @@ describe('transaction reducer', () => { typeInfo: approveTxTypeInfo, status: TransactionStatus.Pending, addedTime: Date.now(), - }) + }), ) } catch (error) { expect(error).toEqual(Error(`addTransaction: Attempted to overwrite tx with id ${id}`)) @@ -129,12 +129,10 @@ describe('transaction reducer', () => { typeInfo: approveTxTypeInfo, status: TransactionStatus.Pending, addedTime: Date.now(), - }) + }), ) } catch (error) { - expect(error).toEqual( - Error(`updateTransaction: Attempted to update a missing tx with id ${id}`) - ) + expect(error).toEqual(Error(`updateTransaction: Attempted to update a missing tx with id ${id}`)) } expect(store.getState()).toEqual({}) }) @@ -167,9 +165,7 @@ describe('transaction reducer', () => { store.dispatch(finalizeTransaction(finalizedTxAction.payload)) } catch (error) { expect(error).toEqual( - Error( - `finalizeTransaction: Attempted to finalize a missing tx with id ${finalizedTxAction.payload.id}` - ) + Error(`finalizeTransaction: Attempted to finalize a missing tx with id ${finalizedTxAction.payload.id}`), ) } expect(store.getState()).toEqual({}) @@ -188,7 +184,7 @@ describe('transaction reducer', () => { typeInfo: approveTxTypeInfo, status: TransactionStatus.Pending, addedTime: Date.now(), - }) + }), ) store.dispatch(finalizeTransaction(finalizedTxAction.payload)) const tx = store.getState()[from]?.[chainId]?.[id] @@ -206,12 +202,10 @@ describe('transaction reducer', () => { chainId: UniverseChainId.Goerli, cancelRequest: {}, id, - }) + }), ) } catch (error) { - expect(error).toEqual( - Error(`cancelTransaction: Attempted to cancel a tx that doesn't exist with id ${id}`) - ) + expect(error).toEqual(Error(`cancelTransaction: Attempted to cancel a tx that doesn't exist with id ${id}`)) } expect(store.getState()).toEqual({}) }) @@ -231,7 +225,7 @@ describe('transaction reducer', () => { typeInfo: approveTxTypeInfo, status: TransactionStatus.Pending, addedTime: Date.now(), - }) + }), ) store.dispatch(cancelTransaction({ chainId, id, address, cancelRequest: {} })) const tx = store.getState()[address]?.[chainId]?.[id] @@ -251,12 +245,10 @@ describe('transaction reducer', () => { chainId: UniverseChainId.Goerli, id, newTxParams, - }) + }), ) } catch (error) { - expect(error).toEqual( - Error(`replaceTransaction: Attempted to replace a tx that doesn't exist with id ${id}`) - ) + expect(error).toEqual(Error(`replaceTransaction: Attempted to replace a tx that doesn't exist with id ${id}`)) } expect(store.getState()).toEqual({}) }) @@ -300,7 +292,7 @@ describe('transaction reducer', () => { typeInfo: approveTxTypeInfo, status: TransactionStatus.Pending, addedTime: Date.now(), - }) + }), ) store.dispatch( addTransaction({ @@ -313,7 +305,7 @@ describe('transaction reducer', () => { typeInfo: approveTxTypeInfo, status: TransactionStatus.Pending, addedTime: Date.now(), - }) + }), ) const txs = store.getState() expect(Object.keys(txs)).toHaveLength(2) diff --git a/packages/wallet/src/features/transactions/slice.ts b/packages/wallet/src/features/transactions/slice.ts index 260384ba6dd..9bbf14e2b6a 100644 --- a/packages/wallet/src/features/transactions/slice.ts +++ b/packages/wallet/src/features/transactions/slice.ts @@ -3,6 +3,7 @@ import { createAction, createSlice, PayloadAction } from '@reduxjs/toolkit' import { providers } from 'ethers' import { assert } from 'utilities/src/errors' +import { isUniswapX } from 'wallet/src/features/transactions/swap/trade/utils' import { ChainIdToTxIdToDetails, FiatPurchaseTransactionInfo, @@ -25,43 +26,35 @@ const slice = createSlice({ reducers: { addTransaction: (state, { payload: transaction }: PayloadAction) => { const { chainId, id, from } = transaction - assert( - !state?.[from]?.[chainId]?.[id], - `addTransaction: Attempted to overwrite tx with id ${id}` - ) + assert(!state?.[from]?.[chainId]?.[id], `addTransaction: Attempted to overwrite tx with id ${id}`) state[from] ??= {} state[from]![chainId] ??= {} state[from]![chainId]![id] = transaction }, updateTransaction: (state, { payload: transaction }: PayloadAction) => { const { chainId, id, from } = transaction - assert( - state?.[from]?.[chainId]?.[id], - `updateTransaction: Attempted to update a missing tx with id ${id}` - ) + assert(state?.[from]?.[chainId]?.[id], `updateTransaction: Attempted to update a missing tx with id ${id}`) state[from]![chainId]![id] = transaction }, - finalizeTransaction: ( - state, - { payload: transaction }: PayloadAction - ) => { - const { chainId, id, status, receipt, from } = transaction - assert( - state?.[from]?.[chainId]?.[id], - `finalizeTransaction: Attempted to finalize a missing tx with id ${id}` - ) + finalizeTransaction: (state, { payload: transaction }: PayloadAction) => { + const { chainId, id, status, receipt, from, hash } = transaction + assert(state?.[from]?.[chainId]?.[id], `finalizeTransaction: Attempted to finalize a missing tx with id ${id}`) state[from]![chainId]![id]!.status = status if (receipt) { state[from]![chainId]![id]!.receipt = receipt } + if (isUniswapX(transaction) && status === TransactionStatus.Success) { + assert(hash, `finalizeTransaction: Attempted to finalize an order without providing the fill tx hash`) + state[from]![chainId]![id]!.hash = hash + } }, deleteTransaction: ( state, - { payload: { chainId, id, address } }: PayloadAction + { payload: { chainId, id, address } }: PayloadAction, ) => { assert( state?.[address]?.[chainId]?.[id], - `deleteTransaction: Attempted to delete a tx that doesn't exist with id ${id}` + `deleteTransaction: Attempted to delete a tx that doesn't exist with id ${id}`, ) delete state[address]![chainId]![id] }, @@ -69,13 +62,11 @@ const slice = createSlice({ state, { payload: { chainId, id, address, cancelRequest }, - }: PayloadAction< - TransactionId & { address: string; cancelRequest: providers.TransactionRequest } - > + }: PayloadAction, ) => { assert( state?.[address]?.[chainId]?.[id], - `cancelTransaction: Attempted to cancel a tx that doesn't exist with id ${id}` + `cancelTransaction: Attempted to cancel a tx that doesn't exist with id ${id}`, ) state[address]![chainId]![id]!.status = TransactionStatus.Cancelling state[address]![chainId]![id]!.cancelRequest = cancelRequest @@ -88,11 +79,11 @@ const slice = createSlice({ TransactionId & { newTxParams: providers.TransactionRequest } & { address: string } - > + >, ) => { assert( state?.[address]?.[chainId]?.[id], - `replaceTransaction: Attempted to replace a tx that doesn't exist with id ${id}` + `replaceTransaction: Attempted to replace a tx that doesn't exist with id ${id}`, ) state[address]![chainId]![id]!.status = TransactionStatus.Replacing }, @@ -100,9 +91,7 @@ const slice = createSlice({ // fiat onramp transactions re-use this slice to store (off-chain) pending txs upsertFiatOnRampTransaction: ( state, - { - payload: transaction, - }: PayloadAction + { payload: transaction }: PayloadAction, ) => { const { chainId, @@ -115,9 +104,7 @@ const slice = createSlice({ state[from] ??= {} state[from]![chainId] ??= {} - const oldTypeInfo = state[from]![chainId]![id]?.typeInfo as - | FiatPurchaseTransactionInfo - | undefined + const oldTypeInfo = state[from]![chainId]![id]?.typeInfo as FiatPurchaseTransactionInfo | undefined state[from]![chainId]![id] = { ...transaction, typeInfo: { ...oldTypeInfo, ...transaction.typeInfo }, @@ -127,9 +114,7 @@ const slice = createSlice({ }) // This action is fired, when user has come back from Moonpay flow using Return to Uniswap button -export const forceFetchFiatOnRampTransactions = createAction( - 'transactions/forceFetchFiatOnRampTransactions' -) +export const forceFetchFiatOnRampTransactions = createAction('transactions/forceFetchFiatOnRampTransactions') export const { addTransaction, diff --git a/packages/wallet/src/features/transactions/swap/CurrencyInputPanel.tsx b/packages/wallet/src/features/transactions/swap/CurrencyInputPanel.tsx index 63258e1bc69..2ea4c74f097 100644 --- a/packages/wallet/src/features/transactions/swap/CurrencyInputPanel.tsx +++ b/packages/wallet/src/features/transactions/swap/CurrencyInputPanel.tsx @@ -1,42 +1,15 @@ /* eslint-disable complexity */ import { Currency, CurrencyAmount } from '@uniswap/sdk-core' -import { - RefObject, - forwardRef, - memo, - useCallback, - useEffect, - useImperativeHandle, - useRef, -} from 'react' -import { - NativeSyntheticEvent, - TextInput, - TextInputProps, - TextInputSelectionChangeEventData, -} from 'react-native' -import { - Easing, - useAnimatedStyle, - useSharedValue, - withRepeat, - withSequence, - withTiming, -} from 'react-native-reanimated' -import { - Flex, - FlexProps, - Text, - TouchableArea, - isWeb, - useIsShortMobileDevice, - useSporeColors, -} from 'ui/src' +import { RefObject, forwardRef, memo, useCallback, useEffect, useImperativeHandle, useRef } from 'react' +import { NativeSyntheticEvent, TextInput, TextInputProps, TextInputSelectionChangeEventData } from 'react-native' +import { Easing, useAnimatedStyle, useSharedValue, withRepeat, withSequence, withTiming } from 'react-native-reanimated' +import { Flex, FlexProps, Text, TouchableArea, isWeb, useIsShortMobileDevice, useSporeColors } from 'ui/src' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { fonts } from 'ui/src/theme' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' -import { ElementName } from 'uniswap/src/features/telemetry/constants' -import { isDetoxBuild } from 'utilities/src/environment' +import { CurrencyField } from 'uniswap/src/features/transactions/transactionState/types' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' +import { isDetoxBuild } from 'utilities/src/environment/constants' import { NumberType } from 'utilities/src/format/types' import { usePrevious } from 'utilities/src/react/hooks' import { SelectTokenButton } from 'wallet/src/components/TokenSelector/SelectTokenButton' @@ -45,7 +18,6 @@ import { MaxAmountButton } from 'wallet/src/components/input/MaxAmountButton' import { useAppFiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' import { useTokenAndFiatDisplayAmounts } from 'wallet/src/features/transactions/hooks/useTokenAndFiatDisplayAmounts' -import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' import { MAX_FIAT_INPUT_DECIMALS } from 'wallet/src/features/transactions/utils' import { errorShakeAnimation } from 'wallet/src/utils/animations' import { useDynamicFontSizing } from 'wallet/src/utils/useDynamicFontSizing' @@ -111,7 +83,7 @@ export const CurrencyInputPanel = memo( onPressDisabled, ...rest }, - forwardedRef + forwardedRef, ): JSX.Element { const colors = useSporeColors() const isShortMobileDevice = useIsShortMobileDevice() @@ -125,7 +97,7 @@ export const CurrencyInputPanel = memo( () => ({ transform: [{ translateX: shakeValue.value }], }), - [shakeValue.value] + [shakeValue.value], ) const triggerShakeAnimation = useCallback(() => { @@ -161,19 +133,12 @@ export const CurrencyInputPanel = memo( } else if (!focus && isTextInputRefActuallyFocused) { inputRef.current?.blur() } - }, [ - currencyField, - focus, - inputRef, - isTextInputRefActuallyFocused, - resetSelection, - value?.length, - ]) + }, [currencyField, focus, inputRef, isTextInputRefActuallyFocused, resetSelection, value?.length]) const { onLayout, fontSize, onSetFontSize } = useDynamicFontSizing( MAX_CHAR_PIXEL_WIDTH, MAX_INPUT_FONT_SIZE, - MIN_INPUT_FONT_SIZE + MIN_INPUT_FONT_SIZE, ) // This is needed to ensure that the text resizes when modified from outside the component (e.g. custom numpad) @@ -192,7 +157,7 @@ export const CurrencyInputPanel = memo( selection: { start, end }, }, }: NativeSyntheticEvent) => selectionChange?.(start, end), - [selectionChange] + [selectionChange], ) // Hide balance if panel is output, and no balance @@ -219,14 +184,15 @@ export const CurrencyInputPanel = memo( const loadingFlexProgress = useSharedValue(1) + // disables looping animation during detox e2e tests which was preventing js thread from idle if (!isDetoxBuild) { loadingFlexProgress.value = withRepeat( withSequence( withTiming(0.4, { duration: 400, easing: Easing.ease }), - withTiming(1, { duration: 400, easing: Easing.ease }) + withTiming(1, { duration: 400, easing: Easing.ease }), ), -1, - true + true, ) } @@ -234,7 +200,7 @@ export const CurrencyInputPanel = memo( () => ({ opacity: isLoading ? loadingFlexProgress.value : 1, }), - [isLoading, loadingFlexProgress] + [isLoading, loadingFlexProgress], ) const onPressDisabledWithShakeAnimation = useCallback((): void => { @@ -248,30 +214,27 @@ export const CurrencyInputPanel = memo( (amount: string) => { onSetMax?.(amount, currencyField) }, - [currencyField, onSetMax] + [currencyField, onSetMax], ) return ( + onPress={disabled ? onPressDisabledWithShakeAnimation : currencyInfo ? onPressIn : onShowTokenSelector} + > + py={isWeb ? '$spacing24' : isShortMobileDevice ? '$spacing8' : '$spacing20'} + > + style={shakeStyle} + > {isFiatMode && ( + mr="$spacing4" + > {fiatCurrencySymbol} )} @@ -292,7 +256,8 @@ export const CurrencyInputPanel = memo( mr="$spacing8" overflow="hidden" style={loadingStyle} - onLayout={onLayout}> + onLayout={onLayout} + > {currencyInfo ? ( {disabled && ( @@ -316,9 +281,7 @@ export const CurrencyInputPanel = memo( // (the text input height is greater than the font size and the input is // centered vertically, so the caret is cut off but the text is not) fontSize={fontSize} - maxDecimals={ - isFiatMode ? MAX_FIAT_INPUT_DECIMALS : currencyInfo.currency.decimals - } + maxDecimals={isFiatMode ? MAX_FIAT_INPUT_DECIMALS : currencyInfo.currency.decimals} maxFontSizeMultiplier={fonts.heading2.maxFontSizeMultiplier} minHeight={2 * MAX_INPUT_FONT_SIZE} overflow="visible" @@ -328,7 +291,7 @@ export const CurrencyInputPanel = memo( py="$none" returnKeyType={showSoftInputOnFocus ? 'done' : undefined} showSoftInputOnFocus={showSoftInputOnFocus} - testID={isOutput ? ElementName.AmountInputOut : ElementName.AmountInputIn} + testID={isOutput ? TestID.AmountInputOut : TestID.AmountInputIn} value={isLoading ? loadingTextValue : value} onChangeText={onSetExactAmount} onPressIn={onPressIn} @@ -347,11 +310,7 @@ export const CurrencyInputPanel = memo( @@ -359,7 +318,9 @@ export const CurrencyInputPanel = memo( {currencyInfo && ( + flexShrink={1} + onPress={disabled ? onPressDisabledWithShakeAnimation : _onToggleIsFiatMode} + > {inputPanelFormattedValue} @@ -390,5 +351,5 @@ export const CurrencyInputPanel = memo( ) - }) + }), ) diff --git a/packages/wallet/src/features/transactions/swap/DecimalPad.tsx b/packages/wallet/src/features/transactions/swap/DecimalPad.tsx index 4106ebfa9ac..aee56bbc116 100644 --- a/packages/wallet/src/features/transactions/swap/DecimalPad.tsx +++ b/packages/wallet/src/features/transactions/swap/DecimalPad.tsx @@ -142,8 +142,7 @@ export const DecimalPad = memo(function DecimalPad({ {row.map((key, keyIndex) => { const isNumberKey = - key.label.charCodeAt(0) >= '0'.charCodeAt(0) && - key.label.charCodeAt(0) <= '9'.charCodeAt(0) + key.label.charCodeAt(0) >= '0'.charCodeAt(0) && key.label.charCodeAt(0) <= '9'.charCodeAt(0) const isKeyDisabled = disabled || disabledKeys[key.label] const shouldTriggerShake = isKeyDisabled && isNumberKey @@ -231,7 +230,8 @@ const KeyButton = memo(function KeyButton({ testID={'decimal-pad-' + label} width={keyWidth} onLongPress={handleLongPress} - onPress={handlePress}> + onPress={handlePress} + > {label === 'backspace' ? ( I18nManager.isRTL ? ( @@ -245,7 +245,8 @@ const KeyButton = memo(function KeyButton({ lineHeight: fonts.heading2.lineHeight * sizeMultiplier.lineHeight, fontSize: fonts.heading2.fontSize * sizeMultiplier.fontSize, }} - textAlign="center"> + textAlign="center" + > { label === '.' ? decimalSeparator : label /* respect phone settings to show decimal separator in the numpad, diff --git a/packages/wallet/src/features/transactions/swap/DecimalPadInput.tsx b/packages/wallet/src/features/transactions/swap/DecimalPadInput.tsx index 5da56eba8bd..78e4427b3eb 100644 --- a/packages/wallet/src/features/transactions/swap/DecimalPadInput.tsx +++ b/packages/wallet/src/features/transactions/swap/DecimalPadInput.tsx @@ -1,12 +1,4 @@ -import { - forwardRef, - memo, - useCallback, - useEffect, - useImperativeHandle, - useMemo, - useState, -} from 'react' +import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useState } from 'react' import { TextInputProps } from 'uniswap/src/components/input/TextInput' import { maxDecimalsReached } from 'utilities/src/format/truncateToMaxDecimals' import { DecimalPad, KeyAction, KeyLabel } from 'wallet/src/features/transactions/swap/DecimalPad' @@ -43,7 +35,7 @@ export const DecimalPadInput = memo( maxDecimals, onTriggerInputShakeAnimation, }, - ref + ref, ): JSX.Element { const [disabledKeys, setDisabledKeys] = useState>>({}) const [maxHeight, setMaxHeight] = useState(null) @@ -86,7 +78,7 @@ export const DecimalPadInput = memo( return cursorAtStart || v.length === 0 }, }), - [getCurrentSelection, maxDecimals] + [getCurrentSelection, maxDecimals], ) const updateDisabledKeys = useCallback( @@ -100,7 +92,7 @@ export const DecimalPadInput = memo( isUpdated = true } return [key, isDisabled] - }) + }), ) // Prevent unnecessary re-renders and return the same value // if no key was updated (react state won't be updated if value is the @@ -109,7 +101,7 @@ export const DecimalPadInput = memo( return isUpdated ? newDisabledKeys : prevDisabledKeys }) }, - [disableKeysConditions] + [disableKeysConditions], ) const updateValue = useCallback( @@ -117,7 +109,7 @@ export const DecimalPadInput = memo( setValue(newValue) updateDisabledKeys(newValue) }, - [setValue, updateDisabledKeys] + [setValue, updateDisabledKeys], ) // TODO(MOB-140): in USD mode, prevent user from typing in more than 2 decimals @@ -133,7 +125,7 @@ export const DecimalPadInput = memo( updateValue(valueRef.current.slice(0, start) + label + valueRef.current.slice(end)) } }, - [updateValue, resetSelection, valueRef, getCurrentSelection] + [updateValue, resetSelection, valueRef, getCurrentSelection], ) const handleDelete = useCallback((): void => { @@ -164,7 +156,7 @@ export const DecimalPadInput = memo( handleDelete() } }, - [disabled, handleInsert, handleDelete] + [disabled, handleInsert, handleDelete], ) const onLongPress = useCallback( @@ -175,7 +167,7 @@ export const DecimalPadInput = memo( resetSelection({ start: 0, end: 0 }) updateValue('') }, - [disabled, updateValue, resetSelection] + [disabled, updateValue, resetSelection], ) return ( @@ -190,5 +182,5 @@ export const DecimalPadInput = memo( onTriggerInputShakeAnimation={onTriggerInputShakeAnimation} /> ) - }) + }), ) diff --git a/packages/wallet/src/features/transactions/swap/GasAndWarningRows.native.tsx b/packages/wallet/src/features/transactions/swap/GasAndWarningRows.native.tsx index 5337541cbe1..fb663538348 100644 --- a/packages/wallet/src/features/transactions/swap/GasAndWarningRows.native.tsx +++ b/packages/wallet/src/features/transactions/swap/GasAndWarningRows.native.tsx @@ -1,3 +1,4 @@ +/* eslint-disable complexity */ import { useCallback, useState } from 'react' import { Keyboard } from 'react-native' import { FadeIn, FadeOut } from 'react-native-reanimated' @@ -5,7 +6,9 @@ import { Flex, Text, TouchableArea, useIsShortMobileDevice, useMedia } from 'ui/ import { Gas } from 'ui/src/components/icons' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { iconSizes } from 'ui/src/theme' +import { CurrencyField } from 'uniswap/src/features/transactions/transactionState/types' import { NumberType } from 'utilities/src/format/types' +import { UniswapXFee } from 'wallet/src/components/network/NetworkFee' import { useUSDValue } from 'wallet/src/features/gas/hooks' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' import { InsufficientNativeTokenWarning } from 'wallet/src/features/transactions/InsufficientNativeTokenWarning/InsufficientNativeTokenWarning' @@ -16,7 +19,8 @@ import { GasAndWarningRowsProps } from 'wallet/src/features/transactions/swap/Ga import { SwapWarningModal } from 'wallet/src/features/transactions/swap/SwapWarningModal' import { useGasFeeHighRelativeToValue } from 'wallet/src/features/transactions/swap/hooks/useGasFeeHighRelativeToValue' import { NetworkFeeWarning } from 'wallet/src/features/transactions/swap/modals/NetworkFeeWarning' -import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' +import { UniswapXInfo } from 'wallet/src/features/transactions/swap/modals/UniswapXInfo' +import { isUniswapX } from 'wallet/src/features/transactions/swap/trade/utils' import { BlockedAddressWarning } from 'wallet/src/features/trm/BlockedAddressWarning' import { useIsBlockedActiveAddress } from 'wallet/src/features/trm/hooks' @@ -25,7 +29,7 @@ export function GasAndWarningRows({ renderEmptyRows }: GasAndWarningRowsProps): const isShortMobileDevice = useIsShortMobileDevice() const { convertFiatAmountFormatted } = useLocalizationContext() - const { gasFee } = useSwapTxContext() + const { gasFee, trade } = useSwapTxContext() const { derivedSwapInfo } = useSwapFormContext() const { chainId, currencyAmountsUSDValue } = derivedSwapInfo @@ -41,8 +45,14 @@ export function GasAndWarningRows({ renderEmptyRows }: GasAndWarningRowsProps): const gasFeeUSD = useUSDValue(chainId, gasFee?.value) const gasFeeFormatted = convertFiatAmountFormatted(gasFeeUSD, NumberType.FiatGasPrice) + const showUniswapXFee = Boolean(gasFeeUSD && trade && isUniswapX(trade)) + const preSavingsGasFeeFormatted = + trade && isUniswapX(trade) + ? convertFiatAmountFormatted(trade.quote.quote.classicGasUseEstimateUSD, NumberType.FiatGasPrice) + : undefined + // only show the gas fee icon and price if we have a valid fee - const showGasFee = Boolean(gasFeeUSD) + const showGasFee = Boolean(gasFeeUSD && !showUniswapXFee) const onSwapWarningClick = useCallback(() => { if (!formScreenWarning?.warning.message) { @@ -60,10 +70,7 @@ export function GasAndWarningRows({ renderEmptyRows }: GasAndWarningRowsProps): return ( <> {showWarningModal && formScreenWarning && ( - setShowWarningModal(false)} - /> + setShowWarningModal(false)} /> )} {/* @@ -87,10 +94,16 @@ export function GasAndWarningRows({ renderEmptyRows }: GasAndWarningRowsProps): )} + {showUniswapXFee && ( + }> + + + + + )} + {showGasFee && ( - }> + }> @@ -103,13 +116,7 @@ export function GasAndWarningRows({ renderEmptyRows }: GasAndWarningRowsProps): {showFormWarning && ( - + {formScreenWarning.Icon && ( { if (!formScreenWarning?.warning.message) { @@ -65,10 +75,7 @@ export function GasAndWarningRows({ return ( <> {showWarningModal && formScreenWarning && ( - setShowWarningModal(false)} - /> + setShowWarningModal(false)} /> )} {/* @@ -99,19 +106,25 @@ export function GasAndWarningRows({ )} + {showUniswapXFee && ( + + + + } + /> + )} + {showGasFee && ( - - + + {gasFeeFormatted} @@ -146,7 +159,8 @@ export function GasAndWarningRows({ // TODO(EXT-526): re-enable `exiting` animation when it's fixed. exiting={undefined} gap="$spacing8" - px="$spacing16"> + px="$spacing16" + > {formScreenWarning.Icon && ( ({ strokeDashoffset: CIRCLE_LENGTH * (1 - progress.value), }), - [progress] + [progress], ) useEffect(() => { @@ -61,7 +58,8 @@ export function HoldToSwapProgressCircle(): JSX.Element { fill="none" height={PROGRESS_CIRCLE_SIZE} viewBox={`0 0 ${PROGRESS_CIRCLE_SIZE} ${PROGRESS_CIRCLE_SIZE}`} - width={PROGRESS_CIRCLE_SIZE}> + width={PROGRESS_CIRCLE_SIZE} + > diff --git a/packages/wallet/src/features/transactions/swap/SwapArrowButton.tsx b/packages/wallet/src/features/transactions/swap/SwapArrowButton.tsx index 76ce922ce74..a782386f7a1 100644 --- a/packages/wallet/src/features/transactions/swap/SwapArrowButton.tsx +++ b/packages/wallet/src/features/transactions/swap/SwapArrowButton.tsx @@ -10,14 +10,7 @@ type SwapArrowButtonProps = Pick< export function SwapArrowButton(props: SwapArrowButtonProps): JSX.Element { const colors = useSporeColors() - const { - testID, - onPress, - disabled, - backgroundColor = '$surface2', - size = iconSizes.icon24, - ...rest - } = props + const { testID, onPress, disabled, backgroundColor = '$surface2', size = iconSizes.icon24, ...rest } = props return useMemo( () => ( + {...rest} + > {/* hack to add 2px more padding without adjusting design system values */} ), - [backgroundColor, disabled, onPress, testID, rest, colors.neutral2.val, size] + [backgroundColor, disabled, onPress, testID, rest, colors.neutral2.val, size], ) } diff --git a/packages/wallet/src/features/transactions/swap/SwapDetails.tsx b/packages/wallet/src/features/transactions/swap/SwapDetails.tsx index 7358b1243fc..e701d52aa78 100644 --- a/packages/wallet/src/features/transactions/swap/SwapDetails.tsx +++ b/packages/wallet/src/features/transactions/swap/SwapDetails.tsx @@ -6,6 +6,7 @@ import { InfoCircleFilled } from 'ui/src/components/icons' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' import Trace from 'uniswap/src/features/telemetry/Trace' import { ElementName } from 'uniswap/src/features/telemetry/constants' +import { CurrencyField } from 'uniswap/src/features/transactions/transactionState/types' import { getSymbolDisplayText } from 'uniswap/src/utils/currency' import { NumberType } from 'utilities/src/format/types' import { GasFeeResult } from 'wallet/src/features/gas/types' @@ -15,14 +16,14 @@ import { TransactionDetails } from 'wallet/src/features/transactions/Transaction import { Warning } from 'wallet/src/features/transactions/WarningModal/types' import { SwapRateRatio } from 'wallet/src/features/transactions/swap/SwapRateRatio' import { Trade } from 'wallet/src/features/transactions/swap/trade/types' +import { isUniswapX } from 'wallet/src/features/transactions/swap/trade/utils' import { DerivedSwapInfo } from 'wallet/src/features/transactions/swap/types' -import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' import { getFormattedCurrencyAmount } from 'wallet/src/utils/currency' import { ValueType, getCurrencyAmount } from 'wallet/src/utils/getCurrencyAmount' const getFeeAmountUsd = ( trade: Trade, - outputCurrencyPricePerUnitExact?: string + outputCurrencyPricePerUnitExact?: string, ): number | undefined => { if (!trade.swapFee || !outputCurrencyPricePerUnitExact) { return @@ -101,9 +102,7 @@ export function SwapDetails({ : undefined // Make text the warning color if user is setting custom slippage higher than auto slippage value - const showSlippageWarning = autoSlippageTolerance - ? acceptedTrade.slippageTolerance > autoSlippageTolerance - : false + const showSlippageWarning = autoSlippageTolerance ? acceptedTrade.slippageTolerance > autoSlippageTolerance : false const feeOnTransferProps: FeeOnTransferFeeGroupProps = useMemo( () => ({ @@ -121,9 +120,11 @@ export function SwapDetails({ acceptedTrade.inputTax, acceptedTrade.outputAmount.currency.symbol, acceptedTrade.outputTax, - ] + ], ) + const preUniswapXGasFeeUSD = isUniswapX(trade) ? trade.quote.quote.classicGasUseEstimateUSD : undefined + return ( + onShowWarning={onShowWarning} + > {t('swap.details.rate')} @@ -164,12 +167,7 @@ export function SwapDetails({ {!customSlippageTolerance ? ( - + {t('swap.settings.slippage.control.auto')} @@ -197,14 +195,10 @@ function AcceptNewQuoteRow({ const { formatCurrencyAmount } = useLocalizationContext() const derivedCurrencyField = - derivedSwapInfo.exactCurrencyField === CurrencyField.INPUT - ? CurrencyField.OUTPUT - : CurrencyField.INPUT + derivedSwapInfo.exactCurrencyField === CurrencyField.INPUT ? CurrencyField.OUTPUT : CurrencyField.INPUT const derivedAmount = derivedSwapInfo.currencyAmounts[derivedCurrencyField] - const derivedSymbol = getSymbolDisplayText( - derivedSwapInfo.currencies[derivedCurrencyField]?.currency.symbol - ) + const derivedSymbol = getSymbolDisplayText(derivedSwapInfo.currencies[derivedCurrencyField]?.currency.symbol) const formattedDerivedAmount = formatCurrencyAmount({ value: derivedAmount, type: NumberType.TokenTx, @@ -227,7 +221,8 @@ function AcceptNewQuoteRow({ justifyContent="space-between" pl="$spacing12" pr="$spacing8" - py="$spacing8"> + py="$spacing8" + > {derivedSwapInfo.exactCurrencyField === CurrencyField.INPUT @@ -235,14 +230,8 @@ function AcceptNewQuoteRow({ : t('swap.details.newQuote.input')} - - {formattedDerivedAmount} {derivedSymbol}{' '} - ({percentageDifference}%) + + {formattedDerivedAmount} {derivedSymbol} ({percentageDifference}%) @@ -253,7 +242,8 @@ function AcceptNewQuoteRow({ borderRadius="$rounded12" px="$spacing8" py="$spacing4" - onPress={onAcceptTrade}> + onPress={onAcceptTrade} + > {t('common.button.accept')} @@ -272,9 +262,7 @@ function calculatePercentageDifference({ acceptedDerivedSwapInfo: DerivedSwapInfo }): string | null { const derivedCurrencyField = - derivedSwapInfo.exactCurrencyField === CurrencyField.INPUT - ? CurrencyField.OUTPUT - : CurrencyField.INPUT + derivedSwapInfo.exactCurrencyField === CurrencyField.INPUT ? CurrencyField.OUTPUT : CurrencyField.INPUT // It's important to convert these to fractions before doing math on them in order to preserve full precision on each step. const newAmount = derivedSwapInfo.currencyAmounts[derivedCurrencyField]?.asFraction diff --git a/packages/wallet/src/features/transactions/swap/SwapFlow.tsx b/packages/wallet/src/features/transactions/swap/SwapFlow.tsx index cae8ca189d6..052778a47d8 100644 --- a/packages/wallet/src/features/transactions/swap/SwapFlow.tsx +++ b/packages/wallet/src/features/transactions/swap/SwapFlow.tsx @@ -4,10 +4,7 @@ import { BottomSheetModal } from 'uniswap/src/components/modals/BottomSheetModal import Trace from 'uniswap/src/features/telemetry/Trace' import { ModalName, SectionName } from 'uniswap/src/features/telemetry/constants' import { logger } from 'utilities/src/logger/logger' -import { - SwapFormContextProvider, - SwapFormState, -} from 'wallet/src/features/transactions/contexts/SwapFormContext' +import { SwapFormContextProvider, SwapFormState } from 'wallet/src/features/transactions/contexts/SwapFormContext' import { SwapScreen, SwapScreenContextProvider, @@ -22,7 +19,7 @@ import { TransactionModalFooterContainer, } from 'wallet/src/features/transactions/swap/TransactionModal' import { TransactionModalProps } from 'wallet/src/features/transactions/swap/TransactionModalProps' -import { TradingApiApolloClient } from 'wallet/src/features/transactions/swap/trade/tradingApi/client' +import { TradingApiApolloClient } from 'wallet/src/features/transactions/swap/trade/api/client' export function SwapFlow({ prefilledState, @@ -103,7 +100,8 @@ function CurrentScreen({ + name={ModalName.SwapReview} + > diff --git a/packages/wallet/src/features/transactions/swap/SwapFormButton.tsx b/packages/wallet/src/features/transactions/swap/SwapFormButton.tsx index a8c7406f4b9..caa0b42d163 100644 --- a/packages/wallet/src/features/transactions/swap/SwapFormButton.tsx +++ b/packages/wallet/src/features/transactions/swap/SwapFormButton.tsx @@ -1,20 +1,20 @@ /* eslint-disable complexity */ import { TFunction } from 'i18next' -import { useCallback, useMemo, useState } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -import { Button, Flex, isWeb, Text, useIsShortMobileDevice } from 'ui/src' +import { AnimatePresence, Button, Flex, SpinningLoader, Text, isWeb, useIsShortMobileDevice } from 'ui/src' import { GraduationCap } from 'ui/src/components/icons' -import { ElementName } from 'uniswap/src/features/telemetry/constants' +import { iconSizes } from 'ui/src/theme' import Trace from 'uniswap/src/features/telemetry/Trace' +import { ElementName } from 'uniswap/src/features/telemetry/constants' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' +import { ONE_SECOND_MS } from 'utilities/src/time/time' import { selectHasSubmittedHoldToSwap, selectHasViewedReviewScreen, } from 'wallet/src/features/behaviorHistory/selectors' import { useSwapFormContext } from 'wallet/src/features/transactions/contexts/SwapFormContext' -import { - SwapScreen, - useSwapScreenContext, -} from 'wallet/src/features/transactions/contexts/SwapScreenContext' +import { SwapScreen, useSwapScreenContext } from 'wallet/src/features/transactions/contexts/SwapScreenContext' import { useTransactionModalContext } from 'wallet/src/features/transactions/contexts/TransactionModalContext' import { useParsedSwapWarnings } from 'wallet/src/features/transactions/hooks/useParsedTransactionWarnings' import { @@ -22,6 +22,7 @@ import { PROGRESS_CIRCLE_SIZE, } from 'wallet/src/features/transactions/swap/HoldToSwapProgressCircle' import { ViewOnlyModal } from 'wallet/src/features/transactions/swap/modals/ViewOnlyModal' +import { isUniswapX } from 'wallet/src/features/transactions/swap/trade/utils' import { isWrapAction } from 'wallet/src/features/transactions/swap/utils' import { WrapType } from 'wallet/src/features/transactions/types' import { createTransactionId } from 'wallet/src/features/transactions/utils' @@ -30,7 +31,9 @@ import { AccountType } from 'wallet/src/features/wallet/accounts/types' import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks' import { useAppSelector } from 'wallet/src/state' -export const HOLD_TO_SWAP_TIMEOUT = 3000 +export const HOLD_TO_SWAP_TIMEOUT = 3 * ONE_SECOND_MS +const KEEP_OPEN_MSG_DELAY = 3 * ONE_SECOND_MS +export const SWAP_BUTTON_TEXT_VARIANT = isWeb ? 'buttonLabel2' : 'buttonLabel1' export function SwapFormButton(): JSX.Element { const { t } = useTranslation() @@ -51,9 +54,9 @@ export function SwapFormButton(): JSX.Element { const noValidSwap = !isWrapAction(wrapType) && !trade.trade const reviewButtonDisabled = - noValidSwap || !!blockingWarning || isBlocked || isBlockedLoading || walletNeedsRestore + noValidSwap || !!blockingWarning || isBlocked || isBlockedLoading || walletNeedsRestore || isSubmitting - const isHoldToSwapPressed = screen === SwapScreen.SwapReviewHoldingToSwap || isSubmitting + const isHoldToSwapPressed = screen === SwapScreen.SwapReviewHoldingToSwap const hasViewedReviewScreen = useAppSelector(selectHasViewedReviewScreen) const hasSubmittedHoldToSwap = useAppSelector(selectHasSubmittedHoldToSwap) @@ -70,7 +73,7 @@ export function SwapFormButton(): JSX.Element { updateSwapForm({ txId: createTransactionId() }) setScreen(nextScreen) }, - [setScreen, updateSwapForm] + [setScreen, updateSwapForm], ) const onReviewPress = useCallback(() => { @@ -100,31 +103,34 @@ export function SwapFormButton(): JSX.Element { const hasButtonWarning = !!blockingWarning?.buttonText const buttonText = blockingWarning?.buttonText ?? t('swap.button.review') const buttonTextColor = hasButtonWarning ? '$neutral2' : '$white' - const buttonBgColor = hasButtonWarning - ? '$surface3' - : isHoldToSwapPressed - ? '$accent2' - : '$accent1' + const buttonBgColor = hasButtonWarning ? '$surface3' : isHoldToSwapPressed || isSubmitting ? '$accent2' : '$accent1' + const buttonOpacity = isViewOnlyWallet ? 0.4 : isSubmitting ? 1 : undefined + + const showUniswapXSubmittingUI = trade.trade && isUniswapX(trade?.trade) && isSubmitting return ( - {!isWeb && !isHoldToSwapPressed && showHoldToSwapTip && } + {!isWeb && !isHoldToSwapPressed && !isSubmitting && showHoldToSwapTip && } diff --git a/packages/wallet/src/features/transactions/swap/SwapTokenSelector.tsx b/packages/wallet/src/features/transactions/swap/SwapTokenSelector.tsx index 9c1a79f7f4f..56b89231475 100644 --- a/packages/wallet/src/features/transactions/swap/SwapTokenSelector.tsx +++ b/packages/wallet/src/features/transactions/swap/SwapTokenSelector.tsx @@ -1,28 +1,45 @@ import { Currency } from '@uniswap/sdk-core' import { useCallback } from 'react' +import { Keyboard, LayoutAnimation } from 'react-native' import { isWeb } from 'ui/src' -import { WalletEventName } from 'uniswap/src/features/telemetry/constants' -import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { TokenSelector, TokenSelectorModal, TokenSelectorProps, TokenSelectorVariation, -} from 'wallet/src/components/TokenSelector/TokenSelector' +} from 'uniswap/src/components/TokenSelector/TokenSelector' +import { AssetType, TradeableAsset } from 'uniswap/src/entities/assets' +import { SearchContext } from 'uniswap/src/features/search/SearchContext' +import { WalletEventName } from 'uniswap/src/features/telemetry/constants' +import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' +import { CurrencyField } from 'uniswap/src/features/transactions/transactionState/types' +import { TokenSelectorFlow } from 'uniswap/src/features/transactions/transfer/types' +import { currencyAddress } from 'uniswap/src/utils/currencyId' import { flowToModalName } from 'wallet/src/components/TokenSelector/flowToModalName' -import { AssetType, TradeableAsset } from 'wallet/src/entities/assets' -import { SearchContext } from 'wallet/src/features/search/SearchContext' import { - SwapFormState, - useSwapFormContext, -} from 'wallet/src/features/transactions/contexts/SwapFormContext' -import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' -import { TokenSelectorFlow } from 'wallet/src/features/transactions/transfer/types' -import { currencyAddress } from 'wallet/src/utils/currencyId' + useAddToSearchHistory, + useCommonTokensOptions, + useFavoriteTokensOptions, + useFilterCallbacks, + usePopularTokensOptions, + usePortfolioTokenOptions, + useTokenSectionsForEmptySearch, + useTokenSectionsForSearchResults, +} from 'wallet/src/components/TokenSelector/hooks' +import { useWalletNavigation } from 'wallet/src/contexts/WalletNavigationContext' +import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' +import { useTokenWarningDismissed } from 'wallet/src/features/tokens/safetyHooks' +import { SwapFormState, useSwapFormContext } from 'wallet/src/features/transactions/contexts/SwapFormContext' +import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' export function SwapTokenSelector(): JSX.Element { const swapContext = useSwapFormContext() + const activeAccountAddress = useActiveAccountAddressWithThrow() const { updateSwapForm, exactCurrencyField, selectingCurrencyField, output, input } = swapContext + const { navigateToBuyOrReceiveWithEmptyWallet } = useWalletNavigation() + // TODO: (MOB-3643) Share localization context with WEB + const { convertFiatAmountFormatted, formatNumberOrString } = useLocalizationContext() + const { registerSearch } = useAddToSearchHistory() if (!selectingCurrencyField) { throw new Error('TokenSelector rendered without `selectingCurrencyField`') @@ -84,11 +101,12 @@ export function SwapTokenSelector(): JSX.Element { // Hide screen when done selecting. onHideTokenSelector() }, - [exactCurrencyField, input, onHideTokenSelector, output, updateSwapForm] + [exactCurrencyField, input, onHideTokenSelector, output, updateSwapForm], ) const props: TokenSelectorProps = { // we need to filter tokens using the chainId of the *other* currency + activeAccountAddress, chainId: selectingCurrencyField === CurrencyField.INPUT ? output?.chainId : input?.chainId, currencyField: selectingCurrencyField, flow: TokenSelectorFlow.Swap, @@ -97,7 +115,21 @@ export function SwapTokenSelector(): JSX.Element { ? TokenSelectorVariation.BalancesAndPopular : TokenSelectorVariation.SuggestedAndFavoritesAndPopular, onClose: onHideTokenSelector, + onDismiss: () => Keyboard.dismiss(), + onPressAnimation: () => LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut), onSelectCurrency, + useCommonTokensOptionsHook: useCommonTokensOptions, + useFavoriteTokensOptionsHook: useFavoriteTokensOptions, + usePopularTokensOptionsHook: usePopularTokensOptions, + usePortfolioTokenOptionsHook: usePortfolioTokenOptions, + useTokenSectionsForEmptySearchHook: useTokenSectionsForEmptySearch, + useTokenSectionsForSearchResultsHook: useTokenSectionsForSearchResults, + useTokenWarningDismissedHook: useTokenWarningDismissed, + useFilterCallbacksHook: useFilterCallbacks, + navigateToBuyOrReceiveWithEmptyWalletCallback: navigateToBuyOrReceiveWithEmptyWallet, + convertFiatAmountFormattedCallback: convertFiatAmountFormatted, + formatNumberOrStringCallback: formatNumberOrString, + addToSearchHistoryCallback: registerSearch, } return isWeb ? : } diff --git a/packages/wallet/src/features/transactions/swap/TransactionAmountsReview.tsx b/packages/wallet/src/features/transactions/swap/TransactionAmountsReview.tsx index 84c6a753e38..afab3d400c9 100644 --- a/packages/wallet/src/features/transactions/swap/TransactionAmountsReview.tsx +++ b/packages/wallet/src/features/transactions/swap/TransactionAmountsReview.tsx @@ -4,11 +4,13 @@ import { ArrowDown, X } from 'ui/src/components/icons' import { iconSizes } from 'ui/src/theme' import { CurrencyLogo } from 'uniswap/src/components/CurrencyLogo/CurrencyLogo' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' +import { CurrencyField } from 'uniswap/src/features/transactions/transactionState/types' import { getSymbolDisplayText } from 'uniswap/src/utils/currency' +import { buildCurrencyId, currencyAddress } from 'uniswap/src/utils/currencyId' import { NumberType } from 'utilities/src/format/types' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' +import { useCurrencyInfo } from 'wallet/src/features/tokens/useCurrencyInfo' import { DerivedSwapInfo } from 'wallet/src/features/transactions/swap/types' -import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' export function TransactionAmountsReview({ acceptedDerivedSwapInfo, @@ -21,72 +23,47 @@ export function TransactionAmountsReview({ }): JSX.Element { const { t } = useTranslation() const colors = useSporeColors() - const { convertFiatAmountFormatted, formatCurrencyAmount, formatNumberOrString } = - useLocalizationContext() + const { convertFiatAmountFormatted, formatCurrencyAmount } = useLocalizationContext() + const { currencyAmountsUSDValue, exactCurrencyField, trade } = acceptedDerivedSwapInfo - const { - currencies, - currencyAmounts, - currencyAmountsUSDValue, - exactAmountToken, - exactCurrencyField, - } = acceptedDerivedSwapInfo - - const currencyInInfo = currencies[CurrencyField.INPUT] - const currencyOutInfo = currencies[CurrencyField.OUTPUT] - - const usdAmountIn = - exactCurrencyField === CurrencyField.INPUT - ? currencyAmountsUSDValue[CurrencyField.INPUT]?.toExact() - : acceptedDerivedSwapInfo?.currencyAmountsUSDValue[CurrencyField.INPUT]?.toExact() - - const usdAmountOut = - exactCurrencyField === CurrencyField.OUTPUT - ? currencyAmountsUSDValue[CurrencyField.OUTPUT]?.toExact() - : acceptedDerivedSwapInfo?.currencyAmountsUSDValue[CurrencyField.OUTPUT]?.toExact() - - const formattedFiatAmountIn = convertFiatAmountFormatted( - usdAmountIn, - NumberType.FiatTokenQuantity - ) - const formattedFiatAmountOut = convertFiatAmountFormatted( - usdAmountOut, - NumberType.FiatTokenQuantity - ) + if (!trade.trade) { + throw new Error('Missing required `trade` to render `TransactionAmountsReview`') + } - const derivedCurrencyField = - exactCurrencyField === CurrencyField.INPUT ? CurrencyField.OUTPUT : CurrencyField.INPUT + // Token amounts + // On review screen, always show values directly from trade object, to match exactly what is submitted on chain + const inputCurrencyAmount = trade.trade.inputAmount + const outputCurrencyAmount = trade.trade.outputAmount - const derivedAmount = formatCurrencyAmount({ - value: acceptedDerivedSwapInfo?.currencyAmounts[derivedCurrencyField], + const formattedTokenAmountIn = formatCurrencyAmount({ + value: inputCurrencyAmount, type: NumberType.TokenTx, }) - - const formattedExactAmountToken = formatNumberOrString({ - value: exactAmountToken, + const formattedTokenAmountOut = formatCurrencyAmount({ + value: outputCurrencyAmount, type: NumberType.TokenTx, }) - const [formattedTokenAmountIn, formattedTokenAmountOut] = - exactCurrencyField === CurrencyField.INPUT - ? [formattedExactAmountToken, derivedAmount] - : [derivedAmount, formattedExactAmountToken] + // USD amounts + const usdAmountIn = currencyAmountsUSDValue[CurrencyField.INPUT]?.toExact() + const usdAmountOut = currencyAmountsUSDValue[CurrencyField.OUTPUT]?.toExact() + const formattedFiatAmountIn = convertFiatAmountFormatted(usdAmountIn, NumberType.FiatTokenQuantity) + const formattedFiatAmountOut = convertFiatAmountFormatted(usdAmountOut, NumberType.FiatTokenQuantity) const shouldDimInput = newTradeRequiresAcceptance && exactCurrencyField === CurrencyField.OUTPUT const shouldDimOutput = newTradeRequiresAcceptance && exactCurrencyField === CurrencyField.INPUT - if ( - !currencyInInfo || - !currencyOutInfo || - !currencyAmounts[CurrencyField.INPUT] || - !currencyAmounts[CurrencyField.OUTPUT] || - !acceptedDerivedSwapInfo.currencyAmounts[CurrencyField.INPUT] || - !acceptedDerivedSwapInfo.currencyAmounts[CurrencyField.OUTPUT] - ) { + // Rebuild currency infos directly from trade object to ensure it matches what is submitted on chain + const currencyInInfo = useCurrencyInfo( + buildCurrencyId(inputCurrencyAmount.currency.chainId, currencyAddress(inputCurrencyAmount.currency)), + ) + const currencyOutInfo = useCurrencyInfo( + buildCurrencyId(outputCurrencyAmount.currency.chainId, currencyAddress(outputCurrencyAmount.currency)), + ) + + if (!currencyInInfo || !currencyOutInfo) { // This should never happen. It's just to keep TS happy. - throw new Error( - 'Missing required props in `derivedSwapInfo` to render `TransactionAmountsReview` screen.' - ) + throw new Error('Missing required props in `derivedSwapInfo` to render `TransactionAmountsReview` screen.') } return ( diff --git a/packages/wallet/src/features/transactions/swap/TransactionModal.native.tsx b/packages/wallet/src/features/transactions/swap/TransactionModal.native.tsx index 2242e2e5b93..9f53a356b77 100644 --- a/packages/wallet/src/features/transactions/swap/TransactionModal.native.tsx +++ b/packages/wallet/src/features/transactions/swap/TransactionModal.native.tsx @@ -1,9 +1,4 @@ -import { - BottomSheetFooter, - BottomSheetView, - KEYBOARD_STATE, - useBottomSheetInternal, -} from '@gorhom/bottom-sheet' +import { BottomSheetFooter, BottomSheetView, KEYBOARD_STATE, useBottomSheetInternal } from '@gorhom/bottom-sheet' import { useMemo } from 'react' import { StyleProp, TouchableWithoutFeedback, ViewStyle } from 'react-native' import { @@ -53,7 +48,7 @@ export function TransactionModal({ animatedPosition.value, [0, insets.top], [0, borderRadii.rounded24], - Extrapolate.CLAMP + Extrapolate.CLAMP, ) return { borderTopLeftRadius: interpolatedRadius, borderTopRightRadius: interpolatedRadius } }) @@ -67,7 +62,7 @@ export function TransactionModal({ }, animatedBorderRadius, ], - [animatedBorderRadius, backgroundColorValue, fullContentHeight, fullscreen] + [animatedBorderRadius, backgroundColorValue, fullContentHeight, fullscreen], ) return ( @@ -81,11 +76,13 @@ export function TransactionModal({ fullScreen={fullscreen} hideHandlebar={fullscreen} name={modalName} - onClose={onClose}> + onClose={onClose} + > + {...transactionContextProps} + > {children} @@ -111,11 +108,7 @@ export function TransactionModalInnerContainer({ {fullscreen && } - + {children} @@ -126,9 +119,7 @@ export function TransactionModalInnerContainer({ ) } -export function TransactionModalFooterContainer({ - children, -}: TransactionModalFooterContainerProps): JSX.Element { +export function TransactionModalFooterContainer({ children }: TransactionModalFooterContainerProps): JSX.Element { const insets = useDeviceInsets() const colors = useSporeColors() @@ -164,12 +155,7 @@ export function TransactionModalFooterContainer({ return ( - + {children} {/* diff --git a/packages/wallet/src/features/transactions/swap/TransactionModal.tsx b/packages/wallet/src/features/transactions/swap/TransactionModal.tsx index dfc810a732c..98524a12694 100644 --- a/packages/wallet/src/features/transactions/swap/TransactionModal.tsx +++ b/packages/wallet/src/features/transactions/swap/TransactionModal.tsx @@ -8,14 +8,10 @@ export function TransactionModal(_: TransactionModalProps): JSX.Element { throw new Error('Implemented in `.native.tsx` and `.web.tsx` files') } -export function TransactionModalInnerContainer( - _: TransactionModalInnerContainerProps -): JSX.Element { +export function TransactionModalInnerContainer(_: TransactionModalInnerContainerProps): JSX.Element { throw new Error('Implemented in `.native.tsx` and `.web.tsx` files') } -export function TransactionModalFooterContainer( - _: TransactionModalFooterContainerProps -): JSX.Element { +export function TransactionModalFooterContainer(_: TransactionModalFooterContainerProps): JSX.Element { throw new Error('Implemented in `.native.tsx` and `.web.tsx` files') } diff --git a/packages/wallet/src/features/transactions/swap/TransactionModal.web.tsx b/packages/wallet/src/features/transactions/swap/TransactionModal.web.tsx index bebc31bcbf5..a5a635280b2 100644 --- a/packages/wallet/src/features/transactions/swap/TransactionModal.web.tsx +++ b/packages/wallet/src/features/transactions/swap/TransactionModal.web.tsx @@ -20,7 +20,8 @@ export function TransactionModal({ bottomSheetViewStyles={{}} openWalletRestoreModal={openWalletRestoreModal} walletNeedsRestore={walletNeedsRestore} - onClose={onClose}> + onClose={onClose} + > {children} @@ -34,9 +35,7 @@ export function TransactionModalInnerContainer({ return {children} } -export function TransactionModalFooterContainer({ - children, -}: TransactionModalFooterContainerProps): JSX.Element { +export function TransactionModalFooterContainer({ children }: TransactionModalFooterContainerProps): JSX.Element { return ( {children} diff --git a/packages/wallet/src/features/transactions/swap/analytics.ts b/packages/wallet/src/features/transactions/swap/analytics.ts index 9b0f8f7cfcb..138af2ff430 100644 --- a/packages/wallet/src/features/transactions/swap/analytics.ts +++ b/packages/wallet/src/features/transactions/swap/analytics.ts @@ -3,16 +3,13 @@ import { Currency, TradeType } from '@uniswap/sdk-core' import { useEffect } from 'react' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { SwapTradeBaseProperties } from 'uniswap/src/features/telemetry/types' +import { CurrencyField } from 'uniswap/src/features/transactions/transactionState/types' +import { getCurrencyAddressForAnalytics } from 'uniswap/src/utils/currencyId' import { NumberType } from 'utilities/src/format/types' -import { - LocalizationContextState, - useLocalizationContext, -} from 'wallet/src/features/language/LocalizationContext' -import { getClassicQuoteFromResponse } from 'wallet/src/features/transactions/swap/trade/tradingApi/utils' +import { LocalizationContextState, useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' +import { getClassicQuoteFromResponse } from 'wallet/src/features/transactions/swap/trade/api/utils' import { Trade } from 'wallet/src/features/transactions/swap/trade/types' import { DerivedSwapInfo } from 'wallet/src/features/transactions/swap/types' -import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' -import { getCurrencyAddressForAnalytics } from 'wallet/src/utils/currencyId' import { ValueType, getCurrencyAmount } from 'wallet/src/utils/getCurrencyAmount' // hook-based analytics because this one is data-lifecycle dependent @@ -29,10 +26,7 @@ export function useSwapAnalytics(derivedSwapInfo: DerivedSwapInfo): void { return } - sendAnalyticsEvent( - SwapEventName.SWAP_QUOTE_RECEIVED, - getBaseTradeAnalyticsProperties({ formatter, trade }) - ) + sendAnalyticsEvent(SwapEventName.SWAP_QUOTE_RECEIVED, getBaseTradeAnalyticsProperties({ formatter, trade })) // We only want to re-run this when we get a new `quoteId`. // eslint-disable-next-line react-hooks/exhaustive-deps }, [quoteId]) @@ -57,9 +51,7 @@ export function getBaseTradeAnalyticsProperties({ const quoteId = getClassicQuoteFromResponse(trade?.quote)?.quoteId - const finalOutputAmount = feeCurrencyAmount - ? trade.outputAmount.subtract(feeCurrencyAmount) - : trade.outputAmount + const finalOutputAmount = feeCurrencyAmount ? trade.outputAmount.subtract(feeCurrencyAmount) : trade.outputAmount return { token_in_symbol: trade.inputAmount.currency.symbol, @@ -92,12 +84,9 @@ export function getBaseTradeAnalyticsPropertiesFromSwapInfo({ const { chainId, currencyAmounts } = derivedSwapInfo const inputCurrencyAmount = currencyAmounts[CurrencyField.INPUT] const outputCurrencyAmount = currencyAmounts[CurrencyField.OUTPUT] - const slippageTolerance = - derivedSwapInfo.customSlippageTolerance ?? derivedSwapInfo.autoSlippageTolerance + const slippageTolerance = derivedSwapInfo.customSlippageTolerance ?? derivedSwapInfo.autoSlippageTolerance - const portionAmount = getClassicQuoteFromResponse( - derivedSwapInfo.trade?.trade?.quote - )?.portionAmount + const portionAmount = getClassicQuoteFromResponse(derivedSwapInfo.trade?.trade?.quote)?.portionAmount const feeCurrencyAmount = getCurrencyAmount({ value: portionAmount, @@ -106,22 +95,14 @@ export function getBaseTradeAnalyticsPropertiesFromSwapInfo({ }) const finalOutputAmount = - outputCurrencyAmount && feeCurrencyAmount - ? outputCurrencyAmount.subtract(feeCurrencyAmount) - : outputCurrencyAmount + outputCurrencyAmount && feeCurrencyAmount ? outputCurrencyAmount.subtract(feeCurrencyAmount) : outputCurrencyAmount return { token_in_symbol: inputCurrencyAmount?.currency.symbol, token_out_symbol: outputCurrencyAmount?.currency.symbol, - token_in_address: inputCurrencyAmount - ? getCurrencyAddressForAnalytics(inputCurrencyAmount?.currency) - : '', - token_out_address: outputCurrencyAmount - ? getCurrencyAddressForAnalytics(outputCurrencyAmount?.currency) - : '', - price_impact_basis_points: derivedSwapInfo.trade.trade?.priceImpact - .multiply(100) - .toSignificant(), + token_in_address: inputCurrencyAmount ? getCurrencyAddressForAnalytics(inputCurrencyAmount?.currency) : '', + token_out_address: outputCurrencyAmount ? getCurrencyAddressForAnalytics(outputCurrencyAmount?.currency) : '', + price_impact_basis_points: derivedSwapInfo.trade.trade?.priceImpact.multiply(100).toSignificant(), estimated_network_fee_usd: undefined, chain_id: chainId, token_in_amount: inputCurrencyAmount?.toExact() ?? '', diff --git a/packages/wallet/src/features/transactions/swap/createSwapFormFromTxDetails.ts b/packages/wallet/src/features/transactions/swap/createSwapFormFromTxDetails.ts index 7fdb98c1c5d..d4a9348222b 100644 --- a/packages/wallet/src/features/transactions/swap/createSwapFormFromTxDetails.ts +++ b/packages/wallet/src/features/transactions/swap/createSwapFormFromTxDetails.ts @@ -1,13 +1,10 @@ import { Currency, TradeType } from '@uniswap/sdk-core' +import { AssetType, CurrencyAsset } from 'uniswap/src/entities/assets' +import { CurrencyField, TransactionState } from 'uniswap/src/features/transactions/transactionState/types' +import { currencyAddress, currencyIdToAddress } from 'uniswap/src/utils/currencyId' import { logger } from 'utilities/src/logger/logger' -import { AssetType, CurrencyAsset } from 'wallet/src/entities/assets' import { getAmountsFromTrade } from 'wallet/src/features/transactions/getAmountsFromTrade' -import { - CurrencyField, - TransactionState, -} from 'wallet/src/features/transactions/transactionState/types' import { TransactionDetails, TransactionType } from 'wallet/src/features/transactions/types' -import { currencyAddress, currencyIdToAddress } from 'wallet/src/utils/currencyId' import { ValueType, getCurrencyAmount } from 'wallet/src/utils/getCurrencyAmount' interface Props { @@ -35,9 +32,7 @@ export function createSwapFormFromTxDetails({ const { typeInfo } = transactionDetails if (typeInfo.type !== TransactionType.Swap) { - throw new Error( - `Tx hash ${txHash} does not correspond to a swap tx. It is of type ${typeInfo.type}` - ) + throw new Error(`Tx hash ${txHash} does not correspond to a swap tx. It is of type ${typeInfo.type}`) } const { inputCurrencyAmountRaw, outputCurrencyAmountRaw } = getAmountsFromTrade(typeInfo) @@ -100,9 +95,7 @@ export function createWrapFormFromTxDetails({ const { typeInfo } = transactionDetails if (typeInfo.type !== TransactionType.Wrap) { - throw new Error( - `Tx hash ${txHash} does not correspond to a wrap tx. It is of type ${typeInfo.type}` - ) + throw new Error(`Tx hash ${txHash} does not correspond to a wrap tx. It is of type ${typeInfo.type}`) } const currencyAmountRaw = typeInfo.currencyAmountRaw diff --git a/packages/wallet/src/features/transactions/swap/customRpc.ts b/packages/wallet/src/features/transactions/swap/customRpc.ts index e21f965c336..838121a5a73 100644 --- a/packages/wallet/src/features/transactions/swap/customRpc.ts +++ b/packages/wallet/src/features/transactions/swap/customRpc.ts @@ -18,7 +18,5 @@ export function useShouldUseMEVBlocker(chainId: Maybe): boolean { const isSwapProtectionSettingEnabled = useSwapProtectionSetting() === SwapProtectionSetting.On const isMevBlockerSupportedOnChain = chainId ? isPrivateRpcSupportedOnChain(chainId) : false - return Boolean( - isMevBlockerFeatureEnabled && isSwapProtectionSettingEnabled && isMevBlockerSupportedOnChain - ) + return Boolean(isMevBlockerFeatureEnabled && isSwapProtectionSettingEnabled && isMevBlockerSupportedOnChain) } diff --git a/packages/wallet/src/features/transactions/swap/hooks/useExactOutputWillFail.test.ts b/packages/wallet/src/features/transactions/swap/hooks/useExactOutputWillFail.test.ts index d02e46d9c68..64d03c159c4 100644 --- a/packages/wallet/src/features/transactions/swap/hooks/useExactOutputWillFail.test.ts +++ b/packages/wallet/src/features/transactions/swap/hooks/useExactOutputWillFail.test.ts @@ -1,10 +1,10 @@ import { renderHook } from '@testing-library/react-hooks' import { Token } from '@uniswap/sdk-core' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' -import { buildCurrency } from 'wallet/src/features/dataApi/utils' +import { buildCurrency } from 'uniswap/src/features/dataApi/utils' +import { CurrencyField } from 'uniswap/src/features/transactions/transactionState/types' import { useExactOutputWillFail } from 'wallet/src/features/transactions/swap/hooks/useExactOutputWillFail' -import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' -import { SAMPLE_CURRENCY_ID_1 } from 'wallet/src/test/fixtures/constants' +import { SAMPLE_CURRENCY_ID_1 } from 'wallet/src/test/fixtures' describe('useExactOutputWillFail', () => { const createToken = (buyFeeBps?: string, sellFeeBps?: string): Token => @@ -33,7 +33,7 @@ describe('useExactOutputWillFail', () => { [CurrencyField.INPUT]: createCurrencyInfo(createToken()), [CurrencyField.OUTPUT]: createCurrencyInfo(createToken()), }, - }) + }), ) expect(result.current).toEqual({ outputTokenHasBuyTax: false, @@ -49,7 +49,7 @@ describe('useExactOutputWillFail', () => { [CurrencyField.INPUT]: createCurrencyInfo(createToken(undefined, '100')), [CurrencyField.OUTPUT]: createCurrencyInfo(createToken()), }, - }) + }), ) expect(result.current.exactOutputWillFail).toBe(true) }) @@ -61,7 +61,7 @@ describe('useExactOutputWillFail', () => { [CurrencyField.INPUT]: createCurrencyInfo(createToken()), [CurrencyField.OUTPUT]: createCurrencyInfo(createToken('100')), }, - }) + }), ) expect(result.current.outputTokenHasBuyTax).toBe(true) }) @@ -73,7 +73,7 @@ describe('useExactOutputWillFail', () => { [CurrencyField.INPUT]: createCurrencyInfo(createToken('100')), [CurrencyField.OUTPUT]: createCurrencyInfo(createToken()), }, - }) + }), ) expect(result.current.exactOutputWouldFailIfCurrenciesSwitched).toBe(true) }) @@ -85,7 +85,7 @@ describe('useExactOutputWillFail', () => { [CurrencyField.INPUT]: createCurrencyInfo(createToken()), [CurrencyField.OUTPUT]: createCurrencyInfo(createToken(undefined, '100')), }, - }) + }), ) expect(result.current.exactOutputWouldFailIfCurrenciesSwitched).toBe(true) }) diff --git a/packages/wallet/src/features/transactions/swap/hooks/useExactOutputWillFail.ts b/packages/wallet/src/features/transactions/swap/hooks/useExactOutputWillFail.ts index 2938d4ddef6..bf82d47cdee 100644 --- a/packages/wallet/src/features/transactions/swap/hooks/useExactOutputWillFail.ts +++ b/packages/wallet/src/features/transactions/swap/hooks/useExactOutputWillFail.ts @@ -1,6 +1,6 @@ import { Token } from '@uniswap/sdk-core' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' -import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' +import { CurrencyField } from 'uniswap/src/features/transactions/transactionState/types' export function hasTokenFee(currencyInfo: Maybe): { hasBuyTax: boolean @@ -29,10 +29,10 @@ export function useExactOutputWillFail({ exactOutputWouldFailIfCurrenciesSwitched: boolean } { const { hasBuyTax: inputTokenHasBuyTax, hasSellTax: inputTokenHasSellTax } = hasTokenFee( - currencies[CurrencyField.INPUT] + currencies[CurrencyField.INPUT], ) const { hasBuyTax: outputTokenHasBuyTax, hasSellTax: outputTokenHasSellTax } = hasTokenFee( - currencies[CurrencyField.OUTPUT] + currencies[CurrencyField.OUTPUT], ) const exactOutputWillFail = inputTokenHasSellTax || outputTokenHasBuyTax const exactOutputWouldFailIfCurrenciesSwitched = inputTokenHasBuyTax || outputTokenHasSellTax diff --git a/packages/wallet/src/features/transactions/swap/hooks/useGasFeeHighRelativeToValue.ts b/packages/wallet/src/features/transactions/swap/hooks/useGasFeeHighRelativeToValue.ts index 90578135f12..1f89663522e 100644 --- a/packages/wallet/src/features/transactions/swap/hooks/useGasFeeHighRelativeToValue.ts +++ b/packages/wallet/src/features/transactions/swap/hooks/useGasFeeHighRelativeToValue.ts @@ -3,7 +3,7 @@ import { useMemo } from 'react' export function useGasFeeHighRelativeToValue( gasFeeUSD: string | undefined, - value: Maybe> + value: Maybe>, ): boolean { return useMemo(() => { if (!value) { diff --git a/packages/wallet/src/features/transactions/swap/hooks/useSwapPrefilledState.ts b/packages/wallet/src/features/transactions/swap/hooks/useSwapPrefilledState.ts index 536ac4abdb0..7ce9e3450c3 100644 --- a/packages/wallet/src/features/transactions/swap/hooks/useSwapPrefilledState.ts +++ b/packages/wallet/src/features/transactions/swap/hooks/useSwapPrefilledState.ts @@ -1,18 +1,16 @@ import { useMemo } from 'react' -import { WalletChainId } from 'uniswap/src/types/chains' -import { getNativeAddress } from 'wallet/src/constants/addresses' -import { AssetType, CurrencyAsset } from 'wallet/src/entities/assets' -import { SwapFormState } from 'wallet/src/features/transactions/contexts/SwapFormContext' +import { getNativeAddress } from 'uniswap/src/constants/addresses' +import { AssetType, CurrencyAsset } from 'uniswap/src/entities/assets' import { CurrencyField, TradeProtocolPreference, TransactionState, -} from 'wallet/src/features/transactions/transactionState/types' -import { areAddressesEqual } from 'wallet/src/utils/addresses' +} from 'uniswap/src/features/transactions/transactionState/types' +import { WalletChainId } from 'uniswap/src/types/chains' +import { areAddressesEqual } from 'uniswap/src/utils/addresses' +import { SwapFormState } from 'wallet/src/features/transactions/contexts/SwapFormContext' -export function useSwapPrefilledState( - initialState: TransactionState | undefined -): SwapFormState | undefined { +export function useSwapPrefilledState(initialState: TransactionState | undefined): SwapFormState | undefined { const swapPrefilledState = useMemo( (): SwapFormState | undefined => initialState @@ -31,7 +29,7 @@ export function useSwapPrefilledState( tradeProtocolPreference: TradeProtocolPreference.Default, } : undefined, - [initialState] + [initialState], ) return swapPrefilledState diff --git a/packages/wallet/src/features/transactions/swap/modals/FeeOnTransferWarning.tsx b/packages/wallet/src/features/transactions/swap/modals/FeeOnTransferWarning.tsx index 6162bc6955e..c16f73b2d31 100644 --- a/packages/wallet/src/features/transactions/swap/modals/FeeOnTransferWarning.tsx +++ b/packages/wallet/src/features/transactions/swap/modals/FeeOnTransferWarning.tsx @@ -3,10 +3,10 @@ import { useTranslation } from 'react-i18next' import { isWeb, useSporeColors } from 'ui/src' import { MoneyBillSend } from 'ui/src/components/icons' import { iconSizes } from 'ui/src/theme' +import { LearnMoreLink } from 'uniswap/src/components/text/LearnMoreLink' import { uniswapUrls } from 'uniswap/src/constants/urls' import { ModalName } from 'uniswap/src/features/telemetry/constants' import { WarningInfo } from 'wallet/src/components/modals/WarningModal/WarningInfo' -import { LearnMoreLink } from 'wallet/src/components/text/LearnMoreLink' export function FeeOnTransferWarning({ children }: PropsWithChildren): JSX.Element { const { t } = useTranslation() @@ -43,7 +43,8 @@ export function FeeOnTransferWarning({ children }: PropsWithChildren): JSX.Eleme text: caption, title, placement: 'top', - }}> + }} + > {children} ) diff --git a/packages/wallet/src/features/transactions/swap/modals/NetworkFeeWarning.tsx b/packages/wallet/src/features/transactions/swap/modals/NetworkFeeWarning.tsx index 7894f826e0e..81aad29ae77 100644 --- a/packages/wallet/src/features/transactions/swap/modals/NetworkFeeWarning.tsx +++ b/packages/wallet/src/features/transactions/swap/modals/NetworkFeeWarning.tsx @@ -2,11 +2,11 @@ import { PropsWithChildren } from 'react' import { useTranslation } from 'react-i18next' import { isWeb, useSporeColors } from 'ui/src' import { Gas } from 'ui/src/components/icons' +import { LearnMoreLink } from 'uniswap/src/components/text/LearnMoreLink' import { uniswapUrls } from 'uniswap/src/constants/urls' import { ModalName } from 'uniswap/src/features/telemetry/constants' import { WarningInfo } from 'wallet/src/components/modals/WarningModal/WarningInfo' import { WarningTooltipProps } from 'wallet/src/components/modals/WarningModal/WarningTooltipProps' -import { LearnMoreLink } from 'wallet/src/components/text/LearnMoreLink' import { WarningSeverity } from 'wallet/src/features/transactions/WarningModal/types' export function NetworkFeeWarning({ @@ -38,12 +38,7 @@ export function NetworkFeeWarning({ backgroundIconColor: colors.surface2.get(), caption: text, closeText: t('common.button.close'), - icon: ( - - ), + icon: , modalName: ModalName.NetworkFeeInfo, severity: WarningSeverity.None, title: t('transaction.networkCost.label'), @@ -53,7 +48,8 @@ export function NetworkFeeWarning({ placement, icon: gasFeeHighRelativeToValue ? : null, }} - trigger={tooltipTrigger}> + trigger={tooltipTrigger} + > {children} ) diff --git a/packages/wallet/src/features/transactions/swap/modals/PriceImpactWarning.tsx b/packages/wallet/src/features/transactions/swap/modals/PriceImpactWarning.tsx index f929f104398..0587cd1e3c8 100644 --- a/packages/wallet/src/features/transactions/swap/modals/PriceImpactWarning.tsx +++ b/packages/wallet/src/features/transactions/swap/modals/PriceImpactWarning.tsx @@ -5,10 +5,7 @@ import { ModalName } from 'uniswap/src/features/telemetry/constants' import { WarningInfo } from 'wallet/src/components/modals/WarningModal/WarningInfo' import { Warning } from 'wallet/src/features/transactions/WarningModal/types' -export function PriceImpactWarning({ - children, - warning, -}: PropsWithChildren<{ warning: Warning }>): JSX.Element { +export function PriceImpactWarning({ children, warning }: PropsWithChildren<{ warning: Warning }>): JSX.Element { const { t } = useTranslation() const caption = warning.message diff --git a/packages/wallet/src/features/transactions/swap/modals/QueuedOrderModal.tsx b/packages/wallet/src/features/transactions/swap/modals/QueuedOrderModal.tsx new file mode 100644 index 00000000000..da2b1cf34e5 --- /dev/null +++ b/packages/wallet/src/features/transactions/swap/modals/QueuedOrderModal.tsx @@ -0,0 +1,157 @@ +import { CurrencyAmount, TradeType } from '@uniswap/sdk-core' +import { t } from 'i18next' +import { useCallback, useMemo } from 'react' +import { useDispatch } from 'react-redux' +import { Button, Flex, Separator, Text, isWeb, useIsShortMobileDevice } from 'ui/src' +import { AlertTriangle } from 'ui/src/components/icons' +import { BottomSheetModal } from 'uniswap/src/components/modals/BottomSheetModal' +import { LearnMoreLink } from 'uniswap/src/components/text/LearnMoreLink' +import { uniswapUrls } from 'uniswap/src/constants/urls' +import { AssetType, TradeableAsset } from 'uniswap/src/entities/assets' +import { FeatureFlags } from 'uniswap/src/features/gating/flags' +import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' +import { ModalName } from 'uniswap/src/features/telemetry/constants' +import { CurrencyField, TransactionState } from 'uniswap/src/features/transactions/transactionState/types' +import { currencyAddress } from 'uniswap/src/utils/currencyId' +import { isMobile } from 'utilities/src/platform' +import { useWalletNavigation } from 'wallet/src/contexts/WalletNavigationContext' +import { useCurrencyInfo } from 'wallet/src/features/tokens/useCurrencyInfo' +import { SwapTransactionDetails } from 'wallet/src/features/transactions/SummaryCards/DetailsModal/SwapTransactionDetails' +import { isSwapTransactionInfo } from 'wallet/src/features/transactions/SummaryCards/DetailsModal/types' +import { ErroredQueuedOrderStatus, useErroredQueuedOrders } from 'wallet/src/features/transactions/hooks' +import { updateTransaction } from 'wallet/src/features/transactions/slice' +import { QueuedOrderStatus, TransactionDetails, TransactionStatus } from 'wallet/src/features/transactions/types' +import { useActiveSignerAccount } from 'wallet/src/features/wallet/hooks' + +const QUEUE_STATUS_TO_MESSAGE = { + [QueuedOrderStatus.AppClosed]: t('swap.warning.queuedOrder.appClosed'), + [QueuedOrderStatus.ApprovalFailed]: t('swap.warning.queuedOrder.approvalFailed'), + [QueuedOrderStatus.WrapFailed]: t('swap.warning.queuedOrder.wrapFailed'), + [QueuedOrderStatus.SubmissionFailed]: t('swap.warning.queuedOrder.submissionFailed'), + [QueuedOrderStatus.Stale]: t('swap.warning.queuedOrder.stale'), +} as const satisfies Record + +export function QueuedOrderModal(): JSX.Element | null { + const uniswapXEnabled = useFeatureFlag(FeatureFlags.UniswapX) + const isShortMobileDevice = useIsShortMobileDevice() + + const account = useActiveSignerAccount() + const erroredQueuedOrders = useErroredQueuedOrders(account?.address ?? null) + const currentFailedOrder = erroredQueuedOrders?.[0] + + const dispatch = useDispatch() + const onCancel = useCallback(() => { + if (!currentFailedOrder) { + return + } + dispatch(updateTransaction({ ...currentFailedOrder, status: TransactionStatus.Canceled })) + }, [dispatch, currentFailedOrder]) + + const { navigateToSwapFlow } = useWalletNavigation() + const transactionState = useTransactionState(currentFailedOrder) + const onRetry = useCallback(() => { + if (transactionState) { + navigateToSwapFlow({ initialState: transactionState }) + onCancel() + } + }, [transactionState, navigateToSwapFlow, onCancel]) + + // If there are no failed orders tracked in state, return nothing. + if (!uniswapXEnabled || !currentFailedOrder || !isSwapTransactionInfo(currentFailedOrder.typeInfo)) { + return null + } + const reason = QUEUE_STATUS_TO_MESSAGE[currentFailedOrder.queueStatus] + // If a wrap tx was involved as part of the order flow, show a message indicating that the user now has WETH, + // unless the wrap failed, in which case the user still has ETH and the message should not be shown. + const showWrapMessage = + Boolean(currentFailedOrder.wrapTxHash) && currentFailedOrder.queueStatus !== QueuedOrderStatus.WrapFailed + + const buttonSize = isShortMobileDevice ? 'small' : 'medium' + + const platformButtonStyling = isWeb ? { flex: 1, flexBasis: 1 } : undefined + + return ( + + + + + + + + + {t('swap.warning.queuedOrder.title')} + + + {reason} + {showWrapMessage && ' '} + {showWrapMessage && <> {t('swap.warning.queuedOrder.wrap.message')}} + + + + + + + + + + + + ) +} + +function useTransactionState(transaction: TransactionDetails | undefined): TransactionState | undefined { + const { typeInfo } = transaction ?? {} + const isSwap = typeInfo && isSwapTransactionInfo(typeInfo) + + const inputCurrency = useCurrencyInfo(isSwap ? typeInfo.inputCurrencyId : undefined)?.currency + const outputCurrency = useCurrencyInfo(isSwap ? typeInfo.outputCurrencyId : undefined)?.currency + + return useMemo(() => { + if (!isSwap || !inputCurrency || !outputCurrency) { + return + } + + const input: TradeableAsset = { + address: currencyAddress(inputCurrency), + chainId: inputCurrency.chainId, + type: AssetType.Currency, + } + + const output: TradeableAsset = { + address: currencyAddress(outputCurrency), + chainId: inputCurrency.chainId, + type: AssetType.Currency, + } + + const exactCurrency = typeInfo.tradeType === TradeType.EXACT_INPUT ? inputCurrency : outputCurrency + const exactCurrencyField = typeInfo.tradeType === TradeType.EXACT_INPUT ? CurrencyField.INPUT : CurrencyField.OUTPUT + const exactAmountTokenRaw = + typeInfo.tradeType === TradeType.EXACT_INPUT ? typeInfo.inputCurrencyAmountRaw : typeInfo.outputCurrencyAmountRaw + + const exactAmountToken = CurrencyAmount.fromRawAmount(exactCurrency, exactAmountTokenRaw).toExact() + + return { + input, + output, + exactCurrencyField, + exactAmountToken, + customSlippageTolerance: typeInfo.slippageTolerance, + } + }, [isSwap, typeInfo, inputCurrency, outputCurrency]) +} diff --git a/packages/wallet/src/features/transactions/swap/modals/SlippageInfoModal.tsx b/packages/wallet/src/features/transactions/swap/modals/SlippageInfoModal.tsx index 8598bb36cf7..9372256dfd4 100644 --- a/packages/wallet/src/features/transactions/swap/modals/SlippageInfoModal.tsx +++ b/packages/wallet/src/features/transactions/swap/modals/SlippageInfoModal.tsx @@ -5,10 +5,10 @@ import AlertTriangleIcon from 'ui/src/assets/icons/alert-triangle.svg' import { Settings } from 'ui/src/components/icons' import { iconSizes } from 'ui/src/theme' import { BottomSheetModal } from 'uniswap/src/components/modals/BottomSheetModal' +import { LearnMoreLink } from 'uniswap/src/components/text/LearnMoreLink' import { uniswapUrls } from 'uniswap/src/constants/urls' import { ModalName } from 'uniswap/src/features/telemetry/constants' import { NumberType } from 'utilities/src/format/types' -import { LearnMoreLink } from 'wallet/src/components/text/LearnMoreLink' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' import { Trade } from 'wallet/src/features/transactions/swap/trade/types' import { slippageToleranceToPercent } from 'wallet/src/features/transactions/swap/utils' @@ -41,15 +41,10 @@ export function SlippageInfoModal({ type: NumberType.TokenTx, }) const tokenSymbol = - trade.tradeType === TradeType.EXACT_INPUT - ? trade.outputAmount.currency.symbol - : trade.inputAmount.currency.symbol + trade.tradeType === TradeType.EXACT_INPUT ? trade.outputAmount.currency.symbol : trade.inputAmount.currency.symbol return ( - + @@ -68,26 +63,21 @@ export function SlippageInfoModal({ gap="$spacing8" px="$spacing16" py="$spacing12" - width="100%"> + width="100%" + > {t('swap.settings.slippage.control.title')} {!isCustomSlippage ? ( - + {t('swap.settings.slippage.control.auto')} ) : null} - + {formatPercent(slippageTolerance)} diff --git a/packages/wallet/src/features/transactions/swap/modals/SwapFeeWarning.tsx b/packages/wallet/src/features/transactions/swap/modals/SwapFeeWarning.tsx index 08c15cfc40e..adaf3b81b7b 100644 --- a/packages/wallet/src/features/transactions/swap/modals/SwapFeeWarning.tsx +++ b/packages/wallet/src/features/transactions/swap/modals/SwapFeeWarning.tsx @@ -3,14 +3,11 @@ import { useTranslation } from 'react-i18next' import { Text, TouchableArea, isWeb, useSporeColors } from 'ui/src' import { uniswapUrls } from 'uniswap/src/constants/urls' import { ModalName } from 'uniswap/src/features/telemetry/constants' +import { openUri } from 'uniswap/src/utils/linking' import { WarningInfo } from 'wallet/src/components/modals/WarningModal/WarningInfo' import { WarningSeverity } from 'wallet/src/features/transactions/WarningModal/types' -import { openUri } from 'wallet/src/utils/linking' -export function SwapFeeWarning({ - noFee, - children, -}: PropsWithChildren<{ noFee: boolean }>): JSX.Element { +export function SwapFeeWarning({ noFee, children }: PropsWithChildren<{ noFee: boolean }>): JSX.Element { const colors = useSporeColors() const { t } = useTranslation() @@ -18,9 +15,7 @@ export function SwapFeeWarning({ await openUri(uniswapUrls.helpArticleUrls.swapFeeInfo) } - const caption = noFee - ? t('swap.warning.uniswapFee.message.default') - : t('swap.warning.uniswapFee.message.included') + const caption = noFee ? t('swap.warning.uniswapFee.message.default') : t('swap.warning.uniswapFee.message.included') return ( + tooltipProps={{ text: caption, placement: 'top' }} + > {children} ) diff --git a/packages/wallet/src/features/transactions/swap/modals/SwapProtectionModal.tsx b/packages/wallet/src/features/transactions/swap/modals/SwapProtectionModal.tsx index 41488f81fe5..bb119d9e060 100644 --- a/packages/wallet/src/features/transactions/swap/modals/SwapProtectionModal.tsx +++ b/packages/wallet/src/features/transactions/swap/modals/SwapProtectionModal.tsx @@ -1,10 +1,10 @@ import { useTranslation } from 'react-i18next' import { useSporeColors } from 'ui/src' import { ShieldCheck } from 'ui/src/components/icons' +import { LearnMoreLink } from 'uniswap/src/components/text/LearnMoreLink' import { uniswapUrls } from 'uniswap/src/constants/urls' import { ModalName } from 'uniswap/src/features/telemetry/constants' import { WarningModal } from 'wallet/src/components/modals/WarningModal/WarningModal' -import { LearnMoreLink } from 'wallet/src/components/text/LearnMoreLink' export function SwapProtectionInfoModal({ onClose }: { onClose: () => void }): JSX.Element { const colors = useSporeColors() @@ -18,7 +18,8 @@ export function SwapProtectionInfoModal({ onClose }: { onClose: () => void }): J icon={} modalName={ModalName.SwapProtection} title={t('swap.settings.protection.title')} - onClose={onClose}> + onClose={onClose} + > ) diff --git a/packages/wallet/src/features/transactions/swap/modals/UniswapXInfo.tsx b/packages/wallet/src/features/transactions/swap/modals/UniswapXInfo.tsx new file mode 100644 index 00000000000..c65fb8f6e55 --- /dev/null +++ b/packages/wallet/src/features/transactions/swap/modals/UniswapXInfo.tsx @@ -0,0 +1,50 @@ +import { PropsWithChildren } from 'react' +import { useTranslation } from 'react-i18next' +import { UniswapXText, isWeb } from 'ui/src' +import { UniswapX } from 'ui/src/components/icons' +import { colors, opacify } from 'ui/src/theme' +import { LearnMoreLink } from 'uniswap/src/components/text/LearnMoreLink' +import { uniswapUrls } from 'uniswap/src/constants/urls' +import { ModalName } from 'uniswap/src/features/telemetry/constants' +import { WarningInfo } from 'wallet/src/components/modals/WarningModal/WarningInfo' +import { WarningTooltipProps } from 'wallet/src/components/modals/WarningModal/WarningTooltipProps' +import { WarningSeverity } from 'wallet/src/features/transactions/WarningModal/types' + +export function UniswapXInfo({ + children, + tooltipTrigger, + placement = 'top', +}: PropsWithChildren<{ + tooltipTrigger?: WarningTooltipProps['trigger'] + placement?: WarningTooltipProps['placement'] +}>): JSX.Element { + const { t } = useTranslation() + + return ( + + } + modalProps={{ + backgroundIconColor: opacify(16, colors.uniswapXPurple), + caption: t('uniswapx.description'), + closeText: t('common.button.close'), + icon: , + modalName: ModalName.UniswapXInfo, + severity: WarningSeverity.None, + title: {t('uniswapx.label')}, + }} + tooltipProps={{ + text: t('uniswapx.description'), + placement, + icon: , + }} + trigger={tooltipTrigger} + > + {children} + + ) +} diff --git a/packages/wallet/src/features/transactions/swap/modals/UniswapXInfoModal.tsx b/packages/wallet/src/features/transactions/swap/modals/UniswapXInfoModal.tsx index 20eac89d0fe..b5ef4e43f8d 100644 --- a/packages/wallet/src/features/transactions/swap/modals/UniswapXInfoModal.tsx +++ b/packages/wallet/src/features/transactions/swap/modals/UniswapXInfoModal.tsx @@ -2,10 +2,10 @@ import { useTranslation } from 'react-i18next' import { UniswapXText, isWeb } from 'ui/src' import { UniswapX } from 'ui/src/components/icons' import { colors, opacify } from 'ui/src/theme' +import { LearnMoreLink } from 'uniswap/src/components/text/LearnMoreLink' import { uniswapUrls } from 'uniswap/src/constants/urls' import { ModalName } from 'uniswap/src/features/telemetry/constants' import { WarningModal } from 'wallet/src/components/modals/WarningModal/WarningModal' -import { LearnMoreLink } from 'wallet/src/components/text/LearnMoreLink' export function UniswapXInfoModal({ onClose }: { onClose: () => void }): JSX.Element { const { t } = useTranslation() @@ -17,10 +17,9 @@ export function UniswapXInfoModal({ onClose }: { onClose: () => void }): JSX.Ele closeText={t('common.button.close')} icon={} modalName={ModalName.UniswapXInfo} - title={ - {t('uniswapx.label')} - } - onClose={onClose}> + title={{t('uniswapx.label')}} + onClose={onClose} + > ) diff --git a/packages/wallet/src/features/transactions/swap/modals/settings/ProtocolPreferenceScreen.tsx b/packages/wallet/src/features/transactions/swap/modals/settings/ProtocolPreferenceScreen.tsx index 817aec7f8c4..3c3888f6c78 100644 --- a/packages/wallet/src/features/transactions/swap/modals/settings/ProtocolPreferenceScreen.tsx +++ b/packages/wallet/src/features/transactions/swap/modals/settings/ProtocolPreferenceScreen.tsx @@ -1,5 +1,5 @@ import { TFunction } from 'i18next' -import { ReactNode, useState } from 'react' +import { ReactNode } from 'react' import { Trans, useTranslation } from 'react-i18next' import { Flex, Text, TouchableArea, UniswapXText } from 'ui/src' import { InfoCircleFilled, UniswapX } from 'ui/src/components/icons' @@ -7,9 +7,9 @@ import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import Trace from 'uniswap/src/features/telemetry/Trace' import { ElementName, ElementNameType } from 'uniswap/src/features/telemetry/constants' +import { TradeProtocolPreference } from 'uniswap/src/features/transactions/transactionState/types' import { isMobileApp } from 'utilities/src/platform' -import { UniswapXInfoModal } from 'wallet/src/features/transactions/swap/modals/UniswapXInfoModal' -import { TradeProtocolPreference } from 'wallet/src/features/transactions/transactionState/types' +import { UniswapXInfo } from 'wallet/src/features/transactions/swap/modals/UniswapXInfo' // TODO(WEB-4297): Update controls on this screen to allow combinations of any/all routing options once supported by TradingAPI. export function ProtocolPreferenceScreen({ @@ -46,10 +46,7 @@ export function ProtocolPreferenceScreen({ ) } -export function getTitleFromProtocolPreference( - preference: TradeProtocolPreference, - t: TFunction -): string { +export function getTitleFromProtocolPreference(preference: TradeProtocolPreference, t: TFunction): string { switch (preference) { case TradeProtocolPreference.Default: return t('swap.settings.routingPreference.option.default.title') @@ -96,14 +93,10 @@ function OptionRow({ borderRadius="$roundedFull" borderWidth="$spacing2" height="$spacing24" - width="$spacing24"> + width="$spacing24" + > {active && ( - + )} @@ -113,40 +106,35 @@ function OptionRow({ } function DefaultOptionDescription(): JSX.Element { - const [showUniswapXModal, setShowUniswapXModal] = useState(false) const uniswapXEnabled = useFeatureFlag(FeatureFlags.UniswapX) const { t } = useTranslation() return ( - {showUniswapXModal && setShowUniswapXModal(false)} />} {t('swap.settings.routingPreference.option.default.description')} {uniswapXEnabled && ( - setShowUniswapXModal(true)}> - - - ), - gradient: , - info: ( - - ), - }} - i18nKey="uniswapx.included" - /> - - + + , + gradient: , + info: ( + + ), + }} + i18nKey="uniswapx.included" + /> + + } + /> )} ) diff --git a/packages/wallet/src/features/transactions/swap/modals/settings/SlippageSettingsRow.native.tsx b/packages/wallet/src/features/transactions/swap/modals/settings/SlippageSettingsRow.native.tsx index 299d9ccd204..d2352b03b50 100644 --- a/packages/wallet/src/features/transactions/swap/modals/settings/SlippageSettingsRow.native.tsx +++ b/packages/wallet/src/features/transactions/swap/modals/settings/SlippageSettingsRow.native.tsx @@ -6,17 +6,13 @@ import { MAX_AUTO_SLIPPAGE_TOLERANCE } from 'wallet/src/constants/transactions' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' import { SlippageSettingsRowProps } from 'wallet/src/features/transactions/swap/modals/settings/SlippageSettingsRowProps' -export function SlippageSettingsRow({ - derivedSwapInfo, - onPress, -}: SlippageSettingsRowProps): JSX.Element { +export function SlippageSettingsRow({ derivedSwapInfo, onPress }: SlippageSettingsRowProps): JSX.Element { const { t } = useTranslation() const { formatPercent } = useLocalizationContext() const { customSlippageTolerance, autoSlippageTolerance } = derivedSwapInfo const isCustomSlippage = !!customSlippageTolerance - const currentSlippage = - customSlippageTolerance ?? autoSlippageTolerance ?? MAX_AUTO_SLIPPAGE_TOLERANCE + const currentSlippage = customSlippageTolerance ?? autoSlippageTolerance ?? MAX_AUTO_SLIPPAGE_TOLERANCE return ( @@ -35,12 +31,7 @@ export function SlippageSettingsRow({ {formatPercent(currentSlippage)} - + diff --git a/packages/wallet/src/features/transactions/swap/modals/settings/SlippageSettingsRow.web.tsx b/packages/wallet/src/features/transactions/swap/modals/settings/SlippageSettingsRow.web.tsx index 85f7f77016d..e36ea0d6104 100644 --- a/packages/wallet/src/features/transactions/swap/modals/settings/SlippageSettingsRow.web.tsx +++ b/packages/wallet/src/features/transactions/swap/modals/settings/SlippageSettingsRow.web.tsx @@ -7,10 +7,7 @@ import { useSlippageSettings } from 'wallet/src/features/transactions/swap/modal const INPUT_MIN_WIDTH = 44 -export function SlippageSettingsRow({ - derivedSwapInfo, - onSlippageChange, -}: SlippageSettingsRowProps): JSX.Element { +export function SlippageSettingsRow({ derivedSwapInfo, onSlippageChange }: SlippageSettingsRowProps): JSX.Element { const { t } = useTranslation() const [inputWidth, setInputWidth] = useState(0) @@ -31,9 +28,7 @@ export function SlippageSettingsRow({ } const backgroundColor = isEditingSlippage ? '$surface2' : '$surface1' - const inputValue = autoSlippageEnabled - ? autoSlippageTolerance.toFixed(2).toString() - : inputSlippageTolerance + const inputValue = autoSlippageEnabled ? autoSlippageTolerance.toFixed(2).toString() : inputSlippageTolerance return ( @@ -49,13 +44,15 @@ export function SlippageSettingsRow({ borderWidth={1} gap="$spacing8" p="$spacing4" - style={inputAnimatedStyle}> + style={inputAnimatedStyle} + > + onPress={onPressAutoSlippage} + > {t('swap.settings.slippage.control.auto')} @@ -85,7 +82,8 @@ export function SlippageSettingsRow({ style={{ position: 'absolute' }} variant="subheading2" zIndex={-1} - onLayout={onInputTextLayout}> + onLayout={onInputTextLayout} + > {inputValue} diff --git a/packages/wallet/src/features/transactions/swap/modals/settings/SlippageSettingsScreen.tsx b/packages/wallet/src/features/transactions/swap/modals/settings/SlippageSettingsScreen.tsx index 28141a27fa2..5b5936431a5 100644 --- a/packages/wallet/src/features/transactions/swap/modals/settings/SlippageSettingsScreen.tsx +++ b/packages/wallet/src/features/transactions/swap/modals/settings/SlippageSettingsScreen.tsx @@ -3,9 +3,9 @@ import { Flex, Text, TouchableArea, isWeb, useSporeColors } from 'ui/src' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { fonts, iconSizes, spacing } from 'ui/src/theme' import { BottomSheetTextInput } from 'uniswap/src/components/modals/BottomSheetModal' +import { LearnMoreLink } from 'uniswap/src/components/text/LearnMoreLink' import { uniswapUrls } from 'uniswap/src/constants/urls' import PlusMinusButton, { PlusMinusButtonType } from 'wallet/src/components/buttons/PlusMinusButton' -import { LearnMoreLink } from 'wallet/src/components/text/LearnMoreLink' import { MAX_CUSTOM_SLIPPAGE_TOLERANCE } from 'wallet/src/constants/transactions' import { SwapSettingsMessage } from 'wallet/src/features/transactions/swap/modals/settings/SwapSettingsMessage' import { useSlippageSettings } from 'wallet/src/features/transactions/swap/modals/settings/useSlippageSettings' @@ -64,7 +64,8 @@ export function SlippageSettingsScreen({ borderWidth={1} gap="$spacing12" p="$spacing16" - style={inputAnimatedStyle}> + style={inputAnimatedStyle} + > {t('swap.settings.slippage.control.auto')} @@ -82,11 +83,7 @@ export function SlippageSettingsScreen({ }), }} textAlign="center" - value={ - autoSlippageEnabled - ? autoSlippageTolerance.toFixed(2).toString() - : inputSlippageTolerance - } + value={autoSlippageEnabled ? autoSlippageTolerance.toFixed(2).toString() : inputSlippageTolerance} onBlur={onBlurSlippageInput} onChangeText={onChangeSlippageInput} onFocus={onFocusSlippageInput} diff --git a/packages/wallet/src/features/transactions/swap/modals/settings/SwapSettingsMessage.tsx b/packages/wallet/src/features/transactions/swap/modals/settings/SwapSettingsMessage.tsx index e40a283f400..be461cc783f 100644 --- a/packages/wallet/src/features/transactions/swap/modals/settings/SwapSettingsMessage.tsx +++ b/packages/wallet/src/features/transactions/swap/modals/settings/SwapSettingsMessage.tsx @@ -51,7 +51,7 @@ export function SwapSettingsMessage({ {getSymbolDisplayText( trade.tradeType === TradeType.EXACT_INPUT ? trade.outputAmount.currency.symbol - : trade.inputAmount.currency.symbol + : trade.inputAmount.currency.symbol, )} {showSlippageWarning ? ( diff --git a/packages/wallet/src/features/transactions/swap/modals/settings/SwapSettingsModal.tsx b/packages/wallet/src/features/transactions/swap/modals/settings/SwapSettingsModal.tsx index 3dd00388628..ed3750a6a33 100644 --- a/packages/wallet/src/features/transactions/swap/modals/settings/SwapSettingsModal.tsx +++ b/packages/wallet/src/features/transactions/swap/modals/settings/SwapSettingsModal.tsx @@ -1,5 +1,6 @@ import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' +import { useDispatch } from 'react-redux' import { Button, Flex, Separator, Text, TouchableArea, isWeb, useSporeColors } from 'ui/src' import { InfoCircleFilled, RotatableChevron } from 'ui/src/components/icons' import { iconSizes } from 'ui/src/theme' @@ -8,6 +9,7 @@ import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { ModalName } from 'uniswap/src/features/telemetry/constants' +import { TradeProtocolPreference } from 'uniswap/src/features/transactions/transactionState/types' import { WalletChainId } from 'uniswap/src/types/chains' import { Switch, WebSwitch } from 'wallet/src/components/buttons/Switch' import { isPrivateRpcSupportedOnChain } from 'wallet/src/features/providers' @@ -19,10 +21,8 @@ import { import { SlippageSettingsRow } from 'wallet/src/features/transactions/swap/modals/settings/SlippageSettingsRow' import { SlippageSettingsScreen } from 'wallet/src/features/transactions/swap/modals/settings/SlippageSettingsScreen' import { DerivedSwapInfo } from 'wallet/src/features/transactions/swap/types' -import { TradeProtocolPreference } from 'wallet/src/features/transactions/transactionState/types' import { useSwapProtectionSetting } from 'wallet/src/features/wallet/hooks' import { SwapProtectionSetting, setSwapProtectionSetting } from 'wallet/src/features/wallet/slice' -import { useAppDispatch } from 'wallet/src/state' enum SwapSettingsModalView { Options, @@ -52,9 +52,7 @@ export function SwapSettingsModal({ const [view, setView] = useState(SwapSettingsModalView.Options) const { customSlippageTolerance } = derivedSwapInfo - const [customSlippageInput, setCustomSlippageInput] = useState( - customSlippageTolerance - ) + const [customSlippageInput, setCustomSlippageInput] = useState(customSlippageTolerance) const getTitle = (): string => { switch (view) { @@ -91,10 +89,7 @@ export function SwapSettingsModal({ ) case SwapSettingsModalView.Slippage: return ( - + ) case SwapSettingsModalView.RoutePreference: return ( @@ -121,11 +116,9 @@ export function SwapSettingsModal({ backgroundColor={colors.surface1.get()} isModalOpen={isOpen} name={ModalName.SwapSettings} - onClose={onSettingsClose}> - + onClose={onSettingsClose} + > + setView(SwapSettingsModalView.Options)}> {innerContent} - @@ -190,9 +178,7 @@ function SwapSettingsOptions({ {t('swap.settings.routingPreference.title')} - setView(SwapSettingsModalView.RoutePreference)}> + setView(SwapSettingsModalView.RoutePreference)}> {tradeProtocolPreferenceTitle} @@ -209,7 +195,7 @@ function SwapSettingsOptions({ function SwapProtectionSettingsRow({ chainId }: { chainId: WalletChainId }): JSX.Element { const { t } = useTranslation() - const dispatch = useAppDispatch() + const dispatch = useDispatch() const swapProtectionSetting = useSwapProtectionSetting() const toggleSwapProtectionSetting = useCallback(() => { diff --git a/packages/wallet/src/features/transactions/swap/modals/settings/useSlippageSettings.ts b/packages/wallet/src/features/transactions/swap/modals/settings/useSlippageSettings.ts index e228985bf3d..cbf4e453b08 100644 --- a/packages/wallet/src/features/transactions/swap/modals/settings/useSlippageSettings.ts +++ b/packages/wallet/src/features/transactions/swap/modals/settings/useSlippageSettings.ts @@ -4,10 +4,7 @@ import { StyleProp, ViewStyle } from 'react-native' import { useAnimatedStyle, useSharedValue } from 'react-native-reanimated' import { HapticFeedback } from 'ui/src' import { PlusMinusButtonType } from 'wallet/src/components/buttons/PlusMinusButton' -import { - MAX_AUTO_SLIPPAGE_TOLERANCE, - MAX_CUSTOM_SLIPPAGE_TOLERANCE, -} from 'wallet/src/constants/transactions' +import { MAX_AUTO_SLIPPAGE_TOLERANCE, MAX_CUSTOM_SLIPPAGE_TOLERANCE } from 'wallet/src/constants/transactions' import { Trade } from 'wallet/src/features/transactions/swap/trade/types' import { DerivedSwapInfo } from 'wallet/src/features/transactions/swap/types' import { errorShakeAnimation } from 'wallet/src/utils/animations' @@ -48,7 +45,7 @@ export function useSlippageSettings({ const [isEditingSlippage, setIsEditingSlippage] = useState(false) const [autoSlippageEnabled, setAutoSlippageEnabled] = useState(!customSlippageTolerance) const [inputSlippageTolerance, setInputSlippageTolerance] = useState( - customSlippageTolerance?.toFixed(2)?.toString() ?? '' + customSlippageTolerance?.toFixed(2)?.toString() ?? '', ) const [inputWarning, setInputWarning] = useState() @@ -71,7 +68,7 @@ export function useSlippageSettings({ () => ({ transform: [{ translateX: inputShakeX.value }], }), - [inputShakeX] + [inputShakeX], ) const onPressAutoSlippage = (): void => { @@ -113,7 +110,7 @@ export function useSlippageSettings({ setInputWarning( t('swap.settings.slippage.warning.max', { maxSlippageTolerance: MAX_CUSTOM_SLIPPAGE_TOLERANCE, - }) + }), ) setInputSlippageTolerance('') } @@ -131,7 +128,7 @@ export function useSlippageSettings({ setInputSlippageTolerance(value) onSlippageChange(parsedValue) }, - [inputShakeX, onSlippageChange, t] + [inputShakeX, onSlippageChange, t], ) const onFocusSlippageInput = useCallback((): void => { @@ -164,8 +161,7 @@ export function useSlippageSettings({ } const newSlippage = - currentSlippageToleranceNum + - (type === PlusMinusButtonType.Plus ? SLIPPAGE_INCREMENT : -SLIPPAGE_INCREMENT) + currentSlippageToleranceNum + (type === PlusMinusButtonType.Plus ? SLIPPAGE_INCREMENT : -SLIPPAGE_INCREMENT) const constrainedNewSlippage = type === PlusMinusButtonType.Plus ? Math.min(newSlippage, MAX_CUSTOM_SLIPPAGE_TOLERANCE) @@ -180,7 +176,7 @@ export function useSlippageSettings({ setInputSlippageTolerance(constrainedNewSlippage.toFixed(2).toString()) onSlippageChange(constrainedNewSlippage) }, - [autoSlippageEnabled, currentSlippageToleranceNum, onSlippageChange, t] + [autoSlippageEnabled, currentSlippageToleranceNum, onSlippageChange, t], ) return { diff --git a/packages/wallet/src/features/transactions/swap/submitOrderSaga.ts b/packages/wallet/src/features/transactions/swap/submitOrderSaga.ts new file mode 100644 index 00000000000..cf35e817345 --- /dev/null +++ b/packages/wallet/src/features/transactions/swap/submitOrderSaga.ts @@ -0,0 +1,112 @@ +import axios from 'axios' +import { call, put, take } from 'typed-redux-saga' +import { uniswapUrls } from 'uniswap/src/constants/urls' +import { WalletEventName } from 'uniswap/src/features/telemetry/constants' +import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' +import { WalletChainId } from 'uniswap/src/types/chains' +import { logger } from 'utilities/src/logger/logger' +import { ONE_SECOND_MS } from 'utilities/src/time/time' +import { OrderRequest, Routing } from 'wallet/src/data/tradingApi/__generated__/index' +import { finalizeTransaction, transactionActions } from 'wallet/src/features/transactions/slice' +import { getBaseTradeAnalyticsProperties } from 'wallet/src/features/transactions/swap/analytics' +import { TRADING_API_HEADERS } from 'wallet/src/features/transactions/swap/trade/api/client' +import { + QueuedOrderStatus, + TransactionStatus, + TransactionTypeInfo, + UniswapXOrderDetails, +} from 'wallet/src/features/transactions/types' +import { createTransactionId } from 'wallet/src/features/transactions/utils' +import { Account } from 'wallet/src/features/wallet/accounts/types' + +// If the app is closed during the waiting period and then reopened, the saga will resume; +// the order should not be submitted if too much time has passed as it may be stale. +const ORDER_STALENESS_THRESHOLD = 45 * ONE_SECOND_MS + +interface SubmitUniswapXOrderParams { + // internal id used for tracking transactions before they're submitted + txId?: string + chainId: WalletChainId + orderParams: OrderRequest + account: Account + typeInfo: TransactionTypeInfo + analytics: ReturnType + approveTxHash?: string + wrapTxHash?: string + onSubmit: () => void + onFailure: () => void +} + +const ORDER_ENDPOINT = uniswapUrls.tradingApiUrl + uniswapUrls.tradingApiPaths.order + +export function* submitUniswapXOrder(params: SubmitUniswapXOrderParams) { + const { orderParams, approveTxHash, wrapTxHash, txId, chainId, typeInfo, account, analytics, onSubmit, onFailure } = + params + + // Wait for approval and/or wrap transactions to confirm, otherwise order submission will fail. + let waitingForApproval = Boolean(approveTxHash) + let waitingForWrap = Boolean(wrapTxHash) + + const orderHash = orderParams.quote.orderId + const order = { + routing: Routing.DUTCH_V2, + orderHash, + id: txId ?? createTransactionId(), + chainId, + typeInfo, + from: account.address, + addedTime: Date.now(), + status: TransactionStatus.Pending, + queueStatus: QueuedOrderStatus.Waiting, + wrapTxHash, + } satisfies UniswapXOrderDetails + + yield* put(transactionActions.addTransaction(order)) + logger.debug('submitOrder', 'addOrder', 'order added:', { chainId, orderHash, ...typeInfo }) + + const waitStartTime = Date.now() + + // Wait for approval and/or wrap + while (waitingForApproval || waitingForWrap) { + const { payload } = yield* take>(finalizeTransaction.type) + + if (Date.now() - waitStartTime > ORDER_STALENESS_THRESHOLD) { + yield* put(transactionActions.updateTransaction({ ...order, queueStatus: QueuedOrderStatus.Stale })) + return + } + + if (payload.hash === approveTxHash) { + if (payload.status !== TransactionStatus.Success) { + yield* put(transactionActions.updateTransaction({ ...order, queueStatus: QueuedOrderStatus.ApprovalFailed })) + onFailure() + return + } + waitingForApproval = false + } else if (payload.hash === wrapTxHash) { + if (payload.status !== TransactionStatus.Success) { + yield* put(transactionActions.updateTransaction({ ...order, queueStatus: QueuedOrderStatus.WrapFailed })) + onFailure() + return + } + waitingForWrap = false + } + } + + // Submit transaction + try { + const addedTime = Date.now() // refresh the addedTime to match the actual submission time + yield* put(transactionActions.updateTransaction({ ...order, queueStatus: QueuedOrderStatus.Submitted, addedTime })) + yield* call(axios.post, ORDER_ENDPOINT, orderParams, { headers: TRADING_API_HEADERS }) + } catch { + // In the rare event that submission fails, we update the order status to prompt the user. + // If the app is closed before this catch block is reached, orderWatcherSaga will handle the failure upon reopening. + yield* put(transactionActions.updateTransaction({ ...order, queueStatus: QueuedOrderStatus.SubmissionFailed })) + onFailure() + return + } + + const properties = { routing: order.routing, order_hash: orderHash, ...analytics } + yield* call(sendAnalyticsEvent, WalletEventName.SwapSubmitted, properties) + + onSubmit() +} diff --git a/packages/wallet/src/features/transactions/swap/swapSaga.test.ts b/packages/wallet/src/features/transactions/swap/swapSaga.test.ts index 71d9a363d3a..e28b93e39af 100644 --- a/packages/wallet/src/features/transactions/swap/swapSaga.test.ts +++ b/packages/wallet/src/features/transactions/swap/swapSaga.test.ts @@ -5,23 +5,21 @@ import { TradeType } from '@uniswap/sdk-core' import { UNIVERSAL_ROUTER_ADDRESS } from '@uniswap/universal-router-sdk' import { expectSaga } from 'redux-saga-test-plan' import { EffectProviders, StaticProvider } from 'redux-saga-test-plan/providers' +import { DAI } from 'uniswap/src/constants/tokens' +import { NativeCurrency } from 'uniswap/src/features/tokens/NativeCurrency' import { UniverseChainId } from 'uniswap/src/types/chains' -import { DAI } from 'wallet/src/constants/tokens' -import { NativeCurrency } from 'wallet/src/features/tokens/NativeCurrency' +import { currencyId } from 'uniswap/src/utils/currencyId' +import { Routing } from 'wallet/src/data/tradingApi/__generated__/index' import { sendTransaction } from 'wallet/src/features/transactions/sendTransactionSaga' import { getBaseTradeAnalyticsProperties } from 'wallet/src/features/transactions/swap/analytics' import { SwapParams, approveAndSwap } from 'wallet/src/features/transactions/swap/swapSaga' -import { Trade } from 'wallet/src/features/transactions/swap/trade/types' -import { - ExactInputSwapTransactionInfo, - TransactionType, -} from 'wallet/src/features/transactions/types' +import { ClassicTrade } from 'wallet/src/features/transactions/swap/trade/types' +import { ExactInputSwapTransactionInfo, TransactionType } from 'wallet/src/features/transactions/types' import { getProvider } from 'wallet/src/features/wallet/context' import { selectWalletSwapProtectionSetting } from 'wallet/src/features/wallet/selectors' import { SwapProtectionSetting } from 'wallet/src/features/wallet/slice' import { signerMnemonicAccount } from 'wallet/src/test/fixtures' import { getTxProvidersMocks } from 'wallet/src/test/mocks' -import { currencyId } from 'wallet/src/utils/currencyId' const account = signerMnemonicAccount() @@ -44,7 +42,7 @@ const transactionTypeInfo: ExactInputSwapTransactionInfo = { const mockTrade = { inputAmount: { currency: new NativeCurrency(CHAIN_ID) }, quote: { amount: MaxUint256 }, -} as unknown as Trade +} as unknown as ClassicTrade const mockApproveTxRequest = { chainId: 1, @@ -62,22 +60,29 @@ const swapParams: SwapParams = { txId: '1', account, analytics: {} as ReturnType, - approveTxRequest: mockApproveTxRequest, - swapTxRequest: mockSwapTxRequest, - swapTypeInfo: transactionTypeInfo, + swapTxContext: { + routing: Routing.CLASSIC, + approveTxRequest: mockApproveTxRequest, + txRequest: mockSwapTxRequest, + trade: mockTrade, + gasFee: { value: '5', loading: false, error: undefined }, + approvalError: false, + }, + onSubmit: jest.fn(), + onFailure: jest.fn(), } const swapParamsWithoutApprove: SwapParams = { - txId: '1', - account, - analytics: {} as ReturnType, - approveTxRequest: undefined, - swapTxRequest: mockSwapTxRequest, - swapTypeInfo: transactionTypeInfo, + ...swapParams, + swapTxContext: { + ...swapParams.swapTxContext, + approveTxRequest: undefined, + }, } const nonce = 1 +// TODO(WEB-4294): The saga runs in these tests are not actually finishing as `getNonceForApproveAndSwap` fails; update to work correctly describe(approveAndSwap, () => { const sharedProviders: (EffectProviders | StaticProvider)[] = [ [select(selectWalletSwapProtectionSetting), SwapProtectionSetting.Off], diff --git a/packages/wallet/src/features/transactions/swap/swapSaga.ts b/packages/wallet/src/features/transactions/swap/swapSaga.ts index e665278e94e..2a844bc3be8 100644 --- a/packages/wallet/src/features/transactions/swap/swapSaga.ts +++ b/packages/wallet/src/features/transactions/swap/swapSaga.ts @@ -1,24 +1,21 @@ -import { providers } from 'ethers' +import { permit2Address } from '@uniswap/permit2-sdk' import { call, select } from 'typed-redux-saga' import { FeatureFlags, getFeatureFlagName } from 'uniswap/src/features/gating/flags' import { Statsig } from 'uniswap/src/features/gating/sdk/statsig' import { RPCType } from 'uniswap/src/types/chains' import { logger } from 'utilities/src/logger/logger' +import { Routing } from 'wallet/src/data/tradingApi/__generated__/index' import { isPrivateRpcSupportedOnChain } from 'wallet/src/features/providers' +import { ValidatedSwapTxContext } from 'wallet/src/features/transactions/contexts/SwapTxContext' import { makeSelectAddressTransactions } from 'wallet/src/features/transactions/selectors' -import { - SendTransactionParams, - sendTransaction, -} from 'wallet/src/features/transactions/sendTransactionSaga' +import { sendTransaction } from 'wallet/src/features/transactions/sendTransactionSaga' import { getBaseTradeAnalyticsProperties } from 'wallet/src/features/transactions/swap/analytics' +import { submitUniswapXOrder } from 'wallet/src/features/transactions/swap/submitOrderSaga' import { isClassic } from 'wallet/src/features/transactions/swap/trade/utils' import { tradeToTransactionInfo } from 'wallet/src/features/transactions/swap/utils' -import { - TransactionStatus, - TransactionType, - TransactionTypeInfo, -} from 'wallet/src/features/transactions/types' -import { Account } from 'wallet/src/features/wallet/accounts/types' +import { wrap } from 'wallet/src/features/transactions/swap/wrapSaga' +import { ApproveTransactionInfo, TransactionStatus, TransactionType } from 'wallet/src/features/transactions/types' +import { SignerMnemonicAccount } from 'wallet/src/features/wallet/accounts/types' import { getProvider } from 'wallet/src/features/wallet/context' import { selectWalletSwapProtectionSetting } from 'wallet/src/features/wallet/selectors' import { SwapProtectionSetting } from 'wallet/src/features/wallet/slice' @@ -26,61 +23,82 @@ import { createMonitoredSaga } from 'wallet/src/utils/saga' export type SwapParams = { txId?: string - account: Account + account: SignerMnemonicAccount analytics: ReturnType - approveTxRequest?: providers.TransactionRequest - swapTxRequest: providers.TransactionRequest - swapTypeInfo: ReturnType + swapTxContext: ValidatedSwapTxContext + onSubmit: () => void + onFailure: () => void } export function* approveAndSwap(params: SwapParams) { try { - const { account, approveTxRequest, swapTxRequest, txId, analytics, swapTypeInfo } = params - if (!swapTxRequest.chainId || !swapTxRequest.to || (approveTxRequest && !approveTxRequest.to)) { - throw new Error('approveAndSwap received incomplete transaction request details') + const { swapTxContext, account, txId, analytics, onSubmit, onFailure } = params + const { trade, routing, approveTxRequest } = swapTxContext + const isUniswapX = routing === Routing.DUTCH_V2 + const { address } = account + + const chainId = swapTxContext.trade.inputAmount.currency.chainId + + // MEV protection is not needed for UniswapX approval and/or wrap transactions. + const submitViaPrivateRpc = !isUniswapX && (yield* call(shouldSubmitViaPrivateRpc, chainId)) + let nonce = yield* call(getNonceForApproveAndSwap, address, chainId, submitViaPrivateRpc) + + // For classic swaps, trigger UI changes immediately after click + if (!isUniswapX) { + onSubmit() } - const { chainId } = swapTxRequest - const submitViaPrivateRpc = yield* call(shouldSubmitViaPrivateRpc, chainId) - const nonce = yield* call( - getNonceForApproveAndSwap, - account.address, - chainId, - submitViaPrivateRpc - ) - - if (approveTxRequest && approveTxRequest.to) { - const typeInfo: TransactionTypeInfo = { + + let approveTxHash: string | undefined + // Approval Logic + if (approveTxRequest) { + const typeInfo: ApproveTransactionInfo = { type: TransactionType.Approve, tokenAddress: approveTxRequest.to, - spender: swapTxRequest.to, + spender: permit2Address(chainId), + } + + const options = { request: approveTxRequest, submitViaPrivateRpc } + const sendTransactionParams = { chainId, account, options, typeInfo, analytics } + // TODO(WEB-4406) - Refactor the approval submission's rpc call latency to not delay wrap submission + approveTxHash = (yield* call(sendTransaction, sendTransactionParams)).transactionResponse.hash + nonce++ + } + + const typeInfo = tradeToTransactionInfo(swapTxContext.trade) + // Swap Logic - UniswapX + if (isUniswapX) { + const { orderParams, wrapTxRequest } = swapTxContext + + let wrapTxHash: string | undefined + // Wrap Logic - UniswapX Eth-input + if (wrapTxRequest) { + const inputCurrencyAmount = trade.inputAmount + const wrapResponse = yield* wrap({ txRequest: { ...wrapTxRequest, nonce }, account, inputCurrencyAmount }) + wrapTxHash = wrapResponse?.transactionResponse.hash } - const sendTransactionParams: SendTransactionParams = { + const submitOrderParams = { + txId, chainId, + orderParams, + approveTxHash, + wrapTxHash, account, - options: { request: approveTxRequest, submitViaPrivateRpc }, typeInfo, analytics, + onSubmit, + onFailure, } - - yield* call(sendTransaction, sendTransactionParams) - } - - const request = { - ...swapTxRequest, - nonce: approveTxRequest ? nonce + 1 : undefined, + yield* call(submitUniswapXOrder, submitOrderParams) } - - const sendTransactionParams: SendTransactionParams = { - txId, - chainId, - account, - options: { request, submitViaPrivateRpc }, - typeInfo: swapTypeInfo, - analytics, + // Swap Logic - Classic + else { + const { txRequest: swapTxRequest } = swapTxContext + const request = { ...swapTxRequest, nonce } + const options = { request, submitViaPrivateRpc } + const sendTransactionParams = { txId, chainId, account, options, typeInfo, analytics } + yield* call(sendTransaction, sendTransactionParams) } - - yield* call(sendTransaction, sendTransactionParams) } catch (error) { logger.error(error, { tags: { file: 'swapSaga', function: 'approveAndSwap' }, @@ -96,11 +114,7 @@ export const { actions: swapActions, } = createMonitoredSaga(approveAndSwap, 'swap') -function* getNonceForApproveAndSwap( - address: Address, - chainId: number, - submitViaPrivateRpc: boolean -) { +function* getNonceForApproveAndSwap(address: Address, chainId: number, submitViaPrivateRpc: boolean) { const rpcType = submitViaPrivateRpc ? RPCType.Private : RPCType.Public const provider = yield* call(getProvider, chainId, rpcType) const nonce = yield* call([provider, provider.getTransactionCount], address, 'pending') @@ -136,6 +150,6 @@ function* getPendingPrivateTxCount(address: Address, chainId: number) { tx.chainId === chainId && tx.status === TransactionStatus.Pending && isClassic(tx) && - Boolean(tx.options.submitViaPrivateRpc) + Boolean(tx.options.submitViaPrivateRpc), ).length } diff --git a/packages/wallet/src/features/transactions/swap/trade/tradingApi/client.ts b/packages/wallet/src/features/transactions/swap/trade/api/client.ts similarity index 79% rename from packages/wallet/src/features/transactions/swap/trade/tradingApi/client.ts rename to packages/wallet/src/features/transactions/swap/trade/api/client.ts index c1ed6d35b3b..bf28696298f 100644 --- a/packages/wallet/src/features/transactions/swap/trade/tradingApi/client.ts +++ b/packages/wallet/src/features/transactions/swap/trade/api/client.ts @@ -5,14 +5,16 @@ import { uniswapUrls } from 'uniswap/src/constants/urls' import { createNewInMemoryCache } from 'uniswap/src/data/cache' import { REQUEST_SOURCE } from 'uniswap/src/data/constants' +export const TRADING_API_HEADERS = { + 'Content-Type': 'application/json', + 'X-API-KEY': config.tradingApiKey, + 'x-request-source': REQUEST_SOURCE, + Origin: uniswapUrls.requestOriginUrl, +} as const + const restLink = new RestLink({ uri: uniswapUrls.tradingApiUrl, - headers: { - 'Content-Type': 'application/json', - 'X-API-KEY': config.tradingApiKey, - 'x-request-source': REQUEST_SOURCE, - Origin: uniswapUrls.requestOriginUrl, - }, + headers: TRADING_API_HEADERS, }) export const TradingApiApolloClient = new ApolloClient({ diff --git a/packages/wallet/src/features/transactions/swap/trade/api/hooks/useSwapTxAndGasInfo.ts b/packages/wallet/src/features/transactions/swap/trade/api/hooks/useSwapTxAndGasInfo.ts new file mode 100644 index 00000000000..d968658cf24 --- /dev/null +++ b/packages/wallet/src/features/transactions/swap/trade/api/hooks/useSwapTxAndGasInfo.ts @@ -0,0 +1,126 @@ +import { providers } from 'ethers' +import { useMemo } from 'react' +import { CurrencyField } from 'uniswap/src/features/transactions/transactionState/types' +import { OrderRequest, Routing } from 'wallet/src/data/tradingApi/__generated__/index' +import { GasFeeResult } from 'wallet/src/features/gas/types' +import { useTokenApprovalInfo } from 'wallet/src/features/transactions/swap/trade/api/hooks/useTokenApprovalInfo' +import { useTransactionRequestInfo } from 'wallet/src/features/transactions/swap/trade/api/hooks/useTransactionRequestInfo' +import { ApprovalAction, ClassicTrade, UniswapXTrade } from 'wallet/src/features/transactions/swap/trade/types' +import { isUniswapX } from 'wallet/src/features/transactions/swap/trade/utils' +import { DerivedSwapInfo } from 'wallet/src/features/transactions/swap/types' +import { sumGasFees } from 'wallet/src/features/transactions/swap/utils' + +export type SwapTxAndGasInfo = ClassicSwapTxAndGasInfo | UniswapXSwapTxAndGasInfo + +export type ClassicSwapTxAndGasInfo = { + routing: Routing.CLASSIC + trade?: ClassicTrade + txRequest?: ValidatedTransactionRequest + approveTxRequest: ValidatedTransactionRequest | undefined + gasFee: GasFeeResult + approvalError: boolean +} + +export type UniswapXSwapTxAndGasInfo = { + routing: Routing.DUTCH_V2 + trade: UniswapXTrade + wrapTxRequest: ValidatedTransactionRequest | undefined + approveTxRequest: ValidatedTransactionRequest | undefined + orderParams?: OrderRequest + gasFee: GasFeeResult + approvalError: boolean +} + +type ValidatedTransactionRequest = providers.TransactionRequest & { to: string; chainId: number } +function validateTransactionRequest( + request?: providers.TransactionRequest | null, +): ValidatedTransactionRequest | undefined { + if (request?.to && request.chainId) { + return { ...request, to: request.to, chainId: request.chainId } + } + return undefined +} + +export function useSwapTxAndGasInfo({ derivedSwapInfo }: { derivedSwapInfo: DerivedSwapInfo }): SwapTxAndGasInfo { + const { + chainId, + wrapType, + currencyAmounts, + trade: { trade }, + } = derivedSwapInfo + + const tokenApprovalInfo = useTokenApprovalInfo({ + chainId, + wrapType, + currencyInAmount: currencyAmounts[CurrencyField.INPUT], + routing: trade?.routing, + }) + + // TODO(MOB-3425) decouple wrap tx from swap tx to simplify UniswapX code + const swapTxInfo = useTransactionRequestInfo({ + derivedSwapInfo, + // Dont send transaction request if invalid or missing approval data + skip: !tokenApprovalInfo?.action || tokenApprovalInfo.action === ApprovalAction.Unknown, + tokenApprovalInfo, + }) + + const approvalError = tokenApprovalInfo?.action === ApprovalAction.Unknown + const gasFeeError = swapTxInfo.gasFeeResult.error || approvalError + + // For UniswapX, do not expect a swap gas fee unless a wrap is involved + const isWraplessUniswapXTrade = trade && isUniswapX(trade) && !trade.needsWrap + const areValuesReady = tokenApprovalInfo && (isWraplessUniswapXTrade || swapTxInfo.gasFeeResult.value !== undefined) + + return useMemo(() => { + // Do not populate gas fee: + // - If errors exist on swap or approval requests. + // - If we don't have both the approval and transaction gas fees. + const totalGasFee = + gasFeeError || !areValuesReady ? undefined : sumGasFees(tokenApprovalInfo?.gasFee, swapTxInfo?.gasFeeResult.value) + + const isGasless = isWraplessUniswapXTrade && tokenApprovalInfo?.action === ApprovalAction.None + + const gasFee = { + value: isGasless ? '0' : totalGasFee, + loading: !tokenApprovalInfo || swapTxInfo.gasFeeResult.loading, + error: gasFeeError, + } + + const approveTxRequest = validateTransactionRequest(tokenApprovalInfo?.txRequest) + + if (trade?.routing === Routing.DUTCH_V2) { + const signature = swapTxInfo.permitSignature + const orderParams = signature ? { signature, quote: trade.quote.quote, routing: Routing.DUTCH_V2 } : undefined + + return { + routing: Routing.DUTCH_V2, + trade, + wrapTxRequest: validateTransactionRequest(swapTxInfo.transactionRequest), + approveTxRequest, + orderParams, + gasFee, + approvalError, + } + } else { + return { + routing: Routing.CLASSIC, + trade: trade ?? undefined, + txRequest: validateTransactionRequest(swapTxInfo.transactionRequest), + approveTxRequest, + gasFee, + approvalError, + } + } + }, [ + isWraplessUniswapXTrade, + gasFeeError, + areValuesReady, + tokenApprovalInfo, + swapTxInfo.gasFeeResult.value, + swapTxInfo.gasFeeResult.loading, + swapTxInfo.permitSignature, + swapTxInfo.transactionRequest, + trade, + approvalError, + ]) +} diff --git a/packages/wallet/src/features/transactions/swap/trade/tradingApi/hooks/useTokenApprovalInfo.ts b/packages/wallet/src/features/transactions/swap/trade/api/hooks/useTokenApprovalInfo.ts similarity index 80% rename from packages/wallet/src/features/transactions/swap/trade/tradingApi/hooks/useTokenApprovalInfo.ts rename to packages/wallet/src/features/transactions/swap/trade/api/hooks/useTokenApprovalInfo.ts index 8bd5cf1ae8c..dd9f10a482e 100644 --- a/packages/wallet/src/features/transactions/swap/trade/tradingApi/hooks/useTokenApprovalInfo.ts +++ b/packages/wallet/src/features/transactions/swap/trade/api/hooks/useTokenApprovalInfo.ts @@ -5,16 +5,13 @@ import { useRestQuery } from 'uniswap/src/data/rest' import { WalletChainId } from 'uniswap/src/types/chains' import { logger } from 'utilities/src/logger/logger' import { ONE_MINUTE_MS } from 'utilities/src/time/time' -import { ApprovalRequest, ApprovalResponse } from 'wallet/src/data/tradingApi/__generated__/index' -import { TradingApiApolloClient } from 'wallet/src/features/transactions/swap/trade/tradingApi/client' +import { ApprovalRequest, ApprovalResponse, Routing } from 'wallet/src/data/tradingApi/__generated__/index' +import { TradingApiApolloClient } from 'wallet/src/features/transactions/swap/trade/api/client' import { getTokenAddressForApi, toTradingApiSupportedChainId, -} from 'wallet/src/features/transactions/swap/trade/tradingApi/utils' -import { - ApprovalAction, - TokenApprovalInfo, -} from 'wallet/src/features/transactions/swap/trade/types' +} from 'wallet/src/features/transactions/swap/trade/api/utils' +import { ApprovalAction, TokenApprovalInfo } from 'wallet/src/features/transactions/swap/trade/types' import { WrapType } from 'wallet/src/features/transactions/types' import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' @@ -22,18 +19,20 @@ interface TokenApprovalInfoParams { chainId: WalletChainId wrapType: WrapType currencyInAmount: Maybe> + routing: Routing | undefined skip?: boolean } export function useTokenApprovalInfo( - params: TokenApprovalInfoParams + params: TokenApprovalInfoParams, ): (TokenApprovalInfo & { gasFee?: string }) | undefined { - const { chainId, wrapType, currencyInAmount, skip } = params + const { chainId, wrapType, currencyInAmount, routing, skip } = params const isWrap = wrapType !== WrapType.NotApplicable const address = useActiveAccountAddressWithThrow() - const currencyIn = currencyInAmount?.currency + // Off-chain orders must have wrapped currencies approved, rather than natives. + const currencyIn = routing === Routing.DUTCH_V2 ? currencyInAmount?.currency.wrapped : currencyInAmount?.currency const amount = currencyInAmount?.quotient.toString() const tokenAddress = getTokenAddressForApi(currencyIn) @@ -62,7 +61,7 @@ export function useTokenApprovalInfo( skip: skip || !approvalRequestArgs || isWrap, }, 'POST', - TradingApiApolloClient + TradingApiApolloClient, ) return useMemo(() => { diff --git a/packages/wallet/src/features/transactions/swap/trade/tradingApi/hooks/useTradingApiTrade.ts b/packages/wallet/src/features/transactions/swap/trade/api/hooks/useTrade.ts similarity index 75% rename from packages/wallet/src/features/transactions/swap/trade/tradingApi/hooks/useTradingApiTrade.ts rename to packages/wallet/src/features/transactions/swap/trade/api/hooks/useTrade.ts index 3d8bde8f72c..4944bccd136 100644 --- a/packages/wallet/src/features/transactions/swap/trade/tradingApi/hooks/useTradingApiTrade.ts +++ b/packages/wallet/src/features/transactions/swap/trade/api/hooks/useTrade.ts @@ -3,35 +3,32 @@ import { TradeType } from '@uniswap/sdk-core' import { useMemo } from 'react' import { uniswapUrls } from 'uniswap/src/constants/urls' import { useRestQuery } from 'uniswap/src/data/rest' +import { isMainnetChainId } from 'uniswap/src/features/chains/utils' +import { DynamicConfigs, PollingIntervalsConfigKey } from 'uniswap/src/features/gating/configs' import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' +import { useDynamicConfigValue, useFeatureFlag } from 'uniswap/src/features/gating/hooks' +import { CurrencyField } from 'uniswap/src/features/transactions/transactionState/types' import { WalletChainId } from 'uniswap/src/types/chains' +import { areCurrencyIdsEqual, currencyId } from 'uniswap/src/utils/currencyId' import { logger } from 'utilities/src/logger/logger' import { ONE_SECOND_MS, inXMinutesUnix } from 'utilities/src/time/time' import { useDebounceWithStatus } from 'utilities/src/time/timing' -import { PollingInterval } from 'wallet/src/constants/misc' -import { - QuoteRequest, - TradeType as TradingApiTradeType, -} from 'wallet/src/data/tradingApi/__generated__/index' -import { isL2Chain } from 'wallet/src/features/chains/utils' +import { QuoteRequest, TradeType as TradingApiTradeType } from 'wallet/src/data/tradingApi/__generated__/index' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' -import { TradingApiApolloClient } from 'wallet/src/features/transactions/swap/trade/tradingApi/client' +import { TradingApiApolloClient } from 'wallet/src/features/transactions/swap/trade/api/client' import { getRoutingPreferenceForSwapRequest, getTokenAddressForApi, toTradingApiSupportedChainId, transformTradingApiResponseToTrade, validateTrade, -} from 'wallet/src/features/transactions/swap/trade/tradingApi/utils' +} from 'wallet/src/features/transactions/swap/trade/api/utils' import { DiscriminatedQuoteResponse, TradeWithStatus, UseTradeArgs, } from 'wallet/src/features/transactions/swap/trade/types' -import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' -import { areCurrencyIdsEqual, currencyId } from 'wallet/src/utils/currencyId' // error strings hardcoded in @uniswap/unified-routing-api // https://github.com/Uniswap/unified-routing-api/blob/020ea371a00d4cc25ce9f9906479b00a43c65f2c/lib/util/errors.ts#L4 @@ -46,7 +43,7 @@ export const SWAP_FORM_DEBOUNCE_TIME_MS = 250 export const API_RATE_LIMIT_ERROR = 'TOO_MANY_REQUESTS' -export function useTradingApiTrade(args: UseTradeArgs): TradeWithStatus { +export function useTrade(args: UseTradeArgs): TradeWithStatus { const { amountSpecified, otherCurrency, @@ -65,36 +62,24 @@ export function useTradingApiTrade(args: UseTradeArgs): TradeWithStatus { /***** Format request arguments ******/ - const [debouncedAmountSpecified, isDebouncing] = useDebounceWithStatus( - amountSpecified, - SWAP_FORM_DEBOUNCE_TIME_MS - ) - const shouldDebounce = - amountSpecified && debouncedAmountSpecified?.currency.chainId === otherCurrency?.chainId + const [debouncedAmountSpecified, isDebouncing] = useDebounceWithStatus(amountSpecified, SWAP_FORM_DEBOUNCE_TIME_MS) + const shouldDebounce = amountSpecified && debouncedAmountSpecified?.currency.chainId === otherCurrency?.chainId const amount = shouldDebounce ? debouncedAmountSpecified : amountSpecified const currencyIn = tradeType === TradeType.EXACT_INPUT ? amount?.currency : otherCurrency const currencyOut = tradeType === TradeType.EXACT_OUTPUT ? amount?.currency : otherCurrency const currencyInEqualsCurrencyOut = - currencyIn && - currencyOut && - areCurrencyIdsEqual(currencyId(currencyIn), currencyId(currencyOut)) + currencyIn && currencyOut && areCurrencyIdsEqual(currencyId(currencyIn), currencyId(currencyOut)) const tokenInChainId = toTradingApiSupportedChainId(currencyIn?.chainId) const tokenOutChainId = toTradingApiSupportedChainId(currencyOut?.chainId) const tokenInAddress = getTokenAddressForApi(currencyIn) const tokenOutAddress = getTokenAddressForApi(currencyOut) - const routingPreference = getRoutingPreferenceForSwapRequest( - tradeProtocolPreference, - uniswapXEnabled, - isUSDQuote - ) + const routingPreference = getRoutingPreferenceForSwapRequest(tradeProtocolPreference, uniswapXEnabled, isUSDQuote) const requestTradeType = - tradeType === TradeType.EXACT_INPUT - ? TradingApiTradeType.EXACT_INPUT - : TradingApiTradeType.EXACT_OUTPUT + tradeType === TradeType.EXACT_INPUT ? TradingApiTradeType.EXACT_INPUT : TradingApiTradeType.EXACT_OUTPUT const skipQuery = skip || @@ -110,7 +95,7 @@ export function useTradingApiTrade(args: UseTradeArgs): TradeWithStatus { // Temporary logging to help debug invalid requests with missing `swappper` param if (!activeAccountAddress) { logger.error(new Error('Missing account address in /swap request'), { - tags: { file: 'useTradingApiTrade', function: 'quote' }, + tags: { file: 'useTrade', function: 'quote' }, extra: { activeAccountAddress, }, @@ -149,7 +134,8 @@ export function useTradingApiTrade(args: UseTradeArgs): TradeWithStatus { /***** Fetch quote from trading API ******/ - const internalPollInterval = pollInterval ?? getPollIntervalByChain(currencyIn?.chainId) + const pollingIntervalForChain = usePollingIntervalByChain(currencyIn?.chainId) + const internalPollInterval = pollInterval ?? pollingIntervalForChain const response = useRestQuery>( uniswapUrls.tradingApiPaths.quote, @@ -166,7 +152,7 @@ export function useTradingApiTrade(args: UseTradeArgs): TradeWithStatus { notifyOnNetworkStatusChange: true, }, 'POST', - TradingApiApolloClient + TradingApiApolloClient, ) const { error, data, loading, networkStatus } = response @@ -176,11 +162,11 @@ export function useTradingApiTrade(args: UseTradeArgs): TradeWithStatus { return useMemo(() => { // Error logging if (error && !isUSDQuote) { - logger.error(error, { tags: { file: 'useTradingApiTrade', function: 'quote' } }) + logger.error(error, { tags: { file: 'useTrade', function: 'quote' } }) } if (data && !data.quote) { logger.error(new Error('Unexpected empty Trading API response'), { - tags: { file: 'useTradingApiTrade', function: 'quote' }, + tags: { file: 'useTrade', function: 'quote' }, extra: { quoteRequestArgs, }, @@ -214,8 +200,7 @@ export function useTradingApiTrade(args: UseTradeArgs): TradeWithStatus { data, }) - const exactCurrencyField = - tradeType === TradeType.EXACT_INPUT ? CurrencyField.INPUT : CurrencyField.OUTPUT + const exactCurrencyField = tradeType === TradeType.EXACT_INPUT ? CurrencyField.INPUT : CurrencyField.OUTPUT const trade = validateTrade({ trade: formattedTrade, @@ -263,8 +248,21 @@ export function useTradingApiTrade(args: UseTradeArgs): TradeWithStatus { ]) } -function getPollIntervalByChain(chainId?: WalletChainId): number { - return isL2Chain(chainId) - ? PollingInterval.AverageL2BlockTime - : PollingInterval.AverageL1BlockTime +const FALLBACK_L1_BLOCK_TIME_MS = 12000 +const FALLBACK_L2_BLOCK_TIME_MS = 3000 + +function usePollingIntervalByChain(chainId?: WalletChainId): number { + const averageL1BlockTimeMs = useDynamicConfigValue( + DynamicConfigs.PollingIntervals, + PollingIntervalsConfigKey.AverageL1BlockTimeMs, + FALLBACK_L1_BLOCK_TIME_MS, + ) + + const averageL2BlockTimeMs = useDynamicConfigValue( + DynamicConfigs.PollingIntervals, + PollingIntervalsConfigKey.AverageL2BlockTimeMs, + FALLBACK_L2_BLOCK_TIME_MS, + ) + + return isMainnetChainId(chainId) ? averageL1BlockTimeMs : averageL2BlockTimeMs } diff --git a/packages/wallet/src/features/transactions/swap/trade/tradingApi/hooks/useTransactionRequestInfo.ts b/packages/wallet/src/features/transactions/swap/trade/api/hooks/useTransactionRequestInfo.ts similarity index 78% rename from packages/wallet/src/features/transactions/swap/trade/tradingApi/hooks/useTransactionRequestInfo.ts rename to packages/wallet/src/features/transactions/swap/trade/api/hooks/useTransactionRequestInfo.ts index 845711020db..6615e336eb7 100644 --- a/packages/wallet/src/features/transactions/swap/trade/tradingApi/hooks/useTransactionRequestInfo.ts +++ b/packages/wallet/src/features/transactions/swap/trade/api/hooks/useTransactionRequestInfo.ts @@ -3,7 +3,11 @@ import { providers } from 'ethers' import { useEffect, useMemo, useRef } from 'react' import { uniswapUrls } from 'uniswap/src/constants/urls' import { useRestQuery } from 'uniswap/src/data/rest' +import { DynamicConfigs, PollingIntervalsConfigKey } from 'uniswap/src/features/gating/configs' +import { useDynamicConfigValue } from 'uniswap/src/features/gating/hooks' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' +import { CurrencyField } from 'uniswap/src/features/transactions/transactionState/types' +import { isDetoxBuild } from 'utilities/src/environment/constants' import { logger } from 'utilities/src/logger/logger' import { ONE_SECOND_MS } from 'utilities/src/time/time' import { @@ -15,25 +19,21 @@ import { useTransactionGasFee } from 'wallet/src/features/gas/hooks' import { GasFeeResult, GasSpeed } from 'wallet/src/features/gas/types' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' import { getBaseTradeAnalyticsPropertiesFromSwapInfo } from 'wallet/src/features/transactions/swap/analytics' +import { TradingApiApolloClient } from 'wallet/src/features/transactions/swap/trade/api/client' +import { getClassicQuoteFromResponse } from 'wallet/src/features/transactions/swap/trade/api/utils' import { useWrapTransactionRequest } from 'wallet/src/features/transactions/swap/trade/hooks/useWrapTransactionRequest' -import { TradingApiApolloClient } from 'wallet/src/features/transactions/swap/trade/tradingApi/client' -import { getClassicQuoteFromResponse } from 'wallet/src/features/transactions/swap/trade/tradingApi/utils' -import { - ApprovalAction, - TokenApprovalInfo, -} from 'wallet/src/features/transactions/swap/trade/types' +import { ApprovalAction, TokenApprovalInfo } from 'wallet/src/features/transactions/swap/trade/types' +import { isUniswapX } from 'wallet/src/features/transactions/swap/trade/utils' import { DerivedSwapInfo } from 'wallet/src/features/transactions/swap/types' import { usePermit2SignatureWithData } from 'wallet/src/features/transactions/swap/usePermit2Signature' -import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' import { WrapType } from 'wallet/src/features/transactions/types' export const UNKNOWN_SIM_ERROR = 'Unknown gas simulation error' -// Poll often to ensure swap quote is never expected to fail -const SWAP_REQUEST_POLL_INTERVAL = ONE_SECOND_MS - -interface TransactionRequestInfo { +const FALLBACK_SWAP_REQUEST_POLL_INTERVAL_MS = 1000 +export interface TransactionRequestInfo { transactionRequest: providers.TransactionRequest | undefined + permitSignature: string | undefined gasFeeResult: GasFeeResult } @@ -60,7 +60,7 @@ export function useTransactionRequestInfo({ const signatureInfo = usePermit2SignatureWithData( currencyAmounts[CurrencyField.INPUT], permitData, - /**skip=*/ !requiresPermit2Sig || skip + /**skip=*/ !requiresPermit2Sig || skip, ) /** @@ -107,27 +107,33 @@ export function useTransactionRequestInfo({ ]) // Wrap transaction request - const isWrapApplicable = derivedSwapInfo.wrapType !== WrapType.NotApplicable + const isUniswapXWrap = trade && isUniswapX(trade) && trade.needsWrap + const isWrapApplicable = derivedSwapInfo.wrapType !== WrapType.NotApplicable || isUniswapXWrap const wrapTxRequest = useWrapTransactionRequest(derivedSwapInfo) const wrapGasFee = useTransactionGasFee(wrapTxRequest, GasSpeed.Urgent, !isWrapApplicable) const skipTransactionRequest = !swapRequestArgs || isWrapApplicable || skip - const { data, error, loading } = useRestQuery< - CreateSwapResponse, - CreateSwapRequest | Record - >( + // We will remove this cast in follow up change to dynamic config typing + const tradingApiSwapRequestMs = useDynamicConfigValue( + DynamicConfigs.PollingIntervals, + PollingIntervalsConfigKey.TradingApiSwapRequestMs, + FALLBACK_SWAP_REQUEST_POLL_INTERVAL_MS, + ) + + const { data, error, loading } = useRestQuery>( uniswapUrls.tradingApiPaths.swap, swapRequestArgs ?? {}, ['swap', 'gasFee', 'requestId', 'txFailureReasons'], { - pollInterval: SWAP_REQUEST_POLL_INTERVAL, + // disables react query polling during e2e testing which was preventing js thread from idle + pollInterval: isDetoxBuild ? undefined : tradingApiSwapRequestMs, clearIfStale: true, - ttlMs: SWAP_REQUEST_POLL_INTERVAL + ONE_SECOND_MS * 5, // Small buffer if connection is lost + ttlMs: tradingApiSwapRequestMs + ONE_SECOND_MS * 5, // Small buffer if connection is lost skip: skipTransactionRequest, }, 'POST', - TradingApiApolloClient + TradingApiApolloClient, ) // We use the gasFee estimate from quote, as its more accurate @@ -135,12 +141,10 @@ export function useTransactionRequestInfo({ const swapGasFee = swapQuote?.gasFee // This is a case where simulation fails on backend, meaning txn is expected to fail - const simulationError = swapQuote?.txFailureReasons?.includes( - TransactionFailureReason.SIMULATION_ERROR - ) + const simulationError = swapQuote?.txFailureReasons?.includes(TransactionFailureReason.SIMULATION_ERROR) const gasEstimateError = useMemo( () => (simulationError ? new Error(UNKNOWN_SIM_ERROR) : error), - [simulationError, error] + [simulationError, error], ) const gasFeeResult = { @@ -186,6 +190,7 @@ export function useTransactionRequestInfo({ return { transactionRequest: isWrapApplicable ? wrapTxRequest : data?.swap, + permitSignature: signatureInfo.signature, gasFeeResult, } } diff --git a/packages/wallet/src/features/transactions/swap/trade/tradingApi/utils.ts b/packages/wallet/src/features/transactions/swap/trade/api/utils.ts similarity index 81% rename from packages/wallet/src/features/transactions/swap/trade/tradingApi/utils.ts rename to packages/wallet/src/features/transactions/swap/trade/api/utils.ts index 75045536c07..420957590b5 100644 --- a/packages/wallet/src/features/transactions/swap/trade/tradingApi/utils.ts +++ b/packages/wallet/src/features/transactions/swap/trade/api/utils.ts @@ -4,11 +4,16 @@ import { UnsignedV2DutchOrderInfo } from '@uniswap/uniswapx-sdk' import { Pair, Route as V2Route } from '@uniswap/v2-sdk' import { FeeAmount, Pool, Route as V3Route } from '@uniswap/v3-sdk' import { BigNumber } from 'ethers' +import { NativeCurrency } from 'uniswap/src/features/tokens/NativeCurrency' +import { CurrencyField, TradeProtocolPreference } from 'uniswap/src/features/transactions/transactionState/types' +import { areAddressesEqual } from 'uniswap/src/utils/addresses' +import { currencyId } from 'uniswap/src/utils/currencyId' import { logger } from 'utilities/src/logger/logger' import { MAX_AUTO_SLIPPAGE_TOLERANCE } from 'wallet/src/constants/transactions' import { ClassicQuote, DutchOrderInfoV2, + OrderStatus, Quote, QuoteResponse, Routing, @@ -19,7 +24,6 @@ import { V3PoolInRoute as TradingApiV3PoolInRoute, } from 'wallet/src/data/tradingApi/__generated__/index' import { LocalizationContextState } from 'wallet/src/features/language/LocalizationContext' -import { NativeCurrency } from 'wallet/src/features/tokens/NativeCurrency' import { getBaseTradeAnalyticsProperties } from 'wallet/src/features/transactions/swap/analytics' import { ClassicTrade, @@ -28,12 +32,7 @@ import { Trade, UniswapXTrade, } from 'wallet/src/features/transactions/swap/trade/types' -import { - CurrencyField, - TradeProtocolPreference, -} from 'wallet/src/features/transactions/transactionState/types' -import { areAddressesEqual } from 'wallet/src/utils/addresses' -import { currencyId } from 'wallet/src/utils/currencyId' +import { TransactionStatus } from 'wallet/src/features/transactions/types' import { ValueType, getCurrencyAmount } from 'wallet/src/utils/getCurrencyAmount' const NATIVE_ADDRESS_FOR_TRADING_API = '0x0000000000000000000000000000000000000000' @@ -47,14 +46,12 @@ interface TradingApiResponseToTradeArgs { data: DiscriminatedQuoteResponse | undefined } -export function transformTradingApiResponseToTrade( - params: TradingApiResponseToTradeArgs -): Trade | null { +export function transformTradingApiResponseToTrade(params: TradingApiResponseToTradeArgs): Trade | null { const { currencyIn, currencyOut, tradeType, deadline, slippageTolerance, data } = params switch (data?.routing) { case Routing.CLASSIC: { - const routes = computeRoutesTradingApi(currencyIn.isNative, currencyOut.isNative, data) + const routes = computeRoutes(currencyIn.isNative, currencyOut.isNative, data) if (!routes) { return null @@ -66,8 +63,7 @@ export function transformTradingApiResponseToTrade( slippageTolerance: slippageTolerance ?? MAX_AUTO_SLIPPAGE_TOLERANCE, v2Routes: routes?.flatMap((r) => (r?.routev2 ? { ...r, routev2: r.routev2 } : [])) ?? [], v3Routes: routes?.flatMap((r) => (r?.routev3 ? { ...r, routev3: r.routev3 } : [])) ?? [], - mixedRoutes: - routes?.flatMap((r) => (r?.mixedRoute ? { ...r, mixedRoute: r.mixedRoute } : [])) ?? [], + mixedRoutes: routes?.flatMap((r) => (r?.mixedRoute ? { ...r, mixedRoute: r.mixedRoute } : [])) ?? [], tradeType, }) } @@ -105,10 +101,10 @@ export function getSwapFee(quoteResponse?: DiscriminatedQuoteResponse): SwapFee * Transforms a trading API quote into an array of routes that can be used to * create a `Trade`. */ -export function computeRoutesTradingApi( +export function computeRoutes( tokenInIsNative: boolean, tokenOutIsNative: boolean, - quoteResponse?: QuoteResponse + quoteResponse?: QuoteResponse, ): | { routev3: V3Route | null @@ -146,13 +142,9 @@ export function computeRoutesTradingApi( throw new Error('Expected all token properties to be present') } - const parsedCurrencyIn = tokenInIsNative - ? NativeCurrency.onChain(tokenIn.chainId) - : parseTokenApi(tokenIn) + const parsedCurrencyIn = tokenInIsNative ? NativeCurrency.onChain(tokenIn.chainId) : parseTokenApi(tokenIn) - const parsedCurrencyOut = tokenOutIsNative - ? NativeCurrency.onChain(tokenOut.chainId) - : parseTokenApi(tokenOut) + const parsedCurrencyOut = tokenOutIsNative ? NativeCurrency.onChain(tokenOut.chainId) : parseTokenApi(tokenOut) try { return quote.route.map((route) => { @@ -179,12 +171,8 @@ export function computeRoutesTradingApi( const isOnlyV3 = isV3OnlyRouteApi(route) return { - routev3: isOnlyV3 - ? new V3Route(route.map(parseV3PoolApi), parsedCurrencyIn, parsedCurrencyOut) - : null, - routev2: isOnlyV2 - ? new V2Route(route.map(parseV2PairApi), parsedCurrencyIn, parsedCurrencyOut) - : null, + routev3: isOnlyV3 ? new V3Route(route.map(parseV3PoolApi), parsedCurrencyIn, parsedCurrencyOut) : null, + routev2: isOnlyV2 ? new V2Route(route.map(parseV2PairApi), parsedCurrencyIn, parsedCurrencyOut) : null, mixedRoute: !isOnlyV3 && !isOnlyV2 ? new MixedRouteSDK(route.map(parseMixedRouteApi), parsedCurrencyIn, parsedCurrencyOut) @@ -232,7 +220,7 @@ function parseTokenApi(token: TradingApiTokenInRoute): Token { /**name=*/ undefined, false, buyFeeBps ? BigNumber.from(buyFeeBps) : undefined, - sellFeeBps ? BigNumber.from(sellFeeBps) : undefined + sellFeeBps ? BigNumber.from(sellFeeBps) : undefined, ) } @@ -253,7 +241,7 @@ function parseV3PoolApi({ parseInt(fee, 10) as FeeAmount, sqrtRatioX96, liquidity, - parseInt(tickCurrent, 10) + parseInt(tickCurrent, 10), ) } @@ -263,7 +251,7 @@ function parseV2PairApi({ reserve0, reserve1 }: TradingApiV2PoolInRoute): Pair { } return new Pair( CurrencyAmount.fromRawAmount(parseTokenApi(reserve0.token), reserve0.quotient), - CurrencyAmount.fromRawAmount(parseTokenApi(reserve1.token), reserve1.quotient) + CurrencyAmount.fromRawAmount(parseTokenApi(reserve1.token), reserve1.quotient), ) } @@ -287,7 +275,7 @@ export function getTokenAddressForApi(currency: Maybe): string | undef } const SUPPORTED_TRADING_API_CHAIN_IDS: number[] = Object.values(TradingApiChainId).filter( - (value): value is number => typeof value === 'number' + (value): value is number => typeof value === 'number', ) // Parse any chain id to check if its supported by the API ChainId type @@ -298,9 +286,7 @@ function isTradingApiSupportedChainId(chainId?: number): chainId is TradingApiCh return Object.values(SUPPORTED_TRADING_API_CHAIN_IDS).includes(chainId) } -export function toTradingApiSupportedChainId( - chainId: Maybe -): TradingApiChainId | undefined { +export function toTradingApiSupportedChainId(chainId: Maybe): TradingApiChainId | undefined { if (!chainId || !isTradingApiSupportedChainId(chainId)) { return undefined } @@ -346,14 +332,8 @@ export function validateTrade({ return null } - const inputsMatch = areAddressesEqual( - currencyIn.wrapped.address, - trade?.inputAmount.currency.wrapped.address - ) - const outputsMatch = areAddressesEqual( - currencyOut.wrapped.address, - trade.outputAmount.currency.wrapped.address - ) + const inputsMatch = areAddressesEqual(currencyIn.wrapped.address, trade?.inputAmount.currency.wrapped.address) + const outputsMatch = areAddressesEqual(currencyOut.wrapped.address, trade.outputAmount.currency.wrapped.address) // TODO(MOB-3028): check if this logic needs any adjustments once we add UniswapX support. // Verify the amount specified in the quote response matches the exact amount from input state @@ -367,21 +347,18 @@ export function validateTrade({ const exactAmountsMatch = exactAmount?.toExact() !== exactAmountFromQuote if (!(tokenAddressesMatch && exactAmountsMatch)) { - logger.error( - new Error(`Mismatched ${!tokenAddressesMatch ? 'address' : 'exact amount'} in swap trade`), - { - tags: { file: 'tradingApi/utils', function: 'validateTrade' }, - extra: { - formState: { - currencyIdIn: currencyId(currencyIn), - currencyIdOut: currencyId(currencyOut), - exactAmount: exactAmount.toExact(), - exactCurrencyField, - }, - tradeProperties: getBaseTradeAnalyticsProperties({ trade, formatter }), + logger.error(new Error(`Mismatched ${!tokenAddressesMatch ? 'address' : 'exact amount'} in swap trade`), { + tags: { file: 'tradingApi/utils', function: 'validateTrade' }, + extra: { + formState: { + currencyIdIn: currencyId(currencyIn), + currencyIdOut: currencyId(currencyOut), + exactAmount: exactAmount.toExact(), + exactCurrencyField, }, - } - ) + tradeProperties: getBaseTradeAnalyticsProperties({ trade, formatter }), + }, + }) return null } @@ -393,7 +370,7 @@ export function validateTrade({ export function getRoutingPreferenceForSwapRequest( protocolPreference: TradeProtocolPreference | undefined, uniswapXEnabled: boolean, - isUSDQuote?: boolean + isUSDQuote?: boolean, ): RoutingPreference { if (isUSDQuote) { return RoutingPreference.CLASSIC @@ -410,3 +387,13 @@ export function getRoutingPreferenceForSwapRequest( return RoutingPreference.CLASSIC } } + +export const ORDER_STATUS_TO_TX_STATUS: { [key in OrderStatus]: TransactionStatus } = { + [OrderStatus.CANCELLED]: TransactionStatus.Canceled, + [OrderStatus.ERROR]: TransactionStatus.Failed, + [OrderStatus.EXPIRED]: TransactionStatus.Expired, + [OrderStatus.FILLED]: TransactionStatus.Success, + [OrderStatus.INSUFFICIENT_FUNDS]: TransactionStatus.InsufficientFunds, + [OrderStatus.OPEN]: TransactionStatus.Pending, + [OrderStatus.UNVERIFIED]: TransactionStatus.Unknown, +} diff --git a/packages/wallet/src/features/transactions/swap/trade/hooks/useAcceptedTrade.ts b/packages/wallet/src/features/transactions/swap/trade/hooks/useAcceptedTrade.ts index 9bc0d6a2eeb..61f93420bec 100644 --- a/packages/wallet/src/features/transactions/swap/trade/hooks/useAcceptedTrade.ts +++ b/packages/wallet/src/features/transactions/swap/trade/hooks/useAcceptedTrade.ts @@ -2,7 +2,13 @@ import { useEffect, useState } from 'react' import { DerivedSwapInfo } from 'wallet/src/features/transactions/swap/types' import { requireAcceptNewTrade } from 'wallet/src/features/transactions/swap/utils' -export function useAcceptedTrade({ derivedSwapInfo }: { derivedSwapInfo?: DerivedSwapInfo }): { +export function useAcceptedTrade({ + derivedSwapInfo, + isSubmitting, +}: { + derivedSwapInfo?: DerivedSwapInfo + isSubmitting: boolean +}): { onAcceptTrade: () => undefined acceptedDerivedSwapInfo?: DerivedSwapInfo newTradeRequiresAcceptance: boolean @@ -12,7 +18,8 @@ export function useAcceptedTrade({ derivedSwapInfo }: { derivedSwapInfo?: Derive const trade = derivedSwapInfo?.trade.trade const acceptedTrade = acceptedDerivedSwapInfo?.trade.trade - const newTradeRequiresAcceptance = requireAcceptNewTrade(acceptedTrade, trade) + // Avoid prompting user to accept new trade if submission is in progress + const newTradeRequiresAcceptance = !isSubmitting && requireAcceptNewTrade(acceptedTrade, trade) useEffect(() => { if (!trade || trade === acceptedTrade) { diff --git a/packages/wallet/src/features/transactions/swap/trade/hooks/useDerivedSwapInfo.ts b/packages/wallet/src/features/transactions/swap/trade/hooks/useDerivedSwapInfo.ts index 72a64f2bb23..d75f7ee985a 100644 --- a/packages/wallet/src/features/transactions/swap/trade/hooks/useDerivedSwapInfo.ts +++ b/packages/wallet/src/features/transactions/swap/trade/hooks/useDerivedSwapInfo.ts @@ -2,20 +2,17 @@ import { TradeType } from '@uniswap/sdk-core' import { useMemo } from 'react' import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' +import { CurrencyField, TransactionState } from 'uniswap/src/features/transactions/transactionState/types' import { UniverseChainId } from 'uniswap/src/types/chains' +import { buildCurrencyId } from 'uniswap/src/utils/currencyId' import { useOnChainCurrencyBalance } from 'wallet/src/features/portfolio/api' import { useCurrencyInfo } from 'wallet/src/features/tokens/useCurrencyInfo' +import { useTrade } from 'wallet/src/features/transactions/swap/trade/api/hooks/useTrade' import { useSetTradeSlippage } from 'wallet/src/features/transactions/swap/trade/hooks/useSetTradeSlippage' import { useUSDCValue } from 'wallet/src/features/transactions/swap/trade/hooks/useUSDCPrice' -import { useTradingApiTrade } from 'wallet/src/features/transactions/swap/trade/tradingApi/hooks/useTradingApiTrade' import { DerivedSwapInfo } from 'wallet/src/features/transactions/swap/types' import { getWrapType, isWrapAction } from 'wallet/src/features/transactions/swap/utils' -import { - CurrencyField, - TransactionState, -} from 'wallet/src/features/transactions/transactionState/types' import { useActiveAccount } from 'wallet/src/features/wallet/hooks' -import { buildCurrencyId } from 'wallet/src/utils/currencyId' import { ValueType, getCurrencyAmount } from 'wallet/src/utils/getCurrencyAmount' /** Returns information derived from the current swap state */ @@ -36,13 +33,11 @@ export function useDerivedSwapInfo(state: TransactionState): DerivedSwapInfo { const activeAccount = useActiveAccount() const currencyInInfo = useCurrencyInfo( - currencyAssetIn ? buildCurrencyId(currencyAssetIn.chainId, currencyAssetIn.address) : undefined + currencyAssetIn ? buildCurrencyId(currencyAssetIn.chainId, currencyAssetIn.address) : undefined, ) const currencyOutInfo = useCurrencyInfo( - currencyAssetOut - ? buildCurrencyId(currencyAssetOut.chainId, currencyAssetOut.address) - : undefined + currencyAssetOut ? buildCurrencyId(currencyAssetOut.chainId, currencyAssetOut.address) : undefined, ) const currencies = useMemo(() => { @@ -58,10 +53,7 @@ export function useDerivedSwapInfo(state: TransactionState): DerivedSwapInfo { const chainId = currencyIn?.chainId ?? currencyOut?.chainId ?? UniverseChainId.Mainnet const { balance: tokenInBalance } = useOnChainCurrencyBalance(currencyIn, activeAccount?.address) - const { balance: tokenOutBalance } = useOnChainCurrencyBalance( - currencyOut, - activeAccount?.address - ) + const { balance: tokenOutBalance } = useOnChainCurrencyBalance(currencyOut, activeAccount?.address) const isExactIn = exactCurrencyField === CurrencyField.INPUT const wrapType = getWrapType(currencyIn, currencyOut) @@ -92,38 +84,25 @@ export function useDerivedSwapInfo(state: TransactionState): DerivedSwapInfo { tradeProtocolPreference: isOptionalRoutingEnabled ? tradeProtocolPreference : undefined, } - const tradingApiTrade = useTradingApiTrade(tradeParams) + const tradeTradeWithoutSlippage = useTrade(tradeParams) // Calculate auto slippage tolerance for trade. If customSlippageTolerance is undefined, then the Trade slippage is set to the calculated value. - const { trade, autoSlippageTolerance } = useSetTradeSlippage( - tradingApiTrade, - customSlippageTolerance - ) + const { trade, autoSlippageTolerance } = useSetTradeSlippage(tradeTradeWithoutSlippage, customSlippageTolerance) const currencyAmounts = useMemo( () => shouldGetQuote ? { [CurrencyField.INPUT]: - exactCurrencyField === CurrencyField.INPUT - ? amountSpecified - : trade.trade?.inputAmount, + exactCurrencyField === CurrencyField.INPUT ? amountSpecified : trade.trade?.inputAmount, [CurrencyField.OUTPUT]: - exactCurrencyField === CurrencyField.OUTPUT - ? amountSpecified - : trade.trade?.outputAmount, + exactCurrencyField === CurrencyField.OUTPUT ? amountSpecified : trade.trade?.outputAmount, } : { [CurrencyField.INPUT]: amountSpecified, [CurrencyField.OUTPUT]: amountSpecified, }, - [ - shouldGetQuote, - exactCurrencyField, - amountSpecified, - trade.trade?.inputAmount, - trade.trade?.outputAmount, - ] + [shouldGetQuote, exactCurrencyField, amountSpecified, trade.trade?.inputAmount, trade.trade?.outputAmount], ) const inputCurrencyUSDValue = useUSDCValue(currencyAmounts[CurrencyField.INPUT]) diff --git a/packages/wallet/src/features/transactions/swap/trade/hooks/useSetTradeSlippage.ts b/packages/wallet/src/features/transactions/swap/trade/hooks/useSetTradeSlippage.ts index 6c5f5f37896..c3898481ad2 100644 --- a/packages/wallet/src/features/transactions/swap/trade/hooks/useSetTradeSlippage.ts +++ b/packages/wallet/src/features/transactions/swap/trade/hooks/useSetTradeSlippage.ts @@ -1,21 +1,18 @@ import { useMemo } from 'react' -import { DynamicConfigs } from 'uniswap/src/features/gating/configs' -import { useDynamicConfig } from 'uniswap/src/features/gating/hooks' -import { - MAX_AUTO_SLIPPAGE_TOLERANCE, - MIN_AUTO_SLIPPAGE_TOLERANCE, -} from 'wallet/src/constants/transactions' -import { isL2Chain, toSupportedChainId } from 'wallet/src/features/chains/utils' -import { useUSDCValue } from 'wallet/src/features/transactions/swap/trade/hooks/useUSDCPrice' +import { isMainnetChainId, toSupportedChainId } from 'uniswap/src/features/chains/utils' +import { DynamicConfigs, SlippageConfigKey } from 'uniswap/src/features/gating/configs' +import { useDynamicConfigValue } from 'uniswap/src/features/gating/hooks' +import { MAX_AUTO_SLIPPAGE_TOLERANCE, MIN_AUTO_SLIPPAGE_TOLERANCE } from 'wallet/src/constants/transactions' import { getClassicQuoteFromResponse, transformTradingApiResponseToTrade, -} from 'wallet/src/features/transactions/swap/trade/tradingApi/utils' +} from 'wallet/src/features/transactions/swap/trade/api/utils' +import { useUSDCValue } from 'wallet/src/features/transactions/swap/trade/hooks/useUSDCPrice' import { Trade, TradeWithStatus } from 'wallet/src/features/transactions/swap/trade/types' export function useSetTradeSlippage( trade: TradeWithStatus, - userSetSlippage?: number + userSetSlippage?: number, ): { trade: TradeWithStatus; autoSlippageTolerance: number } { // Always calculate and return autoSlippageTolerance so the UI can warn user when custom slippage is set higher than auto slippage const autoSlippageTolerance = useCalculateAutoSlippage(trade?.trade) @@ -66,14 +63,12 @@ export function useSetTradeSlippage( function useCalculateAutoSlippage(trade: Maybe): number { const outputAmountUSD = useUSDCValue(trade?.outputAmount)?.toExact() - const minAutoSlippageToleranceL2 = useSlippageValueFromDynamicConfig( - SlippageConfigName.MinAutoSlippageToleranceL2 - ) + const minAutoSlippageToleranceL2 = useSlippageValueFromDynamicConfig(SlippageConfigKey.MinAutoSlippageToleranceL2) return useMemo(() => { const quote = getClassicQuoteFromResponse(trade?.quote) const chainId = toSupportedChainId(quote?.chainId) ?? undefined - const onL2 = isL2Chain(chainId) + const onL2 = !isMainnetChainId(chainId) const gasCostUSD = quote?.gasFeeUSD return calculateAutoSlippage({ onL2, @@ -116,20 +111,8 @@ function calculateAutoSlippage({ return Number(suggestedSlippageTolerance.toFixed(2)) } -enum SlippageConfigName { - MinAutoSlippageToleranceL2, -} - -// Allows us to type the values stored in the JSON dynamic config object for slippage params. -// Names in mapping should exatcly match the JSON object in statsig. -export const SLIPPAGE_CONFIG_NAMES = new Map([ - [SlippageConfigName.MinAutoSlippageToleranceL2, 'minAutoSlippageToleranceL2'], -]) - -function useSlippageValueFromDynamicConfig(configName: SlippageConfigName): number { - const slippageConfig = useDynamicConfig(DynamicConfigs.Slippage) - - const slippageValue = slippageConfig.getValue(SLIPPAGE_CONFIG_NAMES.get(configName)) as string +function useSlippageValueFromDynamicConfig(configName: SlippageConfigKey): number { + const slippageValue = useDynamicConfigValue(DynamicConfigs.Slippage, configName, '') // Format as % number return parseInt(slippageValue, 10) diff --git a/packages/wallet/src/features/transactions/swap/trade/hooks/useShowSwapNetworkNotification.ts b/packages/wallet/src/features/transactions/swap/trade/hooks/useShowSwapNetworkNotification.ts index 0d36bfbb04d..cf321b0dfaa 100644 --- a/packages/wallet/src/features/transactions/swap/trade/hooks/useShowSwapNetworkNotification.ts +++ b/packages/wallet/src/features/transactions/swap/trade/hooks/useShowSwapNetworkNotification.ts @@ -1,13 +1,13 @@ import { useEffect } from 'react' +import { useDispatch } from 'react-redux' import { WalletChainId } from 'uniswap/src/types/chains' import { usePrevious } from 'utilities/src/react/hooks' import { pushNotification } from 'wallet/src/features/notifications/slice' import { AppNotificationType } from 'wallet/src/features/notifications/types' -import { useAppDispatch } from 'wallet/src/state' export function useShowSwapNetworkNotification(chainId?: WalletChainId): void { const prevChainId = usePrevious(chainId) - const appDispatch = useAppDispatch() + const appDispatch = useDispatch() useEffect(() => { // don't fire notification toast for first network selection if (!prevChainId || !chainId || prevChainId === chainId) { @@ -20,7 +20,7 @@ export function useShowSwapNetworkNotification(chainId?: WalletChainId): void { chainId, flow: 'swap', hideDelay: 2000, - }) + }), ) }, [chainId, prevChainId, appDispatch]) } diff --git a/packages/wallet/src/features/transactions/swap/trade/hooks/useSwapCallback.ts b/packages/wallet/src/features/transactions/swap/trade/hooks/useSwapCallback.ts index d2836cd9c0f..43bd5e5c53b 100644 --- a/packages/wallet/src/features/transactions/swap/trade/hooks/useSwapCallback.ts +++ b/packages/wallet/src/features/transactions/swap/trade/hooks/useSwapCallback.ts @@ -1,87 +1,71 @@ import { SwapEventName } from '@uniswap/analytics-events' import { Currency, CurrencyAmount } from '@uniswap/sdk-core' -import { providers } from 'ethers' -import { useMemo } from 'react' +import { useCallback } from 'react' +import { useDispatch } from 'react-redux' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' -import { logger } from 'utilities/src/logger/logger' import { setHasSubmittedHoldToSwap } from 'wallet/src/features/behaviorHistory/slice' -import { GasFeeResult } from 'wallet/src/features/gas/types' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' import { selectSwapStartTimestamp } from 'wallet/src/features/timing/selectors' import { updateSwapStartTimestamp } from 'wallet/src/features/timing/slice' +import { ValidatedSwapTxContext } from 'wallet/src/features/transactions/contexts/SwapTxContext' import { getBaseTradeAnalyticsProperties } from 'wallet/src/features/transactions/swap/analytics' -import { SwapParams, swapActions } from 'wallet/src/features/transactions/swap/swapSaga' -import { getClassicQuoteFromResponse } from 'wallet/src/features/transactions/swap/trade/tradingApi/utils' -import { Trade } from 'wallet/src/features/transactions/swap/trade/types' -import { tradeToTransactionInfo } from 'wallet/src/features/transactions/swap/utils' -import { useActiveAccount } from 'wallet/src/features/wallet/hooks' -import { useAppDispatch, useAppSelector } from 'wallet/src/state' +import { swapActions } from 'wallet/src/features/transactions/swap/swapSaga' +import { getClassicQuoteFromResponse } from 'wallet/src/features/transactions/swap/trade/api/utils' +import { isClassic } from 'wallet/src/features/transactions/swap/trade/utils' +import { SignerMnemonicAccount } from 'wallet/src/features/wallet/accounts/types' +import { useAppSelector } from 'wallet/src/state' import { toStringish } from 'wallet/src/utils/number' -/** Callback to submit trades and track progress */ -export function useSwapCallback( - approveTxRequest: providers.TransactionRequest | undefined, - swapTxRequest: providers.TransactionRequest | undefined, - gasFee: GasFeeResult, - trade: Trade | null | undefined, - currencyInAmountUSD: Maybe>, - currencyOutAmountUSD: Maybe>, - isAutoSlippage: boolean, - onSubmit: () => void, - txId?: string, - isHoldToSwap?: boolean, - +interface SwapCallbackArgs { + account: SignerMnemonicAccount + swapTxContext: ValidatedSwapTxContext + currencyInAmountUSD: Maybe> + currencyOutAmountUSD: Maybe> + isAutoSlippage: boolean + onSubmit: () => void + onFailure: () => void + txId?: string + isHoldToSwap?: boolean isFiatInputMode?: boolean -): () => void { - const appDispatch = useAppDispatch() - const account = useActiveAccount() +} + +/** Callback to submit trades and track progress */ +export function useSwapCallback(): (args: SwapCallbackArgs) => void { + const appDispatch = useDispatch() const formatter = useLocalizationContext() const swapStartTimestamp = useAppSelector(selectSwapStartTimestamp) - return useMemo(() => { - if (!account || !swapTxRequest || !trade || !gasFee.value) { - return () => { - logger.error(new Error('Attempted swap with missing required parameters'), { - tags: { - file: 'swap/hooks', - function: 'useSwapCallback', - }, - extra: { account, swapTxRequest, trade, gasFee }, - }) - } - } - - return () => { - const params: SwapParams = { - txId, + return useCallback( + (args: SwapCallbackArgs) => { + const { account, - analytics: getBaseTradeAnalyticsProperties({ formatter, trade }), - approveTxRequest, - swapTxRequest, - swapTypeInfo: tradeToTransactionInfo(trade), - } + swapTxContext, + txId, + onSubmit, + onFailure, + currencyInAmountUSD, + currencyOutAmountUSD, + isAutoSlippage, + isHoldToSwap, + isFiatInputMode, + } = args + const { trade, gasFee } = swapTxContext - appDispatch(swapActions.trigger(params)) - onSubmit() + const analytics = getBaseTradeAnalyticsProperties({ formatter, trade }) + appDispatch(swapActions.trigger({ swapTxContext, txId, account, analytics, onSubmit, onFailure })) const blockNumber = getClassicQuoteFromResponse(trade?.quote)?.blockNumber?.toString() sendAnalyticsEvent(SwapEventName.SWAP_SUBMITTED_BUTTON_CLICKED, { - ...getBaseTradeAnalyticsProperties({ formatter, trade }), + ...analytics, estimated_network_fee_wei: gasFee.value, - gas_limit: toStringish(swapTxRequest.gasLimit), - token_in_amount_usd: currencyInAmountUSD - ? parseFloat(currencyInAmountUSD.toFixed(2)) - : undefined, - token_out_amount_usd: currencyOutAmountUSD - ? parseFloat(currencyOutAmountUSD.toFixed(2)) - : undefined, + gas_limit: isClassic(swapTxContext) ? toStringish(swapTxContext.txRequest.gasLimit) : undefined, + token_in_amount_usd: currencyInAmountUSD ? parseFloat(currencyInAmountUSD.toFixed(2)) : undefined, + token_out_amount_usd: currencyOutAmountUSD ? parseFloat(currencyOutAmountUSD.toFixed(2)) : undefined, transaction_deadline_seconds: trade.deadline, swap_quote_block_number: blockNumber, is_auto_slippage: isAutoSlippage, - swap_flow_duration_milliseconds: swapStartTimestamp - ? Date.now() - swapStartTimestamp - : undefined, + swap_flow_duration_milliseconds: swapStartTimestamp ? Date.now() - swapStartTimestamp : undefined, is_hold_to_swap: isHoldToSwap, is_fiat_input_mode: isFiatInputMode, }) @@ -93,22 +77,7 @@ export function useSwapCallback( if (isHoldToSwap) { appDispatch(setHasSubmittedHoldToSwap(true)) } - } - }, [ - account, - swapTxRequest, - trade, - gasFee, - appDispatch, - txId, - currencyInAmountUSD, - currencyOutAmountUSD, - approveTxRequest, - onSubmit, - formatter, - isAutoSlippage, - swapStartTimestamp, - isHoldToSwap, - isFiatInputMode, - ]) + }, + [appDispatch, formatter, swapStartTimestamp], + ) } diff --git a/packages/wallet/src/features/transactions/swap/trade/hooks/useUSDCPrice.ts b/packages/wallet/src/features/transactions/swap/trade/hooks/useUSDCPrice.ts index 0f452a4e966..22135a819a8 100644 --- a/packages/wallet/src/features/transactions/swap/trade/hooks/useUSDCPrice.ts +++ b/packages/wallet/src/features/transactions/swap/trade/hooks/useUSDCPrice.ts @@ -1,7 +1,8 @@ import { Currency, CurrencyAmount, Price, Token, TradeType } from '@uniswap/sdk-core' import { useMemo } from 'react' +import { PollingInterval } from 'uniswap/src/constants/misc' import { UniverseChainId } from 'uniswap/src/types/chains' -import { PollingInterval } from 'wallet/src/constants/misc' +import { areCurrencyIdsEqual, currencyId } from 'uniswap/src/utils/currencyId' import { CUSD, USDB, @@ -16,9 +17,8 @@ import { USDT_BNB, USDzC, } from 'wallet/src/constants/tokens' -import { useTradingApiTrade } from 'wallet/src/features/transactions/swap/trade/tradingApi/hooks/useTradingApiTrade' +import { useTrade } from 'wallet/src/features/transactions/swap/trade/api/hooks/useTrade' import { isClassic } from 'wallet/src/features/transactions/swap/trade/utils' -import { areCurrencyIdsEqual, currencyId } from 'wallet/src/utils/currencyId' // Stablecoin amounts used when calculating spot price for a given currency. // The amount is large enough to filter low liquidity pairs. @@ -49,11 +49,11 @@ export function useUSDCPrice(currency?: Currency): Price | u // avoid requesting quotes for stablecoin input const currencyIsStablecoin = Boolean( - stablecoin && currency && areCurrencyIdsEqual(currencyId(currency), currencyId(stablecoin)) + stablecoin && currency && areCurrencyIdsEqual(currencyId(currency), currencyId(stablecoin)), ) const amountSpecified = currencyIsStablecoin ? undefined : quoteAmount - const { trade } = useTradingApiTrade({ + const { trade } = useTrade({ amountSpecified, otherCurrency: currency, tradeType: TradeType.EXACT_OUTPUT, @@ -81,7 +81,7 @@ export function useUSDCPrice(currency?: Currency): Price | u } export function useUSDCValue( - currencyAmount: CurrencyAmount | undefined | null + currencyAmount: CurrencyAmount | undefined | null, ): CurrencyAmount | null { const price = useUSDCPrice(currencyAmount?.currency) diff --git a/packages/wallet/src/features/transactions/swap/trade/hooks/useUSDTokenUpdater.ts b/packages/wallet/src/features/transactions/swap/trade/hooks/useUSDTokenUpdater.ts index 0b130e603bf..0ed4d9902aa 100644 --- a/packages/wallet/src/features/transactions/swap/trade/hooks/useUSDTokenUpdater.ts +++ b/packages/wallet/src/features/transactions/swap/trade/hooks/useUSDTokenUpdater.ts @@ -3,10 +3,7 @@ import React, { useEffect, useRef } from 'react' import { AnyAction } from 'redux' import { NumberType } from 'utilities/src/format/types' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' -import { - STABLECOIN_AMOUNT_OUT, - useUSDCPrice, -} from 'wallet/src/features/transactions/swap/trade/hooks/useUSDCPrice' +import { STABLECOIN_AMOUNT_OUT, useUSDCPrice } from 'wallet/src/features/transactions/swap/trade/hooks/useUSDCPrice' import { updateExactAmountFiat, updateExactAmountToken, @@ -21,12 +18,12 @@ export function useUSDTokenUpdater( isFiatInput: boolean, exactAmountToken: string, exactAmountFiat: string, - exactCurrency?: Currency + exactCurrency?: Currency, ): void { const price = useUSDCPrice(exactCurrency) const shouldUseUSDRef = useRef(isFiatInput) const { convertFiatAmount, formatCurrencyAmount } = useLocalizationContext() - const conversionRate = convertFiatAmount().amount + const conversionRate = convertFiatAmount(1).amount useEffect(() => { shouldUseUSDRef.current = isFiatInput @@ -55,7 +52,7 @@ export function useUSDTokenUpdater( type: NumberType.SwapTradeAmount, placeholder: '', }), - }) + }), ) } @@ -67,9 +64,7 @@ export function useUSDTokenUpdater( const usdPrice = exactCurrencyAmount ? price?.quote(exactCurrencyAmount) : undefined const fiatPrice = parseFloat(usdPrice?.toExact() ?? '0') * conversionRate - return dispatch( - updateExactAmountFiat({ amount: fiatPrice ? fiatPrice.toFixed(NUM_DECIMALS_DISPLAY) : '' }) - ) + return dispatch(updateExactAmountFiat({ amount: fiatPrice ? fiatPrice.toFixed(NUM_DECIMALS_DISPLAY) : '' })) }, [ dispatch, shouldUseUSDRef, diff --git a/packages/wallet/src/features/transactions/swap/trade/hooks/useWrapCallback.ts b/packages/wallet/src/features/transactions/swap/trade/hooks/useWrapCallback.ts index 6fc431f8318..d04c1eff9ae 100644 --- a/packages/wallet/src/features/transactions/swap/trade/hooks/useWrapCallback.ts +++ b/packages/wallet/src/features/transactions/swap/trade/hooks/useWrapCallback.ts @@ -1,23 +1,23 @@ import { Currency, CurrencyAmount } from '@uniswap/sdk-core' import { providers } from 'ethers' import { useMemo } from 'react' +import { useDispatch } from 'react-redux' import { logger } from 'utilities/src/logger/logger' import { isWrapAction } from 'wallet/src/features/transactions/swap/utils' import { WrapParams, tokenWrapActions } from 'wallet/src/features/transactions/swap/wrapSaga' import { WrapType } from 'wallet/src/features/transactions/types' import { useActiveAccount } from 'wallet/src/features/wallet/hooks' -import { useAppDispatch } from 'wallet/src/state' export function useWrapCallback( inputCurrencyAmount: CurrencyAmount | null | undefined, wrapType: WrapType, onSuccess: () => void, txRequest?: providers.TransactionRequest, - txId?: string + txId?: string, ): { wrapCallback: () => void } { - const appDispatch = useAppDispatch() + const appDispatch = useDispatch() const account = useActiveAccount() return useMemo(() => { diff --git a/packages/wallet/src/features/transactions/swap/trade/hooks/useWrapTransactionRequest.ts b/packages/wallet/src/features/transactions/swap/trade/hooks/useWrapTransactionRequest.ts index e314fb9a535..213e210a7d2 100644 --- a/packages/wallet/src/features/transactions/swap/trade/hooks/useWrapTransactionRequest.ts +++ b/packages/wallet/src/features/transactions/swap/trade/hooks/useWrapTransactionRequest.ts @@ -1,59 +1,58 @@ import { Currency, CurrencyAmount } from '@uniswap/sdk-core' import { providers } from 'ethers' import { useCallback } from 'react' +import { CurrencyField } from 'uniswap/src/features/transactions/transactionState/types' import { WalletChainId } from 'uniswap/src/types/chains' import { useAsyncData } from 'utilities/src/react/hooks' +import { Trade } from 'wallet/src/features/transactions/swap/trade/types' +import { isUniswapX } from 'wallet/src/features/transactions/swap/trade/utils' import { DerivedSwapInfo } from 'wallet/src/features/transactions/swap/types' import { getWethContract } from 'wallet/src/features/transactions/swap/wrapSaga' -import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' import { WrapType } from 'wallet/src/features/transactions/types' import { useProvider } from 'wallet/src/features/wallet/context' import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' -export function useWrapTransactionRequest( - derivedSwapInfo: DerivedSwapInfo -): providers.TransactionRequest | undefined { +export function useWrapTransactionRequest(derivedSwapInfo: DerivedSwapInfo): providers.TransactionRequest | undefined { const address = useActiveAccountAddressWithThrow() - const { chainId, wrapType, currencyAmounts } = derivedSwapInfo + const { chainId, wrapType, currencyAmounts, trade } = derivedSwapInfo const provider = useProvider(chainId) - const transactionFetcher = useCallback(() => { - if (!provider || wrapType === WrapType.NotApplicable) { - return - } - - return getWrapTransactionRequest( - provider, - chainId, - address, - wrapType, - currencyAmounts[CurrencyField.INPUT] - ) - }, [address, chainId, wrapType, currencyAmounts, provider]) + const transactionFetcher = useCallback( + () => + getWrapTransactionRequest( + provider, + trade.trade, + chainId, + address, + wrapType, + currencyAmounts[CurrencyField.INPUT], + ), + [address, chainId, wrapType, currencyAmounts, provider, trade.trade], + ) return useAsyncData(transactionFetcher).data } const getWrapTransactionRequest = async ( - provider: providers.Provider, + provider: providers.Provider | null, + trade: Trade | null, chainId: WalletChainId, address: Address, wrapType: WrapType, - currencyAmountIn: Maybe> + currencyAmountIn: Maybe>, ): Promise => { - if (!currencyAmountIn) { + const isUniswapXWrap = trade && isUniswapX(trade) && trade.needsWrap + if (!currencyAmountIn || !provider || (wrapType === WrapType.NotApplicable && !isUniswapXWrap)) { return } const wethContract = await getWethContract(chainId, provider) const wethTx = - wrapType === WrapType.Wrap + wrapType === WrapType.Wrap || isUniswapXWrap ? await wethContract.populateTransaction.deposit({ value: `0x${currencyAmountIn.quotient.toString(16)}`, }) - : await wethContract.populateTransaction.withdraw( - `0x${currencyAmountIn.quotient.toString(16)}` - ) + : await wethContract.populateTransaction.withdraw(`0x${currencyAmountIn.quotient.toString(16)}`) return { ...wethTx, from: address, chainId } } diff --git a/packages/wallet/src/features/transactions/swap/trade/tradingApi/hooks/useSwapTxAndGasInfoTradingApi.ts b/packages/wallet/src/features/transactions/swap/trade/tradingApi/hooks/useSwapTxAndGasInfoTradingApi.ts deleted file mode 100644 index 0b9b30a2939..00000000000 --- a/packages/wallet/src/features/transactions/swap/trade/tradingApi/hooks/useSwapTxAndGasInfoTradingApi.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { providers } from 'ethers' -import { GasFeeResult } from 'wallet/src/features/gas/types' -import { useTokenApprovalInfo } from 'wallet/src/features/transactions/swap/trade/tradingApi/hooks/useTokenApprovalInfo' -import { useTransactionRequestInfo } from 'wallet/src/features/transactions/swap/trade/tradingApi/hooks/useTransactionRequestInfo' -import { ApprovalAction } from 'wallet/src/features/transactions/swap/trade/types' -import { DerivedSwapInfo } from 'wallet/src/features/transactions/swap/types' -import { sumGasFees } from 'wallet/src/features/transactions/swap/utils' -import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' - -interface SwapTxAndGasInfo { - txRequest?: providers.TransactionRequest - approveTxRequest?: providers.TransactionRequest - approvalError?: boolean // block UI if unable to get approval status - gasFee: GasFeeResult -} - -export function useSwapTxAndGasInfoTradingApi({ - derivedSwapInfo, -}: { - derivedSwapInfo: DerivedSwapInfo -}): SwapTxAndGasInfo { - const { chainId, wrapType, currencyAmounts } = derivedSwapInfo - - const tokenApprovalInfo = useTokenApprovalInfo({ - chainId, - wrapType, - currencyInAmount: currencyAmounts[CurrencyField.INPUT], - }) - - const transactionRequestInfo = useTransactionRequestInfo({ - derivedSwapInfo, - // Dont send transaction request if invalid or missing approval data - skip: !tokenApprovalInfo?.action || tokenApprovalInfo.action === ApprovalAction.Unknown, - tokenApprovalInfo, - }) - - const approvalError = tokenApprovalInfo?.action === ApprovalAction.Unknown - const gasFeeError = transactionRequestInfo.gasFeeResult.error || approvalError - - const areValuesReady = - tokenApprovalInfo && transactionRequestInfo.gasFeeResult.value !== undefined - - // Do not populate gas fee: - // - If errors exist on swap or approval requests. - // - If we don't have both the approval and transaction gas fees. - const totalGasFee = - gasFeeError || !areValuesReady - ? undefined - : sumGasFees(tokenApprovalInfo?.gasFee, transactionRequestInfo?.gasFeeResult.value) - - const gasFeeResult = { - value: totalGasFee, - loading: !tokenApprovalInfo || transactionRequestInfo.gasFeeResult.loading, - error: gasFeeError, - } - - return { - txRequest: transactionRequestInfo.transactionRequest, - approveTxRequest: tokenApprovalInfo?.txRequest || undefined, - approvalError, - gasFee: gasFeeResult, - } -} diff --git a/packages/wallet/src/features/transactions/swap/trade/types.ts b/packages/wallet/src/features/transactions/swap/trade/types.ts index 3f31c765f6c..8b27b52a038 100644 --- a/packages/wallet/src/features/transactions/swap/trade/types.ts +++ b/packages/wallet/src/features/transactions/swap/trade/types.ts @@ -6,18 +6,10 @@ import { V2DutchOrderTrade } from '@uniswap/uniswapx-sdk' import { Route as V2RouteSDK } from '@uniswap/v2-sdk' import { Route as V3RouteSDK } from '@uniswap/v3-sdk' import { providers } from 'ethers' -import { PollingInterval } from 'wallet/src/constants/misc' -import { - ClassicQuote, - DutchQuoteV2, - QuoteResponse, - Routing, -} from 'wallet/src/data/tradingApi/__generated__/index' -import { - getSwapFee, - transformToDutchOrderInfo, -} from 'wallet/src/features/transactions/swap/trade/tradingApi/utils' -import { TradeProtocolPreference } from 'wallet/src/features/transactions/transactionState/types' +import { PollingInterval } from 'uniswap/src/constants/misc' +import { TradeProtocolPreference } from 'uniswap/src/features/transactions/transactionState/types' +import { ClassicQuote, DutchQuoteV2, QuoteResponse, Routing } from 'wallet/src/data/tradingApi/__generated__/index' +import { getSwapFee, transformToDutchOrderInfo } from 'wallet/src/features/transactions/swap/trade/api/utils' // TradingAPI team is looking into updating type generation to produce the following types for it's current QuoteResponse type: // See: https://linear.app/uniswap/issue/API-236/explore-changing-the-quote-schema-to-pull-out-a-basequoteresponse @@ -35,11 +27,9 @@ export type ClassicQuoteResponse = QuoteResponse & { export class UniswapXTrade extends V2DutchOrderTrade { readonly routing = Routing.DUTCH_V2 - quote: DutchQuoteResponse - // TODO(WEB-4299): Update trade to include classicGasUseEstimateUSD once trading API supports it. - // classicGasUseEstimateUSD?: number - slippageTolerance: number - swapFee?: SwapFee + readonly quote: DutchQuoteResponse + readonly slippageTolerance: number + readonly swapFee?: SwapFee constructor({ quote, @@ -59,6 +49,10 @@ export class UniswapXTrade extends V2DutchOrderTrade extends RouterSDKTrade { readonly quote?: ClassicQuoteResponse readonly routing = Routing.CLASSIC @@ -125,7 +119,7 @@ export class ClassicTrade< export type Trade< TInput extends Currency = Currency, TOutput extends Currency = Currency, - TTradeType extends TradeType = TradeType + TTradeType extends TradeType = TradeType, > = ClassicTrade | UniswapXTrade export interface TradeWithStatus { diff --git a/packages/wallet/src/features/transactions/swap/trade/utils.ts b/packages/wallet/src/features/transactions/swap/trade/utils.ts index 88b7b1ed8cb..a566e17d657 100644 --- a/packages/wallet/src/features/transactions/swap/trade/utils.ts +++ b/packages/wallet/src/features/transactions/swap/trade/utils.ts @@ -1,13 +1,11 @@ import { Routing } from 'wallet/src/data/tradingApi/__generated__/index' export function isUniswapX( - obj: T + obj: T, ): obj is T & { routing: Routing.DUTCH_V2 | Routing.DUTCH_LIMIT } { return obj.routing === Routing.DUTCH_V2 || obj.routing === Routing.DUTCH_LIMIT } -export function isClassic( - obj: T -): obj is T & { routing: Routing.CLASSIC } { +export function isClassic(obj: T): obj is T & { routing: Routing.CLASSIC } { return obj.routing === Routing.CLASSIC } diff --git a/packages/wallet/src/features/transactions/swap/types.ts b/packages/wallet/src/features/transactions/swap/types.ts index ea2db1924a4..0e250607d3a 100644 --- a/packages/wallet/src/features/transactions/swap/types.ts +++ b/packages/wallet/src/features/transactions/swap/types.ts @@ -1,14 +1,14 @@ import { Currency, CurrencyAmount } from '@uniswap/sdk-core' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' +import { CurrencyField } from 'uniswap/src/features/transactions/transactionState/types' import { WalletChainId } from 'uniswap/src/types/chains' -import { useTradingApiTrade } from 'wallet/src/features/transactions/swap/trade/tradingApi/hooks/useTradingApiTrade' -import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' +import { useTrade } from 'wallet/src/features/transactions/swap/trade/api/hooks/useTrade' import { BaseDerivedInfo } from 'wallet/src/features/transactions/transfer/types' import { WrapType } from 'wallet/src/features/transactions/types' export type DerivedSwapInfo< TInput = CurrencyInfo, - TOutput extends CurrencyInfo = CurrencyInfo + TOutput extends CurrencyInfo = CurrencyInfo, > = BaseDerivedInfo & { chainId: WalletChainId currencies: BaseDerivedInfo['currencies'] & { @@ -25,7 +25,7 @@ export type DerivedSwapInfo< [CurrencyField.OUTPUT]: Maybe> } focusOnCurrencyField: CurrencyField | null - trade: ReturnType + trade: ReturnType wrapType: WrapType selectingCurrencyField?: CurrencyField txId?: string diff --git a/packages/wallet/src/features/transactions/swap/usePermit2Signature.ts b/packages/wallet/src/features/transactions/swap/usePermit2Signature.ts index 6effabb35bf..4f4aaed3f89 100644 --- a/packages/wallet/src/features/transactions/swap/usePermit2Signature.ts +++ b/packages/wallet/src/features/transactions/swap/usePermit2Signature.ts @@ -1,10 +1,4 @@ -import { - AllowanceProvider, - AllowanceTransfer, - MaxUint160, - permit2Address, - PermitSingle, -} from '@uniswap/permit2-sdk' +import { AllowanceProvider, AllowanceTransfer, MaxUint160, permit2Address, PermitSingle } from '@uniswap/permit2-sdk' import { Currency, CurrencyAmount } from '@uniswap/sdk-core' import { UNIVERSAL_ROUTER_ADDRESS } from '@uniswap/universal-router-sdk' import dayjs from 'dayjs' @@ -22,11 +16,7 @@ import { SignerManager } from 'wallet/src/features/wallet/signing/SignerManager' import { signTypedData } from 'wallet/src/features/wallet/signing/signing' const PERMIT2_SIG_VALIDITY_TIME = 30 // minutes -function getPermitStruct( - tokenAddress: string, - nonce: number, - universalRouterAddress: string -): PermitSingle { +function getPermitStruct(tokenAddress: string, nonce: number, universalRouterAddress: string): PermitSingle { return { details: { token: tokenAddress, @@ -56,15 +46,11 @@ async function getPermit2PermitSignature( account: Account, tokenAddress: string, chainId: WalletChainId, - tokenInAmount: string + tokenInAmount: string, ): Promise { try { if (account.type === AccountType.Readonly) { - logger.debug( - 'usePermit2PermitSignature', - 'getPermit2Signature', - 'Cannot sign with a view-only wallet' - ) + logger.debug('usePermit2PermitSignature', 'getPermit2Signature', 'Cannot sign with a view-only wallet') return } @@ -82,11 +68,7 @@ async function getPermit2PermitSignature( } const permitMessage = getPermitStruct(tokenAddress, nonce, universalRouterAddress) - const { domain, types, values } = AllowanceTransfer.getPermitData( - permitMessage, - permit2Address(chainId), - chainId - ) + const { domain, types, values } = AllowanceTransfer.getPermitData(permitMessage, permit2Address(chainId), chainId) const signature = await signTypedData( domain, @@ -95,7 +77,7 @@ async function getPermit2PermitSignature( // alternative would be to modify the sdk to use type aliases over interfaces { ...values }, account, - signerManager + signerManager, ) return { signature, @@ -111,7 +93,7 @@ async function getPermit2PermitSignature( export function usePermit2Signature( currencyInAmount: Maybe>, - skip?: boolean + skip?: boolean, ): { isLoading: boolean data: PermitSignatureInfo | undefined @@ -132,7 +114,7 @@ export function usePermit2Signature( account, currencyIn.address, currencyIn.chainId, - currencyInAmount.quotient.toString() + currencyInAmount.quotient.toString(), ) }, [account, currencyIn, currencyInAmount?.quotient, provider, signerManager, skip]) @@ -143,7 +125,7 @@ export function usePermit2Signature( export function usePermit2SignatureWithData( currencyInAmount: Maybe>, permitData: Maybe, - skip?: boolean + skip?: boolean, ): { isLoading: boolean signature: string | undefined @@ -165,7 +147,7 @@ export function usePermit2SignatureWithData( types as Record, values as Record, account, - signerManager + signerManager, ) }, [account, currencyIn, domain, provider, signerManager, skip, types, values]) diff --git a/packages/wallet/src/features/transactions/swap/utils.test.ts b/packages/wallet/src/features/transactions/swap/utils.test.ts index f2a23cfe542..2e25ae3ecf5 100644 --- a/packages/wallet/src/features/transactions/swap/utils.test.ts +++ b/packages/wallet/src/features/transactions/swap/utils.test.ts @@ -1,22 +1,19 @@ import { Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core' import { Route } from '@uniswap/v3-sdk' +import { UNI, WBTC } from 'uniswap/src/constants/tokens' +import { NativeCurrency } from 'uniswap/src/features/tokens/NativeCurrency' import { UniverseChainId } from 'uniswap/src/types/chains' -import { UNI, WBTC, wrappedNativeCurrency } from 'wallet/src/constants/tokens' -import { NativeCurrency } from 'wallet/src/features/tokens/NativeCurrency' +import { wrappedNativeCurrency } from 'uniswap/src/utils/currency' import { ClassicTrade } from 'wallet/src/features/transactions/swap/trade/types' -import { - getWrapType, - requireAcceptNewTrade, - serializeQueryParams, -} from 'wallet/src/features/transactions/swap/utils' +import { getWrapType, requireAcceptNewTrade, serializeQueryParams } from 'wallet/src/features/transactions/swap/utils' import { WrapType } from 'wallet/src/features/transactions/types' import { mockPool } from 'wallet/src/test/mocks' describe(serializeQueryParams, () => { it('handles the correct types', () => { - expect( - serializeQueryParams({ a: '0x6B175474E89094C44Da98b954EedeAC495271d0F', b: 2, c: false }) - ).toBe('a=0x6B175474E89094C44Da98b954EedeAC495271d0F&b=2&c=false') + expect(serializeQueryParams({ a: '0x6B175474E89094C44Da98b954EedeAC495271d0F', b: 2, c: false })).toBe( + 'a=0x6B175474E89094C44Da98b954EedeAC495271d0F&b=2&c=false', + ) }) it('escapes characters', () => { diff --git a/packages/wallet/src/features/transactions/swap/utils.ts b/packages/wallet/src/features/transactions/swap/utils.ts index f079b669af5..de391ffcf76 100644 --- a/packages/wallet/src/features/transactions/swap/utils.ts +++ b/packages/wallet/src/features/transactions/swap/utils.ts @@ -8,38 +8,33 @@ import { import { FeeOptions } from '@uniswap/v3-sdk' import { BigNumber } from 'ethers' import { AppTFunction } from 'ui/src/i18n/types' +import { AssetType } from 'uniswap/src/entities/assets' import { ElementName, ElementNameType } from 'uniswap/src/features/telemetry/constants' +import { CurrencyField, TransactionState } from 'uniswap/src/features/transactions/transactionState/types' import { UniverseChainId, WalletChainId } from 'uniswap/src/types/chains' import { CurrencyId } from 'uniswap/src/types/currency' import { getSymbolDisplayText } from 'uniswap/src/utils/currency' +import { + areCurrencyIdsEqual, + buildWrappedNativeCurrencyId, + currencyId, + currencyIdToAddress, + currencyIdToChain, +} from 'uniswap/src/utils/currencyId' import { NumberType } from 'utilities/src/format/types' -import { AssetType } from 'wallet/src/entities/assets' import { LocalizationContextState } from 'wallet/src/features/language/LocalizationContext' -import { getClassicQuoteFromResponse } from 'wallet/src/features/transactions/swap/trade/tradingApi/utils' +import { getClassicQuoteFromResponse } from 'wallet/src/features/transactions/swap/trade/api/utils' import { ClassicTrade, Trade } from 'wallet/src/features/transactions/swap/trade/types' -import { isClassic } from 'wallet/src/features/transactions/swap/trade/utils' +import { isClassic, isUniswapX } from 'wallet/src/features/transactions/swap/trade/utils' import { PermitSignatureInfo } from 'wallet/src/features/transactions/swap/usePermit2Signature' -import { - CurrencyField, - TransactionState, -} from 'wallet/src/features/transactions/transactionState/types' import { ExactInputSwapTransactionInfo, ExactOutputSwapTransactionInfo, TransactionType, WrapType, } from 'wallet/src/features/transactions/types' -import { - areCurrencyIdsEqual, - buildWrappedNativeCurrencyId, - currencyId, - currencyIdToAddress, - currencyIdToChain, -} from 'wallet/src/utils/currencyId' -export function serializeQueryParams( - params: Record[0]> -): string { +export function serializeQueryParams(params: Record[0]>): string { const queryString = [] for (const [param, value] of Object.entries(params)) { queryString.push(`${encodeURIComponent(param)}=${encodeURIComponent(value)}`) @@ -49,7 +44,7 @@ export function serializeQueryParams( export function getWrapType( inputCurrency: Currency | null | undefined, - outputCurrency: Currency | null | undefined + outputCurrency: Currency | null | undefined, ): WrapType { if (!inputCurrency || !outputCurrency || inputCurrency.chainId !== outputCurrency.chainId) { return WrapType.NotApplicable @@ -58,15 +53,9 @@ export function getWrapType( const inputChainId = inputCurrency.chainId as WalletChainId const wrappedCurrencyId = buildWrappedNativeCurrencyId(inputChainId) - if ( - inputCurrency.isNative && - areCurrencyIdsEqual(currencyId(outputCurrency), wrappedCurrencyId) - ) { + if (inputCurrency.isNative && areCurrencyIdsEqual(currencyId(outputCurrency), wrappedCurrencyId)) { return WrapType.Wrap - } else if ( - outputCurrency.isNative && - areCurrencyIdsEqual(currencyId(inputCurrency), wrappedCurrencyId) - ) { + } else if (outputCurrency.isNative && areCurrencyIdsEqual(currencyId(inputCurrency), wrappedCurrencyId)) { return WrapType.Unwrap } @@ -77,17 +66,19 @@ export function isWrapAction(wrapType: WrapType): wrapType is WrapType.Unwrap | return wrapType === WrapType.Unwrap || wrapType === WrapType.Wrap } -export function tradeToTransactionInfo( - trade: Trade -): ExactInputSwapTransactionInfo | ExactOutputSwapTransactionInfo { +export function tradeToTransactionInfo(trade: Trade): ExactInputSwapTransactionInfo | ExactOutputSwapTransactionInfo { const slippageTolerancePercent = slippageToleranceToPercent(trade.slippageTolerance) const { quote, slippageTolerance } = trade const { quoteId, gasUseEstimate, routeString } = getClassicQuoteFromResponse(quote) ?? {} + // UniswapX trades wrap native input before swapping + const inputCurrency = isUniswapX(trade) ? trade.inputAmount.currency.wrapped : trade.inputAmount.currency + const outputCurrency = trade.outputAmount.currency + const baseTransactionInfo = { - inputCurrencyId: currencyId(trade.inputAmount.currency), - outputCurrencyId: currencyId(trade.outputAmount.currency), + inputCurrencyId: currencyId(inputCurrency), + outputCurrencyId: currencyId(outputCurrency), slippageTolerance, quoteId, gasUseEstimate, @@ -102,9 +93,7 @@ export function tradeToTransactionInfo( tradeType: TradeType.EXACT_INPUT, inputCurrencyAmountRaw: trade.inputAmount.quotient.toString(), expectedOutputCurrencyAmountRaw: trade.outputAmount.quotient.toString(), - minimumOutputCurrencyAmountRaw: trade - .minimumAmountOut(slippageTolerancePercent) - .quotient.toString(), + minimumOutputCurrencyAmountRaw: trade.minimumAmountOut(slippageTolerancePercent).quotient.toString(), } : { ...baseTransactionInfo, @@ -112,9 +101,7 @@ export function tradeToTransactionInfo( tradeType: TradeType.EXACT_OUTPUT, outputCurrencyAmountRaw: trade.outputAmount.quotient.toString(), expectedInputCurrencyAmountRaw: trade.inputAmount.quotient.toString(), - maximumInputCurrencyAmountRaw: trade - .maximumAmountIn(slippageTolerancePercent) - .quotient.toString(), + maximumInputCurrencyAmountRaw: trade.maximumAmountIn(slippageTolerancePercent).quotient.toString(), } } @@ -136,7 +123,7 @@ export function requireAcceptNewTrade(oldTrade: Maybe, newTrade: Maybe { const price = showInverseRate ? trade.executionPrice.invert() : trade.executionPrice diff --git a/packages/wallet/src/features/transactions/swap/wrapSaga.test.ts b/packages/wallet/src/features/transactions/swap/wrapSaga.test.ts index fe895126e32..279fb5dd00a 100644 --- a/packages/wallet/src/features/transactions/swap/wrapSaga.test.ts +++ b/packages/wallet/src/features/transactions/swap/wrapSaga.test.ts @@ -1,7 +1,7 @@ import { CurrencyAmount } from '@uniswap/sdk-core' import { testSaga } from 'redux-saga-test-plan' +import { NativeCurrency } from 'uniswap/src/features/tokens/NativeCurrency' import { UniverseChainId } from 'uniswap/src/types/chains' -import { NativeCurrency } from 'wallet/src/features/tokens/NativeCurrency' import { sendTransaction } from 'wallet/src/features/transactions/sendTransactionSaga' import { WrapParams, wrap } from 'wallet/src/features/transactions/swap/wrapSaga' import { TransactionType, WrapTransactionInfo } from 'wallet/src/features/transactions/types' @@ -26,10 +26,7 @@ const params: WrapParams = { txId: '1', account, txRequest, - inputCurrencyAmount: CurrencyAmount.fromRawAmount( - NativeCurrency.onChain(UniverseChainId.Mainnet), - '200000' - ), + inputCurrencyAmount: CurrencyAmount.fromRawAmount(NativeCurrency.onChain(UniverseChainId.Mainnet), '200000'), } describe(wrap, () => { @@ -52,7 +49,7 @@ describe(wrap, () => { ...params, inputCurrencyAmount: CurrencyAmount.fromRawAmount( NativeCurrency.onChain(UniverseChainId.Mainnet).wrapped, - '200000' + '200000', ), } testSaga(wrap, unwrapParams) diff --git a/packages/wallet/src/features/transactions/swap/wrapSaga.ts b/packages/wallet/src/features/transactions/swap/wrapSaga.ts index 67d6e8cb90c..9d1b80eee4e 100644 --- a/packages/wallet/src/features/transactions/swap/wrapSaga.ts +++ b/packages/wallet/src/features/transactions/swap/wrapSaga.ts @@ -3,15 +3,11 @@ import { Contract, providers } from 'ethers' import { call } from 'typed-redux-saga' import { Weth } from 'uniswap/src/abis/types' import WETH_ABI from 'uniswap/src/abis/weth.json' +import { getWrappedNativeAddress } from 'uniswap/src/constants/addresses' import { WalletChainId } from 'uniswap/src/types/chains' import { logger } from 'utilities/src/logger/logger' -import { getWrappedNativeAddress } from 'wallet/src/constants/addresses' import { sendTransaction } from 'wallet/src/features/transactions/sendTransactionSaga' -import { - TransactionOptions, - TransactionType, - TransactionTypeInfo, -} from 'wallet/src/features/transactions/types' +import { TransactionOptions, TransactionType, TransactionTypeInfo } from 'wallet/src/features/transactions/types' import { Account } from 'wallet/src/features/wallet/accounts/types' import { createMonitoredSaga } from 'wallet/src/utils/saga' @@ -22,10 +18,7 @@ export type WrapParams = { inputCurrencyAmount: CurrencyAmount } -export async function getWethContract( - chainId: WalletChainId, - provider: providers.Provider -): Promise { +export async function getWethContract(chainId: WalletChainId, provider: providers.Provider): Promise { return new Contract(getWrappedNativeAddress(chainId), WETH_ABI, provider) as Weth } @@ -52,7 +45,7 @@ export function* wrap(params: WrapParams) { request: txRequest, } - yield* call(sendTransaction, { + return yield* call(sendTransaction, { txId, chainId: inputCurrencyAmount.currency.chainId, account, diff --git a/packages/wallet/src/features/transactions/transactionState/transactionState.test.ts b/packages/wallet/src/features/transactions/transactionState/transactionState.test.ts index 799c9da64f7..3efc11190cb 100644 --- a/packages/wallet/src/features/transactions/transactionState/transactionState.test.ts +++ b/packages/wallet/src/features/transactions/transactionState/transactionState.test.ts @@ -1,7 +1,8 @@ import { AnyAction } from '@reduxjs/toolkit' +import { getNativeAddress } from 'uniswap/src/constants/addresses' +import { AssetType, CurrencyAsset } from 'uniswap/src/entities/assets' +import { CurrencyField, TransactionState } from 'uniswap/src/features/transactions/transactionState/types' import { UniverseChainId } from 'uniswap/src/types/chains' -import { getNativeAddress } from 'wallet/src/constants/addresses' -import { AssetType, CurrencyAsset } from 'wallet/src/entities/assets' import { INITIAL_TRANSACTION_STATE, selectCurrency, @@ -10,10 +11,6 @@ import { updateExactAmountFiat, updateExactAmountToken, } from 'wallet/src/features/transactions/transactionState/transactionState' -import { - CurrencyField, - TransactionState, -} from 'wallet/src/features/transactions/transactionState/types' const chainId = UniverseChainId.Goerli const ethAddress = getNativeAddress(UniverseChainId.Goerli) @@ -45,8 +42,8 @@ describe(selectCurrency, () => { selectCurrency({ field: CurrencyField.INPUT, tradeableAsset: ethTradeableAsset, - }) - ) + }), + ), ).toEqual({ ...previousState, [CurrencyField.INPUT]: ethTradeableAsset, @@ -61,8 +58,8 @@ describe(selectCurrency, () => { selectCurrency({ field: CurrencyField.OUTPUT, tradeableAsset: daiTradeableAsset, - }) - ) + }), + ), ).toEqual({ ...previousState, [CurrencyField.OUTPUT]: daiTradeableAsset, @@ -80,8 +77,8 @@ describe(selectCurrency, () => { selectCurrency({ field: CurrencyField.OUTPUT, tradeableAsset: ethTradeableAsset, - }) - ) + }), + ), ).toEqual({ ...previousState, exactCurrencyField: CurrencyField.OUTPUT, @@ -102,8 +99,8 @@ describe(selectCurrency, () => { selectCurrency({ field: CurrencyField.OUTPUT, tradeableAsset: ethTradeableAsset, - }) - ) + }), + ), ).toEqual({ ...previousState, exactCurrencyField: CurrencyField.OUTPUT, @@ -129,8 +126,8 @@ describe(selectCurrency, () => { chainId: otherChainId, type: AssetType.Currency, }, - }) - ) + }), + ), ).toEqual({ ...previousState, exactCurrencyField: CurrencyField.OUTPUT, @@ -171,10 +168,7 @@ describe(updateExactAmountToken, () => { const previousState = { ...testInitialState } expect( - transactionStateReducer( - previousState, - updateExactAmountToken({ field: CurrencyField.INPUT, amount: '1' }) - ) + transactionStateReducer(previousState, updateExactAmountToken({ field: CurrencyField.INPUT, amount: '1' })), ).toEqual({ ...previousState, exactCurrencyField: CurrencyField.INPUT, @@ -186,10 +180,7 @@ describe(updateExactAmountToken, () => { const previousState = { ...testInitialState } expect( - transactionStateReducer( - previousState, - updateExactAmountFiat({ field: CurrencyField.INPUT, amount: '1' }) - ) + transactionStateReducer(previousState, updateExactAmountFiat({ field: CurrencyField.INPUT, amount: '1' })), ).toEqual({ ...previousState, exactCurrencyField: CurrencyField.INPUT, @@ -201,10 +192,7 @@ describe(updateExactAmountToken, () => { const previousState = { ...testInitialState } expect( - transactionStateReducer( - previousState, - updateExactAmountToken({ field: CurrencyField.OUTPUT, amount: '5' }) - ) + transactionStateReducer(previousState, updateExactAmountToken({ field: CurrencyField.OUTPUT, amount: '5' })), ).toEqual({ ...previousState, exactCurrencyField: CurrencyField.OUTPUT, diff --git a/packages/wallet/src/features/transactions/transactionState/transactionState.ts b/packages/wallet/src/features/transactions/transactionState/transactionState.ts index fbbfe0e3908..2a59a0582d0 100644 --- a/packages/wallet/src/features/transactions/transactionState/transactionState.ts +++ b/packages/wallet/src/features/transactions/transactionState/transactionState.ts @@ -1,12 +1,9 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { shallowEqual } from 'react-redux' +import { getNativeAddress } from 'uniswap/src/constants/addresses' +import { AssetType, TradeableAsset } from 'uniswap/src/entities/assets' +import { CurrencyField, TransactionState } from 'uniswap/src/features/transactions/transactionState/types' import { UniverseChainId } from 'uniswap/src/types/chains' -import { getNativeAddress } from 'wallet/src/constants/addresses' -import { AssetType, TradeableAsset } from 'wallet/src/entities/assets' -import { - CurrencyField, - TransactionState, -} from 'wallet/src/features/transactions/transactionState/types' const ETH_TRADEABLE_ASSET: TradeableAsset = { address: getNativeAddress(UniverseChainId.Mainnet), @@ -37,13 +34,9 @@ const slice = createSlice({ * If input/output currencies would be the same, it swaps the order * If network would change, unsets the dependent field */ - selectCurrency: ( - state, - action: PayloadAction<{ field: CurrencyField; tradeableAsset: TradeableAsset }> - ) => { + selectCurrency: (state, action: PayloadAction<{ field: CurrencyField; tradeableAsset: TradeableAsset }>) => { const { field, tradeableAsset } = action.payload - const nonExactField = - field === CurrencyField.INPUT ? CurrencyField.OUTPUT : CurrencyField.INPUT + const nonExactField = field === CurrencyField.INPUT ? CurrencyField.OUTPUT : CurrencyField.INPUT // swap order if tokens are the same if (shallowEqual(tradeableAsset, state[nonExactField])) { @@ -62,9 +55,7 @@ const slice = createSlice({ /** Switches input and output currencies */ switchCurrencySides: (state) => { state.exactCurrencyField = - state.exactCurrencyField === CurrencyField.INPUT - ? CurrencyField.OUTPUT - : CurrencyField.INPUT + state.exactCurrencyField === CurrencyField.INPUT ? CurrencyField.OUTPUT : CurrencyField.INPUT state.focusOnCurrencyField = state.exactCurrencyField ;[state[CurrencyField.INPUT], state[CurrencyField.OUTPUT]] = [ state[CurrencyField.OUTPUT], @@ -77,7 +68,7 @@ const slice = createSlice({ action: PayloadAction<{ field?: CurrencyField amount: string - }> + }>, ) => { const { field, amount } = action.payload if (field) { @@ -91,7 +82,7 @@ const slice = createSlice({ action: PayloadAction<{ field?: CurrencyField amount: string - }> + }>, ) => { const { field, amount } = action.payload if (field) { diff --git a/packages/wallet/src/features/transactions/transactionWatcherSaga.test.ts b/packages/wallet/src/features/transactions/transactionWatcherSaga.test.ts index cbb36c68bb5..2fc88d96ece 100644 --- a/packages/wallet/src/features/transactions/transactionWatcherSaga.test.ts +++ b/packages/wallet/src/features/transactions/transactionWatcherSaga.test.ts @@ -2,11 +2,11 @@ import { faker } from '@faker-js/faker' import { expectSaga } from 'redux-saga-test-plan' import * as matchers from 'redux-saga-test-plan/matchers' import { call, delay } from 'redux-saga/effects' +import { PollingInterval } from 'uniswap/src/constants/misc' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { UniverseChainId } from 'uniswap/src/types/chains' import { sleep } from 'utilities/src/time/timing' -import { PollingInterval } from 'wallet/src/constants/misc' -import { fetchMoonpayTransaction } from 'wallet/src/features/fiatOnRamp/api' +import { fetchFiatOnRampTransaction } from 'wallet/src/features/fiatOnRamp/api' import { attemptCancelTransaction } from 'wallet/src/features/transactions/cancelTransactionSaga' import { addTransaction, @@ -42,12 +42,15 @@ const { const { mockProvider, mockProviderManager } = getTxProvidersMocks(ethersTxReceipt) +const ACTIVE_ACCOUNT_ADDRESS = '0x000000000000000000000000000000000000000001' + describe(transactionWatcher, () => { it('Triggers watchers successfully', () => { const approveTxDetailsPending = transactionDetails({ typeInfo: approveTransactionInfo(), status: TransactionStatus.Pending, hash: faker.datatype.uuid(), + from: ACTIVE_ACCOUNT_ADDRESS, }) return expectSaga(transactionWatcher, { apolloClient: mockApolloClient }) @@ -62,6 +65,7 @@ describe(transactionWatcher, () => { }, }, }, + wallet: { activeAccountAddress: ACTIVE_ACCOUNT_ADDRESS }, }) .provide([ [call(getProvider, UniverseChainId.Mainnet), mockProvider], @@ -104,6 +108,9 @@ describe(watchTransaction, () => { transaction: txDetailsPending, apolloClient: mockApolloClient, }) + .withState({ + wallet: { activeAccountAddress: ACTIVE_ACCOUNT_ADDRESS }, + }) .provide([[call(getProvider, chainId), receiptProvider]]) .put(finalizeTransaction(finalizedTxAction.payload)) .silentRun() @@ -121,12 +128,15 @@ describe(watchTransaction, () => { transaction: txDetailsPending, apolloClient: mockApolloClient, }) + .withState({ + wallet: { activeAccountAddress: ACTIVE_ACCOUNT_ADDRESS }, + }) .provide([ [call(getProvider, chainId), receiptProvider], - [call(attemptCancelTransaction, txDetailsPending), true], + [call(attemptCancelTransaction, txDetailsPending, cancelRequest), true], ]) .dispatch(cancelTransaction({ chainId, id, address: from, cancelRequest })) - .call(attemptCancelTransaction, txDetailsPending) + .call(attemptCancelTransaction, txDetailsPending, cancelRequest) .silentRun() }) @@ -142,6 +152,9 @@ describe(watchTransaction, () => { transaction: txDetailsPending, apolloClient: mockApolloClient, }) + .withState({ + wallet: { activeAccountAddress: ACTIVE_ACCOUNT_ADDRESS }, + }) .provide([ [call(getProvider, chainId), receiptProvider], [call(waitForTxnInvalidated, chainId, id, options.request.nonce), true], @@ -158,7 +171,7 @@ describe(watchFiatOnRampTransaction, () => { return ( expectSaga(watchFiatOnRampTransaction, txDetailsPending) .provide([ - [call(fetchMoonpayTransaction, txDetailsPending), staleTx], + [call(fetchFiatOnRampTransaction, txDetailsPending, false), staleTx], [matchers.call.fn(sendAnalyticsEvent), undefined], ]) .put( @@ -166,7 +179,7 @@ describe(watchFiatOnRampTransaction, () => { address: staleTx.from, id: staleTx.id, chainId: staleTx.chainId, - }) + }), ) // watcher should stop tracking .not.call.fn(sleep) @@ -185,7 +198,7 @@ describe(watchFiatOnRampTransaction, () => { .provide([ { call(effect): TransactionDetails | undefined { - if (effect.fn === fetchMoonpayTransaction) { + if (effect.fn === fetchFiatOnRampTransaction) { switch (fetchCalledCount++) { case 0: case 1: @@ -197,9 +210,9 @@ describe(watchFiatOnRampTransaction, () => { } }, }, - [delay(PollingInterval.Normal), Promise.resolve(() => undefined)], + [delay(PollingInterval.Fast), Promise.resolve(() => undefined)], ]) - .delay(PollingInterval.Normal) + .delay(PollingInterval.Fast) // only called once .put(transactionActions.upsertFiatOnRampTransaction(confirmedTx)) .silentRun() @@ -217,7 +230,7 @@ describe(watchFiatOnRampTransaction, () => { .provide([ { call(effect): TransactionDetails | undefined { - if (effect.fn === fetchMoonpayTransaction) { + if (effect.fn === fetchFiatOnRampTransaction) { switch (fetchCalledCount++) { case 0: case 1: @@ -243,7 +256,7 @@ describe(watchFiatOnRampTransaction, () => { const confirmedTx = { ...txDetailsPending, status: TransactionStatus.Success } return expectSaga(watchFiatOnRampTransaction, txDetailsPending) .provide([ - [call(fetchMoonpayTransaction, txDetailsPending), confirmedTx], + [call(fetchFiatOnRampTransaction, txDetailsPending, false), confirmedTx], [matchers.call.fn(sendAnalyticsEvent), undefined], ]) .put(transactionActions.upsertFiatOnRampTransaction(confirmedTx)) diff --git a/packages/wallet/src/features/transactions/transactionWatcherSaga.ts b/packages/wallet/src/features/transactions/transactionWatcherSaga.ts index 7f879b2eab3..92dcb3fe420 100644 --- a/packages/wallet/src/features/transactions/transactionWatcherSaga.ts +++ b/packages/wallet/src/features/transactions/transactionWatcherSaga.ts @@ -3,8 +3,9 @@ import { ApolloClient, NormalizedCacheObject } from '@apollo/client' import { SwapEventName } from '@uniswap/analytics-events' import { TradeType } from '@uniswap/sdk-core' import { BigNumberish, providers } from 'ethers' -import { call, delay, fork, put, race, select, take, takeEvery } from 'typed-redux-saga' +import { call, delay, fork, put, race, take, takeEvery } from 'typed-redux-saga' import { isWeb } from 'ui/src' +import { PollingInterval } from 'uniswap/src/constants/misc' import { FeatureFlags, getFeatureFlagName } from 'uniswap/src/features/gating/flags' import { Statsig } from 'uniswap/src/features/gating/sdk/statsig' import { @@ -17,26 +18,17 @@ import { sendAnalyticsEvent, sendAppsFlyerEvent } from 'uniswap/src/features/tel import i18n from 'uniswap/src/i18n/i18n' import { WalletChainId } from 'uniswap/src/types/chains' import { logger } from 'utilities/src/logger/logger' -import { PollingInterval } from 'wallet/src/constants/misc' import { selectExtensionBetaFeedbackState } from 'wallet/src/features/behaviorHistory/selectors' -import { - ExtensionBetaFeedbackState, - setExtensionBetaFeedbackState, -} from 'wallet/src/features/behaviorHistory/slice' -import { - fetchFiatOnRampTransaction, - fetchMoonpayTransaction, -} from 'wallet/src/features/fiatOnRamp/api' +import { ExtensionBetaFeedbackState, setExtensionBetaFeedbackState } from 'wallet/src/features/behaviorHistory/slice' +import { fetchFiatOnRampTransaction } from 'wallet/src/features/fiatOnRamp/api' import { FiatOnRampTransactionDetails } from 'wallet/src/features/fiatOnRamp/types' import { pushNotification, setNotificationStatus } from 'wallet/src/features/notifications/slice' import { AppNotificationType } from 'wallet/src/features/notifications/types' import { attemptCancelTransaction } from 'wallet/src/features/transactions/cancelTransactionSaga' +import { OrderWatcher } from 'wallet/src/features/transactions/orderWatcherSaga' import { refetchGQLQueries } from 'wallet/src/features/transactions/refetchGQLQueriesSaga' import { attemptReplaceTransaction } from 'wallet/src/features/transactions/replaceTransactionSaga' -import { - selectIncompleteTransactions, - selectSwapTransactionsCount, -} from 'wallet/src/features/transactions/selectors' +import { selectIncompleteTransactions, selectSwapTransactionsCount } from 'wallet/src/features/transactions/selectors' import { addTransaction, cancelTransaction, @@ -46,38 +38,42 @@ import { updateTransaction, upsertFiatOnRampTransaction, } from 'wallet/src/features/transactions/slice' -import { isUniswapX } from 'wallet/src/features/transactions/swap/trade/utils' +import { isClassic, isUniswapX } from 'wallet/src/features/transactions/swap/trade/utils' import { BaseSwapTransactionInfo, - ClassicTransactionDetails, + FinalizedTransactionDetails, + QueuedOrderStatus, SendTokenTransactionInfo, TransactionDetails, - TransactionReceipt, TransactionStatus, TransactionType, + isFinalizedTx, } from 'wallet/src/features/transactions/types' -import { getFinalizedTransactionStatus } from 'wallet/src/features/transactions/utils' -import { getProvider, getSignerManager } from 'wallet/src/features/wallet/context' -import { selectActiveAccount } from 'wallet/src/features/wallet/selectors' +import { getFinalizedTransactionStatus, receiptFromEthersReceipt } from 'wallet/src/features/transactions/utils' +import { getProvider } from 'wallet/src/features/wallet/context' import { appSelect } from 'wallet/src/state' -export function* transactionWatcher({ - apolloClient, -}: { - apolloClient: ApolloClient -}) { +export function* transactionWatcher({ apolloClient }: { apolloClient: ApolloClient }) { logger.debug('transactionWatcherSaga', 'transactionWatcher', 'Starting tx watcher') yield* fork(watchForFinalizedTransactions) + // Start the order watcher to allow off-chain order updates to propagate to watchTransaction + yield* fork(OrderWatcher.initialize) + // First, fork off watchers for any incomplete txs that are already in store // This allows us to detect completions if a user closed the app before a tx finished const incompleteTransactions = yield* appSelect(selectIncompleteTransactions) for (const transaction of incompleteTransactions) { if (transaction.typeInfo.type === TransactionType.FiatPurchase) { yield* fork(watchFiatOnRampTransaction, transaction as FiatOnRampTransactionDetails) - } else if (isUniswapX(transaction)) { - // TODO(WEB-4296): Add watcher for UniswapX transactions } else { + // If the transaction was a queued UniswapX order that never became submitted, update UI to show failure + if (isUniswapX(transaction) && transaction.queueStatus === QueuedOrderStatus.Waiting) { + const updatedOrder = { ...transaction, queueStatus: QueuedOrderStatus.AppClosed } + yield* put(transactionActions.updateTransaction(updatedOrder)) + continue + } + yield* fork(watchTransaction, { transaction, apolloClient }) } } @@ -91,8 +87,6 @@ export function* transactionWatcher({ try { if (transaction.typeInfo.type === TransactionType.FiatPurchase) { yield* fork(watchFiatOnRampTransaction, transaction as FiatOnRampTransactionDetails) - } else if (isUniswapX(transaction)) { - // TODO(WEB-4296): Add watcher for UniswapX transactions } else { yield* fork(watchTransaction, { transaction, apolloClient }) } @@ -110,52 +104,20 @@ export function* transactionWatcher({ type: AppNotificationType.Error, address: transaction.from, errorMessage: i18n.t('transaction.watcher.error.status'), - }) + }), ) } } } -export function* fetchUpdatedFiatOnRampTransaction( - transaction: FiatOnRampTransactionDetails, - forceFetch: boolean -) { - const activeAccount = yield* select(selectActiveAccount) - if (!activeAccount) { - return - } - const signerManager = yield* call(getSignerManager) - - return yield* call( - fetchFiatOnRampTransaction, - /** previousTransactionDetails= */ transaction, - forceFetch, - activeAccount, - signerManager - ) -} - -export function* fetchUpdatedMoonpayTransaction(transaction: FiatOnRampTransactionDetails) { - return yield* call(fetchMoonpayTransaction, /** previousTransactionDetails= */ transaction) +export function* fetchUpdatedFiatOnRampTransaction(transaction: FiatOnRampTransactionDetails, forceFetch: boolean) { + return yield* call(fetchFiatOnRampTransaction, transaction, forceFetch) } export function* watchFiatOnRampTransaction(transaction: FiatOnRampTransactionDetails) { - // we want to re-fetch this every time we've added a transaction, - // as this feature flag could be changed after the app has started - const useOldMoonpayIntegration = !Statsig.checkGate( - getFeatureFlagName(FeatureFlags.ForAggregator) - ) - const { id } = transaction - logger.debug( - 'transactionWatcherSaga', - 'watchFiatOnRampTransaction', - 'Watching for updates for fiat onramp tx:', - id, - 'useOldMoonpayIntegration:', - useOldMoonpayIntegration - ) + logger.debug('transactionWatcherSaga', 'watchFiatOnRampTransaction', 'Watching for updates for fiat onramp tx:', id) let latestStatus = transaction.status let syncedWithBackend = transaction.typeInfo.syncedWithBackend @@ -163,9 +125,7 @@ export function* watchFiatOnRampTransaction(transaction: FiatOnRampTransactionDe try { while (true) { - const updatedTransaction = yield* useOldMoonpayIntegration - ? fetchUpdatedMoonpayTransaction(transaction) - : fetchUpdatedFiatOnRampTransaction(transaction, forceFetch) + const updatedTransaction = yield* fetchUpdatedFiatOnRampTransaction(transaction, forceFetch) forceFetch = false // We've got an invalid response from backend @@ -180,11 +140,10 @@ export function* watchFiatOnRampTransaction(transaction: FiatOnRampTransactionDe logger.debug( 'transactionWatcherSaga', 'watchFiatOnRampTransaction', - `Updating transaction with id ${id} from status ${transaction.status} to ${updatedTransaction.status}` + `Updating transaction with id ${id} from status ${transaction.status} to ${updatedTransaction.status}`, ) - const isTransfer = - updatedTransaction.typeInfo.inputSymbol === updatedTransaction.typeInfo.outputSymbol + const isTransfer = updatedTransaction.typeInfo.inputSymbol === updatedTransaction.typeInfo.outputSymbol if (updatedTransaction.typeInfo.serviceProvider) { yield* call( sendAnalyticsEvent, @@ -195,7 +154,7 @@ export function* watchFiatOnRampTransaction(transaction: FiatOnRampTransactionDe externalTransactionId: updatedTransaction.id, status: updatedTransaction.status, serviceProvider: updatedTransaction.typeInfo.serviceProvider, - } + }, ) } } @@ -227,7 +186,7 @@ export function* watchFiatOnRampTransaction(transaction: FiatOnRampTransactionDe // try again after a waiting period or when we've come back WebView const raceResult = yield* race({ forceFetch: take(forceFetchFiatOnRampTransactions), - timeout: delay(useOldMoonpayIntegration ? PollingInterval.Normal : PollingInterval.Fast), + timeout: delay(PollingInterval.Fast), }) if (raceResult.forceFetch) { @@ -245,37 +204,45 @@ export function* watchTransaction({ transaction, apolloClient, }: { - transaction: ClassicTransactionDetails + transaction: TransactionDetails apolloClient: ApolloClient }): Generator { - const { chainId, id, hash, options } = transaction + const { chainId, id, hash } = transaction logger.debug('transactionWatcherSaga', 'watchTransaction', 'Watching for updates for tx:', hash) const provider = yield* call(getProvider, chainId) - if (!hash) { - logger.error(new Error('Watching for tx with no hash'), { - tags: { - file: 'transactionWatcherSaga', - function: 'watchTransaction', - }, - extra: { transaction }, - }) - return - } - - const { receipt, cancel, replace, invalidated } = yield* race({ - receipt: call(waitForReceipt, hash, provider), + const nonce = isUniswapX(transaction) ? undefined : transaction.options.request.nonce + const { updatedTransaction, cancel, replace, invalidated } = yield* race({ + updatedTransaction: call(waitForRemoteUpdate, transaction, provider), cancel: call(waitForCancellation, chainId, id), replace: call(waitForReplacement, chainId, id), - invalidated: call(waitForTxnInvalidated, chainId, id, options.request.nonce), + invalidated: call(waitForTxnInvalidated, chainId, id, nonce), }) + // `cancel` and `updatedTransaction` conditions apply to both Classic and UniswapX transactions if (cancel) { // reset watcher for the current txn, as it can still be mined (or invalidated by the new txn) yield* fork(watchTransaction, { transaction, apolloClient }) // Cancel the current txn, which submits a new txn on chain and monitored in state - yield* call(attemptCancelTransaction, transaction) + yield* call(attemptCancelTransaction, transaction, cancel) + return + } + + if (updatedTransaction) { + if (isFinalizedTx(updatedTransaction)) { + // Update the store with tx receipt details + yield* call(finalizeTransaction, { transaction: updatedTransaction, apolloClient }) + return + } else { + yield* put(transactionActions.updateTransaction(updatedTransaction)) + // reset watcher for the current txn, as new statuses can be received for the pending order + yield* fork(watchTransaction, { transaction: updatedTransaction, apolloClient }) + } + } + + // `replace` and `invalidated` conditions do not apply to UniswapX orders + if (isUniswapX(transaction)) { return } @@ -295,19 +262,16 @@ export function* watchTransaction({ type: AppNotificationType.Error, address: transaction.from, errorMessage: i18n.t('transaction.watcher.error.cancel'), - }) + }), ) } return } - - // Update the store with tx receipt details - yield* call(finalizeTransaction, { transaction, ethersReceipt: receipt, apolloClient }) } export async function waitForReceipt( hash: string, - provider: providers.Provider + provider: providers.Provider, ): Promise { const txReceipt = await provider.waitForTransaction(hash) if (txReceipt) { @@ -316,11 +280,49 @@ export async function waitForReceipt( return txReceipt } +function* waitForRemoteUpdate(transaction: TransactionDetails, provider: providers.Provider) { + let hash = transaction.hash + let status = transaction.status + + // For UniswapX orders, we need to wait for the order to be filled before we can get the hash + if (isUniswapX(transaction) && transaction.orderHash && transaction.queueStatus) { + const updatedOrder = yield* call(OrderWatcher.waitForOrderStatus, transaction.orderHash, transaction.queueStatus) + hash = updatedOrder.hash + status = updatedOrder.status + + // Return early if a new status is received, but no hash is provided (meaning the order is not filled) + if (!updatedOrder.hash) { + return updatedOrder + } + } + + // At this point, the tx should either be a classic tx or a filled order, both of which have hashes + if (!hash) { + logger.error(new Error('Watching for tx with no hash'), { + tags: { + file: 'transactionWatcherSaga', + function: 'watchTransaction', + }, + extra: { transaction }, + }) + return + } + + const ethersReceipt = yield* call(waitForReceipt, hash, provider) + const receipt = receiptFromEthersReceipt(ethersReceipt) + + // Classic transaction status is based on receipt, while UniswapX status is based backend response. + if (isClassic(transaction)) { + status = getFinalizedTransactionStatus(transaction.status, ethersReceipt?.status) + } + return { ...transaction, status, receipt, hash } +} + function* waitForCancellation(chainId: WalletChainId, id: string) { while (true) { const { payload } = yield* take>(cancelTransaction.type) if (payload.cancelRequest && payload.chainId === chainId && payload.id === id) { - return true + return payload.cancelRequest } } } @@ -337,14 +339,10 @@ function* waitForReplacement(chainId: WalletChainId, id: string) { * Monitor for transactions with the same nonce as the current transaction. If any duplicate is finalized, it means * the current transaction has been invalidated and wont be picked up on chain. */ -export function* waitForTxnInvalidated( - chainId: WalletChainId, - id: string, - nonce: BigNumberish | undefined -) { +export function* waitForTxnInvalidated(chainId: WalletChainId, id: string, nonce: BigNumberish | undefined) { while (true) { const { payload } = yield* take>( - transactionActions.finalizeTransaction.type + transactionActions.finalizeTransaction.type, ) if ( @@ -361,9 +359,7 @@ export function* waitForTxnInvalidated( /** * Send analytics events for finalized transactions */ -export function logTransactionEvent( - actionData: ReturnType -): void { +export function logTransactionEvent(actionData: ReturnType): void { const { payload } = actionData const { hash, chainId, addedTime, from, typeInfo, receipt, status } = payload const { gasUsed, effectiveGasPrice, confirmedTime } = receipt ?? {} @@ -382,13 +378,10 @@ export function logTransactionEvent( protocol, transactedUSDValue, } = typeInfo as BaseSwapTransactionInfo - const eventName = - status === TransactionStatus.Success - ? SwapEventName.SWAP_TRANSACTION_COMPLETED - : SwapEventName.SWAP_TRANSACTION_FAILED - sendAnalyticsEvent(eventName, { - address: from, + + const baseProperties = { hash, + address: from, chain_id: chainId, added_time: addedTime, confirmed_time: confirmedTime, @@ -404,7 +397,48 @@ export function logTransactionEvent( submitViaPrivateRpc: isUniswapX(payload) ? false : payload.options.submitViaPrivateRpc, protocol, transactedUSDValue, - }) + } + + if (isUniswapX(payload)) { + const { orderHash, routing } = payload + // All local uniswapx swaps should be tracked in redux with an orderHash . + if (!orderHash) { + logger.error(new Error('Attempting to log uniswapx swap event without a orderHash'), { + tags: { + file: 'transactionWatcherSaga', + function: 'logTransactionEvent', + }, + extra: { payload }, + }) + return + } + if (status === TransactionStatus.Success) { + const properties = { ...baseProperties, routing, order_hash: orderHash, hash } + sendAnalyticsEvent(SwapEventName.SWAP_TRANSACTION_COMPLETED, properties) + } else { + const properties = { ...baseProperties, routing, order_hash: orderHash } + sendAnalyticsEvent(SwapEventName.SWAP_TRANSACTION_FAILED, properties) + } + } else { + const { routing } = payload + // All classic swaps should be tracked in redux with a tx hash. + if (!hash) { + logger.error(new Error('Attempting to log swap event without a hash'), { + tags: { + file: 'transactionWatcherSaga', + function: 'logTransactionEvent', + }, + extra: { payload }, + }) + return + } + const properties = { ...baseProperties, routing, hash } + if (status === TransactionStatus.Success) { + sendAnalyticsEvent(SwapEventName.SWAP_TRANSACTION_COMPLETED, properties) + } else { + sendAnalyticsEvent(SwapEventName.SWAP_TRANSACTION_FAILED, properties) + } + } } // Log metrics for confirmed transfers @@ -421,7 +455,7 @@ export function logTransactionEvent( function* watchForFinalizedTransactions() { const state = yield* appSelect(selectExtensionBetaFeedbackState) const extensionBetaFeedbackPromptEnabled = Statsig.checkGate( - getFeatureFlagName(FeatureFlags.ExtensionBetaFeedbackPrompt) + getFeatureFlagName(FeatureFlags.ExtensionBetaFeedbackPrompt), ) if (isWeb && extensionBetaFeedbackPromptEnabled && state === undefined) { @@ -429,15 +463,13 @@ function* watchForFinalizedTransactions() { } } -function* maybeLaunchFeedbackModal( - actionData: ReturnType -) { +function* maybeLaunchFeedbackModal(actionData: ReturnType) { const { payload } = actionData const { typeInfo, status } = payload const { type } = typeInfo const state = yield* appSelect(selectExtensionBetaFeedbackState) const extensionBetaFeedbackPromptEnabled = Statsig.checkGate( - getFeatureFlagName(FeatureFlags.ExtensionBetaFeedbackPrompt) + getFeatureFlagName(FeatureFlags.ExtensionBetaFeedbackPrompt), ) if ( @@ -451,58 +483,14 @@ function* maybeLaunchFeedbackModal( } } -type StatusOverride = - | TransactionStatus.Success - | TransactionStatus.Failed - | TransactionStatus.Canceled - -function* finalizeTransaction({ +export function* finalizeTransaction({ apolloClient, - ethersReceipt, - statusOverride, transaction, }: { apolloClient: ApolloClient - ethersReceipt?: providers.TransactionReceipt | null - statusOverride?: StatusOverride - transaction: TransactionDetails + transaction: FinalizedTransactionDetails }) { - const status = - statusOverride ?? getFinalizedTransactionStatus(transaction.status, ethersReceipt?.status) - - const receipt: TransactionReceipt | undefined = ethersReceipt - ? { - blockHash: ethersReceipt.blockHash, - blockNumber: ethersReceipt.blockNumber, - transactionIndex: ethersReceipt.transactionIndex, - confirmations: ethersReceipt.confirmations, - confirmedTime: Date.now(), - gasUsed: ethersReceipt.gasUsed?.toNumber(), - effectiveGasPrice: ethersReceipt.effectiveGasPrice?.toNumber(), - } - : undefined - - const hash = transaction.hash - - if (!hash) { - logger.error(new Error('Attempting to finalize tx without a hash'), { - tags: { - file: 'transactionWatcherSaga', - function: 'finalizeTransaction', - }, - extra: { transaction }, - }) - return - } - - yield* put( - transactionActions.finalizeTransaction({ - ...transaction, - hash, - status, - receipt, - }) - ) + yield* put(transactionActions.finalizeTransaction(transaction)) // Flip status to true so we can render Notification badge on home yield* put(setNotificationStatus({ address: transaction.from, hasNotifications: true })) @@ -530,6 +518,11 @@ export function* deleteTransaction(transaction: TransactionDetails) { address: transaction.from, id: transaction.id, chainId: transaction.chainId, - }) + }), ) } + +export function* watchTransactionEvents() { + // Watch for finalized transactions to send analytics events + yield* takeEvery(transactionActions.finalizeTransaction.type, logTransactionEvent) +} diff --git a/packages/wallet/src/features/transactions/transfer/TokenSelectorPanel.tsx b/packages/wallet/src/features/transactions/transfer/TokenSelectorPanel.tsx index 5820bfedde8..b2b222468bd 100644 --- a/packages/wallet/src/features/transactions/transfer/TokenSelectorPanel.tsx +++ b/packages/wallet/src/features/transactions/transfer/TokenSelectorPanel.tsx @@ -1,20 +1,31 @@ import { Currency, CurrencyAmount } from '@uniswap/sdk-core' import { useTranslation } from 'react-i18next' +import { Keyboard, LayoutAnimation } from 'react-native' import { Flex, Text, TouchableArea } from 'ui/src' import { RotatableChevron } from 'ui/src/components/icons' import { iconSizes } from 'ui/src/theme' import { CurrencyLogo } from 'uniswap/src/components/CurrencyLogo/CurrencyLogo' +import { TokenSelector, TokenSelectorVariation } from 'uniswap/src/components/TokenSelector/TokenSelector' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' +import { SearchContext } from 'uniswap/src/features/search/SearchContext' +import { CurrencyField } from 'uniswap/src/features/transactions/transactionState/types' +import { TokenSelectorFlow } from 'uniswap/src/features/transactions/transfer/types' import { NumberType } from 'utilities/src/format/types' import { - TokenSelector, - TokenSelectorVariation, -} from 'wallet/src/components/TokenSelector/TokenSelector' + useAddToSearchHistory, + useCommonTokensOptions, + useFavoriteTokensOptions, + useFilterCallbacks, + usePopularTokensOptions, + usePortfolioTokenOptions, + useTokenSectionsForEmptySearch, + useTokenSectionsForSearchResults, +} from 'wallet/src/components/TokenSelector/hooks' import { MaxAmountButton } from 'wallet/src/components/input/MaxAmountButton' +import { useWalletNavigation } from 'wallet/src/contexts/WalletNavigationContext' import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' -import { SearchContext } from 'wallet/src/features/search/SearchContext' -import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' -import { TokenSelectorFlow } from 'wallet/src/features/transactions/transfer/types' +import { useTokenWarningDismissed } from 'wallet/src/features/tokens/safetyHooks' +import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' interface TokenSelectorPanelProps { currencyInfo: Maybe @@ -38,7 +49,11 @@ export function TokenSelectorPanel({ showTokenSelector, }: TokenSelectorPanelProps): JSX.Element { const { t } = useTranslation() - const { formatCurrencyAmount } = useLocalizationContext() + const { formatNumberOrString, convertFiatAmountFormatted, formatCurrencyAmount } = useLocalizationContext() + const { navigateToBuyOrReceiveWithEmptyWallet } = useWalletNavigation() + const { registerSearch } = useAddToSearchHistory() + + const activeAccountAddress = useActiveAccountAddressWithThrow() const showMaxButton = currencyBalance && !currencyBalance.equalTo(0) const formattedCurrencyBalance = formatCurrencyAmount({ @@ -50,11 +65,26 @@ export function TokenSelectorPanel({ return ( Keyboard.dismiss()} + onPressAnimation={() => LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)} onSelectCurrency={onSelectCurrency} /> @@ -89,12 +119,7 @@ export function TokenSelectorPanel({ onSetMax={onSetMax} /> )} - + diff --git a/packages/wallet/src/features/transactions/transfer/TransferAmountInput.tsx b/packages/wallet/src/features/transactions/transfer/TransferAmountInput.tsx index 9ec8deeabdf..211a9458092 100644 --- a/packages/wallet/src/features/transactions/transfer/TransferAmountInput.tsx +++ b/packages/wallet/src/features/transactions/transfer/TransferAmountInput.tsx @@ -5,12 +5,12 @@ import { Flex, FlexProps, Text, TouchableArea } from 'ui/src' import { ArrowUpDown } from 'ui/src/components/icons' import { fonts } from 'ui/src/theme' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' +import { CurrencyField } from 'uniswap/src/features/transactions/transactionState/types' import { AmountInput } from 'wallet/src/components/input/AmountInput' import { useAppFiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks' import { WarningLabel } from 'wallet/src/features/transactions/WarningModal/types' import { ParsedWarnings } from 'wallet/src/features/transactions/hooks/useParsedTransactionWarnings' import { useTokenAndFiatDisplayAmounts } from 'wallet/src/features/transactions/hooks/useTokenAndFiatDisplayAmounts' -import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' import { useDynamicFontSizing } from 'wallet/src/utils/useDynamicFontSizing' type TransferAmountInputProps = { @@ -51,14 +51,14 @@ export function TransferAmountInput({ selection: { start, end }, }, }: NativeSyntheticEvent) => selectionChange?.(start, end), - [selectionChange] + [selectionChange], ) const onChangeText = useCallback( (newValue: string) => { onSetExactAmount(CurrencyField.INPUT, newValue, isFiatInput) }, - [onSetExactAmount, isFiatInput] + [onSetExactAmount, isFiatInput], ) // Display the fiat equivalent amount if the input is in fiat mode, otherwise display the token amount if fiat mode @@ -77,7 +77,7 @@ export function TransferAmountInput({ const { onLayout, fontSize, onSetFontSize } = useDynamicFontSizing( MAX_CHAR_PIXEL_WIDTH, MAX_INPUT_FONT_SIZE, - MIN_INPUT_FONT_SIZE + MIN_INPUT_FONT_SIZE, ) const [containerWidth, setContainerWidth] = useState(0) @@ -102,11 +102,11 @@ export function TransferAmountInput({ const subTextValue = warning ? warning.warning.title : !tokenOrFiatEquivalentAmount - ? // Override empty string from useTokenAndFiatDisplayAmounts to keep UI placeholder text consistent - isFiatInput - ? '0' - : '$0' - : tokenOrFiatEquivalentAmount + ? // Override empty string from useTokenAndFiatDisplayAmounts to keep UI placeholder text consistent + isFiatInput + ? '0' + : '$0' + : tokenOrFiatEquivalentAmount const subTextValueColor = warning ? '$statusCritical' : '$neutral2' const inputColor = !value ? '$neutral3' : '$neutral1' @@ -121,20 +121,11 @@ export function TransferAmountInput({ // Avoid case where onSetFontSize is called before onLayout, resulting in incorrect sizing if view is re-mounted onSetFontSize(value || '0') }} - {...rest}> - + {...rest} + > + {isFiatInput && ( - + {symbol} )} diff --git a/packages/wallet/src/features/transactions/transfer/TransferFormWarnings.tsx b/packages/wallet/src/features/transactions/transfer/TransferFormWarnings.tsx index 2abf9e76cf4..a84eed94076 100644 --- a/packages/wallet/src/features/transactions/transfer/TransferFormWarnings.tsx +++ b/packages/wallet/src/features/transactions/transfer/TransferFormWarnings.tsx @@ -10,11 +10,7 @@ import { WarningSeverity } from 'wallet/src/features/transactions/WarningModal/t import { useAllTransactionsBetweenAddresses } from 'wallet/src/features/transactions/hooks/useAllTransactionsBetweenAddresses' import { useIsSmartContractAddress } from 'wallet/src/features/transactions/transfer/hooks/useIsSmartContractAddress' import { TransferSpeedbump } from 'wallet/src/features/transactions/transfer/types' -import { - useActiveAccountAddressWithThrow, - useDisplayName, - useSignerAccounts, -} from 'wallet/src/features/wallet/hooks' +import { useActiveAccountAddressWithThrow, useDisplayName, useSignerAccounts } from 'wallet/src/features/wallet/hooks' import { DisplayNameType } from 'wallet/src/features/wallet/types' interface TransferFormWarningProps { @@ -42,12 +38,12 @@ export function TransferFormSpeedbumps({ const currentSignerAccounts = useSignerAccounts() const isSignerRecipient = useMemo( () => currentSignerAccounts.some((a) => a.address === recipient), - [currentSignerAccounts, recipient] + [currentSignerAccounts, recipient], ) const { isSmartContractAddress, loading: addressLoading } = useIsSmartContractAddress( recipient, - chainId ?? UniverseChainId.Mainnet + chainId ?? UniverseChainId.Mainnet, ) const shouldWarnSelfSend = isSameAddress(activeAddress, recipient) @@ -57,8 +53,7 @@ export function TransferFormSpeedbumps({ useEffect(() => { setTransferSpeedbump({ - hasWarning: - shouldWarnSmartContract || shouldWarnNewAddress || shouldWarnErc20 || shouldWarnSelfSend, + hasWarning: shouldWarnSmartContract || shouldWarnNewAddress || shouldWarnErc20 || shouldWarnSelfSend, loading: addressLoading, }) }, [ @@ -131,12 +126,9 @@ export function TransferFormSpeedbumps({ severity={WarningSeverity.Medium} title={t('send.warning.newAddress.title')} onClose={onCloseWarning} - onConfirm={onNext}> - + onConfirm={onNext} + > + ) } @@ -162,7 +154,8 @@ const TransferRecipient = ({ gap="$spacing8" px="$spacing16" py="$spacing12" - width="100%"> + width="100%" + > {type === DisplayNameType.ENS ? displayName : address} diff --git a/packages/wallet/src/features/transactions/transfer/TransferReview.tsx b/packages/wallet/src/features/transactions/transfer/TransferReview.tsx index 477f3c1a19f..fe6c5f94f4c 100644 --- a/packages/wallet/src/features/transactions/transfer/TransferReview.tsx +++ b/packages/wallet/src/features/transactions/transfer/TransferReview.tsx @@ -3,6 +3,7 @@ import { useState } from 'react' import { useTranslation } from 'react-i18next' import { iconSizes } from 'ui/src/theme' import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants' +import { CurrencyField } from 'uniswap/src/features/transactions/transactionState/types' import { NumberType } from 'utilities/src/format/types' import { AccountDetails } from 'wallet/src/components/accounts/AccountDetails' import { WarningModal } from 'wallet/src/components/modals/WarningModal/WarningModal' @@ -14,13 +15,9 @@ import { TransactionReview } from 'wallet/src/features/transactions/TransactionR import { WarningSeverity } from 'wallet/src/features/transactions/WarningModal/types' import { ParsedWarnings } from 'wallet/src/features/transactions/hooks/useParsedTransactionWarnings' import { useUSDCValue } from 'wallet/src/features/transactions/swap/trade/hooks/useUSDCPrice' -import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' import { DerivedTransferInfo } from 'wallet/src/features/transactions/transfer/types' import { AccountType } from 'wallet/src/features/wallet/accounts/types' -import { - useActiveAccountAddressWithThrow, - useActiveAccountWithThrow, -} from 'wallet/src/features/wallet/hooks' +import { useActiveAccountAddressWithThrow, useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks' interface TransferFormProps { derivedTransferInfo: DerivedTransferInfo @@ -70,11 +67,7 @@ export function TransferReview({ const { blockingWarning } = warnings const actionButtonDisabled = - !!blockingWarning || - !gasFee.value || - !!gasFee.error || - !txRequest || - account.type === AccountType.Readonly + !!blockingWarning || !gasFee.value || !!gasFee.error || !txRequest || account.type === AccountType.Readonly const actionButtonProps = { disabled: actionButtonDisabled, @@ -83,9 +76,7 @@ export function TransferReview({ onPress: onReviewSubmit, } - const transferWarning = warnings.warnings.find( - (warning) => warning.severity >= WarningSeverity.Medium - ) + const transferWarning = warnings.warnings.find((warning) => warning.severity >= WarningSeverity.Medium) const formattedCurrencyAmount = formatCurrencyAmount({ value: currencyAmounts[CurrencyField.INPUT], diff --git a/packages/wallet/src/features/transactions/transfer/TransferTokenForm.tsx b/packages/wallet/src/features/transactions/transfer/TransferTokenForm.tsx index 0b89b5ebdba..8f3fe70d4e6 100644 --- a/packages/wallet/src/features/transactions/transfer/TransferTokenForm.tsx +++ b/packages/wallet/src/features/transactions/transfer/TransferTokenForm.tsx @@ -12,7 +12,10 @@ import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { useDeviceDimensions } from 'ui/src/hooks/useDeviceDimensions' import { iconSizes, spacing } from 'ui/src/theme' import { TextInputProps } from 'uniswap/src/components/input/TextInput' -import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants' +import { ModalName } from 'uniswap/src/features/telemetry/constants' +import { CurrencyField } from 'uniswap/src/features/transactions/transactionState/types' +import { TokenSelectorFlow } from 'uniswap/src/features/transactions/transfer/types' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { usePrevious } from 'utilities/src/react/hooks' import { TransferArrowButton } from 'wallet/src/components/buttons/TransferArrowButton' import { RecipientInputPanel } from 'wallet/src/components/input/RecipientInputPanel' @@ -27,15 +30,10 @@ import { useTokenSelectorActionHandlers } from 'wallet/src/features/transactions import { useUSDCValue } from 'wallet/src/features/transactions/swap/trade/hooks/useUSDCPrice' import { useUSDTokenUpdater } from 'wallet/src/features/transactions/swap/trade/hooks/useUSDTokenUpdater' import { transactionStateActions } from 'wallet/src/features/transactions/transactionState/transactionState' -import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' import { TransferFormSpeedbumps } from 'wallet/src/features/transactions/transfer/TransferFormWarnings' -import { useOnToggleShowRecipientSelector } from 'wallet/src/features/transactions/transfer/hooks/useOnToggleShowRecipientSelector' +import { useSetShowRecipientSelector } from 'wallet/src/features/transactions/transfer/hooks/useOnToggleShowRecipientSelector' import { useShowSendNetworkNotification } from 'wallet/src/features/transactions/transfer/hooks/useShowSendNetworkNotification' -import { - DerivedTransferInfo, - TokenSelectorFlow, - TransferSpeedbump, -} from 'wallet/src/features/transactions/transfer/types' +import { DerivedTransferInfo, TransferSpeedbump } from 'wallet/src/features/transactions/transfer/types' import { createTransactionId } from 'wallet/src/features/transactions/utils' import { BlockedAddressWarning } from 'wallet/src/features/trm/BlockedAddressWarning' import { useIsBlocked, useIsBlockedActiveAddress } from 'wallet/src/features/trm/hooks' @@ -89,13 +87,7 @@ export function TransferTokenForm({ } = derivedTransferInfo const currencyIn = currencyInInfo?.currency - useUSDTokenUpdater( - dispatch, - isFiatInput, - exactAmountToken, - exactAmountFiat, - currencyIn ?? undefined - ) + useUSDTokenUpdater(dispatch, isFiatInput, exactAmountToken, exactAmountFiat, currencyIn ?? undefined) useShowSendNetworkNotification({ chainId: currencyIn?.chainId }) @@ -109,17 +101,16 @@ export function TransferTokenForm({ hasWarning: false, }) - const { onShowTokenSelector } = useTokenSelectorActionHandlers( - dispatch, - TokenSelectorFlow.Transfer - ) + const { onShowTokenSelector } = useTokenSelectorActionHandlers(dispatch, TokenSelectorFlow.Transfer) const { onSetExactAmount, onSetMax } = useTokenFormActionHandlers(dispatch) - const onToggleShowRecipientSelector = useOnToggleShowRecipientSelector(dispatch) + const onSetShowRecipientSelector = useSetShowRecipientSelector(dispatch) + + const onShowRecipientSelector = useCallback(() => { + onSetShowRecipientSelector(true) + }, [onSetShowRecipientSelector]) - const { isBlocked: isActiveBlocked, isBlockedLoading: isActiveBlockedLoading } = - useIsBlockedActiveAddress() - const { isBlocked: isRecipientBlocked, isBlockedLoading: isRecipientBlockedLoading } = - useIsBlocked(recipient) + const { isBlocked: isActiveBlocked, isBlockedLoading: isActiveBlockedLoading } = useIsBlockedActiveAddress() + const { isBlocked: isRecipientBlocked, isBlockedLoading: isRecipientBlockedLoading } = useIsBlocked(recipient) const isBlocked = isActiveBlocked || isRecipientBlocked const isBlockedLoading = isActiveBlockedLoading || isRecipientBlockedLoading @@ -169,7 +160,7 @@ export function TransferTokenForm({ (start: number, end?: number) => { setInputSelection({ start, end: end ?? start }) }, - [setInputSelection] + [setInputSelection], ) const previsFiatInput = usePrevious(isFiatInput) @@ -203,14 +194,7 @@ export function TransferTokenForm({ start: newPositionFromStartWithPrefix, end: newPositionFromStartWithPrefix, }) - }, [ - isFiatInput, - previsFiatInput, - inputSelection, - setInputSelection, - exactAmountToken, - exactAmountFiat, - ]) + }, [isFiatInput, previsFiatInput, inputSelection, setInputSelection, exactAmountToken, exactAmountFiat]) const onTransferWarningClick = (): void => { Keyboard.dismiss() @@ -219,9 +203,7 @@ export function TransferTokenForm({ const { onToggleFiatInput } = useTokenFormActionHandlers(dispatch) - const transferWarning = warnings.warnings.find( - (warning) => warning.severity >= WarningSeverity.Low - ) + const transferWarning = warnings.warnings.find((warning) => warning.severity >= WarningSeverity.Low) const transferWarningColor = getAlertColor(transferWarning?.severity) const TRANSFER_DIRECTION_BUTTON_SIZE = iconSizes.icon20 @@ -236,11 +218,7 @@ export function TransferTokenForm({ caption={transferWarning.message} confirmText={t('common.button.close')} icon={ - + } modalName={ModalName.SendWarning} severity={transferWarning.severity} @@ -263,7 +241,8 @@ export function TransferTokenForm({ // TODO(EXT-526): re-enable `exiting` animation when it's fixed. exiting={isWeb ? undefined : FadeOut} gap="$spacing2" - onLayout={onInputPanelLayout}> + onLayout={onInputPanelLayout} + > {nftIn ? ( ) : ( @@ -281,13 +260,9 @@ export function TransferTokenForm({ warnings={warnings.warnings} onPressIn={(): void => setCurrencyFieldFocused(true)} onSelectionChange={ - showNativeKeyboard - ? undefined - : (start, end): void => setInputSelection({ start, end }) - } - onSetExactAmount={(value): void => - onSetExactAmount(CurrencyField.INPUT, value, isFiatInput) + showNativeKeyboard ? undefined : (start, end): void => setInputSelection({ start, end }) } + onSetExactAmount={(value): void => onSetExactAmount(CurrencyField.INPUT, value, isFiatInput)} onSetMax={(amount): void => { onSetMax(amount) setCurrencyFieldFocused(false) @@ -306,11 +281,9 @@ export function TransferTokenForm({ TRANSFER_DIRECTION_BUTTON_INNER_PADDING + TRANSFER_DIRECTION_BUTTON_BORDER_WIDTH } - style={StyleSheet.absoluteFill}> - + style={StyleSheet.absoluteFill} + > + @@ -323,12 +296,10 @@ export function TransferTokenForm({ borderBottomRightRadius={transferWarning || isBlocked ? '$none' : '$rounded20'} borderTopLeftRadius="$rounded20" borderTopRightRadius="$rounded20" - justifyContent="center"> + justifyContent="center" + > {recipient && ( - + )} {walletNeedsRestore && ( @@ -344,7 +315,8 @@ export function TransferTokenForm({ borderTopWidth={1} gap="$spacing8" px="$spacing12" - py="$spacing12"> + py="$spacing12" + > + py="$spacing12" + > + onLayout={onDecimalPadLayout} + > {!isWeb && !nftIn && !showNativeKeyboard && ( + testID={TestID.ReviewTransfer} + onPress={onPressReview} + > {t('send.button.review')} diff --git a/packages/wallet/src/features/transactions/transfer/getSendPrefilledState.ts b/packages/wallet/src/features/transactions/transfer/getSendPrefilledState.ts index 3f4e303c5d3..87c1cc3291f 100644 --- a/packages/wallet/src/features/transactions/transfer/getSendPrefilledState.ts +++ b/packages/wallet/src/features/transactions/transfer/getSendPrefilledState.ts @@ -1,10 +1,7 @@ +import { getNativeAddress } from 'uniswap/src/constants/addresses' +import { AssetType, CurrencyAsset } from 'uniswap/src/entities/assets' +import { CurrencyField, TransactionState } from 'uniswap/src/features/transactions/transactionState/types' import { WalletChainId } from 'uniswap/src/types/chains' -import { getNativeAddress } from 'wallet/src/constants/addresses' -import { AssetType, CurrencyAsset } from 'wallet/src/entities/assets' -import { - CurrencyField, - TransactionState, -} from 'wallet/src/features/transactions/transactionState/types' export function getSendPrefilledState({ chainId, diff --git a/packages/wallet/src/features/transactions/transfer/hooks/useDerivedTransferInfo.ts b/packages/wallet/src/features/transactions/transfer/hooks/useDerivedTransferInfo.ts index 3c829b8fc33..619a5b40bb8 100644 --- a/packages/wallet/src/features/transactions/transfer/hooks/useDerivedTransferInfo.ts +++ b/packages/wallet/src/features/transactions/transfer/hooks/useDerivedTransferInfo.ts @@ -1,19 +1,13 @@ import { useMemo } from 'react' +import { AssetType } from 'uniswap/src/entities/assets' +import { CurrencyField, TransactionState } from 'uniswap/src/features/transactions/transactionState/types' import { UniverseChainId } from 'uniswap/src/types/chains' -import { AssetType } from 'wallet/src/entities/assets' +import { buildCurrencyId } from 'uniswap/src/utils/currencyId' import { useNFT } from 'wallet/src/features/nfts/hooks' -import { - useOnChainCurrencyBalance, - useOnChainNativeCurrencyBalance, -} from 'wallet/src/features/portfolio/api' +import { useOnChainCurrencyBalance, useOnChainNativeCurrencyBalance } from 'wallet/src/features/portfolio/api' import { useCurrencyInfo } from 'wallet/src/features/tokens/useCurrencyInfo' -import { - CurrencyField, - TransactionState, -} from 'wallet/src/features/transactions/transactionState/types' import { DerivedTransferInfo } from 'wallet/src/features/transactions/transfer/types' import { useActiveAccount } from 'wallet/src/features/wallet/hooks' -import { buildCurrencyId } from 'wallet/src/utils/currencyId' import { ValueType, getCurrencyAmount } from 'wallet/src/utils/getCurrencyAmount' export function useDerivedTransferInfo(state: TransactionState): DerivedTransferInfo { @@ -32,7 +26,7 @@ export function useDerivedTransferInfo(state: TransactionState): DerivedTransfer const currencyInInfo = useCurrencyInfo( tradeableAsset?.type === AssetType.Currency ? buildCurrencyId(tradeableAsset?.chainId, tradeableAsset?.address) - : undefined + : undefined, ) const currencyIn = currencyInInfo?.currency @@ -41,24 +35,24 @@ export function useDerivedTransferInfo(state: TransactionState): DerivedTransfer tradeableAsset?.address, tradeableAsset?.type === AssetType.ERC1155 || tradeableAsset?.type === AssetType.ERC721 ? tradeableAsset.tokenId - : undefined + : undefined, ) const currencies = useMemo( () => ({ [CurrencyField.INPUT]: currencyInInfo ?? nftIn, }), - [currencyInInfo, nftIn] + [currencyInInfo, nftIn], ) const { balance: tokenInBalance } = useOnChainCurrencyBalance( currencyIn?.isToken ? currencyIn : undefined, - activeAccount?.address + activeAccount?.address, ) const { balance: nativeInBalance } = useOnChainNativeCurrencyBalance( chainId ?? UniverseChainId.Mainnet, - activeAccount?.address + activeAccount?.address, ) const amountSpecified = useMemo( @@ -68,20 +62,20 @@ export function useDerivedTransferInfo(state: TransactionState): DerivedTransfer valueType: ValueType.Exact, currency: currencyIn, }), - [currencyIn, exactAmountToken] + [currencyIn, exactAmountToken], ) const currencyAmounts = useMemo( () => ({ [CurrencyField.INPUT]: amountSpecified, }), - [amountSpecified] + [amountSpecified], ) const currencyBalances = useMemo( () => ({ [CurrencyField.INPUT]: currencyIn?.isNative ? nativeInBalance : tokenInBalance, }), - [currencyIn, nativeInBalance, tokenInBalance] + [currencyIn, nativeInBalance, tokenInBalance], ) return useMemo( () => ({ @@ -112,6 +106,6 @@ export function useDerivedTransferInfo(state: TransactionState): DerivedTransfer recipient, tradeableAsset?.type, txId, - ] + ], ) } diff --git a/packages/wallet/src/features/transactions/transfer/hooks/useIsSmartContractAddress.ts b/packages/wallet/src/features/transactions/transfer/hooks/useIsSmartContractAddress.ts index 8678440fdf0..06c688757ee 100644 --- a/packages/wallet/src/features/transactions/transfer/hooks/useIsSmartContractAddress.ts +++ b/packages/wallet/src/features/transactions/transfer/hooks/useIsSmartContractAddress.ts @@ -5,7 +5,7 @@ import { useProvider } from 'wallet/src/features/wallet/context' export function useIsSmartContractAddress( address: string | undefined, - chainId: WalletChainId + chainId: WalletChainId, ): { loading: boolean isSmartContractAddress: boolean diff --git a/packages/wallet/src/features/transactions/transfer/hooks/useOnSelectRecipient.ts b/packages/wallet/src/features/transactions/transfer/hooks/useOnSelectRecipient.ts index 53bad441f87..32335351e60 100644 --- a/packages/wallet/src/features/transactions/transfer/hooks/useOnSelectRecipient.ts +++ b/packages/wallet/src/features/transactions/transfer/hooks/useOnSelectRecipient.ts @@ -3,15 +3,13 @@ import { useCallback } from 'react' import { selectRecipient } from 'wallet/src/features/transactions/transactionState/transactionState' import { useOnToggleShowRecipientSelector } from 'wallet/src/features/transactions/transfer/hooks/useOnToggleShowRecipientSelector' -export function useOnSelectRecipient( - dispatch: React.Dispatch -): (recipient: Address) => void { +export function useOnSelectRecipient(dispatch: React.Dispatch): (recipient: Address) => void { const onToggleShowRecipientSelector = useOnToggleShowRecipientSelector(dispatch) return useCallback( (recipient: Address) => { onToggleShowRecipientSelector() dispatch(selectRecipient({ recipient })) }, - [dispatch, onToggleShowRecipientSelector] + [dispatch, onToggleShowRecipientSelector], ) } diff --git a/packages/wallet/src/features/transactions/transfer/hooks/useOnToggleShowRecipientSelector.ts b/packages/wallet/src/features/transactions/transfer/hooks/useOnToggleShowRecipientSelector.ts index 67de95f0d9b..46d78e79839 100644 --- a/packages/wallet/src/features/transactions/transfer/hooks/useOnToggleShowRecipientSelector.ts +++ b/packages/wallet/src/features/transactions/transfer/hooks/useOnToggleShowRecipientSelector.ts @@ -11,13 +11,11 @@ export function useOnToggleShowRecipientSelector(dispatch: React.Dispatch -): (show: boolean) => void { +export function useSetShowRecipientSelector(dispatch: React.Dispatch): (show: boolean) => void { return useCallback( (show: boolean) => { dispatch(setShowRecipientSelector(show)) }, - [dispatch] + [dispatch], ) } diff --git a/packages/wallet/src/features/transactions/transfer/hooks/useShowSendNetworkNotification.ts b/packages/wallet/src/features/transactions/transfer/hooks/useShowSendNetworkNotification.ts index 1113c2acbeb..40a90a2e6d9 100644 --- a/packages/wallet/src/features/transactions/transfer/hooks/useShowSendNetworkNotification.ts +++ b/packages/wallet/src/features/transactions/transfer/hooks/useShowSendNetworkNotification.ts @@ -22,7 +22,7 @@ export function useShowSendNetworkNotification({ chainId }: { chainId?: WalletCh type: AppNotificationType.NetworkChanged, chainId, flow: 'send', - }) + }), ) }, ONE_SECOND_MS / 2) }, [chainId, prevChainId, dispatch]) diff --git a/packages/wallet/src/features/transactions/transfer/hooks/useTransferCallback.ts b/packages/wallet/src/features/transactions/transfer/hooks/useTransferCallback.ts index 6e0cd1af77e..5a73c29d6a0 100644 --- a/packages/wallet/src/features/transactions/transfer/hooks/useTransferCallback.ts +++ b/packages/wallet/src/features/transactions/transfer/hooks/useTransferCallback.ts @@ -1,11 +1,11 @@ import { providers } from 'ethers' import { useMemo } from 'react' +import { useDispatch } from 'react-redux' +import { AssetType } from 'uniswap/src/entities/assets' import { WalletChainId } from 'uniswap/src/types/chains' -import { AssetType } from 'wallet/src/entities/assets' import { transferTokenActions } from 'wallet/src/features/transactions/transfer/transferTokenSaga' import { TransferTokenParams } from 'wallet/src/features/transactions/transfer/types' import { useActiveAccount } from 'wallet/src/features/wallet/hooks' -import { useAppDispatch } from 'wallet/src/state' /** Helper transfer callback for ERC20s */ export function useTransferERC20Callback( @@ -15,7 +15,7 @@ export function useTransferERC20Callback( tokenAddress?: Address, amountInWei?: string, transferTxWithGasSettings?: providers.TransactionRequest, - onSubmit?: () => void + onSubmit?: () => void, ): (() => void) | null { const account = useActiveAccount() @@ -32,7 +32,7 @@ export function useTransferERC20Callback( } : undefined, transferTxWithGasSettings, - onSubmit + onSubmit, ) } @@ -44,7 +44,7 @@ export function useTransferNFTCallback( tokenAddress?: Address, tokenId?: string, txRequest?: providers.TransactionRequest, - onSubmit?: () => void + onSubmit?: () => void, ): (() => void) | null { const account = useActiveAccount() @@ -61,7 +61,7 @@ export function useTransferNFTCallback( } : undefined, txRequest, - onSubmit + onSubmit, ) } @@ -69,9 +69,9 @@ export function useTransferNFTCallback( function useTransferCallback( transferTokenParams?: TransferTokenParams, txRequest?: providers.TransactionRequest, - onSubmit?: () => void + onSubmit?: () => void, ): null | (() => void) { - const dispatch = useAppDispatch() + const dispatch = useDispatch() return useMemo(() => { if (!transferTokenParams || !txRequest) { diff --git a/packages/wallet/src/features/transactions/transfer/hooks/useTransferTransactionRequest.ts b/packages/wallet/src/features/transactions/transfer/hooks/useTransferTransactionRequest.ts index 8e44bbea5b6..2a2e20619bf 100644 --- a/packages/wallet/src/features/transactions/transfer/hooks/useTransferTransactionRequest.ts +++ b/packages/wallet/src/features/transactions/transfer/hooks/useTransferTransactionRequest.ts @@ -4,12 +4,13 @@ import ERC1155_ABI from 'uniswap/src/abis/erc1155.json' import ERC20_ABI from 'uniswap/src/abis/erc20.json' import ERC721_ABI from 'uniswap/src/abis/erc721.json' import { Erc1155, Erc20, Erc721 } from 'uniswap/src/abis/types' +import { AssetType } from 'uniswap/src/entities/assets' +import { toSupportedChainId } from 'uniswap/src/features/chains/utils' +import { CurrencyField } from 'uniswap/src/features/transactions/transactionState/types' import { UniverseChainId } from 'uniswap/src/types/chains' +import { currencyAddress, isNativeCurrencyAddress } from 'uniswap/src/utils/currencyId' import { useAsyncData } from 'utilities/src/react/hooks' -import { AssetType } from 'wallet/src/entities/assets' -import { toSupportedChainId } from 'wallet/src/features/chains/utils' import { ContractManager } from 'wallet/src/features/contracts/ContractManager' -import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' import { DerivedTransferInfo, TransferCurrencyParams, @@ -19,10 +20,9 @@ import { import { Account } from 'wallet/src/features/wallet/accounts/types' import { useContractManager, useProvider } from 'wallet/src/features/wallet/context' import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks' -import { currencyAddress, isNativeCurrencyAddress } from 'wallet/src/utils/currencyId' export function useTransferTransactionRequest( - derivedTransferInfo: DerivedTransferInfo + derivedTransferInfo: DerivedTransferInfo, ): providers.TransactionRequest | undefined { const account = useActiveAccountWithThrow() const chainId = toSupportedChainId(derivedTransferInfo.chainId) @@ -44,7 +44,7 @@ async function getTransferTransaction( provider: providers.Provider, contractManager: ContractManager, account: Account, - derivedTransferInfo: DerivedTransferInfo + derivedTransferInfo: DerivedTransferInfo, ): Promise { const params = getTransferParams(account, derivedTransferInfo) if (!params) { @@ -66,13 +66,10 @@ async function getTransferTransaction( function getTransferParams( account: Account, - derivedTransferInfo: DerivedTransferInfo + derivedTransferInfo: DerivedTransferInfo, ): TransferTokenParams | undefined { - const { currencyAmounts, currencyTypes, chainId, recipient, currencyInInfo, nftIn } = - derivedTransferInfo - const tokenAddress = currencyInInfo - ? currencyAddress(currencyInInfo.currency) - : nftIn?.nftContract?.address + const { currencyAmounts, currencyTypes, chainId, recipient, currencyInInfo, nftIn } = derivedTransferInfo + const tokenAddress = currencyInInfo ? currencyAddress(currencyInInfo.currency) : nftIn?.nftContract?.address const amount = currencyAmounts[CurrencyField.INPUT]?.quotient.toString() const assetType = currencyTypes[CurrencyField.INPUT] @@ -117,20 +114,11 @@ function getTransferParams( async function getErc721TransferRequest( params: TransferNFTParams, provider: providers.Provider, - contractManager: ContractManager + contractManager: ContractManager, ): Promise { const { chainId, account, toAddress, tokenAddress, tokenId } = params - const erc721Contract = contractManager.getOrCreateContract( - chainId, - tokenAddress, - provider, - ERC721_ABI - ) - const baseRequest = await erc721Contract.populateTransaction.transferFrom( - account.address, - toAddress, - tokenId - ) + const erc721Contract = contractManager.getOrCreateContract(chainId, tokenAddress, provider, ERC721_ABI) + const baseRequest = await erc721Contract.populateTransaction.transferFrom(account.address, toAddress, tokenId) return { ...baseRequest, @@ -142,15 +130,10 @@ async function getErc721TransferRequest( async function getErc1155TransferRequest( params: TransferNFTParams, provider: providers.Provider, - contractManager: ContractManager + contractManager: ContractManager, ): Promise { const { chainId, account, toAddress, tokenAddress, tokenId } = params - const erc1155Contract = contractManager.getOrCreateContract( - chainId, - tokenAddress, - provider, - ERC1155_ABI - ) + const erc1155Contract = contractManager.getOrCreateContract(chainId, tokenAddress, provider, ERC1155_ABI) // TODO: [MOB-242] handle `non ERC1155 Receiver implement` error const baseRequest = await erc1155Contract.populateTransaction.safeTransferFrom( @@ -158,7 +141,7 @@ async function getErc1155TransferRequest( toAddress, tokenId, /*amount=*/ '1', - /*data=*/ '0x0' + /*data=*/ '0x0', ) return { @@ -181,19 +164,12 @@ function getNativeTransferRequest(params: TransferCurrencyParams): providers.Tra export async function getTokenTransferRequest( params: TransferCurrencyParams, provider: providers.Provider, - contractManager: ContractManager + contractManager: ContractManager, ): Promise { const { account, toAddress, chainId, tokenAddress, amountInWei } = params - const tokenContract = contractManager.getOrCreateContract( - chainId, - tokenAddress, - provider, - ERC20_ABI - ) - const transactionRequest = await tokenContract.populateTransaction.transfer( - toAddress, - amountInWei, - { from: account.address } - ) + const tokenContract = contractManager.getOrCreateContract(chainId, tokenAddress, provider, ERC20_ABI) + const transactionRequest = await tokenContract.populateTransaction.transfer(toAddress, amountInWei, { + from: account.address, + }) return { ...transactionRequest, chainId } } diff --git a/packages/wallet/src/features/transactions/transfer/hooks/useTransferWarnings.test.ts b/packages/wallet/src/features/transactions/transfer/hooks/useTransferWarnings.test.ts index 1a21cdc7785..15d76937d22 100644 --- a/packages/wallet/src/features/transactions/transfer/hooks/useTransferWarnings.test.ts +++ b/packages/wallet/src/features/transactions/transfer/hooks/useTransferWarnings.test.ts @@ -1,15 +1,16 @@ import { CurrencyAmount } from '@uniswap/sdk-core' +import { AssetType } from 'uniswap/src/entities/assets' +import { NativeCurrency } from 'uniswap/src/features/tokens/NativeCurrency' +import { CurrencyField } from 'uniswap/src/features/transactions/transactionState/types' import i18n from 'uniswap/src/i18n/i18n' +import { uniCurrencyInfo } from 'uniswap/src/test/fixtures' import { UniverseChainId } from 'uniswap/src/types/chains' -import { AssetType } from 'wallet/src/entities/assets' import { GQLNftAsset } from 'wallet/src/features/nfts/hooks' -import { NativeCurrency } from 'wallet/src/features/tokens/NativeCurrency' import { WarningLabel } from 'wallet/src/features/transactions/WarningModal/types' -import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' import { getTransferWarnings } from 'wallet/src/features/transactions/transfer/hooks/useTransferWarnings' import { DerivedTransferInfo } from 'wallet/src/features/transactions/transfer/types' import { isOffline } from 'wallet/src/features/transactions/utils' -import { networkDown, networkUnknown, networkUp, uniCurrencyInfo } from 'wallet/src/test/fixtures' +import { networkDown, networkUnknown, networkUp } from 'wallet/src/test/fixtures' const ETH = NativeCurrency.onChain(UniverseChainId.Mainnet) @@ -169,11 +170,7 @@ describe(getTransferWarnings, () => { }, } - const warnings = getTransferWarnings( - i18n.t, - incompleteAndInsufficientBalanceState, - isOffline(networkUp()) - ) + const warnings = getTransferWarnings(i18n.t, incompleteAndInsufficientBalanceState, isOffline(networkUp())) expect(warnings.length).toBe(2) }) }) diff --git a/packages/wallet/src/features/transactions/transfer/hooks/useTransferWarnings.ts b/packages/wallet/src/features/transactions/transfer/hooks/useTransferWarnings.ts index c7517d4a2e0..31ff3fe3f2a 100644 --- a/packages/wallet/src/features/transactions/transfer/hooks/useTransferWarnings.ts +++ b/packages/wallet/src/features/transactions/transfer/hooks/useTransferWarnings.ts @@ -2,7 +2,9 @@ import { useNetInfo } from '@react-native-community/netinfo' import { TFunction } from 'i18next' import _ from 'lodash' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' +import { CurrencyField } from 'uniswap/src/features/transactions/transactionState/types' import { WalletChainId } from 'uniswap/src/types/chains' +import { currencyAddress } from 'uniswap/src/utils/currencyId' import { useMemoCompare } from 'utilities/src/react/hooks' import { GQLNftAsset } from 'wallet/src/features/nfts/hooks' import { getNetworkWarning } from 'wallet/src/features/transactions/WarningModal/getNetworkWarning' @@ -12,15 +14,13 @@ import { WarningLabel, WarningSeverity, } from 'wallet/src/features/transactions/WarningModal/types' -import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' import { DerivedTransferInfo } from 'wallet/src/features/transactions/transfer/types' import { isOffline } from 'wallet/src/features/transactions/utils' -import { currencyAddress } from 'wallet/src/utils/currencyId' export function getTransferWarnings( t: TFunction, derivedTransferInfo: DerivedTransferInfo, - offline: boolean + offline: boolean, ): Warning[] { const warnings: Warning[] = [] @@ -28,8 +28,7 @@ export function getTransferWarnings( warnings.push(getNetworkWarning(t)) } - const { currencyBalances, currencyAmounts, recipient, currencyInInfo, nftIn, chainId } = - derivedTransferInfo + const { currencyBalances, currencyAmounts, recipient, currencyInInfo, nftIn, chainId } = derivedTransferInfo const currencyBalanceIn = currencyBalances[CurrencyField.INPUT] const currencyAmountIn = currencyAmounts[CurrencyField.INPUT] @@ -39,7 +38,7 @@ export function getTransferWarnings( chainId, recipient, !!currencyAmountIn, - !!currencyBalanceIn + !!currencyBalanceIn, ) // insufficient balance @@ -69,10 +68,7 @@ export function getTransferWarnings( return warnings } -export function useTransferWarnings( - t: TFunction, - derivedTransferInfo: DerivedTransferInfo -): Warning[] { +export function useTransferWarnings(t: TFunction, derivedTransferInfo: DerivedTransferInfo): Warning[] { const networkStatus = useNetInfo() // First `useNetInfo` call always results with unknown state, // which we want to ignore here until state is determined, @@ -90,11 +86,9 @@ const checkIsMissingRequiredParams = ( chainId: WalletChainId | undefined, recipient: Address | undefined, hasCurrencyAmount: boolean, - hasCurrencyBalance: boolean + hasCurrencyBalance: boolean, ): boolean => { - const tokenAddress = currencyInInfo - ? currencyAddress(currencyInInfo.currency) - : nftIn?.nftContract?.address + const tokenAddress = currencyInInfo ? currencyAddress(currencyInInfo.currency) : nftIn?.nftContract?.address if (!tokenAddress || !chainId || !recipient) { return true diff --git a/packages/wallet/src/features/transactions/transfer/transferTokenSaga.test.ts b/packages/wallet/src/features/transactions/transfer/transferTokenSaga.test.ts index 50b4073ce96..d84bfa6e864 100644 --- a/packages/wallet/src/features/transactions/transfer/transferTokenSaga.test.ts +++ b/packages/wallet/src/features/transactions/transfer/transferTokenSaga.test.ts @@ -2,16 +2,13 @@ import { call } from '@redux-saga/core/effects' import { BigNumber } from 'ethers' import { expectSaga } from 'redux-saga-test-plan' import * as matchers from 'redux-saga-test-plan/matchers' +import { getNativeAddress } from 'uniswap/src/constants/addresses' +import { DAI } from 'uniswap/src/constants/tokens' +import { AssetType } from 'uniswap/src/entities/assets' import { UniverseChainId } from 'uniswap/src/types/chains' -import { getNativeAddress } from 'wallet/src/constants/addresses' -import { DAI } from 'wallet/src/constants/tokens' -import { AssetType } from 'wallet/src/entities/assets' import { sendTransaction } from 'wallet/src/features/transactions/sendTransactionSaga' import { transferToken } from 'wallet/src/features/transactions/transfer/transferTokenSaga' -import { - TransferCurrencyParams, - TransferNFTParams, -} from 'wallet/src/features/transactions/transfer/types' +import { TransferCurrencyParams, TransferNFTParams } from 'wallet/src/features/transactions/transfer/types' import { SendTokenTransactionInfo, TransactionType } from 'wallet/src/features/transactions/types' import { getContractManager, getProvider } from 'wallet/src/features/wallet/context' import { getTxFixtures, signerMnemonicAccount } from 'wallet/src/test/fixtures' diff --git a/packages/wallet/src/features/transactions/transfer/transferTokenSaga.ts b/packages/wallet/src/features/transactions/transfer/transferTokenSaga.ts index 46684c9097b..91d70001ed9 100644 --- a/packages/wallet/src/features/transactions/transfer/transferTokenSaga.ts +++ b/packages/wallet/src/features/transactions/transfer/transferTokenSaga.ts @@ -4,15 +4,15 @@ import ERC1155_ABI from 'uniswap/src/abis/erc1155.json' import ERC20_ABI from 'uniswap/src/abis/erc20.json' import ERC721_ABI from 'uniswap/src/abis/erc721.json' import { Erc1155, Erc20, Erc721 } from 'uniswap/src/abis/types' +import { AssetType } from 'uniswap/src/entities/assets' import { WalletEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' +import { isNativeCurrencyAddress } from 'uniswap/src/utils/currencyId' import { logger } from 'utilities/src/logger/logger' -import { AssetType } from 'wallet/src/entities/assets' import { sendTransaction } from 'wallet/src/features/transactions/sendTransactionSaga' import { TransferTokenParams } from 'wallet/src/features/transactions/transfer/types' import { SendTokenTransactionInfo, TransactionType } from 'wallet/src/features/transactions/types' import { getContractManager, getProvider } from 'wallet/src/features/wallet/context' -import { isNativeCurrencyAddress } from 'wallet/src/utils/currencyId' import { createMonitoredSaga } from 'wallet/src/utils/saga' type Params = { @@ -63,29 +63,15 @@ function* validateTransfer(transferTokenParams: TransferTokenParams) { switch (type) { case AssetType.ERC1155: { - const erc1155Contract = contractManager.getOrCreateContract( - chainId, - tokenAddress, - provider, - ERC1155_ABI - ) + const erc1155Contract = contractManager.getOrCreateContract(chainId, tokenAddress, provider, ERC1155_ABI) - const balance = yield* call( - erc1155Contract.balanceOf, - account.address, - transferTokenParams.tokenId - ) + const balance = yield* call(erc1155Contract.balanceOf, account.address, transferTokenParams.tokenId) validateTransferAmount('1', balance) return } case AssetType.ERC721: { - const erc721Contract = contractManager.getOrCreateContract( - chainId, - tokenAddress, - provider, - ERC721_ABI - ) + const erc721Contract = contractManager.getOrCreateContract(chainId, tokenAddress, provider, ERC721_ABI) const balance = yield* call(erc721Contract.balanceOf, account.address) validateTransferAmount('1', balance) return @@ -97,12 +83,7 @@ function* validateTransfer(transferTokenParams: TransferTokenParams) { return } - const tokenContract = contractManager.getOrCreateContract( - chainId, - tokenAddress, - provider, - ERC20_ABI - ) + const tokenContract = contractManager.getOrCreateContract(chainId, tokenAddress, provider, ERC20_ABI) const currentBalance = yield* call(tokenContract.balanceOf, account.address) validateTransferAmount(transferTokenParams.amountInWei, currentBalance) } diff --git a/packages/wallet/src/features/transactions/transfer/types.ts b/packages/wallet/src/features/transactions/transfer/types.ts index 4cb5b046f07..2ec37ba6422 100644 --- a/packages/wallet/src/features/transactions/transfer/types.ts +++ b/packages/wallet/src/features/transactions/transfer/types.ts @@ -1,9 +1,9 @@ import { Currency, CurrencyAmount } from '@uniswap/sdk-core' +import { AssetType, NFTAssetType } from 'uniswap/src/entities/assets' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' +import { CurrencyField } from 'uniswap/src/features/transactions/transactionState/types' import { WalletChainId } from 'uniswap/src/types/chains' -import { AssetType, NFTAssetType } from 'wallet/src/entities/assets' import { GQLNftAsset } from 'wallet/src/features/nfts/hooks' -import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' import { Account } from 'wallet/src/features/wallet/accounts/types' interface BaseTransferParams { @@ -58,8 +58,3 @@ export interface TransferSpeedbump { hasWarning: boolean loading: boolean } - -export enum TokenSelectorFlow { - Swap, - Transfer, -} diff --git a/packages/wallet/src/features/transactions/types.ts b/packages/wallet/src/features/transactions/types.ts index a1fab74aeaa..3958e7823db 100644 --- a/packages/wallet/src/features/transactions/types.ts +++ b/packages/wallet/src/features/transactions/types.ts @@ -4,11 +4,11 @@ import { TradeType } from '@uniswap/sdk-core' import { providers } from 'ethers' import { Dispatch } from 'react' import { TransactionListQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { AssetType } from 'uniswap/src/entities/assets' import { FORLogo } from 'uniswap/src/features/fiatOnRamp/types' import { WalletChainId } from 'uniswap/src/types/chains' import { DappInfo } from 'uniswap/src/types/walletConnect' import { Routing } from 'wallet/src/data/tradingApi/__generated__/index' -import { AssetType } from 'wallet/src/entities/assets' import { MoonpayCurrency } from 'wallet/src/features/fiatOnRamp/types' import { GasFeeResult } from 'wallet/src/features/gas/types' import { ParsedWarnings } from 'wallet/src/features/transactions/hooks/useParsedTransactionWarnings' @@ -20,9 +20,7 @@ export enum WrapType { Unwrap, } -export type ChainIdToTxIdToDetails = Partial< - Record -> +export type ChainIdToTxIdToDetails = Partial> // Basic identifying info for a transaction export interface TransactionId { @@ -49,12 +47,14 @@ interface BaseTransactionDetails extends TransactionId { // It may also become optional for classic if we start tracking txs before they're actually sent hash?: string + // TODO(MOB-3679): receipt does not need to be persisted; remove from state receipt?: TransactionReceipt // cancelRequest is the txRequest object to be submitted // in attempt to cancel the current transaction // it should contain all the appropriate gas details in order - // to get submitted first + // to be mined first + // TODO(MOB-3679): cancelRequest does not need to be persisted; remove from state cancelRequest?: providers.TransactionRequest networkFee?: TransactionNetworkFee @@ -72,8 +72,14 @@ export interface UniswapXOrderDetails extends BaseTransactionDetails { // Note: `orderHash` is an off-chain value used to track orders before they're filled on-chain. // UniswapX orders will also have a transaction `hash` if they become filled. - // `orderHash` will be undefined if the object is built from a filled order received from graphql. Once filled, it is not needed for any tracking. + // `orderHash` will be an undefined if the object is built from a filled order received from graphql. Once filled, it is not needed for any tracking. orderHash?: string + + // Used to track status of the order before it is submitted + queueStatus?: QueuedOrderStatus + + // The txHash of the wrap transaction submitted before the order + wrapTxHash?: string } export interface ClassicTransactionDetails extends BaseTransactionDetails { @@ -99,17 +105,36 @@ export enum TransactionStatus { // May want more granular options here later like InMemPool } -// Transaction confirmed on chain -export type FinalizedTransactionStatus = - | TransactionStatus.Success - | TransactionStatus.Failed - | TransactionStatus.Canceled - | TransactionStatus.FailedCancel - -export type FinalizedTransactionDetails = TransactionDetails & { - status: FinalizedTransactionStatus - hash: string -} +export enum QueuedOrderStatus { + Waiting = 'waiting', + ApprovalFailed = 'approvalFailed', + WrapFailed = 'wrapFailed', + AppClosed = 'appClosed', + Stale = 'stale', + SubmissionFailed = 'submissionFailed', + Submitted = 'submitted', +} + +const FINAL_STATUSES = [ + TransactionStatus.Success, + TransactionStatus.Failed, + TransactionStatus.Canceled, + TransactionStatus.FailedCancel, + TransactionStatus.Expired, +] as const +export type FinalizedTransactionStatus = (typeof FINAL_STATUSES)[number] + +export type FinalizedTransactionDetails = TransactionDetails & + ( + | { + status: TransactionStatus.Success + hash: string + } + | { + status: Exclude + hash?: string // Hash may be undefined for non-successful transactions, as the uniswapx backend does not provide hashes for cancelled, failed, or expired orders. + } + ) export type TransactionOptions = { request: providers.TransactionRequest @@ -132,6 +157,7 @@ export interface NFTSummaryInfo { name: string collectionName: string imageURL: string + address: string } export enum NFTTradeType { @@ -180,6 +206,7 @@ export interface ApproveTransactionInfo extends BaseTransactionInfo { tokenAddress: string spender: string approvalAmount?: string + dappInfo?: DappInfoTransactionDetails } export interface BaseSwapTransactionInfo extends BaseTransactionInfo { @@ -300,6 +327,7 @@ export interface NFTMintTransactionInfo extends BaseTransactionInfo { nftSummaryInfo: NFTSummaryInfo purchaseCurrencyId?: string purchaseCurrencyAmountRaw?: string + dappInfo?: DappInfoTransactionDetails } export interface NFTTradeTransactionInfo extends BaseTransactionInfo { @@ -314,6 +342,7 @@ export interface NFTApproveTransactionInfo extends BaseTransactionInfo { type: TransactionType.NFTApprove nftSummaryInfo: NFTSummaryInfo spender: string + dappInfo?: DappInfoTransactionDetails } export interface WCConfirmInfo extends BaseTransactionInfo { @@ -321,9 +350,16 @@ export interface WCConfirmInfo extends BaseTransactionInfo { dapp: DappInfo } +export interface DappInfoTransactionDetails { + name?: string + address: string + icon?: string +} + export interface UnknownTransactionInfo extends BaseTransactionInfo { type: TransactionType.Unknown tokenAddress?: string + dappInfo?: DappInfoTransactionDetails } export type TransactionTypeInfo = @@ -343,24 +379,33 @@ export type TransactionTypeInfo = | OnRampPurchaseInfo | OnRampTransferInfo -export function isConfirmedSwapTypeInfo( - typeInfo: TransactionTypeInfo -): typeInfo is ConfirmedSwapTransactionInfo { +export function isConfirmedSwapTypeInfo(typeInfo: TransactionTypeInfo): typeInfo is ConfirmedSwapTransactionInfo { return Boolean( (typeInfo as ConfirmedSwapTransactionInfo).inputCurrencyAmountRaw && - (typeInfo as ConfirmedSwapTransactionInfo).outputCurrencyAmountRaw + (typeInfo as ConfirmedSwapTransactionInfo).outputCurrencyAmountRaw, ) } -export function isFinalizedTx( - tx: TransactionDetails | FinalizedTransactionDetails -): tx is FinalizedTransactionDetails { - return ( - tx.status === TransactionStatus.Success || - tx.status === TransactionStatus.Failed || - tx.status === TransactionStatus.Canceled || - tx.status === TransactionStatus.FailedCancel - ) +export function isFinalizedTxStatus(status: TransactionStatus): status is FinalizedTransactionStatus { + return FINAL_STATUSES.some((finalStatus) => finalStatus === status) +} + +export function isFinalizedTx(tx: TransactionDetails | FinalizedTransactionDetails): tx is FinalizedTransactionDetails { + const validateFinalizedTx = (): FinalizedTransactionDetails | undefined => { + const { status, hash } = tx + if (status === TransactionStatus.Success) { + if (!hash) { + return undefined + } + return { ...tx, status, hash } + } else if (isFinalizedTxStatus(status)) { + return { ...tx, status } + } + return undefined + } + + // Validation fn prevents & future-proofs the typeguard from illicit casting + return Boolean(validateFinalizedTx()) } export enum TransactionStep { diff --git a/packages/wallet/src/features/transactions/utils.test.ts b/packages/wallet/src/features/transactions/utils.test.ts index 7288e47f371..fc8432f07a0 100644 --- a/packages/wallet/src/features/transactions/utils.test.ts +++ b/packages/wallet/src/features/transactions/utils.test.ts @@ -1,6 +1,7 @@ import { CurrencyAmount } from '@uniswap/sdk-core' +import { MAINNET_CURRENCY } from 'uniswap/src/test/fixtures' import { hasSufficientFundsIncludingGas, isOffline } from 'wallet/src/features/transactions/utils' -import { MAINNET_CURRENCY, networkDown, networkUnknown, networkUp } from 'wallet/src/test/fixtures' +import { networkDown, networkUnknown, networkUp } from 'wallet/src/test/fixtures' const ZERO_ETH = CurrencyAmount.fromRawAmount(MAINNET_CURRENCY, 0) const ONE_ETH = CurrencyAmount.fromRawAmount(MAINNET_CURRENCY, 1e18) diff --git a/packages/wallet/src/features/transactions/utils.ts b/packages/wallet/src/features/transactions/utils.ts index 301283feee8..9519608272a 100644 --- a/packages/wallet/src/features/transactions/utils.ts +++ b/packages/wallet/src/features/transactions/utils.ts @@ -7,6 +7,7 @@ import { isUniswapX } from 'wallet/src/features/transactions/swap/trade/utils' import { FinalizedTransactionStatus, TransactionDetails, + TransactionReceipt, TransactionStatus, } from 'wallet/src/features/transactions/types' import { ValueType, getCurrencyAmount } from 'wallet/src/utils/getCurrencyAmount' @@ -15,7 +16,7 @@ export const MAX_FIAT_INPUT_DECIMALS = 2 export function getSerializableTransactionRequest( request: providers.TransactionRequest, - chainId?: WalletChainId + chainId?: WalletChainId, ): providers.TransactionRequest { // prettier-ignore const { to, from, nonce, gasLimit, gasPrice, data, value, maxPriorityFeePerGas, maxFeePerGas, type } = request @@ -38,7 +39,7 @@ export function getSerializableTransactionRequest( function getNativeCurrencyTotalSpend( value?: CurrencyAmount, gasFee?: string, - nativeCurrency?: NativeCurrency + nativeCurrency?: NativeCurrency, ): Maybe> { if (!gasFee || !nativeCurrency) { return value @@ -59,11 +60,7 @@ export function hasSufficientFundsIncludingGas(params: { nativeCurrencyBalance?: CurrencyAmount }): boolean { const { transactionAmount, gasFee, nativeCurrencyBalance } = params - const totalSpend = getNativeCurrencyTotalSpend( - transactionAmount, - gasFee, - nativeCurrencyBalance?.currency - ) + const totalSpend = getNativeCurrencyTotalSpend(transactionAmount, gasFee, nativeCurrencyBalance?.currency) return !totalSpend || !nativeCurrencyBalance?.lessThan(totalSpend) } @@ -88,7 +85,7 @@ export function isOffline(networkStatus: NetInfoState): boolean { // Based on the current status of the transaction, we determine the new status. export function getFinalizedTransactionStatus( currentStatus: TransactionStatus, - receiptStatus?: number + receiptStatus?: number, ): FinalizedTransactionStatus { if (!receiptStatus) { return TransactionStatus.Failed @@ -100,11 +97,26 @@ export function getFinalizedTransactionStatus( } export function getIsCancelable(tx: TransactionDetails): boolean { - if ( - tx.status === TransactionStatus.Pending && - (isUniswapX(tx) || Object.keys(tx.options?.request).length > 0) - ) { + if (tx.status === TransactionStatus.Pending && (isUniswapX(tx) || Object.keys(tx.options?.request).length > 0)) { return true } return false } + +export function receiptFromEthersReceipt( + ethersReceipt: providers.TransactionReceipt | undefined, +): TransactionReceipt | undefined { + if (!ethersReceipt) { + return undefined + } + + return { + blockHash: ethersReceipt.blockHash, + blockNumber: ethersReceipt.blockNumber, + transactionIndex: ethersReceipt.transactionIndex, + confirmations: ethersReceipt.confirmations, + confirmedTime: Date.now(), + gasUsed: ethersReceipt.gasUsed?.toNumber(), + effectiveGasPrice: ethersReceipt.effectiveGasPrice?.toNumber(), + } +} diff --git a/packages/wallet/src/features/trm/BlockedAddressWarning.tsx b/packages/wallet/src/features/trm/BlockedAddressWarning.tsx index aa5d7d1f3a8..d4814ca0e7d 100644 --- a/packages/wallet/src/features/trm/BlockedAddressWarning.tsx +++ b/packages/wallet/src/features/trm/BlockedAddressWarning.tsx @@ -17,24 +17,17 @@ export function BlockedAddressWarning({ return ( <> - {showBlockedAddressModal && ( - setShowBlockedAddressModal(false)} /> - )} + {showBlockedAddressModal && setShowBlockedAddressModal(false)} />} { Keyboard.dismiss() setShowBlockedAddressModal(true) - }}> + }} + > - + - {isRecipientBlocked - ? t('send.warning.blocked.recipient') - : t('send.warning.blocked.default')} + {isRecipientBlocked ? t('send.warning.blocked.recipient') : t('send.warning.blocked.default')} diff --git a/packages/wallet/src/features/trm/hooks.ts b/packages/wallet/src/features/trm/hooks.ts index c47a4ade9e0..b519a9ef03d 100644 --- a/packages/wallet/src/features/trm/hooks.ts +++ b/packages/wallet/src/features/trm/hooks.ts @@ -21,7 +21,7 @@ export function useIsBlocked(address?: string, isViewOnly = false): IsBlockedRes { ttlMs: ONE_MINUTE_MS * 5, skip: !address || isViewOnly, - } + }, ) return { diff --git a/packages/wallet/src/features/unitags/api.ts b/packages/wallet/src/features/unitags/api.ts index 3944ffae8aa..c4fffadc3e0 100644 --- a/packages/wallet/src/features/unitags/api.ts +++ b/packages/wallet/src/features/unitags/api.ts @@ -32,12 +32,14 @@ const BASE_HEADERS = { const generateAxiosHeaders = async ( signature: string, - firebaseAppCheckToken?: string + firebaseAppCheckToken?: string, ): Promise> => { return { ...BASE_HEADERS, 'x-uni-sig': signature, - ...(firebaseAppCheckToken && { 'x-firebase-app-check': firebaseAppCheckToken }), + ...(firebaseAppCheckToken && { + 'x-firebase-app-check': firebaseAppCheckToken, + }), } } @@ -46,16 +48,18 @@ export function useUnitagClaimEligibilityQuery({ deviceId, isUsernameChange, skip, -}: UnitagClaimEligibilityParams & { skip?: boolean }): ReturnType< - typeof useRestQuery -> { +}: UnitagClaimEligibilityParams & { skip?: boolean }): ReturnType> { return useRestQuery>( - addQueryParamsToEndpoint('/claim/eligibility', { address, deviceId, isUsernameChange }), + addQueryParamsToEndpoint('/claim/eligibility', { + address, + deviceId, + isUsernameChange, + }), { address, deviceId, isUsernameChange }, // dummy body so that cache key is unique per query params ['canClaim', 'errorCode', 'message'], // return all fields { skip, ttlMs: ONE_MINUTE_MS * 2 }, 'GET', - unitagsApolloClient + unitagsApolloClient, ) } @@ -71,11 +75,9 @@ export async function getUnitagAvatarUploadUrl({ signerManager: SignerManager }): ReturnType> { const avatarUploadUrl = `${uniswapUrls.unitagsApiUrl}/username/avatar-upload-url` - const { requestParams, signature } = await createSignedRequestParams<{ username: string }>( - { username }, - account, - signerManager - ) + const { requestParams, signature } = await createSignedRequestParams<{ + username: string + }>({ username }, account, signerManager) const headers = await generateAxiosHeaders(signature) return await axios.get(avatarUploadUrl, { params: requestParams, @@ -96,7 +98,7 @@ export async function deleteUnitag({ const { requestBody, signature } = await createSignedRequestBody( { username }, account, - signerManager + signerManager, ) const headers = await generateAxiosHeaders(signature) return await axios.delete(avatarUploadUrl, { @@ -125,7 +127,7 @@ export async function updateUnitagMetadata({ clearAvatar, }, account, - signerManager + signerManager, ) const headers = await generateAxiosHeaders(signature) return await axios.put(updateMetadataUrl, requestBody, { @@ -156,7 +158,7 @@ export async function claimUnitag({ metadata, }, account, - signerManager + signerManager, ) const headers = await generateAxiosHeaders(signature, firebaseAppCheckToken) return await axios.post(claimUnitagUrl, requestBody, { @@ -182,7 +184,7 @@ export async function changeUnitag({ deviceId, }, account, - signerManager + signerManager, ) const headers = await generateAxiosHeaders(signature) return await axios.post(changeUnitagUrl, requestBody, { @@ -197,7 +199,7 @@ export async function fetchUnitagByAddresses(addresses: Address[]): Promise<{ error?: unknown }> { const unitagAddressesUrl = `${uniswapUrls.unitagsApiUrl}/addresses?addresses=${encodeURIComponent( - addresses.join(',') + addresses.join(','), )}` try { @@ -216,9 +218,9 @@ export async function fetchExtensionWaitlistEligibity(username: string): Promise data?: UnitagWaitlistPositionResponse error?: unknown }> { - const unitagWaitlistPositionUrl = `${ - uniswapUrls.unitagsApiUrl - }/waitlist/position?username=${encodeURIComponent(username)}` + const unitagWaitlistPositionUrl = `${uniswapUrls.unitagsApiUrl}/waitlist/position?username=${encodeURIComponent( + username, + )}` try { const response = await axios.get(unitagWaitlistPositionUrl, { diff --git a/packages/wallet/src/features/unitags/avatars.ts b/packages/wallet/src/features/unitags/avatars.ts index 28b68f726d8..534c5e30595 100644 --- a/packages/wallet/src/features/unitags/avatars.ts +++ b/packages/wallet/src/features/unitags/avatars.ts @@ -1,9 +1,6 @@ import axios from 'axios' import { Platform } from 'react-native' -import { - UnitagAvatarUploadCredentials, - UnitagGetAvatarUploadUrlResponse, -} from 'uniswap/src/features/unitags/types' +import { UnitagAvatarUploadCredentials, UnitagGetAvatarUploadUrlResponse } from 'uniswap/src/features/unitags/types' import { logger } from 'utilities/src/logger/logger' import { getUnitagAvatarUploadUrl, updateUnitagMetadata } from 'wallet/src/features/unitags/api' import { Account } from 'wallet/src/features/wallet/accounts/types' @@ -23,7 +20,7 @@ export function isLocalFileUri(imageUri: string): boolean { export async function uploadFileToS3( imageUri: string, - creds: UnitagAvatarUploadCredentials + creds: UnitagAvatarUploadCredentials, ): Promise<{ success: boolean }> { if (!creds.preSignedUrl || !creds.s3UploadFields) { return { success: false } @@ -139,9 +136,7 @@ export const tryUploadAvatar = async ({ }): Promise<{ success: boolean; skipped: boolean }> => { const needsAvatarUpload = !!avatarImageUri && isLocalFileUri(avatarImageUri) const isPreSignedUrlReady = - !avatarUploadUrlLoading && - !!avatarUploadUrlResponse?.preSignedUrl && - !!avatarUploadUrlResponse?.s3UploadFields + !avatarUploadUrlLoading && !!avatarUploadUrlResponse?.preSignedUrl && !!avatarUploadUrlResponse?.s3UploadFields const shouldTryAvatarUpload = needsAvatarUpload && isPreSignedUrlReady if (!shouldTryAvatarUpload) { diff --git a/packages/wallet/src/features/unitags/hooks.ts b/packages/wallet/src/features/unitags/hooks.ts index 7adbdac51c6..22a67a4eaea 100644 --- a/packages/wallet/src/features/unitags/hooks.ts +++ b/packages/wallet/src/features/unitags/hooks.ts @@ -2,6 +2,7 @@ import { TFunction } from 'i18next' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { getUniqueId } from 'react-native-device-info' +import { useDispatch } from 'react-redux' import { FeatureFlags } from 'uniswap/src/features/gating/flags' import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { UnitagEventName } from 'uniswap/src/features/telemetry/constants' @@ -15,43 +16,26 @@ import { UnitagGetAvatarUploadUrlResponse, } from 'uniswap/src/features/unitags/types' import { UniverseChainId } from 'uniswap/src/types/chains' +import { areAddressesEqual } from 'uniswap/src/utils/addresses' import { logger } from 'utilities/src/logger/logger' import { useAsyncData } from 'utilities/src/react/hooks' import { ONE_SECOND_MS } from 'utilities/src/time/time' import { getFirebaseAppCheckToken } from 'wallet/src/features/appCheck' import { selectExtensionOnboardingState } from 'wallet/src/features/behaviorHistory/selectors' -import { - ExtensionOnboardingState, - setExtensionOnboardingState, -} from 'wallet/src/features/behaviorHistory/slice' +import { ExtensionOnboardingState, setExtensionOnboardingState } from 'wallet/src/features/behaviorHistory/slice' import { useENS } from 'wallet/src/features/ens/useENS' import { pushNotification } from 'wallet/src/features/notifications/slice' import { AppNotificationType } from 'wallet/src/features/notifications/types' import { useOnboardingContext } from 'wallet/src/features/onboarding/OnboardingContext' -import { - claimUnitag, - getUnitagAvatarUploadUrl, - useUnitagClaimEligibilityQuery, -} from 'wallet/src/features/unitags/api' -import { - isLocalFileUri, - uploadAndUpdateAvatarAfterClaim, -} from 'wallet/src/features/unitags/avatars' -import { - AVATAR_UPLOAD_CREDS_EXPIRY_SECONDS, - UNITAG_VALID_REGEX, -} from 'wallet/src/features/unitags/constants' +import { claimUnitag, getUnitagAvatarUploadUrl, useUnitagClaimEligibilityQuery } from 'wallet/src/features/unitags/api' +import { isLocalFileUri, uploadAndUpdateAvatarAfterClaim } from 'wallet/src/features/unitags/avatars' +import { AVATAR_UPLOAD_CREDS_EXPIRY_SECONDS, UNITAG_VALID_REGEX } from 'wallet/src/features/unitags/constants' import { parseUnitagErrorCode } from 'wallet/src/features/unitags/utils' import { Account, AccountType } from 'wallet/src/features/wallet/accounts/types' import { useWalletSigners } from 'wallet/src/features/wallet/context' -import { - useAccounts, - useActiveAccount, - useActiveAccountAddressWithThrow, -} from 'wallet/src/features/wallet/hooks' +import { useAccounts, useActiveAccount, useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' import { SignerManager } from 'wallet/src/features/wallet/signing/SignerManager' -import { useAppDispatch, useAppSelector } from 'wallet/src/state' -import { areAddressesEqual } from 'wallet/src/utils/addresses' +import { useAppSelector } from 'wallet/src/state' const MIN_UNITAG_LENGTH = 3 const MAX_UNITAG_LENGTH = 20 @@ -87,7 +71,7 @@ export const useCanActiveAddressClaimUnitag = (): { export const useCanAddressClaimUnitag = ( address?: Address, - isUsernameChange?: boolean + isUsernameChange?: boolean, ): { canClaimUnitag: boolean; errorCode?: UnitagErrorCodes } => { const { data: deviceId } = useAsyncData(getUniqueId) const { refetchUnitagsCounter } = useUnitagUpdater() @@ -136,7 +120,7 @@ export const getUnitagFormatError = (unitag: string, t: TFunction): string | und export const useCanClaimUnitagName = ( unitagAddress: Address | undefined, - unitag: string | undefined + unitag: string | undefined, ): { error: string | undefined; loading: boolean; requiresENSMatch: boolean } => { const { t } = useTranslation() @@ -146,11 +130,7 @@ export const useCanClaimUnitagName = ( // Skip the backend calls if we found an error const unitagToSearch = error ? undefined : unitag const { loading: unitagLoading, data } = useUnitagQuery(unitagToSearch) - const { loading: ensLoading, address: ensAddress } = useENS( - UniverseChainId.Mainnet, - unitagToSearch, - true - ) + const { loading: ensLoading, address: ensAddress } = useENS(UniverseChainId.Mainnet, unitagToSearch, true) const loading = unitagLoading || ensLoading // Check for availability and ENS match @@ -172,10 +152,10 @@ export const useCanClaimUnitagName = ( export const useClaimUnitag = (): (( claim: UnitagClaim, context: UnitagClaimContext, - account?: Account + account?: Account, ) => Promise<{ claimError?: string }>) => { const { t } = useTranslation() - const dispatch = useAppDispatch() + const dispatch = useDispatch() const { data: deviceId } = useAsyncData(getUniqueId) const accounts = useAccounts() const signerManager = useWalletSigners() @@ -236,7 +216,7 @@ export const useClaimUnitag = (): (( pushNotification({ type: AppNotificationType.Error, errorMessage: t('unitags.claim.error.avatar'), - }) + }), ) } } @@ -266,8 +246,7 @@ export const useAvatarUploadCredsWithRefresh = ({ avatarUploadUrlResponse?: UnitagGetAvatarUploadUrlResponse } => { const [avatarUploadUrlLoading, setAvatarUploadUrlLoading] = useState(false) - const [avatarUploadUrlResponse, setAvatarUploadUrlResponse] = - useState() + const [avatarUploadUrlResponse, setAvatarUploadUrlResponse] = useState() // Re-fetch the avatar upload pre-signed URL every 110 seconds to ensure it's always fresh useEffect(() => { @@ -297,10 +276,7 @@ export const useAvatarUploadCredsWithRefresh = ({ }) // Set up the interval to refetch creds 10 seconds before expiry - const intervalId = setInterval( - fetchAvatarUploadUrl, - (AVATAR_UPLOAD_CREDS_EXPIRY_SECONDS - 10) * ONE_SECOND_MS - ) + const intervalId = setInterval(fetchAvatarUploadUrl, (AVATAR_UPLOAD_CREDS_EXPIRY_SECONDS - 10) * ONE_SECOND_MS) // Clear the interval on component unmount return () => clearInterval(intervalId) @@ -314,7 +290,7 @@ export const useShowExtensionPromoBanner = (): { loading: boolean showExtensionPromoBanner: boolean } => { - const dispatch = useAppDispatch() + const dispatch = useDispatch() const extensionOnboardingEnabledBeta = useFeatureFlag(FeatureFlags.ExtensionOnboarding) const extensionPromotionGAEnabled = useFeatureFlag(FeatureFlags.ExtensionPromotionGA) const extensionOnboardingState = useAppSelector(selectExtensionOnboardingState) @@ -327,10 +303,7 @@ export const useShowExtensionPromoBanner = (): { !activeAccount || activeAccount.type !== AccountType.SignerMnemonic - const { data, error, loading } = useWaitlistPositionQuery( - [activeAccount?.address || ''], - skipBetaWaitlistQuery - ) + const { data, error, loading } = useWaitlistPositionQuery([activeAccount?.address || ''], skipBetaWaitlistQuery) /** Onboarding completed, skip all checks **/ if (extensionOnboardingState === ExtensionOnboardingState.Completed) { @@ -365,10 +338,7 @@ export const useShowExtensionPromoBanner = (): { const canOnboardToExtensionBeta = data?.isAccepted ?? false - if ( - canOnboardToExtensionBeta && - extensionOnboardingState === ExtensionOnboardingState.Undefined - ) { + if (canOnboardToExtensionBeta && extensionOnboardingState === ExtensionOnboardingState.Undefined) { // Store the information locally so that we don't need to check again during onboarding dispatch(setExtensionOnboardingState(ExtensionOnboardingState.ReadyToOnboard)) } diff --git a/packages/wallet/src/features/unitags/utils.ts b/packages/wallet/src/features/unitags/utils.ts index b292e312fb1..96bb20d41c0 100644 --- a/packages/wallet/src/features/unitags/utils.ts +++ b/packages/wallet/src/features/unitags/utils.ts @@ -1,11 +1,7 @@ import { TFunction } from 'i18next' import { UnitagErrorCodes } from 'uniswap/src/features/unitags/types' -export function parseUnitagErrorCode( - t: TFunction, - unitag: string, - errorCode: UnitagErrorCodes -): string { +export function parseUnitagErrorCode(t: TFunction, unitag: string, errorCode: UnitagErrorCodes): string { switch (errorCode) { case UnitagErrorCodes.UnitagNotAvailable: return t('unitags.claim.error.unavailable') diff --git a/packages/wallet/src/features/wallet/Keyring/Keyring.native.ts b/packages/wallet/src/features/wallet/Keyring/Keyring.native.ts index 4442d843cf7..a1cfb57e6d8 100644 --- a/packages/wallet/src/features/wallet/Keyring/Keyring.native.ts +++ b/packages/wallet/src/features/wallet/Keyring/Keyring.native.ts @@ -14,6 +14,10 @@ const { RNEthersRS } = NativeModules * Simple wrapper around RNEthersRS */ class NativeKeyring implements IKeyring { + removeAllMnemonicsAndPrivateKeys(): Promise { + throw new NotImplementedError('removeAllMnemonicsAndPrivateKeys') + } + isUnlocked(): Promise { throw new NotImplementedError('isUnlocked') } diff --git a/packages/wallet/src/features/wallet/Keyring/Keyring.test.ts b/packages/wallet/src/features/wallet/Keyring/Keyring.test.ts index 9a263dcf1ad..a577fcbab8b 100644 --- a/packages/wallet/src/features/wallet/Keyring/Keyring.test.ts +++ b/packages/wallet/src/features/wallet/Keyring/Keyring.test.ts @@ -33,12 +33,8 @@ jest.mock('./crypto', () => ({ }), generateNewSalt: jest .fn() - .mockReturnValue( - new Uint8Array([190, 197, 42, 2, 229, 18, 122, 161, 234, 166, 219, 110, 247, 102, 197, 214]) - ), - generateNewIV: jest - .fn() - .mockReturnValue(new Uint8Array([142, 65, 15, 198, 69, 200, 74, 43, 159, 8, 170, 46])), + .mockReturnValue(new Uint8Array([190, 197, 42, 2, 229, 18, 122, 161, 234, 166, 219, 110, 247, 102, 197, 214])), + generateNewIV: jest.fn().mockReturnValue(new Uint8Array([142, 65, 15, 198, 69, 200, 74, 43, 159, 8, 170, 46])), })) const mockStore = async ({ data }: { data: Record }): Promise => { @@ -152,4 +148,17 @@ describe(WebKeyring, () => { await expect(action()).rejects.toThrow() }) }) + + describe('removeAllMnemonicsAndPrivateKeys', () => { + it('removes all mnemonics', async () => { + const keyring = new WebKeyring() + await keyring.importMnemonic(SAMPLE_SEED, SAMPLE_PASSWORD) + await keyring.importMnemonic(SAMPLE_SEED, SAMPLE_PASSWORD) + + await keyring.removeAllMnemonicsAndPrivateKeys() + + const allMnemonics = await keyring.getMnemonicIds() + expect(allMnemonics).toEqual([]) + }) + }) }) diff --git a/packages/wallet/src/features/wallet/Keyring/Keyring.ts b/packages/wallet/src/features/wallet/Keyring/Keyring.ts index 7ca3a18d6b1..88e224f0800 100644 --- a/packages/wallet/src/features/wallet/Keyring/Keyring.ts +++ b/packages/wallet/src/features/wallet/Keyring/Keyring.ts @@ -4,6 +4,8 @@ import { NotImplementedError } from 'utilities/src/errors' * Provides the generation, storage, and signing logic for mnemonics and private keys. */ export interface IKeyring { + removeAllMnemonicsAndPrivateKeys(): Promise + /** @returns true if the extension is unlocked (encryption key kept in session storage can unencrypt the mnemonic) */ isUnlocked(): Promise @@ -101,6 +103,10 @@ export interface IKeyring { /** Dummy Keyring implementation. */ class NullKeyring implements IKeyring { + removeAllMnemonicsAndPrivateKeys(): Promise { + throw new Error('Method not implemented.') + } + isUnlocked(): Promise { throw new NotImplementedError('isUnlocked') } @@ -130,11 +136,7 @@ class NullKeyring implements IKeyring { } // returns the mnemonicId (derived address at index 0) of the imported mnemonic - importMnemonic( - _mnemonic: string, - _password?: string, - _allowOverwrite?: boolean - ): Promise { + importMnemonic(_mnemonic: string, _password?: string, _allowOverwrite?: boolean): Promise { throw new NotImplementedError('importMnemonic') } @@ -169,11 +171,7 @@ class NullKeyring implements IKeyring { throw new NotImplementedError('removePrivateKey') } - signTransactionHashForAddress( - _address: string, - _hash: string, - _chainId: number - ): Promise { + signTransactionHashForAddress(_address: string, _hash: string, _chainId: number): Promise { throw new NotImplementedError('signTransactionHashForAddress') } diff --git a/packages/wallet/src/features/wallet/Keyring/__mocks__/Keyring.ts b/packages/wallet/src/features/wallet/Keyring/__mocks__/Keyring.ts index 78f2fb522f5..5233ebf7b06 100644 --- a/packages/wallet/src/features/wallet/Keyring/__mocks__/Keyring.ts +++ b/packages/wallet/src/features/wallet/Keyring/__mocks__/Keyring.ts @@ -9,6 +9,10 @@ const privateKeys: { [id: string]: string } = {} const password = faker.word.noun() class MockKeyring implements IKeyring { + removeAllMnemonicsAndPrivateKeys(): Promise { + return Promise.resolve(false) + } + isUnlocked(): Promise { return Promise.resolve(false) } @@ -86,10 +90,7 @@ class MockKeyring implements IKeyring { return Promise.resolve(true) } - async signTransactionForAddress( - address: string, - transaction: providers.TransactionRequest - ): Promise { + async signTransactionForAddress(address: string, transaction: providers.TransactionRequest): Promise { const privateKey = privateKeys[address] if (!privateKey) { return Promise.reject(`No private key found for ${address}`) diff --git a/packages/wallet/src/features/wallet/Keyring/crypto.ts b/packages/wallet/src/features/wallet/Keyring/crypto.ts index 6d9a975593f..8be7880b945 100644 --- a/packages/wallet/src/features/wallet/Keyring/crypto.ts +++ b/packages/wallet/src/features/wallet/Keyring/crypto.ts @@ -36,12 +36,7 @@ interface EncryptParams { additionalData?: string } // encrypts and returns the cipher text -export async function encrypt({ - plaintext, - encryptionKey, - iv, - additionalData, -}: EncryptParams): Promise { +export async function encrypt({ plaintext, encryptionKey, iv, additionalData }: EncryptParams): Promise { const encoder = new TextEncoder() const ciphertext = await crypto.subtle.encrypt( { @@ -50,7 +45,7 @@ export async function encrypt({ additionalData: encoder.encode(additionalData), }, encryptionKey, - encoder.encode(plaintext) + encoder.encode(plaintext), ) return new Uint8Array(ciphertext).toString() } @@ -80,7 +75,7 @@ export async function decrypt({ additionalData: encoder.encode(additionalData), }, encryptionKey, - ciphertext + ciphertext, ) return decoder.decode(result) } catch (error) { @@ -102,10 +97,7 @@ export async function convertBase64SeedToCryptoKey(keyBase64: string): Promise { +export async function getEncryptionKeyFromPassword(password: string, secretPayload: SecretPayload): Promise { const { name, iterations, hash } = secretPayload const salt = decodeFromStorage(secretPayload.salt) const pbkdf2Params = { salt, name, iterations, hash } @@ -114,13 +106,10 @@ export async function getEncryptionKeyFromPassword( new TextEncoder().encode(password), PBKDF2_PARAMS.name, false, - ['deriveKey'] + ['deriveKey'], ) // TODO: This should use Argon2 like ToB recommended for the mobile app // https://github.com/Uniswap/universe/blob/main/apps/mobile/ios/EncryptionHelper.swift - return crypto.subtle.deriveKey(pbkdf2Params, keyMaterial, AES_GCM_PARAMS, true, [ - 'encrypt', - 'decrypt', - ]) + return crypto.subtle.deriveKey(pbkdf2Params, keyMaterial, AES_GCM_PARAMS, true, ['encrypt', 'decrypt']) } diff --git a/packages/wallet/src/features/wallet/accounts/editAccountSaga.ts b/packages/wallet/src/features/wallet/accounts/editAccountSaga.ts index 4788b19586c..487663735e1 100644 --- a/packages/wallet/src/features/wallet/accounts/editAccountSaga.ts +++ b/packages/wallet/src/features/wallet/accounts/editAccountSaga.ts @@ -4,10 +4,7 @@ import { unique } from 'utilities/src/primitives/array' import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring' import { Account, AccountType, BackupType } from 'wallet/src/features/wallet/accounts/types' import { selectAccounts } from 'wallet/src/features/wallet/selectors' -import { - editAccount as editInStore, - removeAccounts as removeAccountsInStore, -} from 'wallet/src/features/wallet/slice' +import { editAccount as editInStore, removeAccounts as removeAccountsInStore } from 'wallet/src/features/wallet/slice' import { appSelect } from 'wallet/src/state' import { createMonitoredSaga } from 'wallet/src/utils/saga' @@ -107,14 +104,7 @@ function* editAccount(params: EditAccountParams) { function* renameAccount(params: RenameParams, account: Account) { const { newName } = params - logger.debug( - 'editAccountSaga', - 'renameAccount', - 'Renaming account', - account.address, - 'to ', - newName - ) + logger.debug('editAccountSaga', 'renameAccount', 'Renaming account', account.address, 'to ', newName) yield* put( editInStore({ address: account.address, @@ -122,7 +112,7 @@ function* renameAccount(params: RenameParams, account: Account) { ...account, name: newName, }, - }) + }), ) } @@ -144,7 +134,7 @@ function* addBackupMethod(params: AddBackupMethodParams, account: Account) { const accounts = yield* appSelect(selectAccounts) const mnemonicAccounts = Object.values(accounts).filter( - (a) => a.type === AccountType.SignerMnemonic && a.mnemonicId === account.mnemonicId + (a) => a.type === AccountType.SignerMnemonic && a.mnemonicId === account.mnemonicId, ) const updatedBackups: BackupType[] = unique([...(account.backups ?? []), backupMethod]) @@ -157,16 +147,16 @@ function* addBackupMethod(params: AddBackupMethodParams, account: Account) { ...mnemonicAccount, backups: updatedBackups, }, - }) + }), ) - }) + }), ) logger.debug( 'editAccountSaga', 'addBackupMethod', 'Adding backup method', - mnemonicAccounts.map((a) => a.address) + mnemonicAccounts.map((a) => a.address), ) } @@ -180,7 +170,7 @@ function* removeBackupMethod(params: RemoveBackupMethodParams, account: Account) const accounts = yield* appSelect(selectAccounts) const mnemonicAccounts = Object.values(accounts).filter( - (a) => a.type === AccountType.SignerMnemonic && a.mnemonicId === account.mnemonicId + (a) => a.type === AccountType.SignerMnemonic && a.mnemonicId === account.mnemonicId, ) const updatedBackups = account.backups?.filter((backup) => backup !== backupMethod) @@ -194,16 +184,16 @@ function* removeBackupMethod(params: RemoveBackupMethodParams, account: Account) ...mnemonicAccount, backups: updatedBackups, }, - }) + }), ) - }) + }), ) logger.debug( 'editAccountSaga', 'removeBackupMethod', 'Removing backup method', - mnemonicAccounts.map((a) => a.address) + mnemonicAccounts.map((a) => a.address), ) } diff --git a/packages/wallet/src/features/wallet/context.tsx b/packages/wallet/src/features/wallet/context.tsx index 30dc1c505bd..d5b33566a13 100644 --- a/packages/wallet/src/features/wallet/context.tsx +++ b/packages/wallet/src/features/wallet/context.tsx @@ -1,12 +1,5 @@ /* eslint-disable @typescript-eslint/explicit-function-return-type */ -import { - createContext, - PropsWithChildren, - useCallback, - useContext, - useEffect, - useState, -} from 'react' +import { createContext, PropsWithChildren, useCallback, useContext, useEffect, useState } from 'react' import { call, getContext } from 'typed-redux-saga' import { RPCType, WalletChainId } from 'uniswap/src/types/chains' import { logger } from 'utilities/src/logger/logger' @@ -42,11 +35,7 @@ export function WalletContextProvider({ children }: PropsWithChildren): // Probably not strictly necessary but more robust than relying on 'organic' re-renders const [contextVersion, updateContextVersion] = useState(0) const incrementContextVersion = useCallback(() => { - logger.debug( - 'walletContext', - 'WalletContextProvider', - `Context update count: ${contextVersion + 1}` - ) + logger.debug('walletContext', 'WalletContextProvider', `Context update count: ${contextVersion + 1}`) updateContextVersion(contextVersion + 1) }, [contextVersion, updateContextVersion]) useEffect(() => { diff --git a/packages/wallet/src/features/wallet/create/createAccountsSaga.test.ts b/packages/wallet/src/features/wallet/create/createAccountsSaga.test.ts index 0a8c9baaa2c..7ca88550a8b 100644 --- a/packages/wallet/src/features/wallet/create/createAccountsSaga.test.ts +++ b/packages/wallet/src/features/wallet/create/createAccountsSaga.test.ts @@ -1,21 +1,14 @@ import dayjs from 'dayjs' import { expectSaga } from 'redux-saga-test-plan' -import { - Account, - AccountType, - SignerMnemonicAccount, -} from 'wallet/src/features/wallet/accounts/types' -import { - CreateAccountsParams, - createAccounts, -} from 'wallet/src/features/wallet/create/createAccountsSaga' +import { Account, AccountType, SignerMnemonicAccount } from 'wallet/src/features/wallet/accounts/types' +import { CreateAccountsParams, createAccounts } from 'wallet/src/features/wallet/create/createAccountsSaga' import { sharedRootReducer } from 'wallet/src/state/reducer' import { ACCOUNT, ACCOUNT2, ACCOUNT3 } from 'wallet/src/test/fixtures' const createNativeAccounts = async ( payload: CreateAccountsParams, initialAccounts = {}, - timeout = 250 + timeout = 250, ): Promise<{ wallet: { accounts: Account @@ -78,7 +71,7 @@ describe(createAccounts, () => { name: 'READONLY ACCOUNT', timeImportedMs: dayjs().valueOf(), }, - } + }, ) const wallets = state.wallet.accounts @@ -103,7 +96,7 @@ describe(createAccounts, () => { name: 'READONLY ACCOUNT', timeImportedMs: dayjs().valueOf(), }, - } + }, ) const wallets = state.wallet.accounts @@ -123,7 +116,7 @@ describe(createAccounts, () => { { accounts: [ACCOUNT2, ACCOUNT] }, { [ACCOUNT3.address]: ACCOUNT3, - } + }, ) const wallets = state.wallet.accounts @@ -142,7 +135,7 @@ describe(createAccounts, () => { { accounts: [ACCOUNT2, ACCOUNT] }, { [ACCOUNT3.address]: ACCOUNT3, - } + }, ) const wallets = state.wallet.accounts diff --git a/packages/wallet/src/features/wallet/create/createAccountsSaga.ts b/packages/wallet/src/features/wallet/create/createAccountsSaga.ts index eaabe5e7b18..b3533dd256b 100644 --- a/packages/wallet/src/features/wallet/create/createAccountsSaga.ts +++ b/packages/wallet/src/features/wallet/create/createAccountsSaga.ts @@ -20,7 +20,7 @@ export function* createAccounts({ accounts }: CreateAccountsParams) { 'createAccountsSaga', 'createAccount', 'New accounts created:', - accounts.map((acc) => acc.address).join(',') + accounts.map((acc) => acc.address).join(','), ) } diff --git a/packages/wallet/src/features/wallet/getAccountId.ts b/packages/wallet/src/features/wallet/getAccountId.ts index 124be3b83e1..f018dc52454 100644 --- a/packages/wallet/src/features/wallet/getAccountId.ts +++ b/packages/wallet/src/features/wallet/getAccountId.ts @@ -1,4 +1,4 @@ -import { AddressStringFormat, normalizeAddress } from 'wallet/src/utils/addresses' +import { AddressStringFormat, normalizeAddress } from 'uniswap/src/utils/addresses' export function getAccountId(address: Address): string { return normalizeAddress(address, AddressStringFormat.Lowercase) diff --git a/packages/wallet/src/features/wallet/hooks.ts b/packages/wallet/src/features/wallet/hooks.ts index 3ac746de211..5a2df8231af 100644 --- a/packages/wallet/src/features/wallet/hooks.ts +++ b/packages/wallet/src/features/wallet/hooks.ts @@ -1,15 +1,12 @@ import { useMemo, useRef } from 'react' import { useUnitagByAddress } from 'uniswap/src/features/unitags/hooks' +import { getValidAddress, sanitizeAddressText, shortenAddress } from 'uniswap/src/utils/addresses' import { trimToLength } from 'utilities/src/primitives/string' import { useENSAvatar, useENSName } from 'wallet/src/features/ens/api' import useIsFocused from 'wallet/src/features/focus/useIsFocused' import { useOnboardingContext } from 'wallet/src/features/onboarding/OnboardingContext' import { UNITAG_SUFFIX } from 'wallet/src/features/unitags/constants' -import { - Account, - AccountType, - SignerMnemonicAccount, -} from 'wallet/src/features/wallet/accounts/types' +import { Account, AccountType, SignerMnemonicAccount } from 'wallet/src/features/wallet/accounts/types' import { makeSelectAccountNotificationSetting, selectAccounts, @@ -25,7 +22,6 @@ import { import { SwapProtectionSetting } from 'wallet/src/features/wallet/slice' import { DisplayName, DisplayNameType } from 'wallet/src/features/wallet/types' import { useAppSelector } from 'wallet/src/state' -import { getValidAddress, sanitizeAddressText, shortenAddress } from 'wallet/src/utils/addresses' const ENS_TRIM_LENGTH = 8 @@ -137,10 +133,7 @@ type DisplayNameOptions = { * @param options.includeUnitagSuffix - Whether to include the unitag suffix (.uni.eth) in returned unitag name * @param options.showLocalName - Whether to show the local wallet name */ -export function useDisplayName( - address: Maybe, - options?: DisplayNameOptions -): DisplayName | undefined { +export function useDisplayName(address: Maybe, options?: DisplayNameOptions): DisplayName | undefined { const defaultOptions = { showShortenedEns: false, includeUnitagSuffix: false, @@ -165,9 +158,7 @@ export function useDisplayName( if (overrideDisplayName) { return { - name: showShortenedEns - ? trimToLength(overrideDisplayName, ENS_TRIM_LENGTH) - : overrideDisplayName, + name: showShortenedEns ? trimToLength(overrideDisplayName, ENS_TRIM_LENGTH) : overrideDisplayName, type: DisplayNameType.ENS, } } diff --git a/packages/wallet/src/features/wallet/selectors.ts b/packages/wallet/src/features/wallet/selectors.ts index 4d27129e773..31a4b0a76aa 100644 --- a/packages/wallet/src/features/wallet/selectors.ts +++ b/packages/wallet/src/features/wallet/selectors.ts @@ -1,11 +1,6 @@ import { createSelector, Selector } from '@reduxjs/toolkit' import { TokenSortableField } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { - Account, - AccountType, - ReadOnlyAccount, - SignerMnemonicAccount, -} from 'wallet/src/features/wallet/accounts/types' +import { Account, AccountType, ReadOnlyAccount, SignerMnemonicAccount } from 'wallet/src/features/wallet/accounts/types' import { SwapProtectionSetting } from 'wallet/src/features/wallet/slice' import { TokensOrderBy } from 'wallet/src/features/wallet/types' import type { RootState } from 'wallet/src/state' @@ -15,32 +10,24 @@ const DEFAULT_TOKENS_ORDER_BY = TokenSortableField.Volume export const selectAccounts = (state: RootState): Record => state.wallet.accounts export const selectSignerMnemonicAccounts = createSelector(selectAccounts, (accounts) => - Object.values(accounts).filter( - (a): a is SignerMnemonicAccount => a.type === AccountType.SignerMnemonic - ) + Object.values(accounts).filter((a): a is SignerMnemonicAccount => a.type === AccountType.SignerMnemonic), ) -export const selectSortedSignerMnemonicAccounts = createSelector( - selectSignerMnemonicAccounts, - (accounts) => - accounts.sort( - (a, b) => - (a as SignerMnemonicAccount).derivationIndex - (b as SignerMnemonicAccount).derivationIndex - ) +export const selectSortedSignerMnemonicAccounts = createSelector(selectSignerMnemonicAccounts, (accounts) => + accounts.sort((a, b) => (a as SignerMnemonicAccount).derivationIndex - (b as SignerMnemonicAccount).derivationIndex), ) export const selectSignerMnemonicAccountExists = createSelector( selectAccounts, - (accounts) => - Object.values(accounts).findIndex((value) => value.type === AccountType.SignerMnemonic) >= 0 + (accounts) => Object.values(accounts).findIndex((value) => value.type === AccountType.SignerMnemonic) >= 0, ) export const selectViewOnlyAccounts = createSelector(selectAccounts, (accounts) => - Object.values(accounts).filter((a): a is ReadOnlyAccount => a.type === AccountType.Readonly) + Object.values(accounts).filter((a): a is ReadOnlyAccount => a.type === AccountType.Readonly), ) export const selectSortedViewOnlyAccounts = createSelector(selectViewOnlyAccounts, (accounts) => - accounts.sort((a, b) => a.timeImportedMs - b.timeImportedMs) + accounts.sort((a, b) => a.timeImportedMs - b.timeImportedMs), ) // Sorted signer accounts, then sorted view-only accounts @@ -49,20 +36,17 @@ export const selectAllAccountsSorted = createSelector( selectSortedViewOnlyAccounts, (signerMnemonicAccounts, viewOnlyAccounts) => { return [...signerMnemonicAccounts, ...viewOnlyAccounts] - } + }, ) -export const selectActiveAccountAddress = (state: RootState): string | null => - state.wallet.activeAccountAddress +export const selectActiveAccountAddress = (state: RootState): string | null => state.wallet.activeAccountAddress export const selectActiveAccount = createSelector( selectAccounts, selectActiveAccountAddress, - (accounts, activeAccountAddress) => - (activeAccountAddress ? accounts[activeAccountAddress] : null) ?? null + (accounts, activeAccountAddress) => (activeAccountAddress ? accounts[activeAccountAddress] : null) ?? null, ) -export const selectFinishedOnboarding = (state: RootState): boolean | undefined => - state.wallet.finishedOnboarding +export const selectFinishedOnboarding = (state: RootState): boolean | undefined => state.wallet.finishedOnboarding export const selectTokensOrderBy = (state: RootState): TokensOrderBy => state.wallet.settings.tokensOrderBy ?? DEFAULT_TOKENS_ORDER_BY @@ -70,26 +54,24 @@ export const selectTokensOrderBy = (state: RootState): TokensOrderBy => export const selectInactiveAccounts = createSelector( selectActiveAccountAddress, selectAccounts, - (activeAddress, accounts) => - Object.values(accounts).filter((account) => account.address !== activeAddress) + (activeAddress, accounts) => Object.values(accounts).filter((account) => account.address !== activeAddress), ) export const makeSelectAccountNotificationSetting = (): Selector => createSelector( selectAccounts, (_: RootState, address: Address) => address, - (accounts, address) => !!accounts[address]?.pushNotificationsEnabled + (accounts, address) => !!accounts[address]?.pushNotificationsEnabled, ) export const selectAnyAddressHasNotificationsEnabled = createSelector(selectAccounts, (accounts) => - Object.values(accounts).some((account) => account.pushNotificationsEnabled) + Object.values(accounts).some((account) => account.pushNotificationsEnabled), ) export const selectWalletHideSmallBalancesSetting = (state: RootState): boolean => state.wallet.settings.hideSmallBalances -export const selectWalletHideSpamTokensSetting = (state: RootState): boolean => - state.wallet.settings.hideSpamTokens +export const selectWalletHideSpamTokensSetting = (state: RootState): boolean => state.wallet.settings.hideSpamTokens export const selectWalletSwapProtectionSetting = (state: RootState): SwapProtectionSetting => state.wallet.settings.swapProtection diff --git a/packages/wallet/src/features/wallet/signing/NativeSigner.native.ts b/packages/wallet/src/features/wallet/signing/NativeSigner.native.ts index 71d7868f046..84f6cfa1e06 100644 --- a/packages/wallet/src/features/wallet/signing/NativeSigner.native.ts +++ b/packages/wallet/src/features/wallet/signing/NativeSigner.native.ts @@ -2,10 +2,10 @@ import { TypedDataDomain, TypedDataField } from '@ethersproject/abstract-signer' import { _TypedDataEncoder } from '@ethersproject/hash' import { Bytes, Signer, UnsignedTransaction, providers, utils } from 'ethers' import { hexlify } from 'ethers/lib/utils' +import { toSupportedChainId } from 'uniswap/src/features/chains/utils' import { UniverseChainId } from 'uniswap/src/types/chains' -import { toSupportedChainId } from 'wallet/src/features/chains/utils' +import { areAddressesEqual } from 'uniswap/src/utils/addresses' import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring.native' -import { areAddressesEqual } from 'wallet/src/utils/addresses' // A signer that uses native keystore to access keys export class NativeSigner extends Signer { @@ -33,23 +33,19 @@ export class NativeSigner extends Signer { } // chainID isn't available here, but is not needed for signing hashes so just default to Mainnet - return Keyring.signHashForAddress( - this.address, - hexlify(message).slice(2), - UniverseChainId.Mainnet - ) + return Keyring.signHashForAddress(this.address, hexlify(message).slice(2), UniverseChainId.Mainnet) } // reference: https://github.com/ethers-io/ethers.js/blob/ce8f1e4015c0f27bf178238770b1325136e3351a/packages/wallet/src.ts/index.ts#L135 async _signTypedData( domain: TypedDataDomain, types: Record>, - value: Record + value: Record, ): Promise { const signature = await Keyring.signHashForAddress( this.address, _TypedDataEncoder.hash(domain, types, value).slice(2), - toSupportedChainId(domain.chainId) || UniverseChainId.Mainnet + toSupportedChainId(domain.chainId) || UniverseChainId.Mainnet, ) return signature } @@ -68,11 +64,7 @@ export class NativeSigner extends Signer { const ut = tx const hashedTx = utils.keccak256(utils.serializeTransaction(ut)) - const signature = await Keyring.signTransactionHashForAddress( - this.address, - hashedTx.slice(2), - tx.chainId - ) + const signature = await Keyring.signTransactionHashForAddress(this.address, hashedTx.slice(2), tx.chainId) return utils.serializeTransaction(ut, `0x${signature}`) } diff --git a/packages/wallet/src/features/wallet/signing/NativeSigner.ts b/packages/wallet/src/features/wallet/signing/NativeSigner.ts index fa9612b844e..bf5019a5f08 100644 --- a/packages/wallet/src/features/wallet/signing/NativeSigner.ts +++ b/packages/wallet/src/features/wallet/signing/NativeSigner.ts @@ -1,10 +1,10 @@ import { TypedDataDomain, TypedDataField } from '@ethersproject/abstract-signer' import { _TypedDataEncoder } from '@ethersproject/hash' import { Signer, UnsignedTransaction, providers, utils } from 'ethers' +import { toSupportedChainId } from 'uniswap/src/features/chains/utils' import { UniverseChainId } from 'uniswap/src/types/chains' -import { toSupportedChainId } from 'wallet/src/features/chains/utils' +import { areAddressesEqual } from 'uniswap/src/utils/addresses' import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring' -import { areAddressesEqual } from 'wallet/src/utils/addresses' /** * A signer that uses a native keyring to access keys @@ -12,7 +12,10 @@ import { areAddressesEqual } from 'wallet/src/utils/addresses' */ export class NativeSigner extends Signer { - constructor(private readonly address: string, provider?: providers.Provider) { + constructor( + private readonly address: string, + provider?: providers.Provider, + ) { super() if (provider && !providers.Provider.isProvider(provider)) { @@ -34,12 +37,12 @@ export class NativeSigner extends Signer { async _signTypedData( domain: TypedDataDomain, types: Record>, - value: Record + value: Record, ): Promise { const signature = await Keyring.signHashForAddress( this.address, _TypedDataEncoder.hash(domain, types, value), - toSupportedChainId(domain.chainId) || UniverseChainId.Mainnet + toSupportedChainId(domain.chainId) || UniverseChainId.Mainnet, ) return signature } @@ -63,7 +66,7 @@ export class NativeSigner extends Signer { const signature = await Keyring.signTransactionHashForAddress( this.address, hashedTx, - tx.chainId || UniverseChainId.Mainnet + tx.chainId || UniverseChainId.Mainnet, ) return utils.serializeTransaction(ut, signature) diff --git a/packages/wallet/src/features/wallet/signing/signing.native.ts b/packages/wallet/src/features/wallet/signing/signing.native.ts index f83e6d7fca3..4ad6aadc8a3 100644 --- a/packages/wallet/src/features/wallet/signing/signing.native.ts +++ b/packages/wallet/src/features/wallet/signing/signing.native.ts @@ -2,18 +2,14 @@ import { ethers, TypedDataDomain, TypedDataField, Wallet } from 'ethers' import { arrayify, isHexString } from 'ethers/lib/utils' +import { ensureLeading0x } from 'uniswap/src/utils/addresses' import { Account } from 'wallet/src/features/wallet/accounts/types' import { NativeSigner } from 'wallet/src/features/wallet/signing/NativeSigner' import { SignerManager } from 'wallet/src/features/wallet/signing/SignerManager' import { EthTypedMessage } from 'wallet/src/features/wallet/signing/types' -import { ensureLeading0x } from 'wallet/src/utils/addresses' // https://docs.ethers.io/v5/api/signer/#Signer--signing-methods -export async function signMessage( - message: string, - account: Account, - signerManager: SignerManager -): Promise { +export async function signMessage(message: string, account: Account, signerManager: SignerManager): Promise { const signer = await signerManager.getSignerForAccount(account) const formattedMessage = isHexString(message) ? arrayify(message) : message const signature = await signer.signMessage(formattedMessage) @@ -25,7 +21,7 @@ export async function signTypedData( types: Record, value: Record, account: Account, - signerManager: SignerManager + signerManager: SignerManager, ): Promise { const signer = await signerManager.getSignerForAccount(account) @@ -43,7 +39,7 @@ export async function signTypedDataMessage( message: string, account: Account, signerManager: SignerManager, - _provider?: ethers.providers.JsonRpcProvider + _provider?: ethers.providers.JsonRpcProvider, ): Promise { const parsedData: EthTypedMessage = JSON.parse(message) // ethers computes EIP712Domain type for you, so we should not pass it in directly @@ -51,11 +47,5 @@ export async function signTypedDataMessage( // https://github.com/ethers-io/ethers.js/issues/687#issuecomment-714069471 delete parsedData.types.EIP712Domain - return signTypedData( - parsedData.domain, - parsedData.types, - parsedData.message, - account, - signerManager - ) + return signTypedData(parsedData.domain, parsedData.types, parsedData.message, account, signerManager) } diff --git a/packages/wallet/src/features/wallet/signing/signing.ts b/packages/wallet/src/features/wallet/signing/signing.ts index 0296c5f7027..bf22a52120d 100644 --- a/packages/wallet/src/features/wallet/signing/signing.ts +++ b/packages/wallet/src/features/wallet/signing/signing.ts @@ -2,18 +2,18 @@ import { ethers, TypedDataDomain, TypedDataField, Wallet } from 'ethers' import { arrayify, isHexString } from 'ethers/lib/utils' +import { ensureLeading0x } from 'uniswap/src/utils/addresses' import { Account } from 'wallet/src/features/wallet/accounts/types' import { NativeSigner } from 'wallet/src/features/wallet/signing/NativeSigner' import { SignerManager } from 'wallet/src/features/wallet/signing/SignerManager' import { EthTypedMessage } from 'wallet/src/features/wallet/signing/types' -import { ensureLeading0x } from 'wallet/src/utils/addresses' // https://docs.ethers.io/v5/api/signer/#Signer--signing-methods export async function signMessage( message: string, account: Account, signerManager: SignerManager, - provider?: ethers.providers.JsonRpcProvider + provider?: ethers.providers.JsonRpcProvider, ): Promise { // Mobile code does not explicitly connect to provider, // Web needs to connect to provider to ensure correct chain @@ -32,7 +32,7 @@ export async function signTypedData( value: Record, account: Account, signerManager: SignerManager, - provider?: ethers.providers.JsonRpcProvider + provider?: ethers.providers.JsonRpcProvider, ): Promise { // Mobile code does not explicitly connect to provider, // Web needs to connect to provider to ensure correct chain @@ -53,7 +53,7 @@ export async function signTypedDataMessage( message: string, account: Account, signerManager: SignerManager, - provider?: ethers.providers.JsonRpcProvider + provider?: ethers.providers.JsonRpcProvider, ): Promise { const parsedData: EthTypedMessage = JSON.parse(message) // ethers computes EIP712Domain type for you, so we should not pass it in directly @@ -61,12 +61,5 @@ export async function signTypedDataMessage( // https://github.com/ethers-io/ethers.js/issues/687#issuecomment-714069471 delete parsedData.types.EIP712Domain - return signTypedData( - parsedData.domain, - parsedData.types, - parsedData.message, - account, - signerManager, - provider - ) + return signTypedData(parsedData.domain, parsedData.types, parsedData.message, account, signerManager, provider) } diff --git a/packages/wallet/src/features/wallet/slice.test.ts b/packages/wallet/src/features/wallet/slice.test.ts index 3e52aff2e67..31178c0352b 100644 --- a/packages/wallet/src/features/wallet/slice.test.ts +++ b/packages/wallet/src/features/wallet/slice.test.ts @@ -51,7 +51,7 @@ describe(walletReducer, () => { it('throws when setting unknown active account', () => { expect(() => store.dispatch(setAccountAsActive(ACCOUNT_1.address))).toThrow( - `Cannot activate missing account ${ACCOUNT_1.address}` + `Cannot activate missing account ${ACCOUNT_1.address}`, ) }) @@ -89,13 +89,13 @@ describe(walletReducer, () => { it('throws when removing unknown active account', () => { expect(() => store.dispatch(removeAccounts([ACCOUNT_1.address]))).toThrow( - `Cannot remove missing account ${ACCOUNT_1.address}` + `Cannot remove missing account ${ACCOUNT_1.address}`, ) }) it('throws when removing unknown active accounts', () => { expect(() => store.dispatch(removeAccounts([ACCOUNT_1.address, ACCOUNT_2.address]))).toThrow( - `Cannot remove missing account ${ACCOUNT_1.address}` + `Cannot remove missing account ${ACCOUNT_1.address}`, ) }) }) diff --git a/packages/wallet/src/features/wallet/slice.ts b/packages/wallet/src/features/wallet/slice.ts index ab34bd2b3d4..14f0dc9a72d 100644 --- a/packages/wallet/src/features/wallet/slice.ts +++ b/packages/wallet/src/features/wallet/slice.ts @@ -1,7 +1,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' +import { areAddressesEqual, getValidAddress } from 'uniswap/src/utils/addresses' import { Account } from 'wallet/src/features/wallet/accounts/types' import { NFTViewType, TokensOrderBy } from 'wallet/src/features/wallet/types' -import { areAddressesEqual, getValidAddress } from 'wallet/src/utils/addresses' export const HIDE_SMALL_USD_BALANCES_THRESHOLD = 1 @@ -80,9 +80,7 @@ const slice = createSlice({ // Reset active account to first account if currently active account is deleted if ( state.activeAccountAddress && - addressesToRemove.some((addressToRemove) => - areAddressesEqual(addressToRemove, state.activeAccountAddress) - ) + addressesToRemove.some((addressToRemove) => areAddressesEqual(addressToRemove, state.activeAccountAddress)) ) { const firstAccountId = Object.keys(state.accounts)[0] state.activeAccountAddress = firstAccountId ?? null @@ -112,7 +110,7 @@ const slice = createSlice({ }, setFinishedOnboarding: ( state, - { payload: { finishedOnboarding } }: PayloadAction<{ finishedOnboarding: boolean }> + { payload: { finishedOnboarding } }: PayloadAction<{ finishedOnboarding: boolean }>, ) => { state.finishedOnboarding = finishedOnboarding }, @@ -121,15 +119,13 @@ const slice = createSlice({ }, setTokensOrderBy: ( state, - { payload: { newTokensOrderBy } }: PayloadAction<{ newTokensOrderBy: TokensOrderBy }> + { payload: { newTokensOrderBy } }: PayloadAction<{ newTokensOrderBy: TokensOrderBy }>, ) => { state.settings.tokensOrderBy = newTokensOrderBy }, setSwapProtectionSetting: ( state, - { - payload: { newSwapProtectionSetting }, - }: PayloadAction<{ newSwapProtectionSetting: SwapProtectionSetting }> + { payload: { newSwapProtectionSetting } }: PayloadAction<{ newSwapProtectionSetting: SwapProtectionSetting }>, ) => { state.settings.swapProtection = newSwapProtectionSetting }, @@ -143,7 +139,7 @@ const slice = createSlice({ state, { payload: { ratingProvided, feedbackProvided }, - }: PayloadAction<{ ratingProvided?: boolean; feedbackProvided?: boolean }> + }: PayloadAction<{ ratingProvided?: boolean; feedbackProvided?: boolean }>, ) => { state.appRatingPromptedMs = Date.now() diff --git a/packages/wallet/src/provider/tamagui-provider.tsx b/packages/wallet/src/provider/tamagui-provider.tsx index 0cc97de0183..0593b4fbf1b 100644 --- a/packages/wallet/src/provider/tamagui-provider.tsx +++ b/packages/wallet/src/provider/tamagui-provider.tsx @@ -1,18 +1,16 @@ import { TamaguiProvider as OGTamaguiProvider, TamaguiProviderProps } from 'ui/src' import { config } from 'ui/src/tamagui.config' +import { isTestEnv } from 'utilities/src/environment' import { useSelectedColorScheme } from 'wallet/src/features/appearance/hooks' // without // this exported Provider is useful for tests -export function TamaguiProvider({ - children, - ...rest -}: Omit): JSX.Element { +export function TamaguiProvider({ children, ...rest }: Omit): JSX.Element { // because we dont always want to wrap all of redux for visual tests, make it default to false if in test mode // this should be done better but release needs hotfix so for now it works const userSelectedColorScheme = useSelectedColorScheme() - const isDark = process.env.NODE_ENV === 'test' ? false : userSelectedColorScheme === 'dark' + const isDark = isTestEnv() ? false : userSelectedColorScheme === 'dark' return ( diff --git a/packages/wallet/src/state/README.md b/packages/wallet/src/state/README.md index 807789159c0..be58696801c 100644 --- a/packages/wallet/src/state/README.md +++ b/packages/wallet/src/state/README.md @@ -14,7 +14,7 @@ Anytime a required property is added or any property is renamed or deleted to/fr ### How to migrate -1. Increment the `MOBILE_STATE_VERSION` and/or `EXTENSION_STATE_VERSION` of `persistConfig` defined in `mobile/src/app/migrations.ts` and `stretch/src/store/migrations.ts`. +1. Increment the `MOBILE_STATE_VERSION` and/or `EXTENSION_STATE_VERSION` of `persistConfig` defined in `mobile/src/app/migrations.ts` and `extension/src/store/migrations.ts`. 2. Create a migration function within `migrations.ts`. The migration key should be the same as the `version` defined in the previous step. 3. Write a test for your each of your migrations within `migrations.test.ts` (you will need to write separate tests for each app). 4. Create a new schema within `schema.ts` for each app and ensure it is being exported by the `getSchema` function at the bottom of the file. diff --git a/packages/wallet/src/state/createMigrate.ts b/packages/wallet/src/state/createMigrate.ts index 7ead403b32f..fda9e9eb840 100644 --- a/packages/wallet/src/state/createMigrate.ts +++ b/packages/wallet/src/state/createMigrate.ts @@ -4,7 +4,7 @@ import { DEFAULT_VERSION } from 'redux-persist/es/constants' import { logger } from 'utilities/src/logger/logger' export function createMigrate( - migrations: MigrationManifest + migrations: MigrationManifest, ): (state: PersistedState, currentVersion: number) => Promise { return function (state: PersistedState, currentVersion: number): Promise { try { @@ -16,11 +16,7 @@ export function createMigrate( const inboundVersion: number = state._persist?.version ?? DEFAULT_VERSION if (inboundVersion === currentVersion) { - logger.debug( - 'redux-persist', - 'createMigrate', - `versions match (${currentVersion}), noop migration` - ) + logger.debug('redux-persist', 'createMigrate', `versions match (${currentVersion}), noop migration`) return Promise.resolve(state) } @@ -36,19 +32,12 @@ export function createMigrate( logger.debug('redux-persist', 'createMigrate', `migrationKeys: ${migrationKeys}`) - const migratedState: PersistedState = migrationKeys.reduce( - (versionState: PersistedState, versionKey) => { - logger.debug( - 'redux-persist', - 'createMigrate', - `running migration for versionKey: ${versionKey}` - ) - // Safe non-null assertion because `versionKey` comes from `Object.keys(migrations)` - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return migrations[versionKey]!(versionState) - }, - state - ) + const migratedState: PersistedState = migrationKeys.reduce((versionState: PersistedState, versionKey) => { + logger.debug('redux-persist', 'createMigrate', `running migration for versionKey: ${versionKey}`) + // Safe non-null assertion because `versionKey` comes from `Object.keys(migrations)` + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return migrations[versionKey]!(versionState) + }, state) return Promise.resolve(migratedState) } catch (error) { diff --git a/packages/wallet/src/state/index.ts b/packages/wallet/src/state/index.ts index 3d4aa4c06a5..ccd9c989cd4 100644 --- a/packages/wallet/src/state/index.ts +++ b/packages/wallet/src/state/index.ts @@ -1,6 +1,6 @@ import type { Middleware, PreloadedState, Reducer, StoreEnhancer } from '@reduxjs/toolkit' import { configureStore } from '@reduxjs/toolkit' -import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux' +import { TypedUseSelectorHook, useSelector } from 'react-redux' import { PersistState } from 'redux-persist' import createSagaMiddleware, { Saga } from 'redux-saga' import { SagaGenerator, select } from 'typed-redux-saga' @@ -76,10 +76,8 @@ export function createStore({ export type RootState = ReturnType & { saga: Record } & { _persist?: PersistState } -export type AppDispatch = ReturnType['dispatch'] export type AppSelector = (state: RootState) => T -export const useAppDispatch: () => AppDispatch = useDispatch export const useAppSelector: TypedUseSelectorHook = useSelector // Use in sagas for better typing when selecting from redux state diff --git a/packages/wallet/src/state/reducer.ts b/packages/wallet/src/state/reducer.ts index 856aeb3c079..107d2ef5bb8 100644 --- a/packages/wallet/src/state/reducer.ts +++ b/packages/wallet/src/state/reducer.ts @@ -1,10 +1,9 @@ import { combineReducers } from 'redux' -import { fiatOnRampAggregatorApi as sharedFiatOnRampAggregatorApi } from 'uniswap/src/features/fiatOnRamp/api' +import { fiatOnRampAggregatorApi } from 'uniswap/src/features/fiatOnRamp/api' import { appearanceSettingsReducer } from 'wallet/src/features/appearance/slice' import { behaviorHistoryReducer } from 'wallet/src/features/behaviorHistory/slice' import { favoritesReducer } from 'wallet/src/features/favorites/slice' import { fiatCurrencySettingsReducer } from 'wallet/src/features/fiatCurrency/slice' -import { fiatOnRampAggregatorApi, fiatOnRampApi } from 'wallet/src/features/fiatOnRamp/api' import { languageSettingsReducer } from 'wallet/src/features/language/slice' import { notificationReducer } from 'wallet/src/features/notifications/slice' import { searchHistoryReducer } from 'wallet/src/features/search/searchHistorySlice' @@ -15,9 +14,7 @@ import { transactionReducer } from 'wallet/src/features/transactions/slice' import { walletReducer } from 'wallet/src/features/wallet/slice' export const sharedReducers = { - [fiatOnRampApi.reducerPath]: fiatOnRampApi.reducer, [fiatOnRampAggregatorApi.reducerPath]: fiatOnRampAggregatorApi.reducer, - [sharedFiatOnRampAggregatorApi.reducerPath]: sharedFiatOnRampAggregatorApi.reducer, appearanceSettings: appearanceSettingsReducer, behaviorHistory: behaviorHistoryReducer, favorites: favoritesReducer, diff --git a/packages/wallet/src/state/saga.ts b/packages/wallet/src/state/saga.ts index d1336e138ee..0b5881bcd51 100644 --- a/packages/wallet/src/state/saga.ts +++ b/packages/wallet/src/state/saga.ts @@ -21,19 +21,14 @@ export interface MonitoredSaga { export type MonitoredSagaReducer = Reducer> -export function getMonitoredSagaReducers( - monitoredSagas: Record -): MonitoredSagaReducer { +export function getMonitoredSagaReducers(monitoredSagas: Record): MonitoredSagaReducer { return combineReducers( - Object.keys(monitoredSagas).reduce( - (acc: { [name: string]: Reducer }, sagaName: string) => { - // Safe non-null assertion because key `sagaName` comes from `Object.keys(monitoredSagas)` - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - acc[sagaName] = monitoredSagas[sagaName]!.reducer - return acc - }, - {} - ) + Object.keys(monitoredSagas).reduce((acc: { [name: string]: Reducer }, sagaName: string) => { + // Safe non-null assertion because key `sagaName` comes from `Object.keys(monitoredSagas)` + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + acc[sagaName] = monitoredSagas[sagaName]!.reducer + return acc + }, {}), ) } diff --git a/packages/wallet/src/state/sharedMigrations.ts b/packages/wallet/src/state/sharedMigrations.ts index 70285cb87ae..ba98dd43aea 100644 --- a/packages/wallet/src/state/sharedMigrations.ts +++ b/packages/wallet/src/state/sharedMigrations.ts @@ -81,7 +81,7 @@ export function activatePendingAccounts(state: any): any { (account: any) => account.type === AccountType.SignerMnemonic && account.mnemonicId === activeAccount.mnemonicId && - account.pending === true + account.pending === true, ) if (activeSignerAccountPendingWallets.length > MAX_WALLET_IMPORT) { diff --git a/packages/wallet/src/state/sharedMigrationsTests.ts b/packages/wallet/src/state/sharedMigrationsTests.ts index cc6f3f9d8f8..912291930de 100644 --- a/packages/wallet/src/state/sharedMigrationsTests.ts +++ b/packages/wallet/src/state/sharedMigrationsTests.ts @@ -492,12 +492,10 @@ export function testActivatePendingAccounts(migration: (state: any) => any, prev } const Schema8PendingAccountsActiveAddressInTheMiddleMigrated = migration( - Schema8PendingAccountsActiveAddressInTheMiddle + Schema8PendingAccountsActiveAddressInTheMiddle, ) - expect( - Object.keys(Schema8PendingAccountsActiveAddressInTheMiddleMigrated.wallet.accounts) - ).toIncludeSameMembers([ + expect(Object.keys(Schema8PendingAccountsActiveAddressInTheMiddleMigrated.wallet.accounts)).toIncludeSameMembers([ '0xTest0', '0xTest1', '0xTest2', @@ -507,12 +505,8 @@ export function testActivatePendingAccounts(migration: (state: any) => any, prev '0xTest7', '0xTest8', ]) - Object.values(Schema8PendingAccountsActiveAddressInTheMiddleMigrated.wallet.accounts).forEach( - (acc: any) => { - expect(acc.pending).toBeUndefined() - } - ) - expect(Schema8PendingAccountsActiveAddressInTheMiddleMigrated.wallet.activeAccountAddress).toBe( - '0xTest5' - ) + Object.values(Schema8PendingAccountsActiveAddressInTheMiddleMigrated.wallet.accounts).forEach((acc: any) => { + expect(acc.pending).toBeUndefined() + }) + expect(Schema8PendingAccountsActiveAddressInTheMiddleMigrated.wallet.activeAccountAddress).toBe('0xTest5') } diff --git a/packages/wallet/src/state/testUtils.ts b/packages/wallet/src/state/testUtils.ts index 26fe674f175..8c136f22ea8 100644 --- a/packages/wallet/src/state/testUtils.ts +++ b/packages/wallet/src/state/testUtils.ts @@ -10,16 +10,10 @@ export const getAllKeysOfNestedObject = (obj: Record, prefix = return [...res] } - if ( - typeof (obj as Record)[el] === 'object' && - (obj as Record)[el] !== null - ) { + if (typeof (obj as Record)[el] === 'object' && (obj as Record)[el] !== null) { return [ ...res, - ...getAllKeysOfNestedObject( - (obj as Record)[el] as Record, - prefix + el + '.' - ), + ...getAllKeysOfNestedObject((obj as Record)[el] as Record, prefix + el + '.'), ] } diff --git a/packages/wallet/src/test/fixtures/gql/activities/index.ts b/packages/wallet/src/test/fixtures/gql/activities/index.ts index 8da5c0936ab..b40c83ce496 100644 --- a/packages/wallet/src/test/fixtures/gql/activities/index.ts +++ b/packages/wallet/src/test/fixtures/gql/activities/index.ts @@ -6,6 +6,8 @@ import { TransactionStatus, TransactionType, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { MAX_FIXTURE_TIMESTAMP, faker } from 'uniswap/src/test/shared' +import { createFixture } from 'uniswap/src/test/utils' import { erc20ApproveAssetChange, erc20TokenTransferOut, @@ -13,8 +15,7 @@ import { } from 'wallet/src/test/fixtures/gql/activities/tokens' import { GQL_CHAINS } from 'wallet/src/test/fixtures/gql/misc' import { gqlTransaction, gqlTransactionDetails } from 'wallet/src/test/fixtures/gql/transactions' -import { MAX_FIXTURE_TIMESTAMP, faker } from 'wallet/src/test/shared' -import { createFixture, randomChoice, randomEnumValue } from 'wallet/src/test/utils' +import { randomChoice, randomEnumValue } from 'wallet/src/test/utils' export * from './nfts' export * from './swap' export * from './tokens' @@ -50,7 +51,7 @@ export const approveAssetActivity = createFixture()(() => transactionStatus: TransactionStatus.Confirmed, assetChanges: [erc20ApproveAssetChange()], }), - }) + }), ) export const erc20SwapAssetActivity = createFixture()(() => @@ -63,7 +64,7 @@ export const erc20SwapAssetActivity = createFixture()(() => transactionStatus: TransactionStatus.Confirmed, assetChanges: [erc20TransferIn(), erc20TokenTransferOut()], }), - }) + }), ) export const erc20ReceiveAssetActivity = createFixture()(() => @@ -76,5 +77,5 @@ export const erc20ReceiveAssetActivity = createFixture()(() => transactionStatus: TransactionStatus.Confirmed, assetChanges: [erc20TransferIn()], }), - }) + }), ) diff --git a/packages/wallet/src/test/fixtures/gql/activities/nfts.ts b/packages/wallet/src/test/fixtures/gql/activities/nfts.ts index f35692f1de1..821a59d4505 100644 --- a/packages/wallet/src/test/fixtures/gql/activities/nfts.ts +++ b/packages/wallet/src/test/fixtures/gql/activities/nfts.ts @@ -5,9 +5,10 @@ import { NftTransfer, TransactionDirection, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { faker } from 'uniswap/src/test/shared' +import { createFixture } from 'uniswap/src/test/utils' import { nftAsset } from 'wallet/src/test/fixtures/gql/assets' -import { faker } from 'wallet/src/test/shared' -import { createFixture, randomEnumValue } from 'wallet/src/test/utils' +import { randomEnumValue } from 'wallet/src/test/utils' export const nftApproval = createFixture()(() => ({ __typename: 'NftApproval', diff --git a/packages/wallet/src/test/fixtures/gql/activities/swap.ts b/packages/wallet/src/test/fixtures/gql/activities/swap.ts index 3a8c8fefc0b..bf7f2caadfe 100644 --- a/packages/wallet/src/test/fixtures/gql/activities/swap.ts +++ b/packages/wallet/src/test/fixtures/gql/activities/swap.ts @@ -3,9 +3,10 @@ import { SwapOrderStatus, SwapOrderType, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { faker } from 'uniswap/src/test/shared' +import { createFixture } from 'uniswap/src/test/utils' import { daiToken, ethToken } from 'wallet/src/test/fixtures/gql/assets' -import { faker } from 'wallet/src/test/shared' -import { createFixture, randomEnumValue } from 'wallet/src/test/utils' +import { randomEnumValue } from 'wallet/src/test/utils' export const swapOrderDetails = createFixture()(() => ({ __typename: 'SwapOrderDetails', diff --git a/packages/wallet/src/test/fixtures/gql/activities/tokens.ts b/packages/wallet/src/test/fixtures/gql/activities/tokens.ts index ea3dea0208e..dc9c759bfc5 100644 --- a/packages/wallet/src/test/fixtures/gql/activities/tokens.ts +++ b/packages/wallet/src/test/fixtures/gql/activities/tokens.ts @@ -5,10 +5,11 @@ import { TokenTransfer, TransactionDirection, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { faker } from 'uniswap/src/test/shared' +import { createFixture } from 'uniswap/src/test/utils' import { amount } from 'wallet/src/test/fixtures/gql/amounts' import { daiToken, ethToken } from 'wallet/src/test/fixtures/gql/assets' -import { faker } from 'wallet/src/test/shared' -import { createFixture, randomEnumValue } from 'wallet/src/test/utils' +import { randomEnumValue } from 'wallet/src/test/utils' /** * Base fixtures @@ -39,7 +40,7 @@ export const tokenTransfer = createFixture()(() => ({ */ export const erc20ApproveAssetChange = createFixture()(() => - tokenApproval({ asset: daiToken(), tokenStandard: TokenStandard.Erc20 }) + tokenApproval({ asset: daiToken(), tokenStandard: TokenStandard.Erc20 }), ) export const erc20TokenTransferOut = createFixture()(() => @@ -48,9 +49,9 @@ export const erc20TokenTransferOut = createFixture()(() => tokenStandard: TokenStandard.Erc20, direction: TransactionDirection.Out, transactedValue: amount({ value: 1, currency: Currency.Usd }), - }) + }), ) export const erc20TransferIn = createFixture()(() => - erc20TokenTransferOut({ direction: TransactionDirection.In }) + erc20TokenTransferOut({ direction: TransactionDirection.In }), ) diff --git a/packages/wallet/src/test/fixtures/gql/amounts.ts b/packages/wallet/src/test/fixtures/gql/amounts.ts index 39c12b51ee1..d618c176d94 100644 --- a/packages/wallet/src/test/fixtures/gql/amounts.ts +++ b/packages/wallet/src/test/fixtures/gql/amounts.ts @@ -3,8 +3,9 @@ import { Currency, TimestampedAmount, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' -import { MAX_FIXTURE_TIMESTAMP, faker } from 'wallet/src/test/shared' -import { createFixture, randomEnumValue } from 'wallet/src/test/utils' +import { MAX_FIXTURE_TIMESTAMP, faker } from 'uniswap/src/test/shared' +import { createFixture } from 'uniswap/src/test/utils' +import { randomEnumValue } from 'wallet/src/test/utils' export const amount = createFixture()(() => ({ __typename: 'Amount', diff --git a/packages/wallet/src/test/fixtures/constants.ts b/packages/wallet/src/test/fixtures/gql/assets/constants.ts similarity index 100% rename from packages/wallet/src/test/fixtures/constants.ts rename to packages/wallet/src/test/fixtures/gql/assets/constants.ts diff --git a/packages/wallet/src/test/fixtures/gql/assets/index.ts b/packages/wallet/src/test/fixtures/gql/assets/index.ts index 776b36b4145..e13db27d2ab 100644 --- a/packages/wallet/src/test/fixtures/gql/assets/index.ts +++ b/packages/wallet/src/test/fixtures/gql/assets/index.ts @@ -1,2 +1,3 @@ +export * from './constants' export * from './nfts' export * from './tokens' diff --git a/packages/wallet/src/test/fixtures/gql/assets/nfts.ts b/packages/wallet/src/test/fixtures/gql/assets/nfts.ts index fdfeba498db..95c071865ba 100644 --- a/packages/wallet/src/test/fixtures/gql/assets/nfts.ts +++ b/packages/wallet/src/test/fixtures/gql/assets/nfts.ts @@ -4,9 +4,10 @@ import { NftCollection, NftContract, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { faker } from 'uniswap/src/test/shared' +import { createFixture } from 'uniswap/src/test/utils' import { GQL_CHAINS, image } from 'wallet/src/test/fixtures/gql/misc' -import { faker } from 'wallet/src/test/shared' -import { createArray, createFixture, randomChoice } from 'wallet/src/test/utils' +import { createArray, randomChoice } from 'wallet/src/test/utils' /** * Base fixtures diff --git a/packages/wallet/src/test/fixtures/gql/assets/tokens.ts b/packages/wallet/src/test/fixtures/gql/assets/tokens.ts index a2895ec9b8c..affd1b5c4c5 100644 --- a/packages/wallet/src/test/fixtures/gql/assets/tokens.ts +++ b/packages/wallet/src/test/fixtures/gql/assets/tokens.ts @@ -12,12 +12,10 @@ import { TokenProjectMarket, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { toGraphQLChain } from 'uniswap/src/features/chains/utils' +import { MAX_FIXTURE_TIMESTAMP, faker } from 'uniswap/src/test/shared' +import { createFixture } from 'uniswap/src/test/utils' import { amounts } from 'wallet/src/test/fixtures/gql/amounts' -import { - get24hPriceChange, - getLatestPrice, - priceHistory, -} from 'wallet/src/test/fixtures/gql/history' +import { get24hPriceChange, getLatestPrice, priceHistory } from 'wallet/src/test/fixtures/gql/history' import { GQL_CHAINS, image } from 'wallet/src/test/fixtures/gql/misc' import { DAI, @@ -29,8 +27,7 @@ import { USDC_POLYGON, WETH, } from 'wallet/src/test/fixtures/lib' -import { MAX_FIXTURE_TIMESTAMP, faker } from 'wallet/src/test/shared' -import { createFixture, randomChoice, randomEnumValue } from 'wallet/src/test/utils' +import { randomChoice, randomEnumValue } from 'wallet/src/test/utils' /** * Base fixtures @@ -88,11 +85,9 @@ type TokenProjectMarketOptions = { priceHistory: (TimestampedAmount | undefined)[] } -export const tokenProjectMarket = createFixture( - () => ({ - priceHistory: priceHistory({ duration: HistoryDuration.Week, size: 7 }), - }) -)(({ priceHistory: history }) => ({ +export const tokenProjectMarket = createFixture(() => ({ + priceHistory: priceHistory({ duration: HistoryDuration.Week, size: 7 }), +}))(({ priceHistory: history }) => ({ __typename: 'TokenProjectMarket', id: faker.datatype.uuid(), priceHistory: history, @@ -141,7 +136,7 @@ export const usdcTokenProject = createFixture token({ sdkToken: USDBC_BASE, market: tokenMarket() }), token({ sdkToken: USDC_OPTIMISM }), ], - }) + }), ) /** @@ -155,9 +150,7 @@ const ethProject = tokenProject({ }) export const ethToken = createFixture()(() => token({ sdkToken: ETH, project: ethProject })) -export const wethToken = createFixture()(() => - token({ sdkToken: WETH, project: ethProject }) -) +export const wethToken = createFixture()(() => token({ sdkToken: WETH, project: ethProject })) const daiProject = tokenProject({ name: 'Dai Stablecoin', @@ -173,12 +166,6 @@ const usdcProject = tokenProject({ isSpam: false, }) -export const usdcToken = createFixture()(() => - token({ sdkToken: USDC, project: usdcProject }) -) -export const usdcBaseToken = createFixture()(() => - token({ sdkToken: USDBC_BASE, project: usdcProject }) -) -export const usdcArbitrumToken = createFixture()(() => - token({ sdkToken: USDC_ARBITRUM, project: usdcProject }) -) +export const usdcToken = createFixture()(() => token({ sdkToken: USDC, project: usdcProject })) +export const usdcBaseToken = createFixture()(() => token({ sdkToken: USDBC_BASE, project: usdcProject })) +export const usdcArbitrumToken = createFixture()(() => token({ sdkToken: USDC_ARBITRUM, project: usdcProject })) diff --git a/packages/wallet/src/test/fixtures/gql/history.ts b/packages/wallet/src/test/fixtures/gql/history.ts index 5105d8d72a2..c738531ba16 100644 --- a/packages/wallet/src/test/fixtures/gql/history.ts +++ b/packages/wallet/src/test/fixtures/gql/history.ts @@ -3,10 +3,11 @@ import { HistoryDuration, TimestampedAmount, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { faker } from 'uniswap/src/test/shared' +import { createFixture } from 'uniswap/src/test/utils' import { ONE_DAY_MS, ONE_HOUR_MS, ONE_MINUTE_MS } from 'utilities/src/time/time' import { amount, timestampedAmount } from 'wallet/src/test/fixtures/gql/amounts' -import { faker } from 'wallet/src/test/shared' -import { createArray, createFixture, randomEnumValue } from 'wallet/src/test/utils' +import { createArray, randomEnumValue } from 'wallet/src/test/utils' /** * Constants @@ -46,7 +47,7 @@ export const priceHistory = createFixture()(() => ({ __typename: 'Transaction', @@ -23,10 +24,7 @@ type TransactionDetailsBaseOptions = { transactionStatus: TransactionStatus } -export const gqlTransactionDetails = createFixture< - TransactionDetails, - TransactionDetailsBaseOptions ->({ +export const gqlTransactionDetails = createFixture({ transactionStatus: randomEnumValue(TransactionStatus), })(({ transactionStatus }) => ({ __typename: 'TransactionDetails', diff --git a/packages/wallet/src/test/fixtures/index.ts b/packages/wallet/src/test/fixtures/index.ts index c2582058659..15c26d7917f 100644 --- a/packages/wallet/src/test/fixtures/index.ts +++ b/packages/wallet/src/test/fixtures/index.ts @@ -1,4 +1,4 @@ -export * from './constants' export * from './gql' +export * from './gql/assets/constants' export * from './lib' export * from './wallet' diff --git a/packages/wallet/src/test/fixtures/lib/ethers.ts b/packages/wallet/src/test/fixtures/lib/ethers.ts index 337731a33d3..adb739d0daa 100644 --- a/packages/wallet/src/test/fixtures/lib/ethers.ts +++ b/packages/wallet/src/test/fixtures/lib/ethers.ts @@ -1,11 +1,7 @@ -import { - TransactionReceipt, - TransactionRequest, - TransactionResponse, -} from '@ethersproject/providers' +import { TransactionReceipt, TransactionRequest, TransactionResponse } from '@ethersproject/providers' import { BigNumber, Transaction } from 'ethers' -import { faker } from 'wallet/src/test/shared' -import { createFixture } from 'wallet/src/test/utils' +import { faker } from 'uniswap/src/test/shared' +import { createFixture } from 'uniswap/src/test/utils' export const ethersTransaction = createFixture()(() => ({ chainId: faker.datatype.number(), diff --git a/packages/wallet/src/test/fixtures/lib/netinfo.ts b/packages/wallet/src/test/fixtures/lib/netinfo.ts index 889854351bc..eae4b353710 100644 --- a/packages/wallet/src/test/fixtures/lib/netinfo.ts +++ b/packages/wallet/src/test/fixtures/lib/netinfo.ts @@ -4,7 +4,7 @@ import { NetInfoStateType, NetInfoUnknownState, } from '@react-native-community/netinfo' -import { createFixture } from 'wallet/src/test/utils' +import { createFixture } from 'uniswap/src/test/utils' export const networkUnknown = createFixture()(() => ({ isConnected: null, diff --git a/packages/wallet/src/test/fixtures/lib/sdk.ts b/packages/wallet/src/test/fixtures/lib/sdk.ts index 4b0a93b4bb5..aa68c7eeadf 100644 --- a/packages/wallet/src/test/fixtures/lib/sdk.ts +++ b/packages/wallet/src/test/fixtures/lib/sdk.ts @@ -1,13 +1,13 @@ import { Token } from '@uniswap/sdk-core' +import { getWrappedNativeAddress } from 'uniswap/src/constants/addresses' import { UniverseChainId } from 'uniswap/src/types/chains' -import { getWrappedNativeAddress } from 'wallet/src/constants/addresses' export const ETH = new Token( UniverseChainId.Mainnet, '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', 18, 'ETH', - 'Ethereum' + 'Ethereum', ) export const WETH = new Token( @@ -15,7 +15,7 @@ export const WETH = new Token( getWrappedNativeAddress(UniverseChainId.Mainnet), 18, 'WETH', - 'Wrapped Ether' + 'Wrapped Ether', ) export const DAI = new Token( @@ -23,7 +23,7 @@ export const DAI = new Token( '0x6b175474e89094c44da98b954eedeac495271d0f', 18, 'DAI', - 'Dai Stablecoin' + 'Dai Stablecoin', ) export const DAI_ARBITRUM_ONE = new Token( @@ -31,7 +31,7 @@ export const DAI_ARBITRUM_ONE = new Token( '0xda10009cbd5d07dd0cecc66161fc93d7c9000da1', 18, 'DAI', - 'Dai stable coin' + 'Dai stable coin', ) export const USDC = new Token( @@ -39,7 +39,7 @@ export const USDC = new Token( '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', 6, 'USDC', - 'USD//C' + 'USD//C', ) export const USDC_ARBITRUM = new Token( @@ -47,7 +47,7 @@ export const USDC_ARBITRUM = new Token( '0xff970a61a04b1ca14834a43f5de4533ebddb5cc8', 6, 'USDC', - 'USD//C' + 'USD//C', ) export const USDBC_BASE = new Token( @@ -55,7 +55,7 @@ export const USDBC_BASE = new Token( '0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca', 6, 'USDbC', - 'USD Base Coin' + 'USD Base Coin', ) export const USDC_OPTIMISM = new Token( @@ -63,7 +63,7 @@ export const USDC_OPTIMISM = new Token( '0x7f5c764cbc14f9669b88837ca1490cca17c31607', 6, 'USDC', - 'USD//C' + 'USD//C', ) export const USDC_POLYGON = new Token( @@ -71,7 +71,7 @@ export const USDC_POLYGON = new Token( '0x2791bca1f2de4661ed88a30c99a7a9449aa84174', 6, 'USDC', - 'USD//C' + 'USD//C', ) export const USDC_GOERLI = new Token( @@ -79,7 +79,7 @@ export const USDC_GOERLI = new Token( '0x07865c6e87b9f70255377e024ace6630c1eaa37f', 6, 'USDC', - 'USD//C' + 'USD//C', ) export const USDT = new Token( @@ -87,7 +87,7 @@ export const USDT = new Token( '0xdac17f958d2ee523a2206206994597c13d831ec7', 6, 'USDT', - 'Tether USD' + 'Tether USD', ) export const USDT_BNB = new Token( @@ -95,7 +95,7 @@ export const USDT_BNB = new Token( '0x55d398326f99059ff775485246999027b3197955', 18, 'USDT', - 'TetherUSD' + 'TetherUSD', ) export const WBTC = new Token( @@ -103,7 +103,7 @@ export const WBTC = new Token( '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599', 8, 'WBTC', - 'Wrapped BTC' + 'Wrapped BTC', ) export const SDK_TOKENS = [ diff --git a/packages/wallet/src/test/fixtures/wallet/accounts.ts b/packages/wallet/src/test/fixtures/wallet/accounts.ts index 634e60ef6af..49d1ce290a3 100644 --- a/packages/wallet/src/test/fixtures/wallet/accounts.ts +++ b/packages/wallet/src/test/fixtures/wallet/accounts.ts @@ -1,3 +1,5 @@ +import { faker } from 'uniswap/src/test/shared' +import { createFixture } from 'uniswap/src/test/utils' import { AccountBase, AccountType, @@ -5,13 +7,8 @@ import { ReadOnlyAccount, SignerMnemonicAccount, } from 'wallet/src/features/wallet/accounts/types' -import { - SAMPLE_SEED_ADDRESS_1, - SAMPLE_SEED_ADDRESS_2, - SAMPLE_SEED_ADDRESS_3, -} from 'wallet/src/test/fixtures/constants' -import { faker } from 'wallet/src/test/shared' -import { createFixture, randomEnumValue } from 'wallet/src/test/utils' +import { SAMPLE_SEED_ADDRESS_1, SAMPLE_SEED_ADDRESS_2, SAMPLE_SEED_ADDRESS_3 } from 'wallet/src/test/fixtures' +import { randomEnumValue } from 'wallet/src/test/utils' /** * Base fixtures diff --git a/packages/wallet/src/test/fixtures/wallet/balances.ts b/packages/wallet/src/test/fixtures/wallet/balances.ts index b1a25707053..b3246a253e9 100644 --- a/packages/wallet/src/test/fixtures/wallet/balances.ts +++ b/packages/wallet/src/test/fixtures/wallet/balances.ts @@ -1,17 +1,13 @@ -import { - Portfolio, - Token, - TokenBalance, -} from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { Portfolio, Token, TokenBalance } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { fromGraphQLChain } from 'uniswap/src/features/chains/utils' import { PortfolioBalance } from 'uniswap/src/features/dataApi/types' -import { fromGraphQLChain } from 'wallet/src/features/chains/utils' -import { buildCurrency } from 'wallet/src/features/dataApi/utils' +import { buildCurrency } from 'uniswap/src/features/dataApi/utils' +import { currencyInfo } from 'uniswap/src/test/fixtures/wallet/currencies' +import { faker } from 'uniswap/src/test/shared' +import { createFixture } from 'uniswap/src/test/utils' +import { currencyId } from 'uniswap/src/utils/currencyId' import { portfolio } from 'wallet/src/test/fixtures/gql' import { tokenBalance } from 'wallet/src/test/fixtures/gql/assets' -import { currencyInfo } from 'wallet/src/test/fixtures/wallet/currencies' -import { faker } from 'wallet/src/test/shared' -import { createFixture } from 'wallet/src/test/utils' -import { currencyId } from 'wallet/src/utils/currencyId' const portfolioBalanceBase = createFixture()(() => ({ cacheId: faker.datatype.uuid(), @@ -71,9 +67,9 @@ type PortfolioBalancesOptions = { portfolio: Portfolio } -export const portfolioBalances = createFixture( - () => ({ portfolio: portfolio() }) -)( +export const portfolioBalances = createFixture(() => ({ + portfolio: portfolio(), +}))( ({ portfolio: { tokenBalances } }) => (tokenBalances ?.map((balance) => { @@ -83,5 +79,5 @@ export const portfolioBalances = createFixture() type: AppNotificationType.Transaction, txType: randomEnumValue(TransactionType), txStatus: randomChoice(FINALIZED_TRANSACTION_STATUSES), - txHash: faker.datatype.uuid(), txId: faker.datatype.uuid(), chainId: randomChoice(WALLET_SUPPORTED_CHAIN_IDS), })) @@ -101,15 +101,13 @@ export const wrapTxNotification = createFixture()(() => ({ unwrapped: faker.datatype.boolean(), })) -const transferCurrencyTxNotificationBase = createFixture()( - () => ({ - ...transactionNotificationBase(), - txType: randomChoice([TransactionType.Send, TransactionType.Receive]), - assetType: AssetType.Currency, - tokenAddress: faker.finance.ethereumAddress(), - currencyAmountRaw: faker.datatype.number().toString(), - }) -) +const transferCurrencyTxNotificationBase = createFixture()(() => ({ + ...transactionNotificationBase(), + txType: randomChoice([TransactionType.Send, TransactionType.Receive]), + assetType: AssetType.Currency, + tokenAddress: faker.finance.ethereumAddress(), + currencyAmountRaw: faker.datatype.number().toString(), +})) export const sendCurrencyTxNotification = createFixture()(() => ({ ...transferCurrencyTxNotificationBase(), @@ -174,14 +172,12 @@ export const chooseCountryNotification = createFixture()( - () => ({ - ...appNotificationBase(), - type: AppNotificationType.AssetVisibility, - visible: faker.datatype.boolean(), - assetName: faker.lorem.words(), - }) -) +export const changeAssetVisibilityNotifiation = createFixture()(() => ({ + ...appNotificationBase(), + type: AppNotificationType.AssetVisibility, + visible: faker.datatype.boolean(), + assetName: faker.lorem.words(), +})) export const swapPendingNotification = createFixture()(() => ({ ...appNotificationBase(), @@ -189,16 +185,13 @@ export const swapPendingNotification = createFixture()( wrapType: randomEnumValue(WrapType), })) -export const transferCurrencyPendingNotification = - createFixture()(() => ({ - ...appNotificationBase(), - type: AppNotificationType.TransferCurrencyPending, - currencyInfo: currencyInfo(), - })) +export const transferCurrencyPendingNotification = createFixture()(() => ({ + ...appNotificationBase(), + type: AppNotificationType.TransferCurrencyPending, + currencyInfo: currencyInfo(), +})) -export const scantasticCompleteNotification = createFixture()( - () => ({ - ...appNotificationBase(), - type: AppNotificationType.ScantasticComplete, - }) -) +export const scantasticCompleteNotification = createFixture()(() => ({ + ...appNotificationBase(), + type: AppNotificationType.ScantasticComplete, +})) diff --git a/packages/wallet/src/test/fixtures/wallet/recipients.ts b/packages/wallet/src/test/fixtures/wallet/recipients.ts index 10adcdf56c8..a41ea71776b 100644 --- a/packages/wallet/src/test/fixtures/wallet/recipients.ts +++ b/packages/wallet/src/test/fixtures/wallet/recipients.ts @@ -1,7 +1,7 @@ import { SectionListData } from 'react-native' +import { faker } from 'uniswap/src/test/shared' +import { createFixture } from 'uniswap/src/test/utils' import { SearchableRecipient } from 'wallet/src/features/address/types' -import { faker } from 'wallet/src/test/shared' -import { createFixture } from 'wallet/src/test/utils' export const searchableRecipient = createFixture()(() => ({ address: faker.finance.ethereumAddress(), @@ -12,10 +12,7 @@ type RecipientSectionOptions = { addresses: string[] } -export const recipientSection = createFixture< - SectionListData, - RecipientSectionOptions ->(() => ({ +export const recipientSection = createFixture, RecipientSectionOptions>(() => ({ addresses: [faker.finance.ethereumAddress(), faker.finance.ethereumAddress()], }))(({ addresses }) => ({ title: faker.lorem.words(), diff --git a/packages/wallet/src/test/fixtures/wallet/redux.ts b/packages/wallet/src/test/fixtures/wallet/redux.ts index b79c1a00bd8..325250bcc8c 100644 --- a/packages/wallet/src/test/fixtures/wallet/redux.ts +++ b/packages/wallet/src/test/fixtures/wallet/redux.ts @@ -1,9 +1,9 @@ import { PreloadedState } from 'redux' +import { createFixture } from 'uniswap/src/test/utils' import { Account } from 'wallet/src/features/wallet/accounts/types' import { WalletState, initialWalletState } from 'wallet/src/features/wallet/slice' import { SharedState } from 'wallet/src/state/reducer' import { signerMnemonicAccount } from 'wallet/src/test/fixtures/wallet/accounts' -import { createFixture } from 'wallet/src/test/utils' type WalletPreloaedStateOptions = { account: Account @@ -21,9 +21,8 @@ type PreloadedSharedStateOptions = { account: Account | undefined } -export const preloadedSharedState = createFixture< - PreloadedState, - PreloadedSharedStateOptions ->({ account: undefined })(({ account }) => ({ +export const preloadedSharedState = createFixture, PreloadedSharedStateOptions>({ + account: undefined, +})(({ account }) => ({ wallet: preloadedWalletState({ account }), })) diff --git a/packages/wallet/src/test/fixtures/wallet/transactions/fixtures.ts b/packages/wallet/src/test/fixtures/wallet/transactions/fixtures.ts index 34dc362f588..a145fb766dc 100644 --- a/packages/wallet/src/test/fixtures/wallet/transactions/fixtures.ts +++ b/packages/wallet/src/test/fixtures/wallet/transactions/fixtures.ts @@ -1,8 +1,10 @@ import { TransactionRequest } from '@ethersproject/providers' import { TradeType } from '@uniswap/sdk-core' +import { AssetType } from 'uniswap/src/entities/assets' +import { faker } from 'uniswap/src/test/shared' +import { createFixture } from 'uniswap/src/test/utils' import { WALLET_SUPPORTED_CHAIN_IDS } from 'uniswap/src/types/chains' import { Routing } from 'wallet/src/data/tradingApi/__generated__/index' -import { AssetType } from 'wallet/src/entities/assets' import { finalizeTransaction } from 'wallet/src/features/transactions/slice' import { ApproveTransactionInfo, @@ -30,8 +32,7 @@ import { WrapTransactionInfo, } from 'wallet/src/features/transactions/types' import { dappInfoWC } from 'wallet/src/test/fixtures/wallet/walletConnect' -import { faker } from 'wallet/src/test/shared' -import { createFixture, randomChoice, randomEnumValue } from 'wallet/src/test/utils' +import { randomChoice, randomEnumValue } from 'wallet/src/test/utils' export const transactionId = createFixture()(() => ({ id: faker.datatype.uuid(), @@ -43,6 +44,7 @@ export const nftSummaryInfo = createFixture()(() => ({ name: faker.lorem.words(), collectionName: faker.lorem.words(), imageURL: faker.image.imageUrl(), + address: faker.finance.ethereumAddress(), })) export const approveTransactionInfo = createFixture()(() => ({ @@ -57,25 +59,21 @@ export const baseSwapTransactionInfo = createFixture()( outputCurrencyId: faker.datatype.uuid(), })) -export const extractInputSwapTransactionInfo = createFixture()( - () => ({ - ...baseSwapTransactionInfo(), - tradeType: TradeType.EXACT_INPUT, - inputCurrencyAmountRaw: faker.datatype.number().toString(), - expectedOutputCurrencyAmountRaw: faker.datatype.number().toString(), - minimumOutputCurrencyAmountRaw: faker.datatype.number().toString(), - }) -) - -export const extractOutputSwapTransactionInfo = createFixture()( - () => ({ - ...baseSwapTransactionInfo(), - tradeType: TradeType.EXACT_OUTPUT, - outputCurrencyAmountRaw: faker.datatype.number().toString(), - expectedInputCurrencyAmountRaw: faker.datatype.number().toString(), - maximumInputCurrencyAmountRaw: faker.datatype.number().toString(), - }) -) +export const extractInputSwapTransactionInfo = createFixture()(() => ({ + ...baseSwapTransactionInfo(), + tradeType: TradeType.EXACT_INPUT, + inputCurrencyAmountRaw: faker.datatype.number().toString(), + expectedOutputCurrencyAmountRaw: faker.datatype.number().toString(), + minimumOutputCurrencyAmountRaw: faker.datatype.number().toString(), +})) + +export const extractOutputSwapTransactionInfo = createFixture()(() => ({ + ...baseSwapTransactionInfo(), + tradeType: TradeType.EXACT_OUTPUT, + outputCurrencyAmountRaw: faker.datatype.number().toString(), + expectedInputCurrencyAmountRaw: faker.datatype.number().toString(), + maximumInputCurrencyAmountRaw: faker.datatype.number().toString(), +})) export const confirmedSwapTransactionInfo = createFixture()(() => ({ ...baseSwapTransactionInfo(), @@ -169,9 +167,7 @@ export const transactionReceipt = createFixture()(() => ({ effectiveGasPrice: faker.datatype.number(), })) -export const finalizedTransactionAction = createFixture>()( - () => ({ - payload: finalizedTransactionDetails(), - type: 'transactions/finalizeTransaction', - }) -) +export const finalizedTransactionAction = createFixture>()(() => ({ + payload: finalizedTransactionDetails(), + type: 'transactions/finalizeTransaction', +})) diff --git a/packages/wallet/src/test/fixtures/wallet/transactions/helpers.ts b/packages/wallet/src/test/fixtures/wallet/transactions/helpers.ts index 6abaa1034aa..6da7816c5a6 100644 --- a/packages/wallet/src/test/fixtures/wallet/transactions/helpers.ts +++ b/packages/wallet/src/test/fixtures/wallet/transactions/helpers.ts @@ -1,6 +1,7 @@ import { TransactionRequest, TransactionResponse } from '@ethersproject/providers' import { BigNumber, providers } from 'ethers' import { merge } from 'lodash' +import { faker } from 'uniswap/src/test/shared' import { finalizeTransaction } from 'wallet/src/features/transactions/slice' import { ClassicTransactionDetails, @@ -18,7 +19,6 @@ import { transactionDetails, transactionReceipt, } from 'wallet/src/test/fixtures/wallet/transactions/fixtures' -import { faker } from 'wallet/src/test/shared' type TxFixtures = { txDetailsPending: T @@ -32,9 +32,7 @@ type TxFixtures = { finalizedTxAction: ReturnType } -export const getTxFixtures = ( - transaction?: T -): TxFixtures => { +export const getTxFixtures = (transaction?: T): TxFixtures => { const txBase = merge( {}, transactionDetails({ @@ -43,7 +41,7 @@ export const getTxFixtures = ( request: ethersTransactionRequest(), }, }), - transaction + transaction, ) // Transaction flow diff --git a/packages/wallet/src/test/fixtures/wallet/walletConnect.ts b/packages/wallet/src/test/fixtures/wallet/walletConnect.ts index b2ca006c065..5a4960913bd 100644 --- a/packages/wallet/src/test/fixtures/wallet/walletConnect.ts +++ b/packages/wallet/src/test/fixtures/wallet/walletConnect.ts @@ -1,6 +1,6 @@ +import { faker } from 'uniswap/src/test/shared' +import { createFixture } from 'uniswap/src/test/utils' import { DappInfoUwULink, DappInfoWC } from 'uniswap/src/types/walletConnect' -import { faker } from 'wallet/src/test/shared' -import { createFixture } from 'wallet/src/test/utils' export const dappInfoWC = createFixture()(() => ({ source: 'walletconnect', diff --git a/packages/wallet/src/test/mocks/gql/mocks.ts b/packages/wallet/src/test/mocks/gql/mocks.ts index 29e15537974..af7419deb76 100644 --- a/packages/wallet/src/test/mocks/gql/mocks.ts +++ b/packages/wallet/src/test/mocks/gql/mocks.ts @@ -5,8 +5,8 @@ import { SwapOrderStatus, TransactionStatus, } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { MAX_FIXTURE_TIMESTAMP, faker } from 'uniswap/src/test/shared' import { GQL_CHAINS } from 'wallet/src/test/fixtures' -import { MAX_FIXTURE_TIMESTAMP, faker } from 'wallet/src/test/shared' import { randomChoice, randomEnumValue } from 'wallet/src/test/utils' export const mocks = { diff --git a/packages/wallet/src/test/mocks/gql/provider.tsx b/packages/wallet/src/test/mocks/gql/provider.tsx index c41d938cc8f..f0f228f6124 100644 --- a/packages/wallet/src/test/mocks/gql/provider.tsx +++ b/packages/wallet/src/test/mocks/gql/provider.tsx @@ -12,10 +12,7 @@ import { getErrorLink, getRestLink } from 'wallet/src/data/links' import { mocks as defaultMocks } from 'wallet/src/test/mocks/gql/mocks' import { defaultResolvers } from 'wallet/src/test/mocks/gql/resolvers' -const GQL_SCHEMA_PATH = path.join( - __dirname, - '../../../../../uniswap/src/data/graphql/uniswap-data-api/schema.graphql' -) +const GQL_SCHEMA_PATH = path.join(__dirname, '../../../../../uniswap/src/data/graphql/uniswap-data-api/schema.graphql') const baseSchema = loadSchemaSync(GQL_SCHEMA_PATH, { loaders: [new GraphQLFileLoader()] }) diff --git a/packages/wallet/src/test/mocks/gql/resolvers.ts b/packages/wallet/src/test/mocks/gql/resolvers.ts index 53b4f814e29..f5dbbbcd580 100644 --- a/packages/wallet/src/test/mocks/gql/resolvers.ts +++ b/packages/wallet/src/test/mocks/gql/resolvers.ts @@ -1,8 +1,5 @@ import { GraphQLJSON } from 'graphql-scalars' -import { - HistoryDuration, - Resolvers, -} from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { HistoryDuration, Resolvers } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { priceHistory, tokenProject } from 'wallet/src/test/fixtures' export const defaultResolvers: Resolvers = { diff --git a/packages/wallet/src/test/mocks/providers.ts b/packages/wallet/src/test/mocks/providers.ts index 836a08bb1c5..826222d662f 100644 --- a/packages/wallet/src/test/mocks/providers.ts +++ b/packages/wallet/src/test/mocks/providers.ts @@ -3,9 +3,9 @@ import { BigNumber, providers } from 'ethers' import ERC20_ABI from 'uniswap/src/abis/erc20.json' import { Erc20 } from 'uniswap/src/abis/types' import WETH_ABI from 'uniswap/src/abis/weth.json' +import { getWrappedNativeAddress } from 'uniswap/src/constants/addresses' +import { DAI } from 'uniswap/src/constants/tokens' import { UniverseChainId, WalletChainId } from 'uniswap/src/types/chains' -import { getWrappedNativeAddress } from 'wallet/src/constants/addresses' -import { DAI } from 'wallet/src/constants/tokens' import { ContractManager } from 'wallet/src/features/contracts/ContractManager' import { SignerManager } from 'wallet/src/features/wallet/signing/SignerManager' import { ethersTransactionReceipt } from 'wallet/src/test/fixtures' @@ -62,12 +62,9 @@ contractManager.getOrCreateContract( UniverseChainId.Mainnet, getWrappedNativeAddress(UniverseChainId.Mainnet), provider, - WETH_ABI + WETH_ABI, ) -export const tokenContract = contractManager.getContract( - UniverseChainId.Mainnet, - DAI.address -) as Erc20 +export const tokenContract = contractManager.getContract(UniverseChainId.Mainnet, DAI.address) as Erc20 export const mockTokenContract = { balanceOf: (): BigNumber => BigNumber.from('1000000000000000000'), diff --git a/packages/wallet/src/test/mocks/sdk.ts b/packages/wallet/src/test/mocks/sdk.ts index c72b94b9dae..945b77c54ac 100644 --- a/packages/wallet/src/test/mocks/sdk.ts +++ b/packages/wallet/src/test/mocks/sdk.ts @@ -1,6 +1,6 @@ import { FeeAmount, Pool } from '@uniswap/v3-sdk' +import { UNI, WBTC } from 'uniswap/src/constants/tokens' import { UniverseChainId } from 'uniswap/src/types/chains' -import { UNI, WBTC } from 'wallet/src/constants/tokens' export const mockPool = new Pool( UNI[UniverseChainId.Mainnet], @@ -8,5 +8,5 @@ export const mockPool = new Pool( FeeAmount.HIGH, '2437312313659959819381354528', '10272714736694327408', - -69633 + -69633, ) diff --git a/packages/wallet/src/test/mocks/utils.ts b/packages/wallet/src/test/mocks/utils.ts index e2105c381c8..36643e7910f 100644 --- a/packages/wallet/src/test/mocks/utils.ts +++ b/packages/wallet/src/test/mocks/utils.ts @@ -73,11 +73,7 @@ export const mockFiatConverter: LocalizationContextState = { formatPercent(_: Maybe): string { throw new Error('Function not implemented.') }, - addFiatSymbolToNumber(_: { - value: Maybe - currencyCode: string - currencySymbol: string - }): string { + addFiatSymbolToNumber(_: { value: Maybe; currencyCode: string; currencySymbol: string }): string { throw new Error('Function not implemented.') }, } diff --git a/packages/wallet/src/test/render.tsx b/packages/wallet/src/test/render.tsx index 046e64e4e02..4eca5747905 100644 --- a/packages/wallet/src/test/render.tsx +++ b/packages/wallet/src/test/render.tsx @@ -13,6 +13,7 @@ import { import React, { PropsWithChildren } from 'react' import { Resolvers } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' import { UnitagUpdaterContextProvider } from 'uniswap/src/features/unitags/context' +import { WalletNavigationContextState, WalletNavigationProvider } from 'wallet/src/contexts/WalletNavigationContext' import { SharedProvider } from 'wallet/src/provider' import { sharedRootReducer, type SharedState } from 'wallet/src/state/reducer' import { AutoMockedApolloProvider } from 'wallet/src/test/mocks' @@ -26,6 +27,20 @@ type ExtendedRenderOptions = RenderOptions & { store?: EnhancedStore } +const mockNavigationFunctions: WalletNavigationContextState = { + navigateToAccountActivityList: jest.fn(), + navigateToAccountTokenList: jest.fn(), + navigateToBuyOrReceiveWithEmptyWallet: jest.fn(), + navigateToNftDetails: jest.fn(), + navigateToNftCollection: jest.fn(), + navigateToSwapFlow: jest.fn(), + navigateToTokenDetails: jest.fn(), + navigateToReceive: jest.fn(), + navigateToSend: jest.fn(), + handleShareNft: jest.fn(), + handleShareToken: jest.fn(), +} + /** * * @param ui Component to render @@ -46,7 +61,7 @@ export function renderWithProviders( middleware: (getDefaultMiddleware) => getDefaultMiddleware(), }), ...renderOptions - }: ExtendedRenderOptions = {} + }: ExtendedRenderOptions = {}, ): RenderResult & { store: EnhancedStore } { @@ -54,7 +69,9 @@ export function renderWithProviders( return ( - {children} + + {children} + ) @@ -84,13 +101,13 @@ type RenderHookWithProvidersResult = // Don't require hookOptions if hook doesn't take any arguments export function renderHookWithProviders( hook: () => R, - hookOptions?: ExtendedRenderHookOptions + hookOptions?: ExtendedRenderHookOptions, ): RenderHookWithProvidersResult // Require hookOptions if hook takes arguments export function renderHookWithProviders( hook: (...args: P) => R, - hookOptions: ExtendedRenderHookOptions

        + hookOptions: ExtendedRenderHookOptions

        , ): RenderHookWithProvidersResult /** @@ -102,7 +119,7 @@ export function renderHookWithProviders( */ export function renderHookWithProviders

        ( hook: (...args: P) => R, - hookOptions?: ExtendedRenderHookOptions

        + hookOptions?: ExtendedRenderHookOptions

        , ): RenderHookWithProvidersResult { const { cache, diff --git a/packages/wallet/src/test/test-utils.ts b/packages/wallet/src/test/test-utils.ts index c6a21d9624c..d456fc81768 100644 --- a/packages/wallet/src/test/test-utils.ts +++ b/packages/wallet/src/test/test-utils.ts @@ -1,6 +1,6 @@ import { renderHookWithProviders, renderWithProviders } from 'wallet/src/test/render' -export { MAX_FIXTURE_TIMESTAMP, faker } from './shared' +export { MAX_FIXTURE_TIMESTAMP, faker } from '../../../uniswap/src/test/shared' export { createArray } from './utils' // re-export everything diff --git a/packages/wallet/src/test/utils/array.ts b/packages/wallet/src/test/utils/array.ts index d5667a30773..42c417223ba 100644 --- a/packages/wallet/src/test/utils/array.ts +++ b/packages/wallet/src/test/utils/array.ts @@ -22,10 +22,7 @@ * console.log(strings); // ["hello", "hello"] * ``` */ -export const createArray = ( - length: L, - factory: (index: number) => T -): ArrayOfLength => { +export const createArray = (length: L, factory: (index: number) => T): ArrayOfLength => { const result = [] for (let i = 0; i < length; i++) { result.push(factory(i)) diff --git a/packages/wallet/src/test/utils/index.ts b/packages/wallet/src/test/utils/index.ts index 11d4c1ad971..7245d93bf08 100644 --- a/packages/wallet/src/test/utils/index.ts +++ b/packages/wallet/src/test/utils/index.ts @@ -1,5 +1,4 @@ export * from './array' -export * from './factory' export * from './random' export * from './resolvers' export * from './wallet' diff --git a/packages/wallet/src/test/utils/random.ts b/packages/wallet/src/test/utils/random.ts index 9f9bdeea742..92f8908a940 100644 --- a/packages/wallet/src/test/utils/random.ts +++ b/packages/wallet/src/test/utils/random.ts @@ -19,9 +19,7 @@ * * @typeparam T Type of the enum object (will be automatically inferred from the provided argument). */ -export const randomEnumValue = >( - enumObj: T -): T[keyof T] => { +export const randomEnumValue = >(enumObj: T): T[keyof T] => { // If enum has different types for keys and values (keys are always strings, // values can be strings or numbers), we need to filter out the keys const keys = Object.keys(enumObj).filter((key) => isNaN(Number(key))) diff --git a/packages/wallet/src/test/utils/resolvers.ts b/packages/wallet/src/test/utils/resolvers.ts index 8cf62b3bfe6..a2e275b0847 100644 --- a/packages/wallet/src/test/utils/resolvers.ts +++ b/packages/wallet/src/test/utils/resolvers.ts @@ -14,38 +14,39 @@ type UndefinedToNull = T extends undefined ? null : T type ResolverReturnType = T extends (...args: any[]) => infer TResult ? TResult : T extends { resolve: (...args: any[]) => infer TResult } - ? TResult - : never - -type ResolverParameters> = T extends ResolverWithResolve< - infer TResult, // only result type is needed to filter selected fields - any, - any, - any -> - ? Parameters> - : T extends ResolverFn - ? Parameters> - : never + ? TResult + : never + +type ResolverParameters> = + T extends ResolverWithResolve< + infer TResult, // only result type is needed to filter selected fields + any, + any, + any + > + ? Parameters> + : T extends ResolverFn + ? Parameters> + : never type ResolverResponses = { [K in keyof T]: Promise> } function isResolverWithResolve>( - resolver: T + resolver: T, ): resolver is Extract> { return typeof resolver === 'object' && resolver !== null && 'resolve' in resolver } function isResolverFunction>( - resolver: T + resolver: T, ): resolver is Extract> { return typeof resolver === 'function' } export function queryResolvers( - resolvers: T + resolvers: T, ): { resolved: ResolverResponses resolvers: { Query: T } @@ -53,10 +54,7 @@ export function queryResolvers( // Create a response object with functions to create and resolve promises const promiseResolvers = {} as Record void> const resolved = Object.fromEntries( - Object.keys(resolvers).map((key) => [ - key, - new Promise((resolve) => (promiseResolvers[key as keyof T] = resolve)), - ]) + Object.keys(resolvers).map((key) => [key, new Promise((resolve) => (promiseResolvers[key as keyof T] = resolve))]), ) as ResolverResponses return { @@ -67,21 +65,16 @@ export function queryResolvers( const key = k as keyof T type R = typeof resolver - const resolve = async ( - ...params: ResolverParameters - ): Promise> => { + const resolve = async (...params: ResolverParameters): Promise> => { const [parent, args, context, info] = params const resolvedValue = isResolverWithResolve(resolver) ? resolver.resolve(parent, args, context, info) : isResolverFunction(resolver) - ? resolver(parent, args, context, info) - : null + ? resolver(parent, args, context, info) + : null - const updatedValue = await filterObjectFields( - info.fieldNodes[0]?.selectionSet, - resolvedValue - ) + const updatedValue = await filterObjectFields(info.fieldNodes[0]?.selectionSet, resolvedValue) // cloneDeepWith returns any type so we need to cast it manually const resultObj = cloneDeepWith(updatedValue, undefinedToNull) as ResolverReturnType @@ -94,7 +87,7 @@ export function queryResolvers( } return [key, resolve] - }) + }), ) as unknown as T, }, } @@ -108,7 +101,7 @@ function isObject(value: T): value is Exclude( selectionSet: SelectionSetNode | undefined, - sourceValue: T | Promise>> | null + sourceValue: T | Promise>> | null, ): Promise { // resolved source value can be a Promise or a plain value const source = await sourceValue diff --git a/packages/wallet/src/test/utils/wallet/balances.ts b/packages/wallet/src/test/utils/wallet/balances.ts index a37bd47b848..2cad1316590 100644 --- a/packages/wallet/src/test/utils/wallet/balances.ts +++ b/packages/wallet/src/test/utils/wallet/balances.ts @@ -1,13 +1,14 @@ import { PortfolioBalance } from 'uniswap/src/features/dataApi/types' import { portfolioBalances } from 'wallet/src/test/fixtures' -export function portfolioBalancesById( - inputBalances?: PortfolioBalance[] -): Record { +export function portfolioBalancesById(inputBalances?: PortfolioBalance[]): Record { const balances = inputBalances ?? portfolioBalances() - return balances.reduce((acc, balance) => { - acc[balance.currencyInfo.currencyId] = balance - return acc - }, {} as Record) + return balances.reduce( + (acc, balance) => { + acc[balance.currencyInfo.currencyId] = balance + return acc + }, + {} as Record, + ) } diff --git a/packages/wallet/src/utils/animations.ts b/packages/wallet/src/utils/animations.ts index 93ace698daa..7797d287038 100644 --- a/packages/wallet/src/utils/animations.ts +++ b/packages/wallet/src/utils/animations.ts @@ -1,12 +1,7 @@ import { Easing, SharedValue, withRepeat, withTiming } from 'react-native-reanimated' export function errorShakeAnimation(input: SharedValue): number { - return withRepeat( - withTiming(5, { duration: 50, easing: Easing.inOut(Easing.ease) }), - 3, - true, - () => { - input.value = 0 - } - ) + return withRepeat(withTiming(5, { duration: 50, easing: Easing.inOut(Easing.ease) }), 3, true, () => { + input.value = 0 + }) } diff --git a/packages/wallet/src/utils/balance.test.ts b/packages/wallet/src/utils/balance.test.ts index da34a0e9cbd..d99cd3a2b35 100644 --- a/packages/wallet/src/utils/balance.test.ts +++ b/packages/wallet/src/utils/balance.test.ts @@ -1,12 +1,7 @@ import { CurrencyAmount } from '@uniswap/sdk-core' import JSBI from 'jsbi' -import { DAI } from 'wallet/src/constants/tokens' -import { - ARBITRUM_CURRENCY, - MAINNET_CURRENCY, - OPTIMISM_CURRENCY, - POLYGON_CURRENCY, -} from 'wallet/src/test/fixtures' +import { DAI } from 'uniswap/src/constants/tokens' +import { ARBITRUM_CURRENCY, MAINNET_CURRENCY, OPTIMISM_CURRENCY, POLYGON_CURRENCY } from 'uniswap/src/test/fixtures' import { MIN_ARBITRUM_FOR_GAS, MIN_ETH_FOR_GAS, @@ -31,7 +26,7 @@ describe(maxAmountSpend, () => { it('reserves gas for large amounts on ETH Mainnet', () => { const amount = CurrencyAmount.fromRawAmount( MAINNET_CURRENCY, - JSBI.add(JSBI.BigInt(99), JSBI.BigInt(MIN_ETH_FOR_GAS)) + JSBI.add(JSBI.BigInt(99), JSBI.BigInt(MIN_ETH_FOR_GAS)), ) const amount1Spend = maxAmountSpend(amount) expect(amount1Spend?.quotient.toString()).toEqual('99') @@ -40,7 +35,7 @@ describe(maxAmountSpend, () => { it('handles small amounts on ETH Mainnet', () => { const amount = CurrencyAmount.fromRawAmount( MAINNET_CURRENCY, - JSBI.subtract(JSBI.BigInt(99), JSBI.BigInt(MIN_ETH_FOR_GAS)) + JSBI.subtract(JSBI.BigInt(99), JSBI.BigInt(MIN_ETH_FOR_GAS)), ) const amount1Spend = maxAmountSpend(amount) expect(amount1Spend?.quotient.toString()).toEqual('0') @@ -51,7 +46,7 @@ describe(maxAmountSpend, () => { it('reserves gas for large amounts on Polygon', () => { const amount = CurrencyAmount.fromRawAmount( POLYGON_CURRENCY, - JSBI.add(JSBI.BigInt(99), JSBI.BigInt(MIN_POLYGON_FOR_GAS)) + JSBI.add(JSBI.BigInt(99), JSBI.BigInt(MIN_POLYGON_FOR_GAS)), ) const amount1Spend = maxAmountSpend(amount) expect(amount1Spend?.quotient.toString()).toEqual('99') @@ -60,7 +55,7 @@ describe(maxAmountSpend, () => { it('handles small amounts on Polygon', () => { const amount = CurrencyAmount.fromRawAmount( POLYGON_CURRENCY, - JSBI.subtract(JSBI.BigInt(99), JSBI.BigInt(MIN_POLYGON_FOR_GAS)) + JSBI.subtract(JSBI.BigInt(99), JSBI.BigInt(MIN_POLYGON_FOR_GAS)), ) const amount1Spend = maxAmountSpend(amount) expect(amount1Spend?.quotient.toString()).toEqual('0') @@ -71,7 +66,7 @@ describe(maxAmountSpend, () => { it('reserves gas for large amounts on Arbitrum', () => { const amount = CurrencyAmount.fromRawAmount( ARBITRUM_CURRENCY, - JSBI.add(JSBI.BigInt(99), JSBI.BigInt(MIN_ARBITRUM_FOR_GAS)) + JSBI.add(JSBI.BigInt(99), JSBI.BigInt(MIN_ARBITRUM_FOR_GAS)), ) const amount1Spend = maxAmountSpend(amount) expect(amount1Spend?.quotient.toString()).toEqual('99') @@ -80,7 +75,7 @@ describe(maxAmountSpend, () => { it('handles small amounts on Arbitrum', () => { const amount = CurrencyAmount.fromRawAmount( ARBITRUM_CURRENCY, - JSBI.subtract(JSBI.BigInt(99), JSBI.BigInt(MIN_ARBITRUM_FOR_GAS)) + JSBI.subtract(JSBI.BigInt(99), JSBI.BigInt(MIN_ARBITRUM_FOR_GAS)), ) const amount1Spend = maxAmountSpend(amount) expect(amount1Spend?.quotient.toString()).toEqual('0') @@ -91,7 +86,7 @@ describe(maxAmountSpend, () => { it('reserves gas for large amounts on Optimism', () => { const amount = CurrencyAmount.fromRawAmount( OPTIMISM_CURRENCY, - JSBI.add(JSBI.BigInt(99), JSBI.BigInt(MIN_OPTIMISM_FOR_GAS)) + JSBI.add(JSBI.BigInt(99), JSBI.BigInt(MIN_OPTIMISM_FOR_GAS)), ) const amount1Spend = maxAmountSpend(amount) expect(amount1Spend?.quotient.toString()).toEqual('99') @@ -100,7 +95,7 @@ describe(maxAmountSpend, () => { it('handles small amounts on Optimism', () => { const amount = CurrencyAmount.fromRawAmount( OPTIMISM_CURRENCY, - JSBI.subtract(JSBI.BigInt(99), JSBI.BigInt(MIN_OPTIMISM_FOR_GAS)) + JSBI.subtract(JSBI.BigInt(99), JSBI.BigInt(MIN_OPTIMISM_FOR_GAS)), ) const amount1Spend = maxAmountSpend(amount) expect(amount1Spend?.quotient.toString()).toEqual('0') diff --git a/packages/wallet/src/utils/balance.ts b/packages/wallet/src/utils/balance.ts index 183ffc6cbd8..c40796a5ab1 100644 --- a/packages/wallet/src/utils/balance.ts +++ b/packages/wallet/src/utils/balance.ts @@ -8,17 +8,17 @@ const NATIVE_CURRENCY_DECIMALS = 18 // TODO(MOB-181): calculate this in a more scientific way export const MIN_ETH_FOR_GAS: JSBI = JSBI.multiply( JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(NATIVE_CURRENCY_DECIMALS - 3)), - JSBI.BigInt(15) + JSBI.BigInt(15), ) // .015 ETH export const MIN_POLYGON_FOR_GAS: JSBI = JSBI.multiply( JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(NATIVE_CURRENCY_DECIMALS - 2)), - JSBI.BigInt(6) + JSBI.BigInt(6), ) // .06 MATIC export const MIN_ARBITRUM_FOR_GAS: JSBI = JSBI.multiply( JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(NATIVE_CURRENCY_DECIMALS - 4)), - JSBI.BigInt(8) + JSBI.BigInt(8), ) // .0008 ETH export const MIN_OPTIMISM_FOR_GAS: JSBI = MIN_ARBITRUM_FOR_GAS @@ -42,9 +42,7 @@ export const MIN_ZKSYNC_FOR_GAS: JSBI = MIN_ARBITRUM_FOR_GAS * https://github.com/Uniswap/interface/blob/main/src/utils/maxAmountSpend.ts * @param currencyAmount to return max of */ -export function maxAmountSpend( - currencyAmount: Maybe> -): Maybe> { +export function maxAmountSpend(currencyAmount: Maybe>): Maybe> { if (!currencyAmount) { return undefined } diff --git a/packages/wallet/src/utils/currency.test.ts b/packages/wallet/src/utils/currency.test.ts index 4e4645581c0..b9cfa53d307 100644 --- a/packages/wallet/src/utils/currency.test.ts +++ b/packages/wallet/src/utils/currency.test.ts @@ -1,12 +1,10 @@ -import { DAI, USDC } from 'wallet/src/constants/tokens' +import { DAI, USDC } from 'uniswap/src/constants/tokens' import { mockLocalizedFormatter, noOpFunction } from 'wallet/src/test/mocks/utils' import { getCurrencyDisplayText, getFormattedCurrencyAmount } from 'wallet/src/utils/currency' describe(getFormattedCurrencyAmount, () => { it('formats valid amount', () => { - expect(getFormattedCurrencyAmount(DAI, '1000000000000000000', mockLocalizedFormatter)).toEqual( - '1.00 ' - ) + expect(getFormattedCurrencyAmount(DAI, '1000000000000000000', mockLocalizedFormatter)).toEqual('1.00 ') }) it('handles invalid Currency', () => { diff --git a/packages/wallet/src/utils/currency.ts b/packages/wallet/src/utils/currency.ts index c89fcbcf779..847413255bd 100644 --- a/packages/wallet/src/utils/currency.ts +++ b/packages/wallet/src/utils/currency.ts @@ -1,7 +1,7 @@ import { Currency } from '@uniswap/sdk-core' +import { getValidAddress, shortenAddress } from 'uniswap/src/utils/addresses' import { getSymbolDisplayText } from 'uniswap/src/utils/currency' import { LocalizationContextState } from 'wallet/src/features/language/LocalizationContext' -import { getValidAddress, shortenAddress } from 'wallet/src/utils/addresses' import { ValueType, getCurrencyAmount } from 'wallet/src/utils/getCurrencyAmount' export function getFormattedCurrencyAmount( @@ -9,7 +9,7 @@ export function getFormattedCurrencyAmount( currencyAmountRaw: string, formatter: LocalizationContextState, isApproximateAmount = false, - valueType = ValueType.Raw + valueType = ValueType.Raw, ): string { const currencyAmount = getCurrencyAmount({ value: currencyAmountRaw, @@ -27,7 +27,7 @@ export function getFormattedCurrencyAmount( export function getCurrencyDisplayText( currency: Maybe, - tokenAddressString: Address | undefined + tokenAddressString: Address | undefined, ): string | undefined { const symbolDisplayText = getSymbolDisplayText(currency?.symbol) diff --git a/packages/wallet/src/utils/currencyId.test.ts b/packages/wallet/src/utils/currencyId.test.ts index 1a04fe672d0..6d4976bae91 100644 --- a/packages/wallet/src/utils/currencyId.test.ts +++ b/packages/wallet/src/utils/currencyId.test.ts @@ -1,7 +1,7 @@ +import { getNativeAddress } from 'uniswap/src/constants/addresses' +import { DAI } from 'uniswap/src/constants/tokens' +import { NativeCurrency } from 'uniswap/src/features/tokens/NativeCurrency' import { UniverseChainId } from 'uniswap/src/types/chains' -import { getNativeAddress } from 'wallet/src/constants/addresses' -import { DAI } from 'wallet/src/constants/tokens' -import { NativeCurrency } from 'wallet/src/features/tokens/NativeCurrency' import { NATIVE_ANALYTICS_ADDRESS_VALUE, areCurrencyIdsEqual, @@ -14,7 +14,7 @@ import { currencyIdToGraphQLAddress, getCurrencyAddressForAnalytics, isNativeCurrencyAddress, -} from 'wallet/src/utils/currencyId' +} from 'uniswap/src/utils/currencyId' const ETH = NativeCurrency.onChain(UniverseChainId.Mainnet) const DAI_ADDRESS = '0x6B175474E89094C44Da98b954EedeAC495271d0F' @@ -25,17 +25,13 @@ describe(currencyId, () => { }) it('builds correct ID for native asset', () => { - expect(currencyId(ETH)).toEqual( - `${UniverseChainId.Mainnet}-${getNativeAddress(UniverseChainId.Mainnet)}` - ) + expect(currencyId(ETH)).toEqual(`${UniverseChainId.Mainnet}-${getNativeAddress(UniverseChainId.Mainnet)}`) }) }) describe(buildCurrencyId, () => { it('builds correct ID for token', () => { - expect(buildCurrencyId(UniverseChainId.Mainnet, DAI.address)).toEqual( - `${UniverseChainId.Mainnet}-${DAI.address}` - ) + expect(buildCurrencyId(UniverseChainId.Mainnet, DAI.address)).toEqual(`${UniverseChainId.Mainnet}-${DAI.address}`) }) }) @@ -45,12 +41,7 @@ describe(areCurrencyIdsEqual, () => { }) it('returns correct comparison between a checksummed and lowercased currencyId', () => { - expect( - areCurrencyIdsEqual( - currencyId(DAI), - `${UniverseChainId.Mainnet}-${DAI.address.toLowerCase()}` - ) - ).toBe(true) + expect(areCurrencyIdsEqual(currencyId(DAI), `${UniverseChainId.Mainnet}-${DAI.address.toLowerCase()}`)).toBe(true) }) it('returns correct comparison for the different currencyIds', () => { @@ -80,35 +71,25 @@ describe(getCurrencyAddressForAnalytics, () => { describe(buildNativeCurrencyId, () => { it('builds correct ID for Mainnet', () => { - expect(buildNativeCurrencyId(UniverseChainId.Mainnet)).toEqual( - `1-0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee` - ) + expect(buildNativeCurrencyId(UniverseChainId.Mainnet)).toEqual(`1-0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee`) }) it('builds correct ID for Polygon', () => { - expect(buildNativeCurrencyId(UniverseChainId.Polygon)).toEqual( - `137-0x0000000000000000000000000000000000001010` - ) + expect(buildNativeCurrencyId(UniverseChainId.Polygon)).toEqual(`137-0x0000000000000000000000000000000000001010`) }) it('builds correct ID for BNB', () => { - expect(buildNativeCurrencyId(UniverseChainId.Bnb)).toEqual( - `56-0xb8c77482e45f1f44de1745f52c74426c631bdd52` - ) + expect(buildNativeCurrencyId(UniverseChainId.Bnb)).toEqual(`56-0xb8c77482e45f1f44de1745f52c74426c631bdd52`) }) }) describe(isNativeCurrencyAddress, () => { it('returns true for native address', () => { - expect( - isNativeCurrencyAddress(UniverseChainId.Mainnet, getNativeAddress(UniverseChainId.Mainnet)) - ).toEqual(true) + expect(isNativeCurrencyAddress(UniverseChainId.Mainnet, getNativeAddress(UniverseChainId.Mainnet))).toEqual(true) }) it('returns true for matic native address', () => { - expect( - isNativeCurrencyAddress(UniverseChainId.Polygon, getNativeAddress(UniverseChainId.Polygon)) - ).toEqual(true) + expect(isNativeCurrencyAddress(UniverseChainId.Polygon, getNativeAddress(UniverseChainId.Polygon))).toEqual(true) }) it('returns true for null currency addresses', () => { @@ -116,9 +97,7 @@ describe(isNativeCurrencyAddress, () => { }) it('returns false for mainnet with Polygon native address', () => { - expect( - isNativeCurrencyAddress(UniverseChainId.Mainnet, getNativeAddress(UniverseChainId.Polygon)) - ).toEqual(false) + expect(isNativeCurrencyAddress(UniverseChainId.Mainnet, getNativeAddress(UniverseChainId.Polygon))).toEqual(false) }) it('returns false for token address', () => { @@ -133,7 +112,7 @@ describe(currencyIdToAddress, () => { it('returns correct address for native asset', () => { expect(currencyIdToAddress(`1-${getNativeAddress(UniverseChainId.Mainnet)}`)).toEqual( - getNativeAddress(UniverseChainId.Mainnet) + getNativeAddress(UniverseChainId.Mainnet), ) }) }) @@ -149,14 +128,12 @@ describe(currencyIdToGraphQLAddress, () => { it('returns MATIC address for Polygon native asset', () => { expect(currencyIdToGraphQLAddress('137-0x0000000000000000000000000000000000001010')).toEqual( - '0x0000000000000000000000000000000000001010' + '0x0000000000000000000000000000000000001010', ) }) it('returns null for BNB native asset', () => { - expect(currencyIdToGraphQLAddress('56-0xB8c77482e45F1F44dE1745F52C74426C631bDD52')).toEqual( - null - ) + expect(currencyIdToGraphQLAddress('56-0xB8c77482e45F1F44dE1745F52C74426C631bDD52')).toEqual(null) }) }) @@ -166,9 +143,7 @@ describe(currencyIdToChain, () => { }) it('returns correct chain for native asset', () => { - expect(currencyIdToChain(`1-${getNativeAddress(UniverseChainId.Mainnet)}`)).toEqual( - UniverseChainId.Mainnet - ) + expect(currencyIdToChain(`1-${getNativeAddress(UniverseChainId.Mainnet)}`)).toEqual(UniverseChainId.Mainnet) }) it('handles invalid currencyId', () => { diff --git a/packages/wallet/src/utils/currencyId.ts b/packages/wallet/src/utils/currencyId.ts index 2aad79f67d6..ac060f784e7 100644 --- a/packages/wallet/src/utils/currencyId.ts +++ b/packages/wallet/src/utils/currencyId.ts @@ -1,10 +1,10 @@ import { Currency } from '@uniswap/sdk-core' +import { getNativeAddress, getWrappedNativeAddress } from 'uniswap/src/constants/addresses' import { DEFAULT_NATIVE_ADDRESS } from 'uniswap/src/constants/chains' +import { toSupportedChainId } from 'uniswap/src/features/chains/utils' import { UniverseChainId, WalletChainId } from 'uniswap/src/types/chains' import { CurrencyId } from 'uniswap/src/types/currency' -import { getNativeAddress, getWrappedNativeAddress } from 'wallet/src/constants/addresses' -import { toSupportedChainId } from 'wallet/src/features/chains/utils' -import { areAddressesEqual } from 'wallet/src/utils/addresses' +import { areAddressesEqual } from 'uniswap/src/utils/addresses' export function currencyId(currency: Currency): CurrencyId { return buildCurrencyId(currency.chainId, currencyAddress(currency)) @@ -44,10 +44,7 @@ export function getCurrencyAddressForAnalytics(currency: Currency): string { return currency.address } -export const isNativeCurrencyAddress = ( - chainId: WalletChainId, - address: Maybe

        -): boolean => { +export const isNativeCurrencyAddress = (chainId: WalletChainId, address: Maybe
        ): boolean => { if (!address) { return true } @@ -64,9 +61,7 @@ export function currencyIdToAddress(_currencyId: string): Address { return currencyIdParts[1] } -function isPolygonChain( - chainId: number -): chainId is UniverseChainId.Polygon | UniverseChainId.PolygonMumbai { +function isPolygonChain(chainId: number): chainId is UniverseChainId.Polygon | UniverseChainId.PolygonMumbai { return chainId === UniverseChainId.PolygonMumbai || chainId === UniverseChainId.Polygon } @@ -88,11 +83,7 @@ export function currencyIdToGraphQLAddress(_currencyId?: string): Address | null } // backend only expects `null` for the native asset, except Polygon & Celo - if ( - isNativeCurrencyAddress(chainId, address) && - !isPolygonChain(chainId) && - !isCeloChain(chainId) - ) { + if (isNativeCurrencyAddress(chainId, address) && !isPolygonChain(chainId) && !isCeloChain(chainId)) { return null } diff --git a/packages/wallet/src/utils/getCurrencyAmount.test.ts b/packages/wallet/src/utils/getCurrencyAmount.test.ts index 8f9b42bc10f..3d4fa107619 100644 --- a/packages/wallet/src/utils/getCurrencyAmount.test.ts +++ b/packages/wallet/src/utils/getCurrencyAmount.test.ts @@ -1,5 +1,5 @@ import { CurrencyAmount } from '@uniswap/sdk-core' -import { DAI } from 'wallet/src/constants/tokens' +import { DAI } from 'uniswap/src/constants/tokens' import { noOpFunction } from 'wallet/src/test/mocks' import { getCurrencyAmount, ValueType } from 'wallet/src/utils/getCurrencyAmount' @@ -9,33 +9,23 @@ const FRACTION_OF_DAI = CurrencyAmount.fromRawAmount(DAI, '1000000000000000') describe(getCurrencyAmount, () => { it('handle undefined inputs', () => { - expect( - getCurrencyAmount({ value: undefined, valueType: ValueType.Raw, currency: undefined }) - ).toBeUndefined() + expect(getCurrencyAmount({ value: undefined, valueType: ValueType.Raw, currency: undefined })).toBeUndefined() }) it('handle undefined float value', () => { - expect( - getCurrencyAmount({ value: undefined, valueType: ValueType.Exact, currency: DAI }) - ).toBeUndefined() + expect(getCurrencyAmount({ value: undefined, valueType: ValueType.Exact, currency: DAI })).toBeUndefined() }) it('handle undefined uin5256 value', () => { - expect( - getCurrencyAmount({ value: undefined, valueType: ValueType.Raw, currency: DAI }) - ).toBeUndefined() + expect(getCurrencyAmount({ value: undefined, valueType: ValueType.Raw, currency: DAI })).toBeUndefined() }) it('handle undefined Currency wit defined float value', () => { - expect( - getCurrencyAmount({ value: '1.6', valueType: ValueType.Exact, currency: undefined }) - ).toBeUndefined() + expect(getCurrencyAmount({ value: '1.6', valueType: ValueType.Exact, currency: undefined })).toBeUndefined() }) it('handle undefined Currency with defined uint256 value', () => { - expect( - getCurrencyAmount({ value: '1000000000', valueType: ValueType.Exact, currency: undefined }) - ).toBeUndefined() + expect(getCurrencyAmount({ value: '1000000000', valueType: ValueType.Exact, currency: undefined })).toBeUndefined() }) it('return null when float value is 0', () => { @@ -43,9 +33,7 @@ describe(getCurrencyAmount, () => { }) it('parse standard float amount', () => { - expect(getCurrencyAmount({ value: '1', valueType: ValueType.Exact, currency: DAI })).toEqual( - ONE_DAI - ) + expect(getCurrencyAmount({ value: '1', valueType: ValueType.Exact, currency: DAI })).toEqual(ONE_DAI) }) it('parse standard raw amount', () => { @@ -54,14 +42,12 @@ describe(getCurrencyAmount, () => { value: '1000000000000000000', valueType: ValueType.Raw, currency: DAI, - }) + }), ).toEqual(ONE_DAI) }) it('parse decimal float amount', () => { - expect(getCurrencyAmount({ value: '0.5', valueType: ValueType.Exact, currency: DAI })).toEqual( - HALF_DAI - ) + expect(getCurrencyAmount({ value: '0.5', valueType: ValueType.Exact, currency: DAI })).toEqual(HALF_DAI) }) it('parse fractional raw amount', () => { @@ -70,7 +56,7 @@ describe(getCurrencyAmount, () => { value: '500000000000000000', valueType: ValueType.Raw, currency: DAI, - }) + }), ).toEqual(HALF_DAI) }) @@ -81,7 +67,7 @@ describe(getCurrencyAmount, () => { value: '0.00000000000000000000001', valueType: ValueType.Exact, currency: DAI, - }) + }), ).toBeNull() }) @@ -92,7 +78,7 @@ describe(getCurrencyAmount, () => { value: '0.1', valueType: ValueType.Raw, currency: DAI, - }) + }), ).toBeNull() }) @@ -102,7 +88,7 @@ describe(getCurrencyAmount, () => { value: '.', valueType: ValueType.Exact, currency: DAI, - }) + }), ).toBeNull() }) @@ -113,7 +99,7 @@ describe(getCurrencyAmount, () => { value: '123as2s', valueType: ValueType.Exact, currency: DAI, - }) + }), ).toBeNull() }) @@ -123,7 +109,7 @@ describe(getCurrencyAmount, () => { value: '0x38d7ea4c68000', valueType: ValueType.Raw, currency: DAI, - }) + }), ).toEqual(FRACTION_OF_DAI) }) }) diff --git a/packages/wallet/src/utils/getCurrencyAmount.ts b/packages/wallet/src/utils/getCurrencyAmount.ts index 680d30e277d..3f952a8cff1 100644 --- a/packages/wallet/src/utils/getCurrencyAmount.ts +++ b/packages/wallet/src/utils/getCurrencyAmount.ts @@ -67,13 +67,7 @@ export function getCurrencyAmount({ } } -const sanitizeTokenAmount = ({ - value, - valueType, -}: { - value: string - valueType: ValueType -}): string => { +const sanitizeTokenAmount = ({ value, valueType }: { value: string; valueType: ValueType }): string => { let sanitizedValue = convertScientificNotationToNumber(value) if (sanitizedValue === '.') { diff --git a/packages/wallet/src/utils/linking.test.ts b/packages/wallet/src/utils/linking.test.ts index d9bcff959bb..a21b37e5b06 100644 --- a/packages/wallet/src/utils/linking.test.ts +++ b/packages/wallet/src/utils/linking.test.ts @@ -3,29 +3,29 @@ import { ExplorerDataType, getExplorerLink } from 'wallet/src/utils/linking' describe(getExplorerLink, () => { it('handles different link cases', () => { - expect( - getExplorerLink(UniverseChainId.ArbitrumOne, 'hash', ExplorerDataType.TRANSACTION) - ).toEqual('https://arbiscan.io/tx/hash') + expect(getExplorerLink(UniverseChainId.ArbitrumOne, 'hash', ExplorerDataType.TRANSACTION)).toEqual( + 'https://arbiscan.io/tx/hash', + ) expect(getExplorerLink(UniverseChainId.Mainnet, 'hash', ExplorerDataType.ADDRESS)).toEqual( - 'https://etherscan.io/address/hash' + 'https://etherscan.io/address/hash', ) expect(getExplorerLink(UniverseChainId.Polygon, 'hash', ExplorerDataType.TOKEN)).toEqual( - 'https://polygonscan.com/token/hash' + 'https://polygonscan.com/token/hash', ) expect(getExplorerLink(UniverseChainId.PolygonMumbai, 'hash', ExplorerDataType.BLOCK)).toEqual( - 'https://mumbai.polygonscan.com/block/hash' + 'https://mumbai.polygonscan.com/block/hash', ) }) it('handles chain with explorer URL', () => { expect(getExplorerLink(UniverseChainId.Goerli, 'hash', ExplorerDataType.TRANSACTION)).toEqual( - 'https://goerli.etherscan.io/tx/hash' + 'https://goerli.etherscan.io/tx/hash', ) }) it('handles Optimism block special case', () => { expect(getExplorerLink(UniverseChainId.Optimism, 'hash', ExplorerDataType.BLOCK)).toEqual( - 'https://optimistic.etherscan.io/tx/hash' + 'https://optimistic.etherscan.io/tx/hash', ) }) }) diff --git a/packages/wallet/src/utils/linking.ts b/packages/wallet/src/utils/linking.ts index fccb1b5b87f..9c64b953419 100644 --- a/packages/wallet/src/utils/linking.ts +++ b/packages/wallet/src/utils/linking.ts @@ -1,82 +1,20 @@ import * as WebBrowser from 'expo-web-browser' import { Linking } from 'react-native' -import { colorsLight } from 'ui/src/theme' import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' import { uniswapUrls } from 'uniswap/src/constants/urls' +import { toUniswapWebAppLink } from 'uniswap/src/features/chains/utils' import { UniverseChainId, WalletChainId } from 'uniswap/src/types/chains' -import { logger } from 'utilities/src/logger/logger' -import { toUniswapWebAppLink } from 'wallet/src/features/chains/utils' -import { - FiatPurchaseTransactionInfo, - ServiceProviderInfo, -} from 'wallet/src/features/transactions/types' -import { currencyIdToChain, currencyIdToGraphQLAddress } from 'wallet/src/utils/currencyId' +import { currencyIdToChain, currencyIdToGraphQLAddress } from 'uniswap/src/utils/currencyId' +import { openUri } from 'uniswap/src/utils/linking' +import { FiatPurchaseTransactionInfo, ServiceProviderInfo } from 'wallet/src/features/transactions/types' export const UNISWAP_APP_NATIVE_TOKEN = 'NATIVE' -const ALLOWED_EXTERNAL_URI_SCHEMES = ['http://', 'https://'] - -/** - * Opens allowed URIs. if isSafeUri is set to true then this will open http:// and https:// as well as some deeplinks. - * Only set this flag to true if you have formed the URL yourself in our own app code. For any URLs from an external source - * isSafeUri must be false and it will only open http:// and https:// URI schemes. - * - * @param openExternalBrowser whether to leave the app and open in system browser. default is false, opens in-app browser window - * @param isSafeUri whether to bypass ALLOWED_EXTERNAL_URI_SCHEMES check - * @param controlsColor When opening in an in-app browser, determines the controls color - **/ -export async function openUri( - uri: string, - openExternalBrowser = false, - isSafeUri = false, - // NOTE: okay to use colors object directly as we want the same color for light/dark modes - controlsColor = colorsLight.accent1 -): Promise { - const trimmedURI = uri.trim() - if (!isSafeUri && !ALLOWED_EXTERNAL_URI_SCHEMES.some((scheme) => trimmedURI.startsWith(scheme))) { - // TODO: [MOB-253] show a visual warning that the link cannot be opened. - logger.error(new Error('User attempted to open potentially unsafe url'), { - tags: { - file: 'linking', - function: 'openUri', - }, - extra: { uri }, - }) - return - } - - const isHttp = /^https?:\/\//.test(trimmedURI) - - // `canOpenURL` returns `false` for App Links / Universal Links, so we just assume any device can handle the `https://` protocol. - const supported = isHttp ? true : await Linking.canOpenURL(uri) - - if (!supported) { - logger.warn('linking', 'openUri', `Cannot open URI: ${uri}`) - return - } - - try { - if (openExternalBrowser) { - await Linking.openURL(uri) - } else { - await WebBrowser.openBrowserAsync(uri, { - controlsColor, - presentationStyle: WebBrowser.WebBrowserPresentationStyle.FULL_SCREEN, - windowFeatures: 'popup=false', - }) - } - } catch (error) { - logger.error(error, { tags: { file: 'linking', function: 'openUri' } }) - } -} export function dismissInAppBrowser(): void { WebBrowser.dismissBrowser() } -export async function openTransactionLink( - hash: string | undefined, - chainId: WalletChainId -): Promise { +export async function openTransactionLink(hash: string | undefined, chainId: WalletChainId): Promise { if (!hash) { return } @@ -101,11 +39,8 @@ const SERVICE_PROVIDER_SUPPORT_URLS: Record = { STRIPE: 'https://support.stripe.com/', } -export async function openLegacyFiatOnRampServiceProviderLink( - serviceProvider: string -): Promise { - const helpUrl = - SERVICE_PROVIDER_SUPPORT_URLS[serviceProvider] ?? 'https://www.moonpay.com/contact-us' +export async function openLegacyFiatOnRampServiceProviderLink(serviceProvider: string): Promise { + const helpUrl = SERVICE_PROVIDER_SUPPORT_URLS[serviceProvider] ?? 'https://www.moonpay.com/contact-us' return openUri(helpUrl) } @@ -122,6 +57,15 @@ export enum ExplorerDataType { TOKEN = 'token', ADDRESS = 'address', BLOCK = 'block', + NFT = 'nft', +} + +/** + * Return the explorer name for the given chain ID + * @param chainId the ID of the chain for which to return the explorer name + */ +export function getExplorerName(chainId: UniverseChainId): string { + return UNIVERSE_CHAIN_INFO[chainId].explorer.name } /** @@ -130,11 +74,7 @@ export enum ExplorerDataType { * @param data the data to return a link for * @param type the type of the data */ -export function getExplorerLink( - chainId: WalletChainId, - data: string, - type: ExplorerDataType -): string { +export function getExplorerLink(chainId: WalletChainId, data: string, type: ExplorerDataType): string { const prefix = UNIVERSE_CHAIN_INFO[chainId].explorer.url switch (type) { @@ -158,6 +98,18 @@ export function getExplorerLink( case ExplorerDataType.ADDRESS: return `${prefix}address/${data}` + + case ExplorerDataType.NFT: + if (chainId === UniverseChainId.Zora) { + // Zora Energy Explorer uses a different URL format of [blockExplorerUrl]/token/[contractAddress]/instance/[tokenId] + // We need to split the data to get the contract address and token ID + const splitData = data.split('/') + const contractAddress = splitData[0] ?? '' + const tokenAddress = splitData[1] ?? '' + return `${prefix}token/${contractAddress}/instance/${tokenAddress}` + } + return `${prefix}nft/${data}` + default: return `${prefix}` } @@ -180,10 +132,7 @@ export function getProfileUrl(walletAddress: string): string { const UTM_TAGS_MOBILE = 'utm_medium=mobile&utm_source=share-tdp' -export function getTokenUrl( - currencyId: string, - addMobileUTMTags: boolean = false -): string | undefined { +export function getTokenUrl(currencyId: string, addMobileUTMTags: boolean = false): string | undefined { const chainId = currencyIdToChain(currencyId) if (!chainId) { return diff --git a/packages/wallet/src/utils/mnemonics.test.ts b/packages/wallet/src/utils/mnemonics.test.ts index 58712b10f36..3b3496bbb7d 100644 --- a/packages/wallet/src/utils/mnemonics.test.ts +++ b/packages/wallet/src/utils/mnemonics.test.ts @@ -3,24 +3,24 @@ import { MnemonicValidationError, translateMnemonicErrorMessage } from 'wallet/s describe(translateMnemonicErrorMessage, () => { it('correct invalid phrase message', () => { - expect( - translateMnemonicErrorMessage(MnemonicValidationError.InvalidPhrase, undefined, i18n.t) - ).toBe('Invalid phrase') + expect(translateMnemonicErrorMessage(MnemonicValidationError.InvalidPhrase, undefined, i18n.t)).toBe( + 'Invalid phrase', + ) }) it('correct invalid word message', () => { const invalidWord = 'gibberish' - expect( - translateMnemonicErrorMessage(MnemonicValidationError.InvalidWord, invalidWord, i18n.t) - ).toBe(`Invalid word: ${invalidWord}`) + expect(translateMnemonicErrorMessage(MnemonicValidationError.InvalidWord, invalidWord, i18n.t)).toBe( + `Invalid word: ${invalidWord}`, + ) }) it('correct incorrect number of words message', () => { - expect( - translateMnemonicErrorMessage(MnemonicValidationError.TooManyWords, undefined, i18n.t) - ).toBe('Recovery phrase must be 12-24 words') - expect( - translateMnemonicErrorMessage(MnemonicValidationError.NotEnoughWords, undefined, i18n.t) - ).toBe('Recovery phrase must be 12-24 words') + expect(translateMnemonicErrorMessage(MnemonicValidationError.TooManyWords, undefined, i18n.t)).toBe( + 'Recovery phrase must be 12-24 words', + ) + expect(translateMnemonicErrorMessage(MnemonicValidationError.NotEnoughWords, undefined, i18n.t)).toBe( + 'Recovery phrase must be 12-24 words', + ) }) }) diff --git a/packages/wallet/src/utils/mnemonics.ts b/packages/wallet/src/utils/mnemonics.ts index a380d3b8c0f..857daa56220 100644 --- a/packages/wallet/src/utils/mnemonics.ts +++ b/packages/wallet/src/utils/mnemonics.ts @@ -13,7 +13,7 @@ export enum MnemonicValidationError { export function translateMnemonicErrorMessage( error: MnemonicValidationError, invalidWord: string | undefined, - t: AppTFunction + t: AppTFunction, ): string { switch (error) { case MnemonicValidationError.InvalidPhrase: diff --git a/packages/wallet/src/utils/password.test.ts b/packages/wallet/src/utils/password.test.ts index 8dd3eeea047..ed4078cbe5f 100644 --- a/packages/wallet/src/utils/password.test.ts +++ b/packages/wallet/src/utils/password.test.ts @@ -11,13 +11,13 @@ describe(isPasswordStrongEnough, () => { isPasswordStrongEnough({ minStrength: PasswordStrength.NONE, currentStrength: PasswordStrength.NONE, - }) + }), ).toBeTruthy() expect( isPasswordStrongEnough({ minStrength: PasswordStrength.MEDIUM, currentStrength: PasswordStrength.MEDIUM, - }) + }), ).toBeTruthy() }) @@ -26,13 +26,13 @@ describe(isPasswordStrongEnough, () => { isPasswordStrongEnough({ minStrength: PasswordStrength.MEDIUM, currentStrength: PasswordStrength.NONE, - }) + }), ).toBeFalsy() expect( isPasswordStrongEnough({ minStrength: PasswordStrength.MEDIUM, currentStrength: PasswordStrength.WEAK, - }) + }), ).toBeFalsy() }) @@ -41,13 +41,13 @@ describe(isPasswordStrongEnough, () => { isPasswordStrongEnough({ minStrength: PasswordStrength.NONE, currentStrength: PasswordStrength.NONE, - }) + }), ).toBeTruthy() expect( isPasswordStrongEnough({ minStrength: PasswordStrength.NONE, currentStrength: PasswordStrength.NONE, - }) + }), ).toBeTruthy() }) }) diff --git a/packages/wallet/src/utils/password.ts b/packages/wallet/src/utils/password.ts index 96e80cefb35..bc14e78231d 100644 --- a/packages/wallet/src/utils/password.ts +++ b/packages/wallet/src/utils/password.ts @@ -98,21 +98,18 @@ export function usePasswordForm(): { currentStrength: passwordStrength, minStrength: PasswordStrength.MEDIUM, }), - [passwordStrength, password] + [passwordStrength, password], ) const debouncedPassword = useDebounce(password, PASSWORD_VALIDATION_DEBOUNCE_MS) const debouncedConfirmPassword = useDebounce(confirmPassword, PASSWORD_VALIDATION_DEBOUNCE_MS) // Used to disable the continue button right away - const passwordsDiffer = useMemo( - () => doPasswordsDiffer(password, confirmPassword), - [password, confirmPassword] - ) + const passwordsDiffer = useMemo(() => doPasswordsDiffer(password, confirmPassword), [password, confirmPassword]) // Used to show the error message after debounce time const debouncedPasswordsDiffer = useMemo( () => doPasswordsDiffer(debouncedPassword, debouncedConfirmPassword), - [debouncedPassword, debouncedConfirmPassword] + [debouncedPassword, debouncedConfirmPassword], ) const enableNext = Boolean(password && confirmPassword) && !isWeakPassword && !passwordsDiffer diff --git a/packages/wallet/src/utils/persistedStorage.ts b/packages/wallet/src/utils/persistedStorage.ts index b57f786f0b7..5d64f09beff 100644 --- a/packages/wallet/src/utils/persistedStorage.ts +++ b/packages/wallet/src/utils/persistedStorage.ts @@ -27,7 +27,7 @@ export class PersistedStorage { return chrome.storage[this.area].set({ [key]: value }) } - removeItem(key: string): Promise { + removeItem(key: string | string[]): Promise { return chrome.storage[this.area].remove(key) } diff --git a/packages/wallet/src/utils/saga.ts b/packages/wallet/src/utils/saga.ts index b17e30656c0..22cab9883a9 100644 --- a/packages/wallet/src/utils/saga.ts +++ b/packages/wallet/src/utils/saga.ts @@ -12,7 +12,7 @@ import { AppNotificationType } from 'wallet/src/features/notifications/types' */ export function createSaga( saga: (params: SagaParams) => unknown, - name: string + name: string, ): { wrappedSaga: () => Generator actions: { @@ -24,9 +24,7 @@ export function createSaga( const wrappedSaga = function* () { while (true) { try { - const trigger = yield* take<{ type: typeof triggerAction.type; payload: SagaParams }>( - triggerAction.type - ) + const trigger = yield* take<{ type: typeof triggerAction.type; payload: SagaParams }>(triggerAction.type) logger.debug('saga', 'wrappedSaga', `${name} triggered`) yield* call(saga, trigger.payload) } catch (error) { @@ -75,7 +73,7 @@ interface MonitoredSagaOptions { export function createMonitoredSaga( saga: (params: SagaParams) => unknown, name: string, - options?: MonitoredSagaOptions + options?: MonitoredSagaOptions, ): { name: string // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -104,16 +102,14 @@ export function createMonitoredSaga( .addCase(resetAction, (state) => { state.status = null state.error = null - }) + }), ) // eslint-disable-next-line @typescript-eslint/no-explicit-any const wrappedSaga = function* (): any { while (true) { try { - const trigger = yield* take<{ type: typeof triggerAction.type; payload: SagaParams }>( - triggerAction.type - ) + const trigger = yield* take<{ type: typeof triggerAction.type; payload: SagaParams }>(triggerAction.type) logger.debug('saga', 'monitoredSaga', `${name} triggered`) yield* put(statusAction(SagaStatus.Started)) const { result, cancel, timeout } = yield* race({ @@ -154,7 +150,7 @@ export function createMonitoredSaga( pushNotification({ type: AppNotificationType.Error, errorMessage, - }) + }), ) } } diff --git a/packages/wallet/src/utils/transaction.ts b/packages/wallet/src/utils/transaction.ts index 6e44b2e4c4a..a59c3895070 100644 --- a/packages/wallet/src/utils/transaction.ts +++ b/packages/wallet/src/utils/transaction.ts @@ -4,9 +4,7 @@ const formatAsHexString = (input?: BigNumberish): string | undefined => input !== undefined ? BigNumber.from(input).toHexString() : input // hexlifyTransaction is idempotent so it's safe to call more than once on a singular transaction request -export function hexlifyTransaction( - transferTxRequest: providers.TransactionRequest -): providers.TransactionRequest { +export function hexlifyTransaction(transferTxRequest: providers.TransactionRequest): providers.TransactionRequest { const { value, nonce, gasLimit, gasPrice, maxPriorityFeePerGas, maxFeePerGas } = transferTxRequest return { ...transferTxRequest, diff --git a/packages/wallet/src/utils/useDynamicFontSizing.test.ts b/packages/wallet/src/utils/useDynamicFontSizing.test.ts index 3bb7d21cd1d..0403ac4b58c 100644 --- a/packages/wallet/src/utils/useDynamicFontSizing.test.ts +++ b/packages/wallet/src/utils/useDynamicFontSizing.test.ts @@ -9,7 +9,7 @@ const MAX_CHAR_PIXEL_WIDTH = 23 describe(useDynamicFontSizing, () => { it('returns maxFontSize if text input element width is not set', () => { const { result } = renderHook(() => - useDynamicFontSizing(MAX_CHAR_PIXEL_WIDTH, MAX_INPUT_FONT_SIZE, MIN_INPUT_FONT_SIZE) + useDynamicFontSizing(MAX_CHAR_PIXEL_WIDTH, MAX_INPUT_FONT_SIZE, MIN_INPUT_FONT_SIZE), ) expect(result.current.fontSize).toBe(MAX_INPUT_FONT_SIZE) @@ -17,7 +17,7 @@ describe(useDynamicFontSizing, () => { it('returns maxFontSize as fontSize if text fits in the container', async () => { const { result } = renderHook(() => - useDynamicFontSizing(MAX_CHAR_PIXEL_WIDTH, MAX_INPUT_FONT_SIZE, MIN_INPUT_FONT_SIZE) + useDynamicFontSizing(MAX_CHAR_PIXEL_WIDTH, MAX_INPUT_FONT_SIZE, MIN_INPUT_FONT_SIZE), ) await act(() => { @@ -31,7 +31,7 @@ describe(useDynamicFontSizing, () => { it('scales down font when text does not fit in the container', async () => { const { result } = renderHook(() => - useDynamicFontSizing(MAX_CHAR_PIXEL_WIDTH, MAX_INPUT_FONT_SIZE, MIN_INPUT_FONT_SIZE) + useDynamicFontSizing(MAX_CHAR_PIXEL_WIDTH, MAX_INPUT_FONT_SIZE, MIN_INPUT_FONT_SIZE), ) await act(() => { @@ -45,7 +45,7 @@ describe(useDynamicFontSizing, () => { it("doesn't return font size less than minFontSize", async () => { const { result } = renderHook(() => - useDynamicFontSizing(MAX_CHAR_PIXEL_WIDTH, MAX_INPUT_FONT_SIZE, MIN_INPUT_FONT_SIZE) + useDynamicFontSizing(MAX_CHAR_PIXEL_WIDTH, MAX_INPUT_FONT_SIZE, MIN_INPUT_FONT_SIZE), ) await act(() => { diff --git a/packages/wallet/src/utils/useDynamicFontSizing.ts b/packages/wallet/src/utils/useDynamicFontSizing.ts index a0807981ac4..e770cc75fc8 100644 --- a/packages/wallet/src/utils/useDynamicFontSizing.ts +++ b/packages/wallet/src/utils/useDynamicFontSizing.ts @@ -4,7 +4,7 @@ import { LayoutChangeEvent } from 'react-native' export function useDynamicFontSizing( maxCharWidthAtMaxFontSize: number, maxFontSize: number, - minFontSize: number + minFontSize: number, ): { onLayout: (event: LayoutChangeEvent) => void fontSize: number @@ -31,7 +31,7 @@ export function useDynamicFontSizing( const newFontSize = Math.round(Math.min(maxFontSize, scaledSizeWithMin)) setFontSize(newFontSize) }, - [fontSize, maxFontSize, minFontSize, maxCharWidthAtMaxFontSize] + [fontSize, maxFontSize, minFontSize, maxCharWidthAtMaxFontSize], ) return { onLayout, fontSize, onSetFontSize } @@ -41,7 +41,7 @@ const getStringWidth = ( value: string, maxCharWidthAtMaxFontSize: number, currentFontSize: number, - maxFontSize: number + maxFontSize: number, ): number => { const widthAtMaxFontSize = value.length * maxCharWidthAtMaxFontSize return widthAtMaxFontSize * (currentFontSize / maxFontSize) diff --git a/packages/wallet/src/utils/useNoYoloParser.ts b/packages/wallet/src/utils/useNoYoloParser.ts new file mode 100644 index 00000000000..dbd7ad302e7 --- /dev/null +++ b/packages/wallet/src/utils/useNoYoloParser.ts @@ -0,0 +1,27 @@ +import { JsonRpcProvider } from '@ethersproject/providers' +import { ExplorerAbiFetcher, Parser, ProxyAbiFetcher } from 'no-yolo-signatures' +import { useMemo } from 'react' +import { UNIVERSE_CHAIN_INFO } from 'uniswap/src/constants/chains' +import { RPCType, WalletChainId } from 'uniswap/src/types/chains' + +export function useNoYoloParser(chainId: WalletChainId): Parser { + const parser = useMemo(() => { + const rpcUrls = UNIVERSE_CHAIN_INFO[chainId].rpcUrls + const apiURL = UNIVERSE_CHAIN_INFO[chainId].explorer.apiURL || '' + + const explorerAbiFetcher = new ExplorerAbiFetcher(apiURL) + + const rpcUrl = + rpcUrls?.appOnly?.http[0] || + rpcUrls?.default?.http[0] || + rpcUrls?.[RPCType.Public]?.http[0] || + rpcUrls?.[RPCType.PublicAlt]?.http[0] + const provider = new JsonRpcProvider(rpcUrl) + + const proxyAbiFetcher = new ProxyAbiFetcher(provider, [explorerAbiFetcher]) + + return new Parser({ abiFetchers: [proxyAbiFetcher, explorerAbiFetcher] }) + }, [chainId]) + + return parser +} diff --git a/scripts/prettier.sh b/scripts/prettier.sh new file mode 100755 index 00000000000..3876434726d --- /dev/null +++ b/scripts/prettier.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# Default check +mode="-c" + +# Can change to write with "--write" +if [[ "$1" == "--write" ]]; then + mode="-w" +fi + +# Store the directory from which the script was called +CALLER_DIR="$(pwd)" + +# Get the directory of the script +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Change to the script directory +cd "$SCRIPT_DIR/.." + +./node_modules/.bin/prettier $mode $CALLER_DIR --ignore-path .prettierignore diff --git a/scripts/turbo-changed.sh b/scripts/turbo-changed.sh index a8f5fe4c9ca..9e67c5ceea4 100755 --- a/scripts/turbo-changed.sh +++ b/scripts/turbo-changed.sh @@ -4,8 +4,8 @@ command=$1 # remove first argument so we pass along options to turbo shift # Validate command input -if [[ ! $command =~ ^(lint|test|typecheck)$ ]]; then - echo "Invalid command: $command. Must be one of: lint, test, typecheck." +if [[ ! $command =~ ^(lint|test|typecheck|format)$ ]]; then + echo "Invalid command: $command. Must be one of: format, lint, test, typecheck." exit 1 fi diff --git a/turbo.json b/turbo.json index 3f53ee70c7b..6f1bf6bc611 100644 --- a/turbo.json +++ b/turbo.json @@ -111,6 +111,13 @@ }, "check:circular": {}, "check:deps:usage": {}, + "format": { + "inputs": [ + "**/*.ts", + "**/*.tsx", + "**/*.js" + ] + }, "lint": { "dependsOn": [ "typecheck", diff --git a/yarn.lock b/yarn.lock index 37c4e3167f1..9f32bfdaf93 100644 --- a/yarn.lock +++ b/yarn.lock @@ -549,7 +549,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-module-imports@npm:^7.0.0, @babel/helper-module-imports@npm:^7.10.4, @babel/helper-module-imports@npm:^7.14.5, @babel/helper-module-imports@npm:^7.16.7, @babel/helper-module-imports@npm:^7.22.15, @babel/helper-module-imports@npm:^7.22.5, @babel/helper-module-imports@npm:^7.24.1": +"@babel/helper-module-imports@npm:^7.0.0, @babel/helper-module-imports@npm:^7.10.4, @babel/helper-module-imports@npm:^7.14.5, @babel/helper-module-imports@npm:^7.22.15, @babel/helper-module-imports@npm:^7.22.5, @babel/helper-module-imports@npm:^7.24.1": version: 7.24.3 resolution: "@babel/helper-module-imports@npm:7.24.3" dependencies: @@ -2097,12 +2097,12 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.10.2, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.16.3, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.20.6, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.22.5, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.7, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.4.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.3, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.9.2": - version: 7.24.4 - resolution: "@babel/runtime@npm:7.24.4" +"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.10.2, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.16.3, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.19.4, @babel/runtime@npm:^7.20.0, @babel/runtime@npm:^7.20.6, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.23.7, @babel/runtime@npm:^7.23.9, @babel/runtime@npm:^7.4.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.3, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.9.2": + version: 7.24.7 + resolution: "@babel/runtime@npm:7.24.7" dependencies: regenerator-runtime: ^0.14.0 - checksum: 2f27d4c0ffac7ae7999ac0385e1106f2a06992a8bdcbf3da06adcac7413863cd08c198c2e4e970041bbea849e17f02e1df18875539b6afba76c781b6b59a07c3 + checksum: d17f29eed6f848ac15cdf4202a910b741facfb0419a9d79e5c7fa37df6362fc3227f1cc2e248cc6db5e53ddffb4caa6686c488e6e80ce3d29c36a4e74c8734ea languageName: node linkType: hard @@ -2744,6 +2744,27 @@ __metadata: languageName: node linkType: hard +"@datadog/browser-core@npm:5.20.0": + version: 5.20.0 + resolution: "@datadog/browser-core@npm:5.20.0" + checksum: 82fb37ee0d02bda6f958549fd7b0c7f075613172bcb27133d2fa2fb91b22f6b4b1002042c0e96960993ebf0cde1d14aa55cdfaa2bc99bc358a793e491563d5c2 + languageName: node + linkType: hard + +"@datadog/browser-logs@npm:^5.20.0": + version: 5.20.0 + resolution: "@datadog/browser-logs@npm:5.20.0" + dependencies: + "@datadog/browser-core": 5.20.0 + peerDependencies: + "@datadog/browser-rum": 5.20.0 + peerDependenciesMeta: + "@datadog/browser-rum": + optional: true + checksum: d3ead69bb892e7d4015c58bdf180a318e0e2efcb572e503334852a458c31014b26b9948b1aec759b6b5a2fc743045b071be6a58685b191249b3bc4235b407fca + languageName: node + linkType: hard + "@dependents/detective-less@npm:^3.0.1": version: 3.0.2 resolution: "@dependents/detective-less@npm:3.0.2" @@ -2789,25 +2810,6 @@ __metadata: languageName: node linkType: hard -"@emotion/babel-plugin@npm:^11.11.0": - version: 11.11.0 - resolution: "@emotion/babel-plugin@npm:11.11.0" - dependencies: - "@babel/helper-module-imports": ^7.16.7 - "@babel/runtime": ^7.18.3 - "@emotion/hash": ^0.9.1 - "@emotion/memoize": ^0.8.1 - "@emotion/serialize": ^1.1.2 - babel-plugin-macros: ^3.1.0 - convert-source-map: ^1.5.0 - escape-string-regexp: ^4.0.0 - find-root: ^1.1.0 - source-map: ^0.5.7 - stylis: 4.2.0 - checksum: 6b363edccc10290f7a23242c06f88e451b5feb2ab94152b18bb8883033db5934fb0e421e2d67d09907c13837c21218a3ac28c51707778a54d6cd3706c0c2f3f9 - languageName: node - linkType: hard - "@emotion/cache@npm:^10.0.27": version: 10.0.29 resolution: "@emotion/cache@npm:10.0.29" @@ -2820,19 +2822,6 @@ __metadata: languageName: node linkType: hard -"@emotion/cache@npm:^11.11.0": - version: 11.11.0 - resolution: "@emotion/cache@npm:11.11.0" - dependencies: - "@emotion/memoize": ^0.8.1 - "@emotion/sheet": ^1.2.2 - "@emotion/utils": ^1.2.1 - "@emotion/weak-memoize": ^0.3.1 - stylis: 4.2.0 - checksum: 8eb1dc22beaa20c21a2e04c284d5a2630a018a9d51fb190e52de348c8d27f4e8ca4bbab003d68b4f6cd9cc1c569ca747a997797e0f76d6c734a660dc29decf08 - languageName: node - linkType: hard - "@emotion/core@npm:^10.0.0": version: 10.3.1 resolution: "@emotion/core@npm:10.3.1" @@ -2867,7 +2856,7 @@ __metadata: languageName: node linkType: hard -"@emotion/hash@npm:^0.9.0, @emotion/hash@npm:^0.9.1": +"@emotion/hash@npm:^0.9.0": version: 0.9.1 resolution: "@emotion/hash@npm:0.9.1" checksum: 716e17e48bf9047bf9383982c071de49f2615310fb4e986738931776f5a823bc1f29c84501abe0d3df91a3803c80122d24e28b57351bca9e01356ebb33d89876 @@ -2883,7 +2872,7 @@ __metadata: languageName: node linkType: hard -"@emotion/is-prop-valid@npm:^1.1.0, @emotion/is-prop-valid@npm:^1.2.2": +"@emotion/is-prop-valid@npm:^1.1.0": version: 1.2.2 resolution: "@emotion/is-prop-valid@npm:1.2.2" dependencies: @@ -2913,27 +2902,6 @@ __metadata: languageName: node linkType: hard -"@emotion/react@npm:^11.10.6": - version: 11.11.4 - resolution: "@emotion/react@npm:11.11.4" - dependencies: - "@babel/runtime": ^7.18.3 - "@emotion/babel-plugin": ^11.11.0 - "@emotion/cache": ^11.11.0 - "@emotion/serialize": ^1.1.3 - "@emotion/use-insertion-effect-with-fallbacks": ^1.0.1 - "@emotion/utils": ^1.2.1 - "@emotion/weak-memoize": ^0.3.1 - hoist-non-react-statics: ^3.3.1 - peerDependencies: - react: ">=16.8.0" - peerDependenciesMeta: - "@types/react": - optional: true - checksum: 6abaa7a05c5e1db31bffca7ac79169f5456990022cbb3794e6903221536609a60420f2b4888dd3f84e9634a304e394130cb88dc32c243a1dedc263e50da329f8 - languageName: node - linkType: hard - "@emotion/serialize@npm:^0.11.15, @emotion/serialize@npm:^0.11.16": version: 0.11.16 resolution: "@emotion/serialize@npm:0.11.16" @@ -2947,19 +2915,6 @@ __metadata: languageName: node linkType: hard -"@emotion/serialize@npm:^1.1.2, @emotion/serialize@npm:^1.1.3, @emotion/serialize@npm:^1.1.4": - version: 1.1.4 - resolution: "@emotion/serialize@npm:1.1.4" - dependencies: - "@emotion/hash": ^0.9.1 - "@emotion/memoize": ^0.8.1 - "@emotion/unitless": ^0.8.1 - "@emotion/utils": ^1.2.1 - csstype: ^3.0.2 - checksum: 71b99f816a9c1d61a87c62cf4928da3894bb62213f3aff38b1ea9790b3368f084af98a3e5453b5055c2f36a7d70318d2fa9955b7b5676c2065b868062375df39 - languageName: node - linkType: hard - "@emotion/sheet@npm:0.9.4": version: 0.9.4 resolution: "@emotion/sheet@npm:0.9.4" @@ -2967,13 +2922,6 @@ __metadata: languageName: node linkType: hard -"@emotion/sheet@npm:^1.2.2": - version: 1.2.2 - resolution: "@emotion/sheet@npm:1.2.2" - checksum: d973273c9c15f1c291ca2269728bf044bd3e92a67bca87943fa9ec6c3cd2b034f9a6bfe95ef1b5d983351d128c75b547b43ff196a00a3875f7e1d269793cecfe - languageName: node - linkType: hard - "@emotion/styled-base@npm:^10.3.0": version: 10.3.0 resolution: "@emotion/styled-base@npm:10.3.0" @@ -3002,26 +2950,6 @@ __metadata: languageName: node linkType: hard -"@emotion/styled@npm:^11.10.6": - version: 11.11.5 - resolution: "@emotion/styled@npm:11.11.5" - dependencies: - "@babel/runtime": ^7.18.3 - "@emotion/babel-plugin": ^11.11.0 - "@emotion/is-prop-valid": ^1.2.2 - "@emotion/serialize": ^1.1.4 - "@emotion/use-insertion-effect-with-fallbacks": ^1.0.1 - "@emotion/utils": ^1.2.1 - peerDependencies: - "@emotion/react": ^11.0.0-rc.0 - react: ">=16.8.0" - peerDependenciesMeta: - "@types/react": - optional: true - checksum: ad5fc42d00e8aa9597f6d9665986036d5ebe0e8f8155af6d95831c5e8fb2319fb837724e6c5cd59e5346f14c3263711b7ce7271d34688e974d1f32ffeecb37ba - languageName: node - linkType: hard - "@emotion/stylis@npm:0.8.5, @emotion/stylis@npm:^0.8.4": version: 0.8.5 resolution: "@emotion/stylis@npm:0.8.5" @@ -3036,22 +2964,6 @@ __metadata: languageName: node linkType: hard -"@emotion/unitless@npm:^0.8.1": - version: 0.8.1 - resolution: "@emotion/unitless@npm:0.8.1" - checksum: 385e21d184d27853bb350999471f00e1429fa4e83182f46cd2c164985999d9b46d558dc8b9cc89975cb337831ce50c31ac2f33b15502e85c299892e67e7b4a88 - languageName: node - linkType: hard - -"@emotion/use-insertion-effect-with-fallbacks@npm:^1.0.1": - version: 1.0.1 - resolution: "@emotion/use-insertion-effect-with-fallbacks@npm:1.0.1" - peerDependencies: - react: ">=16.8.0" - checksum: 700b6e5bbb37a9231f203bb3af11295eed01d73b2293abece0bc2a2237015e944d7b5114d4887ad9a79776504aa51ed2a8b0ddbc117c54495dd01a6b22f93786 - languageName: node - linkType: hard - "@emotion/utils@npm:0.11.3": version: 0.11.3 resolution: "@emotion/utils@npm:0.11.3" @@ -3059,13 +2971,6 @@ __metadata: languageName: node linkType: hard -"@emotion/utils@npm:^1.2.1": - version: 1.2.1 - resolution: "@emotion/utils@npm:1.2.1" - checksum: e0b44be0705b56b079c55faff93952150be69e79b660ae70ddd5b6e09fc40eb1319654315a9f34bb479d7f4ec94be6068c061abbb9e18b9778ae180ad5d97c73 - languageName: node - linkType: hard - "@emotion/weak-memoize@npm:0.2.5": version: 0.2.5 resolution: "@emotion/weak-memoize@npm:0.2.5" @@ -3073,13 +2978,6 @@ __metadata: languageName: node linkType: hard -"@emotion/weak-memoize@npm:^0.3.1": - version: 0.3.1 - resolution: "@emotion/weak-memoize@npm:0.3.1" - checksum: b2be47caa24a8122622ea18cd2d650dbb4f8ad37b636dc41ed420c2e082f7f1e564ecdea68122b546df7f305b159bf5ab9ffee872abd0f052e687428459af594 - languageName: node - linkType: hard - "@esbuild-plugins/node-globals-polyfill@npm:^0.2.3": version: 0.2.3 resolution: "@esbuild-plugins/node-globals-polyfill@npm:0.2.3" @@ -6125,7 +6023,7 @@ __metadata: languageName: node linkType: hard -"@metamask/json-rpc-engine@npm:^7.0.0": +"@metamask/json-rpc-engine@npm:^7.0.0, @metamask/json-rpc-engine@npm:^7.3.2": version: 7.3.3 resolution: "@metamask/json-rpc-engine@npm:7.3.3" dependencies: @@ -6136,14 +6034,25 @@ __metadata: languageName: node linkType: hard -"@metamask/object-multiplex@npm:^1.1.0": - version: 1.3.0 - resolution: "@metamask/object-multiplex@npm:1.3.0" +"@metamask/json-rpc-middleware-stream@npm:^6.0.2": + version: 6.0.2 + resolution: "@metamask/json-rpc-middleware-stream@npm:6.0.2" + dependencies: + "@metamask/json-rpc-engine": ^7.3.2 + "@metamask/safe-event-emitter": ^3.0.0 + "@metamask/utils": ^8.3.0 + readable-stream: ^3.6.2 + checksum: e831041b03e9f48f584f4425188f72b58974f95b60429c9fe8b5561da69c6bbfad2f2b2199acdff06ee718967214b65c05604d4f85f3287186619683487f1060 + languageName: node + linkType: hard + +"@metamask/object-multiplex@npm:^2.0.0": + version: 2.0.0 + resolution: "@metamask/object-multiplex@npm:2.0.0" dependencies: - end-of-stream: ^1.4.4 once: ^1.4.0 - readable-stream: ^2.3.3 - checksum: 4a2b48fc0e1a8f536edbab9f37b637cd91102538ad76ce07bdfad99b90d98b34585a0e5afa62ca9c1d550a0016347568ff0d635e5bf8cfa266d049e1c0ebedc8 + readable-stream: ^3.6.2 + checksum: 54baea752a3ac7c2742c376512e00d4902d383e9da8787574d3b21eb0081523309e24e3915a98f3ae0341d65712b6832d2eb7eeb862f4ef0da1ead52dcde5387 languageName: node linkType: hard @@ -6156,33 +6065,23 @@ __metadata: languageName: node linkType: hard -"@metamask/post-message-stream@npm:^6.1.0": - version: 6.2.0 - resolution: "@metamask/post-message-stream@npm:6.2.0" - dependencies: - "@metamask/utils": ^5.0.0 - readable-stream: 2.3.3 - checksum: 657cdb2dd61a46a4da7f036a97ef0aa9ad8e918d8f8c0fd620eaede4a32c2ff909738a7dfb2b1e6099e7771fd03c3466b60fedab56e39a5cc5507927758e3cb7 - languageName: node - linkType: hard - -"@metamask/providers@npm:^10.2.1": - version: 10.2.1 - resolution: "@metamask/providers@npm:10.2.1" +"@metamask/providers@npm:^15.0.0": + version: 15.0.0 + resolution: "@metamask/providers@npm:15.0.0" dependencies: - "@metamask/object-multiplex": ^1.1.0 - "@metamask/safe-event-emitter": ^2.0.0 - "@types/chrome": ^0.0.136 + "@metamask/json-rpc-engine": ^7.3.2 + "@metamask/json-rpc-middleware-stream": ^6.0.2 + "@metamask/object-multiplex": ^2.0.0 + "@metamask/rpc-errors": ^6.2.1 + "@metamask/safe-event-emitter": ^3.0.0 + "@metamask/utils": ^8.3.0 detect-browser: ^5.2.0 - eth-rpc-errors: ^4.0.2 - extension-port-stream: ^2.0.1 - fast-deep-equal: ^2.0.1 + extension-port-stream: ^3.0.0 + fast-deep-equal: ^3.1.3 is-stream: ^2.0.0 - json-rpc-engine: ^6.1.0 - json-rpc-middleware-stream: ^4.2.1 - pump: ^3.0.0 - webextension-polyfill-ts: ^0.25.0 - checksum: e88b2db8c4673cc6a7e47d9f0531df3fac73f05f8e9ff6d02c3420dfb3c7a82335d9c44876f2d472c44eac36d66491d2022be4f39600bee561d5de8ad59c5b07 + readable-stream: ^3.6.2 + webextension-polyfill: ^0.10.0 + checksum: 42571450e79d69d943384f557f6a61e0f73101d49804fb6e8075d791959f76c42b8ff626f711d434674792d77aead6cb8a32b04a3dcd53598c8aff24cbb1ad25 languageName: node linkType: hard @@ -6210,79 +6109,92 @@ __metadata: languageName: node linkType: hard -"@metamask/sdk-communication-layer@npm:0.14.3": - version: 0.14.3 - resolution: "@metamask/sdk-communication-layer@npm:0.14.3" +"@metamask/sdk-communication-layer@npm:0.18.5": + version: 0.18.5 + resolution: "@metamask/sdk-communication-layer@npm:0.18.5" dependencies: bufferutil: ^4.0.8 - cross-fetch: ^3.1.5 date-fns: ^2.29.3 - eciesjs: ^0.3.16 - eventemitter2: ^6.4.5 - socket.io-client: ^4.5.1 + debug: ^4.3.4 utf-8-validate: ^6.0.3 uuid: ^8.3.2 - checksum: 1a4d89a8bef3c4a08df151a1f95d0eca65f18715a1de3e66ae3b7dd1f7cb58957edb1cba7f1af13ee037b50866d25a0402917c640e380dbbe32534cfa0764398 + peerDependencies: + cross-fetch: ^3.1.5 + eciesjs: ^0.3.16 + eventemitter2: ^6.4.7 + readable-stream: ^3.6.2 + socket.io-client: ^4.5.1 + checksum: 7d8e632e0d8b95a093272a941adcf8b40d257a3b285dac0821c60a8ff36d1915a4998f0950bc3a7bd89dc9f15b0c00034e7d5a71a590218d1e5bdccc9224a2b5 languageName: node linkType: hard -"@metamask/sdk-install-modal-web@npm:0.14.1": - version: 0.14.1 - resolution: "@metamask/sdk-install-modal-web@npm:0.14.1" +"@metamask/sdk-install-modal-web@npm:0.18.5": + version: 0.18.5 + resolution: "@metamask/sdk-install-modal-web@npm:0.18.5" dependencies: - "@emotion/react": ^11.10.6 - "@emotion/styled": ^11.10.6 - i18next: 22.5.1 qr-code-styling: ^1.6.0-rc.1 + peerDependencies: + i18next: 22.5.1 react: ^18.2.0 react-dom: ^18.2.0 react-i18next: ^13.2.2 - checksum: 9122f3d0395514a4a8c2a4da5d805587b4af5d2112c333ea2dd08fa9c2046aea2f0f91bddade05364653b06899b64a849a902d645307e393c557fd878cffd50b + react-native: "*" + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + react-native: + optional: true + checksum: 57fd8dc802ef0d2949c34df6387abf3633bfb0da03cbce570735060991337835a8846694216bfe14d2764fa21cf0d70fc14cecb442b02f8e468f3560acc7b8e7 languageName: node linkType: hard -"@metamask/sdk@npm:0.14.3": - version: 0.14.3 - resolution: "@metamask/sdk@npm:0.14.3" +"@metamask/sdk@npm:0.18.6": + version: 0.18.6 + resolution: "@metamask/sdk@npm:0.18.6" dependencies: "@metamask/onboarding": ^1.0.1 - "@metamask/post-message-stream": ^6.1.0 - "@metamask/providers": ^10.2.1 - "@metamask/sdk-communication-layer": 0.14.3 - "@metamask/sdk-install-modal-web": 0.14.1 - "@react-native-async-storage/async-storage": ^1.17.11 + "@metamask/providers": ^15.0.0 + "@metamask/sdk-communication-layer": 0.18.5 + "@metamask/sdk-install-modal-web": 0.18.5 "@types/dom-screen-wake-lock": ^1.0.0 bowser: ^2.9.0 cross-fetch: ^4.0.0 + debug: ^4.3.4 eciesjs: ^0.3.15 eth-rpc-errors: ^4.0.3 eventemitter2: ^6.4.7 - extension-port-stream: ^2.0.1 i18next: 22.5.1 - i18next-browser-languagedetector: ^7.1.0 + i18next-browser-languagedetector: 7.1.0 obj-multiplex: ^1.0.0 pump: ^3.0.0 qrcode-terminal-nooctal: ^0.12.1 - react-i18next: ^13.2.2 react-native-webview: ^11.26.0 - readable-stream: ^2.3.7 + readable-stream: ^3.6.2 rollup-plugin-visualizer: ^5.9.2 socket.io-client: ^4.5.1 util: ^0.12.4 uuid: ^8.3.2 peerDependencies: + "@react-native-async-storage/async-storage": ^1.19.6 react: ^18.2.0 + react-dom: ^18.2.0 react-native: "*" peerDependenciesMeta: + "@react-native-async-storage/async-storage": + optional: true react: optional: true + react-dom: + optional: true react-native: optional: true - checksum: da43da4b39c558ec2d6a472634e0b4b9fa3f3e46b4397368438c3a86764fac7e3dde386a0b50d9ce5ad43431879ce80178c01cef507eac28e67980006c82eeb6 + checksum: 936ce46f32cdba4dbc3dc892a245e85fbd7ff73c75b5625f2faa30b771b6d0ff2223c5021629361d0e2c8c45f22227f322444df9f6eaf4a961c61b9f06333200 languageName: node linkType: hard -"@metamask/utils@npm:^5.0.0, @metamask/utils@npm:^5.0.1": +"@metamask/utils@npm:^5.0.1": version: 5.0.2 resolution: "@metamask/utils@npm:5.0.2" dependencies: @@ -8158,21 +8070,21 @@ __metadata: languageName: node linkType: hard -"@rive-app/canvas@npm:2.8.3": - version: 2.8.3 - resolution: "@rive-app/canvas@npm:2.8.3" - checksum: 1318aecc3256279b806828c53ddd4d18e3967a61a19563af2d9d86e251b5dc105d8a4839ed191da7cd4eb42f5f2f4456d85c91df35bfbf1d49c75da58a064c87 +"@rive-app/canvas@npm:2.19.0": + version: 2.19.0 + resolution: "@rive-app/canvas@npm:2.19.0" + checksum: 27c9df0a0c32639aee6f48209c13ccd05f16d71b379e0ee36aa0434826a1f4b3bc6d880b8d7e996eb4350b78e194ebc0cae24c3978c8cfda4a0361f4282cba22 languageName: node linkType: hard -"@rive-app/react-canvas@npm:4.6.2": - version: 4.6.2 - resolution: "@rive-app/react-canvas@npm:4.6.2" +"@rive-app/react-canvas@npm:4.13.0": + version: 4.13.0 + resolution: "@rive-app/react-canvas@npm:4.13.0" dependencies: - "@rive-app/canvas": 2.8.3 + "@rive-app/canvas": 2.19.0 peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: 61180635fe7ca6bd60ee99a176fefcdca7a5c3475fd9dd7d263a809c062b1ea1168fec6902326272107afe70eae62917bf65d2e5d5073c40dbaad3baed295598 + checksum: 50f049daf52392d5f86bbac5c93aa49d26bf8e7656b1682947f95623e74d4fa06c55fcc552dd3a95f2e9538dfc3d9e643f488ef42c472027420fe3f0faa992fa languageName: node linkType: hard @@ -11760,16 +11672,6 @@ __metadata: languageName: node linkType: hard -"@types/chrome@npm:^0.0.136": - version: 0.0.136 - resolution: "@types/chrome@npm:0.0.136" - dependencies: - "@types/filesystem": "*" - "@types/har-format": "*" - checksum: af96fdc79fb019d827fdb6269f831921f8f36215ee05a2624436dd2ad4d84d7be12333cc6f54912fb8bae0ca49cbfde5a78de94723bfbd20d309d2e71e274a1b - languageName: node - linkType: hard - "@types/connect-history-api-fallback@npm:^1.3.5": version: 1.3.5 resolution: "@types/connect-history-api-fallback@npm:1.3.5" @@ -13410,10 +13312,10 @@ __metadata: languageName: node linkType: hard -"@uniswap/analytics-events@npm:2.32.0": - version: 2.32.0 - resolution: "@uniswap/analytics-events@npm:2.32.0" - checksum: 7341bb14025bcd72e7367dcc29a1833ad18f316b9310875432d4bbe62da8a2d2f2db473d6e0a10c12a02ff8d1815db0ef66fbb169ad98d80317f2c3fe91bcde2 +"@uniswap/analytics-events@npm:2.34.0": + version: 2.34.0 + resolution: "@uniswap/analytics-events@npm:2.34.0" + checksum: 86d2c6035596ed8c4491a5bc2047cec5d1f1a59cfa9e3337499cbe9e955eeff6a09ad1c864ea64253f694263b1af397a6a96926b26a55699f0f36d3548bad145 languageName: node linkType: hard @@ -13468,8 +13370,6 @@ __metadata: eslint-plugin-storybook: ^0.6.10 eslint-plugin-unused-imports: ^2.0.0 jest: 29.7.0 - prettier: ^2.8.0 - prettier-plugin-organize-imports: 3.2.4 typescript: 5.3.3 peerDependencies: eslint: ^8.0.0 @@ -13483,6 +13383,87 @@ __metadata: languageName: node linkType: hard +"@uniswap/extension@workspace:apps/extension": + version: 0.0.0-use.local + resolution: "@uniswap/extension@workspace:apps/extension" + dependencies: + "@apollo/client": 3.10.4 + "@ethersproject/providers": 5.7.2 + "@metamask/rpc-errors": 6.2.1 + "@pmmmwh/react-refresh-webpack-plugin": ^0.5.10 + "@reduxjs/toolkit": 1.9.3 + "@sentry/browser": 7.80.0 + "@sentry/react": 7.80.0 + "@sentry/webpack-plugin": 2.10.3 + "@svgr/webpack": 8.0.1 + "@tamagui/core": 1.95.1 + "@testing-library/dom": ^7.11.0 + "@testing-library/react": 13.4.0 + "@types/chrome": 0.0.254 + "@types/jest": 29.5.0 + "@types/react": ^18.0.15 + "@types/react-dom": ^18.0.6 + "@types/redux-logger": 3.0.9 + "@types/redux-persist-webextension-storage": 1.0.3 + "@types/ua-parser-js": 0.7.31 + "@types/uuid": 9.0.1 + "@uniswap/analytics-events": 2.34.0 + "@uniswap/eslint-config": "workspace:^" + "@uniswap/sdk-core": 5.3.0 + "@uniswap/universal-router-sdk": 2.2.0 + "@uniswap/v3-sdk": 3.13.0 + "@welldone-software/why-did-you-render": 8.0.1 + clean-webpack-plugin: ^4.0.0 + copy-webpack-plugin: ^11.0.0 + dotenv-webpack: 8.0.1 + esbuild-loader: ^3.0.1 + eslint: 8.44.0 + ethers: 5.7.2 + eventemitter3: 5.0.1 + i18next: 23.10.0 + jest: 29.7.0 + jest-chrome: 0.8.0 + jest-environment-jsdom: 29.5.0 + jest-extended: 4.0.1 + mini-css-extract-plugin: ^2.7.6 + node-polyfill-webpack-plugin: 2.0.1 + react: 18.2.0 + react-dom: 18.2.0 + react-i18next: 14.1.0 + react-native: 0.73.6 + react-native-gesture-handler: 2.15.0 + react-native-reanimated: "npm:react-native-reanimated@3.8.1" + react-native-svg: 15.1.0 + react-native-web: 0.19.10 + react-qr-code: 2.0.12 + react-redux: 8.0.5 + react-refresh: ^0.14.0 + react-router-dom: 6.10.0 + redux: 4.2.1 + redux-logger: 3.0.6 + redux-persist: 6.0.0 + redux-persist-webextension-storage: 1.0.2 + redux-saga: 1.2.2 + serve: ^14.2.0 + statsig-js: 4.41.0 + swc-loader: ^0.2.3 + symbol-observable: 4.0.0 + tamagui-loader: 1.95.1 + typed-redux-saga: 1.5.0 + typescript: 5.3.3 + ua-parser-js: 1.0.37 + ui: "workspace:^" + uniswap: "workspace:^" + utilities: "workspace:^" + uuid: 9.0.0 + wallet: "workspace:^" + webpack: 5.90.0 + webpack-cli: ^5.0.1 + webpack-dev-server: ^4.13.1 + zod: 3.22.4 + languageName: unknown + linkType: soft + "@uniswap/governance@npm:1.0.2": version: 1.0.2 resolution: "@uniswap/governance@npm:1.0.2" @@ -13514,7 +13495,8 @@ __metadata: "@reach/dialog": 0.10.5 "@reach/portal": 0.10.5 "@reduxjs/toolkit": 1.9.3 - "@rive-app/react-canvas": 4.6.2 + "@rive-app/canvas": 2.19.0 + "@rive-app/react-canvas": 4.13.0 "@sentry/browser": 7.80.0 "@sentry/core": 7.80.0 "@sentry/react": 7.80.0 @@ -13558,7 +13540,7 @@ __metadata: "@types/wcag-contrast": 3.0.0 "@types/xml2js": 0.4.14 "@uniswap/analytics": 1.7.0 - "@uniswap/analytics-events": 2.32.0 + "@uniswap/analytics-events": 2.34.0 "@uniswap/default-token-list": 11.19.0 "@uniswap/eslint-config": "workspace:^" "@uniswap/governance": 1.0.2 @@ -13647,7 +13629,6 @@ __metadata: polished: 3.3.2 polyfill-object.fromentries: 1.0.1 postinstall-postinstall: 2.1.0 - prettier: latest process: 0.11.10 qrcode.react: 3.1.0 qs: 6.9.4 @@ -13696,7 +13677,7 @@ __metadata: uuid: 9.0.0 video-extensions: 1.2.0 viem: 2.x - wagmi: 2.5.19 + wagmi: 2.8.4 wcag-contrast: 3.0.0 web-vitals: 2.1.4 webpack: 5.90.0 @@ -13785,7 +13766,7 @@ __metadata: "@testing-library/react-native": 11.5.0 "@types/redux-mock-store": 1.0.6 "@uniswap/analytics": 1.7.0 - "@uniswap/analytics-events": 2.32.0 + "@uniswap/analytics-events": 2.34.0 "@uniswap/eslint-config": "workspace:^" "@uniswap/ethers-rs-mobile": 0.0.5 "@uniswap/sdk-core": 5.3.0 @@ -13805,7 +13786,7 @@ __metadata: core-js: 2.6.12 cross-fetch: 3.1.5 dayjs: 1.11.7 - detox: 20.18.1 + detox: 20.23.0 eslint: 8.44.0 ethers: 5.7.2 expo: 50.0.15 @@ -13973,86 +13954,6 @@ __metadata: languageName: node linkType: hard -"@uniswap/stretch@workspace:apps/stretch": - version: 0.0.0-use.local - resolution: "@uniswap/stretch@workspace:apps/stretch" - dependencies: - "@apollo/client": 3.10.4 - "@ethersproject/providers": 5.7.2 - "@metamask/rpc-errors": 6.2.1 - "@pmmmwh/react-refresh-webpack-plugin": ^0.5.10 - "@reduxjs/toolkit": 1.9.3 - "@sentry/browser": 7.80.0 - "@sentry/react": 7.80.0 - "@sentry/webpack-plugin": 2.10.3 - "@svgr/webpack": 8.0.1 - "@tamagui/core": 1.95.1 - "@testing-library/dom": ^7.11.0 - "@testing-library/react": 13.4.0 - "@types/chrome": 0.0.254 - "@types/jest": 29.5.0 - "@types/react": ^18.0.15 - "@types/react-dom": ^18.0.6 - "@types/redux-logger": 3.0.9 - "@types/redux-persist-webextension-storage": 1.0.3 - "@types/ua-parser-js": 0.7.31 - "@types/uuid": 9.0.1 - "@uniswap/analytics-events": 2.32.0 - "@uniswap/eslint-config": "workspace:^" - "@uniswap/universal-router-sdk": 2.2.0 - "@uniswap/v3-sdk": 3.13.0 - "@welldone-software/why-did-you-render": 8.0.1 - clean-webpack-plugin: ^4.0.0 - copy-webpack-plugin: ^11.0.0 - dotenv-webpack: 8.0.1 - esbuild-loader: ^3.0.1 - eslint: 8.44.0 - ethers: 5.7.2 - eventemitter3: 5.0.1 - i18next: 23.10.0 - jest: 29.7.0 - jest-chrome: 0.8.0 - jest-environment-jsdom: 29.5.0 - jest-extended: 4.0.1 - mini-css-extract-plugin: ^2.7.6 - node-polyfill-webpack-plugin: 2.0.1 - react: 18.2.0 - react-dom: 18.2.0 - react-i18next: 14.1.0 - react-native: 0.73.6 - react-native-gesture-handler: 2.15.0 - react-native-reanimated: "npm:react-native-reanimated@3.8.1" - react-native-svg: 15.1.0 - react-native-web: 0.19.10 - react-qr-code: 2.0.12 - react-redux: 8.0.5 - react-refresh: ^0.14.0 - react-router-dom: 6.10.0 - redux: 4.2.1 - redux-logger: 3.0.6 - redux-persist: 6.0.0 - redux-persist-webextension-storage: 1.0.2 - redux-saga: 1.2.2 - serve: ^14.2.0 - statsig-js: 4.41.0 - swc-loader: ^0.2.3 - symbol-observable: 4.0.0 - tamagui-loader: 1.95.1 - typed-redux-saga: 1.5.0 - typescript: 5.3.3 - ua-parser-js: 1.0.37 - ui: "workspace:^" - uniswap: "workspace:^" - utilities: "workspace:^" - uuid: 9.0.0 - wallet: "workspace:^" - webpack: 5.90.0 - webpack-cli: ^5.0.1 - webpack-dev-server: ^4.13.1 - zod: 3.22.4 - languageName: unknown - linkType: soft - "@uniswap/swap-router-contracts@npm:^1.3.0": version: 1.3.1 resolution: "@uniswap/swap-router-contracts@npm:1.3.1" @@ -14509,50 +14410,30 @@ __metadata: languageName: node linkType: hard -"@wagmi/connectors@npm:4.1.25": - version: 4.1.25 - resolution: "@wagmi/connectors@npm:4.1.25" +"@wagmi/connectors@npm:4.3.6": + version: 4.3.6 + resolution: "@wagmi/connectors@npm:4.3.6" dependencies: "@coinbase/wallet-sdk": 3.9.1 - "@metamask/sdk": 0.14.3 + "@metamask/sdk": 0.18.6 "@safe-global/safe-apps-provider": 0.18.1 "@safe-global/safe-apps-sdk": 8.1.0 - "@walletconnect/ethereum-provider": 2.11.2 + "@walletconnect/ethereum-provider": 2.13.0 "@walletconnect/modal": 2.6.2 peerDependencies: - "@wagmi/core": 2.6.16 - typescript: ">=5.0.4" - viem: 2.x - peerDependenciesMeta: - typescript: - optional: true - checksum: 7b402604ca05bbe04d905f8c9c9c9441c37b0baae5d1610b999d0e86a0c0c573b937c509d901bead3b3fdbd4caaf4a47dfc0ad9115e71ebf91ce6c2d900297c9 - languageName: node - linkType: hard - -"@wagmi/core@npm:2.6.16": - version: 2.6.16 - resolution: "@wagmi/core@npm:2.6.16" - dependencies: - eventemitter3: 5.0.1 - mipd: 0.0.5 - zustand: 4.4.1 - peerDependencies: - "@tanstack/query-core": ">=5.0.0" + "@wagmi/core": 2.9.4 typescript: ">=5.0.4" viem: 2.x peerDependenciesMeta: - "@tanstack/query-core": - optional: true typescript: optional: true - checksum: 6ae6d2f98bb87e2ea039b1e403c07804021fea936882c4a03b3dd67edc8b9ecd3f5e968a321f105c3e2be7186d9980c2e6fa5ac3f4248de06e0677e7ab601fee + checksum: aaa26ae9b2ac4531baa787dc05cbefcf4f9b038d0989d165e8281fa69d717122f3371bfac32ab243edc46be3b3d92d3512005eb32ef2afaa9676f48ba5bf785e languageName: node linkType: hard -"@wagmi/core@patch:@wagmi/core@npm%3A2.6.16#./.yarn/patches/@wagmi-core-npm-2.6.16-1baef7c190.patch::locator=universe%40workspace%3A.": - version: 2.6.16 - resolution: "@wagmi/core@patch:@wagmi/core@npm%3A2.6.16#./.yarn/patches/@wagmi-core-npm-2.6.16-1baef7c190.patch::version=2.6.16&hash=a4dce9&locator=universe%40workspace%3A." +"@wagmi/core@npm:2.9.4": + version: 2.9.4 + resolution: "@wagmi/core@npm:2.9.4" dependencies: eventemitter3: 5.0.1 mipd: 0.0.5 @@ -14566,7 +14447,7 @@ __metadata: optional: true typescript: optional: true - checksum: a3252d86a02c9c971ad9c8ba351ef01b14f960d76f73a45d2d7954d40bcdc9c38dcb6bbf6cb330f84c643a6e171c1dfbdfa58d077438fcadb9be7289c5386110 + checksum: ea6dc673a01aca1723abd14b05eb0691855e9f206f54499cec562dec614667f4ee4a535fec890fff1d9fde820d89a9642a24c89029ca6c6b001c88e5fee8b812 languageName: node linkType: hard @@ -21437,9 +21318,9 @@ __metadata: languageName: node linkType: hard -"detox@npm:20.18.1": - version: 20.18.1 - resolution: "detox@npm:20.18.1" +"detox@npm:20.23.0": + version: 20.23.0 + resolution: "detox@npm:20.23.0" dependencies: ajv: ^8.6.3 bunyan: ^1.8.12 @@ -21453,7 +21334,7 @@ __metadata: funpermaproxy: ^1.1.0 glob: ^8.0.3 ini: ^1.3.4 - jest-environment-emit: ^1.0.5 + jest-environment-emit: ^1.0.8 json-cycle: ^1.3.0 lodash: ^4.17.11 multi-sort-stream: ^1.0.3 @@ -21483,13 +21364,13 @@ __metadata: optional: true bin: detox: local-cli/cli.js - checksum: 29524453f189c5ec71d53fe4b85d11ca7cc925d79ab0f957a8259708b7e85ff741cd8e79ae2374cb84a793d01d2f32d3cb73cacae55ad8e31dff13bf3859a734 + checksum: e466a66ecf24194bb9ce4436539299134e9acc1accd00f04e29b2a53f9caef8d4e237a6d4f6a86b2a7f413b91261448c8daa97a917668db491719c02e951e3bb languageName: node linkType: hard -"detox@patch:detox@npm%3A20.18.1#./.yarn/patches/detox-npm-20.18.1-b532b310b4.patch::locator=universe%40workspace%3A.": - version: 20.18.1 - resolution: "detox@patch:detox@npm%3A20.18.1#./.yarn/patches/detox-npm-20.18.1-b532b310b4.patch::version=20.18.1&hash=9e679b&locator=universe%40workspace%3A." +"detox@patch:detox@npm%3A20.23.0#./.yarn/patches/detox-npm-20.23.0-6d61110e63.patch::locator=universe%40workspace%3A.": + version: 20.23.0 + resolution: "detox@patch:detox@npm%3A20.23.0#./.yarn/patches/detox-npm-20.23.0-6d61110e63.patch::version=20.23.0&hash=a63042&locator=universe%40workspace%3A." dependencies: ajv: ^8.6.3 bunyan: ^1.8.12 @@ -21503,7 +21384,7 @@ __metadata: funpermaproxy: ^1.1.0 glob: ^8.0.3 ini: ^1.3.4 - jest-environment-emit: ^1.0.5 + jest-environment-emit: ^1.0.8 json-cycle: ^1.3.0 lodash: ^4.17.11 multi-sort-stream: ^1.0.3 @@ -21533,7 +21414,7 @@ __metadata: optional: true bin: detox: local-cli/cli.js - checksum: 5a0fb05cf00201af01ca9450367fc0838b983502ef60d28d3e9aa6cb2a0a2d5fb2f2aaad8d1a67275ee3b38c1d6155a763c1c5b05888268b448a8bb710395829 + checksum: 086bb4845abfff283cc30ef2b2f66a8827a91b8cb1a6c4d4aa3bc9c9faac2c6e53d8f305e5b9da967d808202d780a5f8296f769bd3f331c5664c66b69403d3f8 languageName: node linkType: hard @@ -22001,7 +21882,7 @@ __metadata: languageName: node linkType: hard -"eciesjs@npm:^0.3.15, eciesjs@npm:^0.3.16": +"eciesjs@npm:^0.3.15": version: 0.3.18 resolution: "eciesjs@npm:0.3.18" dependencies: @@ -22165,7 +22046,7 @@ __metadata: languageName: node linkType: hard -"end-of-stream@npm:^1.0.0, end-of-stream@npm:^1.1.0, end-of-stream@npm:^1.4.0, end-of-stream@npm:^1.4.1, end-of-stream@npm:^1.4.4": +"end-of-stream@npm:^1.0.0, end-of-stream@npm:^1.1.0, end-of-stream@npm:^1.4.0, end-of-stream@npm:^1.4.1": version: 1.4.4 resolution: "end-of-stream@npm:1.4.4" dependencies: @@ -23580,7 +23461,7 @@ __metadata: languageName: node linkType: hard -"eventemitter2@npm:^6.4.5, eventemitter2@npm:^6.4.7": +"eventemitter2@npm:^6.4.7": version: 6.4.9 resolution: "eventemitter2@npm:6.4.9" checksum: be59577c1e1c35509c7ba0e2624335c35bbcfd9485b8a977384c6cc6759341ea1a98d3cb9dbaa5cea4fff9b687e504504e3f9c2cc1674cf3bd8a43a7c74ea3eb @@ -24126,12 +24007,13 @@ __metadata: languageName: node linkType: hard -"extension-port-stream@npm:^2.0.1": - version: 2.1.1 - resolution: "extension-port-stream@npm:2.1.1" +"extension-port-stream@npm:^3.0.0": + version: 3.0.0 + resolution: "extension-port-stream@npm:3.0.0" dependencies: + readable-stream: ^3.6.2 || ^4.4.2 webextension-polyfill: ">=0.10.0 <1.0" - checksum: aee8bbeb2ed6f69a62f58a89580e0e9002dadb11062edbaedb7bb04cfc5a5e0b0d3980bfeaa1c3ee7e08dec7e5fac26e25497fc2f82000db7653442bd5eca157 + checksum: 4f51d2258a96154c2d916a8a5425636a2b0817763e9277f7dc378d08b6f050c90d185dbde4313d27cf66ad99d4b3116479f9f699c40358c64cccfa524d2b55bf languageName: node linkType: hard @@ -24212,13 +24094,6 @@ __metadata: languageName: node linkType: hard -"fast-deep-equal@npm:^2.0.1": - version: 2.0.1 - resolution: "fast-deep-equal@npm:2.0.1" - checksum: b701835a87985e0ec4925bdf1f0c1e7eb56309b5d12d534d5b4b69d95a54d65bb16861c081781ead55f73f12d6c60ba668713391ee7fbf6b0567026f579b7b0b - languageName: node - linkType: hard - "fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": version: 3.1.3 resolution: "fast-deep-equal@npm:3.1.3" @@ -26378,7 +26253,7 @@ __metadata: languageName: node linkType: hard -"hoist-non-react-statics@npm:^3.0.0, hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.1, hoist-non-react-statics@npm:^3.3.2": +"hoist-non-react-statics@npm:^3.0.0, hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.2": version: 3.3.2 resolution: "hoist-non-react-statics@npm:3.3.2" dependencies: @@ -26780,12 +26655,12 @@ __metadata: languageName: node linkType: hard -"i18next-browser-languagedetector@npm:^7.1.0": - version: 7.2.1 - resolution: "i18next-browser-languagedetector@npm:7.2.1" +"i18next-browser-languagedetector@npm:7.1.0": + version: 7.1.0 + resolution: "i18next-browser-languagedetector@npm:7.1.0" dependencies: - "@babel/runtime": ^7.23.2 - checksum: 159958be2d8f19444e9378512c36c2bf13a8ab85eddac2fc0000198a03dbc28c73a6f44594ab040b242bdc82dfeabb7c1ab805884b5438ee0a48a8e2b52ca062 + "@babel/runtime": ^7.19.4 + checksum: 36981b9a9995ed66387f3735cceffe107ed3cdb6ca278d45fa243fabc65669c0eca095ed4a55a93dac046ca1eb23fd986ec0079723be7ebb8505e6ba25f379bb languageName: node linkType: hard @@ -28557,9 +28432,9 @@ __metadata: languageName: node linkType: hard -"jest-environment-emit@npm:^1.0.5": - version: 1.0.6 - resolution: "jest-environment-emit@npm:1.0.6" +"jest-environment-emit@npm:^1.0.8": + version: 1.0.8 + resolution: "jest-environment-emit@npm:1.0.8" dependencies: bunyamin: ^1.5.2 bunyan: ^2.0.5 @@ -28586,7 +28461,7 @@ __metadata: optional: true jest-environment-node: optional: true - checksum: a5af53deb4c712d2f91cdf1f11f35e823909a30534167fc093a89bf35539c6ed39b51f8b21299f1803e131318f094619e99fa7397389b03195bc0ef213e1dfc9 + checksum: 0c7bafbd3a6e5952f6abb45958f0d2997371d29b29f3876afda48d1d734ccd703577aaac0d5afec2e19dc33a9db0e9458721fe73dbe797f0ced21481d908acfd languageName: node linkType: hard @@ -29879,17 +29754,6 @@ __metadata: languageName: node linkType: hard -"json-rpc-middleware-stream@npm:^4.2.1": - version: 4.2.3 - resolution: "json-rpc-middleware-stream@npm:4.2.3" - dependencies: - "@metamask/safe-event-emitter": ^3.0.0 - json-rpc-engine: ^6.1.0 - readable-stream: ^2.3.3 - checksum: 0907d34935a8b58c3c67626e344272758f684c13175b2e7de2bac37309c3211fca7a129bce042d50faed605615f51fbba01e173bdc2ae6c14d95aefb9bfb4e09 - languageName: node - linkType: hard - "json-rpc-random-id@npm:^1.0.0, json-rpc-random-id@npm:^1.0.1": version: 1.0.1 resolution: "json-rpc-random-id@npm:1.0.1" @@ -35400,21 +35264,21 @@ __metadata: languageName: node linkType: hard -"prettier@npm:^2.1.2, prettier@npm:^2.6.2, prettier@npm:^2.8.0": - version: 2.8.8 - resolution: "prettier@npm:2.8.8" +"prettier@npm:3.3.2": + version: 3.3.2 + resolution: "prettier@npm:3.3.2" bin: - prettier: bin-prettier.js - checksum: b49e409431bf129dd89238d64299ba80717b57ff5a6d1c1a8b1a28b590d998a34e083fa13573bc732bb8d2305becb4c9a4407f8486c81fa7d55100eb08263cf8 + prettier: bin/prettier.cjs + checksum: 5557d8caed0b182f68123c2e1e370ef105251d1dd75800fadaece3d061daf96b1389141634febf776050f9d732c7ae8fd444ff0b4a61b20535e7610552f32c69 languageName: node linkType: hard -"prettier@npm:latest": - version: 2.8.7 - resolution: "prettier@npm:2.8.7" +"prettier@npm:^2.1.2, prettier@npm:^2.6.2": + version: 2.8.8 + resolution: "prettier@npm:2.8.8" bin: prettier: bin-prettier.js - checksum: fdc8f2616f099f5f0d685907f4449a70595a0fc1d081a88919604375989e0d5e9168d6121d8cc6861f21990b31665828e00472544d785d5940ea08a17660c3a6 + checksum: b49e409431bf129dd89238d64299ba80717b57ff5a6d1c1a8b1a28b590d998a34e083fa13573bc732bb8d2305becb4c9a4407f8486c81fa7d55100eb08263cf8 languageName: node linkType: hard @@ -35532,13 +35396,6 @@ __metadata: languageName: node linkType: hard -"process-nextick-args@npm:~1.0.6": - version: 1.0.7 - resolution: "process-nextick-args@npm:1.0.7" - checksum: 41224fbc803ac6c96907461d4dfc20942efa3ca75f2d521bcf7cf0e89f8dec127fb3fb5d76746b8fb468a232ea02d84824fae08e027aec185fd29049c66d49f8 - languageName: node - linkType: hard - "process-warning@npm:^1.0.0": version: 1.0.0 resolution: "process-warning@npm:1.0.0" @@ -36346,24 +36203,6 @@ __metadata: languageName: node linkType: hard -"react-i18next@npm:^13.2.2": - version: 13.5.0 - resolution: "react-i18next@npm:13.5.0" - dependencies: - "@babel/runtime": ^7.22.5 - html-parse-stringify: ^3.0.1 - peerDependencies: - i18next: ">= 23.2.3" - react: ">= 16.8.0" - peerDependenciesMeta: - react-dom: - optional: true - react-native: - optional: true - checksum: 2f68ccd24daf72ddd2d11a526fb3c2b66c11ea4fcd2e24ac7aed42bf57ec7bffa7455ad1dc93679968ff629e9b1896465cdf6d1a61c29b92138ef88098e8dcba - languageName: node - linkType: hard - "react-infinite-scroll-component@npm:6.1.0": version: 6.1.0 resolution: "react-infinite-scroll-component@npm:6.1.0" @@ -37405,22 +37244,7 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:2.3.3": - version: 2.3.3 - resolution: "readable-stream@npm:2.3.3" - dependencies: - core-util-is: ~1.0.0 - inherits: ~2.0.3 - isarray: ~1.0.0 - process-nextick-args: ~1.0.6 - safe-buffer: ~5.1.1 - string_decoder: ~1.0.3 - util-deprecate: ~1.0.1 - checksum: 76f9863065d7edc14abd78e68784048487e83a4b6908336ba3eacb5e9544d642ad60836f91fab16e1dc6ad9e493dfe6c2e5b65f370ec65454d415efa50361a76 - languageName: node - linkType: hard - -"readable-stream@npm:3, readable-stream@npm:^3.0.0, readable-stream@npm:^3.0.6, readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0, readable-stream@npm:^3.5.0, readable-stream@npm:^3.6.0": +"readable-stream@npm:3, readable-stream@npm:^3.0.0, readable-stream@npm:^3.0.6, readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0, readable-stream@npm:^3.5.0, readable-stream@npm:^3.6.0, readable-stream@npm:^3.6.2": version: 3.6.2 resolution: "readable-stream@npm:3.6.2" dependencies: @@ -37431,7 +37255,7 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:^2.0.0, readable-stream@npm:^2.0.1, readable-stream@npm:^2.0.2, readable-stream@npm:^2.0.5, readable-stream@npm:^2.0.6, readable-stream@npm:^2.1.5, readable-stream@npm:^2.3.3, readable-stream@npm:^2.3.5, readable-stream@npm:^2.3.6, readable-stream@npm:^2.3.7, readable-stream@npm:~2.3.6": +"readable-stream@npm:^2.0.0, readable-stream@npm:^2.0.1, readable-stream@npm:^2.0.2, readable-stream@npm:^2.0.5, readable-stream@npm:^2.0.6, readable-stream@npm:^2.1.5, readable-stream@npm:^2.3.3, readable-stream@npm:^2.3.5, readable-stream@npm:^2.3.6, readable-stream@npm:~2.3.6": version: 2.3.8 resolution: "readable-stream@npm:2.3.8" dependencies: @@ -37446,15 +37270,16 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:^4.0.0": - version: 4.4.0 - resolution: "readable-stream@npm:4.4.0" +"readable-stream@npm:^3.6.2 || ^4.4.2, readable-stream@npm:^4.0.0": + version: 4.5.2 + resolution: "readable-stream@npm:4.5.2" dependencies: abort-controller: ^3.0.0 buffer: ^6.0.3 events: ^3.3.0 process: ^0.11.10 - checksum: cc1630c2de134aee92646e77b1770019633000c408fd48609babf2caa53f00ca794928023aa9ad3d435a1044cec87d2ce7e2b7389dd1caf948b65c175edb7f52 + string_decoder: ^1.3.0 + checksum: c4030ccff010b83e4f33289c535f7830190773e274b3fcb6e2541475070bdfd69c98001c3b0cb78763fc00c8b62f514d96c2b10a8bd35d5ce45203a25fa1d33a languageName: node linkType: hard @@ -40249,15 +40074,6 @@ __metadata: languageName: node linkType: hard -"string_decoder@npm:~1.0.3": - version: 1.0.3 - resolution: "string_decoder@npm:1.0.3" - dependencies: - safe-buffer: ~5.1.0 - checksum: 57ef02a148fd1ff2f20fe1accd944505ed3703e78bb28d302d940b2ad3dfb469508f79dcd0275ba1960d9675aa206452f76b2416059a6d0b0200bd7e9f552cdb - languageName: node - linkType: hard - "string_decoder@npm:~1.1.1": version: 1.1.1 resolution: "string_decoder@npm:1.1.1" @@ -40492,13 +40308,6 @@ __metadata: languageName: node linkType: hard -"stylis@npm:4.2.0": - version: 4.2.0 - resolution: "stylis@npm:4.2.0" - checksum: 0eb6cc1b866dc17a6037d0a82ac7fa877eba6a757443e79e7c4f35bacedbf6421fadcab4363b39667b43355cbaaa570a3cde850f776498e5450f32ed2f9b7584 - languageName: node - linkType: hard - "stylus-lookup@npm:^3.0.1": version: 3.0.2 resolution: "stylus-lookup@npm:3.0.2" @@ -42303,6 +42112,7 @@ __metadata: i18next: 23.10.0 jest: 29.7.0 jest-presets: "workspace:^" + qrcode: 1.5.1 react: 18.2.0 react-native: 0.73.6 react-native-fast-image: 8.6.3 @@ -42598,7 +42408,10 @@ __metadata: resolution: "uniswap@workspace:packages/uniswap" dependencies: "@apollo/client": 3.10.4 + "@ethersproject/address": 5.7.0 + "@ethersproject/bignumber": 5.7.0 "@ethersproject/providers": 5.7.2 + "@faker-js/faker": 7.6.0 "@gorhom/bottom-sheet": 4.5.1 "@graphql-codegen/cli": ^3.3.1 "@graphql-codegen/client-preset": ^3.0.1 @@ -42613,7 +42426,8 @@ __metadata: "@testing-library/react-native": 11.5.0 "@typechain/ethers-v5": 7.2.0 "@types/chrome": 0.0.254 - "@uniswap/analytics-events": 2.32.0 + "@types/react-window": 1.8.2 + "@uniswap/analytics-events": 2.34.0 "@uniswap/eslint-config": "workspace:^" "@uniswap/router-sdk": 1.9.2 "@uniswap/sdk-core": 5.3.0 @@ -42622,6 +42436,9 @@ __metadata: eslint: 8.44.0 ethers: 5.7.2 expo-blur: 12.9.2 + expo-clipboard: 5.0.1 + expo-web-browser: 12.8.2 + fuse.js: 6.5.3 get-graphql-schema: ^2.1.2 i18next: 23.10.0 i18next-resources-for-ts: 1.5.0 @@ -42634,15 +42451,20 @@ __metadata: react-native-appsflyer: 6.13.1 react-native-device-info: 10.0.2 react-native-dotenv: 3.2.0 + react-native-gesture-handler: 2.15.0 react-native-reanimated: 3.8.1 react-test-renderer: 18.2.0 + react-virtualized-auto-sizer: 1.0.20 + react-window: 1.8.9 statsig-react: 1.32.0 statsig-react-native: 4.11.0 typechain: 5.2.0 typescript: 5.3.3 ui: "workspace:^" utilities: "workspace:^" - wagmi: 2.5.19 + uuid: 9.0.0 + wagmi: 2.8.4 + wcag-contrast: 3.0.0 languageName: unknown linkType: soft @@ -42696,6 +42518,8 @@ __metadata: husky: ^8.0.3 i18next: 23.10.0 i18next-parser: 8.6.0 + prettier: 3.3.2 + prettier-plugin-organize-imports: 3.2.4 syncpack: ^8.5.14 turbo: 1.10.16 turbo-ignore: ^1.11.3 @@ -43105,6 +42929,7 @@ __metadata: "@amplitude/analytics-react-native": 1.4.0 "@amplitude/analytics-types": 0.13.0 "@apollo/client": 3.10.4 + "@datadog/browser-logs": ^5.20.0 "@ethersproject/abstract-signer": 5.7.0 "@ethersproject/address": 5.7.0 "@ethersproject/constants": 5.7.0 @@ -43117,8 +42942,9 @@ __metadata: "@testing-library/react-hooks": 7.0.2 "@types/chrome": 0.0.254 "@types/react": ^18.0.15 + "@types/uuid": 9.0.1 "@uniswap/analytics": 1.7.0 - "@uniswap/analytics-events": 2.32.0 + "@uniswap/analytics-events": 2.34.0 "@uniswap/eslint-config": "workspace:^" "@uniswap/sdk-core": 5.3.0 aws-appsync-auth-link: 3.0.7 @@ -43131,9 +42957,11 @@ __metadata: jsbi: 3.2.5 react: 18.2.0 react-native: 0.73.6 + react-native-device-info: 10.0.2 react-test-renderer: 18.2.0 subscriptions-transport-ws: 0.11.0 typescript: 5.3.3 + uuid: 9.0.0 zen-observable-ts: 1.2.5 languageName: unknown linkType: soft @@ -43569,12 +43397,12 @@ __metadata: languageName: node linkType: hard -"wagmi@npm:2.5.19": - version: 2.5.19 - resolution: "wagmi@npm:2.5.19" +"wagmi@npm:2.8.4": + version: 2.8.4 + resolution: "wagmi@npm:2.8.4" dependencies: - "@wagmi/connectors": 4.1.25 - "@wagmi/core": 2.6.16 + "@wagmi/connectors": 4.3.6 + "@wagmi/core": 2.9.4 use-sync-external-store: 1.2.0 peerDependencies: "@tanstack/react-query": ">=5.0.0" @@ -43584,7 +43412,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: bdf767c0de083a485d9e4e13dc98e68968aac576eb7be9c1a59e3f9ea9eb6316edfb47b19238eb68c9f151ca5df95bf859c17c2003211f4df841b0ae72c46419 + checksum: 9e470a45da6612ce40721db2c405d9a5ebbccb7a1212c16f829ca45bc8f8d23b0f5419bfb00e6ddc18346146916f4773947baf79bcef836557d61e5b9ce66359 languageName: node linkType: hard @@ -43656,9 +43484,8 @@ __metadata: "@testing-library/react-hooks": 7.0.2 "@testing-library/react-native": 11.5.0 "@types/react": ^18.0.15 - "@types/react-window": 1.8.2 "@types/zxcvbn": 4.4.2 - "@uniswap/analytics-events": 2.32.0 + "@uniswap/analytics-events": 2.34.0 "@uniswap/eslint-config": "workspace:^" "@uniswap/permit2-sdk": 1.3.0 "@uniswap/router-sdk": 1.9.2 @@ -43674,7 +43501,6 @@ __metadata: depcheck: 1.4.7 eslint: 8.44.0 ethers: 5.7.2 - expo-clipboard: 5.0.1 expo-web-browser: 12.8.2 fuse.js: 6.5.3 graphql: 16.6.0 @@ -43686,8 +43512,8 @@ __metadata: jsbi: 3.2.5 lodash: 4.17.21 mockdate: 3.0.5 + no-yolo-signatures: 0.0.2 openapi-typescript-codegen: 0.27.0 - qrcode: 1.5.1 react: 18.2.0 react-i18next: 14.1.0 react-native: 0.73.6 @@ -43705,8 +43531,6 @@ __metadata: react-native-webview: 11.23.1 react-redux: 8.0.5 react-test-renderer: 18.2.0 - react-virtualized-auto-sizer: 1.0.20 - react-window: 1.8.9 redux: 4.2.1 redux-persist: 6.0.0 redux-saga: 1.2.2 @@ -43717,7 +43541,6 @@ __metadata: uniswap: "workspace:^" utilities: "workspace:^" uuid: 9.0.0 - wcag-contrast: 3.0.0 zod: 3.22.4 zxcvbn: 4.4.2 languageName: unknown @@ -43826,29 +43649,13 @@ __metadata: languageName: node linkType: hard -"webextension-polyfill-ts@npm:^0.25.0": - version: 0.25.0 - resolution: "webextension-polyfill-ts@npm:0.25.0" - dependencies: - webextension-polyfill: ^0.7.0 - checksum: c4dc82c86e34cea717a26af549f2822d63e92af52632f5e909ea13b5e7bd9d6110781f10313e36ada2b54c770ebca018bc3784756d12ba3b0b623d285f1a14a7 - languageName: node - linkType: hard - -"webextension-polyfill@npm:>=0.10.0 <1.0": +"webextension-polyfill@npm:>=0.10.0 <1.0, webextension-polyfill@npm:^0.10.0": version: 0.10.0 resolution: "webextension-polyfill@npm:0.10.0" checksum: 4a59036bda571360c2c0b2fb03fe1dc244f233946bcf9a6766f677956c40fd14d270aaa69cdba95e4ac521014afbe4008bfa5959d0ac39f91c990eb206587f91 languageName: node linkType: hard -"webextension-polyfill@npm:^0.7.0": - version: 0.7.0 - resolution: "webextension-polyfill@npm:0.7.0" - checksum: fb738a5de07feb593875e02f25c3ab4276c8736118929556c8d4bdf965bb0f11c96ea263cd397b9b21259e8faf2dce2eaaa42ce08c922d96de7adb5896ec7d10 - languageName: node - linkType: hard - "webidl-conversions@npm:^3.0.0": version: 3.0.1 resolution: "webidl-conversions@npm:3.0.1"

    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 zle�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