Show conflicting proxy implementations warning (#3179)

* Show conflicting proxy implementations warning

Resolves #2884

* fix: add conflicting_implementations field to contract mocks

* fix tests

* proxy info tweaks

* fix ts
This commit is contained in:
tom goriunov
2026-01-05 19:14:56 +01:00
committed by GitHub
parent 2d42dbf1b9
commit eabda23728
33 changed files with 369 additions and 160 deletions
+2
View File
@@ -43,6 +43,7 @@ export const verified: SmartContract = {
file_path: '',
additional_sources: [],
verified_twin_address_hash: null,
conflicting_implementations: null,
};
export const certified: SmartContract = {
@@ -144,6 +145,7 @@ export const nonVerified: SmartContract = {
additional_sources: [],
external_libraries: null,
verified_twin_address_hash: null,
conflicting_implementations: null,
language: null,
license_type: null,
};
+10
View File
@@ -1,5 +1,7 @@
import type { Abi, AbiType } from 'abitype';
import type { AddressImplementation } from './addressParams';
export type SmartContractMethodArgType = AbiType;
export type SmartContractMethodStateMutability = 'view' | 'nonpayable' | 'payable';
@@ -24,6 +26,8 @@ export type SmartContractLicenseType =
export type SmartContractProxyType =
'eip1167' |
'eip1967' |
'eip1967_oz' |
'eip1967_beacon' |
'eip1822' |
'eip930' |
'eip2535' |
@@ -38,6 +42,11 @@ export type SmartContractProxyType =
'unknown' |
null;
export interface SmartContractConflictingImplementation {
proxy_type: NonNullable<SmartContractProxyType>;
implementations: Array<AddressImplementation>;
}
export interface SmartContract {
deployed_bytecode: string | null;
creation_bytecode: string | null;
@@ -53,6 +62,7 @@ export interface SmartContract {
is_verified: boolean | null;
is_verified_via_eth_bytecode_db: boolean | null;
is_changed_bytecode: boolean | null;
conflicting_implementations: Array<SmartContractConflictingImplementation> | null;
// sourcify info >>>
is_verified_via_sourcify: boolean | null;
+4 -4
View File
@@ -32,7 +32,7 @@ test.describe('ABI functionality', () => {
const socket = await createSocket();
await socketServer.joinChannel(socket, 'addresses:' + addressMock.contract.hash.toLowerCase());
await expect(component.getByRole('button', { name: 'Connect wallet' })).toBeVisible();
await expect(component.getByRole('button', { name: 'Connect your wallet' })).toBeVisible();
await component.getByText('FLASHLOAN_PREMIUM_TOTAL').click();
await expect(component.getByLabel('FLASHLOAN_PREMIUM_TOTAL').getByRole('button', { name: 'Read' })).toBeVisible();
});
@@ -48,7 +48,7 @@ test.describe('ABI functionality', () => {
const socket = await createSocket();
await socketServer.joinChannel(socket, 'addresses:' + addressMock.contract.hash.toLowerCase());
await expect(component.getByRole('button', { name: 'Connect wallet' })).toBeHidden();
await expect(component.getByRole('button', { name: 'Connect your wallet' })).toBeHidden();
await component.getByText('FLASHLOAN_PREMIUM_TOTAL').click();
await expect(component.getByLabel('FLASHLOAN_PREMIUM_TOTAL').getByRole('button', { name: 'Read' })).toBeVisible();
});
@@ -63,7 +63,7 @@ test.describe('ABI functionality', () => {
const socket = await createSocket();
await socketServer.joinChannel(socket, 'addresses:' + addressMock.contract.hash.toLowerCase());
await expect(component.getByRole('button', { name: 'Connect wallet' })).toBeVisible();
await expect(component.getByRole('button', { name: 'Connect your wallet' })).toBeVisible();
await component.getByText('setReserveInterestRateStrategyAddress').click();
await expect(component.getByLabel('9.').getByRole('button', { name: 'Simulate' })).toBeEnabled();
await expect(component.getByLabel('9.').getByRole('button', { name: 'Write' })).toBeEnabled();
@@ -85,7 +85,7 @@ test.describe('ABI functionality', () => {
const socket = await createSocket();
await socketServer.joinChannel(socket, 'addresses:' + addressMock.contract.hash.toLowerCase());
await expect(component.getByRole('button', { name: 'Connect wallet' })).toBeHidden();
await expect(component.getByRole('button', { name: 'Connect your wallet' })).toBeHidden();
await component.getByText('setReserveInterestRateStrategyAddress').click();
await expect(component.getByLabel('9.').getByRole('button', { name: 'Simulate' })).toBeEnabled();
await expect(component.getByLabel('9.').getByRole('button', { name: 'Write' })).toBeDisabled();
@@ -0,0 +1,80 @@
import { Grid, GridItem, HStack, Text, VStack } from '@chakra-ui/react';
import React from 'react';
import type { SmartContractConflictingImplementation } from 'types/api/contract';
import { Button } from 'toolkit/chakra/button';
import { DialogActionTrigger, DialogBody, DialogContent, DialogHeader, DialogRoot, DialogTrigger } from 'toolkit/chakra/dialog';
import { Link } from 'toolkit/chakra/link';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import { PROXY_TYPES } from './utils';
interface Props {
data: Array<SmartContractConflictingImplementation>;
children: React.ReactNode;
}
const ConflictingImplementationsModal = ({ data, children }: Props) => {
return (
<DialogRoot size={{ lgDown: 'full', lg: 'md' }}>
<DialogTrigger>
{ children }
</DialogTrigger>
<DialogContent>
<DialogHeader>Detected proxy implementations</DialogHeader>
<DialogBody>
<Text>
Multiple proxy patterns were detected for this contract.
This may be due to an unsupported custom proxy design or due to a malicious proxy spoofing attempt.
Review carefully.
</Text>
<VStack alignItems="stretch" mt={ 6 } textStyle="sm">
{ data.map((item) => {
const addressNum = item.implementations.length;
const addressText = addressNum === 1 ? 'Implementation:' : 'Implementations:';
const proxyType = PROXY_TYPES[item.proxy_type]?.name || PROXY_TYPES.unknown?.name;
return (
<Grid
key={ item.proxy_type }
templateColumns="115px minmax(0px, 1fr)"
w="100%"
p={ 4 }
columnGap={ 5 }
rowGap={ 2 }
borderRadius="md"
bgColor={{ _light: 'blackAlpha.50', _dark: 'whiteAlpha.50' }}
>
<GridItem>Proxy type:</GridItem>
<GridItem>{ proxyType }</GridItem>
<GridItem>{ addressText }</GridItem>
<GridItem>
<VStack alignItems="stretch">
{ item.implementations.map((implementation) => (
<AddressEntity
key={ implementation.address_hash }
address={{ hash: implementation.address_hash, name: implementation.name }}
/>
)) }
</VStack>
</GridItem>
</Grid>
);
}) }
</VStack>
<HStack mt={ 6 } gap={ 6 }>
<DialogActionTrigger asChild>
<Button>Got it, thanks</Button>
</DialogActionTrigger>
<Link external noIcon href="https://discord.com/invite/blockscout">
Contact us
</Link>
</HStack>
</DialogBody>
</DialogContent>
</DialogRoot>
);
};
export default React.memo(ConflictingImplementationsModal);
@@ -18,3 +18,31 @@ test('proxy type without link', async({ render }) => {
const component = await render(<ContractDetailsAlertProxyPattern type="basic_implementation" isLoading={ false }/>);
await expect(component).toHaveScreenshot();
});
test('proxy type with conflicting implementations', async({ render, page }) => {
const component = await render(
<ContractDetailsAlertProxyPattern
type="resolved_delegate_proxy"
isLoading={ false }
conflictingImplementations={ [
{
proxy_type: 'eip1967',
implementations: [
{ address_hash: '0x568AA6C21cCf558C47F2A01B60cc6D549cED2F59' },
],
},
{
proxy_type: 'eip1967_oz',
implementations: [
{ address_hash: '0x7d608aBCf9a3BE6B869E745E6F8dB3434877D60F', name: 'Implementation 3' },
{ address_hash: '0x568AA6C21cCf558C47F2A01B60cc6D549cED2F59' },
],
},
] }
/>,
);
await expect(component).toHaveScreenshot();
page.getByText('View details').click();
await expect(page).toHaveScreenshot();
});
@@ -1,98 +1,50 @@
import { Box } from '@chakra-ui/react';
import React from 'react';
import type { SmartContractProxyType } from 'types/api/contract';
import type { SmartContractConflictingImplementation, SmartContractProxyType } from 'types/api/contract';
import { Alert } from 'toolkit/chakra/alert';
import { Link } from 'toolkit/chakra/link';
import { space } from 'toolkit/utils/htmlEntities';
import ConflictingImplementationsModal from './ConflictingImplementationsModal';
import { PROXY_TYPES } from './utils';
interface Props {
type: NonNullable<SmartContractProxyType>;
isLoading: boolean;
isLoading?: boolean;
conflictingImplementations?: Array<SmartContractConflictingImplementation>;
}
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',
description: 'Minimal proxy',
},
eip1967: {
name: 'EIP-1967',
link: 'https://eips.ethereum.org/EIPS/eip-1967',
description: 'Proxy storage slots',
},
eip1822: {
name: 'EIP-1822',
link: 'https://eips.ethereum.org/EIPS/eip-1822',
description: 'Universal upgradeable proxy standard (UUPS)',
},
eip2535: {
name: 'EIP-2535',
link: 'https://eips.ethereum.org/EIPS/eip-2535',
description: 'Diamond proxy',
},
eip930: {
name: 'ERC-930',
link: 'https://github.com/ethereum/EIPs/issues/930',
description: 'Eternal storage',
},
erc7760: {
name: 'ERC-7760',
link: 'https://eips.ethereum.org/EIPS/eip-7760',
description: 'Minimal Upgradeable Proxies',
},
resolved_delegate_proxy: {
name: 'ResolvedDelegateProxy',
// eslint-disable-next-line max-len
link: 'https://github.com/ethereum-optimism/optimism/blob/9580179013a04b15e6213ae8aa8d43c3f559ed9a/packages/contracts-bedrock/src/legacy/ResolvedDelegateProxy.sol',
description: 'OP stack: legacy proxy contract that makes use of the AddressManager to resolve the implementation address',
},
clone_with_immutable_arguments: {
name: 'Clones with immutable arguments',
link: 'https://github.com/wighawag/clones-with-immutable-args',
},
master_copy: {
name: 'Safe proxy',
link: 'https://github.com/safe-global/safe-smart-account',
},
comptroller: {
name: 'Compound protocol proxy',
link: 'https://github.com/compound-finance/compound-protocol',
},
basic_implementation: {
name: 'public implementation() getter',
},
basic_get_implementation: {
name: 'public getImplementation() getter',
},
unknown: {
name: 'Unknown proxy pattern',
},
};
const ContractCodeProxyPattern = ({ type, isLoading }: Props) => {
const ContractCodeProxyPattern = ({ type, isLoading, conflictingImplementations }: Props) => {
const proxyInfo = PROXY_TYPES[type];
if (!proxyInfo || type === 'unknown') {
return null;
}
const status = conflictingImplementations && conflictingImplementations.length > 0 ? 'warning' : 'success';
return (
<Alert status="warning" whiteSpace="pre-wrap" loading={ isLoading }>
<Alert status={ status } whiteSpace="pre-wrap" loading={ isLoading } descriptionProps={{ flexDir: 'column' }}>
{ proxyInfo.link ? (
<>
<Box>
This proxy smart-contract is detected via <Link href={ proxyInfo.link } external>{ proxyInfo.name }</Link>
{ proxyInfo.description && ` - ${ proxyInfo.description }` }
</>
</Box>
) : (
<>
<Box>
This proxy smart-contract is detected via { proxyInfo.name }
{ proxyInfo.description && ` - ${ proxyInfo.description }` }
</>
</Box>
) }
{ conflictingImplementations && conflictingImplementations.length > 0 && (
<Box mt={ 1 } whiteSpace="pre-wrap">
<span>This contract contains more than one proxy implementation address.{ space }</span>
<ConflictingImplementationsModal data={ conflictingImplementations }>
<Link>View details</Link>
</ConflictingImplementationsModal>
</Box>
) }
</Alert>
);
@@ -60,6 +60,13 @@ const ContractDetailsAlerts = ({ data, isLoading, addressData, channel }: Props)
}
</Alert>
) }
{ addressData.proxy_type && (
<ContractDetailsAlertProxyPattern
type={ addressData.proxy_type }
isLoading={ isLoading }
conflictingImplementations={ data?.conflicting_implementations ?? undefined }
/>
) }
<ContractDetailsAlertVerificationSource data={ data }/>
{ (data?.is_changed_bytecode || isChangedBytecodeSocket) && (
<Alert status="warning">
@@ -82,7 +89,6 @@ const ContractDetailsAlerts = ({ data, isLoading, addressData, channel }: Props)
<span> page</span>
</Alert>
) }
{ addressData.proxy_type && <ContractDetailsAlertProxyPattern type={ addressData.proxy_type } isLoading={ isLoading }/> }
</Flex>
);
};
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

+72
View File
@@ -0,0 +1,72 @@
import type { SmartContractProxyType } from 'types/api/contract';
export 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',
description: 'Minimal Proxy',
},
eip1967: {
name: 'EIP-1967',
link: 'https://eips.ethereum.org/EIPS/eip-1967',
description: 'Transparent Proxy',
},
eip1967_oz: {
name: 'EIP-1967 (Legacy)',
link: 'https://eips.ethereum.org/EIPS/eip-1967',
description: 'Legacy OpenZeppelin Transparent Proxy',
},
eip1967_beacon: {
name: 'EIP-1967 (Beacon)',
link: 'https://eips.ethereum.org/EIPS/eip-1967',
description: 'Beacon Proxy',
},
eip1822: {
name: 'EIP-1822',
link: 'https://eips.ethereum.org/EIPS/eip-1822',
description: 'Universal Upgradeable Proxy Standard (UUPS)',
},
eip2535: {
name: 'EIP-2535',
link: 'https://eips.ethereum.org/EIPS/eip-2535',
description: 'Diamond Proxy',
},
erc7760: {
name: 'ERC-7760',
link: 'https://eips.ethereum.org/EIPS/eip-7760',
description: 'Minimal Upgradeable Proxy',
},
resolved_delegate_proxy: {
name: 'ResolvedDelegateProxy',
// eslint-disable-next-line max-len
link: 'https://github.com/ethereum-optimism/optimism/blob/9580179013a04b15e6213ae8aa8d43c3f559ed9a/packages/contracts-bedrock/src/legacy/ResolvedDelegateProxy.sol',
description: 'OP Stack Proxy',
},
clone_with_immutable_arguments: {
name: 'Clones with immutable arguments',
link: 'https://github.com/wighawag/clones-with-immutable-args',
},
master_copy: {
name: 'Safe Proxy',
link: 'https://github.com/safe-global/safe-smart-account',
},
comptroller: {
name: 'Compound Protocol Proxy',
link: 'https://github.com/compound-finance/compound-protocol',
},
basic_implementation: {
name: 'Generic Proxy',
description: 'implementation() getter',
},
basic_get_implementation: {
name: 'Generic Proxy',
description: 'getImplementation() getter',
},
unknown: {
name: 'Unknown proxy pattern',
},
};
@@ -0,0 +1,27 @@
import { VStack } from '@chakra-ui/react';
import React from 'react';
import type { SmartContractConflictingImplementation, SmartContractProxyType } from 'types/api/contract';
import ContractCodeProxyPattern from '../alerts/ContractDetailsAlertProxyPattern';
import ConnectWalletAlert from './alerts/ConnectWalletAlert';
import ContractCustomAbiAlert from './alerts/ContractCustomAbiAlert';
interface Props {
isLoading?: boolean;
isCustomAbi?: boolean;
proxyType?: SmartContractProxyType;
conflictingImplementations?: Array<SmartContractConflictingImplementation>;
}
const ContractMethodsAlerts = ({ isLoading, isCustomAbi, proxyType, conflictingImplementations }: Props) => {
return (
<VStack gap={ 1 } alignItems="stretch">
<ConnectWalletAlert isLoading={ isLoading }/>
{ proxyType && <ContractCodeProxyPattern isLoading={ isLoading } type={ proxyType } conflictingImplementations={ conflictingImplementations }/> }
{ isCustomAbi && <ContractCustomAbiAlert isLoading={ isLoading }/> }
</VStack>
);
};
export default React.memo(ContractMethodsAlerts);
@@ -11,13 +11,12 @@ import { Button } from 'toolkit/chakra/button';
import { Skeleton } from 'toolkit/chakra/skeleton';
import { useDisclosure } from 'toolkit/hooks/useDisclosure';
import CustomAbiModal from 'ui/customAbi/CustomAbiModal/CustomAbiModal';
import ConnectWalletAlert from 'ui/shared/ConnectWalletAlert';
import RawDataSnippet from 'ui/shared/RawDataSnippet';
import AuthGuard from 'ui/snippets/auth/AuthGuard';
import useIsAuth from 'ui/snippets/auth/useIsAuth';
import ContractAbi from './ContractAbi';
import ContractCustomAbiAlert from './ContractCustomAbiAlert';
import ContractMethodsAlerts from './ContractMethodsAlerts';
import ContractMethodsContainer from './ContractMethodsContainer';
import ContractMethodsFilters from './ContractMethodsFilters';
import useMethodsFilters from './useMethodsFilters';
@@ -76,10 +75,10 @@ const ContractMethodsCustom = ({ isLoading: isLoadingProp }: Props) => {
<Flex flexDir="column" rowGap={ 6 }>
{ currentInfo ? (
<>
<Flex flexDir="column" rowGap={ 1 }>
<ConnectWalletAlert isLoading={ isLoading }/>
<ContractCustomAbiAlert isLoading={ isLoading }/>
</Flex>
<ContractMethodsAlerts
isCustomAbi
isLoading={ isLoading }
/>
<RawDataSnippet
data={ JSON.stringify(abi) }
title="Contract ABI"
@@ -6,11 +6,11 @@ import type { SmartContractMudSystemItem } from 'types/api/contract';
import useApiQuery from 'lib/api/useApiQuery';
import getQueryParamString from 'lib/router/getQueryParamString';
import ConnectWalletAlert from 'ui/shared/ConnectWalletAlert';
import type { Item } from '../ContractSourceAddressSelector';
import ContractSourceAddressSelector from '../ContractSourceAddressSelector';
import ContractAbi from './ContractAbi';
import ContractMethodsAlerts from './ContractMethodsAlerts';
import ContractMethodsContainer from './ContractMethodsContainer';
import ContractMethodsFilters from './ContractMethodsFilters';
import useMethodsFilters from './useMethodsFilters';
@@ -47,7 +47,7 @@ const ContractMethodsMudSystem = ({ items }: Props) => {
return (
<Flex flexDir="column" rowGap={ 6 }>
<ConnectWalletAlert/>
<ContractMethodsAlerts isLoading={ systemInfoQuery.isPending }/>
<div>
<ContractSourceAddressSelector
items={ items }
@@ -20,7 +20,7 @@ test('with one implementation +@mobile', async({ render, mockApiResponse }) => {
];
await mockApiResponse('general:contract', { ...contractMock.verified, abi: methodsMock.read }, { pathParams: { hash: implementations[0].address_hash } });
const component = await render(<ContractMethodsProxy implementations={ implementations }/>, { hooksConfig });
const component = await render(<ContractMethodsProxy implementations={ implementations } proxyType="eip1167"/>, { hooksConfig });
await expect(component).toHaveScreenshot();
});
@@ -36,6 +36,24 @@ test('with multiple implementations +@mobile', async({ render, mockApiResponse }
];
await mockApiResponse('general:contract', { ...contractMock.verified, abi: methodsMock.read }, { pathParams: { hash: implementations[0].address_hash } });
const component = await render(<ContractMethodsProxy implementations={ implementations }/>, { hooksConfig });
const component = await render(
<ContractMethodsProxy
implementations={ implementations }
proxyType="eip1167"
conflictingImplementations={ [
{
proxy_type: 'eip1167',
implementations: [
{ address_hash: '0x2F4F4A52295940C576417d29F22EEb92B440eC89', name: 'HomeBridge' },
],
},
{
proxy_type: 'eip1167',
implementations: [
{ address_hash: '0x2F4F4A52295940C576417d29F22EEb92B440eC89', name: 'HomeBridge' },
],
},
] }
/>, { hooksConfig });
await expect(component).toHaveScreenshot();
});
@@ -3,14 +3,14 @@ import { useRouter } from 'next/router';
import React from 'react';
import type { AddressImplementation } from 'types/api/addressParams';
import type { SmartContractProxyType } from 'types/api/contract';
import type { SmartContractConflictingImplementation, SmartContractProxyType } from 'types/api/contract';
import useApiQuery from 'lib/api/useApiQuery';
import getQueryParamString from 'lib/router/getQueryParamString';
import ConnectWalletAlert from 'ui/shared/ConnectWalletAlert';
import ContractSourceAddressSelector from '../ContractSourceAddressSelector';
import ContractAbi from './ContractAbi';
import ContractMethodsAlerts from './ContractMethodsAlerts';
import ContractMethodsContainer from './ContractMethodsContainer';
import ContractMethodsFilters from './ContractMethodsFilters';
import useMethodsFilters from './useMethodsFilters';
@@ -20,9 +20,10 @@ interface Props {
implementations: Array<AddressImplementation>;
isLoading?: boolean;
proxyType?: SmartContractProxyType;
conflictingImplementations?: Array<SmartContractConflictingImplementation>;
}
const ContractMethodsProxy = ({ implementations, isLoading: isInitialLoading, proxyType }: Props) => {
const ContractMethodsProxy = ({ implementations, isLoading: isInitialLoading, proxyType, conflictingImplementations }: Props) => {
const router = useRouter();
const sourceAddress = getQueryParamString(router.query.source_address);
const tab = getQueryParamString(router.query.tab);
@@ -43,7 +44,7 @@ const ContractMethodsProxy = ({ implementations, isLoading: isInitialLoading, pr
return (
<Flex flexDir="column" rowGap={ 6 }>
<ConnectWalletAlert isLoading={ isInitialLoading }/>
<ContractMethodsAlerts isLoading={ isInitialLoading } proxyType={ proxyType } conflictingImplementations={ conflictingImplementations }/>
<div>
<ContractSourceAddressSelector
items={ implementations }
@@ -4,9 +4,9 @@ import React from 'react';
import type { Abi } from 'viem';
import getQueryParamString from 'lib/router/getQueryParamString';
import ConnectWalletAlert from 'ui/shared/ConnectWalletAlert';
import ContractAbi from './ContractAbi';
import ContractMethodsAlerts from './ContractMethodsAlerts';
import ContractMethodsContainer from './ContractMethodsContainer';
import ContractMethodsFilters from './ContractMethodsFilters';
import useMethodsFilters from './useMethodsFilters';
@@ -29,7 +29,7 @@ const ContractMethodsRegular = ({ abi, isLoading }: Props) => {
return (
<Flex flexDir="column" rowGap={ 6 }>
<ConnectWalletAlert isLoading={ isLoading }/>
<ContractMethodsAlerts isLoading={ isLoading }/>
<ContractMethodsFilters
defaultMethodType={ filters.methodType }
defaultSearchTerm={ filters.searchTerm }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 201 KiB

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 KiB

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 KiB

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 44 KiB

@@ -0,0 +1,80 @@
import { Flex, Spinner } from '@chakra-ui/react';
import React from 'react';
import config from 'configs/app';
import useIsMobile from 'lib/hooks/useIsMobile';
import useWeb3Wallet from 'lib/web3/useWallet';
import { Alert } from 'toolkit/chakra/alert';
import { Button } from 'toolkit/chakra/button';
import { IconButton } from 'toolkit/chakra/icon-button';
import { Skeleton } from 'toolkit/chakra/skeleton';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import IconSvg from 'ui/shared/IconSvg';
interface Props {
isLoading?: boolean;
}
const ConnectWalletAlert = ({ isLoading }: Props) => {
const web3Wallet = useWeb3Wallet({ source: 'Smart contracts' });
const isMobile = useIsMobile();
const handleOpenWalletClick = React.useCallback(() => {
web3Wallet.openModal();
}, [ web3Wallet ]);
const content = (() => {
if (!web3Wallet.isConnected) {
return (
<Flex alignItems="center" flexWrap="wrap" columnGap={ 3 } rowGap={ 2 }>
<span>To interact with the contract, please</span>
<Button
onClick={ web3Wallet.connect }
size="sm"
loading={ web3Wallet.isOpen }
loadingText="Connect your wallet"
>
Connect your wallet
</Button>
</Flex>
);
}
return (
<Flex alignItems="center" columnGap={ 2 }>
<span>Connected to </span>
<AddressEntity
address={{ hash: web3Wallet.address || '' }}
truncation={ isMobile ? 'constant' : 'dynamic' }
fontWeight={ 600 }
noAltHash
noLink
noCopy
/>
{ web3Wallet.isReconnecting ? <Spinner size="sm" m="2px" flexShrink={ 0 }/> : (
<IconButton
aria-label="Open wallet"
variant="icon_secondary"
size="2xs"
onClick={ handleOpenWalletClick }
loading={ web3Wallet.isOpen }
>
<IconSvg name="gear"/>
</IconButton>
) }
</Flex>
);
})();
return (
<Skeleton loading={ isLoading }>
<Alert status="info" descriptionProps={{ alignItems: 'center' }}>
{ content }
</Alert>
</Skeleton>
);
};
const Fallback = () => null;
export default config.features.blockchainInteraction.isEnabled ? ConnectWalletAlert : Fallback;
+1
View File
@@ -84,6 +84,7 @@ export default function useContractTabs({ addressData, isEnabled, hasMudTab, cha
implementations={ verifiedImplementations }
isLoading={ contractQuery.isPlaceholderData }
proxyType={ addressData?.proxy_type }
conflictingImplementations={ contractQuery.data?.conflicting_implementations ?? undefined }
/>
),
},
-67
View File
@@ -1,67 +0,0 @@
import { Flex } from '@chakra-ui/react';
import React from 'react';
import config from 'configs/app';
import useIsMobile from 'lib/hooks/useIsMobile';
import useWeb3Wallet from 'lib/web3/useWallet';
import { Alert } from 'toolkit/chakra/alert';
import { Button } from 'toolkit/chakra/button';
import { Skeleton } from 'toolkit/chakra/skeleton';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
interface Props {
isLoading?: boolean;
}
const ConnectWalletAlert = ({ isLoading }: Props) => {
const web3Wallet = useWeb3Wallet({ source: 'Smart contracts' });
const isMobile = useIsMobile();
const content = (() => {
if (!web3Wallet.isConnected) {
return (
<>
<span>Disconnected</span>
<Button
ml={ 3 }
onClick={ web3Wallet.connect }
size="sm"
variant="outline"
loading={ web3Wallet.isOpen }
loadingText="Connect wallet"
>
Connect wallet
</Button>
</>
);
}
return (
<Flex columnGap={ 2 } rowGap={ 2 } alignItems={{ base: 'flex-start', lg: 'center' }} flexDir={{ base: 'column', lg: 'row' }}>
<Flex alignItems="center">
<span>Connected to </span>
<AddressEntity
address={{ hash: web3Wallet.address || '' }}
truncation={ isMobile ? 'constant' : 'dynamic' }
fontWeight={ 600 }
ml={ 2 }
noAltHash
/>
</Flex>
<Button onClick={ web3Wallet.disconnect } size="sm" variant="outline">Disconnect</Button>
</Flex>
);
})();
return (
<Skeleton loading={ isLoading }>
<Alert status={ web3Wallet.address ? 'success' : 'warning' } descriptionProps={{ alignItems: 'center' }}>
{ content }
</Alert>
</Skeleton>
);
};
const Fallback = () => null;
export default config.features.blockchainInteraction.isEnabled ? ConnectWalletAlert : Fallback;