feat: FHE operations and tags (#3180)

* integrate erc-7984

* minor fix

* FHE operations and tags

* upd

* upd

* fix mobile

* upd

* fix mobile

* Update TxFHEOperations.tsx

* upd

* add hover text

* Update Address.tsx

* Update Address.tsx

* add hover text

* Revert "Merge branch 'both-prs' into fhe-operations"

This reverts commit a8d48de2c077fed37581948ad07576ec751d2003, reversing
changes made to f253cae03dc71d38c3fa92fcad29f7be11467521.

* fix design of fhe ops page

* fix design of fhe ops page

* fix design of fhe ops page

* fix design of fhe ops page

* fix comments

* fix: tab name

* upd

---------

Co-authored-by: Maksym Zub <[email protected]>
This commit is contained in:
l0gun0v
2026-02-24 11:27:32 +01:00
committed by GitHub
co-authored by Maksym Zub
parent f8b493ee2f
commit 40ccfd77aa
13 changed files with 451 additions and 1 deletions
+7
View File
@@ -1,5 +1,6 @@
import type { ApiResource } from '../../types';
import type { TxBlobs } from 'types/api/blobs';
import type { FheOperationsResponse } from 'types/api/fheOperations';
import type { InternalTransactionFilters, InternalTransactionsResponse } from 'types/api/internalTransaction';
import type { LogsResponseTx } from 'types/api/log';
import type { RawTracesResponse } from 'types/api/rawTrace';
@@ -62,6 +63,11 @@ export const GENERAL_API_TX_RESOURCES = {
filterFields: [ ],
paginated: true,
},
tx_fhe_operations: {
path: '/api/v2/transactions/:hash/fhe-operations',
pathParams: [ 'hash' as const ],
},
tx_token_transfers: {
path: '/api/v2/transactions/:hash/token-transfers',
pathParams: [ 'hash' as const ],
@@ -112,6 +118,7 @@ R extends 'general:tx_internal_txs' ? InternalTransactionsResponse :
R extends 'general:tx' ? Transaction :
R extends 'general:tx_logs' ? LogsResponseTx :
R extends 'general:tx_token_transfers' ? TokenTransferResponse :
R extends 'general:tx_fhe_operations' ? FheOperationsResponse :
R extends 'general:tx_raw_trace' ? RawTracesResponse :
R extends 'general:tx_state_changes' ? TxStateChanges :
R extends 'general:tx_blobs' ? TxBlobs :
+28
View File
@@ -0,0 +1,28 @@
import type { FheOperation, FheOperationsResponse } from 'types/api/fheOperations';
import { ADDRESS_PARAMS } from './addressParams';
export const FHE_OPERATION: FheOperation = {
log_index: 0,
operation: 'fhe_add',
type: 'arithmetic',
fhe_type: 'Uint64',
is_scalar: true,
hcu_cost: 100,
hcu_depth: 50,
caller: ADDRESS_PARAMS,
inputs: {},
result: '0x',
block_number: 9004925,
};
export const FHE_OPERATIONS_RESPONSE: FheOperationsResponse = {
items: [
{ ...FHE_OPERATION, log_index: 0 },
{ ...FHE_OPERATION, log_index: 1 },
{ ...FHE_OPERATION, log_index: 2 },
],
total_hcu: 300,
max_depth_hcu: 100,
operation_count: 3,
};
+54
View File
@@ -0,0 +1,54 @@
import type { AddressParam } from './addressParams';
export type FheOperationType =
| 'arithmetic' |
'bitwise' |
'comparison' |
'unary' |
'control' |
'encryption' |
'random';
export type FheType =
| 'Bool' |
'Uint8' |
'Uint16' |
'Uint32' |
'Uint64' |
'Uint128' |
'Uint160' |
'Uint256' |
'Bytes64' |
'Bytes128' |
'Bytes256';
export interface FheOperationInputs {
lhs?: string;
rhs?: string;
ct?: string;
control?: string;
if_true?: string;
if_false?: string;
plaintext?: number;
}
export interface FheOperation {
log_index: number;
operation: string;
type: FheOperationType;
fhe_type: FheType;
is_scalar: boolean;
hcu_cost: number;
hcu_depth: number;
caller: AddressParam | null;
inputs: FheOperationInputs;
result: string;
block_number: number;
}
export interface FheOperationsResponse {
items: Array<FheOperation>;
total_hcu: number;
max_depth_hcu: number;
operation_count: number;
}
+2
View File
@@ -109,6 +109,8 @@ export type Transaction = {
authorization_list?: Array<TxAuthorization>;
// Interop
op_interop_messages?: Array<InteropTransactionInfo>;
// FHE operations
fhe_operations_count?: number;
};
type ArbitrumTransactionData = {
+14 -1
View File
@@ -65,6 +65,8 @@ import PageTitle from 'ui/shared/Page/PageTitle';
const TOKEN_TABS = [ 'tokens_erc20', 'tokens_nfts', 'tokens_nfts_collection', 'tokens_nfts_list' ];
const PREDEFINED_TAG_PRIORITY = 100;
const FHE_TOOLTIP_DESCRIPTION = 'This contract uses Fully Homomorphic Encryption (FHE) to encrypt on-chain data. ' +
'Inputs and most outputs are intentionally hidden, while computations are verified on-chain.';
const txInterpretation = config.features.txInterpretation;
const addressProfileAPIFeature = config.features.addressProfileAPI;
@@ -327,7 +329,18 @@ const AddressPageContent = () => {
const tags: Array<EntityTag> = React.useMemo(() => {
return [
...(addressQuery.data?.public_tags?.map((tag) => ({ slug: tag.label, name: tag.display_name, tagType: 'custom' as const, ordinal: -1 })) || []),
...(addressQuery.data?.public_tags?.map((tag) => {
const isFhe = tag.label.toLowerCase() === 'fhe' || tag.display_name.toLowerCase() === 'fhe';
return {
slug: tag.label,
name: tag.display_name,
tagType: 'custom' as const,
ordinal: -1,
meta: isFhe ? {
tooltipDescription: FHE_TOOLTIP_DESCRIPTION,
} : undefined,
};
}) || []),
addressQuery.data?.celo?.account ? {
slug: 'celo-account',
name: 'Celo account',
+4
View File
@@ -22,6 +22,7 @@ import TxBlobs from 'ui/tx/TxBlobs';
import TxDetails from 'ui/tx/TxDetails';
import TxDetailsDegraded from 'ui/tx/TxDetailsDegraded';
import TxDetailsWrapped from 'ui/tx/TxDetailsWrapped';
import TxFHEOperations from 'ui/tx/TxFHEOperations';
import TxInternals from 'ui/tx/TxInternals';
import TxLogs from 'ui/tx/TxLogs';
import TxRawTrace from 'ui/tx/TxRawTrace';
@@ -88,6 +89,9 @@ const TransactionPageContent = () => {
{ id: 'logs', title: 'Logs', component: <TxLogs txQuery={ txQuery }/> },
{ id: 'state', title: 'State', component: <TxState txQuery={ txQuery }/> },
{ id: 'raw_trace', title: 'Raw trace', component: <TxRawTrace txQuery={ txQuery }/> },
txQuery.data?.fhe_operations_count && txQuery.data.fhe_operations_count > 0 ?
{ id: 'fhe_operations', title: 'FHE operations', component: <TxFHEOperations txQuery={ txQuery }/> } :
undefined,
txQuery.data?.authorization_list?.length ?
{ id: 'authorizations', title: 'Authorizations', component: <TxAuthorizations txQuery={ txQuery }/> } :
undefined,
+67
View File
@@ -0,0 +1,67 @@
import { Box } from '@chakra-ui/react';
import React from 'react';
import useApiQuery from 'lib/api/useApiQuery';
import { FHE_OPERATIONS_RESPONSE } from 'stubs/fheOperations';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DataListDisplay from 'ui/shared/DataListDisplay';
import TxFHEOperationsList from 'ui/tx/fheOperations/TxFHEOperationsList';
import TxFHEOperationsStats from 'ui/tx/fheOperations/TxFHEOperationsStats';
import TxFHEOperationsTable from 'ui/tx/fheOperations/TxFHEOperationsTable';
import TxPendingAlert from 'ui/tx/TxPendingAlert';
import TxSocketAlert from 'ui/tx/TxSocketAlert';
import type { TxQuery } from './useTxQuery';
interface Props {
txQuery: TxQuery;
}
const TxFHEOperations = ({ txQuery }: Props) => {
const hash = txQuery.data?.hash || '';
const isEnabled = Boolean(hash) && Boolean(txQuery.data?.status) && !txQuery.isPlaceholderData;
const { data, isError, isPlaceholderData } = useApiQuery('general:tx_fhe_operations', {
pathParams: { hash },
queryOptions: {
enabled: isEnabled,
retry: false,
placeholderData: FHE_OPERATIONS_RESPONSE,
},
});
if (!txQuery.isPending && !txQuery.isPlaceholderData && !txQuery.isError && txQuery.data && !txQuery.data.status) {
return txQuery.socketStatus ? <TxSocketAlert status={ txQuery.socketStatus }/> : <TxPendingAlert/>;
}
if (txQuery.isError || isError) {
return <DataFetchAlert/>;
}
const content = data ? (
<>
<TxFHEOperationsStats
totalHcu={ data.total_hcu }
maxDepthHcu={ data.max_depth_hcu }
operationCount={ data.operation_count }
isLoading={ isPlaceholderData }
/>
<Box>
<TxFHEOperationsTable data={ data.items } isLoading={ isPlaceholderData }/>
<TxFHEOperationsList data={ data.items } isLoading={ isPlaceholderData }/>
</Box>
</>
) : null;
return (
<DataListDisplay
isError={ isError }
itemsNum={ data?.items?.length ?? 0 }
emptyText="There are no FHE operations for this transaction."
>
{ content }
</DataListDisplay>
);
};
export default React.memo(TxFHEOperations);
@@ -0,0 +1,27 @@
import { Box } from '@chakra-ui/react';
import React from 'react';
import type { FheOperation } from 'types/api/fheOperations';
import TxFHEOperationsListItem from 'ui/tx/fheOperations/TxFHEOperationsListItem';
interface Props {
data: Array<FheOperation>;
isLoading?: boolean;
}
const TxFHEOperationsList = ({ data, isLoading }: Props) => {
return (
<Box hideFrom="lg">
{ data.map((op) => (
<TxFHEOperationsListItem
key={ op.log_index }
{ ...op }
isLoading={ isLoading }
/>
)) }
</Box>
);
};
export default React.memo(TxFHEOperationsList);
@@ -0,0 +1,70 @@
import { Box, Flex, Grid, Text } from '@chakra-ui/react';
import { capitalize } from 'es-toolkit';
import React from 'react';
import type { FheOperation } from 'types/api/fheOperations';
import { Badge } from 'toolkit/chakra/badge';
import { Skeleton } from 'toolkit/chakra/skeleton';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import { getTypeColor } from 'ui/tx/fheOperations/utils';
type Props = FheOperation & { isLoading?: boolean };
const TxFHEOperationsListItem = (props: Props) => {
const { log_index: logIndex, operation, type, fhe_type: fheType, is_scalar: isScalar, hcu_cost: hcuCost, hcu_depth: hcuDepth, caller, isLoading } = props;
return (
<ListItemMobile>
<Flex gap={ 2 } flexWrap="wrap" alignItems="center">
<Badge colorPalette={ getTypeColor(type) } loading={ isLoading }>
{ capitalize(type) }
</Badge>
<Badge colorPalette="gray" loading={ isLoading }>
{ fheType }
</Badge>
<Badge colorPalette="gray" loading={ isLoading }>
{ isScalar ? 'Scalar' : 'Non-scalar' }
</Badge>
</Flex>
<Grid templateColumns="110px 1fr" rowGap={ 2 } columnGap={ 2 }>
<Text fontWeight="medium">Index</Text>
<Skeleton loading={ isLoading } color="text.secondary">
{ logIndex }
</Skeleton>
<Text fontWeight="medium">Operation</Text>
<Skeleton loading={ isLoading } color="text.secondary">
{ operation }
</Skeleton>
<Text fontWeight="medium">HCU cost</Text>
<Skeleton loading={ isLoading } color="text.secondary">
{ hcuCost.toLocaleString() }
</Skeleton>
<Text fontWeight="medium">HCU depth</Text>
<Skeleton loading={ isLoading } color="text.secondary">
{ hcuDepth.toLocaleString() }
</Skeleton>
<Text fontWeight="medium">Caller</Text>
<Box minW={ 0 }>
{ caller && caller.hash ? (
<AddressEntity
address={ caller }
truncation="constant"
isLoading={ isLoading }
/>
) : (
<Text color="text.secondary">—</Text>
) }
</Box>
</Grid>
</ListItemMobile>
);
};
export default React.memo(TxFHEOperationsListItem);
@@ -0,0 +1,43 @@
import { Box } from '@chakra-ui/react';
import React from 'react';
import StatsWidget from 'ui/shared/stats/StatsWidget';
interface Props {
totalHcu: number;
maxDepthHcu: number;
operationCount: number;
isLoading?: boolean;
}
const TxFHEOperationsStats = ({ totalHcu, maxDepthHcu, operationCount, isLoading }: Props) => {
return (
<Box
display="grid"
gridTemplateColumns={{ base: '1fr', lg: 'repeat(3, calc(33.333% - 8px))' }}
gap={ 3 }
mb={ 6 }
>
<StatsWidget
label="Total HCU"
hint="Sum of all Homomorphic Computation Units consumed by FHE operations in this transaction"
value={ (totalHcu || 0).toLocaleString() }
isLoading={ isLoading }
/>
<StatsWidget
label="Max Depth HCU"
hint="Maximum HCU consumed at any single depth level in the FHE operation tree"
value={ (maxDepthHcu || 0).toLocaleString() }
isLoading={ isLoading }
/>
<StatsWidget
label="Operations"
hint="Total number of FHE operations executed in this transaction"
value={ operationCount.toLocaleString() }
isLoading={ isLoading }
/>
</Box>
);
};
export default React.memo(TxFHEOperationsStats);
@@ -0,0 +1,47 @@
import { Box } from '@chakra-ui/react';
import React from 'react';
import type { FheOperation } from 'types/api/fheOperations';
import { AddressHighlightProvider } from 'lib/contexts/addressHighlight';
import { TableBody, TableColumnHeader, TableHeader, TableRoot, TableRow } from 'toolkit/chakra/table';
import TxFHEOperationsTableItem from 'ui/tx/fheOperations/TxFHEOperationsTableItem';
interface Props {
data: Array<FheOperation>;
isLoading?: boolean;
}
const TxFHEOperationsTable = ({ data, isLoading }: Props) => {
return (
<AddressHighlightProvider>
<Box maxW="100%" overflowX="auto" hideBelow="lg">
<TableRoot tableLayout="fixed" minWidth="900px" w="100%">
<TableHeader>
<TableRow>
<TableColumnHeader width="10%">Index</TableColumnHeader>
<TableColumnHeader width="15%">Operation</TableColumnHeader>
<TableColumnHeader width="12%">Type</TableColumnHeader>
<TableColumnHeader width="12%">FHE type</TableColumnHeader>
<TableColumnHeader width="12%">Mode</TableColumnHeader>
<TableColumnHeader width="12%">HCU cost</TableColumnHeader>
<TableColumnHeader width="12%">HCU depth</TableColumnHeader>
<TableColumnHeader width="24%">Caller</TableColumnHeader>
</TableRow>
</TableHeader>
<TableBody>
{ data.map((op) => (
<TxFHEOperationsTableItem
key={ op.log_index }
{ ...op }
isLoading={ isLoading }
/>
)) }
</TableBody>
</TableRoot>
</Box>
</AddressHighlightProvider>
);
};
export default React.memo(TxFHEOperationsTable);
@@ -0,0 +1,71 @@
import { Text } from '@chakra-ui/react';
import { capitalize } from 'es-toolkit';
import React from 'react';
import type { FheOperation } from 'types/api/fheOperations';
import { Badge } from 'toolkit/chakra/badge';
import { Skeleton } from 'toolkit/chakra/skeleton';
import { TableCell, TableRow } from 'toolkit/chakra/table';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import { getTypeColor } from 'ui/tx/fheOperations/utils';
type Props = FheOperation & { isLoading?: boolean };
const TxFHEOperationsTableItem = (props: Props) => {
const { log_index: logIndex, operation, type, fhe_type: fheType, is_scalar: isScalar, hcu_cost: hcuCost, hcu_depth: hcuDepth, caller, isLoading } = props;
const hcuDepthValue = hcuDepth;
return (
<TableRow>
<TableCell verticalAlign="middle">
<Skeleton loading={ isLoading }>
{ logIndex }
</Skeleton>
</TableCell>
<TableCell verticalAlign="middle">
<Skeleton loading={ isLoading }>
{ operation }
</Skeleton>
</TableCell>
<TableCell verticalAlign="middle">
<Badge colorPalette={ getTypeColor(type) } loading={ isLoading }>
{ capitalize(type) }
</Badge>
</TableCell>
<TableCell verticalAlign="middle">
<Badge colorPalette="gray" loading={ isLoading }>
{ fheType }
</Badge>
</TableCell>
<TableCell verticalAlign="middle">
<Badge colorPalette="gray" loading={ isLoading }>
{ isScalar ? 'Scalar' : 'Non-scalar' }
</Badge>
</TableCell>
<TableCell verticalAlign="middle">
<Skeleton loading={ isLoading }>
{ hcuCost.toLocaleString() }
</Skeleton>
</TableCell>
<TableCell verticalAlign="middle">
<Skeleton loading={ isLoading } color="text.secondary">
{ hcuDepthValue.toLocaleString() }
</Skeleton>
</TableCell>
<TableCell verticalAlign="middle">
{ caller && caller.hash ? (
<AddressEntity
address={ caller }
truncation="dynamic"
isLoading={ isLoading }
/>
) : (
<Text color="text.secondary">—</Text>
) }
</TableCell>
</TableRow>
);
};
export default React.memo(TxFHEOperationsTableItem);
+17
View File
@@ -0,0 +1,17 @@
import type { FheOperationType } from 'types/api/fheOperations';
import type { BadgeProps } from 'toolkit/chakra/badge';
// Maps FHE operation types to Blockscout color palette
export function getTypeColor(type: FheOperationType): BadgeProps['colorPalette'] {
const colors: Record<FheOperationType, BadgeProps['colorPalette']> = {
comparison: 'purple',
control: 'orange',
arithmetic: 'blue',
bitwise: 'teal',
encryption: 'cyan',
unary: 'yellow',
random: 'pink',
};
return colors[type] || 'gray';
}