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