diff --git a/icons/contracts/proxy.svg b/icons/contracts/proxy.svg new file mode 100644 index 0000000000..1b75cb210f --- /dev/null +++ b/icons/contracts/proxy.svg @@ -0,0 +1,4 @@ + + + + diff --git a/lib/contexts/addressHighlight.tsx b/lib/contexts/addressHighlight.tsx index 84f5f896ec..085e676ecc 100644 --- a/lib/contexts/addressHighlight.tsx +++ b/lib/contexts/addressHighlight.tsx @@ -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; diff --git a/mocks/address/implementations.ts b/mocks/address/implementations.ts new file mode 100644 index 0000000000..1d77032284 --- /dev/null +++ b/mocks/address/implementations.ts @@ -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 }, +]; diff --git a/public/icons/name.d.ts b/public/icons/name.d.ts index 56d14b31f4..2e73c2878f 100644 --- a/public/icons/name.d.ts +++ b/public/icons/name.d.ts @@ -31,6 +31,7 @@ | "clock" | "coins/bitcoin" | "collection" + | "contracts/proxy" | "contracts/regular_many" | "contracts/regular" | "contracts/verified_many" diff --git a/types/api/addressParams.ts b/types/api/addressParams.ts index 1cb4bdb577..b7bc4393b8 100644 --- a/types/api/addressParams.ts +++ b/types/api/addressParams.ts @@ -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> | null; + implementations: Array | null; name: string | null; is_contract: boolean; is_verified: boolean | null; diff --git a/ui/deposits/optimisticL2/OptimisticDepositsListItem.tsx b/ui/deposits/optimisticL2/OptimisticDepositsListItem.tsx index fc06a6944f..fe8abd7a0a 100644 --- a/ui/deposits/optimisticL2/OptimisticDepositsListItem.tsx +++ b/ui/deposits/optimisticL2/OptimisticDepositsListItem.tsx @@ -67,7 +67,7 @@ const OptimisticDepositsListItem = ({ item, isLoading }: Props) => { L1 txn origin { { /> ) } { test('unverified', async({ render, page }) => { const component = await render( , ); @@ -41,7 +42,7 @@ test.describe('contract', () => { test('verified', async({ render }) => { const component = await render( , ); @@ -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( + , + ); + + 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( + , + ); + + 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( + , + ); + + 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( + , + ); + + 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( diff --git a/ui/shared/entities/address/AddressEntity.tsx b/ui/shared/entities/address/AddressEntity.tsx index 88e30e4a03..1519571bb8 100644 --- a/ui/shared/entities/address/AddressEntity.tsx +++ b/ui/shared/entities/address/AddressEntity.tsx @@ -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; @@ -57,27 +58,18 @@ const Icon = (props: IconProps) => { ); } - if (props.address.is_verified) { - return ( - - - - - - ); - } + 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 ( - + @@ -95,12 +87,18 @@ const Icon = (props: IconProps) => { ); }; -type ContentProps = Omit & Pick; +export type ContentProps = Omit & Pick; 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 ; + } + if (nameText) { const label = ( @@ -140,15 +138,18 @@ const Copy = (props: CopyProps) => { const Container = EntityBase.Container; export interface EntityProps extends EntityBase.EntityBaseProps { - address: Pick; + address: Pick; 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 ( { + 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 ( + + + + + + + + + + + + + Proxy contract + { props.address.name ? ` (${ props.address.name })` : '' } + + + + Implementation{ implementations.length > 1 ? 's' : '' } + { implementationName ? ` (${ implementationName })` : '' } + + + { implementations.map((item) => ( + + )) } + + + + + + + ); +}; + +export default React.memo(AddressEntityContentProxy); diff --git a/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_proxy-contract-with-implementation-name-1.png b/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_proxy-contract-with-implementation-name-1.png new file mode 100644 index 0000000000..e5eee1c270 Binary files /dev/null and b/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_proxy-contract-with-implementation-name-1.png differ diff --git a/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_proxy-contract-with-multiple-implementations-1.png b/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_proxy-contract-with-multiple-implementations-1.png new file mode 100644 index 0000000000..0b4b672cf7 Binary files /dev/null and b/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_proxy-contract-with-multiple-implementations-1.png differ diff --git a/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_proxy-contract-without-any-name-1.png b/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_proxy-contract-without-any-name-1.png new file mode 100644 index 0000000000..e4b4af7bbd Binary files /dev/null and b/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_proxy-contract-without-any-name-1.png differ diff --git a/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_proxy-contract-without-implementation-name-1.png b/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_proxy-contract-without-implementation-name-1.png new file mode 100644 index 0000000000..cb310563fe Binary files /dev/null and b/ui/shared/entities/address/__screenshots__/AddressEntity.pw.tsx_default_proxy-contract-without-implementation-name-1.png differ diff --git a/ui/shared/entities/base/components.tsx b/ui/shared/entities/base/components.tsx index 5d43e8b3c4..f86ec7a798 100644 --- a/ui/shared/entities/base/components.tsx +++ b/ui/shared/entities/base/components.tsx @@ -142,6 +142,7 @@ const Content = chakra(({ className, isLoading, asProp, text, truncation = 'dyna tailLength={ tailLength } /> ); + case 'tail': case 'none': return { text }; } @@ -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 } diff --git a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestAddress.tsx b/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestAddress.tsx index 791b0eee78..179e5691be 100644 --- a/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestAddress.tsx +++ b/ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestAddress.tsx @@ -27,6 +27,7 @@ const SearchBarSuggestAddress = ({ data, isMobile, searchTerm }: Props) => { name: '', is_verified: data.is_smart_contract_verified, ens_domain_name: null, + implementations: null, }} /> ); diff --git a/ui/tokens/TokensTableItem.tsx b/ui/tokens/TokensTableItem.tsx index 2c3234dc40..837433d5ad 100644 --- a/ui/tokens/TokensTableItem.tsx +++ b/ui/tokens/TokensTableItem.tsx @@ -49,6 +49,7 @@ const TokensTableItem = ({ is_contract: true, is_verified: false, ens_domain_name: null, + implementations: null, }; return (