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 ...@@ -16,6 +16,7 @@ NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE=validation
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C 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_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 # api config
NEXT_PUBLIC_API_HOST=blockscout-main.test.aws-k8s.blockscout.com NEXT_PUBLIC_API_HOST=blockscout-main.test.aws-k8s.blockscout.com
......
async function rewrites() { async function rewrites() {
return [ return [
{ source: '/node-api/proxy/:slug*', destination: '/api/proxy' },
{ source: '/node-api/:slug*', destination: '/api/:slug*' }, { source: '/node-api/:slug*', destination: '/api/:slug*' },
{ source: '/proxy/:slug*', destination: '/api/proxy' },
].filter(Boolean); ].filter(Boolean);
} }
......
...@@ -6,8 +6,8 @@ import type { ApiResource } from './resources'; ...@@ -6,8 +6,8 @@ import type { ApiResource } from './resources';
export default function buildUrl( export default function buildUrl(
resource: ApiResource, resource: ApiResource,
pathParams?: Record<string, string>, pathParams?: Record<string, string | undefined>,
queryParams?: Record<string, string | undefined>, queryParams?: Record<string, string | number | undefined>,
) { ) {
// FIXME // FIXME
// 1. I was not able to figure out how to send CORS with credentials from localhost // 1. I was not able to figure out how to send CORS with credentials from localhost
...@@ -24,11 +24,11 @@ export default function buildUrl( ...@@ -24,11 +24,11 @@ export default function buildUrl(
const baseUrl = needProxy ? appConfig.baseUrl : (resource.endpoint || appConfig.api.endpoint); const baseUrl = needProxy ? appConfig.baseUrl : (resource.endpoint || appConfig.api.endpoint);
const basePath = resource.basePath !== undefined ? resource.basePath : appConfig.api.basePath; 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); const url = new URL(compile(path)(pathParams), baseUrl);
queryParams && Object.entries(queryParams).forEach(([ key, value ]) => { queryParams && Object.entries(queryParams).forEach(([ key, value ]) => {
value && url.searchParams.append(key, value); value && url.searchParams.append(key, String(value));
}); });
return url.toString(); 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'; import appConfig from 'configs/app/config';
export interface ApiResource { export interface ApiResource {
...@@ -7,7 +34,7 @@ export interface ApiResource { ...@@ -7,7 +34,7 @@ export interface ApiResource {
} }
export const RESOURCES = { export const RESOURCES = {
// account // ACCOUNT
user_info: { user_info: {
path: '/api/account/v1/user/info', path: '/api/account/v1/user/info',
}, },
...@@ -42,12 +69,132 @@ export const RESOURCES = { ...@@ -42,12 +69,132 @@ export const RESOURCES = {
basePath: appConfig.statsApi.basePath, 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 // DEPRECATED
old_api: { old_api: {
path: '/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 const resourceKey = (x: keyof typeof RESOURCES) => x;
export interface ResourceError<T = unknown> { export interface ResourceError<T = unknown> {
...@@ -58,3 +205,60 @@ export interface ResourceError<T = unknown> { ...@@ -58,3 +205,60 @@ export interface ResourceError<T = unknown> {
} }
export type ResourceErrorAccount<T> = ResourceError<{ errors: T }> 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'; ...@@ -9,8 +9,8 @@ import { RESOURCES } from './resources';
import type { ResourceError, ApiResource } from './resources'; import type { ResourceError, ApiResource } from './resources';
export interface Params { export interface Params {
pathParams?: Record<string, string>; pathParams?: Record<string, string | undefined>;
queryParams?: Record<string, string | undefined>; queryParams?: Record<string, string | number | undefined>;
fetchParams?: Pick<FetchParams, 'body' | 'method'>; fetchParams?: Pick<FetchParams, 'body' | 'method'>;
} }
......
import type { UseQueryOptions } from '@tanstack/react-query'; import type { UseQueryOptions } from '@tanstack/react-query';
import { useQuery } 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 { ResourceError, ResourceName, ResourcePayload } from './resources';
import type { Stats, Charts } from 'types/api/stats';
import type { RESOURCES, ResourceError } from './resources';
import type { Params as ApiFetchParams } from './useApiFetch'; import type { Params as ApiFetchParams } from './useApiFetch';
import useApiFetch from './useApiFetch'; import useApiFetch from './useApiFetch';
interface Params<R extends keyof typeof RESOURCES> extends ApiFetchParams { export interface Params<R extends ResourceName, E = unknown> extends ApiFetchParams {
queryOptions?: Omit<UseQueryOptions<unknown, ResourceError, ResourcePayload<R>>, 'queryKey' | 'queryFn'>; 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, resource: R,
{ queryOptions, pathParams, queryParams, fetchParams }: Params<R> = {}, { queryOptions, pathParams, queryParams, fetchParams }: Params<R, E> = {},
) { ) {
const apiFetch = useApiFetch(); const apiFetch = useApiFetch();
return useQuery<unknown, ResourceError, ResourcePayload<R>>( return useQuery<unknown, ResourceError<E>, ResourcePayload<R>>(
pathParams || queryParams ? [ resource, { ...pathParams, ...queryParams } ] : [ resource ], getResourceKey(resource, { pathParams, queryParams }),
async() => { async() => {
return apiFetch<R, ResourcePayload<R>, ResourceError>(resource, { pathParams, queryParams, fetchParams }); return apiFetch<R, ResourcePayload<R>, ResourceError>(resource, { pathParams, queryParams, fetchParams });
}, queryOptions); }, 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) { export default function getFilterValue<FilterType>(filterValues: ReadonlyArray<FilterType>, val: string | Array<string> | undefined) {
if (val === undefined) {
return;
}
const valArray = []; const valArray = [];
if (typeof val === 'string') { if (typeof val === 'string') {
valArray.push(...val.split(',')); valArray.push(...val.split(','));
......
import type { UseQueryOptions } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { useQuery, useQueryClient } from '@tanstack/react-query'; import mapValues from 'lodash/mapValues';
import { pick, omit } from 'lodash'; import omit from 'lodash/omit';
import pick from 'lodash/pick';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { animateScroll, scroller } from 'react-scroll'; import { animateScroll, scroller } from 'react-scroll';
import { PAGINATION_FIELDS, PAGINATION_FILTERS_FIELDS } from 'types/api/pagination'; import type { PaginatedResources, PaginatedResponse, PaginationFilters } from 'lib/api/resources';
import type { PaginationParams, PaginatedResponse, PaginatedQueryKeys, PaginationFilters } from 'types/api/pagination'; 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<Resource extends PaginatedResources> {
resourceName: Resource;
interface Params<QueryName extends PaginatedQueryKeys> { options?: UseApiQueryParams<Resource>['queryOptions'];
apiPath: string; pathParams?: UseApiQueryParams<Resource>['pathParams'];
queryName: QueryName; filters?: PaginationFilters<Resource>;
queryIds?: Array<string>;
filters?: PaginationFilters<QueryName>;
options?: Omit<UseQueryOptions<unknown, unknown, PaginatedResponse<QueryName>>, 'queryKey' | 'queryFn'>;
scroll?: { elem: string; offset: number }; scroll?: { elem: string; offset: number };
} }
export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>({ export default function useQueryWithPages<Resource extends PaginatedResources>({
queryName, resourceName,
filters, filters,
options, options,
apiPath, pathParams,
queryIds,
scroll, scroll,
}: Params<QueryName>) { }: Params<Resource>) {
const paginationFields = PAGINATION_FIELDS[queryName]; const resource = RESOURCES[resourceName];
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const router = useRouter(); 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 [ 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<Record<number, NextPageParams>>({
const [ pageParams, setPageParams ] = React.useState<Array<PaginationParams<QueryName>>>([ ]); [page]: currPageParams,
const fetch = useFetch(); });
const isMounted = React.useRef(false);
const canGoBackwards = React.useRef(!router.query.page);
const [ hasPagination, setHasPagination ] = React.useState(page > 1); 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(() => { const scrollToTop = useCallback(() => {
scroll ? scroller.scrollTo(scroll.elem, { offset: scroll.offset }) : animateScroll.scrollToTop({ duration: 0 }); scroll ? scroller.scrollTo(scroll.elem, { offset: scroll.offset }) : animateScroll.scrollToTop({ duration: 0 });
}, [ scroll ]); }, [ scroll ]);
const queryResult = useQuery<unknown, unknown, PaginatedResponse<QueryName>>( const queryResult = useApiQuery(resourceName, {
queryKey, pathParams,
async() => { queryParams,
const params: Array<string> = []; queryOptions: {
staleTime: page === 1 ? 0 : Infinity,
Object.entries({ ...filters, ...currPageParams }).forEach(([ key, val ]) => { ...options,
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('&') : '' }`);
}, },
{ staleTime: page === 1 ? 0 : Infinity, ...options }, });
);
const { data } = queryResult; const { data } = queryResult;
const onNextPageClick = useCallback(() => { const onNextPageClick = useCallback(() => {
...@@ -69,9 +65,13 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>( ...@@ -69,9 +65,13 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>(
return; return;
} }
const nextPageParams = data.next_page_params; 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 }; const nextPageQuery = { ...router.query };
Object.entries(nextPageParams).forEach(([ key, val ]) => nextPageQuery[key] = val.toString()); Object.entries(nextPageParams).forEach(([ key, val ]) => nextPageQuery[key] = val.toString());
nextPageQuery.page = String(page + 1); nextPageQuery.page = String(page + 1);
...@@ -80,53 +80,52 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>( ...@@ -80,53 +80,52 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>(
router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true }) router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true })
.then(() => { .then(() => {
scrollToTop(); scrollToTop();
setPage(prev => prev + 1);
}); });
}, [ data?.next_page_params, page, pageParams.length, router, scrollToTop ]); }, [ data?.next_page_params, page, router, scrollToTop ]);
const onPrevPageClick = useCallback(() => { const onPrevPageClick = useCallback(() => {
// returning to the first page // returning to the first page
// we dont have pagination params for the first page // we dont have pagination params for the first page
let nextPageQuery: typeof router.query = { ...router.query }; let nextPageQuery: typeof router.query = { ...router.query };
if (page === 2) { if (page === 2) {
nextPageQuery = omit(router.query, paginationFields, 'page'); nextPageQuery = omit(router.query, resource.paginationFields, 'page');
canGoBackwards.current = true; canGoBackwards.current = true;
} else { } else {
const nextPageParams = { ...pageParams[page - 2] }; const nextPageParams = pageParams[page - 1];
nextPageParams && Object.entries(nextPageParams).forEach(([ key, val ]) => nextPageQuery[key] = val.toString()); nextPageParams && Object.entries(nextPageParams).forEach(([ key, val ]) => nextPageQuery[key] = String(val));
nextPageQuery.page = String(page - 1); nextPageQuery.page = String(page - 1);
} }
router.query = nextPageQuery;
router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true }) router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true })
.then(() => { .then(() => {
scrollToTop(); scrollToTop();
setPage(prev => prev - 1); setPage(prev => prev - 1);
page === 2 && queryClient.removeQueries({ queryKey: [ queryName ] }); page === 2 && queryClient.removeQueries({ queryKey: [ resourceName ] });
}); });
setHasPagination(true); setHasPagination(true);
}, [ router, page, paginationFields, pageParams, queryClient, scrollToTop, queryName ]); }, [ router, page, resource.paginationFields, pageParams, scrollToTop, queryClient, resourceName ]);
const resetPage = useCallback(() => { 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(() => { router.push({ pathname: router.pathname, query: omit(router.query, resource.paginationFields, 'page') }, undefined, { shallow: true }).then(() => {
queryClient.removeQueries({ queryKey: [ queryName ] }); queryClient.removeQueries({ queryKey: [ resourceName ] });
scrollToTop(); scrollToTop();
setPage(1); setPage(1);
setPageParams([ ]); setPageParams({});
canGoBackwards.current = true; canGoBackwards.current = true;
window.setTimeout(() => { window.setTimeout(() => {
// FIXME after router is updated we still have inactive queries for previously visited page (e.g third), where we came from // 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 :) // so have to remove it but with some delay :)
queryClient.removeQueries({ queryKey: [ queryName ], type: 'inactive' }); queryClient.removeQueries({ queryKey: [ resourceName ], type: 'inactive' });
}, 100); }, 100);
}); });
setHasPagination(true); setHasPagination(true);
}, [ queryClient, queryName, router, paginationFields, scrollToTop ]); }, [ queryClient, resourceName, router, resource.paginationFields, scrollToTop ]);
const onFilterChange = useCallback((newFilters: PaginationFilters<QueryName> | undefined) => { const onFilterChange = useCallback((newFilters: PaginationFilters<Resource> | undefined) => {
const newQuery = omit(router.query, PAGINATION_FIELDS[queryName], 'page', PAGINATION_FILTERS_FIELDS[queryName]); const newQuery = omit(router.query, resource.paginationFields, 'page', resource.filterFields);
if (newFilters) { if (newFilters) {
Object.entries(newFilters).forEach(([ key, value ]) => { Object.entries(newFilters).forEach(([ key, value ]) => {
if (value && value.length) { if (value && value.length) {
...@@ -143,12 +142,12 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>( ...@@ -143,12 +142,12 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>(
{ shallow: true }, { shallow: true },
).then(() => { ).then(() => {
setPage(1); setPage(1);
setPageParams([ ]); setPageParams({});
scrollToTop(); 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 nextPageParams = data?.next_page_params;
const pagination = { const pagination = {
...@@ -165,12 +164,12 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>( ...@@ -165,12 +164,12 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>(
React.useEffect(() => { React.useEffect(() => {
if (page !== 1 && isMounted.current) { if (page !== 1 && isMounted.current) {
queryClient.cancelQueries({ queryKey }); queryClient.cancelQueries({ queryKey: [ resourceName ] });
setPage(1); setPage(1);
} }
// hook should run only when queryName has changed // hook should run only when queryName has changed
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [ queryName ]); }, [ resourceName ]);
React.useEffect(() => { React.useEffect(() => {
window.setTimeout(() => { 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) => { ...@@ -11,7 +11,7 @@ const handler = async(_req: NextApiRequest, res: NextApiResponse) => {
} }
const response = await fetchFactory(_req, _req.headers['x-endpoint']?.toString())( 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), _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'; ...@@ -5,9 +5,9 @@ import React from 'react';
import type { SocketMessage } from 'lib/socket/types'; import type { SocketMessage } from 'lib/socket/types';
import type { Address, AddressBlocksValidatedResponse } from 'types/api/address'; import type { Address, AddressBlocksValidatedResponse } from 'types/api/address';
import { QueryKeys } from 'types/client/queries';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import { getResourceKey } from 'lib/api/useApiQuery';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
...@@ -31,8 +31,8 @@ const AddressBlocksValidated = ({ addressQuery }: Props) => { ...@@ -31,8 +31,8 @@ const AddressBlocksValidated = ({ addressQuery }: Props) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const query = useQueryWithPages({ const query = useQueryWithPages({
apiPath: `/node-api/addresses/${ addressQuery.data?.hash }/blocks-validated`, resourceName: 'address_blocks_validated',
queryName: QueryKeys.addressBlocksValidated, pathParams: { id: addressQuery.data?.hash },
options: { options: {
enabled: Boolean(addressQuery.data), enabled: Boolean(addressQuery.data),
}, },
...@@ -46,7 +46,7 @@ const AddressBlocksValidated = ({ addressQuery }: Props) => { ...@@ -46,7 +46,7 @@ const AddressBlocksValidated = ({ addressQuery }: Props) => {
setSocketAlert(false); setSocketAlert(false);
queryClient.setQueryData( queryClient.setQueryData(
[ QueryKeys.addressBlocksValidated, { page: query.pagination.page } ], getResourceKey('address_blocks_validated', { pathParams: { id: addressQuery.data?.hash } }),
(prevData: AddressBlocksValidatedResponse | undefined) => { (prevData: AddressBlocksValidatedResponse | undefined) => {
if (!prevData) { if (!prevData) {
return; return;
...@@ -57,7 +57,7 @@ const AddressBlocksValidated = ({ addressQuery }: Props) => { ...@@ -57,7 +57,7 @@ const AddressBlocksValidated = ({ addressQuery }: Props) => {
items: [ payload.block, ...prevData.items ], items: [ payload.block, ...prevData.items ],
}; };
}); });
}, [ query.pagination.page, queryClient ]); }, [ addressQuery.data?.hash, queryClient ]);
const channel = useSocketChannel({ const channel = useSocketChannel({
topic: `blocks:${ addressQuery.data?.hash.toLowerCase() }`, topic: `blocks:${ addressQuery.data?.hash.toLowerCase() }`,
......
...@@ -4,8 +4,8 @@ import React from 'react'; ...@@ -4,8 +4,8 @@ import React from 'react';
import type { SocketMessage } from 'lib/socket/types'; import type { SocketMessage } from 'lib/socket/types';
import type { Address, AddressCoinBalanceHistoryResponse } from 'types/api/address'; 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 useQueryWithPages from 'lib/hooks/useQueryWithPages';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
...@@ -23,8 +23,8 @@ const AddressCoinBalance = ({ addressQuery }: Props) => { ...@@ -23,8 +23,8 @@ const AddressCoinBalance = ({ addressQuery }: Props) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const coinBalanceQuery = useQueryWithPages({ const coinBalanceQuery = useQueryWithPages({
apiPath: `/node-api/addresses/${ addressQuery.data?.hash }/coin-balance-history`, resourceName: 'address_coin_balance',
queryName: QueryKeys.addressCoinBalanceHistory, pathParams: { id: addressQuery.data?.hash },
options: { options: {
enabled: Boolean(addressQuery.data), enabled: Boolean(addressQuery.data),
}, },
...@@ -38,7 +38,7 @@ const AddressCoinBalance = ({ addressQuery }: Props) => { ...@@ -38,7 +38,7 @@ const AddressCoinBalance = ({ addressQuery }: Props) => {
setSocketAlert(false); setSocketAlert(false);
queryClient.setQueryData( queryClient.setQueryData(
[ QueryKeys.addressCoinBalanceHistory, { page: coinBalanceQuery.pagination.page } ], getResourceKey('address_coin_balance', { pathParams: { id: addressQuery.data?.hash } }),
(prevData: AddressCoinBalanceHistoryResponse | undefined) => { (prevData: AddressCoinBalanceHistoryResponse | undefined) => {
if (!prevData) { if (!prevData) {
return; return;
...@@ -52,7 +52,7 @@ const AddressCoinBalance = ({ addressQuery }: Props) => { ...@@ -52,7 +52,7 @@ const AddressCoinBalance = ({ addressQuery }: Props) => {
], ],
}; };
}); });
}, [ coinBalanceQuery.pagination.page, queryClient ]); }, [ addressQuery.data?.hash, queryClient ]);
const channel = useSocketChannel({ const channel = useSocketChannel({
topic: `addresses:${ addressQuery.data?.hash.toLowerCase() }`, topic: `addresses:${ addressQuery.data?.hash.toLowerCase() }`,
......
import { Box, Flex, Text, Icon, Grid, Link } from '@chakra-ui/react'; import { Box, Flex, Text, Icon, Grid, Link } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query'; import type { UseQueryResult } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { Address as TAddress, AddressCounters, AddressTokenBalance } from 'types/api/address'; import type { Address as TAddress } from 'types/api/address';
import { QueryKeys } from 'types/client/queries';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import blockIcon from 'icons/block.svg'; 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 useIsMobile from 'lib/hooks/useIsMobile';
import link from 'lib/link/link'; import link from 'lib/link/link';
import AddressIcon from 'ui/shared/address/AddressIcon'; import AddressIcon from 'ui/shared/address/AddressIcon';
...@@ -35,24 +33,20 @@ interface Props { ...@@ -35,24 +33,20 @@ interface Props {
const AddressDetails = ({ addressQuery }: Props) => { const AddressDetails = ({ addressQuery }: Props) => {
const router = useRouter(); const router = useRouter();
const fetch = useFetch();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const countersQuery = useQuery<unknown, unknown, AddressCounters>( const countersQuery = useApiQuery('address_counters', {
[ QueryKeys.addressCounters, router.query.id ], pathParams: { id: router.query.id?.toString() },
async() => await fetch(`/node-api/addresses/${ router.query.id }/counters`), queryOptions: {
{
enabled: Boolean(router.query.id) && Boolean(addressQuery.data), enabled: Boolean(router.query.id) && Boolean(addressQuery.data),
}, },
); });
const tokenBalancesQuery = useApiQuery('address_token_balances', {
const tokenBalancesQuery = useQuery<unknown, unknown, Array<AddressTokenBalance>>( pathParams: { id: router.query.id?.toString() },
[ QueryKeys.addressTokenBalances, router.query.id ], queryOptions: {
async() => await fetch(`/node-api/addresses/${ router.query.id }/token-balances`),
{
enabled: Boolean(router.query.id) && Boolean(addressQuery.data), enabled: Boolean(router.query.id) && Boolean(addressQuery.data),
}, },
); });
if (countersQuery.isLoading || addressQuery.isLoading || tokenBalancesQuery.isLoading) { if (countersQuery.isLoading || addressQuery.isLoading || tokenBalancesQuery.isLoading) {
return <AddressDetailsSkeleton/>; return <AddressDetailsSkeleton/>;
......
...@@ -4,11 +4,12 @@ import React from 'react'; ...@@ -4,11 +4,12 @@ import React from 'react';
import * as internalTxsMock from 'mocks/txs/internalTxs'; import * as internalTxsMock from 'mocks/txs/internalTxs';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import AddressInternalTxs from './AddressInternalTxs'; import AddressInternalTxs from './AddressInternalTxs';
const ADDRESS_HASH = internalTxsMock.base.from.hash; 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 = { const hooksConfig = {
router: { router: {
query: { id: ADDRESS_HASH }, query: { id: ADDRESS_HASH },
...@@ -29,7 +30,5 @@ test('base view +@mobile', async({ mount, page }) => { ...@@ -29,7 +30,5 @@ test('base view +@mobile', async({ mount, page }) => {
{ hooksConfig }, { hooksConfig },
); );
await page.waitForResponse(API_URL_TX_INTERNALS),
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
...@@ -6,7 +6,6 @@ import { Element } from 'react-scroll'; ...@@ -6,7 +6,6 @@ import { Element } from 'react-scroll';
import type { AddressFromToFilter } from 'types/api/address'; import type { AddressFromToFilter } from 'types/api/address';
import { AddressFromToFilterValues } from 'types/api/address'; import { AddressFromToFilterValues } from 'types/api/address';
import { QueryKeys } from 'types/client/queries';
import getFilterValueFromQuery from 'lib/getFilterValueFromQuery'; import getFilterValueFromQuery from 'lib/getFilterValueFromQuery';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
...@@ -36,9 +35,8 @@ const AddressInternalTxs = () => { ...@@ -36,9 +35,8 @@ const AddressInternalTxs = () => {
const queryIdStr = queryIdArray[0]; const queryIdStr = queryIdArray[0];
const { data, isLoading, isError, pagination, onFilterChange, isPaginationVisible } = useQueryWithPages({ const { data, isLoading, isError, pagination, onFilterChange, isPaginationVisible } = useQueryWithPages({
apiPath: `/node-api/addresses/${ queryId }/internal-transactions`, resourceName: 'address_internal_txs',
queryName: QueryKeys.addressInternalTxs, pathParams: { id: queryIdStr },
queryIds: queryIdArray,
filters: { filter: filterValue }, filters: { filter: filterValue },
scroll: { elem: SCROLL_ELEM, offset: SCROLL_OFFSET }, scroll: { elem: SCROLL_ELEM, offset: SCROLL_OFFSET },
}); });
......
import castArray from 'lodash/castArray';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { QueryKeys } from 'types/client/queries';
import TokenTransfer from 'ui/shared/TokenTransfer/TokenTransfer'; import TokenTransfer from 'ui/shared/TokenTransfer/TokenTransfer';
const AddressTokenTransfers = () => { const AddressTokenTransfers = () => {
...@@ -12,9 +9,8 @@ const AddressTokenTransfers = () => { ...@@ -12,9 +9,8 @@ const AddressTokenTransfers = () => {
const hash = router.query.id; const hash = router.query.id;
return ( return (
<TokenTransfer <TokenTransfer
path={ `/node-api/addresses/${ hash }/token-transfers` } resourceName="address_token_transfers"
queryName={ QueryKeys.addressTokenTransfers } pathParams={{ id: hash?.toString() }}
queryIds={ castArray(router.query.id) }
baseAddress={ typeof hash === 'string' ? hash : undefined } baseAddress={ typeof hash === 'string' ? hash : undefined }
enableTimeIncrement enableTimeIncrement
/> />
......
...@@ -4,10 +4,11 @@ import React from 'react'; ...@@ -4,10 +4,11 @@ import React from 'react';
import { base as txMock } from 'mocks/txs/tx'; import { base as txMock } from 'mocks/txs/tx';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import AddressTxs from './AddressTxs'; import AddressTxs from './AddressTxs';
const API_URL = '/node-api/addresses/0xd789a607CEac2f0E14867de4EB15b15C9FFB5859/transactions'; const API_URL = buildApiUrl('address_txs', { id: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859' });
const hooksConfig = { const hooksConfig = {
router: { router: {
...@@ -29,7 +30,5 @@ test('address txs +@mobile +@desktop-xl', async({ mount, page }) => { ...@@ -29,7 +30,5 @@ test('address txs +@mobile +@desktop-xl', async({ mount, page }) => {
{ hooksConfig }, { hooksConfig },
); );
await page.waitForResponse(API_URL),
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
...@@ -5,7 +5,6 @@ import { Element } from 'react-scroll'; ...@@ -5,7 +5,6 @@ import { Element } from 'react-scroll';
import type { AddressFromToFilter } from 'types/api/address'; import type { AddressFromToFilter } from 'types/api/address';
import { AddressFromToFilterValues } from 'types/api/address'; import { AddressFromToFilterValues } from 'types/api/address';
import { QueryKeys } from 'types/client/queries';
import getFilterValueFromQuery from 'lib/getFilterValueFromQuery'; import getFilterValueFromQuery from 'lib/getFilterValueFromQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
...@@ -29,9 +28,8 @@ const AddressTxs = () => { ...@@ -29,9 +28,8 @@ const AddressTxs = () => {
const [ filterValue, setFilterValue ] = React.useState<AddressFromToFilter>(getFilterValue(router.query.filter)); const [ filterValue, setFilterValue ] = React.useState<AddressFromToFilter>(getFilterValue(router.query.filter));
const addressTxsQuery = useQueryWithPages({ const addressTxsQuery = useQueryWithPages({
apiPath: `/node-api/addresses/${ router.query.id }/transactions`, resourceName: 'address_txs',
queryName: QueryKeys.addressTxs, pathParams: { id: castArray(router.query.id)[0] },
queryIds: castArray(router.query.id),
filters: { filter: filterValue }, filters: { filter: filterValue },
scroll: { elem: SCROLL_ELEM, offset: SCROLL_OFFSET }, scroll: { elem: SCROLL_ELEM, offset: SCROLL_OFFSET },
}); });
......
import type { UseQueryResult } from '@tanstack/react-query'; import type { UseQueryResult } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import React from 'react'; import React from 'react';
import type { Address, AddressCoinBalanceHistoryChart } from 'types/api/address'; import type { Address } from 'types/api/address';
import { QueryKeys } from 'types/client/queries';
import appConfig from 'configs/app/config'; 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'; import ChartWidget from 'ui/shared/chart/ChartWidget';
interface Props { interface Props {
...@@ -15,13 +13,10 @@ interface Props { ...@@ -15,13 +13,10 @@ interface Props {
} }
const AddressCoinBalanceChart = ({ addressQuery }: Props) => { const AddressCoinBalanceChart = ({ addressQuery }: Props) => {
const fetch = useFetch(); const { data, isLoading, isError } = useApiQuery('address_coin_balance_chart', {
const { data, isLoading, isError } = useQuery<unknown, unknown, AddressCoinBalanceHistoryChart>( pathParams: { id: addressQuery.data?.hash },
[ QueryKeys.addressCoinBalanceHistoryByDay, addressQuery.data?.hash ], queryOptions: { enabled: Boolean(addressQuery.data?.hash) },
async() => fetch(`/node-api/addresses/${ addressQuery.data?.hash }/coin-balance-history-by-day`, });
), {
enabled: Boolean(addressQuery.data?.hash),
});
const items = React.useMemo(() => data?.map(({ date, value }) => ({ const items = React.useMemo(() => data?.map(({ date, value }) => ({
date: new Date(date), date: new Date(date),
......
...@@ -3,9 +3,9 @@ import React from 'react'; ...@@ -3,9 +3,9 @@ import React from 'react';
import type { SocketMessage } from 'lib/socket/types'; import type { SocketMessage } from 'lib/socket/types';
import type { Address } from 'types/api/address'; import type { Address } from 'types/api/address';
import { QueryKeys } from 'types/client/queries';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import { getResourceKey } from 'lib/api/useApiQuery';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
import CurrencyValue from 'ui/shared/CurrencyValue'; import CurrencyValue from 'ui/shared/CurrencyValue';
...@@ -26,7 +26,8 @@ const AddressBalance = ({ data }: Props) => { ...@@ -26,7 +26,8 @@ const AddressBalance = ({ data }: Props) => {
} }
setLastBlockNumber(blockNumber); 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) { if (!prevData) {
return; return;
} }
......
...@@ -4,12 +4,11 @@ import { useRouter } from 'next/router'; ...@@ -4,12 +4,11 @@ import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { UserInfo } from 'types/api/account'; import type { UserInfo } from 'types/api/account';
import { QueryKeys } from 'types/client/queries';
import starFilledIcon from 'icons/star_filled.svg'; import starFilledIcon from 'icons/star_filled.svg';
import starOutlineIcon from 'icons/star_outline.svg'; import starOutlineIcon from 'icons/star_outline.svg';
import { resourceKey } from 'lib/api/resources'; 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 useLoginUrl from 'lib/hooks/useLoginUrl';
import usePreventFocusAfterModalClosing from 'lib/hooks/usePreventFocusAfterModalClosing'; import usePreventFocusAfterModalClosing from 'lib/hooks/usePreventFocusAfterModalClosing';
import WatchlistAddModal from 'ui/watchlist/AddressModal/AddressModal'; import WatchlistAddModal from 'ui/watchlist/AddressModal/AddressModal';
...@@ -42,7 +41,8 @@ const AddressFavoriteButton = ({ className, hash, isAdded }: Props) => { ...@@ -42,7 +41,8 @@ const AddressFavoriteButton = ({ className, hash, isAdded }: Props) => {
}, [ addModalProps, deleteModalProps, isAdded, isAuth, loginUrl ]); }, [ addModalProps, deleteModalProps, isAdded, isAuth, loginUrl ]);
const handleAddOrDeleteSuccess = React.useCallback(async() => { 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.onClose();
}, [ addModalProps, queryClient, router.query.id ]); }, [ addModalProps, queryClient, router.query.id ]);
......
import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { QueryKeys } from 'types/client/queries'; import useApiQuery from 'lib/api/useApiQuery';
import useFetch from 'lib/hooks/useFetch';
const MockAddressPage = ({ children }: { children: JSX.Element }): JSX.Element => { const MockAddressPage = ({ children }: { children: JSX.Element }): JSX.Element => {
const router = useRouter(); const router = useRouter();
const fetch = useFetch();
const { data } = useQuery( const { data } = useApiQuery('address', {
[ QueryKeys.address, router.query.id ], pathParams: { id: router.query.id?.toString() },
async() => await fetch(`/node-api/addresses/${ router.query.id }`), queryOptions: { enabled: Boolean(router.query.id) },
{ });
enabled: Boolean(router.query.id),
},
);
if (!data) { if (!data) {
return <div/>; return <div/>;
......
...@@ -6,13 +6,14 @@ import * as coinBalanceMock from 'mocks/address/coinBalanceHistory'; ...@@ -6,13 +6,14 @@ import * as coinBalanceMock from 'mocks/address/coinBalanceHistory';
import * as tokenBalanceMock from 'mocks/address/tokenBalance'; import * as tokenBalanceMock from 'mocks/address/tokenBalance';
import * as socketServer from 'playwright/fixtures/socketServer'; import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import MockAddressPage from 'ui/address/testUtils/MockAddressPage'; import MockAddressPage from 'ui/address/testUtils/MockAddressPage';
import TokenSelect from './TokenSelect'; import TokenSelect from './TokenSelect';
const ASSET_URL = 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/poa/assets/0xb2a90505dc6680a7a695f7975d0d32EeF610f456/logo.png'; 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 TOKENS_API_URL = buildApiUrl('address_token_balances', { id: '1' });
const ADDRESS_API_URL = '/node-api/addresses/1'; const ADDRESS_API_URL = buildApiUrl('address', { id: '1' });
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { id: '1' }, query: { id: '1' },
......
import { Box, Flex, Icon, IconButton, Skeleton, Tooltip } from '@chakra-ui/react'; 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 { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { SocketMessage } from 'lib/socket/types'; import type { SocketMessage } from 'lib/socket/types';
import type { Address, AddressTokenBalance } from 'types/api/address'; import type { Address } from 'types/api/address';
import { QueryKeys } from 'types/client/queries';
import walletIcon from 'icons/wallet.svg'; 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 useIsMobile from 'lib/hooks/useIsMobile';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
...@@ -18,21 +17,20 @@ import TokenSelectMobile from './TokenSelectMobile'; ...@@ -18,21 +17,20 @@ import TokenSelectMobile from './TokenSelectMobile';
const TokenSelect = () => { const TokenSelect = () => {
const router = useRouter(); const router = useRouter();
const fetch = useFetch();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [ blockNumber, setBlockNumber ] = React.useState<number>(); 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>>( const addressQueryData = queryClient.getQueryData<Address>(addressResourceKey);
[ QueryKeys.addressTokenBalances, addressQueryData?.hash ],
async() => await fetch(`/node-api/addresses/${ addressQueryData?.hash }/token-balances`), const { data, isError, isLoading, refetch } = useApiQuery('address_token_balances', {
{ pathParams: { id: addressQueryData?.hash },
enabled: Boolean(addressQueryData), queryOptions: { enabled: Boolean(addressQueryData) },
}, });
); const balancesResourceKey = getResourceKey('address_token_balances', { pathParams: { id: addressQueryData?.hash } });
const balancesIsFetching = useIsFetching({ queryKey: [ QueryKeys.addressTokenBalances, addressQueryData?.hash ] }); const balancesIsFetching = useIsFetching({ queryKey: balancesResourceKey });
const handleTokenBalanceMessage: SocketMessage.AddressTokenBalance['handler'] = React.useCallback((payload) => { const handleTokenBalanceMessage: SocketMessage.AddressTokenBalance['handler'] = React.useCallback((payload) => {
if (payload.block_number !== blockNumber) { if (payload.block_number !== blockNumber) {
......
...@@ -3,13 +3,14 @@ import React from 'react'; ...@@ -3,13 +3,14 @@ import React from 'react';
import * as blockMock from 'mocks/blocks/block'; import * as blockMock from 'mocks/blocks/block';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import BlockDetails from './BlockDetails'; import BlockDetails from './BlockDetails';
const API_URL = '/node-api/blocks/1'; const API_URL = buildApiUrl('block', { id: '1' });
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { id: 1 }, query: { id: '1' },
}, },
}; };
......
import { Grid, GridItem, Text, Icon, Link, Box, Tooltip } from '@chakra-ui/react'; import { Grid, GridItem, Text, Icon, Link, Box, Tooltip } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import capitalize from 'lodash/capitalize'; import capitalize from 'lodash/capitalize';
import NextLink from 'next/link'; import NextLink from 'next/link';
...@@ -7,17 +6,13 @@ import { useRouter } from 'next/router'; ...@@ -7,17 +6,13 @@ import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { scroller, Element } from 'react-scroll'; 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 appConfig from 'configs/app/config';
import clockIcon from 'icons/clock.svg'; import clockIcon from 'icons/clock.svg';
import flameIcon from 'icons/flame.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 getBlockReward from 'lib/block/getBlockReward';
import { WEI, WEI_IN_GWEI, ZERO } from 'lib/consts'; import { WEI, WEI_IN_GWEI, ZERO } from 'lib/consts';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import useFetch from 'lib/hooks/useFetch';
import { space } from 'lib/html-entities'; import { space } from 'lib/html-entities';
import link from 'lib/link/link'; import link from 'lib/link/link';
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle'; import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
...@@ -35,15 +30,11 @@ import Utilization from 'ui/shared/Utilization/Utilization'; ...@@ -35,15 +30,11 @@ import Utilization from 'ui/shared/Utilization/Utilization';
const BlockDetails = () => { const BlockDetails = () => {
const [ isExpanded, setIsExpanded ] = React.useState(false); const [ isExpanded, setIsExpanded ] = React.useState(false);
const router = useRouter(); const router = useRouter();
const fetch = useFetch();
const { data, isLoading, isError, error } = useQuery<unknown, ResourceError<{ status: number }>, Block>( const { data, isLoading, isError, error } = useApiQuery<'block', { status: number }>('block', {
[ QueryKeys.block, router.query.id ], pathParams: { id: router.query.id?.toString() },
async() => await fetch(`/node-api/blocks/${ router.query.id }`), queryOptions: { enabled: Boolean(router.query.id) },
{ });
enabled: Boolean(router.query.id),
},
);
const handleCutClick = React.useCallback(() => { const handleCutClick = React.useCallback(() => {
setIsExpanded((flag) => !flag); setIsExpanded((flag) => !flag);
......
...@@ -5,8 +5,8 @@ import React from 'react'; ...@@ -5,8 +5,8 @@ import React from 'react';
import type { SocketMessage } from 'lib/socket/types'; import type { SocketMessage } from 'lib/socket/types';
import type { BlockType, BlocksResponse } from 'types/api/block'; 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 useIsMobile from 'lib/hooks/useIsMobile';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
...@@ -21,6 +21,7 @@ import SkeletonTable from 'ui/shared/skeletons/SkeletonTable'; ...@@ -21,6 +21,7 @@ import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
type QueryResult = UseQueryResult<BlocksResponse> & { type QueryResult = UseQueryResult<BlocksResponse> & {
pagination: PaginationProps; pagination: PaginationProps;
isPaginationVisible: boolean;
}; };
interface Props { interface Props {
...@@ -34,18 +35,9 @@ const BlocksContent = ({ type, query }: Props) => { ...@@ -34,18 +35,9 @@ const BlocksContent = ({ type, query }: Props) => {
const [ socketAlert, setSocketAlert ] = React.useState(''); const [ socketAlert, setSocketAlert ] = React.useState('');
const handleNewBlockMessage: SocketMessage.NewBlock['handler'] = React.useCallback((payload) => { const handleNewBlockMessage: SocketMessage.NewBlock['handler'] = React.useCallback((payload) => {
const queryKey = (() => { const queryKey = getResourceKey('blocks', { queryParams: { type } });
switch (type) {
case 'uncle':
return QueryKeys.blocksUncles;
case 'reorg':
return QueryKeys.blocksReorgs;
default:
return QueryKeys.blocks;
}
})();
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; const shouldAddToList = !type || type === payload.block.type;
if (!prevData) { if (!prevData) {
...@@ -56,7 +48,7 @@ const BlocksContent = ({ type, query }: Props) => { ...@@ -56,7 +48,7 @@ const BlocksContent = ({ type, query }: Props) => {
} }
return shouldAddToList ? { ...prevData, items: [ payload.block, ...prevData.items ] } : prevData; return shouldAddToList ? { ...prevData, items: [ payload.block, ...prevData.items ] } : prevData;
}); });
}, [ query.pagination.page, queryClient, type ]); }, [ queryClient, type ]);
const handleSocketClose = React.useCallback(() => { const handleSocketClose = React.useCallback(() => {
setSocketAlert('Connection is lost. Please click here to load new blocks.'); setSocketAlert('Connection is lost. Please click here to load new blocks.');
...@@ -110,11 +102,9 @@ const BlocksContent = ({ type, query }: Props) => { ...@@ -110,11 +102,9 @@ const BlocksContent = ({ type, query }: Props) => {
})(); })();
const isPaginatorHidden = !query.isLoading && !query.isError && query.pagination.page === 1 && !query.pagination.hasNextPage;
return ( return (
<> <>
{ isMobile && !isPaginatorHidden && ( { isMobile && query.isPaginationVisible && (
<ActionBar mt={ -6 }> <ActionBar mt={ -6 }>
<Pagination ml="auto" { ...query.pagination }/> <Pagination ml="auto" { ...query.pagination }/>
</ActionBar> </ActionBar>
......
import { Flex, Box, Text, Skeleton } from '@chakra-ui/react'; import { Flex, Box, Text, Skeleton } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import type { HomeStats } from 'types/api/stats'; import useApiQuery from 'lib/api/useApiQuery';
import { QueryKeys } from 'types/client/queries';
import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import { nbsp } from 'lib/html-entities'; import { nbsp } from 'lib/html-entities';
import type { Props as PaginationProps } from 'ui/shared/Pagination'; import type { Props as PaginationProps } from 'ui/shared/Pagination';
...@@ -18,12 +14,7 @@ interface Props { ...@@ -18,12 +14,7 @@ interface Props {
const BlocksTabSlot = ({ pagination, isPaginationVisible }: Props) => { const BlocksTabSlot = ({ pagination, isPaginationVisible }: Props) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const fetch = useFetch(); const statsQuery = useApiQuery('homepage_stats');
const statsQuery = useQuery<unknown, unknown, HomeStats>(
[ QueryKeys.homeStats ],
() => fetch('/node-api/home-stats'),
);
if (isMobile) { if (isMobile) {
return null; return null;
......
import { Alert, AlertIcon, AlertTitle, chakra } from '@chakra-ui/react'; 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 React from 'react';
import type { SocketMessage } from 'lib/socket/types'; import type { SocketMessage } from 'lib/socket/types';
import type { IndexingStatus } from 'types/api/indexingStatus'; 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 useIsMobile from 'lib/hooks/useIsMobile';
import { nbsp, ndash } from 'lib/html-entities'; import { nbsp, ndash } from 'lib/html-entities';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
const IndexingAlert = ({ className }: { className?: string }) => { const IndexingAlert = ({ className }: { className?: string }) => {
const fetch = useFetch();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const { data } = useQuery<unknown, unknown, IndexingStatus>( const { data } = useApiQuery('homepage_indexing_status');
[ QueryKeys.indexingStatus ],
async() => await fetch(`/node-api/index/indexing-status`),
);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const handleBlocksIndexStatus: SocketMessage.BlocksIndexStatus['handler'] = React.useCallback((payload) => { 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; const newData = prevData ? { ...prevData } : {} as IndexingStatus;
newData.finished_indexing_blocks = payload.finished; newData.finished_indexing_blocks = payload.finished;
...@@ -46,7 +41,7 @@ const IndexingAlert = ({ className }: { className?: string }) => { ...@@ -46,7 +41,7 @@ const IndexingAlert = ({ className }: { className?: string }) => {
}); });
const handleIntermalTxsIndexStatus: SocketMessage.InternalTxsIndexStatus['handler'] = React.useCallback((payload) => { 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; const newData = prevData ? { ...prevData } : {} as IndexingStatus;
newData.finished_indexing = payload.finished; newData.finished_indexing = payload.finished;
......
...@@ -5,11 +5,12 @@ import * as blockMock from 'mocks/blocks/block'; ...@@ -5,11 +5,12 @@ import * as blockMock from 'mocks/blocks/block';
import * as statsMock from 'mocks/stats/index'; import * as statsMock from 'mocks/stats/index';
import * as socketServer from 'playwright/fixtures/socketServer'; import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import LatestBlocks from './LatestBlocks'; import LatestBlocks from './LatestBlocks';
const STATS_API_URL = '/node-api/home-stats'; const STATS_API_URL = buildApiUrl('homepage_stats');
const BLOCKS_API_URL = '/node-api/index/blocks'; const BLOCKS_API_URL = buildApiUrl('homepage_blocks');
export const test = base.extend<socketServer.SocketServerFixture>({ export const test = base.extend<socketServer.SocketServerFixture>({
createSocket: socketServer.createSocket, createSocket: socketServer.createSocket,
......
import { Box, Heading, Flex, Link, Text, VStack, Skeleton } from '@chakra-ui/react'; 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 { AnimatePresence } from 'framer-motion';
import React from 'react'; import React from 'react';
import type { SocketMessage } from 'lib/socket/types'; import type { SocketMessage } from 'lib/socket/types';
import type { Block } from 'types/api/block'; 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 useIsMobile from 'lib/hooks/useIsMobile';
import { nbsp } from 'lib/html-entities'; import { nbsp } from 'lib/html-entities';
import link from 'lib/link/link'; import link from 'lib/link/link';
...@@ -24,20 +22,13 @@ const BLOCK_MARGIN = 12; ...@@ -24,20 +22,13 @@ const BLOCK_MARGIN = 12;
const LatestBlocks = () => { const LatestBlocks = () => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const blocksMaxCount = isMobile ? 2 : 3; const blocksMaxCount = isMobile ? 2 : 3;
const fetch = useFetch(); const { data, isLoading, isError } = useApiQuery('homepage_blocks');
const { data, isLoading, isError } = useQuery<unknown, unknown, Array<Block>>(
[ QueryKeys.indexBlocks ],
async() => await fetch(`/node-api/index/blocks`),
);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const statsQueryResult = useQuery<unknown, unknown, HomeStats>( const statsQueryResult = useApiQuery('homepage_stats');
[ QueryKeys.homeStats ],
() => fetch('/node-api/home-stats'),
);
const handleNewBlockMessage: SocketMessage.NewBlock['handler'] = React.useCallback((payload) => { 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 ] : []; const newData = prevData ? [ ...prevData ] : [];
......
...@@ -6,6 +6,7 @@ import * as statsMock from 'mocks/stats/index'; ...@@ -6,6 +6,7 @@ import * as statsMock from 'mocks/stats/index';
import * as txMock from 'mocks/txs/tx'; import * as txMock from 'mocks/txs/tx';
import * as socketServer from 'playwright/fixtures/socketServer'; import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import LatestTxs from './LatestTxs'; import LatestTxs from './LatestTxs';
...@@ -14,11 +15,11 @@ export const test = base.extend<socketServer.SocketServerFixture>({ ...@@ -14,11 +15,11 @@ export const test = base.extend<socketServer.SocketServerFixture>({
}); });
test('default view +@mobile +@dark-mode', async({ mount, page }) => { 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, status: 200,
body: JSON.stringify(statsMock.base), 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, status: 200,
body: JSON.stringify([ body: JSON.stringify([
txMock.base, txMock.base,
...@@ -47,11 +48,11 @@ test.describe('socket', () => { ...@@ -47,11 +48,11 @@ test.describe('socket', () => {
}; };
test('new item', async({ mount, page, createSocket }) => { 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, status: 200,
body: JSON.stringify(statsMock.base), 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, status: 200,
body: JSON.stringify([ body: JSON.stringify([
txMock.base, txMock.base,
......
import { Box, Heading, Flex, Link, Text, Skeleton } from '@chakra-ui/react'; import { Box, Heading, Flex, Link, Text, Skeleton } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import type { Transaction } from 'types/api/transaction'; import useApiQuery from 'lib/api/useApiQuery';
import { QueryKeys } from 'types/client/queries';
import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import link from 'lib/link/link'; import link from 'lib/link/link';
import TxsNewItemNotice from 'ui/txs/TxsNewItemNotice'; import TxsNewItemNotice from 'ui/txs/TxsNewItemNotice';
...@@ -16,11 +12,7 @@ import LatestTxsItemSkeleton from './LatestTxsItemSkeleton'; ...@@ -16,11 +12,7 @@ import LatestTxsItemSkeleton from './LatestTxsItemSkeleton';
const LatestTransactions = () => { const LatestTransactions = () => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const txsCount = isMobile ? 2 : 6; const txsCount = isMobile ? 2 : 6;
const fetch = useFetch(); const { data, isLoading, isError } = useApiQuery('homepage_txs');
const { data, isLoading, isError } = useQuery<unknown, unknown, Array<Transaction>>(
[ QueryKeys.indexTxs ],
async() => await fetch(`/node-api/index/txs`),
);
let content; let content;
......
...@@ -4,10 +4,11 @@ import React from 'react'; ...@@ -4,10 +4,11 @@ import React from 'react';
import * as statsMock from 'mocks/stats/index'; import * as statsMock from 'mocks/stats/index';
import contextWithEnvs from 'playwright/fixtures/contextWithEnvs'; import contextWithEnvs from 'playwright/fixtures/contextWithEnvs';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import Stats from './Stats'; 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 }) => { test('all items +@mobile +@dark-mode +@desktop-xl', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({ await page.route(API_URL, (route) => route.fulfill({
......
import { Grid } from '@chakra-ui/react'; import { Grid } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import type { HomeStats } from 'types/api/stats';
import { QueryKeys } from 'types/client/queries';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import blockIcon from 'icons/block.svg'; import blockIcon from 'icons/block.svg';
import clockIcon from 'icons/clock-light.svg'; import clockIcon from 'icons/clock-light.svg';
import gasIcon from 'icons/gas.svg'; import gasIcon from 'icons/gas.svg';
import txIcon from 'icons/transactions.svg'; import txIcon from 'icons/transactions.svg';
import walletIcon from 'icons/wallet.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 link from 'lib/link/link';
import StatsGasPrices from './StatsGasPrices'; import StatsGasPrices from './StatsGasPrices';
...@@ -26,12 +22,7 @@ let itemsCount = 5; ...@@ -26,12 +22,7 @@ let itemsCount = 5;
!hasAvgBlockTime && itemsCount--; !hasAvgBlockTime && itemsCount--;
const Stats = () => { const Stats = () => {
const fetch = useFetch(); const { data, isLoading, isError } = useApiQuery('homepage_stats');
const { data, isLoading, isError } = useQuery<unknown, unknown, HomeStats>(
[ QueryKeys.homeStats ],
async() => await fetch(`/node-api/home-stats`),
);
if (isError) { if (isError) {
return null; return null;
......
...@@ -4,11 +4,12 @@ import React from 'react'; ...@@ -4,11 +4,12 @@ import React from 'react';
import * as dailyTxsMock from 'mocks/stats/daily_txs'; import * as dailyTxsMock from 'mocks/stats/daily_txs';
import * as statsMock from 'mocks/stats/index'; import * as statsMock from 'mocks/stats/index';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import ChainIndicators from './ChainIndicators'; import ChainIndicators from './ChainIndicators';
const STATS_API_URL = '/node-api/home-stats'; const STATS_API_URL = buildApiUrl('homepage_stats');
const TX_CHART_API_URL = '/node-api/home-stats/charts/transactions'; const TX_CHART_API_URL = buildApiUrl('homepage_chart_txs');
test('daily txs chart +@mobile +@dark-mode +@dark-mode-mobile', async({ mount, page }) => { test('daily txs chart +@mobile +@dark-mode +@dark-mode-mobile', async({ mount, page }) => {
await page.route(STATS_API_URL, (route) => route.fulfill({ await page.route(STATS_API_URL, (route) => route.fulfill({
......
import { Box, Flex, Icon, Skeleton, Text, Tooltip, useColorModeValue } from '@chakra-ui/react'; import { Box, Flex, Icon, Skeleton, Text, Tooltip, useColorModeValue } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import type { HomeStats } from 'types/api/stats';
import { QueryKeys } from 'types/client/queries';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import infoIcon from 'icons/info.svg'; import infoIcon from 'icons/info.svg';
import useFetch from 'lib/hooks/useFetch'; import useApiQuery from 'lib/api/useApiQuery';
import ChainIndicatorChartContainer from './ChainIndicatorChartContainer'; import ChainIndicatorChartContainer from './ChainIndicatorChartContainer';
import ChainIndicatorItem from './ChainIndicatorItem'; import ChainIndicatorItem from './ChainIndicatorItem';
...@@ -33,12 +29,7 @@ const ChainIndicators = () => { ...@@ -33,12 +29,7 @@ const ChainIndicators = () => {
const indicator = indicators.find(({ id }) => id === selectedIndicator); const indicator = indicators.find(({ id }) => id === selectedIndicator);
const queryResult = useFetchChartData(indicator); const queryResult = useFetchChartData(indicator);
const statsQueryResult = useApiQuery('homepage_stats');
const fetch = useFetch();
const statsQueryResult = useQuery<unknown, unknown, HomeStats>(
[ QueryKeys.homeStats ],
() => fetch('/node-api/home-stats'),
);
const bgColorDesktop = useColorModeValue('white', 'gray.900'); const bgColorDesktop = useColorModeValue('white', 'gray.900');
const bgColorMobile = useColorModeValue('white', 'black'); const bgColorMobile = useColorModeValue('white', 'black');
......
import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/charts';
import type { HomeStats } from 'types/api/stats'; import type { HomeStats } from 'types/api/stats';
import type { QueryKeys } from 'types/client/queries';
import type { TimeChartData } from 'ui/shared/chart/types'; 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 type ChainIndicatorId = 'daily_txs' | 'coin_price' | 'market_cup';
export interface TChainIndicator<Q extends ChartsQueryKeys> { export interface TChainIndicator<R extends ChartsResources> {
id: ChainIndicatorId; id: ChainIndicatorId;
title: string; title: string;
value: (stats: HomeStats) => string; value: (stats: HomeStats) => string;
icon: React.ReactNode; icon: React.ReactNode;
hint?: string; hint?: string;
api: { api: {
queryName: Q; resourceName: R;
path: string; dataFn: (response: ResourcePayload<R>) => TimeChartData;
dataFn: (response: ChartsResponse<Q>) => 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 type { UseQueryResult } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import React from 'react'; 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 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<R extends ChartsResources>(indicator: TChainIndicator<R> | undefined): UseQueryResult<TimeChartData> {
const queryResult = useApiQuery(indicator?.api.resourceName || 'homepage_chart_txs', {
export default function useFetchChartData<Q extends ChartsQueryKeys>(indicator: TChainIndicator<Q> | undefined): UseQueryResult<TimeChartData> { queryOptions: { enabled: Boolean(indicator) },
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) },
);
return React.useMemo(() => { return React.useMemo(() => {
return { return {
...queryResult, ...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>; } as UseQueryResult<TimeChartData>;
}, [ indicator, queryResult ]); }, [ indicator, queryResult ]);
} }
...@@ -2,7 +2,6 @@ import { Icon } from '@chakra-ui/react'; ...@@ -2,7 +2,6 @@ import { Icon } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TChainIndicator } from '../types'; import type { TChainIndicator } from '../types';
import { QueryKeys } from 'types/client/queries';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import globeIcon from 'icons/globe.svg'; import globeIcon from 'icons/globe.svg';
...@@ -11,15 +10,14 @@ import { shortenNumberWithLetter } from 'lib/formatters'; ...@@ -11,15 +10,14 @@ import { shortenNumberWithLetter } from 'lib/formatters';
import { sortByDateDesc } from 'ui/shared/chart/utils/sorts'; import { sortByDateDesc } from 'ui/shared/chart/utils/sorts';
import TokenLogo from 'ui/shared/TokenLogo'; import TokenLogo from 'ui/shared/TokenLogo';
const dailyTxsIndicator: TChainIndicator<QueryKeys.chartsTxs> = { const dailyTxsIndicator: TChainIndicator<'homepage_chart_txs'> = {
id: 'daily_txs', id: 'daily_txs',
title: 'Daily transactions', title: 'Daily transactions',
value: (stats) => shortenNumberWithLetter(Number(stats.transactions_today), undefined, { maximumFractionDigits: 2 }), value: (stats) => shortenNumberWithLetter(Number(stats.transactions_today), undefined, { maximumFractionDigits: 2 }),
icon: <Icon as={ txIcon } boxSize={ 6 } bgColor="#56ACD1" borderRadius="base" color="white"/>, 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.`, hint: `The total daily number of transactions on the blockchain for the last month.`,
api: { api: {
queryName: QueryKeys.chartsTxs, resourceName: 'homepage_chart_txs',
path: '/node-api/home-stats/charts/transactions',
dataFn: (response) => ([ { dataFn: (response) => ([ {
items: response.chart_data items: response.chart_data
.map((item) => ({ date: new Date(item.date), value: item.tx_count })) .map((item) => ({ date: new Date(item.date), value: item.tx_count }))
...@@ -30,15 +28,14 @@ const dailyTxsIndicator: TChainIndicator<QueryKeys.chartsTxs> = { ...@@ -30,15 +28,14 @@ const dailyTxsIndicator: TChainIndicator<QueryKeys.chartsTxs> = {
}, },
}; };
const coinPriceIndicator: TChainIndicator<QueryKeys.chartsMarket> = { const coinPriceIndicator: TChainIndicator<'homepage_chart_market'> = {
id: 'coin_price', id: 'coin_price',
title: `${ appConfig.network.currency.symbol } price`, title: `${ appConfig.network.currency.symbol } price`,
value: (stats) => '$' + Number(stats.coin_price).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }), 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 }/>, icon: <TokenLogo hash={ appConfig.network.currency.address || '' } name={ appConfig.network.currency.name } boxSize={ 6 }/>,
hint: `${ appConfig.network.currency.symbol } token daily price in USD.`, hint: `${ appConfig.network.currency.symbol } token daily price in USD.`,
api: { api: {
queryName: QueryKeys.chartsMarket, resourceName: 'homepage_chart_market',
path: '/node-api/home-stats/charts/market',
dataFn: (response) => ([ { dataFn: (response) => ([ {
items: response.chart_data items: response.chart_data
.map((item) => ({ date: new Date(item.date), value: Number(item.closing_price) })) .map((item) => ({ date: new Date(item.date), value: Number(item.closing_price) }))
...@@ -49,7 +46,7 @@ const coinPriceIndicator: TChainIndicator<QueryKeys.chartsMarket> = { ...@@ -49,7 +46,7 @@ const coinPriceIndicator: TChainIndicator<QueryKeys.chartsMarket> = {
}, },
}; };
const marketPriceIndicator: TChainIndicator<QueryKeys.chartsMarket> = { const marketPriceIndicator: TChainIndicator<'homepage_chart_market'> = {
id: 'market_cup', id: 'market_cup',
title: 'Market cap', title: 'Market cap',
value: (stats) => '$' + shortenNumberWithLetter(Number(stats.market_cap), undefined, { maximumFractionDigits: 0 }), value: (stats) => '$' + shortenNumberWithLetter(Number(stats.market_cap), undefined, { maximumFractionDigits: 0 }),
...@@ -57,8 +54,7 @@ const marketPriceIndicator: TChainIndicator<QueryKeys.chartsMarket> = { ...@@ -57,8 +54,7 @@ const marketPriceIndicator: TChainIndicator<QueryKeys.chartsMarket> = {
// eslint-disable-next-line max-len // 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.', 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: { api: {
queryName: QueryKeys.chartsMarket, resourceName: 'homepage_chart_market',
path: '/node-api/home-stats/charts/market',
dataFn: (response) => ([ { dataFn: (response) => ([ {
items: response.chart_data items: response.chart_data
.map((item) => ({ date: new Date(item.date), value: Number(item.closing_price) * Number(response.available_supply) })) .map((item) => ({ date: new Date(item.date), value: Number(item.closing_price) * Number(response.available_supply) }))
......
import { Flex, Tag } from '@chakra-ui/react'; import { Flex, Tag } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; 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 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 AddressBlocksValidated from 'ui/address/AddressBlocksValidated';
import AddressCoinBalance from 'ui/address/AddressCoinBalance'; import AddressCoinBalance from 'ui/address/AddressCoinBalance';
import AddressDetails from 'ui/address/AddressDetails'; import AddressDetails from 'ui/address/AddressDetails';
...@@ -20,15 +17,11 @@ import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs'; ...@@ -20,15 +17,11 @@ import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
const AddressPageContent = () => { const AddressPageContent = () => {
const router = useRouter(); const router = useRouter();
const fetch = useFetch();
const addressQuery = useQuery<unknown, unknown, Address>( const addressQuery = useApiQuery('address', {
[ QueryKeys.address, router.query.id ], pathParams: { id: router.query.id?.toString() },
async() => await fetch(`/node-api/addresses/${ router.query.id }`), queryOptions: { enabled: Boolean(router.query.id) },
{ });
enabled: Boolean(router.query.id),
},
);
const tags = [ const tags = [
...(addressQuery.data?.private_tags || []), ...(addressQuery.data?.private_tags || []),
......
import { Box, Icon, Link } from '@chakra-ui/react'; import { Box, Icon, Link } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import type { JsonRpcUrlResponse } from 'types/api/jsonRpcUrl';
import config from 'configs/app/config'; import config from 'configs/app/config';
import PlusIcon from 'icons/plus.svg'; 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 AppList from 'ui/apps/AppList';
import AppListSkeleton from 'ui/apps/AppListSkeleton'; import AppListSkeleton from 'ui/apps/AppListSkeleton';
import CategoriesMenu from 'ui/apps/CategoriesMenu'; import CategoriesMenu from 'ui/apps/CategoriesMenu';
...@@ -15,8 +12,6 @@ import FilterInput from 'ui/shared/FilterInput'; ...@@ -15,8 +12,6 @@ import FilterInput from 'ui/shared/FilterInput';
import useMarketplaceApps from '../apps/useMarketplaceApps'; import useMarketplaceApps from '../apps/useMarketplaceApps';
const Apps = () => { const Apps = () => {
const fetch = useFetch();
const { const {
isLoading, isLoading,
category, category,
...@@ -30,10 +25,7 @@ const Apps = () => { ...@@ -30,10 +25,7 @@ const Apps = () => {
handleFavoriteClick, handleFavoriteClick,
} = useMarketplaceApps(); } = useMarketplaceApps();
useQuery<unknown, unknown, JsonRpcUrlResponse>( useApiQuery('config_json_rpc');
[ 'json-rpc-url' ],
async() => await fetch(`/node-api/config/json-rpc-url`),
);
return ( return (
<> <>
......
...@@ -2,7 +2,6 @@ import { Flex, Icon, Link, Tooltip } from '@chakra-ui/react'; ...@@ -2,7 +2,6 @@ import { Flex, Icon, Link, Tooltip } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { QueryKeys } from 'types/client/queries';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types'; import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import eastArrowIcon from 'icons/arrows/east.svg'; import eastArrowIcon from 'icons/arrows/east.svg';
...@@ -30,8 +29,8 @@ const BlockPageContent = () => { ...@@ -30,8 +29,8 @@ const BlockPageContent = () => {
const appProps = useAppContext(); const appProps = useAppContext();
const blockTxsQuery = useQueryWithPages({ const blockTxsQuery = useQueryWithPages({
apiPath: `/node-api/blocks/${ router.query.id }/transactions`, resourceName: 'block_txs',
queryName: QueryKeys.blockTxs, pathParams: { id: router.query.id?.toString() },
options: { options: {
enabled: Boolean(router.query.id && router.query.tab === 'txs'), enabled: Boolean(router.query.id && router.query.tab === 'txs'),
}, },
......
...@@ -5,11 +5,12 @@ import * as blockMock from 'mocks/blocks/block'; ...@@ -5,11 +5,12 @@ import * as blockMock from 'mocks/blocks/block';
import * as statsMock from 'mocks/stats/index'; import * as statsMock from 'mocks/stats/index';
import * as socketServer from 'playwright/fixtures/socketServer'; import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import Blocks from './Blocks'; import Blocks from './Blocks';
const BLOCKS_API_URL = '/node-api/blocks?type=block'; const BLOCKS_API_URL = buildApiUrl('blocks') + '?type=block';
const STATS_API_URL = '/node-api/home-stats'; const STATS_API_URL = buildApiUrl('homepage_stats');
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { tab: 'blocks' }, query: { tab: 'blocks' },
......
...@@ -2,7 +2,6 @@ import { useRouter } from 'next/router'; ...@@ -2,7 +2,6 @@ import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { BlockType } from 'types/api/block'; import type { BlockType } from 'types/api/block';
import { QueryKeys } from 'types/client/queries';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types'; import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
...@@ -19,12 +18,6 @@ const TAB_TO_TYPE: Record<string, BlockType> = { ...@@ -19,12 +18,6 @@ const TAB_TO_TYPE: Record<string, BlockType> = {
uncles: 'uncle', 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 = { const TAB_LIST_PROPS = {
marginBottom: 0, marginBottom: 0,
py: 5, py: 5,
...@@ -35,11 +28,9 @@ const BlocksPageContent = () => { ...@@ -35,11 +28,9 @@ const BlocksPageContent = () => {
const router = useRouter(); const router = useRouter();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const type = router.query.tab && !Array.isArray(router.query.tab) ? TAB_TO_TYPE[router.query.tab] : 'block'; 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({ const blocksQuery = useQueryWithPages({
apiPath: '/node-api/blocks', resourceName: 'blocks',
queryName,
filters: { type }, filters: { type },
}); });
......
...@@ -7,22 +7,23 @@ import * as statsMock from 'mocks/stats/index'; ...@@ -7,22 +7,23 @@ import * as statsMock from 'mocks/stats/index';
import * as txMock from 'mocks/txs/tx'; import * as txMock from 'mocks/txs/tx';
import insertAdText from 'playwright/scripts/insertAdText'; import insertAdText from 'playwright/scripts/insertAdText';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import Home from './Home'; import Home from './Home';
test('default view -@default +@desktop-xl +@mobile +@dark-mode', async({ mount, page }) => { 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, status: 200,
body: JSON.stringify(statsMock.base), 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, status: 200,
body: JSON.stringify([ body: JSON.stringify([
blockMock.base, blockMock.base,
blockMock.base2, blockMock.base2,
]), ]),
})); }));
await page.route('/node-api/index/txs', (route) => route.fulfill({ await page.route(buildApiUrl('homepage_txs'), (route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify([ body: JSON.stringify([
txMock.base, txMock.base,
...@@ -30,7 +31,7 @@ test('default view -@default +@desktop-xl +@mobile +@dark-mode', async({ mount, ...@@ -30,7 +31,7 @@ test('default view -@default +@desktop-xl +@mobile +@dark-mode', async({ mount,
txMock.withTokenTransfer, 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, status: 200,
body: JSON.stringify(dailyTxsMock.base), body: JSON.stringify(dailyTxsMock.base),
})); }));
......
import { Box, Center, useColorMode } from '@chakra-ui/react'; import { Box, Center, useColorMode } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React, { useCallback, useEffect, useRef, useState } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import type { JsonRpcUrlResponse } from 'types/api/jsonRpcUrl';
import type { AppItemOverview } from 'types/client/apps'; import type { AppItemOverview } from 'types/client/apps';
import { QueryKeys } from 'types/client/queries';
import appConfig from 'configs/app/config'; 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 link from 'lib/link/link';
import ContentLoader from 'ui/shared/ContentLoader'; import ContentLoader from 'ui/shared/ContentLoader';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
...@@ -20,18 +17,15 @@ type Props = { ...@@ -20,18 +17,15 @@ type Props = {
const MarketplaceApp = ({ app, isLoading }: Props) => { const MarketplaceApp = ({ app, isLoading }: Props) => {
const [ isFrameLoading, setIsFrameLoading ] = useState(isLoading); const [ isFrameLoading, setIsFrameLoading ] = useState(isLoading);
const { colorMode } = useColorMode(); const { colorMode } = useColorMode();
const fetch = useFetch();
const ref = useRef<HTMLIFrameElement>(null); const ref = useRef<HTMLIFrameElement>(null);
const handleIframeLoad = useCallback(() => { const handleIframeLoad = useCallback(() => {
setIsFrameLoading(false); setIsFrameLoading(false);
}, []); }, []);
const { data: jsonRpcUrlResponse } = useQuery<unknown, unknown, JsonRpcUrlResponse>( const { data: jsonRpcUrlResponse } = useApiQuery('config_json_rpc', {
[ QueryKeys.jsonRpcUrl ], queryOptions: { refetchOnMount: false },
async() => await fetch(`/node-api/config/json-rpc-url`), });
{ refetchOnMount: false },
);
useEffect(() => { useEffect(() => {
if (app && !isFrameLoading) { if (app && !isFrameLoading) {
......
import { Flex, Link, Icon, Tag, Tooltip } from '@chakra-ui/react'; import { Flex, Link, Icon, Tag, Tooltip } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { Transaction } from 'types/api/transaction';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types'; import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import eastArrowIcon from 'icons/arrows/east.svg'; import eastArrowIcon from 'icons/arrows/east.svg';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/appContext'; import { useAppContext } from 'lib/appContext';
import useFetch from 'lib/hooks/useFetch';
import isBrowser from 'lib/isBrowser'; import isBrowser from 'lib/isBrowser';
import networkExplorers from 'lib/networks/networkExplorers'; import networkExplorers from 'lib/networks/networkExplorers';
import AdBanner from 'ui/shared/ad/AdBanner'; import AdBanner from 'ui/shared/ad/AdBanner';
...@@ -35,7 +33,6 @@ const TABS: Array<RoutedTab> = [ ...@@ -35,7 +33,6 @@ const TABS: Array<RoutedTab> = [
const TransactionPageContent = () => { const TransactionPageContent = () => {
const router = useRouter(); const router = useRouter();
const fetch = useFetch();
const appProps = useAppContext(); const appProps = useAppContext();
const isInBrowser = isBrowser(); const isInBrowser = isBrowser();
...@@ -43,13 +40,10 @@ const TransactionPageContent = () => { ...@@ -43,13 +40,10 @@ const TransactionPageContent = () => {
const hasGoBackLink = referrer && referrer.includes('/txs'); const hasGoBackLink = referrer && referrer.includes('/txs');
const { data } = useQuery<unknown, unknown, Transaction>( const { data } = useApiQuery('tx', {
[ 'tx', router.query.id ], pathParams: { id: router.query.id?.toString() },
async() => await fetch(`/node-api/transactions/${ router.query.id }`), queryOptions: { enabled: Boolean(router.query.id) },
{ });
enabled: Boolean(router.query.id),
},
);
const explorersLinks = networkExplorers const explorersLinks = networkExplorers
.filter((explorer) => explorer.paths.tx) .filter((explorer) => explorer.paths.tx)
......
...@@ -2,7 +2,6 @@ import { Box } from '@chakra-ui/react'; ...@@ -2,7 +2,6 @@ import { Box } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { QueryKeys } from 'types/client/queries';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types'; import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
...@@ -26,8 +25,7 @@ const Transactions = () => { ...@@ -26,8 +25,7 @@ const Transactions = () => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const filter = router.query.tab === 'pending' ? 'pending' : 'validated'; const filter = router.query.tab === 'pending' ? 'pending' : 'validated';
const txsQuery = useQueryWithPages({ const txsQuery = useQueryWithPages({
apiPath: '/node-api/transactions', resourceName: filter === 'validated' ? 'txs_validated' : 'txs_pending',
queryName: filter === 'validated' ? QueryKeys.txsValidate : QueryKeys.txsPending,
filters: { filter }, filters: { filter },
}); });
......
...@@ -2,10 +2,11 @@ import { test, expect } from '@playwright/experimental-ct-react'; ...@@ -2,10 +2,11 @@ import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import Page from './Page'; 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 }) => { test('without indexing alert +@mobile', async({ mount }) => {
const component = await mount( const component = await mount(
......
...@@ -2,14 +2,13 @@ import { Box } from '@chakra-ui/react'; ...@@ -2,14 +2,13 @@ import { Box } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react'; import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import { QueryKeys } from 'types/client/queries';
import * as tokenTransferMock from 'mocks/tokens/tokenTransfer'; import * as tokenTransferMock from 'mocks/tokens/tokenTransfer';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import TokenTransfer from './TokenTransfer'; 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 }) => { test('without tx info +@mobile', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({ await page.route(API_URL, (route) => route.fulfill({
...@@ -21,15 +20,13 @@ test('without tx info +@mobile', async({ mount, page }) => { ...@@ -21,15 +20,13 @@ test('without tx info +@mobile', async({ mount, page }) => {
<TestApp> <TestApp>
<Box h={{ base: '134px', lg: 6 }}/> <Box h={{ base: '134px', lg: 6 }}/>
<TokenTransfer <TokenTransfer
path={ API_URL } resourceName="tx_token_transfers"
queryName={ QueryKeys.txTokenTransfers } pathParams={{ id: '1' }}
showTxInfo={ false } showTxInfo={ false }
/> />
</TestApp>, </TestApp>,
); );
await page.waitForResponse(API_URL),
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
...@@ -43,14 +40,12 @@ test('with tx info +@mobile', async({ mount, page }) => { ...@@ -43,14 +40,12 @@ test('with tx info +@mobile', async({ mount, page }) => {
<TestApp> <TestApp>
<Box h={{ base: '134px', lg: 6 }}/> <Box h={{ base: '134px', lg: 6 }}/>
<TokenTransfer <TokenTransfer
path={ API_URL } resourceName="tx_token_transfers"
queryName={ QueryKeys.txTokenTransfers } pathParams={{ id: '1' }}
showTxInfo={ true } showTxInfo={ true }
/> />
</TestApp>, </TestApp>,
); );
await page.waitForResponse(API_URL),
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
...@@ -3,12 +3,12 @@ import { useRouter } from 'next/router'; ...@@ -3,12 +3,12 @@ import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { Element } from 'react-scroll'; 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 { AddressFromToFilterValues } from 'types/api/address';
import type { TokenType } from 'types/api/tokenInfo'; 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 getFilterValueFromQuery from 'lib/getFilterValueFromQuery';
import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery'; import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
...@@ -34,54 +34,56 @@ const SCROLL_OFFSET = -100; ...@@ -34,54 +34,56 @@ const SCROLL_OFFSET = -100;
const getTokenFilterValue = (getFilterValuesFromQuery<TokenType>).bind(null, TOKEN_TYPES); const getTokenFilterValue = (getFilterValuesFromQuery<TokenType>).bind(null, TOKEN_TYPES);
const getAddressFilterValue = (getFilterValueFromQuery<AddressFromToFilter>).bind(null, AddressFromToFilterValues); const getAddressFilterValue = (getFilterValueFromQuery<AddressFromToFilter>).bind(null, AddressFromToFilterValues);
interface Props { interface Props<Resource extends 'tx_token_transfers' | 'address_token_transfers'> {
isLoading?: boolean; isLoading?: boolean;
isDisabled?: boolean; isDisabled?: boolean;
path: string; resourceName: Resource;
queryName: QueryKeys.txTokenTransfers | QueryKeys.addressTokenTransfers;
queryIds?: Array<string>;
baseAddress?: string; baseAddress?: string;
showTxInfo?: boolean; showTxInfo?: boolean;
txHash?: string; txHash?: string;
enableTimeIncrement?: boolean; 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, isLoading: isLoadingProp,
isDisabled, isDisabled,
queryName, resourceName,
queryIds,
path,
baseAddress, baseAddress,
showTxInfo = true, showTxInfo = true,
enableTimeIncrement, enableTimeIncrement,
}: Props) => { pathParams,
}: Props<Resource>) => {
const router = useRouter(); 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) }, { type: getTokenFilterValue(router.query.type), filter: getAddressFilterValue(router.query.filter) },
); );
const { isError, isLoading, data, pagination, onFilterChange, isPaginationVisible } = useQueryWithPages({ const { isError, isLoading, data, pagination, onFilterChange, isPaginationVisible } = useQueryWithPages({
apiPath: path, resourceName,
queryName, pathParams,
queryIds,
options: { enabled: !isDisabled }, options: { enabled: !isDisabled },
filters: filters, filters: filters as PaginationFilters<Resource>,
scroll: { elem: SCROLL_ELEM, offset: SCROLL_OFFSET }, scroll: { elem: SCROLL_ELEM, offset: SCROLL_OFFSET },
}); });
const handleTypeFilterChange = React.useCallback((nextValue: Array<TokenType>) => { const handleTypeFilterChange = React.useCallback((nextValue: Array<TokenType>) => {
onFilterChange({ ...filters, type: nextValue }); onFilterChange({ ...filters, type: nextValue } as PaginationFilters<Resource>);
setFilters((prevState) => ({ ...prevState, type: nextValue })); setFilters((prevState) => ({ ...prevState, type: nextValue }));
}, [ filters, onFilterChange ]); }, [ filters, onFilterChange ]);
const handleAddressFilterChange = React.useCallback((nextValue: string) => { const handleAddressFilterChange = React.useCallback((nextValue: string) => {
const filterVal = getAddressFilterValue(nextValue); const filterVal = getAddressFilterValue(nextValue);
onFilterChange({ ...filters, filter: filterVal }); onFilterChange({ ...filters, filter: filterVal } as PaginationFilters<Resource>);
setFilters((prevState) => ({ ...prevState, filter: filterVal })); setFilters((prevState) => ({ ...prevState, filter: filterVal }));
}, [ filters, onFilterChange ]); }, [ 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 isActionBarHidden = !numActiveFilters && !data?.items.length;
const content = (() => { const content = (() => {
......
...@@ -23,7 +23,7 @@ import { TOKEN_TYPE } from './helpers'; ...@@ -23,7 +23,7 @@ import { TOKEN_TYPE } from './helpers';
interface Props { interface Props {
appliedFiltersNum?: number; appliedFiltersNum?: number;
defaultTypeFilters: Array<TokenType>; defaultTypeFilters: Array<TokenType> | undefined;
onTypeFilterChange: (nextValue: Array<TokenType>) => void; onTypeFilterChange: (nextValue: Array<TokenType>) => void;
withAddressFilter?: boolean; withAddressFilter?: boolean;
onAddressFilterChange?: (nextValue: string) => void; onAddressFilterChange?: (nextValue: string) => void;
......
import { test, expect } from '@playwright/experimental-ct-react'; import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import { RESOURCES } from 'lib/api/resources';
import * as profileMock from 'mocks/user/profile'; import * as profileMock from 'mocks/user/profile';
import authFixture from 'playwright/fixtures/auth'; import authFixture from 'playwright/fixtures/auth';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import ProfileMenuDesktop from './ProfileMenuDesktop'; import ProfileMenuDesktop from './ProfileMenuDesktop';
...@@ -34,7 +34,7 @@ test.describe('auth', () => { ...@@ -34,7 +34,7 @@ test.describe('auth', () => {
}); });
extendedTest('+@dark-mode', async({ mount, page }) => { 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, status: 200,
body: JSON.stringify(profileMock.base), body: JSON.stringify(profileMock.base),
})); }));
......
import { test, expect, devices } from '@playwright/experimental-ct-react'; import { test, expect, devices } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import { RESOURCES } from 'lib/api/resources';
import * as profileMock from 'mocks/user/profile'; import * as profileMock from 'mocks/user/profile';
import authFixture from 'playwright/fixtures/auth'; import authFixture from 'playwright/fixtures/auth';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import ProfileMenuMobile from './ProfileMenuMobile'; import ProfileMenuMobile from './ProfileMenuMobile';
...@@ -30,7 +30,7 @@ test.describe('auth', () => { ...@@ -30,7 +30,7 @@ test.describe('auth', () => {
}); });
extendedTest('base view', async({ mount, page }) => { 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, status: 200,
body: JSON.stringify(profileMock.base), body: JSON.stringify(profileMock.base),
})); }));
......
...@@ -3,10 +3,11 @@ import React from 'react'; ...@@ -3,10 +3,11 @@ import React from 'react';
import * as txMock from 'mocks/txs/tx'; import * as txMock from 'mocks/txs/tx';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import TxDetails from './TxDetails'; import TxDetails from './TxDetails';
const API_URL = '/node-api/transactions/1'; const API_URL = buildApiUrl('tx', { id: '1' });
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { id: 1 }, query: { id: 1 },
......
...@@ -4,12 +4,13 @@ import React from 'react'; ...@@ -4,12 +4,13 @@ import React from 'react';
import * as internalTxsMock from 'mocks/txs/internalTxs'; import * as internalTxsMock from 'mocks/txs/internalTxs';
import * as txMock from 'mocks/txs/tx'; import * as txMock from 'mocks/txs/tx';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import TxInternals from './TxInternals'; import TxInternals from './TxInternals';
const TX_HASH = txMock.base.hash; const TX_HASH = txMock.base.hash;
const API_URL_TX = `/node-api/transactions/${ TX_HASH }`; const API_URL_TX = buildApiUrl('tx', { id: TX_HASH });
const API_URL_TX_INTERNALS = `/node-api/transactions/${ TX_HASH }/internal-transactions`; const API_URL_TX_INTERNALS = buildApiUrl('tx_internal_txs', { id: TX_HASH });
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { id: TX_HASH }, query: { id: TX_HASH },
......
...@@ -2,7 +2,6 @@ import { Box, Text, Show, Hide } from '@chakra-ui/react'; ...@@ -2,7 +2,6 @@ import { Box, Text, Show, Hide } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { InternalTransaction } from 'types/api/internalTransaction'; import type { InternalTransaction } from 'types/api/internalTransaction';
import { QueryKeys } from 'types/client/queries';
import { SECOND } from 'lib/consts'; import { SECOND } from 'lib/consts';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
...@@ -76,9 +75,8 @@ const TxInternals = () => { ...@@ -76,9 +75,8 @@ const TxInternals = () => {
const [ sort, setSort ] = React.useState<Sort>(); const [ sort, setSort ] = React.useState<Sort>();
const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND }); const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND });
const { data, isLoading, isError, pagination, isPaginationVisible } = useQueryWithPages({ const { data, isLoading, isError, pagination, isPaginationVisible } = useQueryWithPages({
apiPath: `/node-api/transactions/${ txInfo.data?.hash }/internal-transactions`, resourceName: 'tx_internal_txs',
queryName: QueryKeys.txInternals, pathParams: { id: txInfo.data?.hash },
queryIds: txInfo.data?.hash ? [ txInfo.data.hash ] : undefined,
options: { options: {
enabled: Boolean(txInfo.data?.hash) && Boolean(txInfo.data?.status), enabled: Boolean(txInfo.data?.hash) && Boolean(txInfo.data?.status),
}, },
......
import { Box, Text } from '@chakra-ui/react'; import { Box, Text } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { QueryKeys } from 'types/client/queries';
import { SECOND } from 'lib/consts'; import { SECOND } from 'lib/consts';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
...@@ -17,9 +15,8 @@ import useFetchTxInfo from 'ui/tx/useFetchTxInfo'; ...@@ -17,9 +15,8 @@ import useFetchTxInfo from 'ui/tx/useFetchTxInfo';
const TxLogs = () => { const TxLogs = () => {
const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND }); const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND });
const { data, isLoading, isError, pagination, isPaginationVisible } = useQueryWithPages({ const { data, isLoading, isError, pagination, isPaginationVisible } = useQueryWithPages({
apiPath: `/node-api/transactions/${ txInfo.data?.hash }/logs`, resourceName: 'tx_logs',
queryName: QueryKeys.txLogs, pathParams: { id: txInfo.data?.hash },
queryIds: txInfo.data?.hash ? [ txInfo.data.hash ] : undefined,
options: { options: {
enabled: Boolean(txInfo.data?.hash) && Boolean(txInfo.data?.status), enabled: Boolean(txInfo.data?.hash) && Boolean(txInfo.data?.status),
}, },
......
import { Flex, Textarea, Skeleton } from '@chakra-ui/react'; import { Flex, Textarea, Skeleton } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { RawTracesResponse } from 'types/api/rawTrace'; import useApiQuery from 'lib/api/useApiQuery';
import { QueryKeys } from 'types/client/queries';
import { SECOND } from 'lib/consts'; import { SECOND } from 'lib/consts';
import useFetch from 'lib/hooks/useFetch';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import TxPendingAlert from 'ui/tx/TxPendingAlert'; import TxPendingAlert from 'ui/tx/TxPendingAlert';
...@@ -16,16 +12,14 @@ import useFetchTxInfo from 'ui/tx/useFetchTxInfo'; ...@@ -16,16 +12,14 @@ import useFetchTxInfo from 'ui/tx/useFetchTxInfo';
const TxRawTrace = () => { const TxRawTrace = () => {
const router = useRouter(); const router = useRouter();
const fetch = useFetch();
const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND }); const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND });
const { data, isLoading, isError } = useQuery<unknown, unknown, RawTracesResponse>( const { data, isLoading, isError } = useApiQuery('tx_raw_trace', {
[ QueryKeys.txRawTrace, router.query.id ], pathParams: { id: router.query.id?.toString() },
async() => await fetch(`/node-api/transactions/${ router.query.id }/raw-trace`), queryOptions: {
{
enabled: Boolean(router.query.id) && Boolean(txInfo.data?.status), enabled: Boolean(router.query.id) && Boolean(txInfo.data?.status),
}, },
); });
if (!txInfo.isLoading && !txInfo.isError && !txInfo.data.status) { if (!txInfo.isLoading && !txInfo.isError && !txInfo.data.status) {
return txInfo.socketStatus ? <TxSocketAlert status={ txInfo.socketStatus }/> : <TxPendingAlert/>; return txInfo.socketStatus ? <TxSocketAlert status={ txInfo.socketStatus }/> : <TxPendingAlert/>;
......
import React from 'react'; import React from 'react';
import { QueryKeys } from 'types/client/queries';
import { SECOND } from 'lib/consts'; import { SECOND } from 'lib/consts';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import TokenTransfer from 'ui/shared/TokenTransfer/TokenTransfer'; import TokenTransfer from 'ui/shared/TokenTransfer/TokenTransfer';
...@@ -20,15 +18,12 @@ const TxTokenTransfer = () => { ...@@ -20,15 +18,12 @@ const TxTokenTransfer = () => {
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
const path = `/node-api/transactions/${ data?.hash }/token-transfers`;
return ( return (
<TokenTransfer <TokenTransfer
isLoading={ isLoading } isLoading={ isLoading }
isDisabled={ !data?.status || !data?.hash } isDisabled={ !data?.status || !data?.hash }
path={ path } resourceName="tx_token_transfers"
queryName={ QueryKeys.txTokenTransfers } pathParams={{ id: data?.hash.toString() }}
queryIds={ data?.hash ? [ data.hash ] : undefined }
showTxInfo={ false } showTxInfo={ false }
txHash={ data?.hash || '' } txHash={ data?.hash || '' }
/> />
......
import type { UseQueryResult } from '@tanstack/react-query'; 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 { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { SocketMessage } from 'lib/socket/types'; import type { SocketMessage } from 'lib/socket/types';
import type { Transaction } from 'types/api/transaction'; 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 delay from 'lib/delay';
import useFetch from 'lib/hooks/useFetch';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
...@@ -23,23 +22,23 @@ type ReturnType = UseQueryResult<Transaction, unknown> & { ...@@ -23,23 +22,23 @@ type ReturnType = UseQueryResult<Transaction, unknown> & {
export default function useFetchTxInfo({ onTxStatusUpdate, updateDelay }: Params | undefined = {}): ReturnType { export default function useFetchTxInfo({ onTxStatusUpdate, updateDelay }: Params | undefined = {}): ReturnType {
const router = useRouter(); const router = useRouter();
const fetch = useFetch();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [ socketStatus, setSocketStatus ] = React.useState<'close' | 'error'>(); const [ socketStatus, setSocketStatus ] = React.useState<'close' | 'error'>();
const queryResult = useQuery<unknown, unknown, Transaction>( const queryResult = useApiQuery('tx', {
[ QueryKeys.tx, router.query.id ], pathParams: { id: router.query.id?.toString() },
async() => await fetch(`/node-api/transactions/${ router.query.id }`), queryOptions: {
{
enabled: Boolean(router.query.id), enabled: Boolean(router.query.id),
refetchOnMount: false, refetchOnMount: false,
}, },
); });
const { data, isError, isLoading } = queryResult; const { data, isError, isLoading } = queryResult;
const handleStatusUpdateMessage: SocketMessage.TxStatusUpdate['handler'] = React.useCallback(async() => { const handleStatusUpdateMessage: SocketMessage.TxStatusUpdate['handler'] = React.useCallback(async() => {
updateDelay && await delay(updateDelay); 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?.();
}, [ onTxStatusUpdate, queryClient, router.query.id, updateDelay ]); }, [ onTxStatusUpdate, queryClient, router.query.id, updateDelay ]);
......
...@@ -18,6 +18,7 @@ import useTxsSort from './useTxsSort'; ...@@ -18,6 +18,7 @@ import useTxsSort from './useTxsSort';
type QueryResult = UseQueryResult<TxsResponse> & { type QueryResult = UseQueryResult<TxsResponse> & {
pagination: PaginationProps; pagination: PaginationProps;
isPaginationVisible: boolean;
}; };
type Props = { type Props = {
...@@ -31,7 +32,6 @@ type Props = { ...@@ -31,7 +32,6 @@ type Props = {
const TxsContent = ({ filter, query, showBlockInfo = true, showSocketInfo = true, currentAddress, enableTimeIncrement }: Props) => { const TxsContent = ({ filter, query, showBlockInfo = true, showSocketInfo = true, currentAddress, enableTimeIncrement }: Props) => {
const { data, isLoading, isError, setSortByField, setSortByValue, sorting } = useTxsSort(query); const { data, isLoading, isError, setSortByField, setSortByValue, sorting } = useTxsSort(query);
const isPaginatorHidden = !isLoading && !isError && query.pagination.page === 1 && !query.pagination.hasNextPage;
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const content = (() => { const content = (() => {
...@@ -86,7 +86,7 @@ const TxsContent = ({ filter, query, showBlockInfo = true, showSocketInfo = true ...@@ -86,7 +86,7 @@ const TxsContent = ({ filter, query, showBlockInfo = true, showSocketInfo = true
sorting={ sorting } sorting={ sorting }
showBlockInfo={ showBlockInfo } showBlockInfo={ showBlockInfo }
showSocketInfo={ showSocketInfo } showSocketInfo={ showSocketInfo }
top={ isPaginatorHidden ? 0 : 80 } top={ query.isPaginationVisible ? 80 : 0 }
currentAddress={ currentAddress } currentAddress={ currentAddress }
enableTimeIncrement={ enableTimeIncrement } enableTimeIncrement={ enableTimeIncrement }
/> />
...@@ -103,7 +103,7 @@ const TxsContent = ({ filter, query, showBlockInfo = true, showSocketInfo = true ...@@ -103,7 +103,7 @@ const TxsContent = ({ filter, query, showBlockInfo = true, showSocketInfo = true
sorting={ sorting } sorting={ sorting }
setSorting={ setSortByValue } setSorting={ setSortByValue }
paginationProps={ query.pagination } paginationProps={ query.pagination }
showPagination={ !isPaginatorHidden } showPagination={ query.isPaginationVisible }
filterComponent={ filter } 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