Commit 2ffc1e57 authored by tom goriunov's avatar tom goriunov Committed by GitHub

Suave customization (#1256)

* new fields in tx details page

* execution node txs page

* tab for wrapped tx details

* add loading state to tabs

* types enhancement

* change review env api host

* add front app paths to review env

* rename fields

* [skip ci] execution node -> computor

* rollback envs for review stand

* add ENV for SUAVE
parent 72550803
...@@ -15,5 +15,6 @@ export { default as safe } from './safe'; ...@@ -15,5 +15,6 @@ export { default as safe } from './safe';
export { default as sentry } from './sentry'; export { default as sentry } from './sentry';
export { default as sol2uml } from './sol2uml'; export { default as sol2uml } from './sol2uml';
export { default as stats } from './stats'; export { default as stats } from './stats';
export { default as suave } from './suave';
export { default as web3Wallet } from './web3Wallet'; export { default as web3Wallet } from './web3Wallet';
export { default as verifiedTokens } from './verifiedTokens'; export { default as verifiedTokens } from './verifiedTokens';
import type { Feature } from './types';
import { getEnvValue } from '../utils';
const title = 'SUAVE chain';
const config: Feature<{ isEnabled: true }> = (() => {
if (getEnvValue('NEXT_PUBLIC_IS_SUAVE_CHAIN') === 'true') {
return Object.freeze({
title,
isEnabled: true,
});
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
...@@ -357,6 +357,7 @@ const schema = yup ...@@ -357,6 +357,7 @@ const schema = yup
NEXT_PUBLIC_PROMOTE_BLOCKSCOUT_IN_TITLE: yup.boolean(), NEXT_PUBLIC_PROMOTE_BLOCKSCOUT_IN_TITLE: yup.boolean(),
NEXT_PUBLIC_OG_DESCRIPTION: yup.string(), NEXT_PUBLIC_OG_DESCRIPTION: yup.string(),
NEXT_PUBLIC_OG_IMAGE_URL: yup.string().test(urlTest), NEXT_PUBLIC_OG_IMAGE_URL: yup.string().test(urlTest),
NEXT_PUBLIC_IS_SUAVE_CHAIN: yup.boolean(),
// 6. External services envs // 6. External services envs
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: yup.string(), NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: yup.string(),
......
...@@ -17,12 +17,14 @@ frontend: ...@@ -17,12 +17,14 @@ frontend:
exact: exact:
# - "/(apps|auth/profile|account)" # - "/(apps|auth/profile|account)"
- "/" - "/"
- "/envs.js"
prefix: prefix:
- "/_next" - "/_next"
- "/node-api" - "/node-api"
- "/account" - "/account"
- "/apps" - "/apps"
- "/static" - "/static"
- "/assets"
- "/favicon" - "/favicon"
- "/auth/profile" - "/auth/profile"
- "/auth/unverified-email" - "/auth/unverified-email"
......
...@@ -35,6 +35,7 @@ The app instance could be customized by passing following variables to NodeJS en ...@@ -35,6 +35,7 @@ The app instance could be customized by passing following variables to NodeJS en
- [Blockchain statistics](ENVS.md#blockchain-statistics) - [Blockchain statistics](ENVS.md#blockchain-statistics)
- [Web3 wallet integration](ENVS.md#web3-wallet-integration-add-token-or-network-to-the-wallet) (add token or network to the wallet) - [Web3 wallet integration](ENVS.md#web3-wallet-integration-add-token-or-network-to-the-wallet) (add token or network to the wallet)
- [Verified tokens info](ENVS.md#verified-tokens-info) - [Verified tokens info](ENVS.md#verified-tokens-info)
- [SUAVE chain](ENVS.md#suave-chain)
- [Sentry error monitoring](ENVS.md#sentry-error-monitoring) - [Sentry error monitoring](ENVS.md#sentry-error-monitoring)
- [3rd party services configuration](ENVS.md#external-services-configuration) - [3rd party services configuration](ENVS.md#external-services-configuration)
...@@ -411,6 +412,16 @@ For the smart contract addresses which are [Safe{Core} accounts](https://safe.gl ...@@ -411,6 +412,16 @@ For the smart contract addresses which are [Safe{Core} accounts](https://safe.gl
&nbsp; &nbsp;
### SUAVE chain
For blockchains that implementing SUAVE architecture additional fields will be shown on the transaction page ("Allowed peekers", "Computor"). Users also will be able to see the list of all transaction for a particular Computor in the separate view.
| Variable | Type| Description | Compulsoriness | Default value | Example value |
| --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_IS_SUAVE_CHAIN | `boolean` | Set to true for blockchains with [SUAVE architecture](https://writings.flashbots.net/mevm-suave-centauri-and-beyond) | Required | - | `true` |
&nbsp;
### Sentry error monitoring ### Sentry error monitoring
| Variable | Type| Description | Compulsoriness | Default value | Example value | | Variable | Type| Description | Compulsoriness | Default value | Example value |
......
...@@ -198,6 +198,11 @@ export const RESOURCES = { ...@@ -198,6 +198,11 @@ export const RESOURCES = {
path: '/api/v2/transactions/watchlist', path: '/api/v2/transactions/watchlist',
filterFields: [ ], filterFields: [ ],
}, },
txs_execution_node: {
path: '/api/v2/transactions/execution-node/:hash',
pathParams: [ 'hash' as const ],
filterFields: [ ],
},
tx: { tx: {
path: '/api/v2/transactions/:hash', path: '/api/v2/transactions/:hash',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
...@@ -533,7 +538,7 @@ export interface ResourceError<T = unknown> { ...@@ -533,7 +538,7 @@ export interface ResourceError<T = unknown> {
export type ResourceErrorAccount<T> = ResourceError<{ errors: T }> export type ResourceErrorAccount<T> = ResourceError<{ errors: T }>
export type PaginatedResources = 'blocks' | 'block_txs' | export type PaginatedResources = 'blocks' | 'block_txs' |
'txs_validated' | 'txs_pending' | 'txs_watchlist' | 'txs_validated' | 'txs_pending' | 'txs_watchlist' | 'txs_execution_node' |
'tx_internal_txs' | 'tx_logs' | 'tx_token_transfers' | 'tx_state_changes' | 'tx_internal_txs' | 'tx_logs' | 'tx_token_transfers' | 'tx_state_changes' |
'addresses' | 'addresses' |
'address_txs' | 'address_internal_txs' | 'address_token_transfers' | 'address_blocks_validated' | 'address_coin_balance' | 'address_txs' | 'address_internal_txs' | 'address_token_transfers' | 'address_blocks_validated' | 'address_coin_balance' |
...@@ -577,6 +582,7 @@ Q extends 'block_withdrawals' ? BlockWithdrawalsResponse : ...@@ -577,6 +582,7 @@ Q extends 'block_withdrawals' ? BlockWithdrawalsResponse :
Q extends 'txs_validated' ? TransactionsResponseValidated : Q extends 'txs_validated' ? TransactionsResponseValidated :
Q extends 'txs_pending' ? TransactionsResponsePending : Q extends 'txs_pending' ? TransactionsResponsePending :
Q extends 'txs_watchlist' ? TransactionsResponseWatchlist : Q extends 'txs_watchlist' ? TransactionsResponseWatchlist :
Q extends 'txs_execution_node' ? TransactionsResponseValidated :
Q extends 'tx' ? Transaction : Q extends 'tx' ? Transaction :
Q extends 'tx_internal_txs' ? InternalTransactionsResponse : Q extends 'tx_internal_txs' ? InternalTransactionsResponse :
Q extends 'tx_logs' ? LogsResponseTx : Q extends 'tx_logs' ? LogsResponseTx :
......
...@@ -5,6 +5,7 @@ type OGPageType = 'Homepage' | 'Root page' | 'Regular page'; ...@@ -5,6 +5,7 @@ type OGPageType = 'Homepage' | 'Root page' | 'Regular page';
const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = { const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
'/': 'Homepage', '/': 'Homepage',
'/txs': 'Root page', '/txs': 'Root page',
'/txs/computor/[hash]': 'Regular page',
'/tx/[hash]': 'Regular page', '/tx/[hash]': 'Regular page',
'/blocks': 'Root page', '/blocks': 'Root page',
'/block/[height_or_hash]': 'Regular page', '/block/[height_or_hash]': 'Regular page',
......
...@@ -8,6 +8,7 @@ const DEFAULT_TEMPLATE = 'Blockscout is the #1 open-source blockchain explorer a ...@@ -8,6 +8,7 @@ const DEFAULT_TEMPLATE = 'Blockscout is the #1 open-source blockchain explorer a
const TEMPLATE_MAP: Record<Route['pathname'], string> = { const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/': DEFAULT_TEMPLATE, '/': DEFAULT_TEMPLATE,
'/txs': DEFAULT_TEMPLATE, '/txs': DEFAULT_TEMPLATE,
'/txs/computor/[hash]': DEFAULT_TEMPLATE,
'/tx/[hash]': 'View transaction %hash% on %network_title%', '/tx/[hash]': 'View transaction %hash% on %network_title%',
'/blocks': DEFAULT_TEMPLATE, '/blocks': DEFAULT_TEMPLATE,
'/block/[height_or_hash]': 'View the transactions, token transfers, and uncles for block %height_or_hash%', '/block/[height_or_hash]': 'View the transactions, token transfers, and uncles for block %height_or_hash%',
......
...@@ -3,6 +3,7 @@ import type { Route } from 'nextjs-routes'; ...@@ -3,6 +3,7 @@ import type { Route } from 'nextjs-routes';
const TEMPLATE_MAP: Record<Route['pathname'], string> = { const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/': 'blockchain explorer', '/': 'blockchain explorer',
'/txs': 'transactions', '/txs': 'transactions',
'/txs/computor/[hash]': 'computor %hash% transactions',
'/tx/[hash]': 'transaction %hash%', '/tx/[hash]': 'transaction %hash%',
'/blocks': 'blocks', '/blocks': 'blocks',
'/block/[height_or_hash]': 'block %height_or_hash%', '/block/[height_or_hash]': 'block %height_or_hash%',
......
...@@ -3,6 +3,7 @@ import type { Route } from 'nextjs-routes'; ...@@ -3,6 +3,7 @@ import type { Route } from 'nextjs-routes';
export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = { export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
'/': 'Homepage', '/': 'Homepage',
'/txs': 'Transactions', '/txs': 'Transactions',
'/txs/computor/[hash]': 'Computor transactions',
'/tx/[hash]': 'Transaction details', '/tx/[hash]': 'Transaction details',
'/blocks': 'Blocks', '/blocks': 'Blocks',
'/block/[height_or_hash]': 'Block details', '/block/[height_or_hash]': 'Block details',
......
...@@ -103,3 +103,13 @@ export const stats: GetServerSideProps<Props> = async(context) => { ...@@ -103,3 +103,13 @@ export const stats: GetServerSideProps<Props> = async(context) => {
return base(context); return base(context);
}; };
export const suave: GetServerSideProps<Props> = async(context) => {
if (!config.features.suave.isEnabled) {
return {
notFound: true,
};
}
return base(context);
};
...@@ -42,6 +42,7 @@ declare module "nextjs-routes" { ...@@ -42,6 +42,7 @@ declare module "nextjs-routes" {
| DynamicRoute<"/token/[hash]/instance/[id]", { "hash": string; "id": string }> | DynamicRoute<"/token/[hash]/instance/[id]", { "hash": string; "id": string }>
| StaticRoute<"/tokens"> | StaticRoute<"/tokens">
| DynamicRoute<"/tx/[hash]", { "hash": string }> | DynamicRoute<"/tx/[hash]", { "hash": string }>
| DynamicRoute<"/txs/computor/[hash]", { "hash": string }>
| StaticRoute<"/txs"> | StaticRoute<"/txs">
| StaticRoute<"/verified-contracts"> | StaticRoute<"/verified-contracts">
| StaticRoute<"/visualize/sol2uml"> | StaticRoute<"/visualize/sol2uml">
......
import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import React from 'react';
import type { Props } from 'nextjs/getServerSideProps';
import PageNextJs from 'nextjs/PageNextJs';
const ComputorTxs = dynamic(() => import('ui/pages/ComputorTxs'), { ssr: false });
const Page: NextPage<Props> = (props: Props) => {
return (
<PageNextJs pathname="/txs/computor/[hash]" query={ props }>
<ComputorTxs/>
</PageNextJs>
);
};
export default Page;
export { suave as getServerSideProps } from 'nextjs/getServerSideProps';
...@@ -9,6 +9,9 @@ export type TransactionRevertReason = { ...@@ -9,6 +9,9 @@ export type TransactionRevertReason = {
raw: string; raw: string;
} | DecodedInput; } | DecodedInput;
type WrappedTransactionFields = 'decoded_input' | 'fee' | 'gas_limit' | 'gas_price' | 'hash' | 'max_fee_per_gas' |
'max_priority_fee_per_gas' | 'method' | 'nonce' | 'raw_input' | 'to' | 'type' | 'value';
export type Transaction = { export type Transaction = {
to: AddressParam | null; to: AddressParam | null;
created_contract: AddressParam | null; created_contract: AddressParam | null;
...@@ -48,6 +51,10 @@ export type Transaction = { ...@@ -48,6 +51,10 @@ export type Transaction = {
l1_gas_price?: string; l1_gas_price?: string;
l1_gas_used?: string; l1_gas_used?: string;
has_error_in_internal_txs: boolean | null; has_error_in_internal_txs: boolean | null;
// SUAVE fields
execution_node?: AddressParam | null;
allowed_peekers?: Array<string>;
wrapped?: Pick<Transaction, WrappedTransactionFields>;
} }
export type TransactionsResponse = TransactionsResponseValidated | TransactionsResponsePending; export type TransactionsResponse = TransactionsResponseValidated | TransactionsResponsePending;
......
...@@ -4,4 +4,6 @@ export type ArrayElement<ArrType> = ArrType extends ReadonlyArray<infer ElementT ...@@ -4,4 +4,6 @@ export type ArrayElement<ArrType> = ArrType extends ReadonlyArray<infer ElementT
export type ExcludeNull<T> = T extends null ? never : T; export type ExcludeNull<T> = T extends null ? never : T;
export type ExcludeUndefined<T> = T extends undefined ? never : T;
export type KeysOfObjectOrNull<T> = keyof ExcludeNull<T>; export type KeysOfObjectOrNull<T> = keyof ExcludeNull<T>;
...@@ -24,6 +24,7 @@ import Icon from 'ui/shared/chakra/Icon'; ...@@ -24,6 +24,7 @@ import Icon from 'ui/shared/chakra/Icon';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import DetailsInfoItemDivider from 'ui/shared/DetailsInfoItemDivider';
import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio'; import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
...@@ -81,15 +82,6 @@ const BlockDetails = ({ query }: Props) => { ...@@ -81,15 +82,6 @@ const BlockDetails = ({ query }: Props) => {
return null; return null;
} }
const sectionGap = (
<GridItem
colSpan={{ base: undefined, lg: 2 }}
mt={{ base: 2, lg: 3 }}
mb={{ base: 0, lg: 3 }}
borderBottom="1px solid"
borderColor="divider"
/>
);
const { totalReward, staticReward, burntFees, txFees } = getBlockReward(data); const { totalReward, staticReward, burntFees, txFees } = getBlockReward(data);
const validatorTitle = getNetworkValidatorTitle(); const validatorTitle = getNetworkValidatorTitle();
...@@ -243,7 +235,7 @@ const BlockDetails = ({ query }: Props) => { ...@@ -243,7 +235,7 @@ const BlockDetails = ({ query }: Props) => {
)) ))
} }
{ sectionGap } <DetailsInfoItemDivider/>
<DetailsInfoItem <DetailsInfoItem
title="Gas used" title="Gas used"
...@@ -442,7 +434,7 @@ const BlockDetails = ({ query }: Props) => { ...@@ -442,7 +434,7 @@ const BlockDetails = ({ query }: Props) => {
</Box> </Box>
</DetailsInfoItem> </DetailsInfoItem>
{ sectionGap } <DetailsInfoItemDivider/>
<DetailsInfoItem <DetailsInfoItem
title="Hash" title="Hash"
......
import { useRouter } from 'next/router';
import React from 'react';
import getQueryParamString from 'lib/router/getQueryParamString';
import { TX } from 'stubs/tx';
import { generateListStub } from 'stubs/utils';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import PageTitle from 'ui/shared/Page/PageTitle';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import TxsContent from 'ui/txs/TxsContent';
const ComputorTxs = () => {
const router = useRouter();
const hash = getQueryParamString(router.query.hash);
const query = useQueryWithPages({
resourceName: 'txs_execution_node',
pathParams: { hash },
options: {
placeholderData: generateListStub<'txs_execution_node'>(TX, 50, { next_page_params: {
block_number: 9005713,
index: 5,
items_count: 50,
filter: 'validated',
} }),
},
});
return (
<>
<PageTitle title="Computor transactions" withTextAd/>
<AddressEntity address={{ hash }} mb={ 6 }/>
<TxsContent
query={ query }
showSocketInfo={ false }
/>
</>
);
};
export default ComputorTxs;
...@@ -3,6 +3,7 @@ import React from 'react'; ...@@ -3,6 +3,7 @@ import React from 'react';
import type { RoutedTab } from 'ui/shared/Tabs/types'; import type { RoutedTab } from 'ui/shared/Tabs/types';
import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/contexts/app'; import { useAppContext } from 'lib/contexts/app';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
...@@ -13,22 +14,16 @@ import EntityTags from 'ui/shared/EntityTags'; ...@@ -13,22 +14,16 @@ import EntityTags from 'ui/shared/EntityTags';
import NetworkExplorers from 'ui/shared/NetworkExplorers'; import NetworkExplorers from 'ui/shared/NetworkExplorers';
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 useTabIndexFromQuery from 'ui/shared/Tabs/useTabIndexFromQuery';
import TxDetails from 'ui/tx/TxDetails'; import TxDetails from 'ui/tx/TxDetails';
import TxDetailsWrapped from 'ui/tx/TxDetailsWrapped';
import TxInternals from 'ui/tx/TxInternals'; import TxInternals from 'ui/tx/TxInternals';
import TxLogs from 'ui/tx/TxLogs'; import TxLogs from 'ui/tx/TxLogs';
import TxRawTrace from 'ui/tx/TxRawTrace'; import TxRawTrace from 'ui/tx/TxRawTrace';
import TxState from 'ui/tx/TxState'; import TxState from 'ui/tx/TxState';
import TxTokenTransfer from 'ui/tx/TxTokenTransfer'; import TxTokenTransfer from 'ui/tx/TxTokenTransfer';
const TABS: Array<RoutedTab> = [
{ id: 'index', title: 'Details', component: <TxDetails/> },
{ id: 'token_transfers', title: 'Token transfers', component: <TxTokenTransfer/> },
{ id: 'internal', title: 'Internal txns', component: <TxInternals/> },
{ id: 'logs', title: 'Logs', component: <TxLogs/> },
{ id: 'state', title: 'State', component: <TxState/> },
{ id: 'raw_trace', title: 'Raw trace', component: <TxRawTrace/> },
];
const TransactionPageContent = () => { const TransactionPageContent = () => {
const router = useRouter(); const router = useRouter();
const appProps = useAppContext(); const appProps = useAppContext();
...@@ -44,6 +39,20 @@ const TransactionPageContent = () => { ...@@ -44,6 +39,20 @@ const TransactionPageContent = () => {
}, },
}); });
const tabs: Array<RoutedTab> = [
{ id: 'index', title: config.features.suave.isEnabled && data?.wrapped ? 'Confidential compute tx details' : 'Details', component: <TxDetails/> },
config.features.suave.isEnabled && data?.wrapped ?
{ id: 'wrapped', title: 'Regular tx details', component: <TxDetailsWrapped data={ data.wrapped }/> } :
undefined,
{ id: 'token_transfers', title: 'Token transfers', component: <TxTokenTransfer/> },
{ id: 'internal', title: 'Internal txns', component: <TxInternals/> },
{ id: 'logs', title: 'Logs', component: <TxLogs/> },
{ id: 'state', title: 'State', component: <TxState/> },
{ id: 'raw_trace', title: 'Raw trace', component: <TxRawTrace/> },
].filter(Boolean);
const tabIndex = useTabIndexFromQuery(tabs);
const tags = ( const tags = (
<EntityTags <EntityTags
isLoading={ isPlaceholderData } isLoading={ isPlaceholderData }
...@@ -75,7 +84,12 @@ const TransactionPageContent = () => { ...@@ -75,7 +84,12 @@ const TransactionPageContent = () => {
backLink={ backLink } backLink={ backLink }
contentAfter={ tags } contentAfter={ tags }
/> />
<RoutedTabs tabs={ TABS }/> { isPlaceholderData ? (
<>
<TabsSkeleton tabs={ tabs } mt={ 6 }/>
{ tabs[tabIndex]?.component }
</>
) : <RoutedTabs tabs={ tabs }/> }
</> </>
); );
}; };
......
import { GridItem } from '@chakra-ui/react'; import { GridItem } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
const TokenInstanceDivider = () => { const DetailsInfoItemDivider = () => {
return ( return (
<GridItem <GridItem
colSpan={{ base: undefined, lg: 2 }} colSpan={{ base: undefined, lg: 2 }}
...@@ -13,4 +13,4 @@ const TokenInstanceDivider = () => { ...@@ -13,4 +13,4 @@ const TokenInstanceDivider = () => {
); );
}; };
export default TokenInstanceDivider; export default DetailsInfoItemDivider;
...@@ -5,6 +5,7 @@ import type { TokenInstance } from 'types/api/token'; ...@@ -5,6 +5,7 @@ import type { TokenInstance } from 'types/api/token';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import DetailsInfoItemDivider from 'ui/shared/DetailsInfoItemDivider';
import DetailsSponsoredItem from 'ui/shared/DetailsSponsoredItem'; import DetailsSponsoredItem from 'ui/shared/DetailsSponsoredItem';
import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import TokenEntity from 'ui/shared/entities/token/TokenEntity'; import TokenEntity from 'ui/shared/entities/token/TokenEntity';
...@@ -12,7 +13,6 @@ import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; ...@@ -12,7 +13,6 @@ import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import NftMedia from 'ui/shared/nft/NftMedia'; import NftMedia from 'ui/shared/nft/NftMedia';
import TokenInstanceCreatorAddress from './details/TokenInstanceCreatorAddress'; import TokenInstanceCreatorAddress from './details/TokenInstanceCreatorAddress';
import TokenInstanceDivider from './details/TokenInstanceDivider';
import TokenInstanceMetadataInfo from './details/TokenInstanceMetadataInfo'; import TokenInstanceMetadataInfo from './details/TokenInstanceMetadataInfo';
import TokenInstanceTransfersCount from './details/TokenInstanceTransfersCount'; import TokenInstanceTransfersCount from './details/TokenInstanceTransfersCount';
...@@ -99,7 +99,7 @@ const TokenInstanceDetails = ({ data, scrollRef, isLoading }: Props) => { ...@@ -99,7 +99,7 @@ const TokenInstanceDetails = ({ data, scrollRef, isLoading }: Props) => {
overflow="hidden" overflow="hidden"
> >
<TokenInstanceMetadataInfo data={ data } isLoading={ isLoading }/> <TokenInstanceMetadataInfo data={ data } isLoading={ isLoading }/>
<TokenInstanceDivider/> <DetailsInfoItemDivider/>
<DetailsSponsoredItem isLoading={ isLoading }/> <DetailsSponsoredItem isLoading={ isLoading }/>
</Grid> </Grid>
</> </>
......
...@@ -6,11 +6,10 @@ import type { MetadataAttributes } from 'types/client/token'; ...@@ -6,11 +6,10 @@ import type { MetadataAttributes } from 'types/client/token';
import parseMetadata from 'lib/token/parseMetadata'; import parseMetadata from 'lib/token/parseMetadata';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import DetailsInfoItemDivider from 'ui/shared/DetailsInfoItemDivider';
import LinkExternal from 'ui/shared/LinkExternal'; import LinkExternal from 'ui/shared/LinkExternal';
import TruncatedValue from 'ui/shared/TruncatedValue'; import TruncatedValue from 'ui/shared/TruncatedValue';
import TokenInstanceDivider from './TokenInstanceDivider';
interface Props { interface Props {
data?: TokenInstance; data?: TokenInstance;
isLoading?: boolean; isLoading?: boolean;
...@@ -71,7 +70,7 @@ const TokenInstanceMetadataInfo = ({ data, isLoading }: Props) => { ...@@ -71,7 +70,7 @@ const TokenInstanceMetadataInfo = ({ data, isLoading }: Props) => {
return ( return (
<> <>
<TokenInstanceDivider/> <DetailsInfoItemDivider/>
{ metadata?.name && ( { metadata?.name && (
<DetailsInfoItem <DetailsInfoItem
title="Name" title="Name"
......
import { Flex, Link, useBoolean } from '@chakra-ui/react';
import React from 'react';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
interface Props {
items: Array<string>;
}
const CUT_LENGTH = 2;
const TxAllowedPeekers = ({ items }: Props) => {
const [ isExpanded, expand ] = useBoolean(false);
return (
<DetailsInfoItem
title="Allowed peekers"
hint="Smart contracts allowed to interact with confidential data"
>
<Flex flexDir="column" rowGap={ 3 } w="100%">
{ items
.slice(0, isExpanded ? undefined : CUT_LENGTH)
.map((item) => <AddressEntity key={ item } address={{ hash: item, is_contract: true }}/>) }
</Flex>
{ items.length > CUT_LENGTH && (
<Link
display="inline-block"
fontSize="sm"
textDecorationLine="underline"
textDecorationStyle="dashed"
onClick={ expand.toggle }
>
{ isExpanded ? 'Hide' : 'Show all' }
</Link>
) }
</DetailsInfoItem>
);
};
export default React.memo(TxAllowedPeekers);
...@@ -17,6 +17,8 @@ import BigNumber from 'bignumber.js'; ...@@ -17,6 +17,8 @@ import BigNumber from 'bignumber.js';
import React from 'react'; import React from 'react';
import { scroller, Element } from 'react-scroll'; import { scroller, Element } from 'react-scroll';
import { route } from 'nextjs-routes';
import config from 'configs/app'; import config from 'configs/app';
import clockIcon from 'icons/clock.svg'; import clockIcon from 'icons/clock.svg';
import flameIcon from 'icons/flame.svg'; import flameIcon from 'icons/flame.svg';
...@@ -32,6 +34,7 @@ import CopyToClipboard from 'ui/shared/CopyToClipboard'; ...@@ -32,6 +34,7 @@ import CopyToClipboard from 'ui/shared/CopyToClipboard';
import CurrencyValue from 'ui/shared/CurrencyValue'; import CurrencyValue from 'ui/shared/CurrencyValue';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import DetailsInfoItemDivider from 'ui/shared/DetailsInfoItemDivider';
import DetailsSponsoredItem from 'ui/shared/DetailsSponsoredItem'; import DetailsSponsoredItem from 'ui/shared/DetailsSponsoredItem';
import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import BlockEntity from 'ui/shared/entities/block/BlockEntity'; import BlockEntity from 'ui/shared/entities/block/BlockEntity';
...@@ -42,8 +45,11 @@ import TextSeparator from 'ui/shared/TextSeparator'; ...@@ -42,8 +45,11 @@ import TextSeparator from 'ui/shared/TextSeparator';
import TxStatus from 'ui/shared/TxStatus'; import TxStatus from 'ui/shared/TxStatus';
import Utilization from 'ui/shared/Utilization/Utilization'; import Utilization from 'ui/shared/Utilization/Utilization';
import TxDetailsActions from 'ui/tx/details/TxDetailsActions'; import TxDetailsActions from 'ui/tx/details/TxDetailsActions';
import TxDetailsGasPrice from 'ui/tx/details/TxDetailsGasPrice';
import TxDetailsOther from 'ui/tx/details/TxDetailsOther';
import TxDetailsTokenTransfers from 'ui/tx/details/TxDetailsTokenTransfers'; import TxDetailsTokenTransfers from 'ui/tx/details/TxDetailsTokenTransfers';
import TxRevertReason from 'ui/tx/details/TxRevertReason'; import TxRevertReason from 'ui/tx/details/TxRevertReason';
import TxAllowedPeekers from 'ui/tx/TxAllowedPeekers';
import TxSocketAlert from 'ui/tx/TxSocketAlert'; import TxSocketAlert from 'ui/tx/TxSocketAlert';
import useFetchTxInfo from 'ui/tx/useFetchTxInfo'; import useFetchTxInfo from 'ui/tx/useFetchTxInfo';
...@@ -107,19 +113,13 @@ const TxDetails = () => { ...@@ -107,19 +113,13 @@ const TxDetails = () => {
</Tooltip> </Tooltip>
) : null; ) : null;
const divider = (
<GridItem
colSpan={{ base: undefined, lg: 2 }}
mt={{ base: 2, lg: 3 }}
mb={{ base: 0, lg: 3 }}
borderBottom="1px solid"
borderColor="divider"
/>
);
return ( return (
<> <>
{ config.chain.isTestnet && <Alert status="warning" mb={ 6 }>This is a testnet transaction only</Alert> } { config.chain.isTestnet && (
<Skeleton mb={ 6 } isLoaded={ !isPlaceholderData }>
<Alert status="warning">This is a testnet transaction only</Alert>
</Skeleton>
) }
<Grid columnGap={ 8 } rowGap={{ base: 3, lg: 3 }} templateColumns={{ base: 'minmax(0, 1fr)', lg: 'max-content minmax(728px, auto)' }}> <Grid columnGap={ 8 } rowGap={{ base: 3, lg: 3 }} templateColumns={{ base: 'minmax(0, 1fr)', lg: 'max-content minmax(728px, auto)' }}>
{ socketStatus && ( { socketStatus && (
<GridItem colSpan={{ base: undefined, lg: 2 }} mb={ 2 }> <GridItem colSpan={{ base: undefined, lg: 2 }} mb={ 2 }>
...@@ -196,14 +196,29 @@ const TxDetails = () => { ...@@ -196,14 +196,29 @@ const TxDetails = () => {
</Skeleton> </Skeleton>
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
{ data.execution_node && (
<DetailsInfoItem
title="Computor"
hint="Node that carried out the confidential computation"
isLoading={ isPlaceholderData }
>
<AddressEntity
address={ data.execution_node }
href={ route({ pathname: '/txs/computor/[hash]', query: { hash: data.execution_node.hash } }) }
/>
</DetailsInfoItem>
) }
{ data.allowed_peekers && data.allowed_peekers.length > 0 && (
<TxAllowedPeekers items={ data.allowed_peekers }/>
) }
<DetailsSponsoredItem isLoading={ isPlaceholderData }/> <DetailsSponsoredItem isLoading={ isPlaceholderData }/>
{ divider } <DetailsInfoItemDivider/>
{ actionsExist && ( { actionsExist && (
<> <>
<TxDetailsActions actions={ data.actions }/> <TxDetailsActions actions={ data.actions }/>
{ divider } <DetailsInfoItemDivider/>
</> </>
) } ) }
...@@ -267,7 +282,7 @@ const TxDetails = () => { ...@@ -267,7 +282,7 @@ const TxDetails = () => {
</DetailsInfoItem> </DetailsInfoItem>
{ data.token_transfers && <TxDetailsTokenTransfers data={ data.token_transfers } txHash={ data.hash }/> } { data.token_transfers && <TxDetailsTokenTransfers data={ data.token_transfers } txHash={ data.hash }/> }
{ divider } <DetailsInfoItemDivider/>
<DetailsInfoItem <DetailsInfoItem
title="Value" title="Value"
...@@ -295,18 +310,7 @@ const TxDetails = () => { ...@@ -295,18 +310,7 @@ const TxDetails = () => {
isLoading={ isPlaceholderData } isLoading={ isPlaceholderData }
/> />
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem <TxDetailsGasPrice gasPrice={ data.gas_price } isLoading={ isPlaceholderData }/>
title="Gas price"
hint="Price per unit of gas specified by the sender. Higher gas prices can prioritize transaction inclusion during times of high usage"
isLoading={ isPlaceholderData }
>
<Skeleton isLoaded={ !isPlaceholderData } mr={ 1 }>
{ BigNumber(data.gas_price).dividedBy(WEI).toFixed() } { config.chain.currency.symbol }
</Skeleton>
<Skeleton isLoaded={ !isPlaceholderData } color="text_secondary">
<span>({ BigNumber(data.gas_price).dividedBy(WEI_IN_GWEI).toFixed() } Gwei)</span>
</Skeleton>
</DetailsInfoItem>
<DetailsInfoItem <DetailsInfoItem
title="Gas usage & limit by txn" title="Gas usage & limit by txn"
hint="Actual gas amount used by the transaction" hint="Actual gas amount used by the transaction"
...@@ -430,39 +434,7 @@ const TxDetails = () => { ...@@ -430,39 +434,7 @@ const TxDetails = () => {
{ isExpanded && ( { isExpanded && (
<> <>
<GridItem colSpan={{ base: undefined, lg: 2 }} mt={{ base: 1, lg: 4 }}/> <GridItem colSpan={{ base: undefined, lg: 2 }} mt={{ base: 1, lg: 4 }}/>
<DetailsInfoItem <TxDetailsOther nonce={ data.nonce } type={ data.type } position={ data.position }/>
title="Other"
hint="Other data related to this transaction"
>
{
[
typeof data.type === 'number' && (
<Box key="type">
<Text as="span" fontWeight="500">Txn type: </Text>
<Text fontWeight="600" as="span">{ data.type }</Text>
{ data.type === 2 && <Text fontWeight="400" as="span" ml={ 1 } variant="secondary">(EIP-1559)</Text> }
</Box>
),
<Box key="nonce">
<Text as="span" fontWeight="500">Nonce: </Text>
<Text fontWeight="600" as="span">{ data.nonce }</Text>
</Box>,
data.position !== null && (
<Box key="position">
<Text as="span" fontWeight="500">Position: </Text>
<Text fontWeight="600" as="span">{ data.position }</Text>
</Box>
),
]
.filter(Boolean)
.map((item, index) => (
<>
{ index !== 0 && <TextSeparator/> }
{ item }
</>
))
}
</DetailsInfoItem>
<DetailsInfoItem <DetailsInfoItem
title="Raw input" title="Raw input"
hint="Binary data included with the transaction. See logs tab for additional info" hint="Binary data included with the transaction. See logs tab for additional info"
......
import { Flex, Grid } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { Transaction } from 'types/api/transaction';
import type { ExcludeUndefined } from 'types/utils';
import config from 'configs/app';
import Tag from 'ui/shared/chakra/Tag';
import CurrencyValue from 'ui/shared/CurrencyValue';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import DetailsInfoItemDivider from 'ui/shared/DetailsInfoItemDivider';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import LogDecodedInputData from 'ui/shared/logs/LogDecodedInputData';
import RawInputData from 'ui/shared/RawInputData';
import TxDetailsGasPrice from 'ui/tx/details/TxDetailsGasPrice';
import TxDetailsOther from 'ui/tx/details/TxDetailsOther';
interface Props {
data: ExcludeUndefined<Transaction['wrapped']>;
}
const TxDetailsWrapped = ({ data }: Props) => {
return (
<Grid columnGap={ 8 } rowGap={{ base: 3, lg: 3 }} templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }}>
<DetailsInfoItem
title="Transaction hash"
hint="Unique character string (TxID) assigned to every verified transaction"
flexWrap="nowrap"
>
<TxEntity hash={ data.hash } noIcon noLink noCopy={ false }/>
</DetailsInfoItem>
<DetailsInfoItem
title="Method"
hint="Transaction method name"
>
<Tag colorScheme="gray">
{ data.method }
</Tag>
</DetailsInfoItem>
<DetailsInfoItemDivider/>
<DetailsInfoItem
title={ data.to?.is_contract ? 'Interacted with contract' : 'To' }
hint="Address (external or contract) receiving the transaction"
flexWrap={{ base: 'wrap', lg: 'nowrap' }}
columnGap={ 3 }
>
<Flex flexWrap="nowrap" alignItems="center" maxW="100%">
<AddressEntity address={ data.to }/>
</Flex>
</DetailsInfoItem>
<DetailsInfoItemDivider/>
<DetailsInfoItem
title="Value"
hint="Value sent in the native token (and USD) if applicable"
>
<CurrencyValue
value={ data.value }
currency={ config.chain.currency.symbol }
flexWrap="wrap"
/>
</DetailsInfoItem>
<DetailsInfoItem
title="Transaction fee"
hint="Total transaction fee"
>
<CurrencyValue
value={ data.fee.value }
currency={ config.chain.currency.symbol }
flexWrap="wrap"
/>
</DetailsInfoItem>
<TxDetailsGasPrice gasPrice={ data.gas_price }/>
{ data.gas_limit && (
<DetailsInfoItem
title="Gas limit"
hint="Maximum amount of gas that can be used by the transaction"
>
{ BigNumber(data.gas_limit).toFormat() }
</DetailsInfoItem>
) }
<DetailsInfoItemDivider/>
<TxDetailsOther type={ data.type } nonce={ data.nonce } position={ null }/>
<DetailsInfoItem
title="Raw input"
hint="Binary data included with the transaction. See logs tab for additional info"
>
<RawInputData hex={ data.raw_input }/>
</DetailsInfoItem>
{ data.decoded_input && (
<DetailsInfoItem
title="Decoded input data"
hint="Decoded input data"
>
<LogDecodedInputData data={ data.decoded_input }/>
</DetailsInfoItem>
) }
</Grid>
);
};
export default TxDetailsWrapped;
import { Skeleton } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import config from 'configs/app';
import { WEI, WEI_IN_GWEI } from 'lib/consts';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
interface Props {
gasPrice: string;
isLoading?: boolean;
}
const TxDetailsGasPrice = ({ gasPrice, isLoading }: Props) => {
return (
<DetailsInfoItem
title="Gas price"
hint="Price per unit of gas specified by the sender. Higher gas prices can prioritize transaction inclusion during times of high usage"
isLoading={ isLoading }
>
<Skeleton isLoaded={ !isLoading } mr={ 1 }>
{ BigNumber(gasPrice).dividedBy(WEI).toFixed() } { config.chain.currency.symbol }
</Skeleton>
<Skeleton isLoaded={ !isLoading } color="text_secondary">
<span>({ BigNumber(gasPrice).dividedBy(WEI_IN_GWEI).toFixed() } Gwei)</span>
</Skeleton>
</DetailsInfoItem>
);
};
export default TxDetailsGasPrice;
import { Box, Text } from '@chakra-ui/react';
import React from 'react';
import type { Transaction } from 'types/api/transaction';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import TextSeparator from 'ui/shared/TextSeparator';
type Props = Pick<Transaction, 'nonce' | 'type' | 'position'>
const TxDetailsOther = ({ nonce, type, position }: Props) => {
return (
<DetailsInfoItem
title="Other"
hint="Other data related to this transaction"
>
{
[
typeof type === 'number' && (
<Box key="type">
<Text as="span" fontWeight="500">Txn type: </Text>
<Text fontWeight="600" as="span">{ type }</Text>
{ type === 2 && <Text fontWeight="400" as="span" ml={ 1 } variant="secondary">(EIP-1559)</Text> }
</Box>
),
<Box key="nonce">
<Text as="span" fontWeight="500">Nonce: </Text>
<Text fontWeight="600" as="span">{ nonce }</Text>
</Box>,
position !== null && position !== undefined && (
<Box key="position">
<Text as="span" fontWeight="500">Position: </Text>
<Text fontWeight="600" as="span">{ position }</Text>
</Box>
),
]
.filter(Boolean)
.map((item, index) => (
<>
{ index !== 0 && <TextSeparator/> }
{ item }
</>
))
}
</DetailsInfoItem>
);
};
export default TxDetailsOther;
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