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 type { Feature } from './types';
import apis from '../apis'; import apis from '../apis';
import { getEnvValue } from '../utils';
const title = 'Address metadata'; const title = 'Address metadata';
const config: Feature<{}> = (() => { const config: Feature<{ isAddressTagsUpdateEnabled: boolean }> = (() => {
if (apis.metadata) { if (apis.metadata) {
return Object.freeze({ return Object.freeze({
title, title,
isEnabled: true, isEnabled: true,
isAddressTagsUpdateEnabled: getEnvValue('NEXT_PUBLIC_METADATA_ADDRESS_TAGS_UPDATE_ENABLED') !== 'false',
}); });
} }
......
...@@ -657,6 +657,25 @@ const bridgedTokensSchema = yup ...@@ -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 const deFiDropdownItemSchema: yup.ObjectSchema<DeFiDropdownItem> = yup
.object({ .object({
text: yup.string().required(), text: yup.string().required(),
...@@ -943,7 +962,6 @@ const schema = yup ...@@ -943,7 +962,6 @@ const schema = yup
NEXT_PUBLIC_VISUALIZE_API_BASE_PATH: yup.string(), NEXT_PUBLIC_VISUALIZE_API_BASE_PATH: yup.string(),
NEXT_PUBLIC_CONTRACT_INFO_API_HOST: yup.string().test(urlTest), NEXT_PUBLIC_CONTRACT_INFO_API_HOST: yup.string().test(urlTest),
NEXT_PUBLIC_NAME_SERVICE_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_ADMIN_SERVICE_API_HOST: yup.string().test(urlTest),
NEXT_PUBLIC_GRAPHIQL_TRANSACTION: yup NEXT_PUBLIC_GRAPHIQL_TRANSACTION: yup
.mixed() .mixed()
...@@ -1098,6 +1116,7 @@ const schema = yup ...@@ -1098,6 +1116,7 @@ const schema = yup
.concat(beaconChainSchema) .concat(beaconChainSchema)
.concat(bridgedTokensSchema) .concat(bridgedTokensSchema)
.concat(sentrySchema) .concat(sentrySchema)
.concat(tacSchema); .concat(tacSchema)
.concat(addressMetadataSchema);
export default schema; export default schema;
...@@ -7,3 +7,5 @@ NEXT_PUBLIC_VIEWS_ADDRESS_BECH_32_PREFIX=foo ...@@ -7,3 +7,5 @@ NEXT_PUBLIC_VIEWS_ADDRESS_BECH_32_PREFIX=foo
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx
NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY=deprecated NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY=deprecated
NEXT_PUBLIC_NETWORK_RPC_URL=['https://example.com','https://example2.com'] 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. ...@@ -657,6 +657,7 @@ This feature allows name tags and other public tags for addresses.
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | | 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_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; &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 = { ...@@ -11,6 +11,9 @@ export const METADATA_API_RESOURCES = {
public_tag_types: { public_tag_types: {
path: '/api/v1/public-tag-types', path: '/api/v1/public-tag-types',
}, },
address_submit: {
path: '/api/v1/addresses\\:submit',
},
} satisfies Record<string, ApiResource>; } satisfies Record<string, ApiResource>;
export type MetadataApiResourceName = `metadata:${ keyof typeof METADATA_API_RESOURCES }`; export type MetadataApiResourceName = `metadata:${ keyof typeof METADATA_API_RESOURCES }`;
......
...@@ -8,6 +8,7 @@ import * as pwConfig from 'playwright/utils/config'; ...@@ -8,6 +8,7 @@ import * as pwConfig from 'playwright/utils/config';
import AddressDetails from './AddressDetails'; import AddressDetails from './AddressDetails';
import MockAddressPage from './testUtils/MockAddressPage'; import MockAddressPage from './testUtils/MockAddressPage';
import type { AddressCountersQuery } from './utils/useAddressCountersQuery';
import type { AddressQuery } from './utils/useAddressQuery'; import type { AddressQuery } from './utils/useAddressQuery';
const ADDRESS_HASH = addressMock.hash; const ADDRESS_HASH = addressMock.hash;
...@@ -22,9 +23,14 @@ test.describe('mobile', () => { ...@@ -22,9 +23,14 @@ test.describe('mobile', () => {
test('contract', async({ render, mockApiResponse, page }) => { test('contract', async({ render, mockApiResponse, page }) => {
await mockApiResponse('general:address', addressMock.contract, { pathParams: { hash: ADDRESS_HASH } }); 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({ await expect(component).toHaveScreenshot({
mask: [ page.locator(pwConfig.adsBannerSelector) ], mask: [ page.locator(pwConfig.adsBannerSelector) ],
...@@ -34,9 +40,14 @@ test.describe('mobile', () => { ...@@ -34,9 +40,14 @@ test.describe('mobile', () => {
test('validator', async({ render, page, mockApiResponse }) => { test('validator', async({ render, page, mockApiResponse }) => {
await mockApiResponse('general:address', addressMock.validator, { pathParams: { hash: ADDRESS_HASH } }); 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({ await expect(component).toHaveScreenshot({
mask: [ page.locator(pwConfig.adsBannerSelector) ], mask: [ page.locator(pwConfig.adsBannerSelector) ],
...@@ -46,9 +57,14 @@ test.describe('mobile', () => { ...@@ -46,9 +57,14 @@ test.describe('mobile', () => {
test('filecoin', async({ render, mockApiResponse, page }) => { test('filecoin', async({ render, mockApiResponse, page }) => {
await mockApiResponse('general:address', addressMock.filecoin, { pathParams: { hash: ADDRESS_HASH } }); 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({ await expect(component).toHaveScreenshot({
mask: [ page.locator(pwConfig.adsBannerSelector) ], mask: [ page.locator(pwConfig.adsBannerSelector) ],
...@@ -59,9 +75,14 @@ test.describe('mobile', () => { ...@@ -59,9 +75,14 @@ test.describe('mobile', () => {
test('contract', async({ render, page, mockApiResponse }) => { test('contract', async({ render, page, mockApiResponse }) => {
await mockApiResponse('general:address', addressMock.contract, { pathParams: { hash: ADDRESS_HASH } }); 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({ await expect(component).toHaveScreenshot({
mask: [ page.locator(pwConfig.adsBannerSelector) ], mask: [ page.locator(pwConfig.adsBannerSelector) ],
...@@ -72,7 +93,6 @@ test('contract', async({ render, page, mockApiResponse }) => { ...@@ -72,7 +93,6 @@ test('contract', async({ render, page, mockApiResponse }) => {
// there's an unexpected timeout occurred in this test // there's an unexpected timeout occurred in this test
test.fixme('token', async({ render, mockApiResponse, injectMetaMaskProvider, page }) => { test.fixme('token', async({ render, mockApiResponse, injectMetaMaskProvider, page }) => {
await mockApiResponse('general:address', addressMock.token, { pathParams: { hash: ADDRESS_HASH } }); 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.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.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 }); 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 ...@@ -81,7 +101,7 @@ test.fixme('token', async({ render, mockApiResponse, injectMetaMaskProvider, pag
const component = await render( const component = await render(
<MockAddressPage> <MockAddressPage>
<AddressDetails addressQuery={{ data: addressMock.token } as AddressQuery}/> <AddressDetails addressQuery={{ data: addressMock.token } as AddressQuery} countersQuery={{ data: countersMock.forToken } as AddressCountersQuery}/>
</MockAddressPage>, </MockAddressPage>,
{ hooksConfig }, { hooksConfig },
); );
...@@ -94,9 +114,14 @@ test.fixme('token', async({ render, mockApiResponse, injectMetaMaskProvider, pag ...@@ -94,9 +114,14 @@ test.fixme('token', async({ render, mockApiResponse, injectMetaMaskProvider, pag
test('validator', async({ render, mockApiResponse, page }) => { test('validator', async({ render, mockApiResponse, page }) => {
await mockApiResponse('general:address', addressMock.validator, { pathParams: { hash: ADDRESS_HASH } }); 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({ await expect(component).toHaveScreenshot({
mask: [ page.locator(pwConfig.adsBannerSelector) ], mask: [ page.locator(pwConfig.adsBannerSelector) ],
...@@ -106,9 +131,14 @@ test('validator', async({ render, mockApiResponse, page }) => { ...@@ -106,9 +131,14 @@ test('validator', async({ render, mockApiResponse, page }) => {
test('filecoin', async({ render, mockApiResponse, page }) => { test('filecoin', async({ render, mockApiResponse, page }) => {
await mockApiResponse('general:address', addressMock.filecoin, { pathParams: { hash: ADDRESS_HASH } }); 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({ await expect(component).toHaveScreenshot({
mask: [ page.locator(pwConfig.adsBannerSelector) ], mask: [ page.locator(pwConfig.adsBannerSelector) ],
......
...@@ -26,24 +26,20 @@ import AddressNetWorth from './details/AddressNetWorth'; ...@@ -26,24 +26,20 @@ import AddressNetWorth from './details/AddressNetWorth';
import AddressSaveOnGas from './details/AddressSaveOnGas'; import AddressSaveOnGas from './details/AddressSaveOnGas';
import FilecoinActorTag from './filecoin/FilecoinActorTag'; import FilecoinActorTag from './filecoin/FilecoinActorTag';
import TokenSelect from './tokenSelect/TokenSelect'; import TokenSelect from './tokenSelect/TokenSelect';
import useAddressCountersQuery from './utils/useAddressCountersQuery'; import type { AddressCountersQuery } from './utils/useAddressCountersQuery';
import type { AddressQuery } from './utils/useAddressQuery'; import type { AddressQuery } from './utils/useAddressQuery';
interface Props { interface Props {
addressQuery: AddressQuery; addressQuery: AddressQuery;
countersQuery: AddressCountersQuery;
isLoading?: boolean; isLoading?: boolean;
} }
const AddressDetails = ({ addressQuery, isLoading }: Props) => { const AddressDetails = ({ addressQuery, countersQuery, isLoading }: Props) => {
const router = useRouter(); const router = useRouter();
const addressHash = getQueryParamString(router.query.hash); const addressHash = getQueryParamString(router.query.hash);
const countersQuery = useAddressCountersQuery({
hash: addressHash,
addressQuery,
});
const error404Data = React.useMemo(() => ({ const error404Data = React.useMemo(() => ({
hash: addressHash || '', hash: addressHash || '',
is_contract: false, is_contract: false,
......
...@@ -8,6 +8,7 @@ import type { EntityTag } from 'ui/shared/EntityTags/types'; ...@@ -8,6 +8,7 @@ import type { EntityTag } from 'ui/shared/EntityTags/types';
import config from 'configs/app'; import config from 'configs/app';
import getCheckedSummedAddress from 'lib/address/getCheckedSummedAddress'; import getCheckedSummedAddress from 'lib/address/getCheckedSummedAddress';
import useAddressMetadataInfoQuery from 'lib/address/useAddressMetadataInfoQuery'; import useAddressMetadataInfoQuery from 'lib/address/useAddressMetadataInfoQuery';
import useAddressMetadataInitUpdate from 'lib/address/useAddressMetadataInitUpdate';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/contexts/app'; import { useAppContext } from 'lib/contexts/app';
import useAddressProfileApiQuery from 'lib/hooks/useAddressProfileApiQuery'; import useAddressProfileApiQuery from 'lib/hooks/useAddressProfileApiQuery';
...@@ -42,6 +43,7 @@ import AddressMetadataAlert from 'ui/address/details/AddressMetadataAlert'; ...@@ -42,6 +43,7 @@ import AddressMetadataAlert from 'ui/address/details/AddressMetadataAlert';
import AddressQrCode from 'ui/address/details/AddressQrCode'; import AddressQrCode from 'ui/address/details/AddressQrCode';
import AddressEnsDomains from 'ui/address/ensDomains/AddressEnsDomains'; import AddressEnsDomains from 'ui/address/ensDomains/AddressEnsDomains';
import SolidityscanReport from 'ui/address/SolidityscanReport'; import SolidityscanReport from 'ui/address/SolidityscanReport';
import useAddressCountersQuery from 'ui/address/utils/useAddressCountersQuery';
import useAddressQuery from 'ui/address/utils/useAddressQuery'; import useAddressQuery from 'ui/address/utils/useAddressQuery';
import useCheckAddressFormat from 'ui/address/utils/useCheckAddressFormat'; import useCheckAddressFormat from 'ui/address/utils/useCheckAddressFormat';
import useCheckDomainNameParam from 'ui/address/utils/useCheckDomainNameParam'; import useCheckDomainNameParam from 'ui/address/utils/useCheckDomainNameParam';
...@@ -86,6 +88,11 @@ const AddressPageContent = () => { ...@@ -86,6 +88,11 @@ const AddressPageContent = () => {
}, },
}); });
const countersQuery = useAddressCountersQuery({
hash,
addressQuery,
});
const userOpsAccountQuery = useApiQuery('general:user_ops_account', { const userOpsAccountQuery = useApiQuery('general:user_ops_account', {
pathParams: { hash }, pathParams: { hash },
queryOptions: { queryOptions: {
...@@ -144,6 +151,12 @@ const AddressPageContent = () => { ...@@ -144,6 +151,12 @@ const AddressPageContent = () => {
handler: handleFetchedBytecodeMessage, 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 isSafeAddress = useIsSafeAddress(!addressQuery.isPlaceholderData && addressQuery.data?.is_contract ? hash : undefined);
const xStarQuery = useFetchXStarScore({ hash }); const xStarQuery = useFetchXStarScore({ hash });
...@@ -159,7 +172,7 @@ const AddressPageContent = () => { ...@@ -159,7 +172,7 @@ const AddressPageContent = () => {
{ {
id: 'index', id: 'index',
title: 'Details', title: 'Details',
component: <AddressDetails addressQuery={ addressQuery } isLoading={ isTabsLoading }/>, component: <AddressDetails addressQuery={ addressQuery } countersQuery={ countersQuery } isLoading={ isTabsLoading }/>,
}, },
addressQuery.data?.is_contract ? { addressQuery.data?.is_contract ? {
id: 'contract', id: 'contract',
...@@ -270,6 +283,7 @@ const AddressPageContent = () => { ...@@ -270,6 +283,7 @@ const AddressPageContent = () => {
].filter(Boolean); ].filter(Boolean);
}, [ }, [
addressQuery, addressQuery,
countersQuery,
contractTabs, contractTabs,
addressTabsCountersQuery.data, addressTabsCountersQuery.data,
userOpsAccountQuery.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