Commit b1b3ba0b authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into tom2drum/issue-1826

parents bd5cb3ae 365de508
...@@ -4,6 +4,7 @@ const RESTRICTED_MODULES = { ...@@ -4,6 +4,7 @@ const RESTRICTED_MODULES = {
{ name: '@chakra-ui/icons', message: 'Using @chakra-ui/icons is prohibited. Please use regular svg-icon instead (see examples in "icons/" folder)' }, { name: '@chakra-ui/icons', message: 'Using @chakra-ui/icons is prohibited. Please use regular svg-icon instead (see examples in "icons/" folder)' },
{ name: '@metamask/providers', message: 'Please lazy-load @metamask/providers or use useProvider hook instead' }, { name: '@metamask/providers', message: 'Please lazy-load @metamask/providers or use useProvider hook instead' },
{ name: '@metamask/post-message-stream', message: 'Please lazy-load @metamask/post-message-stream or use useProvider hook instead' }, { name: '@metamask/post-message-stream', message: 'Please lazy-load @metamask/post-message-stream or use useProvider hook instead' },
{ name: 'playwright/TestApp', message: 'Please use render() fixture from test() function of playwright/lib module' },
], ],
patterns: [ patterns: [
'icons/*', 'icons/*',
......
...@@ -12,8 +12,8 @@ ...@@ -12,8 +12,8 @@
# next.js # next.js
/.next/ /.next/
/out/ /out/
/public/assets/ /public/assets/envs.js
/public/envs.js /public/assets/configs
/public/icons/sprite.svg /public/icons/sprite.svg
/public/icons/README.md /public/icons/README.md
/analyze /analyze
......
...@@ -34,11 +34,13 @@ RUN yarn --frozen-lockfile ...@@ -34,11 +34,13 @@ RUN yarn --frozen-lockfile
FROM node:20.11.0-alpine AS builder FROM node:20.11.0-alpine AS builder
RUN apk add --no-cache --upgrade libc6-compat bash RUN apk add --no-cache --upgrade libc6-compat bash
# pass commit sha and git tag to the app image # pass build args to env variables
ARG GIT_COMMIT_SHA ARG GIT_COMMIT_SHA
ENV NEXT_PUBLIC_GIT_COMMIT_SHA=$GIT_COMMIT_SHA ENV NEXT_PUBLIC_GIT_COMMIT_SHA=$GIT_COMMIT_SHA
ARG GIT_TAG ARG GIT_TAG
ENV NEXT_PUBLIC_GIT_TAG=$GIT_TAG ENV NEXT_PUBLIC_GIT_TAG=$GIT_TAG
ARG NEXT_OPEN_TELEMETRY_ENABLED
ENV NEXT_OPEN_TELEMETRY_ENABLED=$NEXT_OPEN_TELEMETRY_ENABLED
ENV NODE_ENV production ENV NODE_ENV production
...@@ -58,8 +60,8 @@ RUN ./collect_envs.sh ./docs/ENVS.md ...@@ -58,8 +60,8 @@ RUN ./collect_envs.sh ./docs/ENVS.md
# ENV NEXT_TELEMETRY_DISABLED 1 # ENV NEXT_TELEMETRY_DISABLED 1
# Build app for production # Build app for production
RUN yarn build
RUN yarn svg:build-sprite RUN yarn svg:build-sprite
RUN yarn build
### FEATURE REPORTER ### FEATURE REPORTER
......
...@@ -15,6 +15,7 @@ export { default as growthBook } from './growthBook'; ...@@ -15,6 +15,7 @@ export { default as growthBook } from './growthBook';
export { default as marketplace } from './marketplace'; export { default as marketplace } from './marketplace';
export { default as metasuites } from './metasuites'; export { default as metasuites } from './metasuites';
export { default as mixpanel } from './mixpanel'; export { default as mixpanel } from './mixpanel';
export { default as multichainButton } from './multichainButton';
export { default as nameService } from './nameService'; export { default as nameService } from './nameService';
export { default as publicTagsSubmission } from './publicTagsSubmission'; export { default as publicTagsSubmission } from './publicTagsSubmission';
export { default as restApiDocs } from './restApiDocs'; export { default as restApiDocs } from './restApiDocs';
......
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;
...@@ -41,7 +41,7 @@ export const buildExternalAssetFilePath = (name: string, value: string) => { ...@@ -41,7 +41,7 @@ export const buildExternalAssetFilePath = (name: string, value: string) => {
const fileName = name.replace(/^NEXT_PUBLIC_/, '').replace(/_URL$/, '').toLowerCase(); const fileName = name.replace(/^NEXT_PUBLIC_/, '').replace(/_URL$/, '').toLowerCase();
const url = new URL(value); const url = new URL(value);
const fileExtension = url.pathname.match(regexp.FILE_EXTENSION)?.[1]; const fileExtension = url.pathname.match(regexp.FILE_EXTENSION)?.[1];
return `/assets/${ fileName }.${ fileExtension }`; return `/assets/configs/${ fileName }.${ fileExtension }`;
} catch (error) { } catch (error) {
return; return;
} }
......
...@@ -48,13 +48,16 @@ NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-mainnet.safe.global ...@@ -48,13 +48,16 @@ NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-mainnet.safe.global
NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
NEXT_PUBLIC_MARKETPLACE_ENABLED=true NEXT_PUBLIC_MARKETPLACE_ENABLED=true
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json
NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL
NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form
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_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 #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
NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true
NEXT_PUBLIC_SEO_ENHANCED_DATA_ENABLED=true NEXT_PUBLIC_SEO_ENHANCED_DATA_ENABLED=true
...@@ -18,7 +18,7 @@ NEXT_PUBLIC_NETWORK_RPC_URL=https://eth-sepolia.public.blastapi.io ...@@ -18,7 +18,7 @@ NEXT_PUBLIC_NETWORK_RPC_URL=https://eth-sepolia.public.blastapi.io
NEXT_PUBLIC_IS_TESTNET=true NEXT_PUBLIC_IS_TESTNET=true
# api configuration # api configuration
NEXT_PUBLIC_API_HOST=eth-sepolia.blockscout.com NEXT_PUBLIC_API_HOST=eth-sepolia.k8s-dev.blockscout.com
NEXT_PUBLIC_API_BASE_PATH=/ NEXT_PUBLIC_API_BASE_PATH=/
# ui config # ui config
...@@ -47,7 +47,11 @@ NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true ...@@ -47,7 +47,11 @@ NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_AUTH_URL=http://localhost:3000/login NEXT_PUBLIC_AUTH_URL=http://localhost:3000/login
NEXT_PUBLIC_LOGOUT_URL=https://blockscout-goerli.us.auth0.com/v2/logout NEXT_PUBLIC_LOGOUT_URL=https://blockscout-goerli.us.auth0.com/v2/logout
NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace/eth-goerli.json NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/marketplace/eth-goerli.json
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-security-reports/default.json
NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json
NEXT_PUBLIC_MARKETPLACE_ENABLED=true
NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL
NEXT_PUBLIC_STATS_API_HOST=https://stats-sepolia.k8s-dev.blockscout.com NEXT_PUBLIC_STATS_API_HOST=https://stats-sepolia.k8s-dev.blockscout.com
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
......
...@@ -36,7 +36,7 @@ export_envs_from_preset() { ...@@ -36,7 +36,7 @@ export_envs_from_preset() {
export_envs_from_preset export_envs_from_preset
# Download external assets # Download external assets
./download_assets.sh ./public/assets ./download_assets.sh ./public/assets/configs
# Check run-time ENVs values # Check run-time ENVs values
./validate_envs.sh ./validate_envs.sh
......
...@@ -10,7 +10,7 @@ if [ $? -ne 0 ]; then ...@@ -10,7 +10,7 @@ if [ $? -ne 0 ]; then
exit 1 exit 1
else else
cd ../../../ cd ../../../
favicon_folder="./public/favicon/" favicon_folder="./public/assets/favicon/"
echo "⏳ Replacing default favicons with freshly generated pack..." echo "⏳ Replacing default favicons with freshly generated pack..."
if [ -d "$favicon_folder" ]; then if [ -d "$favicon_folder" ]; then
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
echo "🌀 Creating client script with ENV values..." echo "🌀 Creating client script with ENV values..."
# Define the output file name # Define the output file name
output_file="${1:-./public/envs.js}" output_file="${1:-./public/assets/envs.js}"
touch $output_file; touch $output_file;
truncate -s 0 $output_file; truncate -s 0 $output_file;
......
...@@ -15,6 +15,7 @@ import type { ContractCodeIde } from '../../../types/client/contract'; ...@@ -15,6 +15,7 @@ import type { ContractCodeIde } from '../../../types/client/contract';
import { GAS_UNITS } from '../../../types/client/gasTracker'; import { GAS_UNITS } from '../../../types/client/gasTracker';
import type { GasUnit } from '../../../types/client/gasTracker'; import type { GasUnit } from '../../../types/client/gasTracker';
import type { MarketplaceAppOverview, MarketplaceAppSecurityReportRaw, MarketplaceAppSecurityReport } from '../../../types/client/marketplace'; 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 { NAVIGATION_LINK_IDS } from '../../../types/client/navigation-items';
import type { NavItemExternal, NavigationLinkId } from '../../../types/client/navigation-items'; import type { NavItemExternal, NavigationLinkId } from '../../../types/client/navigation-items';
import { ROLLUP_TYPES } from '../../../types/client/rollup'; import { ROLLUP_TYPES } from '../../../types/client/rollup';
...@@ -606,6 +607,19 @@ const schema = yup ...@@ -606,6 +607,19 @@ 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_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_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)),
......
...@@ -9,7 +9,7 @@ export NEXT_PUBLIC_GIT_TAG=$(git describe --tags --abbrev=0) ...@@ -9,7 +9,7 @@ export NEXT_PUBLIC_GIT_TAG=$(git describe --tags --abbrev=0)
../../scripts/collect_envs.sh ../../../docs/ENVS.md ../../scripts/collect_envs.sh ../../../docs/ENVS.md
# Copy test assets # Copy test assets
mkdir -p "./public/assets" mkdir -p "./public/assets/configs"
cp -r ${test_folder}/assets ./public/ cp -r ${test_folder}/assets ./public/
# Build validator script # Build validator script
......
...@@ -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_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'}
...@@ -57,6 +57,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will ...@@ -57,6 +57,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will
- [Sentry error monitoring](ENVS.md#sentry-error-monitoring) - [Sentry error monitoring](ENVS.md#sentry-error-monitoring)
- [OpenTelemetry](ENVS.md#opentelemetry) - [OpenTelemetry](ENVS.md#opentelemetry)
- [Swap button](ENVS.md#swap-button) - [Swap button](ENVS.md#swap-button)
- [Multichain balance button](ENVS.md#multichain-button)
- [3rd party services configuration](ENVS.md#external-services-configuration) - [3rd party services configuration](ENVS.md#external-services-configuration)
&nbsp; &nbsp;
...@@ -672,11 +673,11 @@ The feature enables the Validators page which provides detailed information abou ...@@ -672,11 +673,11 @@ The feature enables the Validators page which provides detailed information abou
### OpenTelemetry ### OpenTelemetry
OpenTelemetry SDK for Node.js app could be enabled by passing `OTEL_SDK_ENABLED=true` variable. Configure the OpenTelemetry Protocol Exporter by using the generic environment variables described in the [OT docs](https://opentelemetry.io/docs/specs/otel/protocol/exporter/#configuration-options). OpenTelemetry SDK for Node.js app could be enabled by passing `OTEL_SDK_ENABLED=true` variable. Configure the OpenTelemetry Protocol Exporter by using the generic environment variables described in the [OT docs](https://opentelemetry.io/docs/specs/otel/protocol/exporter/#configuration-options). Note that this Next.js feature is currently experimental. The Docker image should be built with the `NEXT_OPEN_TELEMETRY_ENABLED=true` argument to enable it.
| Variable | Type| Description | Compulsoriness | Default value | Example value | | Variable | Type| Description | Compulsoriness | Default value | Example value |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| OTEL_SDK_ENABLED | `boolean` | Flag to enable the feature | Required | `false` | `true` | | OTEL_SDK_ENABLED | `boolean` | Run-time flag to enable the feature | Required | `false` | `true` |
&nbsp; &nbsp;
...@@ -690,6 +691,27 @@ If the feature is enabled, a Swap button will be displayed at the top of the exp ...@@ -690,6 +691,27 @@ If the feature is enabled, a Swap button will be displayed at the top of the exp
&nbsp; &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 ## External services configuration
### Google ReCaptcha ### Google ReCaptcha
......
...@@ -46,9 +46,7 @@ const sdk = new NodeSDK({ ...@@ -46,9 +46,7 @@ const sdk = new NodeSDK({
url.pathname.startsWith('/_next/static/') || url.pathname.startsWith('/_next/static/') ||
url.pathname.startsWith('/_next/data/') || url.pathname.startsWith('/_next/data/') ||
url.pathname.startsWith('/assets/') || url.pathname.startsWith('/assets/') ||
url.pathname.startsWith('/static/') || url.pathname.startsWith('/static/')
url.pathname.startsWith('/favicon/') ||
url.pathname.startsWith('/envs.js')
) { ) {
return true; return true;
} }
......
...@@ -16,6 +16,7 @@ export default function parseMetaPayload(meta: AddressMetadataTag['meta']): Addr ...@@ -16,6 +16,7 @@ export default function parseMetaPayload(meta: AddressMetadataTag['meta']): Addr
const stringFields: Array<keyof MetaParsed> = [ const stringFields: Array<keyof MetaParsed> = [
'textColor', 'textColor',
'bgColor', 'bgColor',
'tagIcon',
'tagUrl', 'tagUrl',
'tooltipIcon', 'tooltipIcon',
'tooltipTitle', 'tooltipTitle',
...@@ -25,6 +26,7 @@ export default function parseMetaPayload(meta: AddressMetadataTag['meta']): Addr ...@@ -25,6 +26,7 @@ export default function parseMetaPayload(meta: AddressMetadataTag['meta']): Addr
'appMarketplaceURL', 'appMarketplaceURL',
'appLogoURL', 'appLogoURL',
'appActionButtonText', 'appActionButtonText',
'warpcastHandle',
]; ];
for (const stringField of stringFields) { for (const stringField of stringFields) {
......
import { useQuery } from '@tanstack/react-query';
import type { AddressMetadataInfo } from 'types/api/addressMetadata';
import type { AddressMetadataInfoFormatted, AddressMetadataTagFormatted } from 'types/client/addressMetadata'; import type { AddressMetadataInfoFormatted, AddressMetadataTagFormatted } from 'types/client/addressMetadata';
import config from 'configs/app'; import config from 'configs/app';
import useApiFetch from 'lib/api/useApiFetch'; import useApiQuery from 'lib/api/useApiQuery';
import { getResourceKey } from 'lib/api/useApiQuery';
import parseMetaPayload from './parseMetaPayload'; import parseMetaPayload from './parseMetaPayload';
export default function useAddressMetadataInfoQuery(addresses: Array<string>) { export default function useAddressMetadataInfoQuery(addresses: Array<string>) {
const apiFetch = useApiFetch();
const queryParams = {
addresses,
chainId: config.chain.id,
tagsLimit: '20',
};
const resource = 'address_metadata_info'; const resource = 'address_metadata_info';
// TODO @tom2drum: Improve the typing here return useApiQuery<typeof resource, unknown, AddressMetadataInfoFormatted>(resource, {
// since we are formatting the API data in the select function here queryParams: {
// we cannot use the useApiQuery hook because of its current typing addresses,
// enhance useApiQuery so it can accept an API data and the formatted data types chainId: config.chain.id,
return useQuery<AddressMetadataInfo, unknown, AddressMetadataInfoFormatted>({ tagsLimit: '20',
queryKey: getResourceKey(resource, { queryParams }),
queryFn: async() => {
return apiFetch(resource, { queryParams }) as Promise<AddressMetadataInfo>;
}, },
enabled: addresses.length > 0 && config.features.addressMetadata.isEnabled, queryOptions: {
select: (data) => { enabled: addresses.length > 0 && config.features.addressMetadata.isEnabled,
const addresses = Object.entries(data.addresses) select: (data) => {
.map(([ address, { tags, reputation } ]) => { const addresses = Object.entries(data.addresses)
const formattedTags: Array<AddressMetadataTagFormatted> = tags.map((tag) => ({ ...tag, meta: parseMetaPayload(tag.meta) })); .map(([ address, { tags, reputation } ]) => {
return [ address.toLowerCase(), { tags: formattedTags, reputation } ] as const; const formattedTags: Array<AddressMetadataTagFormatted> = tags.map((tag) => ({ ...tag, meta: parseMetaPayload(tag.meta) }));
}) return [ address.toLowerCase(), { tags: formattedTags, reputation } ] as const;
.reduce((result, item) => { })
result[item[0]] = item[1]; .reduce((result, item) => {
return result; result[item[0]] = item[1];
}, {} as AddressMetadataInfoFormatted['addresses']); return result;
}, {} as AddressMetadataInfoFormatted['addresses']);
return { addresses };
return { addresses };
},
}, },
}); });
} }
...@@ -5,8 +5,8 @@ import type { ResourceError, ResourceName, ResourcePayload } from './resources'; ...@@ -5,8 +5,8 @@ import type { ResourceError, ResourceName, ResourcePayload } from './resources';
import type { Params as ApiFetchParams } from './useApiFetch'; import type { Params as ApiFetchParams } from './useApiFetch';
import useApiFetch from './useApiFetch'; import useApiFetch from './useApiFetch';
export interface Params<R extends ResourceName, E = unknown> extends ApiFetchParams<R> { export interface Params<R extends ResourceName, E = unknown, D = ResourcePayload<R>> extends ApiFetchParams<R> {
queryOptions?: Omit<UseQueryOptions<ResourcePayload<R>, ResourceError<E>, ResourcePayload<R>>, 'queryKey' | 'queryFn'>; queryOptions?: Omit<UseQueryOptions<ResourcePayload<R>, ResourceError<E>, D>, 'queryKey' | 'queryFn'>;
} }
export function getResourceKey<R extends ResourceName>(resource: R, { pathParams, queryParams }: Params<R> = {}) { export function getResourceKey<R extends ResourceName>(resource: R, { pathParams, queryParams }: Params<R> = {}) {
...@@ -17,13 +17,13 @@ export function getResourceKey<R extends ResourceName>(resource: R, { pathParams ...@@ -17,13 +17,13 @@ export function getResourceKey<R extends ResourceName>(resource: R, { pathParams
return [ resource ]; return [ resource ];
} }
export default function useApiQuery<R extends ResourceName, E = unknown>( export default function useApiQuery<R extends ResourceName, E = unknown, D = ResourcePayload<R>>(
resource: R, resource: R,
{ queryOptions, pathParams, queryParams, fetchParams }: Params<R, E> = {}, { queryOptions, pathParams, queryParams, fetchParams }: Params<R, E, D> = {},
) { ) {
const apiFetch = useApiFetch(); const apiFetch = useApiFetch();
return useQuery<ResourcePayload<R>, ResourceError<E>, ResourcePayload<R>>({ return useQuery<ResourcePayload<R>, ResourceError<E>, D>({
// eslint-disable-next-line @tanstack/query/exhaustive-deps // eslint-disable-next-line @tanstack/query/exhaustive-deps
queryKey: getResourceKey(resource, { pathParams, queryParams }), queryKey: getResourceKey(resource, { pathParams, queryParams }),
queryFn: async() => { queryFn: async() => {
......
...@@ -128,7 +128,7 @@ Type extends EventTypes.FILTERS ? { ...@@ -128,7 +128,7 @@ Type extends EventTypes.FILTERS ? {
'Filter name': string; 'Filter name': string;
} : } :
Type extends EventTypes.BUTTON_CLICK ? { Type extends EventTypes.BUTTON_CLICK ? {
'Content': 'Swap button'; 'Content': 'Swap button' | 'Multichain';
'Source': string; 'Source': string;
} : } :
Type extends EventTypes.PROMO_BANNER ? { Type extends EventTypes.PROMO_BANNER ? {
......
...@@ -75,7 +75,7 @@ export const token: Address = { ...@@ -75,7 +75,7 @@ export const token: Address = {
coin_balance: '1', coin_balance: '1',
creation_tx_hash: '0xc38cf7377bf72d6436f63c37b01b24d032101f20ec1849286dc703c712f10c98', creation_tx_hash: '0xc38cf7377bf72d6436f63c37b01b24d032101f20ec1849286dc703c712f10c98',
creator_address_hash: '0x34A9c688512ebdB575e82C50c9803F6ba2916E72', creator_address_hash: '0x34A9c688512ebdB575e82C50c9803F6ba2916E72',
exchange_rate: null, exchange_rate: '0.04311',
implementation_address: null, implementation_address: null,
has_decompiled_code: false, has_decompiled_code: false,
has_logs: false, has_logs: false,
......
import type { AddressCollectionsResponse, AddressNFTsResponse, AddressTokenBalance } from 'types/api/address'; import type { AddressCollectionsResponse, AddressNFTsResponse, AddressTokenBalance, AddressTokensResponse } from 'types/api/address';
import * as tokens from 'mocks/tokens/tokenInfo'; import * as tokens from 'mocks/tokens/tokenInfo';
import * as tokenInstance from 'mocks/tokens/tokenInstance'; import * as tokenInstance from 'mocks/tokens/tokenInstance';
...@@ -119,35 +119,39 @@ export const erc404b: AddressTokenBalance = { ...@@ -119,35 +119,39 @@ export const erc404b: AddressTokenBalance = {
token_id: null, token_id: null,
}; };
export const erc20List = { export const erc20List: AddressTokensResponse = {
items: [ items: [
erc20a, erc20a,
erc20b, erc20b,
erc20c, erc20c,
], ],
next_page_params: null,
}; };
export const erc721List = { export const erc721List: AddressTokensResponse = {
items: [ items: [
erc721a, erc721a,
erc721b, erc721b,
erc721c, erc721c,
], ],
next_page_params: null,
}; };
export const erc1155List = { export const erc1155List: AddressTokensResponse = {
items: [ items: [
erc1155withoutName, erc1155withoutName,
erc1155a, erc1155a,
erc1155b, erc1155b,
], ],
next_page_params: null,
}; };
export const erc404List = { export const erc404List: AddressTokensResponse = {
items: [ items: [
erc404a, erc404a,
erc404b, erc404b,
], ],
next_page_params: null,
}; };
export const nfts: AddressNFTsResponse = { export const nfts: AddressNFTsResponse = {
......
...@@ -51,6 +51,11 @@ export const verified: SmartContract = { ...@@ -51,6 +51,11 @@ export const verified: SmartContract = {
minimal_proxy_address_hash: null, minimal_proxy_address_hash: null,
}; };
export const certified: SmartContract = {
...verified,
certified: true,
};
export const withMultiplePaths: SmartContract = { export const withMultiplePaths: SmartContract = {
...verified, ...verified,
file_path: './simple_storage.sol', file_path: './simple_storage.sol',
......
export const solidityscanReportAverage = { import type { SolidityscanReport } from 'types/api/contract';
export const solidityscanReportAverage: SolidityscanReport = {
scan_report: { scan_report: {
contractname: 'foo',
scan_status: 'scan_done', scan_status: 'scan_done',
scan_summary: { scan_summary: {
issue_severity_distribution: { issue_severity_distribution: {
...@@ -20,8 +23,9 @@ export const solidityscanReportAverage = { ...@@ -20,8 +23,9 @@ export const solidityscanReportAverage = {
}, },
}; };
export const solidityscanReportGreat = { export const solidityscanReportGreat: SolidityscanReport = {
scan_report: { scan_report: {
contractname: 'foo',
scan_status: 'scan_done', scan_status: 'scan_done',
scan_summary: { scan_summary: {
issue_severity_distribution: { issue_severity_distribution: {
...@@ -42,8 +46,9 @@ export const solidityscanReportGreat = { ...@@ -42,8 +46,9 @@ export const solidityscanReportGreat = {
}, },
}; };
export const solidityscanReportLow = { export const solidityscanReportLow: SolidityscanReport = {
scan_report: { scan_report: {
contractname: 'foo',
scan_status: 'scan_done', scan_status: 'scan_done',
scan_summary: { scan_summary: {
issue_severity_distribution: { issue_severity_distribution: {
......
...@@ -35,6 +35,7 @@ export const contract2: VerifiedContract = { ...@@ -35,6 +35,7 @@ export const contract2: VerifiedContract = {
watchlist_names: [], watchlist_names: [],
ens_domain_name: null, ens_domain_name: null,
}, },
certified: true,
coin_balance: '9078234570352343999', coin_balance: '9078234570352343999',
compiler_version: 'v0.3.1+commit.0463ea4c', compiler_version: 'v0.3.1+commit.0463ea4c',
has_constructor_args: true, has_constructor_args: true,
......
...@@ -76,3 +76,17 @@ export const protocolTagWithMeta: AddressMetadataTagApi = { ...@@ -76,3 +76,17 @@ export const protocolTagWithMeta: AddressMetadataTagApi = {
bgColor: '#FF007A', bgColor: '#FF007A',
}, },
}; };
export const warpcastTag: AddressMetadataTagApi = {
slug: 'warpcast-account',
name: 'Farcaster',
tagType: 'protocol',
ordinal: 0,
meta: {
bgColor: '#8465CB',
tagIcon: 'data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20xmlns%3Axlink%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxlink%22%20viewBox%3D%220%200%2032%2029%22%3E%3Cpath%20d%3D%22M%205.507%200.072%20L%2026.097%200.072%20L%2026.097%204.167%20L%2031.952%204.167%20L%2030.725%208.263%20L%2029.686%208.263%20L%2029.686%2024.833%20C%2030.207%2024.833%2030.63%2025.249%2030.63%2025.763%20L%2030.63%2026.88%20L%2030.819%2026.88%20C%2031.341%2026.88%2031.764%2027.297%2031.764%2027.811%20L%2031.764%2028.928%20L%2021.185%2028.928%20L%2021.185%2027.811%20C%2021.185%2027.297%2021.608%2026.88%2022.13%2026.88%20L%2022.319%2026.88%20L%2022.319%2025.763%20C%2022.319%2025.316%2022.639%2024.943%2023.065%2024.853%20L%2023.045%2015.71%20C%2022.711%2012.057%2019.596%209.194%2015.802%209.194%20C%2012.008%209.194%208.893%2012.057%208.559%2015.71%20L%208.539%2024.845%20C%209.043%2024.919%209.663%2025.302%209.663%2025.763%20L%209.663%2026.88%20L%209.852%2026.88%20C%2010.373%2026.88%2010.796%2027.297%2010.796%2027.811%20L%2010.796%2028.928%20L%200.218%2028.928%20L%200.218%2027.811%20C%200.218%2027.297%200.641%2026.88%201.162%2026.88%20L%201.351%2026.88%20L%201.351%2025.763%20C%201.351%2025.249%201.774%2024.833%202.296%2024.833%20L%202.296%208.263%20L%201.257%208.263%20L%200.029%204.167%20L%205.507%204.167%20L%205.507%200.072%20Z%22%20fill%3D%22rgb(255%2C%20255%2C%20255)%22%3E%3C%2Fpath%3E%3Cpath%20d%3D%22M%2026.097%200.072%20L%2026.166%200.072%20L%2026.166%200.004%20L%2026.097%200.004%20Z%20M%205.507%200.072%20L%205.507%200.004%20L%205.438%200.004%20L%205.438%200.072%20Z%20M%2026.097%204.167%20L%2026.028%204.167%20L%2026.028%204.235%20L%2026.097%204.235%20Z%20M%2031.952%204.167%20L%2032.019%204.187%20L%2032.045%204.099%20L%2031.952%204.099%20L%2031.952%204.167%20Z%20M%2030.725%208.263%20L%2030.725%208.331%20L%2030.776%208.331%20L%2030.791%208.282%20Z%20M%2029.686%208.263%20L%2029.686%208.195%20L%2029.617%208.195%20L%2029.617%208.263%20Z%20M%2029.686%2024.833%20L%2029.617%2024.833%20L%2029.617%2024.901%20L%2029.686%2024.901%20Z%20M%2030.63%2026.88%20L%2030.561%2026.88%20L%2030.561%2026.948%20L%2030.63%2026.948%20Z%20M%2031.764%2028.928%20L%2031.764%2028.996%20L%2031.832%2028.996%20L%2031.832%2028.928%20Z%20M%2021.185%2028.928%20L%2021.116%2028.928%20L%2021.116%2028.996%20L%2021.185%2028.996%20Z%20M%2022.319%2026.88%20L%2022.319%2026.948%20L%2022.388%2026.948%20L%2022.388%2026.88%20Z%20M%2023.065%2024.853%20L%2023.08%2024.919%20L%2023.134%2024.908%20L%2023.134%2024.853%20Z%20M%2023.045%2015.71%20L%2023.114%2015.71%20L%2023.114%2015.707%20L%2023.113%2015.704%20Z%20M%208.559%2015.71%20L%208.49%2015.704%20L%208.49%2015.707%20L%208.49%2015.71%20Z%20M%208.539%2024.845%20L%208.47%2024.845%20L%208.469%2024.904%20L%208.528%2024.913%20Z%20M%209.663%2026.88%20L%209.594%2026.88%20L%209.594%2026.948%20L%209.663%2026.948%20Z%20M%2010.796%2028.928%20L%2010.796%2028.996%20L%2010.865%2028.996%20L%2010.865%2028.928%20Z%20M%200.218%2028.928%20L%200.149%2028.928%20L%200.149%2028.996%20L%200.218%2028.996%20Z%20M%201.351%2026.88%20L%201.351%2026.948%20L%201.42%2026.948%20L%201.42%2026.88%20Z%20M%202.296%2024.833%20L%202.296%2024.901%20L%202.365%2024.901%20L%202.365%2024.833%20Z%20M%202.296%208.263%20L%202.365%208.263%20L%202.365%208.195%20L%202.296%208.195%20Z%20M%201.257%208.263%20L%201.191%208.282%20L%201.205%208.331%20L%201.257%208.331%20Z%20M%200.029%204.167%20L%200.029%204.1%20L%20-0.063%204.1%20L%20-0.037%204.187%20L%200.029%204.167%20Z%20M%205.507%204.167%20L%205.507%204.235%20L%205.576%204.235%20L%205.576%204.167%20Z%20M%2026.097%200.004%20L%205.507%200.004%20L%205.507%200.139%20L%2026.097%200.139%20Z%20M%2026.166%204.167%20L%2026.166%200.072%20L%2026.028%200.072%20L%2026.028%204.167%20L%2026.166%204.167%20Z%20M%2031.952%204.099%20L%2026.097%204.099%20L%2026.097%204.235%20L%2031.952%204.235%20Z%20M%2030.791%208.282%20L%2032.019%204.187%20L%2031.886%204.148%20L%2030.658%208.244%20Z%20M%2029.686%208.331%20L%2030.725%208.331%20L%2030.725%208.195%20L%2029.686%208.195%20Z%20M%2029.755%2024.833%20L%2029.755%208.263%20L%2029.617%208.263%20L%2029.617%2024.833%20Z%20M%2030.699%2025.763%20C%2030.699%2025.212%2030.245%2024.765%2029.686%2024.765%20L%2029.686%2024.9%20C%2030.169%2024.9%2030.561%2025.287%2030.561%2025.763%20Z%20M%2030.699%2026.88%20L%2030.699%2025.763%20L%2030.561%2025.763%20L%2030.561%2026.88%20Z%20M%2030.819%2026.813%20L%2030.63%2026.813%20L%2030.63%2026.948%20L%2030.819%2026.948%20Z%20M%2031.832%2027.811%20C%2031.832%2027.26%2031.379%2026.813%2030.819%2026.813%20L%2030.819%2026.948%20C%2031.303%2026.948%2031.695%2027.335%2031.695%2027.811%20Z%20M%2031.832%2028.928%20L%2031.832%2027.811%20L%2031.695%2027.811%20L%2031.695%2028.928%20Z%20M%2026.097%2028.996%20L%2031.764%2028.996%20L%2031.764%2028.86%20L%2026.097%2028.86%20Z%20M%2023.074%2028.996%20L%2026.097%2028.996%20L%2026.097%2028.86%20L%2023.074%2028.86%20Z%20M%2021.185%2028.996%20L%2023.074%2028.996%20L%2023.074%2028.86%20L%2021.185%2028.86%20Z%20M%2021.116%2027.811%20L%2021.116%2028.928%20L%2021.254%2028.928%20L%2021.254%2027.811%20Z%20M%2022.13%2026.813%20C%2021.57%2026.813%2021.116%2027.26%2021.116%2027.811%20L%2021.254%2027.811%20C%2021.254%2027.335%2021.646%2026.948%2022.13%2026.948%20Z%20M%2022.319%2026.813%20L%2022.13%2026.813%20L%2022.13%2026.948%20L%2022.319%2026.948%20Z%20M%2022.25%2025.763%20L%2022.25%2026.88%20L%2022.388%2026.88%20L%2022.388%2025.763%20Z%20M%2023.051%2024.787%20C%2022.593%2024.883%2022.25%2025.284%2022.25%2025.763%20L%2022.388%2025.763%20C%2022.388%2025.349%2022.684%2025.003%2023.08%2024.919%20Z%20M%2022.976%2015.71%20L%2022.996%2024.853%20L%2023.134%2024.853%20L%2023.114%2015.71%20Z%20M%2015.802%209.262%20C%2019.559%209.262%2022.645%2012.098%2022.976%2015.716%20L%2023.113%2015.704%20C%2022.776%2012.016%2019.632%209.126%2015.802%209.126%20Z%20M%208.628%2015.716%20C%208.959%2012.098%2012.044%209.262%2015.802%209.262%20L%2015.802%209.126%20C%2011.972%209.126%208.828%2012.016%208.49%2015.704%20Z%20M%208.608%2024.845%20L%208.628%2015.71%20L%208.49%2015.71%20L%208.47%2024.845%20Z%20M%209.732%2025.763%20C%209.732%2025.502%209.557%2025.273%209.331%2025.105%20C%209.104%2024.935%208.812%2024.817%208.549%2024.778%20L%208.528%2024.912%20C%208.769%2024.948%209.039%2025.057%209.248%2025.213%20C%209.459%2025.37%209.594%2025.563%209.594%2025.763%20Z%20M%209.732%2026.88%20L%209.732%2025.763%20L%209.594%2025.763%20L%209.594%2026.88%20Z%20M%209.852%2026.813%20L%209.663%2026.813%20L%209.663%2026.948%20L%209.852%2026.948%20Z%20M%2010.865%2027.811%20C%2010.865%2027.26%2010.411%2026.813%209.852%2026.813%20L%209.852%2026.948%20C%2010.335%2026.948%2010.727%2027.335%2010.727%2027.811%20Z%20M%2010.865%2028.928%20L%2010.865%2027.811%20L%2010.727%2027.811%20L%2010.727%2028.928%20Z%20M%208.529%2028.996%20L%2010.796%2028.996%20L%2010.796%2028.86%20L%208.529%2028.86%20Z%20M%208.372%2028.996%20L%208.529%2028.996%20L%208.529%2028.86%20L%208.372%2028.86%20Z%20M%205.507%2028.996%20L%208.372%2028.996%20L%208.372%2028.86%20L%205.507%2028.86%20Z%20M%200.218%2028.996%20L%205.507%2028.996%20L%205.507%2028.86%20L%200.218%2028.86%20Z%20M%200.149%2027.811%20L%200.149%2028.928%20L%200.287%2028.928%20L%200.287%2027.811%20Z%20M%201.162%2026.813%20C%200.603%2026.813%200.149%2027.26%200.149%2027.811%20L%200.287%2027.811%20C%200.287%2027.335%200.679%2026.948%201.162%2026.948%20Z%20M%201.351%2026.813%20L%201.162%2026.813%20L%201.162%2026.948%20L%201.351%2026.948%20Z%20M%201.282%2025.763%20L%201.282%2026.88%20L%201.42%2026.88%20L%201.42%2025.763%20Z%20M%202.296%2024.765%20C%201.736%2024.765%201.282%2025.212%201.282%2025.763%20L%201.42%2025.763%20C%201.42%2025.287%201.812%2024.9%202.296%2024.9%20Z%20M%202.227%208.263%20L%202.227%2024.833%20L%202.365%2024.833%20L%202.365%208.263%20Z%20M%201.257%208.331%20L%202.296%208.331%20L%202.296%208.195%20L%201.257%208.195%20Z%20M%20-0.037%204.187%20L%201.191%208.282%20L%201.323%208.244%20L%200.095%204.148%20Z%20M%205.507%204.099%20L%200.029%204.099%20L%200.029%204.235%20L%205.507%204.235%20L%205.507%204.099%20Z%20M%205.438%200.072%20L%205.438%204.167%20L%205.576%204.167%20L%205.576%200.072%20Z%22%20fill%3D%22rgb(255%2C255%2C255)%22%3E%3C%2Fpath%3E%3C%2Fsvg%3E', tagUrl: 'https://warpcast.com/mbj357',
textColor: '#FFFFFF',
tooltipDescription: 'This address is linked to a Farcaster account',
warpcastHandle: 'duckYduck',
},
};
...@@ -96,6 +96,15 @@ export const contract1: SearchResultAddressOrContract = { ...@@ -96,6 +96,15 @@ export const contract1: SearchResultAddressOrContract = {
url: '/address/0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a', url: '/address/0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a',
}; };
export const contract2: SearchResultAddressOrContract = {
address: '0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a',
name: 'Super utko',
type: 'contract' as const,
is_smart_contract_verified: true,
certified: true,
url: '/address/0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a',
};
export const label1: SearchResultLabel = { export const label1: SearchResultLabel = {
address: '0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a', address: '0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a',
name: 'utko', name: 'utko',
......
import _mapValues from 'lodash/mapValues'; import _mapValues from 'lodash/mapValues';
export const base = { import type { HomeStats } from 'types/api/stats';
export const base: HomeStats = {
average_block_time: 6212.0, average_block_time: 6212.0,
coin_price: '0.00199678', coin_price: '0.00199678',
coin_price_change_percentage: -7.42, coin_price_change_percentage: -7.42,
coin_image: 'http://localhost:3100/utia.jpg',
gas_prices: { gas_prices: {
average: { average: {
fiat_price: '1.39', fiat_price: '1.39',
...@@ -41,35 +44,42 @@ export const base = { ...@@ -41,35 +44,42 @@ export const base = {
tvl: '1767425.102766552', tvl: '1767425.102766552',
}; };
export const withBtcLocked = { export const withBtcLocked: HomeStats = {
...base, ...base,
rootstock_locked_btc: '3337493406696977561374', rootstock_locked_btc: '3337493406696977561374',
}; };
export const withoutFiatPrices = { export const withoutFiatPrices: HomeStats = {
...base, ...base,
gas_prices: _mapValues(base.gas_prices, (price) => price ? ({ ...price, fiat_price: null }) : null), gas_prices: _mapValues(base.gas_prices, (price) => price ? ({ ...price, fiat_price: null }) : null),
}; };
export const withoutGweiPrices = { export const withoutGweiPrices: HomeStats = {
...base, ...base,
gas_prices: _mapValues(base.gas_prices, (price) => price ? ({ ...price, price: null }) : null), gas_prices: _mapValues(base.gas_prices, (price) => price ? ({ ...price, price: null }) : null),
}; };
export const withoutBothPrices = { export const withoutBothPrices: HomeStats = {
...base, ...base,
gas_prices: _mapValues(base.gas_prices, (price) => price ? ({ ...price, price: null, fiat_price: null }) : null), gas_prices: _mapValues(base.gas_prices, (price) => price ? ({ ...price, price: null, fiat_price: null }) : null),
}; };
export const withSecondaryCoin = { export const withSecondaryCoin: HomeStats = {
...base, ...base,
secondary_coin_price: '3.398', secondary_coin_price: '3.398',
}; };
export const noChartData = { export const noChartData: HomeStats = {
...base, ...base,
transactions_today: null, transactions_today: null,
coin_price: null, coin_price: null,
market_cap: null, market_cap: null,
tvl: null, tvl: null,
}; };
export const indexingStatus = {
finished_indexing_blocks: false,
indexed_blocks_ratio: '0.1',
finished_indexing: true,
indexed_internal_transactions_ratio: '1',
};
...@@ -8,7 +8,7 @@ export const tokenInfo: TokenInfo = { ...@@ -8,7 +8,7 @@ export const tokenInfo: TokenInfo = {
holders: '46554', holders: '46554',
name: 'ARIANEE', name: 'ARIANEE',
symbol: 'ARIA', symbol: 'ARIA',
type: 'ERC-20', type: 'ERC-20' as const,
total_supply: '1235', total_supply: '1235',
icon_url: 'http://localhost:3000/token-icon.png', icon_url: 'http://localhost:3000/token-icon.png',
}; };
...@@ -27,7 +27,7 @@ export const tokenInfoERC20a: TokenInfo<'ERC-20'> = { ...@@ -27,7 +27,7 @@ export const tokenInfoERC20a: TokenInfo<'ERC-20'> = {
name: 'hyfi.token', name: 'hyfi.token',
symbol: 'HyFi', symbol: 'HyFi',
total_supply: '369000000000000000000000000', total_supply: '369000000000000000000000000',
type: 'ERC-20', type: 'ERC-20' as const,
icon_url: 'http://localhost:3000/token-icon.png', icon_url: 'http://localhost:3000/token-icon.png',
}; };
...@@ -40,7 +40,7 @@ export const tokenInfoERC20b: TokenInfo<'ERC-20'> = { ...@@ -40,7 +40,7 @@ export const tokenInfoERC20b: TokenInfo<'ERC-20'> = {
name: 'USD Coin', name: 'USD Coin',
symbol: 'USDC', symbol: 'USDC',
total_supply: '900000000000000000000000000', total_supply: '900000000000000000000000000',
type: 'ERC-20', type: 'ERC-20' as const,
icon_url: null, icon_url: null,
}; };
...@@ -53,7 +53,7 @@ export const tokenInfoERC20c: TokenInfo<'ERC-20'> = { ...@@ -53,7 +53,7 @@ export const tokenInfoERC20c: TokenInfo<'ERC-20'> = {
name: 'Ethereum', name: 'Ethereum',
symbol: 'ETH', symbol: 'ETH',
total_supply: '1000000000000000000000000', total_supply: '1000000000000000000000000',
type: 'ERC-20', type: 'ERC-20' as const,
icon_url: null, icon_url: null,
}; };
...@@ -66,7 +66,7 @@ export const tokenInfoERC20d: TokenInfo<'ERC-20'> = { ...@@ -66,7 +66,7 @@ export const tokenInfoERC20d: TokenInfo<'ERC-20'> = {
name: 'Zeta', name: 'Zeta',
symbol: 'ZETA', symbol: 'ZETA',
total_supply: '2100000000000000000000000000', total_supply: '2100000000000000000000000000',
type: 'ERC-20', type: 'ERC-20' as const,
icon_url: null, icon_url: null,
}; };
...@@ -79,7 +79,7 @@ export const tokenInfoERC20LongSymbol: TokenInfo<'ERC-20'> = { ...@@ -79,7 +79,7 @@ export const tokenInfoERC20LongSymbol: TokenInfo<'ERC-20'> = {
name: 'Zeta', name: 'Zeta',
symbol: 'ipfs://QmUpFUfVKDCWeZQk5pvDFUxnpQP9N6eLSHhNUy49T1JVtY', symbol: 'ipfs://QmUpFUfVKDCWeZQk5pvDFUxnpQP9N6eLSHhNUy49T1JVtY',
total_supply: '2100000000000000000000000000', total_supply: '2100000000000000000000000000',
type: 'ERC-20', type: 'ERC-20' as const,
icon_url: null, icon_url: null,
}; };
...@@ -92,7 +92,7 @@ export const tokenInfoERC721a: TokenInfo<'ERC-721'> = { ...@@ -92,7 +92,7 @@ export const tokenInfoERC721a: TokenInfo<'ERC-721'> = {
name: 'HyFi Athena', name: 'HyFi Athena',
symbol: 'HYFI_ATHENA', symbol: 'HYFI_ATHENA',
total_supply: '105', total_supply: '105',
type: 'ERC-721', type: 'ERC-721' as const,
icon_url: null, icon_url: null,
}; };
...@@ -105,7 +105,7 @@ export const tokenInfoERC721b: TokenInfo<'ERC-721'> = { ...@@ -105,7 +105,7 @@ export const tokenInfoERC721b: TokenInfo<'ERC-721'> = {
name: 'World Of Women Galaxy', name: 'World Of Women Galaxy',
symbol: 'WOWG', symbol: 'WOWG',
total_supply: null, total_supply: null,
type: 'ERC-721', type: 'ERC-721' as const,
icon_url: null, icon_url: null,
}; };
...@@ -118,7 +118,7 @@ export const tokenInfoERC721c: TokenInfo<'ERC-721'> = { ...@@ -118,7 +118,7 @@ export const tokenInfoERC721c: TokenInfo<'ERC-721'> = {
name: 'Puma', name: 'Puma',
symbol: 'PUMA', symbol: 'PUMA',
total_supply: null, total_supply: null,
type: 'ERC-721', type: 'ERC-721' as const,
icon_url: null, icon_url: null,
}; };
...@@ -131,7 +131,7 @@ export const tokenInfoERC721LongSymbol: TokenInfo<'ERC-721'> = { ...@@ -131,7 +131,7 @@ export const tokenInfoERC721LongSymbol: TokenInfo<'ERC-721'> = {
name: 'Puma', name: 'Puma',
symbol: 'ipfs://QmUpFUfVKDCWeZQk5pvDFUxnpQP9N6eLSHhNUy49T1JVtY', symbol: 'ipfs://QmUpFUfVKDCWeZQk5pvDFUxnpQP9N6eLSHhNUy49T1JVtY',
total_supply: null, total_supply: null,
type: 'ERC-721', type: 'ERC-721' as const,
icon_url: null, icon_url: null,
}; };
...@@ -144,7 +144,7 @@ export const tokenInfoERC1155a: TokenInfo<'ERC-1155'> = { ...@@ -144,7 +144,7 @@ export const tokenInfoERC1155a: TokenInfo<'ERC-1155'> = {
name: 'HyFi Membership', name: 'HyFi Membership',
symbol: 'HYFI_MEMBERSHIP', symbol: 'HYFI_MEMBERSHIP',
total_supply: '482', total_supply: '482',
type: 'ERC-1155', type: 'ERC-1155' as const,
icon_url: null, icon_url: null,
}; };
...@@ -157,7 +157,7 @@ export const tokenInfoERC1155b: TokenInfo<'ERC-1155'> = { ...@@ -157,7 +157,7 @@ export const tokenInfoERC1155b: TokenInfo<'ERC-1155'> = {
name: 'WinkyVerse Collections', name: 'WinkyVerse Collections',
symbol: 'WVC', symbol: 'WVC',
total_supply: '4943', total_supply: '4943',
type: 'ERC-1155', type: 'ERC-1155' as const,
icon_url: null, icon_url: null,
}; };
...@@ -170,7 +170,7 @@ export const tokenInfoERC1155WithoutName: TokenInfo<'ERC-1155'> = { ...@@ -170,7 +170,7 @@ export const tokenInfoERC1155WithoutName: TokenInfo<'ERC-1155'> = {
name: null, name: null,
symbol: null, symbol: null,
total_supply: '482', total_supply: '482',
type: 'ERC-1155', type: 'ERC-1155' as const,
icon_url: null, icon_url: null,
}; };
...@@ -184,7 +184,7 @@ export const tokenInfoERC404: TokenInfo<'ERC-404'> = { ...@@ -184,7 +184,7 @@ export const tokenInfoERC404: TokenInfo<'ERC-404'> = {
name: 'OMNI404', name: 'OMNI404',
symbol: 'O404', symbol: 'O404',
total_supply: '6482275000000000000', total_supply: '6482275000000000000',
type: 'ERC-404', type: 'ERC-404' as const,
}; };
export const bridgedTokenA: TokenInfo<'ERC-20'> = { export const bridgedTokenA: TokenInfo<'ERC-20'> = {
......
import type { TxStateChange } from 'types/api/txStateChanges'; import type { TxStateChange, TxStateChanges } from 'types/api/txStateChanges';
export const mintToken: TxStateChange = { export const mintToken: TxStateChange = {
address: { address: {
...@@ -35,7 +35,7 @@ export const mintToken: TxStateChange = { ...@@ -35,7 +35,7 @@ export const mintToken: TxStateChange = {
type: 'ERC-721', type: 'ERC-721',
icon_url: null, icon_url: null,
}, },
type: 'token', type: 'token' as const,
}; };
export const receiveMintedToken: TxStateChange = { export const receiveMintedToken: TxStateChange = {
...@@ -73,7 +73,7 @@ export const receiveMintedToken: TxStateChange = { ...@@ -73,7 +73,7 @@ export const receiveMintedToken: TxStateChange = {
type: 'ERC-721', type: 'ERC-721',
icon_url: null, icon_url: null,
}, },
type: 'token', type: 'token' as const,
}; };
export const transfer1155Token: TxStateChange = { export const transfer1155Token: TxStateChange = {
...@@ -105,7 +105,7 @@ export const transfer1155Token: TxStateChange = { ...@@ -105,7 +105,7 @@ export const transfer1155Token: TxStateChange = {
type: 'ERC-1155', type: 'ERC-1155',
}, },
token_id: '1', token_id: '1',
type: 'token', type: 'token' as const,
}; };
export const receiveCoin: TxStateChange = { export const receiveCoin: TxStateChange = {
...@@ -125,7 +125,7 @@ export const receiveCoin: TxStateChange = { ...@@ -125,7 +125,7 @@ export const receiveCoin: TxStateChange = {
change: '29726406604060', change: '29726406604060',
is_miner: true, is_miner: true,
token: null, token: null,
type: 'coin', type: 'coin' as const,
}; };
export const sendCoin: TxStateChange = { export const sendCoin: TxStateChange = {
...@@ -145,12 +145,13 @@ export const sendCoin: TxStateChange = { ...@@ -145,12 +145,13 @@ export const sendCoin: TxStateChange = {
change: '-3844844822720562', change: '-3844844822720562',
is_miner: false, is_miner: false,
token: null, token: null,
type: 'coin', type: 'coin' as const,
}; };
export const sendERC20Token = { export const sendERC20Token = {
address: { address: {
hash: '0x7f6479df95Aa3036a3BE02DB6300ea201ABd9981', hash: '0x7f6479df95Aa3036a3BE02DB6300ea201ABd9981',
ens_domain_name: null,
implementation_name: null, implementation_name: null,
is_contract: false, is_contract: false,
is_verified: false, is_verified: false,
...@@ -173,13 +174,13 @@ export const sendERC20Token = { ...@@ -173,13 +174,13 @@ export const sendERC20Token = {
name: 'Tether USD', name: 'Tether USD',
symbol: 'USDT', symbol: 'USDT',
total_supply: '39030615894320966', total_supply: '39030615894320966',
type: 'ERC-20', type: 'ERC-20' as const,
token_id: null, token_id: null,
}, },
type: 'token', type: 'token' as const,
}; };
export const baseResponse = { export const baseResponse: TxStateChanges = {
items: [ items: [
mintToken, mintToken,
receiveMintedToken, receiveMintedToken,
......
/* eslint-disable max-len */
import type { UserOp } from 'types/api/userOps'; import type { UserOp } from 'types/api/userOps';
export const userOpData: UserOp = { export const userOpData: UserOp = {
...@@ -47,7 +48,6 @@ export const userOpData: UserOp = { ...@@ -47,7 +48,6 @@ export const userOpData: UserOp = {
max_fee_per_gas: '1575000898', max_fee_per_gas: '1575000898',
max_priority_fee_per_gas: '1575000898', max_priority_fee_per_gas: '1575000898',
nonce: '79', nonce: '79',
// eslint-disable-next-line max-len
paymaster_and_data: '0x7cea357b5ac0639f89f9e378a1f03aa5005c0a250000000000000000000000000000000000000000000000000000000065b3a8800000000000000000000000000000000000000000000000000000000065aa6e0028fa4c57ac1141bc9ecd8c9243f618ade8ea1db10ab6c1d1798a222a824764ff2269a72ae7a3680fa8b03a80d8a00cdc710eaf37afdcc55f8c9c4defa3fdf2471b', paymaster_and_data: '0x7cea357b5ac0639f89f9e378a1f03aa5005c0a250000000000000000000000000000000000000000000000000000000065b3a8800000000000000000000000000000000000000000000000000000000065aa6e0028fa4c57ac1141bc9ecd8c9243f618ade8ea1db10ab6c1d1798a222a824764ff2269a72ae7a3680fa8b03a80d8a00cdc710eaf37afdcc55f8c9c4defa3fdf2471b',
pre_verification_gas: '48396', pre_verification_gas: '48396',
sender: '0xF0C14FF4404b188fAA39a3507B388998c10FE284', sender: '0xF0C14FF4404b188fAA39a3507B388998c10FE284',
...@@ -64,8 +64,34 @@ export const userOpData: UserOp = { ...@@ -64,8 +64,34 @@ export const userOpData: UserOp = {
is_verified: null, is_verified: null,
name: null, name: null,
}, },
// eslint-disable-next-line max-len
call_data: '0xb61d27f600000000000000000000000059f6aa952df7f048fd076e33e0ea8bb552d5ffd8000000000000000000000000000000000000000000000000003f3d017500800000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000', call_data: '0xb61d27f600000000000000000000000059f6aa952df7f048fd076e33e0ea8bb552d5ffd8000000000000000000000000000000000000000000000000003f3d017500800000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000',
execute_call_data: '0x3cf80e6c',
decoded_call_data: {
method_call: 'execute(address dest, uint256 value, bytes func)',
method_id: 'b61d27f6',
parameters: [
{
name: 'dest',
type: 'address',
value: '0xb0ccffd05f5a87c4c3ceffaa217900422a249915',
},
{
name: 'value',
type: 'uint256',
value: '0',
},
{
name: 'func',
type: 'bytes',
value: '0x3cf80e6c',
},
],
},
decoded_execute_call_data: {
method_call: 'advanceEpoch()',
method_id: '3cf80e6c',
parameters: [],
},
paymaster: { paymaster: {
ens_domain_name: null, ens_domain_name: null,
hash: '0x7ceA357B5AC0639F89F9e378a1f03Aa5005C0a25', hash: '0x7ceA357B5AC0639F89F9e378a1f03Aa5005C0a25',
......
...@@ -46,7 +46,7 @@ const moduleExports = { ...@@ -46,7 +46,7 @@ const moduleExports = {
output: 'standalone', output: 'standalone',
productionBrowserSourceMaps: true, productionBrowserSourceMaps: true,
experimental: { experimental: {
instrumentationHook: true, instrumentationHook: process.env.NEXT_OPEN_TELEMETRY_ENABLED === 'true',
turbo: { turbo: {
rules: { rules: {
'*.svg': { '*.svg': {
......
...@@ -133,6 +133,10 @@ export function app(): CspDev.DirectiveDescriptor { ...@@ -133,6 +133,10 @@ export function app(): CspDev.DirectiveDescriptor {
'*', '*',
], ],
'frame-ancestors': [
KEY_WORDS.SELF,
],
...((() => { ...((() => {
if (!config.features.sentry.isEnabled) { if (!config.features.sentry.isEnabled) {
return {}; return {};
......
...@@ -44,14 +44,14 @@ class MyDocument extends Document { ...@@ -44,14 +44,14 @@ class MyDocument extends Document {
/> />
{ /* eslint-disable-next-line @next/next/no-sync-scripts */ } { /* eslint-disable-next-line @next/next/no-sync-scripts */ }
<script src="/envs.js"/> <script src="/assets/envs.js"/>
{ /* FAVICON */ } { /* FAVICON */ }
<link rel="icon" href="/favicon/favicon.ico" sizes="48x48"/> <link rel="icon" href="/assets/favicon/favicon.ico" sizes="48x48"/>
<link rel="icon" sizes="32x32" type="image/png" href="/favicon/favicon-32x32.png"/> <link rel="icon" sizes="32x32" type="image/png" href="/assets/favicon/favicon-32x32.png"/>
<link rel="icon" sizes="16x16" type="image/png"href="/favicon/favicon-16x16.png"/> <link rel="icon" sizes="16x16" type="image/png"href="/assets/favicon/favicon-16x16.png"/>
<link rel="apple-touch-icon" href="/favicon/apple-touch-icon-180x180.png"/> <link rel="apple-touch-icon" href="/assets/favicon/apple-touch-icon-180x180.png"/>
<link rel="mask-icon" href="/favicon/safari-pinned-tab.svg"/> <link rel="mask-icon" href="/assets/favicon/safari-pinned-tab.svg"/>
<link rel="preload" as="image" href={ svgSprite.href }/> <link rel="preload" as="image" href={ svgSprite.href }/>
</Head> </Head>
......
...@@ -12,9 +12,10 @@ import type { Props as PageProps } from 'nextjs/getServerSideProps'; ...@@ -12,9 +12,10 @@ import type { Props as PageProps } from 'nextjs/getServerSideProps';
import config from 'configs/app'; import config from 'configs/app';
import { AppContextProvider } from 'lib/contexts/app'; import { AppContextProvider } from 'lib/contexts/app';
import { SocketProvider } from 'lib/socket/context'; import { SocketProvider } from 'lib/socket/context';
import * as app from 'playwright/utils/app';
import theme from 'theme'; import theme from 'theme';
import { port as socketPort } from './utils/socket';
export type Props = { export type Props = {
children: React.ReactNode; children: React.ReactNode;
withSocket?: boolean; withSocket?: boolean;
...@@ -74,7 +75,7 @@ const TestApp = ({ children, withSocket, withWalletClient = true, appContext = d ...@@ -74,7 +75,7 @@ const TestApp = ({ children, withSocket, withWalletClient = true, appContext = d
return ( return (
<ChakraProvider theme={ theme }> <ChakraProvider theme={ theme }>
<QueryClientProvider client={ queryClient }> <QueryClientProvider client={ queryClient }>
<SocketProvider url={ withSocket ? `ws://${ config.app.host }:${ app.socketPort }` : undefined }> <SocketProvider url={ withSocket ? `ws://${ config.app.host }:${ socketPort }` : undefined }>
<AppContextProvider { ...appContext }> <AppContextProvider { ...appContext }>
<GrowthBookProvider> <GrowthBookProvider>
<WalletClientProvider withWalletClient={ withWalletClient }> <WalletClientProvider withWalletClient={ withWalletClient }>
......
import type { test } from '@playwright/experimental-ct-react';
import createContextWithStorage from './createContextWithStorage';
interface Env {
name: string;
value: string;
}
/**
* @deprecated please use mockEnvs fixture
*
* @export
* @param {Array<Env>} envs
* @return {*} {Parameters<typeof test.extend>[0]['context']}
*/
export default function contextWithEnvsFixture(envs: Array<Env>): Parameters<typeof test.extend>[0]['context'] {
return async({ browser }, use) => {
const context = await createContextWithStorage(browser, envs);
await use(context);
await context.close();
};
}
import type { test } from '@playwright/experimental-ct-react';
import createContextWithStorage from './createContextWithStorage';
interface Feature {
id: string;
value: unknown;
}
/**
* @deprecated please use mockFeatures fixture
*
* @export
* @param {Array<Feature>} envs
* @return {*} {Parameters<typeof test.extend>[0]['context']}
*/
export default function contextWithFeaturesFixture(envs: Array<Feature>): Parameters<typeof test.extend>[0]['context'] {
return async({ browser }, use) => {
const storageItems = envs.map(({ id, value }) => ({ name: `pw_feature:${ id }`, value: JSON.stringify(value) }));
const context = await createContextWithStorage(browser, storageItems);
await use(context);
await context.close();
};
}
import type { Browser } from '@playwright/test';
import config from 'configs/app';
/**
* @deprecated please use mockEnvs or mockFeatures fixture
*
* @export
* @param {Browser} browser
* @param {Array<{ name: string; value: string }>} localStorage
* @return {*}
*/
export default async function createContextWithEnvs(browser: Browser, localStorage: Array<{ name: string; value: string }>) {
return browser.newContext({
storageState: {
origins: [
{ origin: config.app.baseUrl, localStorage },
],
cookies: [],
},
});
}
...@@ -6,6 +6,7 @@ import type { ResourceName, ResourcePayload } from 'lib/api/resources'; ...@@ -6,6 +6,7 @@ import type { ResourceName, ResourcePayload } from 'lib/api/resources';
interface Options<R extends ResourceName> { interface Options<R extends ResourceName> {
pathParams?: Parameters<typeof buildUrl<R>>[1]; pathParams?: Parameters<typeof buildUrl<R>>[1];
queryParams?: Parameters<typeof buildUrl<R>>[2]; queryParams?: Parameters<typeof buildUrl<R>>[2];
times?: number;
} }
export type MockApiResponseFixture = <R extends ResourceName>(resourceName: R, responseMock: ResourcePayload<R>, options?: Options<R>) => Promise<string>; export type MockApiResponseFixture = <R extends ResourceName>(resourceName: R, responseMock: ResourcePayload<R>, options?: Options<R>) => Promise<string>;
...@@ -17,7 +18,7 @@ const fixture: TestFixture<MockApiResponseFixture, { page: Page }> = async({ pag ...@@ -17,7 +18,7 @@ const fixture: TestFixture<MockApiResponseFixture, { page: Page }> = async({ pag
await page.route(apiUrl, (route) => route.fulfill({ await page.route(apiUrl, (route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify(responseMock), body: JSON.stringify(responseMock),
})); }), { times: options?.times });
return apiUrl; return apiUrl;
}); });
......
...@@ -63,4 +63,10 @@ export const ENVS_MAP: Record<string, Array<[string, string]>> = { ...@@ -63,4 +63,10 @@ export const ENVS_MAP: Record<string, Array<[string, string]>> = {
noNftMarketplaces: [ noNftMarketplaces: [
[ 'NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES', '' ], [ 'NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES', '' ],
], ],
dataAvailability: [
[ 'NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED', 'true' ],
],
nameService: [
[ 'NEXT_PUBLIC_NAME_SERVICE_API_HOST', 'https://localhost:3101' ],
],
}; };
/* eslint-disable no-restricted-imports */
import type { MountOptions } from '@playwright/experimental-ct-react'; import type { MountOptions } from '@playwright/experimental-ct-react';
import type { Locator, TestFixture } from '@playwright/test'; import type { Locator, TestFixture } from '@playwright/test';
import type router from 'next/router'; import type router from 'next/router';
......
...@@ -8,7 +8,7 @@ import type { SmartContractVerificationResponse } from 'types/api/contract'; ...@@ -8,7 +8,7 @@ import type { SmartContractVerificationResponse } from 'types/api/contract';
import type { TokenTransfer } from 'types/api/tokenTransfer'; import type { TokenTransfer } from 'types/api/tokenTransfer';
import type { Transaction } from 'types/api/transaction'; import type { Transaction } from 'types/api/transaction';
import * as app from 'playwright/utils/app'; import { port as socketPort } from '../utils/socket';
export type CreateSocketFixture = () => Promise<WebSocket>; export type CreateSocketFixture = () => Promise<WebSocket>;
...@@ -20,7 +20,7 @@ export interface SocketServerFixture { ...@@ -20,7 +20,7 @@ export interface SocketServerFixture {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
export const createSocket: TestFixture<CreateSocketFixture, { page: Page}> = async({ page }, use) => { export const createSocket: TestFixture<CreateSocketFixture, { page: Page}> = async({ page }, use) => {
const socketServer = new WebSocketServer({ port: app.socketPort }); const socketServer = new WebSocketServer({ port: socketPort });
const connectionPromise = new Promise<WebSocket>((resolve) => { const connectionPromise = new Promise<WebSocket>((resolve) => {
socketServer.on('connection', (socket: WebSocket) => { socketServer.on('connection', (socket: WebSocket) => {
......
export const socketPort = 3200;
import { compile } from 'path-to-regexp';
import config from 'configs/app';
import type { ResourceName, ResourcePathParams } from 'lib/api/resources';
import { RESOURCES } from 'lib/api/resources';
/**
* @deprecated please use fixture mockApiResponse from playwright/lib.tsx for rendering test suite
*
* @export
* @template R
* @param {R} resourceName
* @param {ResourcePathParams<R>} [pathParams]
* @return {*} string
*/
export default function buildApiUrl<R extends ResourceName>(resourceName: R, pathParams?: ResourcePathParams<R>) {
const resource = RESOURCES[resourceName];
const origin = 'endpoint' in resource && resource.endpoint ? resource.endpoint + (resource.basePath ?? '') : config.api.endpoint;
return origin + compile(resource.path)(pathParams);
}
/* eslint-disable max-len */
import { devices } from '@playwright/test'; import { devices } from '@playwright/test';
export const viewport = { export const viewport = {
......
export const port = 3200;
**Directories**
- `/icons` - Folder for SVG-sprite assets, generated at build time.
- `/static` - Folder for static assets that are consistent between app re-runs but may differ from one build version to another.
- `/assets` - Folder for dynamically generated assets during the app start, such as the favicon bundle, ENV variables file, and external app configurations.
\ No newline at end of file
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180"> <svg version="1.0" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180">
<path d="M34.9 2.9c-2.9 2.9-2.9 3-2.9 15 0 6.8-.5 13.1-1.1 14.5-1.9 4-6.6 5.6-16.6 5.6C4.7 38 1.7 39.4.5 44.3.2 45.5.1 75.4.2 110.7l.3 64.2 2.3 2.3c2.1 2.1 3.3 2.3 14.6 2.6 14.8.4 17.9-.6 19.5-6.6.7-2.4 1.1-26.2 1.1-66.9V43.1l2.4-2.8c2.3-2.7 2.9-2.8 13.6-3.3 15-.7 15-.6 15-18.2v-13l-2.9-2.9C63.2 0 63.2 0 50.5 0S37.8 0 34.9 2.9zM111.8 1.5c-3.9 2.2-5.1 7.8-4.6 20.4.5 9.8.6 10.6 3.2 12.8 2.3 2 3.8 2.3 10.5 2.3 4.3 0 9.2.5 11 1.1 6.3 2.2 6.1.3 6.1 71.4v64.7l2.9 2.9c2.9 2.9 3 2.9 15.4 2.9 12 0 12.7-.1 15.6-2.6l3.1-2.6V42.9l-2.5-2.4c-2.2-2.2-3.2-2.5-10.5-2.5-8.1 0-14-1.6-15.6-4.1-.5-.8-1.1-7.6-1.4-15.1C144.3.3 144.6.6 127.2.2c-9.8-.1-13.3.1-15.4 1.3zM72.3 74.4l-2.8 2.4-.3 30.6-.3 30.5 3 3.3 2.9 3.3h13.1c12.9 0 13.1 0 15.8-2.8l2.8-2.7V76.8l-2.8-2.4C101 72.1 100.2 72 88 72s-13 .1-15.7 2.4z"/> <path d="M34.9 2.9c-2.9 2.9-2.9 3-2.9 15 0 6.8-.5 13.1-1.1 14.5-1.9 4-6.6 5.6-16.6 5.6C4.7 38 1.7 39.4.5 44.3.2 45.5.1 75.4.2 110.7l.3 64.2 2.3 2.3c2.1 2.1 3.3 2.3 14.6 2.6 14.8.4 17.9-.6 19.5-6.6.7-2.4 1.1-26.2 1.1-66.9V43.1l2.4-2.8c2.3-2.7 2.9-2.8 13.6-3.3 15-.7 15-.6 15-18.2v-13l-2.9-2.9C63.2 0 63.2 0 50.5 0S37.8 0 34.9 2.9zm76.9-1.4c-3.9 2.2-5.1 7.8-4.6 20.4.5 9.8.6 10.6 3.2 12.8 2.3 2 3.8 2.3 10.5 2.3 4.3 0 9.2.5 11 1.1 6.3 2.2 6.1.3 6.1 71.4v64.7l2.9 2.9c2.9 2.9 3 2.9 15.4 2.9 12 0 12.7-.1 15.6-2.6l3.1-2.6V42.9l-2.5-2.4c-2.2-2.2-3.2-2.5-10.5-2.5-8.1 0-14-1.6-15.6-4.1-.5-.8-1.1-7.6-1.4-15.1C144.3.3 144.6.6 127.2.2c-9.8-.1-13.3.1-15.4 1.3zM72.3 74.4l-2.8 2.4-.3 30.6-.3 30.5 3 3.3 2.9 3.3h13.1c12.9 0 13.1 0 15.8-2.8l2.8-2.7V76.8l-2.8-2.4C101 72.1 100.2 72 88 72s-13 .1-15.7 2.4z"/>
</svg> </svg>
...@@ -24,6 +24,7 @@ ...@@ -24,6 +24,7 @@
| "brands/safe" | "brands/safe"
| "brands/solidity_scan" | "brands/solidity_scan"
| "burger" | "burger"
| "certified"
| "check" | "check"
| "clock-light" | "clock-light"
| "clock" | "clock"
...@@ -146,7 +147,6 @@ ...@@ -146,7 +147,6 @@
| "user_op_slim" | "user_op_slim"
| "user_op" | "user_op"
| "validator" | "validator"
| "verified_token"
| "verified" | "verified"
| "verify-contract" | "verify-contract"
| "wallet" | "wallet"
......
...@@ -21,6 +21,9 @@ export const USER_OP: UserOp = { ...@@ -21,6 +21,9 @@ export const USER_OP: UserOp = {
sender: ADDRESS_HASH, sender: ADDRESS_HASH,
nonce: '0x00b', nonce: '0x00b',
call_data: '0x123', call_data: '0x123',
execute_call_data: null,
decoded_call_data: null,
decoded_execute_call_data: null,
call_gas_limit: '71316', call_gas_limit: '71316',
verification_gas_limit: '91551', verification_gas_limit: '91551',
pre_verification_gas: '53627', pre_verification_gas: '53627',
......
import type { AlertProps } from '@chakra-ui/react'; import type { AlertProps } from '@chakra-ui/react';
import { Alert, AlertIcon, AlertTitle } from '@chakra-ui/react'; import { Alert, AlertIcon, AlertTitle } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import TestApp from 'playwright/TestApp'; import { test, expect } from 'playwright/lib';
test.use({ viewport: { width: 400, height: 720 } }); test.use({ viewport: { width: 400, height: 720 } });
...@@ -29,16 +28,14 @@ const TEST_CASES: Array<AlertProps> = [ ...@@ -29,16 +28,14 @@ const TEST_CASES: Array<AlertProps> = [
TEST_CASES.forEach((props) => { TEST_CASES.forEach((props) => {
const testName = Object.entries(props).map(([ key, value ]) => `${ key }=${ value }`).join(', '); const testName = Object.entries(props).map(([ key, value ]) => `${ key }=${ value }`).join(', ');
test(`${ testName } +@dark-mode`, async({ mount }) => { test(`${ testName } +@dark-mode`, async({ render }) => {
const component = await mount( const component = await render(
<TestApp> <Alert { ...props }>
<Alert { ...props }> <AlertIcon/>
<AlertIcon/> <AlertTitle>
<AlertTitle>
This is alert text This is alert text
</AlertTitle> </AlertTitle>
</Alert> </Alert>,
</TestApp>,
); );
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
......
import { Button } from '@chakra-ui/react'; import { Button } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import TestApp from 'playwright/TestApp'; import { test, expect } from 'playwright/lib';
[ [
{ variant: 'solid' }, { variant: 'solid' },
...@@ -16,40 +15,24 @@ import TestApp from 'playwright/TestApp'; ...@@ -16,40 +15,24 @@ import TestApp from 'playwright/TestApp';
{ variant: 'subtle', colorScheme: 'gray', withDarkMode: true }, { variant: 'subtle', colorScheme: 'gray', withDarkMode: true },
].forEach(({ variant, colorScheme, withDarkMode }) => { ].forEach(({ variant, colorScheme, withDarkMode }) => {
test.describe(`variant ${ variant }${ colorScheme ? ` with ${ colorScheme } color scheme` : '' }${ withDarkMode ? ' +@dark-mode' : '' }`, () => { test.describe(`variant ${ variant }${ colorScheme ? ` with ${ colorScheme } color scheme` : '' }${ withDarkMode ? ' +@dark-mode' : '' }`, () => {
test('base', async({ mount }) => { test('base', async({ render }) => {
const component = await mount( const component = await render(<Button variant={ variant } colorScheme={ colorScheme }>Click me</Button>);
<TestApp>
<Button variant={ variant } colorScheme={ colorScheme }>Click me</Button>
</TestApp>,
);
await expect(component.locator('button')).toHaveScreenshot(); await expect(component.locator('button')).toHaveScreenshot();
}); });
test('disabled', async({ mount }) => { test('disabled', async({ render }) => {
const component = await mount( const component = await render(<Button variant={ variant } colorScheme={ colorScheme } isDisabled>Click me</Button>);
<TestApp>
<Button variant={ variant } colorScheme={ colorScheme } isDisabled>Click me</Button>
</TestApp>,
);
await expect(component.locator('button')).toHaveScreenshot(); await expect(component.locator('button')).toHaveScreenshot();
}); });
test('hovered', async({ mount }) => { test('hovered', async({ render }) => {
const component = await mount( const component = await render(<Button variant={ variant } colorScheme={ colorScheme }>Click me</Button>);
<TestApp>
<Button variant={ variant } colorScheme={ colorScheme }>Click me</Button>
</TestApp>,
);
await component.getByText(/click/i).hover(); await component.getByText(/click/i).hover();
await expect(component.locator('button')).toHaveScreenshot(); await expect(component.locator('button')).toHaveScreenshot();
}); });
test('active', async({ mount }) => { test('active', async({ render }) => {
const component = await mount( const component = await render(<Button variant={ variant } colorScheme={ colorScheme } isActive>Click me</Button>);
<TestApp>
<Button variant={ variant } colorScheme={ colorScheme } isActive>Click me</Button>
</TestApp>,
);
await expect(component.locator('button')).toHaveScreenshot(); await expect(component.locator('button')).toHaveScreenshot();
}); });
}); });
......
import { FormControl, Input, FormLabel } from '@chakra-ui/react'; import { FormControl, Input, FormLabel } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import TestApp from 'playwright/TestApp'; import { test, expect } from 'playwright/lib';
test.use({ viewport: { width: 500, height: 300 } }); test.use({ viewport: { width: 500, height: 300 } });
test.describe('floating label size md +@dark-mode', () => { test.describe('floating label size md +@dark-mode', () => {
test('empty', async({ mount }) => { test('empty', async({ render }) => {
const component = await mount( const component = await render(
<TestApp> <FormControl variant="floating" id="name" isRequired size="md">
<FormControl variant="floating" id="name" isRequired size="md"> <Input required value=""/>
<Input required value=""/> <FormLabel>Smart contract / Address (0x...)</FormLabel>
<FormLabel>Smart contract / Address (0x...)</FormLabel> </FormControl>,
</FormControl>
</TestApp>,
); );
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
...@@ -23,14 +20,12 @@ test.describe('floating label size md +@dark-mode', () => { ...@@ -23,14 +20,12 @@ test.describe('floating label size md +@dark-mode', () => {
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('empty error', async({ mount }) => { test('empty error', async({ render }) => {
const component = await mount( const component = await render(
<TestApp> <FormControl variant="floating" id="name" isRequired size="md">
<FormControl variant="floating" id="name" isRequired size="md"> <Input required value="" isInvalid/>
<Input required value="" isInvalid/> <FormLabel>Smart contract / Address (0x...)</FormLabel>
<FormLabel>Smart contract / Address (0x...)</FormLabel> </FormControl>,
</FormControl>
</TestApp>,
); );
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
...@@ -39,53 +34,45 @@ test.describe('floating label size md +@dark-mode', () => { ...@@ -39,53 +34,45 @@ test.describe('floating label size md +@dark-mode', () => {
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('filled', async({ mount }) => { test('filled', async({ render }) => {
const component = await mount( const component = await render(
<TestApp> <FormControl variant="floating" id="name" isRequired size="md">
<FormControl variant="floating" id="name" isRequired size="md"> <Input required value="foo"/>
<Input required value="foo"/> <FormLabel>Smart contract / Address (0x...)</FormLabel>
<FormLabel>Smart contract / Address (0x...)</FormLabel> </FormControl>,
</FormControl>
</TestApp>,
); );
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('filled disabled', async({ mount }) => { test('filled disabled', async({ render }) => {
const component = await mount( const component = await render(
<TestApp> <FormControl variant="floating" id="name" isRequired size="md">
<FormControl variant="floating" id="name" isRequired size="md"> <Input required value="foo" isDisabled/>
<Input required value="foo" isDisabled/> <FormLabel>Smart contract / Address (0x...)</FormLabel>
<FormLabel>Smart contract / Address (0x...)</FormLabel> </FormControl>,
</FormControl>
</TestApp>,
); );
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('filled read-only', async({ mount }) => { test('filled read-only', async({ render }) => {
const component = await mount( const component = await render(
<TestApp> <FormControl variant="floating" id="name" isRequired size="md">
<FormControl variant="floating" id="name" isRequired size="md"> <Input required value="foo" isReadOnly/>
<Input required value="foo" isReadOnly/> <FormLabel>Smart contract / Address (0x...)</FormLabel>
<FormLabel>Smart contract / Address (0x...)</FormLabel> </FormControl>,
</FormControl>
</TestApp>,
); );
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('filled error', async({ mount }) => { test('filled error', async({ render }) => {
const component = await mount( const component = await render(
<TestApp> <FormControl variant="floating" id="name" isRequired size="md">
<FormControl variant="floating" id="name" isRequired size="md"> <Input required value="foo" isInvalid/>
<Input required value="foo" isInvalid/> <FormLabel>Smart contract / Address (0x...)</FormLabel>
<FormLabel>Smart contract / Address (0x...)</FormLabel> </FormControl>,
</FormControl>
</TestApp>,
); );
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
...@@ -93,14 +80,12 @@ test.describe('floating label size md +@dark-mode', () => { ...@@ -93,14 +80,12 @@ test.describe('floating label size md +@dark-mode', () => {
}); });
test.describe('floating label size lg +@dark-mode', () => { test.describe('floating label size lg +@dark-mode', () => {
test('empty', async({ mount }) => { test('empty', async({ render }) => {
const component = await mount( const component = await render(
<TestApp> <FormControl variant="floating" id="name" isRequired size="lg">
<FormControl variant="floating" id="name" isRequired size="lg"> <Input required value=""/>
<Input required value=""/> <FormLabel>Smart contract / Address (0x...)</FormLabel>
<FormLabel>Smart contract / Address (0x...)</FormLabel> </FormControl>,
</FormControl>
</TestApp>,
); );
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
...@@ -109,14 +94,12 @@ test.describe('floating label size lg +@dark-mode', () => { ...@@ -109,14 +94,12 @@ test.describe('floating label size lg +@dark-mode', () => {
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('filled', async({ mount }) => { test('filled', async({ render }) => {
const component = await mount( const component = await render(
<TestApp> <FormControl variant="floating" id="name" isRequired size="lg">
<FormControl variant="floating" id="name" isRequired size="lg"> <Input required value="foo"/>
<Input required value="foo"/> <FormLabel>Smart contract / Address (0x...)</FormLabel>
<FormLabel>Smart contract / Address (0x...)</FormLabel> </FormControl>,
</FormControl>
</TestApp>,
); );
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
......
...@@ -29,6 +29,20 @@ const size = { ...@@ -29,6 +29,20 @@ const size = {
h: '40px', h: '40px',
borderRadius: 'base', borderRadius: 'base',
}), }),
// TEMPORARY INPUT SIZE!!!
// soon we will migrate to the new size and get rid off this one
// lg -> 60
// md -> 48
// sm -> 40
// xs ->32
sm_md: defineStyle({
fontSize: 'md',
lineHeight: '24px',
px: '8px',
py: '12px',
h: '48px',
borderRadius: 'base',
}),
md: defineStyle({ md: defineStyle({
fontSize: 'md', fontSize: 'md',
lineHeight: '20px', lineHeight: '20px',
...@@ -71,6 +85,10 @@ const sizes = { ...@@ -71,6 +85,10 @@ const sizes = {
field: size.sm, field: size.sm,
addon: size.sm, addon: size.sm,
}), }),
sm_md: definePartsStyle({
field: size.sm_md,
addon: size.sm_md,
}),
md: definePartsStyle({ md: definePartsStyle({
field: size.md, field: size.md,
addon: size.md, addon: size.md,
......
...@@ -15,6 +15,9 @@ const baseStyleTrack = defineStyle((props) => { ...@@ -15,6 +15,9 @@ const baseStyleTrack = defineStyle((props) => {
bg: mode(`${ c }.600`, `${ c }.400`)(props), bg: mode(`${ c }.600`, `${ c }.400`)(props),
}, },
}, },
_focusVisible: {
boxShadow: 'none',
},
}; };
}); });
......
import { Box, Tag } from '@chakra-ui/react'; import { Box, Tag } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import TestApp from 'playwright/TestApp'; import { test, expect } from 'playwright/lib';
[ 'blue', 'gray', 'gray-blue', 'orange', 'green', 'purple', 'cyan', 'teal' ].forEach((colorScheme) => { [ 'blue', 'gray', 'gray-blue', 'orange', 'green', 'purple', 'cyan', 'teal' ].forEach((colorScheme) => {
test(`${ colorScheme } color scheme +@dark-mode`, async({ mount }) => { test(`${ colorScheme } color scheme +@dark-mode`, async({ render }) => {
const component = await mount( const component = await render(<Tag colorScheme={ colorScheme }>content</Tag>);
<TestApp>
<Tag colorScheme={ colorScheme }>content</Tag>
</TestApp>,
);
await expect(component.getByText(/content/i)).toHaveScreenshot(); await expect(component.getByText(/content/i)).toHaveScreenshot();
}); });
}); });
test('with long text', async({ mount }) => { test('with long text', async({ render }) => {
const component = await mount( const component = await render(
<TestApp> <Box w="100px">
<Box w="100px"> <Tag>this is very looooooooooong text</Tag>
<Tag>this is very looooooooooong text</Tag> </Box>,
</Box>
</TestApp>,
); );
await expect(component.getByText(/this/i)).toHaveScreenshot(); await expect(component.getByText(/this/i)).toHaveScreenshot();
}); });
import { Box, Tooltip, Icon } from '@chakra-ui/react'; import { Box, Tooltip, Icon } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import TestApp from 'playwright/TestApp'; import { test, expect } from 'playwright/lib';
test('base view +@dark-mode', async({ mount, page }) => { test('base view +@dark-mode', async({ render, page }) => {
const component = await mount( const component = await render(
<TestApp> <Box m={ 10 }>
<Box m={ 10 }> <Tooltip label="Tooltip content">
<Tooltip label="Tooltip content">
trigger trigger
</Tooltip> </Tooltip>
</Box> </Box>,
</TestApp>,
); );
await component.getByText(/trigger/i).hover(); await component.getByText(/trigger/i).hover();
...@@ -22,17 +19,15 @@ test('base view +@dark-mode', async({ mount, page }) => { ...@@ -22,17 +19,15 @@ test('base view +@dark-mode', async({ mount, page }) => {
// was not able to reproduce in tests issue when Icon is used as trigger for tooltip // was not able to reproduce in tests issue when Icon is used as trigger for tooltip
// https://github.com/chakra-ui/chakra-ui/issues/7107 // https://github.com/chakra-ui/chakra-ui/issues/7107
test.skip('with icon', async({ mount, page }) => { test.fixme('with icon', async({ render, page }) => {
const component = await mount( const component = await render(
<TestApp> <Box m={ 10 }>
<Box m={ 10 }> <Tooltip label="Tooltip content">
<Tooltip label="Tooltip content"> <Icon viewBox="0 0 20 20" boxSize={ 5 } aria-label="Trigger">
<Icon viewBox="0 0 20 20" boxSize={ 5 } aria-label="Trigger"> <circle cx="10" cy="10" r="10"/>
<circle cx="10" cy="10" r="10"/> </Icon>
</Icon> </Tooltip>
</Tooltip> </Box>,
</Box>
</TestApp>,
); );
const tooltip = page.getByText(/tooltip content/i); const tooltip = page.getByText(/tooltip content/i);
......
...@@ -17,7 +17,7 @@ fi ...@@ -17,7 +17,7 @@ fi
# download assets for the running instance # download assets for the running instance
dotenv \ dotenv \
-e $config_file \ -e $config_file \
-- bash -c './deploy/scripts/download_assets.sh ./public/assets' -- bash -c './deploy/scripts/download_assets.sh ./public/assets/configs'
yarn svg:build-sprite yarn svg:build-sprite
echo "" echo ""
......
...@@ -6,7 +6,7 @@ dotenv \ ...@@ -6,7 +6,7 @@ dotenv \
-e .env.local \ -e .env.local \
-e .env.development \ -e .env.development \
-e .env \ -e .env \
-- bash -c './deploy/scripts/download_assets.sh ./public/assets' -- bash -c './deploy/scripts/download_assets.sh ./public/assets/configs'
yarn svg:build-sprite yarn svg:build-sprite
echo "" echo ""
...@@ -20,5 +20,5 @@ dotenv \ ...@@ -20,5 +20,5 @@ dotenv \
-e .env.local \ -e .env.local \
-e .env.development \ -e .env.development \
-e .env \ -e .env \
-- bash -c './deploy/scripts/make_envs_script.sh && next dev -- -p $NEXT_PUBLIC_APP_PORT' | -- bash -c './deploy/scripts/make_envs_script.sh && next dev -p $NEXT_PUBLIC_APP_PORT' |
pino-pretty pino-pretty
\ No newline at end of file
secrets_file="./configs/envs/.env.secrets" secrets_file="./configs/envs/.env.secrets"
favicon_folder="./public/favicon/" favicon_folder="./public/assets/favicon/"
master_url="https://raw.githubusercontent.com/blockscout/frontend/main/tools/scripts/favicon.svg" master_url="https://raw.githubusercontent.com/blockscout/frontend/main/tools/scripts/favicon.svg"
if [ ! -f "$secrets_file" ]; then if [ ! -f "$secrets_file" ]; then
......
...@@ -21,6 +21,7 @@ export interface AddressMetadataTagApi extends Omit<AddressMetadataTag, 'meta'> ...@@ -21,6 +21,7 @@ export interface AddressMetadataTagApi extends Omit<AddressMetadataTag, 'meta'>
meta: { meta: {
textColor?: string; textColor?: string;
bgColor?: string; bgColor?: string;
tagIcon?: string;
tagUrl?: string; tagUrl?: string;
tooltipIcon?: string; tooltipIcon?: string;
tooltipTitle?: string; tooltipTitle?: string;
...@@ -30,6 +31,7 @@ export interface AddressMetadataTagApi extends Omit<AddressMetadataTag, 'meta'> ...@@ -30,6 +31,7 @@ export interface AddressMetadataTagApi extends Omit<AddressMetadataTag, 'meta'>
appMarketplaceURL?: string; appMarketplaceURL?: string;
appLogoURL?: string; appLogoURL?: string;
appActionButtonText?: string; appActionButtonText?: string;
warpcastHandle?: string;
} | null; } | null;
} }
......
...@@ -62,6 +62,7 @@ export interface SmartContract { ...@@ -62,6 +62,7 @@ export interface SmartContract {
minimal_proxy_address_hash: string | null; minimal_proxy_address_hash: string | null;
language: string | null; language: string | null;
license_type: SmartContractLicenseType | null; license_type: SmartContractLicenseType | null;
certified?: boolean;
} }
export type SmartContractDecodedConstructorArg = [ export type SmartContractDecodedConstructorArg = [
......
...@@ -3,6 +3,7 @@ import type { SmartContractLicenseType } from './contract'; ...@@ -3,6 +3,7 @@ import type { SmartContractLicenseType } from './contract';
export interface VerifiedContract { export interface VerifiedContract {
address: AddressParam; address: AddressParam;
certified?: boolean;
coin_balance: string; coin_balance: string;
compiler_version: string; compiler_version: string;
language: 'vyper' | 'yul' | 'solidity'; language: 'vyper' | 'yul' | 'solidity';
......
...@@ -22,6 +22,7 @@ export interface SearchResultAddressOrContract { ...@@ -22,6 +22,7 @@ export interface SearchResultAddressOrContract {
name: string | null; name: string | null;
address: string; address: string;
is_smart_contract_verified: boolean; is_smart_contract_verified: boolean;
certified?: true;
url?: string; // not used by the frontend, we build the url ourselves url?: string; // not used by the frontend, we build the url ourselves
ens_info?: { ens_info?: {
address_hash: string; address_hash: string;
......
...@@ -3,16 +3,17 @@ export type HomeStats = { ...@@ -3,16 +3,17 @@ export type HomeStats = {
total_addresses: string; total_addresses: string;
total_transactions: string; total_transactions: string;
average_block_time: number; average_block_time: number;
coin_image?: string | null;
coin_price: string | null; coin_price: string | null;
coin_price_change_percentage: number | null; // e.g -6.22 coin_price_change_percentage: number | null; // e.g -6.22
total_gas_used: string; total_gas_used: string;
transactions_today: string; transactions_today: string | null;
gas_used_today: string; gas_used_today: string;
gas_prices: GasPrices | null; gas_prices: GasPrices | null;
gas_price_updated_at: string | null; gas_price_updated_at: string | null;
gas_prices_update_in: number; gas_prices_update_in: number;
static_gas_price: string | null; static_gas_price: string | null;
market_cap: string; market_cap: string | null;
network_utilization_percentage: number; network_utilization_percentage: number;
tvl: string | null; tvl: string | null;
rootstock_locked_btc?: string | null; rootstock_locked_btc?: string | null;
......
import type { AddressParamBasic } from './addressParams'; import type { AddressParamBasic } from './addressParams';
import type { DecodedInput } from './decodedInput';
export type UserOpsItem = { export type UserOpsItem = {
hash: string; hash: string;
...@@ -46,6 +47,9 @@ export type UserOp = { ...@@ -46,6 +47,9 @@ export type UserOp = {
signature: string; signature: string;
nonce: string; nonce: string;
call_data: string; call_data: string;
decoded_call_data: DecodedInput | null;
execute_call_data: string | null;
decoded_execute_call_data: DecodedInput | null;
user_logs_start_index: number; user_logs_start_index: number;
user_logs_count: number; user_logs_count: number;
raw: { raw: {
......
export type MultichainProviderConfig = {
name: string;
dapp_id?: string;
url_template: string;
logo?: string;
};
...@@ -14,7 +14,7 @@ import useSocketMessage from 'lib/socket/useSocketMessage'; ...@@ -14,7 +14,7 @@ import useSocketMessage from 'lib/socket/useSocketMessage';
import { currencyUnits } from 'lib/units'; import { currencyUnits } from 'lib/units';
import { BLOCK } from 'stubs/block'; import { BLOCK } from 'stubs/block';
import { generateListStub } from 'stubs/utils'; import { generateListStub } from 'stubs/utils';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar, { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay'; import DataListDisplay from 'ui/shared/DataListDisplay';
import Pagination from 'ui/shared/pagination/Pagination'; import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
...@@ -96,7 +96,7 @@ const AddressBlocksValidated = ({ scrollRef, shouldRender = true }: Props) => { ...@@ -96,7 +96,7 @@ const AddressBlocksValidated = ({ scrollRef, shouldRender = true }: Props) => {
{ socketAlert && <SocketAlert mb={ 6 }/> } { socketAlert && <SocketAlert mb={ 6 }/> }
<Hide below="lg" ssr={ false }> <Hide below="lg" ssr={ false }>
<Table variant="simple" size="sm"> <Table variant="simple" size="sm">
<Thead top={ query.pagination.isVisible ? 80 : 0 }> <Thead top={ query.pagination.isVisible ? ACTION_BAR_HEIGHT_DESKTOP : 0 }>
<Tr> <Tr>
<Th width="17%">Block</Th> <Th width="17%">Block</Th>
<Th width="17%">Age</Th> <Th width="17%">Age</Th>
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import * as balanceHistoryMock from 'mocks/address/coinBalanceHistory'; import * as balanceHistoryMock from 'mocks/address/coinBalanceHistory';
import TestApp from 'playwright/TestApp'; import { test, expect } from 'playwright/lib';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import AddressCoinBalance from './AddressCoinBalance'; import AddressCoinBalance from './AddressCoinBalance';
const addressHash = 'hash'; const addressHash = '0x1234';
const BALANCE_HISTORY_API_URL = buildApiUrl('address_coin_balance', { hash: addressHash });
const BALANCE_HISTORY_CHART_API_URL = buildApiUrl('address_coin_balance_chart', { hash: addressHash });
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { hash: addressHash }, query: { hash: addressHash },
}, },
}; };
test('base view +@dark-mode +@mobile', async({ mount, page }) => { test('base view +@dark-mode +@mobile', async({ render, page, mockApiResponse }) => {
await page.route(BALANCE_HISTORY_API_URL, (route) => route.fulfill({ await mockApiResponse('address_coin_balance', balanceHistoryMock.baseResponse, { pathParams: { hash: addressHash } });
status: 200, await mockApiResponse('address_coin_balance_chart', balanceHistoryMock.chartResponse, { pathParams: { hash: addressHash } });
body: JSON.stringify(balanceHistoryMock.baseResponse), const component = await render(<AddressCoinBalance/>, { hooksConfig });
}));
await page.route(BALANCE_HISTORY_CHART_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(balanceHistoryMock.chartResponse),
}));
const component = await mount(
<TestApp>
<AddressCoinBalance/>
</TestApp>,
{ hooksConfig },
);
await page.waitForFunction(() => { await page.waitForFunction(() => {
return document.querySelector('path[data-name="chart-Balances-small"]')?.getAttribute('opacity') === '1'; return document.querySelector('path[data-name="chart-Balances-small"]')?.getAttribute('opacity') === '1';
}); });
await page.mouse.move(240, 100); await page.mouse.move(240, 100);
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
...@@ -9,7 +9,7 @@ import config from 'configs/app'; ...@@ -9,7 +9,7 @@ import config from 'configs/app';
import useIsInitialLoading from 'lib/hooks/useIsInitialLoading'; import useIsInitialLoading from 'lib/hooks/useIsInitialLoading';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/links/LinkInternal';
interface Props { interface Props {
address: string; address: string;
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import type { WalletProvider } from 'types/web3';
import * as addressMock from 'mocks/address/address'; import * as addressMock from 'mocks/address/address';
import * as countersMock from 'mocks/address/counters'; import * as countersMock from 'mocks/address/counters';
import * as tokensMock from 'mocks/address/tokens'; import * as tokensMock from 'mocks/address/tokens';
import TestApp from 'playwright/TestApp'; import { test, expect, devices } from 'playwright/lib';
import buildApiUrl from 'playwright/utils/buildApiUrl'; import * as pwConfig from 'playwright/utils/config';
import * as configs from 'playwright/utils/configs';
import AddressDetails from './AddressDetails'; import AddressDetails from './AddressDetails';
import MockAddressPage from './testUtils/MockAddressPage'; import MockAddressPage from './testUtils/MockAddressPage';
import type { AddressQuery } from './utils/useAddressQuery'; import type { AddressQuery } from './utils/useAddressQuery';
const ADDRESS_HASH = addressMock.hash; const ADDRESS_HASH = addressMock.hash;
const API_URL_ADDRESS = buildApiUrl('address', { hash: ADDRESS_HASH });
const API_URL_COUNTERS = buildApiUrl('address_counters', { hash: ADDRESS_HASH });
const API_URL_TOKENS_ERC20 = buildApiUrl('address_tokens', { hash: ADDRESS_HASH }) + '?type=ERC-20';
const API_URL_TOKENS_ERC721 = buildApiUrl('address_tokens', { hash: ADDRESS_HASH }) + '?type=ERC-721';
const API_URL_TOKENS_ER1155 = buildApiUrl('address_tokens', { hash: ADDRESS_HASH }) + '?type=ERC-1155';
const API_URL_TOKENS_ERC404 = buildApiUrl('address_tokens', { hash: ADDRESS_HASH }) + '?type=ERC-404';
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { hash: ADDRESS_HASH }, query: { hash: ADDRESS_HASH },
}, },
}; };
test('contract +@mobile', async({ mount, page }) => { test.describe('mobile', () => {
await page.route(API_URL_ADDRESS, (route) => route.fulfill({ test.use({ viewport: devices['iPhone 13 Pro'].viewport });
status: 200,
body: JSON.stringify(addressMock.contract),
}));
await page.route(API_URL_COUNTERS, (route) => route.fulfill({
status: 200,
body: JSON.stringify(countersMock.forContract),
}));
const component = await mount(
<TestApp>
<AddressDetails addressQuery={{ data: addressMock.contract } as AddressQuery}/>
</TestApp>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot({ test('contract', async({ render, mockApiResponse, page }) => {
mask: [ page.locator(configs.adsBannerSelector) ], await mockApiResponse('address', addressMock.contract, { pathParams: { hash: ADDRESS_HASH } });
maskColor: configs.maskColor, await mockApiResponse('address_counters', countersMock.forContract, { pathParams: { hash: ADDRESS_HASH } });
const component = await render(<AddressDetails addressQuery={{ data: addressMock.contract } as AddressQuery}/>, { hooksConfig });
await expect(component).toHaveScreenshot({
mask: [ page.locator(pwConfig.adsBannerSelector) ],
maskColor: pwConfig.maskColor,
});
});
test('validator', async({ render, page, mockApiResponse }) => {
await mockApiResponse('address', addressMock.validator, { pathParams: { hash: ADDRESS_HASH } });
await mockApiResponse('address_counters', countersMock.forValidator, { pathParams: { hash: ADDRESS_HASH } });
const component = await render(<AddressDetails addressQuery={{ data: addressMock.validator } as AddressQuery}/>, { hooksConfig });
await expect(component).toHaveScreenshot({
mask: [ page.locator(pwConfig.adsBannerSelector) ],
maskColor: pwConfig.maskColor,
});
}); });
}); });
test('token', async({ mount, page }) => { test('contract', async({ render, page, mockApiResponse }) => {
await page.route(API_URL_ADDRESS, (route) => route.fulfill({ await mockApiResponse('address', addressMock.contract, { pathParams: { hash: ADDRESS_HASH } });
status: 200, await mockApiResponse('address_counters', countersMock.forContract, { pathParams: { hash: ADDRESS_HASH } });
body: JSON.stringify(addressMock.token),
})); const component = await render(<AddressDetails addressQuery={{ data: addressMock.contract } as AddressQuery}/>, { hooksConfig });
await page.route(API_URL_COUNTERS, (route) => route.fulfill({
status: 200, await expect(component).toHaveScreenshot({
body: JSON.stringify(countersMock.forToken), mask: [ page.locator(pwConfig.adsBannerSelector) ],
})); maskColor: pwConfig.maskColor,
await page.route(API_URL_TOKENS_ERC20, async(route) => route.fulfill({
status: 200,
body: JSON.stringify(tokensMock.erc20List),
}), { times: 1 });
await page.route(API_URL_TOKENS_ERC721, async(route) => route.fulfill({
status: 200,
body: JSON.stringify(tokensMock.erc721List),
}), { times: 1 });
await page.route(API_URL_TOKENS_ER1155, async(route) => route.fulfill({
status: 200,
body: JSON.stringify(tokensMock.erc1155List),
}), { times: 1 });
await page.route(API_URL_TOKENS_ERC404, async(route) => route.fulfill({
status: 200,
body: JSON.stringify(tokensMock.erc404List),
}), { times: 1 });
await page.evaluate(() => {
window.ethereum = {
providers: [ { isMetaMask: true, _events: {} } ],
} as WalletProvider;
}); });
});
const component = await mount( // there's an unexpected timeout occurred in this test
<TestApp> test.fixme('token', async({ render, mockApiResponse, injectMetaMaskProvider, page }) => {
<MockAddressPage> await mockApiResponse('address', addressMock.token, { pathParams: { hash: ADDRESS_HASH } });
<AddressDetails addressQuery={{ data: addressMock.token } as AddressQuery}/> await mockApiResponse('address_counters', countersMock.forToken, { pathParams: { hash: ADDRESS_HASH } });
</MockAddressPage> await mockApiResponse('address_tokens', tokensMock.erc20List, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-20' }, times: 1 });
</TestApp>, await mockApiResponse('address_tokens', tokensMock.erc721List, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-721' }, times: 1 });
await mockApiResponse('address_tokens', tokensMock.erc1155List, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-1155' }, times: 1 });
await mockApiResponse('address_tokens', tokensMock.erc404List, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-404' }, times: 1 });
await injectMetaMaskProvider();
const component = await render(
<MockAddressPage>
<AddressDetails addressQuery={{ data: addressMock.token } as AddressQuery}/>
</MockAddressPage>,
{ hooksConfig }, { hooksConfig },
); );
await expect(component).toHaveScreenshot({ await expect(component).toHaveScreenshot({
mask: [ page.locator(configs.adsBannerSelector) ], mask: [ page.locator(pwConfig.adsBannerSelector) ],
maskColor: configs.maskColor, maskColor: pwConfig.maskColor,
}); });
}); });
test('validator +@mobile', async({ mount, page }) => { test('validator', async({ render, mockApiResponse, page }) => {
await page.route(API_URL_ADDRESS, (route) => route.fulfill({ await mockApiResponse('address', addressMock.validator, { pathParams: { hash: ADDRESS_HASH } });
status: 200, await mockApiResponse('address_counters', countersMock.forValidator, { pathParams: { hash: ADDRESS_HASH } });
body: JSON.stringify(addressMock.validator),
})); const component = await render(<AddressDetails addressQuery={{ data: addressMock.validator } as AddressQuery}/>, { hooksConfig });
await page.route(API_URL_COUNTERS, (route) => route.fulfill({
status: 200,
body: JSON.stringify(countersMock.forValidator),
}));
const component = await mount(
<TestApp>
<AddressDetails addressQuery={{ data: addressMock.validator } as AddressQuery}/>
</TestApp>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot({ await expect(component).toHaveScreenshot({
mask: [ page.locator(configs.adsBannerSelector) ], mask: [ page.locator(pwConfig.adsBannerSelector) ],
maskColor: configs.maskColor, maskColor: pwConfig.maskColor,
}); });
}); });
...@@ -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';
...@@ -17,6 +18,7 @@ import TxEntity from 'ui/shared/entities/tx/TxEntity'; ...@@ -17,6 +18,7 @@ import TxEntity from 'ui/shared/entities/tx/TxEntity';
import AddressBalance from './details/AddressBalance'; import AddressBalance from './details/AddressBalance';
import AddressNameInfo from './details/AddressNameInfo'; import AddressNameInfo from './details/AddressNameInfo';
import AddressNetWorth from './details/AddressNetWorth';
import TokenSelect from './tokenSelect/TokenSelect'; import TokenSelect from './tokenSelect/TokenSelect';
import useAddressCountersQuery from './utils/useAddressCountersQuery'; import useAddressCountersQuery from './utils/useAddressCountersQuery';
import type { AddressQuery } from './utils/useAddressQuery'; import type { AddressQuery } from './utils/useAddressQuery';
...@@ -129,6 +131,17 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => { ...@@ -129,6 +131,17 @@ 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>
) } ) }
{ (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 <DetailsInfoItem
title="Transactions" title="Transactions"
hint="Number of transactions related to this address" hint="Number of transactions related to this address"
......
import { Box } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import * as internalTxsMock from 'mocks/txs/internalTxs'; import * as internalTxsMock from 'mocks/txs/internalTxs';
import TestApp from 'playwright/TestApp'; import { test, expect } from 'playwright/lib';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import AddressInternalTxs from './AddressInternalTxs'; import AddressInternalTxs from './AddressInternalTxs';
const ADDRESS_HASH = internalTxsMock.base.from.hash; const ADDRESS_HASH = internalTxsMock.base.from.hash;
const API_URL_TX_INTERNALS = buildApiUrl('address_internal_txs', { hash: ADDRESS_HASH });
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { hash: ADDRESS_HASH }, query: { hash: ADDRESS_HASH },
}, },
}; };
test('base view +@mobile', async({ mount, page }) => { test('base view +@mobile', async({ render, mockApiResponse }) => {
await page.route(API_URL_TX_INTERNALS, (route) => route.fulfill({ await mockApiResponse('address_internal_txs', internalTxsMock.baseResponse, { pathParams: { hash: ADDRESS_HASH } });
status: 200, const component = await render(
body: JSON.stringify(internalTxsMock.baseResponse), <Box pt={{ base: '134px', lg: 6 }}>
}));
const component = await mount(
<TestApp>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressInternalTxs/> <AddressInternalTxs/>
</TestApp>, </Box>,
{ hooksConfig }, { hooksConfig },
); );
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
import { Box } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import { test as base, expect, devices } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import * as tokenTransferMock from 'mocks/tokens/tokenTransfer'; import * as tokenTransferMock from 'mocks/tokens/tokenTransfer';
import * as socketServer from 'playwright/fixtures/socketServer'; import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp'; import { test, expect, devices } from 'playwright/lib';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import AddressTokenTransfers from './AddressTokenTransfers'; import AddressTokenTransfers from './AddressTokenTransfers';
const CURRENT_ADDRESS = '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859'; const CURRENT_ADDRESS = '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859';
const TOKEN_HASH = '0x1189a607CEac2f0E14867de4EB15b15C9FFB5859';
const API_URL = buildApiUrl('address_token_transfers', { hash: CURRENT_ADDRESS }) +
'?token=0x1189a607CEac2f0E14867de4EB15b15C9FFB5859';
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { hash: CURRENT_ADDRESS, token: '0x1189a607CEac2f0E14867de4EB15b15C9FFB5859' }, query: { hash: CURRENT_ADDRESS, token: TOKEN_HASH },
}, },
}; };
const test = base.extend<socketServer.SocketServerFixture>({
createSocket: socketServer.createSocket,
});
// FIXME // FIXME
// test cases which use socket cannot run in parallel since the socket server always run on the same port // test cases which use socket cannot run in parallel since the socket server always run on the same port
test.describe.configure({ mode: 'serial' }); test.describe.configure({ mode: 'serial' });
test('with token filter and pagination', async({ mount, page }) => { const tokenTransfersWithPagination = {
await page.route(API_URL, (route) => route.fulfill({ items: [ tokenTransferMock.erc1155A ],
status: 200, next_page_params: { block_number: 1, index: 1, items_count: 1 },
body: JSON.stringify({ items: [ tokenTransferMock.erc1155A ], next_page_params: { block_number: 1 } }), };
})); const tokenTransfersWoPagination = {
items: [ tokenTransferMock.erc1155A ],
next_page_params: null,
};
const component = await mount( test('with token filter and pagination', async({ render, mockApiResponse }) => {
<TestApp> await mockApiResponse('address_token_transfers', tokenTransfersWithPagination, {
<Box h={{ base: '134px', lg: 6 }}/> pathParams: { hash: CURRENT_ADDRESS },
queryParams: { token: TOKEN_HASH },
});
const component = await render(
<Box pt={{ base: '134px', lg: 6 }}>
<AddressTokenTransfers/> <AddressTokenTransfers/>
</TestApp>, </Box>,
{ hooksConfig }, { hooksConfig },
); );
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('with token filter and no pagination', async({ mount, page }) => { test('with token filter and no pagination', async({ render, mockApiResponse }) => {
await page.route(API_URL, (route) => route.fulfill({ await mockApiResponse('address_token_transfers', tokenTransfersWoPagination, {
status: 200, pathParams: { hash: CURRENT_ADDRESS },
body: JSON.stringify({ items: [ tokenTransferMock.erc1155A ] }), queryParams: { token: TOKEN_HASH },
})); });
const component = await render(
const component = await mount( <Box pt={{ base: '134px', lg: 6 }}>
<TestApp>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTokenTransfers/> <AddressTokenTransfers/>
</TestApp>, </Box>,
{ hooksConfig }, { hooksConfig },
); );
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test.describe('mobile', () => { test.describe('mobile', () => {
test.use({ viewport: devices['iPhone 13 Pro'].viewport }); test.use({ viewport: devices['iPhone 13 Pro'].viewport });
test('with token filter and pagination', async({ mount, page }) => { test('with token filter and pagination', async({ render, mockApiResponse }) => {
await page.route(API_URL, (route) => route.fulfill({ await mockApiResponse('address_token_transfers', tokenTransfersWithPagination, {
status: 200, pathParams: { hash: CURRENT_ADDRESS },
body: JSON.stringify({ items: [ tokenTransferMock.erc1155A ], next_page_params: { block_number: 1 } }), queryParams: { token: TOKEN_HASH },
})); });
const component = await render(
const component = await mount( <Box pt={{ base: '134px', lg: 6 }}>
<TestApp>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTokenTransfers/> <AddressTokenTransfers/>
</TestApp>, </Box>,
{ hooksConfig }, { hooksConfig },
); );
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('with token filter and no pagination', async({ mount, page }) => { test('with token filter and no pagination', async({ render, mockApiResponse }) => {
await page.route(API_URL, (route) => route.fulfill({ await mockApiResponse('address_token_transfers', tokenTransfersWoPagination, {
status: 200, pathParams: { hash: CURRENT_ADDRESS },
body: JSON.stringify({ items: [ tokenTransferMock.erc1155A ] }), queryParams: { token: TOKEN_HASH },
})); });
const component = await render(
const component = await mount( <Box pt={{ base: '134px', lg: 6 }}>
<TestApp>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTokenTransfers/> <AddressTokenTransfers/>
</TestApp>, </Box>,
{ hooksConfig }, { hooksConfig },
); );
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
}); });
test.describe('socket', () => { test.describe('socket', () => {
test('without overload', async({ mount, page, createSocket }) => { test('without overload', async({ render, mockApiResponse, createSocket, page }) => {
const hooksConfigNoToken = { const hooksConfigNoToken = {
router: { router: {
query: { hash: CURRENT_ADDRESS }, query: { hash: CURRENT_ADDRESS },
}, },
}; };
await mockApiResponse('address_token_transfers', tokenTransfersWithPagination, {
const API_URL_NO_TOKEN = buildApiUrl('address_token_transfers', { hash: CURRENT_ADDRESS }) + '?type='; pathParams: { hash: CURRENT_ADDRESS },
queryParams: { type: [] },
await page.route(API_URL_NO_TOKEN, (route) => route.fulfill({ });
status: 200, await render(
body: JSON.stringify({ items: [ tokenTransferMock.erc1155A ], next_page_params: { block_number: 1 } }), <Box pt={{ base: '134px', lg: 6 }}>
}));
await mount(
<TestApp withSocket>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTokenTransfers/> <AddressTokenTransfers/>
</TestApp>, </Box>,
{ hooksConfig: hooksConfigNoToken }, { hooksConfig: hooksConfigNoToken },
{ withSocket: true },
); );
const socket = await createSocket(); const socket = await createSocket();
...@@ -137,26 +121,22 @@ test.describe('socket', () => { ...@@ -137,26 +121,22 @@ test.describe('socket', () => {
expect(itemsCountNew).toBe(4); expect(itemsCountNew).toBe(4);
}); });
test('with overload', async({ mount, page, createSocket }) => { test('with overload', async({ render, mockApiResponse, page, createSocket }) => {
const hooksConfigNoToken = { const hooksConfigNoToken = {
router: { router: {
query: { hash: CURRENT_ADDRESS }, query: { hash: CURRENT_ADDRESS },
}, },
}; };
await mockApiResponse('address_token_transfers', tokenTransfersWithPagination, {
const API_URL_NO_TOKEN = buildApiUrl('address_token_transfers', { hash: CURRENT_ADDRESS }) + '?type='; pathParams: { hash: CURRENT_ADDRESS },
queryParams: { type: [] },
await page.route(API_URL_NO_TOKEN, (route) => route.fulfill({ });
status: 200, await render(
body: JSON.stringify({ items: [ tokenTransferMock.erc1155A ], next_page_params: { block_number: 1 } }), <Box pt={{ base: '134px', lg: 6 }}>
}));
await mount(
<TestApp withSocket>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTokenTransfers overloadCount={ 2 }/> <AddressTokenTransfers overloadCount={ 2 }/>
</TestApp>, </Box>,
{ hooksConfig: hooksConfigNoToken }, { hooksConfig: hooksConfigNoToken },
{ withSocket: true },
); );
const socket = await createSocket(); const socket = await createSocket();
...@@ -176,26 +156,23 @@ test.describe('socket', () => { ...@@ -176,26 +156,23 @@ test.describe('socket', () => {
expect(counter?.startsWith('1 ')).toBe(true); expect(counter?.startsWith('1 ')).toBe(true);
}); });
test('without overload, with filters', async({ mount, page, createSocket }) => { test('without overload, with filters', async({ render, mockApiResponse, page, createSocket }) => {
const hooksConfigWithFilter = { const hooksConfigWithFilter = {
router: { router: {
query: { hash: CURRENT_ADDRESS, type: 'ERC-1155' }, query: { hash: CURRENT_ADDRESS, type: 'ERC-1155' },
}, },
}; };
await mockApiResponse('address_token_transfers', tokenTransfersWithPagination, {
pathParams: { hash: CURRENT_ADDRESS },
queryParams: { type: 'ERC-1155' },
});
const API_URL_WITH_FILTER = buildApiUrl('address_token_transfers', { hash: CURRENT_ADDRESS }) + '?type=ERC-1155'; await render(
<Box pt={{ base: '134px', lg: 6 }}>
await page.route(API_URL_WITH_FILTER, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ tokenTransferMock.erc1155A ], next_page_params: { block_number: 1 } }),
}));
await mount(
<TestApp withSocket>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTokenTransfers/> <AddressTokenTransfers/>
</TestApp>, </Box>,
{ hooksConfig: hooksConfigWithFilter }, { hooksConfig: hooksConfigWithFilter },
{ withSocket: true },
); );
const socket = await createSocket(); const socket = await createSocket();
...@@ -212,26 +189,23 @@ test.describe('socket', () => { ...@@ -212,26 +189,23 @@ test.describe('socket', () => {
expect(itemsCountNew).toBe(3); expect(itemsCountNew).toBe(3);
}); });
test('with overload, with filters', async({ mount, page, createSocket }) => { test('with overload, with filters', async({ render, mockApiResponse, page, createSocket }) => {
const hooksConfigWithFilter = { const hooksConfigWithFilter = {
router: { router: {
query: { hash: CURRENT_ADDRESS, type: 'ERC-1155' }, query: { hash: CURRENT_ADDRESS, type: 'ERC-1155' },
}, },
}; };
await mockApiResponse('address_token_transfers', tokenTransfersWithPagination, {
pathParams: { hash: CURRENT_ADDRESS },
queryParams: { type: 'ERC-1155' },
});
const API_URL_WITH_FILTER = buildApiUrl('address_token_transfers', { hash: CURRENT_ADDRESS }) + '?type=ERC-1155'; await render(
<Box pt={{ base: '134px', lg: 6 }}>
await page.route(API_URL_WITH_FILTER, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ tokenTransferMock.erc1155A ], next_page_params: { block_number: 1 } }),
}));
await mount(
<TestApp withSocket>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTokenTransfers overloadCount={ 2 }/> <AddressTokenTransfers overloadCount={ 2 }/>
</TestApp>, </Box>,
{ hooksConfig: hooksConfigWithFilter }, { hooksConfig: hooksConfigWithFilter },
{ withSocket: true },
); );
const socket = await createSocket(); const socket = await createSocket();
......
...@@ -20,7 +20,7 @@ import useSocketChannel from 'lib/socket/useSocketChannel'; ...@@ -20,7 +20,7 @@ import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
import { TOKEN_TYPE_IDS } from 'lib/token/tokenTypes'; import { TOKEN_TYPE_IDS } from 'lib/token/tokenTypes';
import { getTokenTransfersStub } from 'stubs/token'; import { getTokenTransfersStub } from 'stubs/token';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar, { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay'; import DataListDisplay from 'ui/shared/DataListDisplay';
import * as TokenEntity from 'ui/shared/entities/token/TokenEntity'; import * as TokenEntity from 'ui/shared/entities/token/TokenEntity';
import HashStringShorten from 'ui/shared/HashStringShorten'; import HashStringShorten from 'ui/shared/HashStringShorten';
...@@ -204,7 +204,7 @@ const AddressTokenTransfers = ({ scrollRef, overloadCount = OVERLOAD_COUNT, shou ...@@ -204,7 +204,7 @@ const AddressTokenTransfers = ({ scrollRef, overloadCount = OVERLOAD_COUNT, shou
data={ data?.items } data={ data?.items }
baseAddress={ currentAddress } baseAddress={ currentAddress }
showTxInfo showTxInfo
top={ isActionBarHidden ? 0 : 80 } top={ isActionBarHidden ? 0 : ACTION_BAR_HEIGHT_DESKTOP }
enableTimeIncrement enableTimeIncrement
showSocketInfo={ pagination.page === 1 && !tokenFilter } showSocketInfo={ pagination.page === 1 && !tokenFilter }
socketInfoAlert={ socketAlert } socketInfoAlert={ socketAlert }
......
This diff is collapsed.
import { Box } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import { test as base, expect } from '@playwright/experimental-ct-react';
import type { Locator } from '@playwright/test'; import type { Locator } from '@playwright/test';
import React from 'react'; import React from 'react';
import * as txMock from 'mocks/txs/tx'; import * as txMock from 'mocks/txs/tx';
import * as socketServer from 'playwright/fixtures/socketServer'; import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp'; import { test, expect } from 'playwright/lib';
import buildApiUrl from 'playwright/utils/buildApiUrl'; import * as pwConfig from 'playwright/utils/config';
import * as configs from 'playwright/utils/configs';
import AddressTxs from './AddressTxs'; import AddressTxs from './AddressTxs';
const CURRENT_ADDRESS = '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859'; const CURRENT_ADDRESS = '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859';
const API_URL = buildApiUrl('address_txs', { hash: CURRENT_ADDRESS });
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { hash: CURRENT_ADDRESS }, query: { hash: CURRENT_ADDRESS },
}, },
}; };
const DEFAULT_PAGINATION = { block_number: 1, index: 1, items_count: 1 };
base.describe('base view', () => { test.describe('base view', () => {
let component: Locator; let component: Locator;
base.beforeEach(async({ page, mount }) => { test.beforeEach(async({ render, mockApiResponse }) => {
await page.route(API_URL, (route) => route.fulfill({ await mockApiResponse(
status: 200, 'address_txs',
body: JSON.stringify({ items: [ {
txMock.base, items: [
{ txMock.base,
...txMock.base, { ...txMock.base, hash: '0x62d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3194' },
hash: '0x62d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3194', ],
}, next_page_params: DEFAULT_PAGINATION,
], next_page_params: { block: 1 } }), },
})); { pathParams: { hash: CURRENT_ADDRESS } },
);
component = await mount( component = await render(
<TestApp> <Box pt={{ base: '134px', lg: 6 }}>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTxs/> <AddressTxs/>
</TestApp>, </Box>,
{ hooksConfig }, { hooksConfig },
); );
}); });
base('+@mobile', async() => { test('+@mobile', async() => {
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
base.describe('screen xl', () => { test.describe('screen xl', () => {
base.use({ viewport: configs.viewport.xl }); test.use({ viewport: pwConfig.viewport.xl });
base('', async() => { test('', async() => {
base.slow(); test.slow();
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
}); });
}); });
base.describe('socket', () => { test.describe('socket', () => {
const test = base.extend<socketServer.SocketServerFixture>({
createSocket: socketServer.createSocket,
});
// FIXME // FIXME
// test cases which use socket cannot run in parallel since the socket server always run on the same port // test cases which use socket cannot run in parallel since the socket server always run on the same port
test.describe.configure({ mode: 'serial' }); test.describe.configure({ mode: 'serial' });
test('without overload', async({ mount, page, createSocket }) => { test('without overload', async({ render, mockApiResponse, page, createSocket }) => {
await page.route(API_URL, (route) => route.fulfill({ await mockApiResponse(
status: 200, 'address_txs',
body: JSON.stringify({ items: [ txMock.base ], next_page_params: { block: 1 } }), { items: [ txMock.base ], next_page_params: DEFAULT_PAGINATION },
})); { pathParams: { hash: CURRENT_ADDRESS } },
);
await mount( await render(
<TestApp withSocket> <Box pt={{ base: '134px', lg: 6 }}>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTxs/> <AddressTxs/>
</TestApp>, </Box>,
{ hooksConfig }, { hooksConfig },
{ withSocket: true },
); );
const socket = await createSocket(); const socket = await createSocket();
...@@ -95,18 +89,19 @@ base.describe('socket', () => { ...@@ -95,18 +89,19 @@ base.describe('socket', () => {
expect(itemsCountNew).toBe(4); expect(itemsCountNew).toBe(4);
}); });
test('with update', async({ mount, page, createSocket }) => { test('with update', async({ render, mockApiResponse, page, createSocket }) => {
await page.route(API_URL, (route) => route.fulfill({ await mockApiResponse(
status: 200, 'address_txs',
body: JSON.stringify({ items: [ txMock.pending ], next_page_params: { block: 1 } }), { items: [ txMock.pending ], next_page_params: DEFAULT_PAGINATION },
})); { pathParams: { hash: CURRENT_ADDRESS } },
);
await mount( await render(
<TestApp withSocket> <Box pt={{ base: '134px', lg: 6 }}>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTxs/> <AddressTxs/>
</TestApp>, </Box>,
{ hooksConfig }, { hooksConfig },
{ withSocket: true },
); );
const socket = await createSocket(); const socket = await createSocket();
...@@ -123,18 +118,19 @@ base.describe('socket', () => { ...@@ -123,18 +118,19 @@ base.describe('socket', () => {
expect(itemsCountNew).toBe(3); expect(itemsCountNew).toBe(3);
}); });
test('with overload', async({ mount, page, createSocket }) => { test('with overload', async({ render, mockApiResponse, page, createSocket }) => {
await page.route(API_URL, (route) => route.fulfill({ await mockApiResponse(
status: 200, 'address_txs',
body: JSON.stringify({ items: [ txMock.base ], next_page_params: { block: 1 } }), { items: [ txMock.base ], next_page_params: DEFAULT_PAGINATION },
})); { pathParams: { hash: CURRENT_ADDRESS } },
);
await mount( await render(
<TestApp withSocket> <Box pt={{ base: '134px', lg: 6 }}>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTxs overloadCount={ 2 }/> <AddressTxs overloadCount={ 2 }/>
</TestApp>, </Box>,
{ hooksConfig }, { hooksConfig },
{ withSocket: true },
); );
const socket = await createSocket(); const socket = await createSocket();
...@@ -154,26 +150,25 @@ base.describe('socket', () => { ...@@ -154,26 +150,25 @@ base.describe('socket', () => {
expect(counter?.startsWith('2 ')).toBe(true); expect(counter?.startsWith('2 ')).toBe(true);
}); });
test('without overload, with filters', async({ mount, page, createSocket }) => { test('without overload, with filters', async({ render, mockApiResponse, page, createSocket }) => {
const hooksConfigWithFilter = { const hooksConfigWithFilter = {
router: { router: {
query: { hash: CURRENT_ADDRESS, filter: 'from' }, query: { hash: CURRENT_ADDRESS, filter: 'from' },
}, },
}; };
const API_URL_WITH_FILTER = buildApiUrl('address_txs', { hash: CURRENT_ADDRESS }) + '?filter=from'; await mockApiResponse(
'address_txs',
await page.route(API_URL_WITH_FILTER, (route) => route.fulfill({ { items: [ txMock.base ], next_page_params: DEFAULT_PAGINATION },
status: 200, { pathParams: { hash: CURRENT_ADDRESS }, queryParams: { filter: 'from' } },
body: JSON.stringify({ items: [ txMock.base ], next_page_params: { block: 1 } }), );
}));
await mount( await render(
<TestApp withSocket> <Box pt={{ base: '134px', lg: 6 }}>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTxs/> <AddressTxs/>
</TestApp>, </Box>,
{ hooksConfig: hooksConfigWithFilter }, { hooksConfig: hooksConfigWithFilter },
{ withSocket: true },
); );
const socket = await createSocket(); const socket = await createSocket();
...@@ -190,26 +185,25 @@ base.describe('socket', () => { ...@@ -190,26 +185,25 @@ base.describe('socket', () => {
expect(itemsCountNew).toBe(3); expect(itemsCountNew).toBe(3);
}); });
test('with overload, with filters', async({ mount, page, createSocket }) => { test('with overload, with filters', async({ render, mockApiResponse, page, createSocket }) => {
const hooksConfigWithFilter = { const hooksConfigWithFilter = {
router: { router: {
query: { hash: CURRENT_ADDRESS, filter: 'from' }, query: { hash: CURRENT_ADDRESS, filter: 'from' },
}, },
}; };
const API_URL_WITH_FILTER = buildApiUrl('address_txs', { hash: CURRENT_ADDRESS }) + '?filter=from'; await mockApiResponse(
'address_txs',
await page.route(API_URL_WITH_FILTER, (route) => route.fulfill({ { items: [ txMock.base ], next_page_params: DEFAULT_PAGINATION },
status: 200, { pathParams: { hash: CURRENT_ADDRESS }, queryParams: { filter: 'from' } },
body: JSON.stringify({ items: [ txMock.base ], next_page_params: { block: 1 } }), );
}));
await mount( await render(
<TestApp withSocket> <Box pt={{ base: '134px', lg: 6 }}>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTxs overloadCount={ 2 }/> <AddressTxs overloadCount={ 2 }/>
</TestApp>, </Box>,
{ hooksConfig: hooksConfigWithFilter }, { hooksConfig: hooksConfigWithFilter },
{ withSocket: true },
); );
const socket = await createSocket(); const socket = await createSocket();
......
...@@ -16,7 +16,7 @@ import useSocketChannel from 'lib/socket/useSocketChannel'; ...@@ -16,7 +16,7 @@ import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
import { TX } from 'stubs/tx'; import { TX } from 'stubs/tx';
import { generateListStub } from 'stubs/utils'; import { generateListStub } from 'stubs/utils';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar, { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar';
import Pagination from 'ui/shared/pagination/Pagination'; import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import getSortParamsFromValue from 'ui/shared/sort/getSortParamsFromValue'; import getSortParamsFromValue from 'ui/shared/sort/getSortParamsFromValue';
...@@ -199,7 +199,7 @@ const AddressTxs = ({ scrollRef, overloadCount = OVERLOAD_COUNT, shouldRender = ...@@ -199,7 +199,7 @@ const AddressTxs = ({ scrollRef, overloadCount = OVERLOAD_COUNT, shouldRender =
showSocketInfo={ addressTxsQuery.pagination.page === 1 } showSocketInfo={ addressTxsQuery.pagination.page === 1 }
socketInfoAlert={ socketAlert } socketInfoAlert={ socketAlert }
socketInfoNum={ newItemsCount } socketInfoNum={ newItemsCount }
top={ 80 } top={ ACTION_BAR_HEIGHT_DESKTOP }
sorting={ sort } sorting={ sort }
setSort={ setSort } setSort={ setSort }
/> />
......
...@@ -6,7 +6,7 @@ import useIsMounted from 'lib/hooks/useIsMounted'; ...@@ -6,7 +6,7 @@ import useIsMounted from 'lib/hooks/useIsMounted';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import { generateListStub } from 'stubs/utils'; import { generateListStub } from 'stubs/utils';
import { WITHDRAWAL } from 'stubs/withdrawals'; import { WITHDRAWAL } from 'stubs/withdrawals';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar, { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay'; import DataListDisplay from 'ui/shared/DataListDisplay';
import Pagination from 'ui/shared/pagination/Pagination'; import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
...@@ -52,7 +52,12 @@ const AddressWithdrawals = ({ scrollRef, shouldRender = true }: Props) => { ...@@ -52,7 +52,12 @@ const AddressWithdrawals = ({ scrollRef, shouldRender = true }: Props) => {
)) } )) }
</Show> </Show>
<Hide below="lg" ssr={ false }> <Hide below="lg" ssr={ false }>
<BeaconChainWithdrawalsTable items={ data.items } view="address" top={ pagination.isVisible ? 80 : 0 } isLoading={ isPlaceholderData }/> <BeaconChainWithdrawalsTable
items={ data.items }
view="address"
top={ pagination.isVisible ? ACTION_BAR_HEIGHT_DESKTOP : 0 }
isLoading={ isPlaceholderData }
/>
</Hide> </Hide>
</> </>
) : null ; ) : null ;
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import * as solidityscanReportMock from 'mocks/contract/solidityscanReport'; import * as solidityscanReportMock from 'mocks/contract/solidityscanReport';
import TestApp from 'playwright/TestApp'; import { test, expect } from 'playwright/lib';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import SolidityscanReport from './SolidityscanReport'; import SolidityscanReport from './SolidityscanReport';
const addressHash = 'hash'; const addressHash = 'hash';
const REPORT_API_URL = buildApiUrl('contract_solidityscan_report', { hash: addressHash });
test('average report +@dark-mode +@mobile', async({ mount, page }) => {
await page.route(REPORT_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(solidityscanReportMock.solidityscanReportAverage),
}));
const component = await mount(
<TestApp>
<SolidityscanReport hash={ addressHash }/>
</TestApp>,
);
test('average report +@dark-mode +@mobile', async({ render, mockApiResponse, page }) => {
await mockApiResponse(
'contract_solidityscan_report',
solidityscanReportMock.solidityscanReportAverage,
{ pathParams: { hash: addressHash } },
);
const component = await render(<SolidityscanReport hash={ addressHash }/>);
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 100, height: 50 } }); await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 100, height: 50 } });
await component.getByLabel('SolidityScan score').click(); await component.getByLabel('SolidityScan score').click();
...@@ -29,16 +21,15 @@ test('average report +@dark-mode +@mobile', async({ mount, page }) => { ...@@ -29,16 +21,15 @@ test('average report +@dark-mode +@mobile', async({ mount, page }) => {
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 400, height: 500 } }); await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 400, height: 500 } });
}); });
test('great report', async({ mount, page }) => { test('great report', async({ render, mockApiResponse, page }) => {
await page.route(REPORT_API_URL, (route) => route.fulfill({ await mockApiResponse(
status: 200, 'contract_solidityscan_report',
body: JSON.stringify(solidityscanReportMock.solidityscanReportGreat), solidityscanReportMock.solidityscanReportGreat,
})); { pathParams: { hash: addressHash } },
);
const component = await mount( const component = await render(
<TestApp> <SolidityscanReport hash={ addressHash }/>,
<SolidityscanReport hash={ addressHash }/>
</TestApp>,
); );
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 100, height: 50 } }); await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 100, height: 50 } });
...@@ -48,16 +39,15 @@ test('great report', async({ mount, page }) => { ...@@ -48,16 +39,15 @@ test('great report', async({ mount, page }) => {
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 400, height: 500 } }); await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 400, height: 500 } });
}); });
test('low report', async({ mount, page }) => { test('low report', async({ render, mockApiResponse, page }) => {
await page.route(REPORT_API_URL, (route) => route.fulfill({ await mockApiResponse(
status: 200, 'contract_solidityscan_report',
body: JSON.stringify(solidityscanReportMock.solidityscanReportLow), solidityscanReportMock.solidityscanReportLow,
})); { pathParams: { hash: addressHash } },
);
const component = await mount( const component = await render(
<TestApp> <SolidityscanReport hash={ addressHash }/>,
<SolidityscanReport hash={ addressHash }/>
</TestApp>,
); );
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 100, height: 50 } }); await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 100, height: 50 } });
......
import { Box, Text, chakra, Icon, Popover, PopoverTrigger, PopoverContent, PopoverBody, useDisclosure } from '@chakra-ui/react'; import { Box, Text, Icon, Popover, PopoverTrigger, PopoverContent, PopoverBody, useDisclosure } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
// This icon doesn't work properly when it is in the sprite // This icon doesn't work properly when it is in the sprite
...@@ -7,17 +7,16 @@ import React from 'react'; ...@@ -7,17 +7,16 @@ import React from 'react';
import solidityScanIcon from 'icons/brands/solidity_scan.svg'; import solidityScanIcon from 'icons/brands/solidity_scan.svg';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import { SOLIDITYSCAN_REPORT } from 'stubs/contract'; 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 SolidityscanReportButton from 'ui/shared/solidityscanReport/SolidityscanReportButton';
import SolidityscanReportDetails from 'ui/shared/solidityscanReport/SolidityscanReportDetails'; import SolidityscanReportDetails from 'ui/shared/solidityscanReport/SolidityscanReportDetails';
import SolidityscanReportScore from 'ui/shared/solidityscanReport/SolidityscanReportScore'; import SolidityscanReportScore from 'ui/shared/solidityscanReport/SolidityscanReportScore';
interface Props { interface Props {
className?: string;
hash: string; hash: string;
} }
const SolidityscanReport = ({ className, hash }: Props) => { const SolidityscanReport = ({ hash }: Props) => {
const { isOpen, onToggle, onClose } = useDisclosure(); const { isOpen, onToggle, onClose } = useDisclosure();
const { data, isPlaceholderData, isError } = useApiQuery('contract_solidityscan_report', { const { data, isPlaceholderData, isError } = useApiQuery('contract_solidityscan_report', {
...@@ -42,7 +41,6 @@ const SolidityscanReport = ({ className, hash }: Props) => { ...@@ -42,7 +41,6 @@ const SolidityscanReport = ({ className, hash }: Props) => {
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy> <Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy>
<PopoverTrigger> <PopoverTrigger>
<SolidityscanReportButton <SolidityscanReportButton
className={ className }
score={ score } score={ score }
isLoading={ isPlaceholderData } isLoading={ isPlaceholderData }
onClick={ onToggle } onClick={ onToggle }
...@@ -69,4 +67,4 @@ const SolidityscanReport = ({ className, hash }: Props) => { ...@@ -69,4 +67,4 @@ const SolidityscanReport = ({ className, hash }: Props) => {
); );
}; };
export default chakra(SolidityscanReport); export default React.memo(SolidityscanReport);
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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