Skip to content

Commit

Permalink
Merge pull request #5390 from LiskHQ/5317-enable-creating-future-tran…
Browse files Browse the repository at this point in the history
…sactions

Implement creating future transactions
  • Loading branch information
ikem-legend authored Oct 25, 2023
2 parents f43a4c7 + 9e3a707 commit 2d292c5
Show file tree
Hide file tree
Showing 31 changed files with 536 additions and 104 deletions.
3 changes: 3 additions & 0 deletions setup/react/assets/images/icons/reset.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
21 changes: 19 additions & 2 deletions src/modules/account/hooks/useAccounts.js
Original file line number Diff line number Diff line change
@@ -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)), []);

Expand All @@ -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,
};
}
34 changes: 34 additions & 0 deletions src/modules/account/hooks/useAccounts.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => ({
Expand Down Expand Up @@ -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);
});
});
13 changes: 13 additions & 0 deletions src/modules/account/store/action.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions src/modules/account/store/actionTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
};
Expand Down
29 changes: 27 additions & 2 deletions src/modules/account/store/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
39 changes: 38 additions & 1 deletion src/modules/account/store/reducer.test.js
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand Down Expand Up @@ -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);
});
});
1 change: 1 addition & 0 deletions src/modules/account/store/selectors.js
Original file line number Diff line number Diff line change
@@ -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 || {};
59 changes: 59 additions & 0 deletions src/modules/auth/hooks/useNonceSync.js
Original file line number Diff line number Diff line change
@@ -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;
58 changes: 58 additions & 0 deletions src/modules/auth/hooks/useNonceSync.test.js
Original file line number Diff line number Diff line change
@@ -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');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ useCurrentAccount.mockReturnValue([mockCurrentAccount, mockSetCurrentAccount]);
useAccounts.mockReturnValue({
getAccountByAddress: () => mockSavedAccounts[0],
accounts: mockSavedAccounts,
setNonceByAccount: jest.fn(),
getNonceByAccount: () => 1,
});

useBlockchainApplicationMeta.mockReturnValue({
Expand Down
1 change: 1 addition & 0 deletions src/modules/legacy/components/Summary/summary.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()]),
}));

Expand Down
Loading

0 comments on commit 2d292c5

Please sign in to comment.