mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
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
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -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;
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
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 },
|
||||
];
|
||||
Vendored
+1
@@ -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
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
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);
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user