Merge branch 'main' into defi-dropdown

This commit is contained in:
Max Alekseenko
2024-05-28 14:17:43 +02:00
457 changed files with 6578 additions and 5963 deletions
+1
View File
@@ -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: '@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: 'playwright/TestApp', message: 'Please use render() fixture from test() function of playwright/lib module' },
],
patterns: [
'icons/*',
+2
View File
@@ -16,7 +16,9 @@ export { default as growthBook } from './growthBook';
export { default as marketplace } from './marketplace';
export { default as metasuites } from './metasuites';
export { default as mixpanel } from './mixpanel';
export { default as multichainButton } from './multichainButton';
export { default as nameService } from './nameService';
export { default as publicTagsSubmission } from './publicTagsSubmission';
export { default as restApiDocs } from './restApiDocs';
export { default as rollup } from './rollup';
export { default as safe } from './safe';
+29
View File
@@ -0,0 +1,29 @@
import type { Feature } from './types';
import type { MultichainProviderConfig } from 'types/client/multichainProviderConfig';
import { getEnvValue, parseEnvJson } from '../utils';
import marketplace from './marketplace';
const value = parseEnvJson<MultichainProviderConfig>(getEnvValue('NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG'));
const title = 'Multichain balance';
const config: Feature<{name: string; logoUrl?: string; urlTemplate: string; dappId?: string }> = (() => {
if (value) {
return Object.freeze({
title,
isEnabled: true,
name: value.name,
logoUrl: value.logo,
urlTemplate: value.url_template,
dappId: marketplace.isEnabled ? value.dapp_id : undefined,
});
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
@@ -0,0 +1,29 @@
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;
+6
View File
@@ -24,6 +24,11 @@ const hiddenLinks = (() => {
return result;
})();
const highlightedRoutes = (() => {
const parsedValue = parseEnvJson<Array<NavigationLinkId>>(getEnvValue('NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES'));
return Array.isArray(parsedValue) ? parsedValue : [];
})();
const defaultColorTheme = (() => {
const envValue = getEnvValue('NEXT_PUBLIC_COLOR_THEME_DEFAULT') as ColorThemeId | undefined;
return COLOR_THEMES.find((theme) => theme.id === envValue);
@@ -43,6 +48,7 @@ const UI = Object.freeze({
dark: getExternalAssetFilePath('NEXT_PUBLIC_NETWORK_ICON_DARK'),
},
hiddenLinks,
highlightedRoutes,
otherLinks: parseEnvJson<Array<NavItemExternal>>(getEnvValue('NEXT_PUBLIC_OTHER_LINKS')) || [],
featuredNetworks: getExternalAssetFilePath('NEXT_PUBLIC_FEATURED_NETWORKS'),
},
+3 -1
View File
@@ -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_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
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_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL
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_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
NEXT_PUBLIC_OG_IMAGE_URL=https://github.com/blockscout/frontend-configs/blob/main/configs/og-images/eth.jpg?raw=true
+1 -1
View File
@@ -56,4 +56,4 @@ NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.blocks
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
#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
+2 -1
View File
@@ -18,7 +18,7 @@ NEXT_PUBLIC_NETWORK_RPC_URL=https://eth-sepolia.public.blastapi.io
NEXT_PUBLIC_IS_TESTNET=true
# 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=/
# ui config
@@ -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_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_NAVIGATION_HIGHLIGHTED_ROUTES=['/blocks','/apps']
## footer
NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/sepolia.json
##views
+1
View File
@@ -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_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_NAVIGATION_HIGHLIGHTED_ROUTES=['/accounts','/apps']
## footer
##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'}]
+21 -17
View File
@@ -16,6 +16,7 @@ import type { DeFiDropdownItem } from '../../../types/client/deFiDropdown';
import { GAS_UNITS } from '../../../types/client/gasTracker';
import type { GasUnit } from '../../../types/client/gasTracker';
import type { MarketplaceAppOverview, MarketplaceAppSecurityReportRaw, MarketplaceAppSecurityReport } from '../../../types/client/marketplace';
import type { MultichainProviderConfig } from '../../../types/client/multichainProviderConfig';
import { NAVIGATION_LINK_IDS } from '../../../types/client/navigation-items';
import type { NavItemExternal, NavigationLinkId } from '../../../types/client/navigation-items';
import { ROLLUP_TYPES } from '../../../types/client/rollup';
@@ -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
.object()
.shape({
@@ -541,6 +527,11 @@ const schema = yup
.transform(replaceQuotes)
.json()
.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_DARK: yup.string().test(urlTest),
NEXT_PUBLIC_NETWORK_ICON: yup.string().test(urlTest),
@@ -607,6 +598,7 @@ const schema = yup
NEXT_PUBLIC_CONTRACT_INFO_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_ADMIN_SERVICE_API_HOST: yup.string().test(urlTest),
NEXT_PUBLIC_GRAPHIQL_TRANSACTION: yup.string().matches(regexp.HEX_REGEXP),
NEXT_PUBLIC_WEB3_WALLETS: yup
.mixed()
@@ -632,6 +624,19 @@ const schema = yup
NEXT_PUBLIC_IS_SUAVE_CHAIN: yup.boolean(),
NEXT_PUBLIC_HAS_USER_OPS: 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_GAS_TRACKER_ENABLED: yup.boolean(),
NEXT_PUBLIC_GAS_TRACKER_UNITS: yup.array().transform(replaceQuotes).json().of(yup.string<GasUnit>().oneOf(GAS_UNITS)),
@@ -658,7 +663,6 @@ const schema = yup
.concat(rollupSchema)
.concat(beaconChainSchema)
.concat(bridgedTokensSchema)
.concat(sentrySchema)
.concat(adminServiceSchema);
.concat(sentrySchema);
export default schema;
@@ -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_DATA_AVAILABILITY_ENABLED=true
NEXT_PUBLIC_FEATURED_NETWORKS=https://example.com
NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/accounts','/apps']
NEXT_PUBLIC_FOOTER_LINKS=https://example.com
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d
NEXT_PUBLIC_HIDE_INDEXING_ALERT_BLOCKS=false
@@ -74,3 +75,4 @@ NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET=false
NEXT_PUBLIC_WEB3_WALLETS=['coinbase','metamask','token_pocket']
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_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'}
+34
View File
@@ -57,6 +57,7 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will
- [Sentry error monitoring](ENVS.md#sentry-error-monitoring)
- [OpenTelemetry](ENVS.md#opentelemetry)
- [Swap button](ENVS.md#swap-button)
- [Multichain balance button](ENVS.md#multichain-button)
- [3rd party services configuration](ENVS.md#external-services-configuration)
&nbsp;
@@ -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_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_HIGHLIGHTED_ROUTES | `Array<string>` | List of menu item routes that should have a lightning label | - | - | `['/accounts']` |
#### Featured network configuration properties
@@ -567,6 +569,17 @@ This feature allows name tags and other public tags for addresses.
&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
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
&nbsp;
### Multichain balance button
If the feature is enabled, a Multichain balance button will be displayed on the address page, which will take you to the portfolio application in the marketplace or to an external site.
| Variable | Type| Description | Compulsoriness | Default value | Example value |
| --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG | `{ name: string; url_template: string; dapp_id?: string; logo?: string }` | Multichain portfolio application config See [below](#multichain-button-configuration-properties) | - | - | `{ name: 'zerion', url_template: 'https://app.zerion.io/{address}/overview', logo: 'https://example.com/icon.svg'` |
&nbsp;
#### Multichain button configuration properties
| Variable | Type| Description | Compulsoriness | Default value | Example value |
| --- | --- | --- | --- | --- | --- |
| name | `string` | Multichain portfolio application name | Required | - | `zerion` |
| url_template | `string` | Url template to the portfolio. Should be a template with `{address}` variable | Required | - | `https://app.zerion.io/{address}/overview` |
| dapp_id | `string` | Set for open a Blockscout dapp page with the portfolio instead of opening external app page | - | - | `zerion` |
| logo | `string` | Multichain portfolio application logo (.svg) url | - | - | `https://example.com/icon.svg` |
&nbsp;
## External services configuration
### Google ReCaptcha

Before

Width:  |  Height:  |  Size: 752 B

After

Width:  |  Height:  |  Size: 752 B

+4
View File
@@ -0,0 +1,4 @@
<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>

After

Width:  |  Height:  |  Size: 282 B

+2
View File
@@ -16,6 +16,7 @@ export default function parseMetaPayload(meta: AddressMetadataTag['meta']): Addr
const stringFields: Array<keyof MetaParsed> = [
'textColor',
'bgColor',
'tagIcon',
'tagUrl',
'tooltipIcon',
'tooltipTitle',
@@ -25,6 +26,7 @@ export default function parseMetaPayload(meta: AddressMetadataTag['meta']): Addr
'appMarketplaceURL',
'appLogoURL',
'appActionButtonText',
'warpcastHandle',
];
for (const stringField of stringFields) {
+20 -32
View File
@@ -1,47 +1,35 @@
import { useQuery } from '@tanstack/react-query';
import type { AddressMetadataInfo } from 'types/api/addressMetadata';
import type { AddressMetadataInfoFormatted, AddressMetadataTagFormatted } from 'types/client/addressMetadata';
import config from 'configs/app';
import useApiFetch from 'lib/api/useApiFetch';
import { getResourceKey } from 'lib/api/useApiQuery';
import useApiQuery from 'lib/api/useApiQuery';
import parseMetaPayload from './parseMetaPayload';
export default function useAddressMetadataInfoQuery(addresses: Array<string>) {
const apiFetch = useApiFetch();
const queryParams = {
addresses,
chainId: config.chain.id,
tagsLimit: '20',
};
const resource = 'address_metadata_info';
// TODO @tom2drum: Improve the typing here
// since we are formatting the API data in the select function here
// we cannot use the useApiQuery hook because of its current typing
// enhance useApiQuery so it can accept an API data and the formatted data types
return useQuery<AddressMetadataInfo, unknown, AddressMetadataInfoFormatted>({
queryKey: getResourceKey(resource, { queryParams }),
queryFn: async() => {
return apiFetch(resource, { queryParams }) as Promise<AddressMetadataInfo>;
return useApiQuery<typeof resource, unknown, AddressMetadataInfoFormatted>(resource, {
queryParams: {
addresses,
chainId: config.chain.id,
tagsLimit: '20',
},
enabled: addresses.length > 0 && config.features.addressMetadata.isEnabled,
select: (data) => {
const addresses = Object.entries(data.addresses)
.map(([ address, { tags, reputation } ]) => {
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];
return result;
}, {} as AddressMetadataInfoFormatted['addresses']);
queryOptions: {
enabled: addresses.length > 0 && config.features.addressMetadata.isEnabled,
select: (data) => {
const addresses = Object.entries(data.addresses)
.map(([ address, { tags, reputation } ]) => {
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];
return result;
}, {} as AddressMetadataInfoFormatted['addresses']);
return { addresses };
return { addresses };
},
},
});
}
+14 -8
View File
@@ -2,7 +2,6 @@ import { getFeaturePayload } from 'configs/app/features/types';
import type {
UserInfo,
CustomAbis,
PublicTags,
ApiKeys,
VerifiedAddressResponse,
TokenInfoApplicationConfig,
@@ -32,7 +31,7 @@ import type {
AddressCoinBalanceHistoryChartOld,
} from 'types/api/address';
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 { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters, BlockWithdrawalsResponse } from 'types/api/block';
import type { ChartMarketResponse, ChartSecondaryCoinPriceResponse, ChartTransactionResponse } from 'types/api/charts';
@@ -147,10 +146,6 @@ export const RESOURCES = {
pathParams: [ 'id' as const ],
filterFields: [ ],
},
public_tags: {
path: '/api/account/v2/user/public_tags/:id?',
pathParams: [ 'id' as const ],
},
private_tags_address: {
path: '/api/account/v2/user/tags/address/:id?',
pathParams: [ 'id' as const ],
@@ -245,7 +240,7 @@ export const RESOURCES = {
filterFields: [ 'name' as const, 'only_active' as const ],
},
// METADATA SERVICE
// METADATA SERVICE & PUBLIC TAGS
address_metadata_info: {
path: '/api/v1/metadata',
endpoint: getFeaturePayload(config.features.addressMetadata)?.api.endpoint,
@@ -256,6 +251,17 @@ export const RESOURCES = {
endpoint: getFeaturePayload(config.features.addressMetadata)?.api.endpoint,
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
visualize_sol2uml: {
@@ -863,7 +869,6 @@ export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>
export type ResourcePayloadA<Q extends ResourceName> =
Q extends 'user_info' ? UserInfo :
Q extends 'custom_abi' ? CustomAbis :
Q extends 'public_tags' ? PublicTags :
Q extends 'private_tags_address' ? AddressTagsResponse :
Q extends 'private_tags_tx' ? TransactionTagsResponse :
Q extends 'api_keys' ? ApiKeys :
@@ -956,6 +961,7 @@ Q extends 'optimistic_l2_deposits_count' ? number :
Q extends 'optimistic_l2_txn_batches_count' ? number :
Q extends 'config_backend_version' ? BackendVersionConfig :
Q extends 'address_metadata_info' ? AddressMetadataInfo :
Q extends 'address_metadata_tag_types' ? PublicTagTypesResponse :
never;
// !!! IMPORTANT !!!
// See comment above
+5 -5
View File
@@ -5,8 +5,8 @@ import type { ResourceError, ResourceName, ResourcePayload } from './resources';
import type { Params as ApiFetchParams } from './useApiFetch';
import useApiFetch from './useApiFetch';
export interface Params<R extends ResourceName, E = unknown> extends ApiFetchParams<R> {
queryOptions?: Omit<UseQueryOptions<ResourcePayload<R>, ResourceError<E>, ResourcePayload<R>>, 'queryKey' | 'queryFn'>;
export interface Params<R extends ResourceName, E = unknown, D = ResourcePayload<R>> extends ApiFetchParams<R> {
queryOptions?: Omit<UseQueryOptions<ResourcePayload<R>, ResourceError<E>, D>, 'queryKey' | 'queryFn'>;
}
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
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,
{ queryOptions, pathParams, queryParams, fetchParams }: Params<R, E> = {},
{ queryOptions, pathParams, queryParams, fetchParams }: Params<R, E, D> = {},
) {
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
queryKey: getResourceKey(resource, { pathParams, queryParams }),
queryFn: async() => {
+5 -6
View File
@@ -237,6 +237,11 @@ export default function useNavItems(): ReturnType {
nextRoute: { pathname: '/gas-tracker' as const },
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,
].filter(Boolean),
},
@@ -255,12 +260,6 @@ export default function useNavItems(): ReturnType {
icon: 'privattags',
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',
nextRoute: { pathname: '/account/api-key' as const },
+1 -1
View File
@@ -27,9 +27,9 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
'/account/watchlist': 'Regular page',
'/account/api-key': 'Regular page',
'/account/custom-abi': 'Regular page',
'/account/public-tags-request': 'Regular page',
'/account/tag-address': 'Regular page',
'/account/verified-addresses': 'Root page',
'/public-tags/submit': 'Regular page',
'/withdrawals': 'Root page',
'/visualize/sol2uml': 'Regular page',
'/csv-export': 'Regular page',
+2 -1
View File
@@ -1,3 +1,4 @@
/* eslint-disable max-len */
import type { Route } from 'nextjs-routes';
// equal og:description
@@ -30,9 +31,9 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/account/watchlist': DEFAULT_TEMPLATE,
'/account/api-key': DEFAULT_TEMPLATE,
'/account/custom-abi': DEFAULT_TEMPLATE,
'/account/public-tags-request': DEFAULT_TEMPLATE,
'/account/tag-address': 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,
'/visualize/sol2uml': DEFAULT_TEMPLATE,
'/csv-export': DEFAULT_TEMPLATE,
+1 -1
View File
@@ -25,9 +25,9 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/account/watchlist': '- watchlist',
'/account/api-key': '- API keys',
'/account/custom-abi': '- custom ABI',
'/account/public-tags-request': '- public tag requests',
'/account/tag-address': '- private tags',
'/account/verified-addresses': '- my verified addresses',
'/public-tags/submit': 'submit public tag',
'/withdrawals': 'withdrawals',
'/visualize/sol2uml': 'Solidity UML diagram',
'/csv-export': 'export data to CSV',
+1 -1
View File
@@ -25,9 +25,9 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
'/account/watchlist': 'Watchlist',
'/account/api-key': 'API keys',
'/account/custom-abi': 'Custom ABI',
'/account/public-tags-request': 'Public tags',
'/account/tag-address': 'Private tags',
'/account/verified-addresses': 'Verified addresses',
'/public-tags/submit': 'Submit public tag',
'/withdrawals': 'Withdrawals',
'/visualize/sol2uml': 'Solidity UML diagram',
'/csv-export': 'Export data to CSV file',
+17
View File
@@ -0,0 +1,17 @@
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;
};
+1 -1
View File
@@ -75,7 +75,7 @@ export const token: Address = {
coin_balance: '1',
creation_tx_hash: '0xc38cf7377bf72d6436f63c37b01b24d032101f20ec1849286dc703c712f10c98',
creator_address_hash: '0x34A9c688512ebdB575e82C50c9803F6ba2916E72',
exchange_rate: null,
exchange_rate: '0.04311',
implementation_address: null,
has_decompiled_code: false,
has_logs: false,
+9 -5
View File
@@ -1,4 +1,4 @@
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 tokenInstance from 'mocks/tokens/tokenInstance';
@@ -119,35 +119,39 @@ export const erc404b: AddressTokenBalance = {
token_id: null,
};
export const erc20List = {
export const erc20List: AddressTokensResponse = {
items: [
erc20a,
erc20b,
erc20c,
],
next_page_params: null,
};
export const erc721List = {
export const erc721List: AddressTokensResponse = {
items: [
erc721a,
erc721b,
erc721c,
],
next_page_params: null,
};
export const erc1155List = {
export const erc1155List: AddressTokensResponse = {
items: [
erc1155withoutName,
erc1155a,
erc1155b,
],
next_page_params: null,
};
export const erc404List = {
export const erc404List: AddressTokensResponse = {
items: [
erc404a,
erc404b,
],
next_page_params: null,
};
export const nfts: AddressNFTsResponse = {
+5
View File
@@ -51,6 +51,11 @@ export const verified: SmartContract = {
minimal_proxy_address_hash: null,
};
export const certified: SmartContract = {
...verified,
certified: true,
};
export const withMultiplePaths: SmartContract = {
...verified,
file_path: './simple_storage.sol',
+8 -3
View File
@@ -1,5 +1,8 @@
export const solidityscanReportAverage = {
import type { SolidityscanReport } from 'types/api/contract';
export const solidityscanReportAverage: SolidityscanReport = {
scan_report: {
contractname: 'foo',
scan_status: 'scan_done',
scan_summary: {
issue_severity_distribution: {
@@ -20,8 +23,9 @@ export const solidityscanReportAverage = {
},
};
export const solidityscanReportGreat = {
export const solidityscanReportGreat: SolidityscanReport = {
scan_report: {
contractname: 'foo',
scan_status: 'scan_done',
scan_summary: {
issue_severity_distribution: {
@@ -42,8 +46,9 @@ export const solidityscanReportGreat = {
},
};
export const solidityscanReportLow = {
export const solidityscanReportLow: SolidityscanReport = {
scan_report: {
contractname: 'foo',
scan_status: 'scan_done',
scan_summary: {
issue_severity_distribution: {
+1
View File
@@ -35,6 +35,7 @@ export const contract2: VerifiedContract = {
watchlist_names: [],
ens_domain_name: null,
},
certified: true,
coin_balance: '9078234570352343999',
compiler_version: 'v0.3.1+commit.0463ea4c',
has_constructor_args: true,
File diff suppressed because one or more lines are too long
+34
View File
@@ -0,0 +1,34 @@
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',
},
],
};
+9
View File
@@ -96,6 +96,15 @@ export const contract1: SearchResultAddressOrContract = {
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 = {
address: '0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a',
name: 'utko',
+17 -7
View File
@@ -1,9 +1,12 @@
import _mapValues from 'lodash/mapValues';
export const base = {
import type { HomeStats } from 'types/api/stats';
export const base: HomeStats = {
average_block_time: 6212.0,
coin_price: '0.00199678',
coin_price_change_percentage: -7.42,
coin_image: 'http://localhost:3100/utia.jpg',
gas_prices: {
average: {
fiat_price: '1.39',
@@ -41,35 +44,42 @@ export const base = {
tvl: '1767425.102766552',
};
export const withBtcLocked = {
export const withBtcLocked: HomeStats = {
...base,
rootstock_locked_btc: '3337493406696977561374',
};
export const withoutFiatPrices = {
export const withoutFiatPrices: HomeStats = {
...base,
gas_prices: _mapValues(base.gas_prices, (price) => price ? ({ ...price, fiat_price: null }) : null),
};
export const withoutGweiPrices = {
export const withoutGweiPrices: HomeStats = {
...base,
gas_prices: _mapValues(base.gas_prices, (price) => price ? ({ ...price, price: null }) : null),
};
export const withoutBothPrices = {
export const withoutBothPrices: HomeStats = {
...base,
gas_prices: _mapValues(base.gas_prices, (price) => price ? ({ ...price, price: null, fiat_price: null }) : null),
};
export const withSecondaryCoin = {
export const withSecondaryCoin: HomeStats = {
...base,
secondary_coin_price: '3.398',
};
export const noChartData = {
export const noChartData: HomeStats = {
...base,
transactions_today: null,
coin_price: null,
market_cap: null,
tvl: null,
};
export const indexingStatus = {
finished_indexing_blocks: false,
indexed_blocks_ratio: '0.1',
finished_indexing: true,
indexed_internal_transactions_ratio: '1',
};
+14 -14
View File
@@ -8,7 +8,7 @@ export const tokenInfo: TokenInfo = {
holders: '46554',
name: 'ARIANEE',
symbol: 'ARIA',
type: 'ERC-20',
type: 'ERC-20' as const,
total_supply: '1235',
icon_url: 'http://localhost:3000/token-icon.png',
};
@@ -27,7 +27,7 @@ export const tokenInfoERC20a: TokenInfo<'ERC-20'> = {
name: 'hyfi.token',
symbol: 'HyFi',
total_supply: '369000000000000000000000000',
type: 'ERC-20',
type: 'ERC-20' as const,
icon_url: 'http://localhost:3000/token-icon.png',
};
@@ -40,7 +40,7 @@ export const tokenInfoERC20b: TokenInfo<'ERC-20'> = {
name: 'USD Coin',
symbol: 'USDC',
total_supply: '900000000000000000000000000',
type: 'ERC-20',
type: 'ERC-20' as const,
icon_url: null,
};
@@ -53,7 +53,7 @@ export const tokenInfoERC20c: TokenInfo<'ERC-20'> = {
name: 'Ethereum',
symbol: 'ETH',
total_supply: '1000000000000000000000000',
type: 'ERC-20',
type: 'ERC-20' as const,
icon_url: null,
};
@@ -66,7 +66,7 @@ export const tokenInfoERC20d: TokenInfo<'ERC-20'> = {
name: 'Zeta',
symbol: 'ZETA',
total_supply: '2100000000000000000000000000',
type: 'ERC-20',
type: 'ERC-20' as const,
icon_url: null,
};
@@ -79,7 +79,7 @@ export const tokenInfoERC20LongSymbol: TokenInfo<'ERC-20'> = {
name: 'Zeta',
symbol: 'ipfs://QmUpFUfVKDCWeZQk5pvDFUxnpQP9N6eLSHhNUy49T1JVtY',
total_supply: '2100000000000000000000000000',
type: 'ERC-20',
type: 'ERC-20' as const,
icon_url: null,
};
@@ -92,7 +92,7 @@ export const tokenInfoERC721a: TokenInfo<'ERC-721'> = {
name: 'HyFi Athena',
symbol: 'HYFI_ATHENA',
total_supply: '105',
type: 'ERC-721',
type: 'ERC-721' as const,
icon_url: null,
};
@@ -105,7 +105,7 @@ export const tokenInfoERC721b: TokenInfo<'ERC-721'> = {
name: 'World Of Women Galaxy',
symbol: 'WOWG',
total_supply: null,
type: 'ERC-721',
type: 'ERC-721' as const,
icon_url: null,
};
@@ -118,7 +118,7 @@ export const tokenInfoERC721c: TokenInfo<'ERC-721'> = {
name: 'Puma',
symbol: 'PUMA',
total_supply: null,
type: 'ERC-721',
type: 'ERC-721' as const,
icon_url: null,
};
@@ -131,7 +131,7 @@ export const tokenInfoERC721LongSymbol: TokenInfo<'ERC-721'> = {
name: 'Puma',
symbol: 'ipfs://QmUpFUfVKDCWeZQk5pvDFUxnpQP9N6eLSHhNUy49T1JVtY',
total_supply: null,
type: 'ERC-721',
type: 'ERC-721' as const,
icon_url: null,
};
@@ -144,7 +144,7 @@ export const tokenInfoERC1155a: TokenInfo<'ERC-1155'> = {
name: 'HyFi Membership',
symbol: 'HYFI_MEMBERSHIP',
total_supply: '482',
type: 'ERC-1155',
type: 'ERC-1155' as const,
icon_url: null,
};
@@ -157,7 +157,7 @@ export const tokenInfoERC1155b: TokenInfo<'ERC-1155'> = {
name: 'WinkyVerse Collections',
symbol: 'WVC',
total_supply: '4943',
type: 'ERC-1155',
type: 'ERC-1155' as const,
icon_url: null,
};
@@ -170,7 +170,7 @@ export const tokenInfoERC1155WithoutName: TokenInfo<'ERC-1155'> = {
name: null,
symbol: null,
total_supply: '482',
type: 'ERC-1155',
type: 'ERC-1155' as const,
icon_url: null,
};
@@ -184,7 +184,7 @@ export const tokenInfoERC404: TokenInfo<'ERC-404'> = {
name: 'OMNI404',
symbol: 'O404',
total_supply: '6482275000000000000',
type: 'ERC-404',
type: 'ERC-404' as const,
};
export const bridgedTokenA: TokenInfo<'ERC-20'> = {
+10 -9
View File
@@ -1,4 +1,4 @@
import type { TxStateChange } from 'types/api/txStateChanges';
import type { TxStateChange, TxStateChanges } from 'types/api/txStateChanges';
export const mintToken: TxStateChange = {
address: {
@@ -35,7 +35,7 @@ export const mintToken: TxStateChange = {
type: 'ERC-721',
icon_url: null,
},
type: 'token',
type: 'token' as const,
};
export const receiveMintedToken: TxStateChange = {
@@ -73,7 +73,7 @@ export const receiveMintedToken: TxStateChange = {
type: 'ERC-721',
icon_url: null,
},
type: 'token',
type: 'token' as const,
};
export const transfer1155Token: TxStateChange = {
@@ -105,7 +105,7 @@ export const transfer1155Token: TxStateChange = {
type: 'ERC-1155',
},
token_id: '1',
type: 'token',
type: 'token' as const,
};
export const receiveCoin: TxStateChange = {
@@ -125,7 +125,7 @@ export const receiveCoin: TxStateChange = {
change: '29726406604060',
is_miner: true,
token: null,
type: 'coin',
type: 'coin' as const,
};
export const sendCoin: TxStateChange = {
@@ -145,12 +145,13 @@ export const sendCoin: TxStateChange = {
change: '-3844844822720562',
is_miner: false,
token: null,
type: 'coin',
type: 'coin' as const,
};
export const sendERC20Token = {
address: {
hash: '0x7f6479df95Aa3036a3BE02DB6300ea201ABd9981',
ens_domain_name: null,
implementation_name: null,
is_contract: false,
is_verified: false,
@@ -173,13 +174,13 @@ export const sendERC20Token = {
name: 'Tether USD',
symbol: 'USDT',
total_supply: '39030615894320966',
type: 'ERC-20',
type: 'ERC-20' as const,
token_id: null,
},
type: 'token',
type: 'token' as const,
};
export const baseResponse = {
export const baseResponse: TxStateChanges = {
items: [
mintToken,
receiveMintedToken,
+28 -2
View File
@@ -1,3 +1,4 @@
/* eslint-disable max-len */
import type { UserOp } from 'types/api/userOps';
export const userOpData: UserOp = {
@@ -47,7 +48,6 @@ export const userOpData: UserOp = {
max_fee_per_gas: '1575000898',
max_priority_fee_per_gas: '1575000898',
nonce: '79',
// eslint-disable-next-line max-len
paymaster_and_data: '0x7cea357b5ac0639f89f9e378a1f03aa5005c0a250000000000000000000000000000000000000000000000000000000065b3a8800000000000000000000000000000000000000000000000000000000065aa6e0028fa4c57ac1141bc9ecd8c9243f618ade8ea1db10ab6c1d1798a222a824764ff2269a72ae7a3680fa8b03a80d8a00cdc710eaf37afdcc55f8c9c4defa3fdf2471b',
pre_verification_gas: '48396',
sender: '0xF0C14FF4404b188fAA39a3507B388998c10FE284',
@@ -64,8 +64,34 @@ export const userOpData: UserOp = {
is_verified: null,
name: null,
},
// eslint-disable-next-line max-len
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: {
ens_domain_name: null,
hash: '0x7ceA357B5AC0639F89F9e378a1f03Aa5005C0a25',
+11
View File
@@ -240,3 +240,14 @@ export const login: GetServerSideProps<Props> = async(context) => {
return base(context);
};
export const publicTagsSubmit: GetServerSideProps<Props> = async(context) => {
if (!config.features.publicTagsSubmission.isEnabled) {
return {
notFound: true,
};
}
return base(context);
};
+1 -1
View File
@@ -9,7 +9,6 @@ declare module "nextjs-routes" {
| StaticRoute<"/404">
| StaticRoute<"/account/api-key">
| StaticRoute<"/account/custom-abi">
| StaticRoute<"/account/public-tags-request">
| StaticRoute<"/account/tag-address">
| StaticRoute<"/account/verified-addresses">
| StaticRoute<"/account/watchlist">
@@ -45,6 +44,7 @@ declare module "nextjs-routes" {
| DynamicRoute<"/op/[hash]", { "hash": string }>
| StaticRoute<"/ops">
| StaticRoute<"/output-roots">
| StaticRoute<"/public-tags/submit">
| StaticRoute<"/search-results">
| StaticRoute<"/stats">
| DynamicRoute<"/token/[hash]", { "hash": string }>
+2 -10
View File
@@ -49,16 +49,8 @@ const oldUrls = [
destination: '/account/custom-abi',
},
{
source: '/account/public_tags_request',
destination: '/account/public-tags-request',
},
{
source: '/account/public_tags_request/:id/edit',
destination: '/account/public-tags-request',
},
{
source: '/account/public_tags_request/new',
destination: '/account/public-tags-request',
source: '/account/public-tags-request',
destination: '/public-tags/submit',
},
// TRANSACTIONS
-19
View File
@@ -1,19 +0,0 @@
import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import React from 'react';
import PageNextJs from 'nextjs/PageNextJs';
const PublicTags = dynamic(() => import('ui/pages/PublicTags'), { ssr: false });
const Page: NextPage = () => {
return (
<PageNextJs pathname="/account/public-tags-request">
<PublicTags/>
</PageNextJs>
);
};
export default Page;
export { account as getServerSideProps } from 'nextjs/getServerSideProps';
+18
View File
@@ -0,0 +1,18 @@
import type { NextPage } from 'next';
import React from 'react';
import PageNextJs from 'nextjs/PageNextJs';
import PublicTagsSubmit from 'ui/pages/PublicTagsSubmit';
const Page: NextPage = () => {
return (
<PageNextJs pathname="/public-tags/submit">
<PublicTagsSubmit/>
</PageNextJs>
);
};
export default Page;
export { publicTagsSubmit as getServerSideProps } from 'nextjs/getServerSideProps';
+3 -2
View File
@@ -12,9 +12,10 @@ import type { Props as PageProps } from 'nextjs/getServerSideProps';
import config from 'configs/app';
import { AppContextProvider } from 'lib/contexts/app';
import { SocketProvider } from 'lib/socket/context';
import * as app from 'playwright/utils/app';
import theme from 'theme';
import { port as socketPort } from './utils/socket';
export type Props = {
children: React.ReactNode;
withSocket?: boolean;
@@ -74,7 +75,7 @@ const TestApp = ({ children, withSocket, withWalletClient = true, appContext = d
return (
<ChakraProvider theme={ theme }>
<QueryClientProvider client={ queryClient }>
<SocketProvider url={ withSocket ? `ws://${ config.app.host }:${ app.socketPort }` : undefined }>
<SocketProvider url={ withSocket ? `ws://${ config.app.host }:${ socketPort }` : undefined }>
<AppContextProvider { ...appContext }>
<GrowthBookProvider>
<WalletClientProvider withWalletClient={ withWalletClient }>
-24
View File
@@ -1,24 +0,0 @@
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();
};
}
@@ -1,25 +0,0 @@
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();
};
}
@@ -1,22 +0,0 @@
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: [],
},
});
}
+2 -1
View File
@@ -6,6 +6,7 @@ import type { ResourceName, ResourcePayload } from 'lib/api/resources';
interface Options<R extends ResourceName> {
pathParams?: Parameters<typeof buildUrl<R>>[1];
queryParams?: Parameters<typeof buildUrl<R>>[2];
times?: number;
}
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
await page.route(apiUrl, (route) => route.fulfill({
status: 200,
body: JSON.stringify(responseMock),
}));
}), { times: options?.times });
return apiUrl;
});
+9
View File
@@ -63,4 +63,13 @@ export const ENVS_MAP: Record<string, Array<[string, string]>> = {
noNftMarketplaces: [
[ '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' ],
],
};
+1
View File
@@ -1,3 +1,4 @@
/* eslint-disable no-restricted-imports */
import type { MountOptions } from '@playwright/experimental-ct-react';
import type { Locator, TestFixture } from '@playwright/test';
import type router from 'next/router';
+2 -2
View File
@@ -8,7 +8,7 @@ import type { SmartContractVerificationResponse } from 'types/api/contract';
import type { TokenTransfer } from 'types/api/tokenTransfer';
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>;
@@ -20,7 +20,7 @@ export interface SocketServerFixture {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
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) => {
socketServer.on('connection', (socket: WebSocket) => {
-1
View File
@@ -1 +0,0 @@
export const socketPort = 3200;
-20
View File
@@ -1,20 +0,0 @@
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);
}
@@ -1,4 +1,3 @@
/* eslint-disable max-len */
import { devices } from '@playwright/test';
export const viewport = {
+1
View File
@@ -0,0 +1 @@
export const port = 3200;
+2 -1
View File
@@ -24,6 +24,7 @@
| "brands/safe"
| "brands/solidity_scan"
| "burger"
| "certified"
| "check"
| "clock-light"
| "clock"
@@ -71,6 +72,7 @@
| "integration/full"
| "integration/partial"
| "key"
| "lightning_navbar"
| "lightning"
| "link"
| "lock"
@@ -147,7 +149,6 @@
| "user_op_slim"
| "user_op"
| "validator"
| "verified_token"
| "verified"
| "verify-contract"
| "wallet"
+1 -14
View File
@@ -1,4 +1,4 @@
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 { TX_HASH } from './tx';
@@ -16,19 +16,6 @@ export const PRIVATE_TAG_TX: TransactionTag = {
transaction_hash: TX_HASH,
};
export const PUBLIC_TAG: PublicTag = {
additional_comment: 'my comment',
addresses: [ ADDRESS_HASH ],
addresses_with_info: [ ADDRESS_PARAMS ],
company: 'Blockscout',
email: '[email protected]',
full_name: 'name',
id: 1,
is_owner: true,
tags: 'placeholder',
website: 'example.com',
};
export const WATCH_LIST_ITEM_WITH_TOKEN_INFO: WatchlistAddress = {
address: ADDRESS_PARAMS,
address_balance: '7072643779453701031672',
+3
View File
@@ -21,6 +21,9 @@ export const USER_OP: UserOp = {
sender: ADDRESS_HASH,
nonce: '0x00b',
call_data: '0x123',
execute_call_data: null,
decoded_call_data: null,
decoded_execute_call_data: null,
call_gas_limit: '71316',
verification_gas_limit: '91551',
pre_verification_gas: '53627',
+8 -11
View File
@@ -1,9 +1,8 @@
import type { AlertProps } 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 TestApp from 'playwright/TestApp';
import { test, expect } from 'playwright/lib';
test.use({ viewport: { width: 400, height: 720 } });
@@ -29,16 +28,14 @@ const TEST_CASES: Array<AlertProps> = [
TEST_CASES.forEach((props) => {
const testName = Object.entries(props).map(([ key, value ]) => `${ key }=${ value }`).join(', ');
test(`${ testName } +@dark-mode`, async({ mount }) => {
const component = await mount(
<TestApp>
<Alert { ...props }>
<AlertIcon/>
<AlertTitle>
test(`${ testName } +@dark-mode`, async({ render }) => {
const component = await render(
<Alert { ...props }>
<AlertIcon/>
<AlertTitle>
This is alert text
</AlertTitle>
</Alert>
</TestApp>,
</AlertTitle>
</Alert>,
);
await expect(component).toHaveScreenshot();
+9 -26
View File
@@ -1,8 +1,7 @@
import { Button } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import TestApp from 'playwright/TestApp';
import { test, expect } from 'playwright/lib';
[
{ variant: 'solid' },
@@ -16,40 +15,24 @@ import TestApp from 'playwright/TestApp';
{ variant: 'subtle', colorScheme: 'gray', withDarkMode: true },
].forEach(({ variant, colorScheme, withDarkMode }) => {
test.describe(`variant ${ variant }${ colorScheme ? ` with ${ colorScheme } color scheme` : '' }${ withDarkMode ? ' +@dark-mode' : '' }`, () => {
test('base', async({ mount }) => {
const component = await mount(
<TestApp>
<Button variant={ variant } colorScheme={ colorScheme }>Click me</Button>
</TestApp>,
);
test('base', async({ render }) => {
const component = await render(<Button variant={ variant } colorScheme={ colorScheme }>Click me</Button>);
await expect(component.locator('button')).toHaveScreenshot();
});
test('disabled', async({ mount }) => {
const component = await mount(
<TestApp>
<Button variant={ variant } colorScheme={ colorScheme } isDisabled>Click me</Button>
</TestApp>,
);
test('disabled', async({ render }) => {
const component = await render(<Button variant={ variant } colorScheme={ colorScheme } isDisabled>Click me</Button>);
await expect(component.locator('button')).toHaveScreenshot();
});
test('hovered', async({ mount }) => {
const component = await mount(
<TestApp>
<Button variant={ variant } colorScheme={ colorScheme }>Click me</Button>
</TestApp>,
);
test('hovered', async({ render }) => {
const component = await render(<Button variant={ variant } colorScheme={ colorScheme }>Click me</Button>);
await component.getByText(/click/i).hover();
await expect(component.locator('button')).toHaveScreenshot();
});
test('active', async({ mount }) => {
const component = await mount(
<TestApp>
<Button variant={ variant } colorScheme={ colorScheme } isActive>Click me</Button>
</TestApp>,
);
test('active', async({ render }) => {
const component = await render(<Button variant={ variant } colorScheme={ colorScheme } isActive>Click me</Button>);
await expect(component.locator('button')).toHaveScreenshot();
});
});
+49 -66
View File
@@ -1,20 +1,17 @@
import { FormControl, Input, FormLabel } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import TestApp from 'playwright/TestApp';
import { test, expect } from 'playwright/lib';
test.use({ viewport: { width: 500, height: 300 } });
test.describe('floating label size md +@dark-mode', () => {
test('empty', async({ mount }) => {
const component = await mount(
<TestApp>
<FormControl variant="floating" id="name" isRequired size="md">
<Input required value=""/>
<FormLabel>Smart contract / Address (0x...)</FormLabel>
</FormControl>
</TestApp>,
test('empty', async({ render }) => {
const component = await render(
<FormControl variant="floating" id="name" isRequired size="md">
<Input required value=""/>
<FormLabel>Smart contract / Address (0x...)</FormLabel>
</FormControl>,
);
await expect(component).toHaveScreenshot();
@@ -23,14 +20,12 @@ test.describe('floating label size md +@dark-mode', () => {
await expect(component).toHaveScreenshot();
});
test('empty error', async({ mount }) => {
const component = await mount(
<TestApp>
<FormControl variant="floating" id="name" isRequired size="md">
<Input required value="" isInvalid/>
<FormLabel>Smart contract / Address (0x...)</FormLabel>
</FormControl>
</TestApp>,
test('empty error', async({ render }) => {
const component = await render(
<FormControl variant="floating" id="name" isRequired size="md">
<Input required value="" isInvalid/>
<FormLabel>Smart contract / Address (0x...)</FormLabel>
</FormControl>,
);
await expect(component).toHaveScreenshot();
@@ -39,53 +34,45 @@ test.describe('floating label size md +@dark-mode', () => {
await expect(component).toHaveScreenshot();
});
test('filled', async({ mount }) => {
const component = await mount(
<TestApp>
<FormControl variant="floating" id="name" isRequired size="md">
<Input required value="foo"/>
<FormLabel>Smart contract / Address (0x...)</FormLabel>
</FormControl>
</TestApp>,
test('filled', async({ render }) => {
const component = await render(
<FormControl variant="floating" id="name" isRequired size="md">
<Input required value="foo"/>
<FormLabel>Smart contract / Address (0x...)</FormLabel>
</FormControl>,
);
await expect(component).toHaveScreenshot();
});
test('filled disabled', async({ mount }) => {
const component = await mount(
<TestApp>
<FormControl variant="floating" id="name" isRequired size="md">
<Input required value="foo" isDisabled/>
<FormLabel>Smart contract / Address (0x...)</FormLabel>
</FormControl>
</TestApp>,
test('filled disabled', async({ render }) => {
const component = await render(
<FormControl variant="floating" id="name" isRequired size="md">
<Input required value="foo" isDisabled/>
<FormLabel>Smart contract / Address (0x...)</FormLabel>
</FormControl>,
);
await expect(component).toHaveScreenshot();
});
test('filled read-only', async({ mount }) => {
const component = await mount(
<TestApp>
<FormControl variant="floating" id="name" isRequired size="md">
<Input required value="foo" isReadOnly/>
<FormLabel>Smart contract / Address (0x...)</FormLabel>
</FormControl>
</TestApp>,
test('filled read-only', async({ render }) => {
const component = await render(
<FormControl variant="floating" id="name" isRequired size="md">
<Input required value="foo" isReadOnly/>
<FormLabel>Smart contract / Address (0x...)</FormLabel>
</FormControl>,
);
await expect(component).toHaveScreenshot();
});
test('filled error', async({ mount }) => {
const component = await mount(
<TestApp>
<FormControl variant="floating" id="name" isRequired size="md">
<Input required value="foo" isInvalid/>
<FormLabel>Smart contract / Address (0x...)</FormLabel>
</FormControl>
</TestApp>,
test('filled error', async({ render }) => {
const component = await render(
<FormControl variant="floating" id="name" isRequired size="md">
<Input required value="foo" isInvalid/>
<FormLabel>Smart contract / Address (0x...)</FormLabel>
</FormControl>,
);
await expect(component).toHaveScreenshot();
@@ -93,14 +80,12 @@ test.describe('floating label size md +@dark-mode', () => {
});
test.describe('floating label size lg +@dark-mode', () => {
test('empty', async({ mount }) => {
const component = await mount(
<TestApp>
<FormControl variant="floating" id="name" isRequired size="lg">
<Input required value=""/>
<FormLabel>Smart contract / Address (0x...)</FormLabel>
</FormControl>
</TestApp>,
test('empty', async({ render }) => {
const component = await render(
<FormControl variant="floating" id="name" isRequired size="lg">
<Input required value=""/>
<FormLabel>Smart contract / Address (0x...)</FormLabel>
</FormControl>,
);
await expect(component).toHaveScreenshot();
@@ -109,14 +94,12 @@ test.describe('floating label size lg +@dark-mode', () => {
await expect(component).toHaveScreenshot();
});
test('filled', async({ mount }) => {
const component = await mount(
<TestApp>
<FormControl variant="floating" id="name" isRequired size="lg">
<Input required value="foo"/>
<FormLabel>Smart contract / Address (0x...)</FormLabel>
</FormControl>
</TestApp>,
test('filled', async({ render }) => {
const component = await render(
<FormControl variant="floating" id="name" isRequired size="lg">
<Input required value="foo"/>
<FormLabel>Smart contract / Address (0x...)</FormLabel>
</FormControl>,
);
await expect(component).toHaveScreenshot();
+3
View File
@@ -15,6 +15,9 @@ const baseStyleTrack = defineStyle((props) => {
bg: mode(`${ c }.600`, `${ c }.400`)(props),
},
},
_focusVisible: {
boxShadow: 'none',
},
};
});
+8 -17
View File
@@ -1,29 +1,20 @@
import { Box, Tag } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-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) => {
test(`${ colorScheme } color scheme +@dark-mode`, async({ mount }) => {
const component = await mount(
<TestApp>
<Tag colorScheme={ colorScheme }>content</Tag>
</TestApp>,
);
test(`${ colorScheme } color scheme +@dark-mode`, async({ render }) => {
const component = await render(<Tag colorScheme={ colorScheme }>content</Tag>);
await expect(component.getByText(/content/i)).toHaveScreenshot();
});
});
test('with long text', async({ mount }) => {
const component = await mount(
<TestApp>
<Box w="100px">
<Tag>this is very looooooooooong text</Tag>
</Box>
</TestApp>,
test('with long text', async({ render }) => {
const component = await render(
<Box w="100px">
<Tag>this is very looooooooooong text</Tag>
</Box>,
);
await expect(component.getByText(/this/i)).toHaveScreenshot();
});
+16 -21
View File
@@ -1,18 +1,15 @@
import { Box, Tooltip, Icon } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import TestApp from 'playwright/TestApp';
import { test, expect } from 'playwright/lib';
test('base view +@dark-mode', async({ mount, page }) => {
const component = await mount(
<TestApp>
<Box m={ 10 }>
<Tooltip label="Tooltip content">
test('base view +@dark-mode', async({ render, page }) => {
const component = await render(
<Box m={ 10 }>
<Tooltip label="Tooltip content">
trigger
</Tooltip>
</Box>
</TestApp>,
</Tooltip>
</Box>,
);
await component.getByText(/trigger/i).hover();
@@ -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
// https://github.com/chakra-ui/chakra-ui/issues/7107
test.skip('with icon', async({ mount, page }) => {
const component = await mount(
<TestApp>
<Box m={ 10 }>
<Tooltip label="Tooltip content">
<Icon viewBox="0 0 20 20" boxSize={ 5 } aria-label="Trigger">
<circle cx="10" cy="10" r="10"/>
</Icon>
</Tooltip>
</Box>
</TestApp>,
test.fixme('with icon', async({ render, page }) => {
const component = await render(
<Box m={ 10 }>
<Tooltip label="Tooltip content">
<Icon viewBox="0 0 20 20" boxSize={ 5 } aria-label="Trigger">
<circle cx="10" cy="10" r="10"/>
</Icon>
</Tooltip>
</Box>,
);
const tooltip = page.getByText(/tooltip content/i);
+6
View File
@@ -21,6 +21,12 @@ const typography = {
lineHeight: '32px',
fontFamily: 'heading',
},
h4: {
fontSize: 'md',
fontWeight: '500',
lineHeight: '24px',
fontFamily: 'heading',
},
},
};
-25
View File
@@ -103,23 +103,6 @@ export type WatchlistResponse = {
} | 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 interface CustomAbi {
@@ -175,14 +158,6 @@ export type TransactionTagErrors = {
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 {
userId: string;
chainId: string;
+14
View File
@@ -21,6 +21,7 @@ export interface AddressMetadataTagApi extends Omit<AddressMetadataTag, 'meta'>
meta: {
textColor?: string;
bgColor?: string;
tagIcon?: string;
tagUrl?: string;
tooltipIcon?: string;
tooltipTitle?: string;
@@ -30,5 +31,18 @@ export interface AddressMetadataTagApi extends Omit<AddressMetadataTag, 'meta'>
appMarketplaceURL?: string;
appLogoURL?: string;
appActionButtonText?: string;
warpcastHandle?: string;
} | null;
}
// TAG SUBMISSION
export interface PublicTagType {
id: string;
type: AddressMetadataTagType;
description: string;
}
export interface PublicTagTypesResponse {
tagTypes: Array<PublicTagType>;
}
+1
View File
@@ -62,6 +62,7 @@ export interface SmartContract {
minimal_proxy_address_hash: string | null;
language: string | null;
license_type: SmartContractLicenseType | null;
certified?: boolean;
}
export type SmartContractDecodedConstructorArg = [
+1
View File
@@ -3,6 +3,7 @@ import type { SmartContractLicenseType } from './contract';
export interface VerifiedContract {
address: AddressParam;
certified?: boolean;
coin_balance: string;
compiler_version: string;
language: 'vyper' | 'yul' | 'solidity';
+1
View File
@@ -22,6 +22,7 @@ export interface SearchResultAddressOrContract {
name: string | null;
address: string;
is_smart_contract_verified: boolean;
certified?: true;
url?: string; // not used by the frontend, we build the url ourselves
ens_info?: {
address_hash: string;
+3 -2
View File
@@ -3,16 +3,17 @@ export type HomeStats = {
total_addresses: string;
total_transactions: string;
average_block_time: number;
coin_image?: string | null;
coin_price: string | null;
coin_price_change_percentage: number | null; // e.g -6.22
total_gas_used: string;
transactions_today: string;
transactions_today: string | null;
gas_used_today: string;
gas_prices: GasPrices | null;
gas_price_updated_at: string | null;
gas_prices_update_in: number;
static_gas_price: string | null;
market_cap: string;
market_cap: string | null;
network_utilization_percentage: number;
tvl: string | null;
rootstock_locked_btc?: string | null;
+4
View File
@@ -1,4 +1,5 @@
import type { AddressParamBasic } from './addressParams';
import type { DecodedInput } from './decodedInput';
export type UserOpsItem = {
hash: string;
@@ -46,6 +47,9 @@ export type UserOp = {
signature: string;
nonce: 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_count: number;
raw: {
+6
View File
@@ -0,0 +1,6 @@
export type MultichainProviderConfig = {
name: string;
dapp_id?: string;
url_template: string;
logo?: string;
};
+6 -23
View File
@@ -1,41 +1,24 @@
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as balanceHistoryMock from 'mocks/address/coinBalanceHistory';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import { test, expect } from 'playwright/lib';
import AddressCoinBalance from './AddressCoinBalance';
const addressHash = 'hash';
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 addressHash = '0x1234';
const hooksConfig = {
router: {
query: { hash: addressHash },
},
};
test('base view +@dark-mode +@mobile', async({ mount, page }) => {
await page.route(BALANCE_HISTORY_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(balanceHistoryMock.baseResponse),
}));
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 },
);
test('base view +@dark-mode +@mobile', async({ render, page, mockApiResponse }) => {
await mockApiResponse('address_coin_balance', balanceHistoryMock.baseResponse, { pathParams: { hash: addressHash } });
await mockApiResponse('address_coin_balance_chart', balanceHistoryMock.chartResponse, { pathParams: { hash: addressHash } });
const component = await render(<AddressCoinBalance/>, { hooksConfig });
await page.waitForFunction(() => {
return document.querySelector('path[data-name="chart-Balances-small"]')?.getAttribute('opacity') === '1';
});
await page.mouse.move(240, 100);
await expect(component).toHaveScreenshot();
});
+1 -1
View File
@@ -9,7 +9,7 @@ import config from 'configs/app';
import useIsInitialLoading from 'lib/hooks/useIsInitialLoading';
import useIsMobile from 'lib/hooks/useIsMobile';
import IconSvg from 'ui/shared/IconSvg';
import LinkInternal from 'ui/shared/LinkInternal';
import LinkInternal from 'ui/shared/links/LinkInternal';
interface Props {
address: string;
+64 -91
View File
@@ -1,121 +1,94 @@
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import type { WalletProvider } from 'types/web3';
import * as addressMock from 'mocks/address/address';
import * as countersMock from 'mocks/address/counters';
import * as tokensMock from 'mocks/address/tokens';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import * as configs from 'playwright/utils/configs';
import { test, expect, devices } from 'playwright/lib';
import * as pwConfig from 'playwright/utils/config';
import AddressDetails from './AddressDetails';
import MockAddressPage from './testUtils/MockAddressPage';
import type { AddressQuery } from './utils/useAddressQuery';
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 = {
router: {
query: { hash: ADDRESS_HASH },
},
};
test('contract +@mobile', async({ mount, page }) => {
await page.route(API_URL_ADDRESS, (route) => route.fulfill({
status: 200,
body: JSON.stringify(addressMock.contract),
}));
await page.route(API_URL_COUNTERS, (route) => route.fulfill({
status: 200,
body: JSON.stringify(countersMock.forContract),
}));
test.describe('mobile', () => {
test.use({ viewport: devices['iPhone 13 Pro'].viewport });
const component = await mount(
<TestApp>
<AddressDetails addressQuery={{ data: addressMock.contract } as AddressQuery}/>
</TestApp>,
test('contract', async({ render, mockApiResponse, page }) => {
await mockApiResponse('address', addressMock.contract, { pathParams: { hash: ADDRESS_HASH } });
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('contract', async({ render, page, mockApiResponse }) => {
await mockApiResponse('address', addressMock.contract, { pathParams: { hash: ADDRESS_HASH } });
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,
});
});
// there's an unexpected timeout occurred in this test
test.fixme('token', async({ render, mockApiResponse, injectMetaMaskProvider, page }) => {
await mockApiResponse('address', addressMock.token, { pathParams: { hash: ADDRESS_HASH } });
await mockApiResponse('address_counters', countersMock.forToken, { pathParams: { hash: ADDRESS_HASH } });
await mockApiResponse('address_tokens', tokensMock.erc20List, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-20' }, times: 1 });
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 },
);
await expect(component).toHaveScreenshot({
mask: [ page.locator(configs.adsBannerSelector) ],
maskColor: configs.maskColor,
mask: [ page.locator(pwConfig.adsBannerSelector) ],
maskColor: pwConfig.maskColor,
});
});
test('token', async({ mount, page }) => {
await page.route(API_URL_ADDRESS, (route) => route.fulfill({
status: 200,
body: JSON.stringify(addressMock.token),
}));
await page.route(API_URL_COUNTERS, (route) => route.fulfill({
status: 200,
body: JSON.stringify(countersMock.forToken),
}));
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 });
test('validator', async({ render, mockApiResponse, page }) => {
await mockApiResponse('address', addressMock.validator, { pathParams: { hash: ADDRESS_HASH } });
await mockApiResponse('address_counters', countersMock.forValidator, { pathParams: { hash: ADDRESS_HASH } });
await page.evaluate(() => {
window.ethereum = {
providers: [ { isMetaMask: true, _events: {} } ],
} as WalletProvider;
});
const component = await mount(
<TestApp>
<MockAddressPage>
<AddressDetails addressQuery={{ data: addressMock.token } as AddressQuery}/>
</MockAddressPage>
</TestApp>,
{ hooksConfig },
);
const component = await render(<AddressDetails addressQuery={{ data: addressMock.validator } as AddressQuery}/>, { hooksConfig });
await expect(component).toHaveScreenshot({
mask: [ page.locator(configs.adsBannerSelector) ],
maskColor: configs.maskColor,
});
});
test('validator +@mobile', async({ mount, page }) => {
await page.route(API_URL_ADDRESS, (route) => route.fulfill({
status: 200,
body: JSON.stringify(addressMock.validator),
}));
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({
mask: [ page.locator(configs.adsBannerSelector) ],
maskColor: configs.maskColor,
mask: [ page.locator(pwConfig.adsBannerSelector) ],
maskColor: pwConfig.maskColor,
});
});
+150 -99
View File
@@ -2,6 +2,7 @@ import { Box, Text, Grid } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import config from 'configs/app';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import useIsMounted from 'lib/hooks/useIsMounted';
import getQueryParamString from 'lib/router/getQueryParamString';
@@ -9,7 +10,7 @@ import AddressCounterItem from 'ui/address/details/AddressCounterItem';
import ServiceDegradationWarning from 'ui/shared/alerts/ServiceDegradationWarning';
import isCustomAppError from 'ui/shared/AppError/isCustomAppError';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import * as DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import DetailsSponsoredItem from 'ui/shared/DetailsSponsoredItem';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import BlockEntity from 'ui/shared/entities/block/BlockEntity';
@@ -17,6 +18,7 @@ import TxEntity from 'ui/shared/entities/tx/TxEntity';
import AddressBalance from './details/AddressBalance';
import AddressNameInfo from './details/AddressNameInfo';
import AddressNetWorth from './details/AddressNetWorth';
import TokenSelect from './tokenSelect/TokenSelect';
import useAddressCountersQuery from './utils/useAddressCountersQuery';
import type { AddressQuery } from './utils/useAddressQuery';
@@ -90,50 +92,80 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }} overflow="hidden"
>
<AddressNameInfo data={ data } isLoading={ addressQuery.isPlaceholderData }/>
{ data.is_contract && data.creation_tx_hash && data.creator_address_hash && (
<DetailsInfoItem
title="Creator"
hint="Transaction and address of creation"
isLoading={ addressQuery.isPlaceholderData }
>
<AddressEntity
address={{ hash: data.creator_address_hash }}
truncation="constant"
noIcon
/>
<Text whiteSpace="pre"> at txn </Text>
<TxEntity hash={ data.creation_tx_hash } truncation="constant" noIcon noCopy={ false }/>
</DetailsInfoItem>
) }
{ data.is_contract && data.implementation_address && (
<DetailsInfoItem
title="Implementation"
hint="Implementation address of the proxy contract"
columnGap={ 1 }
>
<AddressEntity
address={{ hash: data.implementation_address, name: data.implementation_name, is_contract: true }}
<>
<DetailsInfoItem.Label
hint="Transaction and address of creation"
isLoading={ addressQuery.isPlaceholderData }
noIcon
/>
</DetailsInfoItem>
>
Creator
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<AddressEntity
address={{ hash: data.creator_address_hash }}
truncation="constant"
noIcon
/>
<Text whiteSpace="pre"> at txn </Text>
<TxEntity hash={ data.creation_tx_hash } truncation="constant" noIcon noCopy={ false }/>
</DetailsInfoItem.Value>
</>
) }
{ data.is_contract && data.implementation_address && (
<>
<DetailsInfoItem.Label
hint="Implementation address of the proxy contract"
>
Implementation
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<AddressEntity
address={{ hash: data.implementation_address, name: data.implementation_name, is_contract: true }}
isLoading={ addressQuery.isPlaceholderData }
noIcon
/>
</DetailsInfoItem.Value>
</>
) }
<AddressBalance data={ data } isLoading={ addressQuery.isPlaceholderData }/>
{ data.has_tokens && (
<DetailsInfoItem
title="Tokens"
hint="All tokens in the account and total value"
alignSelf="center"
py={ 0 }
>
{ addressQuery.data ? <TokenSelect onClick={ handleCounterItemClick }/> : <Box py="6px">0</Box> }
</DetailsInfoItem>
<>
<DetailsInfoItem.Label
hint="All tokens in the account and total value"
>
Tokens
</DetailsInfoItem.Label>
<DetailsInfoItem.Value py={ addressQuery.data ? 0 : undefined }>
{ addressQuery.data ? <TokenSelect onClick={ handleCounterItemClick }/> : <Box>0</Box> }
</DetailsInfoItem.Value>
</>
) }
<DetailsInfoItem
title="Transactions"
{ (config.features.multichainButton.isEnabled || (data.exchange_rate && data.has_tokens)) && (
<>
<DetailsInfoItem.Label
hint="Total net worth in USD of all tokens for the address"
isLoading={ addressQuery.isPlaceholderData }
>
Net worth
</DetailsInfoItem.Label>
<DetailsInfoItem.Value alignSelf="center">
<AddressNetWorth addressData={ addressQuery.data } addressHash={ addressHash } isLoading={ addressQuery.isPlaceholderData }/>
</DetailsInfoItem.Value>
</>
)
}
<DetailsInfoItem.Label
hint="Number of transactions related to this address"
isLoading={ addressQuery.isPlaceholderData || countersQuery.isPlaceholderData }
>
Transactions
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
{ addressQuery.data ? (
<AddressCounterItem
prop="transactions_count"
@@ -145,78 +177,97 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
/>
) :
0 }
</DetailsInfoItem>
</DetailsInfoItem.Value>
{ data.has_token_transfers && (
<DetailsInfoItem
title="Transfers"
hint="Number of transfers to/from this address"
isLoading={ addressQuery.isPlaceholderData || countersQuery.isPlaceholderData }
>
{ addressQuery.data ? (
<AddressCounterItem
prop="token_transfers_count"
query={ countersQuery }
address={ data.hash }
onClick={ handleCounterItemClick }
isAddressQueryLoading={ addressQuery.isPlaceholderData }
isDegradedData={ addressQuery.isDegradedData }
/>
) :
0 }
</DetailsInfoItem>
<>
<DetailsInfoItem.Label
hint="Number of transfers to/from this address"
isLoading={ addressQuery.isPlaceholderData || countersQuery.isPlaceholderData }
>
Transfers
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
{ addressQuery.data ? (
<AddressCounterItem
prop="token_transfers_count"
query={ countersQuery }
address={ data.hash }
onClick={ handleCounterItemClick }
isAddressQueryLoading={ addressQuery.isPlaceholderData }
isDegradedData={ addressQuery.isDegradedData }
/>
) :
0 }
</DetailsInfoItem.Value>
</>
) }
{ countersQuery.data?.gas_usage_count && (
<DetailsInfoItem
title="Gas used"
hint="Gas used by the address"
isLoading={ addressQuery.isPlaceholderData || countersQuery.isPlaceholderData }
>
{ addressQuery.data ? (
<AddressCounterItem
prop="gas_usage_count"
query={ countersQuery }
address={ data.hash }
onClick={ handleCounterItemClick }
isAddressQueryLoading={ addressQuery.isPlaceholderData }
isDegradedData={ addressQuery.isDegradedData }
/>
) :
0 }
</DetailsInfoItem>
<>
<DetailsInfoItem.Label
hint="Gas used by the address"
isLoading={ addressQuery.isPlaceholderData || countersQuery.isPlaceholderData }
>
Gas used
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
{ addressQuery.data ? (
<AddressCounterItem
prop="gas_usage_count"
query={ countersQuery }
address={ data.hash }
onClick={ handleCounterItemClick }
isAddressQueryLoading={ addressQuery.isPlaceholderData }
isDegradedData={ addressQuery.isDegradedData }
/>
) :
0 }
</DetailsInfoItem.Value>
</>
) }
{ data.has_validated_blocks && (
<DetailsInfoItem
title="Blocks validated"
hint="Number of blocks validated by this validator"
isLoading={ addressQuery.isPlaceholderData || countersQuery.isPlaceholderData }
>
{ addressQuery.data ? (
<AddressCounterItem
prop="validations_count"
query={ countersQuery }
address={ data.hash }
onClick={ handleCounterItemClick }
isAddressQueryLoading={ addressQuery.isPlaceholderData }
isDegradedData={ addressQuery.isDegradedData }
/>
) :
0 }
</DetailsInfoItem>
<>
<DetailsInfoItem.Label
hint="Number of blocks validated by this validator"
isLoading={ addressQuery.isPlaceholderData || countersQuery.isPlaceholderData }
>
Blocks validated
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
{ addressQuery.data ? (
<AddressCounterItem
prop="validations_count"
query={ countersQuery }
address={ data.hash }
onClick={ handleCounterItemClick }
isAddressQueryLoading={ addressQuery.isPlaceholderData }
isDegradedData={ addressQuery.isDegradedData }
/>
) :
0 }
</DetailsInfoItem.Value>
</>
) }
{ data.block_number_balance_updated_at && (
<DetailsInfoItem
title="Last balance update"
hint="Block number in which the address was updated"
alignSelf="center"
py={{ base: '2px', lg: 1 }}
isLoading={ addressQuery.isPlaceholderData }
>
<BlockEntity
number={ data.block_number_balance_updated_at }
<>
<DetailsInfoItem.Label
hint="Block number in which the address was updated"
isLoading={ addressQuery.isPlaceholderData }
/>
</DetailsInfoItem>
>
Last balance update
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<BlockEntity
number={ data.block_number_balance_updated_at }
isLoading={ addressQuery.isPlaceholderData }
/>
</DetailsInfoItem.Value>
</>
) }
<DetailsSponsoredItem isLoading={ addressQuery.isPlaceholderData }/>
</Grid>
</>
+6 -15
View File
@@ -1,34 +1,25 @@
import { Box } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as internalTxsMock from 'mocks/txs/internalTxs';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import { test, expect } from 'playwright/lib';
import AddressInternalTxs from './AddressInternalTxs';
const ADDRESS_HASH = internalTxsMock.base.from.hash;
const API_URL_TX_INTERNALS = buildApiUrl('address_internal_txs', { hash: ADDRESS_HASH });
const hooksConfig = {
router: {
query: { hash: ADDRESS_HASH },
},
};
test('base view +@mobile', async({ mount, page }) => {
await page.route(API_URL_TX_INTERNALS, (route) => route.fulfill({
status: 200,
body: JSON.stringify(internalTxsMock.baseResponse),
}));
const component = await mount(
<TestApp>
<Box h={{ base: '134px', lg: 6 }}/>
test('base view +@mobile', async({ render, mockApiResponse }) => {
await mockApiResponse('address_internal_txs', internalTxsMock.baseResponse, { pathParams: { hash: ADDRESS_HASH } });
const component = await render(
<Box pt={{ base: '134px', lg: 6 }}>
<AddressInternalTxs/>
</TestApp>,
</Box>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
+79 -105
View File
@@ -1,126 +1,110 @@
import { Box } from '@chakra-ui/react';
import { test as base, expect, devices } from '@playwright/experimental-ct-react';
import React from 'react';
import * as tokenTransferMock from 'mocks/tokens/tokenTransfer';
import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import { test, expect, devices } from 'playwright/lib';
import AddressTokenTransfers from './AddressTokenTransfers';
const CURRENT_ADDRESS = '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859';
const API_URL = buildApiUrl('address_token_transfers', { hash: CURRENT_ADDRESS }) +
'?token=0x1189a607CEac2f0E14867de4EB15b15C9FFB5859';
const TOKEN_HASH = '0x1189a607CEac2f0E14867de4EB15b15C9FFB5859';
const hooksConfig = {
router: {
query: { hash: CURRENT_ADDRESS, token: '0x1189a607CEac2f0E14867de4EB15b15C9FFB5859' },
query: { hash: CURRENT_ADDRESS, token: TOKEN_HASH },
},
};
const test = base.extend<socketServer.SocketServerFixture>({
createSocket: socketServer.createSocket,
});
// FIXME
// 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('with token filter and pagination', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ tokenTransferMock.erc1155A ], next_page_params: { block_number: 1 } }),
}));
const tokenTransfersWithPagination = {
items: [ tokenTransferMock.erc1155A ],
next_page_params: { block_number: 1, index: 1, items_count: 1 },
};
const tokenTransfersWoPagination = {
items: [ tokenTransferMock.erc1155A ],
next_page_params: null,
};
const component = await mount(
<TestApp>
<Box h={{ base: '134px', lg: 6 }}/>
test('with token filter and pagination', async({ render, mockApiResponse }) => {
await mockApiResponse('address_token_transfers', tokenTransfersWithPagination, {
pathParams: { hash: CURRENT_ADDRESS },
queryParams: { token: TOKEN_HASH },
});
const component = await render(
<Box pt={{ base: '134px', lg: 6 }}>
<AddressTokenTransfers/>
</TestApp>,
</Box>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
test('with token filter and no pagination', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ tokenTransferMock.erc1155A ] }),
}));
const component = await mount(
<TestApp>
<Box h={{ base: '134px', lg: 6 }}/>
test('with token filter and no pagination', async({ render, mockApiResponse }) => {
await mockApiResponse('address_token_transfers', tokenTransfersWoPagination, {
pathParams: { hash: CURRENT_ADDRESS },
queryParams: { token: TOKEN_HASH },
});
const component = await render(
<Box pt={{ base: '134px', lg: 6 }}>
<AddressTokenTransfers/>
</TestApp>,
</Box>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
test.describe('mobile', () => {
test.use({ viewport: devices['iPhone 13 Pro'].viewport });
test('with token filter and pagination', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ tokenTransferMock.erc1155A ], next_page_params: { block_number: 1 } }),
}));
const component = await mount(
<TestApp>
<Box h={{ base: '134px', lg: 6 }}/>
test('with token filter and pagination', async({ render, mockApiResponse }) => {
await mockApiResponse('address_token_transfers', tokenTransfersWithPagination, {
pathParams: { hash: CURRENT_ADDRESS },
queryParams: { token: TOKEN_HASH },
});
const component = await render(
<Box pt={{ base: '134px', lg: 6 }}>
<AddressTokenTransfers/>
</TestApp>,
</Box>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
test('with token filter and no pagination', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ tokenTransferMock.erc1155A ] }),
}));
const component = await mount(
<TestApp>
<Box h={{ base: '134px', lg: 6 }}/>
test('with token filter and no pagination', async({ render, mockApiResponse }) => {
await mockApiResponse('address_token_transfers', tokenTransfersWoPagination, {
pathParams: { hash: CURRENT_ADDRESS },
queryParams: { token: TOKEN_HASH },
});
const component = await render(
<Box pt={{ base: '134px', lg: 6 }}>
<AddressTokenTransfers/>
</TestApp>,
</Box>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
});
test.describe('socket', () => {
test('without overload', async({ mount, page, createSocket }) => {
test('without overload', async({ render, mockApiResponse, createSocket, page }) => {
const hooksConfigNoToken = {
router: {
query: { hash: CURRENT_ADDRESS },
},
};
const API_URL_NO_TOKEN = buildApiUrl('address_token_transfers', { hash: CURRENT_ADDRESS }) + '?type=';
await page.route(API_URL_NO_TOKEN, (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 }}/>
await mockApiResponse('address_token_transfers', tokenTransfersWithPagination, {
pathParams: { hash: CURRENT_ADDRESS },
queryParams: { type: [] },
});
await render(
<Box pt={{ base: '134px', lg: 6 }}>
<AddressTokenTransfers/>
</TestApp>,
</Box>,
{ hooksConfig: hooksConfigNoToken },
{ withSocket: true },
);
const socket = await createSocket();
@@ -137,26 +121,22 @@ test.describe('socket', () => {
expect(itemsCountNew).toBe(4);
});
test('with overload', async({ mount, page, createSocket }) => {
test('with overload', async({ render, mockApiResponse, page, createSocket }) => {
const hooksConfigNoToken = {
router: {
query: { hash: CURRENT_ADDRESS },
},
};
const API_URL_NO_TOKEN = buildApiUrl('address_token_transfers', { hash: CURRENT_ADDRESS }) + '?type=';
await page.route(API_URL_NO_TOKEN, (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 }}/>
await mockApiResponse('address_token_transfers', tokenTransfersWithPagination, {
pathParams: { hash: CURRENT_ADDRESS },
queryParams: { type: [] },
});
await render(
<Box pt={{ base: '134px', lg: 6 }}>
<AddressTokenTransfers overloadCount={ 2 }/>
</TestApp>,
</Box>,
{ hooksConfig: hooksConfigNoToken },
{ withSocket: true },
);
const socket = await createSocket();
@@ -176,26 +156,23 @@ test.describe('socket', () => {
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 = {
router: {
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 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 }}/>
await render(
<Box pt={{ base: '134px', lg: 6 }}>
<AddressTokenTransfers/>
</TestApp>,
</Box>,
{ hooksConfig: hooksConfigWithFilter },
{ withSocket: true },
);
const socket = await createSocket();
@@ -212,26 +189,23 @@ test.describe('socket', () => {
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 = {
router: {
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 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 }}/>
await render(
<Box pt={{ base: '134px', lg: 6 }}>
<AddressTokenTransfers overloadCount={ 2 }/>
</TestApp>,
</Box>,
{ hooksConfig: hooksConfigWithFilter },
{ withSocket: true },
);
const socket = await createSocket();
+69 -124
View File
@@ -1,81 +1,53 @@
import { Box } from '@chakra-ui/react';
import { test as base, expect, devices } from '@playwright/experimental-ct-react';
import React from 'react';
import type { AddressTokensResponse } from 'types/api/address';
import * as addressMock from 'mocks/address/address';
import * as tokensMock from 'mocks/address/tokens';
import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import { test, expect, devices } from 'playwright/lib';
import AddressTokens from './AddressTokens';
const ADDRESS_HASH = addressMock.withName.hash;
const API_URL_ADDRESS = buildApiUrl('address', { hash: ADDRESS_HASH });
const API_URL_TOKENS = buildApiUrl('address_tokens', { hash: ADDRESS_HASH });
const API_URL_NFT = buildApiUrl('address_nfts', { hash: ADDRESS_HASH }) + '?type=';
const API_URL_COLLECTIONS = buildApiUrl('address_collections', { hash: ADDRESS_HASH }) + '?type=';
const ADDRESS_HASH = addressMock.validator.hash;
const nextPageParams = {
items_count: 50,
token_name: 'aaa',
token_type: '123',
token_type: 'ERC-20' as const,
value: 1,
fiat_value: '1',
};
const test = base.extend({
page: async({ page }, use) => {
const response20 = {
items: [ tokensMock.erc20a, tokensMock.erc20b, tokensMock.erc20c, tokensMock.erc20d ],
next_page_params: nextPageParams,
};
const response721 = {
items: [ tokensMock.erc721a, tokensMock.erc721b, tokensMock.erc721c ],
next_page_params: nextPageParams,
};
const response1155 = {
items: [ tokensMock.erc1155a, tokensMock.erc1155b ],
next_page_params: nextPageParams,
};
const response404 = {
items: [ tokensMock.erc404a, tokensMock.erc404b ],
next_page_params: nextPageParams,
};
test.beforeEach(async({ mockApiResponse }) => {
const response20: AddressTokensResponse = {
items: [ tokensMock.erc20a, tokensMock.erc20b, tokensMock.erc20c, tokensMock.erc20d ],
next_page_params: nextPageParams,
};
const response721: AddressTokensResponse = {
items: [ tokensMock.erc721a, tokensMock.erc721b, tokensMock.erc721c ],
next_page_params: nextPageParams,
};
const response1155: AddressTokensResponse = {
items: [ tokensMock.erc1155a, tokensMock.erc1155b ],
next_page_params: nextPageParams,
};
const response404: AddressTokensResponse = {
items: [ tokensMock.erc404a, tokensMock.erc404b ],
next_page_params: nextPageParams,
};
await page.route(API_URL_ADDRESS, (route) => route.fulfill({
status: 200,
body: JSON.stringify(addressMock.withName),
}));
await page.route(API_URL_TOKENS + '?type=ERC-20', (route) => route.fulfill({
status: 200,
body: JSON.stringify(response20),
}));
await page.route(API_URL_TOKENS + '?type=ERC-721', (route) => route.fulfill({
status: 200,
body: JSON.stringify(response721),
}));
await page.route(API_URL_TOKENS + '?type=ERC-1155', (route) => route.fulfill({
status: 200,
body: JSON.stringify(response1155),
}));
await page.route(API_URL_TOKENS + '?type=ERC-404', (route) => route.fulfill({
status: 200,
body: JSON.stringify(response404),
}));
await page.route(API_URL_NFT, (route) => route.fulfill({
status: 200,
body: JSON.stringify(tokensMock.nfts),
}));
await page.route(API_URL_COLLECTIONS, (route) => route.fulfill({
status: 200,
body: JSON.stringify(tokensMock.collections),
}));
use(page);
},
await mockApiResponse('address', addressMock.validator, { pathParams: { hash: ADDRESS_HASH } });
await mockApiResponse('address_tokens', response20, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-20' } });
await mockApiResponse('address_tokens', response721, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-721' } });
await mockApiResponse('address_tokens', response1155, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-1155' } });
await mockApiResponse('address_tokens', response404, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-404' } });
await mockApiResponse('address_nfts', tokensMock.nfts, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: [] } });
await mockApiResponse('address_collections', tokensMock.collections, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: [] } });
});
test('erc20 +@dark-mode', async({ mount }) => {
test('erc20 +@dark-mode', async({ render }) => {
const hooksConfig = {
router: {
query: { hash: ADDRESS_HASH, tab: 'tokens_erc20' },
@@ -83,18 +55,17 @@ test('erc20 +@dark-mode', async({ mount }) => {
},
};
const component = await mount(
<TestApp>
<Box h={{ base: '134px', lg: 6 }}/>
const component = await render(
<Box pt={{ base: '134px', lg: 6 }}>
<AddressTokens/>
</TestApp>,
</Box>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
test('collections +@dark-mode', async({ mount }) => {
test('collections +@dark-mode', async({ render }) => {
const hooksConfig = {
router: {
query: { hash: ADDRESS_HASH, tab: 'tokens_nfts' },
@@ -102,18 +73,17 @@ test('collections +@dark-mode', async({ mount }) => {
},
};
const component = await mount(
<TestApp>
<Box h={{ base: '134px', lg: 6 }}/>
const component = await render(
<Box pt={{ base: '134px', lg: 6 }}>
<AddressTokens/>
</TestApp>,
</Box>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
test('nfts +@dark-mode', async({ mount }) => {
test('nfts +@dark-mode', async({ render }) => {
const hooksConfig = {
router: {
query: { hash: ADDRESS_HASH, tab: 'tokens_nfts' },
@@ -121,11 +91,10 @@ test('nfts +@dark-mode', async({ mount }) => {
},
};
const component = await mount(
<TestApp>
<Box h={{ base: '134px', lg: 6 }}/>
const component = await render(
<Box pt={{ base: '134px', lg: 6 }}>
<AddressTokens/>
</TestApp>,
</Box>,
{ hooksConfig },
);
@@ -137,7 +106,7 @@ test('nfts +@dark-mode', async({ mount }) => {
test.describe('mobile', () => {
test.use({ viewport: devices['iPhone 13 Pro'].viewport });
test('erc20', async({ mount }) => {
test('erc20', async({ render }) => {
const hooksConfig = {
router: {
query: { hash: ADDRESS_HASH, tab: 'tokens_erc20' },
@@ -145,18 +114,17 @@ test.describe('mobile', () => {
},
};
const component = await mount(
<TestApp>
<Box h={{ base: '134px', lg: 6 }}/>
const component = await render(
<Box pt={{ base: '134px', lg: 6 }}>
<AddressTokens/>
</TestApp>,
</Box>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
test('nfts', async({ mount }) => {
test('nfts', async({ render }) => {
const hooksConfig = {
router: {
query: { hash: ADDRESS_HASH, tab: 'tokens_nfts' },
@@ -164,11 +132,10 @@ test.describe('mobile', () => {
},
};
const component = await mount(
<TestApp>
<Box h={{ base: '134px', lg: 6 }}/>
const component = await render(
<Box pt={{ base: '134px', lg: 6 }}>
<AddressTokens/>
</TestApp>,
</Box>,
{ hooksConfig },
);
@@ -177,7 +144,7 @@ test.describe('mobile', () => {
await expect(component).toHaveScreenshot();
});
test('collections', async({ mount }) => {
test('collections', async({ render }) => {
const hooksConfig = {
router: {
query: { hash: ADDRESS_HASH, tab: 'tokens_nfts' },
@@ -185,11 +152,10 @@ test.describe('mobile', () => {
},
};
const component = await mount(
<TestApp>
<Box h={{ base: '134px', lg: 6 }}/>
const component = await render(
<Box pt={{ base: '134px', lg: 6 }}>
<AddressTokens/>
</TestApp>,
</Box>,
{ hooksConfig },
);
@@ -197,13 +163,10 @@ test.describe('mobile', () => {
});
});
base.describe('update balances via socket', () => {
const test = base.extend<socketServer.SocketServerFixture>({
createSocket: socketServer.createSocket,
});
test.describe('update balances via socket', () => {
test.describe.configure({ mode: 'serial' });
test('', async({ mount, page, createSocket }) => {
test('', async({ render, page, createSocket, mockApiResponse }) => {
test.slow();
const hooksConfig = {
@@ -230,41 +193,23 @@ base.describe('update balances via socket', () => {
next_page_params: null,
};
await page.route(API_URL_ADDRESS, (route) => route.fulfill({
status: 200,
body: JSON.stringify(addressMock.validator),
}));
await page.route(API_URL_TOKENS + '?type=ERC-20', (route) => route.fulfill({
status: 200,
body: JSON.stringify(response20),
}));
await page.route(API_URL_TOKENS + '?type=ERC-721', (route) => route.fulfill({
status: 200,
body: JSON.stringify(response721),
}));
await page.route(API_URL_TOKENS + '?type=ERC-1155', (route) => route.fulfill({
status: 200,
body: JSON.stringify(response1155),
}));
await page.route(API_URL_TOKENS + '?type=ERC-404', (route) => route.fulfill({
status: 200,
body: JSON.stringify(response404),
}));
const erc20ApiUrl = await mockApiResponse('address_tokens', response20, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-20' } });
const erc721ApiUrl = await mockApiResponse('address_tokens', response721, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-721' } });
const erc1155ApiUrl = await mockApiResponse('address_tokens', response1155, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-1155' } });
const erc404ApiUrl = await mockApiResponse('address_tokens', response404, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-404' } });
const component = await mount(
<TestApp withSocket>
<Box>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTokens/>
</Box>
</TestApp>,
const component = await render(
<Box pt={{ base: '134px', lg: 6 }}>
<AddressTokens/>
</Box>,
{ hooksConfig },
{ withSocket: true },
);
await page.waitForResponse(API_URL_TOKENS + '?type=ERC-20');
await page.waitForResponse(API_URL_TOKENS + '?type=ERC-721');
await page.waitForResponse(API_URL_TOKENS + '?type=ERC-1155');
await page.waitForResponse(API_URL_TOKENS + '?type=ERC-404');
await page.waitForResponse(erc20ApiUrl);
await page.waitForResponse(erc721ApiUrl);
await page.waitForResponse(erc1155ApiUrl);
await page.waitForResponse(erc404ApiUrl);
await expect(component).toHaveScreenshot();
+75 -81
View File
@@ -1,84 +1,78 @@
import { Box } from '@chakra-ui/react';
import { test as base, expect } from '@playwright/experimental-ct-react';
import type { Locator } from '@playwright/test';
import React from 'react';
import * as txMock from 'mocks/txs/tx';
import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import * as configs from 'playwright/utils/configs';
import { test, expect } from 'playwright/lib';
import * as pwConfig from 'playwright/utils/config';
import AddressTxs from './AddressTxs';
const CURRENT_ADDRESS = '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859';
const API_URL = buildApiUrl('address_txs', { hash: CURRENT_ADDRESS });
const hooksConfig = {
router: {
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;
base.beforeEach(async({ page, mount }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [
txMock.base,
{
...txMock.base,
hash: '0x62d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3194',
},
], next_page_params: { block: 1 } }),
}));
component = await mount(
<TestApp>
<Box h={{ base: '134px', lg: 6 }}/>
test.beforeEach(async({ render, mockApiResponse }) => {
await mockApiResponse(
'address_txs',
{
items: [
txMock.base,
{ ...txMock.base, hash: '0x62d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3194' },
],
next_page_params: DEFAULT_PAGINATION,
},
{ pathParams: { hash: CURRENT_ADDRESS } },
);
component = await render(
<Box pt={{ base: '134px', lg: 6 }}>
<AddressTxs/>
</TestApp>,
</Box>,
{ hooksConfig },
);
});
base('+@mobile', async() => {
test('+@mobile', async() => {
await expect(component).toHaveScreenshot();
});
base.describe('screen xl', () => {
base.use({ viewport: configs.viewport.xl });
test.describe('screen xl', () => {
test.use({ viewport: pwConfig.viewport.xl });
base('', async() => {
base.slow();
test('', async() => {
test.slow();
await expect(component).toHaveScreenshot();
});
});
});
base.describe('socket', () => {
const test = base.extend<socketServer.SocketServerFixture>({
createSocket: socketServer.createSocket,
});
test.describe('socket', () => {
// FIXME
// 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('without overload', async({ mount, page, createSocket }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ txMock.base ], next_page_params: { block: 1 } }),
}));
test('without overload', async({ render, mockApiResponse, page, createSocket }) => {
await mockApiResponse(
'address_txs',
{ items: [ txMock.base ], next_page_params: DEFAULT_PAGINATION },
{ pathParams: { hash: CURRENT_ADDRESS } },
);
await mount(
<TestApp withSocket>
<Box h={{ base: '134px', lg: 6 }}/>
await render(
<Box pt={{ base: '134px', lg: 6 }}>
<AddressTxs/>
</TestApp>,
</Box>,
{ hooksConfig },
{ withSocket: true },
);
const socket = await createSocket();
@@ -95,18 +89,19 @@ base.describe('socket', () => {
expect(itemsCountNew).toBe(4);
});
test('with update', async({ mount, page, createSocket }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ txMock.pending ], next_page_params: { block: 1 } }),
}));
test('with update', async({ render, mockApiResponse, page, createSocket }) => {
await mockApiResponse(
'address_txs',
{ items: [ txMock.pending ], next_page_params: DEFAULT_PAGINATION },
{ pathParams: { hash: CURRENT_ADDRESS } },
);
await mount(
<TestApp withSocket>
<Box h={{ base: '134px', lg: 6 }}/>
await render(
<Box pt={{ base: '134px', lg: 6 }}>
<AddressTxs/>
</TestApp>,
</Box>,
{ hooksConfig },
{ withSocket: true },
);
const socket = await createSocket();
@@ -123,18 +118,19 @@ base.describe('socket', () => {
expect(itemsCountNew).toBe(3);
});
test('with overload', async({ mount, page, createSocket }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ txMock.base ], next_page_params: { block: 1 } }),
}));
test('with overload', async({ render, mockApiResponse, page, createSocket }) => {
await mockApiResponse(
'address_txs',
{ items: [ txMock.base ], next_page_params: DEFAULT_PAGINATION },
{ pathParams: { hash: CURRENT_ADDRESS } },
);
await mount(
<TestApp withSocket>
<Box h={{ base: '134px', lg: 6 }}/>
await render(
<Box pt={{ base: '134px', lg: 6 }}>
<AddressTxs overloadCount={ 2 }/>
</TestApp>,
</Box>,
{ hooksConfig },
{ withSocket: true },
);
const socket = await createSocket();
@@ -154,26 +150,25 @@ base.describe('socket', () => {
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 = {
router: {
query: { hash: CURRENT_ADDRESS, filter: 'from' },
},
};
const API_URL_WITH_FILTER = buildApiUrl('address_txs', { hash: CURRENT_ADDRESS }) + '?filter=from';
await mockApiResponse(
'address_txs',
{ items: [ txMock.base ], next_page_params: DEFAULT_PAGINATION },
{ pathParams: { hash: CURRENT_ADDRESS }, queryParams: { filter: 'from' } },
);
await page.route(API_URL_WITH_FILTER, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ txMock.base ], next_page_params: { block: 1 } }),
}));
await mount(
<TestApp withSocket>
<Box h={{ base: '134px', lg: 6 }}/>
await render(
<Box pt={{ base: '134px', lg: 6 }}>
<AddressTxs/>
</TestApp>,
</Box>,
{ hooksConfig: hooksConfigWithFilter },
{ withSocket: true },
);
const socket = await createSocket();
@@ -190,26 +185,25 @@ base.describe('socket', () => {
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 = {
router: {
query: { hash: CURRENT_ADDRESS, filter: 'from' },
},
};
const API_URL_WITH_FILTER = buildApiUrl('address_txs', { hash: CURRENT_ADDRESS }) + '?filter=from';
await mockApiResponse(
'address_txs',
{ items: [ txMock.base ], next_page_params: DEFAULT_PAGINATION },
{ pathParams: { hash: CURRENT_ADDRESS }, queryParams: { filter: 'from' } },
);
await page.route(API_URL_WITH_FILTER, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ txMock.base ], next_page_params: { block: 1 } }),
}));
await mount(
<TestApp withSocket>
<Box h={{ base: '134px', lg: 6 }}/>
await render(
<Box pt={{ base: '134px', lg: 6 }}>
<AddressTxs overloadCount={ 2 }/>
</TestApp>,
</Box>,
{ hooksConfig: hooksConfigWithFilter },
{ withSocket: true },
);
const socket = await createSocket();
+31 -41
View File
@@ -1,25 +1,35 @@
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as solidityscanReportMock from 'mocks/contract/solidityscanReport';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import { test, expect } from 'playwright/lib';
import SolidityscanReport from './SolidityscanReport';
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),
}));
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 } });
const component = await mount(
<TestApp>
<SolidityscanReport hash={ addressHash }/>
</TestApp>,
await component.getByLabel('SolidityScan score').click();
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 400, height: 500 } });
});
test('great report', async({ render, mockApiResponse, page }) => {
await mockApiResponse(
'contract_solidityscan_report',
solidityscanReportMock.solidityscanReportGreat,
{ pathParams: { hash: addressHash } },
);
const component = await render(
<SolidityscanReport hash={ addressHash }/>,
);
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 100, height: 50 } });
@@ -29,35 +39,15 @@ test('average report +@dark-mode +@mobile', async({ mount, page }) => {
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 400, height: 500 } });
});
test('great report', async({ mount, page }) => {
await page.route(REPORT_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(solidityscanReportMock.solidityscanReportGreat),
}));
test('low report', async({ render, mockApiResponse, page }) => {
await mockApiResponse(
'contract_solidityscan_report',
solidityscanReportMock.solidityscanReportLow,
{ pathParams: { hash: addressHash } },
);
const component = await mount(
<TestApp>
<SolidityscanReport hash={ addressHash }/>
</TestApp>,
);
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 100, height: 50 } });
await component.getByLabel('SolidityScan score').click();
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 400, height: 500 } });
});
test('low report', async({ mount, page }) => {
await page.route(REPORT_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(solidityscanReportMock.solidityscanReportLow),
}));
const component = await mount(
<TestApp>
<SolidityscanReport hash={ addressHash }/>
</TestApp>,
const component = await render(
<SolidityscanReport hash={ addressHash }/>,
);
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 100, height: 50 } });
+4 -6
View File
@@ -1,4 +1,4 @@
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';
// This icon doesn't work properly when it is in the sprite
@@ -7,17 +7,16 @@ import React from 'react';
import solidityScanIcon from 'icons/brands/solidity_scan.svg';
import useApiQuery from 'lib/api/useApiQuery';
import { SOLIDITYSCAN_REPORT } from 'stubs/contract';
import LinkExternal from 'ui/shared/LinkExternal';
import LinkExternal from 'ui/shared/links/LinkExternal';
import SolidityscanReportButton from 'ui/shared/solidityscanReport/SolidityscanReportButton';
import SolidityscanReportDetails from 'ui/shared/solidityscanReport/SolidityscanReportDetails';
import SolidityscanReportScore from 'ui/shared/solidityscanReport/SolidityscanReportScore';
interface Props {
className?: string;
hash: string;
}
const SolidityscanReport = ({ className, hash }: Props) => {
const SolidityscanReport = ({ hash }: Props) => {
const { isOpen, onToggle, onClose } = useDisclosure();
const { data, isPlaceholderData, isError } = useApiQuery('contract_solidityscan_report', {
@@ -42,7 +41,6 @@ const SolidityscanReport = ({ className, hash }: Props) => {
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy>
<PopoverTrigger>
<SolidityscanReportButton
className={ className }
score={ score }
isLoading={ isPlaceholderData }
onClick={ onToggle }
@@ -69,4 +67,4 @@ const SolidityscanReport = ({ className, hash }: Props) => {
);
};
export default chakra(SolidityscanReport);
export default React.memo(SolidityscanReport);
Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Some files were not shown because too many files have changed in this diff Show More