Skip to content

Commit

Permalink
Public tags: display name tags for addresses (blockscout#1877)
Browse files Browse the repository at this point in the history
* refactor EntityTags component to work with metadata API format

* hook for API metadata info query

* make EntityTag component

* make EntityTag component

* display custom tag colors and sort tags by ordinal field

* add tag popover

* refactoring

* display name tag in the lists

* add mixpanel event and disable link for protocol and generic tags

* adjust demo ENVs

* tests

* fix tests

* change actionURL to tagUrl
  • Loading branch information
tom2drum authored and DaMandal0rian committed May 6, 2024
1 parent 88cc2b1 commit bcd5e13
Show file tree
Hide file tree
Showing 44 changed files with 565 additions and 233 deletions.
2 changes: 1 addition & 1 deletion deploy/values/review/values.yaml.gotmpl
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ frontend:
NEXT_PUBLIC_NETWORK_LOGO: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/sepolia.svg
NEXT_PUBLIC_NETWORK_ICON: https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/sepolia.png
NEXT_PUBLIC_API_HOST: eth-sepolia.k8s-dev.blockscout.com
NEXT_PUBLIC_STATS_API_HOST: https://stats-goerli.k8s-dev.blockscout.com/
NEXT_PUBLIC_STATS_API_HOST: https://stats-sepolia.k8s-dev.blockscout.com/
NEXT_PUBLIC_VISUALIZE_API_HOST: http://visualizer-svc.visualizer-testing.svc.cluster.local/
NEXT_PUBLIC_CONTRACT_INFO_API_HOST: https://contracts-info-test.k8s-dev.blockscout.com
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST: https://admin-rs-test.k8s-dev.blockscout.com
Expand Down
24 changes: 15 additions & 9 deletions lib/address/parseMetaPayload.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { AddressMetadataTag } from 'types/api/addressMetadata';
import type { AddressMetadataTagFormatted } from 'types/client/addressMetadata';

type MetaParsed = NonNullable<AddressMetadataTagFormatted['meta']>;

export default function parseMetaPayload(meta: AddressMetadataTag['meta']): AddressMetadataTagFormatted['meta'] {
try {
const parsedMeta = JSON.parse(meta || '');
Expand All @@ -11,16 +13,20 @@ export default function parseMetaPayload(meta: AddressMetadataTag['meta']): Addr

const result: AddressMetadataTagFormatted['meta'] = {};

if ('textColor' in parsedMeta && typeof parsedMeta.textColor === 'string') {
result.textColor = parsedMeta.textColor;
}

if ('bgColor' in parsedMeta && typeof parsedMeta.bgColor === 'string') {
result.bgColor = parsedMeta.bgColor;
}
const stringFields: Array<keyof MetaParsed> = [
'textColor',
'bgColor',
'tagUrl',
'tooltipIcon',
'tooltipTitle',
'tooltipDescription',
'tooltipUrl',
];

if ('actionURL' in parsedMeta && typeof parsedMeta.actionURL === 'string') {
result.actionURL = parsedMeta.actionURL;
for (const stringField of stringFields) {
if (stringField in parsedMeta && typeof parsedMeta[stringField as keyof typeof parsedMeta] === 'string') {
result[stringField] = parsedMeta[stringField as keyof typeof parsedMeta];
}
}

return result;
Expand Down
9 changes: 9 additions & 0 deletions lib/makePrettyLink.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export default function makePrettyLink(url: string | undefined): { url: string; domain: string } | undefined {
try {
const urlObj = new URL(url ?? '');
return {
url: urlObj.href,
domain: urlObj.hostname,
};
} catch (error) {}
}
4 changes: 4 additions & 0 deletions lib/mixpanel/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ Type extends EventTypes.PAGE_WIDGET ? (
} | {
'Type': 'Security score';
'Source': 'Analyzed contracts popup';
} | {
'Type': 'Address tag';
'Info': string;
'URL': string;
}
) :
Type extends EventTypes.TX_INTERPRETATION_INTERACTION ? {
Expand Down
18 changes: 18 additions & 0 deletions mocks/address/address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,24 @@ export const withEns: AddressParam = {
ens_domain_name: 'kitty.kitty.kitty.cat.eth',
};

export const withNameTag: AddressParam = {
hash: hash,
implementation_name: null,
is_contract: false,
is_verified: null,
name: 'ArianeeStore',
private_tags: [],
watchlist_names: [],
public_tags: [],
ens_domain_name: 'kitty.kitty.kitty.cat.eth',
metadata: {
reputation: null,
tags: [
{ tagType: 'name', name: 'Mrs. Duckie', slug: 'mrs-duckie', ordinal: 0, meta: null },
],
},
};

export const withoutName: AddressParam = {
hash: hash,
implementation_name: null,
Expand Down
71 changes: 49 additions & 22 deletions mocks/metadata/address.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,63 @@
import type { AddressMetadataInfo, AddressMetadataTag } from 'types/api/addressMetadata';
/* eslint-disable max-len */
import type { AddressMetadataTagApi } from 'types/api/addressMetadata';

import { hash } from '../address/address';

export const nameTag1: AddressMetadataTag = {
slug: 'ethermineru',
name: 'Ethermine.ru',
export const nameTag: AddressMetadataTagApi = {
slug: 'quack-quack',
name: 'Quack quack',
tagType: 'name',
ordinal: 0,
ordinal: 99,
meta: null,
};

export const genericTag1: AddressMetadataTag = {
slug: 'ethermine.ru',
name: 'Ethermine.ru',
export const customNameTag: AddressMetadataTagApi = {
slug: 'unicorn-uproar',
name: 'Unicorn Uproar',
tagType: 'name',
ordinal: 777,
meta: {
tagUrl: 'https://example.com',
bgColor: 'linear-gradient(45deg, deeppink, deepskyblue)',
textColor: '#FFFFFF',
},
};

export const genericTag: AddressMetadataTagApi = {
slug: 'duck-owner',
name: 'duck owner 🦆',
tagType: 'generic',
ordinal: 0,
meta: null,
ordinal: 55,
meta: {
bgColor: 'rgba(255,243,12,90%)',
},
};

export const protocolTag1: AddressMetadataTag = {
export const infoTagWithLink: AddressMetadataTagApi = {
slug: 'goosegang',
name: 'GooseGanG GooseGanG GooseGanG GooseGanG GooseGanG GooseGanG GooseGanG',
tagType: 'classifier',
ordinal: 11,
meta: {
tagUrl: 'https://example.com',
},
};

export const tagWithTooltip: AddressMetadataTagApi = {
slug: 'blockscout-heroes',
name: 'BlockscoutHeroes',
tagType: 'classifier',
ordinal: 42,
meta: {
tooltipDescription: 'The Blockscout team, EVM blockchain aficionados, illuminate Ethereum networks with unparalleled insight and prowess, leading the way in blockchain exploration! 🚀🔎',
tooltipIcon: 'https://localhost:3100/icon.svg',
tooltipTitle: 'Blockscout team member',
tooltipUrl: 'https://blockscout.com',
},
};

export const protocolTag: AddressMetadataTagApi = {
slug: 'aerodrome',
name: 'Aerodrome',
tagType: 'protocol',
ordinal: 0,
meta: null,
};

export const baseInfo: AddressMetadataInfo = {
addresses: {
[hash]: {
tags: [ nameTag1, genericTag1, protocolTag1 ],
reputation: null,
},
},
};
14 changes: 14 additions & 0 deletions types/api/addressMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,24 @@ export interface AddressMetadataInfo {

export type AddressMetadataTagType = 'name' | 'generic' | 'classifier' | 'information' | 'note' | 'protocol';

// Response model from Metadata microservice API
export interface AddressMetadataTag {
slug: string;
name: string;
tagType: AddressMetadataTagType;
ordinal: number;
meta: string | null;
}

// Response model from Blockscout API with parsed meta field
export interface AddressMetadataTagApi extends Omit<AddressMetadataTag, 'meta'> {
meta: {
textColor?: string;
bgColor?: string;
tagUrl?: string;
tooltipIcon?: string;
tooltipTitle?: string;
tooltipDescription?: string;
tooltipUrl?: string;
} | null;
}
6 changes: 6 additions & 0 deletions types/api/addressParams.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { AddressMetadataTagApi } from './addressMetadata';

export interface AddressTag {
label: string;
display_name: string;
Expand All @@ -22,6 +24,10 @@ export type AddressParamBasic = {
is_contract: boolean;
is_verified: boolean | null;
ens_domain_name: string | null;
metadata?: {
reputation: number | null;
tags: Array<AddressMetadataTagApi>;
} | null;
}

export type AddressParam = UserTags & AddressParamBasic;
14 changes: 2 additions & 12 deletions types/client/addressMetadata.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { AddressMetadataTagType } from 'types/api/addressMetadata';
import type { AddressMetadataTagApi } from 'types/api/addressMetadata';

export interface AddressMetadataInfoFormatted {
addresses: Record<string, {
Expand All @@ -7,14 +7,4 @@ export interface AddressMetadataInfoFormatted {
}>;
}

export interface AddressMetadataTagFormatted {
slug: string;
name: string;
tagType: AddressMetadataTagType;
ordinal: number;
meta: {
textColor?: string;
bgColor?: string;
actionURL?: string;
} | null;
}
export type AddressMetadataTagFormatted = AddressMetadataTagApi;
42 changes: 29 additions & 13 deletions ui/pages/Address.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import { Box, Flex, HStack, useColorModeValue } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';

import type { EntityTag } from 'ui/shared/EntityTags/types';
import type { RoutedTab } from 'ui/shared/Tabs/types';

import config from 'configs/app';
import useAddressMetadataInfoQuery from 'lib/address/useAddressMetadataInfoQuery';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/contexts/app';
import useContractTabs from 'lib/hooks/useContractTabs';
Expand Down Expand Up @@ -36,7 +38,9 @@ import AccountActionsMenu from 'ui/shared/AccountActionsMenu/AccountActionsMenu'
import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import EnsEntity from 'ui/shared/entities/ens/EnsEntity';
import EntityTags from 'ui/shared/EntityTags';
import EntityTags from 'ui/shared/EntityTags/EntityTags';
import formatUserTags from 'ui/shared/EntityTags/formatUserTags';
import sortEntityTags from 'ui/shared/EntityTags/sortEntityTags';
import IconSvg from 'ui/shared/IconSvg';
import NetworkExplorers from 'ui/shared/NetworkExplorers';
import PageTitle from 'ui/shared/Page/PageTitle';
Expand Down Expand Up @@ -71,6 +75,9 @@ const AddressPageContent = () => {
},
});

const addressesForMetadataQuery = React.useMemo(() => ([ hash ].filter(Boolean)), [ hash ]);
const addressMetadataQuery = useAddressMetadataInfoQuery(addressesForMetadataQuery);

const isLoading = addressQuery.isPlaceholderData || (config.features.userOps.isEnabled && userOpsAccountQuery.isPlaceholderData);
const isTabsLoading = isLoading || addressTabsCountersQuery.isPlaceholderData;

Expand Down Expand Up @@ -185,18 +192,27 @@ const AddressPageContent = () => {
].filter(Boolean);
}, [ addressQuery.data, contractTabs, addressTabsCountersQuery.data, userOpsAccountQuery.data, isTabsLoading ]);

const tags = (
const tags: Array<EntityTag> = React.useMemo(() => {
return [
!addressQuery.data?.is_contract ? { slug: 'eoa', name: 'EOA', tagType: 'custom' as const, ordinal: -1 } : undefined,
config.features.validators.isEnabled && addressQuery.data?.has_validated_blocks ?
{ slug: 'validator', name: 'Validator', tagType: 'custom' as const, ordinal: 10 } :
undefined,
addressQuery.data?.implementation_address ? { slug: 'proxy', name: 'Proxy', tagType: 'custom' as const, ordinal: -1 } : undefined,
addressQuery.data?.token ? { slug: 'token', name: 'Token', tagType: 'custom' as const, ordinal: -1 } : undefined,
isSafeAddress ? { slug: 'safe', name: 'Multisig: Safe', tagType: 'custom' as const, ordinal: -10 } : undefined,
config.features.userOps.isEnabled && userOpsAccountQuery.data ?
{ slug: 'user_ops_acc', name: 'Smart contract wallet', tagType: 'custom' as const, ordinal: -10 } :
undefined,
...formatUserTags(addressQuery.data),
...(addressMetadataQuery.data?.addresses?.[hash.toLowerCase()]?.tags || []),
].filter(Boolean).sort(sortEntityTags);
}, [ addressMetadataQuery.data, addressQuery.data, hash, isSafeAddress, userOpsAccountQuery.data ]);

const titleContentAfter = (
<EntityTags
data={ addressQuery.data }
isLoading={ isLoading }
tagsBefore={ [
!addressQuery.data?.is_contract ? { label: 'eoa', display_name: 'EOA' } : undefined,
config.features.validators.isEnabled && addressQuery.data?.has_validated_blocks ? { label: 'validator', display_name: 'Validator' } : undefined,
addressQuery.data?.implementation_address ? { label: 'proxy', display_name: 'Proxy' } : undefined,
addressQuery.data?.token ? { label: 'token', display_name: 'Token' } : undefined,
isSafeAddress ? { label: 'safe', display_name: 'Multisig: Safe' } : undefined,
config.features.userOps.isEnabled && userOpsAccountQuery.data ? { label: 'user_ops_acc', display_name: 'Smart contract wallet' } : undefined,
] }
tags={ tags }
isLoading={ isLoading || (config.features.addressMetadata.isEnabled && addressMetadataQuery.isPending) }
/>
);

Expand Down Expand Up @@ -260,7 +276,7 @@ const AddressPageContent = () => {
<PageTitle
title={ `${ addressQuery.data?.is_contract ? 'Contract' : 'Address' } details` }
backLink={ backLink }
contentAfter={ tags }
contentAfter={ titleContentAfter }
secondRow={ titleSecondRow }
isLoading={ isLoading }
/>
Expand Down
2 changes: 1 addition & 1 deletion ui/pages/Token.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ const TokenPageContent = () => {
<>
<TextAd mb={ 6 }/>

<TokenPageTitle tokenQuery={ tokenQuery } addressQuery={ addressQuery }/>
<TokenPageTitle tokenQuery={ tokenQuery } addressQuery={ addressQuery } hash={ hashString }/>

<TokenDetails tokenQuery={ tokenQuery }/>

Expand Down
4 changes: 2 additions & 2 deletions ui/pages/Transaction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import getQueryParamString from 'lib/router/getQueryParamString';
import { publicClient } from 'lib/web3/client';
import isCustomAppError from 'ui/shared/AppError/isCustomAppError';
import EntityTags from 'ui/shared/EntityTags';
import EntityTags from 'ui/shared/EntityTags/EntityTags';
import PageTitle from 'ui/shared/Page/PageTitle';
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton';
Expand Down Expand Up @@ -76,7 +76,7 @@ const TransactionPageContent = () => {
const tags = (
<EntityTags
isLoading={ isPlaceholderData }
tagsBefore={ [ data?.tx_tag ? { label: data.tx_tag, display_name: data.tx_tag } : undefined ] }
tags={ data?.tx_tag ? [ { slug: data.tx_tag, name: data.tx_tag, tagType: 'private_tag' as const } ] : [] }
/>
);

Expand Down
Binary file modified ui/pages/__screenshots__/Token.pw.tsx_default_bridged-token-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit bcd5e13

Please sign in to comment.