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
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.0 KiB |
@@ -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 }
|
||||
|
||||
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 201 KiB After Width: | Height: | Size: 202 KiB |
|
Before Width: | Height: | Size: 196 KiB After Width: | Height: | Size: 198 KiB |
|
Before Width: | Height: | Size: 181 KiB After Width: | Height: | Size: 182 KiB |
|
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;
|
||||
@@ -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 }
|
||||
/>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -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;
|
||||