Skip to content

Commit

Permalink
Merge pull request #1912 from blockscout/fe-1802
Browse files Browse the repository at this point in the history
Multi chain balance button
  • Loading branch information
isstuev authored May 27, 2024
2 parents 4e2f5b7 + ae4c59a commit 04f051f
Show file tree
Hide file tree
Showing 84 changed files with 389 additions and 124 deletions.
1 change: 1 addition & 0 deletions configs/app/features/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export { default as growthBook } from './growthBook';
export { default as marketplace } from './marketplace';
export { default as metasuites } from './metasuites';
export { default as mixpanel } from './mixpanel';
export { default as multichainButton } from './multichainButton';
export { default as nameService } from './nameService';
export { default as restApiDocs } from './restApiDocs';
export { default as rollup } from './rollup';
Expand Down
29 changes: 29 additions & 0 deletions configs/app/features/multichainButton.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
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_BALANCE_PROVIDER_CONFIG'));

const title = 'Multichain balance';

const config: Feature<{name: string; logoUrl?: string; urlTemplate: string; dappId?: string }> = (() => {
if (value) {
return Object.freeze({
title,
isEnabled: true,
name: value.name,
logoUrl: value.logo,
urlTemplate: value.url_template,
dappId: marketplace.isEnabled ? value.dapp_id : undefined,
});
}

return Object.freeze({
title,
isEnabled: false,
});
})();

export default config;
2 changes: 2 additions & 0 deletions configs/envs/.env.eth
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,10 @@ 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_BALANCE_PROVIDER_CONFIG={'name': 'zerion', 'url_template': 'https://app.zerion.io/{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
NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true
NEXT_PUBLIC_SEO_ENHANCED_DATA_ENABLED=true

14 changes: 14 additions & 0 deletions deploy/tools/envs-validator/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type { ContractCodeIde } from '../../../types/client/contract';
import { GAS_UNITS } from '../../../types/client/gasTracker';
import type { GasUnit } from '../../../types/client/gasTracker';
import type { MarketplaceAppOverview, MarketplaceAppSecurityReportRaw, MarketplaceAppSecurityReport } from '../../../types/client/marketplace';
import type { MultichainProviderConfig } from '../../../types/client/multichainProviderConfig';
import { NAVIGATION_LINK_IDS } from '../../../types/client/navigation-items';
import type { NavItemExternal, NavigationLinkId } from '../../../types/client/navigation-items';
import { ROLLUP_TYPES } from '../../../types/client/rollup';
Expand Down Expand Up @@ -620,6 +621,19 @@ 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_BALANCE_PROVIDER_CONFIG: yup
.mixed()
.test('shape', 'Invalid schema were provided for NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG, it should have name and url template', (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(),
dapp_id: 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_BALANCE_PROVIDER_CONFIG={'name': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'}

22 changes: 22 additions & 0 deletions docs/ENVS.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will
- [Sentry error monitoring](ENVS.md#sentry-error-monitoring)
- [OpenTelemetry](ENVS.md#opentelemetry)
- [Swap button](ENVS.md#swap-button)
- [Multichain balance button](ENVS.md#multichain-button)
- [3rd party services configuration](ENVS.md#external-services-configuration)

&nbsp;
Expand Down Expand Up @@ -679,6 +680,27 @@ If the feature is enabled, a Swap button will be displayed at the top of the exp

&nbsp;

### Multichain balance button

If the feature is enabled, a Multichain balance button will be displayed on the address page, which will take you to the portfolio application in the marketplace or to an external site.

| Variable | Type| Description | Compulsoriness | Default value | Example value |
| --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG | `{ name: string; url_template: string; dapp_id?: string; logo?: string }` | Multichain portfolio application config See [below](#multichain-button-configuration-properties) | - | - | `{ name: 'zerion', url_template: 'https://app.zerion.io/{address}/overview', logo: 'https://example.com/icon.svg'` |

&nbsp;

#### Multichain button configuration properties

| Variable | Type| Description | Compulsoriness | Default value | Example value |
| --- | --- | --- | --- | --- | --- |
| name | `string` | Multichain portfolio application name | Required | - | `zerion` |
| url_template | `string` | Url template to the portfolio. Should be a template with `{address}` variable | Required | - | `https://app.zerion.io/{address}/overview` |
| dapp_id | `string` | Set for open a Blockscout dapp page with the portfolio instead of opening external app page | - | - | `zerion` |
| logo | `string` | Multichain portfolio application logo (.svg) url | - | - | `https://example.com/icon.svg` |

&nbsp;

## External services configuration

### Google ReCaptcha
Expand Down
2 changes: 1 addition & 1 deletion lib/mixpanel/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ Type extends EventTypes.FILTERS ? {
'Filter name': string;
} :
Type extends EventTypes.BUTTON_CLICK ? {
'Content': 'Swap button';
'Content': 'Swap button' | 'Multichain';
'Source': string;
} :
Type extends EventTypes.PROMO_BANNER ? {
Expand Down
2 changes: 1 addition & 1 deletion mocks/address/address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export const token: Address = {
coin_balance: '1',
creation_tx_hash: '0xc38cf7377bf72d6436f63c37b01b24d032101f20ec1849286dc703c712f10c98',
creator_address_hash: '0x34A9c688512ebdB575e82C50c9803F6ba2916E72',
exchange_rate: null,
exchange_rate: '0.04311',
implementation_address: null,
has_decompiled_code: false,
has_logs: false,
Expand Down
6 changes: 6 additions & 0 deletions types/client/multichainProviderConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type MultichainProviderConfig = {
name: string;
dapp_id?: string;
url_template: string;
logo?: string;
};
2 changes: 1 addition & 1 deletion ui/address/AddressCsvExportLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import config from 'configs/app';
import useIsInitialLoading from 'lib/hooks/useIsInitialLoading';
import useIsMobile from 'lib/hooks/useIsMobile';
import IconSvg from 'ui/shared/IconSvg';
import LinkInternal from 'ui/shared/LinkInternal';
import LinkInternal from 'ui/shared/links/LinkInternal';

interface Props {
address: string;
Expand Down
13 changes: 13 additions & 0 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 All @@ -17,6 +18,7 @@ import TxEntity from 'ui/shared/entities/tx/TxEntity';

import AddressBalance from './details/AddressBalance';
import AddressNameInfo from './details/AddressNameInfo';
import AddressNetWorth from './details/AddressNetWorth';
import TokenSelect from './tokenSelect/TokenSelect';
import useAddressCountersQuery from './utils/useAddressCountersQuery';
import type { AddressQuery } from './utils/useAddressQuery';
Expand Down Expand Up @@ -129,6 +131,17 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
{ addressQuery.data ? <TokenSelect onClick={ handleCounterItemClick }/> : <Box py="6px">0</Box> }
</DetailsInfoItem>
) }
{ (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 } addressHash={ addressHash } isLoading={ addressQuery.isPlaceholderData }/>
</DetailsInfoItem>
)
}
<DetailsInfoItem
title="Transactions"
hint="Number of transactions related to this address"
Expand Down
2 changes: 1 addition & 1 deletion ui/address/SolidityscanReport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import React from 'react';
import solidityScanIcon from 'icons/brands/solidity_scan.svg';
import useApiQuery from 'lib/api/useApiQuery';
import { SOLIDITYSCAN_REPORT } from 'stubs/contract';
import LinkExternal from 'ui/shared/LinkExternal';
import LinkExternal from 'ui/shared/links/LinkExternal';
import SolidityscanReportButton from 'ui/shared/solidityscanReport/SolidityscanReportButton';
import SolidityscanReportDetails from 'ui/shared/solidityscanReport/SolidityscanReportDetails';
import SolidityscanReportScore from 'ui/shared/solidityscanReport/SolidityscanReportScore';
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { NovesResponseData } from 'types/api/noves';

import dayjs from 'lib/date/dayjs';
import IconSvg from 'ui/shared/IconSvg';
import LinkInternal from 'ui/shared/LinkInternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import NovesFromTo from 'ui/shared/Noves/NovesFromTo';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { NovesResponseData } from 'types/api/noves';

import dayjs from 'lib/date/dayjs';
import IconSvg from 'ui/shared/IconSvg';
import LinkInternal from 'ui/shared/LinkInternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
import NovesFromTo from 'ui/shared/Noves/NovesFromTo';

type Props = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { FormSubmitResultWalletClient } from '../types';

import { route } from 'nextjs-routes';

import LinkInternal from 'ui/shared/LinkInternal';
import LinkInternal from 'ui/shared/links/LinkInternal';

interface Props {
result: FormSubmitResultWalletClient['result'];
Expand Down
4 changes: 2 additions & 2 deletions ui/address/contract/ContractCode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ import ContractCertifiedLabel from 'ui/shared/ContractCertifiedLabel';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import Hint from 'ui/shared/Hint';
import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal';
import LinkExternal from 'ui/shared/links/LinkExternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
import RawDataSnippet from 'ui/shared/RawDataSnippet';

import ContractSecurityAudits from './ContractSecurityAudits';
Expand Down
2 changes: 1 addition & 1 deletion ui/address/contract/ContractCodeIdes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React from 'react';

import config from 'configs/app';
import IconSvg from 'ui/shared/IconSvg';
import LinkExternal from 'ui/shared/LinkExternal';
import LinkExternal from 'ui/shared/links/LinkExternal';

interface Props {
className?: string;
Expand Down
2 changes: 1 addition & 1 deletion ui/address/contract/ContractSecurityAudits.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import useApiQuery from 'lib/api/useApiQuery';
import dayjs from 'lib/date/dayjs';
import ContainerWithScrollY from 'ui/shared/ContainerWithScrollY';
import FormModal from 'ui/shared/FormModal';
import LinkExternal from 'ui/shared/LinkExternal';
import LinkExternal from 'ui/shared/links/LinkExternal';

import ContractSubmitAuditForm from './contractSubmitAuditForm/ContractSubmitAuditForm';

Expand Down
2 changes: 1 addition & 1 deletion ui/address/contract/ContractSourceCode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { route } from 'nextjs-routes';
import useApiQuery from 'lib/api/useApiQuery';
import * as stubs from 'stubs/contract';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import LinkInternal from 'ui/shared/LinkInternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
import CodeEditor from 'ui/shared/monaco/CodeEditor';
import formatFilePath from 'ui/shared/monaco/utils/formatFilePath';

Expand Down
2 changes: 1 addition & 1 deletion ui/address/details/AddressBalance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ const AddressBalance = ({ data, isLoading }: Props) => {

return (
<DetailsInfoItem
title="Balance"
title={ `${ currencyUnits.ether } balance` }
hint={ `Address balance in ${ currencyUnits.ether }. Doesn't include ERC20, ERC721 and ERC1155 tokens` }
flexWrap="nowrap"
alignSelf="center"
Expand Down
2 changes: 1 addition & 1 deletion ui/address/details/AddressCounterItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type { AddressCounters } from 'types/api/address';
import { route } from 'nextjs-routes';

import type { ResourceError } from 'lib/api/resources';
import LinkInternal from 'ui/shared/LinkInternal';
import LinkInternal from 'ui/shared/links/LinkInternal';

interface Props {
prop: keyof AddressCounters;
Expand Down
47 changes: 47 additions & 0 deletions ui/address/details/AddressNetWorth.pw.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React from 'react';

import * as addressMock from 'mocks/address/address';
import * as tokensMock from 'mocks/address/tokens';
import { test, expect } from 'playwright/lib';

import AddressNetWorth from './AddressNetWorth';

const ADDRESS_HASH = addressMock.hash;
const ICON_URL = 'https://localhost:3000/my-icon.png';

test.beforeEach(async({ mockApiResponse }) => {
await mockApiResponse('address_tokens', tokensMock.erc20List, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-20' } });
await mockApiResponse('address_tokens', tokensMock.erc721List, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-721' } });
await mockApiResponse('address_tokens', tokensMock.erc1155List, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-1155' } });
await mockApiResponse('address_tokens', tokensMock.erc404List, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-404' } });
});

test('base view', async({ render }) => {
const component = await render(<AddressNetWorth addressData={ addressMock.token } addressHash={ ADDRESS_HASH }/>);

await expect(component).toHaveScreenshot();
});

test('with multichain button internal +@dark-mode', async({ render, mockEnvs, mockAssetResponse }) => {
await mockEnvs([
[
'NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG',
`{"name": "duck", "dapp_id": "duck", "url_template": "https://duck.url/{address}", "logo": "${ ICON_URL }"}` ],
]);
await mockAssetResponse(ICON_URL, './playwright/mocks/image_svg.svg');

const component = await render(<AddressNetWorth addressData={ addressMock.token } addressHash={ ADDRESS_HASH }/>);

await expect(component).toHaveScreenshot();
});

test('with multichain button external', async({ render, mockEnvs, mockAssetResponse }) => {
await mockEnvs([
[ 'NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG', `{"name": "duck", "url_template": "https://duck.url/{address}", "logo": "${ ICON_URL }"}` ],
]);
await mockAssetResponse(ICON_URL, './playwright/mocks/image_svg.svg');

const component = await render(<AddressNetWorth addressData={ addressMock.token } addressHash={ ADDRESS_HASH }/>);

await expect(component).toHaveScreenshot();
});
Loading

0 comments on commit 04f051f

Please sign in to comment.