Commit 5feb5619 authored by tom goriunov's avatar tom goriunov Committed by GitHub

Metadata: send address to fetchers once visited (#2814)

* Metadata: send address to fetchers once visited

Fixes #2723

* fix ts
parent 2b2332c4
import type { Feature } from './types';
import apis from '../apis';
import { getEnvValue } from '../utils';
const title = 'Address metadata';
const config: Feature<{}> = (() => {
const config: Feature<{ isAddressTagsUpdateEnabled: boolean }> = (() => {
if (apis.metadata) {
return Object.freeze({
title,
isEnabled: true,
isAddressTagsUpdateEnabled: getEnvValue('NEXT_PUBLIC_METADATA_ADDRESS_TAGS_UPDATE_ENABLED') !== 'false',
});
}
......
......@@ -657,6 +657,25 @@ const bridgedTokensSchema = yup
}),
});
const addressMetadataSchema = yup
.object()
.shape({
NEXT_PUBLIC_METADATA_SERVICE_API_HOST: yup
.string()
.test(urlTest),
NEXT_PUBLIC_METADATA_ADDRESS_TAGS_UPDATE_ENABLED: yup
.boolean()
.when('NEXT_PUBLIC_METADATA_SERVICE_API_HOST', {
is: (value: string) => Boolean(value),
then: (schema) => schema,
otherwise: (schema) => schema.test(
'not-exist',
'NEXT_PUBLIC_METADATA_ADDRESS_TAGS_UPDATE_ENABLED cannot not be used if NEXT_PUBLIC_METADATA_SERVICE_API_HOST is not defined',
value => value === undefined,
),
}),
});
const deFiDropdownItemSchema: yup.ObjectSchema<DeFiDropdownItem> = yup
.object({
text: yup.string().required(),
......@@ -943,7 +962,6 @@ const schema = yup
NEXT_PUBLIC_VISUALIZE_API_BASE_PATH: yup.string(),
NEXT_PUBLIC_CONTRACT_INFO_API_HOST: yup.string().test(urlTest),
NEXT_PUBLIC_NAME_SERVICE_API_HOST: yup.string().test(urlTest),
NEXT_PUBLIC_METADATA_SERVICE_API_HOST: yup.string().test(urlTest),
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST: yup.string().test(urlTest),
NEXT_PUBLIC_GRAPHIQL_TRANSACTION: yup
.mixed()
......@@ -1098,6 +1116,7 @@ const schema = yup
.concat(beaconChainSchema)
.concat(bridgedTokensSchema)
.concat(sentrySchema)
.concat(tacSchema);
.concat(tacSchema)
.concat(addressMetadataSchema);
export default schema;
......@@ -7,3 +7,5 @@ NEXT_PUBLIC_VIEWS_ADDRESS_BECH_32_PREFIX=foo
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx
NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY=deprecated
NEXT_PUBLIC_NETWORK_RPC_URL=['https://example.com','https://example2.com']
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://example.com
NEXT_PUBLIC_METADATA_ADDRESS_TAGS_UPDATE_ENABLED=false
\ No newline at end of file
......@@ -657,6 +657,7 @@ This feature allows name tags and other public tags for addresses.
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
| --- | --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_METADATA_SERVICE_API_HOST | `string` | Metadata Service API endpoint url | Required | - | `https://metadata.services.blockscout.com` | v1.30.0+ |
| NEXT_PUBLIC_METADATA_ADDRESS_TAGS_UPDATE_ENABLED | `boolean` | Enables requests to the Metadata Service to schedule an update for address tags after the user visits the address page in the app. | - | `true` | `false` | v2.2.0+ |
&nbsp;
......
import React from 'react';
import type { AddressCounters } from 'types/api/address';
import config from 'configs/app';
import useApiFetch from 'lib/api/useApiFetch';
const feature = config.features.addressMetadata;
interface Params {
address: string | undefined;
counters: AddressCounters | undefined;
isEnabled: boolean;
}
const TXS_THRESHOLD = 500;
export default function useAddressMetadataInitUpdate({ address, counters, isEnabled }: Params) {
const apiFetch = useApiFetch();
React.useEffect(() => {
if (
feature.isEnabled &&
feature.isAddressTagsUpdateEnabled &&
address &&
isEnabled &&
counters?.transactions_count && Number(counters.transactions_count) > TXS_THRESHOLD
) {
apiFetch('metadata:address_submit', {
fetchParams: {
method: 'POST',
body: {
addresses: [ address ],
},
},
});
}
}, [ address, apiFetch, counters?.transactions_count, isEnabled ]);
}
......@@ -11,6 +11,9 @@ export const METADATA_API_RESOURCES = {
public_tag_types: {
path: '/api/v1/public-tag-types',
},
address_submit: {
path: '/api/v1/addresses\\:submit',
},
} satisfies Record<string, ApiResource>;
export type MetadataApiResourceName = `metadata:${ keyof typeof METADATA_API_RESOURCES }`;
......
......@@ -8,6 +8,7 @@ import * as pwConfig from 'playwright/utils/config';
import AddressDetails from './AddressDetails';
import MockAddressPage from './testUtils/MockAddressPage';
import type { AddressCountersQuery } from './utils/useAddressCountersQuery';
import type { AddressQuery } from './utils/useAddressQuery';
const ADDRESS_HASH = addressMock.hash;
......@@ -22,9 +23,14 @@ test.describe('mobile', () => {
test('contract', async({ render, mockApiResponse, page }) => {
await mockApiResponse('general:address', addressMock.contract, { pathParams: { hash: ADDRESS_HASH } });
await mockApiResponse('general:address_counters', countersMock.forContract, { pathParams: { hash: ADDRESS_HASH } });
const component = await render(<AddressDetails addressQuery={{ data: addressMock.contract } as AddressQuery}/>, { hooksConfig });
const component = await render(
<AddressDetails
addressQuery={{ data: addressMock.contract } as AddressQuery}
countersQuery={{ data: countersMock.forContract } as AddressCountersQuery}
/>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot({
mask: [ page.locator(pwConfig.adsBannerSelector) ],
......@@ -34,9 +40,14 @@ test.describe('mobile', () => {
test('validator', async({ render, page, mockApiResponse }) => {
await mockApiResponse('general:address', addressMock.validator, { pathParams: { hash: ADDRESS_HASH } });
await mockApiResponse('general:address_counters', countersMock.forValidator, { pathParams: { hash: ADDRESS_HASH } });
const component = await render(<AddressDetails addressQuery={{ data: addressMock.validator } as AddressQuery}/>, { hooksConfig });
const component = await render(
<AddressDetails
addressQuery={{ data: addressMock.validator } as AddressQuery}
countersQuery={{ data: countersMock.forValidator } as AddressCountersQuery}
/>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot({
mask: [ page.locator(pwConfig.adsBannerSelector) ],
......@@ -46,9 +57,14 @@ test.describe('mobile', () => {
test('filecoin', async({ render, mockApiResponse, page }) => {
await mockApiResponse('general:address', addressMock.filecoin, { pathParams: { hash: ADDRESS_HASH } });
await mockApiResponse('general:address_counters', countersMock.forValidator, { pathParams: { hash: ADDRESS_HASH } });
const component = await render(<AddressDetails addressQuery={{ data: addressMock.filecoin } as AddressQuery}/>, { hooksConfig });
const component = await render(
<AddressDetails
addressQuery={{ data: addressMock.filecoin } as AddressQuery}
countersQuery={{ data: countersMock.forValidator } as AddressCountersQuery}
/>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot({
mask: [ page.locator(pwConfig.adsBannerSelector) ],
......@@ -59,9 +75,14 @@ test.describe('mobile', () => {
test('contract', async({ render, page, mockApiResponse }) => {
await mockApiResponse('general:address', addressMock.contract, { pathParams: { hash: ADDRESS_HASH } });
await mockApiResponse('general:address_counters', countersMock.forContract, { pathParams: { hash: ADDRESS_HASH } });
const component = await render(<AddressDetails addressQuery={{ data: addressMock.contract } as AddressQuery}/>, { hooksConfig });
const component = await render(
<AddressDetails
addressQuery={{ data: addressMock.contract } as AddressQuery}
countersQuery={{ data: countersMock.forContract } as AddressCountersQuery}
/>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot({
mask: [ page.locator(pwConfig.adsBannerSelector) ],
......@@ -72,7 +93,6 @@ test('contract', async({ render, page, mockApiResponse }) => {
// there's an unexpected timeout occurred in this test
test.fixme('token', async({ render, mockApiResponse, injectMetaMaskProvider, page }) => {
await mockApiResponse('general:address', addressMock.token, { pathParams: { hash: ADDRESS_HASH } });
await mockApiResponse('general:address_counters', countersMock.forToken, { pathParams: { hash: ADDRESS_HASH } });
await mockApiResponse('general:address_tokens', tokensMock.erc20List, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-20' }, times: 1 });
await mockApiResponse('general:address_tokens', tokensMock.erc721List, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-721' }, times: 1 });
await mockApiResponse('general:address_tokens', tokensMock.erc1155List, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-1155' }, times: 1 });
......@@ -81,7 +101,7 @@ test.fixme('token', async({ render, mockApiResponse, injectMetaMaskProvider, pag
const component = await render(
<MockAddressPage>
<AddressDetails addressQuery={{ data: addressMock.token } as AddressQuery}/>
<AddressDetails addressQuery={{ data: addressMock.token } as AddressQuery} countersQuery={{ data: countersMock.forToken } as AddressCountersQuery}/>
</MockAddressPage>,
{ hooksConfig },
);
......@@ -94,9 +114,14 @@ test.fixme('token', async({ render, mockApiResponse, injectMetaMaskProvider, pag
test('validator', async({ render, mockApiResponse, page }) => {
await mockApiResponse('general:address', addressMock.validator, { pathParams: { hash: ADDRESS_HASH } });
await mockApiResponse('general:address_counters', countersMock.forValidator, { pathParams: { hash: ADDRESS_HASH } });
const component = await render(<AddressDetails addressQuery={{ data: addressMock.validator } as AddressQuery}/>, { hooksConfig });
const component = await render(
<AddressDetails
addressQuery={{ data: addressMock.validator } as AddressQuery}
countersQuery={{ data: countersMock.forValidator } as AddressCountersQuery}
/>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot({
mask: [ page.locator(pwConfig.adsBannerSelector) ],
......@@ -106,9 +131,14 @@ test('validator', async({ render, mockApiResponse, page }) => {
test('filecoin', async({ render, mockApiResponse, page }) => {
await mockApiResponse('general:address', addressMock.filecoin, { pathParams: { hash: ADDRESS_HASH } });
await mockApiResponse('general:address_counters', countersMock.forValidator, { pathParams: { hash: ADDRESS_HASH } });
const component = await render(<AddressDetails addressQuery={{ data: addressMock.filecoin } as AddressQuery}/>, { hooksConfig });
const component = await render(
<AddressDetails
addressQuery={{ data: addressMock.filecoin } as AddressQuery}
countersQuery={{ data: countersMock.forValidator } as AddressCountersQuery}
/>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot({
mask: [ page.locator(pwConfig.adsBannerSelector) ],
......
......@@ -26,24 +26,20 @@ import AddressNetWorth from './details/AddressNetWorth';
import AddressSaveOnGas from './details/AddressSaveOnGas';
import FilecoinActorTag from './filecoin/FilecoinActorTag';
import TokenSelect from './tokenSelect/TokenSelect';
import useAddressCountersQuery from './utils/useAddressCountersQuery';
import type { AddressCountersQuery } from './utils/useAddressCountersQuery';
import type { AddressQuery } from './utils/useAddressQuery';
interface Props {
addressQuery: AddressQuery;
countersQuery: AddressCountersQuery;
isLoading?: boolean;
}
const AddressDetails = ({ addressQuery, isLoading }: Props) => {
const AddressDetails = ({ addressQuery, countersQuery, isLoading }: Props) => {
const router = useRouter();
const addressHash = getQueryParamString(router.query.hash);
const countersQuery = useAddressCountersQuery({
hash: addressHash,
addressQuery,
});
const error404Data = React.useMemo(() => ({
hash: addressHash || '',
is_contract: false,
......
......@@ -8,6 +8,7 @@ import type { EntityTag } from 'ui/shared/EntityTags/types';
import config from 'configs/app';
import getCheckedSummedAddress from 'lib/address/getCheckedSummedAddress';
import useAddressMetadataInfoQuery from 'lib/address/useAddressMetadataInfoQuery';
import useAddressMetadataInitUpdate from 'lib/address/useAddressMetadataInitUpdate';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/contexts/app';
import useAddressProfileApiQuery from 'lib/hooks/useAddressProfileApiQuery';
......@@ -42,6 +43,7 @@ import AddressMetadataAlert from 'ui/address/details/AddressMetadataAlert';
import AddressQrCode from 'ui/address/details/AddressQrCode';
import AddressEnsDomains from 'ui/address/ensDomains/AddressEnsDomains';
import SolidityscanReport from 'ui/address/SolidityscanReport';
import useAddressCountersQuery from 'ui/address/utils/useAddressCountersQuery';
import useAddressQuery from 'ui/address/utils/useAddressQuery';
import useCheckAddressFormat from 'ui/address/utils/useCheckAddressFormat';
import useCheckDomainNameParam from 'ui/address/utils/useCheckDomainNameParam';
......@@ -86,6 +88,11 @@ const AddressPageContent = () => {
},
});
const countersQuery = useAddressCountersQuery({
hash,
addressQuery,
});
const userOpsAccountQuery = useApiQuery('general:user_ops_account', {
pathParams: { hash },
queryOptions: {
......@@ -144,6 +151,12 @@ const AddressPageContent = () => {
handler: handleFetchedBytecodeMessage,
});
useAddressMetadataInitUpdate({
address: hash,
counters: countersQuery.data,
isEnabled: !countersQuery.isPlaceholderData && !countersQuery.isDegradedData,
});
const isSafeAddress = useIsSafeAddress(!addressQuery.isPlaceholderData && addressQuery.data?.is_contract ? hash : undefined);
const xStarQuery = useFetchXStarScore({ hash });
......@@ -159,7 +172,7 @@ const AddressPageContent = () => {
{
id: 'index',
title: 'Details',
component: <AddressDetails addressQuery={ addressQuery } isLoading={ isTabsLoading }/>,
component: <AddressDetails addressQuery={ addressQuery } countersQuery={ countersQuery } isLoading={ isTabsLoading }/>,
},
addressQuery.data?.is_contract ? {
id: 'contract',
......@@ -270,6 +283,7 @@ const AddressPageContent = () => {
].filter(Boolean);
}, [
addressQuery,
countersQuery,
contractTabs,
addressTabsCountersQuery.data,
userOpsAccountQuery.data,
......
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