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
);
}
export function useAddressHighlightContext() {
export function useAddressHighlightContext(disabled?: boolean) {
const context = React.useContext(AddressHighlightContext);
if (context === undefined) {
if (context === undefined || disabled) {
return null;
}
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 @@
| "clock"
| "coins/bitcoin"
| "collection"
| "contracts/proxy"
| "contracts/regular_many"
| "contracts/regular"
| "contracts/verified_many"
......
......@@ -24,9 +24,7 @@ export interface UserTags {
export type AddressParamBasic = {
hash: string;
// API doesn't return hash in this model yet
// will be fixed in the future releases
implementations: Array<Omit<AddressImplementation, 'address'>> | null;
implementations: Array<AddressImplementation> | null;
name: string | null;
is_contract: boolean;
is_verified: boolean | null;
......
......@@ -67,7 +67,7 @@ const OptimisticDepositsListItem = ({ item, isLoading }: Props) => {
<ListItemMobileGrid.Label isLoading={ isLoading }>L1 txn origin</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<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 }
noCopy
truncation="constant"
......
......@@ -59,7 +59,7 @@ const OptimisticDepositsTableItem = ({ item, isLoading }: Props) => {
</Td>
<Td verticalAlign="middle">
<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 }
truncation="constant"
noCopy
......
......@@ -272,7 +272,7 @@ const AddressPageContent = () => {
/>
) }
<AddressEntity
address={{ ...addressQuery.data, hash, name: '', ens_domain_name: '' }}
address={{ ...addressQuery.data, hash, name: '', ens_domain_name: '', implementations: null }}
isLoading={ isLoading }
fontFamily="heading"
fontSize="lg"
......
......@@ -3,6 +3,7 @@ import React from 'react';
import { AddressHighlightProvider } from 'lib/contexts/addressHighlight';
import * as addressMock from 'mocks/address/address';
import * as implementationsMock from 'mocks/address/implementations';
import { test, expect } from 'playwright/lib';
import AddressEntity from './AddressEntity';
......@@ -30,7 +31,7 @@ test.describe('contract', () => {
test('unverified', async({ render, page }) => {
const component = await render(
<AddressEntity
address={{ ...addressMock.contract, is_verified: false }}
address={{ ...addressMock.contract, is_verified: false, implementations: null }}
/>,
);
......@@ -41,7 +42,7 @@ test.describe('contract', () => {
test('verified', async({ render }) => {
const component = await render(
<AddressEntity
address={{ ...addressMock.contract, is_verified: true }}
address={{ ...addressMock.contract, is_verified: true, implementations: null }}
/>,
);
......@@ -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('without alias', async({ render }) => {
const component = await render(
......
......@@ -11,6 +11,7 @@ import { useAddressHighlightContext } from 'lib/contexts/addressHighlight';
import * as EntityBase from 'ui/shared/entities/base/components';
import { getIconProps } from '../base/utils';
import AddressEntityContentProxy from './AddressEntityContentProxy';
import AddressIdenticon from './AddressIdenticon';
type LinkProps = EntityBase.LinkBaseProps & Pick<EntityProps, 'address'>;
......@@ -57,27 +58,18 @@ const Icon = (props: IconProps) => {
);
}
if (props.address.is_verified) {
return (
<Tooltip label="Verified contract">
<span>
<EntityBase.Icon
{ ...props }
name="contracts/verified"
color="green.500"
borderRadius={ 0 }
/>
</span>
</Tooltip>
);
}
const isProxy = Boolean(props.address.implementations?.length);
const isVerified = isProxy ? props.address.is_verified && props.address.implementations?.every(({ name }) => Boolean(name)) : props.address.is_verified;
const contractIconName: EntityBase.IconBaseProps['name'] = props.address.is_verified ? 'contracts/verified' : 'contracts/regular';
const label = (isVerified ? 'verified ' : '') + (isProxy ? 'proxy contract' : 'contract');
return (
<Tooltip label="Contract">
<Tooltip label={ label.slice(0, 1).toUpperCase() + label.slice(1) }>
<span>
<EntityBase.Icon
{ ...props }
name="contracts/regular"
name={ isProxy ? 'contracts/proxy' : contractIconName }
color={ isVerified ? 'green.500' : undefined }
borderRadius={ 0 }
/>
</span>
......@@ -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 nameTag = props.address.metadata?.tags.find(tag => tag.tagType === 'name')?.name;
const nameText = nameTag || props.address.ens_domain_name || props.address.name;
const isProxy = props.address.implementations && props.address.implementations.length > 0;
if (isProxy) {
return <AddressEntityContentProxy { ...props }/>;
}
if (nameText) {
const label = (
<VStack gap={ 0 } py={ 1 } color="inherit">
......@@ -140,15 +138,18 @@ const Copy = (props: CopyProps) => {
const Container = EntityBase.Container;
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;
noHighlight?: boolean;
}
const AddressEntry = (props: EntityProps) => {
const linkProps = _omit(props, [ 'className' ]);
const partsProps = _omit(props, [ 'className', 'onClick' ]);
const context = useAddressHighlightContext();
const context = useAddressHighlightContext(props.noHighlight);
return (
<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
tailLength={ tailLength }
/>
);
case 'tail':
case 'none':
return <chakra.span as={ asProp }>{ text }</chakra.span>;
}
......@@ -153,6 +154,7 @@ const Content = chakra(({ className, isLoading, asProp, text, truncation = 'dyna
isLoaded={ !isLoading }
overflow="hidden"
whiteSpace="nowrap"
textOverflow={ truncation === 'tail' ? 'ellipsis' : undefined }
>
{ children }
</Skeleton>
......
......@@ -27,6 +27,7 @@ const SearchBarSuggestAddress = ({ data, isMobile, searchTerm }: Props) => {
name: '',
is_verified: data.is_smart_contract_verified,
ens_domain_name: null,
implementations: null,
}}
/>
);
......
......@@ -49,6 +49,7 @@ const TokensTableItem = ({
is_contract: true,
is_verified: false,
ens_domain_name: null,
implementations: null,
};
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