Commit c0b1964b authored by tom goriunov's avatar tom goriunov Committed by GitHub

Merge pull request #468 from blockscout/client-api-calls-pt2

call api from client side (part 2)
parents d9721a0f cb603dd3
......@@ -16,6 +16,7 @@ NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C
NEXT_PUBLIC_MARKETPLACE_APP_LIST=[{'author': 'Blockscout','id':'token-approval-tracker','title':'Token Approval Tracker','logo':'https://approval-tracker.vercel.app/icon-192.png','categories':['security','tools'],'shortDescription':'Token Approval Tracker shows all approvals for any ERC20-compliant tokens and NFTs and lets to revoke them or adjust the approved amount.','site':'https://docs.blockscout.com/for-users/blockscout-apps/token-approval-tracker','description':'Token Approval Tracker shows all approvals for any ERC20-compliant tokens and NFTs and lets to revoke them or adjust the approved amount.','url':'https://approval-tracker.vercel.app/'},{'author': 'Revoke','id':'revoke.cash','title':'Revoke.cash','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FVBMGyUFnd6CScjfK7CYQ%252Frevoke_sing.png%3Falt%3Dmedia%26token%3D9ab94986-7ab1-41c8-bf7e-d9ce11d23182','categories':['security','tools'],'shortDescription': 'Revoke.cash comes in as a preventative tool to manage your token allowances and practice proper wallet hygiene. By regularly revoking active allowances you reduce the chances of becoming the victim of allowance exploits.','site': 'https://revoke.cash/about','description': 'Revoke.cash comes in as a preventative tool to manage your token allowances and practice proper wallet hygiene. By regularly revoking active allowances you reduce the chances of becoming the victim of allowance exploits.','url':'https://revoke.cash/'},{'author':'Aave','id': 'aave','title': 'Aave','logo': 'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FrZkUTIUCG7Zx8BW6Em34%252FAave.png%3Falt%3Dmedia%26token%3D249797a4-4c1e-4372-9cd2-3e48e05e5f30','categories':['tools'],'shortDescription':'Aave is a decentralised non-custodial liquidity market protocol where users can participate as suppliers or borrowers. Suppliers provide liquidity to the market to earn a passive income, while borrowers are able to borrow in an overcollateralised (perpetually) or undercollateralised (one-block liquidity) fashion.','site': 'https://docs.aave.com/faq/','description': 'Aave is a decentralised non-custodial liquidity market protocol where users can participate as suppliers or borrowers. Suppliers provide liquidity to the market to earn a passive income, while borrowers are able to borrow in an overcollateralised (perpetually) or undercollateralised (one-block liquidity) fashion.','url': 'https://staging.aave.com/'},{'author':'LooksRare','id':'looksrare','external':true,'title':'LooksRare','logo': 'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FeAI4gy3qPMt68mZOZHAx%252FLooksRare.png%3Falt%3Dmedia%26token%3D44c01439-ae09-40aa-b904-3a9ce5b2e002','categories':['tools'],'shortDescription': 'LooksRare is the web3 NFT Marketplace where traders and collectors have earned over $1.3 Billion in rewards.','site':'https://docs.looksrare.org/about/welcome-to-looksrare','description':'LooksRare is the web3 NFT Marketplace where traders and collectors have earned over $1.3 Billion in rewards.','url': 'https://goerli.looksrare.org/'},{'author':'zkSync Bridge','id':'zksync-bridge','external':true,'title':'zkSync Bridge','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FrtQsaAz9BjGBc35tVAnq%252FzkSync.png%3Falt%3Dmedia%26token%3D5c18171c-8ccf-4a88-8f44-680cbf238115','categories':['security','tools'],'shortDescription':'zkSync 2.0 Goerli Bridge','site':'https://v2-docs.zksync.io/dev/','description':'zkSync 2.0 Goerli Bridge','url':'https://portal.zksync.io/bridge'},{'author':'dYdX','id':'dydx','external':true,'title':'dYdX','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FCrOglR72wpi0UhEscwe4%252Fdxdy.png%3Falt%3Dmedia%26token%3D8811909e-93e3-487c-9614-dffce37223e9','categories': ['security','tools'],'shortDescription':'dYdX is a leading decentralized exchange that currently supports perpetual trading. dYdX runs on smart contracts on the Ethereum blockchain, and allows users to trade with no intermediaries.','site':'https://help.dydx.exchange/en/articles/3047379-introduction-and-overview','description':'dYdX is a leading decentralized exchange that currently supports perpetual trading. dYdX runs on smart contracts on the Ethereum blockchain, and allows users to trade with no intermediaries.','url':'https://trade.stage.dydx.exchange/portfolio/overview'},{'author':'MetalSwap','id':'metalswap','title':'MetalSwap','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252F8xqldTvxb6avrwVVc3rS%252FMetalSwap.png%3Falt%3Dmedia%26token%3D92d2db99-853a-487d-8d8c-8cdeaeaaf014','categories':['security','tools'],'shortDescription':'MetalSwap is a decentralised platform that enables hedging swaps in financial markets with the aim of providing a hedge for commodity traders and an investment opportunity for those who contribute to the shared liquidity of the project.','site':'https://docs.metalswap.finance/','description':'MetalSwap is a decentralised platform that enables hedging swaps in financial markets with the aim of providing a hedge for commodity traders and an investment opportunity for those who contribute to the shared liquidity of the project.','url':'https://demo.metalswap.finance/'},{'author':'FaucetDao','id':'faucetdao','title':'FaucetDao','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252Ffnnt3ZNZhzRwqMM5YYjD%252FPlaceholder.png%3Falt%3Dmedia%26token%3D507571bb-d76f-4d96-a35e-2b278608f7ca','categories':['tools'],'shortDescription':'FaucetDao is a decentralised community fund providing liquidity and support to early-stage well vetted blockchain projects.','site':'https://linktr.ee/faucet_dao','description':'FaucetDao is a decentralised community fund providing liquidity and support to early-stage well vetted blockchain projects.','url':'https://www.faucetdao.shop/swap?chain=goerli'},{'author':'Uniswap','id':'uniswap','title':'Uniswap','logo':'https://www.gitbook.com/cdn-cgi/image/width=32,dpr=2.200000047683716,format=auto/https%3A%2F%2Ffiles.gitbook.com%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-Lq1XoWGmy8zggj_u2fM%252Fuploads%252FJc0QAyeaBmFIL97tSmGv%252FUniswap.png%3Falt%3Dmedia%26token%3D5d25d796-c273-4e22-92fa-ff85206bec76','categories':['tools'],'shortDescription':'Uniswap is a cryptocurrency exchange which uses a decentralized network protocol.','site':'https://docs.uniswap.org/','description':'Uniswap is a cryptocurrency exchange which uses a decentralized network protocol.','url':'https://app.uniswap.org/swap'}]
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
# api config
NEXT_PUBLIC_API_HOST=blockscout-main.test.aws-k8s.blockscout.com
......
async function rewrites() {
return [
{ source: '/node-api/proxy/:slug*', destination: '/api/proxy' },
{ source: '/node-api/:slug*', destination: '/api/:slug*' },
{ source: '/proxy/:slug*', destination: '/api/proxy' },
].filter(Boolean);
}
......
......@@ -6,8 +6,8 @@ import type { ApiResource } from './resources';
export default function buildUrl(
resource: ApiResource,
pathParams?: Record<string, string>,
queryParams?: Record<string, string | undefined>,
pathParams?: Record<string, string | undefined>,
queryParams?: Record<string, string | number | undefined>,
) {
// FIXME
// 1. I was not able to figure out how to send CORS with credentials from localhost
......@@ -24,11 +24,11 @@ export default function buildUrl(
const baseUrl = needProxy ? appConfig.baseUrl : (resource.endpoint || appConfig.api.endpoint);
const basePath = resource.basePath !== undefined ? resource.basePath : appConfig.api.basePath;
const path = needProxy ? '/proxy' + basePath + resource.path : basePath + resource.path;
const path = needProxy ? '/node-api/proxy' + basePath + resource.path : basePath + resource.path;
const url = new URL(compile(path)(pathParams), baseUrl);
queryParams && Object.entries(queryParams).forEach(([ key, value ]) => {
value && url.searchParams.append(key, value);
value && url.searchParams.append(key, String(value));
});
return url.toString();
......
import type { UserInfo, CustomAbis, PublicTags, AddressTags, TransactionTags, ApiKeys, WatchlistAddress } from 'types/api/account';
import type {
Address,
AddressCounters,
AddressTokenBalance,
AddressTransactionsResponse,
AddressTokenTransferResponse,
AddressCoinBalanceHistoryResponse,
AddressCoinBalanceHistoryChart,
AddressBlocksValidatedResponse,
AddressInternalTxsResponse,
AddressTxsFilters,
AddressTokenTransferFilters,
} from 'types/api/address';
import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters } from 'types/api/block';
import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/charts';
import type { IndexingStatus } from 'types/api/indexingStatus';
import type { InternalTransactionsResponse } from 'types/api/internalTransaction';
import type { JsonRpcUrlResponse } from 'types/api/jsonRpcUrl';
import type { LogsResponse } from 'types/api/log';
import type { RawTracesResponse } from 'types/api/rawTrace';
import type { Stats, Charts, HomeStats } from 'types/api/stats';
import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer';
import type { TransactionsResponseValidated, TransactionsResponsePending, Transaction } from 'types/api/transaction';
import type { TTxsFilters } from 'types/api/txsFilters';
import type ArrayElement from 'types/utils/ArrayElement';
import appConfig from 'configs/app/config';
export interface ApiResource {
......@@ -7,7 +34,7 @@ export interface ApiResource {
}
export const RESOURCES = {
// account
// ACCOUNT
user_info: {
path: '/api/account/v1/user/info',
},
......@@ -42,12 +69,132 @@ export const RESOURCES = {
basePath: appConfig.statsApi.basePath,
},
// BLOCKS, TXS
blocks: {
path: '/api/v2/blocks',
paginationFields: [ 'block_number' as const, 'items_count' as const ],
filterFields: [ 'type' as const ],
},
block: {
path: '/api/v2/blocks/:id',
},
block_txs: {
path: '/api/v2/blocks/:id/transactions',
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const ],
filterFields: [],
},
txs_validated: {
path: '/api/v2/transactions',
paginationFields: [ 'block_number' as const, 'items_count' as const, 'filter' as const, 'index' as const ],
filterFields: [ 'filter' as const, 'type' as const, 'method' as const ],
},
txs_pending: {
path: '/api/v2/transactions',
paginationFields: [ 'filter' as const, 'hash' as const, 'inserted_at' as const ],
filterFields: [ 'filter' as const, 'type' as const, 'method' as const ],
},
tx: {
path: '/api/v2/transactions/:id',
},
tx_internal_txs: {
path: '/api/v2/transactions/:id/internal-transactions',
paginationFields: [ 'block_number' as const, 'items_count' as const, 'transaction_hash' as const, 'index' as const, 'transaction_index' as const ],
filterFields: [ ],
},
tx_logs: {
path: '/api/v2/transactions/:id/logs',
paginationFields: [ 'items_count' as const, 'transaction_hash' as const, 'index' as const ],
filterFields: [ ],
},
tx_token_transfers: {
path: '/api/v2/transactions/:id/token-transfers',
paginationFields: [ 'block_number' as const, 'items_count' as const, 'transaction_hash' as const, 'index' as const ],
filterFields: [ 'type' as const ],
},
tx_raw_trace: {
path: '/api/v2/transactions/:id/raw-trace',
},
// ADDRESS
address: {
path: '/api/v2/addresses/:id',
},
address_counters: {
path: '/api/v2/addresses/:id/counters',
},
address_token_balances: {
path: '/api/v2/addresses/:id/token-balances',
},
address_txs: {
path: '/api/v2/addresses/:id/transactions',
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const ],
filterFields: [ 'filter' as const ],
},
address_internal_txs: {
path: '/api/v2/addresses/:id/internal-transactions',
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const, 'transaction_index' as const ],
filterFields: [ 'filter' as const ],
},
address_token_transfers: {
path: '/api/v2/addresses/:id/token-transfers',
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const, 'transaction_index' as const ],
filterFields: [ 'filter' as const, 'type' as const ],
},
address_blocks_validated: {
path: '/api/v2/addresses/:id/blocks-validated',
paginationFields: [ 'items_count' as const, 'block_number' as const ],
filterFields: [ ],
},
address_coin_balance: {
path: '/api/v2/addresses/:id/coin-balance-history',
paginationFields: [ 'items_count' as const, 'block_number' as const ],
filterFields: [ ],
},
address_coin_balance_chart: {
path: '/api/v2/addresses/:id/coin-balance-history-by-day',
},
// HOMEPAGE
homepage_stats: {
path: '/api/v2/stats',
},
homepage_chart_txs: {
path: '/api/v2/stats/charts/transactions',
},
homepage_chart_market: {
path: '/api/v2/stats/charts/market',
},
homepage_blocks: {
path: '/api/v2/main-page/blocks',
},
homepage_txs: {
path: '/api/v2/main-page/transactions',
},
homepage_indexing_status: {
path: '/api/v2/main-page/indexing-status',
},
// CONFIG
config_json_rpc: {
path: '/api/v2/config/json-rpc-url',
},
// DEPRECATED
old_api: {
path: '/api',
},
};
export type ResourceName = keyof typeof RESOURCES;
export type ResourceFiltersKey<R extends ResourceName> = typeof RESOURCES[R] extends {filterFields: Array<unknown>} ?
ArrayElement<typeof RESOURCES[R]['filterFields']> :
never;
export type ResourcePaginationKey<R extends ResourceName> = typeof RESOURCES[R] extends {paginationFields: Array<unknown>} ?
ArrayElement<typeof RESOURCES[R]['paginationFields']> :
never;
export const resourceKey = (x: keyof typeof RESOURCES) => x;
export interface ResourceError<T = unknown> {
......@@ -58,3 +205,60 @@ export interface ResourceError<T = unknown> {
}
export type ResourceErrorAccount<T> = ResourceError<{ errors: T }>
export type PaginatedResources = 'blocks' | 'block_txs' |
'txs_validated' | 'txs_pending' |
'tx_internal_txs' | 'tx_logs' | 'tx_token_transfers' |
'address_txs' | 'address_internal_txs' | 'address_token_transfers' | 'address_blocks_validated' | 'address_coin_balance';
export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>;
/* eslint-disable @typescript-eslint/indent */
export type ResourcePayload<Q extends ResourceName> =
Q extends 'user_info' ? UserInfo :
Q extends 'custom_abi' ? CustomAbis :
Q extends 'public_tags' ? PublicTags :
Q extends 'private_tags_address' ? AddressTags :
Q extends 'private_tags_tx' ? TransactionTags :
Q extends 'api_keys' ? ApiKeys :
Q extends 'watchlist' ? Array<WatchlistAddress> :
Q extends 'homepage_stats' ? HomeStats :
Q extends 'homepage_chart_txs' ? ChartTransactionResponse :
Q extends 'homepage_chart_market' ? ChartMarketResponse :
Q extends 'homepage_blocks' ? Array<Block> :
Q extends 'homepage_txs' ? Array<Transaction> :
Q extends 'homepage_indexing_status' ? IndexingStatus :
Q extends 'stats_counters' ? Stats :
Q extends 'stats_charts' ? Charts :
Q extends 'blocks' ? BlocksResponse :
Q extends 'block' ? Block :
Q extends 'block_txs' ? BlockTransactionsResponse :
Q extends 'txs_validated' ? TransactionsResponseValidated :
Q extends 'txs_pending' ? TransactionsResponsePending :
Q extends 'tx' ? Transaction :
Q extends 'tx_internal_txs' ? InternalTransactionsResponse :
Q extends 'tx_logs' ? LogsResponse :
Q extends 'tx_token_transfers' ? TokenTransferResponse :
Q extends 'tx_raw_trace' ? RawTracesResponse :
Q extends 'address' ? Address :
Q extends 'address_counters' ? AddressCounters :
Q extends 'address_token_balances' ? Array<AddressTokenBalance> :
Q extends 'address_txs' ? AddressTransactionsResponse :
Q extends 'address_internal_txs' ? AddressInternalTxsResponse :
Q extends 'address_token_transfers' ? AddressTokenTransferResponse :
Q extends 'address_blocks_validated' ? AddressBlocksValidatedResponse :
Q extends 'address_coin_balance' ? AddressCoinBalanceHistoryResponse :
Q extends 'address_coin_balance_chart' ? AddressCoinBalanceHistoryChart :
Q extends 'config_json_rpc' ? JsonRpcUrlResponse :
never;
/* eslint-enable @typescript-eslint/indent */
/* eslint-disable @typescript-eslint/indent */
export type PaginationFilters<Q extends PaginatedResources> =
Q extends 'blocks' ? BlockFilters :
Q extends 'txs_validated' | 'txs_pending' ? TTxsFilters :
Q extends 'tx_token_transfers' ? TokenTransferFilters :
Q extends 'address_txs' | 'address_internal_txs' ? AddressTxsFilters :
Q extends 'address_token_transfers' ? AddressTokenTransferFilters :
never;
/* eslint-enable @typescript-eslint/indent */
......@@ -9,8 +9,8 @@ import { RESOURCES } from './resources';
import type { ResourceError, ApiResource } from './resources';
export interface Params {
pathParams?: Record<string, string>;
queryParams?: Record<string, string | undefined>;
pathParams?: Record<string, string | undefined>;
queryParams?: Record<string, string | number | undefined>;
fetchParams?: Pick<FetchParams, 'body' | 'method'>;
}
......
import type { UseQueryOptions } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import type { UserInfo, CustomAbis, PublicTags, AddressTags, TransactionTags, ApiKeys, WatchlistAddress } from 'types/api/account';
import type { Stats, Charts } from 'types/api/stats';
import type { RESOURCES, ResourceError } from './resources';
import type { ResourceError, ResourceName, ResourcePayload } from './resources';
import type { Params as ApiFetchParams } from './useApiFetch';
import useApiFetch from './useApiFetch';
interface Params<R extends keyof typeof RESOURCES> extends ApiFetchParams {
queryOptions?: Omit<UseQueryOptions<unknown, ResourceError, ResourcePayload<R>>, 'queryKey' | 'queryFn'>;
export interface Params<R extends ResourceName, E = unknown> extends ApiFetchParams {
queryOptions?: Omit<UseQueryOptions<unknown, ResourceError<E>, ResourcePayload<R>>, 'queryKey' | 'queryFn'>;
}
export default function useApiQuery<R extends keyof typeof RESOURCES>(
export function getResourceKey<R extends ResourceName>(resource: R, { pathParams, queryParams }: Params<R> = {}) {
if (pathParams || queryParams) {
return [ resource, { ...pathParams, ...queryParams } ];
}
return [ resource ];
}
export default function useApiQuery<R extends ResourceName, E = unknown>(
resource: R,
{ queryOptions, pathParams, queryParams, fetchParams }: Params<R> = {},
{ queryOptions, pathParams, queryParams, fetchParams }: Params<R, E> = {},
) {
const apiFetch = useApiFetch();
return useQuery<unknown, ResourceError, ResourcePayload<R>>(
pathParams || queryParams ? [ resource, { ...pathParams, ...queryParams } ] : [ resource ],
return useQuery<unknown, ResourceError<E>, ResourcePayload<R>>(
getResourceKey(resource, { pathParams, queryParams }),
async() => {
return apiFetch<R, ResourcePayload<R>, ResourceError>(resource, { pathParams, queryParams, fetchParams });
}, queryOptions);
}
export type ResourcePayload<Q extends keyof typeof RESOURCES> =
Q extends 'user_info' ? UserInfo :
Q extends 'custom_abi' ? CustomAbis :
Q extends 'public_tags' ? PublicTags :
Q extends 'private_tags_address' ? AddressTags :
Q extends 'private_tags_tx' ? TransactionTags :
Q extends 'api_keys' ? ApiKeys :
Q extends 'watchlist' ? Array<WatchlistAddress> :
Q extends 'stats_counters' ? Stats :
Q extends 'stats_charts' ? Charts :
never;
export default function getFilterValue<FilterType>(filterValues: ReadonlyArray<FilterType>, val: string | Array<string> | undefined) {
if (val === undefined) {
return;
}
const valArray = [];
if (typeof val === 'string') {
valArray.push(...val.split(','));
......
import type { UseQueryOptions } from '@tanstack/react-query';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { pick, omit } from 'lodash';
import { useQueryClient } from '@tanstack/react-query';
import mapValues from 'lodash/mapValues';
import omit from 'lodash/omit';
import pick from 'lodash/pick';
import { useRouter } from 'next/router';
import React, { useCallback } from 'react';
import { animateScroll, scroller } from 'react-scroll';
import { PAGINATION_FIELDS, PAGINATION_FILTERS_FIELDS } from 'types/api/pagination';
import type { PaginationParams, PaginatedResponse, PaginatedQueryKeys, PaginationFilters } from 'types/api/pagination';
import type { PaginatedResources, PaginatedResponse, PaginationFilters } from 'lib/api/resources';
import { RESOURCES } from 'lib/api/resources';
import type { Params as UseApiQueryParams } from 'lib/api/useApiQuery';
import useApiQuery from 'lib/api/useApiQuery';
import useFetch from 'lib/hooks/useFetch';
interface Params<QueryName extends PaginatedQueryKeys> {
apiPath: string;
queryName: QueryName;
queryIds?: Array<string>;
filters?: PaginationFilters<QueryName>;
options?: Omit<UseQueryOptions<unknown, unknown, PaginatedResponse<QueryName>>, 'queryKey' | 'queryFn'>;
interface Params<Resource extends PaginatedResources> {
resourceName: Resource;
options?: UseApiQueryParams<Resource>['queryOptions'];
pathParams?: UseApiQueryParams<Resource>['pathParams'];
filters?: PaginationFilters<Resource>;
scroll?: { elem: string; offset: number };
}
export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>({
queryName,
export default function useQueryWithPages<Resource extends PaginatedResources>({
resourceName,
filters,
options,
apiPath,
queryIds,
pathParams,
scroll,
}: Params<QueryName>) {
const paginationFields = PAGINATION_FIELDS[queryName];
}: Params<Resource>) {
const resource = RESOURCES[resourceName];
const queryClient = useQueryClient();
const router = useRouter();
type NextPageParams = {
[K in keyof PaginatedResponse<Resource>['next_page_params']]: string;
}
const currPageParams = mapValues(pick(router.query, resource.paginationFields), (value) => value?.toString()) as NextPageParams;
const [ page, setPage ] = React.useState<number>(router.query.page && !Array.isArray(router.query.page) ? Number(router.query.page) : 1);
const currPageParams = pick(router.query, paginationFields);
const [ pageParams, setPageParams ] = React.useState<Array<PaginationParams<QueryName>>>([ ]);
const fetch = useFetch();
const isMounted = React.useRef(false);
const canGoBackwards = React.useRef(!router.query.page);
const [ pageParams, setPageParams ] = React.useState<Record<number, NextPageParams>>({
[page]: currPageParams,
});
const [ hasPagination, setHasPagination ] = React.useState(page > 1);
const queryKey = [ queryName, ...(queryIds || []), { page, filters } ];
const isMounted = React.useRef(false);
const canGoBackwards = React.useRef(!router.query.page);
const queryParams = { ...filters, ...pageParams[page] };
const scrollToTop = useCallback(() => {
scroll ? scroller.scrollTo(scroll.elem, { offset: scroll.offset }) : animateScroll.scrollToTop({ duration: 0 });
}, [ scroll ]);
const queryResult = useQuery<unknown, unknown, PaginatedResponse<QueryName>>(
queryKey,
async() => {
const params: Array<string> = [];
Object.entries({ ...filters, ...currPageParams }).forEach(([ key, val ]) => {
if (Array.isArray(val)) {
val.length && params.push(`${ key }=${ val.join(',') }`);
} else if (val) {
params.push(`${ key }=${ val }`);
}
});
return fetch(`${ apiPath }${ params.length ? '?' + params.join('&') : '' }`);
const queryResult = useApiQuery(resourceName, {
pathParams,
queryParams,
queryOptions: {
staleTime: page === 1 ? 0 : Infinity,
...options,
},
{ staleTime: page === 1 ? 0 : Infinity, ...options },
);
});
const { data } = queryResult;
const onNextPageClick = useCallback(() => {
......@@ -69,9 +65,13 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>(
return;
}
const nextPageParams = data.next_page_params;
if (page >= pageParams.length && data?.next_page_params) {
setPageParams(prev => [ ...prev, nextPageParams ]);
}
setPageParams((prev) => ({
...prev,
[page + 1]: mapValues(nextPageParams, (value) => String(value)) as NextPageParams,
}));
setPage(prev => prev + 1);
const nextPageQuery = { ...router.query };
Object.entries(nextPageParams).forEach(([ key, val ]) => nextPageQuery[key] = val.toString());
nextPageQuery.page = String(page + 1);
......@@ -80,53 +80,52 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>(
router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true })
.then(() => {
scrollToTop();
setPage(prev => prev + 1);
});
}, [ data?.next_page_params, page, pageParams.length, router, scrollToTop ]);
}, [ data?.next_page_params, page, router, scrollToTop ]);
const onPrevPageClick = useCallback(() => {
// returning to the first page
// we dont have pagination params for the first page
let nextPageQuery: typeof router.query = { ...router.query };
if (page === 2) {
nextPageQuery = omit(router.query, paginationFields, 'page');
nextPageQuery = omit(router.query, resource.paginationFields, 'page');
canGoBackwards.current = true;
} else {
const nextPageParams = { ...pageParams[page - 2] };
nextPageParams && Object.entries(nextPageParams).forEach(([ key, val ]) => nextPageQuery[key] = val.toString());
const nextPageParams = pageParams[page - 1];
nextPageParams && Object.entries(nextPageParams).forEach(([ key, val ]) => nextPageQuery[key] = String(val));
nextPageQuery.page = String(page - 1);
}
router.query = nextPageQuery;
router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true })
.then(() => {
scrollToTop();
setPage(prev => prev - 1);
page === 2 && queryClient.removeQueries({ queryKey: [ queryName ] });
page === 2 && queryClient.removeQueries({ queryKey: [ resourceName ] });
});
setHasPagination(true);
}, [ router, page, paginationFields, pageParams, queryClient, scrollToTop, queryName ]);
}, [ router, page, resource.paginationFields, pageParams, scrollToTop, queryClient, resourceName ]);
const resetPage = useCallback(() => {
queryClient.removeQueries({ queryKey: [ queryName ] });
queryClient.removeQueries({ queryKey: [ resourceName ] });
router.push({ pathname: router.pathname, query: omit(router.query, paginationFields, 'page') }, undefined, { shallow: true }).then(() => {
queryClient.removeQueries({ queryKey: [ queryName ] });
router.push({ pathname: router.pathname, query: omit(router.query, resource.paginationFields, 'page') }, undefined, { shallow: true }).then(() => {
queryClient.removeQueries({ queryKey: [ resourceName ] });
scrollToTop();
setPage(1);
setPageParams([ ]);
setPageParams({});
canGoBackwards.current = true;
window.setTimeout(() => {
// FIXME after router is updated we still have inactive queries for previously visited page (e.g third), where we came from
// so have to remove it but with some delay :)
queryClient.removeQueries({ queryKey: [ queryName ], type: 'inactive' });
queryClient.removeQueries({ queryKey: [ resourceName ], type: 'inactive' });
}, 100);
});
setHasPagination(true);
}, [ queryClient, queryName, router, paginationFields, scrollToTop ]);
}, [ queryClient, resourceName, router, resource.paginationFields, scrollToTop ]);
const onFilterChange = useCallback((newFilters: PaginationFilters<QueryName> | undefined) => {
const newQuery = omit(router.query, PAGINATION_FIELDS[queryName], 'page', PAGINATION_FILTERS_FIELDS[queryName]);
const onFilterChange = useCallback((newFilters: PaginationFilters<Resource> | undefined) => {
const newQuery = omit(router.query, resource.paginationFields, 'page', resource.filterFields);
if (newFilters) {
Object.entries(newFilters).forEach(([ key, value ]) => {
if (value && value.length) {
......@@ -143,12 +142,12 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>(
{ shallow: true },
).then(() => {
setPage(1);
setPageParams([ ]);
setPageParams({});
scrollToTop();
});
}, [ queryName, router, scrollToTop, setPageParams, setPage ]);
}, [ router, resource.paginationFields, resource.filterFields, scrollToTop ]);
const hasPaginationParams = Object.keys(currPageParams).length > 0;
const hasPaginationParams = Object.keys(currPageParams || {}).length > 0;
const nextPageParams = data?.next_page_params;
const pagination = {
......@@ -165,12 +164,12 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>(
React.useEffect(() => {
if (page !== 1 && isMounted.current) {
queryClient.cancelQueries({ queryKey });
queryClient.cancelQueries({ queryKey: [ resourceName ] });
setPage(1);
}
// hook should run only when queryName has changed
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ queryName ]);
}, [ resourceName ]);
React.useEffect(() => {
window.setTimeout(() => {
......
import type { NextApiRequest } from 'next';
import getSearchParams from 'lib/api/getSearchParams';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
const searchParamsStr = getSearchParams(req);
return `/v2/addresses/${ req.query.id }/blocks-validated${ searchParamsStr ? '?' + searchParamsStr : '' }`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import type { NextApiRequest } from 'next';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
return `/v2/addresses/${ req.query.id }/coin-balance-history-by-day`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import type { NextApiRequest } from 'next';
import getSearchParams from 'lib/api/getSearchParams';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
const searchParamsStr = getSearchParams(req);
return `/v2/addresses/${ req.query.id }/coin-balance-history${ searchParamsStr ? '?' + searchParamsStr : '' }`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import type { NextApiRequest } from 'next';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
return `/v2/addresses/${ req.query.id }/counters`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import type { NextApiRequest } from 'next';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
return `/v2/addresses/${ req.query.id }`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import type { NextApiRequest } from 'next';
import getSearchParams from 'lib/api/getSearchParams';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
const searchParamsStr = getSearchParams(req);
return `/v2/addresses/${ req.query.id }/internal-transactions${ searchParamsStr ? '?' + searchParamsStr : '' }`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import type { NextApiRequest } from 'next';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
return `/v2/addresses/${ req.query.id }/token-balances`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import type { NextApiRequest } from 'next';
import getSearchParams from 'lib/api/getSearchParams';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
const searchParamsStr = getSearchParams(req);
return `/v2/addresses/${ req.query.id }/token-transfers${ searchParamsStr ? '?' + searchParamsStr : '' }`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import type { NextApiRequest } from 'next';
import getSearchParams from 'lib/api/getSearchParams';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
const searchParamsStr = getSearchParams(req);
return `/v2/addresses/${ req.query.id }/transactions${ searchParamsStr ? '?' + searchParamsStr : '' }`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import type { NextApiRequest } from 'next';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
return `/v2/blocks/${ req.query.id }`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import type { NextApiRequest } from 'next';
import getSearchParams from 'lib/api/getSearchParams';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
const searchParamsStr = getSearchParams(req);
return `/v2/blocks/${ req.query.id }/transactions${ searchParamsStr ? '?' + searchParamsStr : '' }`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import type { NextApiRequest } from 'next';
import getSearchParams from 'lib/api/getSearchParams';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
const searchParamsStr = getSearchParams(req);
return `/v2/blocks${ searchParamsStr ? '?' + searchParamsStr : '' }`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import handler from 'lib/api/handler';
const getUrl = () => {
return `/v2/config/json-rpc-url`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import handler from 'lib/api/handler';
const getUrl = () => '/v2/stats/charts/market';
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import handler from 'lib/api/handler';
const getUrl = () => '/v2/stats/charts/transactions';
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import handler from 'lib/api/handler';
const getUrl = () => '/v2/stats';
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import handler from 'lib/api/handler';
const getUrl = () => '/v2/main-page/blocks';
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import handler from 'lib/api/handler';
const getUrl = () => '/v2/main-page/indexing-status';
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import handler from 'lib/api/handler';
const getUrl = () => '/v2/main-page/transactions';
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
......@@ -11,7 +11,7 @@ const handler = async(_req: NextApiRequest, res: NextApiResponse) => {
}
const response = await fetchFactory(_req, _req.headers['x-endpoint']?.toString())(
_req.url.replace(/^\/proxy/, ''),
_req.url.replace(/^\/node-api\/proxy/, ''),
_pickBy(_pick(_req, [ 'body', 'method' ]), Boolean),
);
......
import type { NextApiRequest } from 'next';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
return `/v2/transactions/${ req.query.id }`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import type { NextApiRequest } from 'next';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
return `/v2/transactions/${ req.query.id }/internal-transactions`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import type { NextApiRequest } from 'next';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
return `/v2/transactions/${ req.query.id }/logs`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import type { NextApiRequest } from 'next';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
return `/v2/transactions/${ req.query.id }/raw-trace`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import type { NextApiRequest } from 'next';
import getSearchParams from 'lib/api/getSearchParams';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
const searchParamsStr = getSearchParams(req);
return `/v2/transactions/${ req.query.id }/token-transfers${ searchParamsStr ? '?' + searchParamsStr : '' }`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import type { NextApiRequest } from 'next';
import getSearchParams from 'lib/api/getSearchParams';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
const searchParamsStr = getSearchParams(req);
return `/v2/transactions/${ searchParamsStr ? '?' + searchParamsStr : '' }`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import { compile } from 'path-to-regexp';
import type { ResourceName } from 'lib/api/resources';
import { RESOURCES } from 'lib/api/resources';
export default function buildApiUrl(resourceName: ResourceName, pathParams?: Record<string, string>) {
const resource = RESOURCES[resourceName];
return compile('/node-api/proxy/poa/core' + resource.path)(pathParams);
}
import type {
AddressTransactionsResponse,
AddressTokenTransferResponse,
AddressTxsFilters,
AddressTokenTransferFilters,
AddressCoinBalanceHistoryResponse,
AddressBlocksValidatedResponse,
AddressInternalTxsResponse,
} from 'types/api/address';
import type { BlocksResponse, BlockTransactionsResponse, BlockFilters } from 'types/api/block';
import type { InternalTransactionsResponse } from 'types/api/internalTransaction';
import type { LogsResponse } from 'types/api/log';
import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer';
import type { TransactionsResponseValidated, TransactionsResponsePending } from 'types/api/transaction';
import type { TTxsFilters } from 'types/api/txsFilters';
import { QueryKeys } from 'types/client/queries';
import type { KeysOfObjectOrNull } from 'types/utils/KeysOfObjectOrNull';
export type PaginatedQueryKeys =
QueryKeys.addressTxs |
QueryKeys.addressTokenTransfers |
QueryKeys.addressInternalTxs |
QueryKeys.blocks |
QueryKeys.blocksReorgs |
QueryKeys.blocksUncles |
QueryKeys.blockTxs |
QueryKeys.txsValidate |
QueryKeys.txsPending |
QueryKeys.txInternals |
QueryKeys.txLogs |
QueryKeys.txTokenTransfers |
QueryKeys.addressCoinBalanceHistory |
QueryKeys.addressBlocksValidated;
export type PaginatedResponse<Q extends PaginatedQueryKeys> =
Q extends QueryKeys.addressInternalTxs ? AddressInternalTxsResponse :
Q extends QueryKeys.addressTxs ? AddressTransactionsResponse :
Q extends QueryKeys.addressTokenTransfers ? AddressTokenTransferResponse :
Q extends (QueryKeys.blocks | QueryKeys.blocksReorgs | QueryKeys.blocksUncles) ? BlocksResponse :
Q extends QueryKeys.blockTxs ? BlockTransactionsResponse :
Q extends QueryKeys.txsValidate ? TransactionsResponseValidated :
Q extends QueryKeys.txsPending ? TransactionsResponsePending :
Q extends QueryKeys.txInternals ? InternalTransactionsResponse :
Q extends QueryKeys.txLogs ? LogsResponse :
Q extends QueryKeys.txTokenTransfers ? TokenTransferResponse :
Q extends QueryKeys.addressCoinBalanceHistory ? AddressCoinBalanceHistoryResponse :
Q extends QueryKeys.addressBlocksValidated ? AddressBlocksValidatedResponse :
never
export type PaginationFilters<Q extends PaginatedQueryKeys> =
Q extends (QueryKeys.addressTxs | QueryKeys.addressInternalTxs) ? AddressTxsFilters :
Q extends QueryKeys.addressTokenTransfers ? AddressTokenTransferFilters :
Q extends QueryKeys.blocks ? BlockFilters :
Q extends QueryKeys.txsValidate ? TTxsFilters :
Q extends QueryKeys.txsPending ? TTxsFilters :
Q extends QueryKeys.txTokenTransfers ? TokenTransferFilters :
never
export type PaginationParams<Q extends PaginatedQueryKeys> = PaginatedResponse<Q>['next_page_params'];
type PaginationFields = {
[K in PaginatedQueryKeys]: Array<KeysOfObjectOrNull<PaginatedResponse<K>['next_page_params']>>
}
export const PAGINATION_FIELDS: PaginationFields = {
[QueryKeys.addressTxs]: [ 'block_number', 'items_count', 'index' ],
[QueryKeys.addressInternalTxs]: [ 'block_number', 'items_count', 'index', 'transaction_index' ],
[QueryKeys.addressTokenTransfers]: [ 'block_number', 'items_count', 'index', 'transaction_hash' ],
[QueryKeys.blocks]: [ 'block_number', 'items_count' ],
[QueryKeys.blocksReorgs]: [ 'block_number', 'items_count' ],
[QueryKeys.blocksUncles]: [ 'block_number', 'items_count' ],
[QueryKeys.blockTxs]: [ 'block_number', 'items_count', 'index' ],
[QueryKeys.txsValidate]: [ 'block_number', 'items_count', 'filter', 'index' ],
[QueryKeys.txsPending]: [ 'filter', 'hash', 'inserted_at' ],
[QueryKeys.txInternals]: [ 'block_number', 'items_count', 'transaction_hash', 'index', 'transaction_index' ],
[QueryKeys.txTokenTransfers]: [ 'block_number', 'items_count', 'transaction_hash', 'index' ],
[QueryKeys.txLogs]: [ 'items_count', 'transaction_hash', 'index' ],
[QueryKeys.addressCoinBalanceHistory]: [ 'items_count', 'block_number' ],
[QueryKeys.addressBlocksValidated]: [ 'items_count', 'block_number' ],
};
type PaginationFiltersFields = {
[K in PaginatedQueryKeys]: Array<KeysOfObjectOrNull<PaginationFilters<K>>>
}
export const PAGINATION_FILTERS_FIELDS: PaginationFiltersFields = {
[QueryKeys.addressTxs]: [ 'filter' ],
[QueryKeys.addressInternalTxs]: [ 'filter' ],
[QueryKeys.addressTokenTransfers]: [ 'filter', 'type' ],
[QueryKeys.addressCoinBalanceHistory]: [],
[QueryKeys.addressBlocksValidated]: [],
[QueryKeys.blocks]: [ 'type' ],
[QueryKeys.txsValidate]: [ 'filter', 'type', 'method' ],
[QueryKeys.txsPending]: [ 'filter', 'type', 'method' ],
[QueryKeys.txTokenTransfers]: [ 'type' ],
[QueryKeys.blocksReorgs]: [],
[QueryKeys.blocksUncles]: [],
[QueryKeys.blockTxs]: [],
[QueryKeys.txInternals]: [],
[QueryKeys.txLogs]: [],
};
export enum QueryKeys {
csrf = 'csrf',
profile = 'profile',
txsValidate = 'txs-validated',
txsPending = 'txs-pending',
homeStats='homeStats',
indexingStatus='indexingStatus',
stats='stats',
charts='stats',
tx = 'tx',
txInternals = 'tx-internals',
txLogs = 'tx-logs',
txRawTrace = 'tx-raw-trace',
txTokenTransfers = 'tx-token-transfers',
blockTxs = 'block-transactions',
block = 'block',
blocks = 'blocks',
blocksReorgs = 'blocks-reorgs',
blocksUncles = 'blocks-uncles',
chartsTxs = 'charts-txs',
chartsMarket = 'charts-market',
indexBlocks='indexBlocks',
indexTxs='indexTxs',
jsonRpcUrl='json-rpc-url',
address='address',
addressCounters='address-counters',
addressTokenBalances='address-token-balances',
addressCoinBalanceHistory='address-coin-balance-history',
addressCoinBalanceHistoryByDay='address-coin-balance-history-by-day',
addressTxs='addressTxs',
addressTokenTransfers='addressTokenTransfers',
addressBlocksValidated='address-blocks-validated',
addressInternalTxs='address-internal-txs',
}
......@@ -5,9 +5,9 @@ import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { Address, AddressBlocksValidatedResponse } from 'types/api/address';
import { QueryKeys } from 'types/client/queries';
import appConfig from 'configs/app/config';
import { getResourceKey } from 'lib/api/useApiQuery';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
......@@ -31,8 +31,8 @@ const AddressBlocksValidated = ({ addressQuery }: Props) => {
const queryClient = useQueryClient();
const query = useQueryWithPages({
apiPath: `/node-api/addresses/${ addressQuery.data?.hash }/blocks-validated`,
queryName: QueryKeys.addressBlocksValidated,
resourceName: 'address_blocks_validated',
pathParams: { id: addressQuery.data?.hash },
options: {
enabled: Boolean(addressQuery.data),
},
......@@ -46,7 +46,7 @@ const AddressBlocksValidated = ({ addressQuery }: Props) => {
setSocketAlert(false);
queryClient.setQueryData(
[ QueryKeys.addressBlocksValidated, { page: query.pagination.page } ],
getResourceKey('address_blocks_validated', { pathParams: { id: addressQuery.data?.hash } }),
(prevData: AddressBlocksValidatedResponse | undefined) => {
if (!prevData) {
return;
......@@ -57,7 +57,7 @@ const AddressBlocksValidated = ({ addressQuery }: Props) => {
items: [ payload.block, ...prevData.items ],
};
});
}, [ query.pagination.page, queryClient ]);
}, [ addressQuery.data?.hash, queryClient ]);
const channel = useSocketChannel({
topic: `blocks:${ addressQuery.data?.hash.toLowerCase() }`,
......
......@@ -4,8 +4,8 @@ import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { Address, AddressCoinBalanceHistoryResponse } from 'types/api/address';
import { QueryKeys } from 'types/client/queries';
import { getResourceKey } from 'lib/api/useApiQuery';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
......@@ -23,8 +23,8 @@ const AddressCoinBalance = ({ addressQuery }: Props) => {
const queryClient = useQueryClient();
const coinBalanceQuery = useQueryWithPages({
apiPath: `/node-api/addresses/${ addressQuery.data?.hash }/coin-balance-history`,
queryName: QueryKeys.addressCoinBalanceHistory,
resourceName: 'address_coin_balance',
pathParams: { id: addressQuery.data?.hash },
options: {
enabled: Boolean(addressQuery.data),
},
......@@ -38,7 +38,7 @@ const AddressCoinBalance = ({ addressQuery }: Props) => {
setSocketAlert(false);
queryClient.setQueryData(
[ QueryKeys.addressCoinBalanceHistory, { page: coinBalanceQuery.pagination.page } ],
getResourceKey('address_coin_balance', { pathParams: { id: addressQuery.data?.hash } }),
(prevData: AddressCoinBalanceHistoryResponse | undefined) => {
if (!prevData) {
return;
......@@ -52,7 +52,7 @@ const AddressCoinBalance = ({ addressQuery }: Props) => {
],
};
});
}, [ coinBalanceQuery.pagination.page, queryClient ]);
}, [ addressQuery.data?.hash, queryClient ]);
const channel = useSocketChannel({
topic: `addresses:${ addressQuery.data?.hash.toLowerCase() }`,
......
import { Box, Flex, Text, Icon, Grid, Link } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import BigNumber from 'bignumber.js';
import { useRouter } from 'next/router';
import React from 'react';
import type { Address as TAddress, AddressCounters, AddressTokenBalance } from 'types/api/address';
import { QueryKeys } from 'types/client/queries';
import type { Address as TAddress } from 'types/api/address';
import appConfig from 'configs/app/config';
import blockIcon from 'icons/block.svg';
import useFetch from 'lib/hooks/useFetch';
import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import link from 'lib/link/link';
import AddressIcon from 'ui/shared/address/AddressIcon';
......@@ -35,24 +33,20 @@ interface Props {
const AddressDetails = ({ addressQuery }: Props) => {
const router = useRouter();
const fetch = useFetch();
const isMobile = useIsMobile();
const countersQuery = useQuery<unknown, unknown, AddressCounters>(
[ QueryKeys.addressCounters, router.query.id ],
async() => await fetch(`/node-api/addresses/${ router.query.id }/counters`),
{
const countersQuery = useApiQuery('address_counters', {
pathParams: { id: router.query.id?.toString() },
queryOptions: {
enabled: Boolean(router.query.id) && Boolean(addressQuery.data),
},
);
const tokenBalancesQuery = useQuery<unknown, unknown, Array<AddressTokenBalance>>(
[ QueryKeys.addressTokenBalances, router.query.id ],
async() => await fetch(`/node-api/addresses/${ router.query.id }/token-balances`),
{
});
const tokenBalancesQuery = useApiQuery('address_token_balances', {
pathParams: { id: router.query.id?.toString() },
queryOptions: {
enabled: Boolean(router.query.id) && Boolean(addressQuery.data),
},
);
});
if (countersQuery.isLoading || addressQuery.isLoading || tokenBalancesQuery.isLoading) {
return <AddressDetailsSkeleton/>;
......
......@@ -4,11 +4,12 @@ import React from 'react';
import * as internalTxsMock from 'mocks/txs/internalTxs';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import AddressInternalTxs from './AddressInternalTxs';
const ADDRESS_HASH = internalTxsMock.base.from.hash;
const API_URL_TX_INTERNALS = `/node-api/addresses/${ ADDRESS_HASH }/internal-transactions`;
const API_URL_TX_INTERNALS = buildApiUrl('address_internal_txs', { id: ADDRESS_HASH });
const hooksConfig = {
router: {
query: { id: ADDRESS_HASH },
......@@ -29,7 +30,5 @@ test('base view +@mobile', async({ mount, page }) => {
{ hooksConfig },
);
await page.waitForResponse(API_URL_TX_INTERNALS),
await expect(component).toHaveScreenshot();
});
......@@ -6,7 +6,6 @@ import { Element } from 'react-scroll';
import type { AddressFromToFilter } from 'types/api/address';
import { AddressFromToFilterValues } from 'types/api/address';
import { QueryKeys } from 'types/client/queries';
import getFilterValueFromQuery from 'lib/getFilterValueFromQuery';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
......@@ -36,9 +35,8 @@ const AddressInternalTxs = () => {
const queryIdStr = queryIdArray[0];
const { data, isLoading, isError, pagination, onFilterChange, isPaginationVisible } = useQueryWithPages({
apiPath: `/node-api/addresses/${ queryId }/internal-transactions`,
queryName: QueryKeys.addressInternalTxs,
queryIds: queryIdArray,
resourceName: 'address_internal_txs',
pathParams: { id: queryIdStr },
filters: { filter: filterValue },
scroll: { elem: SCROLL_ELEM, offset: SCROLL_OFFSET },
});
......
import castArray from 'lodash/castArray';
import { useRouter } from 'next/router';
import React from 'react';
import { QueryKeys } from 'types/client/queries';
import TokenTransfer from 'ui/shared/TokenTransfer/TokenTransfer';
const AddressTokenTransfers = () => {
......@@ -12,9 +9,8 @@ const AddressTokenTransfers = () => {
const hash = router.query.id;
return (
<TokenTransfer
path={ `/node-api/addresses/${ hash }/token-transfers` }
queryName={ QueryKeys.addressTokenTransfers }
queryIds={ castArray(router.query.id) }
resourceName="address_token_transfers"
pathParams={{ id: hash?.toString() }}
baseAddress={ typeof hash === 'string' ? hash : undefined }
enableTimeIncrement
/>
......
......@@ -4,10 +4,11 @@ import React from 'react';
import { base as txMock } from 'mocks/txs/tx';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import AddressTxs from './AddressTxs';
const API_URL = '/node-api/addresses/0xd789a607CEac2f0E14867de4EB15b15C9FFB5859/transactions';
const API_URL = buildApiUrl('address_txs', { id: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859' });
const hooksConfig = {
router: {
......@@ -29,7 +30,5 @@ test('address txs +@mobile +@desktop-xl', async({ mount, page }) => {
{ hooksConfig },
);
await page.waitForResponse(API_URL),
await expect(component).toHaveScreenshot();
});
......@@ -5,7 +5,6 @@ import { Element } from 'react-scroll';
import type { AddressFromToFilter } from 'types/api/address';
import { AddressFromToFilterValues } from 'types/api/address';
import { QueryKeys } from 'types/client/queries';
import getFilterValueFromQuery from 'lib/getFilterValueFromQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
......@@ -29,9 +28,8 @@ const AddressTxs = () => {
const [ filterValue, setFilterValue ] = React.useState<AddressFromToFilter>(getFilterValue(router.query.filter));
const addressTxsQuery = useQueryWithPages({
apiPath: `/node-api/addresses/${ router.query.id }/transactions`,
queryName: QueryKeys.addressTxs,
queryIds: castArray(router.query.id),
resourceName: 'address_txs',
pathParams: { id: castArray(router.query.id)[0] },
filters: { filter: filterValue },
scroll: { elem: SCROLL_ELEM, offset: SCROLL_OFFSET },
});
......
import type { UseQueryResult } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import BigNumber from 'bignumber.js';
import React from 'react';
import type { Address, AddressCoinBalanceHistoryChart } from 'types/api/address';
import { QueryKeys } from 'types/client/queries';
import type { Address } from 'types/api/address';
import appConfig from 'configs/app/config';
import useFetch from 'lib/hooks/useFetch';
import useApiQuery from 'lib/api/useApiQuery';
import ChartWidget from 'ui/shared/chart/ChartWidget';
interface Props {
......@@ -15,13 +13,10 @@ interface Props {
}
const AddressCoinBalanceChart = ({ addressQuery }: Props) => {
const fetch = useFetch();
const { data, isLoading, isError } = useQuery<unknown, unknown, AddressCoinBalanceHistoryChart>(
[ QueryKeys.addressCoinBalanceHistoryByDay, addressQuery.data?.hash ],
async() => fetch(`/node-api/addresses/${ addressQuery.data?.hash }/coin-balance-history-by-day`,
), {
enabled: Boolean(addressQuery.data?.hash),
});
const { data, isLoading, isError } = useApiQuery('address_coin_balance_chart', {
pathParams: { id: addressQuery.data?.hash },
queryOptions: { enabled: Boolean(addressQuery.data?.hash) },
});
const items = React.useMemo(() => data?.map(({ date, value }) => ({
date: new Date(date),
......
......@@ -3,9 +3,9 @@ import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { Address } from 'types/api/address';
import { QueryKeys } from 'types/client/queries';
import appConfig from 'configs/app/config';
import { getResourceKey } from 'lib/api/useApiQuery';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import CurrencyValue from 'ui/shared/CurrencyValue';
......@@ -26,7 +26,8 @@ const AddressBalance = ({ data }: Props) => {
}
setLastBlockNumber(blockNumber);
queryClient.setQueryData([ QueryKeys.address, data.hash ], (prevData: Address | undefined) => {
const queryKey = getResourceKey('address', { pathParams: { id: data.hash } });
queryClient.setQueryData(queryKey, (prevData: Address | undefined) => {
if (!prevData) {
return;
}
......
......@@ -4,12 +4,11 @@ import { useRouter } from 'next/router';
import React from 'react';
import type { UserInfo } from 'types/api/account';
import { QueryKeys } from 'types/client/queries';
import starFilledIcon from 'icons/star_filled.svg';
import starOutlineIcon from 'icons/star_outline.svg';
import { resourceKey } from 'lib/api/resources';
import useApiQuery from 'lib/api/useApiQuery';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import useLoginUrl from 'lib/hooks/useLoginUrl';
import usePreventFocusAfterModalClosing from 'lib/hooks/usePreventFocusAfterModalClosing';
import WatchlistAddModal from 'ui/watchlist/AddressModal/AddressModal';
......@@ -42,7 +41,8 @@ const AddressFavoriteButton = ({ className, hash, isAdded }: Props) => {
}, [ addModalProps, deleteModalProps, isAdded, isAuth, loginUrl ]);
const handleAddOrDeleteSuccess = React.useCallback(async() => {
await queryClient.refetchQueries({ queryKey: [ QueryKeys.address, router.query.id ] });
const queryKey = getResourceKey('address', { pathParams: { id: router.query.id?.toString() } });
await queryClient.refetchQueries({ queryKey });
addModalProps.onClose();
}, [ addModalProps, queryClient, router.query.id ]);
......
import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
import { QueryKeys } from 'types/client/queries';
import useFetch from 'lib/hooks/useFetch';
import useApiQuery from 'lib/api/useApiQuery';
const MockAddressPage = ({ children }: { children: JSX.Element }): JSX.Element => {
const router = useRouter();
const fetch = useFetch();
const { data } = useQuery(
[ QueryKeys.address, router.query.id ],
async() => await fetch(`/node-api/addresses/${ router.query.id }`),
{
enabled: Boolean(router.query.id),
},
);
const { data } = useApiQuery('address', {
pathParams: { id: router.query.id?.toString() },
queryOptions: { enabled: Boolean(router.query.id) },
});
if (!data) {
return <div/>;
......
......@@ -6,13 +6,14 @@ import * as coinBalanceMock from 'mocks/address/coinBalanceHistory';
import * as tokenBalanceMock from 'mocks/address/tokenBalance';
import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import MockAddressPage from 'ui/address/testUtils/MockAddressPage';
import TokenSelect from './TokenSelect';
const ASSET_URL = 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/poa/assets/0xb2a90505dc6680a7a695f7975d0d32EeF610f456/logo.png';
const TOKENS_API_URL = '/node-api/addresses/1/token-balances';
const ADDRESS_API_URL = '/node-api/addresses/1';
const TOKENS_API_URL = buildApiUrl('address_token_balances', { id: '1' });
const ADDRESS_API_URL = buildApiUrl('address', { id: '1' });
const hooksConfig = {
router: {
query: { id: '1' },
......
import { Box, Flex, Icon, IconButton, Skeleton, Tooltip } from '@chakra-ui/react';
import { useQuery, useQueryClient, useIsFetching } from '@tanstack/react-query';
import { useQueryClient, useIsFetching } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { Address, AddressTokenBalance } from 'types/api/address';
import { QueryKeys } from 'types/client/queries';
import type { Address } from 'types/api/address';
import walletIcon from 'icons/wallet.svg';
import useFetch from 'lib/hooks/useFetch';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
......@@ -18,21 +17,20 @@ import TokenSelectMobile from './TokenSelectMobile';
const TokenSelect = () => {
const router = useRouter();
const fetch = useFetch();
const isMobile = useIsMobile();
const queryClient = useQueryClient();
const [ blockNumber, setBlockNumber ] = React.useState<number>();
const addressQueryData = queryClient.getQueryData<Address>([ QueryKeys.address, router.query.id ]);
const addressResourceKey = getResourceKey('address', { pathParams: { id: router.query.id?.toString() } });
const { data, isError, isLoading, refetch } = useQuery<unknown, unknown, Array<AddressTokenBalance>>(
[ QueryKeys.addressTokenBalances, addressQueryData?.hash ],
async() => await fetch(`/node-api/addresses/${ addressQueryData?.hash }/token-balances`),
{
enabled: Boolean(addressQueryData),
},
);
const balancesIsFetching = useIsFetching({ queryKey: [ QueryKeys.addressTokenBalances, addressQueryData?.hash ] });
const addressQueryData = queryClient.getQueryData<Address>(addressResourceKey);
const { data, isError, isLoading, refetch } = useApiQuery('address_token_balances', {
pathParams: { id: addressQueryData?.hash },
queryOptions: { enabled: Boolean(addressQueryData) },
});
const balancesResourceKey = getResourceKey('address_token_balances', { pathParams: { id: addressQueryData?.hash } });
const balancesIsFetching = useIsFetching({ queryKey: balancesResourceKey });
const handleTokenBalanceMessage: SocketMessage.AddressTokenBalance['handler'] = React.useCallback((payload) => {
if (payload.block_number !== blockNumber) {
......
......@@ -3,13 +3,14 @@ import React from 'react';
import * as blockMock from 'mocks/blocks/block';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import BlockDetails from './BlockDetails';
const API_URL = '/node-api/blocks/1';
const API_URL = buildApiUrl('block', { id: '1' });
const hooksConfig = {
router: {
query: { id: 1 },
query: { id: '1' },
},
};
......
import { Grid, GridItem, Text, Icon, Link, Box, Tooltip } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import BigNumber from 'bignumber.js';
import capitalize from 'lodash/capitalize';
import NextLink from 'next/link';
......@@ -7,17 +6,13 @@ import { useRouter } from 'next/router';
import React from 'react';
import { scroller, Element } from 'react-scroll';
import type { Block } from 'types/api/block';
import { QueryKeys } from 'types/client/queries';
import appConfig from 'configs/app/config';
import clockIcon from 'icons/clock.svg';
import flameIcon from 'icons/flame.svg';
import type { ResourceError } from 'lib/api/resources';
import useApiQuery from 'lib/api/useApiQuery';
import getBlockReward from 'lib/block/getBlockReward';
import { WEI, WEI_IN_GWEI, ZERO } from 'lib/consts';
import dayjs from 'lib/date/dayjs';
import useFetch from 'lib/hooks/useFetch';
import { space } from 'lib/html-entities';
import link from 'lib/link/link';
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
......@@ -35,15 +30,11 @@ import Utilization from 'ui/shared/Utilization/Utilization';
const BlockDetails = () => {
const [ isExpanded, setIsExpanded ] = React.useState(false);
const router = useRouter();
const fetch = useFetch();
const { data, isLoading, isError, error } = useQuery<unknown, ResourceError<{ status: number }>, Block>(
[ QueryKeys.block, router.query.id ],
async() => await fetch(`/node-api/blocks/${ router.query.id }`),
{
enabled: Boolean(router.query.id),
},
);
const { data, isLoading, isError, error } = useApiQuery<'block', { status: number }>('block', {
pathParams: { id: router.query.id?.toString() },
queryOptions: { enabled: Boolean(router.query.id) },
});
const handleCutClick = React.useCallback(() => {
setIsExpanded((flag) => !flag);
......
......@@ -5,8 +5,8 @@ import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { BlockType, BlocksResponse } from 'types/api/block';
import { QueryKeys } from 'types/client/queries';
import { getResourceKey } from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
......@@ -21,6 +21,7 @@ import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
type QueryResult = UseQueryResult<BlocksResponse> & {
pagination: PaginationProps;
isPaginationVisible: boolean;
};
interface Props {
......@@ -34,18 +35,9 @@ const BlocksContent = ({ type, query }: Props) => {
const [ socketAlert, setSocketAlert ] = React.useState('');
const handleNewBlockMessage: SocketMessage.NewBlock['handler'] = React.useCallback((payload) => {
const queryKey = (() => {
switch (type) {
case 'uncle':
return QueryKeys.blocksUncles;
case 'reorg':
return QueryKeys.blocksReorgs;
default:
return QueryKeys.blocks;
}
})();
const queryKey = getResourceKey('blocks', { queryParams: { type } });
queryClient.setQueryData([ queryKey, { page: query.pagination.page, filters: { type } } ], (prevData: BlocksResponse | undefined) => {
queryClient.setQueryData(queryKey, (prevData: BlocksResponse | undefined) => {
const shouldAddToList = !type || type === payload.block.type;
if (!prevData) {
......@@ -56,7 +48,7 @@ const BlocksContent = ({ type, query }: Props) => {
}
return shouldAddToList ? { ...prevData, items: [ payload.block, ...prevData.items ] } : prevData;
});
}, [ query.pagination.page, queryClient, type ]);
}, [ queryClient, type ]);
const handleSocketClose = React.useCallback(() => {
setSocketAlert('Connection is lost. Please click here to load new blocks.');
......@@ -110,11 +102,9 @@ const BlocksContent = ({ type, query }: Props) => {
})();
const isPaginatorHidden = !query.isLoading && !query.isError && query.pagination.page === 1 && !query.pagination.hasNextPage;
return (
<>
{ isMobile && !isPaginatorHidden && (
{ isMobile && query.isPaginationVisible && (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...query.pagination }/>
</ActionBar>
......
import { Flex, Box, Text, Skeleton } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React from 'react';
import type { HomeStats } from 'types/api/stats';
import { QueryKeys } from 'types/client/queries';
import useFetch from 'lib/hooks/useFetch';
import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import { nbsp } from 'lib/html-entities';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
......@@ -18,12 +14,7 @@ interface Props {
const BlocksTabSlot = ({ pagination, isPaginationVisible }: Props) => {
const isMobile = useIsMobile();
const fetch = useFetch();
const statsQuery = useQuery<unknown, unknown, HomeStats>(
[ QueryKeys.homeStats ],
() => fetch('/node-api/home-stats'),
);
const statsQuery = useApiQuery('homepage_stats');
if (isMobile) {
return null;
......
import { Alert, AlertIcon, AlertTitle, chakra } from '@chakra-ui/react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useQueryClient } from '@tanstack/react-query';
import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { IndexingStatus } from 'types/api/indexingStatus';
import { QueryKeys } from 'types/client/queries';
import useFetch from 'lib/hooks/useFetch';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import { nbsp, ndash } from 'lib/html-entities';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
const IndexingAlert = ({ className }: { className?: string }) => {
const fetch = useFetch();
const isMobile = useIsMobile();
const { data } = useQuery<unknown, unknown, IndexingStatus>(
[ QueryKeys.indexingStatus ],
async() => await fetch(`/node-api/index/indexing-status`),
);
const { data } = useApiQuery('homepage_indexing_status');
const queryClient = useQueryClient();
const handleBlocksIndexStatus: SocketMessage.BlocksIndexStatus['handler'] = React.useCallback((payload) => {
queryClient.setQueryData([ QueryKeys.indexingStatus ], (prevData: IndexingStatus | undefined) => {
queryClient.setQueryData(getResourceKey('homepage_indexing_status'), (prevData: IndexingStatus | undefined) => {
const newData = prevData ? { ...prevData } : {} as IndexingStatus;
newData.finished_indexing_blocks = payload.finished;
......@@ -46,7 +41,7 @@ const IndexingAlert = ({ className }: { className?: string }) => {
});
const handleIntermalTxsIndexStatus: SocketMessage.InternalTxsIndexStatus['handler'] = React.useCallback((payload) => {
queryClient.setQueryData([ QueryKeys.indexingStatus ], (prevData: IndexingStatus | undefined) => {
queryClient.setQueryData(getResourceKey('homepage_indexing_status'), (prevData: IndexingStatus | undefined) => {
const newData = prevData ? { ...prevData } : {} as IndexingStatus;
newData.finished_indexing = payload.finished;
......
......@@ -5,11 +5,12 @@ import * as blockMock from 'mocks/blocks/block';
import * as statsMock from 'mocks/stats/index';
import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import LatestBlocks from './LatestBlocks';
const STATS_API_URL = '/node-api/home-stats';
const BLOCKS_API_URL = '/node-api/index/blocks';
const STATS_API_URL = buildApiUrl('homepage_stats');
const BLOCKS_API_URL = buildApiUrl('homepage_blocks');
export const test = base.extend<socketServer.SocketServerFixture>({
createSocket: socketServer.createSocket,
......
import { Box, Heading, Flex, Link, Text, VStack, Skeleton } from '@chakra-ui/react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useQueryClient } from '@tanstack/react-query';
import { AnimatePresence } from 'framer-motion';
import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { Block } from 'types/api/block';
import type { HomeStats } from 'types/api/stats';
import { QueryKeys } from 'types/client/queries';
import useFetch from 'lib/hooks/useFetch';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import { nbsp } from 'lib/html-entities';
import link from 'lib/link/link';
......@@ -24,20 +22,13 @@ const BLOCK_MARGIN = 12;
const LatestBlocks = () => {
const isMobile = useIsMobile();
const blocksMaxCount = isMobile ? 2 : 3;
const fetch = useFetch();
const { data, isLoading, isError } = useQuery<unknown, unknown, Array<Block>>(
[ QueryKeys.indexBlocks ],
async() => await fetch(`/node-api/index/blocks`),
);
const { data, isLoading, isError } = useApiQuery('homepage_blocks');
const queryClient = useQueryClient();
const statsQueryResult = useQuery<unknown, unknown, HomeStats>(
[ QueryKeys.homeStats ],
() => fetch('/node-api/home-stats'),
);
const statsQueryResult = useApiQuery('homepage_stats');
const handleNewBlockMessage: SocketMessage.NewBlock['handler'] = React.useCallback((payload) => {
queryClient.setQueryData([ QueryKeys.indexBlocks ], (prevData: Array<Block> | undefined) => {
queryClient.setQueryData(getResourceKey('homepage_blocks'), (prevData: Array<Block> | undefined) => {
const newData = prevData ? [ ...prevData ] : [];
......
......@@ -6,6 +6,7 @@ import * as statsMock from 'mocks/stats/index';
import * as txMock from 'mocks/txs/tx';
import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import LatestTxs from './LatestTxs';
......@@ -14,11 +15,11 @@ export const test = base.extend<socketServer.SocketServerFixture>({
});
test('default view +@mobile +@dark-mode', async({ mount, page }) => {
await page.route('/node-api/home-stats', (route) => route.fulfill({
await page.route(buildApiUrl('homepage_stats'), (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsMock.base),
}));
await page.route('/node-api/index/txs', (route) => route.fulfill({
await page.route(buildApiUrl('homepage_txs'), (route) => route.fulfill({
status: 200,
body: JSON.stringify([
txMock.base,
......@@ -47,11 +48,11 @@ test.describe('socket', () => {
};
test('new item', async({ mount, page, createSocket }) => {
await page.route('/node-api/home-stats', (route) => route.fulfill({
await page.route(buildApiUrl('homepage_stats'), (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsMock.base),
}));
await page.route('/node-api/index/txs', (route) => route.fulfill({
await page.route(buildApiUrl('homepage_txs'), (route) => route.fulfill({
status: 200,
body: JSON.stringify([
txMock.base,
......
import { Box, Heading, Flex, Link, Text, Skeleton } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React from 'react';
import type { Transaction } from 'types/api/transaction';
import { QueryKeys } from 'types/client/queries';
import useFetch from 'lib/hooks/useFetch';
import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import link from 'lib/link/link';
import TxsNewItemNotice from 'ui/txs/TxsNewItemNotice';
......@@ -16,11 +12,7 @@ import LatestTxsItemSkeleton from './LatestTxsItemSkeleton';
const LatestTransactions = () => {
const isMobile = useIsMobile();
const txsCount = isMobile ? 2 : 6;
const fetch = useFetch();
const { data, isLoading, isError } = useQuery<unknown, unknown, Array<Transaction>>(
[ QueryKeys.indexTxs ],
async() => await fetch(`/node-api/index/txs`),
);
const { data, isLoading, isError } = useApiQuery('homepage_txs');
let content;
......
......@@ -4,10 +4,11 @@ import React from 'react';
import * as statsMock from 'mocks/stats/index';
import contextWithEnvs from 'playwright/fixtures/contextWithEnvs';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import Stats from './Stats';
const API_URL = '/node-api/home-stats';
const API_URL = buildApiUrl('homepage_stats');
test('all items +@mobile +@dark-mode +@desktop-xl', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
......
import { Grid } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React from 'react';
import type { HomeStats } from 'types/api/stats';
import { QueryKeys } from 'types/client/queries';
import appConfig from 'configs/app/config';
import blockIcon from 'icons/block.svg';
import clockIcon from 'icons/clock-light.svg';
import gasIcon from 'icons/gas.svg';
import txIcon from 'icons/transactions.svg';
import walletIcon from 'icons/wallet.svg';
import useFetch from 'lib/hooks/useFetch';
import useApiQuery from 'lib/api/useApiQuery';
import link from 'lib/link/link';
import StatsGasPrices from './StatsGasPrices';
......@@ -26,12 +22,7 @@ let itemsCount = 5;
!hasAvgBlockTime && itemsCount--;
const Stats = () => {
const fetch = useFetch();
const { data, isLoading, isError } = useQuery<unknown, unknown, HomeStats>(
[ QueryKeys.homeStats ],
async() => await fetch(`/node-api/home-stats`),
);
const { data, isLoading, isError } = useApiQuery('homepage_stats');
if (isError) {
return null;
......
......@@ -4,11 +4,12 @@ import React from 'react';
import * as dailyTxsMock from 'mocks/stats/daily_txs';
import * as statsMock from 'mocks/stats/index';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import ChainIndicators from './ChainIndicators';
const STATS_API_URL = '/node-api/home-stats';
const TX_CHART_API_URL = '/node-api/home-stats/charts/transactions';
const STATS_API_URL = buildApiUrl('homepage_stats');
const TX_CHART_API_URL = buildApiUrl('homepage_chart_txs');
test('daily txs chart +@mobile +@dark-mode +@dark-mode-mobile', async({ mount, page }) => {
await page.route(STATS_API_URL, (route) => route.fulfill({
......
import { Box, Flex, Icon, Skeleton, Text, Tooltip, useColorModeValue } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React from 'react';
import type { HomeStats } from 'types/api/stats';
import { QueryKeys } from 'types/client/queries';
import appConfig from 'configs/app/config';
import infoIcon from 'icons/info.svg';
import useFetch from 'lib/hooks/useFetch';
import useApiQuery from 'lib/api/useApiQuery';
import ChainIndicatorChartContainer from './ChainIndicatorChartContainer';
import ChainIndicatorItem from './ChainIndicatorItem';
......@@ -33,12 +29,7 @@ const ChainIndicators = () => {
const indicator = indicators.find(({ id }) => id === selectedIndicator);
const queryResult = useFetchChartData(indicator);
const fetch = useFetch();
const statsQueryResult = useQuery<unknown, unknown, HomeStats>(
[ QueryKeys.homeStats ],
() => fetch('/node-api/home-stats'),
);
const statsQueryResult = useApiQuery('homepage_stats');
const bgColorDesktop = useColorModeValue('white', 'gray.900');
const bgColorMobile = useColorModeValue('white', 'black');
......
import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/charts';
import type { HomeStats } from 'types/api/stats';
import type { QueryKeys } from 'types/client/queries';
import type { TimeChartData } from 'ui/shared/chart/types';
export type ChartsQueryKeys = QueryKeys.chartsTxs | QueryKeys.chartsMarket;
import type { ResourcePayload } from 'lib/api/resources';
export type ChartsResources = 'homepage_chart_txs' | 'homepage_chart_market';
export type ChainIndicatorId = 'daily_txs' | 'coin_price' | 'market_cup';
export interface TChainIndicator<Q extends ChartsQueryKeys> {
export interface TChainIndicator<R extends ChartsResources> {
id: ChainIndicatorId;
title: string;
value: (stats: HomeStats) => string;
icon: React.ReactNode;
hint?: string;
api: {
queryName: Q;
path: string;
dataFn: (response: ChartsResponse<Q>) => TimeChartData;
resourceName: R;
dataFn: (response: ResourcePayload<R>) => TimeChartData;
};
}
export type ChartsResponse<Q extends ChartsQueryKeys> =
Q extends QueryKeys.chartsTxs ? ChartTransactionResponse :
Q extends QueryKeys.chartsMarket ? ChartMarketResponse :
never;
import type { UseQueryResult } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import React from 'react';
import type { TChainIndicator, ChartsResponse, ChartsQueryKeys } from './types';
import type { TChainIndicator, ChartsResources } from './types';
import type { TimeChartData } from 'ui/shared/chart/types';
import useFetch from 'lib/hooks/useFetch';
import type { ResourcePayload } from 'lib/api/resources';
import useApiQuery from 'lib/api/useApiQuery';
type NotUndefined<T> = T extends undefined ? never : T;
export default function useFetchChartData<Q extends ChartsQueryKeys>(indicator: TChainIndicator<Q> | undefined): UseQueryResult<TimeChartData> {
const fetch = useFetch();
type ResponseType = ChartsResponse<NotUndefined<typeof indicator>['api']['queryName']>;
const queryResult = useQuery<unknown, unknown, ResponseType>(
[ indicator?.api.queryName ],
() => fetch(indicator?.api.path || ''),
{ enabled: Boolean(indicator) },
);
export default function useFetchChartData<R extends ChartsResources>(indicator: TChainIndicator<R> | undefined): UseQueryResult<TimeChartData> {
const queryResult = useApiQuery(indicator?.api.resourceName || 'homepage_chart_txs', {
queryOptions: { enabled: Boolean(indicator) },
});
return React.useMemo(() => {
return {
...queryResult,
data: queryResult.data && indicator ? indicator.api.dataFn(queryResult.data) : queryResult.data,
data: queryResult.data && indicator ? indicator.api.dataFn(queryResult.data as ResourcePayload<R>) : queryResult.data,
} as UseQueryResult<TimeChartData>;
}, [ indicator, queryResult ]);
}
......@@ -2,7 +2,6 @@ import { Icon } from '@chakra-ui/react';
import React from 'react';
import type { TChainIndicator } from '../types';
import { QueryKeys } from 'types/client/queries';
import appConfig from 'configs/app/config';
import globeIcon from 'icons/globe.svg';
......@@ -11,15 +10,14 @@ import { shortenNumberWithLetter } from 'lib/formatters';
import { sortByDateDesc } from 'ui/shared/chart/utils/sorts';
import TokenLogo from 'ui/shared/TokenLogo';
const dailyTxsIndicator: TChainIndicator<QueryKeys.chartsTxs> = {
const dailyTxsIndicator: TChainIndicator<'homepage_chart_txs'> = {
id: 'daily_txs',
title: 'Daily transactions',
value: (stats) => shortenNumberWithLetter(Number(stats.transactions_today), undefined, { maximumFractionDigits: 2 }),
icon: <Icon as={ txIcon } boxSize={ 6 } bgColor="#56ACD1" borderRadius="base" color="white"/>,
hint: `The total daily number of transactions on the blockchain for the last month.`,
api: {
queryName: QueryKeys.chartsTxs,
path: '/node-api/home-stats/charts/transactions',
resourceName: 'homepage_chart_txs',
dataFn: (response) => ([ {
items: response.chart_data
.map((item) => ({ date: new Date(item.date), value: item.tx_count }))
......@@ -30,15 +28,14 @@ const dailyTxsIndicator: TChainIndicator<QueryKeys.chartsTxs> = {
},
};
const coinPriceIndicator: TChainIndicator<QueryKeys.chartsMarket> = {
const coinPriceIndicator: TChainIndicator<'homepage_chart_market'> = {
id: 'coin_price',
title: `${ appConfig.network.currency.symbol } price`,
value: (stats) => '$' + Number(stats.coin_price).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }),
icon: <TokenLogo hash={ appConfig.network.currency.address || '' } name={ appConfig.network.currency.name } boxSize={ 6 }/>,
hint: `${ appConfig.network.currency.symbol } token daily price in USD.`,
api: {
queryName: QueryKeys.chartsMarket,
path: '/node-api/home-stats/charts/market',
resourceName: 'homepage_chart_market',
dataFn: (response) => ([ {
items: response.chart_data
.map((item) => ({ date: new Date(item.date), value: Number(item.closing_price) }))
......@@ -49,7 +46,7 @@ const coinPriceIndicator: TChainIndicator<QueryKeys.chartsMarket> = {
},
};
const marketPriceIndicator: TChainIndicator<QueryKeys.chartsMarket> = {
const marketPriceIndicator: TChainIndicator<'homepage_chart_market'> = {
id: 'market_cup',
title: 'Market cap',
value: (stats) => '$' + shortenNumberWithLetter(Number(stats.market_cap), undefined, { maximumFractionDigits: 0 }),
......@@ -57,8 +54,7 @@ const marketPriceIndicator: TChainIndicator<QueryKeys.chartsMarket> = {
// eslint-disable-next-line max-len
hint: 'The total market value of a cryptocurrency\'s circulating supply. It is analogous to the free-float capitalization in the stock market. Market Cap = Current Price x Circulating Supply.',
api: {
queryName: QueryKeys.chartsMarket,
path: '/node-api/home-stats/charts/market',
resourceName: 'homepage_chart_market',
dataFn: (response) => ([ {
items: response.chart_data
.map((item) => ({ date: new Date(item.date), value: Number(item.closing_price) * Number(response.available_supply) }))
......
import { Flex, Tag } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
import type { Address } from 'types/api/address';
import { QueryKeys } from 'types/client/queries';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import useFetch from 'lib/hooks/useFetch';
import useApiQuery from 'lib/api/useApiQuery';
import AddressBlocksValidated from 'ui/address/AddressBlocksValidated';
import AddressCoinBalance from 'ui/address/AddressCoinBalance';
import AddressDetails from 'ui/address/AddressDetails';
......@@ -20,15 +17,11 @@ import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
const AddressPageContent = () => {
const router = useRouter();
const fetch = useFetch();
const addressQuery = useQuery<unknown, unknown, Address>(
[ QueryKeys.address, router.query.id ],
async() => await fetch(`/node-api/addresses/${ router.query.id }`),
{
enabled: Boolean(router.query.id),
},
);
const addressQuery = useApiQuery('address', {
pathParams: { id: router.query.id?.toString() },
queryOptions: { enabled: Boolean(router.query.id) },
});
const tags = [
...(addressQuery.data?.private_tags || []),
......
import { Box, Icon, Link } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React from 'react';
import type { JsonRpcUrlResponse } from 'types/api/jsonRpcUrl';
import config from 'configs/app/config';
import PlusIcon from 'icons/plus.svg';
import useFetch from 'lib/hooks/useFetch';
import useApiQuery from 'lib/api/useApiQuery';
import AppList from 'ui/apps/AppList';
import AppListSkeleton from 'ui/apps/AppListSkeleton';
import CategoriesMenu from 'ui/apps/CategoriesMenu';
......@@ -15,8 +12,6 @@ import FilterInput from 'ui/shared/FilterInput';
import useMarketplaceApps from '../apps/useMarketplaceApps';
const Apps = () => {
const fetch = useFetch();
const {
isLoading,
category,
......@@ -30,10 +25,7 @@ const Apps = () => {
handleFavoriteClick,
} = useMarketplaceApps();
useQuery<unknown, unknown, JsonRpcUrlResponse>(
[ 'json-rpc-url' ],
async() => await fetch(`/node-api/config/json-rpc-url`),
);
useApiQuery('config_json_rpc');
return (
<>
......
......@@ -2,7 +2,6 @@ import { Flex, Icon, Link, Tooltip } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import { QueryKeys } from 'types/client/queries';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import eastArrowIcon from 'icons/arrows/east.svg';
......@@ -30,8 +29,8 @@ const BlockPageContent = () => {
const appProps = useAppContext();
const blockTxsQuery = useQueryWithPages({
apiPath: `/node-api/blocks/${ router.query.id }/transactions`,
queryName: QueryKeys.blockTxs,
resourceName: 'block_txs',
pathParams: { id: router.query.id?.toString() },
options: {
enabled: Boolean(router.query.id && router.query.tab === 'txs'),
},
......
......@@ -5,11 +5,12 @@ import * as blockMock from 'mocks/blocks/block';
import * as statsMock from 'mocks/stats/index';
import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import Blocks from './Blocks';
const BLOCKS_API_URL = '/node-api/blocks?type=block';
const STATS_API_URL = '/node-api/home-stats';
const BLOCKS_API_URL = buildApiUrl('blocks') + '?type=block';
const STATS_API_URL = buildApiUrl('homepage_stats');
const hooksConfig = {
router: {
query: { tab: 'blocks' },
......
......@@ -2,7 +2,6 @@ import { useRouter } from 'next/router';
import React from 'react';
import type { BlockType } from 'types/api/block';
import { QueryKeys } from 'types/client/queries';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import useIsMobile from 'lib/hooks/useIsMobile';
......@@ -19,12 +18,6 @@ const TAB_TO_TYPE: Record<string, BlockType> = {
uncles: 'uncle',
};
const TAB_TO_QUERY: Record<string, QueryKeys.blocks | QueryKeys.blocksReorgs | QueryKeys.blocksUncles> = {
blocks: QueryKeys.blocks,
reorgs: QueryKeys.blocksReorgs,
uncles: QueryKeys.blocksUncles,
};
const TAB_LIST_PROPS = {
marginBottom: 0,
py: 5,
......@@ -35,11 +28,9 @@ const BlocksPageContent = () => {
const router = useRouter();
const isMobile = useIsMobile();
const type = router.query.tab && !Array.isArray(router.query.tab) ? TAB_TO_TYPE[router.query.tab] : 'block';
const queryName = router.query.tab && !Array.isArray(router.query.tab) ? TAB_TO_QUERY[router.query.tab] : QueryKeys.blocks;
const blocksQuery = useQueryWithPages({
apiPath: '/node-api/blocks',
queryName,
resourceName: 'blocks',
filters: { type },
});
......
......@@ -7,22 +7,23 @@ import * as statsMock from 'mocks/stats/index';
import * as txMock from 'mocks/txs/tx';
import insertAdText from 'playwright/scripts/insertAdText';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import Home from './Home';
test('default view -@default +@desktop-xl +@mobile +@dark-mode', async({ mount, page }) => {
await page.route('/node-api/home-stats', (route) => route.fulfill({
await page.route(buildApiUrl('homepage_stats'), (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsMock.base),
}));
await page.route('/node-api/index/blocks', (route) => route.fulfill({
await page.route(buildApiUrl('homepage_blocks'), (route) => route.fulfill({
status: 200,
body: JSON.stringify([
blockMock.base,
blockMock.base2,
]),
}));
await page.route('/node-api/index/txs', (route) => route.fulfill({
await page.route(buildApiUrl('homepage_txs'), (route) => route.fulfill({
status: 200,
body: JSON.stringify([
txMock.base,
......@@ -30,7 +31,7 @@ test('default view -@default +@desktop-xl +@mobile +@dark-mode', async({ mount,
txMock.withTokenTransfer,
]),
}));
await page.route('/node-api/home-stats/charts/transactions', (route) => route.fulfill({
await page.route(buildApiUrl('homepage_chart_txs'), (route) => route.fulfill({
status: 200,
body: JSON.stringify(dailyTxsMock.base),
}));
......
import { Box, Center, useColorMode } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import type { JsonRpcUrlResponse } from 'types/api/jsonRpcUrl';
import type { AppItemOverview } from 'types/client/apps';
import { QueryKeys } from 'types/client/queries';
import appConfig from 'configs/app/config';
import useFetch from 'lib/hooks/useFetch';
import useApiQuery from 'lib/api/useApiQuery';
import link from 'lib/link/link';
import ContentLoader from 'ui/shared/ContentLoader';
import Page from 'ui/shared/Page/Page';
......@@ -20,18 +17,15 @@ type Props = {
const MarketplaceApp = ({ app, isLoading }: Props) => {
const [ isFrameLoading, setIsFrameLoading ] = useState(isLoading);
const { colorMode } = useColorMode();
const fetch = useFetch();
const ref = useRef<HTMLIFrameElement>(null);
const handleIframeLoad = useCallback(() => {
setIsFrameLoading(false);
}, []);
const { data: jsonRpcUrlResponse } = useQuery<unknown, unknown, JsonRpcUrlResponse>(
[ QueryKeys.jsonRpcUrl ],
async() => await fetch(`/node-api/config/json-rpc-url`),
{ refetchOnMount: false },
);
const { data: jsonRpcUrlResponse } = useApiQuery('config_json_rpc', {
queryOptions: { refetchOnMount: false },
});
useEffect(() => {
if (app && !isFrameLoading) {
......
import { Flex, Link, Icon, Tag, Tooltip } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
import type { Transaction } from 'types/api/transaction';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import eastArrowIcon from 'icons/arrows/east.svg';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/appContext';
import useFetch from 'lib/hooks/useFetch';
import isBrowser from 'lib/isBrowser';
import networkExplorers from 'lib/networks/networkExplorers';
import AdBanner from 'ui/shared/ad/AdBanner';
......@@ -35,7 +33,6 @@ const TABS: Array<RoutedTab> = [
const TransactionPageContent = () => {
const router = useRouter();
const fetch = useFetch();
const appProps = useAppContext();
const isInBrowser = isBrowser();
......@@ -43,13 +40,10 @@ const TransactionPageContent = () => {
const hasGoBackLink = referrer && referrer.includes('/txs');
const { data } = useQuery<unknown, unknown, Transaction>(
[ 'tx', router.query.id ],
async() => await fetch(`/node-api/transactions/${ router.query.id }`),
{
enabled: Boolean(router.query.id),
},
);
const { data } = useApiQuery('tx', {
pathParams: { id: router.query.id?.toString() },
queryOptions: { enabled: Boolean(router.query.id) },
});
const explorersLinks = networkExplorers
.filter((explorer) => explorer.paths.tx)
......
......@@ -2,7 +2,6 @@ import { Box } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import { QueryKeys } from 'types/client/queries';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import appConfig from 'configs/app/config';
......@@ -26,8 +25,7 @@ const Transactions = () => {
const isMobile = useIsMobile();
const filter = router.query.tab === 'pending' ? 'pending' : 'validated';
const txsQuery = useQueryWithPages({
apiPath: '/node-api/transactions',
queryName: filter === 'validated' ? QueryKeys.txsValidate : QueryKeys.txsPending,
resourceName: filter === 'validated' ? 'txs_validated' : 'txs_pending',
filters: { filter },
});
......
......@@ -2,10 +2,11 @@ import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import Page from './Page';
const API_URL = '/node-api/index/indexing-status';
const API_URL = buildApiUrl('homepage_indexing_status');
test('without indexing alert +@mobile', async({ mount }) => {
const component = await mount(
......
......@@ -2,14 +2,13 @@ import { Box } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import { QueryKeys } from 'types/client/queries';
import * as tokenTransferMock from 'mocks/tokens/tokenTransfer';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import TokenTransfer from './TokenTransfer';
const API_URL = '/node-api/transactions/1/token-transfers';
const API_URL = buildApiUrl('tx_token_transfers', { id: '1' });
test('without tx info +@mobile', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
......@@ -21,15 +20,13 @@ test('without tx info +@mobile', async({ mount, page }) => {
<TestApp>
<Box h={{ base: '134px', lg: 6 }}/>
<TokenTransfer
path={ API_URL }
queryName={ QueryKeys.txTokenTransfers }
resourceName="tx_token_transfers"
pathParams={{ id: '1' }}
showTxInfo={ false }
/>
</TestApp>,
);
await page.waitForResponse(API_URL),
await expect(component).toHaveScreenshot();
});
......@@ -43,14 +40,12 @@ test('with tx info +@mobile', async({ mount, page }) => {
<TestApp>
<Box h={{ base: '134px', lg: 6 }}/>
<TokenTransfer
path={ API_URL }
queryName={ QueryKeys.txTokenTransfers }
resourceName="tx_token_transfers"
pathParams={{ id: '1' }}
showTxInfo={ true }
/>
</TestApp>,
);
await page.waitForResponse(API_URL),
await expect(component).toHaveScreenshot();
});
......@@ -3,12 +3,12 @@ import { useRouter } from 'next/router';
import React from 'react';
import { Element } from 'react-scroll';
import type { AddressTokenTransferFilters, AddressFromToFilter } from 'types/api/address';
import type { AddressFromToFilter } from 'types/api/address';
import { AddressFromToFilterValues } from 'types/api/address';
import type { TokenType } from 'types/api/tokenInfo';
import type { TokenTransferFilters } from 'types/api/tokenTransfer';
import type { QueryKeys } from 'types/client/queries';
import type { PaginationFilters } from 'lib/api/resources';
import type { Params as UseApiQueryParams } from 'lib/api/useApiQuery';
import getFilterValueFromQuery from 'lib/getFilterValueFromQuery';
import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
......@@ -34,54 +34,56 @@ const SCROLL_OFFSET = -100;
const getTokenFilterValue = (getFilterValuesFromQuery<TokenType>).bind(null, TOKEN_TYPES);
const getAddressFilterValue = (getFilterValueFromQuery<AddressFromToFilter>).bind(null, AddressFromToFilterValues);
interface Props {
interface Props<Resource extends 'tx_token_transfers' | 'address_token_transfers'> {
isLoading?: boolean;
isDisabled?: boolean;
path: string;
queryName: QueryKeys.txTokenTransfers | QueryKeys.addressTokenTransfers;
queryIds?: Array<string>;
resourceName: Resource;
baseAddress?: string;
showTxInfo?: boolean;
txHash?: string;
enableTimeIncrement?: boolean;
pathParams?: UseApiQueryParams<Resource>['pathParams'];
}
const TokenTransfer = ({
type State = {
type: Array<TokenType> | undefined;
filter: AddressFromToFilter;
}
const TokenTransfer = <Resource extends 'tx_token_transfers' | 'address_token_transfers'>({
isLoading: isLoadingProp,
isDisabled,
queryName,
queryIds,
path,
resourceName,
baseAddress,
showTxInfo = true,
enableTimeIncrement,
}: Props) => {
pathParams,
}: Props<Resource>) => {
const router = useRouter();
const [ filters, setFilters ] = React.useState<AddressTokenTransferFilters & TokenTransferFilters>(
const [ filters, setFilters ] = React.useState<State>(
{ type: getTokenFilterValue(router.query.type), filter: getAddressFilterValue(router.query.filter) },
);
const { isError, isLoading, data, pagination, onFilterChange, isPaginationVisible } = useQueryWithPages({
apiPath: path,
queryName,
queryIds,
resourceName,
pathParams,
options: { enabled: !isDisabled },
filters: filters,
filters: filters as PaginationFilters<Resource>,
scroll: { elem: SCROLL_ELEM, offset: SCROLL_OFFSET },
});
const handleTypeFilterChange = React.useCallback((nextValue: Array<TokenType>) => {
onFilterChange({ ...filters, type: nextValue });
onFilterChange({ ...filters, type: nextValue } as PaginationFilters<Resource>);
setFilters((prevState) => ({ ...prevState, type: nextValue }));
}, [ filters, onFilterChange ]);
const handleAddressFilterChange = React.useCallback((nextValue: string) => {
const filterVal = getAddressFilterValue(nextValue);
onFilterChange({ ...filters, filter: filterVal });
onFilterChange({ ...filters, filter: filterVal } as PaginationFilters<Resource>);
setFilters((prevState) => ({ ...prevState, filter: filterVal }));
}, [ filters, onFilterChange ]);
const numActiveFilters = filters.type.length + (filters.filter ? 1 : 0);
const numActiveFilters = (filters.type?.length || 0) + (filters.filter ? 1 : 0);
const isActionBarHidden = !numActiveFilters && !data?.items.length;
const content = (() => {
......
......@@ -23,7 +23,7 @@ import { TOKEN_TYPE } from './helpers';
interface Props {
appliedFiltersNum?: number;
defaultTypeFilters: Array<TokenType>;
defaultTypeFilters: Array<TokenType> | undefined;
onTypeFilterChange: (nextValue: Array<TokenType>) => void;
withAddressFilter?: boolean;
onAddressFilterChange?: (nextValue: string) => void;
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import { RESOURCES } from 'lib/api/resources';
import * as profileMock from 'mocks/user/profile';
import authFixture from 'playwright/fixtures/auth';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import ProfileMenuDesktop from './ProfileMenuDesktop';
......@@ -34,7 +34,7 @@ test.describe('auth', () => {
});
extendedTest('+@dark-mode', async({ mount, page }) => {
await page.route('/proxy/poa/core' + RESOURCES.user_info.path, (route) => route.fulfill({
await page.route(buildApiUrl('user_info'), (route) => route.fulfill({
status: 200,
body: JSON.stringify(profileMock.base),
}));
......
import { test, expect, devices } from '@playwright/experimental-ct-react';
import React from 'react';
import { RESOURCES } from 'lib/api/resources';
import * as profileMock from 'mocks/user/profile';
import authFixture from 'playwright/fixtures/auth';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import ProfileMenuMobile from './ProfileMenuMobile';
......@@ -30,7 +30,7 @@ test.describe('auth', () => {
});
extendedTest('base view', async({ mount, page }) => {
await page.route('/proxy/poa/core' + RESOURCES.user_info.path, (route) => route.fulfill({
await page.route(buildApiUrl('user_info'), (route) => route.fulfill({
status: 200,
body: JSON.stringify(profileMock.base),
}));
......
......@@ -3,10 +3,11 @@ import React from 'react';
import * as txMock from 'mocks/txs/tx';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import TxDetails from './TxDetails';
const API_URL = '/node-api/transactions/1';
const API_URL = buildApiUrl('tx', { id: '1' });
const hooksConfig = {
router: {
query: { id: 1 },
......
......@@ -4,12 +4,13 @@ import React from 'react';
import * as internalTxsMock from 'mocks/txs/internalTxs';
import * as txMock from 'mocks/txs/tx';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import TxInternals from './TxInternals';
const TX_HASH = txMock.base.hash;
const API_URL_TX = `/node-api/transactions/${ TX_HASH }`;
const API_URL_TX_INTERNALS = `/node-api/transactions/${ TX_HASH }/internal-transactions`;
const API_URL_TX = buildApiUrl('tx', { id: TX_HASH });
const API_URL_TX_INTERNALS = buildApiUrl('tx_internal_txs', { id: TX_HASH });
const hooksConfig = {
router: {
query: { id: TX_HASH },
......
......@@ -2,7 +2,6 @@ import { Box, Text, Show, Hide } from '@chakra-ui/react';
import React from 'react';
import type { InternalTransaction } from 'types/api/internalTransaction';
import { QueryKeys } from 'types/client/queries';
import { SECOND } from 'lib/consts';
import useIsMobile from 'lib/hooks/useIsMobile';
......@@ -76,9 +75,8 @@ const TxInternals = () => {
const [ sort, setSort ] = React.useState<Sort>();
const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND });
const { data, isLoading, isError, pagination, isPaginationVisible } = useQueryWithPages({
apiPath: `/node-api/transactions/${ txInfo.data?.hash }/internal-transactions`,
queryName: QueryKeys.txInternals,
queryIds: txInfo.data?.hash ? [ txInfo.data.hash ] : undefined,
resourceName: 'tx_internal_txs',
pathParams: { id: txInfo.data?.hash },
options: {
enabled: Boolean(txInfo.data?.hash) && Boolean(txInfo.data?.status),
},
......
import { Box, Text } from '@chakra-ui/react';
import React from 'react';
import { QueryKeys } from 'types/client/queries';
import { SECOND } from 'lib/consts';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import ActionBar from 'ui/shared/ActionBar';
......@@ -17,9 +15,8 @@ import useFetchTxInfo from 'ui/tx/useFetchTxInfo';
const TxLogs = () => {
const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND });
const { data, isLoading, isError, pagination, isPaginationVisible } = useQueryWithPages({
apiPath: `/node-api/transactions/${ txInfo.data?.hash }/logs`,
queryName: QueryKeys.txLogs,
queryIds: txInfo.data?.hash ? [ txInfo.data.hash ] : undefined,
resourceName: 'tx_logs',
pathParams: { id: txInfo.data?.hash },
options: {
enabled: Boolean(txInfo.data?.hash) && Boolean(txInfo.data?.status),
},
......
import { Flex, Textarea, Skeleton } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
import type { RawTracesResponse } from 'types/api/rawTrace';
import { QueryKeys } from 'types/client/queries';
import useApiQuery from 'lib/api/useApiQuery';
import { SECOND } from 'lib/consts';
import useFetch from 'lib/hooks/useFetch';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import TxPendingAlert from 'ui/tx/TxPendingAlert';
......@@ -16,16 +12,14 @@ import useFetchTxInfo from 'ui/tx/useFetchTxInfo';
const TxRawTrace = () => {
const router = useRouter();
const fetch = useFetch();
const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND });
const { data, isLoading, isError } = useQuery<unknown, unknown, RawTracesResponse>(
[ QueryKeys.txRawTrace, router.query.id ],
async() => await fetch(`/node-api/transactions/${ router.query.id }/raw-trace`),
{
const { data, isLoading, isError } = useApiQuery('tx_raw_trace', {
pathParams: { id: router.query.id?.toString() },
queryOptions: {
enabled: Boolean(router.query.id) && Boolean(txInfo.data?.status),
},
);
});
if (!txInfo.isLoading && !txInfo.isError && !txInfo.data.status) {
return txInfo.socketStatus ? <TxSocketAlert status={ txInfo.socketStatus }/> : <TxPendingAlert/>;
......
import React from 'react';
import { QueryKeys } from 'types/client/queries';
import { SECOND } from 'lib/consts';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import TokenTransfer from 'ui/shared/TokenTransfer/TokenTransfer';
......@@ -20,15 +18,12 @@ const TxTokenTransfer = () => {
return <DataFetchAlert/>;
}
const path = `/node-api/transactions/${ data?.hash }/token-transfers`;
return (
<TokenTransfer
isLoading={ isLoading }
isDisabled={ !data?.status || !data?.hash }
path={ path }
queryName={ QueryKeys.txTokenTransfers }
queryIds={ data?.hash ? [ data.hash ] : undefined }
resourceName="tx_token_transfers"
pathParams={{ id: data?.hash.toString() }}
showTxInfo={ false }
txHash={ data?.hash || '' }
/>
......
import type { UseQueryResult } from '@tanstack/react-query';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { Transaction } from 'types/api/transaction';
import { QueryKeys } from 'types/client/queries';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import delay from 'lib/delay';
import useFetch from 'lib/hooks/useFetch';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
......@@ -23,23 +22,23 @@ type ReturnType = UseQueryResult<Transaction, unknown> & {
export default function useFetchTxInfo({ onTxStatusUpdate, updateDelay }: Params | undefined = {}): ReturnType {
const router = useRouter();
const fetch = useFetch();
const queryClient = useQueryClient();
const [ socketStatus, setSocketStatus ] = React.useState<'close' | 'error'>();
const queryResult = useQuery<unknown, unknown, Transaction>(
[ QueryKeys.tx, router.query.id ],
async() => await fetch(`/node-api/transactions/${ router.query.id }`),
{
const queryResult = useApiQuery('tx', {
pathParams: { id: router.query.id?.toString() },
queryOptions: {
enabled: Boolean(router.query.id),
refetchOnMount: false,
},
);
});
const { data, isError, isLoading } = queryResult;
const handleStatusUpdateMessage: SocketMessage.TxStatusUpdate['handler'] = React.useCallback(async() => {
updateDelay && await delay(updateDelay);
queryClient.invalidateQueries({ queryKey: [ QueryKeys.tx, router.query.id ] });
queryClient.invalidateQueries({
queryKey: getResourceKey('tx', { pathParams: { id: router.query.id?.toString() } }),
});
onTxStatusUpdate?.();
}, [ onTxStatusUpdate, queryClient, router.query.id, updateDelay ]);
......
......@@ -18,6 +18,7 @@ import useTxsSort from './useTxsSort';
type QueryResult = UseQueryResult<TxsResponse> & {
pagination: PaginationProps;
isPaginationVisible: boolean;
};
type Props = {
......@@ -31,7 +32,6 @@ type Props = {
const TxsContent = ({ filter, query, showBlockInfo = true, showSocketInfo = true, currentAddress, enableTimeIncrement }: Props) => {
const { data, isLoading, isError, setSortByField, setSortByValue, sorting } = useTxsSort(query);
const isPaginatorHidden = !isLoading && !isError && query.pagination.page === 1 && !query.pagination.hasNextPage;
const isMobile = useIsMobile();
const content = (() => {
......@@ -86,7 +86,7 @@ const TxsContent = ({ filter, query, showBlockInfo = true, showSocketInfo = true
sorting={ sorting }
showBlockInfo={ showBlockInfo }
showSocketInfo={ showSocketInfo }
top={ isPaginatorHidden ? 0 : 80 }
top={ query.isPaginationVisible ? 80 : 0 }
currentAddress={ currentAddress }
enableTimeIncrement={ enableTimeIncrement }
/>
......@@ -103,7 +103,7 @@ const TxsContent = ({ filter, query, showBlockInfo = true, showSocketInfo = true
sorting={ sorting }
setSorting={ setSortByValue }
paginationProps={ query.pagination }
showPagination={ !isPaginatorHidden }
showPagination={ query.isPaginationVisible }
filterComponent={ filter }
/>
) }
......
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