mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
OP Superchain explorer: aggregated views (#3040)
* refactor widgets on home page * coming soon placeholder component * top accounts page * contract data * address ERC-20 tokens list * chain filter for address ERC-20 tokens * token select for address detailed view * address details fields * tokens table * stats widgets on home page * chain indicators graph on the home page * quick search * search results all tab * tabs and chain select on search results page * search results mobile view * add contract info to search and address details * add text filter for tokens and search improvements * load all results inside tab on search results page * token balance info * add support for domains in the full search * Multichain explorer Resolves #2989 * add envs validator for multichain * add some tests * fix search bar on search results page on mobile * home stats fixes * fix ts * optimize amount of queries on address page * add more contract info on address page * add enter key hint to search input * fix tests * fix test * update bg.primary css-variable on color theme change * sitemap for multichain * txs page: render first tab if blob tab is not available on the chain * filter out in active chains from chain select on address page and user ops page * add chain icon to advanced filter table * fix tooltip on entity with shield * fix ts * show contract implementations * fixes * add chain icon to all lists * refactor zetachain chains config type * use chain id as primary option instead of chain slug * refactor chain config type for multichain and essential dapps * refactor chain icon and chain select * rename chain-slug in chain_slug * change default radius of chain icon and update screenshots for multichain tests * change conditions for "isContract" and "isVerified" * test fixes * make shield optional for external chain entities * add chain icon to approvals lists * add stats page for multichain * add chain postfix to chart .png and .csv assets name * fix tests * sync verified contracts language filter and fix loading state in the search results page * fix sol2uml diagram and csv export * regcred from helmfile for review * link fixes * fix state for unknown address * fix incorrect chain in the context during spa transition to the same route on different chain * add links to stats widgets * add mixpanel to L2 demo values * show message of on-demand contract verification * fix screenshots --------- Co-authored-by: Nick Zenchik <[email protected]>
This commit is contained in:
committed by
GitHub
co-authored by
Nick Zenchik
parent
54c63614db
commit
edc54514f1
+21
-5
@@ -19,6 +19,10 @@ export interface ApiPropsFull extends ApiPropsBase {
|
||||
|
||||
const generalApi = (() => {
|
||||
const apiHost = getEnvValue('NEXT_PUBLIC_API_HOST');
|
||||
if (!apiHost) {
|
||||
return;
|
||||
}
|
||||
|
||||
const apiSchema = getEnvValue('NEXT_PUBLIC_API_PROTOCOL') || 'https';
|
||||
const apiPort = getEnvValue('NEXT_PUBLIC_API_PORT');
|
||||
const apiEndpoint = [
|
||||
@@ -101,9 +105,10 @@ const rewardsApi = (() => {
|
||||
});
|
||||
})();
|
||||
|
||||
const multichainApi = (() => {
|
||||
const multichainAggregatorApi = (() => {
|
||||
const apiHost = getEnvValue('NEXT_PUBLIC_MULTICHAIN_AGGREGATOR_API_HOST');
|
||||
if (!apiHost) {
|
||||
const cluster = getEnvValue('NEXT_PUBLIC_MULTICHAIN_CLUSTER');
|
||||
if (!apiHost || !cluster) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -113,12 +118,22 @@ const multichainApi = (() => {
|
||||
return Object.freeze({
|
||||
endpoint: apiHost,
|
||||
socketEndpoint: `wss://${ url.host }`,
|
||||
basePath: stripTrailingSlash(getEnvValue('NEXT_PUBLIC_MULTICHAIN_AGGREGATOR_BASE_PATH') || ''),
|
||||
basePath: `/api/v1/clusters/${ cluster }`,
|
||||
});
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
})();
|
||||
|
||||
const multichainStatsApi = (() => {
|
||||
const apiHost = getEnvValue('NEXT_PUBLIC_MULTICHAIN_STATS_API_HOST');
|
||||
if (!apiHost) {
|
||||
return;
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
endpoint: apiHost,
|
||||
});
|
||||
})();
|
||||
|
||||
const statsApi = (() => {
|
||||
@@ -197,7 +212,7 @@ const zetachainApi = (() => {
|
||||
})();
|
||||
|
||||
export type Apis = {
|
||||
general: ApiPropsFull;
|
||||
general: ApiPropsFull | undefined;
|
||||
} & Partial<Record<Exclude<ApiName, 'general'>, ApiPropsBase>>;
|
||||
|
||||
const apis: Apis = Object.freeze({
|
||||
@@ -207,7 +222,8 @@ const apis: Apis = Object.freeze({
|
||||
clusters: clustersApi,
|
||||
contractInfo: contractInfoApi,
|
||||
metadata: metadataApi,
|
||||
multichain: multichainApi,
|
||||
multichainAggregator: multichainAggregatorApi,
|
||||
multichainStats: multichainStatsApi,
|
||||
rewards: rewardsApi,
|
||||
stats: statsApi,
|
||||
tac: tacApi,
|
||||
|
||||
@@ -3,15 +3,22 @@ import type { Feature } from './types';
|
||||
import apis from '../apis';
|
||||
import { getEnvValue } from '../utils';
|
||||
|
||||
const isEnabled = getEnvValue('NEXT_PUBLIC_OP_SUPERCHAIN_ENABLED') === 'true';
|
||||
const isEnabled = getEnvValue('NEXT_PUBLIC_MULTICHAIN_ENABLED') === 'true';
|
||||
const cluster = getEnvValue('NEXT_PUBLIC_MULTICHAIN_CLUSTER');
|
||||
|
||||
// The feature was initially implemented for OP Superchain interop cluster
|
||||
// but later the project was abandoned by Optimism team.
|
||||
// Now it serves mainly for demo purposes of multichain explorer possible functionalities.
|
||||
// So for now I have kept all naming in the code as it was initially done
|
||||
// and later it could be changed when specific multichain cluster will be implemented.
|
||||
const title = 'OP Superchain interop explorer';
|
||||
|
||||
const config: Feature<{ }> = (() => {
|
||||
if (apis.multichain && isEnabled) {
|
||||
const config: Feature<{ cluster: string }> = (() => {
|
||||
if (apis.multichainAggregator && apis.multichainStats && isEnabled && cluster) {
|
||||
return Object.freeze({
|
||||
title,
|
||||
isEnabled: true,
|
||||
cluster,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { Feature } from './types';
|
||||
|
||||
import apis from '../apis';
|
||||
import opSuperchain from './opSuperchain';
|
||||
|
||||
const title = 'Blockchain statistics';
|
||||
|
||||
const config: Feature<{}> = (() => {
|
||||
if (apis.stats) {
|
||||
if (apis.stats || opSuperchain.isEnabled) {
|
||||
return Object.freeze({
|
||||
title,
|
||||
isEnabled: true,
|
||||
|
||||
@@ -8,24 +8,16 @@ NEXT_PUBLIC_APP_HOST=localhost
|
||||
NEXT_PUBLIC_APP_PORT=3000
|
||||
NEXT_PUBLIC_APP_ENV=development
|
||||
|
||||
# Instance ENVs
|
||||
# TODO @tom2drum make these envs optional for multichain (adjust docs)
|
||||
NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
|
||||
NEXT_PUBLIC_API_BASE_PATH=/
|
||||
NEXT_PUBLIC_API_HOST=localhost
|
||||
NEXT_PUBLIC_API_PORT=3001
|
||||
NEXT_PUBLIC_API_PROTOCOL=http
|
||||
NEXT_PUBLIC_NETWORK_ID=10
|
||||
|
||||
# TODO @tom2drum New ENVs (add to docs)
|
||||
NEXT_PUBLIC_MULTICHAIN_AGGREGATOR_API_HOST=https://multichain-aggregator.k8s-dev.blockscout.com
|
||||
NEXT_PUBLIC_MULTICHAIN_AGGREGATOR_BASE_PATH=/api/v1/clusters/interop
|
||||
NEXT_PUBLIC_OP_SUPERCHAIN_ENABLED=true
|
||||
NEXT_PUBLIC_MULTICHAIN_STATS_API_HOST=http://multichain-search-stats.k8s-dev.blockscout.com
|
||||
NEXT_PUBLIC_MULTICHAIN_ENABLED=true
|
||||
NEXT_PUBLIC_MULTICHAIN_CLUSTER=interop
|
||||
|
||||
# TODO @tom2drum remove this
|
||||
SKIP_ENVS_VALIDATION=true
|
||||
SKIP_ENVS_VALIDATION=false
|
||||
|
||||
NEXT_PUBLIC_API_SPEC_URL=none
|
||||
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cap']
|
||||
NEXT_PUBLIC_HOMEPAGE_STATS=['total_txs','wallet_addresses']
|
||||
NEXT_PUBLIC_API_DOCS_TABS=[]
|
||||
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/optimism-mainnet.json
|
||||
NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/optimism.json
|
||||
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs', 'coin_price', 'market_cap', 'secondary_coin_price']
|
||||
@@ -38,7 +30,6 @@ NEXT_PUBLIC_NETWORK_NAME=OP Superchain
|
||||
NEXT_PUBLIC_NETWORK_SHORT_NAME=OP Superchain
|
||||
NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/optimism-mainnet.png
|
||||
NEXT_PUBLIC_GAS_TRACKER_ENABLED=false
|
||||
NEXT_PUBLIC_NAVIGATION_HIDDEN_LINKS=['eth_rpc_api','rpc_api']
|
||||
NEXT_PUBLIC_HIDE_INDEXING_ALERT_BLOCKS=true
|
||||
NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS=true
|
||||
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=false
|
||||
|
||||
@@ -49,10 +49,12 @@ NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=http://localhost:3006
|
||||
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=http://localhost:3007
|
||||
NEXT_PUBLIC_NAME_SERVICE_API_HOST=http://localhost:3008
|
||||
NEXT_PUBLIC_REWARDS_SERVICE_API_HOST=http://localhost:3009
|
||||
NEXT_PUBLIC_MULTICHAIN_AGGREGATOR_API_HOST=http://localhost:3010
|
||||
NEXT_PUBLIC_TAC_OPERATION_LIFECYCLE_API_HOST=http://localhost:3100
|
||||
NEXT_PUBLIC_USER_OPS_INDEXER_API_HOST=http://localhost:3110
|
||||
NEXT_PUBLIC_ZETACHAIN_SERVICE_API_HOST=http://localhost:3111
|
||||
NEXT_PUBLIC_MULTICHAIN_AGGREGATOR_API_HOST=http://localhost:3012
|
||||
NEXT_PUBLIC_MULTICHAIN_CLUSTER=test
|
||||
NEXT_PUBLIC_MULTICHAIN_STATS_API_HOST=http://localhost:3013
|
||||
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx
|
||||
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx
|
||||
NEXT_PUBLIC_VIEWS_ADDRESS_FORMAT=['base16','bech32']
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { MultichainConfig } from 'types/multichain';
|
||||
import type { EssentialDappsChainConfig } from 'types/client/marketplace';
|
||||
|
||||
import config from 'configs/app';
|
||||
import { isBrowser } from 'toolkit/utils/isBrowser';
|
||||
|
||||
const marketplaceFeature = config.features.marketplace;
|
||||
|
||||
const essentialDappsChains: () => MultichainConfig | undefined = () => {
|
||||
const essentialDappsChains: () => { chains: Array<EssentialDappsChainConfig> } | undefined = () => {
|
||||
if (!marketplaceFeature.isEnabled || !marketplaceFeature.essentialDapps) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { ValidationError } from 'yup';
|
||||
|
||||
import { buildExternalAssetFilePath } from '../../../configs/app/utils';
|
||||
import schema from './schema';
|
||||
import schemaMultichain from './schema_multichain';
|
||||
|
||||
const silent = process.argv.includes('--silent');
|
||||
|
||||
@@ -51,7 +52,12 @@ async function validateEnvs(appEnvs: Record<string, string>) {
|
||||
}
|
||||
}
|
||||
|
||||
await schema.validate(appEnvs, { stripUnknown: false, abortEarly: false });
|
||||
if (appEnvs.NEXT_PUBLIC_MULTICHAIN_ENABLED === 'true') {
|
||||
await schemaMultichain.validate(appEnvs, { stripUnknown: false, abortEarly: false });
|
||||
} else {
|
||||
await schema.validate(appEnvs, { stripUnknown: false, abortEarly: false });
|
||||
}
|
||||
|
||||
!silent && console.log('👍 All good!');
|
||||
} catch (_error) {
|
||||
if (typeof _error === 'object' && _error !== null && 'errors' in _error) {
|
||||
|
||||
@@ -9,12 +9,8 @@ declare module 'yup' {
|
||||
|
||||
import * as yup from 'yup';
|
||||
|
||||
import type { AdButlerConfig } from '../../../types/client/adButlerConfig';
|
||||
import type { AddressProfileAPIConfig } from '../../../types/client/addressProfileAPIConfig';
|
||||
import { SUPPORTED_AD_TEXT_PROVIDERS, SUPPORTED_AD_BANNER_PROVIDERS, SUPPORTED_AD_BANNER_ADDITIONAL_PROVIDERS } from '../../../types/client/adProviders';
|
||||
import type { AdTextProviders, AdBannerProviders, AdBannerAdditionalProviders } from '../../../types/client/adProviders';
|
||||
import { SMART_CONTRACT_EXTRA_VERIFICATION_METHODS, SMART_CONTRACT_LANGUAGE_FILTERS, type ContractCodeIde, type SmartContractVerificationMethodExtra } from '../../../types/client/contract';
|
||||
import type { DeFiDropdownItem } from '../../../types/client/deFiDropdown';
|
||||
import type { GasRefuelProviderConfig } from '../../../types/client/gasRefuelProviderConfig';
|
||||
import { GAS_UNITS } from '../../../types/client/gasTracker';
|
||||
import type { GasUnit } from '../../../types/client/gasTracker';
|
||||
@@ -22,7 +18,6 @@ import type { MarketplaceAppBase, MarketplaceAppSocialInfo, EssentialDappsConfig
|
||||
import type { MultichainProviderConfig } from '../../../types/client/multichainProviderConfig';
|
||||
import type { ApiDocsTabId } from '../../../types/views/apiDocs';
|
||||
import { API_DOCS_TABS } from '../../../types/views/apiDocs';
|
||||
import type { NavItemExternal, NavigationLayout, NavigationPromoBannerConfig } from '../../../types/client/navigation';
|
||||
import { ROLLUP_TYPES } from '../../../types/client/rollup';
|
||||
import type { BridgedTokenChain, TokenBridge } from '../../../types/client/token';
|
||||
import { PROVIDERS as TX_INTERPRETATION_PROVIDERS } from '../../../types/client/txInterpretation';
|
||||
@@ -30,12 +25,7 @@ import { VALIDATORS_CHAIN_TYPE } from '../../../types/client/validators';
|
||||
import type { ValidatorsChainType } from '../../../types/client/validators';
|
||||
import type { WalletType } from '../../../types/client/wallets';
|
||||
import { SUPPORTED_WALLETS } from '../../../types/client/wallets';
|
||||
import type { CustomLink, CustomLinksGroup } from '../../../types/footerLinks';
|
||||
import { CHAIN_INDICATOR_IDS, HOME_STATS_WIDGET_IDS } from '../../../types/homepage';
|
||||
import type { ChainIndicatorId, HeroBannerButtonState, HeroBannerConfig, HomeStatsWidgetId } from '../../../types/homepage';
|
||||
import { type NetworkVerificationTypeEnvs, type NetworkExplorer, type FeaturedNetwork, NETWORK_GROUPS } from '../../../types/networks';
|
||||
import { COLOR_THEME_IDS } from '../../../types/settings';
|
||||
import type { FontFamily } from '../../../types/ui';
|
||||
import { type NetworkVerificationTypeEnvs, type NetworkExplorer } from '../../../types/networks';
|
||||
import type { AddressFormat, AddressViewId, Address3rdPartyWidget } from '../../../types/views/address';
|
||||
import { ADDRESS_FORMATS, ADDRESS_VIEWS_IDS, IDENTICON_TYPES, ADDRESS_3RD_PARTY_WIDGET_PAGES } from '../../../types/views/address';
|
||||
import { BLOCK_FIELDS_IDS } from '../../../types/views/block';
|
||||
@@ -48,38 +38,11 @@ import type { TxExternalTxsConfig } from '../../../types/client/externalTxsConfi
|
||||
|
||||
import { replaceQuotes } from '../../../configs/app/utils';
|
||||
import * as regexp from '../../../toolkit/utils/regexp';
|
||||
import type { IconName } from '../../../ui/shared/IconSvg';
|
||||
import type { CrossChainInfo } from '../../../types/client/crossChainInfo';
|
||||
|
||||
const protocols = [ 'http', 'https' ];
|
||||
|
||||
const urlTest: yup.TestConfig = {
|
||||
name: 'url',
|
||||
test: (value: unknown) => {
|
||||
if (!value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
if (typeof value === 'string') {
|
||||
new URL(value);
|
||||
return true;
|
||||
}
|
||||
} catch (error) {}
|
||||
|
||||
return false;
|
||||
},
|
||||
message: '${path} is not a valid URL',
|
||||
exclusive: true,
|
||||
};
|
||||
|
||||
const getYupValidationErrorMessage = (error: unknown) =>
|
||||
typeof error === 'object' &&
|
||||
error !== null &&
|
||||
'errors' in error &&
|
||||
Array.isArray(error.errors) ?
|
||||
error.errors.join(', ') :
|
||||
'';
|
||||
import type { ZetaChainChainsConfigEnv } from '../../../types/client/zetaChain';
|
||||
import { urlTest, protocols, getYupValidationErrorMessage } from './utils';
|
||||
import * as uiSchemas from './schemas/ui';
|
||||
import * as featuresSchemas from './schemas/features';
|
||||
import servicesSchemas from './schemas/services';
|
||||
|
||||
const marketplaceAppSchema: yup.ObjectSchema<MarketplaceAppBase & MarketplaceAppSocialInfo> = yup
|
||||
.object({
|
||||
@@ -452,20 +415,6 @@ const apiDocsScheme = yup
|
||||
.test(urlTest),
|
||||
});
|
||||
|
||||
const userOpsSchema = yup
|
||||
.object()
|
||||
.shape({
|
||||
NEXT_PUBLIC_HAS_USER_OPS: yup.boolean(),
|
||||
NEXT_PUBLIC_USER_OPS_INDEXER_API_HOST: yup
|
||||
.string()
|
||||
.test(urlTest)
|
||||
.when('NEXT_PUBLIC_HAS_USER_OPS', {
|
||||
is: (value: boolean) => value,
|
||||
then: (schema) => schema,
|
||||
otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_USER_OPS_INDEXER_API_HOST can only be used if NEXT_PUBLIC_HAS_USER_OPS is set to \'true\''),
|
||||
}),
|
||||
});
|
||||
|
||||
const mixpanelSchema = yup
|
||||
.object()
|
||||
.shape({
|
||||
@@ -485,111 +434,12 @@ const mixpanelSchema = yup
|
||||
}),
|
||||
});
|
||||
|
||||
const adButlerConfigSchema = yup
|
||||
.object<AdButlerConfig>()
|
||||
.transform(replaceQuotes)
|
||||
.json()
|
||||
.when('NEXT_PUBLIC_AD_BANNER_PROVIDER', {
|
||||
is: (value: AdBannerProviders) => value === 'adbutler',
|
||||
then: (schema) => schema
|
||||
.shape({
|
||||
id: yup.string().required(),
|
||||
width: yup.number().positive().required(),
|
||||
height: yup.number().positive().required(),
|
||||
})
|
||||
.required(),
|
||||
})
|
||||
.when('NEXT_PUBLIC_AD_BANNER_ADDITIONAL_PROVIDER', {
|
||||
is: (value: AdBannerProviders) => value === 'adbutler',
|
||||
then: (schema) => schema
|
||||
.shape({
|
||||
id: yup.string().required(),
|
||||
width: yup.number().positive().required(),
|
||||
height: yup.number().positive().required(),
|
||||
})
|
||||
.required(),
|
||||
});
|
||||
|
||||
const adsBannerSchema = yup
|
||||
.object()
|
||||
.shape({
|
||||
NEXT_PUBLIC_AD_BANNER_PROVIDER: yup.string<AdBannerProviders>().oneOf(SUPPORTED_AD_BANNER_PROVIDERS),
|
||||
NEXT_PUBLIC_AD_BANNER_ADDITIONAL_PROVIDER: yup.string<AdBannerAdditionalProviders>().oneOf(SUPPORTED_AD_BANNER_ADDITIONAL_PROVIDERS),
|
||||
NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP: adButlerConfigSchema,
|
||||
NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE: adButlerConfigSchema,
|
||||
NEXT_PUBLIC_AD_BANNER_ENABLE_SPECIFY: yup.boolean(),
|
||||
});
|
||||
|
||||
const accountSchema = yup
|
||||
.object()
|
||||
.shape({
|
||||
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED: yup.boolean(),
|
||||
});
|
||||
|
||||
const featuredNetworkSchema: yup.ObjectSchema<FeaturedNetwork> = yup
|
||||
.object()
|
||||
.shape({
|
||||
title: yup.string().required(),
|
||||
url: yup.string().test(urlTest).required(),
|
||||
group: yup.string().oneOf(NETWORK_GROUPS).required(),
|
||||
icon: yup.string().test(urlTest),
|
||||
isActive: yup.boolean(),
|
||||
invertIconInDarkMode: yup.boolean(),
|
||||
});
|
||||
|
||||
const navItemExternalSchema: yup.ObjectSchema<NavItemExternal> = yup
|
||||
.object({
|
||||
text: yup.string().required(),
|
||||
url: yup.string().test(urlTest).required(),
|
||||
});
|
||||
|
||||
const fontFamilySchema: yup.ObjectSchema<FontFamily> = yup
|
||||
.object()
|
||||
.transform(replaceQuotes)
|
||||
.json()
|
||||
.shape({
|
||||
name: yup.string().required(),
|
||||
url: yup.string().test(urlTest).required(),
|
||||
});
|
||||
|
||||
const heroBannerButtonStateSchema: yup.ObjectSchema<HeroBannerButtonState> = yup.object({
|
||||
background: yup.array().max(2).of(yup.string()),
|
||||
text_color: yup.array().max(2).of(yup.string()),
|
||||
});
|
||||
|
||||
const heroBannerSchema: yup.ObjectSchema<HeroBannerConfig> = yup.object()
|
||||
.transform(replaceQuotes)
|
||||
.json()
|
||||
.shape({
|
||||
background: yup.array().max(2).of(yup.string()),
|
||||
text_color: yup.array().max(2).of(yup.string()),
|
||||
border: yup.array().max(2).of(yup.string()),
|
||||
button: yup.object({
|
||||
_default: heroBannerButtonStateSchema,
|
||||
_hover: heroBannerButtonStateSchema,
|
||||
_selected: heroBannerButtonStateSchema,
|
||||
}),
|
||||
search: yup.object({
|
||||
border_width: yup.array().max(2).of(yup.string()),
|
||||
}),
|
||||
});
|
||||
|
||||
const footerLinkSchema: yup.ObjectSchema<CustomLink> = yup
|
||||
.object({
|
||||
text: yup.string().required(),
|
||||
url: yup.string().test(urlTest).required(),
|
||||
iconUrl: yup.array().of(yup.string().required().test(urlTest)),
|
||||
});
|
||||
|
||||
const footerLinkGroupSchema: yup.ObjectSchema<CustomLinksGroup> = yup
|
||||
.object({
|
||||
title: yup.string().required(),
|
||||
links: yup
|
||||
.array()
|
||||
.of(footerLinkSchema)
|
||||
.required(),
|
||||
});
|
||||
|
||||
const networkExplorerSchema: yup.ObjectSchema<NetworkExplorer> = yup
|
||||
.object({
|
||||
title: yup.string().required(),
|
||||
@@ -675,17 +525,6 @@ const addressMetadataSchema = yup
|
||||
}),
|
||||
});
|
||||
|
||||
const deFiDropdownItemSchema: yup.ObjectSchema<DeFiDropdownItem> = yup
|
||||
.object({
|
||||
text: yup.string().required(),
|
||||
icon: yup.string<IconName>(),
|
||||
dappId: yup.string(),
|
||||
url: yup.string().test(urlTest),
|
||||
})
|
||||
.test('oneOfRequired', 'NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS: Either dappId or url is required', function(value) {
|
||||
return Boolean(value.dappId) || Boolean(value.url);
|
||||
}) as yup.ObjectSchema<DeFiDropdownItem>;
|
||||
|
||||
const multichainProviderConfigSchema: yup.ObjectSchema<MultichainProviderConfig> = yup.object({
|
||||
name: yup.string().required(),
|
||||
url_template: yup.string().required(),
|
||||
@@ -693,7 +532,7 @@ const multichainProviderConfigSchema: yup.ObjectSchema<MultichainProviderConfig>
|
||||
dapp_id: yup.string(),
|
||||
});
|
||||
|
||||
const zetaChainCCTXConfigSchema: yup.ObjectSchema<CrossChainInfo> = yup.object({
|
||||
const zetaChainCCTXConfigSchema: yup.ObjectSchema<ZetaChainChainsConfigEnv> = yup.object({
|
||||
chain_id: yup.number().required(),
|
||||
chain_name: yup.string().required(),
|
||||
chain_logo: yup.string(),
|
||||
@@ -865,129 +704,6 @@ const schema = yup
|
||||
NEXT_PUBLIC_API_BASE_PATH: yup.string(),
|
||||
NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL: yup.string().oneOf([ 'ws', 'wss' ]),
|
||||
|
||||
// 4. UI configuration
|
||||
// a. homepage
|
||||
NEXT_PUBLIC_HOMEPAGE_CHARTS: yup
|
||||
.array()
|
||||
.transform(replaceQuotes)
|
||||
.json()
|
||||
.of(yup.string<ChainIndicatorId>().oneOf(CHAIN_INDICATOR_IDS))
|
||||
.test(
|
||||
'stats-api-required',
|
||||
'NEXT_PUBLIC_STATS_API_HOST is required when daily_operational_txs is enabled in NEXT_PUBLIC_HOMEPAGE_CHARTS',
|
||||
function(value) {
|
||||
// daily_operational_txs is presented only in stats microservice
|
||||
if (value?.includes('daily_operational_txs')) {
|
||||
return Boolean(this.parent.NEXT_PUBLIC_STATS_API_HOST);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
),
|
||||
NEXT_PUBLIC_HOMEPAGE_STATS: yup
|
||||
.array()
|
||||
.transform(replaceQuotes)
|
||||
.json()
|
||||
.of(yup.string<HomeStatsWidgetId>().oneOf(HOME_STATS_WIDGET_IDS))
|
||||
.test(
|
||||
'stats-api-required',
|
||||
'NEXT_PUBLIC_STATS_API_HOST is required when total_operational_txs is enabled in NEXT_PUBLIC_HOMEPAGE_STATS',
|
||||
function(value) {
|
||||
// total_operational_txs is presented only in stats microservice
|
||||
if (value?.includes('total_operational_txs')) {
|
||||
return Boolean(this.parent.NEXT_PUBLIC_STATS_API_HOST);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
),
|
||||
NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG: yup
|
||||
.mixed()
|
||||
.test(
|
||||
'shape',
|
||||
(ctx) => {
|
||||
try {
|
||||
heroBannerSchema.validateSync(ctx.originalValue);
|
||||
throw new Error('Unknown validation error');
|
||||
} catch (error: unknown) {
|
||||
const message = getYupValidationErrorMessage(error);
|
||||
return 'Invalid schema were provided for NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG' + (message ? `: ${ message }` : '');
|
||||
}
|
||||
},
|
||||
(data) => {
|
||||
const isUndefined = data === undefined;
|
||||
return isUndefined || heroBannerSchema.isValidSync(data);
|
||||
}),
|
||||
|
||||
// b. sidebar
|
||||
NEXT_PUBLIC_FEATURED_NETWORKS: yup
|
||||
.array()
|
||||
.json()
|
||||
.of(featuredNetworkSchema),
|
||||
NEXT_PUBLIC_FEATURED_NETWORKS_ALL_LINK: yup
|
||||
.string()
|
||||
.when('NEXT_PUBLIC_FEATURED_NETWORKS', {
|
||||
is: (value: Array<unknown> | undefined) => value && value.length > 0,
|
||||
then: (schema) => schema.test(urlTest),
|
||||
otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_FEATURED_NETWORKS_ALL_LINK can only be set when NEXT_PUBLIC_FEATURED_NETWORKS is configured'),
|
||||
}),
|
||||
NEXT_PUBLIC_FEATURED_NETWORKS_MODE: yup
|
||||
.string()
|
||||
.when('NEXT_PUBLIC_FEATURED_NETWORKS', {
|
||||
is: (value: Array<unknown> | undefined) => value && value.length > 0,
|
||||
then: (schema) => schema.oneOf([ 'tabs', 'list' ]),
|
||||
otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_FEATURED_NETWORKS_MODE can only be set when NEXT_PUBLIC_FEATURED_NETWORKS is configured'),
|
||||
}),
|
||||
NEXT_PUBLIC_OTHER_LINKS: yup
|
||||
.array()
|
||||
.transform(replaceQuotes)
|
||||
.json()
|
||||
.of(navItemExternalSchema),
|
||||
NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES: yup
|
||||
.array()
|
||||
.transform(replaceQuotes)
|
||||
.json()
|
||||
.of(yup.string()),
|
||||
NEXT_PUBLIC_NAVIGATION_LAYOUT: yup.string<NavigationLayout>().oneOf([ 'horizontal', 'vertical' ]),
|
||||
NEXT_PUBLIC_NAVIGATION_PROMO_BANNER_CONFIG: yup
|
||||
.mixed()
|
||||
.test('shape', 'Invalid schema were provided for NEXT_PUBLIC_NAVIGATION_PROMO_BANNER_CONFIG, it should be either object with img_url, text, bg_color, text_color, link_url or object with img_url and link_url', (data) => {
|
||||
const isUndefined = data === undefined;
|
||||
const jsonSchema = yup.object<NavigationPromoBannerConfig>().transform(replaceQuotes).json();
|
||||
|
||||
const valueSchema1 = jsonSchema.shape({
|
||||
img_url: yup.string().required(),
|
||||
text: yup.string().required(),
|
||||
bg_color: yup.object().shape({
|
||||
light: yup.string().required(),
|
||||
dark: yup.string().required(),
|
||||
}).required(),
|
||||
text_color: yup.object().shape({
|
||||
light: yup.string().required(),
|
||||
dark: yup.string().required(),
|
||||
}).required(),
|
||||
link_url: yup.string().required(),
|
||||
});
|
||||
|
||||
const valueSchema2 = jsonSchema.shape({
|
||||
img_url: yup.object().shape({
|
||||
small: yup.string().required(),
|
||||
large: yup.string().required(),
|
||||
}).required(),
|
||||
link_url: yup.string().required(),
|
||||
});
|
||||
|
||||
return isUndefined || valueSchema1.isValidSync(data) || valueSchema2.isValidSync(data);
|
||||
}),
|
||||
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),
|
||||
NEXT_PUBLIC_NETWORK_ICON_DARK: yup.string().test(urlTest),
|
||||
|
||||
// c. footer
|
||||
NEXT_PUBLIC_FOOTER_LINKS: yup
|
||||
.array()
|
||||
.json()
|
||||
.of(footerLinkGroupSchema),
|
||||
|
||||
// d. views
|
||||
NEXT_PUBLIC_VIEWS_BLOCK_HIDDEN_FIELDS: yup
|
||||
.array()
|
||||
@@ -1061,7 +777,6 @@ const schema = yup
|
||||
NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED: yup.boolean(),
|
||||
NEXT_PUBLIC_HELIA_VERIFIED_FETCH_ENABLED: yup.boolean(),
|
||||
|
||||
// e. misc
|
||||
NEXT_PUBLIC_NETWORK_EXPLORERS: yup
|
||||
.array()
|
||||
.transform(replaceQuotes)
|
||||
@@ -1073,25 +788,6 @@ const schema = yup
|
||||
.json()
|
||||
.of(contractCodeIdeSchema),
|
||||
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(),
|
||||
NEXT_PUBLIC_FONT_FAMILY_HEADING: yup
|
||||
.mixed()
|
||||
.test('shape', 'Invalid schema were provided for NEXT_PUBLIC_FONT_FAMILY_HEADING', (data) => {
|
||||
const isUndefined = data === undefined;
|
||||
return isUndefined || fontFamilySchema.isValidSync(data);
|
||||
}),
|
||||
NEXT_PUBLIC_FONT_FAMILY_BODY: yup
|
||||
.mixed()
|
||||
.test('shape', 'Invalid schema were provided for NEXT_PUBLIC_FONT_FAMILY_BODY', (data) => {
|
||||
const isUndefined = data === undefined;
|
||||
return isUndefined || fontFamilySchema.isValidSync(data);
|
||||
}),
|
||||
NEXT_PUBLIC_MAX_CONTENT_WIDTH_ENABLED: yup.boolean(),
|
||||
|
||||
// 5. Features configuration
|
||||
NEXT_PUBLIC_STATS_API_HOST: yup.string().test(urlTest),
|
||||
@@ -1117,7 +813,6 @@ const schema = yup
|
||||
}),
|
||||
NEXT_PUBLIC_WEB3_DISABLE_ADD_TOKEN_TO_WALLET: yup.boolean(),
|
||||
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER: yup.string().oneOf(TX_INTERPRETATION_PROVIDERS),
|
||||
NEXT_PUBLIC_AD_TEXT_PROVIDER: yup.string<AdTextProviders>().oneOf(SUPPORTED_AD_TEXT_PROVIDERS),
|
||||
NEXT_PUBLIC_PROMOTE_BLOCKSCOUT_IN_TITLE: yup.boolean(),
|
||||
NEXT_PUBLIC_OG_DESCRIPTION: yup.string(),
|
||||
NEXT_PUBLIC_OG_IMAGE_URL: yup.string().test(urlTest),
|
||||
@@ -1149,11 +844,6 @@ const schema = yup
|
||||
NEXT_PUBLIC_GAS_TRACKER_UNITS: yup.array().transform(replaceQuotes).json().of(yup.string<GasUnit>().oneOf(GAS_UNITS)),
|
||||
NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED: yup.boolean(),
|
||||
NEXT_PUBLIC_ADVANCED_FILTER_ENABLED: yup.boolean(),
|
||||
NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS: yup
|
||||
.array()
|
||||
.transform(replaceQuotes)
|
||||
.json()
|
||||
.of(deFiDropdownItemSchema),
|
||||
NEXT_PUBLIC_FAULT_PROOF_ENABLED: yup.boolean()
|
||||
.when('NEXT_PUBLIC_ROLLUP_TYPE', {
|
||||
is: 'optimistic',
|
||||
@@ -1227,19 +917,18 @@ const schema = yup
|
||||
return isUndefined || valueSchema.isValidSync(data);
|
||||
}),
|
||||
|
||||
// 6. External services envs
|
||||
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: yup.string(),
|
||||
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY: yup.string(),
|
||||
NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: yup.string(),
|
||||
NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY: yup.string(),
|
||||
NEXT_PUBLIC_ROLLBAR_CLIENT_TOKEN: yup.string(),
|
||||
|
||||
|
||||
// Misc
|
||||
NEXT_PUBLIC_USE_NEXT_JS_PROXY: yup.boolean(),
|
||||
})
|
||||
.concat(uiSchemas.homepageSchema)
|
||||
.concat(uiSchemas.navigationSchema)
|
||||
.concat(uiSchemas.footerSchema)
|
||||
.concat(uiSchemas.miscSchema)
|
||||
.concat(featuresSchemas.adsSchema)
|
||||
.concat(featuresSchemas.userOpsSchema)
|
||||
.concat(featuresSchemas.defiDropdownSchema)
|
||||
.concat(servicesSchemas)
|
||||
.concat(accountSchema)
|
||||
.concat(adsBannerSchema)
|
||||
.concat(marketplaceSchema)
|
||||
.concat(rollupSchema)
|
||||
.concat(celoSchema)
|
||||
@@ -1251,7 +940,6 @@ const schema = yup
|
||||
.concat(megaEthSchema)
|
||||
.concat(address3rdPartyWidgetsConfigSchema)
|
||||
.concat(addressMetadataSchema)
|
||||
.concat(userOpsSchema)
|
||||
.concat(flashblocksSchema)
|
||||
.concat(zetaChainSchema);
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
declare module 'yup' {
|
||||
interface StringSchema {
|
||||
// Yup's URL validator is not perfect so we made our own
|
||||
// https://github.com/jquense/yup/pull/1859
|
||||
url(): never;
|
||||
}
|
||||
}
|
||||
|
||||
import * as yup from 'yup';
|
||||
import { urlTest, protocols } from './utils';
|
||||
import * as uiSchemas from './schemas/ui';
|
||||
import * as featuresSchemas from './schemas/features';
|
||||
import servicesSchemas from './schemas/services';
|
||||
import { replaceQuotes } from '../../../configs/app/utils';
|
||||
|
||||
const schema = yup
|
||||
.object()
|
||||
.noUnknown(true, (params) => {
|
||||
return `Unknown ENV variables were provided: ${ params.unknown }`;
|
||||
})
|
||||
.shape({
|
||||
// I. Build-time ENVs
|
||||
// -----------------
|
||||
NEXT_PUBLIC_GIT_TAG: yup.string(),
|
||||
NEXT_PUBLIC_GIT_COMMIT_SHA: yup.string(),
|
||||
|
||||
// II. Run-time ENVs
|
||||
// -----------------
|
||||
// 1. App configuration
|
||||
NEXT_PUBLIC_APP_HOST: yup.string().required(),
|
||||
NEXT_PUBLIC_APP_PROTOCOL: yup.string().oneOf(protocols),
|
||||
NEXT_PUBLIC_APP_PORT: yup.number().positive().integer(),
|
||||
NEXT_PUBLIC_APP_ENV: yup.string(),
|
||||
NEXT_PUBLIC_APP_INSTANCE: yup.string(),
|
||||
|
||||
// 2. Blockchain parameters
|
||||
NEXT_PUBLIC_NETWORK_NAME: yup.string().required(),
|
||||
NEXT_PUBLIC_NETWORK_SHORT_NAME: yup.string(),
|
||||
NEXT_PUBLIC_IS_TESTNET: yup.boolean(),
|
||||
|
||||
// 5. Features configuration
|
||||
// NOTE!: Not all features are supported in multichain mode, and some of them not relevant or enabled per chain basis
|
||||
// Below listed supported features and the features that are enabled by default, so we have to turn them off
|
||||
NEXT_PUBLIC_OG_DESCRIPTION: yup.string(),
|
||||
NEXT_PUBLIC_OG_IMAGE_URL: yup.string().test(urlTest),
|
||||
|
||||
NEXT_PUBLIC_GAS_TRACKER_ENABLED: yup.boolean().equals([false]),
|
||||
NEXT_PUBLIC_ADVANCED_FILTER_ENABLED: yup.boolean().equals([false]),
|
||||
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED: yup.boolean().equals([false]),
|
||||
NEXT_PUBLIC_API_DOCS_TABS: yup.array().transform(replaceQuotes).json().max(0),
|
||||
|
||||
// 6. Multichain configuration
|
||||
NEXT_PUBLIC_MULTICHAIN_ENABLED: yup.boolean(),
|
||||
NEXT_PUBLIC_MULTICHAIN_CLUSTER: yup.string(),
|
||||
NEXT_PUBLIC_MULTICHAIN_AGGREGATOR_API_HOST: yup.string().test(urlTest),
|
||||
NEXT_PUBLIC_MULTICHAIN_STATS_API_HOST: yup.string().test(urlTest),
|
||||
|
||||
// Misc
|
||||
NEXT_PUBLIC_USE_NEXT_JS_PROXY: yup.boolean(),
|
||||
})
|
||||
.concat(uiSchemas.homepageSchema)
|
||||
.concat(uiSchemas.navigationSchema)
|
||||
.concat(uiSchemas.footerSchema)
|
||||
.concat(uiSchemas.miscSchema)
|
||||
.concat(featuresSchemas.adsSchema)
|
||||
.concat(featuresSchemas.userOpsSchema)
|
||||
.concat(featuresSchemas.defiDropdownSchema)
|
||||
.concat(servicesSchemas);
|
||||
|
||||
export default schema;
|
||||
@@ -0,0 +1,75 @@
|
||||
import * as yup from 'yup';
|
||||
import { replaceQuotes } from '../../../../configs/app/utils';
|
||||
import { SUPPORTED_AD_TEXT_PROVIDERS, SUPPORTED_AD_BANNER_PROVIDERS, SUPPORTED_AD_BANNER_ADDITIONAL_PROVIDERS } from '../../../../types/client/adProviders';
|
||||
import type { AdTextProviders, AdBannerProviders, AdBannerAdditionalProviders } from '../../../../types/client/adProviders';
|
||||
import type { AdButlerConfig } from '../../../../types/client/adButlerConfig';
|
||||
import { urlTest } from '../utils';
|
||||
import { IconName } from '../../../../ui/shared/IconSvg';
|
||||
import { DeFiDropdownItem } from '../../../../types/client/deFiDropdown';
|
||||
|
||||
const adButlerConfigSchema = yup
|
||||
.object<AdButlerConfig>()
|
||||
.transform(replaceQuotes)
|
||||
.json()
|
||||
.when('NEXT_PUBLIC_AD_BANNER_PROVIDER', {
|
||||
is: (value: AdBannerProviders) => value === 'adbutler',
|
||||
then: (schema) => schema
|
||||
.shape({
|
||||
id: yup.string().required(),
|
||||
width: yup.number().positive().required(),
|
||||
height: yup.number().positive().required(),
|
||||
})
|
||||
.required(),
|
||||
})
|
||||
.when('NEXT_PUBLIC_AD_BANNER_ADDITIONAL_PROVIDER', {
|
||||
is: (value: AdBannerProviders) => value === 'adbutler',
|
||||
then: (schema) => schema
|
||||
.shape({
|
||||
id: yup.string().required(),
|
||||
width: yup.number().positive().required(),
|
||||
height: yup.number().positive().required(),
|
||||
})
|
||||
.required(),
|
||||
});
|
||||
|
||||
export const adsSchema = yup.object({
|
||||
NEXT_PUBLIC_AD_TEXT_PROVIDER: yup.string<AdTextProviders>().oneOf(SUPPORTED_AD_TEXT_PROVIDERS),
|
||||
NEXT_PUBLIC_AD_BANNER_PROVIDER: yup.string<AdBannerProviders>().oneOf(SUPPORTED_AD_BANNER_PROVIDERS),
|
||||
NEXT_PUBLIC_AD_BANNER_ADDITIONAL_PROVIDER: yup.string<AdBannerAdditionalProviders>().oneOf(SUPPORTED_AD_BANNER_ADDITIONAL_PROVIDERS),
|
||||
NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP: adButlerConfigSchema,
|
||||
NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE: adButlerConfigSchema,
|
||||
NEXT_PUBLIC_AD_BANNER_ENABLE_SPECIFY: yup.boolean(),
|
||||
});
|
||||
|
||||
export const userOpsSchema = yup
|
||||
.object()
|
||||
.shape({
|
||||
NEXT_PUBLIC_HAS_USER_OPS: yup.boolean(),
|
||||
NEXT_PUBLIC_USER_OPS_INDEXER_API_HOST: yup
|
||||
.string()
|
||||
.test(urlTest)
|
||||
.when('NEXT_PUBLIC_HAS_USER_OPS', {
|
||||
is: (value: boolean) => value,
|
||||
then: (schema) => schema,
|
||||
otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_USER_OPS_INDEXER_API_HOST can only be used if NEXT_PUBLIC_HAS_USER_OPS is set to \'true\''),
|
||||
}),
|
||||
});
|
||||
|
||||
const deFiDropdownItemSchema: yup.ObjectSchema<DeFiDropdownItem> = yup
|
||||
.object({
|
||||
text: yup.string().required(),
|
||||
icon: yup.string<IconName>(),
|
||||
dappId: yup.string(),
|
||||
url: yup.string().test(urlTest),
|
||||
})
|
||||
.test('oneOfRequired', 'NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS: Either dappId or url is required', function(value) {
|
||||
return Boolean(value.dappId) || Boolean(value.url);
|
||||
}) as yup.ObjectSchema<DeFiDropdownItem>;
|
||||
|
||||
export const defiDropdownSchema = yup.object({
|
||||
NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS: yup
|
||||
.array()
|
||||
.transform(replaceQuotes)
|
||||
.json()
|
||||
.of(deFiDropdownItemSchema),
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
import * as yup from 'yup';
|
||||
|
||||
// External services envs
|
||||
export default yup.object({
|
||||
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: yup.string(),
|
||||
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY: yup.string(),
|
||||
NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: yup.string(),
|
||||
NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY: yup.string(),
|
||||
NEXT_PUBLIC_ROLLBAR_CLIENT_TOKEN: yup.string(),
|
||||
});
|
||||
@@ -0,0 +1,220 @@
|
||||
import * as yup from 'yup';
|
||||
import { CHAIN_INDICATOR_IDS, ChainIndicatorId, HeroBannerConfig, HeroBannerButtonState, HOME_STATS_WIDGET_IDS, HomeStatsWidgetId } from '../../../../types/homepage';
|
||||
import { replaceQuotes } from '../../../../configs/app/utils';
|
||||
import { getYupValidationErrorMessage, urlTest } from '../utils';
|
||||
import { NavigationLayout, NavigationPromoBannerConfig, NavItemExternal } from '../../../../types/client/navigation';
|
||||
import { FeaturedNetwork, NETWORK_GROUPS } from '../../../../types/networks';
|
||||
import { CustomLink, CustomLinksGroup } from '../../../../types/footerLinks';
|
||||
import { COLOR_THEME_IDS } from '../../../../types/settings';
|
||||
import { FontFamily } from '../../../../types/ui';
|
||||
|
||||
const heroBannerButtonStateSchema: yup.ObjectSchema<HeroBannerButtonState> = yup.object({
|
||||
background: yup.array().max(2).of(yup.string()),
|
||||
text_color: yup.array().max(2).of(yup.string()),
|
||||
});
|
||||
|
||||
const heroBannerSchema: yup.ObjectSchema<HeroBannerConfig> = yup.object()
|
||||
.transform(replaceQuotes)
|
||||
.json()
|
||||
.shape({
|
||||
background: yup.array().max(2).of(yup.string()),
|
||||
text_color: yup.array().max(2).of(yup.string()),
|
||||
border: yup.array().max(2).of(yup.string()),
|
||||
button: yup.object({
|
||||
_default: heroBannerButtonStateSchema,
|
||||
_hover: heroBannerButtonStateSchema,
|
||||
_selected: heroBannerButtonStateSchema,
|
||||
}),
|
||||
search: yup.object({
|
||||
border_width: yup.array().max(2).of(yup.string()),
|
||||
}),
|
||||
});
|
||||
|
||||
export const homepageSchema = yup.object({
|
||||
NEXT_PUBLIC_HOMEPAGE_CHARTS: yup
|
||||
.array()
|
||||
.transform(replaceQuotes)
|
||||
.json()
|
||||
.of(yup.string<ChainIndicatorId>().oneOf(CHAIN_INDICATOR_IDS))
|
||||
.test(
|
||||
'stats-api-required',
|
||||
'NEXT_PUBLIC_STATS_API_HOST is required when daily_operational_txs is enabled in NEXT_PUBLIC_HOMEPAGE_CHARTS',
|
||||
function(value) {
|
||||
// daily_operational_txs is presented only in stats microservice
|
||||
if (value?.includes('daily_operational_txs')) {
|
||||
return Boolean(this.parent.NEXT_PUBLIC_STATS_API_HOST);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
),
|
||||
NEXT_PUBLIC_HOMEPAGE_STATS: yup
|
||||
.array()
|
||||
.transform(replaceQuotes)
|
||||
.json()
|
||||
.of(yup.string<HomeStatsWidgetId>().oneOf(HOME_STATS_WIDGET_IDS))
|
||||
.test(
|
||||
'stats-api-required',
|
||||
'NEXT_PUBLIC_STATS_API_HOST is required when total_operational_txs is enabled in NEXT_PUBLIC_HOMEPAGE_STATS',
|
||||
function(value) {
|
||||
// total_operational_txs is presented only in stats microservice
|
||||
if (value?.includes('total_operational_txs')) {
|
||||
return Boolean(this.parent.NEXT_PUBLIC_STATS_API_HOST);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
),
|
||||
NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG: yup
|
||||
.mixed()
|
||||
.test(
|
||||
'shape',
|
||||
(ctx) => {
|
||||
try {
|
||||
heroBannerSchema.validateSync(ctx.originalValue);
|
||||
throw new Error('Unknown validation error');
|
||||
} catch (error: unknown) {
|
||||
const message = getYupValidationErrorMessage(error);
|
||||
return 'Invalid schema were provided for NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG' + (message ? `: ${ message }` : '');
|
||||
}
|
||||
},
|
||||
(data) => {
|
||||
const isUndefined = data === undefined;
|
||||
return isUndefined || heroBannerSchema.isValidSync(data);
|
||||
}),
|
||||
});
|
||||
|
||||
const featuredNetworkSchema: yup.ObjectSchema<FeaturedNetwork> = yup
|
||||
.object()
|
||||
.shape({
|
||||
title: yup.string().required(),
|
||||
url: yup.string().test(urlTest).required(),
|
||||
group: yup.string().oneOf(NETWORK_GROUPS).required(),
|
||||
icon: yup.string().test(urlTest),
|
||||
isActive: yup.boolean(),
|
||||
invertIconInDarkMode: yup.boolean(),
|
||||
});
|
||||
|
||||
const navItemExternalSchema: yup.ObjectSchema<NavItemExternal> = yup
|
||||
.object({
|
||||
text: yup.string().required(),
|
||||
url: yup.string().test(urlTest).required(),
|
||||
});
|
||||
|
||||
export const navigationSchema = yup.object({
|
||||
NEXT_PUBLIC_FEATURED_NETWORKS: yup
|
||||
.array()
|
||||
.json()
|
||||
.of(featuredNetworkSchema),
|
||||
NEXT_PUBLIC_FEATURED_NETWORKS_ALL_LINK: yup
|
||||
.string()
|
||||
.when('NEXT_PUBLIC_FEATURED_NETWORKS', {
|
||||
is: (value: Array<unknown> | undefined) => value && value.length > 0,
|
||||
then: (schema) => schema.test(urlTest),
|
||||
otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_FEATURED_NETWORKS_ALL_LINK can only be set when NEXT_PUBLIC_FEATURED_NETWORKS is configured'),
|
||||
}),
|
||||
NEXT_PUBLIC_FEATURED_NETWORKS_MODE: yup
|
||||
.string()
|
||||
.when('NEXT_PUBLIC_FEATURED_NETWORKS', {
|
||||
is: (value: Array<unknown> | undefined) => value && value.length > 0,
|
||||
then: (schema) => schema.oneOf([ 'tabs', 'list' ]),
|
||||
otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_FEATURED_NETWORKS_MODE can only be set when NEXT_PUBLIC_FEATURED_NETWORKS is configured'),
|
||||
}),
|
||||
NEXT_PUBLIC_OTHER_LINKS: yup
|
||||
.array()
|
||||
.transform(replaceQuotes)
|
||||
.json()
|
||||
.of(navItemExternalSchema),
|
||||
NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES: yup
|
||||
.array()
|
||||
.transform(replaceQuotes)
|
||||
.json()
|
||||
.of(yup.string()),
|
||||
NEXT_PUBLIC_NAVIGATION_LAYOUT: yup.string<NavigationLayout>().oneOf([ 'horizontal', 'vertical' ]),
|
||||
NEXT_PUBLIC_NAVIGATION_PROMO_BANNER_CONFIG: yup
|
||||
.mixed()
|
||||
.test('shape', 'Invalid schema were provided for NEXT_PUBLIC_NAVIGATION_PROMO_BANNER_CONFIG, it should be either object with img_url, text, bg_color, text_color, link_url or object with img_url and link_url', (data) => {
|
||||
const isUndefined = data === undefined;
|
||||
const jsonSchema = yup.object<NavigationPromoBannerConfig>().transform(replaceQuotes).json();
|
||||
|
||||
const valueSchema1 = jsonSchema.shape({
|
||||
img_url: yup.string().required(),
|
||||
text: yup.string().required(),
|
||||
bg_color: yup.object().shape({
|
||||
light: yup.string().required(),
|
||||
dark: yup.string().required(),
|
||||
}).required(),
|
||||
text_color: yup.object().shape({
|
||||
light: yup.string().required(),
|
||||
dark: yup.string().required(),
|
||||
}).required(),
|
||||
link_url: yup.string().required(),
|
||||
});
|
||||
|
||||
const valueSchema2 = jsonSchema.shape({
|
||||
img_url: yup.object().shape({
|
||||
small: yup.string().required(),
|
||||
large: yup.string().required(),
|
||||
}).required(),
|
||||
link_url: yup.string().required(),
|
||||
});
|
||||
|
||||
return isUndefined || valueSchema1.isValidSync(data) || valueSchema2.isValidSync(data);
|
||||
}),
|
||||
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),
|
||||
NEXT_PUBLIC_NETWORK_ICON_DARK: yup.string().test(urlTest),
|
||||
});
|
||||
|
||||
const footerLinkSchema: yup.ObjectSchema<CustomLink> = yup
|
||||
.object({
|
||||
text: yup.string().required(),
|
||||
url: yup.string().test(urlTest).required(),
|
||||
iconUrl: yup.array().of(yup.string().required().test(urlTest)),
|
||||
});
|
||||
|
||||
const footerLinkGroupSchema: yup.ObjectSchema<CustomLinksGroup> = yup
|
||||
.object({
|
||||
title: yup.string().required(),
|
||||
links: yup
|
||||
.array()
|
||||
.of(footerLinkSchema)
|
||||
.required(),
|
||||
});
|
||||
|
||||
export const footerSchema = yup.object({
|
||||
NEXT_PUBLIC_FOOTER_LINKS: yup
|
||||
.array()
|
||||
.json()
|
||||
.of(footerLinkGroupSchema),
|
||||
});
|
||||
|
||||
const fontFamilySchema: yup.ObjectSchema<FontFamily> = yup
|
||||
.object()
|
||||
.transform(replaceQuotes)
|
||||
.json()
|
||||
.shape({
|
||||
name: yup.string().required(),
|
||||
url: yup.string().test(urlTest).required(),
|
||||
});
|
||||
|
||||
export const miscSchema = yup.object({
|
||||
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(),
|
||||
NEXT_PUBLIC_FONT_FAMILY_HEADING: yup
|
||||
.mixed()
|
||||
.test('shape', 'Invalid schema were provided for NEXT_PUBLIC_FONT_FAMILY_HEADING', (data) => {
|
||||
const isUndefined = data === undefined;
|
||||
return isUndefined || fontFamilySchema.isValidSync(data);
|
||||
}),
|
||||
NEXT_PUBLIC_FONT_FAMILY_BODY: yup
|
||||
.mixed()
|
||||
.test('shape', 'Invalid schema were provided for NEXT_PUBLIC_FONT_FAMILY_BODY', (data) => {
|
||||
const isUndefined = data === undefined;
|
||||
return isUndefined || fontFamilySchema.isValidSync(data);
|
||||
}),
|
||||
NEXT_PUBLIC_MAX_CONTENT_WIDTH_ENABLED: yup.boolean(),
|
||||
});
|
||||
@@ -17,14 +17,16 @@ yarn build
|
||||
|
||||
validate_file() {
|
||||
local test_file="$1"
|
||||
local with_common="$2"
|
||||
|
||||
echo
|
||||
echo "🧿 Validating file '$test_file'..."
|
||||
|
||||
dotenv \
|
||||
-e $test_file \
|
||||
-e $common_file \
|
||||
yarn run validate -- --silent
|
||||
if [ "$with_common" = "true" ]; then
|
||||
dotenv -e "$test_file" -e "$common_file" yarn run validate -- --silent
|
||||
else
|
||||
dotenv -e "$test_file" yarn run validate -- --silent
|
||||
fi
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "👍 All good!"
|
||||
@@ -39,7 +41,12 @@ validate_file() {
|
||||
test_files=($(find "$test_folder" -maxdepth 1 -type f | grep -vE '\/\.env\.common$'))
|
||||
|
||||
for file in "${test_files[@]}"; do
|
||||
validate_file "$file"
|
||||
if [[ "$file" == *".env.multichain" ]]; then
|
||||
validate_file "$file" false
|
||||
else
|
||||
validate_file "$file" true
|
||||
fi
|
||||
|
||||
if [ $? -eq 1 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
NEXT_PUBLIC_APP_HOST=localhost
|
||||
NEXT_PUBLIC_NETWORK_NAME=Testnet
|
||||
NEXT_PUBLIC_MULTICHAIN_AGGREGATOR_API_HOST=https://example.com
|
||||
NEXT_PUBLIC_MULTICHAIN_STATS_API_HOST=http://example.com
|
||||
NEXT_PUBLIC_MULTICHAIN_ENABLED=true
|
||||
NEXT_PUBLIC_MULTICHAIN_CLUSTER=test
|
||||
|
||||
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cap']
|
||||
NEXT_PUBLIC_HOMEPAGE_STATS=['total_txs','wallet_addresses']
|
||||
NEXT_PUBLIC_API_DOCS_TABS=[]
|
||||
NEXT_PUBLIC_FEATURED_NETWORKS=http://example.com
|
||||
NEXT_PUBLIC_FOOTER_LINKS=http://example.com
|
||||
NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['linear-gradient(90deg, rgb(232, 52, 53) 0%, rgb(139, 28, 232) 100%)'],'text_color':['rgb(255, 255, 255)']}
|
||||
NEXT_PUBLIC_NETWORK_ICON=http://example.com
|
||||
NEXT_PUBLIC_NETWORK_ICON_DARK=http://example.com
|
||||
NEXT_PUBLIC_NETWORK_LOGO=http://example.com
|
||||
NEXT_PUBLIC_NETWORK_LOGO_DARK=http://example.com
|
||||
NEXT_PUBLIC_NETWORK_NAME=OP Superchain
|
||||
NEXT_PUBLIC_NETWORK_SHORT_NAME=OP Superchain
|
||||
NEXT_PUBLIC_OG_IMAGE_URL=http://example.com
|
||||
NEXT_PUBLIC_GAS_TRACKER_ENABLED=false
|
||||
NEXT_PUBLIC_HIDE_INDEXING_ALERT_BLOCKS=true
|
||||
NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS=true
|
||||
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=false
|
||||
NEXT_PUBLIC_IS_TESTNET=true
|
||||
NEXT_PUBLIC_USE_NEXT_JS_PROXY=true
|
||||
NEXT_PUBLIC_HAS_USER_OPS=true
|
||||
NEXT_PUBLIC_ADVANCED_FILTER_ENABLED=false
|
||||
@@ -0,0 +1,31 @@
|
||||
import * as yup from 'yup';
|
||||
|
||||
export const protocols = [ 'http', 'https' ];
|
||||
|
||||
export const urlTest: yup.TestConfig = {
|
||||
name: 'url',
|
||||
test: (value: unknown) => {
|
||||
if (!value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
if (typeof value === 'string') {
|
||||
new URL(value);
|
||||
return true;
|
||||
}
|
||||
} catch (error) {}
|
||||
|
||||
return false;
|
||||
},
|
||||
message: '${path} is not a valid URL',
|
||||
exclusive: true,
|
||||
};
|
||||
|
||||
export const getYupValidationErrorMessage = (error: unknown) =>
|
||||
typeof error === 'object' &&
|
||||
error !== null &&
|
||||
'errors' in error &&
|
||||
Array.isArray(error.errors) ?
|
||||
error.errors.join(', ') :
|
||||
'';
|
||||
@@ -7,10 +7,11 @@ import * as viemChains from 'viem/chains';
|
||||
import { pick } from 'es-toolkit';
|
||||
|
||||
import { EssentialDappsConfig } from 'types/client/marketplace';
|
||||
import { ChainConfig } from 'types/multichain';
|
||||
import { getEnvValue, parseEnvJson } from 'configs/app/utils';
|
||||
import {uniq } from 'es-toolkit';
|
||||
import { uniq } from 'es-toolkit';
|
||||
import currentChainConfig from 'configs/app';
|
||||
import appConfig from 'configs/app';
|
||||
import { EssentialDappsChainConfig } from 'types/client/marketplace';
|
||||
|
||||
const currentFilePath = fileURLToPath(import.meta.url);
|
||||
const currentDir = dirname(currentFilePath);
|
||||
@@ -36,21 +37,10 @@ async function getChainscoutInfo(externalChainIds: Array<string>, currentChainId
|
||||
}
|
||||
}
|
||||
|
||||
function getSlug(chainName: string) {
|
||||
return chainName.toLowerCase().replace(/ /g, '-').replace(/[^a-z0-9-]/g, '');
|
||||
}
|
||||
|
||||
function trimChainConfig(config: ChainConfig['config'], logoUrl: string | undefined) {
|
||||
function trimChainConfig(config: typeof appConfig, logoUrl: string | undefined) {
|
||||
return {
|
||||
...pick(config, [ 'app', 'chain' ]),
|
||||
apis: pick(config.apis || {}, [ 'general' ]),
|
||||
UI: {
|
||||
navigation: {
|
||||
icon: {
|
||||
'default': logoUrl,
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -110,17 +100,22 @@ async function run() {
|
||||
const explorerUrls = chainscoutInfo.externals.map(({ explorerUrl }) => explorerUrl).filter(Boolean);
|
||||
console.log(`ℹ️ For ${ explorerUrls.length } chains explorer url was found in static config. Fetching parameters for each chain...`);
|
||||
|
||||
const chainConfigs = await Promise.all(explorerUrls.map(computeChainConfig)) as Array<ChainConfig['config']>;
|
||||
const chainConfigs = await Promise.all(explorerUrls.map(computeChainConfig)) as Array<typeof appConfig>;
|
||||
|
||||
const result = {
|
||||
chains: [ currentChainConfig, ...chainConfigs ].map((config, index) => {
|
||||
chains: [ currentChainConfig, ...chainConfigs ].map((config) => {
|
||||
const chainId = config.chain.id;
|
||||
const chainInfo = [...chainscoutInfo.externals, chainscoutInfo.current].find((chain) => chain?.id === chainId);
|
||||
const logoUrl = [...chainscoutInfo.externals, chainscoutInfo.current].find((chain) => chain?.id === config.chain.id)?.logoUrl;
|
||||
const chainName = (config as { chain: { name: string } })?.chain?.name ?? `Chain ${ index + 1 }`;
|
||||
const chainName = (config as { chain: { name: string } })?.chain?.name ?? `Chain ${ chainId }`;
|
||||
return {
|
||||
slug: getSlug(chainName),
|
||||
config: trimChainConfig(config, logoUrl),
|
||||
id: chainId || '',
|
||||
name: chainName,
|
||||
logo: chainInfo?.logoUrl,
|
||||
explorer_url: chainInfo?.explorerUrl || '',
|
||||
app_config: trimChainConfig(config, logoUrl),
|
||||
contracts: Object.values(viemChains).find(({ id }) => id === Number(config.chain.id))?.contracts
|
||||
};
|
||||
} satisfies EssentialDappsChainConfig;
|
||||
}),
|
||||
};
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ function run() {
|
||||
|
||||
const chainName = config.chain.name ?? '';
|
||||
const chainId = config.chain.id ?? '';
|
||||
const generalApiUrl = config.apis.general.endpoint + config.apis.general.basePath;
|
||||
const generalApiUrl = config.apis.general? config.apis.general.endpoint + config.apis.general.basePath : '';
|
||||
const statsApiUrl = config.apis.stats ? config.apis.stats.endpoint + config.apis.stats.basePath : undefined;
|
||||
|
||||
const rollupFeature = config.features.rollup;
|
||||
|
||||
@@ -3,6 +3,8 @@ import { dirname, resolve as resolvePath } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { Worker } from 'node:worker_threads';
|
||||
|
||||
import { ClusterChainConfig } from 'types/multichain';
|
||||
|
||||
const currentFilePath = fileURLToPath(import.meta.url);
|
||||
const currentDir = dirname(currentFilePath);
|
||||
|
||||
@@ -21,6 +23,19 @@ function getSlug(chainName: string) {
|
||||
return chainName.toLowerCase().replace(/ /g, '-').replace(/[^a-z0-9-]/g, '');
|
||||
}
|
||||
|
||||
async function getChainscoutInfo(chainIds: Array<string>) {
|
||||
const response = await fetch('https://chains.blockscout.com/api/chains');
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch chains info from Chainscout API`);
|
||||
}
|
||||
const chainsInfo = await response.json() as Record<string, { explorers: [ { url: string } ], logo: string }>;
|
||||
|
||||
return chainIds.map((chainId) => ({
|
||||
id: chainId,
|
||||
logoUrl: chainsInfo[chainId]?.logo,
|
||||
}))
|
||||
}
|
||||
|
||||
async function computeChainConfig(url: string): Promise<unknown> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const workerPath = resolvePath(currentDir, 'worker.js');
|
||||
@@ -48,8 +63,9 @@ async function computeChainConfig(url: string): Promise<unknown> {
|
||||
}
|
||||
|
||||
async function getExplorerUrls() {
|
||||
// return EXPLORER_URLS;
|
||||
try {
|
||||
const basePath = (process.env.NEXT_PUBLIC_MULTICHAIN_AGGREGATOR_BASE_PATH ?? '') + '/chains';
|
||||
const basePath = `/api/v1/clusters/${ process.env.NEXT_PUBLIC_MULTICHAIN_CLUSTER }/chains`;
|
||||
const url = new URL(basePath, process.env.NEXT_PUBLIC_MULTICHAIN_AGGREGATOR_API_HOST);
|
||||
|
||||
const response = await fetch(url.toString());
|
||||
@@ -79,14 +95,20 @@ async function run() {
|
||||
}
|
||||
|
||||
const configs = await Promise.all(explorerUrls.map(computeChainConfig));
|
||||
const chainscoutInfo = await getChainscoutInfo(configs.map((config) => config.chain.id));
|
||||
|
||||
const config = {
|
||||
chains: configs.map((config, index) => {
|
||||
const chainName = (config as { chain: { name: string } })?.chain?.name ?? `Chain ${ index + 1 }`;
|
||||
const chainId = config.chain.id;
|
||||
const chainName = (config as { chain: { name: string } })?.chain?.name ?? `Chain ${ chainId }`;
|
||||
return {
|
||||
id: chainId,
|
||||
name: chainName,
|
||||
logo: chainscoutInfo.find((chain) => chain.id === chainId)?.logoUrl,
|
||||
explorer_url: explorerUrls[index],
|
||||
slug: getSlug(chainName),
|
||||
config,
|
||||
};
|
||||
app_config: config,
|
||||
} satisfies ClusterChainConfig;
|
||||
}),
|
||||
};
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ module.exports = {
|
||||
}
|
||||
break;
|
||||
case '/api-docs':
|
||||
if (process.env.NEXT_PUBLIC_API_SPEC_URL === 'none') {
|
||||
if (process.env.NEXT_PUBLIC_API_DOCS_TABS === '[]') {
|
||||
return null;
|
||||
}
|
||||
break;
|
||||
@@ -161,6 +161,34 @@ module.exports = {
|
||||
return null;
|
||||
}
|
||||
break;
|
||||
case '/epochs':
|
||||
if (process.env.NEXT_PUBLIC_CELO_ENABLED !== 'true') {
|
||||
return null;
|
||||
}
|
||||
break;
|
||||
case '/operations':
|
||||
if (!process.env.NEXT_PUBLIC_TAC_OPERATION_LIFECYCLE_API_HOST) {
|
||||
return null;
|
||||
}
|
||||
break;
|
||||
case '/public-tags/submit':
|
||||
if (!process.env.NEXT_PUBLIC_ADMIN_SERVICE_API_HOST || !process.env.NEXT_PUBLIC_METADATA_SERVICE_API_HOST) {
|
||||
return null;
|
||||
}
|
||||
break;
|
||||
case '/txn-withdrawals':
|
||||
if (!process.env.NEXT_PUBLIC_ROLLUP_TYPE || process.env.NEXT_PUBLIC_ROLLUP_TYPE !== 'arbitrum') {
|
||||
return null;
|
||||
}
|
||||
break;
|
||||
// disabled routes for multichain
|
||||
case '/block/countdown':
|
||||
case '/contract-verification':
|
||||
case '/visualize/sol2uml':
|
||||
if (process.env.NEXT_PUBLIC_MULTICHAIN_ENABLED === 'true') {
|
||||
return null;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -172,7 +200,7 @@ module.exports = {
|
||||
};
|
||||
},
|
||||
additionalPaths: async(config) => {
|
||||
if(process.env.NEXT_PUBLIC_OP_SUPERCHAIN_ENABLED === 'true'){
|
||||
if(process.env.NEXT_PUBLIC_MULTICHAIN_ENABLED === 'true'){
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -54,3 +54,4 @@ frontend:
|
||||
envFromSecret:
|
||||
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: ref+vault://deployment-values/blockscout/dev/review-l2?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID
|
||||
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY: ref+vault://deployment-values/blockscout/eth-sepolia/testnet?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/RE_CAPTCHA_CLIENT_KEY
|
||||
NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN: ref+vault://deployment-values/blockscout/common?token_env=VAULT_TOKEN&address=https://vault.k8s.blockscout.com#/NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN
|
||||
|
||||
+18
-3
@@ -79,7 +79,8 @@ All json-like values should be single-quoted. If it contains a hash (`#`) or a d
|
||||
- [DEX pools](#dex-pools)
|
||||
- [Flashblocks](#flashblocks)
|
||||
- [Address 3rd party widgets](#address-3rd-party-widgets)
|
||||
- [ZetaChain](#zetachain)
|
||||
- [ZetaChain](#zetachain-cross-chain-transactions)
|
||||
- [Multichain explorer](#multichain-explorer)
|
||||
- [3rd party services configuration](#external-services-configuration)
|
||||
|
||||
|
||||
@@ -105,7 +106,7 @@ All json-like values should be single-quoted. If it contains a hash (`#`) or a d
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| NEXT_PUBLIC_NETWORK_NAME | `string` | Displayed name of the network | Required | - | `Gnosis Chain` | v1.0.x+ |
|
||||
| NEXT_PUBLIC_NETWORK_SHORT_NAME | `string` | Used for SEO attributes (e.g, page description) | - | - | `OoG` | v1.0.x+ |
|
||||
| NEXT_PUBLIC_NETWORK_ID | `number` | Chain id, see [https://chainlist.org](https://chainlist.org) for the reference | Required | - | `99` | v1.0.x+ |
|
||||
| NEXT_PUBLIC_NETWORK_ID | `number` | Chain id, see [https://chainlist.org](https://chainlist.org) for the reference | Required (except for multichain) | - | `99` | v1.0.x+ |
|
||||
| NEXT_PUBLIC_NETWORK_RPC_URL | `string \| Array<string>` | Chain public RPC server url, see [https://chainlist.org](https://chainlist.org) for the reference. Can contain a single string value, or an array of urls. | - | - | `https://core.poa.network` | v1.0.x+ |
|
||||
| NEXT_PUBLIC_NETWORK_CURRENCY_NAME | `string` | Network currency name | - | - | `Ether` | v1.0.x+ |
|
||||
| NEXT_PUBLIC_NETWORK_CURRENCY_WEI_NAME | `string` | Name of the smallest unit of the native currency (e.g., 'wei' for Ethereum, where 1 ETH = 10^18 wei). Used for displaying gas prices and transaction fees in the smallest denomination. | - | `wei` | `duck` | v1.23.0+ |
|
||||
@@ -125,7 +126,7 @@ All json-like values should be single-quoted. If it contains a hash (`#`) or a d
|
||||
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| NEXT_PUBLIC_API_PROTOCOL | `http \| https` | Main API protocol | - | `https` | `http` | v1.0.x+ |
|
||||
| NEXT_PUBLIC_API_HOST | `string` | Main API host | Required | - | `blockscout.com` | v1.0.x+ |
|
||||
| NEXT_PUBLIC_API_HOST | `string` | Main API host | Required (except for multichain) | - | `blockscout.com` | v1.0.x+ |
|
||||
| NEXT_PUBLIC_API_PORT | `number` | Port where API is running on the host | - | - | `3001` | v1.0.x+ |
|
||||
| NEXT_PUBLIC_API_BASE_PATH | `string` | Base path for Main API endpoint url | - | - | `/poa/core` | v1.0.x+ |
|
||||
| NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL | `ws \| wss` | Main API websocket protocol | - | `wss` | `ws` | v1.0.x+ |
|
||||
@@ -1023,6 +1024,19 @@ This feature enables cross-chain transactions pages and views on ZetaChain insta
|
||||
| tx_url_template | `string` | Transaction url template on external explorer. `{hash}` will be replaced with the transaction hash | - | - | `'https://external.explorer.com/tx/{hash}'` |
|
||||
|
||||
|
||||
|
||||
|
||||
### Multichain explorer
|
||||
|
||||
This feature enables the application to act as an explorer of multiple blockchains united in one cluster. Please note that this feature is currently in demo mode, and a major part of the cross-chain views is not implemented and serves as a placeholder. These will be developed in the future.
|
||||
|
||||
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| NEXT_PUBLIC_MULTICHAIN_ENABLED | `boolean` | The flag that enables the feature | Required | - | `true` | upcoming |
|
||||
| NEXT_PUBLIC_MULTICHAIN_CLUSTER | `string` | Chain's cluster name; used to construct the full URL for requests to the aggregator API service | Required | - | `interop` | upcoming |
|
||||
| NEXT_PUBLIC_MULTICHAIN_AGGREGATOR_API_HOST | `string` | Multichain aggregator API service host | Required | - | `https://multichain-aggregator.k8s-dev.blockscout.com` | upcoming |
|
||||
| NEXT_PUBLIC_MULTICHAIN_STATS_API_HOST | `string` | Multichain statistics API service host | Required | - | `http://multichain-search-stats.k8s-dev.blockscout.com` | upcoming |
|
||||
|
||||
|
||||
|
||||
### Badge claim link
|
||||
@@ -1031,6 +1045,7 @@ This feature enables cross-chain transactions pages and views on ZetaChain insta
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| NEXT_PUBLIC_GAME_BADGE_CLAIM_LINK | `string` | Provide to enable the easter egg badge feature | - | - | `https://example.com` | v1.37.0+ |
|
||||
|
||||
|
||||
|
||||
### Puzzle game badge claim link
|
||||
|
||||
|
||||
Vendored
+2
-1
@@ -1,3 +1,4 @@
|
||||
import type { EssentialDappsChainConfig } from 'types/client/marketplace';
|
||||
import type { MultichainConfig } from 'types/multichain';
|
||||
import type { WalletProvider } from 'types/web3';
|
||||
|
||||
@@ -21,7 +22,7 @@ declare global {
|
||||
abkw: string;
|
||||
__envs: Record<string, string>;
|
||||
__multichainConfig: MultichainConfig;
|
||||
__essentialDappsChains: MultichainConfig;
|
||||
__essentialDappsChains: { chains: Array<EssentialDappsChainConfig> };
|
||||
}
|
||||
|
||||
namespace NodeJS {
|
||||
|
||||
@@ -11,8 +11,8 @@ export default function useAddressMetadataInfoQuery(addresses: Array<string>, is
|
||||
const resource = 'metadata:info';
|
||||
|
||||
const multichainContext = useMultichainContext();
|
||||
const feature = multichainContext?.chain?.config.features.addressMetadata || config.features.addressMetadata;
|
||||
const chainId = multichainContext?.chain?.config.chain.id || config.chain.id;
|
||||
const feature = multichainContext?.chain?.app_config.features.addressMetadata || config.features.addressMetadata;
|
||||
const chainId = multichainContext?.chain?.app_config.chain.id || config.chain.id;
|
||||
|
||||
return useApiQuery<typeof resource, unknown, AddressMetadataInfoFormatted>(resource, {
|
||||
queryParams: {
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import { compile } from 'path-to-regexp';
|
||||
|
||||
import type { ChainConfig } from 'types/multichain';
|
||||
import type { ExternalChainExtended } from 'types/externalChains';
|
||||
|
||||
import config from 'configs/app';
|
||||
|
||||
@@ -13,7 +13,7 @@ export default function buildUrl<R extends ResourceName>(
|
||||
pathParams?: ResourcePathParams<R>,
|
||||
queryParams?: Record<string, string | Array<string> | number | boolean | null | undefined>,
|
||||
noProxy?: boolean,
|
||||
chain?: ChainConfig,
|
||||
chain?: ExternalChainExtended,
|
||||
): string {
|
||||
const { api, resource } = getResourceParams(resourceFullName, chain);
|
||||
const baseUrl = !noProxy && isNeedProxy() ? config.app.baseUrl : api.endpoint;
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import type { ApiName, ApiResource } from './types';
|
||||
import type { ChainConfig } from 'types/multichain';
|
||||
import type { ExternalChainExtended } from 'types/externalChains';
|
||||
|
||||
import config from 'configs/app';
|
||||
|
||||
import type { ResourceName } from './resources';
|
||||
import { RESOURCES } from './resources';
|
||||
|
||||
export default function getResourceParams(resourceFullName: ResourceName, chain?: ChainConfig) {
|
||||
export default function getResourceParams(resourceFullName: ResourceName, chain?: ExternalChainExtended) {
|
||||
const [ apiName, resourceName ] = resourceFullName.split(':') as [ ApiName, string ];
|
||||
|
||||
const apiConfig = (() => {
|
||||
if (chain) {
|
||||
return chain.config.apis[apiName];
|
||||
if (chain?.app_config?.apis) {
|
||||
return chain.app_config.apis[apiName as keyof typeof chain.app_config.apis];
|
||||
}
|
||||
|
||||
return config.apis[apiName];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import appConfig from 'configs/app';
|
||||
|
||||
export default function getSocketUrl(config: typeof appConfig = appConfig) {
|
||||
return `${ config.apis.general.socketEndpoint }${ config.apis.general.basePath ?? '' }/socket/v2`;
|
||||
return config.apis.general ? `${ config.apis.general.socketEndpoint }${ config.apis.general.basePath ?? '' }/socket/v2` : undefined;
|
||||
}
|
||||
|
||||
+13
-5
@@ -12,8 +12,14 @@ import { GENERAL_API_RESOURCES } from './services/general';
|
||||
import type { GeneralApiResourceName, GeneralApiResourcePayload, GeneralApiPaginationFilters, GeneralApiPaginationSorting } from './services/general';
|
||||
import type { MetadataApiResourceName, MetadataApiResourcePayload } from './services/metadata';
|
||||
import { METADATA_API_RESOURCES } from './services/metadata';
|
||||
import type { MultichainApiPaginationFilters, MultichainApiResourceName, MultichainApiResourcePayload } from './services/multichain';
|
||||
import { MULTICHAIN_API_RESOURCES } from './services/multichain';
|
||||
import type {
|
||||
MultichainAggregatorApiPaginationFilters,
|
||||
MultichainAggregatorApiResourceName,
|
||||
MultichainAggregatorApiResourcePayload,
|
||||
} from './services/multichainAggregator';
|
||||
import { MULTICHAIN_AGGREGATOR_API_RESOURCES } from './services/multichainAggregator';
|
||||
import type { MultichainStatsApiResourcePayload, MultichainStatsApiResourceName } from './services/multichainStats';
|
||||
import { MULTICHAIN_STATS_API_RESOURCES } from './services/multichainStats';
|
||||
import type { RewardsApiResourceName, RewardsApiResourcePayload } from './services/rewards';
|
||||
import { REWARDS_API_RESOURCES } from './services/rewards';
|
||||
import type { StatsApiResourceName, StatsApiResourcePayload } from './services/stats';
|
||||
@@ -38,7 +44,8 @@ export const RESOURCES = {
|
||||
contractInfo: CONTRACT_INFO_API_RESOURCES,
|
||||
general: GENERAL_API_RESOURCES,
|
||||
metadata: METADATA_API_RESOURCES,
|
||||
multichain: MULTICHAIN_API_RESOURCES,
|
||||
multichainAggregator: MULTICHAIN_AGGREGATOR_API_RESOURCES,
|
||||
multichainStats: MULTICHAIN_STATS_API_RESOURCES,
|
||||
rewards: REWARDS_API_RESOURCES,
|
||||
stats: STATS_API_RESOURCES,
|
||||
tac: TAC_OPERATION_LIFECYCLE_API_RESOURCES,
|
||||
@@ -73,7 +80,8 @@ R extends ClustersApiResourceName ? ClustersApiResourcePayload<R> :
|
||||
R extends ContractInfoApiResourceName ? ContractInfoApiResourcePayload<R> :
|
||||
R extends GeneralApiResourceName ? GeneralApiResourcePayload<R> :
|
||||
R extends MetadataApiResourceName ? MetadataApiResourcePayload<R> :
|
||||
R extends MultichainApiResourceName ? MultichainApiResourcePayload<R> :
|
||||
R extends MultichainAggregatorApiResourceName ? MultichainAggregatorApiResourcePayload<R> :
|
||||
R extends MultichainStatsApiResourceName ? MultichainStatsApiResourcePayload<R> :
|
||||
R extends RewardsApiResourceName ? RewardsApiResourcePayload<R> :
|
||||
R extends StatsApiResourceName ? StatsApiResourcePayload<R> :
|
||||
R extends TacOperationLifecycleApiResourceName ? TacOperationLifecycleApiResourcePayload<R> :
|
||||
@@ -110,7 +118,7 @@ R extends BensApiResourceName ? BensApiPaginationFilters<R> :
|
||||
R extends ClustersApiResourceName ? ClustersApiPaginationFilters :
|
||||
R extends GeneralApiResourceName ? GeneralApiPaginationFilters<R> :
|
||||
R extends ContractInfoApiResourceName ? ContractInfoApiPaginationFilters<R> :
|
||||
R extends MultichainApiResourceName ? MultichainApiPaginationFilters<R> :
|
||||
R extends MultichainAggregatorApiResourceName ? MultichainAggregatorApiPaginationFilters<R> :
|
||||
R extends TacOperationLifecycleApiResourceName ? TacOperationLifecycleApiPaginationFilters<R> :
|
||||
R extends ZetaChainApiResourceName ? ZetaChainApiPaginationFilters<R> :
|
||||
never;
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import type { ApiResource } from '../types';
|
||||
import type * as multichain from '@blockscout/multichain-aggregator-types';
|
||||
|
||||
export const MULTICHAIN_API_RESOURCES = {
|
||||
interop_messages: {
|
||||
path: '/messages',
|
||||
filterFields: [ 'address' as const ],
|
||||
paginated: true,
|
||||
},
|
||||
interop_messages_count: {
|
||||
path: '/messages/count',
|
||||
filterFields: [ 'address' as const ],
|
||||
},
|
||||
} satisfies Record<string, ApiResource>;
|
||||
|
||||
export type MultichainApiResourceName = `multichain:${ keyof typeof MULTICHAIN_API_RESOURCES }`;
|
||||
|
||||
/* eslint-disable @stylistic/indent */
|
||||
export type MultichainApiResourcePayload<R extends MultichainApiResourceName> =
|
||||
R extends 'multichain:interop_messages' ? multichain.ListInteropMessagesResponse :
|
||||
R extends 'multichain:interop_messages_count' ? multichain.CountInteropMessagesResponse :
|
||||
never;
|
||||
/* eslint-enable @stylistic/indent */
|
||||
|
||||
/* eslint-disable @stylistic/indent */
|
||||
export type MultichainApiPaginationFilters<R extends MultichainApiResourceName> =
|
||||
R extends 'multichain:interop_messages' ? Partial<multichain.ListInteropMessagesRequest> :
|
||||
R extends 'multichain:interop_messages_count' ? Partial<multichain.CountInteropMessagesRequest> :
|
||||
never;
|
||||
/* eslint-enable @stylistic/indent */
|
||||
@@ -0,0 +1,85 @@
|
||||
import type { ApiResource } from '../types';
|
||||
import type * as multichain from '@blockscout/multichain-aggregator-types';
|
||||
import type { AddressTokensResponse, TokensResponse } from 'types/client/multichain-aggregator';
|
||||
|
||||
export const MULTICHAIN_AGGREGATOR_API_RESOURCES = {
|
||||
address: {
|
||||
path: '/addresses/:hash',
|
||||
pathParams: [ 'hash' as const ],
|
||||
},
|
||||
address_tokens: {
|
||||
path: '/addresses/:hash/tokens',
|
||||
pathParams: [ 'hash' as const ],
|
||||
paginated: true,
|
||||
filterFields: [ 'chain_id' as const, 'type' as const ],
|
||||
},
|
||||
tokens: {
|
||||
path: '/tokens',
|
||||
filterFields: [ 'chain_id' as const, 'type' as const, 'query' as const ],
|
||||
paginated: true,
|
||||
},
|
||||
quick_search: {
|
||||
path: '/search\\:quick',
|
||||
filterFields: [ 'q' as const ],
|
||||
},
|
||||
search_addresses: {
|
||||
path: '/search/addresses',
|
||||
filterFields: [ 'q' as const, 'chain_id' as const ],
|
||||
paginated: true,
|
||||
},
|
||||
search_blocks: {
|
||||
path: '/search/blocks',
|
||||
filterFields: [ 'q' as const, 'chain_id' as const ],
|
||||
paginated: true,
|
||||
},
|
||||
search_block_numbers: {
|
||||
path: '/search/block-numbers',
|
||||
filterFields: [ 'q' as const, 'chain_id' as const ],
|
||||
paginated: true,
|
||||
},
|
||||
search_transactions: {
|
||||
path: '/search/transactions',
|
||||
filterFields: [ 'q' as const, 'chain_id' as const ],
|
||||
paginated: true,
|
||||
},
|
||||
search_tokens: {
|
||||
path: '/search/tokens',
|
||||
filterFields: [ 'q' as const, 'chain_id' as const ],
|
||||
paginated: true,
|
||||
},
|
||||
search_nfts: {
|
||||
path: '/search/nfts',
|
||||
filterFields: [ 'q' as const, 'chain_id' as const ],
|
||||
paginated: true,
|
||||
},
|
||||
search_domains: {
|
||||
path: '/search/domains',
|
||||
filterFields: [ 'q' as const, 'chain_id' as const ],
|
||||
paginated: true,
|
||||
},
|
||||
} satisfies Record<string, ApiResource>;
|
||||
|
||||
export type MultichainAggregatorApiResourceName = `multichainAggregator:${ keyof typeof MULTICHAIN_AGGREGATOR_API_RESOURCES }`;
|
||||
|
||||
/* eslint-disable @stylistic/indent */
|
||||
export type MultichainAggregatorApiResourcePayload<R extends MultichainAggregatorApiResourceName> =
|
||||
R extends 'multichainAggregator:address' ? multichain.GetAddressResponse :
|
||||
R extends 'multichainAggregator:address_tokens' ? AddressTokensResponse :
|
||||
R extends 'multichainAggregator:tokens' ? TokensResponse :
|
||||
R extends 'multichainAggregator:quick_search' ? multichain.ClusterQuickSearchResponse :
|
||||
R extends 'multichainAggregator:search_addresses' ? multichain.SearchAddressesResponse :
|
||||
R extends 'multichainAggregator:search_blocks' ? multichain.SearchBlocksResponse :
|
||||
R extends 'multichainAggregator:search_block_numbers' ? multichain.SearchBlockNumbersResponse :
|
||||
R extends 'multichainAggregator:search_transactions' ? multichain.SearchTransactionsResponse :
|
||||
R extends 'multichainAggregator:search_tokens' ? multichain.SearchTokensResponse :
|
||||
R extends 'multichainAggregator:search_nfts' ? multichain.SearchNftsResponse :
|
||||
R extends 'multichainAggregator:search_domains' ? multichain.SearchDomainsResponse :
|
||||
never;
|
||||
/* eslint-enable @stylistic/indent */
|
||||
|
||||
/* eslint-disable @stylistic/indent */
|
||||
export type MultichainAggregatorApiPaginationFilters<R extends MultichainAggregatorApiResourceName> =
|
||||
R extends 'multichainAggregator:address_tokens' ? Partial<multichain.ListAddressTokensRequest> :
|
||||
R extends 'multichainAggregator:tokens' ? Partial<multichain.ListClusterTokensRequest> :
|
||||
never;
|
||||
/* eslint-enable @stylistic/indent */
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { ApiResource } from '../types';
|
||||
import type * as stats from '@blockscout/stats-types';
|
||||
|
||||
export const MULTICHAIN_STATS_API_RESOURCES = {
|
||||
pages_main: {
|
||||
path: '/api/v1/pages/multichain/main',
|
||||
},
|
||||
} satisfies Record<string, ApiResource>;
|
||||
|
||||
export type MultichainStatsApiResourceName = `multichainStats:${ keyof typeof MULTICHAIN_STATS_API_RESOURCES }`;
|
||||
|
||||
/* eslint-disable @stylistic/indent */
|
||||
export type MultichainStatsApiResourcePayload<R extends MultichainStatsApiResourceName> =
|
||||
R extends 'multichainStats:pages_main' ? stats.MainPageMultichainStats :
|
||||
never;
|
||||
/* eslint-enable @stylistic/indent */
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
export type ApiName =
|
||||
'general' | 'admin' | 'bens' | 'contractInfo' | 'clusters' | 'external' |
|
||||
'metadata' | 'multichain' | 'rewards' | 'stats' | 'tac' |
|
||||
'metadata' | 'multichainAggregator' | 'multichainStats' | 'rewards' | 'stats' | 'tac' |
|
||||
'userOps' | 'visualize' | 'zetachain';
|
||||
|
||||
export interface ApiResource {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { omit, pickBy } from 'es-toolkit';
|
||||
import React from 'react';
|
||||
|
||||
import type { CsrfData } from 'types/client/account';
|
||||
import type { ChainConfig } from 'types/multichain';
|
||||
import type { ExternalChainExtended } from 'types/externalChains';
|
||||
|
||||
import isBodyAllowed from 'lib/api/isBodyAllowed';
|
||||
import isNeedProxy from 'lib/api/isNeedProxy';
|
||||
@@ -21,7 +21,7 @@ export interface Params<R extends ResourceName> {
|
||||
queryParams?: Record<string, string | Array<string> | number | boolean | undefined | null>;
|
||||
fetchParams?: Pick<FetchParams, 'body' | 'method' | 'signal' | 'headers'>;
|
||||
logError?: boolean;
|
||||
chain?: ChainConfig;
|
||||
chain?: ExternalChainExtended;
|
||||
}
|
||||
|
||||
export default function useApiFetch() {
|
||||
|
||||
@@ -15,22 +15,24 @@ export interface Params<R extends PaginatedResourceName> {
|
||||
// eslint-disable-next-line max-len
|
||||
queryOptions?: Omit<UseInfiniteQueryOptions<TQueryData<R>, TError, InfiniteData<TQueryData<R>>, TQueryData<R>, QueryKey, TPageParam<R>>, 'queryKey' | 'queryFn' | 'getNextPageParam' | 'initialPageParam'>;
|
||||
pathParams?: ApiFetchParams<R>['pathParams'];
|
||||
queryParams?: Record<string, string | Array<string> | number | boolean | undefined | null>;
|
||||
}
|
||||
|
||||
type ReturnType<Resource extends PaginatedResourceName> = UseInfiniteQueryResult<InfiniteData<ResourcePayload<Resource>>, ResourceError<unknown>>;
|
||||
export type ReturnType<Resource extends PaginatedResourceName> = UseInfiniteQueryResult<InfiniteData<ResourcePayload<Resource>>, ResourceError<unknown>>;
|
||||
|
||||
export default function useApiInfiniteQuery<R extends PaginatedResourceName>({
|
||||
resourceName,
|
||||
queryOptions,
|
||||
pathParams,
|
||||
queryParams,
|
||||
}: Params<R>): ReturnType<R> {
|
||||
const apiFetch = useApiFetch();
|
||||
|
||||
return useInfiniteQuery<TQueryData<R>, TError, InfiniteData<TQueryData<R>>, QueryKey, TPageParam<R>>({
|
||||
queryKey: getResourceKey(resourceName, { pathParams }),
|
||||
queryKey: getResourceKey(resourceName, { pathParams, queryParams }),
|
||||
queryFn: (context) => {
|
||||
const queryParams = 'pageParam' in context ? (context.pageParam || undefined) : undefined;
|
||||
return apiFetch(resourceName, { pathParams, queryParams }) as Promise<TQueryData<R>>;
|
||||
const nextQueryParams = 'pageParam' in context ? (context.pageParam || undefined) : undefined;
|
||||
return apiFetch(resourceName, { pathParams, queryParams: { ...queryParams, ...nextQueryParams } }) as Promise<TQueryData<R>>;
|
||||
},
|
||||
initialPageParam: null,
|
||||
getNextPageParam: (lastPage) => {
|
||||
|
||||
+11
-13
@@ -1,9 +1,8 @@
|
||||
import type { UseQueryOptions } from '@tanstack/react-query';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import type { ChainConfig } from 'types/multichain';
|
||||
import type { ExternalChainExtended } from 'types/externalChains';
|
||||
|
||||
import multichainConfig from 'configs/multichain';
|
||||
import { useMultichainContext } from 'lib/contexts/multichain';
|
||||
import type { Params as FetchParams } from 'lib/hooks/useFetch';
|
||||
|
||||
@@ -12,37 +11,36 @@ import useApiFetch from './useApiFetch';
|
||||
|
||||
export interface Params<R extends ResourceName, E = unknown, D = ResourcePayload<R>> {
|
||||
pathParams?: ResourcePathParams<R>;
|
||||
queryParams?: Record<string, string | Array<string> | number | boolean | undefined>;
|
||||
queryParams?: Record<string, string | Array<string> | number | boolean | undefined | null>;
|
||||
fetchParams?: Pick<FetchParams, 'body' | 'method' | 'headers'>;
|
||||
queryOptions?: Partial<Omit<UseQueryOptions<ResourcePayload<R>, ResourceError<E>, D>, 'queryFn'>>;
|
||||
logError?: boolean;
|
||||
chainSlug?: string;
|
||||
chain?: ChainConfig;
|
||||
chain?: ExternalChainExtended;
|
||||
}
|
||||
|
||||
export interface GetResourceKeyParams<R extends ResourceName, E = unknown, D = ResourcePayload<R>>
|
||||
extends Pick<Params<R, E, D>, 'pathParams' | 'queryParams'> {
|
||||
chainSlug?: string;
|
||||
chainId?: string;
|
||||
}
|
||||
|
||||
export function getResourceKey<R extends ResourceName>(resource: R, { pathParams, queryParams, chainSlug }: GetResourceKeyParams<R> = {}) {
|
||||
export function getResourceKey<R extends ResourceName>(resource: R, { pathParams, queryParams, chainId }: GetResourceKeyParams<R> = {}) {
|
||||
if (pathParams || queryParams) {
|
||||
return [ resource, chainSlug, { ...pathParams, ...queryParams } ].filter(Boolean);
|
||||
return [ resource, chainId, { ...pathParams, ...queryParams } ].filter(Boolean);
|
||||
}
|
||||
|
||||
return [ resource, chainSlug ].filter(Boolean);
|
||||
return [ resource, chainId ].filter(Boolean);
|
||||
}
|
||||
|
||||
export default function useApiQuery<R extends ResourceName, E = unknown, D = ResourcePayload<R>>(
|
||||
resource: R,
|
||||
{ queryOptions, pathParams, queryParams, fetchParams, logError, chainSlug, chain: chainProp }: Params<R, E, D> = {},
|
||||
{ queryOptions, pathParams, queryParams, fetchParams, logError, chain: chainProp }: Params<R, E, D> = {},
|
||||
) {
|
||||
const apiFetch = useApiFetch();
|
||||
const { chain } = useMultichainContext() || (chainProp && { chain: chainProp }) ||
|
||||
{ chain: chainSlug ? multichainConfig()?.chains.find((chain) => chain.slug === chainSlug) : undefined };
|
||||
const multichainContext = useMultichainContext();
|
||||
const chain = chainProp || multichainContext?.chain;
|
||||
|
||||
return useQuery<ResourcePayload<R>, ResourceError<E>, D>({
|
||||
queryKey: queryOptions?.queryKey || getResourceKey(resource, { pathParams, queryParams, chainSlug: chain?.slug }),
|
||||
queryKey: queryOptions?.queryKey || getResourceKey(resource, { pathParams, queryParams, chainId: chain?.id }),
|
||||
queryFn: async({ signal }) => {
|
||||
// all errors and error typing is handled by react-query
|
||||
// so error response will never go to the data
|
||||
|
||||
+19
-23
@@ -1,42 +1,39 @@
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
|
||||
import type { ChainConfig } from 'types/multichain';
|
||||
import type { ClusterChainConfig } from 'types/multichain';
|
||||
|
||||
import multichainConfig from 'configs/multichain';
|
||||
import getChainIdFromSlug from 'lib/multichain/getChainIdFromSlug';
|
||||
import getQueryParamString from 'lib/router/getQueryParamString';
|
||||
|
||||
export type ContextLevel = 'page' | 'tab';
|
||||
|
||||
interface MultichainProviderProps {
|
||||
children: React.ReactNode;
|
||||
chainSlug?: string;
|
||||
level?: ContextLevel;
|
||||
chainId?: string;
|
||||
}
|
||||
|
||||
export interface TMultichainContext {
|
||||
chain: ChainConfig;
|
||||
level?: ContextLevel;
|
||||
chain: ClusterChainConfig;
|
||||
}
|
||||
|
||||
export const MultichainContext = React.createContext<TMultichainContext | null>(null);
|
||||
|
||||
export function MultichainProvider({ children, chainSlug: chainSlugProp, level: levelProp }: MultichainProviderProps) {
|
||||
export function MultichainProvider({ children, chainId: chainIdProp }: MultichainProviderProps) {
|
||||
const router = useRouter();
|
||||
const chainSlugQueryParam = router.pathname.includes('chain-slug') ? getQueryParamString(router.query['chain-slug']) : undefined;
|
||||
const chainSlugQueryParam = router.pathname.includes('chain_slug') ? getQueryParamString(router.query.chain_slug) : undefined;
|
||||
const chainIdQueryParam = router.query.chain_id ? getQueryParamString(router.query.chain_id) : undefined;
|
||||
|
||||
const [ chainSlug, setChainSlug ] = React.useState<string | undefined>(chainSlugProp ?? chainSlugQueryParam);
|
||||
const [ level, setLevel ] = React.useState<ContextLevel | undefined>(levelProp);
|
||||
const [ chainId, setChainId ] = React.useState<string | undefined>(
|
||||
chainIdProp ??
|
||||
chainIdQueryParam ??
|
||||
(chainSlugQueryParam ? getChainIdFromSlug(chainSlugQueryParam) : undefined),
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (chainSlugProp) {
|
||||
setChainSlug(chainSlugProp);
|
||||
if (chainIdProp) {
|
||||
setChainId(chainIdProp);
|
||||
}
|
||||
}, [ chainSlugProp ]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setLevel(levelProp);
|
||||
}, [ levelProp ]);
|
||||
}, [ chainIdProp ]);
|
||||
|
||||
const chain = React.useMemo(() => {
|
||||
const config = multichainConfig();
|
||||
@@ -44,12 +41,12 @@ export function MultichainProvider({ children, chainSlug: chainSlugProp, level:
|
||||
return;
|
||||
}
|
||||
|
||||
if (!chainSlug) {
|
||||
if (!chainId) {
|
||||
return;
|
||||
}
|
||||
|
||||
return config.chains.find((chain) => chain.slug === chainSlug);
|
||||
}, [ chainSlug ]);
|
||||
return config.chains.find((chain) => chain.id === chainId);
|
||||
}, [ chainId ]);
|
||||
|
||||
const value = React.useMemo(() => {
|
||||
if (!chain) {
|
||||
@@ -58,9 +55,8 @@ export function MultichainProvider({ children, chainSlug: chainSlugProp, level:
|
||||
|
||||
return {
|
||||
chain,
|
||||
level,
|
||||
};
|
||||
}, [ chain, level ]);
|
||||
}, [ chain ]);
|
||||
|
||||
return (
|
||||
<MultichainContext.Provider value={ value }>
|
||||
|
||||
@@ -40,7 +40,7 @@ export default function useNavItems(): ReturnType {
|
||||
text: 'Blocks',
|
||||
nextRoute: { pathname: '/blocks' as const },
|
||||
icon: 'block',
|
||||
isActive: pathname === '/blocks' || pathname === '/block/[height_or_hash]' || pathname === '/chain/[chain-slug]/block/[height_or_hash]',
|
||||
isActive: pathname === '/blocks' || pathname === '/block/[height_or_hash]' || pathname === '/chain/[chain_slug]/block/[height_or_hash]',
|
||||
};
|
||||
const txs: NavItem | null = {
|
||||
text: 'Transactions',
|
||||
@@ -50,7 +50,7 @@ export default function useNavItems(): ReturnType {
|
||||
// sorry, but this is how it was designed
|
||||
(pathname === '/txs' && (!config.features.zetachain.isEnabled || !tab || !tab.includes('cctx'))) ||
|
||||
pathname === '/tx/[hash]' ||
|
||||
pathname === '/chain/[chain-slug]/tx/[hash]',
|
||||
pathname === '/chain/[chain_slug]/tx/[hash]',
|
||||
};
|
||||
const cctxs: NavItem | null = config.features.zetachain.isEnabled ? {
|
||||
text: 'Cross-chain transactions',
|
||||
@@ -74,7 +74,7 @@ export default function useNavItems(): ReturnType {
|
||||
text: 'User operations',
|
||||
nextRoute: { pathname: '/ops' as const },
|
||||
icon: 'user_op',
|
||||
isActive: pathname === '/ops' || pathname === '/op/[hash]' || pathname === '/chain/[chain-slug]/op/[hash]',
|
||||
isActive: pathname === '/ops' || pathname === '/op/[hash]' || pathname === '/chain/[chain_slug]/op/[hash]',
|
||||
} : null;
|
||||
|
||||
const verifiedContracts: NavItem | null =
|
||||
@@ -310,7 +310,6 @@ export default function useNavItems(): ReturnType {
|
||||
const otherNavItems: Array<NavItem> | Array<Array<NavItem>> = [
|
||||
config.features.opSuperchain.isEnabled ? {
|
||||
text: 'Verify contract',
|
||||
// TODO @tom2drum adjust URL to Vera
|
||||
url: 'https://vera.blockscout.com',
|
||||
} : {
|
||||
text: 'Verify contract',
|
||||
|
||||
@@ -70,16 +70,17 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
|
||||
'/cc/tx/[hash]': 'Regular page',
|
||||
|
||||
// multichain routes
|
||||
'/chain/[chain-slug]/accounts/label/[slug]': 'Root page',
|
||||
'/chain/[chain-slug]/advanced-filter': 'Regular page',
|
||||
'/chain/[chain-slug]/block/[height_or_hash]': 'Regular page',
|
||||
'/chain/[chain-slug]/block/countdown': 'Regular page',
|
||||
'/chain/[chain-slug]/block/countdown/[height]': 'Regular page',
|
||||
'/chain/[chain-slug]/csv-export': 'Regular page',
|
||||
'/chain/[chain-slug]/op/[hash]': 'Regular page',
|
||||
'/chain/[chain-slug]/token/[hash]': 'Regular page',
|
||||
'/chain/[chain-slug]/token/[hash]/instance/[id]': 'Regular page',
|
||||
'/chain/[chain-slug]/tx/[hash]': 'Regular page',
|
||||
'/chain/[chain_slug]/accounts/label/[slug]': 'Root page',
|
||||
'/chain/[chain_slug]/advanced-filter': 'Regular page',
|
||||
'/chain/[chain_slug]/block/[height_or_hash]': 'Regular page',
|
||||
'/chain/[chain_slug]/block/countdown': 'Regular page',
|
||||
'/chain/[chain_slug]/block/countdown/[height]': 'Regular page',
|
||||
'/chain/[chain_slug]/csv-export': 'Regular page',
|
||||
'/chain/[chain_slug]/op/[hash]': 'Regular page',
|
||||
'/chain/[chain_slug]/token/[hash]': 'Regular page',
|
||||
'/chain/[chain_slug]/token/[hash]/instance/[id]': 'Regular page',
|
||||
'/chain/[chain_slug]/tx/[hash]': 'Regular page',
|
||||
'/chain/[chain_slug]/visualize/sol2uml': 'Regular page',
|
||||
|
||||
// service routes, added only to make typescript happy
|
||||
'/login': 'Regular page',
|
||||
|
||||
@@ -73,16 +73,17 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
|
||||
'/cc/tx/[hash]': DEFAULT_TEMPLATE,
|
||||
|
||||
// multichain routes
|
||||
'/chain/[chain-slug]/accounts/label/[slug]': DEFAULT_TEMPLATE,
|
||||
'/chain/[chain-slug]/advanced-filter': DEFAULT_TEMPLATE,
|
||||
'/chain/[chain-slug]/block/[height_or_hash]': DEFAULT_TEMPLATE,
|
||||
'/chain/[chain-slug]/block/countdown': DEFAULT_TEMPLATE,
|
||||
'/chain/[chain-slug]/block/countdown/[height]': DEFAULT_TEMPLATE,
|
||||
'/chain/[chain-slug]/csv-export': DEFAULT_TEMPLATE,
|
||||
'/chain/[chain-slug]/op/[hash]': DEFAULT_TEMPLATE,
|
||||
'/chain/[chain-slug]/token/[hash]': DEFAULT_TEMPLATE,
|
||||
'/chain/[chain-slug]/token/[hash]/instance/[id]': DEFAULT_TEMPLATE,
|
||||
'/chain/[chain-slug]/tx/[hash]': DEFAULT_TEMPLATE,
|
||||
'/chain/[chain_slug]/accounts/label/[slug]': DEFAULT_TEMPLATE,
|
||||
'/chain/[chain_slug]/advanced-filter': DEFAULT_TEMPLATE,
|
||||
'/chain/[chain_slug]/block/[height_or_hash]': DEFAULT_TEMPLATE,
|
||||
'/chain/[chain_slug]/block/countdown': DEFAULT_TEMPLATE,
|
||||
'/chain/[chain_slug]/block/countdown/[height]': DEFAULT_TEMPLATE,
|
||||
'/chain/[chain_slug]/csv-export': DEFAULT_TEMPLATE,
|
||||
'/chain/[chain_slug]/op/[hash]': DEFAULT_TEMPLATE,
|
||||
'/chain/[chain_slug]/token/[hash]': DEFAULT_TEMPLATE,
|
||||
'/chain/[chain_slug]/token/[hash]/instance/[id]': DEFAULT_TEMPLATE,
|
||||
'/chain/[chain_slug]/tx/[hash]': DEFAULT_TEMPLATE,
|
||||
'/chain/[chain_slug]/visualize/sol2uml': DEFAULT_TEMPLATE,
|
||||
|
||||
// service routes, added only to make typescript happy
|
||||
'/login': DEFAULT_TEMPLATE,
|
||||
|
||||
@@ -74,16 +74,17 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
|
||||
'/cc/tx/[hash]': '%network_name% cross-chain transaction %hash% details',
|
||||
|
||||
// multichain routes
|
||||
'/chain/[chain-slug]/accounts/label/[slug]': '%network_name% addresses search by label',
|
||||
'/chain/[chain-slug]/advanced-filter': '%network_name% advanced filter',
|
||||
'/chain/[chain-slug]/block/[height_or_hash]': '%network_name% block %height_or_hash% details',
|
||||
'/chain/[chain-slug]/block/countdown': '%network_name% block countdown index',
|
||||
'/chain/[chain-slug]/block/countdown/[height]': '%network_name% block %height% countdown',
|
||||
'/chain/[chain-slug]/csv-export': '%network_name% export data to CSV',
|
||||
'/chain/[chain-slug]/op/[hash]': '%network_name% user operation %hash% details',
|
||||
'/chain/[chain-slug]/token/[hash]': '%network_name% token details',
|
||||
'/chain/[chain-slug]/token/[hash]/instance/[id]': '%network_name% token NFT instance',
|
||||
'/chain/[chain-slug]/tx/[hash]': '%network_name% transaction %hash% details',
|
||||
'/chain/[chain_slug]/accounts/label/[slug]': '%network_name% addresses search by label',
|
||||
'/chain/[chain_slug]/advanced-filter': '%network_name% advanced filter',
|
||||
'/chain/[chain_slug]/block/[height_or_hash]': '%network_name% block %height_or_hash% details',
|
||||
'/chain/[chain_slug]/block/countdown': '%network_name% block countdown index',
|
||||
'/chain/[chain_slug]/block/countdown/[height]': '%network_name% block %height% countdown',
|
||||
'/chain/[chain_slug]/csv-export': '%network_name% export data to CSV',
|
||||
'/chain/[chain_slug]/op/[hash]': '%network_name% user operation %hash% details',
|
||||
'/chain/[chain_slug]/token/[hash]': '%network_name% token details',
|
||||
'/chain/[chain_slug]/token/[hash]/instance/[id]': '%network_name% token NFT instance',
|
||||
'/chain/[chain_slug]/tx/[hash]': '%network_name% transaction %hash% details',
|
||||
'/chain/[chain_slug]/visualize/sol2uml': '%network_name% Solidity UML diagram',
|
||||
|
||||
// service routes, added only to make typescript happy
|
||||
'/login': '%network_name% login',
|
||||
|
||||
+11
-10
@@ -68,16 +68,17 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
|
||||
'/cc/tx/[hash]': 'Cross-chain transaction details',
|
||||
|
||||
// multichain routes
|
||||
'/chain/[chain-slug]/accounts/label/[slug]': 'Chain addresses search by label',
|
||||
'/chain/[chain-slug]/advanced-filter': 'Chain advanced filter',
|
||||
'/chain/[chain-slug]/block/[height_or_hash]': 'Chain block details',
|
||||
'/chain/[chain-slug]/block/countdown': 'Chain block countdown index',
|
||||
'/chain/[chain-slug]/block/countdown/[height]': 'Chain block countdown',
|
||||
'/chain/[chain-slug]/csv-export': 'Chain export data to CSV',
|
||||
'/chain/[chain-slug]/op/[hash]': 'Chain user operation details',
|
||||
'/chain/[chain-slug]/token/[hash]': 'Chain token details',
|
||||
'/chain/[chain-slug]/token/[hash]/instance/[id]': 'Chain token NFT instance',
|
||||
'/chain/[chain-slug]/tx/[hash]': 'Chain transaction details',
|
||||
'/chain/[chain_slug]/accounts/label/[slug]': 'Chain addresses search by label',
|
||||
'/chain/[chain_slug]/advanced-filter': 'Chain advanced filter',
|
||||
'/chain/[chain_slug]/block/[height_or_hash]': 'Chain block details',
|
||||
'/chain/[chain_slug]/block/countdown': 'Chain block countdown index',
|
||||
'/chain/[chain_slug]/block/countdown/[height]': 'Chain block countdown',
|
||||
'/chain/[chain_slug]/csv-export': 'Chain export data to CSV',
|
||||
'/chain/[chain_slug]/op/[hash]': 'Chain user operation details',
|
||||
'/chain/[chain_slug]/token/[hash]': 'Chain token details',
|
||||
'/chain/[chain_slug]/token/[hash]/instance/[id]': 'Chain token NFT instance',
|
||||
'/chain/[chain_slug]/tx/[hash]': 'Chain transaction details',
|
||||
'/chain/[chain_slug]/visualize/sol2uml': 'Chain Solidity UML diagram',
|
||||
|
||||
// service routes, added only to make typescript happy
|
||||
'/login': 'Login',
|
||||
|
||||
@@ -12,6 +12,8 @@ import getQueryParamString from 'lib/router/getQueryParamString';
|
||||
|
||||
import * as userProfile from './userProfile';
|
||||
|
||||
const opSuperchainFeature = config.features.opSuperchain;
|
||||
|
||||
export default function useMixpanelInit() {
|
||||
const [ isInited, setIsInited ] = React.useState(false);
|
||||
const router = useRouter();
|
||||
@@ -44,6 +46,7 @@ export default function useMixpanelInit() {
|
||||
Language: window.navigator.language,
|
||||
'Device type': capitalize(deviceType),
|
||||
'User id': uuid,
|
||||
...(opSuperchainFeature.isEnabled ? { 'Cluster name': opSuperchainFeature.cluster } : {}),
|
||||
});
|
||||
mixpanel.identify(uuid);
|
||||
userProfile.set({
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import type * as multichain from '@blockscout/multichain-aggregator-types';
|
||||
|
||||
export function getName(data: { chain_infos: Record<string, multichain.GetAddressResponse_ChainInfo> }) {
|
||||
const chainInfos = Object.values(data.chain_infos ?? {});
|
||||
const isContractChains = chainInfos.filter((chainInfo) => chainInfo.is_contract);
|
||||
const names = isContractChains.map((chainInfo) => chainInfo.contract_name);
|
||||
|
||||
if (names.some((name) => !name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uniqueNames = [ ...new Set(names) ];
|
||||
|
||||
if (uniqueNames.length === 1) {
|
||||
return uniqueNames[0];
|
||||
}
|
||||
}
|
||||
|
||||
export function isContract(data: { chain_infos: Record<string, multichain.GetAddressResponse_ChainInfo> } | undefined) {
|
||||
if (!data) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const chainInfos = Object.values(data.chain_infos ?? {});
|
||||
const isContractChains = chainInfos.filter((chainInfo) => chainInfo.is_contract);
|
||||
return isContractChains.length > chainInfos.length / 2;
|
||||
}
|
||||
|
||||
export function isVerified(data: { chain_infos: Record<string, multichain.GetAddressResponse_ChainInfo> } | undefined) {
|
||||
if (!data) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const chainInfos = Object.values(data.chain_infos ?? {});
|
||||
const isContractChains = chainInfos.filter((chainInfo) => chainInfo.is_contract);
|
||||
const isVerifiedChains = chainInfos.filter((chainInfo) => chainInfo.is_verified);
|
||||
return isVerifiedChains.length > 0 && isVerifiedChains.length === isContractChains.length;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import type { TMultichainContext } from 'lib/contexts/multichain';
|
||||
|
||||
export function getChainDataForList(multichainContext: TMultichainContext | null) {
|
||||
// for now we only show chain icon in the list with chain selector (not in the entire local pages)
|
||||
return multichainContext?.chain && multichainContext.level !== 'page' ? multichainContext.chain : undefined;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import multichainConfig from 'configs/multichain';
|
||||
|
||||
export default function getChainIdFromSlug(slug: string) {
|
||||
const config = multichainConfig();
|
||||
if (!config) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return config.chains.find((chain) => chain.slug === slug)?.id;
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import type { ChainConfig } from 'types/multichain';
|
||||
|
||||
export default function getChainTooltipText(chain: ChainConfig, prefix: string = '') {
|
||||
return `${ prefix }${ chain.config.chain.name } (Chain ID ${ chain.config.chain.id })`;
|
||||
}
|
||||
@@ -3,12 +3,35 @@ import type { Router } from 'next/router';
|
||||
import multichainConfig from 'configs/multichain';
|
||||
import getQueryParamString from 'lib/router/getQueryParamString';
|
||||
|
||||
export default function getChainValueFromQuery(query: Router['query']) {
|
||||
export default function getChainValueFromQuery(
|
||||
query: Router['query'],
|
||||
chainIds?: Array<string>,
|
||||
withAllOption?: boolean,
|
||||
) {
|
||||
const config = multichainConfig();
|
||||
if (!config) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const queryParam = getQueryParamString(query['chain-slug']);
|
||||
return queryParam || config.chains[0].slug;
|
||||
const chainId = getQueryParamString(query.chain_id);
|
||||
const chainSlug = getQueryParamString(query.chain_slug);
|
||||
|
||||
if (chainId) {
|
||||
if (config.chains.some((chain) => chain.id === chainId)) {
|
||||
return chainId;
|
||||
}
|
||||
}
|
||||
|
||||
if (chainSlug) {
|
||||
const chain = config.chains.find((chain) => chain.slug === chainSlug);
|
||||
if (chain) {
|
||||
return chain.id;
|
||||
}
|
||||
}
|
||||
|
||||
if (withAllOption) {
|
||||
return 'all';
|
||||
}
|
||||
|
||||
return config.chains.filter((chain) => !chainIds || (chain.id && chainIds.includes(chain.id)))?.[0]?.id;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import multichainConfig from 'configs/multichain';
|
||||
|
||||
export default function getCurrencySymbol() {
|
||||
const nativeCurrency = multichainConfig()?.chains[0]?.config.chain.currency;
|
||||
const nativeCurrency = multichainConfig()?.chains[0]?.app_config.chain.currency;
|
||||
return nativeCurrency?.symbol;
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import type { ChainConfig } from 'types/multichain';
|
||||
|
||||
export default function getIconUrl(config: ChainConfig) {
|
||||
if (!config.config?.UI?.navigation?.icon || !config.config.app) {
|
||||
return;
|
||||
}
|
||||
|
||||
const iconPath = config.config.UI.navigation.icon.default;
|
||||
|
||||
if (iconPath?.startsWith('http')) {
|
||||
return iconPath;
|
||||
}
|
||||
|
||||
const appUrl = config.config.app.baseUrl;
|
||||
return `${ appUrl }${ iconPath }`;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { pickBy } from 'es-toolkit';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
|
||||
@@ -5,27 +6,40 @@ import getChainValueFromQuery from './getChainValueFromQuery';
|
||||
|
||||
interface Props {
|
||||
persistedParams?: Array<string>;
|
||||
isLoading?: boolean;
|
||||
chainIds?: Array<string>;
|
||||
withAllOption?: boolean;
|
||||
}
|
||||
|
||||
export default function useRoutedChainSelect(props?: Props) {
|
||||
const router = useRouter();
|
||||
|
||||
const [ value, setValue ] = React.useState<Array<string> | undefined>(
|
||||
[ getChainValueFromQuery(router.query) ].filter(Boolean),
|
||||
props?.isLoading ? undefined : [ getChainValueFromQuery(router.query, props?.chainIds, props?.withAllOption) ].filter(Boolean),
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!props?.isLoading) {
|
||||
setValue([ getChainValueFromQuery(router.query, props?.chainIds, props?.withAllOption) ].filter(Boolean));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [ props?.isLoading, props?.chainIds ]);
|
||||
|
||||
const onValueChange = React.useCallback(({ value }: { value: Array<string> }) => {
|
||||
setValue(value);
|
||||
|
||||
const nextQuery = props?.persistedParams ? props.persistedParams.reduce((acc, param) => ({ ...acc, [param]: router.query[param] }), {}) : router.query;
|
||||
const nextQuery = props?.persistedParams ?
|
||||
props.persistedParams.reduce((acc, param) => ({ ...acc, [param]: router.query[param] || undefined }), {}) :
|
||||
router.query;
|
||||
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: {
|
||||
query: pickBy({
|
||||
...nextQuery,
|
||||
'chain-slug': value[0],
|
||||
},
|
||||
chain_id: value[0],
|
||||
}, (value) => value !== undefined),
|
||||
});
|
||||
|
||||
}, [ props?.persistedParams, router ]);
|
||||
|
||||
return React.useMemo(() => ({
|
||||
|
||||
+7
-3
@@ -4,7 +4,11 @@ import appConfig from 'configs/app';
|
||||
import essentialDappsChainsConfig from 'configs/essential-dapps-chains';
|
||||
import multichainConfig from 'configs/multichain';
|
||||
|
||||
const getChainInfo = (config: typeof appConfig = appConfig, contracts?: Chain['contracts']): Chain => {
|
||||
const getChainInfo = (config: Partial<typeof appConfig> = appConfig, contracts?: Chain['contracts']): Chain | undefined => {
|
||||
if (!config.chain || !config.app) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
id: Number(config.chain.id),
|
||||
name: config.chain.name ?? '',
|
||||
@@ -70,7 +74,7 @@ export const clusterChains: Array<Chain> | undefined = (() => {
|
||||
return;
|
||||
}
|
||||
|
||||
return config.chains.map(({ config, contracts }) => getChainInfo(config, contracts)).filter(Boolean);
|
||||
return config.chains.map(({ app_config: config }) => getChainInfo(config)).filter(Boolean);
|
||||
})();
|
||||
|
||||
export const essentialDappsChains: Array<Chain> | undefined = (() => {
|
||||
@@ -80,7 +84,7 @@ export const essentialDappsChains: Array<Chain> | undefined = (() => {
|
||||
return;
|
||||
}
|
||||
|
||||
return config.chains.map(({ config, contracts }) => getChainInfo(config, contracts)).filter(Boolean);
|
||||
return config.chains.map(({ app_config: config, contracts }) => getChainInfo(config, contracts)).filter(Boolean);
|
||||
})();
|
||||
|
||||
export const chains = (() => {
|
||||
|
||||
@@ -3,7 +3,6 @@ import { createPublicClient, http } from 'viem';
|
||||
import { currentChain } from './chains';
|
||||
|
||||
export const publicClient = (() => {
|
||||
// TODO @tom2drum public clients for multichain (currently used only in degradation views)
|
||||
if (currentChain?.rpcUrls.default.http.filter(Boolean).length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ export default function useAddChain() {
|
||||
const { trackUsage } = useRewardsActivity();
|
||||
const multichainContext = useMultichainContext();
|
||||
|
||||
const chainConfig = multichainContext?.chain.config ?? config;
|
||||
const chainConfig = multichainContext?.chain.app_config || config;
|
||||
|
||||
return React.useCallback(async() => {
|
||||
if (!wallet || !provider) {
|
||||
|
||||
@@ -10,7 +10,7 @@ import useDetectWalletEip6963 from './useDetectWalletEip6963';
|
||||
|
||||
export default function useProvider() {
|
||||
const multichainContext = useMultichainContext();
|
||||
const feature = (multichainContext?.chain.config ?? config).features.web3Wallet;
|
||||
const feature = (multichainContext?.chain.app_config ?? config).features.web3Wallet;
|
||||
const wallets = getFeaturePayload(feature)?.wallets;
|
||||
|
||||
const { detect: detectWalletEip6963 } = useDetectWalletEip6963();
|
||||
|
||||
@@ -18,7 +18,7 @@ export default function useSwitchChain() {
|
||||
const { data: { wallet, provider } = {} } = useProvider();
|
||||
const multichainContext = useMultichainContext();
|
||||
|
||||
const chainConfig = multichainContext?.chain.config ?? config;
|
||||
const chainConfig = multichainContext?.chain.app_config ?? config;
|
||||
|
||||
return React.useCallback(() => {
|
||||
if (!wallet || !provider) {
|
||||
|
||||
@@ -11,15 +11,15 @@ import { chains, parentChain } from 'lib/web3/chains';
|
||||
|
||||
const feature = appConfig.features.blockchainInteraction;
|
||||
|
||||
const getChainTransportFromConfig = (config: typeof appConfig, readOnly?: boolean): Record<string, Transport> => {
|
||||
if (!config.chain.id) {
|
||||
const getChainTransportFromConfig = (config: Partial<typeof appConfig> | undefined, readOnly?: boolean): Record<string, Transport> => {
|
||||
if (!config?.chain?.id) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
[config.chain.id]: fallback(
|
||||
config.chain.rpcUrls
|
||||
.concat(readOnly ? `${ config.apis.general.endpoint }/api/eth-rpc` : '')
|
||||
.concat(readOnly && config.apis?.general ? `${ config.apis.general.endpoint }/api/eth-rpc` : '')
|
||||
.filter(Boolean)
|
||||
.map((url) => http(url, { batch: { wait: 100, batchSize: 5 } })),
|
||||
),
|
||||
@@ -36,7 +36,7 @@ const reduceExternalChainsToTransportConfig = (readOnly: boolean): Record<string
|
||||
}
|
||||
|
||||
return chains
|
||||
.map(({ config }) => getChainTransportFromConfig(config, readOnly))
|
||||
.map(({ app_config: config }) => getChainTransportFromConfig(config, readOnly))
|
||||
.reduce((result, item) => {
|
||||
Object.entries(item).map(([ id, transport ]) => {
|
||||
result[id] = transport;
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import type { ChainConfig } from 'types/multichain';
|
||||
import type * as multichain from '@blockscout/multichain-aggregator-types';
|
||||
import type { AddressTokenItem } from 'types/client/multichain-aggregator';
|
||||
import type { ClusterChainConfig } from 'types/multichain';
|
||||
|
||||
export const chainDataA = {
|
||||
slug: 'op-mainnet',
|
||||
config: {
|
||||
name: 'OP Mainnet',
|
||||
id: '420',
|
||||
logo: 'https://example.com/logo_s.png',
|
||||
explorer_url: 'https://op-mainnet.com',
|
||||
app_config: {
|
||||
app: {
|
||||
baseUrl: 'https://op-mainnet.com',
|
||||
},
|
||||
chain: {
|
||||
name: 'OP Mainnet',
|
||||
id: '420',
|
||||
currency: {
|
||||
name: 'Ether',
|
||||
symbol: 'ETH',
|
||||
@@ -18,13 +22,6 @@ export const chainDataA = {
|
||||
'https://rpc.op-mainnet.com',
|
||||
],
|
||||
},
|
||||
UI: {
|
||||
navigation: {
|
||||
icon: {
|
||||
'default': '/logo.png',
|
||||
},
|
||||
},
|
||||
},
|
||||
apis: {
|
||||
general: {
|
||||
host: 'localhost',
|
||||
@@ -44,4 +41,166 @@ export const chainDataA = {
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ChainConfig;
|
||||
} as ClusterChainConfig;
|
||||
|
||||
export const chainDataB = {
|
||||
...chainDataA,
|
||||
id: '421',
|
||||
name: 'OP Testnet',
|
||||
logo: 'https://example.com/logo_md.png',
|
||||
slug: 'op-testnet',
|
||||
explorer_url: 'https://op-testnet.com',
|
||||
app_config: {
|
||||
...chainDataA.app_config,
|
||||
app: {
|
||||
baseUrl: 'https://op-testnet.com',
|
||||
},
|
||||
apis: {
|
||||
general: {
|
||||
...chainDataA.app_config?.apis?.general,
|
||||
port: '4004',
|
||||
endpoint: 'http://localhost:4004',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ClusterChainConfig;
|
||||
|
||||
export const chainDataC = {
|
||||
...chainDataA,
|
||||
id: '422',
|
||||
name: 'OP Devnet',
|
||||
slug: 'op-devnet',
|
||||
explorer_url: 'https://op-devnet.com',
|
||||
logo: undefined,
|
||||
app_config: {
|
||||
...chainDataA.app_config,
|
||||
app: {
|
||||
baseUrl: 'https://op-devnet.com',
|
||||
},
|
||||
apis: {
|
||||
general: {
|
||||
...chainDataA.app_config?.apis?.general,
|
||||
port: '4005',
|
||||
endpoint: 'http://localhost:4005',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as ClusterChainConfig;
|
||||
|
||||
export const addressA: multichain.GetAddressResponse = {
|
||||
hash: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
|
||||
chain_infos: {
|
||||
'420': {
|
||||
coin_balance: '33298149965862412288',
|
||||
is_contract: false,
|
||||
is_verified: false,
|
||||
contract_name: undefined,
|
||||
},
|
||||
'421': {
|
||||
coin_balance: '1836931848855642237',
|
||||
is_contract: true,
|
||||
is_verified: false,
|
||||
contract_name: undefined,
|
||||
},
|
||||
'422': {
|
||||
coin_balance: '0',
|
||||
is_contract: true,
|
||||
is_verified: true,
|
||||
contract_name: 'Test Contract',
|
||||
},
|
||||
},
|
||||
has_tokens: true,
|
||||
has_interop_message_transfers: false,
|
||||
coin_balance: '35135081814718054525',
|
||||
exchange_rate: '123.455',
|
||||
};
|
||||
|
||||
export const tokenA: AddressTokenItem = {
|
||||
token: {
|
||||
address_hash: '0xaA8E23Fb1079EA71e0a56F48a2aA51851D8433D0',
|
||||
circulating_market_cap: '10.01',
|
||||
decimals: '6',
|
||||
holders_count: '1250',
|
||||
icon_url: null,
|
||||
name: 'USDT',
|
||||
symbol: 'USDT',
|
||||
total_supply: '284529257047384793',
|
||||
type: 'ERC-20',
|
||||
exchange_rate: '123.456',
|
||||
chain_infos: {
|
||||
'420': {
|
||||
holders_count: '1250',
|
||||
total_supply: '184529257047384791',
|
||||
is_verified: true,
|
||||
contract_name: 'TestnetERC20',
|
||||
},
|
||||
'421': {
|
||||
holders_count: '1250',
|
||||
total_supply: '100000000000000002',
|
||||
is_verified: true,
|
||||
contract_name: 'TestnetERC20',
|
||||
},
|
||||
},
|
||||
reputation: null,
|
||||
},
|
||||
token_instance: null,
|
||||
token_id: null,
|
||||
value: '9038000000',
|
||||
chain_values: {
|
||||
'420': '1038000000',
|
||||
'421': '8000000000',
|
||||
},
|
||||
};
|
||||
|
||||
export const searchAddressesA: multichain.GetAddressResponse = {
|
||||
hash: '0x0000000002C5fE54822a1eD058AE2F937Fd42769',
|
||||
chain_infos: {
|
||||
'420': {
|
||||
coin_balance: '0',
|
||||
is_contract: false,
|
||||
is_verified: false,
|
||||
contract_name: undefined,
|
||||
},
|
||||
},
|
||||
has_tokens: false,
|
||||
has_interop_message_transfers: false,
|
||||
coin_balance: '0',
|
||||
exchange_rate: '123.456',
|
||||
};
|
||||
|
||||
export const searchAddressesB: multichain.GetAddressResponse = {
|
||||
hash: '0x00883b68A6EcF2ea3D47BD735E5125a0B7625B53',
|
||||
chain_infos: {
|
||||
'420': {
|
||||
coin_balance: '0',
|
||||
is_contract: true,
|
||||
is_verified: true,
|
||||
contract_name: 'USDT',
|
||||
},
|
||||
},
|
||||
has_tokens: false,
|
||||
has_interop_message_transfers: false,
|
||||
coin_balance: '0',
|
||||
exchange_rate: '123.456',
|
||||
};
|
||||
|
||||
export const searchTokenA: multichain.AggregatedTokenInfo = {
|
||||
address_hash: '0x94b008aA00579c1307B0EF2c499aD98a8ce58e58',
|
||||
circulating_market_cap: '162513452583.22',
|
||||
decimals: '6',
|
||||
holders_count: '1148927',
|
||||
icon_url: undefined,
|
||||
name: 'Tether USD',
|
||||
symbol: 'USDT',
|
||||
total_supply: '1000000',
|
||||
type: 'ERC-20' as multichain.TokenType,
|
||||
exchange_rate: '1.00',
|
||||
chain_infos: {
|
||||
'420': {
|
||||
holders_count: '1148927',
|
||||
total_supply: '1000000',
|
||||
is_verified: false,
|
||||
contract_name: undefined,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -11,8 +11,8 @@ export function marketplace(): CspDev.DirectiveDescriptor {
|
||||
}
|
||||
|
||||
const chainsConfig = feature.essentialDapps && essentialDappsChains.getValue();
|
||||
const externalApiEndpoints = chainsConfig?.chains.map((chain) => chain.config.apis.general?.endpoint).filter(Boolean);
|
||||
const defaultRpcUrls = chainsConfig?.chains.map((chain) => chain.config.chain.rpcUrls).flat();
|
||||
const externalApiEndpoints = chainsConfig?.chains.map((chain) => chain.app_config?.apis?.general?.endpoint).filter(Boolean);
|
||||
const defaultRpcUrls = chainsConfig?.chains.map((chain) => chain.app_config?.chain?.rpcUrls).flat().filter(Boolean);
|
||||
|
||||
const liFiHost = feature.essentialDapps?.swap ? 'li.quest' : '';
|
||||
const multisenderHost = feature.essentialDapps?.multisend ? '*.multisender.app' : '';
|
||||
|
||||
@@ -9,18 +9,20 @@ export function multichain(): CspDev.DirectiveDescriptor {
|
||||
}
|
||||
|
||||
const apiEndpoints = value.chains.map((chain) => {
|
||||
return [
|
||||
...Object.values(chain.config.apis).filter(Boolean).map((api) => api.endpoint),
|
||||
...Object.values(chain.config.apis).filter(Boolean).map((api) => api.socketEndpoint),
|
||||
].filter(Boolean);
|
||||
return chain.app_config?.apis ? [
|
||||
...Object.values(chain.app_config.apis).filter(Boolean).map((api) => api.endpoint),
|
||||
...Object.values(chain.app_config.apis).filter(Boolean).map((api) => api.socketEndpoint),
|
||||
].filter(Boolean) : [];
|
||||
}).flat();
|
||||
|
||||
const rpcEndpoints = value.chains.map(({ config }) => config.chain.rpcUrls).flat();
|
||||
const rpcEndpoints = value.chains.map(({ app_config: config }) => config?.chain?.rpcUrls).flat().filter(Boolean);
|
||||
|
||||
return {
|
||||
'connect-src': [
|
||||
...apiEndpoints,
|
||||
...rpcEndpoints,
|
||||
// please see comment in the useFetchParentChainApi.tsx file
|
||||
'https://eth.blockscout.com',
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import { factory } from './utils';
|
||||
|
||||
export const base = factory([ ]);
|
||||
export const block = factory([ guards.notOpSuperchain ]);
|
||||
export const tx = factory([ guards.notOpSuperchain ]);
|
||||
export const token = factory([ guards.notOpSuperchain ]);
|
||||
export const account = factory([ guards.account ]);
|
||||
export const verifiedAddresses = factory([ guards.account, guards.verifiedAddresses ]);
|
||||
export const userOps = factory([ guards.userOps ]);
|
||||
|
||||
@@ -35,15 +35,15 @@ export const factory = (guards: Array<Guard | Array<Guard>>, chainConfig = confi
|
||||
|
||||
export const factoryMultichain = (guards: Array<Guard>) => {
|
||||
return async(context: GetServerSidePropsContext) => {
|
||||
const chainSlug = context.params?.['chain-slug'];
|
||||
const chainSlug = context.params?.chain_slug;
|
||||
const chain = multichainConfig()?.chains.find((chain) => chain.slug === chainSlug);
|
||||
|
||||
if (!chain?.config) {
|
||||
if (!chain?.app_config) {
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
}
|
||||
|
||||
return factory(guards, chain.config)(context);
|
||||
return factory(guards, chain.app_config)(context);
|
||||
};
|
||||
};
|
||||
|
||||
Vendored
+11
-10
@@ -39,16 +39,17 @@ declare module "nextjs-routes" {
|
||||
| StaticRoute<"/block/countdown">
|
||||
| StaticRoute<"/blocks">
|
||||
| DynamicRoute<"/cc/tx/[hash]", { "hash": string }>
|
||||
| DynamicRoute<"/chain/[chain-slug]/accounts/label/[slug]", { "chain-slug": string; "slug": string }>
|
||||
| DynamicRoute<"/chain/[chain-slug]/advanced-filter", { "chain-slug": string }>
|
||||
| DynamicRoute<"/chain/[chain-slug]/block/[height_or_hash]", { "chain-slug": string; "height_or_hash": string }>
|
||||
| DynamicRoute<"/chain/[chain-slug]/block/countdown/[height]", { "chain-slug": string; "height": string }>
|
||||
| DynamicRoute<"/chain/[chain-slug]/block/countdown", { "chain-slug": string }>
|
||||
| DynamicRoute<"/chain/[chain-slug]/csv-export", { "chain-slug": string }>
|
||||
| DynamicRoute<"/chain/[chain-slug]/op/[hash]", { "chain-slug": string; "hash": string }>
|
||||
| DynamicRoute<"/chain/[chain-slug]/token/[hash]", { "chain-slug": string; "hash": string }>
|
||||
| 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 }>
|
||||
| DynamicRoute<"/chain/[chain_slug]/accounts/label/[slug]", { "chain_slug": string; "slug": string }>
|
||||
| DynamicRoute<"/chain/[chain_slug]/advanced-filter", { "chain_slug": string }>
|
||||
| DynamicRoute<"/chain/[chain_slug]/block/[height_or_hash]", { "chain_slug": string; "height_or_hash": string }>
|
||||
| DynamicRoute<"/chain/[chain_slug]/block/countdown/[height]", { "chain_slug": string; "height": string }>
|
||||
| DynamicRoute<"/chain/[chain_slug]/block/countdown", { "chain_slug": string }>
|
||||
| DynamicRoute<"/chain/[chain_slug]/csv-export", { "chain_slug": string }>
|
||||
| DynamicRoute<"/chain/[chain_slug]/op/[hash]", { "chain_slug": string; "hash": string }>
|
||||
| DynamicRoute<"/chain/[chain_slug]/token/[hash]", { "chain_slug": string; "hash": string }>
|
||||
| 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 }>
|
||||
| DynamicRoute<"/chain/[chain_slug]/visualize/sol2uml", { "chain_slug": string }>
|
||||
| StaticRoute<"/chakra">
|
||||
| StaticRoute<"/contract-verification">
|
||||
| StaticRoute<"/csv-export">
|
||||
|
||||
+20
-7
@@ -1,16 +1,29 @@
|
||||
import type { ExternalChain } from 'types/externalChains';
|
||||
|
||||
import type { Route } from 'nextjs-routes';
|
||||
import { route as nextjsRoute } from 'nextjs-routes';
|
||||
|
||||
import type { TMultichainContext } from 'lib/contexts/multichain';
|
||||
import { stripTrailingSlash } from 'toolkit/utils/url';
|
||||
|
||||
export const route = (route: Route, multichainContext?: TMultichainContext | null) => {
|
||||
return nextjsRoute(routeParams(route, multichainContext));
|
||||
export interface RouteParams {
|
||||
external?: boolean;
|
||||
chain?: ExternalChain & { slug?: string };
|
||||
}
|
||||
|
||||
export const route = (route: Route, params?: RouteParams | null) => {
|
||||
const generatedRoute = nextjsRoute(routeParams(route, params));
|
||||
|
||||
if (params && params.chain && params.external) {
|
||||
return stripTrailingSlash(params.chain.explorer_url) + generatedRoute;
|
||||
}
|
||||
|
||||
return generatedRoute;
|
||||
};
|
||||
|
||||
export const routeParams = (route: Route, multichainContext?: TMultichainContext | null): Route => {
|
||||
if (multichainContext) {
|
||||
const pathname = '/chain/[chain-slug]' + route.pathname;
|
||||
return { ...route, pathname, query: { ...route.query, 'chain-slug': multichainContext.chain.slug } } as Route;
|
||||
export const routeParams = (route: Route, params?: RouteParams | null): Route => {
|
||||
if (!params?.external && params?.chain?.slug) {
|
||||
const pathname = '/chain/[chain_slug]' + route.pathname;
|
||||
return { ...route, pathname, query: { ...route.query, chain_slug: params.chain.slug } } as Route;
|
||||
}
|
||||
return route;
|
||||
};
|
||||
|
||||
+2
-2
@@ -44,9 +44,9 @@
|
||||
"dependencies": {
|
||||
"@bigmi/react": "0.5.2",
|
||||
"@blockscout/bens-types": "1.4.1",
|
||||
"@blockscout/multichain-aggregator-types": "1.6.0-alpha.2",
|
||||
"@blockscout/multichain-aggregator-types": "1.6.3-alpha.3",
|
||||
"@blockscout/points-types": "1.4.0-alpha.1",
|
||||
"@blockscout/stats-types": "2.10.0-alpha",
|
||||
"@blockscout/stats-types": "2.11.1",
|
||||
"@blockscout/tac-operation-lifecycle-types": "0.0.1-alpha.6",
|
||||
"@blockscout/visualizer-types": "0.2.0",
|
||||
"@blockscout/zetachain-cctx-types": "^1.0.0-rc.6",
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ const handler = async(nextReq: NextApiRequest, nextRes: NextApiResponse) => {
|
||||
|
||||
const url = new URL(
|
||||
nextReq.url.replace(/^\/node-api\/proxy/, ''),
|
||||
nextReq.headers['x-endpoint']?.toString() || appConfig.apis.general.endpoint,
|
||||
nextReq.headers['x-endpoint']?.toString() || appConfig.apis.general?.endpoint,
|
||||
);
|
||||
const apiRes = await fetchFactory(nextReq)(
|
||||
url.toString(),
|
||||
|
||||
+2
-2
@@ -10,8 +10,8 @@ const AccountsLabelSearch = dynamic(() => import('ui/pages/AccountsLabelSearch')
|
||||
|
||||
const Page: NextPage = () => {
|
||||
return (
|
||||
<PageNextJs pathname="/chain/[chain-slug]/accounts/label/[slug]">
|
||||
<MultichainProvider level="page">
|
||||
<PageNextJs pathname="/chain/[chain_slug]/accounts/label/[slug]">
|
||||
<MultichainProvider>
|
||||
<AccountsLabelSearch/>
|
||||
</MultichainProvider>
|
||||
</PageNextJs>
|
||||
+2
-2
@@ -8,8 +8,8 @@ import AdvancedFilter from 'ui/pages/AdvancedFilter';
|
||||
|
||||
const Page: NextPage = () => {
|
||||
return (
|
||||
<PageNextJs pathname="/chain/[chain-slug]/advanced-filter">
|
||||
<MultichainProvider level="page">
|
||||
<PageNextJs pathname="/chain/[chain_slug]/advanced-filter">
|
||||
<MultichainProvider>
|
||||
<AdvancedFilter/>
|
||||
</MultichainProvider>
|
||||
</PageNextJs>
|
||||
+3
-7
@@ -5,16 +5,12 @@ import React from 'react';
|
||||
import type { Props } from 'nextjs/getServerSideProps/handlers';
|
||||
import PageNextJs from 'nextjs/PageNextJs';
|
||||
|
||||
import { MultichainProvider } from 'lib/contexts/multichain';
|
||||
|
||||
const Block = dynamic(() => import('ui/pages/Block'), { ssr: false });
|
||||
const OpSuperchainBlock = dynamic(() => import('ui/optimismSuperchain/block/OpSuperchainBlock'), { ssr: false });
|
||||
|
||||
const Page: NextPage<Props> = (props: Props) => {
|
||||
return (
|
||||
<PageNextJs pathname="/chain/[chain-slug]/block/[height_or_hash]" query={ props.query }>
|
||||
<MultichainProvider level="page">
|
||||
<Block/>
|
||||
</MultichainProvider>
|
||||
<PageNextJs pathname="/chain/[chain_slug]/block/[height_or_hash]" query={ props.query }>
|
||||
<OpSuperchainBlock/>
|
||||
</PageNextJs>
|
||||
);
|
||||
};
|
||||
+2
-2
@@ -11,8 +11,8 @@ const BlockCountdown = dynamic(() => import('ui/pages/BlockCountdown'), { ssr: f
|
||||
|
||||
const Page: NextPage<Props> = (props: Props) => {
|
||||
return (
|
||||
<PageNextJs pathname="/chain/[chain-slug]/block/countdown/[height]" query={ props.query }>
|
||||
<MultichainProvider level="page">
|
||||
<PageNextJs pathname="/chain/[chain_slug]/block/countdown/[height]" query={ props.query }>
|
||||
<MultichainProvider>
|
||||
<BlockCountdown/>
|
||||
</MultichainProvider>
|
||||
</PageNextJs>
|
||||
+2
-2
@@ -11,8 +11,8 @@ const BlockCountdownIndex = dynamic(() => import('ui/pages/BlockCountdownIndex')
|
||||
|
||||
const Page: NextPage<Props> = (props: Props) => {
|
||||
return (
|
||||
<PageNextJs pathname="/chain/[chain-slug]/block/countdown" query={ props.query }>
|
||||
<MultichainProvider level="page">
|
||||
<PageNextJs pathname="/chain/[chain_slug]/block/countdown" query={ props.query }>
|
||||
<MultichainProvider>
|
||||
<BlockCountdownIndex/>
|
||||
</MultichainProvider>
|
||||
</PageNextJs>
|
||||
+2
-2
@@ -8,8 +8,8 @@ import CsvExport from 'ui/pages/CsvExport';
|
||||
|
||||
const Page: NextPage = () => {
|
||||
return (
|
||||
<PageNextJs pathname="/chain/[chain-slug]/csv-export">
|
||||
<MultichainProvider level="page">
|
||||
<PageNextJs pathname="/chain/[chain_slug]/csv-export">
|
||||
<MultichainProvider>
|
||||
<CsvExport/>
|
||||
</MultichainProvider>
|
||||
</PageNextJs>
|
||||
@@ -11,8 +11,8 @@ const UserOp = dynamic(() => import('ui/pages/UserOp'), { ssr: false });
|
||||
|
||||
const Page: NextPage<Props> = (props: Props) => {
|
||||
return (
|
||||
<PageNextJs pathname="/chain/[chain-slug]/op/[hash]" query={ props.query }>
|
||||
<MultichainProvider level="page">
|
||||
<PageNextJs pathname="/chain/[chain_slug]/op/[hash]" query={ props.query }>
|
||||
<MultichainProvider>
|
||||
<UserOp/>
|
||||
</MultichainProvider>
|
||||
</PageNextJs>
|
||||
+3
-13
@@ -1,29 +1,19 @@
|
||||
import type { NextPage } from 'next';
|
||||
import dynamic from 'next/dynamic';
|
||||
import React from 'react';
|
||||
|
||||
import type { Route } from 'nextjs-routes';
|
||||
import type { Props } from 'nextjs/getServerSideProps/handlers';
|
||||
import PageNextJs from 'nextjs/PageNextJs';
|
||||
|
||||
import multichainConfig from 'configs/multichain';
|
||||
import getSocketUrl from 'lib/api/getSocketUrl';
|
||||
import { MultichainProvider } from 'lib/contexts/multichain';
|
||||
import { SocketProvider } from 'lib/socket/context';
|
||||
import Token from 'ui/pages/Token';
|
||||
const OpSuperchainToken = dynamic(() => import('ui/optimismSuperchain/token/OpSuperchainToken'), { ssr: false });
|
||||
|
||||
const pathname: Route['pathname'] = '/token/[hash]';
|
||||
|
||||
const Page: NextPage<Props<typeof pathname>> = (props: Props<typeof pathname>) => {
|
||||
const chainSlug = props.query?.['chain-slug'];
|
||||
const chainData = multichainConfig()?.chains.find(chain => chain.slug === chainSlug);
|
||||
|
||||
return (
|
||||
<PageNextJs pathname={ pathname } query={ props.query } apiData={ props.apiData }>
|
||||
<SocketProvider url={ getSocketUrl(chainData?.config) }>
|
||||
<MultichainProvider level="page">
|
||||
<Token/>
|
||||
</MultichainProvider>
|
||||
</SocketProvider>
|
||||
<OpSuperchainToken/>
|
||||
</PageNextJs>
|
||||
);
|
||||
};
|
||||
+3
-13
@@ -1,29 +1,19 @@
|
||||
import type { NextPage } from 'next';
|
||||
import dynamic from 'next/dynamic';
|
||||
import React from 'react';
|
||||
|
||||
import type { Route } from 'nextjs-routes';
|
||||
import type { Props } from 'nextjs/getServerSideProps/handlers';
|
||||
import PageNextJs from 'nextjs/PageNextJs';
|
||||
|
||||
import multichainConfig from 'configs/multichain';
|
||||
import getSocketUrl from 'lib/api/getSocketUrl';
|
||||
import { MultichainProvider } from 'lib/contexts/multichain';
|
||||
import { SocketProvider } from 'lib/socket/context';
|
||||
import TokenInstance from 'ui/pages/TokenInstance';
|
||||
const OpSuperchainTokenInstance = dynamic(() => import('ui/optimismSuperchain/tokenInstance/OpSuperchainTokenInstance'), { ssr: false });
|
||||
|
||||
const pathname: Route['pathname'] = '/token/[hash]/instance/[id]';
|
||||
|
||||
const Page: NextPage<Props<typeof pathname>> = (props: Props<typeof pathname>) => {
|
||||
const chainSlug = props.query?.['chain-slug'];
|
||||
const chainData = multichainConfig()?.chains.find(chain => chain.slug === chainSlug);
|
||||
|
||||
return (
|
||||
<PageNextJs pathname={ pathname } query={ props.query } apiData={ props.apiData }>
|
||||
<SocketProvider url={ getSocketUrl(chainData?.config) }>
|
||||
<MultichainProvider level="page">
|
||||
<TokenInstance/>
|
||||
</MultichainProvider>
|
||||
</SocketProvider>
|
||||
<OpSuperchainTokenInstance/>
|
||||
</PageNextJs>
|
||||
);
|
||||
};
|
||||
+3
-7
@@ -5,16 +5,12 @@ import React from 'react';
|
||||
import type { Props } from 'nextjs/getServerSideProps/handlers';
|
||||
import PageNextJs from 'nextjs/PageNextJs';
|
||||
|
||||
import { MultichainProvider } from 'lib/contexts/multichain';
|
||||
|
||||
const Transaction = dynamic(() => import('ui/pages/Transaction'), { ssr: false });
|
||||
const OpSuperchainTx = dynamic(() => import('ui/optimismSuperchain/tx/OpSuperchainTx'), { ssr: false });
|
||||
|
||||
const Page: NextPage<Props> = (props: Props) => {
|
||||
return (
|
||||
<PageNextJs pathname="/chain/[chain-slug]/tx/[hash]" query={ props.query }>
|
||||
<MultichainProvider level="page">
|
||||
<Transaction/>
|
||||
</MultichainProvider>
|
||||
<PageNextJs pathname="/chain/[chain_slug]/tx/[hash]" query={ props.query }>
|
||||
<OpSuperchainTx/>
|
||||
</PageNextJs>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { NextPage } from 'next';
|
||||
import React from 'react';
|
||||
|
||||
import PageNextJs from 'nextjs/PageNextJs';
|
||||
|
||||
import { MultichainProvider } from 'lib/contexts/multichain';
|
||||
import Sol2Uml from 'ui/pages/Sol2Uml';
|
||||
|
||||
const Page: NextPage = () => {
|
||||
return (
|
||||
<PageNextJs pathname="/visualize/sol2uml">
|
||||
<MultichainProvider>
|
||||
<Sol2Uml/>
|
||||
</MultichainProvider>
|
||||
</PageNextJs>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
export { base as getServerSideProps } from 'nextjs/getServerSideProps/multichain';
|
||||
@@ -6,9 +6,16 @@ import type { NextPageWithLayout } from 'nextjs/types';
|
||||
import type { Props } from 'nextjs/getServerSideProps/handlers';
|
||||
import PageNextJs from 'nextjs/PageNextJs';
|
||||
|
||||
import config from 'configs/app';
|
||||
import LayoutSearchResults from 'ui/shared/layout/LayoutSearchResults';
|
||||
|
||||
const SearchResults = dynamic(() => import('ui/pages/SearchResults'), { ssr: false });
|
||||
const SearchResults = dynamic(() => {
|
||||
if (config.features.opSuperchain.isEnabled) {
|
||||
return import('ui/optimismSuperchain/searchResults/SearchResults');
|
||||
}
|
||||
|
||||
return import('ui/pages/SearchResults');
|
||||
}, { ssr: false });
|
||||
|
||||
const Page: NextPageWithLayout<Props> = (props: Props) => {
|
||||
return (
|
||||
|
||||
@@ -10,6 +10,7 @@ import detectBotRequest from 'nextjs/utils/detectBotRequest';
|
||||
import fetchApi from 'nextjs/utils/fetchApi';
|
||||
|
||||
import config from 'configs/app';
|
||||
import { MultichainProvider } from 'lib/contexts/multichain';
|
||||
import dayjs from 'lib/date/dayjs';
|
||||
import getQueryParamString from 'lib/router/getQueryParamString';
|
||||
|
||||
@@ -20,7 +21,13 @@ const pathname: Route['pathname'] = '/stats/[id]';
|
||||
const Page: NextPage<Props<typeof pathname>> = (props: Props<typeof pathname>) => {
|
||||
return (
|
||||
<PageNextJs pathname={ pathname } query={ props.query } apiData={ props.apiData }>
|
||||
<Chart/>
|
||||
{ config.features.opSuperchain.isEnabled ? (
|
||||
<MultichainProvider chainId={ getQueryParamString(props.query?.chain_id) }>
|
||||
<Chart/>
|
||||
</MultichainProvider>
|
||||
) : (
|
||||
<Chart/>
|
||||
) }
|
||||
</PageNextJs>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,12 +3,14 @@ import React from 'react';
|
||||
|
||||
import PageNextJs from 'nextjs/PageNextJs';
|
||||
|
||||
import config from 'configs/app';
|
||||
import OpSuperchainStats from 'ui/optimismSuperchain/stats/OpSuperchainStats';
|
||||
import Stats from 'ui/pages/Stats';
|
||||
|
||||
const Page: NextPage = () => {
|
||||
return (
|
||||
<PageNextJs pathname="/stats">
|
||||
<Stats/>
|
||||
{ config.features.opSuperchain.isEnabled ? <OpSuperchainStats/> : <Stats/> }
|
||||
</PageNextJs>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -10,7 +10,6 @@ import fetchApi from 'nextjs/utils/fetchApi';
|
||||
|
||||
import config from 'configs/app';
|
||||
import getQueryParamString from 'lib/router/getQueryParamString';
|
||||
import OpSuperchainToken from 'ui/optimismSuperchain/token/OpSuperchainToken';
|
||||
import Token from 'ui/pages/Token';
|
||||
|
||||
const pathname: Route['pathname'] = '/token/[hash]';
|
||||
@@ -18,7 +17,7 @@ const pathname: Route['pathname'] = '/token/[hash]';
|
||||
const Page: NextPage<Props<typeof pathname>> = (props: Props<typeof pathname>) => {
|
||||
return (
|
||||
<PageNextJs pathname={ pathname } query={ props.query } apiData={ props.apiData }>
|
||||
{ config.features.opSuperchain.isEnabled ? <OpSuperchainToken/> : <Token/> }
|
||||
<Token/>
|
||||
</PageNextJs>
|
||||
);
|
||||
};
|
||||
@@ -26,7 +25,7 @@ const Page: NextPage<Props<typeof pathname>> = (props: Props<typeof pathname>) =
|
||||
export default Page;
|
||||
|
||||
export const getServerSideProps: GetServerSideProps<Props<typeof pathname>> = async(ctx) => {
|
||||
const baseResponse = await gSSP.base<typeof pathname>(ctx);
|
||||
const baseResponse = await gSSP.token<typeof pathname>(ctx);
|
||||
|
||||
if ('props' in baseResponse && !config.features.opSuperchain.isEnabled) {
|
||||
if (
|
||||
|
||||
@@ -25,7 +25,7 @@ const Page: NextPage<Props<typeof pathname>> = (props: Props<typeof pathname>) =
|
||||
export default Page;
|
||||
|
||||
export const getServerSideProps: GetServerSideProps<Props<typeof pathname>> = async(ctx) => {
|
||||
const baseResponse = await gSSP.base<typeof pathname>(ctx);
|
||||
const baseResponse = await gSSP.token<typeof pathname>(ctx);
|
||||
|
||||
if (config.meta.og.enhancedDataEnabled && 'props' in baseResponse) {
|
||||
const botInfo = detectBotRequest(ctx.req);
|
||||
|
||||
+1
-7
@@ -5,13 +5,7 @@ import React from 'react';
|
||||
import type { Props } from 'nextjs/getServerSideProps/handlers';
|
||||
import PageNextJs from 'nextjs/PageNextJs';
|
||||
|
||||
import config from 'configs/app';
|
||||
|
||||
const Transaction = dynamic(() => {
|
||||
if (config.features.opSuperchain.isEnabled) {
|
||||
return import('ui/optimismSuperchain/tx/OpSuperchainTx');
|
||||
}
|
||||
|
||||
return import('ui/pages/Transaction');
|
||||
}, { ssr: false });
|
||||
|
||||
@@ -25,4 +19,4 @@ const Page: NextPage<Props> = (props: Props) => {
|
||||
|
||||
export default Page;
|
||||
|
||||
export { base as getServerSideProps } from 'nextjs/getServerSideProps/main';
|
||||
export { tx as getServerSideProps } from 'nextjs/getServerSideProps/main';
|
||||
|
||||
@@ -114,7 +114,10 @@ export const ENVS_MAP: Record<string, Array<[string, string]>> = {
|
||||
[ 'NEXT_PUBLIC_CELO_ENABLED', 'true' ],
|
||||
],
|
||||
opSuperchain: [
|
||||
[ 'NEXT_PUBLIC_OP_SUPERCHAIN_ENABLED', 'true' ],
|
||||
[ 'NEXT_PUBLIC_MULTICHAIN_ENABLED', 'true' ],
|
||||
[ 'NEXT_PUBLIC_MULTICHAIN_CLUSTER', 'test' ],
|
||||
[ 'NEXT_PUBLIC_MULTICHAIN_AGGREGATOR_API_HOST', 'http://localhost:3012' ],
|
||||
[ 'NEXT_PUBLIC_MULTICHAIN_STATS_API_HOST', 'http://localhost:3013' ],
|
||||
],
|
||||
clusters: [
|
||||
[ 'NEXT_PUBLIC_CLUSTERS_API_HOST', 'https://api.clusters.xyz' ],
|
||||
|
||||
@@ -10,6 +10,8 @@ const fixture: TestFixture<MockMultichainConfigFixture, { page: Page }> = async(
|
||||
window.__multichainConfig = {
|
||||
chains: [
|
||||
opSuperchainMock.chainDataA,
|
||||
opSuperchainMock.chainDataB,
|
||||
opSuperchainMock.chainDataC,
|
||||
],
|
||||
};
|
||||
}, [ opSuperchainMock ]);
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 260 200">
|
||||
<g clip-path="url(#a)">
|
||||
<path fill="url(#b)" d="M130 163.385c11.598 0 21 9.402 21 21s-9.402 21-21 21-21-9.402-21-21 9.402-21 21-21Zm0 3c-9.941 0-18 8.059-18 18s8.059 18 18 18 18-8.059 18-18-8.059-18-18-18Zm-56-31c11.598 0 21 9.402 21 21s-9.402 21-21 21-21-9.402-21-21 9.402-21 21-21Zm112 0c11.598 0 21 9.402 21 21s-9.402 21-21 21-21-9.402-21-21 9.402-21 21-21Zm-112 3c-9.941 0-18 8.059-18 18s8.059 18 18 18 18-8.059 18-18-8.059-18-18-18Zm112 0c-9.941 0-18 8.059-18 18s8.059 18 18 18 18-8.059 18-18-8.059-18-18-18Zm-167-31c11.598 0 21 9.402 21 21s-9.402 21-21 21-21-9.402-21-21 9.402-21 21-21Zm128.234 33c-3.795 5.44-10.098 9-17.234 9-7.136 0-13.439-3.56-17.234-9h3.819c3.296 3.682 8.084 6 13.415 6a17.953 17.953 0 0 0 13.415-6h3.819Zm94.766-33c11.598 0 21 9.402 21 21s-9.402 21-21 21-21-9.402-21-21 9.402-21 21-21Zm-223 3c-9.941 0-18 8.059-18 18s8.059 18 18 18 18-8.059 18-18-8.059-18-18-18Zm223 0c-9.941 0-18 8.059-18 18s8.059 18 18 18 18-8.059 18-18-8.059-18-18-18Zm-168-31c.338 0 .674.008 1.008.024L74 80.385v2c-9.941 0-18 8.059-18 18s8.059 18 18 18v2l1.11.97c-.368.02-.738.03-1.11.03-11.598 0-21-9.402-21-21s9.402-21 21-21Zm113.876.084c10.719.949 19.124 9.95 19.124 20.916 0 11.171-8.723 20.303-19.729 20.96l1.729-.96v-2.251c8.513-1.429 15-8.83 15-17.749 0-8.92-6.487-16.322-15-17.75v-2.25l-1.124-.916ZM19 50.385c11.598 0 21 9.402 21 21s-9.402 21-21 21-21-9.402-21-21 9.402-21 21-21Zm223 0c11.598 0 21 9.402 21 21s-9.402 21-21 21-21-9.402-21-21 9.402-21 21-21Zm-223 3c-9.941 0-18 8.059-18 18 0 9.94 8.059 18 18 18s18-8.06 18-18c0-9.941-8.059-18-18-18Zm223 0c-9.941 0-18 8.059-18 18 0 9.94 8.059 18 18 18s18-8.06 18-18c0-9.941-8.059-18-18-18Zm-168-31c11.598 0 21 9.402 21 21s-9.402 21-21 21-21-9.402-21-21 9.402-21 21-21Zm112 0c11.598 0 21 9.402 21 21s-9.402 21-21 21-21-9.402-21-21 9.402-21 21-21Zm-112 3c-9.941 0-18 8.059-18 18 0 9.94 8.059 18 18 18s18-8.06 18-18c0-9.941-8.059-18-18-18Zm112 0c-9.941 0-18 8.059-18 18 0 9.94 8.059 18 18 18s18-8.06 18-18c0-9.941-8.059-18-18-18Zm-56 25c7.564 0 14.194 4 17.891 10h-3.643c-3.292-4.258-8.45-7-14.248-7s-10.956 2.742-14.248 7h-3.643c3.697-6 10.327-10 17.891-10Zm0-55c11.598 0 21 9.402 21 21s-9.402 21-21 21-21-9.402-21-21 9.402-21 21-21Zm0 3c-9.941 0-18 8.059-18 18 0 9.94 8.059 18 18 18s18-8.06 18-18c0-9.941-8.059-18-18-18Z"/>
|
||||
<path fill="#E2E8F0" d="M103.588 76.385a24.004 24.004 0 0 1 19.661 10.238l16.056 22.936v.001a16.006 16.006 0 0 0 13.107 6.825h3.932l-5.172-5.172a4 4 0 1 1 5.656-5.656l11.98 11.979c.145.144.28.298.403.463.025.033.048.068.071.102.014.02.029.039.042.059l.065.1.042.068.042.072.062.112c.01.017.017.036.027.053.019.038.038.075.056.114l.034.076a1.71 1.71 0 0 1 .078.188l.039.102.014.043c.037.108.07.217.098.329l.014.06a4 4 0 0 1 .002 1.803c-.005.023-.009.047-.015.07a3.964 3.964 0 0 1-.095.322l-.018.051-.039.101c-.011.03-.022.061-.035.091l-.034.08a2.688 2.688 0 0 1-.099.207l-.028.055-.053.095a3.97 3.97 0 0 1-.157.255c-.013.021-.028.04-.042.06-.023.034-.046.068-.071.102a4.053 4.053 0 0 1-.383.444l-12 12a4 4 0 1 1-5.656-5.656l5.172-5.172h-3.932a24.016 24.016 0 0 1-11.084-2.713 24.01 24.01 0 0 1-8.154-6.94l-.423-.586-16.056-22.935v-.001a16.003 16.003 0 0 0-12.626-6.818l-.481-.007H94a4 4 0 0 1 0-8h9.588Zm13.814 32.163a4.001 4.001 0 0 1 6.556 4.586l-.708 1.012h-.001a24 24 0 0 1-19.661 10.239H94a4 4 0 0 1 0-8h9.588l.481-.007a16.003 16.003 0 0 0 12.625-6.818l.708-1.012Zm33.77-42.991a3.999 3.999 0 0 1 5.656 0l11.98 11.98c.145.143.28.297.403.462.025.033.048.068.071.102.014.02.029.039.042.06a3.959 3.959 0 0 1 .21.35l.028.054.056.114.034.076c.015.032.03.065.043.098a27.47 27.47 0 0 1 .074.192l.014.043c.037.108.07.217.098.329l.014.06a4.004 4.004 0 0 1-.111 2.201l-.015.045-.039.101c-.011.03-.022.061-.035.091-.013.033-.028.065-.043.098l-.034.076-.056.113-.028.055a3.77 3.77 0 0 1-.21.35l-.042.06c-.023.034-.046.068-.071.102a4.066 4.066 0 0 1-.383.444l-12 12a4 4 0 0 1-5.656-5.656l5.172-5.172h-3.932a16.003 16.003 0 0 0-13.103 6.818l.001.001-.708 1.016a4.001 4.001 0 0 1-6.564-4.575l.708-1.015.005-.007a24.004 24.004 0 0 1 19.661-10.238h3.932l-5.172-5.172a4 4 0 0 1 0-5.656Z"/>
|
||||
</g>
|
||||
<defs>
|
||||
<radialGradient id="b" cx="0" cy="0" r="1" gradientTransform="scale(132.5 105) rotate(90 .014 .97)" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#CBD5E0" stop-opacity=".8"/>
|
||||
<stop offset="1" stop-color="#CBD5E0" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<clipPath id="a">
|
||||
<path fill="#fff" d="M0 0h260v200H0z"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
+68
-18
@@ -1,23 +1,73 @@
|
||||
import * as multichain from '@blockscout/multichain-aggregator-types';
|
||||
import type * as multichain from '@blockscout/multichain-aggregator-types';
|
||||
import type * as stats from '@blockscout/stats-types';
|
||||
import type { AddressTokenItem } from 'types/client/multichain-aggregator';
|
||||
|
||||
import { ADDRESS_HASH } from './addressParams';
|
||||
import { TX_HASH } from './tx';
|
||||
|
||||
export const INTEROP_MESSAGE: multichain.InteropMessage = {
|
||||
sender: {
|
||||
hash: ADDRESS_HASH,
|
||||
export const ADDRESS: multichain.GetAddressResponse = {
|
||||
hash: ADDRESS_HASH,
|
||||
chain_infos: {
|
||||
'420120000': {
|
||||
coin_balance: '1000000000000000000000000',
|
||||
is_contract: true,
|
||||
is_verified: true,
|
||||
},
|
||||
},
|
||||
has_tokens: true,
|
||||
has_interop_message_transfers: false,
|
||||
coin_balance: '1000000000000000000000000',
|
||||
exchange_rate: '123.456',
|
||||
};
|
||||
|
||||
export const TOKEN: AddressTokenItem = {
|
||||
token: {
|
||||
address_hash: ADDRESS_HASH,
|
||||
circulating_market_cap: '10.01',
|
||||
decimals: '6',
|
||||
holders_count: '141268',
|
||||
icon_url: null,
|
||||
name: 'SR2USD',
|
||||
symbol: 'sR2USD',
|
||||
total_supply: '943969542711126',
|
||||
type: 'ERC-20' as const,
|
||||
exchange_rate: '123.456',
|
||||
reputation: null,
|
||||
chain_infos: {
|
||||
'11155111': {
|
||||
holders_count: '141268',
|
||||
total_supply: '943969542711126',
|
||||
is_verified: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
token_id: null,
|
||||
token_instance: null,
|
||||
value: '2860471393',
|
||||
chain_values: {
|
||||
'11155111': '2860471393',
|
||||
},
|
||||
};
|
||||
|
||||
export const HOMEPAGE_STATS: stats.MainPageMultichainStats = {
|
||||
total_multichain_txns: {
|
||||
id: 'totalMultichainTxns',
|
||||
value: '741682908',
|
||||
title: 'Total transactions',
|
||||
units: undefined,
|
||||
description: 'Number of transactions across all chains in the cluster',
|
||||
},
|
||||
total_multichain_addresses: {
|
||||
id: 'totalMultichainAddresses',
|
||||
value: '153519638',
|
||||
title: 'Total addresses',
|
||||
units: undefined,
|
||||
description: 'Number of addresses across all chains in the cluster',
|
||||
},
|
||||
yesterday_txns_multichain: {
|
||||
id: 'yesterdayTxnsMultichain',
|
||||
value: '1026175',
|
||||
title: 'Yesterday txns',
|
||||
units: undefined,
|
||||
description: 'Number of transactions yesterday (0:00 - 23:59 UTC) across all chains in the cluste',
|
||||
},
|
||||
target: {
|
||||
hash: ADDRESS_HASH,
|
||||
},
|
||||
nonce: 4261,
|
||||
init_chain_id: '420120000',
|
||||
init_transaction_hash: TX_HASH,
|
||||
timestamp: '2025-06-03T10:43:58.000Z',
|
||||
relay_chain_id: '420120001',
|
||||
relay_transaction_hash: TX_HASH,
|
||||
payload: '0x4f0edcc90000000000000000000000004',
|
||||
message_type: 'coin_transfer',
|
||||
method: 'sendERC20',
|
||||
status: multichain.InteropMessage_Status.PENDING,
|
||||
};
|
||||
|
||||
@@ -65,12 +65,13 @@ interface CollapsibleListProps<T> extends FlexProps {
|
||||
renderItem: (item: T, index: number) => React.ReactNode;
|
||||
triggerProps?: LinkProps;
|
||||
cutLength?: number;
|
||||
text?: [string, string];
|
||||
}
|
||||
|
||||
export const CollapsibleList = <T,>(props: CollapsibleListProps<T>) => {
|
||||
const CUT_LENGTH = 3;
|
||||
|
||||
const { items, renderItem, triggerProps, cutLength = CUT_LENGTH, ...rest } = props;
|
||||
const { items, renderItem, triggerProps, cutLength = CUT_LENGTH, text: textProp, ...rest } = props;
|
||||
|
||||
const [ isExpanded, setIsExpanded ] = React.useState(false);
|
||||
|
||||
@@ -78,6 +79,8 @@ export const CollapsibleList = <T,>(props: CollapsibleListProps<T>) => {
|
||||
setIsExpanded((flag) => !flag);
|
||||
}, []);
|
||||
|
||||
const text = isExpanded ? (textProp?.[1] ?? 'Hide') : (textProp?.[0] ?? 'Show all');
|
||||
|
||||
return (
|
||||
<Flex flexDir="column" w="100%" { ...rest }>
|
||||
{ items.slice(0, isExpanded ? undefined : cutLength).map(renderItem) }
|
||||
@@ -91,7 +94,7 @@ export const CollapsibleList = <T,>(props: CollapsibleListProps<T>) => {
|
||||
onClick={ handleToggle }
|
||||
{ ...triggerProps }
|
||||
>
|
||||
{ isExpanded ? 'Hide' : 'Show all' }
|
||||
{ text }
|
||||
</Link>
|
||||
) }
|
||||
</Flex>
|
||||
|
||||
@@ -101,11 +101,11 @@ export const SelectItem = React.forwardRef<
|
||||
>(function SelectItem(props, ref) {
|
||||
const { item, children, ...rest } = props;
|
||||
|
||||
const startElement = item.icon;
|
||||
const { icon, ...itemProps } = item;
|
||||
|
||||
return (
|
||||
<ChakraSelect.Item key={ item.value } item={ item } { ...rest } ref={ ref }>
|
||||
{ startElement }
|
||||
<ChakraSelect.Item key={ itemProps.value } item={ itemProps } { ...rest } ref={ ref }>
|
||||
{ icon }
|
||||
{ children }
|
||||
<ChakraSelect.ItemIndicator asChild>
|
||||
<Icon boxSize={ 5 } flexShrink={ 0 } ml="auto"><CheckIcon/></Icon>
|
||||
|
||||
@@ -96,7 +96,7 @@ const AdaptiveTabsList = (props: Props) => {
|
||||
flexWrap="nowrap"
|
||||
alignItems="center"
|
||||
whiteSpace="nowrap"
|
||||
bgColor={{ _light: 'white', _dark: 'black' }}
|
||||
bgColor="bg.primary"
|
||||
marginBottom={ 6 }
|
||||
mx={{ base: '-12px', lg: 'unset' }}
|
||||
px={{ base: '12px', lg: 'unset' }}
|
||||
|
||||
@@ -13,10 +13,10 @@ interface Props extends AdaptiveTabsProps {
|
||||
}
|
||||
|
||||
const RoutedTabs = (props: Props) => {
|
||||
const { tabs, onValueChange, preservedParams, ...rest } = props;
|
||||
const { tabs, defaultTabId, onValueChange, preservedParams, ...rest } = props;
|
||||
|
||||
const router = useRouter();
|
||||
const activeTab = useActiveTabFromQuery(props.tabs, props.defaultTabId);
|
||||
const activeTab = useActiveTabFromQuery(tabs, defaultTabId);
|
||||
const tabsRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleValueChange = React.useCallback(({ value }: { value: string }) => {
|
||||
|
||||
@@ -18,7 +18,6 @@ import { useTimeChartController } from './utils/useTimeChartController';
|
||||
|
||||
export interface ChartProps {
|
||||
charts: TimeChartData;
|
||||
title: string;
|
||||
zoomRange?: [ Date, Date ];
|
||||
onZoom: (range: [ Date, Date ]) => void;
|
||||
margin?: ChartMargin;
|
||||
|
||||
@@ -78,7 +78,6 @@ const FullscreenChartModal = ({
|
||||
charts={ charts }
|
||||
handleZoom={ handleZoom }
|
||||
zoomRange={ zoomRange }
|
||||
title={ title }
|
||||
resolution={ resolution }
|
||||
/>
|
||||
</DialogBody>
|
||||
|
||||
@@ -94,7 +94,6 @@ export const ChartWidget = React.memo(({
|
||||
charts={ displayedCharts }
|
||||
isError={ isError }
|
||||
isLoading={ isLoading }
|
||||
title={ title }
|
||||
empty={ !hasNonEmptyCharts }
|
||||
emptyText={ emptyText }
|
||||
handleZoom={ handleZoom }
|
||||
|
||||
@@ -11,7 +11,6 @@ import { ChartWatermark } from './parts/ChartWatermark';
|
||||
|
||||
export interface ChartWidgetContentProps {
|
||||
charts: TimeChartData;
|
||||
title: string;
|
||||
isLoading?: boolean;
|
||||
isError?: boolean;
|
||||
empty?: boolean;
|
||||
@@ -27,7 +26,6 @@ export interface ChartWidgetContentProps {
|
||||
|
||||
export const ChartWidgetContent = React.memo(({
|
||||
charts,
|
||||
title,
|
||||
isLoading,
|
||||
isError,
|
||||
empty,
|
||||
@@ -78,7 +76,6 @@ export const ChartWidgetContent = React.memo(({
|
||||
charts={ charts }
|
||||
zoomRange={ zoomRange }
|
||||
onZoom={ handleZoom }
|
||||
title={ title }
|
||||
isEnlarged={ isEnlarged }
|
||||
noAnimation={ noAnimation }
|
||||
resolution={ resolution }
|
||||
|
||||
@@ -12,6 +12,7 @@ import CsvIcon from 'icons/files/csv.svg';
|
||||
import ImageIcon from 'icons/files/image.svg';
|
||||
import ScopeIcon from 'icons/scope.svg';
|
||||
import ShareIcon from 'icons/share.svg';
|
||||
import { useMultichainContext } from 'lib/contexts/multichain';
|
||||
|
||||
import { useColorModeValue } from '../../../chakra/color-mode';
|
||||
import { IconButton } from '../../../chakra/icon-button';
|
||||
@@ -66,6 +67,12 @@ const ChartMenu = ({
|
||||
|
||||
const isInBrowser = isBrowser();
|
||||
|
||||
const multichainContext = useMultichainContext();
|
||||
|
||||
const chainPostfix = React.useMemo(() => {
|
||||
return multichainContext?.chain.name ? ` on ${ multichainContext.chain.name }` : '';
|
||||
}, [ multichainContext?.chain.name ]);
|
||||
|
||||
const showChartFullscreen = React.useCallback(() => {
|
||||
fullscreenDialog.onOpenChange({ open: true });
|
||||
}, [ fullscreenDialog ]);
|
||||
@@ -89,14 +96,14 @@ const ChartMenu = ({
|
||||
})
|
||||
.then((dataUrl) => {
|
||||
const link = document.createElement('a');
|
||||
link.download = `${ title } (Blockscout chart).png`;
|
||||
link.download = `${ title }${ chainPostfix } (Blockscout chart).png`;
|
||||
link.href = dataUrl;
|
||||
link.click();
|
||||
link.remove();
|
||||
});
|
||||
}
|
||||
}, 100);
|
||||
}, [ pngBackgroundColor, title, chartRef ]);
|
||||
}, [ pngBackgroundColor, title, chainPostfix, chartRef ]);
|
||||
|
||||
const handleSVGSavingClick = React.useCallback(() => {
|
||||
const headerRows = [
|
||||
@@ -106,8 +113,8 @@ const ChartMenu = ({
|
||||
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 ]);
|
||||
saveAsCsv(headerRows, dataRows, `${ title }${ chainPostfix } (Blockscout stats)`);
|
||||
}, [ charts, title, chainPostfix ]);
|
||||
|
||||
// TS thinks window.navigator.share can't be undefined, but it can
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
||||
@@ -29,13 +29,13 @@ dotenv \
|
||||
if [[ "$preset_name" == "optimism_superchain" ]]; then
|
||||
dotenv \
|
||||
-e $config_file \
|
||||
-- bash -c 'cd deploy/tools/multichain-config-generator && yarn install --silent && yarn build && yarn generate'
|
||||
-- bash -c 'cd deploy/tools/multichain-config-generator && yarn install --silent && yarn build && yarn generate' || exit 1
|
||||
fi
|
||||
|
||||
# generate essential dapps chains config if marketplace essential dapps enabled
|
||||
dotenv \
|
||||
-e $config_file \
|
||||
-- bash -c 'cd deploy/tools/essential-dapps-chains-config-generator && yarn install --silent && yarn build && yarn generate'
|
||||
-- bash -c 'cd deploy/tools/essential-dapps-chains-config-generator && yarn install --silent && yarn build && yarn generate' || exit 1
|
||||
|
||||
source ./deploy/scripts/build_sprite.sh
|
||||
echo ""
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
// chain info for external chains (not in the config)
|
||||
// eg. zetachain cctx, interop txs, etc.
|
||||
export type CrossChainInfo = {
|
||||
chain_id: number;
|
||||
chain_name: string | null;
|
||||
chain_logo?: string | null;
|
||||
instance_url?: string;
|
||||
address_url_template?: string;
|
||||
tx_url_template?: string;
|
||||
};
|
||||
@@ -1,3 +1,9 @@
|
||||
import type { Chain } from 'viem';
|
||||
|
||||
import type { ExternalChain } from 'types/externalChains';
|
||||
|
||||
import type config from 'configs/app';
|
||||
|
||||
export type MarketplaceAppBase = {
|
||||
id: string;
|
||||
author: string;
|
||||
@@ -34,6 +40,13 @@ export enum MarketplaceCategory {
|
||||
FAVORITES = 'Favorites',
|
||||
}
|
||||
|
||||
export interface EssentialDappsChainConfig extends ExternalChain {
|
||||
app_config?: Pick<typeof config, 'app' | 'chain'> & {
|
||||
apis: Pick<typeof config['apis'], 'general'>;
|
||||
};
|
||||
contracts?: Chain['contracts'];
|
||||
}
|
||||
|
||||
export type EssentialDappsConfig = {
|
||||
swap?: {
|
||||
chains: Array<string>;
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import type * as bens from '@blockscout/bens-types';
|
||||
import type * as multichain from '@blockscout/multichain-aggregator-types';
|
||||
import type { TokenType } from 'types/api/token';
|
||||
|
||||
// ts-proto generates the wrong token types for AggregatedTokenInfo
|
||||
// moreover, the default values of the fields (= undefined) cannot be stripped off from the generated types
|
||||
// so we need to manually re-define the type to match it with core API token info type
|
||||
export interface AggregatedTokenInfo extends Pick<multichain.AggregatedTokenInfo, 'chain_infos' | 'address_hash'> {
|
||||
type: TokenType;
|
||||
circulating_market_cap: string | null;
|
||||
decimals: string | null;
|
||||
holders_count: string | null;
|
||||
name: string | null;
|
||||
symbol: string | null;
|
||||
total_supply: string | null;
|
||||
exchange_rate: string | null;
|
||||
icon_url: string | null;
|
||||
reputation: null;
|
||||
}
|
||||
|
||||
export interface AddressTokenItem extends Omit<multichain.ListAddressTokensResponse_AggregatedTokenBalanceInfo, 'token' | 'token_id'> {
|
||||
token: AggregatedTokenInfo;
|
||||
token_id: string | null;
|
||||
token_instance: null;
|
||||
}
|
||||
|
||||
export interface AddressTokensResponse extends Omit<multichain.ListAddressTokensResponse, 'items'> {
|
||||
items: Array<AddressTokenItem>;
|
||||
}
|
||||
|
||||
export interface TokensResponse extends Omit<multichain.ListClusterTokensResponse, 'items'> {
|
||||
items: Array<AggregatedTokenInfo>;
|
||||
}
|
||||
|
||||
// types for quick search results
|
||||
export type QuickSearchResultBlock = {
|
||||
type: 'block';
|
||||
block_number: string;
|
||||
block_hash: undefined;
|
||||
chain_id: string;
|
||||
} | {
|
||||
type: 'block';
|
||||
block_number: undefined;
|
||||
block_hash: string;
|
||||
chain_id: string;
|
||||
};
|
||||
|
||||
export interface QuickSearchResultTransaction {
|
||||
type: 'transaction';
|
||||
transaction_hash: string;
|
||||
chain_id: string;
|
||||
}
|
||||
|
||||
export interface QuickSearchResultAddress {
|
||||
type: 'address';
|
||||
address_hash: string;
|
||||
chain_infos: Record<string, multichain.GetAddressResponse_ChainInfo>;
|
||||
}
|
||||
|
||||
export interface QuickSearchResultToken {
|
||||
type: 'token';
|
||||
token_type: 'ERC-20' | 'ERC-721' | 'ERC-1155';
|
||||
name: string;
|
||||
symbol: string;
|
||||
address_hash: string;
|
||||
icon_url: string | null;
|
||||
is_smart_contract_verified: boolean;
|
||||
chain_id: string;
|
||||
reputation: null;
|
||||
total_supply: string | null;
|
||||
exchange_rate: string | null;
|
||||
chain_infos: Record<string, multichain.AggregatedTokenInfo_ChainInfo>;
|
||||
}
|
||||
|
||||
export interface QuickSearchResultDomain {
|
||||
type: 'ens_domain';
|
||||
ens_info: {
|
||||
address_hash: string;
|
||||
expiry_date?: string;
|
||||
name: string;
|
||||
protocol?: bens.ProtocolInfo;
|
||||
};
|
||||
address_hash: string;
|
||||
}
|
||||
|
||||
export type QuickSearchResultItem = QuickSearchResultBlock |
|
||||
QuickSearchResultTransaction |
|
||||
QuickSearchResultAddress |
|
||||
QuickSearchResultToken |
|
||||
QuickSearchResultDomain;
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { CctxListItem } from '@blockscout/zetachain-cctx-types';
|
||||
import type * as api from 'types/api/search';
|
||||
import type * as multichain from 'types/client/multichain-aggregator';
|
||||
|
||||
export interface SearchResultFutureBlock {
|
||||
type: 'block';
|
||||
@@ -18,3 +19,5 @@ export interface SearchResultZetaChainCCTX {
|
||||
}
|
||||
|
||||
export type SearchResultItem = api.SearchResultItem | SearchResultBlock | SearchResultZetaChainCCTX;
|
||||
|
||||
export type QuickSearchResultItem = multichain.QuickSearchResultItem | SearchResultItem;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { AdvancedFilterAge } from 'types/api/advancedFilter';
|
||||
|
||||
import type { ExternalChain } from '../externalChains';
|
||||
|
||||
export const ZETA_CHAIN_CCTX_STATUS_REDUCED_FILTERS = [ 'Success', 'Pending', 'Failed' ] as const;
|
||||
export type StatusReducedFilters = typeof ZETA_CHAIN_CCTX_STATUS_REDUCED_FILTERS[number];
|
||||
|
||||
@@ -19,3 +21,20 @@ export type ZetaChainCCTXFilterParams = {
|
||||
coin_type?: Array<CoinTypeFilter> | CoinTypeFilter;
|
||||
hash?: string;
|
||||
};
|
||||
|
||||
export interface ZetaChainChainsConfigEnv {
|
||||
chain_id: number;
|
||||
chain_name: string | null;
|
||||
chain_logo?: string | null;
|
||||
instance_url?: string;
|
||||
address_url_template?: string;
|
||||
tx_url_template?: string;
|
||||
}
|
||||
|
||||
export type ZetaChainExternalChainConfig = ExternalChain | {
|
||||
id: string;
|
||||
name: string;
|
||||
logo: string | undefined;
|
||||
address_url_template?: string;
|
||||
tx_url_template?: string;
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user