diff --git a/setup/react/assets/images/icons/reset.svg b/setup/react/assets/images/icons/reset.svg
new file mode 100644
index 0000000000..308fd62fd9
--- /dev/null
+++ b/setup/react/assets/images/icons/reset.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/locales/en/common.json b/src/locales/en/common.json
index c970f13c0e..b9d39a42dc 100644
--- a/src/locales/en/common.json
+++ b/src/locales/en/common.json
@@ -458,6 +458,7 @@
"Please click on “Switch to signing account” ": "Please click on “Switch to signing account” ",
"Please confirm the transaction on your {{deviceModel}}": "Please confirm the transaction on your {{deviceModel}}",
"Please drop the JSON here.": "Please drop the JSON here.",
+ "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.": "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.",
"Please enter a valid blockchain app URI.": "Please enter a valid blockchain app URI.",
"Please enter a valid bls key value": "Please enter a valid bls key value",
"Please enter a valid generator key value": "Please enter a valid generator key value",
diff --git a/src/modules/account/hooks/useAccounts.js b/src/modules/account/hooks/useAccounts.js
index e35d549d90..82854f6e2f 100644
--- a/src/modules/account/hooks/useAccounts.js
+++ b/src/modules/account/hooks/useAccounts.js
@@ -1,14 +1,15 @@
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, resetAccountNonce, setAccountNonce } from '../store/action';
// eslint-disable-next-line
export function useAccounts() {
const dispatch = useDispatch();
const accountsObject = useSelector(selectAccounts);
const hwAccounts = useSelector(selectHWAccounts);
+ const nonceMap = useSelector(selectAccountNonce);
const setAccount = useCallback((account) => dispatch(addAccount(account)), []);
const deleteAccountByAddress = useCallback((address) => dispatch(deleteAccount(address)), []);
@@ -22,11 +23,27 @@ export function useAccounts() {
const getAccountByPublicKey = (pubkey) =>
accounts.find((account) => account.metadata.pubkey === pubkey);
+ const setNonceByAccount = (address, nonce, transactionHex) =>
+ dispatch(setAccountNonce(address, nonce, transactionHex));
+
+ const getNonceByAccount = (address) => {
+ const accountNonceMap = nonceMap[address] ?? {};
+ const accountNonceValues = Object.values(accountNonceMap);
+ const nonceList = accountNonceValues.length ? accountNonceValues : [0];
+ return Math.max(...nonceList);
+ };
+
+ const resetNonceByAccount = (address, onChainNonce) =>
+ dispatch(resetAccountNonce(address, onChainNonce));
+
return {
accounts,
setAccount,
deleteAccountByAddress,
getAccountByPublicKey,
getAccountByAddress,
+ setNonceByAccount,
+ getNonceByAccount,
+ resetNonceByAccount,
};
}
diff --git a/src/modules/account/hooks/useAccounts.test.js b/src/modules/account/hooks/useAccounts.test.js
index 1daaa99c20..4b62513277 100644
--- a/src/modules/account/hooks/useAccounts.test.js
+++ b/src/modules/account/hooks/useAccounts.test.js
@@ -3,11 +3,18 @@ import mockSavedAccounts from '@tests/fixtures/accounts';
import actionTypes from '@account/store/actionTypes';
import { useAccounts } from './useAccounts';
+const txHex = 'a24f94966cf213deb90854c41cf1f27906135b7001a49e53a9722ebf5fc67481';
+const accountNonce = 2;
const mockDispatch = jest.fn();
const accountStateObject = { [mockSavedAccounts[0].metadata.address]: mockSavedAccounts[0] };
const mockState = {
account: {
list: accountStateObject,
+ localNonce: {
+ [mockSavedAccounts[0].metadata.address]: {
+ [txHex]: accountNonce,
+ },
+ },
},
};
jest.mock('react-redux', () => ({
@@ -70,4 +77,31 @@ describe('useAccount hook', () => {
expect(mockDispatch).toHaveBeenCalledTimes(1);
expect(mockDispatch).toHaveBeenCalledWith(expectedAction);
});
+
+ it('setNonceByAccount should dispatch an action', async () => {
+ const { setNonceByAccount } = result.current;
+ const expectedAction = {
+ type: actionTypes.setAccountNonce,
+ address: mockSavedAccounts[0].metadata.address,
+ nonce: 2,
+ transactionHex: txHex,
+ };
+ act(() => {
+ setNonceByAccount(mockSavedAccounts[0].metadata.address, 2, txHex);
+ });
+ expect(mockDispatch).toHaveBeenCalledTimes(1);
+ expect(mockDispatch).toHaveBeenCalledWith(expectedAction);
+ });
+
+ it('getNonceByAccount should retrieve stored nonce', async () => {
+ const { getNonceByAccount } = result.current;
+ const storedNonce = getNonceByAccount(mockSavedAccounts[0].metadata.address);
+ expect(storedNonce).toEqual(accountNonce);
+ });
+
+ it('getNonceByAccount should retrieve 0 if no stored nonce', async () => {
+ const { getNonceByAccount } = result.current;
+ const storedNonce = getNonceByAccount(mockSavedAccounts[1].metadata.address);
+ expect(storedNonce).toEqual(0);
+ });
});
diff --git a/src/modules/account/store/action.js b/src/modules/account/store/action.js
index 7d6c1241ef..3bb62c40ec 100644
--- a/src/modules/account/store/action.js
+++ b/src/modules/account/store/action.js
@@ -21,6 +21,19 @@ export const addAccount = (encryptedAccount) => ({
encryptedAccount,
});
+export const setAccountNonce = (address, nonce, transactionHex) => ({
+ type: actionTypes.setAccountNonce,
+ address,
+ nonce,
+ transactionHex,
+});
+
+export const resetAccountNonce = (address, onChainNonce) => ({
+ type: actionTypes.resetAccountNonce,
+ address,
+ nonce: onChainNonce,
+});
+
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..aedc146a00 100644
--- a/src/modules/account/store/actionTypes.js
+++ b/src/modules/account/store/actionTypes.js
@@ -2,6 +2,8 @@ const actionTypes = {
setCurrentAccount: 'SET_CURRENT_ACCOUNT',
updateCurrentAccount: 'UPDATE_CURRENT_ACCOUNT',
addAccount: 'ADD_ACCOUNT',
+ setAccountNonce: 'SET_ACCOUNT_NONCE',
+ resetAccountNonce: 'RESET_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..1758dc1467 100644
--- a/src/modules/account/store/reducer.js
+++ b/src/modules/account/store/reducer.js
@@ -58,14 +58,39 @@ export const list = (state = {}, { type, encryptedAccount, accountDetail, addres
}
};
+export const localNonce = (state = {}, { type, address, nonce, transactionHex }) => {
+ switch (type) {
+ case actionTypes.setAccountNonce:
+ if (state[address]?.[transactionHex]) {
+ return state;
+ }
+ return {
+ ...state,
+ [address]: {
+ ...state[address],
+ [transactionHex]: nonce,
+ },
+ };
+
+ case actionTypes.resetAccountNonce:
+ return {
+ ...state,
+ [address]: { defaultNonce: nonce },
+ };
+
+ 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/reducer.test.js b/src/modules/account/store/reducer.test.js
index 0576455d78..83dceaceca 100644
--- a/src/modules/account/store/reducer.test.js
+++ b/src/modules/account/store/reducer.test.js
@@ -1,6 +1,6 @@
import mockSavedAccounts from '@tests/fixtures/accounts';
import actionTypes from './actionTypes';
-import { list, current } from './reducer';
+import { list, current, localNonce } from './reducer';
describe('Auth reducer', () => {
it('Should return encryptedAccount if setCurrentAccount action type is triggered', async () => {
@@ -82,4 +82,41 @@ describe('Auth reducer', () => {
expect(list(defaultState, actionData)).toEqual({});
expect(current(mockSavedAccounts[0], actionData)).toEqual(mockSavedAccounts[0]);
});
+
+ it('Should set account nonce with required details', async () => {
+ const txHex = 'a24f94966cf213deb90854c41cf1f27906135b7001a49e53a9722ebf5fc67481';
+ const actionData = {
+ type: actionTypes.setAccountNonce,
+ address: mockSavedAccounts[1].metadata.address,
+ nonce: 1,
+ transactionHex: txHex,
+ };
+ const expectedState = {
+ [mockSavedAccounts[1].metadata.address]: {
+ [txHex]: 1,
+ },
+ };
+ expect(localNonce({}, actionData)).toEqual(expectedState);
+ });
+
+ it('Should return existing or default account nonce if no transaction hex is passed', async () => {
+ const txHex = 'a24f94966cf213deb90854c41cf1f27906135b7001a49e53a9722ebf5fc67481';
+ const actionData = {
+ type: actionTypes.setAccountNonce,
+ address: mockSavedAccounts[1].metadata.address,
+ nonce: 1,
+ transactionHex: txHex,
+ };
+ const expectedState = {
+ [mockSavedAccounts[1].metadata.address]: {
+ [txHex]: 1,
+ },
+ };
+ const existingState = {
+ [mockSavedAccounts[1].metadata.address]: {
+ [txHex]: 1,
+ },
+ };
+ expect(localNonce(existingState, actionData)).toEqual(expectedState);
+ });
});
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..2f661b45e5
--- /dev/null
+++ b/src/modules/auth/hooks/useNonceSync.js
@@ -0,0 +1,59 @@
+import { useEffect, useState, useCallback } 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, resetNonceByAccount } = useAccounts();
+ const currentAccountAddress = currentAccount.metadata.address;
+ const { mainChainNetwork } = useSettings('mainChainNetwork');
+ const chainID = currentApplication.chainID;
+ const customConfig = {
+ params: {
+ address: currentAccountAddress,
+ },
+ };
+ const serviceUrl = mainChainNetwork?.serviceUrl;
+ const config = useAuthConfig(customConfig);
+ const authData = queryClient.getQueryData([AUTH, chainID, config, serviceUrl]);
+ const onChainNonce = authData?.data?.nonce ? BigInt(authData?.data.nonce) : BigInt('0');
+ const authNonce = typeof onChainNonce === 'bigint' ? onChainNonce.toString() : onChainNonce;
+
+ const [accountNonce, setAccountNonce] = useState(onChainNonce.toString());
+ const currentAccountNonce = getNonceByAccount(currentAccountAddress);
+
+ // Store nonce by address in accounts store
+ const handleLocalNonce = (currentNonce) => {
+ const storedNonce = BigInt(currentAccountNonce || 0);
+ const localNonce = storedNonce < currentNonce ? currentNonce : storedNonce;
+ const localNonceStr = localNonce.toString();
+ setNonceByAccount(currentAccountAddress, localNonceStr, 'defaultNonce');
+
+ setAccountNonce(localNonceStr);
+ };
+
+ useEffect(() => {
+ handleLocalNonce(onChainNonce);
+ }, [onChainNonce]);
+
+ // Increment nonce after transaction signing
+ const incrementNonce = useCallback((transactionHex) => {
+ const localNonce = BigInt(Math.max(currentAccountNonce, Number(accountNonce))) + BigInt(1);
+ setNonceByAccount(currentAccountAddress, localNonce.toString(), transactionHex);
+ }, []);
+
+ const resetNonce = () => {
+ resetNonceByAccount(currentAccountAddress, authNonce);
+ };
+
+ return { accountNonce, onChainNonce: authNonce, incrementNonce, resetNonce };
+};
+
+export default useNonceSync;
diff --git a/src/modules/auth/hooks/useNonceSync.test.js b/src/modules/auth/hooks/useNonceSync.test.js
new file mode 100644
index 0000000000..2e50cf345f
--- /dev/null
+++ b/src/modules/auth/hooks/useNonceSync.test.js
@@ -0,0 +1,58 @@
+import { renderHook } from '@testing-library/react-hooks';
+import * as ReactQuery from '@tanstack/react-query';
+import { queryWrapper as wrapper } from 'src/utils/test/queryWrapper';
+import mockSavedAccounts from '@tests/fixtures/accounts';
+import { useAccounts } from 'src/modules/account/hooks';
+import { mockAuth } from '@auth/__fixtures__';
+import useNonceSync from './useNonceSync';
+
+const mockedCurrentAccount = mockSavedAccounts[0];
+const mockSetNonceByAccount = jest.fn();
+const mockModifiedMockAuth = { ...mockAuth, data: { ...mockAuth.data, nonce: '2' } };
+
+jest.mock('@account/hooks/useCurrentAccount', () => ({
+ useCurrentAccount: jest.fn(() => [mockedCurrentAccount, jest.fn()]),
+}));
+jest.mock('@account/hooks/useAccounts');
+jest.mock('@auth/hooks/queries/useAuth');
+
+describe('useNonceSync', () => {
+ it('renders properly', async () => {
+ jest
+ .spyOn(ReactQuery, 'useQueryClient')
+ .mockReturnValue({ getQueryData: () => mockModifiedMockAuth });
+ useAccounts.mockReturnValue({
+ accounts: mockedCurrentAccount,
+ setNonceByAccount: mockSetNonceByAccount,
+ getNonceByAccount: jest.fn().mockReturnValue(3),
+ });
+ const { result } = renderHook(() => useNonceSync(), { wrapper });
+ expect(result.current.accountNonce).toEqual('3');
+ result.current.incrementNonce();
+ expect(mockSetNonceByAccount).toHaveBeenCalled();
+ });
+
+ it('renders properly if auth nonce is undefined and no local nonce has been previously stored', () => {
+ jest.spyOn(ReactQuery, 'useQueryClient').mockReturnValue({ getQueryData: () => {} });
+ useAccounts.mockReturnValue({
+ accounts: mockedCurrentAccount,
+ setNonceByAccount: mockSetNonceByAccount,
+ getNonceByAccount: jest.fn().mockReturnValue(undefined),
+ });
+ const { result } = renderHook(() => useNonceSync(), { wrapper });
+ expect(result.current.accountNonce).toEqual('0');
+ });
+
+ it("updates local nonce if it's less than on-chain nonce", () => {
+ jest
+ .spyOn(ReactQuery, 'useQueryClient')
+ .mockReturnValue({ getQueryData: () => mockModifiedMockAuth });
+ useAccounts.mockReturnValue({
+ accounts: mockedCurrentAccount,
+ setNonceByAccount: mockSetNonceByAccount,
+ getNonceByAccount: jest.fn().mockReturnValue(1),
+ });
+ const { result } = renderHook(() => useNonceSync(), { wrapper });
+ expect(result.current.accountNonce).toEqual('2');
+ });
+});
diff --git a/src/modules/blockchainApplication/connection/components/RequestSignMessageDialog/index.test.js b/src/modules/blockchainApplication/connection/components/RequestSignMessageDialog/index.test.js
index e65ee93cd0..2d218ed95e 100644
--- a/src/modules/blockchainApplication/connection/components/RequestSignMessageDialog/index.test.js
+++ b/src/modules/blockchainApplication/connection/components/RequestSignMessageDialog/index.test.js
@@ -48,6 +48,8 @@ useCurrentAccount.mockReturnValue([mockCurrentAccount, mockSetCurrentAccount]);
useAccounts.mockReturnValue({
getAccountByAddress: () => mockSavedAccounts[0],
accounts: mockSavedAccounts,
+ setNonceByAccount: jest.fn(),
+ getNonceByAccount: () => 1,
});
useBlockchainApplicationMeta.mockReturnValue({
diff --git a/src/modules/legacy/components/Summary/summary.test.js b/src/modules/legacy/components/Summary/summary.test.js
index 60e93f272d..a56dd1e14e 100644
--- a/src/modules/legacy/components/Summary/summary.test.js
+++ b/src/modules/legacy/components/Summary/summary.test.js
@@ -20,6 +20,7 @@ const mockedCurrentAccount = mockSavedAccounts[0];
jest.mock('@auth/hooks/queries/useAuth');
jest.mock('@account/hooks', () => ({
+ ...jest.requireActual('@account/hooks'),
useCurrentAccount: jest.fn(() => [mockedCurrentAccount, jest.fn()]),
}));
diff --git a/src/modules/pos/validator/components/ClaimRewardsSummary/ClaimRewardsSummary.test.js b/src/modules/pos/validator/components/ClaimRewardsSummary/ClaimRewardsSummary.test.js
index a8658cce4b..47cd087650 100644
--- a/src/modules/pos/validator/components/ClaimRewardsSummary/ClaimRewardsSummary.test.js
+++ b/src/modules/pos/validator/components/ClaimRewardsSummary/ClaimRewardsSummary.test.js
@@ -1,5 +1,5 @@
-import React from 'react';
-import { render, screen } from '@testing-library/react';
+import { screen, fireEvent } from '@testing-library/react';
+import { smartRender } from 'src/utils/testHelpers';
import mockSavedAccounts from '@tests/fixtures/accounts';
import { useRewardsClaimable } from '@pos/reward/hooks/queries';
import wallets from '@tests/constants/wallets';
@@ -8,10 +8,10 @@ import { useAuth } from '@auth/hooks/queries';
import { mockAuth } from '@auth/__fixtures__';
import { mockAppsTokens } from '@token/fungible/__fixtures__';
import usePosToken from '@pos/validator/hooks/usePosToken';
-import { mount } from 'enzyme';
import ClaimRewardsSummary from './index';
jest.mock('@account/hooks', () => ({
+ ...jest.requireActual('@account/hooks'),
useCurrentAccount: jest.fn(() => [mockSavedAccounts[0], jest.fn()]),
}));
jest.mock('@auth/hooks/queries');
@@ -56,24 +56,25 @@ describe('ClaimRewardsSummary', () => {
useAuth.mockReturnValue({ data: mockAuth });
usePosToken.mockReturnValue({ token: mockAppsTokens.data[0] });
useRewardsClaimable.mockReturnValue({ data: mockRewardsClaimableWithToken });
+ const config = { queryClient: true /* renderType: 'mount' */ };
it('should display properly', async () => {
- render( );
+ smartRender(ClaimRewardsSummary, props, config);
expect(screen.getByText('Confirm')).toBeTruthy();
});
it('should go to prev page when click Go back button', () => {
- const wrapper = mount( );
+ smartRender(ClaimRewardsSummary, props, config);
expect(props.prevStep).not.toBeCalled();
- wrapper.find('button.cancel-button').simulate('click');
+ fireEvent.click(screen.getByAltText('arrowLeftTailed'));
expect(props.prevStep).toBeCalled();
});
it('should submit transaction and action function when click in confirm button', () => {
- const wrapper = mount( );
+ smartRender(ClaimRewardsSummary, props, config);
expect(props.nextStep).not.toBeCalled();
- wrapper.find('button.confirm-button').simulate('click');
+ fireEvent.click(screen.getByText('Confirm'));
expect(props.nextStep).toBeCalledWith({
actionFunction: props.claimedRewards,
formProps: props.formProps,
diff --git a/src/modules/pos/validator/components/RegisterValidatorSummary/RegisterValidatorSummary.test.js b/src/modules/pos/validator/components/RegisterValidatorSummary/RegisterValidatorSummary.test.js
index f62d85dd88..21812cab63 100644
--- a/src/modules/pos/validator/components/RegisterValidatorSummary/RegisterValidatorSummary.test.js
+++ b/src/modules/pos/validator/components/RegisterValidatorSummary/RegisterValidatorSummary.test.js
@@ -1,6 +1,4 @@
-import React from 'react';
-import { mount } from 'enzyme';
-import { mountWithRouterAndStore } from 'src/utils/testHelpers';
+import { smartRender } from 'src/utils/testHelpers';
import accounts from '@tests/constants/wallets';
import { useAuth } from '@auth/hooks/queries';
import mockSavedAccounts from '@tests/fixtures/accounts';
@@ -10,8 +8,10 @@ import Summary from './RegisterValidatorSummary';
const mockedCurrentAccount = mockSavedAccounts[0];
jest.mock('@auth/hooks/queries');
jest.mock('@account/hooks', () => ({
+ ...jest.requireActual('@account/hooks'),
useCurrentAccount: jest.fn(() => [mockedCurrentAccount, jest.fn()]),
}));
+const config = { renderType: 'mount', queryClient: true };
describe('Validator Registration Summary', () => {
const props = {
@@ -58,7 +58,7 @@ describe('Validator Registration Summary', () => {
useAuth.mockReturnValue({ data: mockAuth });
it('renders properly Summary component', () => {
- const wrapper = mount( );
+ const wrapper = smartRender(Summary, props, config).wrapper;
expect(wrapper).toContainMatchingElement('.username-label');
expect(wrapper).toContainMatchingElement('.username');
expect(wrapper).toContainMatchingElement('.address');
@@ -67,14 +67,14 @@ describe('Validator Registration Summary', () => {
});
it('go to prev page when click Go back button', () => {
- const wrapper = mount( );
+ const wrapper = smartRender(Summary, props, config).wrapper;
expect(props.prevStep).not.toBeCalled();
wrapper.find('button.cancel-button').simulate('click');
expect(props.prevStep).toBeCalled();
});
it('submit user data when click in confirm button', () => {
- const wrapper = mountWithRouterAndStore(Summary, props, {}, {});
+ const wrapper = smartRender(Summary, props, config).wrapper;
expect(props.nextStep).not.toBeCalled();
wrapper.find('button.confirm-button').simulate('click');
expect(props.nextStep).toBeCalledWith({
diff --git a/src/modules/pos/validator/components/StakeSummary/StakeSummary.test.js b/src/modules/pos/validator/components/StakeSummary/StakeSummary.test.js
index 5b97398c8b..fff01c6220 100644
--- a/src/modules/pos/validator/components/StakeSummary/StakeSummary.test.js
+++ b/src/modules/pos/validator/components/StakeSummary/StakeSummary.test.js
@@ -1,4 +1,4 @@
-import { mountWithRouter } from 'src/utils/testHelpers';
+import { mountWithRouterAndQueryClient } from 'src/utils/testHelpers';
import { useAuth } from '@auth/hooks/queries';
import { mockAuth } from 'src/modules/auth/__fixtures__';
import accounts from '@tests/constants/wallets';
@@ -11,6 +11,7 @@ const mockedCurrentAccount = mockSavedAccounts[0];
jest.mock('@auth/hooks/queries');
jest.mock('@account/hooks', () => ({
+ ...jest.requireActual('@account/hooks'),
useCurrentAccount: jest.fn(() => [mockedCurrentAccount, jest.fn()]),
}));
jest.mock('@pos/validator/hooks/usePosToken');
@@ -151,13 +152,13 @@ describe('StakingQueue.Summary', () => {
usePosToken.mockReturnValue({ token: mockAppsTokens.data[0] });
it('renders properly', () => {
- const wrapper = mountWithRouter(Summary, props);
+ const wrapper = mountWithRouterAndQueryClient(Summary, props);
expect(wrapper).toContainMatchingElement('StakeStats');
});
it('renders properly when only new stakes are present', () => {
- const wrapper = mountWithRouter(Summary, {
+ const wrapper = mountWithRouterAndQueryClient(Summary, {
...props,
added,
});
@@ -166,7 +167,7 @@ describe('StakingQueue.Summary', () => {
});
it('renders properly when only removed stakes are present', () => {
- const wrapper = mountWithRouter(Summary, {
+ const wrapper = mountWithRouterAndQueryClient(Summary, {
...props,
removed,
});
@@ -175,7 +176,7 @@ describe('StakingQueue.Summary', () => {
});
it('renders properly when only edited stakes are present', () => {
- const wrapper = mountWithRouter(Summary, {
+ const wrapper = mountWithRouterAndQueryClient(Summary, {
...props,
edited,
});
@@ -184,7 +185,7 @@ describe('StakingQueue.Summary', () => {
});
it('renders properly when a mixture of stakes are present', () => {
- const wrapper = mountWithRouter(Summary, {
+ const wrapper = mountWithRouterAndQueryClient(Summary, {
...props,
edited,
removed,
@@ -195,7 +196,7 @@ describe('StakingQueue.Summary', () => {
});
it('should render rewards', () => {
- const wrapper = mountWithRouter(Summary, { ...props, edited, removed, added });
+ const wrapper = mountWithRouterAndQueryClient(Summary, { ...props, edited, removed, added });
const addedItemList = wrapper.find('[data-testid="stake-item"]').at(0);
const editedItemList = wrapper.find('[data-testid="stake-item"]').at(4);
const removedItemList = wrapper.find('[data-testid="stake-item"]').at(9);
@@ -205,7 +206,7 @@ describe('StakingQueue.Summary', () => {
});
it('calls props.nextStep with properties when confirm button is clicked', () => {
- const wrapper = mountWithRouter(Summary, props);
+ const wrapper = mountWithRouterAndQueryClient(Summary, props);
wrapper.find('button.confirm-button').simulate('click');
expect(props.nextStep).toHaveBeenCalledWith({
@@ -221,7 +222,7 @@ describe('StakingQueue.Summary', () => {
});
it('calls props.nextStep when transaction is confirmed', () => {
- const wrapper = mountWithRouter(Summary, {
+ const wrapper = mountWithRouterAndQueryClient(Summary, {
...props,
added,
removed,
diff --git a/src/modules/pos/validator/components/UnlockBalanceSummary/unlockBalanceSummary.test.js b/src/modules/pos/validator/components/UnlockBalanceSummary/unlockBalanceSummary.test.js
index f803d873ad..b8bbccfa82 100644
--- a/src/modules/pos/validator/components/UnlockBalanceSummary/unlockBalanceSummary.test.js
+++ b/src/modules/pos/validator/components/UnlockBalanceSummary/unlockBalanceSummary.test.js
@@ -1,5 +1,4 @@
-import React from 'react';
-import { mount } from 'enzyme';
+import { smartRender } from 'src/utils/testHelpers';
import wallets from '@tests/constants/wallets';
import mockSavedAccounts from '@tests/fixtures/accounts';
import { useAuth } from '@auth/hooks/queries';
@@ -9,7 +8,12 @@ import { mockAuth } from 'src/modules/auth/__fixtures__';
import Summary from './UnlockBalanceSummary';
const mockedCurrentAccount = mockSavedAccounts[0];
+const config = {
+ queryClient: true,
+ renderType: 'mount',
+};
jest.mock('@account/hooks', () => ({
+ ...jest.requireActual('@account/hooks'),
useCurrentAccount: jest.fn(() => [mockedCurrentAccount, jest.fn()]),
}));
jest.mock('@auth/hooks/queries');
@@ -59,7 +63,7 @@ describe('Locked balance Summary', () => {
usePosToken.mockReturnValue({ token: mockAppsTokens.data[0] });
it('renders properly Summary component', () => {
- const wrapper = mount( );
+ const wrapper = smartRender(Summary, props, config).wrapper;
expect(wrapper).toContainMatchingElement('.address-label');
expect(wrapper).toContainMatchingElement('.amount-label');
expect(wrapper).toContainMatchingElement('button.confirm-button');
@@ -67,14 +71,14 @@ describe('Locked balance Summary', () => {
});
it('go to prev page when click Go back button', () => {
- const wrapper = mount( );
+ const wrapper = smartRender(Summary, props, config).wrapper;
expect(props.prevStep).not.toBeCalled();
wrapper.find('button.cancel-button').simulate('click');
expect(props.prevStep).toBeCalled();
});
it('submit transaction and action function when click in confirm button', () => {
- const wrapper = mount( );
+ const wrapper = smartRender(Summary, props, config).wrapper;
expect(props.nextStep).not.toBeCalled();
wrapper.find('button.confirm-button').simulate('click');
expect(props.nextStep).toBeCalledWith({
diff --git a/src/modules/token/fungible/components/SendSummary/Summary.js b/src/modules/token/fungible/components/SendSummary/Summary.js
index 5d82243b62..02698ab0d5 100644
--- a/src/modules/token/fungible/components/SendSummary/Summary.js
+++ b/src/modules/token/fungible/components/SendSummary/Summary.js
@@ -14,10 +14,11 @@ const Summary = ({
}) => {
const onConfirmAction = {
label: t('Send'),
- onClick: () => {
+ onClick: (modifiedTransactionJSON) => {
+ /* istanbul ignore next */
nextStep({
formProps,
- transactionJSON,
+ transactionJSON: modifiedTransactionJSON || transactionJSON,
selectedPriority,
actionFunction: tokensTransferred,
});
diff --git a/src/modules/token/fungible/components/SendSummary/summary.test.js b/src/modules/token/fungible/components/SendSummary/summary.test.js
index c7817bb5c8..c839cbead1 100644
--- a/src/modules/token/fungible/components/SendSummary/summary.test.js
+++ b/src/modules/token/fungible/components/SendSummary/summary.test.js
@@ -1,4 +1,4 @@
-import { mountWithRouter } from 'src/utils/testHelpers';
+import { smartRender } from 'src/utils/testHelpers';
import { tokenMap } from '@token/fungible/consts/tokens';
import mockBlockchainApplications from '@tests/fixtures/blockchainApplicationsManage';
import { mockAppTokens } from '@tests/fixtures/token';
@@ -15,8 +15,10 @@ import { mockAppsTokens } from '../../__fixtures__';
const mockedCurrentAccount = mockSavedAccounts[0];
jest.mock('@auth/hooks/queries');
jest.mock('@account/hooks', () => ({
+ ...jest.requireActual('@account/hooks'),
useCurrentAccount: jest.fn(() => [mockedCurrentAccount, jest.fn()]),
}));
+const config = { queryClient: true, renderType: 'mount' };
describe('Summary', () => {
let wrapper;
@@ -72,7 +74,7 @@ describe('Summary', () => {
},
},
};
- wrapper = mountWithRouter(Summary, { ...props });
+ wrapper = smartRender(Summary, props, config).wrapper;
});
useAuth.mockReturnValue({ data: mockAuth });
@@ -98,21 +100,25 @@ describe('Summary', () => {
});
it('should show props.fields.recipient.title if it is present', () => {
- wrapper = mountWithRouter(Summary, {
- ...{
- ...props,
- formProps: {
- ...props.formProps,
- params: {
- ...props.formProps.params,
- recipient: {
- ...props.formProps.params.recipient,
- title,
+ wrapper = smartRender(
+ Summary,
+ {
+ ...{
+ ...props,
+ formProps: {
+ ...props.formProps,
+ params: {
+ ...props.formProps.params,
+ recipient: {
+ ...props.formProps.params.recipient,
+ title,
+ },
},
},
},
},
- });
+ config
+ ).wrapper;
expect(wrapper.find('.recipient-address')).toIncludeText(
props.transactionJSON.params.recipientAddress
);
diff --git a/src/modules/transaction/components/TransactionInfo/TransactionInfo.css b/src/modules/transaction/components/TransactionInfo/TransactionInfo.css
index 13c7f434f6..8eb2893cdf 100644
--- a/src/modules/transaction/components/TransactionInfo/TransactionInfo.css
+++ b/src/modules/transaction/components/TransactionInfo/TransactionInfo.css
@@ -194,3 +194,13 @@
margin-left: 10px;
}
}
+
+.warning {
+ vertical-align: bottom;
+ margin-right: 5px;
+}
+
+.reset {
+ vertical-align: text-bottom;
+ cursor: pointer;
+}
diff --git a/src/modules/transaction/components/TransactionInfo/TransactionInfo.js b/src/modules/transaction/components/TransactionInfo/TransactionInfo.js
index 86954a39c3..97c489f238 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,9 @@ const TransactionInfo = ({
date,
token,
summaryInfo,
+ nonceWarning,
+ canResetNonce,
+ resetTxNonce,
}) => {
const isRegisterMultisignature =
joinModuleAndCommand(transactionJSON) === MODULE_COMMANDS_NAME_MAP.registerMultisignature;
@@ -71,7 +75,13 @@ const TransactionInfo = ({
{date || '-'}
- {t('Nonce')}
+
+ {t('Nonce')}{' '}
+ {nonceWarning && }
+ {canResetNonce && (
+
+ )}
+
{BigInt(transactionJSON.nonce).toString()}
diff --git a/src/modules/transaction/components/TxComposer/index.js b/src/modules/transaction/components/TxComposer/index.js
index 6599399ed5..d3d6ba9950 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: 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..25bf501a6d 100644
--- a/src/modules/transaction/components/TxSignatureCollector/TxSignatureCollector.js
+++ b/src/modules/transaction/components/TxSignatureCollector/TxSignatureCollector.js
@@ -1,5 +1,6 @@
import React, { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
+import { cryptography } from '@liskhq/lisk-client';
import { TertiaryButton } from '@theme/buttons';
import { useCommandSchema } from '@network/hooks';
import Icon from '@theme/Icon';
@@ -7,9 +8,10 @@ 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';
+import { joinModuleAndCommand, fromTransactionJSON, encodeTransaction } from '../../utils';
import { MODULE_COMMANDS_NAME_MAP } from '../../configuration/moduleCommand';
import useTxInitiatorAccount from '../../hooks/useTxInitiatorAccount';
@@ -50,6 +52,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 +107,16 @@ const TxSignatureCollector = ({
});
};
- const onEnterPasswordSuccess = ({ privateKey }) =>
+ const onEnterPasswordSuccess = ({ privateKey }) => {
+ const paramsSchema = moduleCommandSchemas[moduleCommand];
+ const transaction = fromTransactionJSON(transactionJSON, paramsSchema);
+ const buffer = encodeTransaction(transaction, paramsSchema);
+ const transactionHex = cryptography.utils.hash(buffer).toString('hex');
+ if (isTransactionAuthor) {
+ incrementNonce(transactionHex);
+ }
txVerification(privateKey, currentAccount?.metadata.pubkey);
+ };
useEffect(() => {
if (!isEmpty(transactions.signedTransaction)) {
diff --git a/src/modules/transaction/components/TxSignatureCollector/TxSignatureCollector.test.js b/src/modules/transaction/components/TxSignatureCollector/TxSignatureCollector.test.js
index 6d6028cf19..065b537014 100644
--- a/src/modules/transaction/components/TxSignatureCollector/TxSignatureCollector.test.js
+++ b/src/modules/transaction/components/TxSignatureCollector/TxSignatureCollector.test.js
@@ -1,7 +1,6 @@
-import React from 'react';
-import { cryptography } from '@liskhq/lisk-client';
-import { screen, render, fireEvent, waitFor } from '@testing-library/react';
-import { renderWithStore, renderWithRouterAndStore } from 'src/utils/testHelpers';
+import { cryptography, codec } from '@liskhq/lisk-client';
+import { screen, fireEvent, waitFor } from '@testing-library/react';
+import { smartRender } from 'src/utils/testHelpers';
import { useCurrentAccount } from 'src/modules/account/hooks';
import mockSavedAccounts from '@tests/fixtures/accounts';
import { useCommandSchema } from '@network/hooks/useCommandsSchema';
@@ -90,9 +89,18 @@ jest.mock('@account/hooks', () => ({
useCurrentAccount: jest.fn(() => [mockCurrentAccount, mockSetCurrentAccount]),
useAccounts: jest.fn(() => ({
getAccountByAddress: jest.fn().mockReturnValue(mockCurrentAccount),
+ getNonceByAccount: jest.fn().mockReturnValue(2),
+ setNonceByAccount: jest.fn(),
})),
}));
jest.spyOn(cryptography.address, 'getLisk32AddressFromPublicKey').mockReturnValue(address);
+jest.spyOn(codec.codec, 'encode');
+
+const config = {
+ queryClient: true,
+ store: true,
+ storeInfo: mockAppState,
+};
beforeAll(() => {
window.Worker = WorkerMock;
@@ -133,6 +141,7 @@ describe('TxSignatureCollector', () => {
nextStep: jest.fn(),
statusInfo: {},
};
+ const txHex = 'a24f94966cf213deb90854c41cf1f27906135b7001a49e53a9722ebf5fc67481';
useCommandSchema.mockReturnValue({
moduleCommandSchemas: mockCommandParametersSchemas.data.commands.reduce(
@@ -145,6 +154,7 @@ describe('TxSignatureCollector', () => {
txInitiatorAccount: { ...mockAuth.data, ...mockAuth.meta, keys: { ...mockAuth.data } },
isLoading: false,
});
+ codec.codec.encode.mockReturnValue(txHex);
beforeEach(() => {
jest.clearAllMocks();
@@ -152,7 +162,7 @@ describe('TxSignatureCollector', () => {
});
it('should render password input fit not used with HW', () => {
- renderWithStore(TxSignatureCollector, props, mockAppState);
+ smartRender(TxSignatureCollector, props, config);
expect(screen.getByText('Enter your account password')).toBeInTheDocument();
expect(
screen.getByText('Please enter your account password to sign this transaction.')
@@ -180,8 +190,9 @@ describe('TxSignatureCollector', () => {
isHW: true,
},
};
+ const updatedConfig = { ...config, storeInfo: mockDisconnectedAppState };
useCurrentAccount.mockReturnValue([mockHWAcct, mockSetCurrentAccount]);
- renderWithRouterAndStore(TxSignatureCollector, props, mockDisconnectedAppState);
+ smartRender(TxSignatureCollector, props, updatedConfig);
expect(screen.getByText('Reconnect to hardware wallet')).toBeInTheDocument();
});
@@ -199,8 +210,9 @@ describe('TxSignatureCollector', () => {
isHW: true,
},
};
+ const updatedConfig = { ...config, storeInfo: mockConnectedAppState };
useCurrentAccount.mockReturnValue([mockHWAcct, mockSetCurrentAccount]);
- renderWithRouterAndStore(TxSignatureCollector, props, mockConnectedAppState);
+ smartRender(TxSignatureCollector, props, updatedConfig);
expect(
screen.getByText('Please confirm the transaction on your Ledger S Plus')
).toBeInTheDocument();
@@ -230,8 +242,9 @@ describe('TxSignatureCollector', () => {
isHW: true,
},
};
+ const updatedConfig = { ...config, storeInfo: mockConnectedAppState };
useCurrentAccount.mockReturnValue([mockHWAcct, mockSetCurrentAccount]);
- renderWithStore(TxSignatureCollector, props, mockConnectedAppState);
+ smartRender(TxSignatureCollector, props, updatedConfig);
expect(props.actionFunction).toHaveBeenCalled();
});
@@ -244,7 +257,7 @@ describe('TxSignatureCollector', () => {
command: 'registerMultisignature',
},
};
- renderWithStore(TxSignatureCollector, formProps, mockAppState);
+ smartRender(TxSignatureCollector, formProps, config);
fireEvent.change(screen.getByPlaceholderText('Enter password'), {
target: { value: 'DeykUBjUn7uZHYv!' },
});
@@ -255,7 +268,7 @@ describe('TxSignatureCollector', () => {
});
it('should call action function on continue button click', async () => {
- renderWithStore(TxSignatureCollector, props, mockAppState);
+ smartRender(TxSignatureCollector, props, config);
fireEvent.change(screen.getByPlaceholderText('Enter password'), {
target: { value: 'DeykUBjUn7uZHYv!' },
});
@@ -269,7 +282,7 @@ describe('TxSignatureCollector', () => {
it('should call action function if no keys', async () => {
useAuth.mockReturnValue({ isLoading: false });
- renderWithStore(TxSignatureCollector, props, mockAppState);
+ smartRender(TxSignatureCollector, props, config);
fireEvent.change(screen.getByPlaceholderText('Enter password'), {
target: { value: 'DeykUBjUn7uZHYv!' },
});
@@ -280,7 +293,7 @@ describe('TxSignatureCollector', () => {
});
it('should call action function on continue button click', async () => {
- renderWithStore(TxSignatureCollector, props, mockAppState);
+ smartRender(TxSignatureCollector, props, config);
fireEvent.change(screen.getByPlaceholderText('Enter password'), {
target: { value: 'DeykUBjUn7uZHYv!' },
});
@@ -298,7 +311,7 @@ describe('TxSignatureCollector', () => {
txSignatureError: 'error',
},
};
- render( );
+ smartRender(TxSignatureCollector, errorProps, config);
expect(props.nextStep).toHaveBeenCalled();
});
@@ -310,7 +323,7 @@ describe('TxSignatureCollector', () => {
signedTransaction: { id: '123', signatures: [] },
},
};
- renderWithStore(TxSignatureCollector, signedTransactionProps, mockAppState);
+ smartRender(TxSignatureCollector, signedTransactionProps, config);
expect(props.nextStep).toHaveBeenCalled();
});
});
diff --git a/src/modules/transaction/components/TxSummarizer/index.js b/src/modules/transaction/components/TxSummarizer/index.js
index f55bd5435c..09b5a4ba1b 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 React, { useState } from 'react';
+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,max-statements
const TxSummarizer = ({
title,
children,
@@ -32,6 +34,22 @@ const TxSummarizer = ({
hasNoTopCancelButton,
noFeeStatus,
}) => {
+ const [modifiedTransactionJSON, setModifiedTransactionJSON] = useState(transactionJSON);
+ const { onChainNonce, resetNonce } = useNonceSync();
+ const isTransactionAuthor = transactionJSON.senderPublicKey === wallet.summary.publicKey;
+ const isNonceEqual = modifiedTransactionJSON.nonce === onChainNonce;
+ const nonceWarning = isTransactionAuthor && !isNonceEqual;
+ const canResetNonce = nonceWarning && !transactionJSON.signatures?.length;
+
+ /* istanbul ignore next */
+ const resetTxNonce = () => {
+ setModifiedTransactionJSON({
+ ...modifiedTransactionJSON,
+ nonce: onChainNonce,
+ });
+ resetNonce();
+ };
+
const fee = !(
wallet.summary.isMultisignature ||
formProps.moduleCommand === MODULE_COMMANDS_NAME_MAP.registerMultisignature
@@ -47,6 +65,11 @@ const TxSummarizer = ({
),
};
+ const modifiedConfirmButton = {
+ ...confirmButton,
+ onClick: () => confirmButton.onClick(modifiedTransactionJSON),
+ };
+
return (
{title && (
@@ -72,9 +95,12 @@ const TxSummarizer = ({
token={token}
summaryInfo={summaryInfo}
formProps={formProps}
- transactionJSON={transactionJSON}
+ transactionJSON={modifiedTransactionJSON}
account={wallet}
isMultisignature={wallet.summary.isMultisignature}
+ nonceWarning={nonceWarning}
+ canResetNonce={canResetNonce}
+ resetTxNonce={resetTxNonce}
/>
{!noFeeStatus && !!fee && (
@@ -100,10 +126,20 @@ const TxSummarizer = ({
)}
+ {nonceWarning && (
+
+
+
+ {t(
+ '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.'
+ )}
+
+
+ )}
({
+ ...jest.requireActual('@account/hooks'),
useCurrentAccount: jest.fn(() => [mockedCurrentAccount, jest.fn()]),
}));
+jest.mock('src/modules/auth/hooks/useNonceSync');
describe('TxSummarizer', () => {
let props;
@@ -65,8 +67,8 @@ describe('TxSummarizer', () => {
fee: BigInt(141000),
module: 'token',
command: 'transfer',
- senderPublicKey: convertStringToBinary(wallets.genesis.summary.publicKey),
- nonce: BigInt(2),
+ senderPublicKey: wallets.genesis.summary.publicKey,
+ nonce: '2',
params: {
recipientAddress: wallets.genesis.summary.address,
amount: BigInt(112300000),
@@ -77,14 +79,17 @@ describe('TxSummarizer', () => {
});
useAuth.mockReturnValue({ data: mockAuth });
useTokenBalances.mockReturnValue({ data: mockAppsTokens.data[0] });
+ const config = { queryClient: true, renderType: 'mount' };
it('should render title', () => {
- const wrapper = mountWithRouter(TxSummarizer, props);
+ useNonceSync.mockReturnValue({ onChainNonce: '2' });
+ const wrapper = smartRender(TxSummarizer, props, config).wrapper;
expect(wrapper.find('h2').text()).toEqual(props.title);
});
it('should call action functions of each button', () => {
- const wrapper = mountWithRouter(TxSummarizer, props);
+ useNonceSync.mockReturnValue({ onChainNonce: '2' });
+ const wrapper = smartRender(TxSummarizer, props, config).wrapper;
wrapper.find('.confirm-button').at(0).simulate('click');
expect(props.confirmButton.onClick).toHaveBeenCalled();
wrapper.find('.cancel-button').at(0).simulate('click');
@@ -92,7 +97,8 @@ describe('TxSummarizer', () => {
});
it('should display HW illustration', () => {
- let wrapper = mountWithRouter(TxSummarizer, props);
+ useNonceSync.mockReturnValue({ onChainNonce: '2' });
+ let wrapper = smartRender(TxSummarizer, props, config).wrapper;
const newProps = {
...props,
wallet: {
@@ -101,19 +107,20 @@ describe('TxSummarizer', () => {
},
};
expect(wrapper.find('.illustration')).not.toExist();
- wrapper = mountWithRouter(TxSummarizer, newProps);
+ wrapper = smartRender(TxSummarizer, newProps, config).wrapper;
expect(wrapper.find('.illustration')).toExist();
});
it('should mount its children', () => {
const mountProps = { ...props, children: };
- const wrapper = mountWithRouter(TxSummarizer, mountProps);
+ const wrapper = smartRender(TxSummarizer, mountProps, config).wrapper;
expect(wrapper.find('.child-span')).toExist();
});
it('should display tx fee for regular account', () => {
// Regular account
- const wrapper = mountWithRouter(TxSummarizer, props);
+ useNonceSync.mockReturnValue({ onChainNonce: '2' });
+ const wrapper = smartRender(TxSummarizer, props, config).wrapper;
expect(wrapper.find('.fee-value-test')).toExist();
expect(wrapper.find('.fee-value-test').text()).toContain('1 LSK');
@@ -157,11 +164,50 @@ describe('TxSummarizer', () => {
},
},
};
+ useNonceSync.mockReturnValue({ onChainNonce: '2' });
- const wrapper = mountWithRouter(TxSummarizer, multisigProps);
+ const wrapper = smartRender(TxSummarizer, multisigProps, config).wrapper;
expect(wrapper.find('.info-numberOfSignatures').at(0).text()).toEqual('Required signatures2');
expect(wrapper.find('.member-info').at(0).find('p span').text()).toEqual('(Mandatory)');
expect(wrapper.find('.member-info').at(1).find('p span').text()).toEqual('(Optional)');
expect(wrapper.find('.member-info').at(2).find('p span').text()).toEqual('(Optional)');
});
+
+ it('should display transaction nonce warning if nonce is higher than auth nonce', () => {
+ const multisigProps = {
+ ...props,
+ formProps: {
+ isValid: true,
+ moduleCommand: MODULE_COMMANDS_NAME_MAP.transfer,
+ composedFees: [
+ {
+ label: 'TransactionFee',
+ value: '0.0023 LSK',
+ },
+ ],
+ fields: {
+ token: mockTokensBalance.data[0],
+ sendingChain: mockBlockchainApplications[0],
+ recipientChain: { ...blockchainApplicationsExplore[0], logo: { png: '', svg: '' } },
+ },
+ },
+ transactionJSON: {
+ ...props.transactionJSON,
+ signatures: [],
+ moduleCommand: MODULE_COMMANDS_NAME_MAP.transfer,
+ params: {
+ mandatoryKeys: [wallets.genesis.summary.publicKey],
+ optionalKeys: [wallets.validator.summary.publicKey, wallets.multiSig.summary.publicKey],
+ numberOfSignatures: 2,
+ },
+ },
+ };
+ useNonceSync.mockReturnValue({ onChainNonce: '4' });
+
+ const wrapper = smartRender(TxSummarizer, multisigProps, config).wrapper;
+ expect(wrapper.find('img.warning')).toBeTruthy();
+ expect(wrapper.find('.nonceWarning').text()).toEqual(
+ '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.'
+ );
+ });
});
diff --git a/src/modules/transaction/components/TxSummarizer/txSummarizer.css b/src/modules/transaction/components/TxSummarizer/txSummarizer.css
index 5473285988..2bceb096df 100644
--- a/src/modules/transaction/components/TxSummarizer/txSummarizer.css
+++ b/src/modules/transaction/components/TxSummarizer/txSummarizer.css
@@ -221,3 +221,30 @@
margin-top: 16px;
}
}
+
+.nonceWarning {
+ display: flex;
+ flex-direction: row;
+ padding: 8px;
+ background-color: var(--color-warning-bg);
+ border: 1px solid var(--color-warning-border);
+ border-radius: 5px;
+ margin-bottom: -20px;
+
+ & > p {
+ @mixin contentNormal;
+
+ margin: 0;
+ font-size: 14px;
+ line-height: 18px;
+ color: var(--color-constant-zodiac-blue);
+ text-align: left;
+ }
+
+ & > img {
+ align-self: flex-start;
+ margin-right: 10px;
+ width: 20px;
+ height: 16px;
+ }
+}
diff --git a/src/modules/wallet/components/RegisterMultisigForm/index.test.js b/src/modules/wallet/components/RegisterMultisigForm/index.test.js
index a35567d779..d75a461cfc 100644
--- a/src/modules/wallet/components/RegisterMultisigForm/index.test.js
+++ b/src/modules/wallet/components/RegisterMultisigForm/index.test.js
@@ -41,6 +41,7 @@ const mockEstimateFeeResponse = {
};
jest.mock('@account/hooks', () => ({
+ ...jest.requireActual('@account/hooks'),
useCurrentAccount: jest.fn(() => [mockCurrentAccount]),
}));
jest.mock('@transaction/hooks/queries/useTransactionEstimateFees');
diff --git a/src/modules/wallet/components/RegisterMultisigSummary/summary.test.js b/src/modules/wallet/components/RegisterMultisigSummary/summary.test.js
index 99548c465a..1c31bd172c 100644
--- a/src/modules/wallet/components/RegisterMultisigSummary/summary.test.js
+++ b/src/modules/wallet/components/RegisterMultisigSummary/summary.test.js
@@ -17,6 +17,7 @@ const mockedCurrentAccount = mockSavedAccounts[0];
jest.mock('@auth/hooks/queries');
jest.mock('@network/hooks/useCommandsSchema');
jest.mock('@account/hooks', () => ({
+ ...jest.requireActual('@account/hooks'),
useCurrentAccount: jest.fn(() => [mockedCurrentAccount, jest.fn()]),
}));
@@ -100,6 +101,8 @@ describe('Multisignature Summary component', () => {
},
};
+ const config = { queryClient: true };
+
beforeEach(() => {
hwManager.signTransactionByHW.mockResolvedValue({});
});
@@ -114,7 +117,7 @@ describe('Multisignature Summary component', () => {
});
it('Should call props.nextStep', async () => {
- smartRender(Summary, props);
+ smartRender(Summary, props, config);
await waitFor(() => {
fireEvent.click(screen.getByText('Sign'));
});
@@ -130,7 +133,7 @@ describe('Multisignature Summary component', () => {
});
it('Should call props.prevStep', async () => {
- smartRender(Summary, props);
+ smartRender(Summary, props, config);
await waitFor(() => {
fireEvent.click(screen.getByText('Edit'));
});
@@ -138,7 +141,7 @@ describe('Multisignature Summary component', () => {
});
it('Should render properly', () => {
- smartRender(Summary, props);
+ smartRender(Summary, props, config);
expect(screen.queryAllByTestId('member-info').length).toEqual(
props.transactionJSON.params.mandatoryKeys.length +
props.transactionJSON.params.optionalKeys.length
@@ -147,7 +150,11 @@ describe('Multisignature Summary component', () => {
});
it('Should be in edit mode', () => {
- smartRender(Summary, { ...props, authQuery: { data: { data: { numberOfSignatures: 3 } } } });
+ smartRender(
+ Summary,
+ { ...props, authQuery: { data: { data: { numberOfSignatures: 3 } } } },
+ config
+ );
expect(screen.getByText('Edit multisignature account')).toBeTruthy();
});
@@ -161,7 +168,7 @@ describe('Multisignature Summary component', () => {
signedTransaction: {},
},
};
- smartRender(Summary, newProps);
+ smartRender(Summary, newProps, config);
expect(props.nextStep).not.toHaveBeenCalledWith();
});
});
diff --git a/src/modules/wallet/components/overview/overview.test.js b/src/modules/wallet/components/overview/overview.test.js
index 037bc54164..84ff2a793f 100644
--- a/src/modules/wallet/components/overview/overview.test.js
+++ b/src/modules/wallet/components/overview/overview.test.js
@@ -1,6 +1,7 @@
import React from 'react';
import { MemoryRouter } from 'react-router';
import { render, screen } from '@testing-library/react';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import numeral from 'numeral';
import mockSavedAccounts from '@tests/fixtures/accounts';
import { useTokenBalances } from '@token/fungible/hooks/queries';
@@ -62,12 +63,14 @@ describe('Overview', () => {
useValidators.mockReturnValue({ data: mockValidators });
useBlocks.mockReturnValue({ data: mockBlocks });
useLatestBlock.mockReturnValue({ data: mockBlocks.data[0] });
-
+ const queryClient = new QueryClient();
render(
-
-
-
-
+
+
+
+
+
+
);
expect(screen.getByText('Request')).toBeTruthy();
diff --git a/src/theme/Icon/index.js b/src/theme/Icon/index.js
index efe5bbfd56..45d8772398 100644
--- a/src/theme/Icon/index.js
+++ b/src/theme/Icon/index.js
@@ -202,6 +202,7 @@ import liskIcon from '@setup/react/assets/images/icons/lisk-icon.svg';
import initialiseIcon from '@setup/react/assets/images/icons/initialise-icon.svg';
import initialiseRegistration from '@setup/react/assets/images/icons/initialise-registration.svg';
import warningYellow from '@setup/react/assets/images/icons/warning-yellow.svg';
+import reset from '@setup/react/assets/images/icons/reset.svg';
import linkIcon from '@setup/react/assets/images/icons/link-icon.svg';
import refresh from '@setup/react/assets/images/icons/refresh.svg';
import refreshActive from '@setup/react/assets/images/icons/refresh-active.svg';
@@ -427,6 +428,7 @@ export const icons = {
initialiseIcon,
initialiseRegistration,
warningYellow,
+ reset,
linkIcon,
arrowRightWithStroke,
arrowRightWithStrokeDark,