Skip to content

Commit

Permalink
fix schema validation for NEXT_PUBLIC_MULTICHAIN_PROVIDER_CONFIG
Browse files Browse the repository at this point in the history
  • Loading branch information
isstuev committed May 16, 2024
1 parent 354bf54 commit 7707c83
Show file tree
Hide file tree
Showing 9 changed files with 48 additions and 49 deletions.
28 changes: 4 additions & 24 deletions configs/app/features/multichainButton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
} else if (marketplace.isEnabled) {
return Object.freeze({
...enabledOptions,
dappId: value.url,
});
}
url_template: value.url_template,
});
}

return Object.freeze({
Expand Down
2 changes: 1 addition & 1 deletion configs/envs/.env.eth
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blocksc
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL
NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form
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
Expand Down
17 changes: 12 additions & 5 deletions deploy/tools/envs-validator/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
name: yup.string().required(),
url: yup.string().required(),
logo: yup.string(),
}).nullable().notRequired(),
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_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_GAS_TRACKER_ENABLED: yup.boolean(),
NEXT_PUBLIC_GAS_TRACKER_UNITS: yup.array().transform(replaceQuotes).json().of(yup.string<GasUnit>().oneOf(GAS_UNITS)),
Expand Down
2 changes: 2 additions & 0 deletions deploy/tools/envs-validator/test/.env.base
Original file line number Diff line number Diff line change
Expand Up @@ -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'}

4 changes: 2 additions & 2 deletions docs/ENVS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion types/client/multichainProviderConfig.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export type MultichainProviderConfig = {
name: string;
url: string;
url_template: string;
logo?: string;
};
5 changes: 3 additions & 2 deletions ui/address/AddressDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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>
)
}
Expand Down
10 changes: 5 additions & 5 deletions ui/address/details/AddressNetWorth.pw.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
);

Expand All @@ -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>,
);

Expand All @@ -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>,
);

Expand Down
27 changes: 18 additions & 9 deletions ui/address/details/AddressNetWorth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,29 @@ 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';

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',
Expand Down Expand Up @@ -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 }
Expand All @@ -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>
Expand Down

0 comments on commit 7707c83

Please sign in to comment.