From a84bd29c8ca7601cf0f3f7c05c3d3753c75e4c80 Mon Sep 17 00:00:00 2001 From: bgptr <52497040+bgptr@users.noreply.github.com> Date: Thu, 6 Apr 2023 14:33:35 +0200 Subject: [PATCH] [launcher] speed up the setup wallet process (#3846) + remove gettingVSPInfo step from the setup view process + fix loading status on process manages/unmanaged views + cleanup process managed and unmanaged tickets + add tests --- app/actions/ClientActions.js | 2 +- app/actions/VSPActions.js | 86 ++-- .../ProcessManagedTickets.jsx | 67 ++-- .../ProcessManagedTickets/index.js | 1 + .../ProcessUnmanagedTickets.jsx | 85 ++-- .../ProcessUnmanagedTickets.module.css | 10 +- .../ProcessUnmanagedTickets/hooks.js | 31 ++ .../ProcessUnmanagedTickets/index.js | 1 + .../views/GetStartedPage/SetupWallet/hooks.js | 78 +--- app/stateMachines/SetupWalletConfigMachine.js | 6 - test/unit/actions/VSPActions.spec.js | 6 +- .../SetupWallet/ProcessManagedTickets.spec.js | 374 +++++++++++++++++ .../ProcessUnmanagedTickets.spec.js | 375 ++++++++++++++++++ 13 files changed, 913 insertions(+), 209 deletions(-) create mode 100644 app/components/views/GetStartedPage/SetupWallet/ProcessManagedTickets/index.js create mode 100644 app/components/views/GetStartedPage/SetupWallet/ProcessUnmanagedTickets/hooks.js create mode 100644 app/components/views/GetStartedPage/SetupWallet/ProcessUnmanagedTickets/index.js create mode 100644 test/unit/components/views/GetStaredPage/SetupWallet/ProcessManagedTickets.spec.js create mode 100644 test/unit/components/views/GetStaredPage/SetupWallet/ProcessUnmanagedTickets.spec.js diff --git a/app/actions/ClientActions.js b/app/actions/ClientActions.js index 9267354b00..6ee969c349 100644 --- a/app/actions/ClientActions.js +++ b/app/actions/ClientActions.js @@ -81,7 +81,7 @@ const startWalletServicesTrigger = () => (dispatch, getState) => if (privacyEnabled) { dispatch(getAccountMixerServiceAttempt()); } - dispatch(discoverAvailableVSPs()); + await dispatch(discoverAvailableVSPs()); await dispatch(getNextAddressAttempt(0)); await dispatch(getPeerInfo()); await dispatch(getTicketPriceAttempt()); diff --git a/app/actions/VSPActions.js b/app/actions/VSPActions.js index ad9b2054d3..de1e289cb6 100644 --- a/app/actions/VSPActions.js +++ b/app/actions/VSPActions.js @@ -294,43 +294,46 @@ export const GETVSPSPUBKEYS_SUCCESS = "GETVSPSPUBKEYS_SUCCESS"; export const GETVSPSPUBKEYS_FAILED = "GETVSPSPUBKEYS_FAILED"; // getVSPsPubkeys gets all available vsps and its pubkeys, so we can store it. -export const getVSPsPubkeys = () => async (dispatch) => { - try { +export const getVSPsPubkeys = () => (dispatch) => + new Promise((resolve, reject) => { dispatch({ type: GETVSPSPUBKEYS_ATTEMPT }); - const vsps = await dispatch(discoverAvailableVSPs()); - if (!isArray(vsps)) { - throw new Error("INVALID_VSPS"); - } - await Promise.all( - vsps.map((vsp) => { - return new Promise((resolve) => - dispatch(getVSPInfo(vsp.host)) - .then(({ pubkey }) => { - if (pubkey) { - vsp.pubkey = pubkey; - resolve(vsp); - } else { - resolve(null); - } - }) - .catch(() => { - resolve(null); - // Skip to the next vsp. - }) - ); - }) - ).then((result) => { - const availableVSPsPubkeys = result.filter((vsp) => !!vsp?.pubkey); - dispatch({ - type: GETVSPSPUBKEYS_SUCCESS, - availableVSPsPubkeys + dispatch(discoverAvailableVSPs()).then((vsps) => { + if (!isArray(vsps)) { + dispatch({ + type: GETVSPSPUBKEYS_FAILED, + error: new Error("INVALID_VSPS") + }); + return reject("INVALID_VSPS"); + } + + Promise.all( + vsps.map((vsp) => { + return new Promise((resolveVspInfo) => + dispatch(getVSPInfo(vsp.host)) + .then(({ pubkey }) => { + if (pubkey) { + vsp.pubkey = pubkey; + resolveVspInfo(vsp); + } else { + resolveVspInfo(null); + } + }) + .catch(() => { + resolveVspInfo(null); + // Skip to the next vsp. + }) + ); + }) + ).then((result) => { + const availableVSPsPubkeys = result.filter((vsp) => !!vsp?.pubkey); + dispatch({ + type: GETVSPSPUBKEYS_SUCCESS, + availableVSPsPubkeys + }); + resolve(availableVSPsPubkeys); }); - return availableVSPsPubkeys; }); - } catch (error) { - dispatch({ type: GETVSPSPUBKEYS_FAILED, error }); - } -}; + }); export const PROCESSMANAGEDTICKETS_ATTEMPT = "PROCESSMANAGEDTICKETS_ATTEMPT"; export const PROCESSMANAGEDTICKETS_SUCCESS = "PROCESSMANAGEDTICKETS_SUCCESS"; @@ -340,14 +343,14 @@ export const PROCESSMANAGEDTICKETS_FAILED = "PROCESSMANAGEDTICKETS_FAILED"; // synced, and sync them. export const processManagedTickets = (passphrase) => async (dispatch, getState) => { + dispatch({ type: PROCESSMANAGEDTICKETS_ATTEMPT }); const walletService = sel.walletService(getState()); let availableVSPsPubkeys = sel.getAvailableVSPsPubkeys(getState()); - if (!availableVSPsPubkeys) { - availableVSPsPubkeys = await dispatch(getVSPsPubkeys()); - } try { - dispatch({ type: PROCESSMANAGEDTICKETS_ATTEMPT }); + if (!availableVSPsPubkeys) { + availableVSPsPubkeys = await dispatch(getVSPsPubkeys()); + } let feeAccount, changeAccount; const mixedAccount = sel.getMixedAccount(getState()); if (mixedAccount) { @@ -386,13 +389,6 @@ export const processManagedTickets = dispatch({ type: PROCESSMANAGEDTICKETS_SUCCESS }); } catch (error) { dispatch({ type: PROCESSMANAGEDTICKETS_FAILED, error }); - if ( - String(error).indexOf( - "wallet.Unlock: invalid passphrase:: secretkey.DeriveKey" - ) > 0 - ) { - throw "Invalid private passphrase, please try again."; - } throw error; } }; diff --git a/app/components/views/GetStartedPage/SetupWallet/ProcessManagedTickets/ProcessManagedTickets.jsx b/app/components/views/GetStartedPage/SetupWallet/ProcessManagedTickets/ProcessManagedTickets.jsx index 177a1c1b69..43a4e62a80 100644 --- a/app/components/views/GetStartedPage/SetupWallet/ProcessManagedTickets/ProcessManagedTickets.jsx +++ b/app/components/views/GetStartedPage/SetupWallet/ProcessManagedTickets/ProcessManagedTickets.jsx @@ -1,26 +1,15 @@ -import { useState, useEffect } from "react"; import { Subtitle } from "shared"; import { FormattedMessage as T } from "react-intl"; import { PassphraseModalButton, InvisibleButton } from "buttons"; import styles from "./ProcessUnmanagedTickets.module.css"; -import { VSPSelect } from "inputs"; +import { useDaemonStartup } from "hooks"; -export default ({ - cancel, - send, - onProcessTickets, - title, - description, - noVspSelection, - error, - isProcessingManaged -}) => { - const [isValid, setIsValid] = useState(false); - const [vsp, setVSP] = useState(null); +export default ({ cancel, send, error }) => { + const { onProcessManagedTickets, isProcessingManaged } = useDaemonStartup(); const onSubmitContinue = (passphrase) => { // send a continue so we can go to the loading state - onProcessTickets(passphrase) + onProcessManagedTickets(passphrase) .then(() => send({ type: "CONTINUE" })) .catch((error) => { send({ type: "ERROR", error }); @@ -28,23 +17,28 @@ export default ({ return; }; - useEffect(() => { - if (noVspSelection) { - setIsValid(true); - return; - } - if (vsp) { - setIsValid(true); - } - }, [vsp, noVspSelection]); - return (
- -
{description}
- {!noVspSelection && ( - - )} + + } + /> +
+ { + + } +
{error &&
{error}
}
} - disabled={!isValid || isProcessingManaged} + disabled={isProcessingManaged} loading={isProcessingManaged} /> - {!isProcessingManaged && ( - - - - )} + + +
); diff --git a/app/components/views/GetStartedPage/SetupWallet/ProcessManagedTickets/index.js b/app/components/views/GetStartedPage/SetupWallet/ProcessManagedTickets/index.js new file mode 100644 index 0000000000..3cf044f124 --- /dev/null +++ b/app/components/views/GetStartedPage/SetupWallet/ProcessManagedTickets/index.js @@ -0,0 +1 @@ +export { default } from "./ProcessManagedTickets"; diff --git a/app/components/views/GetStartedPage/SetupWallet/ProcessUnmanagedTickets/ProcessUnmanagedTickets.jsx b/app/components/views/GetStartedPage/SetupWallet/ProcessUnmanagedTickets/ProcessUnmanagedTickets.jsx index 9706ef6c7d..4cb171e1b8 100644 --- a/app/components/views/GetStartedPage/SetupWallet/ProcessUnmanagedTickets/ProcessUnmanagedTickets.jsx +++ b/app/components/views/GetStartedPage/SetupWallet/ProcessUnmanagedTickets/ProcessUnmanagedTickets.jsx @@ -1,52 +1,44 @@ -import { useState, useEffect } from "react"; import { Subtitle } from "shared"; import { FormattedMessage as T } from "react-intl"; import { PassphraseModalButton, InvisibleButton } from "buttons"; import styles from "./ProcessUnmanagedTickets.module.css"; import { VSPSelect } from "inputs"; +import { useProcessUnmanagedTickets } from "./hooks"; -export default ({ - cancel, - send, - onProcessTickets, - title, - description, - noVspSelection, - isProcessingUnmanaged, - error, - availableVSPs -}) => { - const [isValid, setIsValid] = useState(false); - const [vsp, setVSP] = useState(null); - - const onSubmitContinue = (passphrase) => { - onProcessTickets(passphrase, vsp.host, vsp.pubkey) - .then(() => send({ type: "CONTINUE" })) - .catch((error) => { - send({ type: "ERROR", error }); - }); - }; - - useEffect(() => { - if (noVspSelection) { - setIsValid(true); - return; - } - if (vsp) { - setIsValid(true); - } - }, [vsp, noVspSelection]); +export default ({ cancel, send, error }) => { + const { + isProcessingUnmanaged, + availableVSPs, + vsp, + setVSP, + onSubmitContinue + } = useProcessUnmanagedTickets({ send }); return (
- -
{description}
- {!noVspSelection && ( - - )} + + } + /> +
+ { + + } +
+ {error &&
{error}
}
} - disabled={!isValid || isProcessingUnmanaged} + disabled={!vsp || isProcessingUnmanaged} loading={isProcessingUnmanaged} /> - {!isProcessingUnmanaged && ( - - - - )} + + +
); diff --git a/app/components/views/GetStartedPage/SetupWallet/ProcessUnmanagedTickets/ProcessUnmanagedTickets.module.css b/app/components/views/GetStartedPage/SetupWallet/ProcessUnmanagedTickets/ProcessUnmanagedTickets.module.css index 5f49ebe945..c3a6e8cc39 100644 --- a/app/components/views/GetStartedPage/SetupWallet/ProcessUnmanagedTickets/ProcessUnmanagedTickets.module.css +++ b/app/components/views/GetStartedPage/SetupWallet/ProcessUnmanagedTickets/ProcessUnmanagedTickets.module.css @@ -16,5 +16,13 @@ } .buttonWrapper { - margin-top: 10px; + margin-top: 2rem; +} + +.buttonWrapper .skipButton { + margin-left: 1rem; +} + +.description { + margin-bottom: 1rem; } diff --git a/app/components/views/GetStartedPage/SetupWallet/ProcessUnmanagedTickets/hooks.js b/app/components/views/GetStartedPage/SetupWallet/ProcessUnmanagedTickets/hooks.js new file mode 100644 index 0000000000..9a6a687822 --- /dev/null +++ b/app/components/views/GetStartedPage/SetupWallet/ProcessUnmanagedTickets/hooks.js @@ -0,0 +1,31 @@ +import { useState } from "react"; +import { useSettings } from "hooks"; +import { useSelector } from "react-redux"; +import { useDaemonStartup } from "hooks"; +import { getAvailableVSPs } from "selectors"; + +export const useProcessUnmanagedTickets = ({ send }) => { + const { onProcessUnmanagedTickets, isProcessingUnmanaged } = + useDaemonStartup(); + const { isVSPListingEnabled } = useSettings(); + const availableVSPs = isVSPListingEnabled + ? useSelector(getAvailableVSPs) + : []; + const [vsp, setVSP] = useState(null); + + const onSubmitContinue = (passphrase) => { + onProcessUnmanagedTickets(passphrase, vsp.host, vsp.pubkey) + .then(() => send({ type: "CONTINUE" })) + .catch((error) => { + send({ type: "ERROR", error }); + }); + }; + + return { + isProcessingUnmanaged, + availableVSPs, + vsp, + setVSP, + onSubmitContinue + }; +}; diff --git a/app/components/views/GetStartedPage/SetupWallet/ProcessUnmanagedTickets/index.js b/app/components/views/GetStartedPage/SetupWallet/ProcessUnmanagedTickets/index.js new file mode 100644 index 0000000000..4ea3490f6b --- /dev/null +++ b/app/components/views/GetStartedPage/SetupWallet/ProcessUnmanagedTickets/index.js @@ -0,0 +1 @@ +export { default } from "./ProcessUnmanagedTickets"; diff --git a/app/components/views/GetStartedPage/SetupWallet/hooks.js b/app/components/views/GetStartedPage/SetupWallet/hooks.js index d6195921e7..d5b77e04a9 100644 --- a/app/components/views/GetStartedPage/SetupWallet/hooks.js +++ b/app/components/views/GetStartedPage/SetupWallet/hooks.js @@ -3,26 +3,22 @@ import { useService } from "@xstate/react"; import { IMMATURE, LIVE, UNMINED } from "constants/decrediton"; import { FormattedMessage as T } from "react-intl"; import SettingMixedAccount from "./SetMixedAcctPage/SetMixedAcctPage"; -import ProcessUnmanagedTickets from "./ProcessUnmanagedTickets/ProcessUnmanagedTickets"; -import ProcessManagedTickets from "./ProcessManagedTickets/ProcessManagedTickets"; +import ProcessUnmanagedTickets from "./ProcessUnmanagedTickets"; +import ProcessManagedTickets from "./ProcessManagedTickets"; import SettingAccountsPassphrase from "./SetAccountsPassphrase"; import ResendVotesToRecentlyUpdatedVSPs from "./ResendVotesToRecentlyUpdatedVSPs"; import { useDaemonStartup, useAccounts, usePrevious } from "hooks"; -import { useDispatch, useSelector } from "react-redux"; +import { useDispatch } from "react-redux"; import { checkAllAccountsEncrypted, setAccountsPass } from "actions/ControlActions"; import { - getVSPsPubkeys, setCanDisableProcessManaged, getRecentlyUpdatedUsedVSPs, getNotAbstainVotes } from "actions/VSPActions"; import { ExternalLink } from "shared"; -import { DecredLoading } from "indicators"; -import * as sel from "selectors"; -import { useSettings } from "hooks"; export const useWalletSetup = (settingUpWalletRef) => { const dispatch = useDispatch(); @@ -32,17 +28,11 @@ export const useWalletSetup = (settingUpWalletRef) => { const { getCoinjoinOutputspByAcct, stakeTransactions, - onProcessManagedTickets, goToHome, - onProcessUnmanagedTickets, - isProcessingUnmanaged, isProcessingManaged, needsProcessManagedTickets } = useDaemonStartup(); - const { isVSPListingEnabled } = useSettings(); - const availableVSPs = useSelector(sel.getAvailableVSPs); - const { mixedAccount } = useAccounts(); const previousState = usePrevious(current); @@ -58,11 +48,6 @@ export const useWalletSetup = (settingUpWalletRef) => { [dispatch] ); - const onGetVSPsPubkeys = useCallback( - () => dispatch(getVSPsPubkeys()), - [dispatch] - ); - const onGetRecentlyUpdatedUsedVSPs = useCallback( () => dispatch(getRecentlyUpdatedUsedVSPs()), [dispatch] @@ -204,16 +189,6 @@ export const useWalletSetup = (settingUpWalletRef) => { } } break; - case "gettingVSPInfo": - // if no live tickets, we can skip it. - if (!hasLiveVSPdTickets) { - sendContinue(); - } else { - component = h(DecredLoading); - await onGetVSPsPubkeys(); - sendContinue(); - } - break; case "processingManagedTickets": // if no live tickets, we can skip it. if (!hasLiveVSPdTickets || !needsProcessManagedTickets) { @@ -221,28 +196,8 @@ export const useWalletSetup = (settingUpWalletRef) => { } else { component = h(ProcessManagedTickets, { error, - onSendContinue: sendContinue, - onSendError, send, - cancel: onSkipProcessManaged, - onProcessTickets: onProcessManagedTickets, - title: ( - - ), - isProcessingManaged: isProcessingManaged, - noVspSelection: true, - description: ( - - ) + cancel: onSkipProcessManaged }); } break; @@ -276,24 +231,7 @@ export const useWalletSetup = (settingUpWalletRef) => { send, onSendContinue: sendContinue, onSendError, - onProcessTickets: onProcessUnmanagedTickets, - isProcessingUnmanaged: isProcessingUnmanaged, - cancel: onSendBack, - availableVSPs: isVSPListingEnabled ? availableVSPs : [], - title: ( - - ), - description: ( - - ) + cancel: onSendBack }); } break; @@ -328,24 +266,18 @@ export const useWalletSetup = (settingUpWalletRef) => { getCoinjoinOutputspByAcct, goToHome, mixedAccount, - onProcessManagedTickets, send, stakeTransactions, sendContinue, isProcessingManaged, - isProcessingUnmanaged, needsProcessManagedTickets, - onProcessUnmanagedTickets, onSendBack, onSendError, previousState, current, - availableVSPs, onCheckAcctsPass, onProcessAccounts, - onGetVSPsPubkeys, onSkipProcessManaged, - isVSPListingEnabled, onGetRecentlyUpdatedUsedVSPs, onGetNotAbstainVotes ]); diff --git a/app/stateMachines/SetupWalletConfigMachine.js b/app/stateMachines/SetupWalletConfigMachine.js index e86e867bcc..ca38dffe9d 100644 --- a/app/stateMachines/SetupWalletConfigMachine.js +++ b/app/stateMachines/SetupWalletConfigMachine.js @@ -20,12 +20,6 @@ export const SetupWalletConfigMachine = Machine({ }, settingMixedAccount: { on: { - CONTINUE: "gettingVSPInfo" - } - }, - gettingVSPInfo: { - on: { - BACK: "processingManagedTickets", CONTINUE: "processingManagedTickets" } }, diff --git a/test/unit/actions/VSPActions.spec.js b/test/unit/actions/VSPActions.spec.js index a3acf114cf..30e401be72 100644 --- a/test/unit/actions/VSPActions.spec.js +++ b/test/unit/actions/VSPActions.spec.js @@ -348,7 +348,11 @@ test("test getVSPsPubkeys (error)", async () => { const testErrorMessage = "test-error-message"; wallet.getAllVSPs = jest.fn(() => Promise.reject(testErrorMessage)); const store = createStore({}); - await store.dispatch(vspActions.getVSPsPubkeys()); + try { + await store.dispatch(vspActions.getVSPsPubkeys()); + } catch (error) { + console.log({ error }); + } expect(store.getState().vsp.availableVSPsPubkeys).toEqual(undefined); expect(store.getState().vsp.availableVSPsPubkeysError).toEqual( diff --git a/test/unit/components/views/GetStaredPage/SetupWallet/ProcessManagedTickets.spec.js b/test/unit/components/views/GetStaredPage/SetupWallet/ProcessManagedTickets.spec.js new file mode 100644 index 0000000000..852fa3e84e --- /dev/null +++ b/test/unit/components/views/GetStaredPage/SetupWallet/ProcessManagedTickets.spec.js @@ -0,0 +1,374 @@ +import ProcessManagedTickets from "components/views/GetStartedPage/SetupWallet/ProcessManagedTickets"; +import { render } from "test-utils.js"; +import { screen, wait } from "@testing-library/react"; +import user from "@testing-library/user-event"; +import * as sel from "selectors"; +import * as wal from "wallet"; +import * as arrs from "../../../../../../app/helpers/arrays"; +const selectors = sel; +const wallet = wal; +const arrays = arrs; +import { + VSP_FEE_PROCESS_ERRORED, + VSP_FEE_PROCESS_STARTED, + VSP_FEE_PROCESS_PAID, + VSP_FEE_PROCESS_CONFIRMED +} from "constants"; +import { + defaultMockAvailableMainnetVsps, + defaultMockAvailableTestnetVsps, + defaultMockAvailableInvalidVsps, + mockPubkeys, + fetchTimes +} from "../../../../actions/vspMocks"; +import { + testBalances, + changeAccountNumber, + mixedAccountNumber, + defaultAccountNumber, + mockUnlockLockAndGetAccountsAttempt +} from "../../../../actions/accountMocks"; +import { cloneDeep } from "lodash"; + +const mockAvailableMainnetVsps = cloneDeep(defaultMockAvailableMainnetVsps); +const mockAvailableMainnetVspsPubkeys = cloneDeep( + defaultMockAvailableMainnetVsps +).map((v) => ({ ...v, pubkey: `${v.host}-pubkey` })); +const mockSend = jest.fn(() => {}); +const mockCancel = jest.fn(() => {}); + +const testPassphrase = "test-passphrase"; +const testWalletService = "test-wallet-service"; +const testError = "test-error"; + +let mockProcessManagedTickets; +let mockGetVSPTicketsByFeeStatus; +let mockGetVSPTrackedTickets; +let mockGetAllVSPs; +let mockGetVSPInfo; +beforeEach(() => { + selectors.getAvailableVSPsPubkeys = jest.fn(() => null); + selectors.balances = jest.fn(() => cloneDeep(testBalances)); + selectors.unlockableAccounts = jest.fn(() => + cloneDeep(testBalances).filter( + (acct) => acct.accountNumber < Math.pow(2, 31) - 1 && acct.encrypted + ) + ); + arrays.shuffle = jest.fn((arr) => arr); + selectors.getVSPInfoTimeoutTime = jest.fn(() => 100); + selectors.isTestNet = jest.fn(() => false); + selectors.resendVSPDVoteChoicesAttempt = jest.fn(() => false); + selectors.walletService = jest.fn(() => testWalletService); + selectors.defaultSpendingAccount = jest.fn(() => ({ + value: defaultAccountNumber + })); + selectors.getMixedAccount = jest.fn(() => mixedAccountNumber); + selectors.getChangeAccount = jest.fn(() => changeAccountNumber); + mockGetAllVSPs = wallet.getAllVSPs = jest.fn(() => [ + ...cloneDeep(mockAvailableMainnetVsps), + ...cloneDeep(defaultMockAvailableTestnetVsps), + ...cloneDeep(defaultMockAvailableInvalidVsps) + ]); + mockGetVSPInfo = wallet.getVSPInfo = jest.fn((host) => { + if (!mockPubkeys[host]) { + return Promise.reject("invalid host"); + } + + return new Promise((resolve, reject) => { + setTimeout(() => { + mockPubkeys[host] !== "invalid" + ? resolve({ data: { pubkey: mockPubkeys[host] } }) + : mockPubkeys[host] + ? resolve({ data: {} }) + : reject("invalid host"); + }, fetchTimes[host]); + }); + }); + mockProcessManagedTickets = wallet.processManagedTickets = jest.fn(() => {}); + mockGetVSPTrackedTickets = wallet.getVSPTrackedTickets = jest.fn(() => + Promise.resolve() + ); + mockGetVSPTicketsByFeeStatus = wallet.getVSPTicketsByFeeStatus = jest.fn(() => + Promise.resolve({ + ticketHashes: [] + }) + ); +}); + +const getSkipButton = () => screen.getByRole("button", { name: "Skip" }); +const getCancelButton = () => screen.getByRole("button", { name: "Cancel" }); +const getContinueButton = () => + screen.getByRole("button", { name: "Continue" }); +const getModalContinueButton = () => + screen.getAllByRole("button", { name: "Continue" })[1]; + +const initialState = { + grpc: { + walletService: testWalletService + } +}; + +test("skip ProcessManagedTickets and show error", () => { + render( + + ); + user.click(getSkipButton()); + expect(screen.getByText(testError)).toBeInTheDocument(); + expect(mockCancel).toHaveBeenCalled(); +}); + +test("do ProcessManagedTickets - in a private wallet", async () => { + mockUnlockLockAndGetAccountsAttempt(); + render(, { + initialState + }); + const continueButton = getContinueButton(); + user.click(continueButton); + + expect(screen.getByText("Passphrase")).toBeInTheDocument(); + + // cancel first + user.click(getCancelButton()); + + user.click(continueButton); + user.type(screen.getByLabelText("Private Passphrase"), testPassphrase); + user.click(getModalContinueButton()); + + await wait(() => expect(mockSend).toHaveBeenCalled()); + + expect(mockProcessManagedTickets).toHaveBeenNthCalledWith( + 1, + testWalletService, + defaultMockAvailableMainnetVsps[0].host, + mockPubkeys[`https://${mockAvailableMainnetVsps[0].host}`], + mixedAccountNumber, + changeAccountNumber + ); + + expect(mockProcessManagedTickets).toHaveBeenNthCalledWith( + 2, + testWalletService, + defaultMockAvailableMainnetVsps[3].host, + mockPubkeys[`https://${mockAvailableMainnetVsps[3].host}`], + mixedAccountNumber, + changeAccountNumber + ); + + expect(mockProcessManagedTickets).toHaveBeenNthCalledWith( + 3, + testWalletService, + defaultMockAvailableMainnetVsps[5].host, + mockPubkeys[`https://${mockAvailableMainnetVsps[5].host}`], + mixedAccountNumber, + changeAccountNumber + ); + + expect(mockGetVSPTrackedTickets).toHaveBeenCalledTimes(1); + + expect(mockGetVSPTicketsByFeeStatus).toHaveBeenNthCalledWith( + 1, + testWalletService, + VSP_FEE_PROCESS_ERRORED + ); + expect(mockGetVSPTicketsByFeeStatus).toHaveBeenNthCalledWith( + 2, + testWalletService, + VSP_FEE_PROCESS_STARTED + ); + expect(mockGetVSPTicketsByFeeStatus).toHaveBeenNthCalledWith( + 3, + testWalletService, + VSP_FEE_PROCESS_PAID + ); + expect(mockGetVSPTicketsByFeeStatus).toHaveBeenNthCalledWith( + 4, + testWalletService, + VSP_FEE_PROCESS_CONFIRMED + ); + + expect(mockGetAllVSPs).toHaveBeenCalled(); + expect(mockGetVSPInfo).toHaveBeenCalled(); +}); + +test("do ProcessManagedTickets - in a default wallet, available vps pubkeys have been already fetched", async () => { + selectors.getAvailableVSPsPubkeys = jest.fn( + () => mockAvailableMainnetVspsPubkeys + ); + selectors.getMixedAccount = jest.fn(() => null); + selectors.getChangeAccount = jest.fn(() => null); + mockUnlockLockAndGetAccountsAttempt(); + render(, { + initialState + }); + const continueButton = getContinueButton(); + user.click(continueButton); + + expect(screen.getByText("Passphrase")).toBeInTheDocument(); + + // cancel first + user.click(getCancelButton()); + + user.click(continueButton); + user.type(screen.getByLabelText("Private Passphrase"), testPassphrase); + user.click(getModalContinueButton()); + + await wait(() => expect(mockSend).toHaveBeenCalled()); + + expect(mockProcessManagedTickets).toHaveBeenNthCalledWith( + 1, + testWalletService, + defaultMockAvailableMainnetVsps[0].host, + `${mockAvailableMainnetVsps[0].host}-pubkey`, + defaultAccountNumber, + defaultAccountNumber + ); + + expect(mockProcessManagedTickets).toHaveBeenNthCalledWith( + 2, + testWalletService, + defaultMockAvailableMainnetVsps[1].host, + `${mockAvailableMainnetVsps[1].host}-pubkey`, + defaultAccountNumber, + defaultAccountNumber + ); + + expect(mockProcessManagedTickets).toHaveBeenNthCalledWith( + 3, + testWalletService, + defaultMockAvailableMainnetVsps[2].host, + `${mockAvailableMainnetVsps[2].host}-pubkey`, + defaultAccountNumber, + defaultAccountNumber + ); + + expect(mockGetVSPTrackedTickets).toHaveBeenCalledTimes(1); + + expect(mockGetVSPTicketsByFeeStatus).toHaveBeenNthCalledWith( + 1, + testWalletService, + VSP_FEE_PROCESS_ERRORED + ); + expect(mockGetVSPTicketsByFeeStatus).toHaveBeenNthCalledWith( + 2, + testWalletService, + VSP_FEE_PROCESS_STARTED + ); + expect(mockGetVSPTicketsByFeeStatus).toHaveBeenNthCalledWith( + 3, + testWalletService, + VSP_FEE_PROCESS_PAID + ); + expect(mockGetVSPTicketsByFeeStatus).toHaveBeenNthCalledWith( + 4, + testWalletService, + VSP_FEE_PROCESS_CONFIRMED + ); + + expect(mockGetAllVSPs).not.toHaveBeenCalled(); + expect(mockGetVSPInfo).not.toHaveBeenCalled(); +}); + +test("do ProcessManagedTickets - in a default wallet, available vps pubkeys have been already fetched", async () => { + selectors.getAvailableVSPsPubkeys = jest.fn( + () => mockAvailableMainnetVspsPubkeys + ); + selectors.getMixedAccount = jest.fn(() => null); + selectors.getChangeAccount = jest.fn(() => null); + mockUnlockLockAndGetAccountsAttempt(); + render(, { + initialState + }); + const continueButton = getContinueButton(); + user.click(continueButton); + + expect(screen.getByText("Passphrase")).toBeInTheDocument(); + + // cancel first + user.click(getCancelButton()); + + user.click(continueButton); + user.type(screen.getByLabelText("Private Passphrase"), testPassphrase); + user.click(getModalContinueButton()); + + await wait(() => expect(mockSend).toHaveBeenCalled()); + + expect(mockProcessManagedTickets).toHaveBeenNthCalledWith( + 1, + testWalletService, + defaultMockAvailableMainnetVsps[0].host, + `${mockAvailableMainnetVsps[0].host}-pubkey`, + defaultAccountNumber, + defaultAccountNumber + ); + + expect(mockProcessManagedTickets).toHaveBeenNthCalledWith( + 2, + testWalletService, + defaultMockAvailableMainnetVsps[1].host, + `${mockAvailableMainnetVsps[1].host}-pubkey`, + defaultAccountNumber, + defaultAccountNumber + ); + + expect(mockProcessManagedTickets).toHaveBeenNthCalledWith( + 3, + testWalletService, + defaultMockAvailableMainnetVsps[2].host, + `${mockAvailableMainnetVsps[2].host}-pubkey`, + defaultAccountNumber, + defaultAccountNumber + ); + + expect(mockGetVSPTrackedTickets).toHaveBeenCalledTimes(1); + + expect(mockGetVSPTicketsByFeeStatus).toHaveBeenNthCalledWith( + 1, + testWalletService, + VSP_FEE_PROCESS_ERRORED + ); + expect(mockGetVSPTicketsByFeeStatus).toHaveBeenNthCalledWith( + 2, + testWalletService, + VSP_FEE_PROCESS_STARTED + ); + expect(mockGetVSPTicketsByFeeStatus).toHaveBeenNthCalledWith( + 3, + testWalletService, + VSP_FEE_PROCESS_PAID + ); + expect(mockGetVSPTicketsByFeeStatus).toHaveBeenNthCalledWith( + 4, + testWalletService, + VSP_FEE_PROCESS_CONFIRMED + ); + + expect(mockGetAllVSPs).not.toHaveBeenCalled(); + expect(mockGetVSPInfo).not.toHaveBeenCalled(); +}); + +test("do ProcessManagedTickets - failed to fetch vsps", async () => { + mockUnlockLockAndGetAccountsAttempt(); + mockGetAllVSPs = wallet.getAllVSPs = jest.fn(() => { + throw testError; + }); + render(, { + initialState + }); + const continueButton = getContinueButton(); + user.click(continueButton); + + user.type(screen.getByLabelText("Private Passphrase"), testPassphrase); + user.click(getModalContinueButton()); + + await wait(() => expect(mockSend).toHaveBeenCalled()); + + expect(mockGetAllVSPs).toHaveBeenCalled(); + expect(mockProcessManagedTickets).not.toHaveBeenCalled(); + expect(mockGetVSPTrackedTickets).not.toHaveBeenCalled(); + expect(mockGetVSPTicketsByFeeStatus).not.toHaveBeenCalled(); + expect(mockGetVSPInfo).not.toHaveBeenCalled(); +}); diff --git a/test/unit/components/views/GetStaredPage/SetupWallet/ProcessUnmanagedTickets.spec.js b/test/unit/components/views/GetStaredPage/SetupWallet/ProcessUnmanagedTickets.spec.js new file mode 100644 index 0000000000..791647422a --- /dev/null +++ b/test/unit/components/views/GetStaredPage/SetupWallet/ProcessUnmanagedTickets.spec.js @@ -0,0 +1,375 @@ +import ProcessUnmanagedTickets from "components/views/GetStartedPage/SetupWallet/ProcessUnmanagedTickets"; +import { render } from "test-utils.js"; +import { screen, wait } from "@testing-library/react"; +import user from "@testing-library/user-event"; +import * as sel from "selectors"; +import * as wal from "wallet"; +import * as arrs from "../../../../../../app/helpers/arrays"; +const selectors = sel; +const wallet = wal; +const arrays = arrs; +import { DEFAULT_LIGHT_THEME_NAME } from "pi-ui"; +import { + VSP_FEE_PROCESS_ERRORED, + VSP_FEE_PROCESS_STARTED, + VSP_FEE_PROCESS_PAID, + VSP_FEE_PROCESS_CONFIRMED, + EXTERNALREQUEST_STAKEPOOL_LISTING +} from "constants"; +import { + defaultMockAvailableMainnetVsps, + defaultMockAvailableTestnetVsps, + defaultMockAvailableInvalidVsps, + mockPubkeys, + fetchTimes +} from "../../../../actions/vspMocks"; +import { + testBalances, + changeAccountNumber, + mixedAccountNumber, + defaultAccountNumber, + mockUnlockLockAndGetAccountsAttempt +} from "../../../../actions/accountMocks"; +import { cloneDeep } from "lodash"; + +const mockAvailableMainnetVsps = cloneDeep(defaultMockAvailableMainnetVsps); +const mockSend = jest.fn(() => {}); +const mockCancel = jest.fn(() => {}); + +const testPassphrase = "test-passphrase"; +const testWalletService = "test-wallet-service"; +const testError = "test-error"; +const testCustomVspHost = "custom-vsp-host"; +const testCustomVspHostPubkey = "test-custom-vsp-host-pubkey"; + +let mockProcessUnmanagedTickets; +let mockGetVSPTicketsByFeeStatus; +let mockGetAllVSPs; +let mockGetVSPInfo; +beforeEach(() => { + selectors.getAvailableVSPsPubkeys = jest.fn(() => null); + selectors.balances = jest.fn(() => cloneDeep(testBalances)); + selectors.unlockableAccounts = jest.fn(() => + cloneDeep(testBalances).filter( + (acct) => acct.accountNumber < Math.pow(2, 31) - 1 && acct.encrypted + ) + ); + arrays.shuffle = jest.fn((arr) => arr); + selectors.getVSPInfoTimeoutTime = jest.fn(() => 100); + selectors.isTestNet = jest.fn(() => false); + selectors.resendVSPDVoteChoicesAttempt = jest.fn(() => false); + selectors.walletService = jest.fn(() => testWalletService); + selectors.defaultSpendingAccount = jest.fn(() => ({ + value: defaultAccountNumber + })); + selectors.getMixedAccount = jest.fn(() => mixedAccountNumber); + selectors.getChangeAccount = jest.fn(() => changeAccountNumber); + mockGetAllVSPs = wallet.getAllVSPs = jest.fn(() => [ + ...cloneDeep(mockAvailableMainnetVsps), + ...cloneDeep(defaultMockAvailableTestnetVsps), + ...cloneDeep(defaultMockAvailableInvalidVsps) + ]); + mockGetVSPInfo = wallet.getVSPInfo = jest.fn((host) => { + if (host === `https://${testCustomVspHost}`) { + return Promise.resolve({ data: { pubkey: testCustomVspHostPubkey } }); + } + if (!mockPubkeys[host]) { + return Promise.reject("invalid host"); + } + + return new Promise((resolve, reject) => { + setTimeout(() => { + mockPubkeys[host] !== "invalid" + ? resolve({ data: { pubkey: mockPubkeys[host] } }) + : mockPubkeys[host] + ? resolve({ data: {} }) + : reject("invalid host"); + }, fetchTimes[host]); + }); + }); + mockProcessUnmanagedTickets = wallet.processUnmanagedTicketsStartup = jest.fn( + () => {} + ); + wallet.getVSPTrackedTickets = jest.fn(() => Promise.resolve()); + mockGetVSPTicketsByFeeStatus = wallet.getVSPTicketsByFeeStatus = jest.fn(() => + Promise.resolve({ + ticketHashes: [] + }) + ); + + selectors.getAvailableVSPs = jest.fn(() => mockAvailableMainnetVsps); +}); + +const getSkipButton = () => screen.getByRole("button", { name: "Skip" }); +const getCancelButton = () => screen.getByRole("button", { name: "Cancel" }); +const getContinueButton = () => + screen.getByRole("button", { name: "Continue" }); +const getModalContinueButton = () => + screen.getAllByRole("button", { name: "Continue" })[1]; + +const initialState = { + grpc: { + walletService: testWalletService + }, + settings: { + currentSettings: { + theme: DEFAULT_LIGHT_THEME_NAME, + allowedExternalRequests: [EXTERNALREQUEST_STAKEPOOL_LISTING] + }, + tempSettings: { + theme: DEFAULT_LIGHT_THEME_NAME, + allowedExternalRequests: [EXTERNALREQUEST_STAKEPOOL_LISTING] + } + } +}; + +test("skip ProcessUnmanagedTickets and show error", () => { + render( + + ); + expect(screen.getByText(testError)).toBeInTheDocument(); + user.click(getSkipButton()); + expect(mockCancel).toHaveBeenCalled(); +}); + +test("do ProcessUnmanagedTickets - in a private wallet", async () => { + mockUnlockLockAndGetAccountsAttempt(); + render(, { + initialState + }); + const continueButton = getContinueButton(); + expect(continueButton.disabled).toBe(true); + + user.click(screen.getByText("Select VSP...")); + user.click(screen.getByText(mockAvailableMainnetVsps[0].host)); + expect(screen.getByText("Loading")).toBeInTheDocument(); + await wait(() => expect(getContinueButton().disabled).toBeFalsy()); + expect(screen.queryByText("Loading")).not.toBeInTheDocument(); + expect( + screen.getByText(mockAvailableMainnetVsps[0].host) + ).toBeInTheDocument(); + + user.click(continueButton); + expect(screen.getByText("Passphrase")).toBeInTheDocument(); + + // cancel first + user.click(getCancelButton()); + + user.click(continueButton); + user.type(screen.getByLabelText("Private Passphrase"), testPassphrase); + user.click(getModalContinueButton()); + + await wait(() => expect(mockSend).toHaveBeenCalled()); + + expect(mockProcessUnmanagedTickets).toHaveBeenCalledWith( + testWalletService, + defaultMockAvailableMainnetVsps[0].host, + mockPubkeys[`https://${mockAvailableMainnetVsps[0].host}`], + mixedAccountNumber, + changeAccountNumber + ); + + expect(mockGetVSPTicketsByFeeStatus).toHaveBeenNthCalledWith( + 1, + testWalletService, + VSP_FEE_PROCESS_ERRORED + ); + expect(mockGetVSPTicketsByFeeStatus).toHaveBeenNthCalledWith( + 2, + testWalletService, + VSP_FEE_PROCESS_STARTED + ); + expect(mockGetVSPTicketsByFeeStatus).toHaveBeenNthCalledWith( + 3, + testWalletService, + VSP_FEE_PROCESS_PAID + ); + expect(mockGetVSPTicketsByFeeStatus).toHaveBeenNthCalledWith( + 4, + testWalletService, + VSP_FEE_PROCESS_CONFIRMED + ); + + expect(mockGetAllVSPs).not.toHaveBeenCalled(); + expect(mockGetVSPInfo).toHaveBeenCalled(); +}); + +test("do ProcessUnmanagedTickets - in a default wallet", async () => { + selectors.getMixedAccount = jest.fn(() => null); + selectors.getChangeAccount = jest.fn(() => null); + mockUnlockLockAndGetAccountsAttempt(); + render(, { + initialState + }); + const continueButton = getContinueButton(); + expect(continueButton.disabled).toBe(true); + + user.click(screen.getByText("Select VSP...")); + user.click(screen.getByText(mockAvailableMainnetVsps[0].host)); + expect(screen.getByText("Loading")).toBeInTheDocument(); + await wait(() => expect(getContinueButton().disabled).toBeFalsy()); + expect(screen.queryByText("Loading")).not.toBeInTheDocument(); + expect( + screen.getByText(mockAvailableMainnetVsps[0].host) + ).toBeInTheDocument(); + + user.click(continueButton); + expect(screen.getByText("Passphrase")).toBeInTheDocument(); + + // cancel first + user.click(getCancelButton()); + + user.click(continueButton); + user.type(screen.getByLabelText("Private Passphrase"), testPassphrase); + user.click(getModalContinueButton()); + + await wait(() => expect(mockSend).toHaveBeenCalled()); + + expect(mockProcessUnmanagedTickets).toHaveBeenCalledWith( + testWalletService, + defaultMockAvailableMainnetVsps[0].host, + mockPubkeys[`https://${mockAvailableMainnetVsps[0].host}`], + defaultAccountNumber, + defaultAccountNumber + ); + + expect(mockGetVSPTicketsByFeeStatus).toHaveBeenNthCalledWith( + 1, + testWalletService, + VSP_FEE_PROCESS_ERRORED + ); + expect(mockGetVSPTicketsByFeeStatus).toHaveBeenNthCalledWith( + 2, + testWalletService, + VSP_FEE_PROCESS_STARTED + ); + expect(mockGetVSPTicketsByFeeStatus).toHaveBeenNthCalledWith( + 3, + testWalletService, + VSP_FEE_PROCESS_PAID + ); + expect(mockGetVSPTicketsByFeeStatus).toHaveBeenNthCalledWith( + 4, + testWalletService, + VSP_FEE_PROCESS_CONFIRMED + ); + + expect(mockGetAllVSPs).not.toHaveBeenCalled(); + expect(mockGetVSPInfo).toHaveBeenCalled(); +}); + +test("do ProcessUnmanagedTickets - vsp listing is not enabled", async () => { + mockUnlockLockAndGetAccountsAttempt(); + render(, { + initialState: cloneDeep({ + ...initialState, + settings: { + ...initialState.settings, + tempSettings: { + ...initialState.settings.tempSettings, + allowedExternalRequests: [] + } + } + }) + }); + + const continueButton = getContinueButton(); + expect(continueButton.disabled).toBe(true); + + user.type(screen.getByRole("combobox"), testCustomVspHost); + user.click(screen.getByText(`Create "${testCustomVspHost}"`)); + expect(screen.getByText("Loading")).toBeInTheDocument(); + await wait(() => + expect(screen.getByText(testCustomVspHost)).toBeInTheDocument() + ); + + user.click(continueButton); + expect(screen.getByText("Passphrase")).toBeInTheDocument(); + + // cancel first + user.click(getCancelButton()); + + user.click(continueButton); + user.type(screen.getByLabelText("Private Passphrase"), testPassphrase); + user.click(getModalContinueButton()); + + await wait(() => expect(mockSend).toHaveBeenCalled()); + + expect(mockProcessUnmanagedTickets).toHaveBeenCalledWith( + testWalletService, + testCustomVspHost, + testCustomVspHostPubkey, + mixedAccountNumber, + changeAccountNumber + ); + + expect(mockGetVSPTicketsByFeeStatus).toHaveBeenNthCalledWith( + 1, + testWalletService, + VSP_FEE_PROCESS_ERRORED + ); + expect(mockGetVSPTicketsByFeeStatus).toHaveBeenNthCalledWith( + 2, + testWalletService, + VSP_FEE_PROCESS_STARTED + ); + expect(mockGetVSPTicketsByFeeStatus).toHaveBeenNthCalledWith( + 3, + testWalletService, + VSP_FEE_PROCESS_PAID + ); + expect(mockGetVSPTicketsByFeeStatus).toHaveBeenNthCalledWith( + 4, + testWalletService, + VSP_FEE_PROCESS_CONFIRMED + ); + + expect(mockGetAllVSPs).not.toHaveBeenCalled(); + expect(mockGetVSPInfo).toHaveBeenCalled(); +}); + +test("do ProcessUnManagedTickets - failed", async () => { + mockProcessUnmanagedTickets = wallet.processUnmanagedTicketsStartup = jest.fn( + () => { + throw testError; + } + ); + mockUnlockLockAndGetAccountsAttempt(); + render(, { + initialState + }); + const continueButton = getContinueButton(); + expect(continueButton.disabled).toBe(true); + + user.click(screen.getByText("Select VSP...")); + user.click(screen.getByText(mockAvailableMainnetVsps[0].host)); + expect(screen.getByText("Loading")).toBeInTheDocument(); + await wait(() => expect(getContinueButton().disabled).toBeFalsy()); + expect(screen.queryByText("Loading")).not.toBeInTheDocument(); + expect( + screen.getByText(mockAvailableMainnetVsps[0].host) + ).toBeInTheDocument(); + + user.click(continueButton); + expect(screen.getByText("Passphrase")).toBeInTheDocument(); + + user.click(continueButton); + user.type(screen.getByLabelText("Private Passphrase"), testPassphrase); + user.click(getModalContinueButton()); + + await wait(() => expect(mockSend).toHaveBeenCalled()); + + expect(mockProcessUnmanagedTickets).toHaveBeenCalledWith( + testWalletService, + defaultMockAvailableMainnetVsps[0].host, + mockPubkeys[`https://${mockAvailableMainnetVsps[0].host}`], + mixedAccountNumber, + changeAccountNumber + ); + + expect(mockGetVSPTicketsByFeeStatus).not.toHaveBeenCalled(); +});