mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
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:
committed by
GitHub
co-authored by
Maksym Zub
parent
f8b493ee2f
commit
40ccfd77aa
@@ -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 :
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
@@ -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';
|
||||
}
|
||||
Reference in New Issue
Block a user