Commit 1274f1e4 authored by isstuev's avatar isstuev

add user ops tx interpretation

parent 154cdad6
...@@ -53,6 +53,7 @@ NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED='true' ...@@ -53,6 +53,7 @@ NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED='true'
NEXT_PUBLIC_HAS_BEACON_CHAIN=true NEXT_PUBLIC_HAS_BEACON_CHAIN=true
NEXT_PUBLIC_HAS_USER_OPS=true NEXT_PUBLIC_HAS_USER_OPS=true
NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.blockscout.com/?address={hash}&blockscout=eth-goerli.blockscout.com','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}] NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.blockscout.com/?address={hash}&blockscout=eth-goerli.blockscout.com','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
#meta #meta
NEXT_PUBLIC_OG_IMAGE_URL=https://github.com/blockscout/frontend-configs/blob/main/configs/og-images/eth-goerli.png?raw=true NEXT_PUBLIC_OG_IMAGE_URL=https://github.com/blockscout/frontend-configs/blob/main/configs/og-images/eth-goerli.png?raw=true
\ No newline at end of file
...@@ -662,6 +662,10 @@ export const RESOURCES = { ...@@ -662,6 +662,10 @@ export const RESOURCES = {
path: '/api/v2/proxy/account-abstraction/accounts/:hash', path: '/api/v2/proxy/account-abstraction/accounts/:hash',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
}, },
user_op_interpretation: {
path: '/api/v2/proxy/account-abstraction/operations/:hash/summary',
pathParams: [ 'hash' as const ],
},
// VALIDATORS // VALIDATORS
validators: { validators: {
...@@ -881,6 +885,7 @@ Q extends 'domains_lookup' ? EnsDomainLookupResponse : ...@@ -881,6 +885,7 @@ Q extends 'domains_lookup' ? EnsDomainLookupResponse :
Q extends 'user_ops' ? UserOpsResponse : Q extends 'user_ops' ? UserOpsResponse :
Q extends 'user_op' ? UserOp : Q extends 'user_op' ? UserOp :
Q extends 'user_ops_account' ? UserOpsAccount : Q extends 'user_ops_account' ? UserOpsAccount :
Q extends 'user_op_interpretation'? TxInterpretationResponse :
never; never;
/* eslint-enable @typescript-eslint/indent */ /* eslint-enable @typescript-eslint/indent */
......
...@@ -13,7 +13,6 @@ import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError'; ...@@ -13,7 +13,6 @@ import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import { USER_OP } from 'stubs/userOps'; import { USER_OP } from 'stubs/userOps';
import TextAd from 'ui/shared/ad/TextAd'; import TextAd from 'ui/shared/ad/TextAd';
import UserOpEntity from 'ui/shared/entities/userOp/UserOpEntity';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs'; import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton'; import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton';
...@@ -23,6 +22,7 @@ import TxTokenTransfer from 'ui/tx/TxTokenTransfer'; ...@@ -23,6 +22,7 @@ import TxTokenTransfer from 'ui/tx/TxTokenTransfer';
import useTxQuery from 'ui/tx/useTxQuery'; import useTxQuery from 'ui/tx/useTxQuery';
import UserOpDetails from 'ui/userOp/UserOpDetails'; import UserOpDetails from 'ui/userOp/UserOpDetails';
import UserOpRaw from 'ui/userOp/UserOpRaw'; import UserOpRaw from 'ui/userOp/UserOpRaw';
import UserOpSubHeading from 'ui/userOp/UserOpSubHeading';
const UserOp = () => { const UserOp = () => {
const router = useRouter(); const router = useRouter();
...@@ -90,7 +90,7 @@ const UserOp = () => { ...@@ -90,7 +90,7 @@ const UserOp = () => {
throwOnAbsentParamError(hash); throwOnAbsentParamError(hash);
throwOnResourceLoadError(userOpQuery); throwOnResourceLoadError(userOpQuery);
const titleSecondRow = <UserOpEntity hash={ hash } noLink noCopy={ false } fontWeight={ 500 } fontFamily="heading"/>; const titleSecondRow = <UserOpSubHeading hash={ hash }/>;
return ( return (
<> <>
......
...@@ -9,9 +9,10 @@ const SCROLL_GRADIENT_HEIGHT = 48; ...@@ -9,9 +9,10 @@ const SCROLL_GRADIENT_HEIGHT = 48;
type Props = { type Props = {
children: React.ReactNode; children: React.ReactNode;
isLoading?: boolean; isLoading?: boolean;
type: 'tx' | 'user_op';
} }
const TxDetailsActions = ({ children, isLoading }: Props) => { const DetailsActionsWrapper = ({ children, isLoading, type }: Props) => {
const containerRef = React.useRef<HTMLDivElement>(null); const containerRef = React.useRef<HTMLDivElement>(null);
const [ hasScroll, setHasScroll ] = React.useState(false); const [ hasScroll, setHasScroll ] = React.useState(false);
...@@ -25,8 +26,8 @@ const TxDetailsActions = ({ children, isLoading }: Props) => { ...@@ -25,8 +26,8 @@ const TxDetailsActions = ({ children, isLoading }: Props) => {
return ( return (
<DetailsInfoItem <DetailsInfoItem
title="Transaction action" title={ `${ type === 'tx' ? 'Transaction' : 'User operation' } action` }
hint="Highlighted events of the transaction" hint={ `Highlighted events of the ${ type === 'tx' ? 'transaction' : 'user operation' }` }
note={ hasScroll ? 'Scroll to see more' : undefined } note={ hasScroll ? 'Scroll to see more' : undefined }
position="relative" position="relative"
isLoading={ isLoading } isLoading={ isLoading }
...@@ -47,4 +48,4 @@ const TxDetailsActions = ({ children, isLoading }: Props) => { ...@@ -47,4 +48,4 @@ const TxDetailsActions = ({ children, isLoading }: Props) => {
); );
}; };
export default React.memo(TxDetailsActions); export default React.memo(DetailsActionsWrapper);
...@@ -66,7 +66,7 @@ type ContentProps = Omit<EntityBase.ContentBaseProps, 'text'> & Pick<EntityProps ...@@ -66,7 +66,7 @@ type ContentProps = Omit<EntityBase.ContentBaseProps, 'text'> & Pick<EntityProps
const Content = chakra((props: ContentProps) => { const Content = chakra((props: ContentProps) => {
const nameString = [ const nameString = [
!props.onlySymbol && (props.token.name ?? 'Unnamed token'), !props.onlySymbol && (props.token.name ?? 'Unnamed token'),
props.onlySymbol && (props.token.symbol ?? ''), props.onlySymbol && (props.token.symbol ?? props.token.name ?? 'Unnamed token'),
props.token.symbol && props.jointSymbol && !props.onlySymbol && `(${ props.token.symbol })`, props.token.symbol && props.jointSymbol && !props.onlySymbol && `(${ props.token.symbol })`,
].filter(Boolean).join(' '); ].filter(Boolean).join(' ');
......
...@@ -5,10 +5,10 @@ import config from 'configs/app'; ...@@ -5,10 +5,10 @@ import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import { TX_INTERPRETATION } from 'stubs/txInterpretation'; import { TX_INTERPRETATION } from 'stubs/txInterpretation';
import AccountActionsMenu from 'ui/shared/AccountActionsMenu/AccountActionsMenu'; import AccountActionsMenu from 'ui/shared/AccountActionsMenu/AccountActionsMenu';
import { TX_ACTIONS_BLOCK_ID } from 'ui/shared/DetailsActionsWrapper';
import TxEntity from 'ui/shared/entities/tx/TxEntity'; import TxEntity from 'ui/shared/entities/tx/TxEntity';
import NetworkExplorers from 'ui/shared/NetworkExplorers'; import NetworkExplorers from 'ui/shared/NetworkExplorers';
import { TX_ACTIONS_BLOCK_ID } from 'ui/tx/details/txDetailsActions/TxDetailsActionsWrapper'; import TxInterpretation from 'ui/shared/tx/interpretation/TxInterpretation';
import TxInterpretation from 'ui/tx/interpretation/TxInterpretation';
import type { TxQuery } from './useTxQuery'; import type { TxQuery } from './useTxQuery';
......
...@@ -2,10 +2,9 @@ import React from 'react'; ...@@ -2,10 +2,9 @@ import React from 'react';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import { TX_INTERPRETATION } from 'stubs/txInterpretation'; import { TX_INTERPRETATION } from 'stubs/txInterpretation';
import DetailsActionsWrapper from 'ui/shared/DetailsActionsWrapper';
import DetailsInfoItemDivider from 'ui/shared/DetailsInfoItemDivider'; import DetailsInfoItemDivider from 'ui/shared/DetailsInfoItemDivider';
import TxInterpretation from 'ui/tx/interpretation/TxInterpretation'; import TxInterpretation from 'ui/shared/tx/interpretation/TxInterpretation';
import TxDetailsActionsWrapper from './TxDetailsActionsWrapper';
interface Props { interface Props {
hash?: string; hash?: string;
...@@ -30,7 +29,7 @@ const TxDetailsActionsInterpretation = ({ hash, isTxDataLoading }: Props) => { ...@@ -30,7 +29,7 @@ const TxDetailsActionsInterpretation = ({ hash, isTxDataLoading }: Props) => {
return ( return (
<> <>
<TxDetailsActionsWrapper isLoading={ isTxDataLoading || txInterpretationQuery.isPlaceholderData }> <DetailsActionsWrapper isLoading={ isTxDataLoading || txInterpretationQuery.isPlaceholderData } type="tx">
{ actions.map((action, index: number) => ( { actions.map((action, index: number) => (
<TxInterpretation <TxInterpretation
key={ index } key={ index }
...@@ -39,7 +38,7 @@ const TxDetailsActionsInterpretation = ({ hash, isTxDataLoading }: Props) => { ...@@ -39,7 +38,7 @@ const TxDetailsActionsInterpretation = ({ hash, isTxDataLoading }: Props) => {
/> />
), ),
) } ) }
</TxDetailsActionsWrapper> </DetailsActionsWrapper>
<DetailsInfoItemDivider/> <DetailsInfoItemDivider/>
</> </>
); );
......
...@@ -2,10 +2,10 @@ import React from 'react'; ...@@ -2,10 +2,10 @@ import React from 'react';
import type { TxAction } from 'types/api/txAction'; import type { TxAction } from 'types/api/txAction';
import DetailsActionsWrapper from 'ui/shared/DetailsActionsWrapper';
import DetailsInfoItemDivider from 'ui/shared/DetailsInfoItemDivider'; import DetailsInfoItemDivider from 'ui/shared/DetailsInfoItemDivider';
import TxDetailsAction from './TxDetailsAction'; import TxDetailsAction from './TxDetailsAction';
import TxDetailsActionsWrapper from './TxDetailsActionsWrapper';
interface Props { interface Props {
actions: Array<TxAction>; actions: Array<TxAction>;
...@@ -15,9 +15,9 @@ interface Props { ...@@ -15,9 +15,9 @@ interface Props {
const TxDetailsActionsRaw = ({ actions, isLoading }: Props) => { const TxDetailsActionsRaw = ({ actions, isLoading }: Props) => {
return ( return (
<> <>
<TxDetailsActionsWrapper isLoading={ isLoading }> <DetailsActionsWrapper isLoading={ isLoading } type="tx">
{ actions.map((action, index: number) => <TxDetailsAction key={ index } action={ action }/>) } { actions.map((action, index: number) => <TxDetailsAction key={ index } action={ action }/>) }
</TxDetailsActionsWrapper> </DetailsActionsWrapper>
<DetailsInfoItemDivider/> <DetailsInfoItemDivider/>
</> </>
); );
......
...@@ -26,6 +26,8 @@ import UserOpSponsorType from 'ui/shared/userOps/UserOpSponsorType'; ...@@ -26,6 +26,8 @@ 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 UserOpDetailsActions from './UserOpDetailsActions';
interface Props { interface Props {
query: UseQueryResult<UserOp, ResourceError>; query: UseQueryResult<UserOp, ResourceError>;
} }
...@@ -168,6 +170,8 @@ const UserOpDetails = ({ query }: Props) => { ...@@ -168,6 +170,8 @@ const UserOpDetails = ({ query }: Props) => {
<AddressStringOrParam address={ data.entry_point } isLoading={ isPlaceholderData }/> <AddressStringOrParam address={ data.entry_point } isLoading={ isPlaceholderData }/>
</DetailsInfoItem> </DetailsInfoItem>
{ config.features.txInterpretation.isEnabled && <UserOpDetailsActions hash={ data.hash } isUserOpDataLoading={ isPlaceholderData }/> }
{ /* CUT */ } { /* CUT */ }
<GridItem colSpan={{ base: undefined, lg: 2 }}> <GridItem colSpan={{ base: undefined, lg: 2 }}>
<Element name={ CUT_LINK_NAME }> <Element name={ CUT_LINK_NAME }>
......
import React from 'react';
import useApiQuery from 'lib/api/useApiQuery';
import { TX_INTERPRETATION } from 'stubs/txInterpretation';
import DetailsActionsWrapper from 'ui/shared/DetailsActionsWrapper';
import DetailsInfoItemDivider from 'ui/shared/DetailsInfoItemDivider';
import TxInterpretation from 'ui/shared/tx/interpretation/TxInterpretation';
interface Props {
hash?: string;
isUserOpDataLoading: boolean;
}
const TxDetailsActionsInterpretation = ({ hash, isUserOpDataLoading }: Props) => {
const interpretationQuery = useApiQuery('user_op_interpretation', {
pathParams: { hash },
queryOptions: {
enabled: Boolean(hash) && !isUserOpDataLoading,
placeholderData: TX_INTERPRETATION,
refetchOnMount: false,
},
});
const actions = interpretationQuery.data?.data.summaries;
if (!actions || actions.length < 2) {
return null;
}
return (
<>
<DetailsActionsWrapper isLoading={ isUserOpDataLoading || interpretationQuery.isPlaceholderData } type="user_op">
{ actions.map((action, index: number) => (
<TxInterpretation
key={ index }
summary={ action }
isLoading={ isUserOpDataLoading || interpretationQuery.isPlaceholderData }
/>
),
) }
</DetailsActionsWrapper>
<DetailsInfoItemDivider/>
</>
);
};
export default TxDetailsActionsInterpretation;
import { Flex, Link } from '@chakra-ui/react';
// import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
// import type { UserOp } from 'types/api/userOps';
import config from 'configs/app';
// import type { ResourceError } from 'lib/api/resources';
import useApiQuery from 'lib/api/useApiQuery';
import { TX_INTERPRETATION } from 'stubs/txInterpretation';
import { TX_ACTIONS_BLOCK_ID } from 'ui/shared/DetailsActionsWrapper';
import UserOpEntity from 'ui/shared/entities/userOp/UserOpEntity';
import TxInterpretation from 'ui/shared/tx/interpretation/TxInterpretation';
type Props = {
hash?: string;
// userOpQuery: UseQueryResult<UserOp, ResourceError<unknown>>;
}
const UserOpSubHeading = ({ hash }: Props) => {
const hasInterpretationFeature = config.features.txInterpretation.isEnabled;
const txInterpretationQuery = useApiQuery('user_op_interpretation', {
pathParams: { hash },
queryOptions: {
enabled: Boolean(hash) && hasInterpretationFeature,
placeholderData: TX_INTERPRETATION,
},
});
const hasInterpretation = hasInterpretationFeature &&
(txInterpretationQuery.isPlaceholderData || Boolean(txInterpretationQuery.data?.data.summaries.length));
const hasViewAllInterpretationsLink =
!txInterpretationQuery.isPlaceholderData && txInterpretationQuery.data?.data.summaries && txInterpretationQuery.data?.data.summaries.length > 1;
if (hasInterpretation) {
return (
<Flex mr={{ base: 0, lg: 6 }} flexWrap="wrap" alignItems="center">
<TxInterpretation
summary={ txInterpretationQuery.data?.data.summaries[0] }
isLoading={ txInterpretationQuery.isPlaceholderData }
fontSize="lg"
mr={ hasViewAllInterpretationsLink ? 3 : 0 }
/>
{ hasViewAllInterpretationsLink &&
<Link href={ `#${ TX_ACTIONS_BLOCK_ID }` }>View all</Link> }
</Flex>
);
// fallback will be added later
// } else if (hasInterpretationFeature && userOpQuery.data?.decoded_call_data.method_call && userOpQuery.data?.sender && userOpQuery.data?.to) {
// return (
// <TxInterpretation
// summary={{
// summary_template: `{sender_hash} called {method} on {receiver_hash}`,
// summary_template_variables: {
// sender_hash: {
// type: 'address',
// value: txQuery.data.from,
// },
// method: {
// type: 'method',
// value: txQuery.data.method,
// },
// receiver_hash: {
// type: 'address',
// value: txQuery.data.to,
// },
// },
// }}
// isLoading={ txQuery.isPlaceholderData }
// fontSize="lg"
// />
// );
} else {
return <UserOpEntity hash={ hash } noLink noCopy={ false } fontWeight={ 500 } fontFamily="heading"/>;
}
};
export default UserOpSubHeading;
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