Commit 411cd1dc authored by isstuev's avatar isstuev

address user ops

parent d1d615e6
......@@ -77,7 +77,7 @@ import type {
import type { TxInterpretationResponse } from 'types/api/txInterpretation';
import type { TTxsFilters } from 'types/api/txsFilters';
import type { TxStateChanges } from 'types/api/txStateChanges';
import type { UserOpsResponse, UserOp, UserOpsFilters } from 'types/api/userOps';
import type { UserOpsResponse, UserOp, UserOpsFilters, UserOpsAccount } from 'types/api/userOps';
import type { VerifiedContractsSorting } from 'types/api/verifiedContracts';
import type { VisualizedContract } from 'types/api/visualization';
import type { WithdrawalsResponse, WithdrawalsCounters } from 'types/api/withdrawals';
......@@ -583,13 +583,17 @@ export const RESOURCES = {
// USER OPS
user_ops: {
path: '/api/v2/proxy/account-abstraction/operations',
filterFields: [ 'transaction_hash' as const ],
filterFields: [ 'transaction_hash' as const, 'sender' as const ],
},
user_op: {
path: '/api/v2/proxy/account-abstraction/operations/:hash',
pathParams: [ 'hash' as const ],
},
user_ops_account: {
path: '/api/v2/proxy/account-abstraction/accounts/:hash',
pathParams: [ 'hash' as const ],
},
// CONFIGS
config_backend_version: {
......@@ -768,6 +772,7 @@ Q extends 'domain_events' ? EnsDomainEventsResponse :
Q extends 'domains_lookup' ? EnsDomainLookupResponse :
Q extends 'user_ops' ? UserOpsResponse :
Q extends 'user_op' ? UserOp :
Q extends 'user_ops_account' ? UserOpsAccount :
never;
/* eslint-enable @typescript-eslint/indent */
......
import type { UserOpsItem, UserOp } from 'types/api/userOps';
import type { UserOpsItem, UserOp, UserOpsAccount } from 'types/api/userOps';
export const USER_OPS_ITEM: UserOpsItem = {
hash: '0xb94fab8f31f83001a23e20b2ce3cdcfb284c57a64b9a073e0e09c018bc701978',
......@@ -39,4 +39,10 @@ export const USER_OP: UserOp = {
sponsor_type: 'paymaster_sponsor',
fee: '17927001792700',
timestamp: '1704994440',
user_logs_count: 1,
user_logs_start_index: 2,
};
export const USER_OPS_ACCOUNT: UserOpsAccount = {
total_ops: 1,
};
......@@ -53,4 +53,9 @@ export type UserOp = {
export type UserOpsFilters = {
transaction_hash?: string;
sender?: string;
}
export type UserOpsAccount = {
total_ops: number;
}
import { useRouter } from 'next/router';
import React from 'react';
import getQueryParamString from 'lib/router/getQueryParamString';
import { USER_OPS_ITEM } from 'stubs/userOps';
import { generateListStub } from 'stubs/utils';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import UserOpsContent from 'ui/userOps/UserOpsContent';
type Props = {
scrollRef?: React.RefObject<HTMLDivElement>;
}
const AddressUserOps = ({ scrollRef }: Props) => {
const router = useRouter();
const hash = getQueryParamString(router.query.hash);
const userOpsQuery = useQueryWithPages({
resourceName: 'user_ops',
scrollRef,
options: {
enabled: Boolean(hash),
placeholderData: generateListStub<'user_ops'>(USER_OPS_ITEM, 50, { next_page_params: {
page_token: '10355938,0x5956a847d8089e254e02e5111cad6992b99ceb9e5c2dc4343fd53002834c4dc6',
page_size: 50,
} }),
},
filters: { sender: hash },
});
return <UserOpsContent query={ userOpsQuery } showSender={ false }/>;
};
export default AddressUserOps;
......@@ -11,6 +11,7 @@ import useContractTabs from 'lib/hooks/useContractTabs';
import useIsSafeAddress from 'lib/hooks/useIsSafeAddress';
import getQueryParamString from 'lib/router/getQueryParamString';
import { ADDRESS_INFO, ADDRESS_TABS_COUNTERS } from 'stubs/address';
import { USER_OPS_ACCOUNT } from 'stubs/userOps';
import AddressBlocksValidated from 'ui/address/AddressBlocksValidated';
import AddressCoinBalance from 'ui/address/AddressCoinBalance';
import AddressContract from 'ui/address/AddressContract';
......@@ -20,6 +21,7 @@ import AddressLogs from 'ui/address/AddressLogs';
import AddressTokens from 'ui/address/AddressTokens';
import AddressTokenTransfers from 'ui/address/AddressTokenTransfers';
import AddressTxs from 'ui/address/AddressTxs';
import AddressUserOps from 'ui/address/AddressUserOps';
import AddressWithdrawals from 'ui/address/AddressWithdrawals';
import AddressFavoriteButton from 'ui/address/details/AddressFavoriteButton';
import AddressQrCode from 'ui/address/details/AddressQrCode';
......@@ -62,6 +64,14 @@ const AddressPageContent = () => {
},
});
const userOpsAccountQuery = useApiQuery('user_ops_account', {
pathParams: { hash },
queryOptions: {
enabled: Boolean(hash),
placeholderData: USER_OPS_ACCOUNT,
},
});
const isSafeAddress = useIsSafeAddress(!addressQuery.isPlaceholderData && addressQuery.data?.is_contract ? hash : undefined);
const contractTabs = useContractTabs(addressQuery.data);
......@@ -74,6 +84,14 @@ const AddressPageContent = () => {
count: addressTabsCountersQuery.data?.transactions_count,
component: <AddressTxs scrollRef={ tabsScrollRef }/>,
},
config.features.userOps.isEnabled && Boolean(userOpsAccountQuery.data?.total_ops) ?
{
id: 'user_ops',
title: 'User operations',
count: userOpsAccountQuery.data?.total_ops,
component: <AddressUserOps/>,
} :
undefined,
config.features.beaconChain.isEnabled && addressTabsCountersQuery.data?.withdrawals_count ?
{
id: 'withdrawals',
......@@ -140,7 +158,7 @@ const AddressPageContent = () => {
subTabs: contractTabs.map(tab => tab.id),
} : undefined,
].filter(Boolean);
}, [ addressQuery.data, contractTabs, addressTabsCountersQuery.data ]);
}, [ addressQuery.data, contractTabs, addressTabsCountersQuery.data, userOpsAccountQuery.data ]);
const tags = (
<EntityTags
......@@ -151,6 +169,7 @@ const AddressPageContent = () => {
addressQuery.data?.implementation_address ? { label: 'proxy', display_name: 'Proxy' } : undefined,
addressQuery.data?.token ? { label: 'token', display_name: 'Token' } : undefined,
isSafeAddress ? { label: 'safe', display_name: 'Multisig: Safe' } : undefined,
userOpsAccountQuery.data?.total_ops ? { label: 'user_ops_acc', display_name: 'Smart contract wallet' } : undefined,
] }
/>
);
......@@ -222,7 +241,10 @@ const AddressPageContent = () => {
<AddressDetails addressQuery={ addressQuery } scrollRef={ tabsScrollRef }/>
{ /* should stay before tabs to scroll up with pagination */ }
<Box ref={ tabsScrollRef }></Box>
{ (addressQuery.isPlaceholderData || addressTabsCountersQuery.isPlaceholderData) ? <TabsSkeleton tabs={ tabs }/> : content }
{ (addressQuery.isPlaceholderData || addressTabsCountersQuery.isPlaceholderData || userOpsAccountQuery.isPlaceholderData) ?
<TabsSkeleton tabs={ tabs }/> :
content
}
</>
);
};
......
import { Hide, Show } from '@chakra-ui/react';
import React from 'react';
import { USER_OPS_ITEM } from 'stubs/userOps';
import { generateListStub } from 'stubs/utils';
import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import UserOpsListItem from 'ui/userOps/UserOpsListItem';
import UserOpsTable from 'ui/userOps/UserOpsTable';
import UserOpsContent from 'ui/userOps/UserOpsContent';
const UserOps = () => {
const { data, isError, isPlaceholderData, pagination } = useQueryWithPages({
const query = useQueryWithPages({
resourceName: 'user_ops',
options: {
placeholderData: generateListStub<'user_ops'>(USER_OPS_ITEM, 50, { next_page_params: {
......@@ -22,39 +17,10 @@ const UserOps = () => {
},
});
const content = data?.items ? (
<>
<Show below="lg" ssr={ false }>
{ data.items.map(((item, index) => (
<UserOpsListItem
key={ item.hash + (isPlaceholderData ? String(index) : '') }
item={ item }
isLoading={ isPlaceholderData }
/>
))) }
</Show>
<Hide below="lg" ssr={ false }>
<UserOpsTable items={ data.items } top={ pagination.isVisible ? 80 : 0 } isLoading={ isPlaceholderData }/>
</Hide>
</>
) : null;
const actionBar = pagination.isVisible ? (
<ActionBar mt={ -6 } alignItems="center">
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
) : null;
return (
<>
<PageTitle title="User operations" withTextAd/>
<DataListDisplay
isError={ isError }
items={ data?.items }
emptyText="There are no user operations."
content={ content }
actionBar={ actionBar }
/>
<UserOpsContent query={ query }/>
</>
);
};
......
import { Hide, Show } from '@chakra-ui/react';
import React from 'react';
import { SECOND } from 'lib/consts';
import { USER_OPS_ITEM } from 'stubs/userOps';
import { generateListStub } from 'stubs/utils';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DataListDisplay from 'ui/shared/DataListDisplay';
import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import TxPendingAlert from 'ui/tx/TxPendingAlert';
import TxSocketAlert from 'ui/tx/TxSocketAlert';
import useFetchTxInfo from 'ui/tx/useFetchTxInfo';
import UserOpsListItem from 'ui/userOps/UserOpsListItem';
import UserOpsTable from 'ui/userOps/UserOpsTable';
import UserOpsContent from 'ui/userOps/UserOpsContent';
const TxTokenTransfer = () => {
const TxUserOps = () => {
const txsInfo = useFetchTxInfo({ updateDelay: 5 * SECOND });
const userOpsQuery = useQueryWithPages({
......@@ -32,42 +26,7 @@ const TxTokenTransfer = () => {
return txsInfo.socketStatus ? <TxSocketAlert status={ txsInfo.socketStatus }/> : <TxPendingAlert/>;
}
if (txsInfo.isError || userOpsQuery.isError) {
return <DataFetchAlert/>;
}
const content = userOpsQuery.data?.items ? (
<>
<Hide below="lg" ssr={ false }>
<UserOpsTable items={ userOpsQuery.data?.items } top={ userOpsQuery.pagination.isVisible ? 0 : 80 } isLoading={ userOpsQuery.isPlaceholderData }/>
</Hide>
<Show below="lg" ssr={ false }>
{ userOpsQuery.data.items.map(((item, index) => (
<UserOpsListItem
key={ item.hash + (userOpsQuery.isPlaceholderData ? String(index) : '') }
item={ item }
isLoading={ userOpsQuery.isPlaceholderData }
/>
))) }
</Show>
</>
) : null;
const actionBar = userOpsQuery.pagination.isVisible ? (
<ActionBar mt={ -6 } alignItems="center">
<Pagination ml="auto" { ...userOpsQuery.pagination }/>
</ActionBar>
) : null;
return (
<DataListDisplay
isError={ txsInfo.isError || userOpsQuery.isError }
items={ userOpsQuery.data?.items }
emptyText="There are no user operations."
content={ content }
actionBar={ actionBar }
/>
);
return <UserOpsContent query={ userOpsQuery } showTx={ false }/>;
};
export default TxTokenTransfer;
export default TxUserOps;
import { Hide, Show } from '@chakra-ui/react';
import React from 'react';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DataListDisplay from 'ui/shared/DataListDisplay';
import Pagination from 'ui/shared/pagination/Pagination';
import type { QueryWithPagesResult } from 'ui/shared/pagination/useQueryWithPages';
import UserOpsListItem from 'ui/userOps/UserOpsListItem';
import UserOpsTable from 'ui/userOps/UserOpsTable';
type Props = {
query: QueryWithPagesResult<'user_ops'>;
showTx?: boolean;
showSender?: boolean;
};
const UserOpsContent = ({ query, showTx = true, showSender = true }: Props) => {
if (query.isError) {
return <DataFetchAlert/>;
}
const content = query.data?.items ? (
<>
<Hide below="lg" ssr={ false }>
<UserOpsTable
items={ query.data.items }
top={ query.pagination.isVisible ? 0 : 80 }
isLoading={ query.isPlaceholderData }
showTx={ showTx }
showSender={ showSender }
/>
</Hide>
<Show below="lg" ssr={ false }>
{ query.data.items.map(((item, index) => (
<UserOpsListItem
key={ item.hash + (query.isPlaceholderData ? String(index) : '') }
item={ item }
isLoading={ query.isPlaceholderData }
showTx={ showTx }
showSender={ showSender }
/>
))) }
</Show>
</>
) : null;
const actionBar = query.pagination.isVisible ? (
<ActionBar mt={ -6 } alignItems="center">
<Pagination ml="auto" { ...query.pagination }/>
</ActionBar>
) : null;
return (
<DataListDisplay
isError={ query.isError }
items={ query.data?.items }
emptyText="There are no user operations."
content={ content }
actionBar={ actionBar }
/>
);
};
export default UserOpsContent;
......@@ -16,9 +16,11 @@ import UserOpStatus from 'ui/shared/userOps/UserOpStatus';
type Props = {
item: UserOpsItem;
isLoading?: boolean;
showTx: boolean;
showSender: boolean;
};
const UserOpsListItem = ({ item, isLoading }: Props) => {
const UserOpsListItem = ({ item, isLoading, showTx, showSender }: Props) => {
// format will be fixed on the back-end
const timeAgo = dayjs(Number(item.timestamp) * 1000).fromNow();
......@@ -40,22 +42,30 @@ const UserOpsListItem = ({ item, isLoading }: Props) => {
<UserOpStatus status={ item.status } isLoading={ isLoading }/>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Sender</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<UserOpsAddress
address={ item.address }
isLoading={ isLoading }
/>
</ListItemMobileGrid.Value>
{ showSender && (
<>
<ListItemMobileGrid.Label isLoading={ isLoading }>Sender</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<UserOpsAddress
address={ item.address }
isLoading={ isLoading }
/>
</ListItemMobileGrid.Value>
</>
) }
<ListItemMobileGrid.Label isLoading={ isLoading }>Tx hash</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<TxEntity
hash={ item.transaction_hash }
isLoading={ isLoading }
noIcon
/>
</ListItemMobileGrid.Value>
{ showTx && (
<>
<ListItemMobileGrid.Label isLoading={ isLoading }>Tx hash</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<TxEntity
hash={ item.transaction_hash }
isLoading={ isLoading }
noIcon
/>
</ListItemMobileGrid.Value>
</>
) }
<ListItemMobileGrid.Label isLoading={ isLoading }>Block</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
......
......@@ -12,9 +12,11 @@ import UserOpsTableItem from './UserOpsTableItem';
items: Array<UserOpsItem>;
isLoading?: boolean;
top: number;
showTx: boolean;
showSender: boolean;
};
const UserOpsTable = ({ items, isLoading, top }: Props) => {
const UserOpsTable = ({ items, isLoading, top, showTx, showSender }: Props) => {
return (
<Table variant="simple" size="sm">
<Thead top={ top }>
......@@ -22,15 +24,21 @@ const UserOpsTable = ({ items, isLoading, top }: Props) => {
<Th w="60%">User op hash</Th>
<Th w="110px">Age</Th>
<Th w="140px">Status</Th>
<Th w="160px">Sender</Th>
<Th w="160px">Tx hash</Th>
{ showSender && <Th w="160px">Sender</Th> }
{ showTx && <Th w="160px">Tx hash</Th> }
<Th w="40%">Block</Th>
{ !config.UI.views.tx.hiddenFields?.tx_fee && <Th w="120px" isNumeric>{ `Fee ${ config.chain.currency.symbol }` }</Th> }
</Tr>
</Thead>
<Tbody>
{ items.map((item, index) => (
<UserOpsTableItem key={ (isLoading ? String(index) : '') } item={ item } isLoading={ isLoading }/>
<UserOpsTableItem
key={ (isLoading ? String(index) : '') }
item={ item }
isLoading={ isLoading }
showSender={ showSender }
showTx={ showTx }
/>
)) }
</Tbody>
</Table>
......
......@@ -15,9 +15,11 @@ import UserOpStatus from 'ui/shared/userOps/UserOpStatus';
type Props = {
item: UserOpsItem;
isLoading?: boolean;
showTx: boolean;
showSender: boolean;
};
const WithdrawalsTableItem = ({ item, isLoading }: Props) => {
const UserOpsTableItem = ({ item, isLoading, showTx, showSender }: Props) => {
// will be fixed on the back-end
const timeAgo = dayjs(Number(item.timestamp) * 1000).fromNow();
......@@ -32,21 +34,25 @@ const WithdrawalsTableItem = ({ item, isLoading }: Props) => {
<Td verticalAlign="middle">
<UserOpStatus status={ item.status } isLoading={ isLoading }/>
</Td>
<Td verticalAlign="middle">
<UserOpsAddress
address={ item.address }
isLoading={ isLoading }
truncation="constant"
/>
</Td>
<Td verticalAlign="middle">
<TxEntity
hash={ item.transaction_hash }
isLoading={ isLoading }
truncation="constant"
noIcon
/>
</Td>
{ showSender && (
<Td verticalAlign="middle">
<UserOpsAddress
address={ item.address }
isLoading={ isLoading }
truncation="constant"
/>
</Td>
) }
{ showTx && (
<Td verticalAlign="middle">
<TxEntity
hash={ item.transaction_hash }
isLoading={ isLoading }
truncation="constant"
noIcon
/>
</Td>
) }
<Td verticalAlign="middle">
<BlockEntity
number={ item.block_number }
......@@ -65,4 +71,4 @@ const WithdrawalsTableItem = ({ item, isLoading }: Props) => {
);
};
export default WithdrawalsTableItem;
export default UserOpsTableItem;
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