mirror of
https://github.com/luxfi/explore.git
synced 2026-07-27 05:54:15 +00:00
Support EIP-7702 addresses (#2523)
* tx info customizations * authorizations tab * implement change for address page * amends
This commit is contained in:
@@ -21,6 +21,7 @@ on:
|
||||
- eth_sepolia
|
||||
- eth_goerli
|
||||
- filecoin
|
||||
- mekong
|
||||
- optimism
|
||||
- optimism_celestia
|
||||
- optimism_sepolia
|
||||
|
||||
Vendored
+1
@@ -369,6 +369,7 @@
|
||||
"eth_goerli",
|
||||
"eth_sepolia",
|
||||
"filecoin",
|
||||
"mekong",
|
||||
"optimism",
|
||||
"optimism_celestia",
|
||||
"optimism_sepolia",
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Set of ENVs for Mekong network explorer
|
||||
# https://mekong.blockscout.com
|
||||
# This is an auto-generated file. To update all values, run "yarn dev:preset:sync --name=mekong"
|
||||
|
||||
# Local ENVs
|
||||
NEXT_PUBLIC_APP_PROTOCOL=http
|
||||
NEXT_PUBLIC_APP_HOST=localhost
|
||||
NEXT_PUBLIC_APP_PORT=3000
|
||||
NEXT_PUBLIC_APP_ENV=development
|
||||
NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
|
||||
|
||||
# Instance ENVs
|
||||
NEXT_PUBLIC_API_BASE_PATH=/
|
||||
NEXT_PUBLIC_API_HOST=mekong.blockscout.com
|
||||
NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
|
||||
NEXT_PUBLIC_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/sherblockHolmesBadge
|
||||
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x7c7d9e09a5e0e6441a81efe57dbcf08848cd18a1f4238e28152faead390066a4
|
||||
NEXT_PUBLIC_HAS_BEACON_CHAIN=true
|
||||
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
|
||||
NEXT_PUBLIC_IS_TESTNET=true
|
||||
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
|
||||
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
|
||||
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=ETH
|
||||
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
|
||||
NEXT_PUBLIC_NETWORK_ID=7078815900
|
||||
NEXT_PUBLIC_NETWORK_NAME=Mekong
|
||||
NEXT_PUBLIC_NETWORK_RPC_URL=https://rpc.mekong.ethpandaops.io
|
||||
NEXT_PUBLIC_NETWORK_SHORT_NAME=Mekong
|
||||
NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true
|
||||
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
|
||||
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
|
||||
@@ -60,6 +60,12 @@ export const withoutName: AddressParam = {
|
||||
ens_domain_name: null,
|
||||
};
|
||||
|
||||
export const delegated: AddressParam = {
|
||||
...withoutName,
|
||||
is_verified: true,
|
||||
proxy_type: 'eip7702',
|
||||
};
|
||||
|
||||
export const token: Address = {
|
||||
hash: hash,
|
||||
implementations: null,
|
||||
|
||||
@@ -14,6 +14,7 @@ const PRESETS = {
|
||||
garnet: 'https://explorer.garnetchain.com',
|
||||
filecoin: 'https://filecoin.blockscout.com',
|
||||
gnosis: 'https://gnosis.blockscout.com',
|
||||
mekong: 'https://mekong.blockscout.com',
|
||||
optimism: 'https://optimism.blockscout.com',
|
||||
optimism_celestia: 'https://opcelestia-raspberry.gelatoscout.com',
|
||||
optimism_sepolia: 'https://optimism-sepolia.blockscout.com',
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Transaction } from 'types/api/transaction';
|
||||
|
||||
import type { UserTags, AddressImplementation, AddressParam, AddressFilecoinParams } from './addressParams';
|
||||
import type { Block, EpochRewardsType } from './block';
|
||||
import type { SmartContractProxyType } from './contract';
|
||||
import type { InternalTransaction } from './internalTransaction';
|
||||
import type { MudWorldSchema, MudWorldTable } from './mudWorlds';
|
||||
import type { NFTTokenType, TokenInfo, TokenInstance, TokenType } from './token';
|
||||
@@ -31,6 +32,7 @@ export interface Address extends UserTags {
|
||||
name: string | null;
|
||||
token: TokenInfo | null;
|
||||
watchlist_address_id: number | null;
|
||||
proxy_type?: SmartContractProxyType | null;
|
||||
}
|
||||
|
||||
export interface AddressZilliqaParams {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { AddressMetadataTagApi } from './addressMetadata';
|
||||
import type { SmartContractProxyType } from './contract';
|
||||
|
||||
export interface AddressImplementation {
|
||||
address: string;
|
||||
@@ -59,6 +60,7 @@ export type AddressParamBasic = {
|
||||
tags: Array<AddressMetadataTagApi>;
|
||||
} | null;
|
||||
filecoin?: AddressFilecoinParams;
|
||||
proxy_type?: SmartContractProxyType | null;
|
||||
};
|
||||
|
||||
export type AddressParam = UserTags & AddressParamBasic;
|
||||
|
||||
@@ -25,6 +25,7 @@ export type SmartContractProxyType =
|
||||
| 'eip1822'
|
||||
| 'eip930'
|
||||
| 'eip2535'
|
||||
| 'eip7702'
|
||||
| 'master_copy'
|
||||
| 'basic_implementation'
|
||||
| 'basic_get_implementation'
|
||||
|
||||
@@ -105,6 +105,8 @@ export type Transaction = {
|
||||
translation?: NovesTxTranslation;
|
||||
arbitrum?: ArbitrumTransactionData;
|
||||
scroll?: ScrollTransactionData;
|
||||
// EIP-7702
|
||||
authorization_list?: Array<TxAuthorization>;
|
||||
};
|
||||
|
||||
type ArbitrumTransactionData = {
|
||||
@@ -206,3 +208,10 @@ export type ScrollTransactionData = {
|
||||
l2_block_status: ScrollL2BlockStatus;
|
||||
queue_index: number;
|
||||
};
|
||||
|
||||
export interface TxAuthorization {
|
||||
address: string;
|
||||
authority: string;
|
||||
chain_id: number;
|
||||
nonce: number;
|
||||
}
|
||||
|
||||
@@ -170,6 +170,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
|
||||
<AddressImplementations
|
||||
data={ data.implementations }
|
||||
isLoading={ addressQuery.isPlaceholderData }
|
||||
proxyType={ data.proxy_type }
|
||||
/>
|
||||
) }
|
||||
|
||||
|
||||
@@ -40,7 +40,8 @@ const ContractDetails = ({ addressHash, channel, mainContractQuery }: Props) =>
|
||||
const addressInfo = queryClient.getQueryData<AddressInfo>(getResourceKey('address', { pathParams: { hash: addressHash } }));
|
||||
|
||||
const sourceItems: Array<AddressImplementation> = React.useMemo(() => {
|
||||
const currentAddressItem = { address: addressHash, name: addressInfo?.name || 'Current contract' };
|
||||
const currentAddressDefaultName = addressInfo?.proxy_type === 'eip7702' ? 'Delegate address' : 'Current contract';
|
||||
const currentAddressItem = { address: addressHash, name: addressInfo?.name || currentAddressDefaultName };
|
||||
if (!addressInfo || !addressInfo.implementations || addressInfo.implementations.length === 0) {
|
||||
return [ currentAddressItem ];
|
||||
}
|
||||
|
||||
@@ -9,11 +9,11 @@ interface Props {
|
||||
type: NonNullable<SmartContractProxyType>;
|
||||
}
|
||||
|
||||
const PROXY_TYPES: Record<NonNullable<SmartContractProxyType>, {
|
||||
const PROXY_TYPES: Partial<Record<NonNullable<SmartContractProxyType>, {
|
||||
name: string;
|
||||
link?: string;
|
||||
description?: string;
|
||||
}> = {
|
||||
}>> = {
|
||||
eip1167: {
|
||||
name: 'EIP-1167',
|
||||
link: 'https://eips.ethereum.org/EIPS/eip-1167',
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
|
||||
import type { AddressImplementation } from 'types/api/addressParams';
|
||||
import type { SmartContractProxyType } from 'types/api/contract';
|
||||
|
||||
import useApiQuery from 'lib/api/useApiQuery';
|
||||
import getQueryParamString from 'lib/router/getQueryParamString';
|
||||
@@ -18,9 +19,10 @@ import { formatAbi } from './utils';
|
||||
interface Props {
|
||||
implementations: Array<AddressImplementation>;
|
||||
isLoading?: boolean;
|
||||
proxyType?: SmartContractProxyType;
|
||||
}
|
||||
|
||||
const ContractMethodsProxy = ({ implementations, isLoading: isInitialLoading }: Props) => {
|
||||
const ContractMethodsProxy = ({ implementations, isLoading: isInitialLoading, proxyType }: Props) => {
|
||||
const router = useRouter();
|
||||
const sourceAddress = getQueryParamString(router.query.source_address);
|
||||
const tab = getQueryParamString(router.query.tab);
|
||||
@@ -48,7 +50,7 @@ const ContractMethodsProxy = ({ implementations, isLoading: isInitialLoading }:
|
||||
selectedItem={ selectedItem }
|
||||
onItemSelect={ setSelectedItem }
|
||||
isLoading={ isInitialLoading }
|
||||
label="Implementation address"
|
||||
label={ proxyType === 'eip7702' ? 'Delegate address' : 'Implementation address' }
|
||||
mb={ 3 }
|
||||
/>
|
||||
<ContractMethodsFilters
|
||||
|
||||
@@ -26,7 +26,7 @@ interface Props {
|
||||
|
||||
export default function useContractDetailsTabs({ data, isLoading, addressHash, sourceAddress }: Props): Array<Tab> {
|
||||
|
||||
const canBeVerified = !data?.is_self_destructed && !data?.is_verified;
|
||||
const canBeVerified = !data?.is_self_destructed && !data?.is_verified && data?.proxy_type !== 'eip7702';
|
||||
|
||||
return React.useMemo(() => {
|
||||
const verificationButton = (
|
||||
|
||||
@@ -88,7 +88,13 @@ export default function useContractTabs(data: Address | undefined, isPlaceholder
|
||||
verifiedImplementations.length > 0 && {
|
||||
id: [ 'read_write_proxy' as const, 'read_proxy' as const, 'write_proxy' as const ],
|
||||
title: 'Read/Write proxy',
|
||||
component: <ContractMethodsProxy implementations={ verifiedImplementations } isLoading={ contractQuery.isPlaceholderData }/>,
|
||||
component: (
|
||||
<ContractMethodsProxy
|
||||
implementations={ verifiedImplementations }
|
||||
isLoading={ contractQuery.isPlaceholderData }
|
||||
proxyType={ contractQuery.data?.proxy_type }
|
||||
/>
|
||||
),
|
||||
},
|
||||
config.features.account.isEnabled && {
|
||||
id: [ 'read_write_custom_methods' as const, 'read_custom_methods' as const, 'write_custom_methods' as const ],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import type { AddressImplementation } from 'types/api/addressParams';
|
||||
import type { SmartContractProxyType } from 'types/api/contract';
|
||||
|
||||
import * as DetailsInfoItem from 'ui/shared/DetailsInfoItem';
|
||||
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
|
||||
@@ -8,20 +9,26 @@ import AddressEntity from 'ui/shared/entities/address/AddressEntity';
|
||||
interface Props {
|
||||
data: Array<AddressImplementation>;
|
||||
isLoading?: boolean;
|
||||
proxyType?: SmartContractProxyType;
|
||||
}
|
||||
|
||||
const AddressImplementations = ({ data, isLoading }: Props) => {
|
||||
const AddressImplementations = ({ data, isLoading, proxyType }: Props) => {
|
||||
const hasManyItems = data.length > 1;
|
||||
const [ hasScroll, setHasScroll ] = React.useState(false);
|
||||
|
||||
const text = proxyType === 'eip7702' ? 'Delegate address' : `Implementation${ hasManyItems ? 's' : '' }`;
|
||||
const hint = proxyType === 'eip7702' ?
|
||||
'Account\'s executable code address' :
|
||||
`Implementation${ hasManyItems ? 's' : '' } address${ hasManyItems ? 'es' : '' } of the proxy contract`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<DetailsInfoItem.Label
|
||||
hint={ `Implementation${ hasManyItems ? 's' : '' } address${ hasManyItems ? 'es' : '' } of the proxy contract` }
|
||||
hint={ hint }
|
||||
isLoading={ isLoading }
|
||||
hasScroll={ hasScroll }
|
||||
>
|
||||
{ `Implementation${ hasManyItems ? 's' : '' }` }
|
||||
{ text }
|
||||
</DetailsInfoItem.Label>
|
||||
<DetailsInfoItem.ValueWithScroll
|
||||
gradientHeight={ 48 }
|
||||
|
||||
+11
-4
@@ -239,16 +239,18 @@ const AddressPageContent = () => {
|
||||
addressQuery.data?.is_contract ? {
|
||||
id: 'contract',
|
||||
title: () => {
|
||||
const tabName = addressQuery.data.proxy_type === 'eip7702' ? 'Code' : 'Contract';
|
||||
|
||||
if (addressQuery.data.is_verified) {
|
||||
return (
|
||||
<>
|
||||
<span>Contract</span>
|
||||
<span>{ tabName }</span>
|
||||
<IconSvg name="status/success" boxSize="14px" color="green.500" ml={ 1 }/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return 'Contract';
|
||||
return tabName;
|
||||
},
|
||||
component: (
|
||||
<AddressContract
|
||||
@@ -279,7 +281,12 @@ const AddressPageContent = () => {
|
||||
config.features.validators.isEnabled && addressQuery.data?.has_validated_blocks ?
|
||||
{ slug: 'validator', name: 'Validator', tagType: 'custom' as const, ordinal: PREDEFINED_TAG_PRIORITY } :
|
||||
undefined,
|
||||
addressQuery.data?.implementations?.length ? { slug: 'proxy', name: 'Proxy', tagType: 'custom' as const, ordinal: PREDEFINED_TAG_PRIORITY } : undefined,
|
||||
addressQuery.data?.implementations?.length && addressQuery.data?.proxy_type !== 'eip7702' ?
|
||||
{ slug: 'proxy', name: 'Proxy', tagType: 'custom' as const, ordinal: PREDEFINED_TAG_PRIORITY } :
|
||||
undefined,
|
||||
addressQuery.data?.implementations?.length && addressQuery.data?.proxy_type === 'eip7702' ?
|
||||
{ slug: 'eip7702', name: 'EOA+code', tagType: 'custom' as const, ordinal: PREDEFINED_TAG_PRIORITY } :
|
||||
undefined,
|
||||
addressQuery.data?.token ? { slug: 'token', name: 'Token', tagType: 'custom' as const, ordinal: PREDEFINED_TAG_PRIORITY } : undefined,
|
||||
isSafeAddress ? { slug: 'safe', name: 'Multisig: Safe', tagType: 'custom' as const, ordinal: -10 } : undefined,
|
||||
addressProfileAPIFeature.isEnabled && usernameApiTag ? {
|
||||
@@ -417,7 +424,7 @@ const AddressPageContent = () => {
|
||||
<>
|
||||
<TextAd mb={ 6 }/>
|
||||
<PageTitle
|
||||
title={ `${ addressQuery.data?.is_contract ? 'Contract' : 'Address' } details` }
|
||||
title={ `${ addressQuery.data?.is_contract && addressQuery.data?.proxy_type !== 'eip7702' ? 'Contract' : 'Address' } details` }
|
||||
backLink={ backLink }
|
||||
contentAfter={ titleContentAfter }
|
||||
secondRow={ titleSecondRow }
|
||||
|
||||
@@ -16,6 +16,7 @@ import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
|
||||
import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton';
|
||||
import useTabIndexFromQuery from 'ui/shared/Tabs/useTabIndexFromQuery';
|
||||
import TxAssetFlows from 'ui/tx/TxAssetFlows';
|
||||
import TxAuthorizations from 'ui/tx/TxAuthorizations';
|
||||
import TxBlobs from 'ui/tx/TxBlobs';
|
||||
import TxDetails from 'ui/tx/TxDetails';
|
||||
import TxDetailsDegraded from 'ui/tx/TxDetailsDegraded';
|
||||
@@ -69,6 +70,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?.authorization_list?.length ?
|
||||
{ id: 'authorizations', title: 'Authorizations', component: <TxAuthorizations txQuery={ txQuery }/> } :
|
||||
undefined,
|
||||
].filter(Boolean);
|
||||
})();
|
||||
|
||||
|
||||
@@ -154,6 +154,16 @@ test('with ENS', async({ render }) => {
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test('delegated address +@dark-mode', async({ render }) => {
|
||||
const component = await render(
|
||||
<AddressEntity
|
||||
address={ addressMock.delegated }
|
||||
/>,
|
||||
);
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test('with name tag', async({ render }) => {
|
||||
const component = await render(
|
||||
<AddressEntity
|
||||
|
||||
@@ -14,6 +14,7 @@ import * as EntityBase from 'ui/shared/entities/base/components';
|
||||
|
||||
import { distributeEntityProps, getIconProps } from '../base/utils';
|
||||
import AddressEntityContentProxy from './AddressEntityContentProxy';
|
||||
import AddressIconDelegated from './AddressIconDelegated';
|
||||
import AddressIdenticon from './AddressIdenticon';
|
||||
|
||||
type LinkProps = EntityBase.LinkBaseProps & Pick<EntityProps, 'address'>;
|
||||
@@ -51,7 +52,9 @@ const Icon = (props: IconProps) => {
|
||||
return <Skeleton { ...styles } borderRadius="full" flexShrink={ 0 }/>;
|
||||
}
|
||||
|
||||
if (props.address.is_contract) {
|
||||
const isDelegatedAddress = props.address.proxy_type === 'eip7702';
|
||||
|
||||
if (props.address.is_contract && !isDelegatedAddress) {
|
||||
if (props.isSafeAddress) {
|
||||
return (
|
||||
<EntityBase.Icon
|
||||
@@ -80,13 +83,22 @@ const Icon = (props: IconProps) => {
|
||||
);
|
||||
}
|
||||
|
||||
const label = (() => {
|
||||
if (isDelegatedAddress) {
|
||||
return props.address.is_verified ? 'EOA + verified code' : 'EOA + code';
|
||||
}
|
||||
})();
|
||||
|
||||
return (
|
||||
<Flex marginRight={ styles.marginRight }>
|
||||
<AddressIdenticon
|
||||
size={ props.size === 'lg' ? 30 : 20 }
|
||||
hash={ getDisplayedAddress(props.address) }
|
||||
/>
|
||||
</Flex>
|
||||
<Tooltip label={ label }>
|
||||
<Flex marginRight={ styles.marginRight } position="relative">
|
||||
<AddressIdenticon
|
||||
size={ props.size === 'lg' ? 30 : 20 }
|
||||
hash={ getDisplayedAddress(props.address) }
|
||||
/>
|
||||
{ isDelegatedAddress && <AddressIconDelegated isVerified={ Boolean(props.address.is_verified) }/> }
|
||||
</Flex>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -97,7 +109,7 @@ const Content = chakra((props: ContentProps) => {
|
||||
const nameTag = props.address.metadata?.tags.find(tag => tag.tagType === 'name')?.name;
|
||||
const nameText = nameTag || props.address.ens_domain_name || props.address.name;
|
||||
|
||||
const isProxy = props.address.implementations && props.address.implementations.length > 0;
|
||||
const isProxy = props.address.implementations && props.address.implementations.length > 0 && props.address.proxy_type !== 'eip7702';
|
||||
|
||||
if (isProxy) {
|
||||
return <AddressEntityContentProxy { ...props }/>;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Box, useColorModeValue } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
interface Props {
|
||||
isVerified: boolean;
|
||||
}
|
||||
|
||||
const AddressIconDelegated = ({ isVerified }: Props) => {
|
||||
const bgColor = useColorModeValue('var(--chakra-colors-white)', 'var(--chakra-colors-black)');
|
||||
const defaultColor = useColorModeValue('gray.500', 'gray.400');
|
||||
|
||||
return (
|
||||
<Box position="absolute" boxSize="14px" top="-2px" right="-2px" color={ isVerified ? 'green.500' : defaultColor }>
|
||||
<svg
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect width="14" height="6" rx="2" x="0" y="4" fill={ bgColor }/>
|
||||
<rect width="6" height="14" rx="2" x="4" y="0" fill={ bgColor }/>
|
||||
<rect width="10" height="2" rx="0.5" x="2" y="6" fill="currentColor"/>
|
||||
<rect width="2" height="10" rx="0.5" x="6" y="2" fill="currentColor"/>
|
||||
</svg>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(AddressIconDelegated);
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
@@ -0,0 +1,43 @@
|
||||
import { Show, Hide } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import DataListDisplay from 'ui/shared/DataListDisplay';
|
||||
import TxPendingAlert from 'ui/tx/TxPendingAlert';
|
||||
import TxSocketAlert from 'ui/tx/TxSocketAlert';
|
||||
|
||||
import TxAuthorizationsList from './authorizations/TxAuthorizationsList';
|
||||
import TxAuthorizationsTable from './authorizations/TxAuthorizationsTable';
|
||||
import type { TxQuery } from './useTxQuery';
|
||||
|
||||
interface Props {
|
||||
txQuery: TxQuery;
|
||||
}
|
||||
|
||||
const TxAuthorizations = ({ txQuery }: Props) => {
|
||||
|
||||
if (!txQuery.isPlaceholderData && !txQuery.isError && !txQuery.data?.status) {
|
||||
return txQuery.socketStatus ? <TxSocketAlert status={ txQuery.socketStatus }/> : <TxPendingAlert/>;
|
||||
}
|
||||
|
||||
const content = (
|
||||
<>
|
||||
<Show below="lg" ssr={ false }>
|
||||
<TxAuthorizationsList data={ txQuery.data?.authorization_list } isLoading={ txQuery.isPlaceholderData }/>
|
||||
</Show>
|
||||
<Hide below="lg" ssr={ false }>
|
||||
<TxAuthorizationsTable data={ txQuery.data?.authorization_list } isLoading={ txQuery.isPlaceholderData }/>
|
||||
</Hide>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<DataListDisplay
|
||||
isError={ txQuery.isError }
|
||||
items={ txQuery.data?.authorization_list }
|
||||
emptyText="There are no authorizations for this transaction."
|
||||
content={ content }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TxAuthorizations;
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { TxAuthorization } from 'types/api/transaction';
|
||||
|
||||
import TxAuthorizationsListItem from './TxAuthorizationsListItem';
|
||||
|
||||
interface Props {
|
||||
data: Array<TxAuthorization> | undefined;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const TxAuthorizationsList = ({ data, isLoading }: Props) => {
|
||||
return (
|
||||
<Box>
|
||||
{ data?.map((item, index) => <TxAuthorizationsListItem key={ item.nonce.toString() + (isLoading ? index : '') } { ...item } isLoading={ isLoading }/>) }
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default TxAuthorizationsList;
|
||||
@@ -0,0 +1,38 @@
|
||||
import { HStack } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { TxAuthorization } from 'types/api/transaction';
|
||||
|
||||
import config from 'configs/app';
|
||||
import Skeleton from 'ui/shared/chakra/Skeleton';
|
||||
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
|
||||
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
|
||||
|
||||
interface Props extends TxAuthorization {
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const TxAuthorizationsListItem = ({ address, authority, chain_id: chainId, nonce, isLoading }: Props) => {
|
||||
return (
|
||||
<ListItemMobile rowGap={ 3 } fontSize="sm">
|
||||
<HStack spacing={ 3 } w="100%">
|
||||
<Skeleton isLoaded={ !isLoading } fontWeight={ 500 }>Address</Skeleton>
|
||||
<AddressEntity address={{ hash: address }} isLoading={ isLoading } noIcon/>
|
||||
</HStack>
|
||||
<HStack spacing={ 3 } w="100%">
|
||||
<Skeleton isLoaded={ !isLoading } fontWeight={ 500 }>Authority</Skeleton>
|
||||
<AddressEntity address={{ hash: authority }} isLoading={ isLoading } noIcon/>
|
||||
</HStack>
|
||||
<HStack spacing={ 3 }>
|
||||
<Skeleton isLoaded={ !isLoading } fontWeight={ 500 }>Chain</Skeleton>
|
||||
<Skeleton isLoaded={ !isLoading } color="text_secondary">{ chainId === Number(config.chain.id) ? 'this' : 'any' }</Skeleton>
|
||||
</HStack>
|
||||
<HStack spacing={ 3 }>
|
||||
<Skeleton isLoaded={ !isLoading } fontWeight={ 500 }>Nonce</Skeleton>
|
||||
<Skeleton isLoaded={ !isLoading } color="text_secondary">{ nonce }</Skeleton>
|
||||
</HStack>
|
||||
</ListItemMobile>
|
||||
);
|
||||
};
|
||||
|
||||
export default TxAuthorizationsListItem;
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Table, Tbody, Tr, Th } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { TxAuthorization } from 'types/api/transaction';
|
||||
|
||||
import { AddressHighlightProvider } from 'lib/contexts/addressHighlight';
|
||||
import { default as Thead } from 'ui/shared/TheadSticky';
|
||||
|
||||
import TxAuthorizationsTableItem from './TxAuthorizationsTableItem';
|
||||
|
||||
interface Props {
|
||||
data: Array<TxAuthorization> | undefined;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const TxAuthorizationsTable = ({ data, isLoading }: Props) => {
|
||||
return (
|
||||
<AddressHighlightProvider>
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th width="50%">Address</Th>
|
||||
<Th width="50%">Authority</Th>
|
||||
<Th width="120px">Chain</Th>
|
||||
<Th width="120px" isNumeric>Nonce</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{ data?.map((item, index) => (
|
||||
<TxAuthorizationsTableItem key={ item.nonce.toString() + (isLoading ? index : '') } { ...item } isLoading={ isLoading }/>
|
||||
)) }
|
||||
</Tbody>
|
||||
</Table>
|
||||
</AddressHighlightProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default TxAuthorizationsTable;
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Tr, Td } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { TxAuthorization } from 'types/api/transaction';
|
||||
|
||||
import config from 'configs/app';
|
||||
import Skeleton from 'ui/shared/chakra/Skeleton';
|
||||
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
|
||||
|
||||
interface Props extends TxAuthorization {
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const TxAuthorizationsItem = ({ address, authority, chain_id: chainId, nonce, isLoading }: Props) => {
|
||||
return (
|
||||
<Tr alignItems="top">
|
||||
<Td>
|
||||
<AddressEntity address={{ hash: address }} isLoading={ isLoading } noIcon/>
|
||||
</Td>
|
||||
<Td verticalAlign="middle">
|
||||
<AddressEntity address={{ hash: authority }} isLoading={ isLoading } noIcon/>
|
||||
</Td>
|
||||
<Td verticalAlign="middle">
|
||||
<Skeleton isLoaded={ !isLoading } display="inline-block">
|
||||
{ chainId === Number(config.chain.id) ? 'this' : 'any' }
|
||||
</Skeleton>
|
||||
</Td>
|
||||
<Td isNumeric verticalAlign="middle">
|
||||
<Skeleton isLoaded={ !isLoading } display="inline-block">
|
||||
{ nonce }
|
||||
</Skeleton>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(TxAuthorizationsItem);
|
||||
@@ -25,6 +25,7 @@ const TxDetailsOther = ({ nonce, type, position, queueIndex }: Props) => {
|
||||
<Text fontWeight="600" as="span">{ type }</Text>
|
||||
{ type === 2 && <Text fontWeight="400" as="span" ml={ 1 } variant="secondary">(EIP-1559)</Text> }
|
||||
{ type === 3 && <Text fontWeight="400" as="span" ml={ 1 } variant="secondary">(EIP-4844)</Text> }
|
||||
{ type === 4 && <Text fontWeight="400" as="span" ml={ 1 } variant="secondary">(EIP-7702)</Text> }
|
||||
</Box>
|
||||
),
|
||||
queueIndex !== undefined ? (
|
||||
|
||||
Reference in New Issue
Block a user