Skip to content

Commit 751da19

Browse files
authored
Support EIP-7702 addresses (blockscout#2523)
* tx info customizations * authorizations tab * implement change for address page * amends
1 parent a0a3b38 commit 751da19

29 files changed

+332
-22
lines changed

.github/workflows/deploy-review.yml

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ on:
2121
- eth_sepolia
2222
- eth_goerli
2323
- filecoin
24+
- mekong
2425
- optimism
2526
- optimism_celestia
2627
- optimism_sepolia

.vscode/tasks.json

+1
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,7 @@
369369
"eth_goerli",
370370
"eth_sepolia",
371371
"filecoin",
372+
"mekong",
372373
"optimism",
373374
"optimism_celestia",
374375
"optimism_sepolia",

configs/envs/.env.mekong

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Set of ENVs for Mekong network explorer
2+
# https://mekong.blockscout.com
3+
# This is an auto-generated file. To update all values, run "yarn dev:preset:sync --name=mekong"
4+
5+
# Local ENVs
6+
NEXT_PUBLIC_APP_PROTOCOL=http
7+
NEXT_PUBLIC_APP_HOST=localhost
8+
NEXT_PUBLIC_APP_PORT=3000
9+
NEXT_PUBLIC_APP_ENV=development
10+
NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
11+
12+
# Instance ENVs
13+
NEXT_PUBLIC_API_BASE_PATH=/
14+
NEXT_PUBLIC_API_HOST=mekong.blockscout.com
15+
NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
16+
NEXT_PUBLIC_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/sherblockHolmesBadge
17+
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x7c7d9e09a5e0e6441a81efe57dbcf08848cd18a1f4238e28152faead390066a4
18+
NEXT_PUBLIC_HAS_BEACON_CHAIN=true
19+
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
20+
NEXT_PUBLIC_IS_TESTNET=true
21+
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
22+
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
23+
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=ETH
24+
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
25+
NEXT_PUBLIC_NETWORK_ID=7078815900
26+
NEXT_PUBLIC_NETWORK_NAME=Mekong
27+
NEXT_PUBLIC_NETWORK_RPC_URL=https://rpc.mekong.ethpandaops.io
28+
NEXT_PUBLIC_NETWORK_SHORT_NAME=Mekong
29+
NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true
30+
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
31+
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com

mocks/address/address.ts

+6
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ export const withoutName: AddressParam = {
6060
ens_domain_name: null,
6161
};
6262

63+
export const delegated: AddressParam = {
64+
...withoutName,
65+
is_verified: true,
66+
proxy_type: 'eip7702',
67+
};
68+
6369
export const token: Address = {
6470
hash: hash,
6571
implementations: null,

tools/preset-sync/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const PRESETS = {
1414
garnet: 'https://explorer.garnetchain.com',
1515
filecoin: 'https://filecoin.blockscout.com',
1616
gnosis: 'https://gnosis.blockscout.com',
17+
mekong: 'https://mekong.blockscout.com',
1718
optimism: 'https://optimism.blockscout.com',
1819
optimism_celestia: 'https://opcelestia-raspberry.gelatoscout.com',
1920
optimism_sepolia: 'https://optimism-sepolia.blockscout.com',

types/api/address.ts

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { Transaction } from 'types/api/transaction';
22

33
import type { UserTags, AddressImplementation, AddressParam, AddressFilecoinParams } from './addressParams';
44
import type { Block, EpochRewardsType } from './block';
5+
import type { SmartContractProxyType } from './contract';
56
import type { InternalTransaction } from './internalTransaction';
67
import type { MudWorldSchema, MudWorldTable } from './mudWorlds';
78
import type { NFTTokenType, TokenInfo, TokenInstance, TokenType } from './token';
@@ -31,6 +32,7 @@ export interface Address extends UserTags {
3132
name: string | null;
3233
token: TokenInfo | null;
3334
watchlist_address_id: number | null;
35+
proxy_type?: SmartContractProxyType | null;
3436
}
3537

3638
export interface AddressZilliqaParams {

types/api/addressParams.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { AddressMetadataTagApi } from './addressMetadata';
2+
import type { SmartContractProxyType } from './contract';
23

34
export interface AddressImplementation {
45
address: string;
@@ -59,6 +60,7 @@ export type AddressParamBasic = {
5960
tags: Array<AddressMetadataTagApi>;
6061
} | null;
6162
filecoin?: AddressFilecoinParams;
63+
proxy_type?: SmartContractProxyType | null;
6264
};
6365

6466
export type AddressParam = UserTags & AddressParamBasic;

types/api/contract.ts

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export type SmartContractProxyType =
2525
| 'eip1822'
2626
| 'eip930'
2727
| 'eip2535'
28+
| 'eip7702'
2829
| 'master_copy'
2930
| 'basic_implementation'
3031
| 'basic_get_implementation'

types/api/transaction.ts

+9
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ export type Transaction = {
105105
translation?: NovesTxTranslation;
106106
arbitrum?: ArbitrumTransactionData;
107107
scroll?: ScrollTransactionData;
108+
// EIP-7702
109+
authorization_list?: Array<TxAuthorization>;
108110
};
109111

110112
type ArbitrumTransactionData = {
@@ -206,3 +208,10 @@ export type ScrollTransactionData = {
206208
l2_block_status: ScrollL2BlockStatus;
207209
queue_index: number;
208210
};
211+
212+
export interface TxAuthorization {
213+
address: string;
214+
authority: string;
215+
chain_id: number;
216+
nonce: number;
217+
}

ui/address/AddressDetails.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
170170
<AddressImplementations
171171
data={ data.implementations }
172172
isLoading={ addressQuery.isPlaceholderData }
173+
proxyType={ data.proxy_type }
173174
/>
174175
) }
175176

ui/address/contract/ContractDetails.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ const ContractDetails = ({ addressHash, channel, mainContractQuery }: Props) =>
4040
const addressInfo = queryClient.getQueryData<AddressInfo>(getResourceKey('address', { pathParams: { hash: addressHash } }));
4141

4242
const sourceItems: Array<AddressImplementation> = React.useMemo(() => {
43-
const currentAddressItem = { address: addressHash, name: addressInfo?.name || 'Current contract' };
43+
const currentAddressDefaultName = addressInfo?.proxy_type === 'eip7702' ? 'Delegate address' : 'Current contract';
44+
const currentAddressItem = { address: addressHash, name: addressInfo?.name || currentAddressDefaultName };
4445
if (!addressInfo || !addressInfo.implementations || addressInfo.implementations.length === 0) {
4546
return [ currentAddressItem ];
4647
}

ui/address/contract/alerts/ContractDetailsAlertProxyPattern.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ interface Props {
99
type: NonNullable<SmartContractProxyType>;
1010
}
1111

12-
const PROXY_TYPES: Record<NonNullable<SmartContractProxyType>, {
12+
const PROXY_TYPES: Partial<Record<NonNullable<SmartContractProxyType>, {
1313
name: string;
1414
link?: string;
1515
description?: string;
16-
}> = {
16+
}>> = {
1717
eip1167: {
1818
name: 'EIP-1167',
1919
link: 'https://eips.ethereum.org/EIPS/eip-1167',

ui/address/contract/methods/ContractMethodsProxy.tsx

+4-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useRouter } from 'next/router';
33
import React from 'react';
44

55
import type { AddressImplementation } from 'types/api/addressParams';
6+
import type { SmartContractProxyType } from 'types/api/contract';
67

78
import useApiQuery from 'lib/api/useApiQuery';
89
import getQueryParamString from 'lib/router/getQueryParamString';
@@ -18,9 +19,10 @@ import { formatAbi } from './utils';
1819
interface Props {
1920
implementations: Array<AddressImplementation>;
2021
isLoading?: boolean;
22+
proxyType?: SmartContractProxyType;
2123
}
2224

23-
const ContractMethodsProxy = ({ implementations, isLoading: isInitialLoading }: Props) => {
25+
const ContractMethodsProxy = ({ implementations, isLoading: isInitialLoading, proxyType }: Props) => {
2426
const router = useRouter();
2527
const sourceAddress = getQueryParamString(router.query.source_address);
2628
const tab = getQueryParamString(router.query.tab);
@@ -48,7 +50,7 @@ const ContractMethodsProxy = ({ implementations, isLoading: isInitialLoading }:
4850
selectedItem={ selectedItem }
4951
onItemSelect={ setSelectedItem }
5052
isLoading={ isInitialLoading }
51-
label="Implementation address"
53+
label={ proxyType === 'eip7702' ? 'Delegate address' : 'Implementation address' }
5254
mb={ 3 }
5355
/>
5456
<ContractMethodsFilters

ui/address/contract/useContractDetailsTabs.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ interface Props {
2626

2727
export default function useContractDetailsTabs({ data, isLoading, addressHash, sourceAddress }: Props): Array<Tab> {
2828

29-
const canBeVerified = !data?.is_self_destructed && !data?.is_verified;
29+
const canBeVerified = !data?.is_self_destructed && !data?.is_verified && data?.proxy_type !== 'eip7702';
3030

3131
return React.useMemo(() => {
3232
const verificationButton = (

ui/address/contract/useContractTabs.tsx

+7-1
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,13 @@ export default function useContractTabs(data: Address | undefined, isPlaceholder
8888
verifiedImplementations.length > 0 && {
8989
id: [ 'read_write_proxy' as const, 'read_proxy' as const, 'write_proxy' as const ],
9090
title: 'Read/Write proxy',
91-
component: <ContractMethodsProxy implementations={ verifiedImplementations } isLoading={ contractQuery.isPlaceholderData }/>,
91+
component: (
92+
<ContractMethodsProxy
93+
implementations={ verifiedImplementations }
94+
isLoading={ contractQuery.isPlaceholderData }
95+
proxyType={ contractQuery.data?.proxy_type }
96+
/>
97+
),
9298
},
9399
config.features.account.isEnabled && {
94100
id: [ 'read_write_custom_methods' as const, 'read_custom_methods' as const, 'write_custom_methods' as const ],

ui/address/details/AddressImplementations.tsx

+10-3
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,34 @@
11
import React from 'react';
22

33
import type { AddressImplementation } from 'types/api/addressParams';
4+
import type { SmartContractProxyType } from 'types/api/contract';
45

56
import * as DetailsInfoItem from 'ui/shared/DetailsInfoItem';
67
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
78

89
interface Props {
910
data: Array<AddressImplementation>;
1011
isLoading?: boolean;
12+
proxyType?: SmartContractProxyType;
1113
}
1214

13-
const AddressImplementations = ({ data, isLoading }: Props) => {
15+
const AddressImplementations = ({ data, isLoading, proxyType }: Props) => {
1416
const hasManyItems = data.length > 1;
1517
const [ hasScroll, setHasScroll ] = React.useState(false);
1618

19+
const text = proxyType === 'eip7702' ? 'Delegate address' : `Implementation${ hasManyItems ? 's' : '' }`;
20+
const hint = proxyType === 'eip7702' ?
21+
'Account\'s executable code address' :
22+
`Implementation${ hasManyItems ? 's' : '' } address${ hasManyItems ? 'es' : '' } of the proxy contract`;
23+
1724
return (
1825
<>
1926
<DetailsInfoItem.Label
20-
hint={ `Implementation${ hasManyItems ? 's' : '' } address${ hasManyItems ? 'es' : '' } of the proxy contract` }
27+
hint={ hint }
2128
isLoading={ isLoading }
2229
hasScroll={ hasScroll }
2330
>
24-
{ `Implementation${ hasManyItems ? 's' : '' }` }
31+
{ text }
2532
</DetailsInfoItem.Label>
2633
<DetailsInfoItem.ValueWithScroll
2734
gradientHeight={ 48 }

ui/pages/Address.tsx

+11-4
Original file line numberDiff line numberDiff line change
@@ -239,16 +239,18 @@ const AddressPageContent = () => {
239239
addressQuery.data?.is_contract ? {
240240
id: 'contract',
241241
title: () => {
242+
const tabName = addressQuery.data.proxy_type === 'eip7702' ? 'Code' : 'Contract';
243+
242244
if (addressQuery.data.is_verified) {
243245
return (
244246
<>
245-
<span>Contract</span>
247+
<span>{ tabName }</span>
246248
<IconSvg name="status/success" boxSize="14px" color="green.500" ml={ 1 }/>
247249
</>
248250
);
249251
}
250252

251-
return 'Contract';
253+
return tabName;
252254
},
253255
component: (
254256
<AddressContract
@@ -279,7 +281,12 @@ const AddressPageContent = () => {
279281
config.features.validators.isEnabled && addressQuery.data?.has_validated_blocks ?
280282
{ slug: 'validator', name: 'Validator', tagType: 'custom' as const, ordinal: PREDEFINED_TAG_PRIORITY } :
281283
undefined,
282-
addressQuery.data?.implementations?.length ? { slug: 'proxy', name: 'Proxy', tagType: 'custom' as const, ordinal: PREDEFINED_TAG_PRIORITY } : undefined,
284+
addressQuery.data?.implementations?.length && addressQuery.data?.proxy_type !== 'eip7702' ?
285+
{ slug: 'proxy', name: 'Proxy', tagType: 'custom' as const, ordinal: PREDEFINED_TAG_PRIORITY } :
286+
undefined,
287+
addressQuery.data?.implementations?.length && addressQuery.data?.proxy_type === 'eip7702' ?
288+
{ slug: 'eip7702', name: 'EOA+code', tagType: 'custom' as const, ordinal: PREDEFINED_TAG_PRIORITY } :
289+
undefined,
283290
addressQuery.data?.token ? { slug: 'token', name: 'Token', tagType: 'custom' as const, ordinal: PREDEFINED_TAG_PRIORITY } : undefined,
284291
isSafeAddress ? { slug: 'safe', name: 'Multisig: Safe', tagType: 'custom' as const, ordinal: -10 } : undefined,
285292
addressProfileAPIFeature.isEnabled && usernameApiTag ? {
@@ -417,7 +424,7 @@ const AddressPageContent = () => {
417424
<>
418425
<TextAd mb={ 6 }/>
419426
<PageTitle
420-
title={ `${ addressQuery.data?.is_contract ? 'Contract' : 'Address' } details` }
427+
title={ `${ addressQuery.data?.is_contract && addressQuery.data?.proxy_type !== 'eip7702' ? 'Contract' : 'Address' } details` }
421428
backLink={ backLink }
422429
contentAfter={ titleContentAfter }
423430
secondRow={ titleSecondRow }

ui/pages/Transaction.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
1616
import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton';
1717
import useTabIndexFromQuery from 'ui/shared/Tabs/useTabIndexFromQuery';
1818
import TxAssetFlows from 'ui/tx/TxAssetFlows';
19+
import TxAuthorizations from 'ui/tx/TxAuthorizations';
1920
import TxBlobs from 'ui/tx/TxBlobs';
2021
import TxDetails from 'ui/tx/TxDetails';
2122
import TxDetailsDegraded from 'ui/tx/TxDetailsDegraded';
@@ -69,6 +70,9 @@ const TransactionPageContent = () => {
6970
{ id: 'logs', title: 'Logs', component: <TxLogs txQuery={ txQuery }/> },
7071
{ id: 'state', title: 'State', component: <TxState txQuery={ txQuery }/> },
7172
{ id: 'raw_trace', title: 'Raw trace', component: <TxRawTrace txQuery={ txQuery }/> },
73+
txQuery.data?.authorization_list?.length ?
74+
{ id: 'authorizations', title: 'Authorizations', component: <TxAuthorizations txQuery={ txQuery }/> } :
75+
undefined,
7276
].filter(Boolean);
7377
})();
7478

ui/shared/entities/address/AddressEntity.pw.tsx

+10
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,16 @@ test('with ENS', async({ render }) => {
154154
await expect(component).toHaveScreenshot();
155155
});
156156

157+
test('delegated address +@dark-mode', async({ render }) => {
158+
const component = await render(
159+
<AddressEntity
160+
address={ addressMock.delegated }
161+
/>,
162+
);
163+
164+
await expect(component).toHaveScreenshot();
165+
});
166+
157167
test('with name tag', async({ render }) => {
158168
const component = await render(
159169
<AddressEntity

ui/shared/entities/address/AddressEntity.tsx

+20-8
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import * as EntityBase from 'ui/shared/entities/base/components';
1414

1515
import { distributeEntityProps, getIconProps } from '../base/utils';
1616
import AddressEntityContentProxy from './AddressEntityContentProxy';
17+
import AddressIconDelegated from './AddressIconDelegated';
1718
import AddressIdenticon from './AddressIdenticon';
1819

1920
type LinkProps = EntityBase.LinkBaseProps & Pick<EntityProps, 'address'>;
@@ -51,7 +52,9 @@ const Icon = (props: IconProps) => {
5152
return <Skeleton { ...styles } borderRadius="full" flexShrink={ 0 }/>;
5253
}
5354

54-
if (props.address.is_contract) {
55+
const isDelegatedAddress = props.address.proxy_type === 'eip7702';
56+
57+
if (props.address.is_contract && !isDelegatedAddress) {
5558
if (props.isSafeAddress) {
5659
return (
5760
<EntityBase.Icon
@@ -80,13 +83,22 @@ const Icon = (props: IconProps) => {
8083
);
8184
}
8285

86+
const label = (() => {
87+
if (isDelegatedAddress) {
88+
return props.address.is_verified ? 'EOA + verified code' : 'EOA + code';
89+
}
90+
})();
91+
8392
return (
84-
<Flex marginRight={ styles.marginRight }>
85-
<AddressIdenticon
86-
size={ props.size === 'lg' ? 30 : 20 }
87-
hash={ getDisplayedAddress(props.address) }
88-
/>
89-
</Flex>
93+
<Tooltip label={ label }>
94+
<Flex marginRight={ styles.marginRight } position="relative">
95+
<AddressIdenticon
96+
size={ props.size === 'lg' ? 30 : 20 }
97+
hash={ getDisplayedAddress(props.address) }
98+
/>
99+
{ isDelegatedAddress && <AddressIconDelegated isVerified={ Boolean(props.address.is_verified) }/> }
100+
</Flex>
101+
</Tooltip>
90102
);
91103
};
92104

@@ -97,7 +109,7 @@ const Content = chakra((props: ContentProps) => {
97109
const nameTag = props.address.metadata?.tags.find(tag => tag.tagType === 'name')?.name;
98110
const nameText = nameTag || props.address.ens_domain_name || props.address.name;
99111

100-
const isProxy = props.address.implementations && props.address.implementations.length > 0;
112+
const isProxy = props.address.implementations && props.address.implementations.length > 0 && props.address.proxy_type !== 'eip7702';
101113

102114
if (isProxy) {
103115
return <AddressEntityContentProxy { ...props }/>;

0 commit comments

Comments
 (0)