diff --git a/.github/workflows/macstadium-builds.yml b/.github/workflows/macstadium-builds.yml index 555ec762658..eb66d59324a 100644 --- a/.github/workflows/macstadium-builds.yml +++ b/.github/workflows/macstadium-builds.yml @@ -8,7 +8,8 @@ jobs: # Job to install dependencies build: runs-on: ["self-hosted"] - if: github.event.pull_request.draft == false + timeout-minutes: 75 + if: github.event.pull_request.draft == false && github.event.pull_request.merged == false concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true diff --git a/.github/workflows/macstadium-tests.yml b/.github/workflows/macstadium-tests.yml index dae6d82cc72..b06e42ae654 100644 --- a/.github/workflows/macstadium-tests.yml +++ b/.github/workflows/macstadium-tests.yml @@ -82,6 +82,7 @@ jobs: # iOS build and e2e tests e2e-ios: runs-on: ["self-hosted"] + timeout-minutes: 60 needs: install-deps steps: - name: Download Yarn cache @@ -105,10 +106,11 @@ jobs: key: xcode-cache-deriveddata-${{ github.workflow }}-${{ github.sha }} restore-keys: xcode-cache-deriveddata-${{ github.workflow }}- - # Detox iOS e2e tests - - name: Run iOS e2e tests with retry + - name: Modify env and build app in release mode run: | sed -i'' -e "s/IS_TESTING=false/IS_TESTING=true/" .env && rm -f .env-e yarn detox build --configuration ios.sim.release - ./scripts/run-retry-tests.sh 3 - \ No newline at end of file + + - name: Detox iOS e2e tests + run: | + ./scripts/run-retry-tests.sh 3 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 45ffe98480f..071c82e9d60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,48 @@ and this project adheres to [Semantic Versioning](http://semver.org/) ### Fixed +## [1.9.37] (https://github.com/rainbow-me/rainbow/releases/tag/v1.9.37) + +### Added + +- Implement NFTs v2 Arc endpoint (#5973) +- Added mutation and queries needed for spindl integration (#6031) +- Added translations for degen mode and popular in rainbow (#6020) +- Added featured results to the dapp browser trending dapps section on the discover screen (#6046, #6049) +- Implemented perceived finality where a pending transaction is detected and confirmed, we are flagging the affected assets’ addresses and refetching updated user assets balances from BE (#6037) +- Implemented Mobile Wallet Protocol (#6061) +- Added ability to hide collectibles section (#6073) + +### Changed + +- Use chainId instead of network parts 1 and 2 (#5981, #5997) +- Shortened popular tokens list from 6 to 3 (#6028) +- Removed old logger and cleaned up logging (#6021) +- Added support for navigating to swap settings as a route (#6036) +- Upgraded some packages to the latest version (#6040) +- Cleaned up e2e on Android (#5970) +- Bumped WC and did some refactoring (#6047, #6064) +- Bumped webpack from 5.90.3 to 5.94.0 (#6048) +- Bumped fastlane (#6062) + +### Fixed + +- Fixed an issue on android where a user couldn’t access dexscreener in the dapp browser (#6003) +- Fixed Dapp Browser webview height on Android devices (#6004) +- Fixed Android button navigation colors (#6005) +- Fixed TokenToBuyList line break in search results when favoriting a token (#6002) +- Fixed a bug where the terminal UI for ETH rewards was missing (#6007) +- Fixed a bug where a user’s favorites wouldn’t migrate after updating app (#6029) +- Fixed an issue where we were not able to build android locally (#6027) +- Fixed a sentry error boundary crash where users would see the oops something went wrong message (#6044) +- Fixed an issue when certain sites won’t load if using the http prefix (#6054) +- Fixed an issue on ERC20 sends that would show contract address instead of recipient address (#6052) +- Fixed some crashes on PFP button, and send flow (#6063) +- Fixed a bug where WC was not confirming transactions (#6074) +- Fixed a bug where attempting to send an ENS caused a crash (#6075) +- Fixed a discrepancy where gas on l2s were showing higher in send flow than in swaps flow (#6076) +- Fixed a wrong ID being used for spindl integration (#6078) + ## [1.9.36] (https://github.com/rainbow-me/rainbow/releases/tag/v1.9.36) ### Fixed diff --git a/android/app/build.gradle b/android/app/build.gradle index 7297b466b66..3cef38731ed 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -130,8 +130,8 @@ android { applicationId "me.rainbow" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 228 - versionName "1.9.37" + versionCode 230 + versionName "1.9.38" missingDimensionStrategy 'react-native-camera', 'general' renderscriptTargetApi 23 renderscriptSupportModeEnabled true diff --git a/e2e/9_swaps.spec.ts b/e2e/9_swaps.spec.ts index 4655c019275..a9d4a5577d1 100644 --- a/e2e/9_swaps.spec.ts +++ b/e2e/9_swaps.spec.ts @@ -60,13 +60,14 @@ describe('Swap Sheet Interaction Flow', () => { it('Should open swap screen with 50% inputAmount for inputAsset', async () => { await device.disableSynchronization(); await tap('swap-button'); - await delayTime('long'); + await delayTime('very-long'); - await swipeUntilVisible('token-to-buy-dai-1', 'token-to-buy-list', 'up', 100); - await swipe('token-to-buy-list', 'up', 'slow', 0.1); + // flaky + // await swipeUntilVisible('token-to-buy-dai-1', 'token-to-buy-list', 'up', 100); + await swipe('token-to-buy-list', 'up', 'slow', 0.2); await tap('token-to-buy-dai-1'); - await delayTime('medium'); + await delayTime('very-long'); const swapInput = await fetchElementAttributes('swap-asset-input'); diff --git a/ios/Rainbow.xcodeproj/project.pbxproj b/ios/Rainbow.xcodeproj/project.pbxproj index f7203347248..d64d7724f8c 100644 --- a/ios/Rainbow.xcodeproj/project.pbxproj +++ b/ios/Rainbow.xcodeproj/project.pbxproj @@ -1836,7 +1836,7 @@ "$(PROJECT_DIR)", ); LLVM_LTO = YES; - MARKETING_VERSION = 1.9.37; + MARKETING_VERSION = 1.9.38; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = ( "$(inherited)", @@ -1900,7 +1900,7 @@ "$(PROJECT_DIR)", ); LLVM_LTO = YES; - MARKETING_VERSION = 1.9.37; + MARKETING_VERSION = 1.9.38; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = ( "$(inherited)", @@ -2017,7 +2017,7 @@ "$(PROJECT_DIR)", ); LLVM_LTO = YES; - MARKETING_VERSION = 1.9.37; + MARKETING_VERSION = 1.9.38; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = ( "$(inherited)", @@ -2133,7 +2133,7 @@ "$(PROJECT_DIR)", ); LLVM_LTO = YES; - MARKETING_VERSION = 1.9.37; + MARKETING_VERSION = 1.9.38; OTHER_CFLAGS = "$(inherited)"; OTHER_LDFLAGS = ( "$(inherited)", diff --git a/jest.config.js b/jest.config.js index eac3bf98b7a..b7972094ada 100644 --- a/jest.config.js +++ b/jest.config.js @@ -16,7 +16,7 @@ module.exports = { ], }, transformIgnorePatterns: [ - 'node_modules/(?!((jest-)?react-native|react-native-keyboard-area|imgix-core-js|react-native-payments|@react-native-firebase|@react-native(-community)?|react-native-reanimated)/)', + 'node_modules/(?!((jest-)?react-native|react-native-keyboard-area|imgix-core-js|react-native-payments|@react-native-firebase|@react-native(-community)?|react-native-reanimated|react-native-linear-gradient)/)', ], moduleNameMapper: { ...pathsToModuleNameMapper(compilerOptions.paths, { diff --git a/package.json b/package.json index 37c442c9cde..7c3768849db 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "Rainbow", - "version": "1.9.37-1", + "version": "1.9.38-1", "private": true, "scripts": { "setup": "yarn graphql-codegen:install && yarn ds:install && yarn allow-scripts && yarn postinstall && yarn graphql-codegen", @@ -21,6 +21,7 @@ "clean:packager": "watchman watch-del-all", "clean:node": "rm -rf node_modules", "nuke": "./scripts/nuke.sh", + "rc-push": "./scripts/rc-push.sh", "detox:android:release": "detox build -c android.emu.release && detox test -c android.emu.release", "detox:android:tests": "detox test -c android.emu.debug --maxWorkers 2 -- --bail 1", "detox:android": "detox build -c android.emu.debug && yarn detox:android:tests", diff --git a/scripts/rc-push.sh b/scripts/rc-push.sh new file mode 100755 index 00000000000..a6b254bbf06 --- /dev/null +++ b/scripts/rc-push.sh @@ -0,0 +1,63 @@ +#!/bin/bash +source .env + +# Get the current branch name +BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD) + +# Define the regular expression for the RC branch +BRANCH_REGEX="^rc-v([0-9]+)\.([0-9]+)\.([0-9]+)$" + +# Exit early if the branch name doesn't match the pattern +if [[ ! $BRANCH_NAME =~ $BRANCH_REGEX ]]; then + echo "Error: you are not on the RC branch" >&2 + exit 1 +fi + +# Extract version numbers from the branch name +MAJOR=${BASH_REMATCH[1]} +MINOR=${BASH_REMATCH[2]} +PATCH=${BASH_REMATCH[3]} + +# Create a tag from the version numbers (without the rc- prefix) +TAG="v$MAJOR.$MINOR.$PATCH" +echo "Creating tag $TAG" + +# Create the git tag +git tag "$TAG" + +# Push the tag to the remote repository +git push --tags + +# Force push the current branch to master +echo "Force pushing branch $BRANCH_NAME to master" +git push origin "$BRANCH_NAME:master" --force + +# Get the latest commit SHA on the master branch +COMMIT_SHA=$(git rev-parse master) + +# Get the current UTC time in ISO 8601 format +DEPLOYED_AT=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + +# App name, repository, and environment (fill in the correct values) +APP_NAME="rainbow" +REPO_NAME="rainbow-me/rainbow" +ENVIRONMENT="production" + +echo "Notifying Swarmia about the new release" + +# Make the HTTP request to Swarmia +curl -X POST \ + https://hook.swarmia.com/deployments \ + -H "Authorization: " \ + -H "Content-Type: application/json" \ + -d '{ + "version": "'"$TAG"'", + "appName": "'"$APP_NAME"'", + "environment": "'"$ENVIRONMENT"'", + "deployedAt": "'"$DEPLOYED_AT"'", + "commitSha": "'"$COMMIT_SHA"'", + "repositoryFullName": "'"$REPO_NAME"'" + }' + +echo ""; +echo "Release created succesfully" \ No newline at end of file diff --git a/src/__swaps__/screens/Swap/Swap.tsx b/src/__swaps__/screens/Swap/Swap.tsx index f04d1e23b9f..6b1b4a72e99 100644 --- a/src/__swaps__/screens/Swap/Swap.tsx +++ b/src/__swaps__/screens/Swap/Swap.tsx @@ -28,6 +28,7 @@ import { useSwapsStore } from '@/state/swaps/swapsStore'; import { SwapWarning } from './components/SwapWarning'; import { clearCustomGasSettings } from './hooks/useCustomGas'; import { SwapProvider, useSwapContext } from './providers/swap-provider'; +import { useAccountSettings } from '@/hooks'; import { NavigateToSwapSettingsTrigger } from './components/NavigateToSwapSettingsTrigger'; /** README @@ -133,7 +134,7 @@ const useCleanupOnUnmount = () => { }; const WalletAddressObserver = () => { - const currentWalletAddress = userAssetsStore(state => state.associatedWalletAddress); + const { accountAddress } = useAccountSettings(); const { setAsset } = useSwapContext(); const setNewInputAsset = useCallback(() => { @@ -157,7 +158,7 @@ const WalletAddressObserver = () => { }, [setAsset]); useAnimatedReaction( - () => currentWalletAddress, + () => accountAddress, (current, previous) => { const didWalletAddressChange = previous && current !== previous; diff --git a/src/__swaps__/screens/Swap/components/CoinRow.tsx b/src/__swaps__/screens/Swap/components/CoinRow.tsx index 37c7feb897c..27a2a892de6 100644 --- a/src/__swaps__/screens/Swap/components/CoinRow.tsx +++ b/src/__swaps__/screens/Swap/components/CoinRow.tsx @@ -11,7 +11,7 @@ import * as i18n from '@/languages'; import { RainbowNetworkObjects } from '@/networks'; import { BASE_DEGEN_ADDRESS, DEGEN_CHAIN_DEGEN_ADDRESS, ETH_ADDRESS } from '@/references'; import { toggleFavorite } from '@/resources/favorites'; -import { userAssetsStore } from '@/state/assets/userAssets'; +import { useUserAssetsStore } from '@/state/assets/userAssets'; import { ethereumUtils, haptics, showActionSheetWithOptions } from '@/utils'; import { startCase } from 'lodash'; import React, { useCallback, useMemo } from 'react'; @@ -69,7 +69,7 @@ interface OutputCoinRowProps extends PartialAsset { type CoinRowProps = InputCoinRowProps | OutputCoinRowProps; export function CoinRow({ isFavorite, onPress, output, uniqueId, testID, ...assetProps }: CoinRowProps) { - const inputAsset = userAssetsStore(state => (output ? undefined : state.getUserAsset(uniqueId))); + const inputAsset = useUserAssetsStore(state => (output ? undefined : state.getUserAsset(uniqueId))); const outputAsset = output ? (assetProps as PartialAsset) : undefined; const asset = output ? outputAsset : inputAsset; diff --git a/src/__swaps__/screens/Swap/components/SearchInput.tsx b/src/__swaps__/screens/Swap/components/SearchInput.tsx index d764efacd92..1a7badab7f1 100644 --- a/src/__swaps__/screens/Swap/components/SearchInput.tsx +++ b/src/__swaps__/screens/Swap/components/SearchInput.tsx @@ -4,7 +4,7 @@ import { opacity } from '@/__swaps__/utils/swaps'; import { Input } from '@/components/inputs'; import { Bleed, Box, Column, Columns, Text, useColorMode, useForegroundColor } from '@/design-system'; import * as i18n from '@/languages'; -import { userAssetsStore } from '@/state/assets/userAssets'; +import { userAssetsStore, useUserAssetsStore } from '@/state/assets/userAssets'; import { useSwapsStore } from '@/state/swaps/swapsStore'; import React from 'react'; import Animated, { @@ -39,7 +39,7 @@ export const SearchInput = ({ const label = useForegroundColor('label'); const labelQuaternary = useForegroundColor('labelQuaternary'); - const onInputSearchQueryChange = userAssetsStore(state => state.setSearchQuery); + const onInputSearchQueryChange = useUserAssetsStore(state => state.setSearchQuery); const onOutputSearchQueryChange = useDebouncedCallback((text: string) => useSwapsStore.setState({ outputSearchQuery: text }), 100, { leading: false, diff --git a/src/__swaps__/screens/Swap/components/TokenList/ChainSelection.tsx b/src/__swaps__/screens/Swap/components/TokenList/ChainSelection.tsx index 00c53e26585..8111f03e637 100644 --- a/src/__swaps__/screens/Swap/components/TokenList/ChainSelection.tsx +++ b/src/__swaps__/screens/Swap/components/TokenList/ChainSelection.tsx @@ -15,7 +15,7 @@ import { ContextMenuButton } from '@/components/context-menu'; import { AnimatedText, Bleed, Box, Inline, Text, TextIcon, globalColors, useColorMode } from '@/design-system'; import { useAccountAccentColor } from '@/hooks'; import { useSharedValueState } from '@/hooks/reanimated/useSharedValueState'; -import { userAssetsStore } from '@/state/assets/userAssets'; +import { userAssetsStore, useUserAssetsStore } from '@/state/assets/userAssets'; import { swapsStore } from '@/state/swaps/swapsStore'; import { showActionSheetWithOptions } from '@/utils'; import { OnPressMenuItemEventObject } from 'react-native-ios-context-menu'; @@ -31,8 +31,11 @@ export const ChainSelection = memo(function ChainSelection({ allText, output }: const { selectedOutputChainId, setSelectedOutputChainId } = useSwapContext(); // chains sorted by balance on output, chains without balance hidden on input - const balanceSortedChainList = userAssetsStore(state => (output ? state.getBalanceSortedChainList() : state.getChainsWithBalance())); - const inputListFilter = useSharedValue(userAssetsStore.getState().filter); + const { balanceSortedChainList, filter } = useUserAssetsStore(state => ({ + balanceSortedChainList: output ? state.getBalanceSortedChainList() : state.getChainsWithBalance(), + filter: state.filter, + })); + const inputListFilter = useSharedValue(filter); const accentColor = useMemo(() => { if (c.contrast(accountColor, isDarkMode ? '#191A1C' : globalColors.white100) < (isDarkMode ? 2.125 : 1.5)) { @@ -189,7 +192,7 @@ export const ChainSelection = memo(function ChainSelection({ allText, output }: const ChainButtonIcon = ({ output }: { output: boolean | undefined }) => { const { selectedOutputChainId: animatedSelectedOutputChainId } = useSwapContext(); - const userAssetsFilter = userAssetsStore(state => (output ? undefined : state.filter)); + const userAssetsFilter = useUserAssetsStore(state => (output ? undefined : state.filter)); const selectedOutputChainId = useSharedValueState(animatedSelectedOutputChainId, { pauseSync: !output }); return ( diff --git a/src/__swaps__/screens/Swap/components/TokenList/TokenToSellList.tsx b/src/__swaps__/screens/Swap/components/TokenList/TokenToSellList.tsx index 38e95e2475a..5b12988476b 100644 --- a/src/__swaps__/screens/Swap/components/TokenList/TokenToSellList.tsx +++ b/src/__swaps__/screens/Swap/components/TokenList/TokenToSellList.tsx @@ -8,7 +8,7 @@ import { getStandardizedUniqueIdWorklet } from '@/__swaps__/utils/swaps'; import { analyticsV2 } from '@/analytics'; import { useDelayedMount } from '@/hooks/useDelayedMount'; import * as i18n from '@/languages'; -import { userAssetsStore } from '@/state/assets/userAssets'; +import { userAssetsStore, useUserAssetsStore } from '@/state/assets/userAssets'; import { swapsStore } from '@/state/swaps/swapsStore'; import { DEVICE_WIDTH } from '@/utils/deviceUtils'; import React, { useCallback, useMemo } from 'react'; @@ -38,7 +38,7 @@ export const TokenToSellList = () => { const TokenToSellListComponent = () => { const { inputProgress, internalSelectedInputAsset, internalSelectedOutputAsset, isFetching, isQuoteStale, setAsset } = useSwapContext(); - const userAssetIds = userAssetsStore(state => state.getFilteredUserAssetIds()); + const userAssetIds = useUserAssetsStore(state => state.getFilteredUserAssetIds()); const handleSelectToken = useCallback( (token: ParsedSearchAsset | null) => { diff --git a/src/__swaps__/screens/Swap/hooks/useAssetsToSell.ts b/src/__swaps__/screens/Swap/hooks/useAssetsToSell.ts index 53efd2f9cc1..e78faa661c4 100644 --- a/src/__swaps__/screens/Swap/hooks/useAssetsToSell.ts +++ b/src/__swaps__/screens/Swap/hooks/useAssetsToSell.ts @@ -1,5 +1,4 @@ import { useMemo } from 'react'; -import { Address } from 'viem'; import { selectUserAssetsList, @@ -9,8 +8,7 @@ import { import { useUserAssets } from '@/__swaps__/screens/Swap/resources/assets'; import { ParsedAssetsDictByChain, ParsedSearchAsset, UserAssetFilter } from '@/__swaps__/types/assets'; import { useAccountSettings, useDebounce } from '@/hooks'; -import { userAssetsStore } from '@/state/assets/userAssets'; -import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; +import { useUserAssetsStore } from '@/state/assets/userAssets'; const sortBy = (by: UserAssetFilter) => { switch (by) { @@ -24,18 +22,17 @@ const sortBy = (by: UserAssetFilter) => { export const useAssetsToSell = () => { const { accountAddress: currentAddress, nativeCurrency: currentCurrency } = useAccountSettings(); - const filter = userAssetsStore(state => state.filter); - const searchQuery = userAssetsStore(state => state.inputSearchQuery); + const { filter, searchQuery } = useUserAssetsStore(state => ({ + filter: state.filter, + searchQuery: state.inputSearchQuery, + })); const debouncedAssetToSellFilter = useDebounce(searchQuery, 200); - const { connectedToHardhat } = useConnectedToHardhatStore(); - const { data: userAssets = [] } = useUserAssets( { - address: currentAddress as Address, + address: currentAddress, currency: currentCurrency, - testnetMode: connectedToHardhat, }, { select: data => diff --git a/src/__swaps__/screens/Swap/providers/swap-provider.tsx b/src/__swaps__/screens/Swap/providers/swap-provider.tsx index 4740f3b61c0..7f5088d402b 100644 --- a/src/__swaps__/screens/Swap/providers/swap-provider.tsx +++ b/src/__swaps__/screens/Swap/providers/swap-provider.tsx @@ -599,7 +599,10 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { if (didSelectedAssetChange) { const assetToSet = insertUserAssetBalance - ? { ...asset, balance: (asset && userAssetsStore.getState().getUserAsset(asset.uniqueId)?.balance) || asset?.balance } + ? { + ...asset, + balance: (asset && userAssetsStore.getState().getUserAsset(asset.uniqueId)?.balance) || asset?.balance, + } : asset; if (isSameAsOtherAsset) { diff --git a/src/__swaps__/screens/Swap/resources/assets/userAssets.ts b/src/__swaps__/screens/Swap/resources/assets/userAssets.ts index 37dc8c9f185..b3a5325b3a3 100644 --- a/src/__swaps__/screens/Swap/resources/assets/userAssets.ts +++ b/src/__swaps__/screens/Swap/resources/assets/userAssets.ts @@ -15,6 +15,7 @@ import { greaterThan } from '@/__swaps__/utils/numbers'; import { fetchUserAssetsByChain } from './userAssetsByChain'; import { fetchHardhatBalances, fetchHardhatBalancesByChainId } from '@/resources/assets/hardhatAssets'; +import { useConnectedToHardhatStore } from '@/state/connectedToHardhat'; const addysHttp = new RainbowFetchClient({ baseURL: 'https://addys.p.rainbow.me/v3', @@ -31,27 +32,27 @@ export const USER_ASSETS_STALE_INTERVAL = 30000; // Query Types export type UserAssetsArgs = { - address: Address; + address: Address | string; currency: SupportedCurrencyKey; testnetMode?: boolean; }; type SetUserAssetsArgs = { - address: Address; + address: Address | string; currency: SupportedCurrencyKey; userAssets?: UserAssetsResult; testnetMode?: boolean; }; type SetUserDefaultsArgs = { - address: Address; + address: Address | string; currency: SupportedCurrencyKey; staleTime: number; testnetMode?: boolean; }; type FetchUserAssetsArgs = { - address: Address; + address: Address | string; currency: SupportedCurrencyKey; testnetMode?: boolean; }; @@ -160,7 +161,7 @@ async function userAssetsQueryFunctionRetryByChain({ currency, testnetMode, }: { - address: Address; + address: Address | string; chainIds: ChainId[]; currency: SupportedCurrencyKey; testnetMode?: boolean; @@ -230,10 +231,12 @@ export async function parseUserAssets({ // Query Hook export function useUserAssets( - { address, currency, testnetMode }: UserAssetsArgs, + { address, currency }: UserAssetsArgs, config: QueryConfigWithSelect = {} ) { - return useQuery(userAssetsQueryKey({ address, currency, testnetMode }), userAssetsQueryFunction, { + const { connectedToHardhat } = useConnectedToHardhatStore(); + + return useQuery(userAssetsQueryKey({ address, currency, testnetMode: connectedToHardhat }), userAssetsQueryFunction, { ...config, refetchInterval: USER_ASSETS_REFETCH_INTERVAL, staleTime: process.env.IS_TESTING === 'true' ? 0 : 1000, diff --git a/src/__swaps__/screens/Swap/resources/assets/userAssetsByChain.ts b/src/__swaps__/screens/Swap/resources/assets/userAssetsByChain.ts index b2b130aea98..864e75a5366 100644 --- a/src/__swaps__/screens/Swap/resources/assets/userAssetsByChain.ts +++ b/src/__swaps__/screens/Swap/resources/assets/userAssetsByChain.ts @@ -25,7 +25,7 @@ const addysHttp = new RainbowFetchClient({ // Query Types export type UserAssetsByChainArgs = { - address: Address; + address: Address | string; chainId: ChainId; currency: SupportedCurrencyKey; }; diff --git a/src/components/FeaturedResult/FeaturedResultCard.tsx b/src/components/FeaturedResult/FeaturedResultCard.tsx index 594449258ea..319b89274f1 100644 --- a/src/components/FeaturedResult/FeaturedResultCard.tsx +++ b/src/components/FeaturedResult/FeaturedResultCard.tsx @@ -25,7 +25,7 @@ export const FeaturedResultCard = ({ featuredResultId, onNavigate, Card, ...prop } await trackFeaturedResult({ - featuredResultCreativeId: featuredResult.advertiserId, + featuredResultCreativeId: featuredResult.id, placementId: featuredResult.placementSlug, impressionId: featuredResult.impressionId, type: TrackFeaturedResultType.Impression, @@ -40,7 +40,7 @@ export const FeaturedResultCard = ({ featuredResultId, onNavigate, Card, ...prop const [cta] = featuredResult.ctas || []; await trackFeaturedResult({ - featuredResultCreativeId: featuredResult.advertiserId, + featuredResultCreativeId: featuredResult.id, placementId: featuredResult.placementSlug, impressionId: featuredResult.impressionId, type: TrackFeaturedResultType.Click, diff --git a/src/components/MobileWalletProtocolListener.tsx b/src/components/MobileWalletProtocolListener.tsx index 1c5dd960d4e..27a678834cb 100644 --- a/src/components/MobileWalletProtocolListener.tsx +++ b/src/components/MobileWalletProtocolListener.tsx @@ -1,25 +1,52 @@ import { useEffect, useRef } from 'react'; -import { addDiagnosticLogListener, getAndroidIntentUrl, useMobileWalletProtocolHost } from '@coinbase/mobile-wallet-protocol-host'; +import { + addDiagnosticLogListener, + getAndroidIntentUrl, + isHandshakeAction, + useMobileWalletProtocolHost, +} from '@coinbase/mobile-wallet-protocol-host'; import { handleMobileWalletProtocolRequest } from '@/utils/requestNavigationHandlers'; import { logger, RainbowError } from '@/logger'; import { IS_ANDROID, IS_DEV } from '@/env'; export const MobileWalletProtocolListener = () => { - const { message, handleRequestUrl, sendFailureToClient, ...mwpProps } = useMobileWalletProtocolHost(); + const { message, handleRequestUrl, sendFailureToClient, session, ...mwpProps } = useMobileWalletProtocolHost(); const lastMessageUuidRef = useRef(null); + const pendingMessageRef = useRef(null); useEffect(() => { - if (message && lastMessageUuidRef.current !== message.uuid) { - lastMessageUuidRef.current = message.uuid; - try { - handleMobileWalletProtocolRequest({ request: message, ...mwpProps }); - } catch (error) { - logger.error(new RainbowError('Error handling Mobile Wallet Protocol request'), { - error, - }); + const handleMessage = async () => { + if (message && lastMessageUuidRef.current !== message.uuid) { + lastMessageUuidRef.current = message.uuid; + + // Check if it's a handshake request + const isHandshake = message.actions.some(isHandshakeAction); + + if (isHandshake || session) { + try { + await handleMobileWalletProtocolRequest({ request: message, session, ...mwpProps }); + } catch (error) { + logger.error(new RainbowError('Error handling Mobile Wallet Protocol request'), { + error, + }); + } + } else { + // Store the message to process once we have a valid session + pendingMessageRef.current = message; + } } + }; + + handleMessage(); + }, [message, session, mwpProps]); + + useEffect(() => { + if (session && pendingMessageRef.current) { + const pendingMessage = pendingMessageRef.current; + pendingMessageRef.current = null; + handleMobileWalletProtocolRequest({ request: pendingMessage, session, ...mwpProps }); } - }, [message, mwpProps]); + }, [session, mwpProps]); useEffect(() => { if (IS_DEV) { diff --git a/src/components/asset-list/RecyclerAssetList2/NFTEmptyState.tsx b/src/components/asset-list/RecyclerAssetList2/NFTEmptyState.tsx index cae66bb27ae..3e04aa7039f 100644 --- a/src/components/asset-list/RecyclerAssetList2/NFTEmptyState.tsx +++ b/src/components/asset-list/RecyclerAssetList2/NFTEmptyState.tsx @@ -5,7 +5,6 @@ import * as i18n from '@/languages'; import { TokenFamilyHeaderHeight } from './NFTLoadingSkeleton'; import { MINTS, NFTS_ENABLED, useExperimentalFlag } from '@/config'; import { useRemoteConfig } from '@/model/remoteConfig'; -import { IS_TEST } from '@/env'; import { useMints } from '@/resources/mints'; import { useAccountSettings } from '@/hooks'; import { GestureHandlerButton } from '@/__swaps__/screens/Swap/components/GestureHandlerButton'; @@ -66,8 +65,8 @@ export function NFTEmptyState() { data: { featuredMint }, } = useMints({ walletAddress: accountAddress }); - const nftsEnabled = (useExperimentalFlag(NFTS_ENABLED) || nfts_enabled) && !IS_TEST; - const mintsEnabled = (useExperimentalFlag(MINTS) || mints_enabled) && !IS_TEST; + const nftsEnabled = useExperimentalFlag(NFTS_ENABLED) || nfts_enabled; + const mintsEnabled = useExperimentalFlag(MINTS) || mints_enabled; if (!nftsEnabled) return null; diff --git a/src/components/asset-list/RecyclerAssetList2/NFTLoadingSkeleton.tsx b/src/components/asset-list/RecyclerAssetList2/NFTLoadingSkeleton.tsx index 4476b9aa2a9..ed226889fb1 100644 --- a/src/components/asset-list/RecyclerAssetList2/NFTLoadingSkeleton.tsx +++ b/src/components/asset-list/RecyclerAssetList2/NFTLoadingSkeleton.tsx @@ -6,7 +6,6 @@ import { opacity } from '@/__swaps__/utils/swaps'; import { deviceUtils } from '@/utils'; import { NFTS_ENABLED, useExperimentalFlag } from '@/config'; import { useRemoteConfig } from '@/model/remoteConfig'; -import { IS_TEST } from '@/env'; export const TokenFamilyHeaderHeight = 50; @@ -49,7 +48,7 @@ const NFTItem = () => { const NFTLoadingSkeleton = ({ items = 5 }) => { const { nfts_enabled } = useRemoteConfig(); - const nftsEnabled = (useExperimentalFlag(NFTS_ENABLED) || nfts_enabled) && !IS_TEST; + const nftsEnabled = useExperimentalFlag(NFTS_ENABLED) || nfts_enabled; if (!nftsEnabled) return null; diff --git a/src/components/asset-list/RecyclerAssetList2/WrappedCollectiblesHeader.tsx b/src/components/asset-list/RecyclerAssetList2/WrappedCollectiblesHeader.tsx index de588f3e021..dd2e35bc445 100644 --- a/src/components/asset-list/RecyclerAssetList2/WrappedCollectiblesHeader.tsx +++ b/src/components/asset-list/RecyclerAssetList2/WrappedCollectiblesHeader.tsx @@ -6,7 +6,6 @@ import { NftCollectionSortCriterion } from '@/graphql/__generated__/arc'; import useNftSort from '@/hooks/useNFTsSortBy'; import { useRemoteConfig } from '@/model/remoteConfig'; import { NFTS_ENABLED, useExperimentalFlag } from '@/config'; -import { IS_TEST } from '@/env'; const TokenFamilyHeaderHeight = 48; @@ -34,7 +33,7 @@ const getMenuItemIcon = (value: NftCollectionSortCriterion) => { const CollectiblesHeader = () => { const { nfts_enabled } = useRemoteConfig(); - const nftsEnabled = (useExperimentalFlag(NFTS_ENABLED) || nfts_enabled) && !IS_TEST; + const nftsEnabled = useExperimentalFlag(NFTS_ENABLED) || nfts_enabled; const { nftSort, updateNFTSort } = useNftSort(); if (!nftsEnabled) return null; diff --git a/src/components/asset-list/RecyclerAssetList2/WrappedNFT.tsx b/src/components/asset-list/RecyclerAssetList2/WrappedNFT.tsx index e17a6e41bf8..4cdf97d21d7 100644 --- a/src/components/asset-list/RecyclerAssetList2/WrappedNFT.tsx +++ b/src/components/asset-list/RecyclerAssetList2/WrappedNFT.tsx @@ -1,8 +1,4 @@ import React, { useCallback, useMemo } from 'react'; -import { - // @ts-ignore - IS_TESTING, -} from 'react-native-dotenv'; import { UniqueTokenCard } from '../../unique-token'; import { Box, BoxProps } from '@/design-system'; import { UniqueAsset } from '@/entities'; @@ -11,7 +7,6 @@ import { useNavigation } from '@/navigation'; import Routes from '@/navigation/routesNames'; import { useRemoteConfig } from '@/model/remoteConfig'; import { NFTS_ENABLED, useExperimentalFlag } from '@/config'; -import { IS_TEST } from '@/env'; export default React.memo(function WrappedNFT({ onPress, @@ -25,7 +20,7 @@ export default React.memo(function WrappedNFT({ externalAddress?: string; }) { const { nfts_enabled } = useRemoteConfig(); - const nftsEnabled = (useExperimentalFlag(NFTS_ENABLED) || nfts_enabled) && !IS_TEST; + const nftsEnabled = useExperimentalFlag(NFTS_ENABLED) || nfts_enabled; const assetCollectible = useCollectible(uniqueId, externalAddress); diff --git a/src/components/asset-list/RecyclerAssetList2/WrappedTokenFamilyHeader.tsx b/src/components/asset-list/RecyclerAssetList2/WrappedTokenFamilyHeader.tsx index 9781c473237..d0192584da6 100644 --- a/src/components/asset-list/RecyclerAssetList2/WrappedTokenFamilyHeader.tsx +++ b/src/components/asset-list/RecyclerAssetList2/WrappedTokenFamilyHeader.tsx @@ -4,7 +4,6 @@ import { useLatestCallback, useOpenFamilies } from '@/hooks'; import { ThemeContextProps } from '@/theme'; import { useRemoteConfig } from '@/model/remoteConfig'; import { NFTS_ENABLED, useExperimentalFlag } from '@/config'; -import { IS_TEST } from '@/env'; type Props = { name: string; @@ -16,7 +15,7 @@ type Props = { export default React.memo(function WrappedTokenFamilyHeader({ name, total, image, theme, testID }: Props) { const { nfts_enabled } = useRemoteConfig(); - const nftsEnabled = (useExperimentalFlag(NFTS_ENABLED) || nfts_enabled) && !IS_TEST; + const nftsEnabled = useExperimentalFlag(NFTS_ENABLED) || nfts_enabled; const { openFamilies, updateOpenFamilies } = useOpenFamilies(); const isFamilyOpen = openFamilies[name]; diff --git a/src/components/asset-list/RecyclerAssetList2/core/getLayoutProvider.tsx b/src/components/asset-list/RecyclerAssetList2/core/getLayoutProvider.tsx index ffe67eafd07..f9c98e01050 100644 --- a/src/components/asset-list/RecyclerAssetList2/core/getLayoutProvider.tsx +++ b/src/components/asset-list/RecyclerAssetList2/core/getLayoutProvider.tsx @@ -2,8 +2,8 @@ import { Dimension, Layout, LayoutManager, LayoutProvider } from 'recyclerlistvi import ViewDimensions from './ViewDimensions'; import { BaseCellType, CellType } from './ViewTypes'; import { deviceUtils } from '@/utils'; -import { getRemoteConfig, RainbowConfig } from '@/model/remoteConfig'; -import { NFTS_ENABLED, REMOTE_CARDS, useExperimentalFlag } from '@/config'; +import { RainbowConfig } from '@/model/remoteConfig'; +import { NFTS_ENABLED, REMOTE_CARDS } from '@/config'; import { useContext } from 'react'; import { RainbowContextType } from '@/helpers/RainbowContext'; import { IS_TEST } from '@/env'; @@ -50,8 +50,8 @@ const getLayoutProvider = ({ experimentalConfig: ReturnType>['config']; remoteConfig: RainbowConfig; }) => { - const remoteCardsEnabled = (remoteConfig.remote_cards_enabled || experimentalConfig[REMOTE_CARDS]) && !IS_TEST; - const nftsEnabled = (remoteConfig.nfts_enabled || experimentalConfig[NFTS_ENABLED]) && !IS_TEST; + const remoteCardsEnabled = remoteConfig.remote_cards_enabled || experimentalConfig[REMOTE_CARDS]; + const nftsEnabled = remoteConfig.nfts_enabled || experimentalConfig[NFTS_ENABLED]; const indicesToOverride = []; for (let i = 0; i < briefSectionsData.length; i++) { diff --git a/src/components/context-menu-buttons/ChainContextMenu.tsx b/src/components/context-menu-buttons/ChainContextMenu.tsx index 5f79b99c36e..9a31a3d4911 100644 --- a/src/components/context-menu-buttons/ChainContextMenu.tsx +++ b/src/components/context-menu-buttons/ChainContextMenu.tsx @@ -4,8 +4,8 @@ import { ContextMenuButton } from '@/components/context-menu'; import { Bleed, Box, Inline, Text, TextProps } from '@/design-system'; import * as i18n from '@/languages'; import { ChainId, ChainNameDisplay } from '@/networks/types'; +import { useUserAssetsStore } from '@/state/assets/userAssets'; import { showActionSheetWithOptions } from '@/utils'; -import { userAssetsStore } from '@/state/assets/userAssets'; import { chainNameForChainIdWithMainnetSubstitution } from '@/__swaps__/utils/chains'; interface DefaultButtonOptions { @@ -49,7 +49,7 @@ export const ChainContextMenu = ({ textWeight = 'heavy', } = defaultButtonOptions; - const balanceSortedChains = userAssetsStore(state => + const balanceSortedChains = useUserAssetsStore(state => // eslint-disable-next-line no-nested-ternary chainsToDisplay ? chainsToDisplay : excludeChainsWithNoBalance ? state.getChainsWithBalance() : state.getBalanceSortedChainList() ); diff --git a/src/config/experimental.ts b/src/config/experimental.ts index 2fe04bebc3b..6aa5824dbdb 100644 --- a/src/config/experimental.ts +++ b/src/config/experimental.ts @@ -69,7 +69,7 @@ export const defaultConfig: Record = { [DEGEN_MODE]: { settings: true, value: false }, [FEATURED_RESULTS]: { settings: true, value: false }, [CLAIMABLES]: { settings: true, value: false }, - [NFTS_ENABLED]: { settings: true, value: false }, + [NFTS_ENABLED]: { settings: true, value: !!IS_TEST }, }; const storageKey = 'config'; diff --git a/src/env.ts b/src/env.ts index bc5191f1d44..759160212d2 100644 --- a/src/env.ts +++ b/src/env.ts @@ -28,5 +28,5 @@ export const IS_DEV = (typeof __DEV__ === 'boolean' && __DEV__) || !!Number(ENAB export const IS_TEST = IS_TESTING === 'true'; export const IS_PROD = !IS_DEV && !IS_TEST; -export const RPC_PROXY_BASE_URL = IS_PROD ? RPC_PROXY_BASE_URL_PROD : RPC_PROXY_BASE_URL_DEV; -export const RPC_PROXY_API_KEY = IS_PROD ? RPC_PROXY_API_KEY_PROD : RPC_PROXY_API_KEY_DEV; +export const RPC_PROXY_BASE_URL = RPC_PROXY_BASE_URL_PROD; +export const RPC_PROXY_API_KEY = RPC_PROXY_API_KEY_PROD; diff --git a/src/keychain/index.ts b/src/keychain/index.ts index 0ef3d78771a..782d3f06a8d 100644 --- a/src/keychain/index.ts +++ b/src/keychain/index.ts @@ -115,6 +115,7 @@ export async function get(key: string, options: KeychainOptions = {}): Promise({ privateKey = await loadPrivateKey(addressToUse, isHardwareWallet); } - if (privateKey === -1 || privateKey === -2) { + // kc.ErrorType.UserCanceled means the user cancelled, so we don't wanna do anything + // kc.ErrorType.NotAuthenticated means the user is not authenticated (maybe removed biometrics). + // In this case we show an alert inside loadPrivateKey + if (privateKey === kc.ErrorType.UserCanceled || privateKey === kc.ErrorType.NotAuthenticated) { return null; } if (isHardwareWalletKey(privateKey)) { @@ -536,7 +539,10 @@ export const oldLoadSeedPhrase = async (): Promise => export const loadAddress = (): Promise => keychain.loadString(addressKey) as Promise; -export const loadPrivateKey = async (address: EthereumAddress, hardware: boolean): Promise => { +export const loadPrivateKey = async ( + address: EthereumAddress, + hardware: boolean +): Promise => { try { const isSeedPhraseMigrated = await keychain.loadString(oldSeedPhraseMigratedKey); @@ -550,8 +556,8 @@ export const loadPrivateKey = async (address: EthereumAddress, hardware: boolean if (!privateKey) { const privateKeyData = await getKeyForWallet(address, hardware); - if (privateKeyData === -1) { - return -1; + if (privateKeyData === kc.ErrorType.UserCanceled || privateKeyData === kc.ErrorType.NotAuthenticated) { + return privateKeyData; } privateKey = privateKeyData?.privateKey ?? null; } @@ -911,9 +917,12 @@ export const saveKeyForWallet = async ( * @desc Gets wallet keys for the given address depending wallet type * @param address The wallet address. * @param hardware If the wallet is a hardware wallet. - * @return null | PrivateKeyData | -1 + * @return null | PrivateKeyData | kc.ErrorType.UserCanceled | kc.ErrorType.NotAuthenticated */ -export const getKeyForWallet = async (address: EthereumAddress, hardware: boolean): Promise => { +export const getKeyForWallet = async ( + address: EthereumAddress, + hardware: boolean +): Promise => { if (hardware) { return await getHardwareKey(address); } else { @@ -971,9 +980,11 @@ export const saveHardwareKey = async ( /** * @desc Gets wallet private key for a given address. * @param address The wallet address. - * @return null | PrivateKeyData | -1 + * @return null | PrivateKeyData | kc.ErrorType.UserCanceled | kc.ErrorType.NotAuthenticated */ -export const getPrivateKey = async (address: EthereumAddress): Promise => { +export const getPrivateKey = async ( + address: EthereumAddress +): Promise => { try { const key = `${address}_${privateKeyKey}`; const options = { authenticationPrompt }; @@ -984,11 +995,26 @@ export const getPrivateKey = async (address: EthereumAddress): Promise ({ ...buildCoolModalConfig({ ...route.params, - backgroundOpacity: route?.params?.source === RequestSource.WALLETCONNECT ? 1 : 0.7, + backgroundOpacity: [RequestSource.WALLETCONNECT, RequestSource.MOBILE_WALLET_PROTOCOL].includes(route?.params?.source) ? 1 : 0.7, cornerRadius: 0, springDamping: 1, topOffset: 0, diff --git a/src/resources/assets/UserAssetsQuery.ts b/src/resources/assets/UserAssetsQuery.ts index 47db136d5e2..62865333662 100644 --- a/src/resources/assets/UserAssetsQuery.ts +++ b/src/resources/assets/UserAssetsQuery.ts @@ -48,7 +48,7 @@ const fetchUserAssetsForChainIds = async ({ let url = `https://addys.p.rainbow.me/v3/${chainIdsString}/${address}/assets?currency=${currency.toLowerCase()}`; if (staleBalanceParam) { - url += url + staleBalanceParam; + url = url + staleBalanceParam; } const response = await rainbowFetch(url, { diff --git a/src/screens/SendSheet.js b/src/screens/SendSheet.js index a2c9e95f960..c3ac286b3ea 100644 --- a/src/screens/SendSheet.js +++ b/src/screens/SendSheet.js @@ -461,6 +461,7 @@ export default function SendSheet(props) { from: accountAddress, gasLimit: gasLimitToUse, network: currentChainIdNetwork, + chainId: currentChainId, nonce: nextNonce ?? (await getNextNonce({ address: accountAddress, chainId: currentChainId })), to: toAddress, ...gasParams, diff --git a/src/screens/SignTransactionSheet.tsx b/src/screens/SignTransactionSheet.tsx index 11be9876981..a558fc97067 100644 --- a/src/screens/SignTransactionSheet.tsx +++ b/src/screens/SignTransactionSheet.tsx @@ -72,6 +72,7 @@ import { useNonceForDisplay } from '@/hooks/useNonceForDisplay'; import { useProviderSetup } from '@/hooks/useProviderSetup'; import { useTransactionSubmission } from '@/hooks/useSubmitTransaction'; import { useConfirmTransaction } from '@/hooks/useConfirmTransaction'; +import { toChecksumAddress } from 'ethereumjs-util'; type SignTransactionSheetParams = { transactionDetails: RequestData; @@ -104,8 +105,6 @@ export const SignTransactionSheet = () => { source, } = routeParams; - console.log({ specifiedAddress }); - const addressToUse = specifiedAddress ?? accountAddress; const { provider, nativeAsset } = useProviderSetup(chainId, addressToUse); @@ -334,7 +333,7 @@ export const SignTransactionSheet = () => { screen: SCREEN_FOR_REQUEST_SOURCE[source], operation: TimeToSignOperation.KeychainRead, })({ - address: accountInfo.address, + address: toChecksumAddress(accountInfo.address), provider: providerToUse, timeTracking: { screen: SCREEN_FOR_REQUEST_SOURCE[source], diff --git a/src/state/assets/userAssets.ts b/src/state/assets/userAssets.ts index 0dda7203474..7fb04b01e4f 100644 --- a/src/state/assets/userAssets.ts +++ b/src/state/assets/userAssets.ts @@ -1,10 +1,13 @@ import { ParsedSearchAsset, UniqueId, UserAssetFilter } from '@/__swaps__/types/assets'; import { Address } from 'viem'; import { RainbowError, logger } from '@/logger'; -import store from '@/redux/store'; +import reduxStore, { AppState } from '@/redux/store'; import { ETH_ADDRESS, SUPPORTED_CHAIN_IDS, supportedNativeCurrencies } from '@/references'; import { createRainbowStore } from '@/state/internal/createRainbowStore'; +import { useStore } from 'zustand'; +import { useCallback } from 'react'; import { swapsStore } from '@/state/swaps/swapsStore'; +import { useSelector } from 'react-redux'; import { ChainId } from '@/networks/types'; import { useConnectedToHardhatStore } from '../connectedToHardhat'; @@ -26,7 +29,6 @@ const getDefaultCacheKeys = (): Set => { const CACHE_ITEMS_TO_PRESERVE = getDefaultCacheKeys(); export interface UserAssetsState { - associatedWalletAddress: Address | undefined; chainBalances: Map; currentAbortController: AbortController; filter: UserAssetFilter; @@ -44,7 +46,7 @@ export interface UserAssetsState { selectUserAssets: (selector: (asset: ParsedSearchAsset) => boolean) => Generator<[UniqueId, ParsedSearchAsset], void, unknown>; setSearchCache: (queryKey: string, filteredIds: UniqueId[]) => void; setSearchQuery: (query: string) => void; - setUserAssets: (associatedWalletAddress: Address, userAssets: Map | ParsedSearchAsset[]) => void; + setUserAssets: (userAssets: Map | ParsedSearchAsset[]) => void; } // NOTE: We are serializing Map as an Array<[UniqueId, ParsedSearchAsset]> @@ -122,227 +124,257 @@ function deserializeUserAssetsState(serializedState: string) { }; } -export const userAssetsStore = createRainbowStore( - (set, get) => ({ - associatedWalletAddress: undefined, - chainBalances: new Map(), - currentAbortController: new AbortController(), - filter: 'all', - idsByChain: new Map(), - inputSearchQuery: '', - searchCache: new Map(), - userAssets: new Map(), - - getBalanceSortedChainList: () => { - const chainBalances = [...get().chainBalances.entries()]; - chainBalances.sort(([, balanceA], [, balanceB]) => balanceB - balanceA); - return chainBalances.map(([chainId]) => chainId); - }, - - getChainsWithBalance: () => { - const chainBalances = [...get().chainBalances.entries()]; - const chainsWithBalances = chainBalances.filter(([, balance]) => !!balance); - return chainsWithBalances.map(([chainId]) => chainId); - }, - - getFilteredUserAssetIds: () => { - const { filter, inputSearchQuery: rawSearchQuery, selectUserAssetIds, setSearchCache } = get(); - - const smallBalanceThreshold = supportedNativeCurrencies[store.getState().settings.nativeCurrency].userAssetsSmallThreshold; - - const inputSearchQuery = rawSearchQuery.trim().toLowerCase(); - const queryKey = getSearchQueryKey({ filter, searchQuery: inputSearchQuery }); - - // Use an external function to get the cache to prevent updates in response to changes in the cache - const cachedData = getCurrentCache().get(queryKey); - - // Check if the search results are already cached - if (cachedData) { - return cachedData; - } else { - const chainIdFilter = filter === 'all' ? null : filter; - const searchRegex = inputSearchQuery.length > 0 ? new RegExp(escapeRegExp(inputSearchQuery), 'i') : null; - - const filteredIds = Array.from( - selectUserAssetIds( - asset => - (+asset.native?.balance?.amount ?? 0) > smallBalanceThreshold && - (!chainIdFilter || asset.chainId === chainIdFilter) && - (!searchRegex || - searchRegex.test(asset.name) || - searchRegex.test(asset.symbol) || - asset.address.toLowerCase() === inputSearchQuery), - filter - ) - ); - - setSearchCache(queryKey, filteredIds); - - return filteredIds; - } - }, - getHighestValueEth: () => { - const preferredNetwork = swapsStore.getState().preferredNetwork; - const assets = get().userAssets; +export const createUserAssetsStore = (address: Address | string) => + createRainbowStore( + (set, get) => ({ + chainBalances: new Map(), + currentAbortController: new AbortController(), + filter: 'all', + idsByChain: new Map(), + inputSearchQuery: '', + searchCache: new Map(), + userAssets: new Map(), + + getBalanceSortedChainList: () => { + const chainBalances = [...get().chainBalances.entries()]; + chainBalances.sort(([, balanceA], [, balanceB]) => balanceB - balanceA); + return chainBalances.map(([chainId]) => chainId); + }, + + getChainsWithBalance: () => { + const chainBalances = [...get().chainBalances.entries()]; + const chainsWithBalances = chainBalances.filter(([, balance]) => !!balance); + return chainsWithBalances.map(([chainId]) => chainId); + }, + + getFilteredUserAssetIds: () => { + const { filter, inputSearchQuery: rawSearchQuery, selectUserAssetIds, setSearchCache } = get(); + + const smallBalanceThreshold = supportedNativeCurrencies[reduxStore.getState().settings.nativeCurrency].userAssetsSmallThreshold; + + const inputSearchQuery = rawSearchQuery.trim().toLowerCase(); + const queryKey = getSearchQueryKey({ filter, searchQuery: inputSearchQuery }); + + // Use an external function to get the cache to prevent updates in response to changes in the cache + const cachedData = getCurrentSearchCache()?.get(queryKey); + + // Check if the search results are already cached + if (cachedData) { + return cachedData; + } else { + const chainIdFilter = filter === 'all' ? null : filter; + const searchRegex = inputSearchQuery.length > 0 ? new RegExp(escapeRegExp(inputSearchQuery), 'i') : null; + + const filteredIds = Array.from( + selectUserAssetIds( + asset => + (+asset.native?.balance?.amount ?? 0) > smallBalanceThreshold && + (!chainIdFilter || asset.chainId === chainIdFilter) && + (!searchRegex || + searchRegex.test(asset.name) || + searchRegex.test(asset.symbol) || + asset.address.toLowerCase() === inputSearchQuery), + filter + ) + ); + + setSearchCache(queryKey, filteredIds); + + return filteredIds; + } + }, + getHighestValueEth: () => { + const preferredNetwork = swapsStore.getState().preferredNetwork; + const assets = get().userAssets; - let highestValueEth = null; + let highestValueEth = null; - for (const [, asset] of assets) { - if (asset.mainnetAddress !== ETH_ADDRESS) continue; + for (const [, asset] of assets) { + if (asset.mainnetAddress !== ETH_ADDRESS) continue; - if (preferredNetwork && asset.chainId === preferredNetwork) { - return asset; - } + if (preferredNetwork && asset.chainId === preferredNetwork) { + return asset; + } - if (!highestValueEth || asset.balance > highestValueEth.balance) { - highestValueEth = asset; + if (!highestValueEth || asset.balance > highestValueEth.balance) { + highestValueEth = asset; + } } - } - return highestValueEth; - }, + return highestValueEth; + }, - getUserAsset: (uniqueId: UniqueId) => get().userAssets.get(uniqueId) || null, + getUserAsset: (uniqueId: UniqueId) => get().userAssets.get(uniqueId) || null, - getUserAssets: () => Array.from(get().userAssets.values()) || [], + getUserAssets: () => Array.from(get().userAssets.values()) || [], - selectUserAssetIds: function* (selector: (asset: ParsedSearchAsset) => boolean, filter?: UserAssetFilter) { - const { currentAbortController, idsByChain, userAssets } = get(); + selectUserAssetIds: function* (selector: (asset: ParsedSearchAsset) => boolean, filter?: UserAssetFilter) { + const { currentAbortController, idsByChain, userAssets } = get(); - const assetIds = filter ? idsByChain.get(filter) || [] : idsByChain.get('all') || []; + const assetIds = filter ? idsByChain.get(filter) || [] : idsByChain.get('all') || []; - for (const id of assetIds) { - if (currentAbortController?.signal.aborted) { - return; - } - const asset = userAssets.get(id); - if (asset && selector(asset)) { - yield id; + for (const id of assetIds) { + if (currentAbortController?.signal.aborted) { + return; + } + const asset = userAssets.get(id); + if (asset && selector(asset)) { + yield id; + } } - } - }, + }, - selectUserAssets: function* (selector: (asset: ParsedSearchAsset) => boolean) { - const { currentAbortController, userAssets } = get(); + selectUserAssets: function* (selector: (asset: ParsedSearchAsset) => boolean) { + const { currentAbortController, userAssets } = get(); - for (const [id, asset] of userAssets) { - if (currentAbortController?.signal.aborted) { - return; - } - if (selector(asset)) { - yield [id, asset]; + for (const [id, asset] of userAssets) { + if (currentAbortController?.signal.aborted) { + return; + } + if (selector(asset)) { + yield [id, asset]; + } } - } - }, + }, - setSearchQuery: query => - set(state => { - const { currentAbortController } = state; + setSearchQuery: query => + set(state => { + const { currentAbortController } = state; - // Abort any ongoing search work - currentAbortController.abort(); + // Abort any ongoing search work + currentAbortController.abort(); - // Create a new AbortController for the new query - const abortController = new AbortController(); + // Create a new AbortController for the new query + const abortController = new AbortController(); - return { inputSearchQuery: query.trim().toLowerCase(), currentAbortController: abortController }; - }), + return { inputSearchQuery: query.trim().toLowerCase(), currentAbortController: abortController }; + }), - setSearchCache: (queryKey: string, filteredIds: UniqueId[]) => { - set(state => { - const newCache = new Map(state.searchCache).set(queryKey, filteredIds); + setSearchCache: (queryKey: string, filteredIds: UniqueId[]) => { + set(state => { + const newCache = new Map(state.searchCache).set(queryKey, filteredIds); - // Prune the cache if it exceeds the maximum size - if (newCache.size > SEARCH_CACHE_MAX_ENTRIES) { - // Get the oldest key that isn't a key to preserve - for (const key of newCache.keys()) { - if (!CACHE_ITEMS_TO_PRESERVE.has(key)) { - newCache.delete(key); - break; + // Prune the cache if it exceeds the maximum size + if (newCache.size > SEARCH_CACHE_MAX_ENTRIES) { + // Get the oldest key that isn't a key to preserve + for (const key of newCache.keys()) { + if (!CACHE_ITEMS_TO_PRESERVE.has(key)) { + newCache.delete(key); + break; + } } } - } - - return { searchCache: newCache }; - }); - }, - setUserAssets: (associatedWalletAddress: Address, userAssets: Map | ParsedSearchAsset[]) => - set(() => { - const idsByChain = new Map(); - const unsortedChainBalances = new Map(); - - userAssets.forEach(asset => { - const balance = Number(asset.native.balance.amount) ?? 0; - unsortedChainBalances.set(asset.chainId, (unsortedChainBalances.get(asset.chainId) || 0) + balance); - idsByChain.set(asset.chainId, (idsByChain.get(asset.chainId) || []).concat(asset.uniqueId)); + return { searchCache: newCache }; }); + }, + + setUserAssets: (userAssets: Map | ParsedSearchAsset[]) => + set(() => { + const idsByChain = new Map(); + const unsortedChainBalances = new Map(); + + userAssets.forEach(asset => { + const balance = Number(asset.native.balance.amount) ?? 0; + unsortedChainBalances.set(asset.chainId, (unsortedChainBalances.get(asset.chainId) || 0) + balance); + idsByChain.set(asset.chainId, (idsByChain.get(asset.chainId) || []).concat(asset.uniqueId)); + }); + + // Ensure all supported chains are in the map with a fallback value of 0 + SUPPORTED_CHAIN_IDS({ testnetMode: useConnectedToHardhatStore.getState().connectedToHardhat }).forEach(chainId => { + if (!unsortedChainBalances.has(chainId)) { + unsortedChainBalances.set(chainId, 0); + idsByChain.set(chainId, []); + } + }); - // Ensure all supported chains are in the map with a fallback value of 0 - SUPPORTED_CHAIN_IDS({ testnetMode: useConnectedToHardhatStore.getState().connectedToHardhat }).forEach(chainId => { - if (!unsortedChainBalances.has(chainId)) { - unsortedChainBalances.set(chainId, 0); - idsByChain.set(chainId, []); - } - }); + // Sort the existing map by balance in descending order + const sortedEntries = Array.from(unsortedChainBalances.entries()).sort(([, balanceA], [, balanceB]) => balanceB - balanceA); + const chainBalances = new Map(); - // Sort the existing map by balance in descending order - const sortedEntries = Array.from(unsortedChainBalances.entries()).sort(([, balanceA], [, balanceB]) => balanceB - balanceA); - const chainBalances = new Map(); + sortedEntries.forEach(([chainId, balance]) => { + chainBalances.set(chainId, balance); + idsByChain.set(chainId, idsByChain.get(chainId) || []); + }); - sortedEntries.forEach(([chainId, balance]) => { - chainBalances.set(chainId, balance); - idsByChain.set(chainId, idsByChain.get(chainId) || []); - }); + const isMap = userAssets instanceof Map; + const allIdsArray = isMap ? Array.from(userAssets.keys()) : userAssets.map(asset => asset.uniqueId); + const userAssetsMap = isMap ? userAssets : new Map(userAssets.map(asset => [asset.uniqueId, asset])); - const isMap = userAssets instanceof Map; - const allIdsArray = isMap ? Array.from(userAssets.keys()) : userAssets.map(asset => asset.uniqueId); - const userAssetsMap = isMap ? userAssets : new Map(userAssets.map(asset => [asset.uniqueId, asset])); + idsByChain.set('all', allIdsArray); - idsByChain.set('all', allIdsArray); + const smallBalanceThreshold = supportedNativeCurrencies[reduxStore.getState().settings.nativeCurrency].userAssetsSmallThreshold; - const smallBalanceThreshold = supportedNativeCurrencies[store.getState().settings.nativeCurrency].userAssetsSmallThreshold; + const filteredAllIdsArray = allIdsArray.filter(id => { + const asset = userAssetsMap.get(id); + return asset && (+asset.native?.balance?.amount ?? 0) > smallBalanceThreshold; + }); - const filteredAllIdsArray = allIdsArray.filter(id => { - const asset = userAssetsMap.get(id); - return asset && (+asset.native?.balance?.amount ?? 0) > smallBalanceThreshold; - }); + const searchCache = new Map(); - const searchCache = new Map(); + Array.from(chainBalances.keys()).forEach(userAssetFilter => { + const filteredIds = (idsByChain.get(userAssetFilter) || []).filter(id => filteredAllIdsArray.includes(id)); + searchCache.set(`${userAssetFilter}`, filteredIds); + }); - Array.from(chainBalances.keys()).forEach(userAssetFilter => { - const filteredIds = (idsByChain.get(userAssetFilter) || []).filter(id => filteredAllIdsArray.includes(id)); - searchCache.set(`${userAssetFilter}`, filteredIds); - }); + searchCache.set('all', filteredAllIdsArray); - searchCache.set('all', filteredAllIdsArray); - - if (isMap) { - return { associatedWalletAddress, chainBalances, idsByChain, searchCache, userAssets }; - } else - return { - associatedWalletAddress, - chainBalances, - idsByChain, - searchCache, - userAssets: userAssetsMap, - }; - }), - }), - { - deserializer: deserializeUserAssetsState, - partialize: state => ({ - associatedWalletAddress: state.associatedWalletAddress, - chainBalances: state.chainBalances, - idsByChain: state.idsByChain, - userAssets: state.userAssets, + if (isMap) { + return { chainBalances, idsByChain, searchCache, userAssets }; + } else + return { + chainBalances, + idsByChain, + searchCache, + userAssets: userAssetsMap, + }; + }), }), - serializer: serializeUserAssetsState, - storageKey: 'userAssets', - version: 3, + { + storageKey: `userAssets_${address}`, + version: 0, + serializer: serializeUserAssetsState, + deserializer: deserializeUserAssetsState, + } + ); + +type UserAssetsStoreType = ReturnType; + +interface StoreManagerState { + stores: Map
; +} + +const storeManager = createRainbowStore(() => ({ + stores: new Map(), +})); + +function getOrCreateStore(address?: Address | string): UserAssetsStoreType { + const accountAddress = address ?? reduxStore.getState().settings.accountAddress; + const { stores } = storeManager.getState(); + let store = stores.get(accountAddress); + + if (!store) { + store = createUserAssetsStore(accountAddress); + storeManager.setState(state => ({ + stores: new Map(state.stores).set(accountAddress, store as UserAssetsStoreType), + })); } -); -function getCurrentCache(): Map { - return userAssetsStore.getState().searchCache; + return store; +} + +export const userAssetsStore = { + getState: () => getOrCreateStore().getState(), + setState: (partial: Partial | ((state: UserAssetsState) => Partial)) => + getOrCreateStore().setState(partial), +}; + +export function useUserAssetsStore(selector: (state: UserAssetsState) => T) { + const address = useSelector((state: AppState) => state.settings.accountAddress); + const store = getOrCreateStore(address); + return useStore(store, useCallback(selector, [])); +} + +function getCurrentSearchCache(): Map | undefined { + return getOrCreateStore().getState().searchCache; } diff --git a/src/state/sync/UserAssetsSync.tsx b/src/state/sync/UserAssetsSync.tsx index 4c8c379c775..139d4ddb547 100644 --- a/src/state/sync/UserAssetsSync.tsx +++ b/src/state/sync/UserAssetsSync.tsx @@ -1,4 +1,3 @@ -import { Address } from 'viem'; import { useAccountSettings } from '@/hooks'; import { userAssetsStore } from '@/state/assets/userAssets'; import { useSwapsStore } from '@/state/swaps/swapsStore'; @@ -6,31 +5,27 @@ import { selectUserAssetsList, selectorFilterByUserChains } from '@/__swaps__/sc import { ParsedSearchAsset } from '@/__swaps__/types/assets'; import { useUserAssets } from '@/__swaps__/screens/Swap/resources/assets'; import { ChainId } from '@/networks/types'; -import { useConnectedToHardhatStore } from '../connectedToHardhat'; export const UserAssetsSync = function UserAssetsSync() { - const { accountAddress: currentAddress, nativeCurrency: currentCurrency } = useAccountSettings(); + const { accountAddress, nativeCurrency: currentCurrency } = useAccountSettings(); - const userAssetsWalletAddress = userAssetsStore(state => state.associatedWalletAddress); const isSwapsOpen = useSwapsStore(state => state.isSwapsOpen); - const { connectedToHardhat } = useConnectedToHardhatStore(); useUserAssets( { - address: currentAddress as Address, + address: accountAddress, currency: currentCurrency, - testnetMode: connectedToHardhat, }, { - enabled: !isSwapsOpen || userAssetsWalletAddress !== currentAddress, + enabled: !isSwapsOpen, select: data => selectorFilterByUserChains({ data, selector: selectUserAssetsList, }), onSuccess: data => { - if (!isSwapsOpen || userAssetsWalletAddress !== currentAddress) { - userAssetsStore.getState().setUserAssets(currentAddress as Address, data as ParsedSearchAsset[]); + if (!isSwapsOpen) { + userAssetsStore.getState().setUserAssets(data as ParsedSearchAsset[]); const inputAsset = userAssetsStore.getState().getHighestValueEth(); useSwapsStore.setState({ diff --git a/src/utils/ethereumUtils.ts b/src/utils/ethereumUtils.ts index 00ac9f63577..4427f48be09 100644 --- a/src/utils/ethereumUtils.ts +++ b/src/utils/ethereumUtils.ts @@ -502,6 +502,8 @@ const calculateL1FeeOptimism = async (tx: RainbowTransaction, provider: Provider newTx.nonce = Number(await provider.getTransactionCount(newTx.from)); } + // @ts-expect-error operand should be optional + delete newTx?.chainId; // @ts-expect-error operand should be optional delete newTx?.from; // @ts-expect-error gas is not in type RainbowTransaction