From 04016c2def9f88ae2d05c523a1684c8dc8d04dd8 Mon Sep 17 00:00:00 2001 From: Ikemefuna Obioha Date: Fri, 13 Oct 2023 19:45:31 +0100 Subject: [PATCH 01/21] Implemented initial account nonce sync --- src/modules/account/hooks/useAccounts.js | 13 +++++- src/modules/account/store/action.js | 6 +++ src/modules/account/store/actionTypes.js | 1 + src/modules/account/store/reducer.js | 15 ++++++- src/modules/account/store/selectors.js | 1 + src/modules/auth/hooks/useNonceSync.js | 52 ++++++++++++++++++++++++ 6 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 src/modules/auth/hooks/useNonceSync.js diff --git a/src/modules/account/hooks/useAccounts.js b/src/modules/account/hooks/useAccounts.js index e35d549d90..67f3ea852f 100644 --- a/src/modules/account/hooks/useAccounts.js +++ b/src/modules/account/hooks/useAccounts.js @@ -1,8 +1,8 @@ import { useMemo, useCallback } from 'react'; import { useSelector, useDispatch } from 'react-redux'; -import { selectAccounts } from '@account/store/selectors'; +import { selectAccounts, selectAccountNonce } from '@account/store/selectors'; import { selectHWAccounts } from '@hardwareWallet/store/selectors/hwSelectors'; -import { addAccount, deleteAccount } from '../store/action'; +import { addAccount, deleteAccount, setAccountNonce } from '../store/action'; // eslint-disable-next-line export function useAccounts() { @@ -22,11 +22,20 @@ export function useAccounts() { const getAccountByPublicKey = (pubkey) => accounts.find((account) => account.metadata.pubkey === pubkey); + const setNonceByAccount = useCallback( + (address, nonce) => dispatch(setAccountNonce(address, nonce)), + [] + ); + + const getNonceByAccount = (address) => useSelector(selectAccountNonce)[address]; + return { accounts, setAccount, deleteAccountByAddress, getAccountByPublicKey, getAccountByAddress, + setNonceByAccount, + getNonceByAccount, }; } diff --git a/src/modules/account/store/action.js b/src/modules/account/store/action.js index 7d6c1241ef..ef5eed97af 100644 --- a/src/modules/account/store/action.js +++ b/src/modules/account/store/action.js @@ -21,6 +21,12 @@ export const addAccount = (encryptedAccount) => ({ encryptedAccount, }); +export const setAccountNonce = (address, nonce) => ({ + type: actionTypes.addAccount, + address, + nonce, +}); + export const updateAccount = ({ encryptedAccount, accountDetail }) => ({ type: actionTypes.updateAccount, encryptedAccount, diff --git a/src/modules/account/store/actionTypes.js b/src/modules/account/store/actionTypes.js index cffef02dc4..e091a0ea68 100644 --- a/src/modules/account/store/actionTypes.js +++ b/src/modules/account/store/actionTypes.js @@ -2,6 +2,7 @@ const actionTypes = { setCurrentAccount: 'SET_CURRENT_ACCOUNT', updateCurrentAccount: 'UPDATE_CURRENT_ACCOUNT', addAccount: 'ADD_ACCOUNT', + setAccountNonce: 'SET_ACCOUNT_NONCE', updateAccount: 'UPDATE_ACCOUNT', deleteAccount: 'DELETE_ACCOUNT', }; diff --git a/src/modules/account/store/reducer.js b/src/modules/account/store/reducer.js index 0c4b9d8a24..4e8ad296b0 100644 --- a/src/modules/account/store/reducer.js +++ b/src/modules/account/store/reducer.js @@ -58,14 +58,25 @@ export const list = (state = {}, { type, encryptedAccount, accountDetail, addres } }; +export const localNonce = (state = {}, { type, address, nonce }) => { + switch (type) { + case actionTypes.setAccountNonce: + state[address] = nonce; + return state; + + default: + return state; + } +}; + const persistConfig = { key: 'account', storage, - whitelist: ['list', 'current'], // only navigation will be persisted + whitelist: ['list', 'current', 'localNonce'], // only navigation will be persisted blacklist: [], }; -const accountReducer = combineReducers({ current, list }); +const accountReducer = combineReducers({ current, list, localNonce }); // eslint-disable-next-line import/prefer-default-export export const account = persistReducer(persistConfig, accountReducer); diff --git a/src/modules/account/store/selectors.js b/src/modules/account/store/selectors.js index 04ace7e353..c59ae294bd 100644 --- a/src/modules/account/store/selectors.js +++ b/src/modules/account/store/selectors.js @@ -1,2 +1,3 @@ export const selectCurrentAccount = (state) => state.account.current; export const selectAccounts = (state) => state.account.list || []; +export const selectAccountNonce = (state) => state.account.localNonce || {}; diff --git a/src/modules/auth/hooks/useNonceSync.js b/src/modules/auth/hooks/useNonceSync.js new file mode 100644 index 0000000000..5b57d4452b --- /dev/null +++ b/src/modules/auth/hooks/useNonceSync.js @@ -0,0 +1,52 @@ +import { useCallback, useMemo } from 'react'; +import { useQueryClient } from '@tanstack/react-query'; +import { useCurrentApplication } from '@blockchainApplication/manage/hooks'; +import { useCurrentAccount, useAccounts } from '@account/hooks'; +import useSettings from '@settings/hooks/useSettings'; +import { AUTH } from 'src/const/queries'; +import { useAuthConfig } from './queries'; + +// eslint-disable-next-line max-statements +const useNonceSync = () => { + const queryClient = useQueryClient(); + const [currentApplication] = useCurrentApplication(); + const [currentAccount] = useCurrentAccount(); + const { setNonceByAccount, getNonceByAccount } = useAccounts(); + const currentAccountNonce = getNonceByAccount(currentAccount.metadata.address); + const { mainChainNetwork } = useSettings('mainChainNetwork'); + const chainID = currentApplication.chainID; + const address = currentAccount.metadata.address; + const customConfig = { + params: { + address, + }, + }; + const baseUrl = mainChainNetwork?.serviceUrl; + const config = useAuthConfig(customConfig); + const authData = queryClient.getQueryData([AUTH, chainID, config, baseUrl]); + const onChainNonce = authData?.data.nonce; + + // Store nonce by address in account + const handleLocalNonce = useCallback((currentNonce) => { + let localNonce = parseInt(currentAccountNonce ?? 0, 10); + if (localNonce < currentNonce) { + localNonce = currentNonce; + } + setNonceByAccount(currentAccount.metadata.address, localNonce); + + return localNonce; + }, []); + + // Call incrementNonce after transaction signing + const incrementNonce = useCallback(() => { + let localNonce = currentAccountNonce ?? 0; + localNonce += 1; + setNonceByAccount(currentAccount.metadata.address, localNonce); + }, []); + + const accountNonce = useMemo(() => handleLocalNonce(parseInt(onChainNonce, 10)), [onChainNonce]); + + return { accountNonce, incrementNonce }; +}; + +export default useNonceSync; From b7bbd4ba23ca89df8e97660cf85f8bf4ba2ecba8 Mon Sep 17 00:00:00 2001 From: Ikemefuna Obioha Date: Tue, 17 Oct 2023 15:44:03 +0100 Subject: [PATCH 02/21] Optimized nonce hook and minor Redux fix --- src/modules/account/store/action.js | 2 +- src/modules/auth/hooks/useNonceSync.js | 15 +++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/modules/account/store/action.js b/src/modules/account/store/action.js index ef5eed97af..22b3157ecd 100644 --- a/src/modules/account/store/action.js +++ b/src/modules/account/store/action.js @@ -22,7 +22,7 @@ export const addAccount = (encryptedAccount) => ({ }); export const setAccountNonce = (address, nonce) => ({ - type: actionTypes.addAccount, + type: actionTypes.setAccountNonce, address, nonce, }); diff --git a/src/modules/auth/hooks/useNonceSync.js b/src/modules/auth/hooks/useNonceSync.js index 5b57d4452b..565c30f35b 100644 --- a/src/modules/auth/hooks/useNonceSync.js +++ b/src/modules/auth/hooks/useNonceSync.js @@ -1,4 +1,4 @@ -import { useCallback, useMemo } from 'react'; +import { useEffect, useState, useCallback /* useMemo */ } from 'react'; import { useQueryClient } from '@tanstack/react-query'; import { useCurrentApplication } from '@blockchainApplication/manage/hooks'; import { useCurrentAccount, useAccounts } from '@account/hooks'; @@ -25,17 +25,22 @@ const useNonceSync = () => { const config = useAuthConfig(customConfig); const authData = queryClient.getQueryData([AUTH, chainID, config, baseUrl]); const onChainNonce = authData?.data.nonce; + const [accountNonce, setAccountNonce] = useState(onChainNonce); // Store nonce by address in account - const handleLocalNonce = useCallback((currentNonce) => { + const handleLocalNonce = (currentNonce) => { let localNonce = parseInt(currentAccountNonce ?? 0, 10); if (localNonce < currentNonce) { localNonce = currentNonce; } setNonceByAccount(currentAccount.metadata.address, localNonce); - return localNonce; - }, []); + setAccountNonce(localNonce); + }; + + useEffect(() => { + handleLocalNonce(parseInt(onChainNonce, 10)); + }, [onChainNonce]); // Call incrementNonce after transaction signing const incrementNonce = useCallback(() => { @@ -44,8 +49,6 @@ const useNonceSync = () => { setNonceByAccount(currentAccount.metadata.address, localNonce); }, []); - const accountNonce = useMemo(() => handleLocalNonce(parseInt(onChainNonce, 10)), [onChainNonce]); - return { accountNonce, incrementNonce }; }; From c656a9add29cb096f44d6e3ada3d06776709605a Mon Sep 17 00:00:00 2001 From: Ikemefuna Obioha Date: Tue, 17 Oct 2023 15:50:03 +0100 Subject: [PATCH 03/21] Implemented nonce hook in transaction signing process --- .../TransactionInfo/TransactionInfo.css | 4 +++ .../TransactionInfo/TransactionInfo.js | 7 +++- .../components/TxComposer/index.js | 14 ++++---- .../TxSignatureCollector.js | 8 ++++- .../components/TxSummarizer/index.js | 33 ++++++++++++++----- .../components/TxSummarizer/txSummarizer.css | 27 +++++++++++++++ 6 files changed, 76 insertions(+), 17 deletions(-) diff --git a/src/modules/transaction/components/TransactionInfo/TransactionInfo.css b/src/modules/transaction/components/TransactionInfo/TransactionInfo.css index 13c7f434f6..999fd3bf3c 100644 --- a/src/modules/transaction/components/TransactionInfo/TransactionInfo.css +++ b/src/modules/transaction/components/TransactionInfo/TransactionInfo.css @@ -194,3 +194,7 @@ margin-left: 10px; } } + +.warning { + vertical-align: bottom; +} diff --git a/src/modules/transaction/components/TransactionInfo/TransactionInfo.js b/src/modules/transaction/components/TransactionInfo/TransactionInfo.js index 86954a39c3..ff0bc84a09 100644 --- a/src/modules/transaction/components/TransactionInfo/TransactionInfo.js +++ b/src/modules/transaction/components/TransactionInfo/TransactionInfo.js @@ -3,6 +3,7 @@ import { withTranslation } from 'react-i18next'; import WalletVisual from '@wallet/components/walletVisual'; import TokenAmount from '@token/fungible/components/tokenAmount'; import { extractAddressFromPublicKey } from '@wallet/utils/account'; +import Icon from '@theme/Icon'; import styles from './TransactionInfo.css'; import CustomTransactionInfo from './CustomTransactionInfo'; import { joinModuleAndCommand } from '../../utils'; @@ -37,6 +38,7 @@ const TransactionInfo = ({ date, token, summaryInfo, + nonceWarning, }) => { const isRegisterMultisignature = joinModuleAndCommand(transactionJSON) === MODULE_COMMANDS_NAME_MAP.registerMultisignature; @@ -71,7 +73,10 @@ const TransactionInfo = ({
- +
diff --git a/src/modules/transaction/components/TxComposer/index.js b/src/modules/transaction/components/TxComposer/index.js index 6599399ed5..39e554ba77 100644 --- a/src/modules/transaction/components/TxComposer/index.js +++ b/src/modules/transaction/components/TxComposer/index.js @@ -4,10 +4,10 @@ import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; import useTransactionPriority from '@transaction/hooks/useTransactionPriority'; import { useSchemas } from '@transaction/hooks/queries/useSchemas'; -import { useAuth } from '@auth/hooks/queries'; +import useNonceSync from '@auth/hooks/useNonceSync'; import { useCurrentAccount } from '@account/hooks'; -import Box from 'src/theme/box'; -import BoxFooter from 'src/theme/box/footer'; +import Box from '@theme/box'; +import BoxFooter from '@theme/box/footer'; import TransactionPriority from '@transaction/components/TransactionPriority'; import { fromTransactionJSON, @@ -20,8 +20,8 @@ import { convertFromBaseDenom, convertToBaseDenom } from '@token/fungible/utils/ import { useDeprecatedAccount } from '@account/hooks/useDeprecatedAccount'; import { useTransactionFee } from '@transaction/hooks/useTransactionFee'; import { useTokenBalances } from '@token/fungible/hooks/queries'; -import { PrimaryButton } from 'src/theme/buttons'; -import { useCommandSchema } from 'src/modules/network/hooks'; +import { PrimaryButton } from '@theme/buttons'; +import { useCommandSchema } from '@network/hooks'; import Feedback from './Feedback'; import { getFeeStatus } from '../../utils/helpers'; import { MODULE_COMMANDS_NAME_MAP } from '../../configuration/moduleCommand'; @@ -49,7 +49,7 @@ const TxComposer = ({ metadata: { pubkey, address }, }, ] = useCurrentAccount(); - const { data: auth } = useAuth({ config: { params: { address } } }); + const { accountNonce } = useNonceSync(); const { fields } = formProps; const [customFee, setCustomFee] = useState({}); const [feedback, setFeedBack] = useState(formProps.feedback); @@ -65,7 +65,7 @@ const TxComposer = ({ const transactionJSON = { module, command, - nonce: auth?.data?.nonce, + nonce: String(accountNonce), fee: 0, senderPublicKey: pubkey, params: commandParams, diff --git a/src/modules/transaction/components/TxSignatureCollector/TxSignatureCollector.js b/src/modules/transaction/components/TxSignatureCollector/TxSignatureCollector.js index 44f4ff5348..56d0371275 100644 --- a/src/modules/transaction/components/TxSignatureCollector/TxSignatureCollector.js +++ b/src/modules/transaction/components/TxSignatureCollector/TxSignatureCollector.js @@ -7,6 +7,7 @@ import { isEmpty } from 'src/utils/helpers'; import EnterPasswordForm from '@auth/components/EnterPasswordForm'; import { useAuth } from '@auth/hooks/queries'; import { useCurrentAccount } from '@account/hooks'; +import useNonceSync from '@auth/hooks/useNonceSync'; import HWSigning from '@hardwareWallet/components/HWSigning/HWSigning'; import styles from './txSignatureCollector.css'; import { joinModuleAndCommand } from '../../utils'; @@ -50,6 +51,7 @@ const TxSignatureCollector = ({ const moduleCommand = joinModuleAndCommand(transactionJSON); const isRegisterMultisignature = moduleCommand === MODULE_COMMANDS_NAME_MAP.registerMultisignature; + const { incrementNonce } = useNonceSync(); const txVerification = (privateKey = undefined, publicKey = undefined) => { /** * Non-multisignature account @@ -104,8 +106,12 @@ const TxSignatureCollector = ({ }); }; - const onEnterPasswordSuccess = ({ privateKey }) => + const onEnterPasswordSuccess = ({ privateKey }) => { + if (isTransactionAuthor) { + incrementNonce(); + } txVerification(privateKey, currentAccount?.metadata.pubkey); + }; useEffect(() => { if (!isEmpty(transactions.signedTransaction)) { diff --git a/src/modules/transaction/components/TxSummarizer/index.js b/src/modules/transaction/components/TxSummarizer/index.js index f55bd5435c..7c9c2d4b8e 100644 --- a/src/modules/transaction/components/TxSummarizer/index.js +++ b/src/modules/transaction/components/TxSummarizer/index.js @@ -1,18 +1,20 @@ import React from 'react'; -import { MODULE_COMMANDS_NAME_MAP } from 'src/modules/transaction/configuration/moduleCommand'; -import Box from 'src/theme/box'; -import BoxHeader from 'src/theme/box/header'; -import { TertiaryButton } from 'src/theme/buttons'; -import BoxContent from 'src/theme/box/content'; -import Illustration from 'src/modules/common/components/illustration'; -import Tooltip from 'src/theme/Tooltip'; +import { MODULE_COMMANDS_NAME_MAP } from '@transaction/configuration/moduleCommand'; +import Box from '@theme/box'; +import BoxHeader from '@theme/box/header'; +import { TertiaryButton } from '@theme/buttons'; +import BoxContent from '@theme/box/content'; +import Illustration from '@common/components/illustration'; +import Tooltip from '@theme/Tooltip'; import { tokenMap } from '@token/fungible/consts/tokens'; -import Icon from 'src/theme/Icon'; +import Icon from '@theme/Icon'; +import useNonceSync from '@auth/hooks/useNonceSync'; import TransactionInfo from '../TransactionInfo'; import Footer from './footer'; import styles from './txSummarizer.css'; import FeeSummarizer from './FeeSummarizer'; +// eslint-disable-next-line complexity const TxSummarizer = ({ title, children, @@ -32,6 +34,10 @@ const TxSummarizer = ({ hasNoTopCancelButton, noFeeStatus, }) => { + const { accountNonce } = useNonceSync(); + const isTransactionAuthor = transactionJSON.senderPublicKey === wallet.summary.publicKey; + const isNonceEqual = transactionJSON.nonce === String(accountNonce); + const nonceWarning = isTransactionAuthor && !isNonceEqual; const fee = !( wallet.summary.isMultisignature || formProps.moduleCommand === MODULE_COMMANDS_NAME_MAP.registerMultisignature @@ -75,6 +81,7 @@ const TxSummarizer = ({ transactionJSON={transactionJSON} account={wallet} isMultisignature={wallet.summary.isMultisignature} + nonceWarning={nonceWarning} /> {!noFeeStatus && !!fee && (
@@ -100,6 +107,16 @@ const TxSummarizer = ({
)} + {nonceWarning && ( +
+ +

+ Please ensure to send the previously initiated transactions. Check the transaction + sequence, and broadcast them only after they have been fully signed, in their original + order. +

+
+ )}