Commit 24eb4fc0 authored by tom goriunov's avatar tom goriunov Committed by GitHub

API resource refactoring (#2699)

* add prefix to API resources

* fix pw tests

* refactor config

* remove unnecessary prefixes in resource names

* fixes

* update screenshots
parent b54be82e
import { stripTrailingSlash } from 'toolkit/utils/url';
import { getEnvValue } from './utils';
const apiHost = getEnvValue('NEXT_PUBLIC_API_HOST');
const apiSchema = getEnvValue('NEXT_PUBLIC_API_PROTOCOL') || 'https';
const apiPort = getEnvValue('NEXT_PUBLIC_API_PORT');
const apiEndpoint = [
apiSchema || 'https',
'://',
apiHost,
apiPort && ':' + apiPort,
].filter(Boolean).join('');
const socketSchema = getEnvValue('NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL') || 'wss';
const socketEndpoint = [
socketSchema,
'://',
apiHost,
apiPort && ':' + apiPort,
].filter(Boolean).join('');
const api = Object.freeze({
host: apiHost,
protocol: apiSchema,
port: apiPort,
endpoint: apiEndpoint,
socket: socketEndpoint,
basePath: stripTrailingSlash(getEnvValue('NEXT_PUBLIC_API_BASE_PATH') || ''),
});
export default api;
import type { ApiName } from 'lib/api/types';
import { stripTrailingSlash } from 'toolkit/utils/url';
import { getEnvValue } from './utils';
interface ApiPropsBase {
endpoint: string;
basePath?: string;
}
interface ApiPropsFull extends ApiPropsBase {
host: string;
protocol: string;
port?: string;
socketEndpoint: string;
}
const generalApi = (() => {
const apiHost = getEnvValue('NEXT_PUBLIC_API_HOST');
const apiSchema = getEnvValue('NEXT_PUBLIC_API_PROTOCOL') || 'https';
const apiPort = getEnvValue('NEXT_PUBLIC_API_PORT');
const apiEndpoint = [
apiSchema || 'https',
'://',
apiHost,
apiPort && ':' + apiPort,
].filter(Boolean).join('');
const socketSchema = getEnvValue('NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL') || 'wss';
const socketEndpoint = [
socketSchema,
'://',
apiHost,
apiPort && ':' + apiPort,
].filter(Boolean).join('');
return Object.freeze({
endpoint: apiEndpoint,
basePath: stripTrailingSlash(getEnvValue('NEXT_PUBLIC_API_BASE_PATH') || ''),
socketEndpoint: socketEndpoint,
host: apiHost ?? '',
protocol: apiSchema ?? 'https',
port: apiPort,
});
})();
const adminApi = (() => {
const apiHost = getEnvValue('NEXT_PUBLIC_ADMIN_SERVICE_API_HOST');
if (!apiHost) {
return;
}
return Object.freeze({
endpoint: apiHost,
});
})();
const bensApi = (() => {
const apiHost = getEnvValue('NEXT_PUBLIC_NAME_SERVICE_API_HOST');
if (!apiHost) {
return;
}
return Object.freeze({
endpoint: apiHost,
});
})();
const contractInfoApi = (() => {
const apiHost = getEnvValue('NEXT_PUBLIC_CONTRACT_INFO_API_HOST');
if (!apiHost) {
return;
}
return Object.freeze({
endpoint: apiHost,
});
})();
const metadataApi = (() => {
const apiHost = getEnvValue('NEXT_PUBLIC_METADATA_SERVICE_API_HOST');
if (!apiHost) {
return;
}
return Object.freeze({
endpoint: apiHost,
});
})();
const rewardsApi = (() => {
const apiHost = getEnvValue('NEXT_PUBLIC_REWARDS_SERVICE_API_HOST');
if (!apiHost) {
return;
}
return Object.freeze({
endpoint: apiHost,
});
})();
const statsApi = (() => {
const apiHost = getEnvValue('NEXT_PUBLIC_STATS_API_HOST');
if (!apiHost) {
return;
}
return Object.freeze({
endpoint: apiHost,
basePath: stripTrailingSlash(getEnvValue('NEXT_PUBLIC_STATS_API_BASE_PATH') || ''),
});
})();
const visualizeApi = (() => {
const apiHost = getEnvValue('NEXT_PUBLIC_VISUALIZE_API_HOST');
if (!apiHost) {
return;
}
return Object.freeze({
endpoint: apiHost,
basePath: stripTrailingSlash(getEnvValue('NEXT_PUBLIC_VISUALIZE_API_BASE_PATH') || ''),
});
})();
type Apis = {
general: ApiPropsFull;
} & Partial<Record<Exclude<ApiName, 'general'>, ApiPropsBase>>;
const apis: Apis = Object.freeze({
general: generalApi,
admin: adminApi,
bens: bensApi,
contractInfo: contractInfoApi,
metadata: metadataApi,
rewards: rewardsApi,
stats: statsApi,
visualize: visualizeApi,
});
export default apis;
import type { Feature } from './types';
import { getEnvValue } from '../utils';
const apiHost = getEnvValue('NEXT_PUBLIC_METADATA_SERVICE_API_HOST');
import apis from '../apis';
const title = 'Address metadata';
const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => {
if (apiHost) {
const config: Feature<{}> = (() => {
if (apis.metadata) {
return Object.freeze({
title,
isEnabled: true,
api: {
endpoint: apiHost,
basePath: '',
},
});
}
......
import type { Feature } from './types';
import { getEnvValue } from '../utils';
import apis from '../apis';
import account from './account';
import verifiedTokens from './verifiedTokens';
const adminServiceApiHost = getEnvValue('NEXT_PUBLIC_ADMIN_SERVICE_API_HOST');
const title = 'Address verification in "My account"';
const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => {
if (account.isEnabled && verifiedTokens.isEnabled && adminServiceApiHost) {
const config: Feature<{}> = (() => {
if (account.isEnabled && verifiedTokens.isEnabled && apis.admin) {
return Object.freeze({
title: 'Address verification in "My account"',
isEnabled: true,
api: {
endpoint: adminServiceApiHost,
basePath: '',
},
});
}
......
import type { Feature } from './types';
import apis from '../apis';
import chain from '../chain';
import { getEnvValue, getExternalAssetFilePath } from '../utils';
......@@ -9,7 +10,6 @@ const configUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_CONFIG_URL')
const submitFormUrl = getEnvValue('NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM');
const suggestIdeasFormUrl = getEnvValue('NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM');
const categoriesUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL');
const adminServiceApiHost = getEnvValue('NEXT_PUBLIC_ADMIN_SERVICE_API_HOST');
const securityReportsUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL');
const featuredApp = getEnvValue('NEXT_PUBLIC_MARKETPLACE_FEATURED_APP');
const bannerContentUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL');
......@@ -22,7 +22,7 @@ const title = 'Marketplace';
const config: Feature<(
{ configUrl: string } |
{ api: { endpoint: string; basePath: string } }
{ api: { endpoint: string; basePath?: string } }
) & {
submitFormUrl: string;
categoriesUrl: string | undefined;
......@@ -58,14 +58,11 @@ const config: Feature<(
configUrl,
...props,
});
} else if (adminServiceApiHost) {
} else if (apis.admin) {
return Object.freeze({
title,
isEnabled: true,
api: {
endpoint: adminServiceApiHost,
basePath: '',
},
api: apis.admin,
...props,
});
}
......
import type { Feature } from './types';
import { getEnvValue } from '../utils';
const apiHost = getEnvValue('NEXT_PUBLIC_NAME_SERVICE_API_HOST');
import apis from '../apis';
const title = 'Name service integration';
const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => {
if (apiHost) {
const config: Feature<{}> = (() => {
if (apis.bens) {
return Object.freeze({
title,
isEnabled: true,
api: {
endpoint: apiHost,
basePath: '',
},
});
}
......
import type { Feature } from './types';
import apis from '../apis';
import { getEnvValue } from '../utils';
const contractInfoApiHost = getEnvValue('NEXT_PUBLIC_CONTRACT_INFO_API_HOST');
const dexPoolsEnabled = getEnvValue('NEXT_PUBLIC_DEX_POOLS_ENABLED') === 'true';
const title = 'DEX Pools';
const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => {
if (contractInfoApiHost && dexPoolsEnabled) {
const config: Feature<{ }> = (() => {
if (apis.contractInfo && dexPoolsEnabled) {
return Object.freeze({
title,
isEnabled: true,
api: {
endpoint: contractInfoApiHost,
basePath: '',
},
});
}
......
import type { Feature } from './types';
import apis from '../apis';
import services from '../services';
import { getEnvValue } from '../utils';
import addressMetadata from './addressMetadata';
const apiHost = getEnvValue('NEXT_PUBLIC_ADMIN_SERVICE_API_HOST');
const title = 'Public tag submission';
const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => {
if (services.reCaptchaV2.siteKey && addressMetadata.isEnabled && apiHost) {
const config: Feature<{}> = (() => {
if (services.reCaptchaV2.siteKey && addressMetadata.isEnabled && apis.admin) {
return Object.freeze({
title,
isEnabled: true,
api: {
endpoint: apiHost,
basePath: '',
},
});
}
......
import type { Feature } from './types';
import { getEnvValue } from '../utils';
import apis from '../apis';
import account from './account';
import blockchainInteraction from './blockchainInteraction';
const apiHost = getEnvValue('NEXT_PUBLIC_REWARDS_SERVICE_API_HOST');
const title = 'Rewards service integration';
const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => {
if (apiHost && account.isEnabled && blockchainInteraction.isEnabled) {
const config: Feature<{}> = (() => {
if (apis.rewards && account.isEnabled && blockchainInteraction.isEnabled) {
return Object.freeze({
title,
isEnabled: true,
api: {
endpoint: apiHost,
basePath: '',
},
});
}
......
import type { Feature } from './types';
import { stripTrailingSlash } from 'toolkit/utils/url';
import { getEnvValue } from '../utils';
const apiEndpoint = getEnvValue('NEXT_PUBLIC_VISUALIZE_API_HOST');
import apis from '../apis';
const title = 'Solidity to UML diagrams';
const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => {
if (apiEndpoint) {
const config: Feature<{}> = (() => {
if (apis.visualize) {
return Object.freeze({
title,
isEnabled: true,
api: {
endpoint: apiEndpoint,
basePath: stripTrailingSlash(getEnvValue('NEXT_PUBLIC_VISUALIZE_API_BASE_PATH') || ''),
},
});
}
......
import type { Feature } from './types';
import { stripTrailingSlash } from 'toolkit/utils/url';
import { getEnvValue } from '../utils';
const apiEndpoint = getEnvValue('NEXT_PUBLIC_STATS_API_HOST');
import apis from '../apis';
const title = 'Blockchain statistics';
const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => {
if (apiEndpoint) {
const config: Feature<{}> = (() => {
if (apis.stats) {
return Object.freeze({
title,
isEnabled: true,
api: {
endpoint: apiEndpoint,
basePath: stripTrailingSlash(getEnvValue('NEXT_PUBLIC_STATS_API_BASE_PATH') || ''),
},
});
}
......
import type { Feature } from './types';
import { getEnvValue } from '../utils';
const contractInfoApiHost = getEnvValue('NEXT_PUBLIC_CONTRACT_INFO_API_HOST');
import apis from '../apis';
const title = 'Verified tokens info';
const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => {
if (contractInfoApiHost) {
const config: Feature<{}> = (() => {
if (apis.contractInfo) {
return Object.freeze({
title,
isEnabled: true,
api: {
endpoint: contractInfoApiHost,
basePath: '',
},
});
}
......
import api from './api';
import apis from './apis';
import app from './app';
import chain from './chain';
import * as features from './features';
......@@ -9,7 +9,7 @@ import UI from './ui';
const config = Object.freeze({
app,
chain,
api,
apis,
UI,
features,
services,
......
......@@ -7,15 +7,9 @@ NEXT_PUBLIC_APP_PROTOCOL=http
NEXT_PUBLIC_APP_HOST=localhost
NEXT_PUBLIC_APP_PORT=3000
NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_APP_INSTANCE=rubber_duck
NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED=true
# Instance ENVs
NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP={ "id": "632019", "width": "728", "height": "90" }
NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE={ "id": "632018", "width": "320", "height": "100" }
NEXT_PUBLIC_AD_BANNER_ADDITIONAL_PROVIDER=adbutler
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs-test.k8s-dev.blockscout.com
NEXT_PUBLIC_API_BASE_PATH=/
NEXT_PUBLIC_API_HOST=eth-sepolia.k8s-dev.blockscout.com
......@@ -23,28 +17,33 @@ NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout
NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info-test.k8s-dev.blockscout.com
NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED=true
NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS=[{'text':'Swap','icon':'swap','dappId':'uniswap'},{'text':'Payment link','icon':'payment_link','dappId':'peanut-protocol'},{'text':'Get gas','icon':'gas','dappId':'smol-refuel'}]
NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS=[{'text':'Swapscout','icon':'swap','dappId':'swapscout'},{'text':'Payment link','icon':'payment_link','dappId':'peanut-protocol'},{'text':'Get gas','icon':'gas','dappId':'smol-refuel'}]
NEXT_PUBLIC_DEX_POOLS_ENABLED=true
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/eth-sepolia.json
NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/sepolia.json
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xbf69c7abc4fee283b59a9633dadfdaedde5c5ee0fba3e80a08b5b8a3acbd4363
NEXT_PUBLIC_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/sherblockHolmesBadge
NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG={'name': 'Need gas?', 'url_template': 'https://smolrefuel.com/?outboundChain={chainId}', 'dapp_id': 'smol-refuel', 'logo': 'https://blockscout-content.s3.amazonaws.com/smolrefuel-logo-action-button.png'}
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x93e00d4d48cf0dc229f5102e18277fa1bb6130d5b319697a87698a35cf67f706
NEXT_PUBLIC_HAS_BEACON_CHAIN=true
NEXT_PUBLIC_HAS_USER_OPS=true
NEXT_PUBLIC_HELIA_VERIFIED_FETCH_ENABLED=false
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=rgba(51, 53, 67, 1)
NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=rgba(165, 252, 122, 1)
NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['rgba(51, 53, 67, 1)'],'text_color':['rgba(165, 252, 122, 1)']}
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_IS_TESTNET=true
NEXT_PUBLIC_LOGOUT_URL=https://blockscout-goerli.us.auth0.com/v2/logout
NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json
NEXT_PUBLIC_MARKETPLACE_ENABLED=true
NEXT_PUBLIC_MARKETPLACE_GRAPH_LINKS_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/refs/heads/marketplace-graph-test/test-configs/marketplace-graph-links.json
NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID=appGkvtmKI7fXE4Vs
NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/test-configs/marketplace-security-report-mock.json
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL
NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata-test.k8s-dev.blockscout.com
NEXT_PUBLIC_METASUITES_ENABLED=true
NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG=[{'name': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'}]
NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG=[{'name': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview?utm_source=blockscout&utm_medium=address', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'},{'name': 'zapper', 'url_template': 'https://zapper.xyz/account/{address}?utm_source=blockscout&utm_medium=address', 'logo': 'https://blockscout-content.s3.amazonaws.com/zapper-icon.png'}]
NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens-rs-test.k8s-dev.blockscout.com
NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/blocks','/apps']
NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/pools']
NEXT_PUBLIC_NAVIGATION_LAYOUT=horizontal
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether
......@@ -64,8 +63,13 @@ NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-c
NEXT_PUBLIC_OTHER_LINKS=[{'url':'https://sepolia.drpc.org?ref=559183','text':'Public RPC'}]
NEXT_PUBLIC_REWARDS_SERVICE_API_HOST=https://points.k8s-dev.blockscout.com
NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-sepolia.safe.global
NEXT_PUBLIC_SAVE_ON_GAS_ENABLED=true
NEXT_PUBLIC_SEO_ENHANCED_DATA_ENABLED=true
NEXT_PUBLIC_STATS_API_HOST=https://stats-sepolia.k8s-dev.blockscout.com
NEXT_PUBLIC_STATS_API_BASE_PATH=/stats-service
NEXT_PUBLIC_STATS_API_HOST=https://eth-sepolia.k8s-dev.blockscout.com
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
NEXT_PUBLIC_VIEWS_ADDRESS_IDENTICON_TYPE=nouns
NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED=true
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer-test.k8s-dev.blockscout.com
NEXT_PUBLIC_XSTAR_SCORE_URL=https://docs.xname.app/the-solution-adaptive-proof-of-humanity-on-blockchain/xhs-scoring-algorithm?utm_source=blockscout&utm_medium=address
\ No newline at end of file
......@@ -52,6 +52,7 @@ NEXT_PUBLIC_CONTRACT_INFO_API_HOST=http://localhost:3005
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_RE_CAPTCHA_APP_SITE_KEY=xxx
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx
NEXT_PUBLIC_VIEWS_ADDRESS_FORMAT=['base16','bech32']
......
......@@ -7,7 +7,7 @@ import parseMetaPayload from './parseMetaPayload';
export default function useAddressMetadataInfoQuery(addresses: Array<string>, isEnabled = true) {
const resource = 'address_metadata_info';
const resource = 'metadata:info';
return useApiQuery<typeof resource, unknown, AddressMetadataInfoFormatted>(resource, {
queryParams: {
......
import buildUrl from './buildUrl';
test('builds URL for resource without path params', () => {
const url = buildUrl('config_backend_version');
const url = buildUrl('general:config_backend_version');
expect(url).toBe('https://localhost:3003/api/v2/config/backend-version');
});
test('builds URL for resource with path params', () => {
const url = buildUrl('block', { height_or_hash: '42' });
const url = buildUrl('general:block', { height_or_hash: '42' });
expect(url).toBe('https://localhost:3003/api/v2/blocks/42');
});
describe('falsy query parameters', () => {
test('leaves "false" as query parameter', () => {
const url = buildUrl('block', { height_or_hash: '42' }, { includeTx: false });
const url = buildUrl('general:block', { height_or_hash: '42' }, { includeTx: false });
expect(url).toBe('https://localhost:3003/api/v2/blocks/42?includeTx=false');
});
test('leaves "null" as query parameter', () => {
const url = buildUrl('block', { height_or_hash: '42' }, { includeTx: null });
const url = buildUrl('general:block', { height_or_hash: '42' }, { includeTx: null });
expect(url).toBe('https://localhost:3003/api/v2/blocks/42?includeTx=null');
});
test('strips out empty string as query parameter', () => {
const url = buildUrl('block', { height_or_hash: '42' }, { includeTx: null, sort: '' });
const url = buildUrl('general:block', { height_or_hash: '42' }, { includeTx: null, sort: '' });
expect(url).toBe('https://localhost:3003/api/v2/blocks/42?includeTx=null');
});
test('strips out "undefined" as query parameter', () => {
const url = buildUrl('block', { height_or_hash: '42' }, { includeTx: null, sort: undefined });
const url = buildUrl('general:block', { height_or_hash: '42' }, { includeTx: null, sort: undefined });
expect(url).toBe('https://localhost:3003/api/v2/blocks/42?includeTx=null');
});
});
test('builds URL with array-like query parameters', () => {
const url = buildUrl('block', { height_or_hash: '42' }, { includeTx: [ '0x11', '0x22' ], sort: 'asc' });
const url = buildUrl('general:block', { height_or_hash: '42' }, { includeTx: [ '0x11', '0x22' ], sort: 'asc' });
expect(url).toBe('https://localhost:3003/api/v2/blocks/42?includeTx=0x11%2C0x22&sort=asc');
});
test('builds URL for resource with custom API endpoint', () => {
const url = buildUrl('token_verified_info', { chainId: '42', hash: '0x11' });
const url = buildUrl('contractInfo:token_verified_info', { chainId: '42', hash: '0x11' });
expect(url).toBe('https://localhost:3005/api/v1/chains/42/token-infos/0x11');
});
......@@ -2,19 +2,19 @@ import { compile } from 'path-to-regexp';
import config from 'configs/app';
import getResourceParams from './getResourceParams';
import isNeedProxy from './isNeedProxy';
import { RESOURCES } from './resources';
import type { ApiResource, ResourceName, ResourcePathParams } from './resources';
import type { ResourceName, ResourcePathParams } from './resources';
export default function buildUrl<R extends ResourceName>(
resourceName: R,
resourceFullName: R,
pathParams?: ResourcePathParams<R>,
queryParams?: Record<string, string | Array<string> | number | boolean | null | undefined>,
noProxy?: boolean,
): string {
const resource: ApiResource = RESOURCES[resourceName];
const baseUrl = !noProxy && isNeedProxy() ? config.app.baseUrl : (resource.endpoint || config.api.endpoint);
const basePath = resource.basePath !== undefined ? resource.basePath : config.api.basePath;
const { api, resource } = getResourceParams(resourceFullName);
const baseUrl = !noProxy && isNeedProxy() ? config.app.baseUrl : api.endpoint;
const basePath = api.basePath ?? '';
const path = !noProxy && isNeedProxy() ? '/node-api/proxy' + basePath + resource.path : basePath + resource.path;
const url = new URL(compile(path)(pathParams), baseUrl);
......
import type { ApiName, ApiResource } from './types';
import config from 'configs/app';
import type { ResourceName } from './resources';
import { RESOURCES } from './resources';
export default function getResourceParams(resourceFullName: ResourceName) {
const [ apiName, resourceName ] = resourceFullName.split(':') as [ ApiName, string ];
const apiConfig = config.apis[apiName];
if (!apiConfig) {
throw new Error(`API config for ${ apiName } not found`);
}
return {
api: apiConfig,
apiName,
resource: RESOURCES[apiName][resourceName as keyof typeof RESOURCES[ApiName]] as ApiResource,
};
}
......@@ -9,5 +9,5 @@ export default function isNeedProxy() {
return true;
}
return config.app.host === 'localhost' && config.app.host !== config.api.host;
return config.app.host === 'localhost' && config.app.host !== config.apis.general.host;
}
This diff is collapsed.
import type { ApiResource } from '../types';
import type { TokenInfoApplicationConfig, TokenInfoApplications } from 'types/api/account';
import type { MarketplaceAppOverview } from 'types/client/marketplace';
export const ADMIN_API_RESOURCES = {
public_tag_application: {
path: '/api/v1/chains/:chainId/metadata-submissions/tag',
pathParams: [ 'chainId' as const ],
},
token_info_applications_config: {
path: '/api/v1/chains/:chainId/token-info-submissions/selectors',
pathParams: [ 'chainId' as const ],
},
token_info_applications: {
path: '/api/v1/chains/:chainId/token-info-submissions{/:id}',
pathParams: [ 'chainId' as const, 'id' as const ],
},
marketplace_dapps: {
path: '/api/v1/chains/:chainId/marketplace/dapps',
pathParams: [ 'chainId' as const ],
},
marketplace_dapp: {
path: '/api/v1/chains/:chainId/marketplace/dapps/:dappId',
pathParams: [ 'chainId' as const, 'dappId' as const ],
},
} satisfies Record<string, ApiResource>;
export type AdminApiResourceName = `admin:${ keyof typeof ADMIN_API_RESOURCES }`;
/* eslint-disable @stylistic/indent */
export type AdminApiResourcePayload<R extends AdminApiResourceName> =
R extends 'admin:token_info_applications_config' ? TokenInfoApplicationConfig :
R extends 'admin:token_info_applications' ? TokenInfoApplications :
R extends 'admin:marketplace_dapps' ? Array<MarketplaceAppOverview> :
R extends 'admin:marketplace_dapp' ? MarketplaceAppOverview :
never;
/* eslint-enable @stylistic/indent */
import type { ApiResource } from '../types';
import type * as bens from '@blockscout/bens-types';
import type { EnsAddressLookupFilters, EnsDomainLookupFilters, EnsLookupSorting } from 'types/api/ens';
export const BENS_API_RESOURCES = {
addresses_lookup: {
path: '/api/v1/:chainId/addresses\\:lookup',
pathParams: [ 'chainId' as const ],
filterFields: [ 'address' as const, 'resolved_to' as const, 'owned_by' as const, 'only_active' as const, 'protocols' as const ],
paginated: true,
},
address_domain: {
path: '/api/v1/:chainId/addresses/:address',
pathParams: [ 'chainId' as const, 'address' as const ],
},
domain_info: {
path: '/api/v1/:chainId/domains/:name',
pathParams: [ 'chainId' as const, 'name' as const ],
},
domain_events: {
path: '/api/v1/:chainId/domains/:name/events',
pathParams: [ 'chainId' as const, 'name' as const ],
},
domains_lookup: {
path: '/api/v1/:chainId/domains\\:lookup',
pathParams: [ 'chainId' as const ],
filterFields: [ 'name' as const, 'only_active' as const, 'protocols' as const ],
paginated: true,
},
domain_protocols: {
path: '/api/v1/:chainId/protocols',
pathParams: [ 'chainId' as const ],
},
} satisfies Record<string, ApiResource>;
export type BensApiResourceName = `bens:${ keyof typeof BENS_API_RESOURCES }`;
/* eslint-disable @stylistic/indent */
export type BensApiResourcePayload<R extends BensApiResourceName> =
R extends 'bens:addresses_lookup' ? bens.LookupAddressResponse :
R extends 'bens:address_domain' ? bens.GetAddressResponse :
R extends 'bens:domain_info' ? bens.DetailedDomain :
R extends 'bens:domain_events' ? bens.ListDomainEventsResponse :
R extends 'bens:domains_lookup' ? bens.LookupDomainNameResponse :
R extends 'bens:domain_protocols' ? bens.GetProtocolsResponse :
never;
/* eslint-enable @stylistic/indent */
/* eslint-disable @stylistic/indent */
export type BensApiPaginationFilters<R extends BensApiResourceName> =
R extends 'bens:addresses_lookup' ? EnsAddressLookupFilters :
R extends 'bens:domains_lookup' ? EnsDomainLookupFilters :
never;
/* eslint-enable @stylistic/indent */
/* eslint-disable @stylistic/indent */
export type BensApiPaginationSorting<R extends BensApiResourceName> =
R extends 'bens:addresses_lookup' ? EnsLookupSorting :
R extends 'bens:domains_lookup' ? EnsLookupSorting :
never;
/* eslint-enable @stylistic/indent */
import type { ApiResource } from '../types';
import type { VerifiedAddressResponse } from 'types/api/account';
import type { Pool, PoolsResponse } from 'types/api/pools';
import type { TokenVerifiedInfo } from 'types/api/token';
export const CONTRACT_INFO_API_RESOURCES = {
address_verification: {
path: '/api/v1/chains/:chainId/verified-addresses:type',
pathParams: [ 'chainId' as const, 'type' as const ],
},
verified_addresses: {
path: '/api/v1/chains/:chainId/verified-addresses',
pathParams: [ 'chainId' as const ],
},
token_verified_info: {
path: '/api/v1/chains/:chainId/token-infos/:hash',
pathParams: [ 'chainId' as const, 'hash' as const ],
},
pools: {
path: '/api/v1/chains/:chainId/pools',
pathParams: [ 'chainId' as const ],
filterFields: [ 'query' as const ],
paginated: true,
},
pool: {
path: '/api/v1/chains/:chainId/pools/:hash',
pathParams: [ 'chainId' as const, 'hash' as const ],
},
} satisfies Record<string, ApiResource>;
export type ContractInfoApiResourceName = `contractInfo:${ keyof typeof CONTRACT_INFO_API_RESOURCES }`;
/* eslint-disable @stylistic/indent */
export type ContractInfoApiResourcePayload<R extends ContractInfoApiResourceName> =
R extends 'contractInfo:verified_addresses' ? VerifiedAddressResponse :
R extends 'contractInfo:token_verified_info' ? TokenVerifiedInfo :
R extends 'contractInfo:pools' ? PoolsResponse :
R extends 'contractInfo:pool' ? Pool :
never;
/* eslint-enable @stylistic/indent */
/* eslint-disable @stylistic/indent */
export type ContractInfoApiPaginationFilters<R extends ContractInfoApiResourceName> =
R extends 'contractInfo:pools' ? { query: string } :
never;
/* eslint-enable @stylistic/indent */
import type { ApiResource } from '../../types';
import type { AddressTagsResponse, ApiKeys, CustomAbis, TransactionTagsResponse, UserInfo, WatchlistResponse } from 'types/api/account';
export const GENERAL_API_ACCOUNT_RESOURCES = {
// ACCOUNT
csrf: {
path: '/api/account/v2/get_csrf',
},
user_info: {
path: '/api/account/v2/user/info',
},
custom_abi: {
path: '/api/account/v2/user/custom_abis{/:id}',
pathParams: [ 'id' as const ],
},
watchlist: {
path: '/api/account/v2/user/watchlist{/:id}',
pathParams: [ 'id' as const ],
filterFields: [ ],
paginated: true,
},
private_tags_address: {
path: '/api/account/v2/user/tags/address{/:id}',
pathParams: [ 'id' as const ],
filterFields: [ ],
paginated: true,
},
private_tags_tx: {
path: '/api/account/v2/user/tags/transaction{/:id}',
pathParams: [ 'id' as const ],
filterFields: [ ],
paginated: true,
},
api_keys: {
path: '/api/account/v2/user/api_keys{/:id}',
pathParams: [ 'id' as const ],
},
// AUTH
auth_send_otp: {
path: '/api/account/v2/send_otp',
},
auth_confirm_otp: {
path: '/api/account/v2/confirm_otp',
},
auth_siwe_message: {
path: '/api/account/v2/siwe_message',
},
auth_siwe_verify: {
path: '/api/account/v2/authenticate_via_wallet',
},
auth_link_email: {
path: '/api/account/v2/email/link',
},
auth_link_address: {
path: '/api/account/v2/address/link',
},
auth_logout: {
path: '/api/account/auth/logout',
},
} satisfies Record<string, ApiResource>;
export type GeneralApiAccountResourceName = `general:${ keyof typeof GENERAL_API_ACCOUNT_RESOURCES }`;
/* eslint-disable @stylistic/indent */
export type GeneralApiAccountResourcePayload<R extends GeneralApiAccountResourceName> =
R extends 'general:user_info' ? UserInfo :
R extends 'general:custom_abi' ? CustomAbis :
R extends 'general:private_tags_address' ? AddressTagsResponse :
R extends 'general:private_tags_tx' ? TransactionTagsResponse :
R extends 'general:api_keys' ? ApiKeys :
R extends 'general:watchlist' ? WatchlistResponse :
never;
/* eslint-enable @stylistic/indent */
import type { ApiResource } from '../../types';
import type {
AddressCounters,
AddressBlocksValidatedResponse,
AddressTokensResponse,
AddressCollectionsResponse,
AddressEpochRewardsResponse,
AddressNFTsResponse,
AddressWithdrawalsResponse,
AddressXStarResponse,
AddressCoinBalanceHistoryChart,
AddressCoinBalanceHistoryResponse,
AddressTokenTransferResponse,
AddressInternalTxsResponse,
AddressTransactionsResponse,
AddressTabsCounters,
Address,
AddressTxsFilters,
AddressTokenTransferFilters,
AddressTokensFilter,
AddressNFTTokensFilter,
} from 'types/api/address';
import type { AddressesMetadataSearchFilters, AddressesMetadataSearchResult, AddressesResponse } from 'types/api/addresses';
import type { LogsResponseAddress } from 'types/api/log';
import type { TransactionsSorting } from 'types/api/transaction';
export const GENERAL_API_ADDRESS_RESOURCES = {
// ADDRESSES
addresses: {
path: '/api/v2/addresses/',
filterFields: [ ],
paginated: true,
},
addresses_metadata_search: {
path: '/api/v2/proxy/metadata/addresses',
filterFields: [ 'slug' as const, 'tag_type' as const ],
paginated: true,
},
// ADDRESS INFO
address: {
path: '/api/v2/addresses/:hash',
pathParams: [ 'hash' as const ],
},
address_counters: {
path: '/api/v2/addresses/:hash/counters',
pathParams: [ 'hash' as const ],
},
address_tabs_counters: {
path: '/api/v2/addresses/:hash/tabs-counters',
pathParams: [ 'hash' as const ],
},
address_txs: {
path: '/api/v2/addresses/:hash/transactions',
pathParams: [ 'hash' as const ],
filterFields: [ 'filter' as const ],
paginated: true,
},
address_internal_txs: {
path: '/api/v2/addresses/:hash/internal-transactions',
pathParams: [ 'hash' as const ],
filterFields: [ 'filter' as const ],
paginated: true,
},
address_token_transfers: {
path: '/api/v2/addresses/:hash/token-transfers',
pathParams: [ 'hash' as const ],
filterFields: [ 'filter' as const, 'type' as const, 'token' as const ],
paginated: true,
},
address_blocks_validated: {
path: '/api/v2/addresses/:hash/blocks-validated',
pathParams: [ 'hash' as const ],
filterFields: [ ],
paginated: true,
},
address_coin_balance: {
path: '/api/v2/addresses/:hash/coin-balance-history',
pathParams: [ 'hash' as const ],
filterFields: [ ],
paginated: true,
},
address_coin_balance_chart: {
path: '/api/v2/addresses/:hash/coin-balance-history-by-day',
pathParams: [ 'hash' as const ],
filterFields: [ ],
},
address_logs: {
path: '/api/v2/addresses/:hash/logs',
pathParams: [ 'hash' as const ],
filterFields: [ ],
paginated: true,
},
address_tokens: {
path: '/api/v2/addresses/:hash/tokens',
pathParams: [ 'hash' as const ],
filterFields: [ 'type' as const ],
paginated: true,
},
address_nfts: {
path: '/api/v2/addresses/:hash/nft',
pathParams: [ 'hash' as const ],
filterFields: [ 'type' as const ],
paginated: true,
},
address_collections: {
path: '/api/v2/addresses/:hash/nft/collections',
pathParams: [ 'hash' as const ],
filterFields: [ 'type' as const ],
paginated: true,
},
address_withdrawals: {
path: '/api/v2/addresses/:hash/withdrawals',
pathParams: [ 'hash' as const ],
filterFields: [],
paginated: true,
},
address_epoch_rewards: {
path: '/api/v2/addresses/:hash/election-rewards',
pathParams: [ 'hash' as const ],
filterFields: [],
paginated: true,
},
address_xstar_score: {
path: '/api/v2/proxy/3dparty/xname/addresses/:hash',
pathParams: [ 'hash' as const ],
},
} satisfies Record<string, ApiResource>;
export type GeneralApiAddressResourceName = `general:${ keyof typeof GENERAL_API_ADDRESS_RESOURCES }`;
/* eslint-disable @stylistic/indent */
export type GeneralApiAddressResourcePayload<R extends GeneralApiAddressResourceName> =
R extends 'general:addresses' ? AddressesResponse :
R extends 'general:addresses_metadata_search' ? AddressesMetadataSearchResult :
R extends 'general:address' ? Address :
R extends 'general:address_counters' ? AddressCounters :
R extends 'general:address_tabs_counters' ? AddressTabsCounters :
R extends 'general:address_txs' ? AddressTransactionsResponse :
R extends 'general:address_internal_txs' ? AddressInternalTxsResponse :
R extends 'general:address_token_transfers' ? AddressTokenTransferResponse :
R extends 'general:address_blocks_validated' ? AddressBlocksValidatedResponse :
R extends 'general:address_coin_balance' ? AddressCoinBalanceHistoryResponse :
R extends 'general:address_coin_balance_chart' ? AddressCoinBalanceHistoryChart :
R extends 'general:address_logs' ? LogsResponseAddress :
R extends 'general:address_tokens' ? AddressTokensResponse :
R extends 'general:address_nfts' ? AddressNFTsResponse :
R extends 'general:address_collections' ? AddressCollectionsResponse :
R extends 'general:address_withdrawals' ? AddressWithdrawalsResponse :
R extends 'general:address_epoch_rewards' ? AddressEpochRewardsResponse :
R extends 'general:address_xstar_score' ? AddressXStarResponse :
never;
/* eslint-enable @stylistic/indent */
/* eslint-disable @stylistic/indent */
export type GeneralApiAddressPaginationFilters<R extends GeneralApiAddressResourceName> =
R extends 'general:addresses_metadata_search' ? AddressesMetadataSearchFilters :
R extends 'general:address_txs' | 'general:address_internal_txs' ? AddressTxsFilters :
R extends 'general:address_token_transfers' ? AddressTokenTransferFilters :
R extends 'general:address_tokens' ? AddressTokensFilter :
R extends 'general:address_nfts' ? AddressNFTTokensFilter :
R extends 'general:address_collections' ? AddressNFTTokensFilter :
never;
/* eslint-enable @stylistic/indent */
/* eslint-disable @stylistic/indent */
export type GeneralApiAddressPaginationSorting<R extends GeneralApiAddressResourceName> =
R extends 'general:address_txs' ? TransactionsSorting :
never;
/* eslint-enable @stylistic/indent */
import type { ApiResource } from '../../types';
import type {
BlocksResponse,
BlockTransactionsResponse,
Block,
BlockFilters,
BlockWithdrawalsResponse,
BlockCountdownResponse,
BlockEpoch,
BlockEpochElectionRewardDetailsResponse,
} from 'types/api/block';
import type { TTxsWithBlobsFilters } from 'types/api/txsFilters';
export const GENERAL_API_BLOCK_RESOURCES = {
blocks: {
path: '/api/v2/blocks',
filterFields: [ 'type' as const ],
paginated: true,
},
block: {
path: '/api/v2/blocks/:height_or_hash',
pathParams: [ 'height_or_hash' as const ],
},
block_txs: {
path: '/api/v2/blocks/:height_or_hash/transactions',
pathParams: [ 'height_or_hash' as const ],
filterFields: [ 'type' as const ],
paginated: true,
},
block_withdrawals: {
path: '/api/v2/blocks/:height_or_hash/withdrawals',
pathParams: [ 'height_or_hash' as const ],
filterFields: [],
paginated: true,
},
block_epoch: {
path: '/api/v2/blocks/:height_or_hash/epoch',
pathParams: [ 'height_or_hash' as const ],
filterFields: [],
},
block_election_rewards: {
path: '/api/v2/blocks/:height_or_hash/election-rewards/:reward_type',
pathParams: [ 'height_or_hash' as const, 'reward_type' as const ],
filterFields: [],
paginated: true,
},
} satisfies Record<string, ApiResource>;
export type GeneralApiBlockResourceName = `general:${ keyof typeof GENERAL_API_BLOCK_RESOURCES }`;
/* eslint-disable @stylistic/indent */
export type GeneralApiBlockResourcePayload<R extends GeneralApiBlockResourceName> =
R extends 'general:blocks' ? BlocksResponse :
R extends 'general:block' ? Block :
R extends 'general:block_countdown' ? BlockCountdownResponse :
R extends 'general:block_txs' ? BlockTransactionsResponse :
R extends 'general:block_withdrawals' ? BlockWithdrawalsResponse :
R extends 'general:block_epoch' ? BlockEpoch :
R extends 'general:block_election_rewards' ? BlockEpochElectionRewardDetailsResponse :
never;
/* eslint-enable @stylistic/indent */
/* eslint-disable @stylistic/indent */
export type GeneralApiBlockPaginationFilters<R extends GeneralApiBlockResourceName> =
R extends 'general:blocks' ? BlockFilters :
R extends 'general:block_txs' ? TTxsWithBlobsFilters :
never;
/* eslint-enable @stylistic/indent */
import type { ApiResource } from '../../types';
import type {
SmartContract,
SmartContractSecurityAudits,
SmartContractVerificationConfigRaw,
} from 'types/api/contract';
import type { VerifiedContractsResponse, VerifiedContractsCounters, VerifiedContractsFilters } from 'types/api/contracts';
import type { VerifiedContractsSorting } from 'types/api/verifiedContracts';
export const GENERAL_API_CONTRACT_RESOURCES = {
contract: {
path: '/api/v2/smart-contracts/:hash',
pathParams: [ 'hash' as const ],
},
contract_verification_config: {
path: '/api/v2/smart-contracts/verification/config',
},
contract_verification_via: {
path: '/api/v2/smart-contracts/:hash/verification/via/:method',
pathParams: [ 'hash' as const, 'method' as const ],
},
contract_solidity_scan_report: {
path: '/api/v2/proxy/3dparty/solidityscan/smart-contracts/:hash/report',
pathParams: [ 'hash' as const ],
},
contract_security_audits: {
path: '/api/v2/smart-contracts/:hash/audit-reports',
pathParams: [ 'hash' as const ],
},
verified_contracts: {
path: '/api/v2/smart-contracts',
filterFields: [ 'q' as const, 'filter' as const ],
paginated: true,
},
verified_contracts_counters: {
path: '/api/v2/smart-contracts/counters',
},
} satisfies Record<string, ApiResource>;
export type GeneralApiContractResourceName = `general:${ keyof typeof GENERAL_API_CONTRACT_RESOURCES }`;
/* eslint-disable @stylistic/indent */
export type GeneralApiContractResourcePayload<R extends GeneralApiContractResourceName> =
R extends 'general:contract' ? SmartContract :
R extends 'general:contract_solidity_scan_report' ? unknown :
R extends 'general:verified_contracts' ? VerifiedContractsResponse :
R extends 'general:verified_contracts_counters' ? VerifiedContractsCounters :
R extends 'general:contract_verification_config' ? SmartContractVerificationConfigRaw :
R extends 'general:contract_security_audits' ? SmartContractSecurityAudits :
never;
/* eslint-enable @stylistic/indent */
/* eslint-disable @stylistic/indent */
export type GeneralApiContractPaginationFilters<R extends GeneralApiContractResourceName> =
R extends 'general:verified_contracts' ? VerifiedContractsFilters :
never;
/* eslint-enable @stylistic/indent */
/* eslint-disable @stylistic/indent */
export type GeneralApiContractPaginationSorting<R extends GeneralApiContractResourceName> =
R extends 'general:verified_contracts' ? VerifiedContractsSorting :
never;
/* eslint-enable @stylistic/indent */
import type { ApiResource } from '../../types';
import type { GeneralApiAccountResourceName, GeneralApiAccountResourcePayload } from './account';
import { GENERAL_API_ACCOUNT_RESOURCES } from './account';
import type {
GeneralApiAddressPaginationFilters,
GeneralApiAddressPaginationSorting,
GeneralApiAddressResourceName,
GeneralApiAddressResourcePayload,
} from './address';
import { GENERAL_API_ADDRESS_RESOURCES } from './address';
import type {
GeneralApiBlockPaginationFilters,
GeneralApiBlockResourceName, GeneralApiBlockResourcePayload } from './block';
import { GENERAL_API_BLOCK_RESOURCES } from './block';
import { GENERAL_API_CONTRACT_RESOURCES } from './contract';
import type {
GeneralApiContractPaginationFilters,
GeneralApiContractPaginationSorting,
GeneralApiContractResourceName,
GeneralApiContractResourcePayload,
} from './contract';
import type {
GeneralApiMiscPaginationFilters,
GeneralApiMiscPaginationSorting,
GeneralApiMiscResourceName,
GeneralApiMiscResourcePayload,
} from './misc';
import { GENERAL_API_MISC_RESOURCES } from './misc';
import type {
GeneralApiRollupPaginationFilters,
GeneralApiRollupPaginationSorting,
GeneralApiRollupResourceName,
GeneralApiRollupResourcePayload,
} from './rollup';
import { GENERAL_API_ROLLUP_RESOURCES } from './rollup';
import type {
GeneralApiTokenPaginationFilters,
GeneralApiTokenPaginationSorting,
GeneralApiTokenResourceName,
GeneralApiTokenResourcePayload,
} from './token';
import { GENERAL_API_TOKEN_RESOURCES } from './token';
import type { GeneralApiTxResourceName, GeneralApiTxResourcePayload, GeneralApiTxPaginationFilters } from './tx';
import { GENERAL_API_TX_RESOURCES } from './tx';
import type { GeneralApiV1ResourceName, GeneralApiV1ResourcePayload } from './v1';
import { GENERAL_API_V1_RESOURCES } from './v1';
export const GENERAL_API_RESOURCES = {
...GENERAL_API_ACCOUNT_RESOURCES,
...GENERAL_API_ADDRESS_RESOURCES,
...GENERAL_API_BLOCK_RESOURCES,
...GENERAL_API_CONTRACT_RESOURCES,
...GENERAL_API_MISC_RESOURCES,
...GENERAL_API_ROLLUP_RESOURCES,
...GENERAL_API_TOKEN_RESOURCES,
...GENERAL_API_TX_RESOURCES,
...GENERAL_API_V1_RESOURCES,
} satisfies Record<string, ApiResource>;
export type GeneralApiResourceName = `general:${ keyof typeof GENERAL_API_RESOURCES }`;
/* eslint-disable @stylistic/indent */
export type GeneralApiResourcePayload<R extends GeneralApiResourceName> =
R extends GeneralApiAccountResourceName ? GeneralApiAccountResourcePayload<R> :
R extends GeneralApiAddressResourceName ? GeneralApiAddressResourcePayload<R> :
R extends GeneralApiBlockResourceName ? GeneralApiBlockResourcePayload<R> :
R extends GeneralApiContractResourceName ? GeneralApiContractResourcePayload<R> :
R extends GeneralApiMiscResourceName ? GeneralApiMiscResourcePayload<R> :
R extends GeneralApiRollupResourceName ? GeneralApiRollupResourcePayload<R> :
R extends GeneralApiTokenResourceName ? GeneralApiTokenResourcePayload<R> :
R extends GeneralApiTxResourceName ? GeneralApiTxResourcePayload<R> :
R extends GeneralApiV1ResourceName ? GeneralApiV1ResourcePayload<R> :
never;
/* eslint-enable @stylistic/indent */
/* eslint-disable @stylistic/indent */
export type GeneralApiPaginationFilters<R extends GeneralApiResourceName> =
R extends GeneralApiAddressResourceName ? GeneralApiAddressPaginationFilters<R> :
R extends GeneralApiBlockResourceName ? GeneralApiBlockPaginationFilters<R> :
R extends GeneralApiContractResourceName ? GeneralApiContractPaginationFilters<R> :
R extends GeneralApiMiscResourceName ? GeneralApiMiscPaginationFilters<R> :
R extends GeneralApiRollupResourceName ? GeneralApiRollupPaginationFilters<R> :
R extends GeneralApiTokenResourceName ? GeneralApiTokenPaginationFilters<R> :
R extends GeneralApiTxResourceName ? GeneralApiTxPaginationFilters<R> :
never;
/* eslint-enable @stylistic/indent */
/* eslint-disable @stylistic/indent */
export type GeneralApiPaginationSorting<R extends GeneralApiResourceName> =
R extends GeneralApiAddressResourceName ? GeneralApiAddressPaginationSorting<R> :
R extends GeneralApiContractResourceName ? GeneralApiContractPaginationSorting<R> :
R extends GeneralApiMiscResourceName ? GeneralApiMiscPaginationSorting<R> :
R extends GeneralApiRollupResourceName ? GeneralApiRollupPaginationSorting<R> :
R extends GeneralApiTokenResourceName ? GeneralApiTokenPaginationSorting<R> :
never;
/* eslint-enable @stylistic/indent */
This diff is collapsed.
This diff is collapsed.
import type { ApiResource } from '../../types';
import type {
TokenCounters,
TokenInfo,
TokenHolders,
TokenInventoryResponse,
TokenInstance,
TokenInstanceTransfersCount,
TokenInventoryFilters,
} from 'types/api/token';
import type { TokensResponse, TokensFilters, TokensSorting, TokenInstanceTransferResponse, TokensBridgedFilters } from 'types/api/tokens';
import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer';
export const GENERAL_API_TOKEN_RESOURCES = {
// TOKEN
token: {
path: '/api/v2/tokens/:hash',
pathParams: [ 'hash' as const ],
},
token_counters: {
path: '/api/v2/tokens/:hash/counters',
pathParams: [ 'hash' as const ],
},
token_holders: {
path: '/api/v2/tokens/:hash/holders',
pathParams: [ 'hash' as const ],
filterFields: [],
paginated: true,
},
token_transfers: {
path: '/api/v2/tokens/:hash/transfers',
pathParams: [ 'hash' as const ],
filterFields: [],
paginated: true,
},
token_inventory: {
path: '/api/v2/tokens/:hash/instances',
pathParams: [ 'hash' as const ],
filterFields: [ 'holder_address_hash' as const ],
paginated: true,
},
tokens: {
path: '/api/v2/tokens',
filterFields: [ 'q' as const, 'type' as const ],
paginated: true,
},
tokens_bridged: {
path: '/api/v2/tokens/bridged',
filterFields: [ 'q' as const, 'chain_ids' as const ],
paginated: true,
},
// TOKEN INSTANCE
token_instance: {
path: '/api/v2/tokens/:hash/instances/:id',
pathParams: [ 'hash' as const, 'id' as const ],
},
token_instance_transfers_count: {
path: '/api/v2/tokens/:hash/instances/:id/transfers-count',
pathParams: [ 'hash' as const, 'id' as const ],
},
token_instance_transfers: {
path: '/api/v2/tokens/:hash/instances/:id/transfers',
pathParams: [ 'hash' as const, 'id' as const ],
filterFields: [],
paginated: true,
},
token_instance_holders: {
path: '/api/v2/tokens/:hash/instances/:id/holders',
pathParams: [ 'hash' as const, 'id' as const ],
filterFields: [],
paginated: true,
},
token_instance_refresh_metadata: {
path: '/api/v2/tokens/:hash/instances/:id/refetch-metadata',
pathParams: [ 'hash' as const, 'id' as const ],
filterFields: [],
},
// TOKEN TRANSFERS
token_transfers_all: {
path: '/api/v2/token-transfers',
filterFields: [ 'type' as const ],
paginated: true,
},
} satisfies Record<string, ApiResource>;
export type GeneralApiTokenResourceName = `general:${ keyof typeof GENERAL_API_TOKEN_RESOURCES }`;
/* eslint-disable @stylistic/indent */
export type GeneralApiTokenResourcePayload<R extends GeneralApiTokenResourceName> =
R extends 'general:token' ? TokenInfo :
R extends 'general:token_counters' ? TokenCounters :
R extends 'general:token_transfers' ? TokenTransferResponse :
R extends 'general:token_holders' ? TokenHolders :
R extends 'general:token_instance' ? TokenInstance :
R extends 'general:token_instance_transfers_count' ? TokenInstanceTransfersCount :
R extends 'general:token_instance_transfers' ? TokenInstanceTransferResponse :
R extends 'general:token_instance_holders' ? TokenHolders :
R extends 'general:token_inventory' ? TokenInventoryResponse :
R extends 'general:tokens' ? TokensResponse :
R extends 'general:tokens_bridged' ? TokensResponse :
R extends 'general:token_transfers_all' ? TokenTransferResponse :
never;
/* eslint-enable @stylistic/indent */
/* eslint-disable @stylistic/indent */
export type GeneralApiTokenPaginationFilters<R extends GeneralApiTokenResourceName> =
R extends 'general:token_transfers' ? TokenTransferFilters :
R extends 'general:token_inventory' ? TokenInventoryFilters :
R extends 'general:tokens' ? TokensFilters :
R extends 'general:tokens_bridged' ? TokensBridgedFilters :
R extends 'general:token_transfers_all' ? TokenTransferFilters :
never;
/* eslint-enable @stylistic/indent */
/* eslint-disable @stylistic/indent */
export type GeneralApiTokenPaginationSorting<R extends GeneralApiTokenResourceName> =
R extends 'general:tokens' ? TokensSorting :
R extends 'general:tokens_bridged' ? TokensSorting :
never;
/* eslint-enable @stylistic/indent */
import type { ApiResource } from '../../types';
import type { TxBlobs } from 'types/api/blobs';
import type { InternalTransactionsResponse } from 'types/api/internalTransaction';
import type { LogsResponseTx } from 'types/api/log';
import type { RawTracesResponse } from 'types/api/rawTrace';
import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer';
import type {
TransactionsResponseValidated,
TransactionsResponsePending,
Transaction,
TransactionsResponseWatchlist,
TransactionsResponseWithBlobs,
TransactionsStats,
} from 'types/api/transaction';
import type { TxInterpretationResponse } from 'types/api/txInterpretation';
import type { TTxsFilters, TTxsWithBlobsFilters } from 'types/api/txsFilters';
import type { TxStateChanges } from 'types/api/txStateChanges';
export const GENERAL_API_TX_RESOURCES = {
txs_stats: {
path: '/api/v2/transactions/stats',
},
txs_validated: {
path: '/api/v2/transactions',
filterFields: [ 'filter' as const, 'type' as const, 'method' as const ],
paginated: true,
},
txs_pending: {
path: '/api/v2/transactions',
filterFields: [ 'filter' as const, 'type' as const, 'method' as const ],
paginated: true,
},
txs_with_blobs: {
path: '/api/v2/transactions',
filterFields: [ 'type' as const ],
paginated: true,
},
txs_watchlist: {
path: '/api/v2/transactions/watchlist',
filterFields: [ ],
paginated: true,
},
txs_execution_node: {
path: '/api/v2/transactions/execution-node/:hash',
pathParams: [ 'hash' as const ],
filterFields: [ ],
paginated: true,
},
tx: {
path: '/api/v2/transactions/:hash',
pathParams: [ 'hash' as const ],
},
tx_internal_txs: {
path: '/api/v2/transactions/:hash/internal-transactions',
pathParams: [ 'hash' as const ],
filterFields: [ ],
paginated: true,
},
tx_logs: {
path: '/api/v2/transactions/:hash/logs',
pathParams: [ 'hash' as const ],
filterFields: [ ],
paginated: true,
},
tx_token_transfers: {
path: '/api/v2/transactions/:hash/token-transfers',
pathParams: [ 'hash' as const ],
filterFields: [ 'type' as const ],
paginated: true,
},
tx_raw_trace: {
path: '/api/v2/transactions/:hash/raw-trace',
pathParams: [ 'hash' as const ],
},
tx_state_changes: {
path: '/api/v2/transactions/:hash/state-changes',
pathParams: [ 'hash' as const ],
filterFields: [],
paginated: true,
},
tx_blobs: {
path: '/api/v2/transactions/:hash/blobs',
pathParams: [ 'hash' as const ],
paginated: true,
},
tx_interpretation: {
path: '/api/v2/transactions/:hash/summary',
pathParams: [ 'hash' as const ],
},
tx_external_transactions: {
path: '/api/v2/transactions/:hash/external-transactions',
pathParams: [ 'hash' as const ],
},
internal_txs: {
path: '/api/v2/internal-transactions',
paginated: true,
},
} satisfies Record<string, ApiResource>;
export type GeneralApiTxResourceName = `general:${ keyof typeof GENERAL_API_TX_RESOURCES }`;
/* eslint-disable @stylistic/indent */
export type GeneralApiTxResourcePayload<R extends GeneralApiTxResourceName> =
R extends 'general:txs_stats' ? TransactionsStats :
R extends 'general:txs_validated' ? TransactionsResponseValidated :
R extends 'general:txs_pending' ? TransactionsResponsePending :
R extends 'general:txs_with_blobs' ? TransactionsResponseWithBlobs :
R extends 'general:txs_watchlist' ? TransactionsResponseWatchlist :
R extends 'general:txs_execution_node' ? TransactionsResponseValidated :
R extends 'general:tx_internal_txs' ? InternalTransactionsResponse :
R extends 'general:tx' ? Transaction :
R extends 'general:tx_logs' ? LogsResponseTx :
R extends 'general:tx_token_transfers' ? TokenTransferResponse :
R extends 'general:tx_raw_trace' ? RawTracesResponse :
R extends 'general:tx_state_changes' ? TxStateChanges :
R extends 'general:tx_blobs' ? TxBlobs :
R extends 'general:tx_interpretation' ? TxInterpretationResponse :
R extends 'general:tx_external_transactions' ? Array<string> :
R extends 'general:internal_txs' ? InternalTransactionsResponse :
never;
/* eslint-enable @stylistic/indent */
/* eslint-disable @stylistic/indent */
export type GeneralApiTxPaginationFilters<R extends GeneralApiTxResourceName> =
R extends 'general:txs_validated' | 'general:txs_pending' ? TTxsFilters :
R extends 'general:txs_with_blobs' ? TTxsWithBlobsFilters :
R extends 'general:tx_token_transfers' ? TokenTransferFilters :
never;
/* eslint-enable @stylistic/indent */
import type { ApiResource } from '../../types';
import type { BlockCountdownResponse } from 'types/api/block';
export const GENERAL_API_V1_RESOURCES = {
csv_export_txs: {
path: '/api/v1/transactions-csv',
},
csv_export_internal_txs: {
path: '/api/v1/internal-transactions-csv',
},
csv_export_token_transfers: {
path: '/api/v1/token-transfers-csv',
},
csv_export_logs: {
path: '/api/v1/logs-csv',
},
csv_export_epoch_rewards: {
path: '/api/v1/celo-election-rewards-csv',
},
graphql: {
path: '/api/v1/graphql',
},
block_countdown: {
path: '/api',
},
} satisfies Record<string, ApiResource>;
export type GeneralApiV1ResourceName = `general:${ keyof typeof GENERAL_API_V1_RESOURCES }`;
/* eslint-disable @stylistic/indent */
export type GeneralApiV1ResourcePayload<R extends GeneralApiV1ResourceName> =
R extends 'general:block_countdown' ? BlockCountdownResponse :
never;
/* eslint-enable @stylistic/indent */
import type { ApiResource } from '../types';
import type { AddressMetadataInfo, PublicTagTypesResponse } from 'types/api/addressMetadata';
export const METADATA_API_RESOURCES = {
info: {
path: '/api/v1/metadata',
},
tags_search: {
path: '/api/v1/tags:search',
},
public_tag_types: {
path: '/api/v1/public-tag-types',
},
} satisfies Record<string, ApiResource>;
export type MetadataApiResourceName = `metadata:${ keyof typeof METADATA_API_RESOURCES }`;
/* eslint-disable @stylistic/indent */
export type MetadataApiResourcePayload<R extends MetadataApiResourceName> =
R extends 'metadata:info' ? AddressMetadataInfo :
R extends 'metadata:public_tag_types' ? PublicTagTypesResponse :
never;
/* eslint-enable @stylistic/indent */
import type { ApiResource } from '../types';
import type * as rewards from '@blockscout/points-types';
export const REWARDS_API_RESOURCES = {
config: {
path: '/api/v1/config',
},
check_ref_code: {
path: '/api/v1/auth/code/:code',
pathParams: [ 'code' as const ],
},
nonce: {
path: '/api/v1/auth/nonce',
},
check_user: {
path: '/api/v1/auth/user/:address',
pathParams: [ 'address' as const ],
},
login: {
path: '/api/v1/auth/login',
},
logout: {
path: '/api/v1/auth/logout',
},
user_balances: {
path: '/api/v1/user/balances',
},
user_daily_check: {
path: '/api/v1/user/daily/check',
},
user_daily_claim: {
path: '/api/v1/user/daily/claim',
},
user_referrals: {
path: '/api/v1/user/referrals',
},
user_check_activity_pass: {
path: '/api/v1/activity/check-pass',
filterFields: [ 'address' as const ],
},
user_activity: {
path: '/api/v1/user/activity/rewards',
},
user_activity_track_tx: {
path: '/api/v1/user/activity/track/transaction',
},
user_activity_track_tx_confirm: {
path: '/api/v1/activity/track/transaction/confirm',
},
user_activity_track_contract: {
path: '/api/v1/user/activity/track/contract',
},
user_activity_track_contract_confirm: {
path: '/api/v1/activity/track/contract/confirm',
},
user_activity_track_usage: {
path: '/api/v1/user/activity/track/usage',
},
instances: {
path: '/api/v1/instances',
},
} satisfies Record<string, ApiResource>;
export type RewardsApiResourceName = `rewards:${ keyof typeof REWARDS_API_RESOURCES }`;
/* eslint-disable @stylistic/indent */
export type RewardsApiResourcePayload<R extends RewardsApiResourceName> =
R extends 'rewards:config' ? rewards.GetConfigResponse :
R extends 'rewards:check_ref_code' ? rewards.AuthCodeResponse :
R extends 'rewards:nonce' ? rewards.AuthNonceResponse :
R extends 'rewards:check_user' ? rewards.AuthUserResponse :
R extends 'rewards:login' ? rewards.AuthLoginResponse :
R extends 'rewards:user_balances' ? rewards.GetUserBalancesResponse :
R extends 'rewards:user_daily_check' ? rewards.DailyRewardCheckResponse :
R extends 'rewards:user_daily_claim' ? rewards.DailyRewardClaimResponse :
R extends 'rewards:user_referrals' ? rewards.GetReferralDataResponse :
R extends 'rewards:user_check_activity_pass' ? rewards.CheckActivityPassResponse :
R extends 'rewards:user_activity' ? rewards.GetActivityRewardsResponse :
R extends 'rewards:user_activity_track_tx' ? rewards.PreSubmitTransactionResponse :
R extends 'rewards:user_activity_track_contract' ? rewards.PreVerifyContractResponse :
R extends 'rewards:instances' ? rewards.GetInstancesResponse :
never;
/* eslint-enable @stylistic/indent */
import type { ApiResource } from '../types';
import type * as stats from '@blockscout/stats-types';
export const STATS_API_RESOURCES = {
counters: {
path: '/api/v1/counters',
},
lines: {
path: '/api/v1/lines',
},
line: {
path: '/api/v1/lines/:id',
pathParams: [ 'id' as const ],
},
pages_main: {
path: '/api/v1/pages/main',
},
pages_transactions: {
path: '/api/v1/pages/transactions',
},
pages_contracts: {
path: '/api/v1/pages/contracts',
},
} satisfies Record<string, ApiResource>;
export type StatsApiResourceName = `stats:${ keyof typeof STATS_API_RESOURCES }`;
/* eslint-disable @stylistic/indent */
export type StatsApiResourcePayload<R extends StatsApiResourceName> =
R extends 'stats:counters' ? stats.Counters :
R extends 'stats:lines' ? stats.LineCharts :
R extends 'stats:line' ? stats.LineChart :
R extends 'stats:pages_main' ? stats.MainPageStats :
R extends 'stats:pages_transactions' ? stats.TransactionsPageStats :
R extends 'stats:pages_contracts' ? stats.ContractsPageStats :
never;
/* eslint-enable @stylistic/indent */
import type { ApiResource } from '../types';
export type IsPaginated<R extends ApiResource> = R extends { paginated: true } ? true : false;
import type { ApiResource } from '../types';
import type * as visualizer from '@blockscout/visualizer-types';
export const VISUALIZE_API_RESOURCES = {
solidity_contract: {
path: '/api/v1/solidity\\:visualize-contracts',
},
} satisfies Record<string, ApiResource>;
export type VisualizeApiResourceName = `visualize:${ keyof typeof VISUALIZE_API_RESOURCES }`;
/* eslint-disable @stylistic/indent */
export type VisualizeApiResourcePayload<R extends VisualizeApiResourceName> =
R extends 'visualize:solidity_contract' ? visualizer.VisualizeResponse :
never;
/* eslint-enable @stylistic/indent */
export type ApiName = 'general' | 'admin' | 'bens' | 'contractInfo' | 'metadata' | 'rewards' | 'stats' | 'visualize';
export interface ApiResource {
path: string;
pathParams?: Array<string>;
filterFields?: Array<string>;
paginated?: boolean;
headers?: RequestInit['headers'];
}
......@@ -13,8 +13,8 @@ import type { Params as FetchParams } from 'lib/hooks/useFetch';
import useFetch from 'lib/hooks/useFetch';
import buildUrl from './buildUrl';
import { RESOURCES } from './resources';
import type { ApiResource, ResourceName, ResourcePathParams } from './resources';
import getResourceParams from './getResourceParams';
import type { ResourceName, ResourcePathParams } from './resources';
export interface Params<R extends ResourceName> {
pathParams?: ResourcePathParams<R>;
......@@ -26,7 +26,7 @@ export interface Params<R extends ResourceName> {
export default function useApiFetch() {
const fetch = useFetch();
const queryClient = useQueryClient();
const { token: csrfToken } = queryClient.getQueryData<CsrfData>(getResourceKey('csrf')) || {};
const { token: csrfToken } = queryClient.getQueryData<CsrfData>(getResourceKey('general:csrf')) || {};
return React.useCallback(<R extends ResourceName, SuccessType = unknown, ErrorType = unknown>(
resourceName: R,
......@@ -34,12 +34,12 @@ export default function useApiFetch() {
) => {
const apiToken = cookies.get(cookies.NAMES.API_TOKEN);
const resource: ApiResource = RESOURCES[resourceName];
const { api, apiName, resource } = getResourceParams(resourceName);
const url = buildUrl(resourceName, pathParams, queryParams);
const withBody = isBodyAllowed(fetchParams?.method);
const headers = pickBy({
'x-endpoint': resource.endpoint && isNeedProxy() ? resource.endpoint : undefined,
Authorization: resource.endpoint && resource.needAuth ? apiToken : undefined,
'x-endpoint': api.endpoint && apiName !== 'general' && isNeedProxy() ? api.endpoint : undefined,
Authorization: [ 'admin', 'contractInfo' ].includes(apiName) ? apiToken : undefined,
'x-csrf-token': withBody && csrfToken ? csrfToken : undefined,
...resource.headers,
...fetchParams?.headers,
......
import type { InfiniteData, QueryKey, UseInfiniteQueryResult, UseInfiniteQueryOptions } from '@tanstack/react-query';
import { useInfiniteQuery } from '@tanstack/react-query';
import type { PaginatedResources, ResourceError, ResourcePayload } from 'lib/api/resources';
import useApiFetch from 'lib/api/useApiFetch';
import type { Params as ApiFetchParams } from 'lib/api/useApiFetch';
import type { PaginatedResourceName, ResourceError, ResourcePayload } from './resources';
import useApiFetch from './useApiFetch';
import type { Params as ApiFetchParams } from './useApiFetch';
import { getResourceKey } from './useApiQuery';
type TQueryData<R extends PaginatedResources> = ResourcePayload<R>;
type TQueryData<R extends PaginatedResourceName> = ResourcePayload<R>;
type TError = ResourceError<unknown>;
type TPageParam<R extends PaginatedResources> = ApiFetchParams<R>['queryParams'] | null;
type TPageParam<R extends PaginatedResourceName> = ApiFetchParams<R>['queryParams'] | null;
export interface Params<R extends PaginatedResources> {
export interface Params<R extends PaginatedResourceName> {
resourceName: R;
// 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'];
}
type ReturnType<Resource extends PaginatedResources> = UseInfiniteQueryResult<InfiniteData<ResourcePayload<Resource>>, ResourceError<unknown>>;
type ReturnType<Resource extends PaginatedResourceName> = UseInfiniteQueryResult<InfiniteData<ResourcePayload<Resource>>, ResourceError<unknown>>;
export default function useApiInfiniteQuery<R extends PaginatedResources>({
export default function useApiInfiniteQuery<R extends PaginatedResourceName>({
resourceName,
queryOptions,
pathParams,
......
......@@ -4,6 +4,8 @@ import React from 'react';
import getErrorObjPayload from 'lib/errors/getErrorObjPayload';
import getErrorObjStatusCode from 'lib/errors/getErrorObjStatusCode';
import type { ResourceName } from './resources';
export const retry = (failureCount: number, error: unknown) => {
const errorPayload = getErrorObjPayload<{ status: number }>(error);
const status = errorPayload?.status || getErrorObjStatusCode(error);
......@@ -28,13 +30,14 @@ export default function useQueryClientConfig() {
return false;
}
const EXTERNAL_API_RESOURCES = [
const EXTERNAL_API_RESOURCES: Array<ResourceName | 'safe_transaction_api' | 'gas_hawk_saving_potential'> = [
'general:contract_solidity_scan_report',
'general:address_xstar_score',
'general:noves_transaction',
'general:noves_address_history',
'general:noves_describe_txs',
// these resources are not proxied by the backend
'safe_transaction_api',
'contract_solidity_scan_report',
'address_xstar_score',
'noves_transaction',
'noves_address_history',
'noves_describe_txs',
'gas_hawk_saving_potential',
];
const isExternalApiResource = EXTERNAL_API_RESOURCES.some((resource) => query.queryKey[0] === resource);
......
......@@ -77,8 +77,8 @@ function getMessageToSign(address: string, nonce: string, isLogin?: boolean, ref
const referralText = refCode ? ` Referral code: ${ refCode }` : '';
const body = isLogin ? signInText : signUpText + referralText;
const urlObj = window.location.hostname === 'localhost' && feature.isEnabled ?
new URL(feature.api.endpoint) :
const urlObj = window.location.hostname === 'localhost' && config.apis.rewards ?
new URL(config.apis.rewards.endpoint) :
window.location;
return [
......@@ -146,18 +146,18 @@ export function RewardsContextProvider({ children }: Props) {
{ headers: { Authorization: `Bearer ${ apiToken }` } },
], [ apiToken ]);
const balancesQuery = useApiQuery('rewards_user_balances', { queryOptions, fetchParams });
const dailyRewardQuery = useApiQuery('rewards_user_daily_check', { queryOptions, fetchParams });
const referralsQuery = useApiQuery('rewards_user_referrals', { queryOptions, fetchParams });
const rewardsConfigQuery = useApiQuery('rewards_config', { queryOptions: { enabled: feature.isEnabled } });
const checkUserQuery = useApiQuery('rewards_check_user', { queryOptions: { enabled: feature.isEnabled }, pathParams: { address } });
const balancesQuery = useApiQuery('rewards:user_balances', { queryOptions, fetchParams });
const dailyRewardQuery = useApiQuery('rewards:user_daily_check', { queryOptions, fetchParams });
const referralsQuery = useApiQuery('rewards:user_referrals', { queryOptions, fetchParams });
const rewardsConfigQuery = useApiQuery('rewards:config', { queryOptions: { enabled: feature.isEnabled } });
const checkUserQuery = useApiQuery('rewards:check_user', { queryOptions: { enabled: feature.isEnabled }, pathParams: { address } });
// Reset queries when the API token is removed
useEffect(() => {
if (isInitialized && !apiToken) {
queryClient.resetQueries({ queryKey: getResourceKey('rewards_user_balances'), exact: true });
queryClient.resetQueries({ queryKey: getResourceKey('rewards_user_daily_check'), exact: true });
queryClient.resetQueries({ queryKey: getResourceKey('rewards_user_referrals'), exact: true });
queryClient.resetQueries({ queryKey: getResourceKey('rewards:user_balances'), exact: true });
queryClient.resetQueries({ queryKey: getResourceKey('rewards:user_daily_check'), exact: true });
queryClient.resetQueries({ queryKey: getResourceKey('rewards:user_referrals'), exact: true });
}
}, [ isInitialized, apiToken, queryClient ]);
......@@ -203,9 +203,9 @@ export function RewardsContextProvider({ children }: Props) {
throw new Error();
}
const [ nonceResponse, checkCodeResponse ] = await Promise.all([
apiFetch('rewards_nonce') as Promise<rewards.AuthNonceResponse>,
apiFetch('rewards:nonce') as Promise<rewards.AuthNonceResponse>,
refCode ?
apiFetch('rewards_check_ref_code', { pathParams: { code: refCode } }) as Promise<rewards.AuthCodeResponse> :
apiFetch('rewards:check_ref_code', { pathParams: { code: refCode } }) as Promise<rewards.AuthCodeResponse> :
Promise.resolve({ valid: true, reward: undefined }),
]);
if (!checkCodeResponse.valid) {
......@@ -217,7 +217,7 @@ export function RewardsContextProvider({ children }: Props) {
await switchChainAsync({ chainId: Number(config.chain.id) });
const message = getMessageToSign(address, nonceResponse.nonce, checkUserQuery.data?.exists, refCode);
const signature = await signMessageAsync({ message });
const loginResponse = await apiFetch('rewards_login', {
const loginResponse = await apiFetch('rewards:login', {
fetchParams: {
method: 'POST',
body: {
......@@ -241,7 +241,7 @@ export function RewardsContextProvider({ children }: Props) {
// Claim daily reward
const claim = useCallback(async() => {
try {
await apiFetch('rewards_user_daily_claim', {
await apiFetch('rewards:user_daily_claim', {
fetchParams: {
method: 'POST',
...fetchParams,
......
......@@ -10,10 +10,10 @@ export default function useGetCsrfToken() {
const nodeApiFetch = useFetch();
return useQuery({
queryKey: getResourceKey('csrf'),
queryKey: getResourceKey('general:csrf'),
queryFn: async() => {
if (!isNeedProxy()) {
const url = buildUrl('csrf');
const url = buildUrl('general:csrf');
const apiResponse = await fetch(url, { credentials: 'include' });
const csrfFromHeader = apiResponse.headers.get('x-bs-account-csrf');
......
......@@ -13,11 +13,11 @@ const feature = config.features.rewards;
const LAST_EXPLORE_TIME_KEY = 'rewards_activity_last_explore_time';
type RewardsActivityEndpoint =
| 'rewards_user_activity_track_tx'
| 'rewards_user_activity_track_tx_confirm'
| 'rewards_user_activity_track_contract'
| 'rewards_user_activity_track_contract_confirm'
| 'rewards_user_activity_track_usage';
| 'rewards:user_activity_track_tx'
| 'rewards:user_activity_track_tx_confirm'
| 'rewards:user_activity_track_contract'
| 'rewards:user_activity_track_contract_confirm'
| 'rewards:user_activity_track_usage';
export default function useRewardsActivity() {
const { apiToken } = useRewardsContext();
......@@ -25,7 +25,7 @@ export default function useRewardsActivity() {
const lastExploreTime = useRef<number>(0);
const profileQuery = useProfileQuery();
const checkActivityPassQuery = useApiQuery('rewards_user_check_activity_pass', {
const checkActivityPassQuery = useApiQuery('rewards:user_check_activity_pass', {
queryOptions: {
enabled: feature.isEnabled && Boolean(apiToken) && Boolean(profileQuery.data?.address_hash),
},
......@@ -61,7 +61,7 @@ export default function useRewardsActivity() {
const trackTransaction = useCallback(async(from: string, to: string) => {
return (
await makeRequest('rewards_user_activity_track_tx', {
await makeRequest('rewards:user_activity_track_tx', {
from_address: from,
to_address: to,
chain_id: config.chain.id ?? '',
......@@ -70,12 +70,12 @@ export default function useRewardsActivity() {
}, [ makeRequest ]);
const trackTransactionConfirm = useCallback((hash: string, token: string) =>
makeRequest('rewards_user_activity_track_tx_confirm', { tx_hash: hash, token }),
makeRequest('rewards:user_activity_track_tx_confirm', { tx_hash: hash, token }),
[ makeRequest ],
);
const trackContract = useCallback(async(address: string) =>
makeRequest('rewards_user_activity_track_contract', {
makeRequest('rewards:user_activity_track_contract', {
address,
chain_id: config.chain.id ?? '',
}),
......@@ -99,7 +99,7 @@ export default function useRewardsActivity() {
} catch {}
}
return makeRequest('rewards_user_activity_track_usage', {
return makeRequest('rewards:user_activity_track_usage', {
action,
chain_id: config.chain.id ?? '',
});
......
......@@ -11,7 +11,7 @@ interface Params {
hash: string;
}
const RESOURCE_NAME = 'contract_solidity_scan_report';
const RESOURCE_NAME = 'general:contract_solidity_scan_report';
const ERROR_NAME = 'Invalid response schema';
export default function useFetchReport({ hash }: Params) {
......
......@@ -10,7 +10,7 @@ export default function useAccountWithDomain(isEnabled: boolean) {
const isQueryEnabled = config.features.nameService.isEnabled && Boolean(address) && Boolean(isEnabled);
const domainQuery = useApiQuery('address_domain', {
const domainQuery = useApiQuery('bens:address_domain', {
pathParams: {
chainId: config.chain.id,
address,
......
......@@ -20,7 +20,7 @@ const wagmi = (() => {
[currentChain.id]: fallback(
config.chain.rpcUrls
.map((url) => http(url))
.concat(http(`${ config.api.endpoint }/api/eth-rpc`)),
.concat(http(`${ config.apis.general.endpoint }/api/eth-rpc`)),
),
...(parentChain ? { [parentChain.id]: http(parentChain.rpcUrls.default.http[0]) } : {}),
},
......
......@@ -9,7 +9,7 @@ interface Params {
hash: string;
}
const RESOURCE_NAME = 'address_xstar_score';
const RESOURCE_NAME = 'general:address_xstar_score';
const ERROR_NAME = 'Invalid response schema';
export default function useFetchXStarScore({ hash }: Params) {
......
......@@ -40,15 +40,8 @@ export function app(): CspDev.DirectiveDescriptor {
config.app.isDev ? 'ws://localhost:3000/_next/webpack-hmr' : '',
// APIs
config.api.endpoint,
config.api.socket,
getFeaturePayload(config.features.stats)?.api.endpoint,
getFeaturePayload(config.features.sol2uml)?.api.endpoint,
getFeaturePayload(config.features.verifiedTokens)?.api.endpoint,
getFeaturePayload(config.features.addressVerification)?.api.endpoint,
getFeaturePayload(config.features.nameService)?.api.endpoint,
getFeaturePayload(config.features.addressMetadata)?.api.endpoint,
getFeaturePayload(config.features.rewards)?.api.endpoint,
...Object.values(config.apis).filter(Boolean).map((api) => api.endpoint),
config.apis.general.socketEndpoint,
// chain RPC server
...config.chain.rpcUrls,
......
import { compile } from 'path-to-regexp';
import config from 'configs/app';
import { RESOURCES } from 'lib/api/resources';
import type { ApiResource, ResourceName } from 'lib/api/resources';
import getResourceParams from 'lib/api/getResourceParams';
import type { ResourceName } from 'lib/api/resources';
export default function buildUrl(
_resource: ApiResource | ResourceName,
_resource: ResourceName,
pathParams?: Record<string, string | undefined>,
queryParams?: Record<string, string | number | undefined>,
) {
const resource: ApiResource = typeof _resource === 'string' ? RESOURCES[_resource] : _resource;
const baseUrl = resource.endpoint || config.api.endpoint;
const basePath = resource.basePath !== undefined ? resource.basePath : config.api.basePath;
const { resource, api } = getResourceParams(_resource);
const baseUrl = api.endpoint;
const basePath = api.basePath ?? '';
const path = basePath + resource.path;
const url = new URL(compile(path)(pathParams), baseUrl);
......
......@@ -3,21 +3,14 @@ import fetch, { AbortError } from 'node-fetch';
import buildUrl from 'nextjs/utils/buildUrl';
import { httpLogger } from 'nextjs/utils/logger';
import { RESOURCES } from 'lib/api/resources';
import type { ResourceName, ResourcePathParams, ResourcePayload } from 'lib/api/resources';
import metrics from 'lib/monitoring/metrics';
import { SECOND } from 'toolkit/utils/consts';
type Params<R extends ResourceName> = (
{
type Params<R extends ResourceName> = {
resource: R;
pathParams?: ResourcePathParams<R>;
queryParams?: Record<string, string | number | undefined>;
} | {
url: string;
route: string;
}
) & {
timeout?: number;
};
......@@ -27,15 +20,14 @@ export default async function fetchApi<R extends ResourceName = never, S = Resou
controller.abort();
}, params.timeout || SECOND);
const url = 'url' in params ? params.url : buildUrl(params.resource, params.pathParams, params.queryParams);
const route = 'route' in params ? params.route : RESOURCES[params.resource]['path'];
const url = buildUrl(params.resource, params.pathParams, params.queryParams);
const end = metrics?.apiRequestDuration.startTimer();
try {
const response = await fetch(url, { signal: controller.signal });
const duration = end?.({ route, code: response.status });
const duration = end?.({ route: params.resource, code: response.status });
if (response.status === 200) {
httpLogger.logger.info({ message: 'API fetch', url, code: response.status, duration });
} else {
......@@ -45,7 +37,7 @@ export default async function fetchApi<R extends ResourceName = never, S = Resou
return await response.json() as Promise<S>;
} catch (error) {
const code = error instanceof AbortError ? 504 : 500;
const duration = end?.({ route, code });
const duration = end?.({ route: params.resource, code });
httpLogger.logger.error({ message: 'API fetch', url, code, duration });
} finally {
clearTimeout(timeout);
......
......@@ -82,7 +82,7 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
<QueryClientProvider client={ queryClient }>
<GrowthBookProvider growthbook={ growthBook }>
<ScrollDirectionProvider>
<SocketProvider url={ `${ config.api.socket }${ config.api.basePath }/socket/v2` }>
<SocketProvider url={ `${ config.apis.general.socketEndpoint }${ config.apis.general.basePath ?? '' }/socket/v2` }>
<RewardsContextProvider>
<MarketplaceContextProvider>
<SettingsContextProvider>
......
......@@ -32,7 +32,7 @@ export const getServerSideProps: GetServerSideProps<Props<typeof pathname>> = as
if (botInfo?.type === 'social_preview') {
const addressData = await fetchApi({
resource: 'address',
resource: 'general:address',
pathParams: { hash: getQueryParamString(ctx.query.hash) },
timeout: 1_000,
});
......
......@@ -7,7 +7,7 @@ import { httpLogger } from 'nextjs/utils/logger';
export default async function csrfHandler(_req: NextApiRequest, res: NextApiResponse) {
httpLogger(_req, res);
const url = buildUrl('csrf');
const url = buildUrl('general:csrf');
const response = await fetchFactory(_req)(url);
if (response.status === 200) {
......
......@@ -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.api.endpoint,
nextReq.headers['x-endpoint']?.toString() || appConfig.apis.general.endpoint,
);
const apiRes = await fetchFactory(nextReq)(
url.toString(),
......
import type { GetServerSideProps } from 'next';
import dynamic from 'next/dynamic';
import fetch from 'node-fetch';
import React from 'react';
import type { NextPageWithLayout } from 'nextjs/types';
......@@ -49,19 +50,25 @@ export const getServerSideProps: GetServerSideProps<Props<typeof pathname>> = as
const appData = await(async() => {
if ('configUrl' in feature) {
const appList = await fetchApi<never, Array<MarketplaceAppOverview>>({
url: config.app.baseUrl + feature.configUrl,
route: '/marketplace_config',
timeout: 1_000,
});
const controller = new AbortController();
const timeout = setTimeout(() => {
controller.abort();
}, 1_000);
try {
const response = await fetch(feature.configUrl, { signal: controller.signal });
const appList = await response.json() as Array<MarketplaceAppOverview>;
clearTimeout(timeout);
if (appList && Array.isArray(appList)) {
return appList.find(app => app.id === getQueryParamString(ctx.query.id));
}
} catch (error) {} finally {
clearTimeout(timeout);
}
} else {
return await fetchApi({
resource: 'marketplace_dapp',
resource: 'admin:marketplace_dapp',
pathParams: { dappId: getQueryParamString(ctx.query.id), chainId: config.chain.id },
timeout: 1_000,
});
......
......@@ -36,7 +36,7 @@ export const getServerSideProps: GetServerSideProps<Props<typeof pathname>> = as
(config.meta.og.enhancedDataEnabled && detectBotRequest(ctx.req)?.type === 'social_preview')
) {
const chartData = await fetchApi({
resource: 'stats_line',
resource: 'stats:line',
pathParams: { id: getQueryParamString(ctx.query.id) },
queryParams: { from: dayjs().format('YYYY-MM-DD'), to: dayjs().format('YYYY-MM-DD') },
timeout: 1000,
......
......@@ -33,7 +33,7 @@ export const getServerSideProps: GetServerSideProps<Props<typeof pathname>> = as
(config.meta.og.enhancedDataEnabled && detectBotRequest(ctx.req)?.type === 'social_preview')
) {
const tokenData = await fetchApi({
resource: 'token',
resource: 'general:token',
pathParams: { hash: getQueryParamString(ctx.query.hash) },
timeout: 500,
});
......
......@@ -32,7 +32,7 @@ export const getServerSideProps: GetServerSideProps<Props<typeof pathname>> = as
if (botInfo?.type === 'social_preview') {
const tokenData = await fetchApi({
resource: 'token',
resource: 'general:token',
pathParams: { hash: getQueryParamString(ctx.query.hash) },
timeout: 1_000,
});
......
......@@ -85,10 +85,10 @@ export const ENVS_MAP: Record<string, Array<[string, string]>> = {
[ 'NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED', 'true' ],
],
nameService: [
[ 'NEXT_PUBLIC_NAME_SERVICE_API_HOST', 'https://localhost:3101' ],
[ 'NEXT_PUBLIC_NAME_SERVICE_API_HOST', 'https://localhost:3008' ],
],
rewardsService: [
[ 'NEXT_PUBLIC_REWARDS_SERVICE_API_HOST', 'http://localhost:3003' ],
[ 'NEXT_PUBLIC_REWARDS_SERVICE_API_HOST', 'http://localhost:3009' ],
],
addressBech32Format: [
[ 'NEXT_PUBLIC_ADDRESS_FORMAT', '["bech32","base16"]' ],
......
......@@ -66,26 +66,26 @@ export const TOKEN_HOLDER_ERC_1155: TokenHolder = {
export const getTokenHoldersStub = (type?: TokenType, pagination: TokenHoldersPagination | null = null): TokenHolders => {
switch (type) {
case 'ERC-721':
return generateListStub<'token_holders'>(TOKEN_HOLDER_ERC_20, 50, { next_page_params: pagination });
return generateListStub<'general:token_holders'>(TOKEN_HOLDER_ERC_20, 50, { next_page_params: pagination });
case 'ERC-1155':
return generateListStub<'token_holders'>(TOKEN_HOLDER_ERC_1155, 50, { next_page_params: pagination });
return generateListStub<'general:token_holders'>(TOKEN_HOLDER_ERC_1155, 50, { next_page_params: pagination });
case 'ERC-404':
return generateListStub<'token_holders'>(TOKEN_HOLDER_ERC_1155, 50, { next_page_params: pagination });
return generateListStub<'general:token_holders'>(TOKEN_HOLDER_ERC_1155, 50, { next_page_params: pagination });
default:
return generateListStub<'token_holders'>(TOKEN_HOLDER_ERC_20, 50, { next_page_params: pagination });
return generateListStub<'general:token_holders'>(TOKEN_HOLDER_ERC_20, 50, { next_page_params: pagination });
}
};
export const getTokenInstanceHoldersStub = (type?: TokenType, pagination: TokenHoldersPagination | null = null): TokenHolders => {
switch (type) {
case 'ERC-721':
return generateListStub<'token_instance_holders'>(TOKEN_HOLDER_ERC_20, 10, { next_page_params: pagination });
return generateListStub<'general:token_instance_holders'>(TOKEN_HOLDER_ERC_20, 10, { next_page_params: pagination });
case 'ERC-1155':
return generateListStub<'token_instance_holders'>(TOKEN_HOLDER_ERC_1155, 10, { next_page_params: pagination });
return generateListStub<'general:token_instance_holders'>(TOKEN_HOLDER_ERC_1155, 10, { next_page_params: pagination });
case 'ERC-404':
return generateListStub<'token_instance_holders'>(TOKEN_HOLDER_ERC_1155, 10, { next_page_params: pagination });
return generateListStub<'general:token_instance_holders'>(TOKEN_HOLDER_ERC_1155, 10, { next_page_params: pagination });
default:
return generateListStub<'token_instance_holders'>(TOKEN_HOLDER_ERC_20, 10, { next_page_params: pagination });
return generateListStub<'general:token_instance_holders'>(TOKEN_HOLDER_ERC_20, 10, { next_page_params: pagination });
}
};
......@@ -140,26 +140,26 @@ export const TOKEN_TRANSFER_ERC_404: TokenTransfer = {
export const getTokenTransfersStub = (type?: TokenType, pagination: TokenTransferPagination | null = null): TokenTransferResponse => {
switch (type) {
case 'ERC-721':
return generateListStub<'token_transfers'>(TOKEN_TRANSFER_ERC_721, 50, { next_page_params: pagination });
return generateListStub<'general:token_transfers'>(TOKEN_TRANSFER_ERC_721, 50, { next_page_params: pagination });
case 'ERC-1155':
return generateListStub<'token_transfers'>(TOKEN_TRANSFER_ERC_1155, 50, { next_page_params: pagination });
return generateListStub<'general:token_transfers'>(TOKEN_TRANSFER_ERC_1155, 50, { next_page_params: pagination });
case 'ERC-404':
return generateListStub<'token_transfers'>(TOKEN_TRANSFER_ERC_404, 50, { next_page_params: pagination });
return generateListStub<'general:token_transfers'>(TOKEN_TRANSFER_ERC_404, 50, { next_page_params: pagination });
default:
return generateListStub<'token_transfers'>(TOKEN_TRANSFER_ERC_20, 50, { next_page_params: pagination });
return generateListStub<'general:token_transfers'>(TOKEN_TRANSFER_ERC_20, 50, { next_page_params: pagination });
}
};
export const getTokenInstanceTransfersStub = (type?: TokenType, pagination: TokenInstanceTransferPagination | null = null): TokenInstanceTransferResponse => {
switch (type) {
case 'ERC-721':
return generateListStub<'token_instance_transfers'>(TOKEN_TRANSFER_ERC_721, 10, { next_page_params: pagination });
return generateListStub<'general:token_instance_transfers'>(TOKEN_TRANSFER_ERC_721, 10, { next_page_params: pagination });
case 'ERC-1155':
return generateListStub<'token_instance_transfers'>(TOKEN_TRANSFER_ERC_1155, 10, { next_page_params: pagination });
return generateListStub<'general:token_instance_transfers'>(TOKEN_TRANSFER_ERC_1155, 10, { next_page_params: pagination });
case 'ERC-404':
return generateListStub<'token_instance_transfers'>(TOKEN_TRANSFER_ERC_404, 10, { next_page_params: pagination });
return generateListStub<'general:token_instance_transfers'>(TOKEN_TRANSFER_ERC_404, 10, { next_page_params: pagination });
default:
return generateListStub<'token_instance_transfers'>(TOKEN_TRANSFER_ERC_20, 10, { next_page_params: pagination });
return generateListStub<'general:token_instance_transfers'>(TOKEN_TRANSFER_ERC_20, 10, { next_page_params: pagination });
}
};
......
import type { ArrayElement } from 'types/utils';
import type { PaginatedResources, PaginatedResponse, PaginatedResponseItems } from 'lib/api/resources';
import type { PaginatedResourceName, PaginatedResourceResponse, PaginatedResourceResponseItems } from 'lib/api/resources';
export function generateListStub<Resource extends PaginatedResources>(
stub: ArrayElement<PaginatedResponseItems<Resource>>,
export function generateListStub<Resource extends PaginatedResourceName>(
stub: ArrayElement<PaginatedResourceResponseItems<Resource>>,
num = 50,
rest: Omit<PaginatedResponse<Resource>, 'items'>,
rest: Omit<PaginatedResourceResponse<Resource>, 'items'>,
) {
return {
items: Array(num).fill(stub),
......
......@@ -37,11 +37,11 @@ const AddressAccountHistory = ({ shouldRender = true, isQueryEnabled = true }: P
const [ filterValue, setFilterValue ] = React.useState<NovesHistoryFilterValue>(getFilterValue(router.query.filter));
const { data, isError, pagination, isPlaceholderData } = useQueryWithPages({
resourceName: 'noves_address_history',
resourceName: 'general:noves_address_history',
pathParams: { address: currentAddress },
options: {
enabled: isQueryEnabled,
placeholderData: generateListStub<'noves_address_history'>(NOVES_TRANSLATE, 10, { hasNextPage: false, pageNumber: 1, pageSize: 10 }),
placeholderData: generateListStub<'general:noves_address_history'>(NOVES_TRANSLATE, 10, { hasNextPage: false, pageNumber: 1, pageSize: 10 }),
},
});
......
......@@ -41,11 +41,11 @@ const AddressBlocksValidated = ({ shouldRender = true, isQueryEnabled = true }:
const addressHash = String(router.query.hash);
const query = useQueryWithPages({
resourceName: 'address_blocks_validated',
resourceName: 'general:address_blocks_validated',
pathParams: { hash: addressHash },
options: {
enabled: isQueryEnabled,
placeholderData: generateListStub<'address_blocks_validated'>(
placeholderData: generateListStub<'general:address_blocks_validated'>(
BLOCK,
50,
{
......@@ -66,7 +66,7 @@ const AddressBlocksValidated = ({ shouldRender = true, isQueryEnabled = true }:
setSocketAlert('');
queryClient.setQueryData(
getResourceKey('address_blocks_validated', { pathParams: { hash: addressHash } }),
getResourceKey('general:address_blocks_validated', { pathParams: { hash: addressHash } }),
(prevData: AddressBlocksValidatedResponse | undefined) => {
if (!prevData) {
return;
......
......@@ -13,8 +13,8 @@ const hooksConfig = {
};
test('base view +@dark-mode', async({ render, page, mockApiResponse }) => {
await mockApiResponse('address_coin_balance', balanceHistoryMock.baseResponse, { pathParams: { hash: addressHash } });
await mockApiResponse('address_coin_balance_chart', balanceHistoryMock.chartResponse, { pathParams: { hash: addressHash } });
await mockApiResponse('general:address_coin_balance', balanceHistoryMock.baseResponse, { pathParams: { hash: addressHash } });
await mockApiResponse('general:address_coin_balance_chart', balanceHistoryMock.chartResponse, { pathParams: { hash: addressHash } });
const component = await render(<AddressCoinBalance/>, { hooksConfig });
await page.waitForFunction(() => {
return document.querySelector('path[data-name="chart-Balances-small"]')?.getAttribute('opacity') === '1';
......@@ -28,8 +28,8 @@ test.describe('mobile', () => {
test.use({ viewport: devices['iPhone 13 Pro'].viewport });
test('base view', async({ render, page, mockApiResponse }) => {
await mockApiResponse('address_coin_balance', balanceHistoryMock.baseResponse, { pathParams: { hash: addressHash } });
await mockApiResponse('address_coin_balance_chart', balanceHistoryMock.chartResponse, { pathParams: { hash: addressHash } });
await mockApiResponse('general:address_coin_balance', balanceHistoryMock.baseResponse, { pathParams: { hash: addressHash } });
await mockApiResponse('general:address_coin_balance_chart', balanceHistoryMock.chartResponse, { pathParams: { hash: addressHash } });
const component = await render(<AddressCoinBalance/>, { hooksConfig });
await page.waitForFunction(() => {
return document.querySelector('path[data-name="chart-Balances-small"]')?.getAttribute('opacity') === '1';
......
......@@ -33,12 +33,12 @@ const AddressCoinBalance = ({ shouldRender = true, isQueryEnabled = true }: Prop
const addressHash = getQueryParamString(router.query.hash);
const coinBalanceQuery = useQueryWithPages({
resourceName: 'address_coin_balance',
resourceName: 'general:address_coin_balance',
pathParams: { hash: addressHash },
scrollRef,
options: {
enabled: isQueryEnabled,
placeholderData: generateListStub<'address_coin_balance'>(
placeholderData: generateListStub<'general:address_coin_balance'>(
ADDRESS_COIN_BALANCE,
50,
{
......@@ -59,7 +59,7 @@ const AddressCoinBalance = ({ shouldRender = true, isQueryEnabled = true }: Prop
setSocketAlert(false);
queryClient.setQueryData(
getResourceKey('address_coin_balance', { pathParams: { hash: addressHash } }),
getResourceKey('general:address_coin_balance', { pathParams: { hash: addressHash } }),
(prevData: AddressCoinBalanceHistoryResponse | undefined) => {
if (!prevData) {
return;
......
......@@ -12,9 +12,9 @@ import AddressContract from './AddressContract.pwstory';
const hash = addressMock.contract.hash;
test.beforeEach(async({ mockApiResponse }) => {
await mockApiResponse('address', addressMock.contract, { pathParams: { hash } });
await mockApiResponse('general:address', addressMock.contract, { pathParams: { hash } });
await mockApiResponse(
'contract',
'general:contract',
{ ...contractInfoMock.verified, abi: [ ...contractMethodsMock.read, ...contractMethodsMock.write ] },
{ pathParams: { hash } },
);
......
......@@ -10,7 +10,7 @@ import AddressContract from './AddressContract';
const AddressContractPwStory = () => {
const router = useRouter();
const hash = getQueryParamString(router.query.hash);
const addressQuery = useApiQuery('address', { pathParams: { hash } });
const addressQuery = useApiQuery('general:address', { pathParams: { hash } });
const { tabs } = useContractTabs(addressQuery.data, false);
return <AddressContract tabs={ tabs } shouldRender={ true } isLoading={ false }/>;
};
......
......@@ -21,8 +21,8 @@ test.describe('mobile', () => {
test.use({ viewport: devices['iPhone 13 Pro'].viewport });
test('contract', async({ render, mockApiResponse, page }) => {
await mockApiResponse('address', addressMock.contract, { pathParams: { hash: ADDRESS_HASH } });
await mockApiResponse('address_counters', countersMock.forContract, { pathParams: { hash: ADDRESS_HASH } });
await mockApiResponse('general:address', addressMock.contract, { pathParams: { hash: ADDRESS_HASH } });
await mockApiResponse('general:address_counters', countersMock.forContract, { pathParams: { hash: ADDRESS_HASH } });
const component = await render(<AddressDetails addressQuery={{ data: addressMock.contract } as AddressQuery}/>, { hooksConfig });
......@@ -33,8 +33,8 @@ test.describe('mobile', () => {
});
test('validator', async({ render, page, mockApiResponse }) => {
await mockApiResponse('address', addressMock.validator, { pathParams: { hash: ADDRESS_HASH } });
await mockApiResponse('address_counters', countersMock.forValidator, { pathParams: { hash: ADDRESS_HASH } });
await mockApiResponse('general:address', addressMock.validator, { pathParams: { hash: ADDRESS_HASH } });
await mockApiResponse('general:address_counters', countersMock.forValidator, { pathParams: { hash: ADDRESS_HASH } });
const component = await render(<AddressDetails addressQuery={{ data: addressMock.validator } as AddressQuery}/>, { hooksConfig });
......@@ -45,8 +45,8 @@ test.describe('mobile', () => {
});
test('filecoin', async({ render, mockApiResponse, page }) => {
await mockApiResponse('address', addressMock.filecoin, { pathParams: { hash: ADDRESS_HASH } });
await mockApiResponse('address_counters', countersMock.forValidator, { pathParams: { hash: ADDRESS_HASH } });
await mockApiResponse('general:address', addressMock.filecoin, { pathParams: { hash: ADDRESS_HASH } });
await mockApiResponse('general:address_counters', countersMock.forValidator, { pathParams: { hash: ADDRESS_HASH } });
const component = await render(<AddressDetails addressQuery={{ data: addressMock.filecoin } as AddressQuery}/>, { hooksConfig });
......@@ -58,8 +58,8 @@ test.describe('mobile', () => {
});
test('contract', async({ render, page, mockApiResponse }) => {
await mockApiResponse('address', addressMock.contract, { pathParams: { hash: ADDRESS_HASH } });
await mockApiResponse('address_counters', countersMock.forContract, { pathParams: { hash: ADDRESS_HASH } });
await mockApiResponse('general:address', addressMock.contract, { pathParams: { hash: ADDRESS_HASH } });
await mockApiResponse('general:address_counters', countersMock.forContract, { pathParams: { hash: ADDRESS_HASH } });
const component = await render(<AddressDetails addressQuery={{ data: addressMock.contract } as AddressQuery}/>, { hooksConfig });
......@@ -71,12 +71,12 @@ test('contract', async({ render, page, mockApiResponse }) => {
// there's an unexpected timeout occurred in this test
test.fixme('token', async({ render, mockApiResponse, injectMetaMaskProvider, page }) => {
await mockApiResponse('address', addressMock.token, { pathParams: { hash: ADDRESS_HASH } });
await mockApiResponse('address_counters', countersMock.forToken, { pathParams: { hash: ADDRESS_HASH } });
await mockApiResponse('address_tokens', tokensMock.erc20List, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-20' }, times: 1 });
await mockApiResponse('address_tokens', tokensMock.erc721List, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-721' }, times: 1 });
await mockApiResponse('address_tokens', tokensMock.erc1155List, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-1155' }, times: 1 });
await mockApiResponse('address_tokens', tokensMock.erc404List, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-404' }, times: 1 });
await mockApiResponse('general:address', addressMock.token, { pathParams: { hash: ADDRESS_HASH } });
await mockApiResponse('general:address_counters', countersMock.forToken, { pathParams: { hash: ADDRESS_HASH } });
await mockApiResponse('general:address_tokens', tokensMock.erc20List, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-20' }, times: 1 });
await mockApiResponse('general:address_tokens', tokensMock.erc721List, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-721' }, times: 1 });
await mockApiResponse('general:address_tokens', tokensMock.erc1155List, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-1155' }, times: 1 });
await mockApiResponse('general:address_tokens', tokensMock.erc404List, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-404' }, times: 1 });
await injectMetaMaskProvider();
const component = await render(
......@@ -93,8 +93,8 @@ test.fixme('token', async({ render, mockApiResponse, injectMetaMaskProvider, pag
});
test('validator', async({ render, mockApiResponse, page }) => {
await mockApiResponse('address', addressMock.validator, { pathParams: { hash: ADDRESS_HASH } });
await mockApiResponse('address_counters', countersMock.forValidator, { pathParams: { hash: ADDRESS_HASH } });
await mockApiResponse('general:address', addressMock.validator, { pathParams: { hash: ADDRESS_HASH } });
await mockApiResponse('general:address_counters', countersMock.forValidator, { pathParams: { hash: ADDRESS_HASH } });
const component = await render(<AddressDetails addressQuery={{ data: addressMock.validator } as AddressQuery}/>, { hooksConfig });
......@@ -105,8 +105,8 @@ test('validator', async({ render, mockApiResponse, page }) => {
});
test('filecoin', async({ render, mockApiResponse, page }) => {
await mockApiResponse('address', addressMock.filecoin, { pathParams: { hash: ADDRESS_HASH } });
await mockApiResponse('address_counters', countersMock.forValidator, { pathParams: { hash: ADDRESS_HASH } });
await mockApiResponse('general:address', addressMock.filecoin, { pathParams: { hash: ADDRESS_HASH } });
await mockApiResponse('general:address_counters', countersMock.forValidator, { pathParams: { hash: ADDRESS_HASH } });
const component = await render(<AddressDetails addressQuery={{ data: addressMock.filecoin } as AddressQuery}/>, { hooksConfig });
......
......@@ -14,7 +14,7 @@ const hooksConfig = {
};
test('base view +@mobile', async({ render, mockApiResponse }) => {
await mockApiResponse('address_epoch_rewards', epochRewards, { pathParams: { hash: ADDRESS_HASH } });
await mockApiResponse('general:address_epoch_rewards', epochRewards, { pathParams: { hash: ADDRESS_HASH } });
const component = await render(
<Box pt={{ base: '134px', lg: 6 }}>
<AddressEpochRewards/>
......
......@@ -27,13 +27,13 @@ const AddressEpochRewards = ({ shouldRender = true, isQueryEnabled = true }: Pro
const hash = getQueryParamString(router.query.hash);
const rewardsQuery = useQueryWithPages({
resourceName: 'address_epoch_rewards',
resourceName: 'general:address_epoch_rewards',
pathParams: {
hash,
},
options: {
enabled: isQueryEnabled && Boolean(hash),
placeholderData: generateListStub<'address_epoch_rewards'>(EPOCH_REWARD_ITEM, 50, { next_page_params: {
placeholderData: generateListStub<'general:address_epoch_rewards'>(EPOCH_REWARD_ITEM, 50, { next_page_params: {
amount: '1',
items_count: 50,
type: 'voter',
......
......@@ -15,7 +15,7 @@ const hooksConfig = {
test('base view +@mobile', async({ render, mockApiResponse }) => {
test.slow();
await mockApiResponse('address_internal_txs', internalTxsMock.baseResponse, { pathParams: { hash: ADDRESS_HASH } });
await mockApiResponse('general:address_internal_txs', internalTxsMock.baseResponse, { pathParams: { hash: ADDRESS_HASH } });
const component = await render(
<Box pt={{ base: '134px', lg: 6 }}>
<AddressInternalTxs/>
......
......@@ -36,12 +36,12 @@ const AddressInternalTxs = ({ shouldRender = true, isQueryEnabled = true }: Prop
const hash = getQueryParamString(router.query.hash);
const { data, isPlaceholderData, isError, pagination, onFilterChange } = useQueryWithPages({
resourceName: 'address_internal_txs',
resourceName: 'general:address_internal_txs',
pathParams: { hash },
filters: { filter: filterValue },
options: {
enabled: isQueryEnabled,
placeholderData: generateListStub<'address_internal_txs'>(
placeholderData: generateListStub<'general:address_internal_txs'>(
INTERNAL_TX,
50,
{
......
......@@ -25,11 +25,11 @@ const AddressLogs = ({ shouldRender = true, isQueryEnabled = true }: Props) => {
const hash = getQueryParamString(router.query.hash);
const { data, isPlaceholderData, isError, pagination } = useQueryWithPages({
resourceName: 'address_logs',
resourceName: 'general:address_logs',
pathParams: { hash },
options: {
enabled: isQueryEnabled,
placeholderData: generateListStub<'address_logs'>(LOG, 3, { next_page_params: {
placeholderData: generateListStub<'general:address_logs'>(LOG, 3, { next_page_params: {
block_number: 9005750,
index: 42,
items_count: 50,
......
......@@ -28,7 +28,7 @@ const tokenTransfersWoPagination = {
};
test('with pagination', async({ render, mockApiResponse }) => {
await mockApiResponse('address_token_transfers', tokenTransfersWithPagination, {
await mockApiResponse('general:address_token_transfers', tokenTransfersWithPagination, {
pathParams: { hash: CURRENT_ADDRESS },
queryParams: { type: [] },
});
......@@ -42,7 +42,7 @@ test('with pagination', async({ render, mockApiResponse }) => {
});
test('without pagination', async({ render, mockApiResponse }) => {
await mockApiResponse('address_token_transfers', tokenTransfersWoPagination, {
await mockApiResponse('general:address_token_transfers', tokenTransfersWoPagination, {
pathParams: { hash: CURRENT_ADDRESS },
queryParams: { type: [] },
});
......@@ -59,7 +59,7 @@ test.describe('mobile', () => {
test.use({ viewport: devices['iPhone 13 Pro'].viewport });
test('with pagination', async({ render, mockApiResponse }) => {
await mockApiResponse('address_token_transfers', tokenTransfersWithPagination, {
await mockApiResponse('general:address_token_transfers', tokenTransfersWithPagination, {
pathParams: { hash: CURRENT_ADDRESS },
queryParams: { type: [] },
});
......@@ -73,7 +73,7 @@ test.describe('mobile', () => {
});
test('without pagination', async({ render, mockApiResponse }) => {
await mockApiResponse('address_token_transfers', tokenTransfersWoPagination, {
await mockApiResponse('general:address_token_transfers', tokenTransfersWoPagination, {
pathParams: { hash: CURRENT_ADDRESS },
queryParams: { type: [] },
});
......@@ -94,7 +94,7 @@ test.describe('socket', () => {
query: { hash: CURRENT_ADDRESS },
},
};
await mockApiResponse('address_token_transfers', tokenTransfersWithPagination, {
await mockApiResponse('general:address_token_transfers', tokenTransfersWithPagination, {
pathParams: { hash: CURRENT_ADDRESS },
queryParams: { type: [] },
});
......@@ -127,7 +127,7 @@ test.describe('socket', () => {
query: { hash: CURRENT_ADDRESS },
},
};
await mockApiResponse('address_token_transfers', tokenTransfersWithPagination, {
await mockApiResponse('general:address_token_transfers', tokenTransfersWithPagination, {
pathParams: { hash: CURRENT_ADDRESS },
queryParams: { type: [] },
});
......@@ -163,7 +163,7 @@ test.describe('socket', () => {
query: { hash: CURRENT_ADDRESS, type: 'ERC-1155' },
},
};
await mockApiResponse('address_token_transfers', tokenTransfersWithPagination, {
await mockApiResponse('general:address_token_transfers', tokenTransfersWithPagination, {
pathParams: { hash: CURRENT_ADDRESS },
queryParams: { type: 'ERC-1155' },
});
......@@ -197,7 +197,7 @@ test.describe('socket', () => {
query: { hash: CURRENT_ADDRESS, type: 'ERC-1155' },
},
};
await mockApiResponse('address_token_transfers', tokenTransfersWithPagination, {
await mockApiResponse('general:address_token_transfers', tokenTransfersWithPagination, {
pathParams: { hash: CURRENT_ADDRESS },
queryParams: { type: 'ERC-1155' },
});
......
......@@ -84,7 +84,7 @@ const AddressTokenTransfers = ({ overloadCount = OVERLOAD_COUNT, shouldRender =
);
const { isError, isPlaceholderData, data, pagination, onFilterChange } = useQueryWithPages({
resourceName: 'address_token_transfers',
resourceName: 'general:address_token_transfers',
pathParams: { hash: currentAddress },
filters,
options: {
......@@ -132,7 +132,7 @@ const AddressTokenTransfers = ({ overloadCount = OVERLOAD_COUNT, shouldRender =
if (newItems.length > 0) {
queryClient.setQueryData(
getResourceKey('address_token_transfers', { pathParams: { hash: currentAddress }, queryParams: { ...filters } }),
getResourceKey('general:address_token_transfers', { pathParams: { hash: currentAddress }, queryParams: { ...filters } }),
(prevData: AddressTokenTransferResponse | undefined) => {
if (!prevData) {
return;
......
......@@ -39,13 +39,13 @@ test.beforeEach(async({ mockApiResponse }) => {
next_page_params: nextPageParams,
};
await mockApiResponse('address', addressMock.validator, { pathParams: { hash: ADDRESS_HASH } });
await mockApiResponse('address_tokens', response20, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-20' } });
await mockApiResponse('address_tokens', response721, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-721' } });
await mockApiResponse('address_tokens', response1155, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-1155' } });
await mockApiResponse('address_tokens', response404, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-404' } });
await mockApiResponse('address_nfts', tokensMock.nfts, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: [] } });
await mockApiResponse('address_collections', tokensMock.collections, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: [] } });
await mockApiResponse('general:address', addressMock.validator, { pathParams: { hash: ADDRESS_HASH } });
await mockApiResponse('general:address_tokens', response20, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-20' } });
await mockApiResponse('general:address_tokens', response721, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-721' } });
await mockApiResponse('general:address_tokens', response1155, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-1155' } });
await mockApiResponse('general:address_tokens', response404, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-404' } });
await mockApiResponse('general:address_nfts', tokensMock.nfts, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: [] } });
await mockApiResponse('general:address_collections', tokensMock.collections, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: [] } });
});
test('erc20 +@dark-mode', async({ render }) => {
......@@ -198,10 +198,18 @@ test.describe('update balances via socket', () => {
next_page_params: null,
};
const erc20ApiUrl = await mockApiResponse('address_tokens', response20, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-20' } });
const erc721ApiUrl = await mockApiResponse('address_tokens', response721, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-721' } });
const erc1155ApiUrl = await mockApiResponse('address_tokens', response1155, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-1155' } });
const erc404ApiUrl = await mockApiResponse('address_tokens', response404, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-404' } });
const erc20ApiUrl = await mockApiResponse('general:address_tokens', response20, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-20' } });
const erc721ApiUrl = await mockApiResponse('general:address_tokens', response721, { pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-721' } });
const erc1155ApiUrl = await mockApiResponse(
'general:address_tokens',
response1155,
{ pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-1155' } },
);
const erc404ApiUrl = await mockApiResponse(
'general:address_tokens',
response404,
{ pathParams: { hash: ADDRESS_HASH }, queryParams: { type: 'ERC-404' } },
);
const component = await render(
<Box pt={{ base: '134px', lg: 6 }}>
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
......@@ -21,11 +21,11 @@ const AddressUserOps = ({ scrollRef, shouldRender = true, isQueryEnabled = true
const hash = getQueryParamString(router.query.hash);
const userOpsQuery = useQueryWithPages({
resourceName: 'user_ops',
resourceName: 'general:user_ops',
scrollRef,
options: {
enabled: isQueryEnabled && Boolean(hash),
placeholderData: generateListStub<'user_ops'>(USER_OPS_ITEM, 50, { next_page_params: {
placeholderData: generateListStub<'general:user_ops'>(USER_OPS_ITEM, 50, { next_page_params: {
page_token: '10355938,0x5956a847d8089e254e02e5111cad6992b99ceb9e5c2dc4343fd53002834c4dc6',
page_size: 50,
} }),
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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