diff --git a/src/components/new-safe/create/__tests__/useSyncSafeCreationStep.test.ts b/src/components/new-safe/create/__tests__/useSyncSafeCreationStep.test.ts index cd8d2a011e..807f848364 100644 --- a/src/components/new-safe/create/__tests__/useSyncSafeCreationStep.test.ts +++ b/src/components/new-safe/create/__tests__/useSyncSafeCreationStep.test.ts @@ -3,14 +3,27 @@ import useSyncSafeCreationStep from '@/components/new-safe/create/useSyncSafeCre import * as wallet from '@/hooks/wallets/useWallet' import * as localStorage from '@/services/local-storage/useLocalStorage' import type { ConnectedWallet } from '@/services/onboard' +import * as usePendingSafe from '../steps/StatusStep/usePendingSafe' +import * as useIsWrongChain from '@/hooks/useIsWrongChain' describe('useSyncSafeCreationStep', () => { + const mockPendingSafe = { + name: 'joyful-rinkeby-safe', + threshold: 1, + owners: [], + saltNonce: 123, + address: '0x10', + } + const setPendingSafeSpy = jest.fn() + beforeEach(() => { jest.clearAllMocks() + const setPendingSafeSpy = jest.fn() }) it('should go to the first step if no wallet is connected', async () => { jest.spyOn(wallet, 'default').mockReturnValue(null) + jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([undefined, setPendingSafeSpy]) const mockSetStep = jest.fn() renderHook(() => useSyncSafeCreationStep(mockSetStep)) @@ -21,6 +34,8 @@ describe('useSyncSafeCreationStep', () => { it('should go to the fourth step if there is a pending safe', async () => { jest.spyOn(localStorage, 'default').mockReturnValue([{}, jest.fn()]) jest.spyOn(wallet, 'default').mockReturnValue({ address: '0x1' } as ConnectedWallet) + jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([mockPendingSafe, setPendingSafeSpy]) + const mockSetStep = jest.fn() renderHook(() => useSyncSafeCreationStep(mockSetStep)) @@ -28,9 +43,25 @@ describe('useSyncSafeCreationStep', () => { expect(mockSetStep).toHaveBeenCalledWith(4) }) + it('should go to the second step if the wrong chain is connected', async () => { + jest.spyOn(localStorage, 'default').mockReturnValue([{}, jest.fn()]) + jest.spyOn(wallet, 'default').mockReturnValue({ address: '0x1' } as ConnectedWallet) + jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([undefined, setPendingSafeSpy]) + jest.spyOn(useIsWrongChain, 'default').mockReturnValue(true) + + const mockSetStep = jest.fn() + + renderHook(() => useSyncSafeCreationStep(mockSetStep)) + + expect(mockSetStep).toHaveBeenCalledWith(1) + }) + it('should not do anything if wallet is connected and there is no pending safe', async () => { jest.spyOn(localStorage, 'default').mockReturnValue([undefined, jest.fn()]) jest.spyOn(wallet, 'default').mockReturnValue({ address: '0x1' } as ConnectedWallet) + jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([undefined, setPendingSafeSpy]) + jest.spyOn(useIsWrongChain, 'default').mockReturnValue(false) + const mockSetStep = jest.fn() renderHook(() => useSyncSafeCreationStep(mockSetStep)) diff --git a/src/components/new-safe/create/steps/ConnectWalletStep/index.tsx b/src/components/new-safe/create/steps/ConnectWalletStep/index.tsx index 5e6d9a928a..222ddaa645 100644 --- a/src/components/new-safe/create/steps/ConnectWalletStep/index.tsx +++ b/src/components/new-safe/create/steps/ConnectWalletStep/index.tsx @@ -8,15 +8,14 @@ import type { NewSafeFormData } from '@/components/new-safe/create' import type { StepRenderProps } from '@/components/new-safe/CardStepper/useCardStepper' import useSyncSafeCreationStep from '@/components/new-safe/create/useSyncSafeCreationStep' import layoutCss from '@/components/new-safe/create/styles.module.css' -import useLocalStorage from '@/services/local-storage/useLocalStorage' -import { type PendingSafeData, SAFE_PENDING_CREATION_STORAGE_KEY } from '@/components/new-safe/create/steps/StatusStep' import useConnectWallet from '@/components/common/ConnectWallet/useConnectWallet' import KeyholeIcon from '@/components/common/icons/KeyholeIcon' import PairingDescription from '@/components/common/PairingDetails/PairingDescription' import PairingQRCode from '@/components/common/PairingDetails/PairingQRCode' +import { usePendingSafe } from '../StatusStep/usePendingSafe' const ConnectWalletStep = ({ onSubmit, setStep }: StepRenderProps) => { - const [pendingSafe] = useLocalStorage(SAFE_PENDING_CREATION_STORAGE_KEY) + const [pendingSafe] = usePendingSafe() const wallet = useWallet() const chain = useCurrentChain() const isSupported = isPairingSupported(chain?.disabledWallets) diff --git a/src/components/new-safe/create/steps/ReviewStep/index.tsx b/src/components/new-safe/create/steps/ReviewStep/index.tsx index c5ebf035f4..7ec15641ff 100644 --- a/src/components/new-safe/create/steps/ReviewStep/index.tsx +++ b/src/components/new-safe/create/steps/ReviewStep/index.tsx @@ -15,8 +15,6 @@ import { getReadOnlyFallbackHandlerContract } from '@/services/contracts/safeCon import { computeNewSafeAddress } from '@/components/new-safe/create/logic' import useWallet from '@/hooks/wallets/useWallet' import { useWeb3 } from '@/hooks/wallets/web3' -import useLocalStorage from '@/services/local-storage/useLocalStorage' -import { type PendingSafeData, SAFE_PENDING_CREATION_STORAGE_KEY } from '@/components/new-safe/create/steps/StatusStep' import useSyncSafeCreationStep from '@/components/new-safe/create/useSyncSafeCreationStep' import ArrowBackIcon from '@mui/icons-material/ArrowBack' import NetworkWarning from '@/components/new-safe/create/NetworkWarning' @@ -27,6 +25,7 @@ import { useLeastRemainingRelays } from '@/hooks/useRemainingRelays' import classnames from 'classnames' import { hasRemainingRelays } from '@/utils/relaying' import { BigNumber } from 'ethers' +import { usePendingSafe } from '../StatusStep/usePendingSafe' const ReviewStep = ({ data, onSubmit, onBack, setStep }: StepRenderProps) => { const isWrongChain = useIsWrongChain() @@ -36,7 +35,7 @@ const ReviewStep = ({ data, onSubmit, onBack, setStep }: StepRenderProps Date.now(), []) - const [_, setPendingSafe] = useLocalStorage(SAFE_PENDING_CREATION_STORAGE_KEY) + const [_, setPendingSafe] = usePendingSafe() const [executionMethod, setExecutionMethod] = useState(ExecutionMethod.RELAY) const ownerAddresses = useMemo(() => data.owners.map((owner) => owner.address), [data.owners]) diff --git a/src/components/new-safe/create/steps/StatusStep/StatusStepper.tsx b/src/components/new-safe/create/steps/StatusStep/StatusStepper.tsx index fe45f4b764..6ab2693f59 100644 --- a/src/components/new-safe/create/steps/StatusStep/StatusStepper.tsx +++ b/src/components/new-safe/create/steps/StatusStep/StatusStepper.tsx @@ -2,10 +2,11 @@ import { Box, Step, StepConnector, Stepper, Typography } from '@mui/material' import css from '@/components/new-safe/create/steps/StatusStep/styles.module.css' import EthHashInfo from '@/components/common/EthHashInfo' import { SafeCreationStatus } from '@/components/new-safe/create/steps/StatusStep/useSafeCreation' -import type { PendingSafeData } from '@/components/new-safe/create/steps/StatusStep/index' import StatusStep from '@/components/new-safe/create/steps/StatusStep/StatusStep' +import { usePendingSafe } from './usePendingSafe' -const StatusStepper = ({ pendingSafe, status }: { pendingSafe: PendingSafeData; status: SafeCreationStatus }) => { +const StatusStepper = ({ status }: { status: SafeCreationStatus }) => { + const [pendingSafe] = usePendingSafe() if (!pendingSafe?.safeAddress) return null return ( diff --git a/src/components/new-safe/create/steps/StatusStep/__tests__/index.test.tsx b/src/components/new-safe/create/steps/StatusStep/__tests__/index.test.tsx index c779c95179..31b5aae5ec 100644 --- a/src/components/new-safe/create/steps/StatusStep/__tests__/index.test.tsx +++ b/src/components/new-safe/create/steps/StatusStep/__tests__/index.test.tsx @@ -17,12 +17,6 @@ describe('StatusStep', () => { />, ) - expect(useSafeCreationSpy).toHaveBeenCalledWith( - undefined, - expect.anything(), - SafeCreationStatus.PROCESSING, - expect.anything(), - true, - ) + expect(useSafeCreationSpy).toHaveBeenCalledWith(SafeCreationStatus.PROCESSING, expect.anything(), true) }) }) diff --git a/src/components/new-safe/create/steps/StatusStep/__tests__/usePendingSafe.test.ts b/src/components/new-safe/create/steps/StatusStep/__tests__/usePendingSafe.test.ts new file mode 100644 index 0000000000..145df2ebe5 --- /dev/null +++ b/src/components/new-safe/create/steps/StatusStep/__tests__/usePendingSafe.test.ts @@ -0,0 +1,71 @@ +import { renderHook } from '@/tests/test-utils' +import { usePendingSafe } from '../usePendingSafe' + +import { hexZeroPad } from 'ethers/lib/utils' +import { useCurrentChain } from '@/hooks/useChains' + +// mock useCurrentChain +jest.mock('@/hooks/useChains', () => ({ + useCurrentChain: jest.fn(() => ({ + shortName: 'gor', + chainId: '5', + chainName: 'Goerli', + features: [], + })), +})) + +describe('usePendingSafe()', () => { + const mockPendingSafe1 = { + name: 'joyful-rinkeby-safe', + threshold: 1, + owners: [], + saltNonce: 123, + address: hexZeroPad('0x10', 20), + } + const mockPendingSafe2 = { + name: 'joyful-rinkeby-safe', + threshold: 1, + owners: [], + saltNonce: 123, + address: hexZeroPad('0x10', 20), + } + + beforeEach(() => { + window.localStorage.clear() + }) + it('Should initially be undefined', () => { + const { result } = renderHook(() => usePendingSafe()) + expect(result.current[0]).toBeUndefined() + }) + + it('Should set the pendingSafe per ChainId', async () => { + const { result, rerender } = renderHook(() => usePendingSafe()) + + result.current[1](mockPendingSafe1) + + rerender() + + expect(result.current[0]).toEqual(mockPendingSafe1) + ;(useCurrentChain as jest.Mock).mockImplementation(() => ({ + shortName: 'eth', + chainId: '1', + chainName: 'Ethereum', + features: [], + })) + + rerender() + expect(result.current[0]).toEqual(undefined) + + result.current[1](mockPendingSafe2) + rerender() + expect(result.current[0]).toEqual(mockPendingSafe2) + ;(useCurrentChain as jest.Mock).mockImplementation(() => ({ + shortName: 'gor', + chainId: '5', + chainName: 'Goerli', + features: [], + })) + rerender() + expect(result.current[0]).toEqual(mockPendingSafe1) + }) +}) diff --git a/src/components/new-safe/create/steps/StatusStep/__tests__/useSafeCreation.test.ts b/src/components/new-safe/create/steps/StatusStep/__tests__/useSafeCreation.test.ts index 4c3b3e4c58..1ad617b11a 100644 --- a/src/components/new-safe/create/steps/StatusStep/__tests__/useSafeCreation.test.ts +++ b/src/components/new-safe/create/steps/StatusStep/__tests__/useSafeCreation.test.ts @@ -6,6 +6,7 @@ import * as wallet from '@/hooks/wallets/useWallet' import * as logic from '@/components/new-safe/create/logic' import * as contracts from '@/services/contracts/safeContracts' import * as txMonitor from '@/services/tx/txMonitor' +import * as usePendingSafe from '@/components/new-safe/create/steps/StatusStep/usePendingSafe' import { JsonRpcProvider, Web3Provider } from '@ethersproject/providers' import type { ConnectedWallet } from '@/hooks/wallets/useOnboard' import type { ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' @@ -34,9 +35,7 @@ describe('useSafeCreation', () => { saltNonce: 123, address: '0x10', } - const mockSetPendingSafe = jest.fn() - const mockStatus = SafeCreationStatus.AWAITING const mockSetStatus = jest.fn() const mockProvider: Web3Provider = new Web3Provider(jest.fn()) @@ -66,8 +65,8 @@ describe('useSafeCreation', () => { it('should create a safe with gas params if there is no txHash and status is AWAITING', async () => { const createSafeSpy = jest.spyOn(logic, 'createNewSafe').mockReturnValue(Promise.resolve({} as Safe)) - - renderHook(() => useSafeCreation(mockPendingSafe, mockSetPendingSafe, mockStatus, mockSetStatus, false)) + jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([mockPendingSafe, mockSetPendingSafe]) + renderHook(() => useSafeCreation(mockStatus, mockSetStatus, false)) await waitFor(() => { expect(createSafeSpy).toHaveBeenCalled() @@ -97,9 +96,11 @@ describe('useSafeCreation', () => { features: [FEATURES.EIP1559], } as unknown as ChainInfo), ) + jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([mockPendingSafe, mockSetPendingSafe]) + const createSafeSpy = jest.spyOn(logic, 'createNewSafe').mockReturnValue(Promise.resolve({} as Safe)) - renderHook(() => useSafeCreation(mockPendingSafe, mockSetPendingSafe, mockStatus, mockSetStatus, false)) + renderHook(() => useSafeCreation(mockStatus, mockSetStatus, false)) await waitFor(() => { expect(createSafeSpy).toHaveBeenCalled() @@ -115,10 +116,11 @@ describe('useSafeCreation', () => { it('should create a safe with no gas params if the gas estimation threw, there is no txHash and status is AWAITING', async () => { jest.spyOn(gasPrice, 'default').mockReturnValue([undefined, Error('Error for testing'), false]) + jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([mockPendingSafe, mockSetPendingSafe]) const createSafeSpy = jest.spyOn(logic, 'createNewSafe').mockReturnValue(Promise.resolve({} as Safe)) - renderHook(() => useSafeCreation(mockPendingSafe, mockSetPendingSafe, mockStatus, mockSetStatus, false)) + renderHook(() => useSafeCreation(mockStatus, mockSetStatus, false)) await waitFor(() => { expect(createSafeSpy).toHaveBeenCalled() @@ -133,10 +135,11 @@ describe('useSafeCreation', () => { it('should not create a safe if there is no txHash, status is AWAITING but gas is loading', async () => { jest.spyOn(gasPrice, 'default').mockReturnValue([undefined, undefined, true]) + jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([mockPendingSafe, mockSetPendingSafe]) const createSafeSpy = jest.spyOn(logic, 'createNewSafe').mockReturnValue(Promise.resolve({} as Safe)) - renderHook(() => useSafeCreation(mockPendingSafe, mockSetPendingSafe, mockStatus, mockSetStatus, false)) + renderHook(() => useSafeCreation(mockStatus, mockSetStatus, false)) await waitFor(() => { expect(createSafeSpy).not.toHaveBeenCalled() @@ -145,66 +148,51 @@ describe('useSafeCreation', () => { it('should not create a safe if the status is not AWAITING', async () => { const createSafeSpy = jest.spyOn(logic, 'createNewSafe') + jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([mockPendingSafe, mockSetPendingSafe]) - renderHook(() => - useSafeCreation(mockPendingSafe, mockSetPendingSafe, SafeCreationStatus.WALLET_REJECTED, mockSetStatus, false), - ) + renderHook(() => useSafeCreation(SafeCreationStatus.WALLET_REJECTED, mockSetStatus, false)) await waitFor(() => { expect(createSafeSpy).not.toHaveBeenCalled() }) - renderHook(() => - useSafeCreation(mockPendingSafe, mockSetPendingSafe, SafeCreationStatus.PROCESSING, mockSetStatus, false), - ) + renderHook(() => useSafeCreation(SafeCreationStatus.PROCESSING, mockSetStatus, false)) await waitFor(() => { expect(createSafeSpy).not.toHaveBeenCalled() }) - renderHook(() => - useSafeCreation(mockPendingSafe, mockSetPendingSafe, SafeCreationStatus.ERROR, mockSetStatus, false), - ) + renderHook(() => useSafeCreation(SafeCreationStatus.ERROR, mockSetStatus, false)) await waitFor(() => { expect(createSafeSpy).not.toHaveBeenCalled() }) - renderHook(() => - useSafeCreation(mockPendingSafe, mockSetPendingSafe, SafeCreationStatus.REVERTED, mockSetStatus, false), - ) + renderHook(() => useSafeCreation(SafeCreationStatus.REVERTED, mockSetStatus, false)) await waitFor(() => { expect(createSafeSpy).not.toHaveBeenCalled() }) - renderHook(() => - useSafeCreation(mockPendingSafe, mockSetPendingSafe, SafeCreationStatus.TIMEOUT, mockSetStatus, false), - ) + renderHook(() => useSafeCreation(SafeCreationStatus.TIMEOUT, mockSetStatus, false)) await waitFor(() => { expect(createSafeSpy).not.toHaveBeenCalled() }) - renderHook(() => - useSafeCreation(mockPendingSafe, mockSetPendingSafe, SafeCreationStatus.SUCCESS, mockSetStatus, false), - ) + renderHook(() => useSafeCreation(SafeCreationStatus.SUCCESS, mockSetStatus, false)) await waitFor(() => { expect(createSafeSpy).not.toHaveBeenCalled() }) - renderHook(() => - useSafeCreation(mockPendingSafe, mockSetPendingSafe, SafeCreationStatus.INDEXED, mockSetStatus, false), - ) + renderHook(() => useSafeCreation(SafeCreationStatus.INDEXED, mockSetStatus, false)) await waitFor(() => { expect(createSafeSpy).not.toHaveBeenCalled() }) - renderHook(() => - useSafeCreation(mockPendingSafe, mockSetPendingSafe, SafeCreationStatus.INDEX_FAILED, mockSetStatus, false), - ) + renderHook(() => useSafeCreation(SafeCreationStatus.INDEX_FAILED, mockSetStatus, false)) await waitFor(() => { expect(createSafeSpy).not.toHaveBeenCalled() @@ -213,16 +201,11 @@ describe('useSafeCreation', () => { it('should not create a safe if there is a txHash', async () => { const createSafeSpy = jest.spyOn(logic, 'createNewSafe') + jest + .spyOn(usePendingSafe, 'usePendingSafe') + .mockReturnValue([{ ...mockPendingSafe, txHash: '0x123' }, mockSetPendingSafe]) - renderHook(() => - useSafeCreation( - { ...mockPendingSafe, txHash: '0x123' }, - mockSetPendingSafe, - SafeCreationStatus.AWAITING, - mockSetStatus, - false, - ), - ) + renderHook(() => useSafeCreation(SafeCreationStatus.AWAITING, mockSetStatus, false)) await waitFor(() => { expect(createSafeSpy).not.toHaveBeenCalled() @@ -231,16 +214,22 @@ describe('useSafeCreation', () => { it('should watch a tx if there is a txHash and a tx object', async () => { const watchSafeTxSpy = jest.spyOn(logic, 'checkSafeCreationTx') - - renderHook(() => - useSafeCreation( - { ...mockPendingSafe, txHash: '0x123', tx: mockSafeInfo }, - mockSetPendingSafe, - mockStatus, - mockSetStatus, - false, - ), - ) + jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([ + { + ...mockPendingSafe, + txHash: '0x123', + tx: { + data: '0x', + from: '0x1234', + nonce: 0, + startBlock: 0, + to: '0x456', + value: BigNumber.from(0), + }, + }, + mockSetPendingSafe, + ]) + renderHook(() => useSafeCreation(mockStatus, mockSetStatus, false)) await waitFor(() => { expect(watchSafeTxSpy).toHaveBeenCalledTimes(1) @@ -249,17 +238,24 @@ describe('useSafeCreation', () => { it('should watch a tx even if no wallet is connected', async () => { jest.spyOn(wallet, 'default').mockReturnValue(null) + jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([ + { + ...mockPendingSafe, + txHash: '0x123', + tx: { + data: '0x', + from: '0x1234', + nonce: 0, + startBlock: 0, + to: '0x456', + value: BigNumber.from(0), + }, + }, + mockSetPendingSafe, + ]) const watchSafeTxSpy = jest.spyOn(logic, 'checkSafeCreationTx') - renderHook(() => - useSafeCreation( - { ...mockPendingSafe, txHash: '0x123', tx: mockSafeInfo }, - mockSetPendingSafe, - mockStatus, - mockSetStatus, - false, - ), - ) + renderHook(() => useSafeCreation(mockStatus, mockSetStatus, false)) await waitFor(() => { expect(watchSafeTxSpy).toHaveBeenCalledTimes(1) @@ -268,10 +264,8 @@ describe('useSafeCreation', () => { it('should not watch a tx if there is no txHash', async () => { const watchSafeTxSpy = jest.spyOn(logic, 'checkSafeCreationTx') - - renderHook(() => - useSafeCreation({ ...mockPendingSafe, tx: mockSafeInfo }, mockSetPendingSafe, mockStatus, mockSetStatus, false), - ) + jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([mockPendingSafe, mockSetPendingSafe]) + renderHook(() => useSafeCreation(mockStatus, mockSetStatus, false)) await waitFor(() => { expect(watchSafeTxSpy).not.toHaveBeenCalled() @@ -280,10 +274,21 @@ describe('useSafeCreation', () => { it('should not watch a tx if there is no tx object', async () => { const watchSafeTxSpy = jest.spyOn(logic, 'checkSafeCreationTx') - - renderHook(() => - useSafeCreation({ ...mockPendingSafe, txHash: '0x123' }, mockSetPendingSafe, mockStatus, mockSetStatus, false), - ) + jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([ + { + ...mockPendingSafe, + tx: { + data: '0x', + from: '0x1234', + nonce: 0, + startBlock: 0, + to: '0x456', + value: BigNumber.from(0), + }, + }, + mockSetPendingSafe, + ]) + renderHook(() => useSafeCreation(mockStatus, mockSetStatus, false)) await waitFor(() => { expect(watchSafeTxSpy).not.toHaveBeenCalled() @@ -291,15 +296,23 @@ describe('useSafeCreation', () => { }) it('should set a PROCESSING state when watching a tx', async () => { - renderHook(() => - useSafeCreation( - { ...mockPendingSafe, txHash: '0x123', tx: mockSafeInfo }, - mockSetPendingSafe, - mockStatus, - mockSetStatus, - false, - ), - ) + jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([ + { + ...mockPendingSafe, + txHash: '0x123', + tx: { + data: '0x', + from: '0x1234', + nonce: 0, + startBlock: 0, + to: '0x456', + value: BigNumber.from(0), + }, + }, + mockSetPendingSafe, + ]) + + renderHook(() => useSafeCreation(mockStatus, mockSetStatus, false)) await waitFor(() => { expect(mockSetStatus).toHaveBeenCalledWith(SafeCreationStatus.PROCESSING) @@ -308,14 +321,17 @@ describe('useSafeCreation', () => { it('should set a PROCESSING state and monitor relay taskId after successfully tx relay', async () => { jest.spyOn(logic, 'relaySafeCreation').mockResolvedValue('0x456') - + jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([ + { + ...mockPendingSafe, + }, + mockSetPendingSafe, + ]) const txMonitorSpy = jest.spyOn(txMonitor, 'waitForCreateSafeTx').mockImplementation(jest.fn()) const initialStatus = SafeCreationStatus.PROCESSING - renderHook(() => - useSafeCreation({ ...mockPendingSafe, tx: mockSafeInfo }, mockSetPendingSafe, initialStatus, mockSetStatus, true), - ) + renderHook(() => useSafeCreation(initialStatus, mockSetStatus, true)) await waitFor(() => { expect(mockSetStatus).toHaveBeenCalledWith(SafeCreationStatus.PROCESSING) diff --git a/src/components/new-safe/create/steps/StatusStep/__tests__/useSafeCreationEffects.test.ts b/src/components/new-safe/create/steps/StatusStep/__tests__/useSafeCreationEffects.test.ts index 456cbf8f6b..2ffa008d1b 100644 --- a/src/components/new-safe/create/steps/StatusStep/__tests__/useSafeCreationEffects.test.ts +++ b/src/components/new-safe/create/steps/StatusStep/__tests__/useSafeCreationEffects.test.ts @@ -3,15 +3,18 @@ import { SafeCreationStatus } from '@/components/new-safe/create/steps/StatusSte import { type SafeInfo } from '@safe-global/safe-gateway-typescript-sdk' import * as web3 from '@/hooks/wallets/web3' import * as pendingSafe from '@/components/new-safe/create/logic' +import * as usePendingSafe from '@/components/new-safe/create/steps/StatusStep/usePendingSafe' +import * as addressbook from '@/components/new-safe/create/logic/address-book' import { Web3Provider } from '@ethersproject/providers' -import type { PendingSafeData } from '@/components/new-safe/create/types' import useSafeCreationEffects from '@/components/new-safe/create/steps/StatusStep/useSafeCreationEffects' -import type { NamedAddress } from '@/components/new-safe/create/types' +import type { PendingSafeData } from '@/components/new-safe/create/types' +import { hexZeroPad } from 'ethers/lib/utils' describe('useSafeCreationEffects', () => { beforeEach(() => { jest.resetAllMocks() jest.spyOn(pendingSafe, 'pollSafeInfo').mockImplementation(jest.fn(() => Promise.resolve({} as SafeInfo))) + jest.spyOn(addressbook, 'updateAddressBook').mockReturnValue(() => {}) const mockProvider: Web3Provider = new Web3Provider(jest.fn()) jest.spyOn(web3, 'useWeb3').mockImplementation(() => mockProvider) @@ -20,12 +23,13 @@ describe('useSafeCreationEffects', () => { it('should clear the tx hash if it exists on ERROR or REVERTED', () => { const setStatusSpy = jest.fn() const setPendingSafeSpy = jest.fn() + jest + .spyOn(usePendingSafe, 'usePendingSafe') + .mockReturnValue([{ txHash: '0x123' } as PendingSafeData, setPendingSafeSpy]) renderHook(() => useSafeCreationEffects({ status: SafeCreationStatus.ERROR, - pendingSafe: { txHash: '0x10' } as PendingSafeData, - setPendingSafe: setPendingSafeSpy, setStatus: setStatusSpy, }), ) @@ -36,12 +40,10 @@ describe('useSafeCreationEffects', () => { it('should not clear the tx hash if it doesnt exist on ERROR or REVERTED', () => { const setStatusSpy = jest.fn() const setPendingSafeSpy = jest.fn() - + jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([{} as PendingSafeData, setPendingSafeSpy]) renderHook(() => useSafeCreationEffects({ status: SafeCreationStatus.ERROR, - pendingSafe: {} as PendingSafeData, - setPendingSafe: setPendingSafeSpy, setStatus: setStatusSpy, }), ) @@ -53,15 +55,12 @@ describe('useSafeCreationEffects', () => { const pollSafeInfoSpy = jest.spyOn(pendingSafe, 'pollSafeInfo') const setStatusSpy = jest.fn() const setPendingSafeSpy = jest.fn() - + jest + .spyOn(usePendingSafe, 'usePendingSafe') + .mockReturnValue([{ safeAddress: hexZeroPad('0x123', 20) } as PendingSafeData, setPendingSafeSpy]) renderHook(() => useSafeCreationEffects({ status: SafeCreationStatus.SUCCESS, - pendingSafe: { - safeAddress: '0x1', - owners: [] as NamedAddress[], - } as PendingSafeData, - setPendingSafe: setPendingSafeSpy, setStatus: setStatusSpy, }), ) @@ -73,12 +72,10 @@ describe('useSafeCreationEffects', () => { const pollSafeInfoSpy = jest.spyOn(pendingSafe, 'pollSafeInfo') const setStatusSpy = jest.fn() const setPendingSafeSpy = jest.fn() - + jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([{} as PendingSafeData, setPendingSafeSpy]) renderHook(() => useSafeCreationEffects({ status: SafeCreationStatus.SUCCESS, - pendingSafe: undefined, - setPendingSafe: setPendingSafeSpy, setStatus: setStatusSpy, }), ) diff --git a/src/components/new-safe/create/steps/StatusStep/index.tsx b/src/components/new-safe/create/steps/StatusStep/index.tsx index a40e1a38d9..255ad37572 100644 --- a/src/components/new-safe/create/steps/StatusStep/index.tsx +++ b/src/components/new-safe/create/steps/StatusStep/index.tsx @@ -4,13 +4,11 @@ import { useRouter } from 'next/router' import Track from '@/components/common/Track' import { CREATE_SAFE_EVENTS } from '@/services/analytics/events/createLoadSafe' -import useLocalStorage from '@/services/local-storage/useLocalStorage' import StatusMessage from '@/components/new-safe/create/steps/StatusStep/StatusMessage' import useWallet from '@/hooks/wallets/useWallet' import useIsWrongChain from '@/hooks/useIsWrongChain' import type { NewSafeFormData } from '@/components/new-safe/create' import type { StepRenderProps } from '@/components/new-safe/CardStepper/useCardStepper' -import type { PendingSafeTx } from '@/components/new-safe/create/types' import useSafeCreationEffects from '@/components/new-safe/create/steps/StatusStep/useSafeCreationEffects' import { SafeCreationStatus, useSafeCreation } from '@/components/new-safe/create/steps/StatusStep/useSafeCreation' import StatusStepper from '@/components/new-safe/create/steps/StatusStep/StatusStepper' @@ -20,43 +18,37 @@ import layoutCss from '@/components/new-safe/create/styles.module.css' import { AppRoutes } from '@/config/routes' import { lightPalette } from '@safe-global/safe-react-components' import { useCurrentChain } from '@/hooks/useChains' - -export const SAFE_PENDING_CREATION_STORAGE_KEY = 'pendingSafe' - -export type PendingSafeData = NewSafeFormData & { - txHash?: string - tx?: PendingSafeTx - taskId?: string -} +import { usePendingSafe } from './usePendingSafe' +import useSyncSafeCreationStep from '../../useSyncSafeCreationStep' export const getInitialCreationStatus = (willRelay: boolean): SafeCreationStatus => willRelay ? SafeCreationStatus.PROCESSING : SafeCreationStatus.AWAITING -export const CreateSafeStatus = ({ data, setProgressColor }: StepRenderProps) => { - const [pendingSafe, setPendingSafe] = useLocalStorage(SAFE_PENDING_CREATION_STORAGE_KEY) +export const CreateSafeStatus = ({ data, setProgressColor, setStep }: StepRenderProps) => { const router = useRouter() const chainInfo = useCurrentChain() const chainPrefix = chainInfo?.shortName || '' const wallet = useWallet() const isWrongChain = useIsWrongChain() const isConnected = wallet && !isWrongChain + const [pendingSafe, setPendingSafe] = usePendingSafe() + useSyncSafeCreationStep(setStep) // The willRelay flag can come from the previous step or from local storage const willRelay = !!(data.willRelay || pendingSafe?.willRelay) const initialStatus = getInitialCreationStatus(willRelay) const [status, setStatus] = useState(initialStatus) - const { handleCreateSafe } = useSafeCreation(pendingSafe, setPendingSafe, status, setStatus, willRelay) + const { handleCreateSafe } = useSafeCreation(status, setStatus, willRelay) useSafeCreationEffects({ - pendingSafe, - setPendingSafe, status, setStatus, }) const onClose = useCallback(() => { setPendingSafe(undefined) + router.push(AppRoutes.welcome) }, [router, setPendingSafe]) @@ -103,7 +95,7 @@ export const CreateSafeStatus = ({ data, setProgressColor }: StepRenderProps - + )} diff --git a/src/components/new-safe/create/steps/StatusStep/usePendingSafe.ts b/src/components/new-safe/create/steps/StatusStep/usePendingSafe.ts new file mode 100644 index 0000000000..08c3ac543e --- /dev/null +++ b/src/components/new-safe/create/steps/StatusStep/usePendingSafe.ts @@ -0,0 +1,29 @@ +import { useCurrentChain } from '@/hooks/useChains' +import useLocalStorage from '@/services/local-storage/useLocalStorage' +import { useCallback } from 'react' +import type { PendingSafeByChain, PendingSafeData } from '../../types' + +const SAFE_PENDING_CREATION_STORAGE_KEY = 'pendingSafe_v2' + +export const usePendingSafe = (): [PendingSafeData | undefined, (safe: PendingSafeData | undefined) => void] => { + const [pendingSafes, setPendingSafes] = useLocalStorage(SAFE_PENDING_CREATION_STORAGE_KEY) + + const chainInfo = useCurrentChain() + + const pendingSafe = chainInfo && pendingSafes?.[chainInfo.chainId] + const setPendingSafe = useCallback( + (safe: PendingSafeData | undefined) => { + if (!chainInfo?.chainId) { + return + } + + // Always copy the object because useLocalStorage does not check for deep equality when writing back to ls + const newPendingSafes = pendingSafes ? { ...pendingSafes } : {} + newPendingSafes[chainInfo.chainId] = safe + setPendingSafes(newPendingSafes) + }, + [chainInfo?.chainId, pendingSafes, setPendingSafes], + ) + + return [pendingSafe, setPendingSafe] +} diff --git a/src/components/new-safe/create/steps/StatusStep/useSafeCreation.ts b/src/components/new-safe/create/steps/StatusStep/useSafeCreation.ts index ec4823a5fa..ba0c44dfc7 100644 --- a/src/components/new-safe/create/steps/StatusStep/useSafeCreation.ts +++ b/src/components/new-safe/create/steps/StatusStep/useSafeCreation.ts @@ -4,7 +4,7 @@ import { useWeb3, useWeb3ReadOnly } from '@/hooks/wallets/web3' import { useCurrentChain } from '@/hooks/useChains' import useWallet from '@/hooks/wallets/useWallet' import type { EthersError } from '@/utils/ethers-utils' -import { getInitialCreationStatus, type PendingSafeData } from '@/components/new-safe/create/steps/StatusStep/index' +import { getInitialCreationStatus } from '@/components/new-safe/create/steps/StatusStep/index' import type { PendingSafeTx } from '@/components/new-safe/create/types' import { createNewSafe, @@ -24,6 +24,7 @@ import useGasPrice from '@/hooks/useGasPrice' import { hasFeature } from '@/utils/chains' import { FEATURES } from '@safe-global/safe-gateway-typescript-sdk' import type { DeploySafeProps } from '@safe-global/safe-core-sdk' +import { usePendingSafe } from './usePendingSafe' export enum SafeCreationStatus { AWAITING, @@ -38,8 +39,6 @@ export enum SafeCreationStatus { } export const useSafeCreation = ( - pendingSafe: PendingSafeData | undefined, - setPendingSafe: Dispatch>, status: SafeCreationStatus, setStatus: Dispatch>, willRelay: boolean, @@ -47,6 +46,7 @@ export const useSafeCreation = ( const [isCreating, setIsCreating] = useState(false) const [isWatching, setIsWatching] = useState(false) const dispatch = useAppDispatch() + const [pendingSafe, setPendingSafe] = usePendingSafe() const wallet = useWallet() const provider = useWeb3() @@ -63,9 +63,9 @@ export const useSafeCreation = ( async (txHash: string, tx: PendingSafeTx) => { setStatus(SafeCreationStatus.PROCESSING) trackEvent(CREATE_SAFE_EVENTS.SUBMIT_CREATE_SAFE) - setPendingSafe((prev) => (prev ? { ...prev, txHash, tx } : undefined)) + setPendingSafe(pendingSafe ? { ...pendingSafe, txHash, tx } : undefined) }, - [setStatus, setPendingSafe], + [setStatus, setPendingSafe, pendingSafe], ) const handleCreateSafe = useCallback(async () => { @@ -81,7 +81,7 @@ export const useSafeCreation = ( if (willRelay) { const taskId = await relaySafeCreation(chain, ownersAddresses, threshold, saltNonce) - setPendingSafe((prev) => (prev ? { ...prev, taskId } : undefined)) + setPendingSafe(pendingSafe ? { ...pendingSafe, taskId } : undefined) setStatus(SafeCreationStatus.PROCESSING) waitForCreateSafeTx(taskId, setStatus) } else { diff --git a/src/components/new-safe/create/steps/StatusStep/useSafeCreationEffects.ts b/src/components/new-safe/create/steps/StatusStep/useSafeCreationEffects.ts index 16cdcd7fc8..2d2d6128fb 100644 --- a/src/components/new-safe/create/steps/StatusStep/useSafeCreationEffects.ts +++ b/src/components/new-safe/create/steps/StatusStep/useSafeCreationEffects.ts @@ -6,21 +6,18 @@ import { CREATE_SAFE_EVENTS, trackEvent } from '@/services/analytics' import { updateAddressBook } from '@/components/new-safe/create/logic/address-book' import { useAppDispatch } from '@/store' import useChainId from '@/hooks/useChainId' -import type { PendingSafeData } from '@/components/new-safe/create/steps/StatusStep/index' +import { usePendingSafe } from './usePendingSafe' const useSafeCreationEffects = ({ - pendingSafe, - setPendingSafe, status, setStatus, }: { - pendingSafe: PendingSafeData | undefined - setPendingSafe: Dispatch> status: SafeCreationStatus setStatus: Dispatch> }) => { const dispatch = useAppDispatch() const chainId = useChainId() + const [pendingSafe, setPendingSafe] = usePendingSafe() useEffect(() => { if (status === SafeCreationStatus.SUCCESS) { @@ -58,7 +55,7 @@ const useSafeCreationEffects = ({ status === SafeCreationStatus.REVERTED ) { if (pendingSafe?.txHash) { - setPendingSafe((prev) => (prev ? { ...prev, txHash: undefined, tx: undefined } : undefined)) + setPendingSafe(pendingSafe ? { ...pendingSafe, txHash: undefined, tx: undefined } : undefined) } return } diff --git a/src/components/new-safe/create/types.d.ts b/src/components/new-safe/create/types.d.ts index bbc66177b9..0aedda9b54 100644 --- a/src/components/new-safe/create/types.d.ts +++ b/src/components/new-safe/create/types.d.ts @@ -1,4 +1,5 @@ import type { BigNumber } from 'ethers' +import type { NewSafeFormData } from '@/components/new-safe/create' export type NamedAddress = { name: string @@ -6,12 +7,6 @@ export type NamedAddress = { ens?: string } -// TODO: Split this type up for create and add safe since NamedAddress only makes sense when adding a safe -export type SafeFormData = NamedAddress & { - threshold: number - owners: NamedAddress[] -} - export type PendingSafeTx = { data: string from: string @@ -21,11 +16,10 @@ export type PendingSafeTx = { startBlock: number } -export type PendingSafeData = SafeFormData & { +export type PendingSafeData = NewSafeFormData & { txHash?: string tx?: PendingSafeTx - safeAddress?: string - saltNonce: number + taskId?: string } export type PendingSafeByChain = Record diff --git a/src/components/new-safe/create/useSyncSafeCreationStep.ts b/src/components/new-safe/create/useSyncSafeCreationStep.ts index c20ca3cbeb..6b4ad6ff94 100644 --- a/src/components/new-safe/create/useSyncSafeCreationStep.ts +++ b/src/components/new-safe/create/useSyncSafeCreationStep.ts @@ -1,25 +1,34 @@ import { useEffect } from 'react' -import useLocalStorage from '@/services/local-storage/useLocalStorage' import type { StepRenderProps } from '@/components/new-safe/CardStepper/useCardStepper' -import type { PendingSafeData } from '@/components/new-safe/create/steps/StatusStep' import type { NewSafeFormData } from '@/components/new-safe/create/index' -import { SAFE_PENDING_CREATION_STORAGE_KEY } from '@/components/new-safe/create/steps/StatusStep' import useWallet from '@/hooks/wallets/useWallet' +import { usePendingSafe } from './steps/StatusStep/usePendingSafe' +import useIsWrongChain from '@/hooks/useIsWrongChain' const useSyncSafeCreationStep = (setStep: StepRenderProps['setStep']) => { - const [pendingSafe] = useLocalStorage(SAFE_PENDING_CREATION_STORAGE_KEY) + const [pendingSafe] = usePendingSafe() const wallet = useWallet() + const isWrongChain = useIsWrongChain() useEffect(() => { + // Jump to the status screen if there is already a tx submitted + if (pendingSafe) { + setStep(4) + return + } + + // Jump to connect wallet step if there is no wallet and no pending Safe if (!wallet) { setStep(0) + return } - // Jump to the status screen if there is already a tx submitted - if (pendingSafe) { - setStep(4) + // Jump to choose name and network step if the wallet is connected to the wrong chain and there is no pending Safe + if (isWrongChain) { + setStep(1) + return } - }, [wallet, setStep, pendingSafe]) + }, [wallet, setStep, pendingSafe, isWrongChain]) } export default useSyncSafeCreationStep