Skip to content

Commit

Permalink
Support EIP-7702 addresses (#2523)
Browse files Browse the repository at this point in the history
* tx info customizations

* authorizations tab

* implement change for address page

* amends
  • Loading branch information
tom2drum authored Jan 31, 2025
1 parent a0a3b38 commit 751da19
Show file tree
Hide file tree
Showing 29 changed files with 332 additions and 22 deletions.
1 change: 1 addition & 0 deletions .github/workflows/deploy-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ on:
- eth_sepolia
- eth_goerli
- filecoin
- mekong
- optimism
- optimism_celestia
- optimism_sepolia
Expand Down
1 change: 1 addition & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,7 @@
"eth_goerli",
"eth_sepolia",
"filecoin",
"mekong",
"optimism",
"optimism_celestia",
"optimism_sepolia",
Expand Down
31 changes: 31 additions & 0 deletions configs/envs/.env.mekong
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions mocks/address/address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions tools/preset-sync/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 2 additions & 0 deletions types/api/address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions types/api/addressParams.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { AddressMetadataTagApi } from './addressMetadata';
import type { SmartContractProxyType } from './contract';

export interface AddressImplementation {
address: string;
Expand Down Expand Up @@ -59,6 +60,7 @@ export type AddressParamBasic = {
tags: Array<AddressMetadataTagApi>;
} | null;
filecoin?: AddressFilecoinParams;
proxy_type?: SmartContractProxyType | null;
};

export type AddressParam = UserTags & AddressParamBasic;
1 change: 1 addition & 0 deletions types/api/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export type SmartContractProxyType =
| 'eip1822'
| 'eip930'
| 'eip2535'
| 'eip7702'
| 'master_copy'
| 'basic_implementation'
| 'basic_get_implementation'
Expand Down
9 changes: 9 additions & 0 deletions types/api/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ export type Transaction = {
translation?: NovesTxTranslation;
arbitrum?: ArbitrumTransactionData;
scroll?: ScrollTransactionData;
// EIP-7702
authorization_list?: Array<TxAuthorization>;
};

type ArbitrumTransactionData = {
Expand Down Expand Up @@ -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;
}
1 change: 1 addition & 0 deletions ui/address/AddressDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
<AddressImplementations
data={ data.implementations }
isLoading={ addressQuery.isPlaceholderData }
proxyType={ data.proxy_type }
/>
) }

Expand Down
3 changes: 2 additions & 1 deletion ui/address/contract/ContractDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 ];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
6 changes: 4 additions & 2 deletions ui/address/contract/methods/ContractMethodsProxy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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);
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion ui/address/contract/useContractDetailsTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand Down
8 changes: 7 additions & 1 deletion ui/address/contract/useContractTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 ],
Expand Down
13 changes: 10 additions & 3 deletions ui/address/details/AddressImplementations.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,34 @@
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';

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 }
Expand Down
15 changes: 11 additions & 4 deletions ui/pages/Address.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 ? {
Expand Down Expand Up @@ -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 }
Expand Down
4 changes: 4 additions & 0 deletions ui/pages/Transaction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
})();

Expand Down
10 changes: 10 additions & 0 deletions ui/shared/entities/address/AddressEntity.pw.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 20 additions & 8 deletions ui/shared/entities/address/AddressEntity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'>;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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>
);
};

Expand All @@ -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 }/>;
Expand Down
Loading

0 comments on commit 751da19

Please sign in to comment.