Commit f8621e35 authored by Max Alekseenko's avatar Max Alekseenko

Merge branch 'main' into defi-dropdown

parents 44eabdac b5757e40
...@@ -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/*',
......
...@@ -16,7 +16,9 @@ export { default as growthBook } from './growthBook'; ...@@ -16,7 +16,9 @@ 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 restApiDocs } from './restApiDocs'; export { default as restApiDocs } from './restApiDocs';
export { default as rollup } from './rollup'; export { default as rollup } from './rollup';
export { default as safe } from './safe'; export { default as safe } from './safe';
......
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;
import type { Feature } from './types';
import services from '../services';
import { getEnvValue } from '../utils';
import addressMetadata from './addressMetadata';
const apiHost = getEnvValue('NEXT_PUBLIC_ADMIN_SERVICE_API_HOST');
const title = 'Public tag submission';
const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => {
if (services.reCaptcha.siteKey && addressMetadata.isEnabled && apiHost) {
return Object.freeze({
title,
isEnabled: true,
api: {
endpoint: apiHost,
basePath: '',
},
});
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
...@@ -24,6 +24,11 @@ const hiddenLinks = (() => { ...@@ -24,6 +24,11 @@ const hiddenLinks = (() => {
return result; return result;
})(); })();
const highlightedRoutes = (() => {
const parsedValue = parseEnvJson<Array<NavigationLinkId>>(getEnvValue('NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES'));
return Array.isArray(parsedValue) ? parsedValue : [];
})();
const defaultColorTheme = (() => { const defaultColorTheme = (() => {
const envValue = getEnvValue('NEXT_PUBLIC_COLOR_THEME_DEFAULT') as ColorThemeId | undefined; const envValue = getEnvValue('NEXT_PUBLIC_COLOR_THEME_DEFAULT') as ColorThemeId | undefined;
return COLOR_THEMES.find((theme) => theme.id === envValue); return COLOR_THEMES.find((theme) => theme.id === envValue);
...@@ -43,6 +48,7 @@ const UI = Object.freeze({ ...@@ -43,6 +48,7 @@ const UI = Object.freeze({
dark: getExternalAssetFilePath('NEXT_PUBLIC_NETWORK_ICON_DARK'), dark: getExternalAssetFilePath('NEXT_PUBLIC_NETWORK_ICON_DARK'),
}, },
hiddenLinks, hiddenLinks,
highlightedRoutes,
otherLinks: parseEnvJson<Array<NavItemExternal>>(getEnvValue('NEXT_PUBLIC_OTHER_LINKS')) || [], otherLinks: parseEnvJson<Array<NavItemExternal>>(getEnvValue('NEXT_PUBLIC_OTHER_LINKS')) || [],
featuredNetworks: getExternalAssetFilePath('NEXT_PUBLIC_FEATURED_NETWORKS'), featuredNetworks: getExternalAssetFilePath('NEXT_PUBLIC_FEATURED_NETWORKS'),
}, },
......
...@@ -48,12 +48,14 @@ NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-mainnet.safe.global ...@@ -48,12 +48,14 @@ 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_DEFI_DROPDOWN_ITEMS=[{'text':'Swap','icon':'swap','dappId':'uniswap'},{'text':'Payment link','icon':'payment_link','dappId':'peanut-protocol'},{'text':'Get gas','icon':'gas','dappId':'smol-refuel'}] NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS=[{'text':'Swap','icon':'swap','dappId':'uniswap'},{'text':'Payment link','icon':'payment_link','dappId':'peanut-protocol'},{'text':'Get gas','icon':'gas','dappId':'smol-refuel'}]
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
......
...@@ -56,4 +56,4 @@ NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.blocks ...@@ -56,4 +56,4 @@ NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.blocks
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
#meta #meta
NEXT_PUBLIC_OG_IMAGE_URL=https://github.com/blockscout/frontend-configs/blob/main/configs/og-images/eth-goerli.png?raw=true NEXT_PUBLIC_OG_IMAGE_URL=https://github.com/blockscout/frontend-configs/blob/main/configs/og-images/eth-goerli.png?raw=true
\ No newline at end of file
...@@ -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
...@@ -33,6 +33,7 @@ NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/front ...@@ -33,6 +33,7 @@ NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/front
NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/sepolia.png NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/sepolia.png
NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/sepolia.png NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/sepolia.png
NEXT_PUBLIC_OTHER_LINKS=[{'url':'https://sepolia.drpc.org?ref=559183','text':'Public RPC'}] NEXT_PUBLIC_OTHER_LINKS=[{'url':'https://sepolia.drpc.org?ref=559183','text':'Public RPC'}]
NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/blocks','/apps']
## footer ## footer
NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/sepolia.json NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/sepolia.json
##views ##views
......
...@@ -28,6 +28,7 @@ NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] ...@@ -28,6 +28,7 @@ NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/featured-networks/eth-goerli.json NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/featured-networks/eth-goerli.json
NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/network-logos/goerli.svg NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/network-logos/goerli.svg
NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/network-icons/goerli.svg NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/network-icons/goerli.svg
NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/accounts','/apps']
## footer ## footer
##views ##views
NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES=[{'name':'LooksRare','collection_url':'https://goerli.looksrare.org/collections/{hash}','instance_url':'https://goerli.looksrare.org/collections/{hash}/{id}','logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/nft-marketplace-logos/looks-rare.png'}] NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES=[{'name':'LooksRare','collection_url':'https://goerli.looksrare.org/collections/{hash}','instance_url':'https://goerli.looksrare.org/collections/{hash}/{id}','logo_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/nft-marketplace-logos/looks-rare.png'}]
......
...@@ -16,6 +16,7 @@ import type { DeFiDropdownItem } from '../../../types/client/deFiDropdown'; ...@@ -16,6 +16,7 @@ import type { DeFiDropdownItem } from '../../../types/client/deFiDropdown';
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';
...@@ -354,21 +355,6 @@ const accountSchema = yup ...@@ -354,21 +355,6 @@ const accountSchema = yup
}), }),
}); });
const adminServiceSchema = yup
.object()
.shape({
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST: yup
.string()
.when([ 'NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED', 'NEXT_PUBLIC_MARKETPLACE_ENABLED' ], {
is: (value1: boolean, value2: boolean) => value1 || value2,
then: (schema) => schema.test(urlTest),
otherwise: (schema) => schema.max(
-1,
'NEXT_PUBLIC_ADMIN_SERVICE_API_HOST cannot not be used if NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED or NEXT_PUBLIC_MARKETPLACE_ENABLED is not set to "true"',
),
}),
});
const featuredNetworkSchema: yup.ObjectSchema<FeaturedNetwork> = yup const featuredNetworkSchema: yup.ObjectSchema<FeaturedNetwork> = yup
.object() .object()
.shape({ .shape({
...@@ -541,6 +527,11 @@ const schema = yup ...@@ -541,6 +527,11 @@ const schema = yup
.transform(replaceQuotes) .transform(replaceQuotes)
.json() .json()
.of(yup.string<NavigationLinkId>().oneOf(NAVIGATION_LINK_IDS)), .of(yup.string<NavigationLinkId>().oneOf(NAVIGATION_LINK_IDS)),
NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES: yup
.array()
.transform(replaceQuotes)
.json()
.of(yup.string()),
NEXT_PUBLIC_NETWORK_LOGO: yup.string().test(urlTest), NEXT_PUBLIC_NETWORK_LOGO: yup.string().test(urlTest),
NEXT_PUBLIC_NETWORK_LOGO_DARK: yup.string().test(urlTest), NEXT_PUBLIC_NETWORK_LOGO_DARK: yup.string().test(urlTest),
NEXT_PUBLIC_NETWORK_ICON: yup.string().test(urlTest), NEXT_PUBLIC_NETWORK_ICON: yup.string().test(urlTest),
...@@ -607,6 +598,7 @@ const schema = yup ...@@ -607,6 +598,7 @@ const schema = yup
NEXT_PUBLIC_CONTRACT_INFO_API_HOST: yup.string().test(urlTest), NEXT_PUBLIC_CONTRACT_INFO_API_HOST: yup.string().test(urlTest),
NEXT_PUBLIC_NAME_SERVICE_API_HOST: yup.string().test(urlTest), NEXT_PUBLIC_NAME_SERVICE_API_HOST: yup.string().test(urlTest),
NEXT_PUBLIC_METADATA_SERVICE_API_HOST: yup.string().test(urlTest), NEXT_PUBLIC_METADATA_SERVICE_API_HOST: yup.string().test(urlTest),
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST: yup.string().test(urlTest),
NEXT_PUBLIC_GRAPHIQL_TRANSACTION: yup.string().matches(regexp.HEX_REGEXP), NEXT_PUBLIC_GRAPHIQL_TRANSACTION: yup.string().matches(regexp.HEX_REGEXP),
NEXT_PUBLIC_WEB3_WALLETS: yup NEXT_PUBLIC_WEB3_WALLETS: yup
.mixed() .mixed()
...@@ -632,6 +624,19 @@ const schema = yup ...@@ -632,6 +624,19 @@ const schema = yup
NEXT_PUBLIC_IS_SUAVE_CHAIN: yup.boolean(), NEXT_PUBLIC_IS_SUAVE_CHAIN: yup.boolean(),
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_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)),
...@@ -658,7 +663,6 @@ const schema = yup ...@@ -658,7 +663,6 @@ const schema = yup
.concat(rollupSchema) .concat(rollupSchema)
.concat(beaconChainSchema) .concat(beaconChainSchema)
.concat(bridgedTokensSchema) .concat(bridgedTokensSchema)
.concat(sentrySchema) .concat(sentrySchema);
.concat(adminServiceSchema);
export default schema; export default schema;
...@@ -26,6 +26,7 @@ NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.blocks ...@@ -26,6 +26,7 @@ NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.blocks
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://example.com NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://example.com
NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED=true NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED=true
NEXT_PUBLIC_FEATURED_NETWORKS=https://example.com NEXT_PUBLIC_FEATURED_NETWORKS=https://example.com
NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/accounts','/apps']
NEXT_PUBLIC_FOOTER_LINKS=https://example.com NEXT_PUBLIC_FOOTER_LINKS=https://example.com
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d
NEXT_PUBLIC_HIDE_INDEXING_ALERT_BLOCKS=false NEXT_PUBLIC_HIDE_INDEXING_ALERT_BLOCKS=false
...@@ -74,3 +75,4 @@ NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET=false ...@@ -74,3 +75,4 @@ 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_VALIDATORS_CHAIN_TYPE=stability NEXT_PUBLIC_VALIDATORS_CHAIN_TYPE=stability
NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS=[{'text':'Swap','icon':'swap','dappId':'uniswap'},{'text':'Payment link','icon':'payment_link','url':'https://example.com'}] NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS=[{'text':'Swap','icon':'swap','dappId':'uniswap'},{'text':'Payment link','icon':'payment_link','url':'https://example.com'}]
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;
...@@ -128,6 +129,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will ...@@ -128,6 +129,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will
| NEXT_PUBLIC_FEATURED_NETWORKS | `string` | URL of configuration file (`.json` format only) which contains list of featured networks that will be shown in the network menu. See [below](#featured-network-configuration-properties) list of available properties for particular network | - | - | `https://example.com/featured_networks_config.json` | | NEXT_PUBLIC_FEATURED_NETWORKS | `string` | URL of configuration file (`.json` format only) which contains list of featured networks that will be shown in the network menu. See [below](#featured-network-configuration-properties) list of available properties for particular network | - | - | `https://example.com/featured_networks_config.json` |
| NEXT_PUBLIC_OTHER_LINKS | `Array<{url: string; text: string}>` | List of links for the "Other" navigation menu | - | - | `[{'url':'https://blockscout.com','text':'Blockscout'}]` | | NEXT_PUBLIC_OTHER_LINKS | `Array<{url: string; text: string}>` | List of links for the "Other" navigation menu | - | - | `[{'url':'https://blockscout.com','text':'Blockscout'}]` |
| NEXT_PUBLIC_NAVIGATION_HIDDEN_LINKS | `Array<LinkId>` | List of external links hidden in the navigation. Supported ids are `eth_rpc_api`, `rpc_api` | - | - | `['eth_rpc_api']` | | NEXT_PUBLIC_NAVIGATION_HIDDEN_LINKS | `Array<LinkId>` | List of external links hidden in the navigation. Supported ids are `eth_rpc_api`, `rpc_api` | - | - | `['eth_rpc_api']` |
| NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES | `Array<string>` | List of menu item routes that should have a lightning label | - | - | `['/accounts']` |
#### Featured network configuration properties #### Featured network configuration properties
...@@ -567,6 +569,17 @@ This feature allows name tags and other public tags for addresses. ...@@ -567,6 +569,17 @@ This feature allows name tags and other public tags for addresses.
&nbsp; &nbsp;
### Public tag submission
This feature allows you to submit an application with a public address tag.
| Variable | Type| Description | Compulsoriness | Default value | Example value |
| --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_METADATA_SERVICE_API_HOST | `string` | Metadata Service API endpoint url | Required | - | `https://metadata.services.blockscout.com` |
| NEXT_PUBLIC_ADMIN_SERVICE_API_HOST | `string` | Admin Service API endpoint url | Required | - | `https://admin-rs.services.blockscout.com` |
&nbsp;
### Data Availability ### Data Availability
This feature enables views related to blob transactions (EIP-4844), such as the Blob Txns tab on the Transactions page and the Blob details page. This feature enables views related to blob transactions (EIP-4844), such as the Blob Txns tab on the Transactions page and the Blob details page.
...@@ -679,6 +692,27 @@ If the feature is enabled, a single button or a dropdown (if more than 1 item is ...@@ -679,6 +692,27 @@ If the feature is enabled, a single button or a dropdown (if more than 1 item is
&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
......
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.375 16v-5.006H.676L7.697 0h7.9v6l.027 10H5.374Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.72 6.01V0L2.63 10.15h4.02V16l6.66-9.99H8.72Z" fill="#ED8936"/>
</svg>
...@@ -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 };
},
}, },
}); });
} }
...@@ -2,7 +2,6 @@ import { getFeaturePayload } from 'configs/app/features/types'; ...@@ -2,7 +2,6 @@ import { getFeaturePayload } from 'configs/app/features/types';
import type { import type {
UserInfo, UserInfo,
CustomAbis, CustomAbis,
PublicTags,
ApiKeys, ApiKeys,
VerifiedAddressResponse, VerifiedAddressResponse,
TokenInfoApplicationConfig, TokenInfoApplicationConfig,
...@@ -32,7 +31,7 @@ import type { ...@@ -32,7 +31,7 @@ import type {
AddressCoinBalanceHistoryChartOld, AddressCoinBalanceHistoryChartOld,
} from 'types/api/address'; } from 'types/api/address';
import type { AddressesResponse } from 'types/api/addresses'; import type { AddressesResponse } from 'types/api/addresses';
import type { AddressMetadataInfo } from 'types/api/addressMetadata'; import type { AddressMetadataInfo, PublicTagTypesResponse } from 'types/api/addressMetadata';
import type { TxBlobs, Blob } from 'types/api/blobs'; import type { TxBlobs, Blob } from 'types/api/blobs';
import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters, BlockWithdrawalsResponse } from 'types/api/block'; import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters, BlockWithdrawalsResponse } from 'types/api/block';
import type { ChartMarketResponse, ChartSecondaryCoinPriceResponse, ChartTransactionResponse } from 'types/api/charts'; import type { ChartMarketResponse, ChartSecondaryCoinPriceResponse, ChartTransactionResponse } from 'types/api/charts';
...@@ -147,10 +146,6 @@ export const RESOURCES = { ...@@ -147,10 +146,6 @@ export const RESOURCES = {
pathParams: [ 'id' as const ], pathParams: [ 'id' as const ],
filterFields: [ ], filterFields: [ ],
}, },
public_tags: {
path: '/api/account/v2/user/public_tags/:id?',
pathParams: [ 'id' as const ],
},
private_tags_address: { private_tags_address: {
path: '/api/account/v2/user/tags/address/:id?', path: '/api/account/v2/user/tags/address/:id?',
pathParams: [ 'id' as const ], pathParams: [ 'id' as const ],
...@@ -245,7 +240,7 @@ export const RESOURCES = { ...@@ -245,7 +240,7 @@ export const RESOURCES = {
filterFields: [ 'name' as const, 'only_active' as const ], filterFields: [ 'name' as const, 'only_active' as const ],
}, },
// METADATA SERVICE // METADATA SERVICE & PUBLIC TAGS
address_metadata_info: { address_metadata_info: {
path: '/api/v1/metadata', path: '/api/v1/metadata',
endpoint: getFeaturePayload(config.features.addressMetadata)?.api.endpoint, endpoint: getFeaturePayload(config.features.addressMetadata)?.api.endpoint,
...@@ -256,6 +251,17 @@ export const RESOURCES = { ...@@ -256,6 +251,17 @@ export const RESOURCES = {
endpoint: getFeaturePayload(config.features.addressMetadata)?.api.endpoint, endpoint: getFeaturePayload(config.features.addressMetadata)?.api.endpoint,
basePath: getFeaturePayload(config.features.addressMetadata)?.api.basePath, basePath: getFeaturePayload(config.features.addressMetadata)?.api.basePath,
}, },
address_metadata_tag_types: {
path: '/api/v1/public-tag-types',
endpoint: getFeaturePayload(config.features.addressMetadata)?.api.endpoint,
basePath: getFeaturePayload(config.features.addressMetadata)?.api.basePath,
},
public_tag_application: {
path: '/api/v1/chains/:chainId/metadata-submissions/tag',
pathParams: [ 'chainId' as const ],
endpoint: getFeaturePayload(config.features.publicTagsSubmission)?.api.endpoint,
basePath: getFeaturePayload(config.features.publicTagsSubmission)?.api.basePath,
},
// VISUALIZATION // VISUALIZATION
visualize_sol2uml: { visualize_sol2uml: {
...@@ -863,7 +869,6 @@ export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q> ...@@ -863,7 +869,6 @@ export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>
export type ResourcePayloadA<Q extends ResourceName> = export type ResourcePayloadA<Q extends ResourceName> =
Q extends 'user_info' ? UserInfo : Q extends 'user_info' ? UserInfo :
Q extends 'custom_abi' ? CustomAbis : Q extends 'custom_abi' ? CustomAbis :
Q extends 'public_tags' ? PublicTags :
Q extends 'private_tags_address' ? AddressTagsResponse : Q extends 'private_tags_address' ? AddressTagsResponse :
Q extends 'private_tags_tx' ? TransactionTagsResponse : Q extends 'private_tags_tx' ? TransactionTagsResponse :
Q extends 'api_keys' ? ApiKeys : Q extends 'api_keys' ? ApiKeys :
...@@ -956,6 +961,7 @@ Q extends 'optimistic_l2_deposits_count' ? number : ...@@ -956,6 +961,7 @@ Q extends 'optimistic_l2_deposits_count' ? number :
Q extends 'optimistic_l2_txn_batches_count' ? number : Q extends 'optimistic_l2_txn_batches_count' ? number :
Q extends 'config_backend_version' ? BackendVersionConfig : Q extends 'config_backend_version' ? BackendVersionConfig :
Q extends 'address_metadata_info' ? AddressMetadataInfo : Q extends 'address_metadata_info' ? AddressMetadataInfo :
Q extends 'address_metadata_tag_types' ? PublicTagTypesResponse :
never; never;
// !!! IMPORTANT !!! // !!! IMPORTANT !!!
// See comment above // See comment above
......
...@@ -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() => {
......
...@@ -237,6 +237,11 @@ export default function useNavItems(): ReturnType { ...@@ -237,6 +237,11 @@ export default function useNavItems(): ReturnType {
nextRoute: { pathname: '/gas-tracker' as const }, nextRoute: { pathname: '/gas-tracker' as const },
isActive: pathname.startsWith('/gas-tracker'), isActive: pathname.startsWith('/gas-tracker'),
}, },
config.features.publicTagsSubmission.isEnabled && {
text: 'Submit public tag',
nextRoute: { pathname: '/public-tags/submit' as const },
isActive: pathname.startsWith('/public-tags/submit'),
},
...config.UI.sidebar.otherLinks, ...config.UI.sidebar.otherLinks,
].filter(Boolean), ].filter(Boolean),
}, },
...@@ -255,12 +260,6 @@ export default function useNavItems(): ReturnType { ...@@ -255,12 +260,6 @@ export default function useNavItems(): ReturnType {
icon: 'privattags', icon: 'privattags',
isActive: pathname === '/account/tag-address', isActive: pathname === '/account/tag-address',
}, },
{
text: 'Public tags',
nextRoute: { pathname: '/account/public-tags-request' as const },
icon: 'publictags',
isActive: pathname === '/account/public-tags-request',
},
{ {
text: 'API keys', text: 'API keys',
nextRoute: { pathname: '/account/api-key' as const }, nextRoute: { pathname: '/account/api-key' as const },
......
...@@ -27,9 +27,9 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = { ...@@ -27,9 +27,9 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
'/account/watchlist': 'Regular page', '/account/watchlist': 'Regular page',
'/account/api-key': 'Regular page', '/account/api-key': 'Regular page',
'/account/custom-abi': 'Regular page', '/account/custom-abi': 'Regular page',
'/account/public-tags-request': 'Regular page',
'/account/tag-address': 'Regular page', '/account/tag-address': 'Regular page',
'/account/verified-addresses': 'Root page', '/account/verified-addresses': 'Root page',
'/public-tags/submit': 'Regular page',
'/withdrawals': 'Root page', '/withdrawals': 'Root page',
'/visualize/sol2uml': 'Regular page', '/visualize/sol2uml': 'Regular page',
'/csv-export': 'Regular page', '/csv-export': 'Regular page',
......
/* eslint-disable max-len */
import type { Route } from 'nextjs-routes'; import type { Route } from 'nextjs-routes';
// equal og:description // equal og:description
...@@ -30,9 +31,9 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = { ...@@ -30,9 +31,9 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/account/watchlist': DEFAULT_TEMPLATE, '/account/watchlist': DEFAULT_TEMPLATE,
'/account/api-key': DEFAULT_TEMPLATE, '/account/api-key': DEFAULT_TEMPLATE,
'/account/custom-abi': DEFAULT_TEMPLATE, '/account/custom-abi': DEFAULT_TEMPLATE,
'/account/public-tags-request': DEFAULT_TEMPLATE,
'/account/tag-address': DEFAULT_TEMPLATE, '/account/tag-address': DEFAULT_TEMPLATE,
'/account/verified-addresses': DEFAULT_TEMPLATE, '/account/verified-addresses': DEFAULT_TEMPLATE,
'/public-tags/submit': 'Propose a new public tag for your address, contract or set of contracts for your dApp. Our team will review and approve your submission. Public tags are incredible tool which helps users identify contracts and addresses.',
'/withdrawals': DEFAULT_TEMPLATE, '/withdrawals': DEFAULT_TEMPLATE,
'/visualize/sol2uml': DEFAULT_TEMPLATE, '/visualize/sol2uml': DEFAULT_TEMPLATE,
'/csv-export': DEFAULT_TEMPLATE, '/csv-export': DEFAULT_TEMPLATE,
......
...@@ -25,9 +25,9 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = { ...@@ -25,9 +25,9 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/account/watchlist': '- watchlist', '/account/watchlist': '- watchlist',
'/account/api-key': '- API keys', '/account/api-key': '- API keys',
'/account/custom-abi': '- custom ABI', '/account/custom-abi': '- custom ABI',
'/account/public-tags-request': '- public tag requests',
'/account/tag-address': '- private tags', '/account/tag-address': '- private tags',
'/account/verified-addresses': '- my verified addresses', '/account/verified-addresses': '- my verified addresses',
'/public-tags/submit': 'submit public tag',
'/withdrawals': 'withdrawals', '/withdrawals': 'withdrawals',
'/visualize/sol2uml': 'Solidity UML diagram', '/visualize/sol2uml': 'Solidity UML diagram',
'/csv-export': 'export data to CSV', '/csv-export': 'export data to CSV',
......
...@@ -25,9 +25,9 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = { ...@@ -25,9 +25,9 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
'/account/watchlist': 'Watchlist', '/account/watchlist': 'Watchlist',
'/account/api-key': 'API keys', '/account/api-key': 'API keys',
'/account/custom-abi': 'Custom ABI', '/account/custom-abi': 'Custom ABI',
'/account/public-tags-request': 'Public tags',
'/account/tag-address': 'Private tags', '/account/tag-address': 'Private tags',
'/account/verified-addresses': 'Verified addresses', '/account/verified-addresses': 'Verified addresses',
'/public-tags/submit': 'Submit public tag',
'/withdrawals': 'Withdrawals', '/withdrawals': 'Withdrawals',
'/visualize/sol2uml': 'Solidity UML diagram', '/visualize/sol2uml': 'Solidity UML diagram',
'/csv-export': 'Export data to CSV file', '/csv-export': 'Export data to CSV file',
......
export const COLOR_HEX_REGEXP = /^#[A-Fa-f\d]{3,6}$/;
export const validator = (value: string | undefined) => {
if (!value || value.length === 0) {
return true;
}
if (value.length !== 4 && value.length !== 7) {
return 'Invalid length';
}
if (!COLOR_HEX_REGEXP.test(value)) {
return 'Invalid hex code';
}
return true;
};
...@@ -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',
},
};
export const publicTagTypes = {
tagTypes: [
{
id: '96f9db76-02fc-477d-a003-640a0c5e7e15',
type: 'name' as const,
description: 'Alias for the address',
},
{
id: 'e75f396e-f52a-44c9-8790-a1dbae496b72',
type: 'generic' as const,
description: 'Group classification for the address',
},
{
id: '11a2d4f3-412e-4eb7-b663-86c6f48cdec3',
type: 'information' as const,
description: 'Tags with custom data for the address, e.g. additional link to project, or classification details, or minor account details',
},
{
id: 'd37443d4-748f-4314-a4a0-283b666e9f29',
type: 'classifier' as const,
description: 'E.g. "ERC20", "Contract", "CEX", "DEX", "NFT"',
},
{
id: 'ea9d0f91-9b46-44ff-be70-128bac468f6f',
type: 'protocol' as const,
description: 'Special tag type for protocol-related contracts, e.g. for bridges',
},
{
id: 'd2600acb-473c-445f-ac72-ed6fef53e06a',
type: 'note' as const,
description: 'Short general-purpose description for the address',
},
],
};
...@@ -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',
......
...@@ -240,3 +240,14 @@ export const login: GetServerSideProps<Props> = async(context) => { ...@@ -240,3 +240,14 @@ export const login: GetServerSideProps<Props> = async(context) => {
return base(context); return base(context);
}; };
export const publicTagsSubmit: GetServerSideProps<Props> = async(context) => {
if (!config.features.publicTagsSubmission.isEnabled) {
return {
notFound: true,
};
}
return base(context);
};
...@@ -9,7 +9,6 @@ declare module "nextjs-routes" { ...@@ -9,7 +9,6 @@ declare module "nextjs-routes" {
| StaticRoute<"/404"> | StaticRoute<"/404">
| StaticRoute<"/account/api-key"> | StaticRoute<"/account/api-key">
| StaticRoute<"/account/custom-abi"> | StaticRoute<"/account/custom-abi">
| StaticRoute<"/account/public-tags-request">
| StaticRoute<"/account/tag-address"> | StaticRoute<"/account/tag-address">
| StaticRoute<"/account/verified-addresses"> | StaticRoute<"/account/verified-addresses">
| StaticRoute<"/account/watchlist"> | StaticRoute<"/account/watchlist">
...@@ -45,6 +44,7 @@ declare module "nextjs-routes" { ...@@ -45,6 +44,7 @@ declare module "nextjs-routes" {
| DynamicRoute<"/op/[hash]", { "hash": string }> | DynamicRoute<"/op/[hash]", { "hash": string }>
| StaticRoute<"/ops"> | StaticRoute<"/ops">
| StaticRoute<"/output-roots"> | StaticRoute<"/output-roots">
| StaticRoute<"/public-tags/submit">
| StaticRoute<"/search-results"> | StaticRoute<"/search-results">
| StaticRoute<"/stats"> | StaticRoute<"/stats">
| DynamicRoute<"/token/[hash]", { "hash": string }> | DynamicRoute<"/token/[hash]", { "hash": string }>
......
...@@ -49,16 +49,8 @@ const oldUrls = [ ...@@ -49,16 +49,8 @@ const oldUrls = [
destination: '/account/custom-abi', destination: '/account/custom-abi',
}, },
{ {
source: '/account/public_tags_request', source: '/account/public-tags-request',
destination: '/account/public-tags-request', destination: '/public-tags/submit',
},
{
source: '/account/public_tags_request/:id/edit',
destination: '/account/public-tags-request',
},
{
source: '/account/public_tags_request/new',
destination: '/account/public-tags-request',
}, },
// TRANSACTIONS // TRANSACTIONS
......
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import React from 'react'; import React from 'react';
import PageNextJs from 'nextjs/PageNextJs'; import PageNextJs from 'nextjs/PageNextJs';
const PublicTags = dynamic(() => import('ui/pages/PublicTags'), { ssr: false }); import PublicTagsSubmit from 'ui/pages/PublicTagsSubmit';
const Page: NextPage = () => { const Page: NextPage = () => {
return ( return (
<PageNextJs pathname="/account/public-tags-request"> <PageNextJs pathname="/public-tags/submit">
<PublicTags/> <PublicTagsSubmit/>
</PageNextJs> </PageNextJs>
); );
}; };
export default Page; export default Page;
export { account as getServerSideProps } from 'nextjs/getServerSideProps'; export { publicTagsSubmit as getServerSideProps } from 'nextjs/getServerSideProps';
...@@ -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,13 @@ export const ENVS_MAP: Record<string, Array<[string, string]>> = { ...@@ -63,4 +63,13 @@ export const ENVS_MAP: Record<string, Array<[string, string]>> = {
noNftMarketplaces: [ noNftMarketplaces: [
[ 'NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES', '' ], [ 'NEXT_PUBLIC_VIEWS_NFT_MARKETPLACES', '' ],
], ],
navigationHighlightedRoutes: [
[ 'NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES', '["/blocks", "/apps"]' ],
],
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;
...@@ -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"
...@@ -71,6 +72,7 @@ ...@@ -71,6 +72,7 @@
| "integration/full" | "integration/full"
| "integration/partial" | "integration/partial"
| "key" | "key"
| "lightning_navbar"
| "lightning" | "lightning"
| "link" | "link"
| "lock" | "lock"
...@@ -147,7 +149,6 @@ ...@@ -147,7 +149,6 @@
| "user_op_slim" | "user_op_slim"
| "user_op" | "user_op"
| "validator" | "validator"
| "verified_token"
| "verified" | "verified"
| "verify-contract" | "verify-contract"
| "wallet" | "wallet"
......
import type { PublicTag, AddressTag, TransactionTag, ApiKey, CustomAbi, VerifiedAddress, TokenInfoApplication, WatchlistAddress } from 'types/api/account'; import type { AddressTag, TransactionTag, ApiKey, CustomAbi, VerifiedAddress, TokenInfoApplication, WatchlistAddress } from 'types/api/account';
import { ADDRESS_PARAMS, ADDRESS_HASH } from './addressParams'; import { ADDRESS_PARAMS, ADDRESS_HASH } from './addressParams';
import { TX_HASH } from './tx'; import { TX_HASH } from './tx';
...@@ -16,19 +16,6 @@ export const PRIVATE_TAG_TX: TransactionTag = { ...@@ -16,19 +16,6 @@ export const PRIVATE_TAG_TX: TransactionTag = {
transaction_hash: TX_HASH, transaction_hash: TX_HASH,
}; };
export const PUBLIC_TAG: PublicTag = {
additional_comment: 'my comment',
addresses: [ ADDRESS_HASH ],
addresses_with_info: [ ADDRESS_PARAMS ],
company: 'Blockscout',
email: 'john.doe@example.com',
full_name: 'name',
id: 1,
is_owner: true,
tags: 'placeholder',
website: 'example.com',
};
export const WATCH_LIST_ITEM_WITH_TOKEN_INFO: WatchlistAddress = { export const WATCH_LIST_ITEM_WITH_TOKEN_INFO: WatchlistAddress = {
address: ADDRESS_PARAMS, address: ADDRESS_PARAMS,
address_balance: '7072643779453701031672', address_balance: '7072643779453701031672',
......
...@@ -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();
......
...@@ -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);
......
...@@ -21,6 +21,12 @@ const typography = { ...@@ -21,6 +21,12 @@ const typography = {
lineHeight: '32px', lineHeight: '32px',
fontFamily: 'heading', fontFamily: 'heading',
}, },
h4: {
fontSize: 'md',
fontWeight: '500',
lineHeight: '24px',
fontFamily: 'heading',
},
}, },
}; };
......
...@@ -103,23 +103,6 @@ export type WatchlistResponse = { ...@@ -103,23 +103,6 @@ export type WatchlistResponse = {
} | null; } | null;
} }
export interface PublicTag {
website: string;
tags: string; // tag_1;tag_2;tag_3 etc.
is_owner: boolean;
id: number;
full_name: string;
email: string;
company: string;
addresses: Array<string>;
addresses_with_info: Array<AddressParam>;
additional_comment: string;
}
export type PublicTagNew = Omit<PublicTag, 'id' | 'addresses_with_info'>
export type PublicTags = Array<PublicTag>;
export type CustomAbis = Array<CustomAbi> export type CustomAbis = Array<CustomAbi>
export interface CustomAbi { export interface CustomAbi {
...@@ -175,14 +158,6 @@ export type TransactionTagErrors = { ...@@ -175,14 +158,6 @@ export type TransactionTagErrors = {
identity_id?: Array<string>; identity_id?: Array<string>;
} }
export type PublicTagErrors = {
additional_comment: Array<string>;
addresses: Array<string>;
email: Array<string>;
full_name: Array<string>;
tags: Array<string>;
}
export interface VerifiedAddress { export interface VerifiedAddress {
userId: string; userId: string;
chainId: string; chainId: string;
......
...@@ -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,5 +31,18 @@ export interface AddressMetadataTagApi extends Omit<AddressMetadataTag, 'meta'> ...@@ -30,5 +31,18 @@ export interface AddressMetadataTagApi extends Omit<AddressMetadataTag, 'meta'>
appMarketplaceURL?: string; appMarketplaceURL?: string;
appLogoURL?: string; appLogoURL?: string;
appActionButtonText?: string; appActionButtonText?: string;
warpcastHandle?: string;
} | null; } | null;
} }
// TAG SUBMISSION
export interface PublicTagType {
id: string;
type: AddressMetadataTagType;
description: string;
}
export interface PublicTagTypesResponse {
tagTypes: Array<PublicTagType>;
}
...@@ -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;
};
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,
}); });
}); });
This diff is collapsed.
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();
......
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();
......
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.
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