Commit 17e3b6b2 authored by tom goriunov's avatar tom goriunov Committed by GitHub

API documentation page (#2725)

* add tabs to api-docs page and put swagger widgets in accordion

* move GraphQL widget into tab

* refactor API docs feature

* add swagger for user ops, bens and tac services

* update screenshots

* fix ts

* fix screenshot
parent 319f7ff6
...@@ -143,6 +143,17 @@ const tacApi = (() => { ...@@ -143,6 +143,17 @@ const tacApi = (() => {
}); });
})(); })();
const userOpsApi = (() => {
const apiHost = getEnvValue('NEXT_PUBLIC_USER_OPS_INDEXER_API_HOST');
if (!apiHost) {
return;
}
return Object.freeze({
endpoint: apiHost,
});
})();
const visualizeApi = (() => { const visualizeApi = (() => {
const apiHost = getEnvValue('NEXT_PUBLIC_VISUALIZE_API_HOST'); const apiHost = getEnvValue('NEXT_PUBLIC_VISUALIZE_API_HOST');
if (!apiHost) { if (!apiHost) {
...@@ -169,6 +180,7 @@ const apis: Apis = Object.freeze({ ...@@ -169,6 +180,7 @@ const apis: Apis = Object.freeze({
rewards: rewardsApi, rewards: rewardsApi,
stats: statsApi, stats: statsApi,
tac: tacApi, tac: tacApi,
userOps: userOpsApi,
visualize: visualizeApi, visualize: visualizeApi,
}); });
......
import type { Feature } from './types';
import type { ApiDocsTabId } from 'types/views/apiDocs';
import { API_DOCS_TABS } from 'types/views/apiDocs';
import { getEnvValue, parseEnvJson } from '../utils';
const graphqlDefaultTxnHash = getEnvValue('NEXT_PUBLIC_GRAPHIQL_TRANSACTION');
const tabs = (() => {
const value = (parseEnvJson<Array<ApiDocsTabId>>(getEnvValue('NEXT_PUBLIC_API_DOCS_TABS')) || API_DOCS_TABS)
.filter((tab) => API_DOCS_TABS.includes(tab))
.filter((tab) => !graphqlDefaultTxnHash && tab === 'graphql_api' ? false : true);
return value.length > 0 ? value : undefined;
})();
const title = 'API documentation';
const config: Feature<{
tabs: Array<ApiDocsTabId>;
coreApiSwaggerUrl: string;
graphqlDefaultTxnHash?: string;
}> = (() => {
if (tabs) {
return Object.freeze({
title,
isEnabled: true,
tabs,
coreApiSwaggerUrl: getEnvValue('NEXT_PUBLIC_API_SPEC_URL') || `https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml`,
graphqlDefaultTxnHash,
});
}
return Object.freeze({
title,
isEnabled: false,
});
})();
export default config;
import type { Feature } from './types';
import { getEnvValue } from '../utils';
const defaultTxHash = getEnvValue('NEXT_PUBLIC_GRAPHIQL_TRANSACTION');
const title = 'GraphQL API documentation';
const config: Feature<{ defaultTxHash: string | undefined }> = (() => {
if (defaultTxHash === 'none') {
return Object.freeze({
title,
isEnabled: false,
});
}
return Object.freeze({
title,
isEnabled: true,
defaultTxHash,
});
})();
export default config;
...@@ -5,6 +5,7 @@ export { default as addressMetadata } from './addressMetadata'; ...@@ -5,6 +5,7 @@ export { default as addressMetadata } from './addressMetadata';
export { default as address3rdPartyWidgets } from './address3rdPartyWidgets'; export { default as address3rdPartyWidgets } from './address3rdPartyWidgets';
export { default as adsBanner } from './adsBanner'; export { default as adsBanner } from './adsBanner';
export { default as adsText } from './adsText'; export { default as adsText } from './adsText';
export { default as apiDocs } from './apiDocs';
export { default as beaconChain } from './beaconChain'; export { default as beaconChain } from './beaconChain';
export { default as bridgedTokens } from './bridgedTokens'; export { default as bridgedTokens } from './bridgedTokens';
export { default as blockchainInteraction } from './blockchainInteraction'; export { default as blockchainInteraction } from './blockchainInteraction';
...@@ -19,7 +20,6 @@ export { default as faultProofSystem } from './faultProofSystem'; ...@@ -19,7 +20,6 @@ export { default as faultProofSystem } from './faultProofSystem';
export { default as gasTracker } from './gasTracker'; export { default as gasTracker } from './gasTracker';
export { default as getGasButton } from './getGasButton'; export { default as getGasButton } from './getGasButton';
export { default as googleAnalytics } from './googleAnalytics'; export { default as googleAnalytics } from './googleAnalytics';
export { default as graphqlApiDocs } from './graphqlApiDocs';
export { default as growthBook } from './growthBook'; export { default as growthBook } from './growthBook';
export { default as marketplace } from './marketplace'; export { default as marketplace } from './marketplace';
export { default as metasuites } from './metasuites'; export { default as metasuites } from './metasuites';
...@@ -30,7 +30,6 @@ export { default as nameService } from './nameService'; ...@@ -30,7 +30,6 @@ export { default as nameService } from './nameService';
export { default as opSuperchain } from './opSuperchain'; export { default as opSuperchain } from './opSuperchain';
export { default as pools } from './pools'; export { default as pools } from './pools';
export { default as publicTagsSubmission } from './publicTagsSubmission'; export { default as publicTagsSubmission } from './publicTagsSubmission';
export { default as restApiDocs } from './restApiDocs';
export { default as rewards } from './rewards'; export { default as rewards } from './rewards';
export { default as rollbar } from './rollbar'; export { default as rollbar } from './rollbar';
export { default as rollup } from './rollup'; export { default as rollup } from './rollup';
......
import type { Feature } from './types';
import { getEnvValue } from '../utils';
const DEFAULT_URL = `https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml`;
const envValue = getEnvValue('NEXT_PUBLIC_API_SPEC_URL');
const title = 'REST API documentation';
const config: Feature<{ specUrl: string }> = (() => {
if (envValue === 'none') {
return Object.freeze({
title,
isEnabled: false,
});
}
return Object.freeze({
title,
isEnabled: true,
specUrl: envValue || DEFAULT_URL,
});
})();
export default config;
import type { ContractCodeIde } from 'types/client/contract'; import type { ContractCodeIde } from 'types/client/contract';
import { NAVIGATION_LINK_IDS, type NavItemExternal, type NavigationLinkId, type NavigationLayout } from 'types/client/navigation'; import { type NavItemExternal, type NavigationLayout } from 'types/client/navigation';
import { HOME_STATS_WIDGET_IDS, type ChainIndicatorId, type HeroBannerConfig, type HomeStatsWidgetId } from 'types/homepage'; import { HOME_STATS_WIDGET_IDS, type ChainIndicatorId, type HeroBannerConfig, type HomeStatsWidgetId } from 'types/homepage';
import type { NetworkExplorer } from 'types/networks'; import type { NetworkExplorer } from 'types/networks';
import type { ColorThemeId } from 'types/settings'; import type { ColorThemeId } from 'types/settings';
...@@ -11,21 +11,6 @@ import * as features from './features'; ...@@ -11,21 +11,6 @@ import * as features from './features';
import * as views from './ui/views'; import * as views from './ui/views';
import { getEnvValue, getExternalAssetFilePath, parseEnvJson } from './utils'; import { getEnvValue, getExternalAssetFilePath, parseEnvJson } from './utils';
const hiddenLinks = (() => {
const parsedValue = parseEnvJson<Array<NavigationLinkId>>(getEnvValue('NEXT_PUBLIC_NAVIGATION_HIDDEN_LINKS')) || [];
if (!Array.isArray(parsedValue)) {
return undefined;
}
const result = NAVIGATION_LINK_IDS.reduce((result, item) => {
result[item] = parsedValue.includes(item);
return result;
}, {} as Record<NavigationLinkId, boolean>);
return result;
})();
const homePageStats: Array<HomeStatsWidgetId> = (() => { const homePageStats: Array<HomeStatsWidgetId> = (() => {
const parsedValue = parseEnvJson<Array<HomeStatsWidgetId>>(getEnvValue('NEXT_PUBLIC_HOMEPAGE_STATS')); const parsedValue = parseEnvJson<Array<HomeStatsWidgetId>>(getEnvValue('NEXT_PUBLIC_HOMEPAGE_STATS'));
...@@ -43,7 +28,7 @@ const homePageStats: Array<HomeStatsWidgetId> = (() => { ...@@ -43,7 +28,7 @@ const homePageStats: Array<HomeStatsWidgetId> = (() => {
})(); })();
const highlightedRoutes = (() => { const highlightedRoutes = (() => {
const parsedValue = parseEnvJson<Array<NavigationLinkId>>(getEnvValue('NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES')); const parsedValue = parseEnvJson<Array<string>>(getEnvValue('NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES'));
return Array.isArray(parsedValue) ? parsedValue : []; return Array.isArray(parsedValue) ? parsedValue : [];
})(); })();
...@@ -62,7 +47,6 @@ const UI = Object.freeze({ ...@@ -62,7 +47,6 @@ const UI = Object.freeze({
'default': getExternalAssetFilePath('NEXT_PUBLIC_NETWORK_ICON'), 'default': getExternalAssetFilePath('NEXT_PUBLIC_NETWORK_ICON'),
dark: getExternalAssetFilePath('NEXT_PUBLIC_NETWORK_ICON_DARK'), dark: getExternalAssetFilePath('NEXT_PUBLIC_NETWORK_ICON_DARK'),
}, },
hiddenLinks,
highlightedRoutes, highlightedRoutes,
otherLinks: parseEnvJson<Array<NavItemExternal>>(getEnvValue('NEXT_PUBLIC_OTHER_LINKS')) || [], otherLinks: parseEnvJson<Array<NavItemExternal>>(getEnvValue('NEXT_PUBLIC_OTHER_LINKS')) || [],
featuredNetworks: getExternalAssetFilePath('NEXT_PUBLIC_FEATURED_NETWORKS'), featuredNetworks: getExternalAssetFilePath('NEXT_PUBLIC_FEATURED_NETWORKS'),
......
...@@ -9,6 +9,8 @@ NEXT_PUBLIC_APP_PORT=3000 ...@@ -9,6 +9,8 @@ NEXT_PUBLIC_APP_PORT=3000
NEXT_PUBLIC_APP_ENV=development NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
NEXT_PUBLIC_USER_OPS_INDEXER_API_HOST=https://user-ops-indexer-base-mainnet.k8s-prod-2.blockscout.com
# Instance ENVs # Instance ENVs
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
NEXT_PUBLIC_API_BASE_PATH=/ NEXT_PUBLIC_API_BASE_PATH=/
......
...@@ -54,6 +54,7 @@ NEXT_PUBLIC_METADATA_SERVICE_API_HOST=http://localhost:3007 ...@@ -54,6 +54,7 @@ NEXT_PUBLIC_METADATA_SERVICE_API_HOST=http://localhost:3007
NEXT_PUBLIC_NAME_SERVICE_API_HOST=http://localhost:3008 NEXT_PUBLIC_NAME_SERVICE_API_HOST=http://localhost:3008
NEXT_PUBLIC_REWARDS_SERVICE_API_HOST=http://localhost:3009 NEXT_PUBLIC_REWARDS_SERVICE_API_HOST=http://localhost:3009
NEXT_PUBLIC_TAC_OPERATION_LIFECYCLE_API_HOST=http://localhost:3100 NEXT_PUBLIC_TAC_OPERATION_LIFECYCLE_API_HOST=http://localhost:3100
NEXT_PUBLIC_USER_OPS_INDEXER_API_HOST=http://localhost:3110
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx
NEXT_PUBLIC_VIEWS_ADDRESS_FORMAT=['base16','bech32'] NEXT_PUBLIC_VIEWS_ADDRESS_FORMAT=['base16','bech32']
......
...@@ -22,7 +22,6 @@ NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] ...@@ -22,7 +22,6 @@ NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=radial-gradient(farthest-corner at 0% 0%, rgba(183, 148, 244, 0.80) 0%, rgba(0, 163, 196, 0.80) 100%) NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=radial-gradient(farthest-corner at 0% 0%, rgba(183, 148, 244, 0.80) 0%, rgba(0, 163, 196, 0.80) 100%)
NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=rgb(255,255,255) NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=rgb(255,255,255)
NEXT_PUBLIC_IS_TESTNET=true NEXT_PUBLIC_IS_TESTNET=true
NEXT_PUBLIC_NAVIGATION_HIDDEN_LINKS=[]
NEXT_PUBLIC_NAVIGATION_LAYOUT=vertical NEXT_PUBLIC_NAVIGATION_LAYOUT=vertical
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=ETH NEXT_PUBLIC_NETWORK_CURRENCY_NAME=ETH
......
...@@ -20,8 +20,9 @@ import { GAS_UNITS } from '../../../types/client/gasTracker'; ...@@ -20,8 +20,9 @@ import { GAS_UNITS } from '../../../types/client/gasTracker';
import type { GasUnit } from '../../../types/client/gasTracker'; import type { GasUnit } from '../../../types/client/gasTracker';
import type { MarketplaceAppOverview, MarketplaceAppSecurityReportRaw, MarketplaceAppSecurityReport } from '../../../types/client/marketplace'; import type { MarketplaceAppOverview, MarketplaceAppSecurityReportRaw, MarketplaceAppSecurityReport } from '../../../types/client/marketplace';
import type { MultichainProviderConfig } from '../../../types/client/multichainProviderConfig'; import type { MultichainProviderConfig } from '../../../types/client/multichainProviderConfig';
import { NAVIGATION_LINK_IDS } from '../../../types/client/navigation'; import type { ApiDocsTabId } from '../../../types/views/apiDocs';
import type { NavItemExternal, NavigationLinkId, NavigationLayout } from '../../../types/client/navigation'; import { API_DOCS_TABS } from '../../../types/views/apiDocs';
import type { NavItemExternal, NavigationLayout } from '../../../types/client/navigation';
import { ROLLUP_TYPES } from '../../../types/client/rollup'; import { ROLLUP_TYPES } from '../../../types/client/rollup';
import type { BridgedTokenChain, TokenBridge } from '../../../types/client/token'; import type { BridgedTokenChain, TokenBridge } from '../../../types/client/token';
import { PROVIDERS as TX_INTERPRETATION_PROVIDERS } from '../../../types/client/txInterpretation'; import { PROVIDERS as TX_INTERPRETATION_PROVIDERS } from '../../../types/client/txInterpretation';
...@@ -450,6 +451,35 @@ const celoSchema = yup ...@@ -450,6 +451,35 @@ const celoSchema = yup
NEXT_PUBLIC_CELO_ENABLED: yup.boolean(), NEXT_PUBLIC_CELO_ENABLED: yup.boolean(),
}); });
const apiDocsScheme = yup
.object()
.shape({
NEXT_PUBLIC_API_DOCS_TABS: yup.array()
.transform(replaceQuotes)
.json()
.of(yup.string<ApiDocsTabId>().oneOf(API_DOCS_TABS)),
NEXT_PUBLIC_API_SPEC_URL: yup
.string()
.test(urlTest),
NEXT_PUBLIC_GRAPHIQL_TRANSACTION: yup
.string()
.matches(regexp.HEX_REGEXP),
});
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 adButlerConfigSchema = yup const adButlerConfigSchema = yup
.object<AdButlerConfig>() .object<AdButlerConfig>()
.transform(replaceQuotes) .transform(replaceQuotes)
...@@ -870,11 +900,6 @@ const schema = yup ...@@ -870,11 +900,6 @@ const schema = yup
.transform(replaceQuotes) .transform(replaceQuotes)
.json() .json()
.of(navItemExternalSchema), .of(navItemExternalSchema),
NEXT_PUBLIC_NAVIGATION_HIDDEN_LINKS: yup
.array()
.transform(replaceQuotes)
.json()
.of(yup.string<NavigationLinkId>().oneOf(NAVIGATION_LINK_IDS)),
NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES: yup NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES: yup
.array() .array()
.transform(replaceQuotes) .transform(replaceQuotes)
...@@ -988,14 +1013,6 @@ const schema = yup ...@@ -988,14 +1013,6 @@ const schema = yup
NEXT_PUBLIC_MAX_CONTENT_WIDTH_ENABLED: yup.boolean(), NEXT_PUBLIC_MAX_CONTENT_WIDTH_ENABLED: yup.boolean(),
// 5. Features configuration // 5. Features configuration
NEXT_PUBLIC_API_SPEC_URL: yup
.mixed()
.test('shape', 'Invalid schema were provided for NEXT_PUBLIC_API_SPEC_URL, it should be either URL-string or "none" string literal', (data) => {
const isNoneSchema = yup.string().oneOf([ 'none' ]);
const isUrlStringSchema = yup.string().test(urlTest);
return isNoneSchema.isValidSync(data) || isUrlStringSchema.isValidSync(data);
}),
NEXT_PUBLIC_STATS_API_HOST: yup.string().test(urlTest), NEXT_PUBLIC_STATS_API_HOST: yup.string().test(urlTest),
NEXT_PUBLIC_STATS_API_BASE_PATH: yup.string(), NEXT_PUBLIC_STATS_API_BASE_PATH: yup.string(),
NEXT_PUBLIC_VISUALIZE_API_HOST: yup.string().test(urlTest), NEXT_PUBLIC_VISUALIZE_API_HOST: yup.string().test(urlTest),
...@@ -1003,14 +1020,6 @@ const schema = yup ...@@ -1003,14 +1020,6 @@ const schema = yup
NEXT_PUBLIC_CONTRACT_INFO_API_HOST: yup.string().test(urlTest), NEXT_PUBLIC_CONTRACT_INFO_API_HOST: yup.string().test(urlTest),
NEXT_PUBLIC_NAME_SERVICE_API_HOST: yup.string().test(urlTest), NEXT_PUBLIC_NAME_SERVICE_API_HOST: yup.string().test(urlTest),
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST: yup.string().test(urlTest), NEXT_PUBLIC_ADMIN_SERVICE_API_HOST: yup.string().test(urlTest),
NEXT_PUBLIC_GRAPHIQL_TRANSACTION: yup
.mixed()
.test('shape', 'Invalid schema were provided for NEXT_PUBLIC_GRAPHIQL_TRANSACTION, it should be either Hex-string or "none" string literal', (data) => {
const isNoneSchema = yup.string().oneOf([ 'none' ]);
const isHashStringSchema = yup.string().matches(regexp.HEX_REGEXP);
return isNoneSchema.isValidSync(data) || isHashStringSchema.isValidSync(data);
}),
NEXT_PUBLIC_WEB3_WALLETS: yup NEXT_PUBLIC_WEB3_WALLETS: yup
.mixed() .mixed()
.test('shape', 'Invalid schema were provided for NEXT_PUBLIC_WEB3_WALLETS, it should be either array or "none" string literal', (data) => { .test('shape', 'Invalid schema were provided for NEXT_PUBLIC_WEB3_WALLETS, it should be either array or "none" string literal', (data) => {
...@@ -1033,7 +1042,6 @@ const schema = yup ...@@ -1033,7 +1042,6 @@ const schema = yup
NEXT_PUBLIC_SEO_ENHANCED_DATA_ENABLED: yup.boolean(), NEXT_PUBLIC_SEO_ENHANCED_DATA_ENABLED: yup.boolean(),
NEXT_PUBLIC_SAFE_TX_SERVICE_URL: yup.string().test(urlTest), NEXT_PUBLIC_SAFE_TX_SERVICE_URL: yup.string().test(urlTest),
NEXT_PUBLIC_IS_SUAVE_CHAIN: yup.boolean(), NEXT_PUBLIC_IS_SUAVE_CHAIN: yup.boolean(),
NEXT_PUBLIC_HAS_USER_OPS: yup.boolean(),
NEXT_PUBLIC_METASUITES_ENABLED: yup.boolean(), NEXT_PUBLIC_METASUITES_ENABLED: yup.boolean(),
NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG: yup NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG: yup
.array() .array()
...@@ -1156,8 +1164,10 @@ const schema = yup ...@@ -1156,8 +1164,10 @@ const schema = yup
.concat(beaconChainSchema) .concat(beaconChainSchema)
.concat(bridgedTokensSchema) .concat(bridgedTokensSchema)
.concat(sentrySchema) .concat(sentrySchema)
.concat(apiDocsScheme)
.concat(tacSchema) .concat(tacSchema)
.concat(address3rdPartyWidgetsConfigSchema) .concat(address3rdPartyWidgetsConfigSchema)
.concat(addressMetadataSchema); .concat(addressMetadataSchema)
.concat(userOpsSchema);
export default schema; export default schema;
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=none NEXT_PUBLIC_API_DOCS_TABS=[]
NEXT_PUBLIC_API_SPEC_URL=none
NEXT_PUBLIC_VIEWS_CONTRACT_EXTRA_VERIFICATION_METHODS=none NEXT_PUBLIC_VIEWS_CONTRACT_EXTRA_VERIFICATION_METHODS=none
NEXT_PUBLIC_HOMEPAGE_STATS=[] NEXT_PUBLIC_HOMEPAGE_STATS=[]
NEXT_PUBLIC_VIEWS_ADDRESS_FORMAT=['base16','bech32'] NEXT_PUBLIC_VIEWS_ADDRESS_FORMAT=['base16','bech32']
...@@ -9,3 +8,5 @@ NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY=deprecated ...@@ -9,3 +8,5 @@ NEXT_PUBLIC_RE_CAPTCHA_V3_APP_SITE_KEY=deprecated
NEXT_PUBLIC_NETWORK_RPC_URL=['https://example.com','https://example2.com'] NEXT_PUBLIC_NETWORK_RPC_URL=['https://example.com','https://example2.com']
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://example.com NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://example.com
NEXT_PUBLIC_METADATA_ADDRESS_TAGS_UPDATE_ENABLED=false NEXT_PUBLIC_METADATA_ADDRESS_TAGS_UPDATE_ENABLED=false
NEXT_PUBLIC_HAS_USER_OPS=true
NEXT_PUBLIC_USER_OPS_INDEXER_API_HOST=https://example.com
\ No newline at end of file
...@@ -46,7 +46,6 @@ NEXT_PUBLIC_IS_TESTNET=true ...@@ -46,7 +46,6 @@ NEXT_PUBLIC_IS_TESTNET=true
NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE='<a href="#">Hello</a>' NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE='<a href="#">Hello</a>'
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://example.com NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://example.com
NEXT_PUBLIC_METASUITES_ENABLED=true NEXT_PUBLIC_METASUITES_ENABLED=true
NEXT_PUBLIC_NAVIGATION_HIDDEN_LINKS=['eth_rpc_api','rpc_api']
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18 NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
......
...@@ -13,3 +13,4 @@ ...@@ -13,3 +13,4 @@
| NEXT_PUBLIC_SWAP_BUTTON_URL | `string` | Application ID in the marketplace or website URL | - | - | `uniswap` | v1.24.0 | v1.31.0 | Replaced by NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS | | NEXT_PUBLIC_SWAP_BUTTON_URL | `string` | Application ID in the marketplace or website URL | - | - | `uniswap` | v1.24.0 | v1.31.0 | Replaced by NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS |
| NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME | `boolean` | Set to false if average block time is useless for the network | - | `true` | `false` | v1.0.x+ | v1.35.0 | Replaced by NEXT_PUBLIC_HOMEPAGE_STATS | | NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME | `boolean` | Set to false if average block time is useless for the network | - | `true` | `false` | v1.0.x+ | v1.35.0 | Replaced by NEXT_PUBLIC_HOMEPAGE_STATS |
| NEXT_PUBLIC_CELO_L2_UPGRADE_BLOCK | `number` | Indicates the block number when the Celo-type chain transitioned to L2. This is used to display links to the Epoch block page from a regular block page. | - | - | `26369280` | v1.37.0+ | v2.2.0 | Removed; configuration done on the API side | | NEXT_PUBLIC_CELO_L2_UPGRADE_BLOCK | `number` | Indicates the block number when the Celo-type chain transitioned to L2. This is used to display links to the Epoch block page from a regular block page. | - | - | `26369280` | v1.37.0+ | v2.2.0 | Removed; configuration done on the API side |
| NEXT_PUBLIC_NAVIGATION_HIDDEN_LINKS | `Array<LinkId>` | List of external links hidden in the navigation. Supported ids are `eth_rpc_api`, `rpc_api` | - | - | `['eth_rpc_api']` | v1.16.0+ | v2.3.0 | Use NEXT_PUBLIC_API_DOCS_TABS instead to hide tabs on the API docs page. |
...@@ -47,7 +47,7 @@ All json-like values should be single-quoted. If it contains a hash (`#`) or a d ...@@ -47,7 +47,7 @@ All json-like values should be single-quoted. If it contains a hash (`#`) or a d
- [Mixpanel analytics](#mixpanel-analytics) - [Mixpanel analytics](#mixpanel-analytics)
- [GrowthBook feature flagging and A/B testing](#growthbook-feature-flagging-and-ab-testing) - [GrowthBook feature flagging and A/B testing](#growthbook-feature-flagging-and-ab-testing)
- [GraphQL API documentation](#graphql-api-documentation) - [GraphQL API documentation](#graphql-api-documentation)
- [REST API documentation](#rest-api-documentation) - [API documentation](#api-documentation)
- [Marketplace](#marketplace) - [Marketplace](#marketplace)
- [Solidity to UML diagrams](#solidity-to-uml-diagrams) - [Solidity to UML diagrams](#solidity-to-uml-diagrams)
- [Blockchain statistics](#blockchain-statistics) - [Blockchain statistics](#blockchain-statistics)
...@@ -163,7 +163,6 @@ _Note_ Here, all values are arrays of up to two strings. The first string repres ...@@ -163,7 +163,6 @@ _Note_ Here, all values are arrays of up to two strings. The first string repres
| NEXT_PUBLIC_NETWORK_ICON_DARK | `string` | Network icon for dark color mode; if not provided, **inverted** regular icon will be used instead | - | - | `https://placekitten.com/60/60` | v1.0.x+ | | NEXT_PUBLIC_NETWORK_ICON_DARK | `string` | Network icon for dark color mode; if not provided, **inverted** regular icon will be used instead | - | - | `https://placekitten.com/60/60` | v1.0.x+ |
| NEXT_PUBLIC_FEATURED_NETWORKS | `string` | URL of configuration file (`.json` format only) or file content string representation. It contains list of featured networks that will be shown in the network menu. See [below](#featured-network-configuration-properties) list of available properties for particular network | - | - | `https://example.com/featured_networks_config.json` \| `[{'title':'Astar(EVM)','url':'https://astar.blockscout.com/','group':'Mainnets','icon':'https://example.com/astar.svg'}]` | v1.0.x+ | | NEXT_PUBLIC_FEATURED_NETWORKS | `string` | URL of configuration file (`.json` format only) or file content string representation. It contains list of featured networks that will be shown in the network menu. See [below](#featured-network-configuration-properties) list of available properties for particular network | - | - | `https://example.com/featured_networks_config.json` \| `[{'title':'Astar(EVM)','url':'https://astar.blockscout.com/','group':'Mainnets','icon':'https://example.com/astar.svg'}]` | v1.0.x+ |
| NEXT_PUBLIC_OTHER_LINKS | `Array<{url: string; text: string}>` | List of links for the "Other" navigation menu | - | - | `[{'url':'https://blockscout.com','text':'Blockscout'}]` | v1.0.x+ | | NEXT_PUBLIC_OTHER_LINKS | `Array<{url: string; text: string}>` | List of links for the "Other" navigation menu | - | - | `[{'url':'https://blockscout.com','text':'Blockscout'}]` | v1.0.x+ |
| NEXT_PUBLIC_NAVIGATION_HIDDEN_LINKS | `Array<LinkId>` | List of external links hidden in the navigation. Supported ids are `eth_rpc_api`, `rpc_api` | - | - | `['eth_rpc_api']` | v1.16.0+ |
| NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES | `Array<string>` | List of menu item routes that should have a lightning label | - | - | `['/accounts']` | v1.31.0+ | | NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES | `Array<string>` | List of menu item routes that should have a lightning label | - | - | `['/accounts']` | v1.31.0+ |
| NEXT_PUBLIC_NAVIGATION_LAYOUT | `vertical \| horizontal` | Navigation menu layout type | - | `vertical` | `horizontal` | v1.32.0+ | | NEXT_PUBLIC_NAVIGATION_LAYOUT | `vertical \| horizontal` | Navigation menu layout type | - | `vertical` | `horizontal` | v1.32.0+ |
...@@ -460,6 +459,7 @@ This feature is **enabled by default** with the `coinzilla` ads provider. To swi ...@@ -460,6 +459,7 @@ This feature is **enabled by default** with the `coinzilla` ads provider. To swi
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | | Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
| --- | --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_HAS_USER_OPS | `boolean` | Set to true to show user operations related data and pages | - | - | `true` | v1.23.0+ | | NEXT_PUBLIC_HAS_USER_OPS | `boolean` | Set to true to show user operations related data and pages | - | - | `true` | v1.23.0+ |
| NEXT_PUBLIC_USER_OPS_INDEXER_API_HOST | `boolean` | The user operations indexer API host; pass to show API documentation for the service | - | - | `true` | v2.3.0+ |
&nbsp; &nbsp;
...@@ -527,23 +527,13 @@ This feature is **enabled by default** with the `coinzilla` ads provider. To swi ...@@ -527,23 +527,13 @@ This feature is **enabled by default** with the `coinzilla` ads provider. To swi
&nbsp; &nbsp;
### GraphQL API documentation ### API documentation
This feature is **always enabled**, but you can disable it by passing `none` value to `NEXT_PUBLIC_GRAPHIQL_TRANSACTION` variable.
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
| --- | --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_GRAPHIQL_TRANSACTION | `string` | Txn hash for default query at GraphQl playground page. Pass `none` to disable the feature. | - | - | `0x4a0ed8ddf751a7cb5297f827699117b0f6d21a0b2907594d300dc9fed75c7e62` | v1.0.x+ |
&nbsp;
### REST API documentation
This feature is **always enabled**, but you can disable it by passing `none` value to `NEXT_PUBLIC_API_SPEC_URL` variable.
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | | Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
| --- | --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_API_SPEC_URL | `string` | Spec to be displayed on `/api-docs` page. Pass `none` to disable the feature. | - | `https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml` | `https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml` | v1.0.x+ | | NEXT_PUBLIC_API_DOCS_TABS | `Array<TabId>` | Controls which tabs appear on the API documentation page. Possible values for `TabId` are `rest_api`, `eth_rpc_api`, `rpc_api`, and `graphql_api`. **Note** that this variable has a default value, so the feature is enabled by default. Pass an empty array to disable it. | - | `['rest_api','eth_rpc_api','rpc_api','graphql_api']` | `[]` | v2.3.x+ |
| NEXT_PUBLIC_API_SPEC_URL | `string` | Spec of Blockscout core API to be displayed on the page. | - | `https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml` | `https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml` | v1.0.x+ |
| NEXT_PUBLIC_GRAPHIQL_TRANSACTION | `string` | Txn hash for default query at GraphQl API. | - | - | `0x4a0ed8ddf751a7cb5297f827699117b0f6d21a0b2907594d300dc9fed75c7e62` | v1.0.x+ |
&nbsp; &nbsp;
......
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 30 30">
<path fill="currentColor" d="m17.613 5.751 5.082 2.85a2.255 2.255 0 0 1 3.123-.077c.153.14.282.301.389.477.606 1.028.238 2.339-.82 2.928-.18.1-.372.175-.575.223v5.699c1.18.273 1.91 1.423 1.627 2.569-.05.204-.13.4-.239.581-.61 1.023-1.958 1.374-3.014.782a2.18 2.18 0 0 1-.542-.429l-5.052 2.832c.383 1.124-.245 2.335-1.402 2.706a2.275 2.275 0 0 1-.69.108c-1.216.001-2.203-.955-2.204-2.136 0-.205.03-.41.09-.609l-5.083-2.847a2.252 2.252 0 0 1-3.117.07 2.098 2.098 0 0 1-.072-3.026 2.224 2.224 0 0 1 1.075-.603l.001-5.699c-1.184-.276-1.913-1.433-1.628-2.584.048-.198.127-.389.23-.566.61-1.024 1.96-1.374 3.015-.782.183.101.35.228.495.377l5.086-2.85c-.347-1.133.32-2.322 1.484-2.657.203-.059.416-.088.628-.088 1.216-.001 2.203.954 2.204 2.134.001.209-.03.418-.091.617Zm-.53.877a.799.799 0 0 1-.06.058l6.654 11.19c.027-.009.058-.016.084-.023v-5.707c-1.179-.283-1.897-1.442-1.604-2.588.006-.024.012-.049.02-.072l-5.095-2.858Zm-3.106.059-.062-.06L8.82 9.479c.337 1.135-.337 2.318-1.505 2.645l-.078.021v5.708l.086.023 6.655-11.19-.002.001Zm2.138.507a2.285 2.285 0 0 1-1.228 0L8.234 18.383c.302.283.517.645.618 1.041h13.297c.1-.398.317-.761.622-1.044L16.115 7.194Zm1.016 16.227 5.06-2.838a1.715 1.715 0 0 1-.04-.142H8.85l-.022.083 5.087 2.852c.4-.404.962-.653 1.586-.653.646 0 1.226.269 1.63.698Z"/>
</svg>
...@@ -22,6 +22,7 @@ import type { ...@@ -22,6 +22,7 @@ import type {
TacOperationLifecycleApiResourceName, TacOperationLifecycleApiResourceName,
TacOperationLifecycleApiResourcePayload, TacOperationLifecycleApiResourcePayload,
} from './services/tac-operation-lifecycle'; } from './services/tac-operation-lifecycle';
import { USER_OPS_API_RESOURCES } from './services/userOps';
import type { IsPaginated } from './services/utils'; import type { IsPaginated } from './services/utils';
import { VISUALIZE_API_RESOURCES } from './services/visualize'; import { VISUALIZE_API_RESOURCES } from './services/visualize';
import type { VisualizeApiResourceName, VisualizeApiResourcePayload } from './services/visualize'; import type { VisualizeApiResourceName, VisualizeApiResourcePayload } from './services/visualize';
...@@ -36,6 +37,7 @@ export const RESOURCES = { ...@@ -36,6 +37,7 @@ export const RESOURCES = {
rewards: REWARDS_API_RESOURCES, rewards: REWARDS_API_RESOURCES,
stats: STATS_API_RESOURCES, stats: STATS_API_RESOURCES,
tac: TAC_OPERATION_LIFECYCLE_API_RESOURCES, tac: TAC_OPERATION_LIFECYCLE_API_RESOURCES,
userOps: USER_OPS_API_RESOURCES,
visualize: VISUALIZE_API_RESOURCES, visualize: VISUALIZE_API_RESOURCES,
} satisfies Record<ApiName, Record<string, ApiResource>>; } satisfies Record<ApiName, Record<string, ApiResource>>;
......
import type { ApiResource } from '../types';
export const USER_OPS_API_RESOURCES = {
} satisfies Record<string, ApiResource>;
export type UserOpsApiResourceName = `userOps:${ keyof typeof USER_OPS_API_RESOURCES }`;
export type UserOpsApiResourcePayload = never;
export type ApiName = 'general' | 'admin' | 'bens' | 'contractInfo' | 'metadata' | 'multichain' | 'rewards' | 'stats' | 'tac' | 'visualize'; export type ApiName = 'general' | 'admin' | 'bens' | 'contractInfo' | 'metadata' | 'multichain' | 'rewards' | 'stats' | 'tac' | 'userOps' | 'visualize';
export interface ApiResource { export interface ApiResource {
path: string; path: string;
......
...@@ -237,30 +237,12 @@ export default function useNavItems(): ReturnType { ...@@ -237,30 +237,12 @@ export default function useNavItems(): ReturnType {
}, },
].filter(Boolean); ].filter(Boolean);
const apiNavItems: Array<NavItem> = [ const apiNavItem: NavItem | null = config.features.apiDocs.isEnabled ? {
config.features.restApiDocs.isEnabled ? { text: 'API',
text: 'REST API',
nextRoute: { pathname: '/api-docs' as const }, nextRoute: { pathname: '/api-docs' as const },
icon: 'restAPI', icon: 'restAPI',
isActive: pathname === '/api-docs', isActive: pathname.startsWith('/api-docs'),
} : null, } : null;
config.features.graphqlApiDocs.isEnabled ? {
text: 'GraphQL',
nextRoute: { pathname: '/graphiql' as const },
icon: 'graphQL',
isActive: pathname === '/graphiql',
} : null,
!config.UI.navigation.hiddenLinks?.rpc_api && {
text: 'RPC API',
icon: 'RPC',
url: 'https://docs.blockscout.com/for-users/api/rpc-endpoints',
},
!config.UI.navigation.hiddenLinks?.eth_rpc_api && {
text: 'Eth RPC API',
icon: 'RPC',
url: ' https://docs.blockscout.com/for-users/api/eth-rpc',
},
].filter(Boolean);
const otherNavItems: Array<NavItem> | Array<Array<NavItem>> = [ const otherNavItems: Array<NavItem> | Array<Array<NavItem>> = [
{ {
...@@ -311,12 +293,7 @@ export default function useNavItems(): ReturnType { ...@@ -311,12 +293,7 @@ export default function useNavItems(): ReturnType {
icon: 'stats', icon: 'stats',
isActive: pathname.startsWith('/stats'), isActive: pathname.startsWith('/stats'),
} : null, } : null,
apiNavItems.length > 0 && { apiNavItem,
text: 'API',
icon: 'restAPI',
isActive: apiNavItems.some(item => isInternalItem(item) && item.isActive),
subItems: apiNavItems,
},
{ {
text: 'Other', text: 'Other',
icon: 'gear', icon: 'gear',
......
...@@ -12,7 +12,6 @@ const CANONICAL_ROUTES: Array<Route['pathname']> = [ ...@@ -12,7 +12,6 @@ const CANONICAL_ROUTES: Array<Route['pathname']> = [
'/tokens', '/tokens',
'/stats', '/stats',
'/api-docs', '/api-docs',
'/graphiql',
'/gas-tracker', '/gas-tracker',
'/apps', '/apps',
]; ];
......
...@@ -26,7 +26,6 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = { ...@@ -26,7 +26,6 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
'/stats': 'Root page', '/stats': 'Root page',
'/stats/[id]': 'Regular page', '/stats/[id]': 'Regular page',
'/api-docs': 'Regular page', '/api-docs': 'Regular page',
'/graphiql': 'Regular page',
'/search-results': 'Regular page', '/search-results': 'Regular page',
'/auth/profile': 'Root page', '/auth/profile': 'Root page',
'/account/merits': 'Regular page', '/account/merits': 'Regular page',
......
...@@ -29,7 +29,6 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = { ...@@ -29,7 +29,6 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/stats': DEFAULT_TEMPLATE, '/stats': DEFAULT_TEMPLATE,
'/stats/[id]': DEFAULT_TEMPLATE, '/stats/[id]': DEFAULT_TEMPLATE,
'/api-docs': DEFAULT_TEMPLATE, '/api-docs': DEFAULT_TEMPLATE,
'/graphiql': DEFAULT_TEMPLATE,
'/search-results': DEFAULT_TEMPLATE, '/search-results': DEFAULT_TEMPLATE,
'/auth/profile': DEFAULT_TEMPLATE, '/auth/profile': DEFAULT_TEMPLATE,
'/account/merits': DEFAULT_TEMPLATE, '/account/merits': DEFAULT_TEMPLATE,
......
...@@ -26,7 +26,6 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = { ...@@ -26,7 +26,6 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/stats': '%network_name% stats - %network_name% network insights', '/stats': '%network_name% stats - %network_name% network insights',
'/stats/[id]': '%network_name% stats - %id% chart', '/stats/[id]': '%network_name% stats - %id% chart',
'/api-docs': '%network_name% API docs - %network_name% developer tools', '/api-docs': '%network_name% API docs - %network_name% developer tools',
'/graphiql': 'GraphQL for %network_name% - %network_name% data query',
'/search-results': '%network_name% search result for %q%', '/search-results': '%network_name% search result for %q%',
'/auth/profile': '%network_name% - my profile', '/auth/profile': '%network_name% - my profile',
'/account/merits': '%network_name% - Merits', '/account/merits': '%network_name% - Merits',
......
...@@ -24,7 +24,6 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = { ...@@ -24,7 +24,6 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
'/stats': 'Stats', '/stats': 'Stats',
'/stats/[id]': 'Stats chart', '/stats/[id]': 'Stats chart',
'/api-docs': 'REST API', '/api-docs': 'REST API',
'/graphiql': 'GraphQL',
'/search-results': 'Search results', '/search-results': 'Search results',
'/auth/profile': 'Profile', '/auth/profile': 'Profile',
'/account/merits': 'Merits', '/account/merits': 'Merits',
......
...@@ -193,17 +193,7 @@ Promise<GetServerSidePropsResult<Props<Pathname>>> => { ...@@ -193,17 +193,7 @@ Promise<GetServerSidePropsResult<Props<Pathname>>> => {
}; };
export const apiDocs: GetServerSideProps<Props> = async(context) => { export const apiDocs: GetServerSideProps<Props> = async(context) => {
if (!config.features.restApiDocs.isEnabled) { if (!config.features.apiDocs.isEnabled) {
return {
notFound: true,
};
}
return base(context);
};
export const graphIQl: GetServerSideProps<Props> = async(context) => {
if (!config.features.graphqlApiDocs.isEnabled) {
return { return {
notFound: true, notFound: true,
}; };
......
...@@ -50,7 +50,6 @@ declare module "nextjs-routes" { ...@@ -50,7 +50,6 @@ declare module "nextjs-routes" {
| DynamicRoute<"/epochs/[number]", { "number": string }> | DynamicRoute<"/epochs/[number]", { "number": string }>
| StaticRoute<"/epochs"> | StaticRoute<"/epochs">
| StaticRoute<"/gas-tracker"> | StaticRoute<"/gas-tracker">
| StaticRoute<"/graphiql">
| StaticRoute<"/"> | StaticRoute<"/">
| StaticRoute<"/internal-txs"> | StaticRoute<"/internal-txs">
| StaticRoute<"/interop-messages"> | StaticRoute<"/interop-messages">
......
...@@ -342,10 +342,19 @@ const ETHERSCAN_URLS = [ ...@@ -342,10 +342,19 @@ const ETHERSCAN_URLS = [
}, },
]; ];
const DEPRECATED_ROUTES = [
{
source: '/graphiql',
destination: '/api-docs?tab=graphql_api',
permanent: false,
},
];
async function redirects() { async function redirects() {
return [ return [
...OLD_UI_URLS.map((item) => ({ ...item, permanent: false })), ...OLD_UI_URLS.map((item) => ({ ...item, permanent: false })),
...ETHERSCAN_URLS.map((item) => ({ ...item, permanent: true })), ...ETHERSCAN_URLS.map((item) => ({ ...item, permanent: true })),
...DEPRECATED_ROUTES,
]; ];
} }
......
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import React from 'react'; import React from 'react';
import PageNextJs from 'nextjs/PageNextJs'; import PageNextJs from 'nextjs/PageNextJs';
import config from 'configs/app'; const ApiDocs = dynamic(() => import('ui/pages/ApiDocs'), { ssr: false });
import SwaggerUI from 'ui/apiDocs/SwaggerUI';
import PageTitle from 'ui/shared/Page/PageTitle';
const Page: NextPage = () => { const Page: NextPage = () => {
return ( return (
<PageNextJs pathname="/api-docs"> <PageNextJs pathname="/api-docs">
<PageTitle <ApiDocs/>
title={ config.meta.seo.enhancedDataEnabled ? `${ config.chain.name } API documentation` : 'API documentation' }
/>
<SwaggerUI/>
</PageNextJs> </PageNextJs>
); );
}; };
......
import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import React from 'react';
import PageNextJs from 'nextjs/PageNextJs';
import config from 'configs/app';
import ContentLoader from 'ui/shared/ContentLoader';
import PageTitle from 'ui/shared/Page/PageTitle';
const GraphQL = dynamic(() => import('ui/graphQL/GraphQL'), {
loading: () => <ContentLoader/>,
ssr: false,
});
const Page: NextPage = () => {
return (
<PageNextJs pathname="/graphiql">
<PageTitle
title={ config.meta.seo.enhancedDataEnabled ? `GraphiQL ${ config.chain.name } interface` : 'GraphQL playground' }
/>
<GraphQL/>
</PageNextJs>
);
};
export default Page;
export { graphIQl as getServerSideProps } from 'nextjs/getServerSideProps';
...@@ -53,6 +53,7 @@ export const ENVS_MAP: Record<string, Array<[string, string]>> = { ...@@ -53,6 +53,7 @@ export const ENVS_MAP: Record<string, Array<[string, string]>> = {
], ],
userOps: [ userOps: [
[ 'NEXT_PUBLIC_HAS_USER_OPS', 'true' ], [ 'NEXT_PUBLIC_HAS_USER_OPS', 'true' ],
[ 'NEXT_PUBLIC_USER_OPS_INDEXER_API_HOST', 'http://localhost:3110' ],
], ],
hasContractAuditReports: [ hasContractAuditReports: [
[ 'NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS', 'true' ], [ 'NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS', 'true' ],
......
...@@ -80,7 +80,6 @@ ...@@ -80,7 +80,6 @@
| "gear" | "gear"
| "globe-b" | "globe-b"
| "globe" | "globe"
| "graphQL"
| "heart_filled" | "heart_filled"
| "heart_outline" | "heart_outline"
| "hourglass_slim" | "hourglass_slim"
......
import { Accordion, Icon } from '@chakra-ui/react'; import { Accordion, Icon } from '@chakra-ui/react';
import * as React from 'react'; import * as React from 'react';
import { scroller } from 'react-scroll';
import IndicatorIcon from 'icons/arrows/east-mini.svg'; import IndicatorIcon from 'icons/arrows/east-mini.svg';
...@@ -89,3 +90,37 @@ export const AccordionRoot = (props: Accordion.RootProps) => { ...@@ -89,3 +90,37 @@ export const AccordionRoot = (props: Accordion.RootProps) => {
}; };
export const AccordionItem = Accordion.Item; export const AccordionItem = Accordion.Item;
export function useAccordion(items: Array<{ id: string }>) {
const [ value, setValue ] = React.useState<Array<string>>([]);
const onValueChange = React.useCallback(({ value }: { value: Array<string> }) => {
setValue(value);
}, []);
const scrollToItemFromUrl = React.useCallback(() => {
const hash = window.location.hash.replace('#', '');
if (!hash) {
return;
}
const itemToScroll = items.find((item) => item.id === hash);
if (itemToScroll) {
scroller.scrollTo(itemToScroll.id, {
duration: 500,
smooth: true,
offset: -100,
});
setValue([ itemToScroll.id ]);
}
}, [ items ]);
return React.useMemo(() => {
return {
value,
onValueChange,
scrollToItemFromUrl,
};
}, [ value, onValueChange, scrollToItemFromUrl ]);
}
...@@ -31,9 +31,4 @@ export type NavGroupItem = NavItemCommon & { ...@@ -31,9 +31,4 @@ export type NavGroupItem = NavItemCommon & {
subItems: Array<NavItem> | Array<Array<NavItem>>; subItems: Array<NavItem> | Array<Array<NavItem>>;
}; };
import type { ArrayElement } from '../utils';
export const NAVIGATION_LINK_IDS = [ 'rpc_api', 'eth_rpc_api' ] as const;
export type NavigationLinkId = ArrayElement<typeof NAVIGATION_LINK_IDS>;
export type NavigationLayout = 'vertical' | 'horizontal'; export type NavigationLayout = 'vertical' | 'horizontal';
export const API_DOCS_TABS = [
'rest_api',
'eth_rpc_api',
'rpc_api',
'graphql_api',
] as const;
export type ApiDocsTabId = typeof API_DOCS_TABS[ number ];
import { Box, Text } from '@chakra-ui/react';
import React from 'react';
import { Link } from 'toolkit/chakra/link';
const EthRpcApi = () => {
return (
<Box>
<Text>
In addition to the custom RPC endpoints documented here,
the Blockscout ETH RPC API supports 3 methods in the exact format specified for Ethereum nodes,
ee the Ethereum JSON-RPC Specification for more details.
</Text>
<Link href="https://docs.blockscout.com/for-users/api/eth-rpc" external mt={ 6 }>View examples</Link>
</Box>
);
};
export default React.memo(EthRpcApi);
...@@ -9,7 +9,7 @@ import 'graphiql/graphiql.css'; ...@@ -9,7 +9,7 @@ import 'graphiql/graphiql.css';
import { useColorMode } from 'toolkit/chakra/color-mode'; import { useColorMode } from 'toolkit/chakra/color-mode';
import { isBrowser } from 'toolkit/utils/isBrowser'; import { isBrowser } from 'toolkit/utils/isBrowser';
const feature = config.features.graphqlApiDocs; const feature = config.features.apiDocs;
const graphQLStyle = { const graphQLStyle = {
'.graphiql-container': { '.graphiql-container': {
...@@ -35,13 +35,13 @@ const GraphQL = () => { ...@@ -35,13 +35,13 @@ const GraphQL = () => {
} }
}, [ colorMode, graphqlTheme ]); }, [ colorMode, graphqlTheme ]);
if (!feature.isEnabled) { if (!feature.isEnabled || !feature.graphqlDefaultTxnHash) {
return null; return null;
} }
const initialQuery = `{ const initialQuery = `{
transaction( transaction(
hash: "${ feature.defaultTxHash }" hash: "${ feature.graphqlDefaultTxnHash }"
) { ) {
hash hash
blockNumber blockNumber
......
import React from 'react';
import { route } from 'nextjs-routes';
import config from 'configs/app';
import { AccordionItem, AccordionItemContent, AccordionItemTrigger, AccordionRoot, useAccordion } from 'toolkit/chakra/accordion';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import SwaggerUI from './SwaggerUI';
import { REST_API_SECTIONS } from './utils';
const RestApi = () => {
const { value, onValueChange, scrollToItemFromUrl } = useAccordion(REST_API_SECTIONS);
React.useEffect(() => {
scrollToItemFromUrl();
// runs only on mount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ ]);
if (REST_API_SECTIONS.length === 0) {
return null;
}
if (REST_API_SECTIONS.length === 1) {
return <SwaggerUI { ...REST_API_SECTIONS[0].swagger }/>;
}
return (
<AccordionRoot onValueChange={ onValueChange } value={ value }>
{ REST_API_SECTIONS.map((section, index) => (
<AccordionItem key={ index } value={ section.id }>
<AccordionItemTrigger>
<CopyToClipboard
text={ config.app.baseUrl + route({ pathname: '/api-docs', query: { tab: 'rest_api' }, hash: section.id }) }
type="link"
ml={ 0 }
mr={ 1 }
as="div"
/>
{ section.title }
</AccordionItemTrigger>
<AccordionItemContent>
<SwaggerUI { ...section.swagger }/>
</AccordionItemContent>
</AccordionItem>
)) }
</AccordionRoot>
);
};
export default React.memo(RestApi);
import { Box, Text } from '@chakra-ui/react';
import React from 'react';
import { Link } from 'toolkit/chakra/link';
const RpcApi = () => {
return (
<Box>
<Text>
This API is provided for developers transitioning applications from Etherscan to BlockScout and applications requiring general API and data support.
It supports GET and POST requests.
</Text>
<Link href="https://docs.blockscout.com/for-users/api/rpc-endpoints" external mt={ 6 }>View modules</Link>
</Box>
);
};
export default React.memo(RpcApi);
...@@ -9,15 +9,12 @@ import { Box, useToken } from '@chakra-ui/react'; ...@@ -9,15 +9,12 @@ import { Box, useToken } from '@chakra-ui/react';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import React from 'react'; import React from 'react';
import config from 'configs/app'; import type { SwaggerRequest } from './types';
import ContentLoader from 'ui/shared/ContentLoader'; import ContentLoader from 'ui/shared/ContentLoader';
import 'swagger-ui-react/swagger-ui.css'; import 'swagger-ui-react/swagger-ui.css';
const feature = config.features.restApiDocs;
const DEFAULT_SERVER = 'blockscout.com/poa/core';
const NeverShowInfoPlugin = () => { const NeverShowInfoPlugin = () => {
return { return {
components: { components: {
...@@ -28,7 +25,12 @@ const NeverShowInfoPlugin = () => { ...@@ -28,7 +25,12 @@ const NeverShowInfoPlugin = () => {
}; };
}; };
const SwaggerUI = () => { interface Props {
url: string;
requestInterceptor?: (request: SwaggerRequest) => SwaggerRequest;
}
const SwaggerUI = ({ url, requestInterceptor }: Props) => {
const mainColor = { _light: 'blackAlpha.800', _dark: 'whiteAlpha.800' }; const mainColor = { _light: 'blackAlpha.800', _dark: 'whiteAlpha.800' };
const borderColor = useToken('colors', 'border.divider'); const borderColor = useToken('colors', 'border.divider');
const mainBgColor = { _light: 'blackAlpha.100', _dark: 'whiteAlpha.200' }; const mainBgColor = { _light: 'blackAlpha.100', _dark: 'whiteAlpha.200' };
...@@ -111,32 +113,12 @@ const SwaggerUI = () => { ...@@ -111,32 +113,12 @@ const SwaggerUI = () => {
}, },
}; };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const reqInterceptor = React.useCallback((req: any) => {
if (!req.loadSpec) {
const newUrl = new URL(req.url.replace(DEFAULT_SERVER, config.apis.general.host));
newUrl.protocol = config.apis.general.protocol + ':';
if (config.apis.general.port) {
newUrl.port = config.apis.general.port;
}
req.url = newUrl.toString();
}
return req;
}, []);
if (!feature.isEnabled) {
return null;
}
return ( return (
<Box css={ swaggerStyle }> <Box css={ swaggerStyle }>
<SwaggerUIReact <SwaggerUIReact
url={ feature.specUrl } url={ url }
plugins={ [ NeverShowInfoPlugin ] } plugins={ [ NeverShowInfoPlugin ] }
requestInterceptor={ reqInterceptor } requestInterceptor={ requestInterceptor }
/> />
</Box> </Box>
); );
......
export interface SwaggerRequest {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[k: string]: any;
}
import type { SwaggerRequest } from './types';
import config from 'configs/app';
import type { ApiPropsBase } from 'configs/app/apis';
const feature = config.features.apiDocs;
const microserviceRequestInterceptorFactory = (api: ApiPropsBase) => (req: SwaggerRequest) => {
try {
const url = new URL(req.url);
if (api?.basePath && !url.pathname.includes(api.basePath)) {
url.pathname = (api?.basePath ?? '') + url.pathname;
}
req.url = url.toString();
} catch (error) {}
return req;
};
const getMicroserviceSwaggerUrl = (api: ApiPropsBase) => `${ api.endpoint }${ api.basePath ?? '' }/api/v1/docs/swagger.yaml`;
export const REST_API_SECTIONS = [
feature.isEnabled && {
id: 'blockscout-core-api',
title: 'Blockscout core API',
swagger: {
url: feature.coreApiSwaggerUrl,
requestInterceptor: (req: SwaggerRequest) => {
const DEFAULT_SERVER = 'blockscout.com/poa/core';
if (!req.loadSpec) {
const newUrl = new URL(req.url.replace(DEFAULT_SERVER, config.apis.general.host));
newUrl.protocol = config.apis.general.protocol + ':';
if (config.apis.general.port) {
newUrl.port = config.apis.general.port;
}
req.url = newUrl.toString();
}
return req;
},
},
},
config.apis.stats && {
id: 'stats-api',
title: 'Stats API',
swagger: {
url: getMicroserviceSwaggerUrl(config.apis.stats),
requestInterceptor: microserviceRequestInterceptorFactory(config.apis.stats),
},
},
config.apis.bens && {
id: 'bens-api',
title: 'Name service API',
swagger: {
url: getMicroserviceSwaggerUrl(config.apis.bens),
requestInterceptor: microserviceRequestInterceptorFactory(config.apis.bens),
},
},
config.apis.userOps && {
id: 'user-ops-api',
title: 'User ops indexer API',
swagger: {
url: getMicroserviceSwaggerUrl(config.apis.userOps),
requestInterceptor: microserviceRequestInterceptorFactory(config.apis.userOps),
},
},
config.apis.tac && {
id: 'tac-api',
title: 'TAC operation lifecycle API',
swagger: {
url: getMicroserviceSwaggerUrl(config.apis.tac),
requestInterceptor: microserviceRequestInterceptorFactory(config.apis.tac),
},
},
].filter(Boolean);
import { Text } from '@chakra-ui/react';
import React from 'react';
import type { TabItemRegular } from 'toolkit/components/AdaptiveTabs/types';
import config from 'configs/app';
import RoutedTabs from 'toolkit/components/RoutedTabs/RoutedTabs';
import EthRpcApi from 'ui/apiDocs/EthRpcApi';
import GraphQL from 'ui/apiDocs/GraphQL';
import RestApi from 'ui/apiDocs/RestApi';
import RpcApi from 'ui/apiDocs/RpcApi';
import { REST_API_SECTIONS } from 'ui/apiDocs/utils';
import PageTitle from 'ui/shared/Page/PageTitle';
const feature = config.features.apiDocs;
const ApiDocs = () => {
const tabs: Array<TabItemRegular> = [
{ id: 'rest_api', title: 'REST API', component: <RestApi/>, count: REST_API_SECTIONS.length },
{ id: 'eth_rpc_api', title: 'ETH RPC API', component: <EthRpcApi/> },
{ id: 'rpc_api', title: 'RPC API endpoints', component: <RpcApi/> },
{ id: 'graphql_api', title: 'GraphQL API', component: <GraphQL/> },
].filter(({ id }) => feature.isEnabled && feature.tabs.includes(id));
return (
<>
<PageTitle
title={ config.meta.seo.enhancedDataEnabled ? `${ config.chain.name } API documentation` : 'API documentation' }
/>
{ tabs.length > 0 ? <RoutedTabs tabs={ tabs }/> : <Text>No API documentation available</Text> }
</>
);
};
export default React.memo(ApiDocs);
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment