Commit 751da19e authored by tom goriunov's avatar tom goriunov Committed by GitHub

Support EIP-7702 addresses (#2523)

* tx info customizations

* authorizations tab

* implement change for address page

* amends
parent a0a3b384
...@@ -21,6 +21,7 @@ on: ...@@ -21,6 +21,7 @@ on:
- eth_sepolia - eth_sepolia
- eth_goerli - eth_goerli
- filecoin - filecoin
- mekong
- optimism - optimism
- optimism_celestia - optimism_celestia
- optimism_sepolia - optimism_sepolia
......
...@@ -369,6 +369,7 @@ ...@@ -369,6 +369,7 @@
"eth_goerli", "eth_goerli",
"eth_sepolia", "eth_sepolia",
"filecoin", "filecoin",
"mekong",
"optimism", "optimism",
"optimism_celestia", "optimism_celestia",
"optimism_sepolia", "optimism_sepolia",
......
# 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
\ No newline at end of file
...@@ -60,6 +60,12 @@ export const withoutName: AddressParam = { ...@@ -60,6 +60,12 @@ export const withoutName: AddressParam = {
ens_domain_name: null, ens_domain_name: null,
}; };
export const delegated: AddressParam = {
...withoutName,
is_verified: true,
proxy_type: 'eip7702',
};
export const token: Address = { export const token: Address = {
hash: hash, hash: hash,
implementations: null, implementations: null,
......
...@@ -14,6 +14,7 @@ const PRESETS = { ...@@ -14,6 +14,7 @@ const PRESETS = {
garnet: 'https://explorer.garnetchain.com', garnet: 'https://explorer.garnetchain.com',
filecoin: 'https://filecoin.blockscout.com', filecoin: 'https://filecoin.blockscout.com',
gnosis: 'https://gnosis.blockscout.com', gnosis: 'https://gnosis.blockscout.com',
mekong: 'https://mekong.blockscout.com',
optimism: 'https://optimism.blockscout.com', optimism: 'https://optimism.blockscout.com',
optimism_celestia: 'https://opcelestia-raspberry.gelatoscout.com', optimism_celestia: 'https://opcelestia-raspberry.gelatoscout.com',
optimism_sepolia: 'https://optimism-sepolia.blockscout.com', optimism_sepolia: 'https://optimism-sepolia.blockscout.com',
......
...@@ -2,6 +2,7 @@ import type { Transaction } from 'types/api/transaction'; ...@@ -2,6 +2,7 @@ import type { Transaction } from 'types/api/transaction';
import type { UserTags, AddressImplementation, AddressParam, AddressFilecoinParams } from './addressParams'; import type { UserTags, AddressImplementation, AddressParam, AddressFilecoinParams } from './addressParams';
import type { Block, EpochRewardsType } from './block'; import type { Block, EpochRewardsType } from './block';
import type { SmartContractProxyType } from './contract';
import type { InternalTransaction } from './internalTransaction'; import type { InternalTransaction } from './internalTransaction';
import type { MudWorldSchema, MudWorldTable } from './mudWorlds'; import type { MudWorldSchema, MudWorldTable } from './mudWorlds';
import type { NFTTokenType, TokenInfo, TokenInstance, TokenType } from './token'; import type { NFTTokenType, TokenInfo, TokenInstance, TokenType } from './token';
...@@ -31,6 +32,7 @@ export interface Address extends UserTags { ...@@ -31,6 +32,7 @@ export interface Address extends UserTags {
name: string | null; name: string | null;
token: TokenInfo | null; token: TokenInfo | null;
watchlist_address_id: number | null; watchlist_address_id: number | null;
proxy_type?: SmartContractProxyType | null;
} }
export interface AddressZilliqaParams { export interface AddressZilliqaParams {
......
import type { AddressMetadataTagApi } from './addressMetadata'; import type { AddressMetadataTagApi } from './addressMetadata';
import type { SmartContractProxyType } from './contract';
export interface AddressImplementation { export interface AddressImplementation {
address: string; address: string;
...@@ -59,6 +60,7 @@ export type AddressParamBasic = { ...@@ -59,6 +60,7 @@ export type AddressParamBasic = {
tags: Array<AddressMetadataTagApi>; tags: Array<AddressMetadataTagApi>;
} | null; } | null;
filecoin?: AddressFilecoinParams; filecoin?: AddressFilecoinParams;
proxy_type?: SmartContractProxyType | null;
}; };
export type AddressParam = UserTags & AddressParamBasic; export type AddressParam = UserTags & AddressParamBasic;
...@@ -25,6 +25,7 @@ export type SmartContractProxyType = ...@@ -25,6 +25,7 @@ export type SmartContractProxyType =
| 'eip1822' | 'eip1822'
| 'eip930' | 'eip930'
| 'eip2535' | 'eip2535'
| 'eip7702'
| 'master_copy' | 'master_copy'
| 'basic_implementation' | 'basic_implementation'
| 'basic_get_implementation' | 'basic_get_implementation'
......
...@@ -105,6 +105,8 @@ export type Transaction = { ...@@ -105,6 +105,8 @@ export type Transaction = {
translation?: NovesTxTranslation; translation?: NovesTxTranslation;
arbitrum?: ArbitrumTransactionData; arbitrum?: ArbitrumTransactionData;
scroll?: ScrollTransactionData; scroll?: ScrollTransactionData;
// EIP-7702
authorization_list?: Array<TxAuthorization>;
}; };
type ArbitrumTransactionData = { type ArbitrumTransactionData = {
...@@ -206,3 +208,10 @@ export type ScrollTransactionData = { ...@@ -206,3 +208,10 @@ export type ScrollTransactionData = {
l2_block_status: ScrollL2BlockStatus; l2_block_status: ScrollL2BlockStatus;
queue_index: number; queue_index: number;
}; };
export interface TxAuthorization {
address: string;
authority: string;
chain_id: number;
nonce: number;
}
...@@ -170,6 +170,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => { ...@@ -170,6 +170,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
<AddressImplementations <AddressImplementations
data={ data.implementations } data={ data.implementations }
isLoading={ addressQuery.isPlaceholderData } isLoading={ addressQuery.isPlaceholderData }
proxyType={ data.proxy_type }
/> />
) } ) }
......
...@@ -40,7 +40,8 @@ const ContractDetails = ({ addressHash, channel, mainContractQuery }: Props) => ...@@ -40,7 +40,8 @@ const ContractDetails = ({ addressHash, channel, mainContractQuery }: Props) =>
const addressInfo = queryClient.getQueryData<AddressInfo>(getResourceKey('address', { pathParams: { hash: addressHash } })); const addressInfo = queryClient.getQueryData<AddressInfo>(getResourceKey('address', { pathParams: { hash: addressHash } }));
const sourceItems: Array<AddressImplementation> = React.useMemo(() => { 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) { if (!addressInfo || !addressInfo.implementations || addressInfo.implementations.length === 0) {
return [ currentAddressItem ]; return [ currentAddressItem ];
} }
......
...@@ -9,11 +9,11 @@ interface Props { ...@@ -9,11 +9,11 @@ interface Props {
type: NonNullable<SmartContractProxyType>; type: NonNullable<SmartContractProxyType>;
} }
const PROXY_TYPES: Record<NonNullable<SmartContractProxyType>, { const PROXY_TYPES: Partial<Record<NonNullable<SmartContractProxyType>, {
name: string; name: string;
link?: string; link?: string;
description?: string; description?: string;
}> = { }>> = {
eip1167: { eip1167: {
name: 'EIP-1167', name: 'EIP-1167',
link: 'https://eips.ethereum.org/EIPS/eip-1167', link: 'https://eips.ethereum.org/EIPS/eip-1167',
......
...@@ -3,6 +3,7 @@ import { useRouter } from 'next/router'; ...@@ -3,6 +3,7 @@ import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { AddressImplementation } from 'types/api/addressParams'; import type { AddressImplementation } from 'types/api/addressParams';
import type { SmartContractProxyType } from 'types/api/contract';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
...@@ -18,9 +19,10 @@ import { formatAbi } from './utils'; ...@@ -18,9 +19,10 @@ import { formatAbi } from './utils';
interface Props { interface Props {
implementations: Array<AddressImplementation>; implementations: Array<AddressImplementation>;
isLoading?: boolean; isLoading?: boolean;
proxyType?: SmartContractProxyType;
} }
const ContractMethodsProxy = ({ implementations, isLoading: isInitialLoading }: Props) => { const ContractMethodsProxy = ({ implementations, isLoading: isInitialLoading, proxyType }: Props) => {
const router = useRouter(); const router = useRouter();
const sourceAddress = getQueryParamString(router.query.source_address); const sourceAddress = getQueryParamString(router.query.source_address);
const tab = getQueryParamString(router.query.tab); const tab = getQueryParamString(router.query.tab);
...@@ -48,7 +50,7 @@ const ContractMethodsProxy = ({ implementations, isLoading: isInitialLoading }: ...@@ -48,7 +50,7 @@ const ContractMethodsProxy = ({ implementations, isLoading: isInitialLoading }:
selectedItem={ selectedItem } selectedItem={ selectedItem }
onItemSelect={ setSelectedItem } onItemSelect={ setSelectedItem }
isLoading={ isInitialLoading } isLoading={ isInitialLoading }
label="Implementation address" label={ proxyType === 'eip7702' ? 'Delegate address' : 'Implementation address' }
mb={ 3 } mb={ 3 }
/> />
<ContractMethodsFilters <ContractMethodsFilters
......
...@@ -26,7 +26,7 @@ interface Props { ...@@ -26,7 +26,7 @@ interface Props {
export default function useContractDetailsTabs({ data, isLoading, addressHash, sourceAddress }: Props): Array<Tab> { 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(() => { return React.useMemo(() => {
const verificationButton = ( const verificationButton = (
......
...@@ -88,7 +88,13 @@ export default function useContractTabs(data: Address | undefined, isPlaceholder ...@@ -88,7 +88,13 @@ export default function useContractTabs(data: Address | undefined, isPlaceholder
verifiedImplementations.length > 0 && { verifiedImplementations.length > 0 && {
id: [ 'read_write_proxy' as const, 'read_proxy' as const, 'write_proxy' as const ], id: [ 'read_write_proxy' as const, 'read_proxy' as const, 'write_proxy' as const ],
title: 'Read/Write proxy', 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 && { config.features.account.isEnabled && {
id: [ 'read_write_custom_methods' as const, 'read_custom_methods' as const, 'write_custom_methods' as const ], id: [ 'read_write_custom_methods' as const, 'read_custom_methods' as const, 'write_custom_methods' as const ],
......
import React from 'react'; import React from 'react';
import type { AddressImplementation } from 'types/api/addressParams'; import type { AddressImplementation } from 'types/api/addressParams';
import type { SmartContractProxyType } from 'types/api/contract';
import * as DetailsInfoItem from 'ui/shared/DetailsInfoItem'; import * as DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import AddressEntity from 'ui/shared/entities/address/AddressEntity';
...@@ -8,20 +9,26 @@ import AddressEntity from 'ui/shared/entities/address/AddressEntity'; ...@@ -8,20 +9,26 @@ import AddressEntity from 'ui/shared/entities/address/AddressEntity';
interface Props { interface Props {
data: Array<AddressImplementation>; data: Array<AddressImplementation>;
isLoading?: boolean; isLoading?: boolean;
proxyType?: SmartContractProxyType;
} }
const AddressImplementations = ({ data, isLoading }: Props) => { const AddressImplementations = ({ data, isLoading, proxyType }: Props) => {
const hasManyItems = data.length > 1; const hasManyItems = data.length > 1;
const [ hasScroll, setHasScroll ] = React.useState(false); 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 ( return (
<> <>
<DetailsInfoItem.Label <DetailsInfoItem.Label
hint={ `Implementation${ hasManyItems ? 's' : '' } address${ hasManyItems ? 'es' : '' } of the proxy contract` } hint={ hint }
isLoading={ isLoading } isLoading={ isLoading }
hasScroll={ hasScroll } hasScroll={ hasScroll }
> >
{ `Implementation${ hasManyItems ? 's' : '' }` } { text }
</DetailsInfoItem.Label> </DetailsInfoItem.Label>
<DetailsInfoItem.ValueWithScroll <DetailsInfoItem.ValueWithScroll
gradientHeight={ 48 } gradientHeight={ 48 }
......
...@@ -239,16 +239,18 @@ const AddressPageContent = () => { ...@@ -239,16 +239,18 @@ const AddressPageContent = () => {
addressQuery.data?.is_contract ? { addressQuery.data?.is_contract ? {
id: 'contract', id: 'contract',
title: () => { title: () => {
const tabName = addressQuery.data.proxy_type === 'eip7702' ? 'Code' : 'Contract';
if (addressQuery.data.is_verified) { if (addressQuery.data.is_verified) {
return ( return (
<> <>
<span>Contract</span> <span>{ tabName }</span>
<IconSvg name="status/success" boxSize="14px" color="green.500" ml={ 1 }/> <IconSvg name="status/success" boxSize="14px" color="green.500" ml={ 1 }/>
</> </>
); );
} }
return 'Contract'; return tabName;
}, },
component: ( component: (
<AddressContract <AddressContract
...@@ -279,7 +281,12 @@ const AddressPageContent = () => { ...@@ -279,7 +281,12 @@ const AddressPageContent = () => {
config.features.validators.isEnabled && addressQuery.data?.has_validated_blocks ? config.features.validators.isEnabled && addressQuery.data?.has_validated_blocks ?
{ slug: 'validator', name: 'Validator', tagType: 'custom' as const, ordinal: PREDEFINED_TAG_PRIORITY } : { slug: 'validator', name: 'Validator', tagType: 'custom' as const, ordinal: PREDEFINED_TAG_PRIORITY } :
undefined, 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, 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, isSafeAddress ? { slug: 'safe', name: 'Multisig: Safe', tagType: 'custom' as const, ordinal: -10 } : undefined,
addressProfileAPIFeature.isEnabled && usernameApiTag ? { addressProfileAPIFeature.isEnabled && usernameApiTag ? {
...@@ -417,7 +424,7 @@ const AddressPageContent = () => { ...@@ -417,7 +424,7 @@ const AddressPageContent = () => {
<> <>
<TextAd mb={ 6 }/> <TextAd mb={ 6 }/>
<PageTitle <PageTitle
title={ `${ addressQuery.data?.is_contract ? 'Contract' : 'Address' } details` } title={ `${ addressQuery.data?.is_contract && addressQuery.data?.proxy_type !== 'eip7702' ? 'Contract' : 'Address' } details` }
backLink={ backLink } backLink={ backLink }
contentAfter={ titleContentAfter } contentAfter={ titleContentAfter }
secondRow={ titleSecondRow } secondRow={ titleSecondRow }
......
...@@ -16,6 +16,7 @@ import RoutedTabs from 'ui/shared/Tabs/RoutedTabs'; ...@@ -16,6 +16,7 @@ import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton'; import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton';
import useTabIndexFromQuery from 'ui/shared/Tabs/useTabIndexFromQuery'; import useTabIndexFromQuery from 'ui/shared/Tabs/useTabIndexFromQuery';
import TxAssetFlows from 'ui/tx/TxAssetFlows'; import TxAssetFlows from 'ui/tx/TxAssetFlows';
import TxAuthorizations from 'ui/tx/TxAuthorizations';
import TxBlobs from 'ui/tx/TxBlobs'; import TxBlobs from 'ui/tx/TxBlobs';
import TxDetails from 'ui/tx/TxDetails'; import TxDetails from 'ui/tx/TxDetails';
import TxDetailsDegraded from 'ui/tx/TxDetailsDegraded'; import TxDetailsDegraded from 'ui/tx/TxDetailsDegraded';
...@@ -69,6 +70,9 @@ const TransactionPageContent = () => { ...@@ -69,6 +70,9 @@ const TransactionPageContent = () => {
{ id: 'logs', title: 'Logs', component: <TxLogs txQuery={ txQuery }/> }, { id: 'logs', title: 'Logs', component: <TxLogs txQuery={ txQuery }/> },
{ id: 'state', title: 'State', component: <TxState txQuery={ txQuery }/> }, { id: 'state', title: 'State', component: <TxState txQuery={ txQuery }/> },
{ id: 'raw_trace', title: 'Raw trace', component: <TxRawTrace 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); ].filter(Boolean);
})(); })();
......
...@@ -154,6 +154,16 @@ test('with ENS', async({ render }) => { ...@@ -154,6 +154,16 @@ test('with ENS', async({ render }) => {
await expect(component).toHaveScreenshot(); 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 }) => { test('with name tag', async({ render }) => {
const component = await render( const component = await render(
<AddressEntity <AddressEntity
......
...@@ -14,6 +14,7 @@ import * as EntityBase from 'ui/shared/entities/base/components'; ...@@ -14,6 +14,7 @@ import * as EntityBase from 'ui/shared/entities/base/components';
import { distributeEntityProps, getIconProps } from '../base/utils'; import { distributeEntityProps, getIconProps } from '../base/utils';
import AddressEntityContentProxy from './AddressEntityContentProxy'; import AddressEntityContentProxy from './AddressEntityContentProxy';
import AddressIconDelegated from './AddressIconDelegated';
import AddressIdenticon from './AddressIdenticon'; import AddressIdenticon from './AddressIdenticon';
type LinkProps = EntityBase.LinkBaseProps & Pick<EntityProps, 'address'>; type LinkProps = EntityBase.LinkBaseProps & Pick<EntityProps, 'address'>;
...@@ -51,7 +52,9 @@ const Icon = (props: IconProps) => { ...@@ -51,7 +52,9 @@ const Icon = (props: IconProps) => {
return <Skeleton { ...styles } borderRadius="full" flexShrink={ 0 }/>; 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) { if (props.isSafeAddress) {
return ( return (
<EntityBase.Icon <EntityBase.Icon
...@@ -80,13 +83,22 @@ const Icon = (props: IconProps) => { ...@@ -80,13 +83,22 @@ const Icon = (props: IconProps) => {
); );
} }
const label = (() => {
if (isDelegatedAddress) {
return props.address.is_verified ? 'EOA + verified code' : 'EOA + code';
}
})();
return ( return (
<Flex marginRight={ styles.marginRight }> <Tooltip label={ label }>
<AddressIdenticon <Flex marginRight={ styles.marginRight } position="relative">
size={ props.size === 'lg' ? 30 : 20 } <AddressIdenticon
hash={ getDisplayedAddress(props.address) } size={ props.size === 'lg' ? 30 : 20 }
/> hash={ getDisplayedAddress(props.address) }
</Flex> />
{ isDelegatedAddress && <AddressIconDelegated isVerified={ Boolean(props.address.is_verified) }/> }
</Flex>
</Tooltip>
); );
}; };
...@@ -97,7 +109,7 @@ const Content = chakra((props: ContentProps) => { ...@@ -97,7 +109,7 @@ const Content = chakra((props: ContentProps) => {
const nameTag = props.address.metadata?.tags.find(tag => tag.tagType === 'name')?.name; const nameTag = props.address.metadata?.tags.find(tag => tag.tagType === 'name')?.name;
const nameText = nameTag || props.address.ens_domain_name || props.address.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) { if (isProxy) {
return <AddressEntityContentProxy { ...props }/>; return <AddressEntityContentProxy { ...props }/>;
......
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);
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;
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;
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;
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;
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) => { ...@@ -25,6 +25,7 @@ const TxDetailsOther = ({ nonce, type, position, queueIndex }: Props) => {
<Text fontWeight="600" as="span">{ type }</Text> <Text fontWeight="600" as="span">{ type }</Text>
{ type === 2 && <Text fontWeight="400" as="span" ml={ 1 } variant="secondary">(EIP-1559)</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 === 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> </Box>
), ),
queueIndex !== undefined ? ( queueIndex !== undefined ? (
......
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