Commit eb61e491 authored by isstuev's avatar isstuev

fix schema validation for NEXT_PUBLIC_MULTICHAIN_PROVIDER_CONFIG

parent 70ddce7a
......@@ -2,40 +2,20 @@ import type { Feature } from './types';
import type { MultichainProviderConfig } from 'types/client/multichainProviderConfig';
import { getEnvValue, parseEnvJson } from '../utils';
import marketplace from './marketplace';
const value = parseEnvJson<MultichainProviderConfig>(getEnvValue('NEXT_PUBLIC_MULTICHAIN_PROVIDER_CONFIG'));
const title = 'Multichain button';
function isValidUrl(string: string) {
try {
new URL(string);
return true;
} catch (error) {
return false;
}
}
const config: Feature<{name: string; logoUrl?: string } & ({ dappId: string } | { url: string })> = (() => {
const config: Feature<{name: string; logoUrl?: string; url_template: string }> = (() => {
if (value) {
const enabledOptions = {
return Object.freeze({
title,
isEnabled: true as const,
name: value.name,
logoUrl: value.logo,
};
if (isValidUrl(value.url)) {
return Object.freeze({
...enabledOptions,
url: value.url,
url_template: value.url_template,
});
} else if (marketplace.isEnabled) {
return Object.freeze({
...enabledOptions,
dappId: value.url,
});
}
}
return Object.freeze({
......
......@@ -54,7 +54,7 @@ NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKj
NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-security-reports/default.json
NEXT_PUBLIC_MARKETPLACE_FEATURED_APP=gearbox-protocol
NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
NEXT_PUBLIC_MULTICHAIN_PROVIDER_CONFIG={'name': 'zerion', 'url': 'zerion', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'}
NEXT_PUBLIC_MULTICHAIN_PROVIDER_CONFIG={'name': 'zerion', 'url_template': '/apps/zerion/{address}/overview', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'}
#meta
NEXT_PUBLIC_OG_IMAGE_URL=https://github.com/blockscout/frontend-configs/blob/main/configs/og-images/eth.jpg?raw=true
......
......@@ -621,11 +621,18 @@ const schema = yup
NEXT_PUBLIC_HAS_USER_OPS: yup.boolean(),
NEXT_PUBLIC_METASUITES_ENABLED: yup.boolean(),
NEXT_PUBLIC_SWAP_BUTTON_URL: yup.string(),
NEXT_PUBLIC_MULTICHAIN_PROVIDER_CONFIG: yup.object<MultichainProviderConfig>().transform(replaceQuotes).json().shape({
NEXT_PUBLIC_MULTICHAIN_PROVIDER_CONFIG: yup
.mixed()
.test('shape', 'Invalid schema were provided for NEXT_PUBLIC_MULTICHAIN_PROVIDER_CONFIG, it should have name and url props', (data) => {
const isUndefined = data === undefined;
const valueSchema = yup.object<MultichainProviderConfig>().transform(replaceQuotes).json().shape({
name: yup.string().required(),
url: yup.string().required(),
url_template: yup.string().required(),
logo: yup.string(),
}).nullable().notRequired(),
});
return isUndefined || valueSchema.isValidSync(data);
}),
NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE: yup.string<ValidatorsChainType>().oneOf(VALIDATORS_CHAIN_TYPE),
NEXT_PUBLIC_GAS_TRACKER_ENABLED: yup.boolean(),
NEXT_PUBLIC_GAS_TRACKER_UNITS: yup.array().transform(replaceQuotes).json().of(yup.string<GasUnit>().oneOf(GAS_UNITS)),
......
......@@ -74,3 +74,5 @@ NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET=false
NEXT_PUBLIC_WEB3_WALLETS=['coinbase','metamask','token_pocket']
NEXT_PUBLIC_SWAP_BUTTON_URL=uniswap
NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE=stability
NEXT_PUBLIC_MULTICHAIN_PROVIDER_CONFIG={'name': 'zerion', 'url_template': '/apps/zerion/{address}/overview', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'}
......@@ -686,7 +686,7 @@ If the feature is enabled, a Multichain balance button will be displayed on the
| Variable | Type| Description | Compulsoriness | Default value | Example value |
| --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_MULTICHAIN_PROVIDER_CONFIG | `{ name: string; url: string; logo?: string }` | Multichain portfolio application config See [below](#multichain-button-configuration-properties) | - | - | `{ name: 'zerion', url: 'zerion', logo: 'https://example.com/icon.svg'` |
| NEXT_PUBLIC_MULTICHAIN_PROVIDER_CONFIG | `{ name: string; url_template: string; logo?: string }` | Multichain portfolio application config See [below](#multichain-button-configuration-properties) | - | - | `{ name: 'zerion', url_template: '/apps/zerion/{address}/overview', logo: 'https://example.com/icon.svg'` |
&nbsp;
......@@ -695,7 +695,7 @@ If the feature is enabled, a Multichain balance button will be displayed on the
| Variable | Type| Description | Compulsoriness | Default value | Example value |
| --- | --- | --- | --- | --- | --- |
| name | `string` | Multichain portfolio application name | Required | - | `zerion` |
| url | `string` | Application ID in the marketplace or website URL | Required | - | `zerion` |
| url_template | `string` | Absolute (for external links) or relative (for internal links) path template to the portfolio. Should be a template with `{address}` variable | Required | - | `/apps/zerion/{address}/overview` |
| logo | `string` | Multichain portfolio application logo (.svg) url | - | - | `https://example.com/icon.svg` |
&nbsp;
......
export type MultichainProviderConfig = {
name: string;
url: string;
url_template: string;
logo?: string;
};
......@@ -2,6 +2,7 @@ import { Box, Text, Grid } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import config from 'configs/app';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import useIsMounted from 'lib/hooks/useIsMounted';
import getQueryParamString from 'lib/router/getQueryParamString';
......@@ -130,14 +131,14 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
{ addressQuery.data ? <TokenSelect onClick={ handleCounterItemClick }/> : <Box py="6px">0</Box> }
</DetailsInfoItem>
) }
{ (data.exchange_rate && data.has_tokens) && (
{ (config.features.multichainButton.isEnabled || (data.exchange_rate && data.has_tokens)) && (
<DetailsInfoItem
title="Net worth"
hint="Total net worth in USD of all tokens for the address"
alignSelf="center"
isLoading={ addressQuery.isPlaceholderData }
>
<AddressNetWorth addressData={ addressQuery.data } isLoading={ addressQuery.isPlaceholderData }/>
<AddressNetWorth addressData={ addressQuery.data } addressHash={ addressHash } isLoading={ addressQuery.isPlaceholderData }/>
</DetailsInfoItem>
)
}
......
......@@ -20,7 +20,7 @@ test.beforeEach(async({ mockApiResponse }) => {
test('base view', async({ mount }) => {
const component = await mount(
<TestApp>
<AddressNetWorth addressData={ addressMock.token }/>
<AddressNetWorth addressData={ addressMock.token } addressHash={ ADDRESS_HASH }/>
</TestApp>,
);
......@@ -29,13 +29,13 @@ test('base view', async({ mount }) => {
test('with multichain button internal +@dark-mode', async({ mount, mockEnvs, mockAssetResponse }) => {
await mockEnvs([
[ 'NEXT_PUBLIC_MULTICHAIN_PROVIDER_CONFIG', `{"name": "zerion", "url": "zerion", "logo": "${ ICON_URL }"}` ],
[ 'NEXT_PUBLIC_MULTICHAIN_PROVIDER_CONFIG', `{"name": "zerion", "url_template": "/apps/zerion/{address}/overview", "logo": "${ ICON_URL }"}` ],
]);
await mockAssetResponse(ICON_URL, './playwright/mocks/image_svg.svg');
const component = await mount(
<TestApp>
<AddressNetWorth addressData={ addressMock.token }/>
<AddressNetWorth addressData={ addressMock.token } addressHash={ ADDRESS_HASH }/>
</TestApp>,
);
......@@ -44,13 +44,13 @@ test('with multichain button internal +@dark-mode', async({ mount, mockEnvs, moc
test('with multichain button external', async({ mount, mockEnvs, mockAssetResponse }) => {
await mockEnvs([
[ 'NEXT_PUBLIC_MULTICHAIN_PROVIDER_CONFIG', `{"name": "zerion", "url": "https://duck.url", "logo": "${ ICON_URL }"}` ],
[ 'NEXT_PUBLIC_MULTICHAIN_PROVIDER_CONFIG', `{"name": "zerion", "url_template": "https://duck.url/{address}", "logo": "${ ICON_URL }"}` ],
]);
await mockAssetResponse(ICON_URL, './playwright/mocks/image_svg.svg');
const component = await mount(
<TestApp>
<AddressNetWorth addressData={ addressMock.token }/>
<AddressNetWorth addressData={ addressMock.token } addressHash={ ADDRESS_HASH }/>
</TestApp>,
);
......
......@@ -4,11 +4,10 @@ import React from 'react';
import type { Address } from 'types/api/address';
import { route } from 'nextjs-routes';
import config from 'configs/app';
import getCurrencyValue from 'lib/getCurrencyValue';
import * as mixpanel from 'lib/mixpanel/index';
import * as regexp from 'lib/regexp';
import LinkExternal from 'ui/shared/links/LinkExternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
import TextSeparator from 'ui/shared/TextSeparator';
......@@ -16,15 +15,18 @@ import TextSeparator from 'ui/shared/TextSeparator';
import { getTokensTotalInfo } from '../utils/tokenUtils';
import useFetchTokens from '../utils/useFetchTokens';
const TEMPLATE_ADDRESS = '{address}';
const multichainFeature = config.features.multichainButton;
type Props = {
addressHash: string;
addressData?: Address;
isLoading?: boolean;
}
const AddressNetWorth = ({ addressData, isLoading }: Props) => {
const { data, isError, isPending } = useFetchTokens({ hash: addressData?.hash });
const AddressNetWorth = ({ addressData, isLoading, addressHash }: Props) => {
const { data, isError, isPending } = useFetchTokens({ hash: addressData?.hash, enabled: addressData?.has_tokens });
const { usdBn: nativeUsd } = getCurrencyValue({
value: addressData?.coin_balance || '0',
......@@ -63,20 +65,27 @@ const AddressNetWorth = ({ addressData, isLoading }: Props) => {
onClick: onMultichainClick,
};
const urlString = multichainFeature.url_template.replace(TEMPLATE_ADDRESS, addressHash);
const isExternal = regexp.URL_PREFIX.test(urlString);
const url = isExternal ? new URL(urlString) : new URL(urlString, config.app.baseUrl);
url.searchParams.append('utm_source', 'blockscout');
url.searchParams.append('utm_medium', 'address');
multichainItem = (
<>
<TextSeparator mx={ 3 } color="gray.500"/>
<Text mr={ 2 }>Multichain</Text>
{ 'url' in multichainFeature ? (
{ isExternal ? (
<LinkExternal
href={ multichainFeature.url }
href={ url.toString() }
{ ...linkProps }
>
{ buttonContent }
</LinkExternal>
) : (
<LinkInternal
href={ route({ pathname: '/apps/[id]', query: { id: multichainFeature.dappId, utm_source: 'blockscout', utm_medium: 'address-page' } }) }
href={ url.toString() }
{ ...linkProps }
>
{ buttonContent }
......@@ -87,9 +96,9 @@ const AddressNetWorth = ({ addressData, isLoading }: Props) => {
}
return (
<Skeleton display="flex" alignItems="center" isLoaded={ !isLoading && !isPending }>
<Skeleton display="flex" alignItems="center" isLoaded={ !isLoading && !(addressData?.has_tokens && isPending) }>
<Text>
{ isError ? 'N/A' : `${ prefix }$${ totalUsd.toFormat(2) }` }
{ (isError || !addressData?.exchange_rate) ? 'N/A' : `${ prefix }$${ totalUsd.toFormat(2) }` }
</Text>
{ multichainItem }
</Skeleton>
......
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