{
{
/>
) }
{
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 (
|