Commit 4e2f5b7f authored by tom goriunov's avatar tom goriunov Committed by GitHub

User Operations: add switcher to call data (#1950)

* User Operations: add switcher to call data

Fixes #1762

* update screenshots

* move toggler above decoded method signature on mobile
parent 8f9971e6
/* eslint-disable max-len */
import type { UserOp } from 'types/api/userOps'; import type { UserOp } from 'types/api/userOps';
export const userOpData: UserOp = { export const userOpData: UserOp = {
...@@ -47,7 +48,6 @@ export const userOpData: UserOp = { ...@@ -47,7 +48,6 @@ export const userOpData: UserOp = {
max_fee_per_gas: '1575000898', max_fee_per_gas: '1575000898',
max_priority_fee_per_gas: '1575000898', max_priority_fee_per_gas: '1575000898',
nonce: '79', nonce: '79',
// eslint-disable-next-line max-len
paymaster_and_data: '0x7cea357b5ac0639f89f9e378a1f03aa5005c0a250000000000000000000000000000000000000000000000000000000065b3a8800000000000000000000000000000000000000000000000000000000065aa6e0028fa4c57ac1141bc9ecd8c9243f618ade8ea1db10ab6c1d1798a222a824764ff2269a72ae7a3680fa8b03a80d8a00cdc710eaf37afdcc55f8c9c4defa3fdf2471b', paymaster_and_data: '0x7cea357b5ac0639f89f9e378a1f03aa5005c0a250000000000000000000000000000000000000000000000000000000065b3a8800000000000000000000000000000000000000000000000000000000065aa6e0028fa4c57ac1141bc9ecd8c9243f618ade8ea1db10ab6c1d1798a222a824764ff2269a72ae7a3680fa8b03a80d8a00cdc710eaf37afdcc55f8c9c4defa3fdf2471b',
pre_verification_gas: '48396', pre_verification_gas: '48396',
sender: '0xF0C14FF4404b188fAA39a3507B388998c10FE284', sender: '0xF0C14FF4404b188fAA39a3507B388998c10FE284',
...@@ -64,8 +64,34 @@ export const userOpData: UserOp = { ...@@ -64,8 +64,34 @@ export const userOpData: UserOp = {
is_verified: null, is_verified: null,
name: null, name: null,
}, },
// eslint-disable-next-line max-len
call_data: '0xb61d27f600000000000000000000000059f6aa952df7f048fd076e33e0ea8bb552d5ffd8000000000000000000000000000000000000000000000000003f3d017500800000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000', call_data: '0xb61d27f600000000000000000000000059f6aa952df7f048fd076e33e0ea8bb552d5ffd8000000000000000000000000000000000000000000000000003f3d017500800000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000',
execute_call_data: '0x3cf80e6c',
decoded_call_data: {
method_call: 'execute(address dest, uint256 value, bytes func)',
method_id: 'b61d27f6',
parameters: [
{
name: 'dest',
type: 'address',
value: '0xb0ccffd05f5a87c4c3ceffaa217900422a249915',
},
{
name: 'value',
type: 'uint256',
value: '0',
},
{
name: 'func',
type: 'bytes',
value: '0x3cf80e6c',
},
],
},
decoded_execute_call_data: {
method_call: 'advanceEpoch()',
method_id: '3cf80e6c',
parameters: [],
},
paymaster: { paymaster: {
ens_domain_name: null, ens_domain_name: null,
hash: '0x7ceA357B5AC0639F89F9e378a1f03Aa5005C0a25', hash: '0x7ceA357B5AC0639F89F9e378a1f03Aa5005C0a25',
......
...@@ -21,6 +21,9 @@ export const USER_OP: UserOp = { ...@@ -21,6 +21,9 @@ export const USER_OP: UserOp = {
sender: ADDRESS_HASH, sender: ADDRESS_HASH,
nonce: '0x00b', nonce: '0x00b',
call_data: '0x123', call_data: '0x123',
execute_call_data: null,
decoded_call_data: null,
decoded_execute_call_data: null,
call_gas_limit: '71316', call_gas_limit: '71316',
verification_gas_limit: '91551', verification_gas_limit: '91551',
pre_verification_gas: '53627', pre_verification_gas: '53627',
......
...@@ -15,6 +15,9 @@ const baseStyleTrack = defineStyle((props) => { ...@@ -15,6 +15,9 @@ const baseStyleTrack = defineStyle((props) => {
bg: mode(`${ c }.600`, `${ c }.400`)(props), bg: mode(`${ c }.600`, `${ c }.400`)(props),
}, },
}, },
_focusVisible: {
boxShadow: 'none',
},
}; };
}); });
......
import type { AddressParamBasic } from './addressParams'; import type { AddressParamBasic } from './addressParams';
import type { DecodedInput } from './decodedInput';
export type UserOpsItem = { export type UserOpsItem = {
hash: string; hash: string;
...@@ -46,6 +47,9 @@ export type UserOp = { ...@@ -46,6 +47,9 @@ export type UserOp = {
signature: string; signature: string;
nonce: string; nonce: string;
call_data: string; call_data: string;
decoded_call_data: DecodedInput | null;
execute_call_data: string | null;
decoded_execute_call_data: DecodedInput | null;
user_logs_start_index: number; user_logs_start_index: number;
user_logs_count: number; user_logs_count: number;
raw: { raw: {
......
...@@ -21,6 +21,7 @@ test('base view', async({ render, mockTextAd, mockApiResponse }) => { ...@@ -21,6 +21,7 @@ test('base view', async({ render, mockTextAd, mockApiResponse }) => {
await mockTextAd(); await mockTextAd();
await mockApiResponse('user_op', userOpData, { pathParams: { hash: userOpData.hash } }); await mockApiResponse('user_op', userOpData, { pathParams: { hash: userOpData.hash } });
const component = await render(<UserOp/>, { hooksConfig }); const component = await render(<UserOp/>, { hooksConfig });
await component.getByText('View details').click();
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
...@@ -31,6 +32,7 @@ test.describe('mobile', () => { ...@@ -31,6 +32,7 @@ test.describe('mobile', () => {
await mockTextAd(); await mockTextAd();
await mockApiResponse('user_op', userOpData, { pathParams: { hash: userOpData.hash } }); await mockApiResponse('user_op', userOpData, { pathParams: { hash: userOpData.hash } });
const component = await render(<UserOp/>, { hooksConfig }); const component = await render(<UserOp/>, { hooksConfig });
await component.getByText('View details').click();
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
}); });
...@@ -9,25 +9,29 @@ const OPTIONS: Array<DataType> = [ 'Hex', 'UTF-8' ]; ...@@ -9,25 +9,29 @@ const OPTIONS: Array<DataType> = [ 'Hex', 'UTF-8' ];
interface Props { interface Props {
hex: string; hex: string;
rightSlot?: React.ReactNode;
} }
const RawInputData = ({ hex }: Props) => { const RawInputData = ({ hex, rightSlot: rightSlotProp }: Props) => {
const [ selectedDataType, setSelectedDataType ] = React.useState<DataType>('Hex'); const [ selectedDataType, setSelectedDataType ] = React.useState<DataType>('Hex');
const handleSelectChange = React.useCallback((event: React.ChangeEvent<HTMLSelectElement>) => { const handleSelectChange = React.useCallback((event: React.ChangeEvent<HTMLSelectElement>) => {
setSelectedDataType(event.target.value as DataType); setSelectedDataType(event.target.value as DataType);
}, []); }, []);
const select = ( const rightSlot = (
<>
<Select size="xs" borderRadius="base" value={ selectedDataType } onChange={ handleSelectChange } w="auto" mr="auto"> <Select size="xs" borderRadius="base" value={ selectedDataType } onChange={ handleSelectChange } w="auto" mr="auto">
{ OPTIONS.map((option) => <option key={ option } value={ option }>{ option }</option>) } { OPTIONS.map((option) => <option key={ option } value={ option }>{ option }</option>) }
</Select> </Select>
{ rightSlotProp }
</>
); );
return ( return (
<RawDataSnippet <RawDataSnippet
data={ selectedDataType === 'Hex' ? hex : hexToUtf8(hex) } data={ selectedDataType === 'Hex' ? hex : hexToUtf8(hex) }
rightSlot={ select } rightSlot={ rightSlot }
textareaMaxHeight="220px" textareaMaxHeight="220px"
textareaMinHeight="160px" textareaMinHeight="160px"
w="100%" w="100%"
......
...@@ -2,17 +2,27 @@ import React from 'react'; ...@@ -2,17 +2,27 @@ import React from 'react';
import type { DecodedInput } from 'types/api/decodedInput'; import type { DecodedInput } from 'types/api/decodedInput';
import useIsMobile from 'lib/hooks/useIsMobile';
import LogDecodedInputDataHeader from './LogDecodedInputDataHeader'; import LogDecodedInputDataHeader from './LogDecodedInputDataHeader';
import LogDecodedInputDataTable from './LogDecodedInputDataTable'; import LogDecodedInputDataTable from './LogDecodedInputDataTable';
interface Props { interface Props {
data: DecodedInput; data: DecodedInput;
isLoading?: boolean; isLoading?: boolean;
rightSlot?: React.ReactNode;
} }
const LogDecodedInputData = ({ data, isLoading }: Props) => { const LogDecodedInputData = ({ data, isLoading, rightSlot }: Props) => {
const isMobile = useIsMobile();
return ( return (
<> <>
<LogDecodedInputDataHeader methodId={ data.method_id } methodCall={ data.method_call } isLoading={ isLoading }/> { isMobile ? rightSlot : null }
<LogDecodedInputDataHeader
methodId={ data.method_id }
methodCall={ data.method_call }
isLoading={ isLoading }
rightSlot={ isMobile ? null : rightSlot }
/>
{ data.parameters.length > 0 && <LogDecodedInputDataTable data={ data.parameters } isLoading={ isLoading }/> } { data.parameters.length > 0 && <LogDecodedInputDataTable data={ data.parameters } isLoading={ isLoading }/> }
</> </>
); );
......
...@@ -7,6 +7,7 @@ interface Props { ...@@ -7,6 +7,7 @@ interface Props {
methodId: string; methodId: string;
methodCall: string; methodCall: string;
isLoading?: boolean; isLoading?: boolean;
rightSlot?: React.ReactNode;
} }
const Item = ({ label, children, isLoading }: { label: string; children: React.ReactNode; isLoading?: boolean}) => { const Item = ({ label, children, isLoading }: { label: string; children: React.ReactNode; isLoading?: boolean}) => {
...@@ -16,7 +17,7 @@ const Item = ({ label, children, isLoading }: { label: string; children: React.R ...@@ -16,7 +17,7 @@ const Item = ({ label, children, isLoading }: { label: string; children: React.R
rowGap={ 2 } rowGap={ 2 }
px={{ base: 0, lg: 4 }} px={{ base: 0, lg: 4 }}
flexDir={{ base: 'column', lg: 'row' }} flexDir={{ base: 'column', lg: 'row' }}
alignItems="flex-start" alignItems={{ base: 'flex-start', lg: 'center' }}
> >
<Skeleton fontWeight={ 600 } w={{ base: 'auto', lg: '80px' }} flexShrink={ 0 } isLoaded={ !isLoading }> <Skeleton fontWeight={ 600 } w={{ base: 'auto', lg: '80px' }} flexShrink={ 0 } isLoaded={ !isLoading }>
{ label } { label }
...@@ -26,17 +27,21 @@ const Item = ({ label, children, isLoading }: { label: string; children: React.R ...@@ -26,17 +27,21 @@ const Item = ({ label, children, isLoading }: { label: string; children: React.R
); );
}; };
const LogDecodedInputDataHeader = ({ methodId, methodCall, isLoading }: Props) => { const LogDecodedInputDataHeader = ({ methodId, methodCall, isLoading, rightSlot }: Props) => {
return ( return (
<VStack <VStack
align="flex-start" align="flex-start"
divider={ <Divider/> } divider={ <Divider/> }
fontSize="sm" fontSize="sm"
lineHeight={ 5 } lineHeight={ 5 }
flexGrow={ 1 }
> >
<Flex columnGap={ 2 } w="100%">
<Item label="Method id" isLoading={ isLoading }> <Item label="Method id" isLoading={ isLoading }>
<Tag isLoading={ isLoading }>{ methodId }</Tag> <Tag isLoading={ isLoading }>{ methodId }</Tag>
</Item> </Item>
{ rightSlot }
</Flex>
<Item label="Call" isLoading={ isLoading }> <Item label="Call" isLoading={ isLoading }>
<Skeleton isLoaded={ !isLoading } whiteSpace="pre-wrap">{ methodCall }</Skeleton> <Skeleton isLoaded={ !isLoading } whiteSpace="pre-wrap">{ methodCall }</Skeleton>
</Item> </Item>
......
import React from 'react';
import type { UserOp } from 'types/api/userOps';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import RawInputData from 'ui/shared/RawInputData';
import UserOpCallDataSwitch from './UserOpCallDataSwitch';
interface Props {
data: UserOp;
}
const UserOpDecodedCallData = ({ data }: Props) => {
const [ callData, setCallData ] = React.useState(data.call_data || data.execute_call_data);
const handleSwitchChange = React.useCallback((isChecked: boolean) => {
setCallData(isChecked ? data.execute_call_data : data.call_data);
}, [ data ]);
if (!callData) {
return null;
}
const toggler = data.execute_call_data ? (
<UserOpCallDataSwitch
onChange={ handleSwitchChange }
initialValue={ !data.call_data }
isDisabled={ !data.call_data }
ml={{ base: 3, lg: 'auto' }}
/>
) : null;
return (
<DetailsInfoItem
title="Call data"
hint="Data that’s passed to the sender for execution"
>
<RawInputData hex={ callData } rightSlot={ toggler }/>
</DetailsInfoItem>
);
};
export default React.memo(UserOpDecodedCallData);
import { chakra, FormLabel, FormControl, Switch } from '@chakra-ui/react';
import React from 'react';
import Hint from 'ui/shared/Hint';
interface Props {
onChange: (isChecked: boolean) => void;
initialValue?: boolean;
isDisabled?: boolean;
className?: string;
}
const UserOpCallDataSwitch = ({ className, initialValue, isDisabled, onChange }: Props) => {
const [ isChecked, setIsChecked ] = React.useState(initialValue ?? false);
const handleChange = React.useCallback(() => {
setIsChecked((prevValue) => {
const nextValue = !prevValue;
onChange(nextValue);
return nextValue;
});
}, [ onChange ]);
return (
<FormControl className={ className } display="flex" columnGap={ 2 } ml="auto" w="fit-content">
<FormLabel htmlFor="isExternal" fontSize="sm" lineHeight={ 5 } fontWeight={ 600 } m={ 0 }>
Show external call data
</FormLabel>
<Switch id="isExternal" isChecked={ isChecked } isDisabled={ isDisabled } onChange={ handleChange }/>
<Hint label="Inner call data is a predicted decoded call from this user operation"/>
</FormControl>
);
};
export default React.memo(chakra(UserOpCallDataSwitch));
import React from 'react';
import type { UserOp } from 'types/api/userOps';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import LogDecodedInputData from 'ui/shared/logs/LogDecodedInputData';
import UserOpCallDataSwitch from './UserOpCallDataSwitch';
interface Props {
data: UserOp;
}
const UserOpDecodedCallData = ({ data }: Props) => {
const [ callData, setCallData ] = React.useState(data.decoded_call_data || data.decoded_execute_call_data);
const handleSwitchChange = React.useCallback((isChecked: boolean) => {
setCallData(isChecked ? data.decoded_execute_call_data : data.decoded_call_data);
}, [ data ]);
if (!callData) {
return null;
}
const toggler = data.decoded_execute_call_data ? (
<UserOpCallDataSwitch
onChange={ handleSwitchChange }
initialValue={ !data.decoded_call_data }
isDisabled={ !data.decoded_call_data }
ml={{ base: 0, lg: 'auto' }}
/>
) : null;
return (
<DetailsInfoItem
title="Decoded call data"
hint="Decoded call data"
flexDir={{ base: 'column', lg: 'row' }}
alignItems={{ base: 'flex-start', lg: 'center' }}
>
<LogDecodedInputData data={ callData } rightSlot={ toggler }/>
</DetailsInfoItem>
);
};
export default React.memo(UserOpDecodedCallData);
...@@ -22,11 +22,12 @@ import AddressStringOrParam from 'ui/shared/entities/address/AddressStringOrPara ...@@ -22,11 +22,12 @@ import AddressStringOrParam from 'ui/shared/entities/address/AddressStringOrPara
import BlockEntity from 'ui/shared/entities/block/BlockEntity'; import BlockEntity from 'ui/shared/entities/block/BlockEntity';
import TxEntity from 'ui/shared/entities/tx/TxEntity'; import TxEntity from 'ui/shared/entities/tx/TxEntity';
import UserOpEntity from 'ui/shared/entities/userOp/UserOpEntity'; import UserOpEntity from 'ui/shared/entities/userOp/UserOpEntity';
import RawInputData from 'ui/shared/RawInputData';
import UserOpSponsorType from 'ui/shared/userOps/UserOpSponsorType'; import UserOpSponsorType from 'ui/shared/userOps/UserOpSponsorType';
import UserOpStatus from 'ui/shared/userOps/UserOpStatus'; import UserOpStatus from 'ui/shared/userOps/UserOpStatus';
import Utilization from 'ui/shared/Utilization/Utilization'; import Utilization from 'ui/shared/Utilization/Utilization';
import UserOpCallData from './UserOpCallData';
import UserOpDecodedCallData from './UserOpDecodedCallData';
import UserOpDetailsActions from './UserOpDetailsActions'; import UserOpDetailsActions from './UserOpDetailsActions';
interface Props { interface Props {
...@@ -300,12 +301,10 @@ const UserOpDetails = ({ query }: Props) => { ...@@ -300,12 +301,10 @@ const UserOpDetails = ({ query }: Props) => {
> >
{ data.nonce } { data.nonce }
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem
title="Call data" <UserOpCallData data={ data }/>
hint="Data that’s passed to the sender for execution"
> <UserOpDecodedCallData data={ data }/>
<RawInputData hex={ data.call_data }/>
</DetailsInfoItem>
</> </>
) } ) }
</Grid> </Grid>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment