From db7b91c99fd8e6e714572963320995830904058f Mon Sep 17 00:00:00 2001 From: Usame Algan Date: Thu, 6 Jul 2023 15:23:48 +0200 Subject: [PATCH] fix: Write tests for ApprovalEditor --- .../tx/ApprovalEditor/ApprovalEditor.test.tsx | 314 ++++-------------- .../ApprovalEditorForm.test.tsx | 110 ++++++ .../hooks/useApprovalInfos.test.ts | 147 ++++++++ .../ApprovalEditor/hooks/useApprovalInfos.ts | 24 +- src/components/tx/ApprovalEditor/index.tsx | 9 +- .../security/modules/ApprovalModule/index.ts | 6 +- 6 files changed, 344 insertions(+), 266 deletions(-) create mode 100644 src/components/tx/ApprovalEditor/ApprovalEditorForm.test.tsx create mode 100644 src/components/tx/ApprovalEditor/hooks/useApprovalInfos.test.ts diff --git a/src/components/tx/ApprovalEditor/ApprovalEditor.test.tsx b/src/components/tx/ApprovalEditor/ApprovalEditor.test.tsx index 82eb20b2b1..993deb5bba 100644 --- a/src/components/tx/ApprovalEditor/ApprovalEditor.test.tsx +++ b/src/components/tx/ApprovalEditor/ApprovalEditor.test.tsx @@ -1,253 +1,85 @@ -import { fireEvent, getAllByRole, render, waitFor } from '@/tests/test-utils' +import { render } from '@/tests/test-utils' import ApprovalEditor from '.' import { TokenType } from '@safe-global/safe-gateway-typescript-sdk' -import { hexZeroPad, Interface } from 'ethers/lib/utils' -import { ERC20__factory, Multi_send_call_only__factory } from '@/types/contracts' -import type { BaseTransaction } from '@safe-global/safe-apps-sdk' -import { encodeMultiSendData } from '@safe-global/safe-core-sdk/dist/src/utils/transactions/utils' -import { getMultiSendCallOnlyContractAddress } from '@/services/contracts/safeContracts' -import { type SafeSignature, type SafeTransaction } from '@safe-global/safe-core-sdk-types' -import { getAllByTestId } from '@testing-library/dom' -import { ApprovalEditorForm } from '@/components/tx/ApprovalEditor/ApprovalEditorForm' -import Approvals from '@/components/tx/ApprovalEditor/Approvals' - -const ERC20_INTERFACE = ERC20__factory.createInterface() - -const createNonApproveCallData = (to: string, value: string) => { - return ERC20_INTERFACE.encodeFunctionData('transfer', [to, value]) -} - -const renderEditor = async (txs: BaseTransaction[], updateTxs?: (newTxs: BaseTransaction[]) => void) => { - if (txs.length === 0) { - // eslint-disable-next-line react/display-name - return () => - } - - let txData: string - let to: string - if (txs.length > 1) { - const multiSendCallData = encodeMultiSendData(txs.map((tx) => ({ ...tx, operation: 0 }))) - txData = Multi_send_call_only__factory.createInterface().encodeFunctionData('multiSend', [multiSendCallData]) - to = getMultiSendCallOnlyContractAddress('1') || '0x1' - } else { - txData = txs[0].data - to = txs[0].to - } - - const safeTx: SafeTransaction = { - data: { - to, - data: txData, - baseGas: 0, - gasPrice: 0, - gasToken: '0x0', - nonce: 1, - operation: txs.length > 1 ? 1 : 0, - refundReceiver: '0x0', - safeTxGas: 0, - value: '0x0', - }, - signatures: new Map(), - addSignature: function (signature: SafeSignature): void { - throw new Error('Function not implemented.') - }, - encodedSignatures: function (): string { - throw new Error('Function not implemented.') - }, - } - // eslint-disable-next-line react/display-name - return () => -} +import { OperationType } from '@safe-global/safe-core-sdk-types' +import * as approvalInfos from '@/components/tx/ApprovalEditor/hooks/useApprovalInfos' +import { createMockSafeTransaction } from '@/tests/transactions' describe('ApprovalEditor', () => { beforeEach(() => { jest.clearAllMocks() - localStorage.clear() }) - // Edit mode is used in the ReviewSafeAppsTxModal - // There we pass in an array of BaseTransactions. - describe('in edit mode', () => { - const updateCallback = jest.fn() - - describe('should render null', () => { - it('for empty txs', async () => { - const Editor = await renderEditor([], updateCallback) - const result = render() - expect(result.container).toBeEmptyDOMElement() - }) - - it('for a single tx containing an approve call with wrong params', async () => { - const testInterface = new Interface(['function approve(address, uint256, uint8)']) - const txs = [ - { - to: hexZeroPad('0x123', 20), - data: testInterface.encodeFunctionData('approve', [hexZeroPad('0x2', 20), '123', '1']), - value: '0', - }, - ] - const Editor = await renderEditor(txs, updateCallback) - const result = render() - expect(result.container).toBeEmptyDOMElement() - }) - - it('for multiple non approve txs', async () => { - const txs = [ - { - to: hexZeroPad('0x123', 20), - data: createNonApproveCallData(hexZeroPad('0x2', 20), '200'), - value: '0', - }, - { - to: hexZeroPad('0x123', 20), - data: createNonApproveCallData(hexZeroPad('0x3', 20), '12'), - value: '0', - }, - ] - const Editor = await renderEditor(txs, updateCallback) - - const result = render() - expect(result.container).toBeEmptyDOMElement() - }) - }) - - it('should render and edit multiple txs', async () => { - const tokenAddress1 = hexZeroPad('0x123', 20) - const tokenAddress2 = hexZeroPad('0x234', 20) - - const mockApprovalInfos = [ - { - tokenInfo: { symbol: 'TST', decimals: 18, address: tokenAddress1, type: TokenType.ERC20 }, - tokenAddress: '0x1', - spender: '0x2', - amount: '4200000', - amountFormatted: '420.0', - }, - { - tokenInfo: { symbol: 'TST', decimals: 18, address: tokenAddress2, type: TokenType.ERC20 }, - tokenAddress: '0x1', - spender: '0x2', - amount: '6900000', - amountFormatted: '69.0', - }, - ] - - const result = render() - - // All approvals are rendered - const approvalItems = getAllByTestId(result.container, 'approval-item') - expect(approvalItems).toHaveLength(2) - - // One button for each approval - const buttons = getAllByRole(result.container, 'button') - expect(buttons).toHaveLength(2) - - // First approval value is rendered - await waitFor(() => { - const amountInput = result.container.querySelector('input[name="approvals.0"]') as HTMLInputElement - expect(amountInput).not.toBeNull() - expect(amountInput).toHaveValue('420.0') - expect(amountInput).toBeEnabled() - }) - - // Change value of first approval - const amountInput1 = result.container.querySelector('input[name="approvals.0"]') as HTMLInputElement - fireEvent.change(amountInput1!, { target: { value: '123' } }) - fireEvent.click(buttons[0]) - - expect(updateCallback).toHaveBeenCalledWith(['123', '69.0']) - - // Second approval value is rendered - await waitFor(() => { - const amountInput = result.container.querySelector('input[name="approvals.1"]') as HTMLInputElement - expect(amountInput).not.toBeNull() - expect(amountInput).toHaveValue('69.0') - expect(amountInput).toBeEnabled() - }) - - // Change value of second approval - const amountInput2 = result.container.querySelector('input[name="approvals.1"]') as HTMLInputElement - fireEvent.change(amountInput2!, { target: { value: '456' } }) - fireEvent.click(buttons[1]) - - expect(updateCallback).toHaveBeenCalledWith(['123', '456']) - }) - - it('should render and edit single tx', async () => { - const tokenAddress = hexZeroPad('0x123', 20) - - const mockApprovalInfo = { - tokenInfo: { symbol: 'TST', decimals: 18, address: tokenAddress, type: TokenType.ERC20 }, - tokenAddress: '0x1', - spender: '0x2', - amount: '4200000', - amountFormatted: '420.0', - } - - const result = render() - - // Approval item is rendered - const approvalItem = result.getByTestId('approval-item') - expect(approvalItem).not.toBeNull() - - // Input with correct value is rendered - await waitFor(() => { - const amountInput = result.container.querySelector('input[name="approvals.0"]') as HTMLInputElement - expect(amountInput).not.toBeNull() - expect(amountInput).toHaveValue('420.0') - expect(amountInput).toBeEnabled() - }) - - // Change value and save - const amountInput = result.container.querySelector('input[name="approvals.0"]') as HTMLInputElement - const saveButton = result.getByRole('button') - - fireEvent.change(amountInput!, { target: { value: '100' } }) - fireEvent.click(saveButton) - - expect(updateCallback).toHaveBeenCalledWith(['100']) - }) + it('returns null if there is no safe transaction', () => { + const result = render() + + expect(result.container).toBeEmptyDOMElement() }) - // Readonly mode is used in the confirmationsModal - // It passes decodedTxData and txDetails instead of an array of base transactions and no update function - describe('in readonly mode', () => { - describe('should render null', () => { - it('for a single tx containing no approve call', async () => { - const txs: BaseTransaction[] = [ - { - to: hexZeroPad('0x123', 20), - data: createNonApproveCallData(hexZeroPad('0x2', 20), '20'), - value: '420', - }, - ] - const Editor = await renderEditor(txs) - const result = render() - expect(result.container).toBeEmptyDOMElement() - }) - - describe('should render approval(s)', () => { - it('for single approval tx of token in balances', async () => { - const tokenAddress = hexZeroPad('0x123', 20) - - const mockApprovalInfo = { - tokenInfo: { symbol: 'TST', decimals: 18, address: tokenAddress, type: TokenType.ERC20 }, - tokenAddress: '0x1', - spender: '0x2', - amount: '100', - amountFormatted: '0.1', - } - - const result = render() - - const approvalItem = result.getByTestId('approval-item') - - expect(approvalItem).toBeInTheDocument() - expect(approvalItem).toHaveTextContent('Token') - expect(approvalItem).toHaveTextContent('TST') - expect(approvalItem).toHaveTextContent('0.1') - }) - }) - }) + it('returns null if there are no approvals', () => { + const mockSafeTx = createMockSafeTransaction({ to: '0x1', data: '0x', operation: OperationType.DelegateCall }) + jest.spyOn(approvalInfos, 'useApprovalInfos').mockReturnValue([[], undefined, false]) + const result = render() + + expect(result.container).toBeEmptyDOMElement() + }) + + it('renders an error', async () => { + jest + .spyOn(approvalInfos, 'useApprovalInfos') + .mockReturnValue([undefined, new Error('Error parsing approvals'), false]) + const mockSafeTx = createMockSafeTransaction({ to: '0x1', data: '0x', operation: OperationType.DelegateCall }) + + const result = render() + + expect(await result.queryByText('Error while decoding approval transactions.')).toBeInTheDocument() + }) + + it('renders a loading skeleton', async () => { + jest.spyOn(approvalInfos, 'useApprovalInfos').mockReturnValue([undefined, undefined, true]) + const mockSafeTx = createMockSafeTransaction({ to: '0x1', data: '0x', operation: OperationType.DelegateCall }) + + const result = render() + + expect(await result.queryByTestId('approval-editor-loading')).toBeInTheDocument() + }) + + it('renders a read-only view if there is no update callback', async () => { + const mockApprovalInfo = { + tokenInfo: { symbol: 'TST', decimals: 18, address: '0x3', type: TokenType.ERC20 }, + tokenAddress: '0x1', + spender: '0x2', + amount: '4200000', + amountFormatted: '420.0', + } + jest.spyOn(approvalInfos, 'useApprovalInfos').mockReturnValue([[mockApprovalInfo], undefined, false]) + const mockSafeTx = createMockSafeTransaction({ to: '0x1', data: '0x', operation: OperationType.DelegateCall }) + + const result = render() + + const amountInput = result.container.querySelector('input[name="approvals.0"]') as HTMLInputElement + + expect(amountInput).not.toBeInTheDocument() + expect(result.getByText('TST')) + expect(result.getByText('420')) + expect(result.getByText('0x2')) + }) + + it('renders a form if there is an update callback', async () => { + const mockApprovalInfo = { + tokenInfo: { symbol: 'TST', decimals: 18, address: '0x3', type: TokenType.ERC20 }, + tokenAddress: '0x1', + spender: '0x2', + amount: '4200000', + amountFormatted: '420.0', + } + jest.spyOn(approvalInfos, 'useApprovalInfos').mockReturnValue([[mockApprovalInfo], undefined, false]) + const mockSafeTx = createMockSafeTransaction({ to: '0x1', data: '0x', operation: OperationType.DelegateCall }) + + const result = render() + + const amountInput = result.container.querySelector('input[name="approvals.0"]') as HTMLInputElement + + expect(amountInput).toBeInTheDocument() }) }) diff --git a/src/components/tx/ApprovalEditor/ApprovalEditorForm.test.tsx b/src/components/tx/ApprovalEditor/ApprovalEditorForm.test.tsx new file mode 100644 index 0000000000..2b7650baa7 --- /dev/null +++ b/src/components/tx/ApprovalEditor/ApprovalEditorForm.test.tsx @@ -0,0 +1,110 @@ +import { fireEvent, getAllByRole, render, waitFor } from '@/tests/test-utils' +import { hexZeroPad } from 'ethers/lib/utils' +import { TokenType } from '@safe-global/safe-gateway-typescript-sdk' +import { ApprovalEditorForm } from '@/components/tx/ApprovalEditor/ApprovalEditorForm' +import { getAllByTestId } from '@testing-library/dom' + +describe('ApprovalEditorForm', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + const updateCallback = jest.fn() + + it('should render and edit multiple txs', async () => { + const tokenAddress1 = hexZeroPad('0x123', 20) + const tokenAddress2 = hexZeroPad('0x234', 20) + + const mockApprovalInfos = [ + { + tokenInfo: { symbol: 'TST', decimals: 18, address: tokenAddress1, type: TokenType.ERC20 }, + tokenAddress: '0x1', + spender: '0x2', + amount: '4200000', + amountFormatted: '420.0', + }, + { + tokenInfo: { symbol: 'TST', decimals: 18, address: tokenAddress2, type: TokenType.ERC20 }, + tokenAddress: '0x1', + spender: '0x2', + amount: '6900000', + amountFormatted: '69.0', + }, + ] + + const result = render() + + // All approvals are rendered + const approvalItems = getAllByTestId(result.container, 'approval-item') + expect(approvalItems).toHaveLength(2) + + // One button for each approval + const buttons = getAllByRole(result.container, 'button') + expect(buttons).toHaveLength(2) + + // First approval value is rendered + await waitFor(() => { + const amountInput = result.container.querySelector('input[name="approvals.0"]') as HTMLInputElement + expect(amountInput).not.toBeNull() + expect(amountInput).toHaveValue('420.0') + expect(amountInput).toBeEnabled() + }) + + // Change value of first approval + const amountInput1 = result.container.querySelector('input[name="approvals.0"]') as HTMLInputElement + fireEvent.change(amountInput1!, { target: { value: '123' } }) + fireEvent.click(buttons[0]) + + expect(updateCallback).toHaveBeenCalledWith(['123', '69.0']) + + // Second approval value is rendered + await waitFor(() => { + const amountInput = result.container.querySelector('input[name="approvals.1"]') as HTMLInputElement + expect(amountInput).not.toBeNull() + expect(amountInput).toHaveValue('69.0') + expect(amountInput).toBeEnabled() + }) + + // Change value of second approval + const amountInput2 = result.container.querySelector('input[name="approvals.1"]') as HTMLInputElement + fireEvent.change(amountInput2!, { target: { value: '456' } }) + fireEvent.click(buttons[1]) + + expect(updateCallback).toHaveBeenCalledWith(['123', '456']) + }) + + it('should render and edit single tx', async () => { + const tokenAddress = hexZeroPad('0x123', 20) + + const mockApprovalInfo = { + tokenInfo: { symbol: 'TST', decimals: 18, address: tokenAddress, type: TokenType.ERC20 }, + tokenAddress: '0x1', + spender: '0x2', + amount: '4200000', + amountFormatted: '420.0', + } + + const result = render() + + // Approval item is rendered + const approvalItem = result.getByTestId('approval-item') + expect(approvalItem).not.toBeNull() + + // Input with correct value is rendered + await waitFor(() => { + const amountInput = result.container.querySelector('input[name="approvals.0"]') as HTMLInputElement + expect(amountInput).not.toBeNull() + expect(amountInput).toHaveValue('420.0') + expect(amountInput).toBeEnabled() + }) + + // Change value and save + const amountInput = result.container.querySelector('input[name="approvals.0"]') as HTMLInputElement + const saveButton = result.getByRole('button') + + fireEvent.change(amountInput!, { target: { value: '100' } }) + fireEvent.click(saveButton) + + expect(updateCallback).toHaveBeenCalledWith(['100']) + }) +}) diff --git a/src/components/tx/ApprovalEditor/hooks/useApprovalInfos.test.ts b/src/components/tx/ApprovalEditor/hooks/useApprovalInfos.test.ts new file mode 100644 index 0000000000..63b6112702 --- /dev/null +++ b/src/components/tx/ApprovalEditor/hooks/useApprovalInfos.test.ts @@ -0,0 +1,147 @@ +import { renderHook } from '@/tests/test-utils' +import { hexZeroPad, Interface } from 'ethers/lib/utils' +import { useApprovalInfos } from '@/components/tx/ApprovalEditor/hooks/useApprovalInfos' +import { waitFor } from '@testing-library/react' +import { createMockSafeTransaction } from '@/tests/transactions' +import { OperationType } from '@safe-global/safe-core-sdk-types' +import { ERC20__factory } from '@/types/contracts' +import { type ApprovalInfo } from '@/components/tx/ApprovalEditor/utils/approvals' +import * as balances from '@/hooks/useBalances' +import { TokenType } from '@safe-global/safe-gateway-typescript-sdk' +import { BigNumber } from '@ethersproject/bignumber' +import * as getTokenInfo from '@/utils/tokens' + +const ERC20_INTERFACE = ERC20__factory.createInterface() + +const createNonApproveCallData = (to: string, value: string) => { + return ERC20_INTERFACE.encodeFunctionData('transfer', [to, value]) +} + +describe('useApprovalInfos', () => { + beforeEach(() => { + jest.restoreAllMocks() + }) + + it('returns undefined if no Safe Transaction exists', async () => { + const { result } = renderHook(() => useApprovalInfos(undefined)) + + expect(result.current).toStrictEqual([undefined, undefined, true]) + + await waitFor(() => { + expect(result.current).toStrictEqual([undefined, undefined, false]) + }) + }) + + it('returns an empty array if the transaction does not contain any approvals', async () => { + const mockSafeTx = createMockSafeTransaction({ + to: hexZeroPad('0x123', 20), + data: createNonApproveCallData(hexZeroPad('0x2', 20), '20'), + operation: OperationType.DelegateCall, + }) + + const { result } = renderHook(() => useApprovalInfos(mockSafeTx)) + + await waitFor(() => { + expect(result.current).toStrictEqual([[], undefined, false]) + }) + }) + + it('returns an ApprovalInfo if the transaction contains an approval', async () => { + const testInterface = new Interface(['function approve(address, uint256)']) + + const mockSafeTx = createMockSafeTransaction({ + to: hexZeroPad('0x123', 20), + data: testInterface.encodeFunctionData('approve', [hexZeroPad('0x2', 20), '123']), + operation: OperationType.DelegateCall, + }) + + const { result } = renderHook(() => useApprovalInfos(mockSafeTx)) + + const mockApproval: ApprovalInfo = { + amount: BigNumber.from('123'), + amountFormatted: '0.000000000000000123', + spender: '0x0000000000000000000000000000000000000002', + tokenAddress: '0x0000000000000000000000000000000000000123', + tokenInfo: undefined, + } + + await waitFor(() => { + expect(result.current).toEqual([[mockApproval], undefined, false]) + }) + }) + + it('returns an ApprovalInfo with token infos if the token exists in balances', async () => { + const mockBalanceItem = { + balance: '40', + fiatBalance: '40', + fiatConversion: '1', + tokenInfo: { + address: hexZeroPad('0x123', 20), + decimals: 18, + logoUri: '', + name: 'Hidden Token', + symbol: 'HT', + type: TokenType.ERC20, + }, + } + + jest + .spyOn(balances, 'default') + .mockReturnValue({ balances: { fiatTotal: '0', items: [mockBalanceItem] }, error: undefined, loading: false }) + const testInterface = new Interface(['function approve(address, uint256)']) + + const mockSafeTx = createMockSafeTransaction({ + to: hexZeroPad('0x123', 20), + data: testInterface.encodeFunctionData('approve', [hexZeroPad('0x2', 20), '123']), + operation: OperationType.DelegateCall, + }) + + const { result } = renderHook(() => useApprovalInfos(mockSafeTx)) + + const mockApproval: ApprovalInfo = { + amount: BigNumber.from('123'), + amountFormatted: '0.000000000000000123', + spender: '0x0000000000000000000000000000000000000002', + tokenAddress: '0x0000000000000000000000000000000000000123', + tokenInfo: mockBalanceItem.tokenInfo, + } + + await waitFor(() => { + expect(result.current).toEqual([[mockApproval], undefined, false]) + }) + }) + + it('fetches token info for an approval if its missing', async () => { + const mockTokenInfo = { + address: '0x0000000000000000000000000000000000000123', + symbol: 'HT', + decimals: 18, + type: TokenType.ERC20, + } + const fetchMock = jest + .spyOn(getTokenInfo, 'getERC20TokenInfoOnChain') + .mockReturnValue(Promise.resolve(mockTokenInfo)) + const testInterface = new Interface(['function approve(address, uint256)']) + + const mockSafeTx = createMockSafeTransaction({ + to: hexZeroPad('0x123', 20), + data: testInterface.encodeFunctionData('approve', [hexZeroPad('0x2', 20), '123']), + operation: OperationType.DelegateCall, + }) + + const { result } = renderHook(() => useApprovalInfos(mockSafeTx)) + + const mockApproval: ApprovalInfo = { + amount: BigNumber.from('123'), + amountFormatted: '0.000000000000000123', + spender: '0x0000000000000000000000000000000000000002', + tokenAddress: '0x0000000000000000000000000000000000000123', + tokenInfo: mockTokenInfo, + } + + await waitFor(() => { + expect(result.current).toEqual([[mockApproval], undefined, false]) + expect(fetchMock).toHaveBeenCalledTimes(1) + }) + }) +}) diff --git a/src/components/tx/ApprovalEditor/hooks/useApprovalInfos.ts b/src/components/tx/ApprovalEditor/hooks/useApprovalInfos.ts index a53323ac7f..2479996e16 100644 --- a/src/components/tx/ApprovalEditor/hooks/useApprovalInfos.ts +++ b/src/components/tx/ApprovalEditor/hooks/useApprovalInfos.ts @@ -1,7 +1,6 @@ import useAsync from '@/hooks/useAsync' import useBalances from '@/hooks/useBalances' -import { ApprovalModule, type ApprovalModuleResponse } from '@/services/security/modules/ApprovalModule' -import type { SecurityResponse } from '@/services/security/modules/types' +import { ApprovalModule } from '@/services/security/modules/ApprovalModule' import { getERC20TokenInfoOnChain, UNLIMITED_APPROVAL_AMOUNT } from '@/utils/tokens' import { type SafeTransaction } from '@safe-global/safe-core-sdk-types' import { type TokenInfo } from '@safe-global/safe-gateway-typescript-sdk' @@ -18,24 +17,15 @@ export type ApprovalInfo = { const ApprovalModuleInstance = new ApprovalModule() -const useApprovalData = (safeTransaction: SafeTransaction | undefined) => { - return useAsync>(() => { - if (!safeTransaction) { - return - } - - return ApprovalModuleInstance.scanTransaction({ safeTransaction }) - }, [safeTransaction]) -} - -// TODO: Write tests for this hook export const useApprovalInfos = (safeTransaction: SafeTransaction | undefined) => { - const [approvals] = useApprovalData(safeTransaction) - const { balances } = useBalances() - return useAsync( + return useAsync( async () => { + if (!safeTransaction) return + + const approvals = await ApprovalModuleInstance.scanTransaction({ safeTransaction }) + if (!approvals || !approvals.payload || approvals.payload.length === 0) return Promise.resolve([]) return Promise.all( @@ -55,7 +45,7 @@ export const useApprovalInfos = (safeTransaction: SafeTransaction | undefined) = }), ) }, - [balances.items.length, approvals], + [safeTransaction, balances.items.length], false, // Do not clear data on balance updates ) } diff --git a/src/components/tx/ApprovalEditor/index.tsx b/src/components/tx/ApprovalEditor/index.tsx index 88ba00f6de..c0655cf04d 100644 --- a/src/components/tx/ApprovalEditor/index.tsx +++ b/src/components/tx/ApprovalEditor/index.tsx @@ -25,7 +25,6 @@ const Title = () => { ) } -// TODO: Write tests for this component export const ApprovalEditor = ({ safeTransaction, updateTransaction, @@ -35,15 +34,15 @@ export const ApprovalEditor = ({ }) => { const [readableApprovals, error, loading] = useApprovalInfos(safeTransaction) - if (!readableApprovals || readableApprovals.length === 0 || !safeTransaction) { + if (readableApprovals?.length === 0 || !safeTransaction) { return null } - const extractedTxs = decodeSafeTxToBaseTransactions(safeTransaction) - const updateApprovals = (approvals: string[]) => { if (!updateTransaction) return + const extractedTxs = decodeSafeTxToBaseTransactions(safeTransaction) + const updatedTxs = updateApprovalTxs(approvals, readableApprovals, extractedTxs) updateTransaction(updatedTxs) } @@ -56,7 +55,7 @@ export const ApprovalEditor = ({ {error ? ( Error while decoding approval transactions. ) : loading || !readableApprovals ? ( - + ) : isReadOnly ? ( ) : ( diff --git a/src/services/security/modules/ApprovalModule/index.ts b/src/services/security/modules/ApprovalModule/index.ts index b2b8087da6..a14b9ddfac 100644 --- a/src/services/security/modules/ApprovalModule/index.ts +++ b/src/services/security/modules/ApprovalModule/index.ts @@ -21,7 +21,7 @@ const MULTISEND_SIGNATURE_HASH = id('multiSend(bytes)').slice(0, 10) const ERC20_INTERFACE = ERC20__factory.createInterface() export class ApprovalModule implements SecurityModule { - private scanInnerTransaction(txPartial: { to: string; data: string }): Approval[] { + private static scanInnerTransaction(txPartial: { to: string; data: string }): Approval[] { if (txPartial.data.startsWith(APPROVAL_SIGNATURE_HASH)) { const [spender, amount] = ERC20_INTERFACE.decodeFunctionData('approve', txPartial.data) return [ @@ -42,9 +42,9 @@ export class ApprovalModule implements SecurityModule this.scanInnerTransaction(tx))) + approvalInfos.push(...innerTxs.flatMap((tx) => ApprovalModule.scanInnerTransaction(tx))) } else { - approvalInfos.push(...this.scanInnerTransaction({ to: safeTransaction.data.to, data: safeTxData })) + approvalInfos.push(...ApprovalModule.scanInnerTransaction({ to: safeTransaction.data.to, data: safeTxData })) } if (approvalInfos.length > 0) {