Commit 45eb8741 authored by tom goriunov's avatar tom goriunov Committed by GitHub

Implementation name behind proxy name (#2071)

* icon for proxy contract

* add custom tooltip for proxy contract

* add name to tooltip and refactor

* tests

* center the popup content
parent 18b419ef
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.392.45C3.644.163 3.984 0 4.34 0h8.038a.63.63 0 0 1 .474.225L17.54 5.61a.83.83 0 0 1 .196.544v12.308c0 .408-.141.799-.393 1.087-.25.289-.592.451-.947.451H4.34c-.356 0-.696-.162-.948-.45A1.661 1.661 0 0 1 3 18.461V1.538c0-.408.141-.799.392-1.087Zm.948 1.088h6.87v4.497c0 .388.315.702.702.702h4.485v11.725H4.34V1.538Zm8.274.59 2.791 3.205h-2.791V2.128Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.233 8.8a2.293 2.293 0 0 0-1.613.644l-.006.006-.595.591a.564.564 0 1 0 .795.8l.592-.589a1.166 1.166 0 0 1 1.649 1.648l-1.034 1.034a1.168 1.168 0 0 1-1.758-.126.564.564 0 1 0-.903.676 2.293 2.293 0 0 0 3.458.247l1.038-1.037.007-.007a2.294 2.294 0 0 0-1.63-3.887Zm-2.259 2.403a2.294 2.294 0 0 0-1.786.666l-1.037 1.037-.007.008a2.293 2.293 0 0 0 3.243 3.242l.006-.007.592-.591a.564.564 0 0 0-.797-.797l-.588.587A1.166 1.166 0 0 1 7.952 13.7l1.034-1.034a1.167 1.167 0 0 1 1.758.126.564.564 0 0 0 .903-.675 2.293 2.293 0 0 0-1.673-.914Z" fill="currentColor"/>
</svg>
...@@ -60,9 +60,9 @@ export function AddressHighlightProvider({ children }: AddressHighlightProviderP ...@@ -60,9 +60,9 @@ export function AddressHighlightProvider({ children }: AddressHighlightProviderP
); );
} }
export function useAddressHighlightContext() { export function useAddressHighlightContext(disabled?: boolean) {
const context = React.useContext(AddressHighlightContext); const context = React.useContext(AddressHighlightContext);
if (context === undefined) { if (context === undefined || disabled) {
return null; return null;
} }
return context; return context;
......
export const multiple = [
{ address: '0xA84d24bD8ACE4d349C5f8c5DeeDd8bc071Ce5e2b', name: null },
{ address: '0xc9e91eDeA9DC16604022e4E5b437Df9c64EdB05A', name: 'Diamond' },
{ address: '0x2041832c62C0F89426b48B5868146C0b1fcd23E7', name: null },
{ address: '0x5f7DC6ECcF05594429671F83cc0e42EE18bC0974', name: 'VariablePriceFacet' },
{ address: '0x7abC92E242e88e4B0d6c5Beb4Df80e94D2c8A78c', name: null },
{ address: '0x84178a0c58A860eCCFB7E3aeA64a09543062A356', name: 'MultiSaleFacet' },
{ address: '0x33aD95537e63e9f09d96dE201e10715Ed40D9400', name: 'SVGTemplatesFacet' },
{ address: '0xfd86Aa7f902185a8Df9859c25E4BF52D3DaDd9FA', name: 'ERC721AReceiverFacet' },
{ address: '0x6945a35df18e59Ce09fec4B6cD3C4F9cFE6369de', name: null },
];
...@@ -31,6 +31,7 @@ ...@@ -31,6 +31,7 @@
| "clock" | "clock"
| "coins/bitcoin" | "coins/bitcoin"
| "collection" | "collection"
| "contracts/proxy"
| "contracts/regular_many" | "contracts/regular_many"
| "contracts/regular" | "contracts/regular"
| "contracts/verified_many" | "contracts/verified_many"
......
...@@ -24,9 +24,7 @@ export interface UserTags { ...@@ -24,9 +24,7 @@ export interface UserTags {
export type AddressParamBasic = { export type AddressParamBasic = {
hash: string; hash: string;
// API doesn't return hash in this model yet implementations: Array<AddressImplementation> | null;
// will be fixed in the future releases
implementations: Array<Omit<AddressImplementation, 'address'>> | null;
name: string | null; name: string | null;
is_contract: boolean; is_contract: boolean;
is_verified: boolean | null; is_verified: boolean | null;
......
...@@ -67,7 +67,7 @@ const OptimisticDepositsListItem = ({ item, isLoading }: Props) => { ...@@ -67,7 +67,7 @@ const OptimisticDepositsListItem = ({ item, isLoading }: Props) => {
<ListItemMobileGrid.Label isLoading={ isLoading }>L1 txn origin</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>L1 txn origin</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value> <ListItemMobileGrid.Value>
<AddressEntityL1 <AddressEntityL1
address={{ hash: item.l1_tx_origin, name: '', is_contract: false, is_verified: false, ens_domain_name: null }} address={{ hash: item.l1_tx_origin, name: '', is_contract: false, is_verified: false, ens_domain_name: null, implementations: null }}
isLoading={ isLoading } isLoading={ isLoading }
noCopy noCopy
truncation="constant" truncation="constant"
......
...@@ -59,7 +59,7 @@ const OptimisticDepositsTableItem = ({ item, isLoading }: Props) => { ...@@ -59,7 +59,7 @@ const OptimisticDepositsTableItem = ({ item, isLoading }: Props) => {
</Td> </Td>
<Td verticalAlign="middle"> <Td verticalAlign="middle">
<AddressEntityL1 <AddressEntityL1
address={{ hash: item.l1_tx_origin, name: '', is_contract: false, is_verified: false, ens_domain_name: null }} address={{ hash: item.l1_tx_origin, name: '', is_contract: false, is_verified: false, ens_domain_name: null, implementations: null }}
isLoading={ isLoading } isLoading={ isLoading }
truncation="constant" truncation="constant"
noCopy noCopy
......
...@@ -272,7 +272,7 @@ const AddressPageContent = () => { ...@@ -272,7 +272,7 @@ const AddressPageContent = () => {
/> />
) } ) }
<AddressEntity <AddressEntity
address={{ ...addressQuery.data, hash, name: '', ens_domain_name: '' }} address={{ ...addressQuery.data, hash, name: '', ens_domain_name: '', implementations: null }}
isLoading={ isLoading } isLoading={ isLoading }
fontFamily="heading" fontFamily="heading"
fontSize="lg" fontSize="lg"
......
...@@ -3,6 +3,7 @@ import React from 'react'; ...@@ -3,6 +3,7 @@ import React from 'react';
import { AddressHighlightProvider } from 'lib/contexts/addressHighlight'; import { AddressHighlightProvider } from 'lib/contexts/addressHighlight';
import * as addressMock from 'mocks/address/address'; import * as addressMock from 'mocks/address/address';
import * as implementationsMock from 'mocks/address/implementations';
import { test, expect } from 'playwright/lib'; import { test, expect } from 'playwright/lib';
import AddressEntity from './AddressEntity'; import AddressEntity from './AddressEntity';
...@@ -30,7 +31,7 @@ test.describe('contract', () => { ...@@ -30,7 +31,7 @@ test.describe('contract', () => {
test('unverified', async({ render, page }) => { test('unverified', async({ render, page }) => {
const component = await render( const component = await render(
<AddressEntity <AddressEntity
address={{ ...addressMock.contract, is_verified: false }} address={{ ...addressMock.contract, is_verified: false, implementations: null }}
/>, />,
); );
...@@ -41,7 +42,7 @@ test.describe('contract', () => { ...@@ -41,7 +42,7 @@ test.describe('contract', () => {
test('verified', async({ render }) => { test('verified', async({ render }) => {
const component = await render( const component = await render(
<AddressEntity <AddressEntity
address={{ ...addressMock.contract, is_verified: true }} address={{ ...addressMock.contract, is_verified: true, implementations: null }}
/>, />,
); );
...@@ -49,6 +50,58 @@ test.describe('contract', () => { ...@@ -49,6 +50,58 @@ test.describe('contract', () => {
}); });
}); });
test.describe('proxy contract', () => {
test.use({ viewport: { width: 500, height: 300 } });
test('with implementation name', async({ render, page }) => {
const component = await render(
<AddressEntity
address={ addressMock.contract }
/>,
);
await component.getByText(/home/i).hover();
await expect(page.getByText('Proxy contract')).toBeVisible();
await expect(page).toHaveScreenshot();
});
test('without implementation name', async({ render, page }) => {
const component = await render(
<AddressEntity
address={{ ...addressMock.contract, implementations: [ { address: addressMock.contract.implementations?.[0].address } ] }}
/>,
);
await component.getByText(/eternal/i).hover();
await expect(page.getByText('Proxy contract')).toBeVisible();
await expect(page).toHaveScreenshot();
});
test('without any name', async({ render, page }) => {
const component = await render(
<AddressEntity
address={{ ...addressMock.contract, name: undefined, implementations: [ { address: addressMock.contract.implementations?.[0].address } ] }}
/>,
);
await component.getByText(addressMock.contract.hash.slice(0, 4)).hover();
await expect(page.getByText('Proxy contract')).toBeVisible();
await expect(page).toHaveScreenshot();
});
test('with multiple implementations', async({ render, page }) => {
const component = await render(
<AddressEntity
address={{ ...addressMock.contract, implementations: implementationsMock.multiple }}
/>,
);
await component.getByText(/eternal/i).hover();
await expect(page.getByText('Proxy contract')).toBeVisible();
await expect(page).toHaveScreenshot();
});
});
test.describe('loading', () => { test.describe('loading', () => {
test('without alias', async({ render }) => { test('without alias', async({ render }) => {
const component = await render( const component = await render(
......
...@@ -11,6 +11,7 @@ import { useAddressHighlightContext } from 'lib/contexts/addressHighlight'; ...@@ -11,6 +11,7 @@ import { useAddressHighlightContext } from 'lib/contexts/addressHighlight';
import * as EntityBase from 'ui/shared/entities/base/components'; import * as EntityBase from 'ui/shared/entities/base/components';
import { getIconProps } from '../base/utils'; import { getIconProps } from '../base/utils';
import AddressEntityContentProxy from './AddressEntityContentProxy';
import AddressIdenticon from './AddressIdenticon'; import AddressIdenticon from './AddressIdenticon';
type LinkProps = EntityBase.LinkBaseProps & Pick<EntityProps, 'address'>; type LinkProps = EntityBase.LinkBaseProps & Pick<EntityProps, 'address'>;
...@@ -57,27 +58,18 @@ const Icon = (props: IconProps) => { ...@@ -57,27 +58,18 @@ const Icon = (props: IconProps) => {
); );
} }
if (props.address.is_verified) { const isProxy = Boolean(props.address.implementations?.length);
return ( const isVerified = isProxy ? props.address.is_verified && props.address.implementations?.every(({ name }) => Boolean(name)) : props.address.is_verified;
<Tooltip label="Verified contract"> const contractIconName: EntityBase.IconBaseProps['name'] = props.address.is_verified ? 'contracts/verified' : 'contracts/regular';
<span> const label = (isVerified ? 'verified ' : '') + (isProxy ? 'proxy contract' : 'contract');
<EntityBase.Icon
{ ...props }
name="contracts/verified"
color="green.500"
borderRadius={ 0 }
/>
</span>
</Tooltip>
);
}
return ( return (
<Tooltip label="Contract"> <Tooltip label={ label.slice(0, 1).toUpperCase() + label.slice(1) }>
<span> <span>
<EntityBase.Icon <EntityBase.Icon
{ ...props } { ...props }
name="contracts/regular" name={ isProxy ? 'contracts/proxy' : contractIconName }
color={ isVerified ? 'green.500' : undefined }
borderRadius={ 0 } borderRadius={ 0 }
/> />
</span> </span>
...@@ -95,12 +87,18 @@ const Icon = (props: IconProps) => { ...@@ -95,12 +87,18 @@ const Icon = (props: IconProps) => {
); );
}; };
type ContentProps = Omit<EntityBase.ContentBaseProps, 'text'> & Pick<EntityProps, 'address'>; export type ContentProps = Omit<EntityBase.ContentBaseProps, 'text'> & Pick<EntityProps, 'address'>;
const Content = chakra((props: ContentProps) => { const Content = chakra((props: ContentProps) => {
const nameTag = props.address.metadata?.tags.find(tag => tag.tagType === 'name')?.name; const nameTag = props.address.metadata?.tags.find(tag => tag.tagType === 'name')?.name;
const nameText = nameTag || props.address.ens_domain_name || props.address.name; const nameText = nameTag || props.address.ens_domain_name || props.address.name;
const isProxy = props.address.implementations && props.address.implementations.length > 0;
if (isProxy) {
return <AddressEntityContentProxy { ...props }/>;
}
if (nameText) { if (nameText) {
const label = ( const label = (
<VStack gap={ 0 } py={ 1 } color="inherit"> <VStack gap={ 0 } py={ 1 } color="inherit">
...@@ -140,15 +138,18 @@ const Copy = (props: CopyProps) => { ...@@ -140,15 +138,18 @@ const Copy = (props: CopyProps) => {
const Container = EntityBase.Container; const Container = EntityBase.Container;
export interface EntityProps extends EntityBase.EntityBaseProps { export interface EntityProps extends EntityBase.EntityBaseProps {
address: Pick<AddressParam, 'hash' | 'name' | 'is_contract' | 'is_verified' | 'ens_domain_name' | 'metadata'>; address: Pick<AddressParam,
'hash' | 'name' | 'is_contract' | 'is_verified' | 'implementations' | 'ens_domain_name' | 'metadata'
>;
isSafeAddress?: boolean; isSafeAddress?: boolean;
noHighlight?: boolean;
} }
const AddressEntry = (props: EntityProps) => { const AddressEntry = (props: EntityProps) => {
const linkProps = _omit(props, [ 'className' ]); const linkProps = _omit(props, [ 'className' ]);
const partsProps = _omit(props, [ 'className', 'onClick' ]); const partsProps = _omit(props, [ 'className', 'onClick' ]);
const context = useAddressHighlightContext(); const context = useAddressHighlightContext(props.noHighlight);
return ( return (
<Container <Container
......
import { Box, DarkMode, Popover, PopoverBody, PopoverContent, PopoverTrigger, Portal, useColorModeValue, Flex, PopoverArrow } from '@chakra-ui/react';
import React from 'react';
import * as EntityBase from 'ui/shared/entities/base/components';
import type { ContentProps } from './AddressEntity';
import AddressEntity from './AddressEntity';
const AddressEntityContentProxy = (props: ContentProps) => {
const bgColor = useColorModeValue('gray.700', 'gray.900');
const implementations = props.address.implementations;
const handleClick = React.useCallback((event: React.MouseEvent) => {
event.stopPropagation();
}, []);
if (!implementations || implementations.length === 0) {
return null;
}
const colNum = Math.min(implementations.length, 3);
const implementationName = implementations.length === 1 && implementations[0].name ? implementations[0].name : undefined;
return (
<Popover trigger="hover" isLazy>
<PopoverTrigger>
<Box display="inline-flex" w="100%">
<EntityBase.Content
{ ...props }
truncation={ implementationName || props.address.name ? 'tail' : 'dynamic' }
text={ implementationName || props.address.name || props.address.hash }
/>
</Box>
</PopoverTrigger>
<Portal>
<DarkMode>
<PopoverContent bgColor={ bgColor } w="fit-content" borderRadius="sm" maxW={{ base: '100vw', lg: '410px' }} onClick={ handleClick }>
<PopoverArrow bgColor={ bgColor }/>
<PopoverBody color="white" p={ 2 } fontSize="sm" lineHeight={ 5 } textAlign="center">
<Box fontWeight={ 600 }>
Proxy contract
{ props.address.name ? ` (${ props.address.name })` : '' }
</Box>
<AddressEntity address={{ hash: props.address.hash }} noLink noIcon noHighlight justifyContent="center"/>
<Box fontWeight={ 600 } mt={ 2 }>
Implementation{ implementations.length > 1 ? 's' : '' }
{ implementationName ? ` (${ implementationName })` : '' }
</Box>
<Flex flexWrap="wrap" columnGap={ 3 }>
{ implementations.map((item) => (
<AddressEntity
key={ item.address }
address={{ hash: item.address }}
noLink
noIcon
noHighlight
minW={ `calc((100% - ${ colNum - 1 } * 12px) / ${ colNum })` }
flex={ 1 }
justifyContent={ colNum === 1 ? 'center' : undefined }
/>
)) }
</Flex>
</PopoverBody>
</PopoverContent>
</DarkMode>
</Portal>
</Popover>
);
};
export default React.memo(AddressEntityContentProxy);
...@@ -142,6 +142,7 @@ const Content = chakra(({ className, isLoading, asProp, text, truncation = 'dyna ...@@ -142,6 +142,7 @@ const Content = chakra(({ className, isLoading, asProp, text, truncation = 'dyna
tailLength={ tailLength } tailLength={ tailLength }
/> />
); );
case 'tail':
case 'none': case 'none':
return <chakra.span as={ asProp }>{ text }</chakra.span>; return <chakra.span as={ asProp }>{ text }</chakra.span>;
} }
...@@ -153,6 +154,7 @@ const Content = chakra(({ className, isLoading, asProp, text, truncation = 'dyna ...@@ -153,6 +154,7 @@ const Content = chakra(({ className, isLoading, asProp, text, truncation = 'dyna
isLoaded={ !isLoading } isLoaded={ !isLoading }
overflow="hidden" overflow="hidden"
whiteSpace="nowrap" whiteSpace="nowrap"
textOverflow={ truncation === 'tail' ? 'ellipsis' : undefined }
> >
{ children } { children }
</Skeleton> </Skeleton>
......
...@@ -27,6 +27,7 @@ const SearchBarSuggestAddress = ({ data, isMobile, searchTerm }: Props) => { ...@@ -27,6 +27,7 @@ const SearchBarSuggestAddress = ({ data, isMobile, searchTerm }: Props) => {
name: '', name: '',
is_verified: data.is_smart_contract_verified, is_verified: data.is_smart_contract_verified,
ens_domain_name: null, ens_domain_name: null,
implementations: null,
}} }}
/> />
); );
......
...@@ -49,6 +49,7 @@ const TokensTableItem = ({ ...@@ -49,6 +49,7 @@ const TokensTableItem = ({
is_contract: true, is_contract: true,
is_verified: false, is_verified: false,
ens_domain_name: null, ens_domain_name: null,
implementations: null,
}; };
return ( return (
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment