diff --git a/src/components/common/BatchSidebar/BatchTxItem.tsx b/src/components/common/BatchSidebar/BatchTxItem.tsx index 468f7e810c..86e4cafeba 100644 --- a/src/components/common/BatchSidebar/BatchTxItem.tsx +++ b/src/components/common/BatchSidebar/BatchTxItem.tsx @@ -10,7 +10,7 @@ import css from './styles.module.css' type BatchTxItemProps = DraftBatchItem & { count: number - onDelete: () => void + onDelete?: () => void } const BatchTxItem = ({ count, timestamp, txDetails, onDelete }: BatchTxItemProps) => { @@ -39,11 +39,15 @@ const BatchTxItem = ({ count, timestamp, txDetails, onDelete }: BatchTxItemProps - + {onDelete && ( + <> + - - - + + + + + )} ) diff --git a/src/components/common/BatchSidebar/index.tsx b/src/components/common/BatchSidebar/index.tsx index 43e0a01ace..53607682ae 100644 --- a/src/components/common/BatchSidebar/index.tsx +++ b/src/components/common/BatchSidebar/index.tsx @@ -1,6 +1,4 @@ import type { SyntheticEvent } from 'react' -import type { TransactionDetails } from '@safe-global/safe-gateway-typescript-sdk' -import { type MetaTransactionData, OperationType } from '@safe-global/safe-core-sdk-types' import { useCallback, useContext } from 'react' import { Button, Divider, Drawer, SvgIcon, Typography } from '@mui/material' import { useDraftBatch, useUpdateBatch } from '@/hooks/useDraftBatch' @@ -11,15 +9,6 @@ import BatchTxItem from './BatchTxItem' import ConfirmBatchFlow from '@/components/tx-flow/flows/ConfirmBatch' import PlusIcon from '@/public/images/common/plus.svg' -const getData = (txDetails: TransactionDetails): MetaTransactionData => { - return { - to: txDetails.txData?.to.value ?? '', - value: txDetails.txData?.value ?? '0', - data: txDetails.txData?.hexData ?? '0x', - operation: OperationType.Call, // only calls can be batched - } -} - const BatchSidebar = ({ isOpen, onToggle }: { isOpen: boolean; onToggle: (open: boolean) => void }) => { const { setTxFlow } = useContext(TxModalContext) const batchTxs = useDraftBatch() @@ -44,7 +33,7 @@ const BatchSidebar = ({ isOpen, onToggle }: { isOpen: boolean; onToggle: (open: e.preventDefault() if (!batchTxs.length) return closeSidebar() - setTxFlow( getData(item.txDetails))} onSubmit={clearBatch} />) + setTxFlow(, undefined, false) }, [setTxFlow, batchTxs, closeSidebar, clearBatch], ) diff --git a/src/components/tx-flow/flows/ConfirmBatch/index.tsx b/src/components/tx-flow/flows/ConfirmBatch/index.tsx index ce15eee245..63b354225f 100644 --- a/src/components/tx-flow/flows/ConfirmBatch/index.tsx +++ b/src/components/tx-flow/flows/ConfirmBatch/index.tsx @@ -1,33 +1,56 @@ import { type ReactElement, useContext, useEffect } from 'react' +import { type TransactionDetails } from '@safe-global/safe-gateway-typescript-sdk' import SignOrExecuteForm from '@/components/tx/SignOrExecuteForm' import { createMultiSendCallOnlyTx } from '@/services/tx/tx-sender' import { SafeTxContext } from '../../SafeTxProvider' import type { MetaTransactionData } from '@safe-global/safe-core-sdk-types' +import { OperationType } from '@safe-global/safe-core-sdk-types' import TxLayout from '../../common/TxLayout' import BatchIcon from '@/public/images/common/batch.svg' +import { useDraftBatch } from '@/hooks/useDraftBatch' +import BatchTxItem from '@/components/common/BatchSidebar/BatchTxItem' type ConfirmBatchProps = { - calls: MetaTransactionData[] onSubmit: () => void } -const ConfirmBatch = ({ calls, onSubmit }: ConfirmBatchProps): ReactElement => { +const getData = (txDetails: TransactionDetails): MetaTransactionData => { + return { + to: txDetails.txData?.to.value ?? '', + value: txDetails.txData?.value ?? '0', + data: txDetails.txData?.hexData ?? '0x', + operation: OperationType.Call, // only calls can be batched + } +} + +const ConfirmBatch = ({ onSubmit }: ConfirmBatchProps): ReactElement => { const { setSafeTx, setSafeTxError } = useContext(SafeTxContext) + const batchTxs = useDraftBatch() useEffect(() => { + const calls = batchTxs.map((tx) => getData(tx.txDetails)) createMultiSendCallOnlyTx(calls).then(setSafeTx).catch(setSafeTxError) - }, [calls, setSafeTx, setSafeTxError]) + }, [batchTxs, setSafeTx, setSafeTxError]) - return + return ( + + {batchTxs.map((item, index) => ( + + ))} + + ) } const ConfirmBatchFlow = (props: ConfirmBatchProps) => { + const { length } = useDraftBatch() + return ( 1 ? 's' : ''}`} icon={BatchIcon} step={0} + isBatch > diff --git a/src/components/tx/DecodedTx/index.test.tsx b/src/components/tx/DecodedTx/index.test.tsx index 983f8c4b73..d0d796688f 100644 --- a/src/components/tx/DecodedTx/index.test.tsx +++ b/src/components/tx/DecodedTx/index.test.tsx @@ -1,6 +1,5 @@ import { fireEvent, render } from '@/tests/test-utils' import { type SafeTransaction } from '@safe-global/safe-core-sdk-types' -import * as gatewayMethods from '@safe-global/safe-gateway-typescript-sdk' import DecodedTx from '.' import { waitFor } from '@testing-library/react' @@ -24,6 +23,21 @@ describe('DecodedTx', () => { }, } as SafeTransaction } + decodedData={{ + method: 'Native token transfer', + parameters: [ + { + name: 'to', + type: 'address', + value: '0x3430d04E42a722c5Ae52C5Bffbf1F230C2677600', + }, + { + name: 'value', + type: 'uint256', + value: '1000000', + }, + ], + }} />, ) @@ -37,24 +51,6 @@ describe('DecodedTx', () => { }) it('should render an ERC20 transfer', async () => { - jest.spyOn(gatewayMethods, 'getDecodedData').mockReturnValue( - Promise.resolve({ - method: 'transfer', - parameters: [ - { - name: 'to', - type: 'address', - value: '0x474e5Ded6b5D078163BFB8F6dBa355C3aA5478C8', - }, - { - name: 'value', - type: 'uint256', - value: '16745726664999765048', - }, - ], - }), - ) - const result = render( { }, } as SafeTransaction } + decodedData={{ + method: 'transfer', + parameters: [ + { + name: 'to', + type: 'address', + value: '0x474e5Ded6b5D078163BFB8F6dBa355C3aA5478C8', + }, + { + name: 'value', + type: 'uint256', + value: '16745726664999765048', + }, + ], + }} />, ) @@ -88,73 +99,6 @@ describe('DecodedTx', () => { }) it('should render a multisend transaction', async () => { - jest.spyOn(gatewayMethods, 'getDecodedData').mockReturnValue( - Promise.resolve({ - method: 'multiSend', - parameters: [ - { - name: 'transactions', - type: 'bytes', - value: '0x0057f1887a8bf19b14fc0df', - valueDecoded: [ - { - operation: 0, - to: '0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85', - value: '0', - data: '0x42842e0e0000000000000000000', - dataDecoded: { - method: 'safeTransferFrom', - parameters: [ - { - name: 'from', - type: 'address', - value: '0xA77DE01e157f9f57C7c4A326eeE9C4874D0598b6', - }, - { - name: 'to', - type: 'address', - value: '0x474e5Ded6b5D078163BFB8F6dBa355C3aA5478C8', - }, - { - name: 'tokenId', - type: 'uint256', - value: '52964617156216674852059480948658573966398315289847646343083345905048987083870', - }, - ], - }, - }, - { - operation: 0, - to: '0xD014e20A75437a4bd0FbB40498FF94e6F337c3e9', - value: '0', - data: '0x42842e0e000000000000000000000000a77de', - dataDecoded: { - method: 'safeTransferFrom', - parameters: [ - { - name: 'from', - type: 'address', - value: '0xA77DE01e157f9f57C7c4A326eeE9C4874D0598b6', - }, - { - name: 'to', - type: 'address', - value: '0x474e5Ded6b5D078163BFB8F6dBa355C3aA5478C8', - }, - { - name: 'tokenId', - type: 'uint256', - value: '412', - }, - ], - }, - }, - ], - }, - ], - }), - ) - const result = render( { }, } as SafeTransaction } + decodedData={{ + method: 'multiSend', + parameters: [ + { + name: 'transactions', + type: 'bytes', + value: '0x0057f1887a8bf19b14fc0df', + valueDecoded: [ + { + operation: 0, + to: '0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85', + value: '0', + data: '0x42842e0e0000000000000000000', + dataDecoded: { + method: 'safeTransferFrom', + parameters: [ + { + name: 'from', + type: 'address', + value: '0xA77DE01e157f9f57C7c4A326eeE9C4874D0598b6', + }, + { + name: 'to', + type: 'address', + value: '0x474e5Ded6b5D078163BFB8F6dBa355C3aA5478C8', + }, + { + name: 'tokenId', + type: 'uint256', + value: '52964617156216674852059480948658573966398315289847646343083345905048987083870', + }, + ], + }, + }, + { + operation: 0, + to: '0xD014e20A75437a4bd0FbB40498FF94e6F337c3e9', + value: '0', + data: '0x42842e0e000000000000000000000000a77de', + dataDecoded: { + method: 'safeTransferFrom', + parameters: [ + { + name: 'from', + type: 'address', + value: '0xA77DE01e157f9f57C7c4A326eeE9C4874D0598b6', + }, + { + name: 'to', + type: 'address', + value: '0x474e5Ded6b5D078163BFB8F6dBa355C3aA5478C8', + }, + { + name: 'tokenId', + type: 'uint256', + value: '412', + }, + ], + }, + }, + ], + }, + ], + }} />, ) @@ -185,14 +193,6 @@ describe('DecodedTx', () => { }) it('should render a function call without parameters', async () => { - // Wrapped token deposit function - jest.spyOn(gatewayMethods, 'getDecodedData').mockReturnValue( - Promise.resolve({ - method: 'deposit', - parameters: [], - }), - ) - const result = render( { }, } as SafeTransaction } + decodedData={{ + method: 'deposit', + parameters: [], + }} />, ) diff --git a/src/components/tx/DecodedTx/index.tsx b/src/components/tx/DecodedTx/index.tsx index 8939a26f53..5dc64947c7 100644 --- a/src/components/tx/DecodedTx/index.tsx +++ b/src/components/tx/DecodedTx/index.tsx @@ -1,4 +1,4 @@ -import { type SyntheticEvent, type ReactElement, useMemo, memo } from 'react' +import { type SyntheticEvent, type ReactElement, memo } from 'react' import { Accordion, AccordionDetails, @@ -10,23 +10,14 @@ import { Typography, } from '@mui/material' import { OperationType, type SafeTransaction } from '@safe-global/safe-core-sdk-types' -import { - type DecodedDataResponse, - getDecodedData, - getTransactionDetails, - type TransactionDetails, - Operation, -} from '@safe-global/safe-gateway-typescript-sdk' +import type { DecodedDataResponse } from '@safe-global/safe-gateway-typescript-sdk' +import { getTransactionDetails, type TransactionDetails, Operation } from '@safe-global/safe-gateway-typescript-sdk' import useChainId from '@/hooks/useChainId' import useAsync from '@/hooks/useAsync' import { MethodDetails } from '@/components/transactions/TxDetails/TxData/DecodedData/MethodDetails' import ErrorMessage from '../ErrorMessage' import Summary, { PartialSummary } from '@/components/transactions/TxDetails/Summary' import { trackEvent, MODALS_EVENTS } from '@/services/analytics' -import { isEmptyHexData } from '@/utils/hex' -import ApprovalEditor from '@/components/tx/ApprovalEditor' -import { ErrorBoundary } from '@sentry/react' -import { getNativeTransferData } from '@/services/tx/tokenTransferParams' import Multisend from '@/components/transactions/TxDetails/TxData/DecodedData/Multisend' import InfoIcon from '@/public/images/notifications/info.svg' import ExternalLink from '@/components/common/ExternalLink' @@ -37,48 +28,35 @@ import accordionCss from '@/styles/accordion.module.css' type DecodedTxProps = { tx?: SafeTransaction txId?: string + decodedData?: DecodedDataResponse + decodedDataError?: Error + decodedDataLoading?: boolean } -const DecodedTx = ({ tx, txId }: DecodedTxProps): ReactElement | null => { +const DecodedTx = ({ + tx, + decodedData, + decodedDataError, + decodedDataLoading = false, + txId, +}: DecodedTxProps): ReactElement | null => { const chainId = useChainId() - const encodedData = tx?.data.data - const isEmptyData = !!encodedData && isEmptyHexData(encodedData) - const isRejection = isEmptyData && tx?.data.value === '0' - const nativeTransfer = isEmptyData && !isRejection ? getNativeTransferData(tx?.data) : undefined - - const [decodedData = nativeTransfer, decodedDataError, decodedDataLoading] = useAsync(() => { - if (!encodedData || isEmptyData) return - return getDecodedData(chainId, encodedData) - }, [chainId, encodedData, isEmptyData]) const isMultisend = !!decodedData?.parameters?.[0]?.valueDecoded const [txDetails, txDetailsError, txDetailsLoading] = useAsync(() => { if (!txId) return return getTransactionDetails(chainId, txId) - }, []) - - const approvalEditorTx = useMemo(() => { - if (!decodedData || !txDetails?.txData) { - return undefined - } - return { ...decodedData, to: txDetails?.txData?.to.value } - }, [decodedData, txDetails?.txData]) + }, [chainId, txId]) const onChangeExpand = (_: SyntheticEvent, expanded: boolean) => { trackEvent({ ...MODALS_EVENTS.TX_DETAILS, label: expanded ? 'Open' : 'Close' }) } - if (isRejection || !tx) return null + if (!decodedData) return null return (
- {approvalEditorTx && ( - Error parsing data
}> - - - )} - {isMultisend && ( { const isCreation = safeTx?.signatures.size === 0 const isNewExecutableTx = useImmediatelyExecutable() && isCreation const isCorrectNonce = useValidateNonce(safeTx) + const decodedTx = useDecodeTx(safeTx) + const isMultisend = props.isBatch || isMultisendTx(decodedTx[0]) // If checkbox is checked and the transaction is executable, execute it, otherwise sign it const canExecute = isCorrectNonce && (props.isExecutable || isNewExecutableTx) @@ -45,7 +50,17 @@ const SignOrExecuteForm = (props: SignOrExecuteProps): ReactElement => { {props.children} - + Error parsing data}> + + + + @@ -74,7 +89,11 @@ const SignOrExecuteForm = (props: SignOrExecuteProps): ReactElement => { - {willExecute ? : } + {willExecute ? ( + + ) : ( + + )} ) diff --git a/src/hooks/useDecodeTx.ts b/src/hooks/useDecodeTx.ts new file mode 100644 index 0000000000..c1d963fa33 --- /dev/null +++ b/src/hooks/useDecodeTx.ts @@ -0,0 +1,27 @@ +import { type SafeTransaction } from '@safe-global/safe-core-sdk-types' +import { type DecodedDataResponse, getDecodedData } from '@safe-global/safe-gateway-typescript-sdk' +import { getNativeTransferData } from '@/services/tx/tokenTransferParams' +import { isEmptyHexData } from '@/utils/hex' +import type { AsyncResult } from './useAsync' +import useAsync from './useAsync' +import useChainId from './useChainId' + +const useDecodeTx = (tx?: SafeTransaction): AsyncResult => { + const chainId = useChainId() + const encodedData = tx?.data.data + const isEmptyData = !!encodedData && isEmptyHexData(encodedData) + const isRejection = isEmptyData && tx?.data.value === '0' + const nativeTransfer = isEmptyData && !isRejection ? getNativeTransferData(tx?.data) : undefined + + return useAsync(() => { + if (nativeTransfer) return Promise.resolve(nativeTransfer) + if (!encodedData || isEmptyData) return + return getDecodedData(chainId, encodedData) + }, [chainId, encodedData, isEmptyData, nativeTransfer]) +} + +export const isMultisendTx = (decodedData?: DecodedDataResponse): boolean => { + return !!decodedData?.parameters?.[0]?.valueDecoded +} + +export default useDecodeTx