From e4d6fe70896efffd56ab0a1dd3624f701a7c8506 Mon Sep 17 00:00:00 2001 From: Usame Algan Date: Fri, 2 Feb 2024 15:16:11 +0100 Subject: [PATCH] refactor: Move creation logic out of Review component --- .../new-safe/create/logic/index.test.ts | 359 +++++++++--------- .../create/steps/ReviewStep/index.tsx | 37 +- src/features/counterfactual/utils.ts | 50 ++- 3 files changed, 229 insertions(+), 217 deletions(-) diff --git a/src/components/new-safe/create/logic/index.test.ts b/src/components/new-safe/create/logic/index.test.ts index 652004ec79..368507bbb9 100644 --- a/src/components/new-safe/create/logic/index.test.ts +++ b/src/components/new-safe/create/logic/index.test.ts @@ -7,7 +7,6 @@ import { relaySafeCreation, handleSafeCreationError, } from '@/components/new-safe/create/logic/index' - import { type ErrorCode } from 'ethers' import { EthersTxReplacedReason } from '@/utils/ethers-utils' import { SafeCreationStatus } from '@/components/new-safe/create/steps/StatusStep/useSafeCreation' @@ -58,237 +57,235 @@ jest.mock('@safe-global/protocol-kit', () => { } }) -describe('create logic', () => { - describe('checkSafeCreationTx', () => { - let waitForTxSpy = jest.spyOn(provider, 'waitForTransaction') +describe('checkSafeCreationTx', () => { + let waitForTxSpy = jest.spyOn(provider, 'waitForTransaction') - beforeEach(() => { - jest.resetAllMocks() + beforeEach(() => { + jest.resetAllMocks() - jest.spyOn(web3, 'getWeb3ReadOnly').mockImplementation(() => provider) + jest.spyOn(web3, 'getWeb3ReadOnly').mockImplementation(() => provider) - waitForTxSpy = jest.spyOn(provider, 'waitForTransaction') - jest.spyOn(provider, 'getBlockNumber').mockReturnValue(Promise.resolve(4)) - jest.spyOn(provider, 'getTransaction').mockReturnValue(Promise.resolve(mockTransaction as TransactionResponse)) - }) + waitForTxSpy = jest.spyOn(provider, 'waitForTransaction') + jest.spyOn(provider, 'getBlockNumber').mockReturnValue(Promise.resolve(4)) + jest.spyOn(provider, 'getTransaction').mockReturnValue(Promise.resolve(mockTransaction as TransactionResponse)) + }) - it('returns SUCCESS if promise was resolved', async () => { - const receipt = { - status: 1, - } as TransactionReceipt + it('returns SUCCESS if promise was resolved', async () => { + const receipt = { + status: 1, + } as TransactionReceipt - waitForTxSpy.mockImplementationOnce(() => Promise.resolve(receipt)) + waitForTxSpy.mockImplementationOnce(() => Promise.resolve(receipt)) - const result = await checkSafeCreationTx(provider, mockPendingTx, '0x0', jest.fn()) + const result = await checkSafeCreationTx(provider, mockPendingTx, '0x0', jest.fn()) - expect(result).toBe(SafeCreationStatus.SUCCESS) - }) + expect(result).toBe(SafeCreationStatus.SUCCESS) + }) - it('returns REVERTED if transaction was reverted', async () => { - const receipt = { - status: 0, - } as TransactionReceipt + it('returns REVERTED if transaction was reverted', async () => { + const receipt = { + status: 0, + } as TransactionReceipt - waitForTxSpy.mockImplementationOnce(() => Promise.resolve(receipt)) + waitForTxSpy.mockImplementationOnce(() => Promise.resolve(receipt)) - const result = await checkSafeCreationTx(provider, mockPendingTx, '0x0', jest.fn()) + const result = await checkSafeCreationTx(provider, mockPendingTx, '0x0', jest.fn()) - expect(result).toBe(SafeCreationStatus.REVERTED) - }) + expect(result).toBe(SafeCreationStatus.REVERTED) + }) - it('returns TIMEOUT if transaction could not be found within the timeout limit', async () => { - const mockEthersError = { - ...new Error(), - code: 'TIMEOUT' as ErrorCode, - } + it('returns TIMEOUT if transaction could not be found within the timeout limit', async () => { + const mockEthersError = { + ...new Error(), + code: 'TIMEOUT' as ErrorCode, + } - waitForTxSpy.mockImplementationOnce(() => Promise.reject(mockEthersError)) + waitForTxSpy.mockImplementationOnce(() => Promise.reject(mockEthersError)) - const result = await checkSafeCreationTx(provider, mockPendingTx, '0x0', jest.fn()) + const result = await checkSafeCreationTx(provider, mockPendingTx, '0x0', jest.fn()) - expect(result).toBe(SafeCreationStatus.TIMEOUT) - }) + expect(result).toBe(SafeCreationStatus.TIMEOUT) + }) - it('returns SUCCESS if transaction was replaced', async () => { - const mockEthersError = { - ...new Error(), - code: 'TRANSACTION_REPLACED', - reason: 'repriced', - } - waitForTxSpy.mockImplementationOnce(() => Promise.reject(mockEthersError)) + it('returns SUCCESS if transaction was replaced', async () => { + const mockEthersError = { + ...new Error(), + code: 'TRANSACTION_REPLACED', + reason: 'repriced', + } + waitForTxSpy.mockImplementationOnce(() => Promise.reject(mockEthersError)) - const result = await checkSafeCreationTx(provider, mockPendingTx, '0x0', jest.fn()) + const result = await checkSafeCreationTx(provider, mockPendingTx, '0x0', jest.fn()) - expect(result).toBe(SafeCreationStatus.SUCCESS) - }) + expect(result).toBe(SafeCreationStatus.SUCCESS) + }) - it('returns ERROR if transaction was cancelled', async () => { - const mockEthersError = { - ...new Error(), - code: 'TRANSACTION_REPLACED', - reason: 'cancelled', - } - waitForTxSpy.mockImplementationOnce(() => Promise.reject(mockEthersError)) + it('returns ERROR if transaction was cancelled', async () => { + const mockEthersError = { + ...new Error(), + code: 'TRANSACTION_REPLACED', + reason: 'cancelled', + } + waitForTxSpy.mockImplementationOnce(() => Promise.reject(mockEthersError)) - const result = await checkSafeCreationTx(provider, mockPendingTx, '0x0', jest.fn()) + const result = await checkSafeCreationTx(provider, mockPendingTx, '0x0', jest.fn()) - expect(result).toBe(SafeCreationStatus.ERROR) - }) + expect(result).toBe(SafeCreationStatus.ERROR) }) +}) - describe('handleSafeCreationError', () => { - it('returns WALLET_REJECTED if the tx was rejected in the wallet', () => { - const mockEthersError = { - ...new Error(), - code: 'ACTION_REJECTED' as ErrorCode, - reason: '' as EthersTxReplacedReason, - receipt: {} as TransactionReceipt, - } +describe('handleSafeCreationError', () => { + it('returns WALLET_REJECTED if the tx was rejected in the wallet', () => { + const mockEthersError = { + ...new Error(), + code: 'ACTION_REJECTED' as ErrorCode, + reason: '' as EthersTxReplacedReason, + receipt: {} as TransactionReceipt, + } - const result = handleSafeCreationError(mockEthersError) + const result = handleSafeCreationError(mockEthersError) - expect(result).toEqual(SafeCreationStatus.WALLET_REJECTED) - }) + expect(result).toEqual(SafeCreationStatus.WALLET_REJECTED) + }) - it('returns WALLET_REJECTED if the tx was rejected via WC', () => { - const mockEthersError = { - ...new Error(), - code: 'UNKNOWN_ERROR' as ErrorCode, - reason: '' as EthersTxReplacedReason, - receipt: {} as TransactionReceipt, - message: 'rejected', - } + it('returns WALLET_REJECTED if the tx was rejected via WC', () => { + const mockEthersError = { + ...new Error(), + code: 'UNKNOWN_ERROR' as ErrorCode, + reason: '' as EthersTxReplacedReason, + receipt: {} as TransactionReceipt, + message: 'rejected', + } - const result = handleSafeCreationError(mockEthersError) + const result = handleSafeCreationError(mockEthersError) - expect(result).toEqual(SafeCreationStatus.WALLET_REJECTED) - }) + expect(result).toEqual(SafeCreationStatus.WALLET_REJECTED) + }) - it('returns ERROR if the tx was cancelled', () => { - const mockEthersError = { - ...new Error(), - code: 'TRANSACTION_REPLACED' as ErrorCode, - reason: EthersTxReplacedReason.cancelled, - receipt: {} as TransactionReceipt, - } + it('returns ERROR if the tx was cancelled', () => { + const mockEthersError = { + ...new Error(), + code: 'TRANSACTION_REPLACED' as ErrorCode, + reason: EthersTxReplacedReason.cancelled, + receipt: {} as TransactionReceipt, + } - const result = handleSafeCreationError(mockEthersError) + const result = handleSafeCreationError(mockEthersError) - expect(result).toEqual(SafeCreationStatus.ERROR) - }) + expect(result).toEqual(SafeCreationStatus.ERROR) + }) - it('returns SUCCESS if the tx was replaced', () => { - const mockEthersError = { - ...new Error(), - code: 'TRANSACTION_REPLACED' as ErrorCode, - reason: EthersTxReplacedReason.replaced, - receipt: {} as TransactionReceipt, - } + it('returns SUCCESS if the tx was replaced', () => { + const mockEthersError = { + ...new Error(), + code: 'TRANSACTION_REPLACED' as ErrorCode, + reason: EthersTxReplacedReason.replaced, + receipt: {} as TransactionReceipt, + } - const result = handleSafeCreationError(mockEthersError) + const result = handleSafeCreationError(mockEthersError) - expect(result).toEqual(SafeCreationStatus.SUCCESS) - }) + expect(result).toEqual(SafeCreationStatus.SUCCESS) + }) - it('returns SUCCESS if the tx was repriced', () => { - const mockEthersError = { - ...new Error(), - code: 'TRANSACTION_REPLACED' as ErrorCode, - reason: EthersTxReplacedReason.repriced, - receipt: {} as TransactionReceipt, - } + it('returns SUCCESS if the tx was repriced', () => { + const mockEthersError = { + ...new Error(), + code: 'TRANSACTION_REPLACED' as ErrorCode, + reason: EthersTxReplacedReason.repriced, + receipt: {} as TransactionReceipt, + } - const result = handleSafeCreationError(mockEthersError) + const result = handleSafeCreationError(mockEthersError) - expect(result).toEqual(SafeCreationStatus.SUCCESS) - }) + expect(result).toEqual(SafeCreationStatus.SUCCESS) + }) - it('returns ERROR if the tx was not rejected, cancelled or replaced', () => { - const mockEthersError = { - ...new Error(), - code: 'UNKNOWN_ERROR' as ErrorCode, - reason: '' as EthersTxReplacedReason, - receipt: {} as TransactionReceipt, - } + it('returns ERROR if the tx was not rejected, cancelled or replaced', () => { + const mockEthersError = { + ...new Error(), + code: 'UNKNOWN_ERROR' as ErrorCode, + reason: '' as EthersTxReplacedReason, + receipt: {} as TransactionReceipt, + } - const result = handleSafeCreationError(mockEthersError) + const result = handleSafeCreationError(mockEthersError) - expect(result).toEqual(SafeCreationStatus.ERROR) - }) + expect(result).toEqual(SafeCreationStatus.ERROR) + }) - it('returns REVERTED if the tx failed', () => { - const mockEthersError = { - ...new Error(), - code: 'UNKNOWN_ERROR' as ErrorCode, - reason: '' as EthersTxReplacedReason, - receipt: { - status: 0, - } as TransactionReceipt, - } + it('returns REVERTED if the tx failed', () => { + const mockEthersError = { + ...new Error(), + code: 'UNKNOWN_ERROR' as ErrorCode, + reason: '' as EthersTxReplacedReason, + receipt: { + status: 0, + } as TransactionReceipt, + } - const result = handleSafeCreationError(mockEthersError) + const result = handleSafeCreationError(mockEthersError) - expect(result).toEqual(SafeCreationStatus.REVERTED) - }) + expect(result).toEqual(SafeCreationStatus.REVERTED) }) +}) - describe('createNewSafeViaRelayer', () => { - const owner1 = toBeHex('0x1', 20) - const owner2 = toBeHex('0x2', 20) +describe('createNewSafeViaRelayer', () => { + const owner1 = toBeHex('0x1', 20) + const owner2 = toBeHex('0x2', 20) - const mockChainInfo = { - chainId: '5', - l2: false, - } as ChainInfo + const mockChainInfo = { + chainId: '5', + l2: false, + } as ChainInfo - beforeAll(() => { - jest.resetAllMocks() - jest.spyOn(web3, 'getWeb3ReadOnly').mockImplementation(() => provider) - }) + beforeAll(() => { + jest.resetAllMocks() + jest.spyOn(web3, 'getWeb3ReadOnly').mockImplementation(() => provider) + }) - it('returns taskId if create Safe successfully relayed', async () => { - const sponsoredCallSpy = jest.spyOn(relaying, 'sponsoredCall').mockResolvedValue({ taskId: '0x123' }) - - const expectedSaltNonce = 69 - const expectedThreshold = 1 - const proxyFactoryAddress = await (await getReadOnlyProxyFactoryContract('5', LATEST_SAFE_VERSION)).getAddress() - const readOnlyFallbackHandlerContract = await getReadOnlyFallbackHandlerContract('5', LATEST_SAFE_VERSION) - const safeContractAddress = await (await getReadOnlyGnosisSafeContract(mockChainInfo)).getAddress() - - const expectedInitializer = Gnosis_safe__factory.createInterface().encodeFunctionData('setup', [ - [owner1, owner2], - expectedThreshold, - ZERO_ADDRESS, - EMPTY_DATA, - await readOnlyFallbackHandlerContract.getAddress(), - ZERO_ADDRESS, - 0, - ZERO_ADDRESS, - ]) - - const expectedCallData = Proxy_factory__factory.createInterface().encodeFunctionData('createProxyWithNonce', [ - safeContractAddress, - expectedInitializer, - expectedSaltNonce, - ]) - - const taskId = await relaySafeCreation(mockChainInfo, [owner1, owner2], expectedThreshold, expectedSaltNonce) - - expect(taskId).toEqual('0x123') - expect(sponsoredCallSpy).toHaveBeenCalledTimes(1) - expect(sponsoredCallSpy).toHaveBeenCalledWith({ - chainId: '5', - to: proxyFactoryAddress, - data: expectedCallData, - }) + it('returns taskId if create Safe successfully relayed', async () => { + const sponsoredCallSpy = jest.spyOn(relaying, 'sponsoredCall').mockResolvedValue({ taskId: '0x123' }) + + const expectedSaltNonce = 69 + const expectedThreshold = 1 + const proxyFactoryAddress = await (await getReadOnlyProxyFactoryContract('5', LATEST_SAFE_VERSION)).getAddress() + const readOnlyFallbackHandlerContract = await getReadOnlyFallbackHandlerContract('5', LATEST_SAFE_VERSION) + const safeContractAddress = await (await getReadOnlyGnosisSafeContract(mockChainInfo)).getAddress() + + const expectedInitializer = Gnosis_safe__factory.createInterface().encodeFunctionData('setup', [ + [owner1, owner2], + expectedThreshold, + ZERO_ADDRESS, + EMPTY_DATA, + await readOnlyFallbackHandlerContract.getAddress(), + ZERO_ADDRESS, + 0, + ZERO_ADDRESS, + ]) + + const expectedCallData = Proxy_factory__factory.createInterface().encodeFunctionData('createProxyWithNonce', [ + safeContractAddress, + expectedInitializer, + expectedSaltNonce, + ]) + + const taskId = await relaySafeCreation(mockChainInfo, [owner1, owner2], expectedThreshold, expectedSaltNonce) + + expect(taskId).toEqual('0x123') + expect(sponsoredCallSpy).toHaveBeenCalledTimes(1) + expect(sponsoredCallSpy).toHaveBeenCalledWith({ + chainId: '5', + to: proxyFactoryAddress, + data: expectedCallData, }) + }) - it('should throw an error if relaying fails', () => { - const relayFailedError = new Error('Relay failed') + it('should throw an error if relaying fails', () => { + const relayFailedError = new Error('Relay failed') - jest.spyOn(relaying, 'sponsoredCall').mockRejectedValue(relayFailedError) + jest.spyOn(relaying, 'sponsoredCall').mockRejectedValue(relayFailedError) - expect(relaySafeCreation(mockChainInfo, [owner1, owner2], 1, 69)).rejects.toEqual(relayFailedError) - }) + expect(relaySafeCreation(mockChainInfo, [owner1, owner2], 1, 69)).rejects.toEqual(relayFailedError) }) }) diff --git a/src/components/new-safe/create/steps/ReviewStep/index.tsx b/src/components/new-safe/create/steps/ReviewStep/index.tsx index b29705e583..a5e5b3af4e 100644 --- a/src/components/new-safe/create/steps/ReviewStep/index.tsx +++ b/src/components/new-safe/create/steps/ReviewStep/index.tsx @@ -1,14 +1,9 @@ import { getAvailableSaltNonce } from '@/components/new-safe/create/logic/utils' import ErrorMessage from '@/components/tx/ErrorMessage' -import { AppRoutes } from '@/config/routes' -import { addUndeployedSafe } from '@/features/counterfactual/store/undeployedSafeSlice' +import { createCounterfactualSafe } from '@/features/counterfactual/utils' import useWalletCanPay from '@/hooks/useWalletCanPay' import { useAppDispatch } from '@/store' -import { addOrUpdateSafe } from '@/store/addedSafesSlice' -import { upsertAddressBookEntry } from '@/store/addressBookSlice' -import { defaultSafeInfo } from '@/store/safeInfoSlice' import { FEATURES } from '@/utils/chains' -import type { SafeVersion } from '@safe-global/safe-core-sdk-types' import { useRouter } from 'next/router' import { useMemo, useState } from 'react' import { Button, Grid, Typography, Divider, Box, Alert } from '@mui/material' @@ -169,35 +164,7 @@ const ReviewStep = ({ data, onSubmit, onBack, setStep }: StepRenderProps ({ - value: owner.address, - name: owner.name || owner.ens, - })), - chainId: chain.chainId, - }, - }), - ) - router.push({ pathname: AppRoutes.home, query: { safe: `${chain.shortName}:${safeAddress}` } }) + createCounterfactualSafe(chain, safeAddress, saltNonce, data, dispatch, props, router) return } diff --git a/src/features/counterfactual/utils.ts b/src/features/counterfactual/utils.ts index 1eebef7403..0430b54f01 100644 --- a/src/features/counterfactual/utils.ts +++ b/src/features/counterfactual/utils.ts @@ -1,7 +1,14 @@ +import type { NewSafeFormData } from '@/components/new-safe/create' import { LATEST_SAFE_VERSION } from '@/config/constants' +import { AppRoutes } from '@/config/routes' +import { addUndeployedSafe } from '@/features/counterfactual/store/undeployedSafeSlice' +import type { AppDispatch } from '@/store' +import { addOrUpdateSafe } from '@/store/addedSafesSlice' +import { upsertAddressBookEntry } from '@/store/addressBookSlice' import { defaultSafeInfo } from '@/store/safeInfoSlice' -import type { PredictedSafeProps } from '@safe-global/protocol-kit' +import type { DeploySafeProps, PredictedSafeProps } from '@safe-global/protocol-kit' import { ZERO_ADDRESS } from '@safe-global/protocol-kit/dist/src/utils/constants' +import type { SafeVersion } from '@safe-global/safe-core-sdk-types' import { type ChainInfo, ImplementationVersionState, @@ -9,6 +16,7 @@ import { TokenType, } from '@safe-global/safe-gateway-typescript-sdk' import type { BrowserProvider } from 'ethers' +import type { NextRouter } from 'next/router' export const getUndeployedSafeInfo = (undeployedSafe: PredictedSafeProps, address: string, chainId: string) => { return Promise.resolve({ @@ -46,3 +54,43 @@ export const getCounterfactualBalance = async (safeAddress: string, provider?: B ], } } + +export const createCounterfactualSafe = ( + chain: ChainInfo, + safeAddress: string, + saltNonce: string, + data: NewSafeFormData, + dispatch: AppDispatch, + props: DeploySafeProps, + router: NextRouter, +) => { + const undeployedSafe = { + chainId: chain.chainId, + address: safeAddress, + safeProps: { + safeAccountConfig: props.safeAccountConfig, + safeDeploymentConfig: { + saltNonce, + safeVersion: LATEST_SAFE_VERSION as SafeVersion, + }, + }, + } + + dispatch(addUndeployedSafe(undeployedSafe)) + dispatch(upsertAddressBookEntry({ chainId: chain.chainId, address: safeAddress, name: data.name })) + dispatch( + addOrUpdateSafe({ + safe: { + ...defaultSafeInfo, + address: { value: safeAddress, name: data.name }, + threshold: data.threshold, + owners: data.owners.map((owner) => ({ + value: owner.address, + name: owner.name || owner.ens, + })), + chainId: chain.chainId, + }, + }), + ) + router.push({ pathname: AppRoutes.home, query: { safe: `${chain.shortName}:${safeAddress}` } }) +}