diff --git a/lib/api/resources.ts b/lib/api/resources.ts index 5a779c13eb..5044fbfe5c 100644 --- a/lib/api/resources.ts +++ b/lib/api/resources.ts @@ -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 */ diff --git a/stubs/userOps.ts b/stubs/userOps.ts index 32e5f3340d..cb0c5217af 100644 --- a/stubs/userOps.ts +++ b/stubs/userOps.ts @@ -1,4 +1,4 @@ -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, }; diff --git a/types/api/userOps.ts b/types/api/userOps.ts index fc055ca29f..e0ed25a213 100644 --- a/types/api/userOps.ts +++ b/types/api/userOps.ts @@ -53,4 +53,9 @@ export type UserOp = { export type UserOpsFilters = { transaction_hash?: string; + sender?: string; +} + +export type UserOpsAccount = { + total_ops: number; } diff --git a/ui/address/AddressUserOps.tsx b/ui/address/AddressUserOps.tsx new file mode 100644 index 0000000000..7c93c83b47 --- /dev/null +++ b/ui/address/AddressUserOps.tsx @@ -0,0 +1,35 @@ +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; +} + +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 ; +}; + +export default AddressUserOps; diff --git a/ui/pages/Address.tsx b/ui/pages/Address.tsx index c0cf60c530..44d9cf8741 100644 --- a/ui/pages/Address.tsx +++ b/ui/pages/Address.tsx @@ -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: , }, + config.features.userOps.isEnabled && Boolean(userOpsAccountQuery.data?.total_ops) ? + { + id: 'user_ops', + title: 'User operations', + count: userOpsAccountQuery.data?.total_ops, + component: , + } : + 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 = ( { 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 = () => { { /* should stay before tabs to scroll up with pagination */ } - { (addressQuery.isPlaceholderData || addressTabsCountersQuery.isPlaceholderData) ? : content } + { (addressQuery.isPlaceholderData || addressTabsCountersQuery.isPlaceholderData || userOpsAccountQuery.isPlaceholderData) ? + : + content + } ); }; diff --git a/ui/pages/UserOps.tsx b/ui/pages/UserOps.tsx index a912b67eea..189421d712 100644 --- a/ui/pages/UserOps.tsx +++ b/ui/pages/UserOps.tsx @@ -1,18 +1,13 @@ -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 ? ( - <> - - { data.items.map(((item, index) => ( - - ))) } - - - - - - ) : null; - - const actionBar = pagination.isVisible ? ( - - - - ) : null; - return ( <> - + ); }; diff --git a/ui/tx/TxUserOps.tsx b/ui/tx/TxUserOps.tsx index 1f20c2e1b9..51d05a5ac7 100644 --- a/ui/tx/TxUserOps.tsx +++ b/ui/tx/TxUserOps.tsx @@ -1,21 +1,15 @@ -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 ? : ; } - if (txsInfo.isError || userOpsQuery.isError) { - return ; - } - - const content = userOpsQuery.data?.items ? ( - <> - - - - - { userOpsQuery.data.items.map(((item, index) => ( - - ))) } - - - ) : null; - - const actionBar = userOpsQuery.pagination.isVisible ? ( - - - - ) : null; - - return ( - - ); + return ; }; -export default TxTokenTransfer; +export default TxUserOps; diff --git a/ui/userOps/UserOpsContent.tsx b/ui/userOps/UserOpsContent.tsx new file mode 100644 index 0000000000..841e303791 --- /dev/null +++ b/ui/userOps/UserOpsContent.tsx @@ -0,0 +1,66 @@ +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 ; + } + + const content = query.data?.items ? ( + <> + + + + + { query.data.items.map(((item, index) => ( + + ))) } + + + ) : null; + + const actionBar = query.pagination.isVisible ? ( + + + + ) : null; + + return ( + + ); +}; + +export default UserOpsContent; diff --git a/ui/userOps/UserOpsListItem.tsx b/ui/userOps/UserOpsListItem.tsx index d3bf28fe86..09ede6c0fc 100644 --- a/ui/userOps/UserOpsListItem.tsx +++ b/ui/userOps/UserOpsListItem.tsx @@ -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) => { - Sender - - - + { showSender && ( + <> + Sender + + + + + ) } - Tx hash - - - + { showTx && ( + <> + Tx hash + + + + + ) } Block diff --git a/ui/userOps/UserOpsTable.tsx b/ui/userOps/UserOpsTable.tsx index 1c030e5d81..dbcc9e2246 100644 --- a/ui/userOps/UserOpsTable.tsx +++ b/ui/userOps/UserOpsTable.tsx @@ -12,9 +12,11 @@ import UserOpsTableItem from './UserOpsTableItem'; items: Array; isLoading?: boolean; top: number; + showTx: boolean; + showSender: boolean; }; -const UserOpsTable = ({ items, isLoading, top }: Props) => { +const UserOpsTable = ({ items, isLoading, top, showTx, showSender }: Props) => { return ( @@ -22,15 +24,21 @@ const UserOpsTable = ({ items, isLoading, top }: Props) => { - - + { showSender && } + { showTx && } { !config.UI.views.tx.hiddenFields?.tx_fee && } { items.map((item, index) => ( - + )) }
User op hash Age StatusSenderTx hashSenderTx hashBlock{ `Fee ${ config.chain.currency.symbol }` }
diff --git a/ui/userOps/UserOpsTableItem.tsx b/ui/userOps/UserOpsTableItem.tsx index 0585b84e93..752683c93f 100644 --- a/ui/userOps/UserOpsTableItem.tsx +++ b/ui/userOps/UserOpsTableItem.tsx @@ -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) => { - - - - - - + { showSender && ( + + + + ) } + { showTx && ( + + + + ) } { ); }; -export default WithdrawalsTableItem; +export default UserOpsTableItem;