From 2622bbc2b207b7ad4fd1365fc10557bc0115519b Mon Sep 17 00:00:00 2001 From: Skylar Barrera Date: Fri, 9 Feb 2024 15:49:53 -0500 Subject: [PATCH] chore: migrate nonce state (#5395) * chore: migrate nonce state * try * audit: ip * clean up * rm redux dep * disable tests --- CODEOWNERS | 1 - src/entities/index.ts | 1 - src/entities/nonce.ts | 12 -- src/handlers/localstorage/nonceManager.ts | 13 -- src/hooks/index.ts | 1 - src/hooks/useCurrentNonce.ts | 39 ---- src/hooks/useENSRegistrationActionHandler.ts | 31 +-- src/hooks/useLoadGlobalLateData.ts | 6 +- src/redux/data.ts | 26 +-- src/redux/nonceManager.ts | 110 ---------- src/redux/reducers.ts | 2 - src/screens/ExchangeModal.tsx | 7 +- src/screens/SendSheet.js | 6 +- .../SettingsSheet/components/DevSection.tsx | 6 +- src/screens/SignTransactionSheet.tsx | 14 +- src/screens/TransactionConfirmationScreen.js | 9 +- src/state/nonces/index.test.skip | 196 ++++++++++++++++++ src/state/nonces/index.ts | 71 +++++++ 18 files changed, 315 insertions(+), 236 deletions(-) delete mode 100644 src/entities/nonce.ts delete mode 100644 src/handlers/localstorage/nonceManager.ts delete mode 100644 src/redux/nonceManager.ts create mode 100644 src/state/nonces/index.test.skip create mode 100644 src/state/nonces/index.ts diff --git a/CODEOWNERS b/CODEOWNERS index 459b7493e32..a62ae9ff3b8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -30,7 +30,6 @@ src/model/wallet.ts @skylarbarrera @jinchung # redux src/redux/gas.ts @skylarbarrera @benisgold @jinchung -src/redux/nonceManager.ts @derHowie @skylarbarrera @benisgold @jinchung # navigation src/navigation @skylarbarrera @benisgold diff --git a/src/entities/index.ts b/src/entities/index.ts index b2d4526a954..98229e3f3a4 100644 --- a/src/entities/index.ts +++ b/src/entities/index.ts @@ -28,7 +28,6 @@ export type { export { NativeCurrencyKeys } from './nativeCurrencyTypes'; export type { NativeCurrencyKey } from './nativeCurrencyTypes'; export type Numberish = string | number; -export type { NonceManager } from './nonce'; export { default as ProtocolTypeNames, ProtocolType } from './protocolTypes'; export type { UniqueAsset } from './uniqueAssets'; export type { diff --git a/src/entities/nonce.ts b/src/entities/nonce.ts deleted file mode 100644 index 8e48c0c5c03..00000000000 --- a/src/entities/nonce.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { EthereumAddress } from '.'; -import { Network } from '@/helpers/networkTypes'; - -interface NetworkNonceInfo { - nonce: number; -} - -type AccountNonceInfo = Partial>; - -export interface NonceManager { - [key: EthereumAddress]: AccountNonceInfo; -} diff --git a/src/handlers/localstorage/nonceManager.ts b/src/handlers/localstorage/nonceManager.ts deleted file mode 100644 index 9731dbb3cc5..00000000000 --- a/src/handlers/localstorage/nonceManager.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { getGlobal, saveGlobal } from './common'; -import { NonceManager } from '@/entities'; - -export const NONCE_MANAGER = 'nonceManager'; -const noncesVersion = '0.0.2'; - -export const getNonceManager = async (): Promise => { - const nonceManager = await getGlobal(NONCE_MANAGER, {}, noncesVersion); - - return nonceManager; -}; - -export const saveNonceManager = (nonceManager: NonceManager) => saveGlobal(NONCE_MANAGER, nonceManager, noncesVersion); diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 30521cd03f6..da08ce55e32 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -114,7 +114,6 @@ export { default as useForceUpdate } from './useForceUpdate'; export { default as useOnAvatarPress } from './useOnAvatarPress'; export { default as useAdditionalAssetData } from './useAdditionalAssetData'; export { default as useImportingWallet } from './useImportingWallet'; -export { default as useCurrentNonce } from './useCurrentNonce'; export { default as usePersistentAspectRatio } from './usePersistentAspectRatio'; export { default as useFeesPanelInputRefs } from './useFeesPanelInputRefs'; export { default as useHardwareBack, useHardwareBackOnFocus } from './useHardwareBack'; diff --git a/src/hooks/useCurrentNonce.ts b/src/hooks/useCurrentNonce.ts index b9df459ba56..e69de29bb2d 100644 --- a/src/hooks/useCurrentNonce.ts +++ b/src/hooks/useCurrentNonce.ts @@ -1,39 +0,0 @@ -import { useCallback } from 'react'; -import { useSelector } from 'react-redux'; -import { EthereumAddress } from '@/entities'; -import { getProviderForNetwork } from '@/handlers/web3'; -import { Network } from '@/helpers/networkTypes'; -import { AppState } from '@/redux/store'; -import logger from '@/utils/logger'; - -export default function useCurrentNonce(accountAddress: EthereumAddress, network?: Network) { - const nonceInState = useSelector((state: AppState) => { - if (!network || !accountAddress) return undefined; - return state.nonceManager[accountAddress.toLowerCase()]?.[network]?.nonce; - }); - const getNextNonce = useCallback(async () => { - try { - if (!network || !accountAddress) return undefined; - const provider = await getProviderForNetwork(network); - const transactionCount = await provider.getTransactionCount(accountAddress, 'pending'); - const transactionIndex = transactionCount - 1; - const nextNonceBase = !nonceInState || transactionIndex > nonceInState ? transactionIndex : nonceInState; - const nextNonce = nextNonceBase + 1; - - logger.log('Use current nonce: ', { - accountAddress, - network, - nextNonce, - nonceInState, - transactionCount, - }); - - return nextNonce; - } catch (e) { - logger.log('Error determining next nonce: ', e); - return undefined; - } - }, [accountAddress, network, nonceInState]); - - return getNextNonce; -} diff --git a/src/hooks/useENSRegistrationActionHandler.ts b/src/hooks/useENSRegistrationActionHandler.ts index d8032b6439b..b63e56edfc7 100644 --- a/src/hooks/useENSRegistrationActionHandler.ts +++ b/src/hooks/useENSRegistrationActionHandler.ts @@ -7,7 +7,7 @@ import { avatarMetadataAtom } from '../components/ens-registration/RegistrationA import { coverMetadataAtom } from '../components/ens-registration/RegistrationCover/RegistrationCover'; import { ENSActionParameters, RapActionTypes } from '../raps/common'; import usePendingTransactions from './usePendingTransactions'; -import { useAccountSettings, useCurrentNonce, useENSRegistration, useWalletENSAvatar, useWallets } from '.'; +import { useAccountSettings, useENSRegistration, useWalletENSAvatar, useWallets } from '.'; import { Records, RegistrationParameters } from '@/entities'; import { fetchResolver } from '@/handlers/ens'; import { saveNameFromLabelhash } from '@/handlers/localstorage/ens'; @@ -19,6 +19,8 @@ import { executeRap } from '@/raps'; import { timeUnits } from '@/references'; import Routes from '@/navigation/routesNames'; import { labelhash, logger } from '@/utils'; +import { getNextNonce } from '@/state/nonces'; +import { Network } from '@/networks/types'; const NOOP = () => null; @@ -46,8 +48,7 @@ export default function useENSRegistrationActionHandler( step: keyof typeof REGISTRATION_STEPS; } = {} as any ) { - const { accountAddress, network } = useAccountSettings(); - const getNextNonce = useCurrentNonce(accountAddress, network); + const { accountAddress } = useAccountSettings(); const { registrationParameters } = useENSRegistration(); const { navigate, goBack } = useNavigation(); const { getPendingTransactionByHash } = usePendingTransactions(); @@ -92,7 +93,7 @@ export default function useENSRegistrationActionHandler( const salt = generateSalt(); const [nonce, rentPrice] = await Promise.all([ - getNextNonce(), + getNextNonce({ network: Network.mainnet, address: accountAddress }), getRentPrice(registrationParameters.name.replace(ENS_DOMAIN, ''), duration), ]); @@ -116,7 +117,7 @@ export default function useENSRegistrationActionHandler( callback; }); }, - [getNextNonce, registrationParameters, duration, accountAddress, isHardwareWallet, goBack] + [registrationParameters, duration, accountAddress, isHardwareWallet, goBack] ); const speedUpCommitAction = useCallback( @@ -147,7 +148,7 @@ export default function useENSRegistrationActionHandler( } const [nonce, rentPrice, changedRecords] = await Promise.all([ - getNextNonce(), + getNextNonce({ network: Network.mainnet, address: accountAddress }), getRentPrice(name.replace(ENS_DOMAIN, ''), duration), uploadRecordImages(registrationParameters.changedRecords, { avatar: avatarMetadata, @@ -169,7 +170,7 @@ export default function useENSRegistrationActionHandler( updateAvatarsOnNextBlock.current = true; }, - [accountAddress, avatarMetadata, coverMetadata, getNextNonce, registrationParameters, sendReverseRecord] + [accountAddress, avatarMetadata, coverMetadata, registrationParameters, sendReverseRecord] ); const renewAction = useCallback( @@ -182,7 +183,7 @@ export default function useENSRegistrationActionHandler( return; } - const nonce = await getNextNonce(); + const nonce = await getNextNonce({ network: Network.mainnet, address: accountAddress }); const rentPrice = await getRentPrice(name.replace(ENS_DOMAIN, ''), duration); const registerEnsRegistrationParameters: ENSActionParameters = { @@ -194,7 +195,7 @@ export default function useENSRegistrationActionHandler( await executeRap(wallet, RapActionTypes.renewENS, registerEnsRegistrationParameters, callback); }, - [duration, getNextNonce, registrationParameters] + [accountAddress, duration, registrationParameters] ); const setNameAction = useCallback( @@ -207,7 +208,7 @@ export default function useENSRegistrationActionHandler( return; } - const nonce = await getNextNonce(); + const nonce = await getNextNonce({ network: Network.mainnet, address: accountAddress }); const registerEnsRegistrationParameters: ENSActionParameters = { ...formatENSActionParams(registrationParameters), @@ -218,7 +219,7 @@ export default function useENSRegistrationActionHandler( await executeRap(wallet, RapActionTypes.setNameENS, registerEnsRegistrationParameters, callback); }, - [accountAddress, getNextNonce, registrationParameters] + [accountAddress, registrationParameters] ); const setRecordsAction = useCallback( @@ -230,7 +231,7 @@ export default function useENSRegistrationActionHandler( } const [nonce, changedRecords, resolver] = await Promise.all([ - getNextNonce(), + getNextNonce({ network: Network.mainnet, address: accountAddress }), uploadRecordImages(registrationParameters.changedRecords, { avatar: avatarMetadata, header: coverMetadata, @@ -251,7 +252,7 @@ export default function useENSRegistrationActionHandler( updateAvatarsOnNextBlock.current = true; }, - [accountAddress, avatarMetadata, coverMetadata, getNextNonce, registrationParameters, sendReverseRecord] + [accountAddress, avatarMetadata, coverMetadata, registrationParameters, sendReverseRecord] ); const transferAction = useCallback( @@ -268,7 +269,7 @@ export default function useENSRegistrationActionHandler( return; } - const nonce = await getNextNonce(); + const nonce = await getNextNonce({ network: Network.mainnet, address: accountAddress }); const transferEnsParameters: ENSActionParameters = { ...formatENSActionParams({ @@ -288,7 +289,7 @@ export default function useENSRegistrationActionHandler( return { nonce: newNonce }; }, - [accountAddress, getNextNonce, registrationParameters] + [accountAddress, registrationParameters] ); const actions = useMemo( diff --git a/src/hooks/useLoadGlobalLateData.ts b/src/hooks/useLoadGlobalLateData.ts index 53890465048..3cf2e691c1f 100644 --- a/src/hooks/useLoadGlobalLateData.ts +++ b/src/hooks/useLoadGlobalLateData.ts @@ -2,7 +2,6 @@ import { useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { getWalletBalances, WALLET_BALANCES_FROM_STORAGE } from '@/handlers/localstorage/walletBalances'; import { queryClient } from '@/react-query'; -import { nonceManagerLoadState } from '@/redux/nonceManager'; import { AppState } from '@/redux/store'; import { promiseUtils } from '@/utils'; import logger from '@/utils/logger'; @@ -26,9 +25,6 @@ export default function useLoadGlobalLateData() { logger.sentry('Load wallet global late data'); const promises = []; - // wallet nonces - const p1 = dispatch(nonceManagerLoadState()); - // mainnet eth balances for all wallets const p2 = loadWalletBalanceNamesToCache(); @@ -50,7 +46,7 @@ export default function useLoadGlobalLateData() { // tx method names const p7 = dispatch(transactionSignaturesLoadState()); - promises.push(p1, p2, p3, p4, p5, p6, p7); + promises.push(p2, p3, p4, p5, p6, p7); // @ts-expect-error ts-migrate(2345) FIXME: Argument of type '(Promise | ((dispatch: Dis... Remove this comment to see the full error message return promiseUtils.PromiseAllWithFails(promises); diff --git a/src/redux/data.ts b/src/redux/data.ts index 53e19da1630..c4b3ddafb96 100644 --- a/src/redux/data.ts +++ b/src/redux/data.ts @@ -2,7 +2,6 @@ import { StaticJsonRpcProvider, TransactionResponse } from '@ethersproject/provi import { isEmpty, isNil, mapValues, partition } from 'lodash'; import { Dispatch } from 'redux'; import { ThunkDispatch } from 'redux-thunk'; -import { decrementNonce, incrementNonce } from './nonceManager'; import { AppGetState, AppState } from './store'; import { NativeCurrencyKeys, @@ -43,6 +42,7 @@ import { queryClient } from '@/react-query'; import { RainbowAddressAssets } from '@/resources/assets/types'; import { nftsQueryKey } from '@/resources/nfts'; import { getProvider } from 'e2e/helpers'; +import { nonceStore } from '@/state/nonces'; const BACKUP_SHEET_DELAY_MS = android ? 10000 : 3000; @@ -358,9 +358,9 @@ const checkForUpdatedNonce = const [latestTx] = txSortedByDescendingNonce; const addressFrom = latestTx?.address_from; const nonce = latestTx?.nonce; - if (addressFrom && nonce) { - // @ts-ignore-next-line - dispatch(incrementNonce(addressFrom!, nonce, network)); + if (!isNil(addressFrom) && nonce) { + const { setNonce } = nonceStore.getState(); + setNonce({ address: addressFrom, currentNonce: nonce, network }); } } }; @@ -587,11 +587,9 @@ export const dataAddNewTransaction = loggr.debug('dataAddNewTransaction: adding pending transactions', {}, loggr.DebugContext.f2c); - if (parsedTransaction.from && parsedTransaction.nonce) { - dispatch( - // @ts-ignore-next-line - incrementNonce(parsedTransaction.from, parsedTransaction.nonce, parsedTransaction.network) - ); + if (parsedTransaction.from && parsedTransaction.nonce && parsedTransaction.network) { + const { setNonce } = nonceStore.getState(); + setNonce({ address: parsedTransaction.from, currentNonce: parsedTransaction.nonce, network: parsedTransaction.network }); } if (!disableTxnWatcher || network !== Network.mainnet || parsedTransaction?.network) { loggr.debug('dataAddNewTransaction: watching new pending transactions', {}, loggr.DebugContext.f2c); @@ -714,9 +712,13 @@ export const dataWatchPendingTransactions = pendingTransactionData = await getTransactionFlashbotStatus(updatedPendingTransaction, txHash); if (pendingTransactionData && !pendingTransactionData.pending) { txStatusesDidChange = true; - // decrement the nonce since it was dropped - // @ts-ignore-next-line - dispatch(decrementNonce(tx.from!, tx.nonce!, Network.mainnet)); + if (tx.from && tx.network) { + const { setNonce, nonces } = nonceStore.getState(); + const currentNonce = nonces[tx.from!][tx.network].currentNonce; + if (currentNonce) { + setNonce({ address: tx.from, currentNonce: currentNonce - 1, network: tx.network }); + } + } } } if (pendingTransactionData) { diff --git a/src/redux/nonceManager.ts b/src/redux/nonceManager.ts deleted file mode 100644 index 286d6d944f0..00000000000 --- a/src/redux/nonceManager.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { isNil } from 'lodash'; -import { AppDispatch, AppGetState } from './store'; -import { EthereumAddress, NonceManager } from '@/entities'; -import { getNonceManager, saveNonceManager } from '@/handlers/localstorage/nonceManager'; -import { Network } from '@/helpers/networkTypes'; -import logger from '@/utils/logger'; - -interface NonceManagerLoadSuccessAction { - type: typeof NONCE_MANAGER_LOAD_SUCCESS; - payload: NonceManager; -} - -interface NonceManagerUpdateNonceAction { - type: typeof NONCE_MANAGER_UPDATE_NONCE; - payload: NonceManager; -} - -type NonceManagerActionType = NonceManagerLoadSuccessAction | NonceManagerUpdateNonceAction; - -// -- Constants --------------------------------------- // -const NONCE_MANAGER_LOAD_SUCCESS = 'NONCE_MANAGER_LOAD_SUCCESS'; -const NONCE_MANAGER_LOAD_FAILURE = 'NONCE_MANAGER_LOAD_FAILURE'; -const NONCE_MANAGER_UPDATE_NONCE = 'NONCE_MANAGER_UPDATE_NONCE'; - -// -- Actions ---------------------------------------- // -export const nonceManagerLoadState = () => async (dispatch: AppDispatch) => { - try { - const nonceManager = await getNonceManager(); - if (nonceManager) { - dispatch({ - payload: nonceManager, - type: NONCE_MANAGER_LOAD_SUCCESS, - }); - } - } catch (error) { - dispatch({ type: NONCE_MANAGER_LOAD_FAILURE }); - } -}; - -export const incrementNonce = - (accountAddress: EthereumAddress, nonce: number, network = Network.mainnet) => - (dispatch: AppDispatch) => - dispatch(updateNonce(accountAddress, nonce, network)); - -export const decrementNonce = - (accountAddress: EthereumAddress, nonce: number, network = Network.mainnet) => - (dispatch: AppDispatch) => - dispatch(updateNonce(accountAddress, nonce, network, false)); - -export const updateNonce = - (accountAddress: EthereumAddress, nonce: number, network = Network.mainnet, increment: boolean = true) => - (dispatch: AppDispatch, getState: AppGetState) => { - const { nonceManager: currentNonceData } = getState(); - const currentNonce = currentNonceData[accountAddress.toLowerCase()]?.[network]?.nonce; - const counterShouldBeUpdated = isNil(currentNonce) || (increment ? currentNonce < nonce : currentNonce >= nonce); - - if (counterShouldBeUpdated) { - const newNonce = increment ? nonce : nonce - 1; - logger.log('Updating nonce: ', accountAddress, network, newNonce); - - const lcAccountAddress = accountAddress.toLowerCase(); - const updatedNonceManager = { - ...currentNonceData, - [lcAccountAddress]: { - ...(currentNonceData[lcAccountAddress] || {}), - [network]: { nonce: newNonce }, - }, - }; - dispatch({ - payload: updatedNonceManager, - type: NONCE_MANAGER_UPDATE_NONCE, - }); - saveNonceManager(updatedNonceManager); - } - }; -export const resetNonces = (accountAddress: EthereumAddress) => async (dispatch: AppDispatch, getState: AppGetState) => { - const { nonceManager: currentNonceData } = getState(); - - const currentAccountAddress = accountAddress.toLowerCase(); - - const updatedNonceManager: NonceManager = { - ...currentNonceData, - [currentAccountAddress]: {}, - }; - - dispatch({ - payload: updatedNonceManager, - type: NONCE_MANAGER_UPDATE_NONCE, - }); - saveNonceManager(updatedNonceManager); -}; -// -- Reducer ----------------------------------------- // -const INITIAL_STATE: NonceManager = {}; - -export default (state = INITIAL_STATE, action: NonceManagerActionType) => { - switch (action.type) { - case NONCE_MANAGER_LOAD_SUCCESS: - return { - ...state, - ...action.payload, - }; - case NONCE_MANAGER_UPDATE_NONCE: - return { - ...state, - ...action.payload, - }; - default: - return state; - } -}; diff --git a/src/redux/reducers.ts b/src/redux/reducers.ts index b4424dc7dc7..fc5c4e49c8e 100644 --- a/src/redux/reducers.ts +++ b/src/redux/reducers.ts @@ -11,7 +11,6 @@ import gas from './gas'; import hiddenTokens from './hiddenTokens'; import imageMetadata from './imageMetadata'; import keyboardHeight from './keyboardHeight'; -import nonceManager from './nonceManager'; import requests from './requests'; import settings from './settings'; import showcaseTokens from './showcaseTokens'; @@ -32,7 +31,6 @@ export default combineReducers({ hiddenTokens, imageMetadata, keyboardHeight, - nonceManager, requests, settings, showcaseTokens, diff --git a/src/screens/ExchangeModal.tsx b/src/screens/ExchangeModal.tsx index ab7365fb4d4..6262045942f 100644 --- a/src/screens/ExchangeModal.tsx +++ b/src/screens/ExchangeModal.tsx @@ -34,7 +34,6 @@ import { delay, greaterThan } from '@/helpers/utilities'; import { useAccountSettings, useColorForAsset, - useCurrentNonce, useGas, usePrevious, usePriceImpactDetails, @@ -71,6 +70,7 @@ import Animated from 'react-native-reanimated'; import { handleReviewPromptAction } from '@/utils/reviewAlert'; import { ReviewPromptAction } from '@/storage/schema'; import { SwapPriceImpactType } from '@/hooks/usePriceImpactDetails'; +import { getNextNonce } from '@/state/nonces'; export const DEFAULT_SLIPPAGE_BIPS = { [Network.mainnet]: 100, @@ -218,8 +218,6 @@ export default function ExchangeModal({ fromDiscover, ignoreInitialTypeCheck, te return ethereumUtils.getBasicSwapGasLimit(Number(chainId)); }, [chainId]); - const getNextNonce = useCurrentNonce(accountAddress, currentNetwork); - const { result: { derivedValues: { inputAmount, nativeAmount, outputAmount }, @@ -430,7 +428,7 @@ export default function ExchangeModal({ fromDiscover, ignoreInitialTypeCheck, te } }; logger.log('[exchange - handle submit] rap'); - const nonce = await getNextNonce(); + const nonce = await getNextNonce({ address: accountAddress, network: currentNetwork }); const { independentField, independentValue, slippageInBips, source } = store.getState().swap; const swapParameters = { chainId, @@ -517,7 +515,6 @@ export default function ExchangeModal({ fromDiscover, ignoreInitialTypeCheck, te currentNetwork, debouncedIsHighPriceImpact, flashbots, - getNextNonce, goBack, inputAmount, inputCurrency, diff --git a/src/screens/SendSheet.js b/src/screens/SendSheet.js index e2c1a5fb3cc..bf5ab9574b9 100644 --- a/src/screens/SendSheet.js +++ b/src/screens/SendSheet.js @@ -64,6 +64,7 @@ import { NoResultsType } from '@/components/list/NoResults'; import { setHardwareTXError } from '@/navigation/HardwareWalletTxNavigator'; import { Wallet } from '@ethersproject/wallet'; import { getNetworkObj } from '@/networks'; +import { getNextNonce } from '@/state/nonces'; const sheetHeight = deviceUtils.dimensions.height - (IS_ANDROID ? 30 : 10); const statusBarHeight = IS_IOS ? safeAreaInsetValues.top : StatusBar.currentHeight; @@ -134,8 +135,6 @@ export default function SendSheet(props) { const prevNetwork = usePrevious(currentNetwork); const [currentInput, setCurrentInput] = useState(''); - const getNextNonce = useCurrentNonce(accountAddress, currentNetwork); - const { params } = useRoute(); const assetOverride = params?.asset; const prevAssetOverride = usePrevious(assetOverride); @@ -451,7 +450,7 @@ export default function SendSheet(props) { from: accountAddress, gasLimit: gasLimitToUse, network: currentNetwork, - nonce: nextNonce ?? (await getNextNonce()), + nonce: nextNonce ?? (await getNextNonce({ address: accountAddress, network: currentNetwork })), to: toAddress, ...gasParams, }; @@ -518,7 +517,6 @@ export default function SendSheet(props) { ensProfile?.data?.contenthash, ensProfile?.data?.records, gasLimit, - getNextNonce, isENS, isSufficientGas, isValidAddress, diff --git a/src/screens/SettingsSheet/components/DevSection.tsx b/src/screens/SettingsSheet/components/DevSection.tsx index ea8e3cdc4eb..f697e6f8d28 100644 --- a/src/screens/SettingsSheet/components/DevSection.tsx +++ b/src/screens/SettingsSheet/components/DevSection.tsx @@ -51,8 +51,8 @@ import { isAuthenticated } from '@/utils/authentication'; import { DATA_UPDATE_PENDING_TRANSACTIONS_SUCCESS } from '@/redux/data'; import { saveLocalPendingTransactions } from '@/handlers/localstorage/accountLocal'; import { getFCMToken } from '@/notifications/tokens'; -import { resetNonces } from '@/redux/nonceManager'; import { removeGlobalNotificationSettings } from '@/notifications/settings/settings'; +import { nonceStore } from '@/state/nonces'; const DevSection = () => { const { navigate } = useNavigation(); @@ -190,7 +190,9 @@ const DevSection = () => { }); // reset nonces - resetNonces(accountAddress); + const { clearNonces } = nonceStore.getState(); + + clearNonces(); }; const clearLocalStorage = async () => { diff --git a/src/screens/SignTransactionSheet.tsx b/src/screens/SignTransactionSheet.tsx index 9afba0f40e9..8b2d934177a 100644 --- a/src/screens/SignTransactionSheet.tsx +++ b/src/screens/SignTransactionSheet.tsx @@ -42,7 +42,6 @@ import { TransactionSimulationAsset, TransactionSimulationMeta, TransactionSimulationResult, - TransactionSimulationChange, TransactionScanResultType, } from '@/graphql/__generated__/metadataPOST'; import { Network } from '@/networks/types'; @@ -60,7 +59,7 @@ import { useDispatch, useSelector } from 'react-redux'; import { AppState } from '../redux/store'; import { findWalletWithAccount } from '@/helpers/findWalletWithAccount'; import { getAccountProfileInfo } from '@/helpers/accountInfo'; -import { useAccountSettings, useClipboard, useCurrentNonce, useDimensions, useGas, useWallets } from '@/hooks'; +import { useAccountSettings, useClipboard, useDimensions, useGas, useWallets } from '@/hooks'; import ImageAvatar from '@/components/contacts/ImageAvatar'; import { ContactAvatar } from '@/components/contacts'; import { IS_IOS } from '@/env'; @@ -95,6 +94,7 @@ import { isAddress } from '@ethersproject/address'; import { methodRegistryLookupAndParse } from '@/utils/methodRegistry'; import { sanitizeTypedData } from '@/utils/signingUtils'; import { hexToNumber, isHex } from 'viem'; +import { getNextNonce } from '@/state/nonces'; const COLLAPSED_CARD_HEIGHT = 56; const MAX_CARD_HEIGHT = 176; @@ -346,8 +346,6 @@ export const SignTransactionSheet = () => { walletNames, ]); - const getNextNonce = useCurrentNonce(accountInfo.address, currentNetwork!); - useEffect(() => { setCurrentNetwork( ethereumUtils.getNetworkFromChainId( @@ -393,7 +391,7 @@ export const SignTransactionSheet = () => { (async () => { if (accountInfo.address && currentNetwork && !isMessageRequest && !nonceForDisplay) { try { - const nonce = await getNextNonce(); + const nonce = await getNextNonce({ address: accountInfo.address, network: currentNetwork }); if (nonce || nonce === 0) { const nonceAsString = nonce.toString(); setNonceForDisplay(nonceAsString); @@ -650,7 +648,7 @@ export const SignTransactionSheet = () => { const sendInsteadOfSign = transactionDetails.payload.method === SEND_TRANSACTION; const txPayload = req; let { gas, gasLimit: gasLimitFromPayload } = txPayload; - + if (!currentNetwork) return; try { logger.debug( 'WC: gas suggested by dapp', @@ -684,7 +682,8 @@ export const SignTransactionSheet = () => { const cleanTxPayload = omitFlatten(txPayload, ['gasPrice', 'maxFeePerGas', 'maxPriorityFeePerGas']); const gasParams = parseGasParamsForTransaction(selectedGasFee); const calculatedGasLimit = gas || gasLimitFromPayload || gasLimit; - const nonce = await getNextNonce(); + + const nonce = await getNextNonce({ address: accountInfo.address, network: currentNetwork }); let txPayloadUpdated = { ...cleanTxPayload, ...gasParams, @@ -815,7 +814,6 @@ export const SignTransactionSheet = () => { req, selectedGasFee, gasLimit, - getNextNonce, provider, currentNetwork, accountInfo.address, diff --git a/src/screens/TransactionConfirmationScreen.js b/src/screens/TransactionConfirmationScreen.js index 3eaba6146d9..fe231278044 100644 --- a/src/screens/TransactionConfirmationScreen.js +++ b/src/screens/TransactionConfirmationScreen.js @@ -85,7 +85,7 @@ import { handleSessionRequestResponse } from '@/walletConnect'; import { isAddress } from '@ethersproject/address'; import { logger, RainbowError } from '@/logger'; import { getNetworkObj } from '@/networks'; -import { IS_IOS } from '@/env'; +import { getNextNonce } from '@/state/nonces'; const springConfig = { damping: 500, @@ -200,8 +200,6 @@ export default function TransactionConfirmationScreen() { }; }, [walletConnector?._accounts, walletNames, wallets, walletConnectV2RequestValues]); - const getNextNonce = useCurrentNonce(accountInfo.address, currentNetwork); - const isTestnet = isTestnetNetwork(currentNetwork); const isL2 = isL2Network(currentNetwork); const disableFlashbotsPostMerge = !flashbots_enabled; @@ -548,7 +546,7 @@ export default function TransactionConfirmationScreen() { const cleanTxPayload = omitFlatten(txPayload, ['gasPrice', 'maxFeePerGas', 'maxPriorityFeePerGas']); const gasParams = parseGasParamsForTransaction(selectedGasFee); const calculatedGasLimit = gas || gasLimitFromPayload || gasLimit; - const nonce = await getNextNonce(); + const nonce = await getNextNonce({ address: accountInfo.address, network: currentNetwork }); let txPayloadUpdated = { ...cleanTxPayload, ...gasParams, @@ -654,14 +652,13 @@ export default function TransactionConfirmationScreen() { params, selectedGasFee, gasLimit, - getNextNonce, - currentNetwork, provider, accountInfo.address, accountInfo.isHardwareWallet, callback, dappName, dappUrl, + currentNetwork, isFocused, requestId, closeScreen, diff --git a/src/state/nonces/index.test.skip b/src/state/nonces/index.test.skip new file mode 100644 index 00000000000..c8267489668 --- /dev/null +++ b/src/state/nonces/index.test.skip @@ -0,0 +1,196 @@ +const TEST_ADDRESS_1 = '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266'; + +const TEST_ADDRESS_2 = '0x70997970c51812dc3a010c7d01b50e0d17dc79c8'; + +const TEST_ADDRESS_3 = '0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc'; + +import { nonceStore } from '.'; + +// from networks/types to get around mocking a bunch of imports + +enum Network { + arbitrum = 'arbitrum', + goerli = 'goerli', + mainnet = 'mainnet', + optimism = 'optimism', + polygon = 'polygon', + base = 'base', + bsc = 'bsc', + zora = 'zora', + gnosis = 'gnosis', +} + +test('should be able to set nonce for one wallet in one network', async () => { + const { nonces, setNonce } = nonceStore.getState(); + expect(nonces).toStrictEqual({}); + setNonce({ + address: TEST_ADDRESS_1, + currentNonce: 1, + latestConfirmedNonce: 1, + network: Network.mainnet, + }); + const newNonces = nonceStore.getState().nonces; + expect(newNonces).toStrictEqual({ + [TEST_ADDRESS_1]: { + [Network.mainnet]: { + currentNonce: 1, + latestConfirmedNonce: 1, + }, + }, + }); +}); + +test('should be able to set nonce for same wallet in a different network', async () => { + const { setNonce } = nonceStore.getState(); + setNonce({ + address: TEST_ADDRESS_1, + currentNonce: 4, + latestConfirmedNonce: 4, + network: Network.optimism, + }); + const newNonces = nonceStore.getState().nonces; + expect(newNonces).toStrictEqual({ + [TEST_ADDRESS_1]: { + [Network.mainnet]: { + currentNonce: 1, + latestConfirmedNonce: 1, + }, + [Network.optimism]: { + currentNonce: 4, + latestConfirmedNonce: 4, + }, + }, + }); +}); + +test('should be able to set nonce for other wallet in one network', async () => { + const { setNonce } = nonceStore.getState(); + setNonce({ + address: TEST_ADDRESS_2, + currentNonce: 2, + latestConfirmedNonce: 2, + network: Network.mainnet, + }); + const newNonces = nonceStore.getState().nonces; + expect(newNonces).toStrictEqual({ + [TEST_ADDRESS_1]: { + [Network.mainnet]: { + currentNonce: 1, + latestConfirmedNonce: 1, + }, + [Network.optimism]: { + currentNonce: 4, + latestConfirmedNonce: 4, + }, + }, + [TEST_ADDRESS_2]: { + [Network.mainnet]: { + currentNonce: 2, + latestConfirmedNonce: 2, + }, + }, + }); +}); + +test('should be able to set nonce for other wallet in other network', async () => { + const { setNonce } = nonceStore.getState(); + setNonce({ + address: TEST_ADDRESS_3, + currentNonce: 3, + latestConfirmedNonce: 3, + network: Network.arbitrum, + }); + const newNonces = nonceStore.getState().nonces; + expect(newNonces).toStrictEqual({ + [TEST_ADDRESS_1]: { + [Network.mainnet]: { + currentNonce: 1, + latestConfirmedNonce: 1, + }, + [Network.optimism]: { + currentNonce: 4, + latestConfirmedNonce: 4, + }, + }, + [TEST_ADDRESS_2]: { + [Network.mainnet]: { + currentNonce: 2, + latestConfirmedNonce: 2, + }, + }, + [TEST_ADDRESS_3]: { + [Network.arbitrum]: { + currentNonce: 3, + latestConfirmedNonce: 3, + }, + }, + }); +}); + +test('should be able to set nonce nonce information correctly', async () => { + const { setNonce, getNonce } = nonceStore.getState(); + setNonce({ + address: TEST_ADDRESS_3, + currentNonce: 3, + latestConfirmedNonce: 3, + network: Network.arbitrum, + }); + const nonces11 = getNonce({ + address: TEST_ADDRESS_1, + network: Network.mainnet, + }); + const nonces12 = getNonce({ + address: TEST_ADDRESS_1, + network: Network.optimism, + }); + const nonces2 = getNonce({ + address: TEST_ADDRESS_2, + network: Network.mainnet, + }); + const nonces3 = getNonce({ + address: TEST_ADDRESS_3, + network: Network.arbitrum, + }); + expect(nonces11?.currentNonce).toEqual(1); + expect(nonces11?.latestConfirmedNonce).toEqual(1); + expect(nonces12?.currentNonce).toEqual(4); + expect(nonces12?.latestConfirmedNonce).toEqual(4); + expect(nonces2?.currentNonce).toEqual(2); + expect(nonces2?.latestConfirmedNonce).toEqual(2); + expect(nonces3?.currentNonce).toEqual(3); + expect(nonces3?.latestConfirmedNonce).toEqual(3); +}); + +test('should be able to update nonce', async () => { + const { setNonce, getNonce } = nonceStore.getState(); + setNonce({ + address: TEST_ADDRESS_1, + currentNonce: 30, + latestConfirmedNonce: 30, + network: Network.mainnet, + }); + const updatedNonce = getNonce({ + address: TEST_ADDRESS_1, + network: Network.mainnet, + }); + const oldNonce = getNonce({ + address: TEST_ADDRESS_1, + network: Network.optimism, + }); + expect(updatedNonce?.currentNonce).toStrictEqual(30); + expect(updatedNonce?.latestConfirmedNonce).toStrictEqual(30); + expect(oldNonce?.currentNonce).toStrictEqual(4); + expect(oldNonce?.latestConfirmedNonce).toStrictEqual(4); + setNonce({ + address: TEST_ADDRESS_1, + currentNonce: 31, + latestConfirmedNonce: 30, + network: Network.mainnet, + }); + const updatedNonceSecondTime = getNonce({ + address: TEST_ADDRESS_1, + network: Network.mainnet, + }); + expect(updatedNonceSecondTime?.currentNonce).toStrictEqual(31); + expect(updatedNonceSecondTime?.latestConfirmedNonce).toStrictEqual(30); +}); diff --git a/src/state/nonces/index.ts b/src/state/nonces/index.ts new file mode 100644 index 00000000000..855611be8c2 --- /dev/null +++ b/src/state/nonces/index.ts @@ -0,0 +1,71 @@ +import create from 'zustand'; +import { createStore } from '../internal/createStore'; +import { Network } from '@/networks/types'; +import { getProviderForNetwork } from '@/handlers/web3'; + +type NonceData = { + currentNonce?: number; + latestConfirmedNonce?: number; +}; + +type GetNonceArgs = { + address: string; + network: Network; +}; + +type UpdateNonceArgs = NonceData & GetNonceArgs; + +export async function getNextNonce({ address, network }: { address: string; network: Network }) { + const { getNonce } = nonceStore.getState(); + const localNonceData = getNonce({ address, network }); + const localNonce = localNonceData?.currentNonce || 0; + const provider = await getProviderForNetwork(network); + const txCountIncludingPending = await provider.getTransactionCount(address, 'pending'); + if (!localNonce && !txCountIncludingPending) return 0; + const ret = Math.max(localNonce + 1, txCountIncludingPending); + return ret; +} + +export interface CurrentNonceState { + nonces: Record>; + setNonce: ({ address, currentNonce, latestConfirmedNonce, network }: UpdateNonceArgs) => void; + getNonce: ({ address, network }: GetNonceArgs) => NonceData | null; + clearNonces: () => void; +} + +export const nonceStore = createStore( + (set, get) => ({ + nonces: {}, + setNonce: ({ address, currentNonce, latestConfirmedNonce, network }) => { + const { nonces: oldNonces } = get(); + const addressAndChainIdNonces = oldNonces?.[address]?.[network] || {}; + set({ + nonces: { + ...oldNonces, + [address]: { + ...oldNonces[address], + [network]: { + currentNonce: currentNonce ?? addressAndChainIdNonces?.currentNonce, + latestConfirmedNonce: latestConfirmedNonce ?? addressAndChainIdNonces?.latestConfirmedNonce, + }, + }, + }, + }); + }, + getNonce: ({ address, network }) => { + const { nonces } = get(); + return nonces[address]?.[network] ?? null; + }, + clearNonces: () => { + set({ nonces: {} }); + }, + }), + { + persist: { + name: 'nonces', + version: 0, + }, + } +); + +export const useNonceStore = create(nonceStore);