Merge branch 'main' into defi-dropdown
@@ -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/*',
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
@@ -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'),
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'}]
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
|
||||
### 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` |
|
||||
|
||||
|
||||
|
||||
### 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
|
||||
|
||||
|
||||
|
||||
### 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'` |
|
||||
|
||||
|
||||
|
||||
#### 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` |
|
||||
|
||||
|
||||
|
||||
## External services configuration
|
||||
|
||||
### Google ReCaptcha
|
||||
|
||||
|
Before Width: | Height: | Size: 752 B After Width: | Height: | Size: 752 B |
@@ -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 |
@@ -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) {
|
||||
|
||||
@@ -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 };
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,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() => {
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
@@ -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'> = {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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 }>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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 }>
|
||||
|
||||
@@ -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: [],
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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,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';
|
||||
|
||||
@@ -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 +0,0 @@
|
||||
export const socketPort = 3200;
|
||||
@@ -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 = {
|
||||
@@ -0,0 +1 @@
|
||||
export const port = 3200;
|
||||
@@ -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,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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -15,6 +15,9 @@ const baseStyleTrack = defineStyle((props) => {
|
||||
bg: mode(`${ c }.600`, `${ c }.400`)(props),
|
||||
},
|
||||
},
|
||||
_focusVisible: {
|
||||
boxShadow: 'none',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -21,6 +21,12 @@ const typography = {
|
||||
lineHeight: '32px',
|
||||
fontFamily: 'heading',
|
||||
},
|
||||
h4: {
|
||||
fontSize: 'md',
|
||||
fontWeight: '500',
|
||||
lineHeight: '24px',
|
||||
fontFamily: 'heading',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export type MultichainProviderConfig = {
|
||||
name: string;
|
||||
dapp_id?: string;
|
||||
url_template: string;
|
||||
logo?: string;
|
||||
};
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 } });
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 33 KiB |