Fixes for release v2.4.0 (#3079)
* fix links in approvals list * pass check-summed hash to safe API call * make tx hash optional for token transfers * fix multiple public tag submission * update screenshots * use DataListDisplay for revoke * force to send credential only with general API requests * prefer formatted date label over date default formatting when saving chart data as csv * add abort failed status for zeta chain CCTX * update specify sdk * rename ENV for native token address * add ENV to hide native coin price in topbar * rename routes for name services * fix chart csv export file * fix tests --------- Co-authored-by: Max Alekseenko <nnwen0609@gmail.com> Co-authored-by: isstuev <isstuev@gmail.com>
@@ -4,13 +4,12 @@ import { getEnvValue } from '../utils';
|
||||
|
||||
const title = 'Celo chain';
|
||||
|
||||
const config: Feature<{ nativeTokenAddress?: string }> = (() => {
|
||||
const config: Feature<{ }> = (() => {
|
||||
|
||||
if (getEnvValue('NEXT_PUBLIC_CELO_ENABLED') === 'true') {
|
||||
return Object.freeze({
|
||||
title,
|
||||
isEnabled: true,
|
||||
nativeTokenAddress: getEnvValue('NEXT_PUBLIC_CELO_NATIVE_TOKEN_ADDRESS'),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import type { Feature } from './types';
|
||||
|
||||
import apis from '../apis';
|
||||
import { getEnvValue } from '../utils';
|
||||
|
||||
const title = 'Clusters universal name service';
|
||||
|
||||
const config: Feature<{ cdnUrl: string }> = (() => {
|
||||
const cdnUrl = getEnvValue('NEXT_PUBLIC_CLUSTERS_CDN_URL') || 'https://cdn.clusters.xyz';
|
||||
|
||||
if (apis.clusters) {
|
||||
return Object.freeze({
|
||||
title,
|
||||
isEnabled: true,
|
||||
cdnUrl,
|
||||
});
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
title,
|
||||
isEnabled: false,
|
||||
cdnUrl,
|
||||
});
|
||||
})();
|
||||
|
||||
export default config;
|
||||
@@ -10,7 +10,6 @@ export { default as beaconChain } from './beaconChain';
|
||||
export { default as bridgedTokens } from './bridgedTokens';
|
||||
export { default as blockchainInteraction } from './blockchainInteraction';
|
||||
export { default as celo } from './celo';
|
||||
export { default as clusters } from './clusters';
|
||||
export { default as csvExport } from './csvExport';
|
||||
export { default as dataAvailability } from './dataAvailability';
|
||||
export { default as deFiDropdown } from './deFiDropdown';
|
||||
@@ -29,7 +28,7 @@ export { default as metasuites } from './metasuites';
|
||||
export { default as mixpanel } from './mixpanel';
|
||||
export { default as mudFramework } from './mudFramework';
|
||||
export { default as multichainButton } from './multichainButton';
|
||||
export { default as nameService } from './nameService';
|
||||
export { default as nameServices } from './nameServices';
|
||||
export { default as opSuperchain } from './opSuperchain';
|
||||
export { default as pools } from './pools';
|
||||
export { default as publicTagsSubmission } from './publicTagsSubmission';
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import type { Feature } from './types';
|
||||
|
||||
import apis from '../apis';
|
||||
|
||||
const title = 'Name service integration';
|
||||
|
||||
const config: Feature<{}> = (() => {
|
||||
if (apis.bens) {
|
||||
return Object.freeze({
|
||||
title,
|
||||
isEnabled: true,
|
||||
});
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
title,
|
||||
isEnabled: false,
|
||||
});
|
||||
})();
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { Feature } from './types';
|
||||
|
||||
import apis from '../apis';
|
||||
import { getEnvValue } from '../utils';
|
||||
|
||||
const title = 'Name services integration';
|
||||
|
||||
const config: Feature<{ ens: { isEnabled: boolean }; clusters: { isEnabled: boolean; cdnUrl: string } }> = (() => {
|
||||
if (apis.bens || apis.clusters) {
|
||||
return Object.freeze({
|
||||
title,
|
||||
isEnabled: true,
|
||||
ens: {
|
||||
isEnabled: apis.bens ? true : false,
|
||||
},
|
||||
clusters: {
|
||||
isEnabled: apis.clusters ? true : false,
|
||||
cdnUrl: getEnvValue('NEXT_PUBLIC_CLUSTERS_CDN_URL') || 'https://cdn.clusters.xyz',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
title,
|
||||
isEnabled: false,
|
||||
});
|
||||
})();
|
||||
|
||||
export default config;
|
||||
@@ -99,6 +99,9 @@ const UI = Object.freeze({
|
||||
body: parseEnvJson<FontFamily>(getEnvValue('NEXT_PUBLIC_FONT_FAMILY_BODY')),
|
||||
},
|
||||
maxContentWidth: getEnvValue('NEXT_PUBLIC_MAX_CONTENT_WIDTH_ENABLED') === 'false' ? false : true,
|
||||
nativeCoinPrice: {
|
||||
isHidden: getEnvValue('NEXT_PUBLIC_HIDE_NATIVE_COIN_PRICE') === 'true' ? true : false,
|
||||
},
|
||||
});
|
||||
|
||||
export default UI;
|
||||
|
||||
@@ -82,6 +82,7 @@ const config = Object.freeze({
|
||||
bech32Prefix,
|
||||
},
|
||||
hiddenViews,
|
||||
nativeTokenAddress: getEnvValue('NEXT_PUBLIC_VIEWS_ADDRESS_NATIVE_TOKEN_ADDRESS'),
|
||||
solidityscanEnabled: getEnvValue('NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED') === 'true',
|
||||
extraVerificationMethods,
|
||||
languageFilters,
|
||||
|
||||
@@ -9,7 +9,7 @@ NEXT_PUBLIC_APP_PORT=3000
|
||||
NEXT_PUBLIC_APP_ENV=development
|
||||
NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
|
||||
|
||||
NEXT_PUBLIC_CELO_NATIVE_TOKEN_ADDRESS=0x471EcE3750Da237f93B8E339c536989b8978a438
|
||||
NEXT_PUBLIC_VIEWS_ADDRESS_NATIVE_TOKEN_ADDRESS=0x471EcE3750Da237f93B8E339c536989b8978a438
|
||||
|
||||
# Instance ENVs
|
||||
NEXT_PUBLIC_ADDRESS_3RD_PARTY_WIDGETS=['talentprotocol', 'efp', 'webacy', 'deepdao', 'humanpassport', 'bankless', 'blockscoutbadges']
|
||||
|
||||
@@ -399,16 +399,6 @@ const rollupSchema = yup
|
||||
.object()
|
||||
.shape({
|
||||
NEXT_PUBLIC_CELO_ENABLED: yup.boolean(),
|
||||
NEXT_PUBLIC_CELO_NATIVE_TOKEN_ADDRESS: yup
|
||||
.string()
|
||||
.min(42)
|
||||
.max(42)
|
||||
.matches(regexp.HEX_REGEXP_WITH_0X)
|
||||
.when('NEXT_PUBLIC_CELO_ENABLED', {
|
||||
is: (value: boolean) => value,
|
||||
then: (schema) => schema,
|
||||
otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_CELO_NATIVE_TOKEN_ADDRESS can only be used if NEXT_PUBLIC_CELO_ENABLED is set to \'true\''),
|
||||
}),
|
||||
});
|
||||
|
||||
const megaEthSchema = yup
|
||||
@@ -981,6 +971,11 @@ const schema = yup
|
||||
then: (schema) => schema.required().min(1).max(83),
|
||||
otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_VIEWS_ADDRESS_BECH_32_PREFIX is required if NEXT_PUBLIC_VIEWS_ADDRESS_FORMAT contains "bech32"'),
|
||||
}),
|
||||
NEXT_PUBLIC_VIEWS_ADDRESS_NATIVE_TOKEN_ADDRESS: yup
|
||||
.string()
|
||||
.min(42)
|
||||
.max(42)
|
||||
.matches(regexp.HEX_REGEXP_WITH_0X),
|
||||
|
||||
NEXT_PUBLIC_VIEWS_ADDRESS_HIDDEN_VIEWS: yup
|
||||
.array()
|
||||
@@ -1043,6 +1038,7 @@ const schema = yup
|
||||
NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS: yup.boolean(),
|
||||
NEXT_PUBLIC_HIDE_INDEXING_ALERT_BLOCKS: yup.boolean(),
|
||||
NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS: yup.boolean(),
|
||||
NEXT_PUBLIC_HIDE_NATIVE_COIN_PRICE: yup.boolean(),
|
||||
NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE: yup.string(),
|
||||
NEXT_PUBLIC_COLOR_THEME_DEFAULT: yup.string().oneOf(COLOR_THEME_IDS),
|
||||
NEXT_PUBLIC_COLOR_THEME_OVERRIDES: yup.object().transform(replaceQuotes).json(),
|
||||
|
||||
@@ -32,6 +32,7 @@ NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c18099
|
||||
NEXT_PUBLIC_HELIA_VERIFIED_FETCH_ENABLED=false
|
||||
NEXT_PUBLIC_HIDE_INDEXING_ALERT_BLOCKS=false
|
||||
NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS=false
|
||||
NEXT_PUBLIC_HIDE_NATIVE_COIN_PRICE=false
|
||||
NEXT_PUBLIC_MAX_CONTENT_WIDTH_ENABLED=false
|
||||
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
|
||||
NEXT_PUBLIC_HOMEPAGE_STATS=['total_blocks','average_block_time','total_txs','wallet_addresses','gas_tracker','current_epoch']
|
||||
@@ -68,6 +69,7 @@ NEXT_PUBLIC_USE_NEXT_JS_PROXY=false
|
||||
NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE=gradient_avatar
|
||||
NEXT_PUBLIC_VIEWS_ADDRESS_FORMAT=['base16']
|
||||
NEXT_PUBLIC_VIEWS_ADDRESS_HIDDEN_VIEWS=['top_accounts']
|
||||
NEXT_PUBLIC_VIEWS_ADDRESS_NATIVE_TOKEN_ADDRESS=0x471EcE3750Da237f93B8E339c536989b8978a438
|
||||
NEXT_PUBLIC_VIEWS_CONTRACT_EXTRA_VERIFICATION_METHODS=['solidity-hardhat','solidity-foundry']
|
||||
NEXT_PUBLIC_VIEWS_CONTRACT_LANGUAGE_FILTERS=['solidity','vyper','yul','scilla','geas','stylus_rust']
|
||||
NEXT_PUBLIC_VIEWS_BLOCK_HIDDEN_FIELDS=['burnt_fees','total_reward']
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
NEXT_PUBLIC_CELO_ENABLED=true
|
||||
NEXT_PUBLIC_CELO_NATIVE_TOKEN_ADDRESS=0x471EcE3750Da237f93B8E339c536989b8978a438
|
||||
NEXT_PUBLIC_CELO_ENABLED=true
|
||||
@@ -279,6 +279,7 @@ Settings for meta tags, OG tags and SEO
|
||||
| NEXT_PUBLIC_VIEWS_ADDRESS_FORMAT | `Array<"base16" \| "bech32">` | Displayed address format, could be either `base16` standard or [`bech32`](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#bech32) standard. If the array contains multiple values, the address format toggle will appear in the UI, allowing the user to switch between formats. The first item in the array will be the default format. | - | `'["base16"]'` | `'["bech32", "base16"]'` | v1.36.0+ |
|
||||
| NEXT_PUBLIC_VIEWS_ADDRESS_BECH_32_PREFIX | `string` | Human-readable prefix of `bech32` address format. | Required, if `NEXT_PUBLIC_VIEWS_ADDRESS_FORMAT` contains "bech32" value | - | `duck` | v1.36.0+ |
|
||||
| NEXT_PUBLIC_VIEWS_ADDRESS_HIDDEN_VIEWS | `Array<AddressViewId>` | Address views that should not be displayed. See below the list of the possible id values. | - | - | `'["top_accounts"]'` | v1.15.0+ |
|
||||
| NEXT_PUBLIC_VIEWS_ADDRESS_NATIVE_TOKEN_ADDRESS | `string` | The address of a native ERC-20 token that shadows the balance of the native coin; used to exclude its balance from the net worth value of user tokens. | - | - | `0x471EcE3750Da237f93B8E339c536989b8978a438` | v2.4.0+ |
|
||||
| NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED | `boolean` | Set to `true` if SolidityScan reports are supported | - | - | `true` | v1.19.0+ |
|
||||
| NEXT_PUBLIC_VIEWS_CONTRACT_EXTRA_VERIFICATION_METHODS | `Array<'solidity-hardhat' \| 'solidity-foundry'>` | Pass an array of additional methods from which users can choose while verifying a smart contract. Both methods are available by default, pass `'none'` string to disable them all. | - | - | `['solidity-hardhat']` | v1.33.0+ |
|
||||
| NEXT_PUBLIC_VIEWS_CONTRACT_LANGUAGE_FILTERS | `Array<'solidity' \| 'vyper' \| 'yul' \| 'scilla' \| 'geas' \| 'stylus_rust'>` | Pass an array of contract languages that will be displayed as options in the filter on the verified contract page. | - | `['solidity','vyper','yul','geas','stylus_rust']` | `['solidity','vyper','yul','scilla']` | v1.37.0+ |
|
||||
@@ -354,6 +355,7 @@ Settings for meta tags, OG tags and SEO
|
||||
| NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS | `boolean` | Set to `true` to enable Submit Audit form on the contract page | - | `false` | `true` | v1.25.0+ |
|
||||
| NEXT_PUBLIC_HIDE_INDEXING_ALERT_BLOCKS | `boolean` | Set to `true` to hide indexing alert in the page header about indexing chain's blocks | - | `false` | `true` | v1.17.0+ |
|
||||
| NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS | `boolean` | Set to `true` to hide indexing alert in the page footer about indexing block's internal transactions | - | `false` | `true` | v1.17.0+ |
|
||||
| NEXT_PUBLIC_HIDE_NATIVE_COIN_PRICE | `boolean` | Set to `true` to hide the native coin price in the top bar | - | `false` | `true` | v2.4.0+ |
|
||||
| NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE | `string` | Used for displaying custom announcements or alerts in the header of the site. Could be a regular string or a HTML code. | - | - | `Hello world! 🤪` | v1.13.0+ |
|
||||
| NEXT_PUBLIC_COLOR_THEME_DEFAULT | `'light' \| 'dim' \| 'midnight' \| 'dark'` | Preferred color theme of the app | - | - | `midnight` | v1.30.0+ |
|
||||
| NEXT_PUBLIC_COLOR_THEME_OVERRIDES | `string` | Color overrides for the default theme; pass a JSON-like string that represents a subset of the `DEFAULT_THEME_COLORS` object (see `toolkit/theme/foundations/colors.ts`) to customize the app's main colors. See [here](https://www.figma.com/design/4In0X8UADoZaTfZ34HaZ3K/Blockscout-design-system?node-id=29124-23813&t=XOv4ahHUSsTDlNkN-4) the Figma worksheet with description of available color tokens. | - | - | `{'text':{'primary':{'_light':{'value':'rgba(16,17,18,0.80)'},'_dark':{'value':'rgba(222,217,217)'}}}}` | v2.3.0+ |
|
||||
@@ -816,7 +818,6 @@ For blockchains that use the Celo platform.
|
||||
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| NEXT_PUBLIC_CELO_ENABLED | `boolean` | Indicates that it is a Celo-based chain. | - | - | `true` | v1.37.0+ |
|
||||
| NEXT_PUBLIC_CELO_NATIVE_TOKEN_ADDRESS | `string` | The address of the CELO ERC-20 token. Used to exclude its balance from the net worth value of user tokens. | - | - | `0x471EcE3750Da237f93B8E339c536989b8978a438` | v2.3.0+ |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ module.exports = {
|
||||
if (pathname === '/tx/[hash]') {
|
||||
return `/tx/${ query.hash || 'test-hash' }`;
|
||||
}
|
||||
if (pathname === '/clusters/[name]') {
|
||||
return `/clusters/${ query.name || 'test-cluster' }`;
|
||||
if (pathname === '/name-services/clusters/[name]') {
|
||||
return `/name-services/clusters/${ query.name || 'test-cluster' }`;
|
||||
}
|
||||
|
||||
return pathname || '/';
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useQueryClient } from '@tanstack/react-query';
|
||||
import { omit, pickBy } from 'es-toolkit';
|
||||
import React from 'react';
|
||||
|
||||
import type { ApiName } from './types';
|
||||
import type { CsrfData } from 'types/client/account';
|
||||
import type { ChainConfig } from 'types/multichain';
|
||||
|
||||
@@ -17,6 +18,22 @@ import buildUrl from './buildUrl';
|
||||
import getResourceParams from './getResourceParams';
|
||||
import type { ResourceName, ResourcePathParams } from './resources';
|
||||
|
||||
function needCredentials(apiName: ApiName) {
|
||||
if (![ 'general' ].includes(apiName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// currently, the cookies are used only for the following features
|
||||
if (
|
||||
config.features.account.isEnabled ||
|
||||
config.UI.views.token.hideScamTokensEnabled
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export interface Params<R extends ResourceName> {
|
||||
pathParams?: ResourcePathParams<R>;
|
||||
queryParams?: Record<string, string | Array<string> | number | boolean | undefined | null>;
|
||||
@@ -50,11 +67,7 @@ export default function useApiFetch() {
|
||||
return fetch<SuccessType, ErrorType>(
|
||||
url,
|
||||
{
|
||||
// as of today, we use cookies only
|
||||
// for user authentication in My account
|
||||
// for API rate-limits (cannot use in the condition though, but we agreed with devops team that should not be an issue)
|
||||
// change condition here if something is changed
|
||||
credentials: config.features.account.isEnabled ? 'include' : 'same-origin',
|
||||
credentials: needCredentials(apiName) ? 'include' : 'same-origin',
|
||||
headers,
|
||||
...(fetchParams ? omit(fetchParams, [ 'headers' ]) : {}),
|
||||
},
|
||||
|
||||
@@ -7,8 +7,10 @@ jest.mock('lib/api/useApiQuery', () => ({
|
||||
|
||||
jest.mock('configs/app', () => ({
|
||||
features: {
|
||||
clusters: {
|
||||
nameServices: {
|
||||
isEnabled: true,
|
||||
ens: { isEnabled: true },
|
||||
clusters: { isEnabled: true },
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import config from 'configs/app';
|
||||
import useApiQuery from 'lib/api/useApiQuery';
|
||||
|
||||
const feature = config.features.nameServices;
|
||||
|
||||
export function useAddressClusters(addressHash: string, isEnabled: boolean = true) {
|
||||
return useApiQuery('clusters:get_clusters_by_address', {
|
||||
queryParams: {
|
||||
@@ -9,7 +11,7 @@ export function useAddressClusters(addressHash: string, isEnabled: boolean = tru
|
||||
}),
|
||||
},
|
||||
queryOptions: {
|
||||
enabled: Boolean(addressHash) && config.features.clusters.isEnabled && isEnabled,
|
||||
enabled: Boolean(addressHash) && feature.isEnabled && feature.clusters.isEnabled && isEnabled,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -82,11 +82,11 @@ export default function useNavItems(): ReturnType {
|
||||
icon: 'verified',
|
||||
isActive: pathname === '/verified-contracts',
|
||||
};
|
||||
const nameLookup = config.features.nameService.isEnabled || config.features.clusters.isEnabled ? {
|
||||
const nameLookup = config.features.nameServices.isEnabled ? {
|
||||
text: 'Name services lookup',
|
||||
nextRoute: { pathname: '/name-domains' as const },
|
||||
nextRoute: { pathname: '/name-services' as const },
|
||||
icon: 'name_services',
|
||||
isActive: pathname === '/name-domains' || pathname === '/name-domains/[name]' || pathname === '/clusters/[name]',
|
||||
isActive: pathname.startsWith('/name-services'),
|
||||
} : null;
|
||||
const validators = config.features.validators.isEnabled ? {
|
||||
text: 'Validators',
|
||||
|
||||
@@ -7,7 +7,7 @@ const CANONICAL_ROUTES: Array<Route['pathname']> = [
|
||||
'/txs',
|
||||
'/ops',
|
||||
'/verified-contracts',
|
||||
'/name-domains',
|
||||
'/name-services',
|
||||
'/withdrawals',
|
||||
'/tokens',
|
||||
'/stats',
|
||||
|
||||
@@ -24,7 +24,6 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
|
||||
'/apps': 'Root page',
|
||||
'/apps/[id]': 'Regular page',
|
||||
'/essential-dapps/[id]': 'Regular page',
|
||||
'/clusters/[name]': 'Regular page',
|
||||
'/stats': 'Root page',
|
||||
'/stats/[id]': 'Regular page',
|
||||
'/uptime': 'Root page',
|
||||
@@ -52,8 +51,9 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
|
||||
'/ops': 'Root page',
|
||||
'/op/[hash]': 'Regular page',
|
||||
'/404': 'Regular page',
|
||||
'/name-domains': 'Root page',
|
||||
'/name-domains/[name]': 'Regular page',
|
||||
'/name-services': 'Root page',
|
||||
'/name-services/domains/[name]': 'Regular page',
|
||||
'/name-services/clusters/[name]': 'Regular page',
|
||||
'/validators': 'Root page',
|
||||
'/validators/[id]': 'Regular page',
|
||||
'/epochs': 'Root page',
|
||||
|
||||
@@ -27,7 +27,6 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
|
||||
'/apps': DEFAULT_TEMPLATE,
|
||||
'/apps/[id]': DEFAULT_TEMPLATE,
|
||||
'/essential-dapps/[id]': DEFAULT_TEMPLATE,
|
||||
'/clusters/[name]': '%cluster_name% cluster | %app_name%',
|
||||
'/stats': DEFAULT_TEMPLATE,
|
||||
'/stats/[id]': DEFAULT_TEMPLATE,
|
||||
'/uptime': DEFAULT_TEMPLATE,
|
||||
@@ -55,8 +54,9 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
|
||||
'/ops': DEFAULT_TEMPLATE,
|
||||
'/op/[hash]': DEFAULT_TEMPLATE,
|
||||
'/404': DEFAULT_TEMPLATE,
|
||||
'/name-domains': DEFAULT_TEMPLATE,
|
||||
'/name-domains/[name]': DEFAULT_TEMPLATE,
|
||||
'/name-services': DEFAULT_TEMPLATE,
|
||||
'/name-services/domains/[name]': DEFAULT_TEMPLATE,
|
||||
'/name-services/clusters/[name]': DEFAULT_TEMPLATE,
|
||||
'/validators': DEFAULT_TEMPLATE,
|
||||
'/validators/[id]': DEFAULT_TEMPLATE,
|
||||
'/epochs': DEFAULT_TEMPLATE,
|
||||
|
||||
@@ -51,8 +51,9 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
|
||||
'/ops': 'User operations on %network_name% - %network_name% explorer',
|
||||
'/op/[hash]': '%network_name% user operation %hash%',
|
||||
'/404': '%network_name% error - page not found',
|
||||
'/name-domains': '%network_name% name domains - %network_name% explorer',
|
||||
'/name-domains/[name]': '%network_name% %name% domain details',
|
||||
'/name-services': '%network_name% name services - %network_name% explorer',
|
||||
'/name-services/domains/[name]': '%network_name% %name% domain details',
|
||||
'/name-services/clusters/[name]': '%network_name% %name% cluster details',
|
||||
'/validators': '%network_name% validators list',
|
||||
'/validators/[id]': '%network_name% validator %id% details',
|
||||
'/epochs': '%network_name% epochs',
|
||||
@@ -66,7 +67,6 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
|
||||
'/interop-messages': '%network_name% interop messages',
|
||||
'/operations': '%network_name% operations',
|
||||
'/operation/[id]': '%network_name% operation %id%',
|
||||
'/clusters/[name]': 'Clusters details for %name%',
|
||||
'/cc/tx/[hash]': '%network_name% cross-chain transaction %hash% details',
|
||||
|
||||
// multichain routes
|
||||
|
||||
@@ -22,7 +22,6 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
|
||||
'/apps': 'DApps',
|
||||
'/apps/[id]': 'DApp',
|
||||
'/essential-dapps/[id]': 'Essential dapps',
|
||||
'/clusters/[name]': 'Cluster details',
|
||||
'/stats': 'Stats',
|
||||
'/stats/[id]': 'Stats chart',
|
||||
'/uptime': 'Uptime',
|
||||
@@ -50,8 +49,9 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
|
||||
'/ops': 'User operations',
|
||||
'/op/[hash]': 'User operation details',
|
||||
'/404': '404',
|
||||
'/name-domains': 'Domains search and resolve',
|
||||
'/name-domains/[name]': 'Domain details',
|
||||
'/name-services': 'Domains search and resolve',
|
||||
'/name-services/domains/[name]': 'Domain details',
|
||||
'/name-services/clusters/[name]': 'Cluster details',
|
||||
'/validators': 'Validators list',
|
||||
'/validators/[id]': 'Validator details',
|
||||
'/epochs': 'Epochs',
|
||||
|
||||
@@ -5,10 +5,12 @@ import useApiQuery from 'lib/api/useApiQuery';
|
||||
|
||||
import useAccount from './useAccount';
|
||||
|
||||
const feature = config.features.nameServices;
|
||||
|
||||
export default function useAccountWithDomain(isEnabled: boolean) {
|
||||
const { address, isConnecting } = useAccount();
|
||||
|
||||
const isQueryEnabled = config.features.nameService.isEnabled && Boolean(address) && Boolean(isEnabled);
|
||||
const isQueryEnabled = feature.isEnabled && feature.ens.isEnabled && Boolean(address) && Boolean(isEnabled);
|
||||
|
||||
const domainQuery = useApiQuery('bens:address_domain', {
|
||||
pathParams: {
|
||||
|
||||
@@ -84,8 +84,18 @@ export const suave: Guard = (chainConfig: typeof config) => async() => {
|
||||
}
|
||||
};
|
||||
|
||||
export const nameService: Guard = (chainConfig: typeof config) => async() => {
|
||||
if (!chainConfig.features.nameService.isEnabled) {
|
||||
export const nameServiceEns: Guard = (chainConfig: typeof config) => async() => {
|
||||
const feature = chainConfig.features.nameServices;
|
||||
if (!feature.isEnabled || !feature.ens.isEnabled) {
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const nameServiceClusters: Guard = (chainConfig: typeof config) => async() => {
|
||||
const feature = chainConfig.features.nameServices;
|
||||
if (!feature.isEnabled || !feature.clusters.isEnabled) {
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
@@ -181,14 +191,6 @@ export const pools: Guard = (chainConfig: typeof config) => async() => {
|
||||
}
|
||||
};
|
||||
|
||||
export const clusters: Guard = (chainConfig: typeof config) => async() => {
|
||||
if (!chainConfig.features.clusters.isEnabled) {
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const zetaChainCCTX: Guard = (chainConfig: typeof config) => async() => {
|
||||
if (!chainConfig.features.zetachain.isEnabled) {
|
||||
return {
|
||||
|
||||
@@ -12,9 +12,9 @@ export const apiDocs = factory([ guards.apiDocs ]);
|
||||
export const csvExport = factory([ guards.csvExport ]);
|
||||
export const stats = factory([ guards.stats ]);
|
||||
export const suave = factory([ guards.suave ]);
|
||||
export const nameDomain = factory([ guards.nameService, guards.clusters ]);
|
||||
export const clusters = factory([ guards.clusters ]);
|
||||
export const nameDomains = factory([ [ guards.nameService, guards.clusters ] ]);
|
||||
export const nameServiceEns = factory([ guards.nameServiceEns ]);
|
||||
export const nameServiceClusters = factory([ guards.nameServiceClusters ]);
|
||||
export const nameServices = factory([ [ guards.nameServiceEns, guards.nameServiceClusters ] ]);
|
||||
export const accounts = factory([ guards.accounts ]);
|
||||
export const accountsLabelSearch = factory([ guards.accountsLabelSearch ]);
|
||||
export const validators = factory([ guards.validators ]);
|
||||
|
||||
@@ -50,7 +50,6 @@ declare module "nextjs-routes" {
|
||||
| DynamicRoute<"/chain/[chain-slug]/token/[hash]/instance/[id]", { "chain-slug": string; "hash": string; "id": string }>
|
||||
| DynamicRoute<"/chain/[chain-slug]/tx/[hash]", { "chain-slug": string; "hash": string }>
|
||||
| StaticRoute<"/chakra">
|
||||
| DynamicRoute<"/clusters/[name]", { "name": string }>
|
||||
| StaticRoute<"/contract-verification">
|
||||
| StaticRoute<"/csv-export">
|
||||
| StaticRoute<"/deposits">
|
||||
@@ -64,8 +63,9 @@ declare module "nextjs-routes" {
|
||||
| StaticRoute<"/interop-messages">
|
||||
| StaticRoute<"/login">
|
||||
| StaticRoute<"/mud-worlds">
|
||||
| DynamicRoute<"/name-domains/[name]", { "name": string }>
|
||||
| StaticRoute<"/name-domains">
|
||||
| DynamicRoute<"/name-services/clusters/[name]", { "name": string }>
|
||||
| DynamicRoute<"/name-services/domains/[name]", { "name": string }>
|
||||
| StaticRoute<"/name-services">
|
||||
| DynamicRoute<"/op/[hash]", { "hash": string }>
|
||||
| DynamicRoute<"/operation/[id]", { "id": string }>
|
||||
| StaticRoute<"/operations">
|
||||
|
||||
@@ -352,7 +352,17 @@ const DEPRECATED_ROUTES = [
|
||||
{
|
||||
source: '/graphiql',
|
||||
destination: '/api-docs?tab=graphql_api',
|
||||
permanent: false,
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/name-domains',
|
||||
destination: '/name-services',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/name-domains/:name',
|
||||
destination: '/name-services/domains/:name',
|
||||
permanent: true,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -54,8 +54,8 @@
|
||||
"@emotion/react": "11.14.0",
|
||||
"@growthbook/growthbook-react": "0.21.0",
|
||||
"@helia/verified-fetch": "2.6.12",
|
||||
"@lifi/widget": "3.25.0",
|
||||
"@lifi/wallet-management": "3.6.1",
|
||||
"@lifi/widget": "3.25.0",
|
||||
"@metamask/post-message-stream": "^7.0.0",
|
||||
"@metamask/providers": "^10.2.1",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
@@ -80,7 +80,7 @@
|
||||
"@scure/base": "1.1.9",
|
||||
"@slise/embed-react": "^2.2.0",
|
||||
"@solana/wallet-adapter-react": "0.15.39",
|
||||
"@specify-sh/sdk": "0.4.1",
|
||||
"@specify-sh/sdk": "0.4.2",
|
||||
"@tanstack/react-query": "5.55.4",
|
||||
"@tanstack/react-query-devtools": "5.55.4",
|
||||
"@types/papaparse": "^5.3.5",
|
||||
|
||||
@@ -9,7 +9,7 @@ const Cluster = dynamic(() => import('ui/pages/Cluster'), { ssr: false });
|
||||
|
||||
const Page: NextPage<Props> = (props: Props) => {
|
||||
return (
|
||||
<PageNextJs pathname="/clusters/[name]" query={ props.query }>
|
||||
<PageNextJs pathname="/name-services/clusters/[name]" query={ props.query }>
|
||||
<Cluster/>
|
||||
</PageNextJs>
|
||||
);
|
||||
@@ -17,4 +17,4 @@ const Page: NextPage<Props> = (props: Props) => {
|
||||
|
||||
export default Page;
|
||||
|
||||
export { clusters as getServerSideProps } from 'nextjs/getServerSideProps/main';
|
||||
export { nameServiceClusters as getServerSideProps } from 'nextjs/getServerSideProps/main';
|
||||
@@ -9,7 +9,7 @@ const NameDomain = dynamic(() => import('ui/pages/NameDomain'), { ssr: false });
|
||||
|
||||
const Page: NextPage<Props> = (props: Props) => {
|
||||
return (
|
||||
<PageNextJs pathname="/name-domains/[name]" query={ props.query }>
|
||||
<PageNextJs pathname="/name-services/domains/[name]" query={ props.query }>
|
||||
<NameDomain/>
|
||||
</PageNextJs>
|
||||
);
|
||||
@@ -17,4 +17,4 @@ const Page: NextPage<Props> = (props: Props) => {
|
||||
|
||||
export default Page;
|
||||
|
||||
export { nameDomain as getServerSideProps } from 'nextjs/getServerSideProps/main';
|
||||
export { nameServiceEns as getServerSideProps } from 'nextjs/getServerSideProps/main';
|
||||
@@ -4,16 +4,16 @@ import React from 'react';
|
||||
|
||||
import PageNextJs from 'nextjs/PageNextJs';
|
||||
|
||||
const NameDomains = dynamic(() => import('ui/pages/NameDomains'), { ssr: false });
|
||||
const NameServices = dynamic(() => import('ui/pages/NameServices'), { ssr: false });
|
||||
|
||||
const Page: NextPage = () => {
|
||||
return (
|
||||
<PageNextJs pathname="/name-domains">
|
||||
<NameDomains/>
|
||||
<PageNextJs pathname="/name-services">
|
||||
<NameServices/>
|
||||
</PageNextJs>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
export { nameDomains as getServerSideProps } from 'nextjs/getServerSideProps/main';
|
||||
export { nameServices as getServerSideProps } from 'nextjs/getServerSideProps/main';
|
||||
@@ -112,7 +112,6 @@ export const ENVS_MAP: Record<string, Array<[string, string]>> = {
|
||||
],
|
||||
celo: [
|
||||
[ 'NEXT_PUBLIC_CELO_ENABLED', 'true' ],
|
||||
[ 'NEXT_PUBLIC_CELO_NATIVE_TOKEN_ADDRESS', '0x471EcE3750Da237f93B8E339c536989b8978a438' ],
|
||||
],
|
||||
opSuperchain: [
|
||||
[ 'NEXT_PUBLIC_OP_SUPERCHAIN_ENABLED', 'true' ],
|
||||
|
||||
@@ -99,16 +99,15 @@ const ChartMenu = ({
|
||||
}, [ pngBackgroundColor, title, chartRef ]);
|
||||
|
||||
const handleSVGSavingClick = React.useCallback(() => {
|
||||
charts.forEach((chart) => {
|
||||
const headerRows = [
|
||||
'Date', 'Value',
|
||||
];
|
||||
const dataRows = chart.items.map((item) => [
|
||||
dayjs(item.date).format('YYYY-MM-DD'), String(item.value),
|
||||
]);
|
||||
saveAsCsv(headerRows, dataRows, `${ chart.name } (Blockscout stats)`);
|
||||
});
|
||||
}, [ charts ]);
|
||||
const headerRows = [
|
||||
'Date', ...charts.map((chart) => chart.name),
|
||||
];
|
||||
const dataRows = charts[0].items.map((item, index) => [
|
||||
item.dateLabel ?? dayjs(item.date).format('YYYY-MM-DD'),
|
||||
...charts.map((chart) => String(chart.items[index].value)),
|
||||
]);
|
||||
saveAsCsv(headerRows, dataRows, `${ title } (Blockscout stats)`);
|
||||
}, [ charts, title ]);
|
||||
|
||||
// TS thinks window.navigator.share can't be undefined, but it can
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
||||
@@ -51,7 +51,7 @@ export type TokenTotal = Erc20TotalPayload | Erc721TotalPayload | Erc1155TotalPa
|
||||
|
||||
interface TokenTransferBase {
|
||||
type: 'token_transfer' | 'token_burning' | 'token_spawning' | 'token_minting';
|
||||
transaction_hash: string;
|
||||
transaction_hash: string | null;
|
||||
from: AddressParam;
|
||||
to: AddressParam;
|
||||
timestamp: string;
|
||||
|
||||
@@ -272,8 +272,7 @@ test.describe('update balances via socket', () => {
|
||||
|
||||
test('native token', async({ render, mockEnvs }) => {
|
||||
await mockEnvs([
|
||||
[ 'NEXT_PUBLIC_CELO_ENABLED', 'true' ],
|
||||
[ 'NEXT_PUBLIC_CELO_NATIVE_TOKEN_ADDRESS', tokenInfo.tokenInfoERC20c.address_hash ],
|
||||
[ 'NEXT_PUBLIC_VIEWS_ADDRESS_NATIVE_TOKEN_ADDRESS', tokenInfo.tokenInfoERC20c.address_hash ],
|
||||
]);
|
||||
const hooksConfig = {
|
||||
router: {
|
||||
|
||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 23 KiB |
@@ -99,7 +99,7 @@ const AddressClusters = ({ query, addressHash }: Props) => {
|
||||
</div>
|
||||
{ showMoreLink && (
|
||||
<Link
|
||||
href={ route({ pathname: '/name-domains', query: { q: addressHash, tab: 'directories' } }) }
|
||||
href={ route({ pathname: '/name-services', query: { q: addressHash, tab: 'directories' } }) }
|
||||
>
|
||||
<span>More results</span>
|
||||
<chakra.span color="text.secondary"> ({ totalRecords })</chakra.span>
|
||||
|
||||
@@ -127,7 +127,7 @@ const AddressEnsDomains = ({ query, addressHash, mainDomainName }: Props) => {
|
||||
) }
|
||||
{ (ownedDomains.length > 9 || resolvedDomains.length > 9) && (
|
||||
<Link
|
||||
href={ route({ pathname: '/name-domains', query: { owned_by: 'true', resolved_to: 'true', address: addressHash } }) }
|
||||
href={ route({ pathname: '/name-services', query: { tab: 'domains', owned_by: 'true', resolved_to: 'true', address: addressHash } }) }
|
||||
>
|
||||
<span> More results</span>
|
||||
<chakra.span color="text.secondary"> ({ totalRecords })</chakra.span>
|
||||
|
||||
@@ -129,8 +129,7 @@ test('long values', async({ render, page, mockApiResponse }) => {
|
||||
|
||||
test('native token', async({ render, mockEnvs, page }) => {
|
||||
await mockEnvs([
|
||||
[ 'NEXT_PUBLIC_CELO_ENABLED', 'true' ],
|
||||
[ 'NEXT_PUBLIC_CELO_NATIVE_TOKEN_ADDRESS', tokenInfoERC20c.address_hash ],
|
||||
[ 'NEXT_PUBLIC_VIEWS_ADDRESS_NATIVE_TOKEN_ADDRESS', tokenInfoERC20c.address_hash ],
|
||||
]);
|
||||
|
||||
await render(
|
||||
|
||||
@@ -13,15 +13,14 @@ import TruncatedValue from 'ui/shared/TruncatedValue';
|
||||
|
||||
import type { TokenEnhancedData } from '../utils/tokenUtils';
|
||||
|
||||
const celoFeature = config.features.celo;
|
||||
|
||||
interface Props {
|
||||
data: TokenEnhancedData;
|
||||
}
|
||||
|
||||
const TokenSelectItem = ({ data }: Props) => {
|
||||
|
||||
const isNativeToken = celoFeature.isEnabled && data.token.address_hash.toLowerCase() === celoFeature.nativeTokenAddress?.toLowerCase();
|
||||
const isNativeToken = config.UI.views.address.nativeTokenAddress &&
|
||||
data.token.address_hash.toLowerCase() === config.UI.views.address.nativeTokenAddress.toLowerCase();
|
||||
|
||||
const secondRow = (() => {
|
||||
switch (data.token.type) {
|
||||
|
||||
@@ -12,8 +12,6 @@ import AddressEntity from 'ui/shared/entities/address/AddressEntity';
|
||||
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
|
||||
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
|
||||
|
||||
const celoFeature = config.features.celo;
|
||||
|
||||
type Props = AddressTokenBalance & { isLoading: boolean };
|
||||
|
||||
const ERC20TokensListItem = ({ token, value, isLoading }: Props) => {
|
||||
@@ -23,7 +21,8 @@ const ERC20TokensListItem = ({ token, value, isLoading }: Props) => {
|
||||
usd: tokenValue,
|
||||
} = getCurrencyValue({ value: value, exchangeRate: token.exchange_rate, decimals: token.decimals, accuracy: 8, accuracyUsd: 2 });
|
||||
|
||||
const isNativeToken = celoFeature.isEnabled && token.address_hash.toLowerCase() === celoFeature.nativeTokenAddress?.toLowerCase();
|
||||
const isNativeToken = config.UI.views.address.nativeTokenAddress &&
|
||||
token.address_hash.toLowerCase() === config.UI.views.address.nativeTokenAddress.toLowerCase();
|
||||
|
||||
return (
|
||||
<ListItemMobile rowGap={ 2 }>
|
||||
|
||||
@@ -14,8 +14,6 @@ import TokenEntity from 'ui/shared/entities/token/TokenEntity';
|
||||
|
||||
type Props = AddressTokenBalance & { isLoading: boolean };
|
||||
|
||||
const celoFeature = config.features.celo;
|
||||
|
||||
const ERC20TokensTableItem = ({
|
||||
token,
|
||||
value,
|
||||
@@ -27,7 +25,8 @@ const ERC20TokensTableItem = ({
|
||||
usd: tokenValue,
|
||||
} = getCurrencyValue({ value: value, exchangeRate: token.exchange_rate, decimals: token.decimals, accuracy: 8, accuracyUsd: 2 });
|
||||
|
||||
const isNativeToken = celoFeature.isEnabled && token.address_hash.toLowerCase() === celoFeature.nativeTokenAddress?.toLowerCase();
|
||||
const isNativeToken = config.UI.views.address.nativeTokenAddress &&
|
||||
token.address_hash.toLowerCase() === config.UI.views.address.nativeTokenAddress.toLowerCase();
|
||||
|
||||
return (
|
||||
<TableRow role="group" >
|
||||
|
||||
@@ -7,10 +7,9 @@ import config from 'configs/app';
|
||||
import sumBnReducer from 'lib/bigint/sumBnReducer';
|
||||
import { ZERO } from 'toolkit/utils/consts';
|
||||
|
||||
const celoFeature = config.features.celo;
|
||||
|
||||
const isNativeToken = (token: TokenEnhancedData) =>
|
||||
celoFeature.isEnabled && token.token.address_hash.toLowerCase() === celoFeature.nativeTokenAddress?.toLowerCase();
|
||||
config.UI.views.address.nativeTokenAddress &&
|
||||
token.token.address_hash.toLowerCase() === config.UI.views.address.nativeTokenAddress.toLowerCase();
|
||||
|
||||
export type TokenEnhancedData = AddressTokenBalance & {
|
||||
usd?: BigNumber ;
|
||||
|
||||
@@ -6,10 +6,12 @@ import useApiQuery from 'lib/api/useApiQuery';
|
||||
|
||||
const DOMAIN_NAME_REGEXP = /.\../;
|
||||
|
||||
const feature = config.features.nameServices;
|
||||
|
||||
export default function useCheckDomainNameParam(hashOrDomainName: string) {
|
||||
const router = useRouter();
|
||||
const maybeDomainName = DOMAIN_NAME_REGEXP.test(hashOrDomainName);
|
||||
const isQueryEnabled = config.features.nameService.isEnabled && maybeDomainName;
|
||||
const isQueryEnabled = feature.isEnabled && feature.ens.isEnabled && maybeDomainName;
|
||||
const [ isLoading, setIsLoading ] = React.useState(isQueryEnabled);
|
||||
|
||||
const domainLookupQuery = useApiQuery('bens:domains_lookup', {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Box, Flex } from '@chakra-ui/react';
|
||||
import type { AllowanceType } from 'types/client/revoke';
|
||||
import type { ChainConfig } from 'types/multichain';
|
||||
|
||||
import EmptySearchResult from 'ui/shared/EmptySearchResult';
|
||||
import DataListDisplay from 'ui/shared/DataListDisplay';
|
||||
|
||||
import ApprovalsListItem from './ApprovalsListItem';
|
||||
import ApprovalsTable from './ApprovalsTable';
|
||||
@@ -23,25 +23,20 @@ export default function Approvals({
|
||||
isAddressMatch,
|
||||
hideApproval,
|
||||
}: Props) {
|
||||
return (
|
||||
const content = (
|
||||
<>
|
||||
<Box hideFrom="lg">
|
||||
<Flex flexDirection="column">
|
||||
{ approvals.map((approval, index) => (
|
||||
<ApprovalsListItem
|
||||
key={ index }
|
||||
selectedChain={ selectedChain }
|
||||
approval={ approval }
|
||||
isLoading={ isLoading }
|
||||
isAddressMatch={ isAddressMatch }
|
||||
hideApproval={ hideApproval }
|
||||
/>
|
||||
)) }
|
||||
{ !isLoading && !approvals.length && (
|
||||
<EmptySearchResult text="No approvals found"/>
|
||||
) }
|
||||
</Flex>
|
||||
</Box>
|
||||
<Flex hideFrom="lg" flexDirection="column">
|
||||
{ approvals.map((approval, index) => (
|
||||
<ApprovalsListItem
|
||||
key={ index }
|
||||
selectedChain={ selectedChain }
|
||||
approval={ approval }
|
||||
isLoading={ isLoading }
|
||||
isAddressMatch={ isAddressMatch }
|
||||
hideApproval={ hideApproval }
|
||||
/>
|
||||
)) }
|
||||
</Flex>
|
||||
<Box hideBelow="lg">
|
||||
<ApprovalsTable
|
||||
selectedChain={ selectedChain }
|
||||
@@ -53,4 +48,17 @@ export default function Approvals({
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<DataListDisplay
|
||||
itemsNum={ approvals.length }
|
||||
isError={ false }
|
||||
filterProps={{
|
||||
emptyFilteredText: 'No approvals found',
|
||||
hasActiveFilters: true,
|
||||
}}
|
||||
>
|
||||
{ content }
|
||||
</DataListDisplay>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -72,14 +72,16 @@ export default function ApprovalsListItem({
|
||||
}}
|
||||
isLoading={ isLoading }
|
||||
noCopy
|
||||
noLink
|
||||
jointSymbol
|
||||
href={ selectedChain?.config.app.baseUrl + route({ pathname: '/token/[hash]', query: { hash: approval.address } }) }
|
||||
link={{ noIcon: true, external: true }}
|
||||
/>
|
||||
<AddressEntity
|
||||
address={{ hash: approval.address }}
|
||||
truncation="constant"
|
||||
noIcon
|
||||
isLoading={ isLoading }
|
||||
href={ selectedChain?.config.app.baseUrl + route({ pathname: '/token/[hash]', query: { hash: approval.address } }) }
|
||||
href={ selectedChain?.config.app.baseUrl + route({ pathname: '/address/[hash]', query: { hash: approval.address } }) }
|
||||
link={{ noIcon: true, external: true }}
|
||||
/>
|
||||
</ListItemMobileGrid.Value>
|
||||
|
||||
@@ -4,7 +4,6 @@ import type { AllowanceType } from 'types/client/revoke';
|
||||
import type { ChainConfig } from 'types/multichain';
|
||||
|
||||
import { TableBody, TableColumnHeader, TableHeaderSticky, TableRoot, TableRow } from 'toolkit/chakra/table';
|
||||
import EmptySearchResult from 'ui/shared/EmptySearchResult';
|
||||
import TimeFormatToggle from 'ui/shared/time/TimeFormatToggle';
|
||||
|
||||
import ApprovalsTableItem from './ApprovalsTableItem';
|
||||
@@ -25,42 +24,37 @@ export default function ApprovalsTable({
|
||||
hideApproval,
|
||||
}: Props) {
|
||||
return (
|
||||
<>
|
||||
<TableRoot>
|
||||
<TableHeaderSticky top={ 136 }>
|
||||
<TableRow>
|
||||
<TableColumnHeader w="30%">Token</TableColumnHeader>
|
||||
<TableColumnHeader w="15%">Approved spender</TableColumnHeader>
|
||||
<TableColumnHeader w="20%" isNumeric>
|
||||
Approved amount
|
||||
</TableColumnHeader>
|
||||
<TableColumnHeader w="17%" isNumeric>
|
||||
Value at risk
|
||||
</TableColumnHeader>
|
||||
<TableColumnHeader w={ isAddressMatch ? '30px' : '50px' }/>
|
||||
<TableColumnHeader w="18%">
|
||||
Last updated
|
||||
<TimeFormatToggle/>
|
||||
</TableColumnHeader>
|
||||
{ isAddressMatch && <TableColumnHeader w="95px" isNumeric/> }
|
||||
</TableRow>
|
||||
</TableHeaderSticky>
|
||||
<TableBody>
|
||||
{ approvals.map((approval, index) => (
|
||||
<ApprovalsTableItem
|
||||
key={ index }
|
||||
selectedChain={ selectedChain }
|
||||
approval={ approval }
|
||||
isLoading={ isLoading }
|
||||
isAddressMatch={ isAddressMatch }
|
||||
hideApproval={ hideApproval }
|
||||
/>
|
||||
)) }
|
||||
</TableBody>
|
||||
</TableRoot>
|
||||
{ !isLoading && !approvals.length && (
|
||||
<EmptySearchResult text="No approvals found"/>
|
||||
) }
|
||||
</>
|
||||
<TableRoot>
|
||||
<TableHeaderSticky top={ 136 }>
|
||||
<TableRow>
|
||||
<TableColumnHeader w="30%">Token</TableColumnHeader>
|
||||
<TableColumnHeader w="15%">Approved spender</TableColumnHeader>
|
||||
<TableColumnHeader w="20%" isNumeric>
|
||||
Approved amount
|
||||
</TableColumnHeader>
|
||||
<TableColumnHeader w="17%" isNumeric>
|
||||
Value at risk
|
||||
</TableColumnHeader>
|
||||
<TableColumnHeader w={ isAddressMatch ? '30px' : '50px' }/>
|
||||
<TableColumnHeader w="18%">
|
||||
Last updated
|
||||
<TimeFormatToggle/>
|
||||
</TableColumnHeader>
|
||||
{ isAddressMatch && <TableColumnHeader w="95px" isNumeric/> }
|
||||
</TableRow>
|
||||
</TableHeaderSticky>
|
||||
<TableBody>
|
||||
{ approvals.map((approval, index) => (
|
||||
<ApprovalsTableItem
|
||||
key={ index }
|
||||
selectedChain={ selectedChain }
|
||||
approval={ approval }
|
||||
isLoading={ isLoading }
|
||||
isAddressMatch={ isAddressMatch }
|
||||
hideApproval={ hideApproval }
|
||||
/>
|
||||
)) }
|
||||
</TableBody>
|
||||
</TableRoot>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ export default function ApprovalsTableItem({
|
||||
truncation="constant"
|
||||
noIcon
|
||||
isLoading={ isLoading }
|
||||
href={ selectedChain?.config.app.baseUrl + route({ pathname: '/token/[hash]', query: { hash: approval.address } }) }
|
||||
href={ selectedChain?.config.app.baseUrl + route({ pathname: '/address/[hash]', query: { hash: approval.address } }) }
|
||||
link={{ variant: 'secondary', noIcon: true, external: true }}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 86 KiB |
@@ -121,7 +121,10 @@ const NameDomainDetails = ({ query }: Props) => {
|
||||
<Link
|
||||
flexShrink={ 0 }
|
||||
display="inline-flex"
|
||||
href={ route({ pathname: '/name-domains', query: { owned_by: 'true', resolved_to: 'true', address: query.data.registrant.hash } }) }
|
||||
href={ route({
|
||||
pathname: '/name-services',
|
||||
query: { tab: 'domains', owned_by: 'true', resolved_to: 'true', address: query.data.registrant.hash },
|
||||
}) }
|
||||
>
|
||||
<IconSvg name="search" boxSize={ 5 } isLoading={ isLoading }/>
|
||||
</Link>
|
||||
@@ -150,7 +153,10 @@ const NameDomainDetails = ({ query }: Props) => {
|
||||
<Link
|
||||
flexShrink={ 0 }
|
||||
display="inline-flex"
|
||||
href={ route({ pathname: '/name-domains', query: { owned_by: 'true', resolved_to: 'true', address: query.data.owner.hash } }) }
|
||||
href={ route({
|
||||
pathname: '/name-services',
|
||||
query: { tab: 'domains', owned_by: 'true', resolved_to: 'true', address: query.data.owner.hash },
|
||||
}) }
|
||||
>
|
||||
<IconSvg name="search" boxSize={ 5 } isLoading={ isLoading }/>
|
||||
</Link>
|
||||
@@ -179,7 +185,10 @@ const NameDomainDetails = ({ query }: Props) => {
|
||||
<Link
|
||||
flexShrink={ 0 }
|
||||
display="inline-flex"
|
||||
href={ route({ pathname: '/name-domains', query: { owned_by: 'true', resolved_to: 'true', address: query.data.wrapped_owner.hash } }) }
|
||||
href={ route({
|
||||
pathname: '/name-services',
|
||||
query: { tab: 'domains', owned_by: 'true', resolved_to: 'true', address: query.data.wrapped_owner.hash },
|
||||
}) }
|
||||
>
|
||||
<IconSvg name="search" boxSize={ 5 } isLoading={ isLoading }/>
|
||||
</Link>
|
||||
|
||||
@@ -28,6 +28,8 @@ const ClustersDirectoryTableItem = ({ item, isLoading, isClusterDetailsLoading }
|
||||
isLoading={ isLoading }
|
||||
fontWeight={ 500 }
|
||||
noLink={ !isEvmAddress(item.owner) }
|
||||
w="fit-content"
|
||||
maxW="100%"
|
||||
/>
|
||||
) }
|
||||
{ !item.owner && <Skeleton loading={ isLoading }>—</Skeleton> }
|
||||
@@ -23,7 +23,7 @@ import NameDomainsTable from './NameDomainsTable';
|
||||
import type { Sort, SortField } from './utils';
|
||||
import { SORT_OPTIONS, getNextSortValue } from './utils';
|
||||
|
||||
const NameDomainsContent = () => {
|
||||
const NameDomains = () => {
|
||||
const router = useRouter();
|
||||
|
||||
const q = getQueryParamString(router.query.name) || getQueryParamString(router.query.address);
|
||||
@@ -244,4 +244,4 @@ const NameDomainsContent = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default NameDomainsContent;
|
||||
export default NameDomains;
|
||||
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
@@ -68,6 +68,7 @@ const PREDEFINED_TAG_PRIORITY = 100;
|
||||
const txInterpretation = config.features.txInterpretation;
|
||||
const addressProfileAPIFeature = config.features.addressProfileAPI;
|
||||
const xScoreFeature = config.features.xStarScore;
|
||||
const nameServicesFeature = config.features.nameServices;
|
||||
|
||||
const AddressPageContent = () => {
|
||||
const router = useRouter();
|
||||
@@ -126,7 +127,7 @@ const AddressPageContent = () => {
|
||||
order: 'ASC',
|
||||
},
|
||||
queryOptions: {
|
||||
enabled: Boolean(hash) && config.features.nameService.isEnabled,
|
||||
enabled: Boolean(hash) && nameServicesFeature.isEnabled && nameServicesFeature.ens.isEnabled,
|
||||
},
|
||||
});
|
||||
const addressMainDomain = !addressQuery.isPlaceholderData ?
|
||||
@@ -166,7 +167,7 @@ const AddressPageContent = () => {
|
||||
isEnabled: !countersQuery.isPlaceholderData && !countersQuery.isDegradedData,
|
||||
});
|
||||
|
||||
const isSafeAddress = useIsSafeAddress(!addressQuery.isPlaceholderData && addressQuery.data?.is_contract ? hash : undefined);
|
||||
const isSafeAddress = useIsSafeAddress(!addressQuery.isPlaceholderData && addressQuery.data?.is_contract ? addressQuery.data.hash : undefined);
|
||||
|
||||
const xStarQuery = useFetchXStarScore({ hash });
|
||||
|
||||
@@ -450,9 +451,9 @@ const AddressPageContent = () => {
|
||||
<HStack ml="auto" gap={ 2 }/>
|
||||
{ !isLoading && addressQuery.data?.is_contract && addressQuery.data?.is_verified && config.UI.views.address.solidityscanEnabled &&
|
||||
<SolidityscanReport hash={ hash }/> }
|
||||
{ !isLoading && config.features.nameService.isEnabled &&
|
||||
{ !isLoading && nameServicesFeature.isEnabled && nameServicesFeature.ens.isEnabled &&
|
||||
<AddressEnsDomains query={ addressEnsDomainsQuery } addressHash={ hash } mainDomainName={ addressQuery.data?.ens_domain_name }/> }
|
||||
{ !isLoading && config.features.clusters.isEnabled &&
|
||||
{ !isLoading && nameServicesFeature.isEnabled && nameServicesFeature.clusters.isEnabled &&
|
||||
<AddressClusters query={ addressClustersQuery } addressHash={ hash }/> }
|
||||
<NetworkExplorers type="address" pathParam={ hash }/>
|
||||
</Flex>
|
||||
|
||||
@@ -69,7 +69,10 @@ const NameDomain = () => {
|
||||
<Link
|
||||
flexShrink={ 0 }
|
||||
display="inline-flex"
|
||||
href={ route({ pathname: '/name-domains', query: { owned_by: 'true', resolved_to: 'true', address: infoQuery.data?.resolved_address?.hash } }) }
|
||||
href={ route({
|
||||
pathname: '/name-services',
|
||||
query: { tab: 'domains', owned_by: 'true', resolved_to: 'true', address: infoQuery.data?.resolved_address?.hash },
|
||||
}) }
|
||||
>
|
||||
<IconSvg name="search" boxSize={ 5 } isLoading={ isLoading }/>
|
||||
</Link>
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import type { TabItemRegular } from 'toolkit/components/AdaptiveTabs/types';
|
||||
|
||||
import config from 'configs/app';
|
||||
import RoutedTabs from 'toolkit/components/RoutedTabs/RoutedTabs';
|
||||
import Clusters from 'ui/nameDomains/directories/Clusters';
|
||||
import NameDomainsContent from 'ui/nameDomains/domains/NameDomainsContent';
|
||||
import PageTitle from 'ui/shared/Page/PageTitle';
|
||||
|
||||
const NameDomains = () => {
|
||||
const tabs: Array<TabItemRegular> = [
|
||||
config.features.nameService.isEnabled && { id: 'domains', title: 'Domains', component: <NameDomainsContent/> },
|
||||
config.features.clusters.isEnabled && { id: 'directories', title: 'Directories', component: <Clusters/> },
|
||||
].filter(Boolean);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageTitle
|
||||
title={ config.meta.seo.enhancedDataEnabled ? `${ config.chain.name } name domains` : 'Name services lookup' }
|
||||
withTextAd
|
||||
/>
|
||||
<RoutedTabs tabs={ tabs }/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NameDomains;
|
||||
@@ -7,7 +7,7 @@ import * as ensDomainMock from 'mocks/ens/domain';
|
||||
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
|
||||
import { test, expect, devices } from 'playwright/lib';
|
||||
|
||||
import NameDomains from './NameDomains';
|
||||
import NameServices from './NameServices';
|
||||
|
||||
test.describe('domains', () => {
|
||||
|
||||
@@ -49,7 +49,7 @@ test.describe('domains', () => {
|
||||
|
||||
test('default view', async({ render }) => {
|
||||
test.slow();
|
||||
const component = await render(<NameDomains/>, { hooksConfig });
|
||||
const component = await render(<NameServices/>, { hooksConfig });
|
||||
await expect(component).toHaveScreenshot({ timeout: 10_000 });
|
||||
});
|
||||
|
||||
@@ -57,13 +57,13 @@ test.describe('domains', () => {
|
||||
test.use({ viewport: devices['iPhone 13 Pro'].viewport });
|
||||
|
||||
test('default view', async({ render }) => {
|
||||
const component = await render(<NameDomains/>, { hooksConfig });
|
||||
const component = await render(<NameServices/>, { hooksConfig });
|
||||
await expect(component).toHaveScreenshot({ timeout: 10_000 });
|
||||
});
|
||||
});
|
||||
|
||||
test('filters', async({ render, page }) => {
|
||||
const component = await render(<NameDomains/>, { hooksConfig });
|
||||
const component = await render(<NameServices/>, { hooksConfig });
|
||||
|
||||
await component.getByRole('button', { name: 'Filter' }).click();
|
||||
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 250, height: 500 } });
|
||||
@@ -120,12 +120,12 @@ test.describe('directories', () => {
|
||||
});
|
||||
|
||||
test('directory view', async({ render }) => {
|
||||
const component = await render(<NameDomains/>, { hooksConfig: hooksConfig.directories });
|
||||
const component = await render(<NameServices/>, { hooksConfig: hooksConfig.directories });
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test('leaderboard view', async({ render }) => {
|
||||
const component = await render(<NameDomains/>, { hooksConfig: hooksConfig.leaderboard });
|
||||
const component = await render(<NameServices/>, { hooksConfig: hooksConfig.leaderboard });
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
@@ -133,12 +133,12 @@ test.describe('directories', () => {
|
||||
test.use({ viewport: devices['iPhone 13 Pro'].viewport });
|
||||
|
||||
test('directory view', async({ render }) => {
|
||||
const component = await render(<NameDomains/>, { hooksConfig: hooksConfig.directories });
|
||||
const component = await render(<NameServices/>, { hooksConfig: hooksConfig.directories });
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test('leaderboard view', async({ render }) => {
|
||||
const component = await render(<NameDomains/>, { hooksConfig: hooksConfig.leaderboard });
|
||||
const component = await render(<NameServices/>, { hooksConfig: hooksConfig.leaderboard });
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
|
||||
import type { TabItemRegular } from 'toolkit/components/AdaptiveTabs/types';
|
||||
|
||||
import config from 'configs/app';
|
||||
import RoutedTabs from 'toolkit/components/RoutedTabs/RoutedTabs';
|
||||
import Clusters from 'ui/nameServices/directories/Clusters';
|
||||
import NameDomains from 'ui/nameServices/domains/NameDomains';
|
||||
import PageTitle from 'ui/shared/Page/PageTitle';
|
||||
|
||||
const feature = config.features.nameServices;
|
||||
|
||||
const NameServices = () => {
|
||||
const tabs: Array<TabItemRegular> = [
|
||||
feature.isEnabled && feature.ens.isEnabled && { id: 'domains', title: 'Domains', component: <NameDomains/> },
|
||||
feature.isEnabled && feature.clusters.isEnabled && { id: 'directories', title: 'Directories', component: <Clusters/> },
|
||||
].filter(Boolean);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageTitle
|
||||
title={ config.meta.seo.enhancedDataEnabled ? `${ config.chain.name } name services` : 'Name services lookup' }
|
||||
withTextAd
|
||||
/>
|
||||
<RoutedTabs tabs={ tabs }/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NameServices;
|
||||
@@ -32,6 +32,8 @@ import HeaderMobile from 'ui/snippets/header/HeaderMobile';
|
||||
import SearchBarSuggestBlockCountdown from 'ui/snippets/searchBar/SearchBarSuggest/SearchBarSuggestBlockCountdown';
|
||||
import useSearchQuery from 'ui/snippets/searchBar/useSearchQuery';
|
||||
|
||||
const nameServicesFeature = config.features.nameServices;
|
||||
|
||||
const SearchResultsPageContent = () => {
|
||||
const router = useRouter();
|
||||
const withRedirectCheck = getQueryParamString(router.query.redirect) === 'true';
|
||||
@@ -114,7 +116,7 @@ const SearchResultsPageContent = () => {
|
||||
if (!config.features.dataAvailability.isEnabled && item.type === 'blob') {
|
||||
return false;
|
||||
}
|
||||
if (!config.features.nameService.isEnabled && item.type === 'ens_domain') {
|
||||
if ((!nameServicesFeature.isEnabled || !nameServicesFeature.ens.isEnabled) && item.type === 'ens_domain') {
|
||||
return false;
|
||||
}
|
||||
if (!config.features.tac.isEnabled && item.type === 'tac_operation') {
|
||||
|
||||
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 70 KiB |
@@ -56,18 +56,23 @@ const PublicTagsSubmitForm = ({ config, userInfo, onSubmitResult }: Props) => {
|
||||
}, [ router ]);
|
||||
|
||||
const onFormSubmit: SubmitHandler<FormFields> = React.useCallback(async(data) => {
|
||||
|
||||
const token = await recaptcha.executeAsync();
|
||||
|
||||
if (!token) {
|
||||
throw new Error('ReCaptcha is not solved');
|
||||
}
|
||||
|
||||
const requestsBody = convertFormDataToRequestsBody(data);
|
||||
|
||||
const result = await Promise.all(requestsBody.map(async(body) => {
|
||||
return recaptcha.executeAsync()
|
||||
.then(() => {
|
||||
return apiFetch<'admin:public_tag_application', unknown, { message: string }>('admin:public_tag_application', {
|
||||
pathParams: { chainId: appConfig.chain.id },
|
||||
fetchParams: {
|
||||
method: 'POST',
|
||||
body: { submission: body },
|
||||
},
|
||||
});
|
||||
return apiFetch<'admin:public_tag_application', unknown, { message: string }>(
|
||||
'admin:public_tag_application', {
|
||||
pathParams: { chainId: appConfig.chain.id },
|
||||
fetchParams: {
|
||||
method: 'POST',
|
||||
body: { submission: body },
|
||||
},
|
||||
})
|
||||
.then(() => ({ error: null, payload: body }))
|
||||
.catch((error: unknown) => {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import { getFeaturePayload } from 'configs/app/features/types';
|
||||
|
||||
import config from 'configs/app';
|
||||
import { Image } from 'toolkit/chakra/image';
|
||||
import type { ImageProps } from 'toolkit/chakra/image';
|
||||
@@ -12,6 +10,8 @@ interface ClusterIconProps extends Omit<ImageProps, 'src' | 'alt'> {
|
||||
clusterName: string;
|
||||
}
|
||||
|
||||
const nameServicesFeature = config.features.nameServices;
|
||||
|
||||
const ClusterIcon = ({
|
||||
clusterName,
|
||||
boxSize = 5,
|
||||
@@ -20,7 +20,7 @@ const ClusterIcon = ({
|
||||
flexShrink = 0,
|
||||
...imageProps
|
||||
}: ClusterIconProps) => {
|
||||
const clustersFeature = getFeaturePayload(config.features.clusters);
|
||||
const clustersFeature = nameServicesFeature.isEnabled && nameServicesFeature.clusters.isEnabled ? nameServicesFeature.clusters : undefined;
|
||||
|
||||
const fallbackElement = (
|
||||
<Box
|
||||
|
||||
@@ -72,15 +72,17 @@ const TokenTransferListItem = ({
|
||||
{ total && 'token_id' in total && total.token_id !== null && token && (
|
||||
<NftEntity hash={ token.address_hash } id={ total.token_id } instance={ total.token_instance } isLoading={ isLoading }/>
|
||||
) }
|
||||
{ showTxInfo && txHash && (
|
||||
{ showTxInfo && (
|
||||
<Flex justifyContent="space-between" alignItems="center" lineHeight="24px" width="100%">
|
||||
<TxEntity
|
||||
isLoading={ isLoading }
|
||||
hash={ txHash }
|
||||
truncation="constant_long"
|
||||
fontWeight="700"
|
||||
chain={ chainData }
|
||||
/>
|
||||
{ txHash && (
|
||||
<TxEntity
|
||||
isLoading={ isLoading }
|
||||
hash={ txHash }
|
||||
truncation="constant_long"
|
||||
fontWeight="700"
|
||||
chain={ chainData }
|
||||
/>
|
||||
) }
|
||||
<TimeWithTooltip
|
||||
timestamp={ timestamp }
|
||||
enableIncrement={ enableTimeIncrement }
|
||||
|
||||
@@ -50,11 +50,17 @@ const TokenTransferTableItem = ({
|
||||
|
||||
return (
|
||||
<TableRow alignItems="top">
|
||||
{ showTxInfo && txHash && (
|
||||
{ showTxInfo && (
|
||||
<TableCell>
|
||||
<Box my="3px" textAlign="center">
|
||||
<TxAdditionalInfo hash={ txHash } isLoading={ isLoading }/>
|
||||
</Box>
|
||||
{
|
||||
txHash ? (
|
||||
<Box my="3px" textAlign="center">
|
||||
<TxAdditionalInfo hash={ txHash } isLoading={ isLoading }/>
|
||||
</Box>
|
||||
) : (
|
||||
<div/>
|
||||
)
|
||||
}
|
||||
</TableCell>
|
||||
) }
|
||||
{ chainData && (
|
||||
@@ -89,16 +95,20 @@ const TokenTransferTableItem = ({
|
||||
/>
|
||||
) }
|
||||
</TableCell>
|
||||
{ showTxInfo && txHash && (
|
||||
{ showTxInfo && (
|
||||
<TableCell>
|
||||
<TxEntity
|
||||
hash={ txHash }
|
||||
isLoading={ isLoading }
|
||||
fontWeight={ 600 }
|
||||
noIcon
|
||||
mt="7px"
|
||||
truncation="constant_long"
|
||||
/>
|
||||
{ txHash ? (
|
||||
<TxEntity
|
||||
hash={ txHash }
|
||||
isLoading={ isLoading }
|
||||
fontWeight={ 600 }
|
||||
noIcon
|
||||
mt={ 1 }
|
||||
truncation="constant_long"
|
||||
/>
|
||||
) : (
|
||||
<Skeleton loading={ isLoading } mt={ 1 }>-</Skeleton>
|
||||
) }
|
||||
<TimeWithTooltip
|
||||
timestamp={ timestamp }
|
||||
enableIncrement={ enableTimeIncrement }
|
||||
|
||||
|
Before Width: | Height: | Size: 121 KiB After Width: | Height: | Size: 119 KiB |
@@ -1,35 +1,21 @@
|
||||
import React from 'react';
|
||||
|
||||
import config from 'configs/app';
|
||||
import { Link } from 'toolkit/chakra/link';
|
||||
import { Tag, type TagProps } from 'toolkit/chakra/tag';
|
||||
import { Tooltip } from 'toolkit/chakra/tooltip';
|
||||
|
||||
const feature = config.features.celo;
|
||||
|
||||
interface Props extends TagProps {}
|
||||
|
||||
const NativeTokenTag = (props: Props) => {
|
||||
|
||||
const handleLinkClick = React.useCallback((event: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
event.stopPropagation();
|
||||
}, []);
|
||||
|
||||
if (!feature.isEnabled || !feature.nativeTokenAddress) {
|
||||
if (!config.UI.views.address.nativeTokenAddress) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tooltipContent = (
|
||||
<>
|
||||
<span>This ERC-20 token represents the native CELO balance for this address and isn’t counted twice. </span>
|
||||
<Link href="https://docs.celo.org/what-is-celo/using-celo/protocol/celo-token" external onClick={ handleLinkClick }>
|
||||
Learn more
|
||||
</Link>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip content={ tooltipContent } interactive>
|
||||
<Tooltip
|
||||
content={ `This ERC-20 token represents the native ${ config.chain.currency.symbol } balance for this address and isn’t counted twice` }
|
||||
>
|
||||
<Tag { ...props }>Native token</Tag>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
@@ -24,7 +24,7 @@ describe('ClustersEntity', () => {
|
||||
render(<ClustersEntity clusterName={ mockClusterName }/>);
|
||||
|
||||
const link = screen.getByRole('link');
|
||||
expect(link.getAttribute('href')).toBe('/clusters/test-cluster');
|
||||
expect(link.getAttribute('href')).toBe('/name-services/clusters/test-cluster');
|
||||
});
|
||||
|
||||
it('should render without link when noLink is true', () => {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Box, chakra, Flex, Text } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import { getFeaturePayload } from 'configs/app/features/types';
|
||||
|
||||
import { route } from 'nextjs-routes';
|
||||
|
||||
import config from 'configs/app';
|
||||
@@ -15,10 +13,13 @@ import IconSvg from 'ui/shared/IconSvg';
|
||||
|
||||
import { distributeEntityProps, getIconProps } from '../base/utils';
|
||||
|
||||
const nameServicesFeature = config.features.nameServices;
|
||||
const clustersFeature = nameServicesFeature.isEnabled && nameServicesFeature.clusters.isEnabled ? nameServicesFeature.clusters : undefined;
|
||||
|
||||
type LinkProps = EntityBase.LinkBaseProps & Pick<EntityProps, 'clusterName'>;
|
||||
|
||||
const Link = chakra((props: LinkProps) => {
|
||||
const defaultHref = route({ pathname: '/clusters/[name]', query: { name: encodeURIComponent(props.clusterName) } });
|
||||
const defaultHref = route({ pathname: '/name-services/clusters/[name]', query: { name: encodeURIComponent(props.clusterName) } });
|
||||
|
||||
return (
|
||||
<EntityBase.Link
|
||||
@@ -72,7 +73,7 @@ const Icon = (props: IconProps) => {
|
||||
borderRadius="base"
|
||||
mr={ 2 }
|
||||
flexShrink={ 0 }
|
||||
src={ `${ getFeaturePayload(config.features.clusters)?.cdnUrl || '' }/profile-image/${ props.clusterName }` }
|
||||
src={ `${ clustersFeature?.cdnUrl || '' }/profile-image/${ props.clusterName }` }
|
||||
alt={ `${ props.clusterName } profile` }
|
||||
fallback={ fallbackElement }
|
||||
/>
|
||||
|
||||
@@ -17,7 +17,7 @@ import { distributeEntityProps, getIconProps } from '../base/utils';
|
||||
type LinkProps = EntityBase.LinkBaseProps & Pick<EntityProps, 'domain'>;
|
||||
|
||||
const Link = chakra((props: LinkProps) => {
|
||||
const defaultHref = route({ pathname: '/name-domains/[name]', query: { name: props.domain } });
|
||||
const defaultHref = route({ pathname: '/name-services/domains/[name]', query: { name: props.domain } });
|
||||
|
||||
return (
|
||||
<EntityBase.Link
|
||||
|
||||
@@ -4,6 +4,8 @@ import type { SearchResultItem } from 'types/client/search';
|
||||
|
||||
import config from 'configs/app';
|
||||
|
||||
const nameServicesFeature = config.features.nameServices;
|
||||
|
||||
export type ApiCategory =
|
||||
'token' |
|
||||
'nft' |
|
||||
@@ -48,11 +50,11 @@ if (config.features.dataAvailability.isEnabled) {
|
||||
searchCategories.push({ id: 'blob', title: 'Blobs', tabTitle: 'Blobs' });
|
||||
}
|
||||
|
||||
if (config.features.nameService.isEnabled) {
|
||||
if (nameServicesFeature.isEnabled && nameServicesFeature.ens.isEnabled) {
|
||||
searchCategories.unshift({ id: 'domain', title: 'Names', tabTitle: 'Names' });
|
||||
}
|
||||
|
||||
if (config.features.clusters.isEnabled) {
|
||||
if (nameServicesFeature.isEnabled && nameServicesFeature.clusters.isEnabled) {
|
||||
searchCategories.unshift({ id: 'cluster', title: 'Cluster Name', tabTitle: 'Cluster' });
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,8 @@ import {
|
||||
WEI_VAR_NAME,
|
||||
} from './utils';
|
||||
|
||||
const nameServicesFeature = config.features.nameServices;
|
||||
|
||||
interface Props extends BoxProps {
|
||||
summary?: TxInterpretationSummary;
|
||||
isLoading?: boolean;
|
||||
@@ -102,7 +104,7 @@ const TxInterpretationElementByType = (
|
||||
</chakra.span>
|
||||
);
|
||||
case 'domain': {
|
||||
if (config.features.nameService.isEnabled) {
|
||||
if (nameServicesFeature.isEnabled && nameServicesFeature.ens.isEnabled) {
|
||||
return (
|
||||
<chakra.span display="inline-block" verticalAlign="top" _notFirst={{ marginLeft: 1 }}>
|
||||
<EnsEntity
|
||||
|
||||
@@ -10,6 +10,9 @@ import { Input } from 'toolkit/chakra/input';
|
||||
import { InputGroup } from 'toolkit/chakra/input-group';
|
||||
import { ClearButton } from 'toolkit/components/buttons/ClearButton';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
|
||||
const nameServicesFeature = config.features.nameServices;
|
||||
|
||||
interface Props extends Omit<HTMLChakraProps<'form'>, 'onChange'> {
|
||||
onChange?: (value: string) => void;
|
||||
onSubmit?: (event: FormEvent<HTMLFormElement>) => void;
|
||||
@@ -77,7 +80,7 @@ const SearchBarInput = (
|
||||
}, [ handleKeyPress ]);
|
||||
|
||||
const getPlaceholder = () => {
|
||||
const clusterText = config.features.clusters.isEnabled ? ' / cluster ' : '';
|
||||
const clusterText = nameServicesFeature.isEnabled && nameServicesFeature.clusters.isEnabled ? ' / cluster ' : '';
|
||||
return `Search by address / txn hash / block / token${ clusterText }/... `;
|
||||
};
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ const defaultUseQueryResult: Partial<UseQueryResult> = {
|
||||
|
||||
jest.mock('configs/app', () => ({
|
||||
features: {
|
||||
clusters: { isEnabled: true },
|
||||
nameServices: { isEnabled: true, ens: { isEnabled: true }, clusters: { isEnabled: true } },
|
||||
rollbar: { isEnabled: false },
|
||||
},
|
||||
UI: {
|
||||
|
||||
@@ -10,6 +10,9 @@ import { getResourceKey } from 'lib/api/useApiQuery';
|
||||
|
||||
import useQuickSearchQuery from './useQuickSearchQuery';
|
||||
|
||||
const nameServicesFeature = config.features.nameServices;
|
||||
const isClustersEnabled = nameServicesFeature.isEnabled && nameServicesFeature.clusters.isEnabled;
|
||||
|
||||
function isClusterSearch(term: string): boolean {
|
||||
const trimmed = term.trim();
|
||||
const hasTrailingSlash = trimmed.endsWith('/');
|
||||
@@ -52,7 +55,7 @@ function transformClusterToSearchResult(cluster: {
|
||||
export default function useSearchWithClusters() {
|
||||
const quickSearch = useQuickSearchQuery();
|
||||
|
||||
const isClusterQuery = config.features.clusters.isEnabled ?
|
||||
const isClusterQuery = isClustersEnabled ?
|
||||
isClusterSearch(quickSearch.debouncedSearchTerm) : false;
|
||||
|
||||
const clusterName = isClusterQuery ? extractClusterName(quickSearch.debouncedSearchTerm) : '';
|
||||
@@ -75,7 +78,7 @@ export default function useSearchWithClusters() {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
enabled: config.features.clusters.isEnabled && isClusterQuery && clusterName.length > 0,
|
||||
enabled: isClustersEnabled && isClusterQuery && clusterName.length > 0,
|
||||
select: (data) => {
|
||||
if (!data?.result?.data) return [];
|
||||
return [ transformClusterToSearchResult(data.result.data, data.result.data.owner) ];
|
||||
@@ -83,7 +86,7 @@ export default function useSearchWithClusters() {
|
||||
});
|
||||
|
||||
const combinedQuery = React.useMemo(() => {
|
||||
if (!config.features.clusters.isEnabled || !isClusterQuery) {
|
||||
if (!isClustersEnabled || !isClusterQuery) {
|
||||
return quickSearch.query;
|
||||
}
|
||||
|
||||
@@ -98,5 +101,5 @@ export default function useSearchWithClusters() {
|
||||
combinedQuery,
|
||||
]);
|
||||
|
||||
return config.features.clusters.isEnabled ? result : quickSearch;
|
||||
return isClustersEnabled ? result : quickSearch;
|
||||
}
|
||||
|
||||
@@ -49,13 +49,17 @@ const TopBarStats = () => {
|
||||
return <div/>;
|
||||
}
|
||||
|
||||
const hasNativeCoinPrice = data?.coin_price && !config.UI.nativeCoinPrice.isHidden;
|
||||
const hasSecondaryCoinPrice = data?.secondary_coin_price && config.chain.secondaryCoin.symbol && (hasNativeCoinPrice ? !isMobile : true);
|
||||
const hasGasInfo = data?.gas_prices && data.gas_prices.average !== null && config.features.gasTracker.isEnabled;
|
||||
|
||||
return (
|
||||
<Flex
|
||||
alignItems="center"
|
||||
fontSize="xs"
|
||||
fontWeight={ 500 }
|
||||
>
|
||||
{ data?.coin_price && (
|
||||
{ hasNativeCoinPrice && (
|
||||
<Flex columnGap={ 1 }>
|
||||
<Skeleton loading={ isPlaceholderData }>
|
||||
<chakra.span color="text.secondary">{ config.chain.currency.symbol } </chakra.span>
|
||||
@@ -70,7 +74,7 @@ const TopBarStats = () => {
|
||||
) }
|
||||
</Flex>
|
||||
) }
|
||||
{ !isMobile && data?.secondary_coin_price && config.chain.secondaryCoin.symbol && (
|
||||
{ hasSecondaryCoinPrice && (
|
||||
<Flex columnGap={ 1 } ml={ data?.coin_price ? 3 : 0 }>
|
||||
<Skeleton loading={ isPlaceholderData }>
|
||||
<chakra.span color="text.secondary">{ config.chain.secondaryCoin.symbol } </chakra.span>
|
||||
@@ -78,14 +82,14 @@ const TopBarStats = () => {
|
||||
</Skeleton>
|
||||
</Flex>
|
||||
) }
|
||||
{ data?.coin_price && config.features.gasTracker.isEnabled && <TextSeparator/> }
|
||||
{ data?.gas_prices && data.gas_prices.average !== null && config.features.gasTracker.isEnabled && (
|
||||
{ (hasNativeCoinPrice || hasSecondaryCoinPrice) && hasGasInfo && <TextSeparator/> }
|
||||
{ hasGasInfo && (
|
||||
<>
|
||||
<Skeleton loading={ isPlaceholderData } display={{ base: 'none', lg: 'inline-flex' }} whiteSpace="pre-wrap">
|
||||
<chakra.span color="text.secondary">Gas </chakra.span>
|
||||
<GasInfoTooltip data={ data } dataUpdatedAt={ dataUpdatedAt } placement={ !data?.coin_price ? 'bottom-start' : undefined }>
|
||||
<Link>
|
||||
<GasPrice data={ data.gas_prices.average }/>
|
||||
<GasPrice data={ data.gas_prices?.average ?? null }/>
|
||||
</Link>
|
||||
</GasInfoTooltip>
|
||||
</Skeleton>
|
||||
|
||||
@@ -40,12 +40,14 @@ const TokenTransferListItem = ({
|
||||
return (
|
||||
<ListItemMobile rowGap={ 3 }>
|
||||
<Flex justifyContent="space-between" alignItems="center" lineHeight="24px" width="100%">
|
||||
<TxEntity
|
||||
isLoading={ isLoading }
|
||||
hash={ txHash }
|
||||
truncation="constant_long"
|
||||
fontWeight="700"
|
||||
/>
|
||||
{ txHash && (
|
||||
<TxEntity
|
||||
isLoading={ isLoading }
|
||||
hash={ txHash }
|
||||
truncation="constant_long"
|
||||
fontWeight="700"
|
||||
/>
|
||||
) }
|
||||
<TimeWithTooltip
|
||||
timestamp={ timestamp }
|
||||
enableIncrement
|
||||
|
||||
@@ -40,13 +40,15 @@ const TokenTransferTableItem = ({
|
||||
<TableRow alignItems="top">
|
||||
<TableCell>
|
||||
<Flex flexDirection="column" alignItems="flex-start" mt="5px" rowGap={ 3 }>
|
||||
<TxEntity
|
||||
hash={ txHash }
|
||||
isLoading={ isLoading }
|
||||
fontWeight={ 600 }
|
||||
noIcon
|
||||
truncation="constant_long"
|
||||
/>
|
||||
{ txHash ? (
|
||||
<TxEntity
|
||||
hash={ txHash }
|
||||
isLoading={ isLoading }
|
||||
fontWeight={ 600 }
|
||||
noIcon
|
||||
truncation="constant_long"
|
||||
/>
|
||||
) : <Skeleton loading={ isLoading }>-</Skeleton> }
|
||||
<TimeWithTooltip
|
||||
timestamp={ timestamp }
|
||||
enableIncrement
|
||||
|
||||
@@ -34,10 +34,14 @@ const TokenTransfersListItem = ({ item, isLoading, chainData }: Props) => {
|
||||
|
||||
return (
|
||||
<ListItemMobileGrid.Container>
|
||||
<ListItemMobileGrid.Label isLoading={ isLoading }>Txn hash</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
<TxEntity hash={ item.transaction_hash } isLoading={ isLoading } truncation="constant_long" noIcon={ !chainData } chain={ chainData }/>
|
||||
</ListItemMobileGrid.Value>
|
||||
{ item.transaction_hash && (
|
||||
<>
|
||||
<ListItemMobileGrid.Label isLoading={ isLoading }>Txn hash</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
<TxEntity hash={ item.transaction_hash } isLoading={ isLoading } truncation="constant_long" noIcon={ !chainData } chain={ chainData }/>
|
||||
</ListItemMobileGrid.Value>
|
||||
</>
|
||||
) }
|
||||
|
||||
<ListItemMobileGrid.Label isLoading={ isLoading }>Age</ListItemMobileGrid.Label>
|
||||
<ListItemMobileGrid.Value>
|
||||
|
||||
@@ -40,13 +40,17 @@ const TokenTransferTableItem = ({ item, isLoading, chainData }: Props) => {
|
||||
</TableCell>
|
||||
) }
|
||||
<TableCell>
|
||||
<TxEntity
|
||||
hash={ item.transaction_hash }
|
||||
isLoading={ isLoading }
|
||||
fontWeight={ 600 }
|
||||
noIcon
|
||||
truncation="constant_long"
|
||||
/>
|
||||
{ item.transaction_hash ? (
|
||||
<TxEntity
|
||||
hash={ item.transaction_hash }
|
||||
isLoading={ isLoading }
|
||||
fontWeight={ 600 }
|
||||
noIcon
|
||||
truncation="constant_long"
|
||||
/>
|
||||
) : (
|
||||
<Skeleton loading={ isLoading }>-</Skeleton>
|
||||
) }
|
||||
<TimeWithTooltip
|
||||
timestamp={ item.timestamp }
|
||||
enableIncrement
|
||||
|
||||
@@ -206,8 +206,9 @@ const ZetaChainCCTXDetailsLifecycleOut = ({ outboundParam, tx, isLoading, isLast
|
||||
/>
|
||||
</>
|
||||
);
|
||||
text = `Abort executed`;
|
||||
color = 'text.success';
|
||||
const isFailed = tx.cctx_status?.is_abort_refunded === false;
|
||||
text = isFailed ? `Abort failed` : `Abort executed`;
|
||||
color = isFailed ? 'text.error' : 'text.success';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7488,10 +7488,10 @@
|
||||
rpc-websockets "^9.0.2"
|
||||
superstruct "^2.0.2"
|
||||
|
||||
"@specify-sh/sdk@0.4.1":
|
||||
version "0.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@specify-sh/sdk/-/sdk-0.4.1.tgz#016cd004c35fa64b8a510daac675edfce012958f"
|
||||
integrity sha512-IY5xUFbZo3CYfBh+iRqIP3sguUyJhgY/hWA2EvBrwwOrgLL9vjJVEI+ijSYRlNBBTTEHreB0KC6eK0USO87jYQ==
|
||||
"@specify-sh/sdk@0.4.2":
|
||||
version "0.4.2"
|
||||
resolved "https://registry.yarnpkg.com/@specify-sh/sdk/-/sdk-0.4.2.tgz#409072780661e9ba6c9d773403a1252b26ddc92b"
|
||||
integrity sha512-vskPvwpE3SWkmrOPhfMgvmvxvC+XNL93FTTF4SD3cPNZDsry5qfPNIW6TU1OTh3DWk7JMl/3X605CJUUAdEYmw==
|
||||
dependencies:
|
||||
cross-fetch "4.0.0"
|
||||
|
||||
|
||||